【Linux驱动】Linux阻塞IO —— 阻塞读取按键状态(等待队列实现)

上一节获取按键状态时,是在应用层以循环的方式不断读取按键状态,但是我们实际关注的只是当按键被按下时发生的情况,所以大多数时间拿到的状态都是我们不需要的结果。

【Linux驱动】Linux阻塞IO —— 阻塞读取按键状态(等待队列实现)_第1张图片

对此,当按键被释放时,让 read 接口处于阻塞状态,等按键被按下再解除阻塞。

一、等待队列API

要使用等待队列涉及到两个概念:等待队列头、等待项

等待队列通常使用链表实现,等待队列头便是链表的头节点,在Linux内核中使用 wait_queue_head_t 类型来表示等待队列头;等待项是等待队列中的一个子节点,通常是以线程为单位,将等待项加入到等待队列,相当于让线程处于休眠状态,在Linux内核中使用 wait_queue_t 类型来表示。相关API声明在 文件中。

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 接口函数不会主动陷入阻塞,需要我们手动设置进程状态;当等待项被唤醒时,会自动从等待队列移除。接口原型如下:

/**
 * @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。

3、唤醒等待队列中的等待项

所谓唤醒,其实是将线程/进程由休眠态转变为就绪态,换句话说是将等待项(进程)从等待队列移到运行队列

  • 唤醒:先移出等待队列,再移入运行队列
  • 移除:移出等待队列

唤醒等待项的接口原型如下:

/**
 * @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 状态的进程。所以在将等待项加入到等待队列以后务必要设置进程的状态

4、从等待队列移除等待项

当遇到一些异常情况导致进程终止时,我们需要主动将等待项从等待队列移除。接口原型如下:

/**
 * @param q     等待队列头
 * @param wait  等待项
 */
void remove_wait_queue(wait_queue_head_t *q, wait_queue_t *wait);

二、内核API

1、设置进程状态

如果使用 wake_up_interruptible 来唤醒等待项(进程),需要在将等待项(进程)加入到等待队列后,设置进程的状态为 TASK_INTERRUPTIBLE。在内核中使用 __set_current_state 来设置进程状态。比如要设为 TASK_INTERRUPTIBLE

__set_current_state(TASK_INTERRUPTIBLE);

当进程进入到执行队列,此时要将进程状态设为运行状态。

__set_current_state(TASK_RUNNING);

2、重新调度进程

调用 schedule 函数会主动触发调度器,让内核选择下一个要运行的进程。在调用 schedule 函数之前,会先调用 __set_current_state 将进程置于睡眠态或等待条件的状态。这样的话进程会在当前位置阻塞,下一次进程被唤醒时,会从阻塞的地方继续向下执行。

schedule();

三、驱动实现

1、定义等待队列

在字符设备的结构体中声明一个等待队列的头节点,附加一个设备状态

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;

2、初始化等待队列

在驱动入口函数中初始化该等待队列,初始化设备状态变量,因为我们计划在按键处于释放状态时,新建一个等待项并添加到等待队列,所以需要一个变量来保存按键状态。

/* 驱动入口函数 */
static int __init xxx_init(void)
{
	/* 初始化等待队列头 */
	init_waitqueue_head(&chrdev.wait_head);
    chrdev.status = Released;
   
    // ... 
}

3、向等待队列添加等待项

每当应用层调用 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");

    // ...
}

4、唤醒等待队列

当按键任务处理完毕,此时需要唤醒等待队列中的等待项。这里是搭配定时器实现了按键消抖,真正的处理逻辑在定时器的回调函数中。

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() 的下一行开始运行

【Linux驱动】Linux阻塞IO —— 阻塞读取按键状态(等待队列实现)_第2张图片

你可能感兴趣的:(linux,运维,服务器)