上一节获取按键状态时,是在应用层以循环的方式不断读取按键状态,但是我们实际关注的只是当按键被按下时发生的情况,所以大多数时间拿到的状态都是我们不需要的结果。
对此,当按键被释放时,让 read 接口处于阻塞状态,等按键被按下再解除阻塞。
要使用等待队列涉及到两个概念:等待队列头、等待项
等待队列通常使用链表实现,等待队列头便是链表的头节点,在Linux内核中使用 wait_queue_head_t 类型来表示等待队列头;等待项是等待队列中的一个子节点,通常是以线程为单位,将等待项加入到等待队列,相当于让线程处于休眠状态,在Linux内核中使用 wait_queue_t 类型来表示。相关API声明在
在使用等待队列之前,一般需要“声明 + 初始化”,接口原型如下(本质是宏)
/**
* @param q 等待队列
*/
void init_waitqueue_head(wait_queue_head_t *q);
向等待队列添加等待项之前需要先初始化等待项,等待项的初始化比较特殊,无需事先声明变量,使用的接口原型如下
/**
* @param name 等待项的名字
* @param tsk 当前所属任务(进程),一般填current,current 是一个全局变量,表示当前进程
*/
DECLARE_WAITQUEUE(name, tsk);
// 示例
// wait 自己拟定的变量名(无需事先声明),可以认为此处便是在“声明+定义”一个等待项
// current Linux内核的全局变量,表示当前进程
DECLARE_WAITQUEUE(wait, current);
向等待队列添加等待项,add_wait_queue 接口函数不会主动陷入阻塞,需要我们手动设置进程状态;当等待项被唤醒时,会自动从等待队列移除。接口原型如下:
/**
* @param q 等待队列头
* @param wait 等待项,需要和前面 DECLARE_WAITQUEUE 的第一个参数保持一致
*/
void add_wait_queue(wait_queue_head_t *q, wait_queue_t *wait);
拓展:wait_event_interruptible 在符合等待条件的时候,会自动进入到等待队列挂起,被唤醒时从挂起的位置开始继续运行。但由于无法确定等待条件何时触发,也就不知道在哪个位置挂起,这里推荐使用 add_wait_queue。
所谓唤醒,其实是将线程/进程由休眠态转变为就绪态,换句话说是将等待项(进程)从等待队列移到运行队列。
唤醒等待项的接口原型如下:
/**
* @param q 等待队列头
*/
void wake_up(wait_queue_head_t *q);
void wake_up_interruptible(wait_queue_head_t *q);
注意:wake_up 函数可以唤醒处于 TASK_INTERRUPTIBLE 和 TASK_UNINTERRUPTIBLE 状态的进 程,而 wake_up_interruptible 函数只能唤醒处于 TASK_INTERRUPTIBLE 状态的进程。所以在将等待项加入到等待队列以后务必要设置进程的状态。
当遇到一些异常情况导致进程终止时,我们需要主动将等待项从等待队列移除。接口原型如下:
/**
* @param q 等待队列头
* @param wait 等待项
*/
void remove_wait_queue(wait_queue_head_t *q, wait_queue_t *wait);
如果使用 wake_up_interruptible 来唤醒等待项(进程),需要在将等待项(进程)加入到等待队列后,设置进程的状态为 TASK_INTERRUPTIBLE。在内核中使用 __set_current_state 来设置进程状态。比如要设为 TASK_INTERRUPTIBLE
__set_current_state(TASK_INTERRUPTIBLE);
当进程进入到执行队列,此时要将进程状态设为运行状态。
__set_current_state(TASK_RUNNING);
调用 schedule 函数会主动触发调度器,让内核选择下一个要运行的进程。在调用 schedule 函数之前,会先调用 __set_current_state 将进程置于睡眠态或等待条件的状态。这样的话进程会在当前位置阻塞,下一次进程被唤醒时,会从阻塞的地方继续向下执行。
schedule();
在字符设备的结构体中声明一个等待队列的头节点,附加一个设备状态
typedef enum {
Pressed = 0U,
Released = 1U
}key_status;
struct chrdev_t
{
// ... ...
key_status status; /* 设备状态 */
wait_queue_head_t wait_head; /* 等待队列的头节点 */
};
static struct chrdev_t chrdev;
在驱动入口函数中初始化该等待队列,初始化设备状态变量,因为我们计划在按键处于释放状态时,新建一个等待项并添加到等待队列,所以需要一个变量来保存按键状态。
/* 驱动入口函数 */
static int __init xxx_init(void)
{
/* 初始化等待队列头 */
init_waitqueue_head(&chrdev.wait_head);
chrdev.status = Released;
// ...
}
每当应用层调用 read 接口,在 read 操作函数中,判断按键状态,如果为释放状态则添加到等待队列,同时将当前进程设为休眠态;等待队列中的等待项被唤醒时,再重新设为运行态。
static ssize_t chrdev_read(struct file *pfile, char __user * pbuf, size_t size, loff_t * poff)
{
// open 操作函数中 pfile->private_data = &chrdev;
struct chrdev_t* pdev = pfile->private_data;
if (pdev->status == Released)
{
/* 初始化等待项 */
DECLARE_WAITQUEUE(wait_item, current);
printk("加入等待队列\n");
add_wait_queue(&pdev->wait_head, &wait_item); // 添加到等待队列
__set_current_state(TASK_INTERRUPTIBLE); // 当前进程设为休眠态
schedule(); // 重新调度进程(此时会阻塞,等待被唤醒)
__set_current_state(TASK_RUNNING); // 被唤醒后,设为运行态
}
printk("等待项被唤醒\n");
// ...
}
当按键任务处理完毕,此时需要唤醒等待队列中的等待项。这里是搭配定时器实现了按键消抖,真正的处理逻辑在定时器的回调函数中。
static irqreturn_t key0_handler(int irq, void * dev)
{
mod_timer(&timer, timer_delay(50));
return IRQ_RETVAL(IRQ_HANDLED);
}
/* 定时器回调函数 */
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 函数来检测是否加入了等待队列,以及按键按下是否被唤醒。
#include
#include
#include
#include
#include
#include
void printHelp()
{
printf("usage: ./xxxApp \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() 的下一行开始运行