Linux驅動開發(十三):阻塞與非阻塞IO——等待隊列

阻塞與非阻塞IO

阻塞式IO在請求資源時如果不能獲取到設備資源,會將應用程序掛起,知道資源可以被獲取
在這裏插入圖片描述
非阻塞式IO則會輪詢等待知道設備資源可以使用或者直接放棄
在這裏插入圖片描述
使用非阻塞訪問從設備讀取數據,當設備不可用或數據位準備好時會立即返回錯誤碼,表示數據讀取失敗,應用程序會再次讀取數據,一直往復循環,直到數據讀取成功
調用read函數的時候加上O_NONBLOCK就是非阻塞方式打開
這篇博客先記錄阻塞式IO的處理方式——使用等待隊列

等待隊列概述

首先我們設想這樣一個情景,應用程序要獲取按鍵的鍵值,如果我們一直循環讀取的話這無疑是十分佔用資源的,甚至我們的CPU會被喫滿,我們可以這樣來實現,當按鍵沒有按下的時候,驅動程序就讓按鍵掃描進程進入休眠狀態(釋放資源給別的進程或任務使用),當按鍵按下時(資源可用)驅動程序再去喚醒按鍵掃描進程,喚醒後會立刻獲得資源,然後這時按鍵掃描進程就可以去讀鍵值。
如何在驅動程序中實現上面的將應用程序的進程休眠和喚醒呢?
要實現這種機制,根本上需要驅動程序具備能夠檢查,檢測到設備可用不可用的功能!
而Linux內核提供了等待隊列給我們來實現這樣的功能

相關實現

等待隊列頭

阻塞訪問需要在文件可以操作的時候喚醒進程,一般在中斷函數裏面完成喚醒操作
Linux內核提供了等待隊列來實現阻塞進程的喚醒工作,使用等待隊列需要創建一個等待隊列頭
定義在\linux\wait.h

struct wait_queue_head {
	spinlock_t		lock;
	struct list_head	head;
};

定義後需要進行初始化,使用宏定義init_waitqueue_head(wq_head)來初始化等待隊列頭
也可以使用DECLARE_WAIT_QUEUE_HEAD(name)來一次性完成

#define init_waitqueue_head(q)				\
	do {						\
		static struct lock_class_key __key;	\
							\
		__init_waitqueue_head((q), #q, &__key);	\
	} while (0)
#define DECLARE_WAIT_QUEUE_HEAD(name) \
	wait_queue_head_t name = __WAIT_QUEUE_HEAD_INITIALIZER(name)

等待隊列項

wait_queue_entr

struct wait_queue_entry {
	unsigned int		flags;
	void			*private;
	wait_queue_func_t	func;
	struct list_head	entry;
};

宏DECLARE_WAITQUEUE(name, tsk)定義並初始化一個等待隊列項

#define DECLARE_WAITQUEUE(name, tsk)					\
	wait_queue_t name = __WAITQUEUE_INITIALIZER(name, tsk)

name是等待隊列項的名字,tsk表示這個等待隊列項屬於哪個任務(進程),一般設置爲current,內核中的current是一個全局變量,表示當前進程
所以該宏給當前進程創建了一個等待隊列項

將隊列項添加/移除等待列表頭

當設備不可訪問時需要將進程對應的等待隊列項添加到前面的等待隊列頭中
只有添加到等待隊列頭中以後進程才能進入休眠態
設備可以訪問時從等待隊列頭中移除
添加:

void add_wait_queue(struct wait_queue_head *wq_head, struct wait_queue_entry *wq_entry);

移除:

remove_wait_queue(struct wait_queue_head *wq_head, struct wait_queue_entry *wq_entry);

等待喚醒

wake_up(x)
wake_up_interruptible(x)

x是要喚醒的等待隊列頭
wake_up函數可以喚醒處於 TASK_INTERRUPTIBLE和TASK_UNINTERRUPTIBLE狀態的進程,而 wake_up_interruptible函數只能喚醒處於 TASK_INTERRUPTIBLE狀態的進程。

等待時間

除了主動喚醒以外也可以設置等待隊列等待某個事件,當滿足這個事件以後自動喚醒等待隊列中的進程
在這裏插入圖片描述
在這裏插入圖片描述

實驗代碼與分析

實驗代碼


struct irq_keydesc{
    int gpio;                               //gpio num
    int irqnum;                             //irq num
    unsigned char value;                    //key value
    char name[10];                          //irq name
    irqreturn_t (*handler) (int, void*);    //ird handler
};

struct irqkey_dev
{
    dev_t devid;
	...
    wait_queue_head_t r_wait;
};

struct irqkey_dev key;//key device

//key irq_handler
static irqreturn_t key0_handler(int irq, void *dev_id)
{
    struct irqkey_dev *dev = (struct irqkey_dev *)dev_id;

    dev->curkeynum = 0;
    dev->timer.data = (volatile long)dev_id;
    mod_timer(&dev->timer, jiffies + msecs_to_jiffies(10));
    return 1;
}
//timer function 
void timer_function(unsigned long arg)
{
    unsigned char value;
    unsigned char num;
    struct irq_keydesc *keydesc;
    struct irqkey_dev *dev = (struct irqkey_dev *)arg;

    num = dev->curkeynum;
    keydesc = &dev->irqkeydesc[num];
    value = gpio_get_value(keydesc->gpio);
    if(value == 0)
    {
        atomic_set(&dev->releasekey, keydesc->value);
    }
    else
    {
        atomic_set(&dev->keyvalue, 0x80 | keydesc->value);
        atomic_set(&dev->releasekey, 1);
    }

    //wake up process
    if(atomic_read(&dev->releasekey))
    {
        wake_up_interruptible(&dev->r_wait);
    }
}


static int keyio_init(void)
{
    char name[10];
    int ret;
	...
    //4.request irq
    key.irqkeydesc[0].handler = key0_handler;
    key.irqkeydesc[0].value = KEYVALUE;

    ret = request_irq(key.irqkeydesc[0].irqnum,
				key.irqkeydesc[0].handler,
				IRQF_TRIGGER_FALLING|IRQF_TRIGGER_RISING,
				key.irqkeydesc[0].name, 
				&key);
    
    //5.create timer
    init_timer(&key.timer);
    key.timer.function = timer_function;

    init_waitqueue_head(&key.r_wait);
    return 0;
}

static ssize_t key_read(struct file *filp,char __user *buf,size_t cnt,loff_t *offt)
{
    int ret;
    unsigned char value = 0;
    unsigned char release = 0;
    struct irqkey_dev *dev = (struct irqkey_dev *)filp->private_data;

#if 0
    ret = wait_event_interruptible(dev->r_wait, atomic_read(&dev->releasekey));
    if(ret)
    {
        goto wait_error;
    }
#endif

    DECLARE_WAITQUEUE(wait, current);//定義一個等待隊列
    if(atomic_read(&dev->releasekey) == 0)//沒有按鍵按下
    {
        add_wait_queue(&dev->r_wait, &wait);//添加到等待隊列頭
        __set_current_state(TASK_INTERRUPTIBLE);//設置任務狀態
        schedule();//進行一次任務切換
        //if(signal_pending(current))//判斷是否爲信號引起的喚醒
        if (signal_pending(current))
        {
            ret = -ERESTARTSYS;
            goto wait_error;
        }
    }
    remove_wait_queue(&dev->r_wait, &wait);
   // printk(KERN_EMERG "key_read enter!\n");
    value = atomic_read(&dev->keyvalue);
    release = atomic_read(&dev->releasekey);
    if(release)
    {
        if(value & 0x80)
        {
            value &= ~0x80;
            ret = copy_to_user(buf, &value, sizeof(value));
        }
        else
        {
            return -EINVAL;
        }
        atomic_set(&dev->releasekey, 0);
    }   
    else
    {
        return -EINVAL;
    }
     
    return 0;
wait_error:
    set_current_state(TASK_RUNNING);//設置任務爲運行態
    remove_wait_queue(&dev->r_wait, &wait);//將等待隊列移除
    return ret;
}

代碼分析

在自定義的設備結構體irqkey_dev中我們定義了一個等待隊列頭
在對按鍵初始化的時候我們使用init_waitqueue_head對等待隊列頭進行初始化

init_waitqueue_head(&key.r_wait);

我們主要關注key_read()函數,在這裏我們完成了等待隊列的主要工作

DECLARE_WAITQUEUE(wait, current);//定義一個等待隊列
    if(atomic_read(&dev->releasekey) == 0)//沒有按鍵按下
    {
        add_wait_queue(&dev->r_wait, &wait);//添加到等待隊列頭
        __set_current_state(TASK_INTERRUPTIBLE);//設置任務狀態
        schedule();//進行一次任務切換
        //if(signal_pending(current))//判斷是否爲信號引起的喚醒
        if (signal_pending(current))
        {
            ret = -ERESTARTSYS;
            goto wait_error;
        }
    }
    remove_wait_queue(&dev->r_wait, &wait);
  ...
  wait_error:
    set_current_state(TASK_RUNNING);//設置任務爲運行態
    remove_wait_queue(&dev->r_wait, &wait);//將等待隊列移除
    return ret;

以上這段代碼我成了等待隊列的定義,並將等待隊列加入到了我們之前初始化過的等待隊列頭
當按鍵沒有按下時我們使用__set_current_state將當前任務設置爲TASK_INTERRUPTIBLE(睡眠狀態,等待一些事件的發生,淺睡眠)
然後使用schedule()來進行一次調度,這是CPU的使用權就被交出去了
在醒來時還要注意,由於調度出去的時候進程狀態是TASK_INTERRUPTIBLE,所以喚醒它的可能是信號,因此我們首先通過signal_pending(current)瞭解是不是信號喚醒的,如果是的話返回-ERESTARTSYS,當然在返回前還要將任務設置爲運行態並將等待隊列移除出等待隊列頭
而進程的喚醒一般是在中斷中進行的,我們的按鍵是通過中斷來捕獲的,但還有一個定時器來進行按鍵防抖,所以我們的進程喚醒放在了定時器的回調函數中

 //wake up process
 if(atomic_read(&dev->releasekey))
 {
     wake_up_interruptible(&dev->r_wait);
 }

當我們檢測到按鍵確實被按下後,調用wake_up_interruptible來喚醒剛纔進入TASK_INTERRUPTIBLE狀態的任務
被喚醒的進程將會執行下面的代碼

value = atomic_read(&dev->keyvalue);
release = atomic_read(&dev->releasekey);
if(release)
{
    if(value & 0x80)
    {
        value &= ~0x80;
        ret = copy_to_user(buf, &value, sizeof(value));
    }
    else
    {
        return -EINVAL;
    }
    atomic_set(&dev->releasekey, 0);
}   
else
{
    return -EINVAL;
}

從自定義設備結構體中讀取鍵值並將數據拷貝到用戶空間

這樣我們就實現了使用等待隊列的阻塞訪問

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章