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;
}

从自定义设备结构体中读取键值并将数据拷贝到用户空间

这样我们就实现了使用等待队列的阻塞访问

发布了123 篇原创文章 · 获赞 598 · 访问量 34万+

猜你喜欢

转载自blog.csdn.net/a568713197/article/details/103110960