1. 程式人生 > 實用技巧 >Linux 驅動框架---驅動中的阻塞

Linux 驅動框架---驅動中的阻塞

描述和API

阻塞IO和非阻塞IO的應用程式設計時的處理機制是不同的,如果是非阻塞IO在訪問資源未就緒時就直接返回-EAGAIN,反之阻塞IO則會使當前使用者程序睡眠直到資源可用。從應用場景來說兩種方式分別適應不同的使用場景。而驅動開發不可避免的需要支援兩種訪問方式。如果不是採用現成的子框架而自己實現檔案操作底層介面部分時就需要自己實現這一機制。檔案的訪問方式除了在開啟檔案時指定外還可以在開啟以後通過fcnt和ioctl進行修改和獲取。

阻塞IO在實現過程依賴兩個重要的資料結構等待佇列頭(wait_queue_head_t)和等待佇列成員(wait_queue_t)具體的定義如下:

struct __wait_queue_head {
    spinlock_t        lock;
    struct list_head    task_list;
};

typedef struct __wait_queue wait_queue_t;

struct __wait_queue {
    unsigned int        flags;
#define WQ_FLAG_EXCLUSIVE    0x01
    void            *private;
    wait_queue_func_t    func;
    struct list_head    task_list;
};

實際上都是Linux核心關於連結串列管理的通用方式再加上互斥資源的封裝操作。使用也特別簡單直接看API和具體的使用方法。

定義

使用等待佇列前需要先定義一個等待佇列頭,可以使用動態和靜態的方式進行定義。等待佇列成員相同也可以使用兩種方式建立,不過核心都提供了方便的巨集用來完成對等待成員的定義和初始化。

初始化

等待佇列頭的初始化使用init_waitqueue_head(wait_queue_head_t* head)進行初始化。而等待佇列成員常使用__WAITQUEUE_INITIALIZER如下巨集進行定義和初始化。

//初始化佇列頭
#define init_waitqueue_head(q)                \
    do
{ \ static struct lock_class_key __key; \ \ __init_waitqueue_head((q), #q, &__key); \ } while (0) //初始化佇列成員 #define __WAITQUEUE_INITIALIZER(name, tsk) { \ .private = tsk, \ .func = default_wake_function, \ .task_list = { NULL, NULL } } #define DECLARE_WAITQUEUE(name, tsk) \ wait_queue_t name = __WAITQUEUE_INITIALIZER(name, tsk)

新增和移除等待佇列

//將wait 加入到 head 佇列中
void add_wait_queue(wait_queue_head_t *head,wait_queue_t *wait )
//將wait 從 head 佇列中移除
void remove_wait_queue(wait_queue_head_t *head,wait_queue_t *wait )

在佇列上等待事件

wait_event(wq, condition) 注意傳入的引數wq為佇列實體而不是地址,condition為“並”邏輯條件。

#define wait_event(wq, condition)                    \
do {                                    \
    if (condition)                            \
        break;                            \
    __wait_event(wq, condition);                    \
} while (0)

#define __wait_event(wq, condition)                 \
    (void)___wait_event(wq, condition, TASK_UNINTERRUPTIBLE, 0, 0,  \
                schedule())

#define ___wait_event(wq, condition, state, exclusive, ret, cmd)    \
({                                    \
    __label__ __out;                        \
    wait_queue_t __wait;                        \
    long __ret = ret;    /* explicit shadow */            \
                                    \
    INIT_LIST_HEAD(&__wait.task_list);                \
    if (exclusive)                            \
        __wait.flags = WQ_FLAG_EXCLUSIVE;            \
    else                                \
        __wait.flags = 0;                    \
                                    \
    for (;;) {                            \
        long __int = prepare_to_wait_event(&wq, &__wait, state);\
                                    \
        if (condition)                        \
            break;                        \
                                    \
        if (___wait_is_interruptible(state) && __int) {        \
            __ret = __int;                    \
            if (exclusive) {                \
                abort_exclusive_wait(&wq, &__wait,    \
                             state, NULL);    \
                goto __out;                \
            }                        \
            break;                        \
        }                            \
                                    \
        cmd;                            \
    }                                \
    finish_wait(&wq, &__wait);                    \
__out:    __ret;                                \
})

可以中斷的介面wait_event_interruptible(wq,condition)

#define wait_event_interruptible(wq, condition)                \
({                                    \
    int __ret = 0;                            \
    if (!(condition))                        \
        __ret = __wait_event_interruptible(wq, condition);    \
    __ret;                                \
})

#define __wait_event_interruptible(wq, condition)            \
    ___wait_event(wq, condition, TASK_INTERRUPTIBLE, 0, 0,        \
              schedule())

支援超時的介面wait_event_timeout(wq,condition,timeout)

#define wait_event_timeout(wq, condition, timeout)            \
({                                    \
    long __ret = timeout;                        \
    if (!___wait_cond_timeout(condition))                \
        __ret = __wait_event_timeout(wq, condition, timeout);    \
    __ret;                                \
})

#define ___wait_cond_timeout(condition)                    \
({                                    \
    bool __cond = (condition);                    \
    if (__cond && !__ret)                        \
        __ret = 1;                        \
    __cond || !__ret;                        \
})

#define __wait_event_timeout(wq, condition, timeout)            \
    ___wait_event(wq, ___wait_cond_timeout(condition),        \
              TASK_UNINTERRUPTIBLE, 0, timeout,            \
              __ret = schedule_timeout(__ret))

支援中斷和超時wait_event_interruptible_timeout(wq,condition,timeout)

#define wait_event_interruptible_timeout(wq, condition, timeout)    \
({                                    \
    long __ret = timeout;                        \
    if (!___wait_cond_timeout(condition))                \
        __ret = __wait_event_interruptible_timeout(wq,        \
                        condition, timeout);    \
    __ret;                                \
})


#define __wait_event_interruptible_timeout(wq, condition, timeout)    \
    ___wait_event(wq, ___wait_cond_timeout(condition),        \
              TASK_INTERRUPTIBLE, 0, timeout,            \
              __ret = schedule_timeout(__ret))

通過觀察以上介面最終都是通過呼叫___wait_event(wq,condition,state,exclusive,ret,cmd)介面來實現的等待事件。

喚醒佇列

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

需要注意的是兩個介面都用來喚醒一個佇列上的所有等待程序,而wake_up可以喚醒TASK_INTERRUPTIBLR和TASK_UNINTERRUPTIBLE兩種狀態的程序。而wake_up_interruptible僅能用來喚醒TASK_INTERRUPTIBLR狀態的程序即呼叫wait_event_interruptible和wait_event_interruptible_timeout()介面進入等待的程序。除此之外還有兩個介面用於快速在一個訪問介面中睡眠;

sleep_on(wait_queue_head_t* queue);
interruptible_sleep_on(wait_queue_head_t* queue);

兩個介面都是會定義一個等待成員並將其新增到等待佇列上設定程序為對應狀態後睡眠直到資源可用。

使用示例

在驅動私有資料中先定義一個等待佇列頭並在模組安裝時初始化。

int xxx_init(void)
{
    ...
    init_waitqueue_head(&xxx_wait_head);
    ...
}

阻塞

static ssize_t xxx_write(struct file* file ,const char* buf,size_t cnt,lofft_t *ppos)
{
    //從私有資料中或全域性的方式拿到等待佇列頭xxx_queue
    
    //訪問介面中
    DECLARE_WAITQUEUE(xx_wait,currrent);//currrent 為當前程序PCB
    add_wait_queue(&xxx_queue,&xx_wait);
    ...
    if(file->flags & O_NONBLOCK)
    {
        return -EAGAIN;
    }else
    {
        __set_current_state(TASK_INTERRUPTIABLE);
        schedule();
        if(signal_pending(currrent)){
            //因為此處是可訊號中斷睡眠的所以,有可能因為訊號喚醒
            //所以需要判斷是否是因為訊號喚醒,如是則返回請重新呼叫系統呼叫
            return -ERESTARTSYS;
        }
    }
    ...
}

喚醒

static ssize_t xxx_read(struct file* file ,const char* buf,size_t cnt,lofft_t *ppos)
{
    //從私有資料中或全域性的方式拿到等待佇列頭xxx_queue
    //訪問介面中
    ....
    //或wake_up(&xxx_queue) 與前面阻塞相對應
    wake_up_interruptible(&xxx_queue)
    ....
    
}

這裡示例未使用wait_event相關的介面,但實際上這些介面的實現類似上面使用過程的。如__wait_event_interruptible的舊版核心實現,這裡看舊版是因為舊版的巨集介面封裝更加容易理解。

#define __wait_event_interruptible(wq, condition, ret)            \
do {                                    \
    DEFINE_WAIT(__wait);                        \
                                    \
    for (;;) {                            \
        prepare_to_wait(&wq, &__wait, TASK_INTERRUPTIBLE);    \
        if (condition)                        \
            break;                        \
        if (!signal_pending(current)) {                \
            schedule();                    \
            continue;                    \
        }                            \
        ret = -ERESTARTSYS;                    \
        break;                            \
    }                                \
    finish_wait(&wq, &__wait);                    \
} while (0)

驅動的輪詢支援

應用程式設計過程中常常也會使用非阻塞的IO操作,因為可能應用的邏輯是判斷是否有資料可用如果有則執行A處理,沒有則執行B處理。如果在IO操作時阻塞則就無法執行B處理過程,最重要的是如果需要同時操作多個IO時如果非阻塞的方式開啟則會降低程式的效能,因為頻繁的呼叫系統介面然後返回 -EAGAIN且需要同時處理多個IO的情況下就需要編寫很多的讀取判斷函式介面,過多的系統呼叫會降低程式的執行效能所以需要IO輪詢介面從而實現IO多路複用系統提供了select()和poll()呼叫,在核心內部最後都會呼叫驅動提供的poll()介面。poll機制的實現基本思路是

1、裝置驅動定義一個等待佇列

2、poll介面呼叫將呼叫程序掛接到這個佇列上(poll_wait(file,&xxx_queue,wait))

3、由中斷或其他機制喚醒這個佇列

4、返回對應事件型別的掩碼(POLLRDNORM 、POLLIN資料可讀、POLLRDNORM 、POLLOUT資料可寫)

示例:

static DECLARE_WAIT_QUEUE_HEAD(button_waitq);

static unsigned drivers_poll(struct file *file, poll_table *wait)
{
    unsigned int mask = 0;
    poll_wait(file, &button_waitq, wait); /* 將程序掛接到button_waitq等待佇列下 */

    /* 根據實際情況,標記事件型別 */
    if (ev_press)
        mask |= POLLIN | POLLRDNORM;

    /* 如果mask為0,那麼證明沒有請求事件發生;如果非零說明有時間發生 */
    return mask;
}
poll_wait介面詳情:
/*
 * Do not touch the structure directly, use the access functions
 * poll_does_not_wait() and poll_requested_events() instead.
 */
typedef struct poll_table_struct {
    poll_queue_proc _qproc;
    unsigned long _key;
} poll_table;

static inline void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p)
{
    if (p && p->_qproc && wait_address)
        p->_qproc(filp, wait_address, p);
}

它是將當前程序新增到由系統提供poll_table中,實際的作用就是在驅動喚醒wait_address指定的等待佇列頭時可以同時喚醒因為呼叫輪敘介面select或這poll介面而進入睡眠的程序。至此就是驅動中阻塞相關使用的簡單學習歸納了,後續在實際使用中在提煉總結。