天天看點

linux驅動系列學習之阻塞與非阻塞IO(六)

一. 阻塞與非阻塞IO概念

    阻塞操作是指在執行裝置操作時,若不能擷取資源,則挂起程序進入休眠狀态,等待可滿足條件後進行操作。被挂起的程序從排程器隊列移動到挂起隊列(睡眠狀态)。當操作驅動程式read、write操作時,應用程式希望以阻塞的方式通路裝置,驅動程式需要提供響應的能力。在read、write中,當資源不可操作時,需要把程序挂起,直到資源 可用才擷取資源并傳回,整個過程仍然進行了正确的通路,應用層不可見,不能感覺到這個挂起的過程。而非阻塞通路時,資源不可用,read、write操作會立即傳回,并傳回-EAGAIN。

    在阻塞IO中,當程序進入到休眠狀态時,需要一個地方将其喚醒,一般是在中斷裡面。若沒有地方将其喚醒,則程序一直休眠。而非阻塞IO則不斷嘗試,直到可以擷取資源。Linux系統中提供了多種方式解決阻塞與非阻塞的問題。本文介紹等待隊列和輪詢兩種方式。

二、等待隊列

    在linux驅動程式中,可以使用等待隊列來實作阻塞程序的喚醒。其以隊列作為基礎的資料結構,與程序排程機制緊密結合,可以用來同步對系統資源的通路。

1. 定義等待隊列頭

wait_queue_head_t m_queue;   
           

2. 初始化

init_waitqueue_head(&m_queue);
           

或者使用宏DECLARE_WAIT_QUEUE_HEAD(name),定義并初始化一個等待隊列。

DECLARE_WAIT_QUEUE_HEAD(name);
           

3. 定義等待隊列元素

DECLARE_WAITQUEUE(name,tsk);  //tsk最終是一個void*指針
定義并初始化一個名為name的等待隊列元素。
           

4. 添加/移除等待隊列

void add_wait_queue(wait_queue_head_t *q,wait_queue_t *wait); //隊列元素添加到隊列頭部q
           

其中,q為雙向連結清單

void remove_wait_queue(wait_queue_head_t *q,wait_queue_t *wait);//從隊列頭部q中移除
           

5. 等待事件

wait_event(queue,condition);
wait_event_interruptible(queue,condition);
wait_event_timeout(queue,condition,timeout);
wait_event_interruptible_timeout(queue,condition,timeout);
           

        queue作為等待隊列頭部的隊列被喚醒,condition必須被滿足,否則繼續等待。

xxx_interruptible表示可以被信号打斷。xxx_timeout表示阻塞等待的逾時事件,

以jiffy為機關時間,在timeout到達時,不論condition是否被滿足均傳回。

6. 喚醒隊列

void wake_up(wait_queue_head_t *q);
void wake_up_interruptible(wait_queue_head_t *q);
           

喚醒以q為等待隊列頭部的所有程序。

wake_up與wait_event成對使用,wake_up_interruptible與wait_event_interruptible成對使用。

7. 在等待隊列上睡眠

sleep_on(wait_queue_head_t *q);
interruptible_sleep_on(wait_queue_head_t *q);
           

sleep_on說将程序的狀态轉為休眠狀态,将其挂到q對應的等待隊列上,知道資源可用,

q上的程序被喚醒。sleep_on與wake_up成對使用。

示例代碼

static ssize_t xxx_write(struct file *file,const char *buffer,size_t count,loff_t *ppos)
{
    DECLARE_WAITQUEUE(wait,current);
    add_wait_queue(&xxx_wait,&wait);
    
    //等待裝置緩沖區可寫
    do{
        avail = device_writable(...);   //裝置寫入狀态
        if(avail < 0){               //裝置不可寫
            if(file_f_flags & O_NONBLOCK){    //非阻塞
                ret = -EAGAIN;
                goto out;
            }
            __set_current_state(TASK_INTERRUPTIBLE);  //設定程序狀态
            schedule();     //切換到其他程序
            if(signal_pending(current)){       //程序切換回來時,判斷是不是因為信号喚醒
                ret = -ERESTARTSYS;
                goto out;
            }
        }
    }while(avail < 0);
    device_writable(...);  
    out:
    remove_wait_queue(&xxx_wait,&wait);
    set_current_state(TASK_RUNNING);
    return ret;
}
           

三、輪詢操作

        概念:在使用者程式中,select和poll也是與裝置阻塞與非阻塞通路資訊息息相關的,使用非阻塞IO的應用程式通常使用select和poll系統調用查詢是否對裝置進行無阻塞的通路,select和poll系統調用最終會調用到驅動的poll函數。

int select(int numfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds,  struct timeval *timeout);
           

        readfds、writefds、exceptfds是被select監視的讀、寫和異常處理的檔案描述符。numfds的值是需要檢查的号碼最高的fd加1。readfds檔案集中任何一個檔案變得可讀,select傳回。writefds檔案集中任何一個檔案變得可寫,select傳回。

接下來操作是設定、清除、判斷檔案描述符集合。

void FD_CLR(int fd, fd_set *set);将fd從set中清除出去(在集合中是1,清除出去就成0,set其實是位圖)
int FD_ISSET(int fd, fd_set *set);判斷fd是否在集合中;
void FD_SET(int fd, fd_set *set);将fd設定到集合中去;
void FD_ZERO(fd_set *set); 将set清空成0;
           

poll與select原理相似,原型為

int poll(struct pollfd *fds, nfds_t nfds, int timeout);
           

當多路複用的檔案數量龐大、IO操作頻繁時,select和poll操作性能表現比較差,這時應該使用epoll,epoll不會随着檔案數量的增大,性能下降明顯。

在驅動程式中,poll函數的原型是

unsigned int (*poll)(struct file *filp,struct poll_table* wait);
           

filp:檔案指針

wait:輪詢表指針,

1)對可能引起裝置檔案狀态變化的等待隊列調用poll_wait函數,将對應的等待隊列頭部添加到poll_table中。

2)傳回表示是否能對裝置進行無阻塞讀、寫通路的掩碼。

poll_wait函數原型如下

void poll_wait(struct file *filp, wait_queue_head_t *queue, poll_table *wait);
           

        該函數并不會引起阻塞,它的工作是把目前程序添加到wait指定的等待清單中。實際作用是讓喚醒參數queue對應的等待隊列可喚醒因select()而休眠的程序。

        驅動poll函數應該傳回裝置資源的可擷取狀态,即POLLIN、POLLOUT、POLLPRI、POLLERR、POLLNVAL等宏的位"或"結果。

每個宏表示一種裝置狀态,如下:

常量                    說明
POLLIN             普通或優先級帶資料可讀
POLLRDNORM         普通資料可讀
POLLRDBAND         優先級帶資料可讀
POLLPRI            高優先級資料可讀
POLLOUT            普通資料可寫
POLLWRNORM         普通資料可寫
POLLWRBAND         優先級帶資料可寫
POLLERR            發生錯誤
POLLHUP            發生挂起
POLLNVAL           描述字不是一個打開的檔案
           

poll驅動函數的示例

static unsigned int xxx_poll(struct file *filp, poll_table *wait)
{
    unsigned int mask = 0;
    struct xxx_dev *dev = filp->private_data;     //獲得裝置結構指針
    ...
    down(&dev->sem);
    poll_wait(filp, &dev->r_wait, wait);    //加讀等待對列頭
    poll_wait(filp ,&dev->w_wait, wait);    //加寫等待隊列頭
   
    if(...)                          //可讀
    {
          mask |= POLLIN | POLLRDNORM;    //辨別資料可獲得
     }
    if(...)                           //可寫
    {
          mask |= POLLOUT | POLLRDNORM;    //辨別資料可寫入
     }
    ..
    up(&dev->sem);
    return mask;
}
           

四、總結

        阻塞和非阻塞是兩種不同的IO操作模式,阻塞在暫時不可進行IO操作時會讓程序休眠,非阻塞則不然。在驅動程式中,阻塞IO一般基于等待隊列或者基于等待隊列的其他Linux核心API實作,等待隊列可用于同步驅動中事件發生的先後順序。使用非阻塞IO的應用程式可借助輪詢函數查詢裝置是否能立即執行,調用驅動中的poll函數。驅動poll函數本身不會被阻塞,但是與poll、select、epoll相關的系統調用則會阻塞地等待至少一個檔案描述符集合可通路或者逾時。

參考書:Linux裝置驅動開發詳解(基于最新的Linux4.0核心) 宋寶華著

              Linux裝置驅動程式   J & G著  

繼續閱讀