阻塞IO主要有两种方式,简单休眠和高级休眠,详细可以参考《linux设备驱动程序》,非阻塞IO比较简单,只需要判断file->f_flags是否设置了O_NONBLOCK标志,如果设置了此标志就是非阻塞IO,内核struct file_operations结构体中对应的函数在资源不可用时直接返回-EAGAIN即可,所以就在阻塞IO例程中加入了非阻塞IO的处理,就不单独讲解了。
一、简单休眠
简单休眠只使用等待队列即可,类型为wait_queue_head_t,当资源不可用时,需要使用宏定义wait_event_interruptible(wq, condition),这个宏定义展开之后跟下述高级休眠其实是类似的。
先介绍驱动结构,驱动主要针对按键输入,读取按键是否按下,驱动检测到按键按下并松开,才会通知应用发生了一次按键事件。在中断处理函数中,我们调用mod_timer()函数,10毫秒后启动定时器函数,来消抖,定时器处理函数中会调度一次工作队列,因为这里需要用到信号量获取临界区资源,但是定时器处理函数不允许阻塞,所以只能将此放入到工作队列处理函数中执行。之所以需要使用信号量,是因为此按键驱动允许多个应用程序来同时open按键设备文件,并读取按键值。
驱动中read函数代码如下:
static ssize_t blockio_read(struct file *file, char __user *buf, size_t count, loff_t *ppos)
{
struct blockio_dev * dev = (struct blockio_dev *)file->private_data;
int ret = 0;
ret = down_interruptible(&dev->sem);
if(ret){
return -ERESTARTSYS;
}
while(!dev->key_release){
up(&dev->sem);
if(file->f_flags & O_NONBLOCK){
return -EAGAIN;
}
//printk("\"%s\" reading: going to sleep.\n", current->comm);
if(wait_event_interruptible(dev->r_wait, dev->key_release))
return -ERESTARTSYS;
if(down_interruptible(&dev->sem))
return -ERESTARTSYS;
}
if(dev->key_release){
dev->key_release = 0;
ret = copy_to_user(buf, &dev->key_value, sizeof(dev->key_value));
if(ret){
up(&dev->sem);
return -EFAULT;
}
up(&dev->sem);
return sizeof(dev->key_value);
}
up(&dev->sem);
return 0;
}
可以看到read函数首先获取信号量,成功之后,在临界区使用while循环测试是否有按键按下,如果有,则不进行休眠,直接将按键值拷贝到用户空间,并释放信号量再返回,如果没有按键按下,则释放信号量,如果应用程序非阻塞读,直接返回-EAGAIN,如果阻塞读,则进行休眠,调用wait_event_interruptible宏,休眠的时候会在临界区外读取一下是否有按键按下,如果有,则wait_event_interruptible()直接返回0,不进行休眠,然后接下来down_interruptible()获取信号量,接着while循环再次测试按键是否按下;如果调用wait_event_interruptible时没有按键按下,则直接进行休眠,等待别的进程唤醒,然后获取信号量测试是否有按键按下。
所以,while循环结束后,肯定是按键按下了,并且已经成功竞争到信号量。所以接着调用copy_to_user()函数将按键键值拷贝到用户空间,然后释放信号量。
二、高级休眠
高级休眠和简单休眠的区别在于wait_event_interruptible()宏调用的东西,开发人员需要自己手动调用,优点是支持独占等待(详见ldd) 等简单休眠不支持的操作。
static ssize_t blockio_read(struct file *file, char __user *buf, size_t count, loff_t *ppos)
{
struct blockio_dev * dev = (struct blockio_dev *)file->private_data;
int ret = 0;
ret = down_interruptible(&dev->sem);
if(ret){
return -ERESTARTSYS;
}
while(!dev->key_release){
DEFINE_WAIT(wait);
up(&dev->sem);
if(file->f_flags & O_NONBLOCK){
return -EAGAIN;
}
//printk("\"%s\" reading: going to sleep.\n", current->comm);
prepare_to_wait(&dev->r_wait, &wait, TASK_INTERRUPTIBLE);
if(!dev->key_release)
schedule();
finish_wait(&dev->r_wait, &wait);
if(signal_pending(current))
return -ERESTARTSYS;
if(down_interruptible(&dev->sem))
return -ERESTARTSYS;
}
if(dev->key_release){
dev->key_release = 0;
ret = copy_to_user(buf, &dev->key_value, sizeof(dev->key_value));
if(ret){
up(&dev->sem);
return -EFAULT;
}
up(&dev->sem);
return sizeof(dev->key_value);
}
up(&dev->sem);
return 0;
}
可以看到我们只是在while的定义域内声明了DEFINE_WAIT(wait),并将wait_event_interruptible()宏换成了prepare_to_wait()和finish_wait()以及signal_pending()等调用,其他没有任何更改。注意,在其他地方可能见到,一下这种方式的写法:
while(dev->key_release == 0) {
DECLARE_WAITQUEUE(wait, current);
up(&dev->sem);
if(file->f_flags | NONBLOCK){
return -EAGAIN;
}
add_wait_queue(&dev->r_wait, &wait); /* 添加到等待队列头 */
__set_current_state(TASK_INTERRUPTIBLE);/* 设置任务状态 */
if(dev->key_release)
schedule(); /* 进行一次任务切换 */
__set_current_state(TASK_RUNNING); /*设置为运行状态 */
remove_wait_queue(&dev->r_wait, &wait); /*将等待队列移除 */
if(signal_pending(current)) { /* 判断是否为信号引起的唤醒 */
return -ERESTARTSYS;
}
if(down_interruptible(&dev->sem))
return -ERESTARTSYS;
}
这种接口是老的接口,比较繁琐不推荐使用。我也没有研究,所以写的可能有误。
关于高级休眠的注意点,比较关键的一点是,调度之前一定要对等待条件进行再次判断,也就是schedule()之前的if语句是必须的:
if(!dev->key_release)
schedule();
详细的原因可以参考ldd,第六章,阻塞IO,我这里简要分析一下:
1.如果我们等待的事件即dev->key_release被置1,在prepare_to_wait(&dev->r_wait, &wait, TASK_INTERRUPTIBLE)之前发生,那么,其他进程在将dev->key_release置1的同时肯定也调用了wake_up_interruptible()进行唤醒本进程,但是,prepare_to_wait()会将进程状态置为TASK_INTERRUPTIBLE,然后如果if不进行判断,直接调用schedule()则会直接进行休眠,从而丢掉本次被唤醒的机会,进入更长时间的休眠。
2.如果我们等待的事件即dev->key_release被置1,在prepare_to_wait(&dev->r_wait, &wait, TASK_INTERRUPTIBLE)之后发生,那么,if判断其实没有发挥作用,其他进程在将dev->key_release置1的同时肯定也调用了wake_up_interruptible()进行唤醒本进程,所以prepare_to_wait()设置的TASK_INTERRUPTIBLE,会被其他进程调用的wake_up_interruptible()覆盖,然后即使没有if判断,直接调用schedule()函数也不会引发休眠,而是直接返回。
3..如果我们等待的事件即dev->key_release被置1,在if语句和schedule()函数之间发生,没有任何影响,其实这种情况已经包含在2中了。
完整的代码参见:
里面包含了一些对定时器,工作队列的设置,以及工作队列函数中关于wake_up_interruptible(&dev->r_wait)函数的调用,另外还有poll函数的实现。