上一節(jié)獲取按鍵狀態(tài)時,是在應用層以循環(huán)的方式不斷讀取按鍵狀態(tài),但是我們實際關注的只是當按鍵被按下時發(fā)生的情況,所以大多數(shù)時間拿到的狀態(tài)都是我們不需要的結果。
對此,當按鍵被釋放時,讓 read 接口處于阻塞狀態(tài),等按鍵被按下再解除阻塞。
一、等待隊列API
要使用等待隊列涉及到兩個概念:等待隊列頭、等待項
等待隊列通常使用鏈表實現(xiàn),等待隊列頭便是鏈表的頭節(jié)點,在Linux內核中使用?wait_queue_head_t 類型來表示等待隊列頭;等待項是等待隊列中的一個子節(jié)點,通常是以線程為單位,將等待項加入到等待隊列,相當于讓線程處于休眠狀態(tài),在Linux內核中使用?wait_queue_t 類型來表示。相關API聲明在 <linux/wait.h> 文件中。
1、初始化等待隊列
在使用等待隊列之前,一般需要“聲明 + 初始化”,接口原型如下(本質是宏)
/**
* @param q 等待隊列
*/
void init_waitqueue_head(wait_queue_head_t *q);
2、向等待隊列添加等待項
向等待隊列添加等待項之前需要先初始化等待項,等待項的初始化比較特殊,無需事先聲明變量,使用的接口原型如下
/**
* @param name 等待項的名字
* @param tsk 當前所屬任務(進程),一般填current,current 是一個全局變量,表示當前進程
*/
DECLARE_WAITQUEUE(name, tsk);
// 示例
// wait 自己擬定的變量名(無需事先聲明),可以認為此處便是在“聲明+定義”一個等待項
// current Linux內核的全局變量,表示當前進程
DECLARE_WAITQUEUE(wait, current);
向等待隊列添加等待項,add_wait_queue 接口函數(shù)不會主動陷入阻塞,需要我們手動設置進程狀態(tài);當?shù)却棻粏拘褧r,會自動從等待隊列移除。接口原型如下:
/**
* @param q 等待隊列頭
* @param wait 等待項,需要和前面 DECLARE_WAITQUEUE 的第一個參數(shù)保持一致
*/
void add_wait_queue(wait_queue_head_t *q, wait_queue_t *wait);
拓展:wait_event_interruptible 在符合等待條件的時候,會自動進入到等待隊列掛起,被喚醒時從掛起的位置開始繼續(xù)運行。但由于無法確定等待條件何時觸發(fā),也就不知道在哪個位置掛起,這里推薦使用?add_wait_queue。
3、喚醒等待隊列中的等待項
所謂喚醒,其實是將線程/進程由休眠態(tài)轉變?yōu)榫途w態(tài),換句話說是將等待項(進程)從等待隊列移到運行隊列。
- 喚醒:先移出等待隊列,再移入運行隊列
- 移除:移出等待隊列
喚醒等待項的接口原型如下:
/**
* @param q 等待隊列頭
*/
void wake_up(wait_queue_head_t *q);
void wake_up_interruptible(wait_queue_head_t *q);
注意:wake_up 函數(shù)可以喚醒處于 TASK_INTERRUPTIBLE 和 TASK_UNINTERRUPTIBLE 狀態(tài)的進 程,而 wake_up_interruptible 函數(shù)只能喚醒處于 TASK_INTERRUPTIBLE 狀態(tài)的進程。所以在將等待項加入到等待隊列以后務必要設置進程的狀態(tài)。
4、從等待隊列移除等待項
當遇到一些異常情況導致進程終止時,我們需要主動將等待項從等待隊列移除。接口原型如下:
/**
* @param q 等待隊列頭
* @param wait 等待項
*/
void remove_wait_queue(wait_queue_head_t *q, wait_queue_t *wait);
二、內核API
1、設置進程狀態(tài)
如果使用?wake_up_interruptible 來喚醒等待項(進程),需要在將等待項(進程)加入到等待隊列后,設置進程的狀態(tài)為?TASK_INTERRUPTIBLE。在內核中使用?__set_current_state 來設置進程狀態(tài)。比如要設為?TASK_INTERRUPTIBLE
__set_current_state(TASK_INTERRUPTIBLE);
當進程進入到執(zhí)行隊列,此時要將進程狀態(tài)設為運行狀態(tài)。
__set_current_state(TASK_RUNNING);
2、重新調度進程
調用 schedule?函數(shù)會主動觸發(fā)調度器,讓內核選擇下一個要運行的進程。在調用 schedule?函數(shù)之前,會先調用 __set_current_state?將進程置于睡眠態(tài)或等待條件的狀態(tài)。這樣的話進程會在當前位置阻塞,下一次進程被喚醒時,會從阻塞的地方繼續(xù)向下執(zhí)行。
schedule();
三、驅動實現(xiàn)
1、定義等待隊列
在字符設備的結構體中聲明一個等待隊列的頭節(jié)點,附加一個設備狀態(tài)
typedef enum {
Pressed = 0U,
Released = 1U
}key_status;
struct chrdev_t
{
// ... ...
key_status status; /* 設備狀態(tài) */
wait_queue_head_t wait_head; /* 等待隊列的頭節(jié)點 */
};
static struct chrdev_t chrdev;
2、初始化等待隊列
在驅動入口函數(shù)中初始化該等待隊列,初始化設備狀態(tài)變量,因為我們計劃在按鍵處于釋放狀態(tài)時,新建一個等待項并添加到等待隊列,所以需要一個變量來保存按鍵狀態(tài)。
/* 驅動入口函數(shù) */
static int __init xxx_init(void)
{
/* 初始化等待隊列頭 */
init_waitqueue_head(&chrdev.wait_head);
chrdev.status = Released;
// ...
}
3、向等待隊列添加等待項
每當應用層調用 read 接口,在 read 操作函數(shù)中,判斷按鍵狀態(tài),如果為釋放狀態(tài)則添加到等待隊列,同時將當前進程設為休眠態(tài);等待隊列中的等待項被喚醒時,再重新設為運行態(tài)。
static ssize_t chrdev_read(struct file *pfile, char __user * pbuf, size_t size, loff_t * poff)
{
// open 操作函數(shù)中 pfile->private_data = &chrdev;
struct chrdev_t* pdev = pfile->private_data;
if (pdev->status == Released)
{
/* 初始化等待項 */
DECLARE_WAITQUEUE(wait_item, current);
printk("加入等待隊列\(zhòng)n");
add_wait_queue(&pdev->wait_head, &wait_item); // 添加到等待隊列
__set_current_state(TASK_INTERRUPTIBLE); // 當前進程設為休眠態(tài)
schedule(); // 重新調度進程(此時會阻塞,等待被喚醒)
__set_current_state(TASK_RUNNING); // 被喚醒后,設為運行態(tài)
}
printk("等待項被喚醒\n");
// ...
}
4、喚醒等待隊列
當按鍵任務處理完畢,此時需要喚醒等待隊列中的等待項。這里是搭配定時器實現(xiàn)了按鍵消抖,真正的處理邏輯在定時器的回調函數(shù)中。
static irqreturn_t key0_handler(int irq, void * dev)
{
mod_timer(&timer, timer_delay(50));
return IRQ_RETVAL(IRQ_HANDLED);
}
/* 定時器回調函數(shù) */
void timer_callback(unsigned long arg)
{
struct chrdev_t* pdev = (struct chrdev_t*)arg;
pdev->status = Pressed;
// 按鍵處理邏輯
pdev->status = Released;
// 喚醒等待項
wake_up_interruptible(&pdev->wait_head);
}
四、應用測試
在應用程序中,調用一次 read 函數(shù)來檢測是否加入了等待隊列,以及按鍵按下是否被喚醒。
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
void printHelp()
{
printf("usage: ./xxxApp <driver_path>\n");
}
int main(int argc, char* argv[])
{
if (argc != 2)
{
printHelp();
return -1;
}
char* driver_path = argv[1];
int state = 0;
int ret = 0;
int fd = 0;
fd = open(driver_path, O_RDONLY);
if (fd < 0)
{
perror("open file failed");
return -2;
}
ret = read(fd, &state, sizeof(state));
if (ret < 0)
{
printf("read data error\n");
}
close(fd);
return 0;
}
程序開始運行時,因為加入到等待隊列,一開始會阻塞
當按鍵按下,等待項被喚醒,進程會從掛起的地方,也就是 schedule() 的下一行開始運行文章來源:http://www.zghlxwxcb.cn/news/detail-784836.html
文章來源地址http://www.zghlxwxcb.cn/news/detail-784836.html
到了這里,關于【Linux驅動】Linux阻塞IO —— 阻塞讀取按鍵狀態(tài)(等待隊列實現(xiàn))的文章就介紹完了。如果您還想了解更多內容,請在右上角搜索TOY模板網以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網!