本文简介:
阻塞和非阻塞I/O是设备访问的两种不同模式,驱动程序可以灵活地支持用户空间对设备的这两种访问方式。
8.1节讲解阻塞I/O和非阻塞I/O的区别,实现阻塞I/O的等待队列机制,以及在globalfifo设备驱动中增加对阻塞I/O支持的方法,并进行了用户空间的验证。
8.2节讲解设备驱动的轮训(poll)操作的概念和编程方法,poll操作可以帮助用户了解是否能对设备进行无阻塞访问。
8.3节讲解在globalfifo中增加poll操作的方法,并进行了用户空间的验证。
8.1 阻塞与非阻塞I/O
阻塞操作是指在执行设备操作时若不能获得资源则挂起进程,直到满足可操作的条件后再进行操作。被挂起的进程进入休眠状态,被从调度器的运行队列移走,直到等待的条件被满足。而非阻塞操作的进程在不能进行设备操作时并不挂起,它或者放弃,或者不停地查询,直到可以进行操作为止。
驱动程序通常需要提供这样的能力:当应用程序进行read()、write()等系统调用时,若设备的资源不能获取,而用户又希望以阻塞地方式访问设备,驱动程序应在设备驱动的read()、write()等操作中将进程阻塞直到资源可以获取,此后,应用程序的read()、write()等调用才返回,整个过程仍然进行了正确的设备访问,用户并没有感知到;若用户以非阻塞的方式访问设备文件,则当设备资源不可获取时,设备驱动的xxx_read()、xxx_write()等操作应立即返回,read()、write()等系统调用也随即被返回。
阻塞从字面上听起来似乎意味着低效率,实则不然,如果设备驱动不阻塞,则用户想获取设备资源只能不停地查询,这反而会无谓地耗费CPU资源。而阻塞访问时,不能获取资源的进程将进入休眠,它将CPU资源让给其他进程。
因为阻塞的进程会进入休眠状态,因此,必须确保有一个地方能够唤醒休眠的进程。唤醒进程的地方最大可能发生在中断里面,因为硬件资源获得的同时往往伴随着一个中断。
代码清单8.1和代码清单8.2分别演示了以阻塞和非阻塞方式读取串口一个字符的代码。实际的串口编程中,若使用非阻塞模式,还可借助信号(sigaction)以异步方式访问串口以提高CPU利用率,而这里仅仅是为了说明阻塞与非阻塞的区别。
代码清单8.1 阻塞地读取串口一个字符
char buf;
fd = open("/dev/ttyS1", O_RDWR);
...
res = read(fd, &buf, 1); //当串口上有输入时才返回
if(res == 1)
printf("%c\n", buf);
代码清单8.2 非阻塞地读取串口一个字符
char buf;
fd = open("/dev/ttyS1", O_RDWR | O_NONBLOCK);
...
while(read(fd,&buf,1) != 1); //串口上无输入也返回,所以要循环尝试读取串口
printf("%c\n", buf);
一、等待队列
在Linux驱动程序中,可以使用等待队列(wait queque)来实现阻塞进程的唤醒。wait queue很早就作为一个基本的功能单位出现在Linux内核里了,它以队列为基础数据结构,与进程调度机制紧密结合,能够用于实现内核中的异步事件通知机制。等待队列可以用来同步对系统资源的访问,第七章中所讲述的信号量在内核中也依赖等待队列来实现。
Linux2.6提供如下关于等待队列的操作。
1、定义等待队列头
wait_queue_head_t my_queque;
2、初始化等待队列头
init_waitqueue_head(&my_queue);
而下面的DECLARE_WAIT_QUEUE_HEAD()宏定义可以作为定义并初始化等待队列头的“快捷方式”。
DECLARE_WAIT_QUEUE_HEAN (name)
3、定义等待队列
DECLARE_WAITQUEUE(name, tsk)
该宏用于定义并初始化一个名为name的等待队列。
4、添加/移除等待队列
void fastcall add_wait_queue(wait_queue_head_t *q, wait_queue_t *wait);
void fastcall remove_wait_queue(wait_queue_head_t *q, wait_queue_t *wait);
add_wait_queue用于将等待队列wait添加到等待队列头q指向的等待队列链表中,而remove_wait_queue()用于将等待队列wait从附属的等待队列头q指向的等待队列链表中移除。
5、等待事件
wait_event(queue, condition)
wait_event_interruptible(queue, condition)
wait_event_timeout(queue, condition, timeout)
wait_event_interruptible_timeout(queue, condition, timeout)
等待第一个参数queue作为等待队列头的等待队列被唤醒,而且第二个参数condition必须满足,否则阻塞。wait_event()和wait_event_interruptible()的区别在于后者可被信号打断,而前者不能。加上_timeout后的宏意味着阻塞等待的超时时间,以jiffy为单位,在第三个参数的timeout到达时,不论condition是否满足,均返回。
wait_event()的定义如代码清单8.3所示,从其源代码可以看出,当condition满足时,wait_event()会立即返回,否则, 阻塞等待condition满足。
代码清单8.3 wait_event()函数
#define wait_event(wq, condition)
do{
if(condition) /*条件满足立即返回*/
break;
__wait_event(wq, condition); /*添加等待队列并阻塞*/
}while(0)
#define __wait_event(wq, condition)
do{
DEFINE_WAIT(__wait);
for(;;){
prepare_to_wait(&wq, &__wait, TASK_UNINTERRUPTIBLE);
if(condition)
break;
schedule(); /*放弃CPU*/
}
finished_wait(&wq, &__wait);
}while(0)
void fastcall
prepare_to_wait(wait_queue_head_t *q, wait_queue_t *wait, int state){
unsigned long flags;
wait->flags &= ~WQ_FLAG_EXCLUSIVE;
spin_lock_irqsave(&q->lock, flags);
if(list_empty(&wait->task_list))
__add_wait_queue(q, wait); //添加等待队列
if(is_sync_wait(wait))
set_current_state(state); //改变当前进程的状态为休眠
spin_unlock_irqrestore(&q->lock, flags);
}
void fastcall finish_wait(wait_queue_head_t *q, wait_queue_t *wait){
unsigned long flags;
__set_current_state(TASK_RUNNING); //恢复当前进程的状态为TASK_RUNNING
if(!list_empty_careful(&wait->task_list)){
spin_lock_irqsave(&q->lock, flags);
list_del_init(&wait->task_list);
spin_unlock_irqrestore(&q->lock, flags);
}
}
6、唤醒队列
void wake_up(wait_queue_head_t *queue);
void wake_up_interruptible(wait_queue_head_t *queue);
上述操作会唤醒以queue作为等待队列头的所有等待队列中所有属于该等待队列头的等待队列所对应的进程。
wake_up()应与wait_event()或wait_event_timeout()成对使用,而wake_up_interruptible()则应与wait_event_interruptible()或wait_event_interruptible_timeout()成对使用。wake_up()可唤醒处于TASK_INTERRUPTIBLE和TASK_UNINTERRUPTIBLE的进程,而wake_up_interruptible()只能唤醒处于TASK_INTERRUPTIBLE的进程。
7、在等待队列上睡眠
sleep_on(wait_queue_head_t *q);
interruptible_sleep_on(wait_queue_head_t *q);
sleep_on()函数的作用就是将目前进程的状态设置成TASK_UNINTERRUPTIBLE,并定义一个等待队列,之后把它附属到等待队列头q,直到资源可获得,q引导的等待队列被唤醒。
interruptible_sleep_on()与sleep_on()函数类似,其作用是将目前进程的状态设置成TASK_INTERRUPTIBLE,并定义一个等待队列,之后把它附属到等待队列头q,直到资源可获得,q引导的等待队列被唤醒或者进程收到信号。
sleep_on()函数应与wake_up()成对使用,interruptible_sleep_on()应与wake_up_interruptible()成对使用。
代码清单8.4和代码清单8.5分别列出了sleep_on()和interruptible_sleep_on()函数的源代码。
从代码清单8.4和代码清单8.5可以看出,不论是sleep_on()还是interruptible_sleep_on(),其流程都如下所示。
(1)定义并初始化一个等待队列,将进程状态改变位TASK_UNINTERRUPTIBLE(不能被信号打断)或TASK_INTERRUPTIBLE(可以被信号打断),并将等待队列添加到等待队列头。
(2)通过schedule()放弃CPU,调度其他进程执行。
(3)进程被其他地方唤醒,将等待队列移出等待队列头。
在内核中使用set_current_state()函数或__add_wait_queue()函数来实现目前进程状态的改变,直接采用current->state = TASK_UNINTERRUPTIBLE类似的赋值语句也是可行的。通常而言,set_current_state()函数在任何环境下都可以使用,不会存在并发问题,但是效率要低于__add_wait_queue()。
因此,在许多设备驱动中,并不调用sleep_on()或interruptible_sleep_on(),而是亲自进行进程的状态改变和切换,如代码清单8.6所示。
代码清单8.6 在驱动程序中改变进程状态并调用schedule()
static ssize_t xxx_write(struct file *file, const char *buffer, size_t count, loff_t *ppos){
...
DECLARE_WAITQUEUE(wait, current); /*定义等待队列*/
__add_wait_queue(&xxx_wait, &wait); //添加等待队列
ret = count;
/*等待设备缓冲区可写*/
do{
avail = device_writeable(...);
if(avail < 0)
__set_current_state(TASK_INTERRUPTIBLE); //改变进程状态
if(avail < 0){
if(file->f_flags & O_NONBLOCK){
//非阻塞
if(!ret)
ret = -EAGAIN;
goto out;
}
schedule(); //调度其他进程执行
if(signal_pending(current)){
//如果是因为信号唤醒
if(!ret)
ret = -ERESTARTSYS;
goto out;
}
}while(avail < 0);
/*写设备缓冲区*/
device_write(...)
out:
remove_wait_queue(&xxx_wait, &wait); //将等待队列移出等待队列头
set_current_state(TASK_RUNNING); //设置进程状态为TASK_RUNNING
return ret;
}
二、支持阻塞操作的globalfifo设备驱动
现在我们给globalfifo增加这样的约束:把globalfifo中的全局内存变成一个FIFO,只有当FIFO中有数据地时候(即有进程把数据写到这个FIFO而且没有被读进程读空),读进程才能把数据读出,而且读取后地数据会从globalfifo的全局内存中被拿掉;只有当FIFO非满时(即还有一些空间未被写,或写满后被读进程从这个FIFO中读出了数据),写进程才能往这个FIFO中写入数据。
现在,将这个globalfifo重命名为“globalfifo”,在这个globalfifo中,读FIFO将唤醒写FIFO,而写FIFO也将唤醒读FIFO。首先,需要修改设备结构体,在其中增加两个等待队列头,分别对应于读和写,如代码清单8.7所示。
代码清单8.7 globalfifo设备结构体
struct globalfifo_dev{
struct cdev cdev; /*cdev结构体*/
unsigned int current_len; /*fifo有效数据长度*/
unsigned char mem[GLOBALFIFO_SIZE]; /*全局内存*/
struct semaphore sem; /*并发控制用的信号量*/
wait_queue_head_t r_wait; /*阻塞读用的等待队列头*/
wait_queue_head_t w_wait; /*阻塞写用的等待队列头*/
};
与globalmem设备结构体的另一个不同是增加了current_len成员用于表征目前FIFO中有效数据的长度。
这个等待队列须在设备驱动模块加载函数中调用init_waitqueue_head()被初始化,新的设备驱动模块加载函数如代码清单8.8所示。
代码清单8.8 globalfifo设备驱动模块加载函数
int globalfifo_init(void){
int ret;
dev_t devno = MKDEV(globalfifo_major, 0);
/*申请设备号*/
if(globalfifo_major)
ret = register_chrdev_region(devno, 1, "globalfifo");
else{
//动态申请设备号
ret = alloc_chrdev_region(&devno, 0, 1, "globalfifo");
globalfifo_major = MAJOR(devno);
}
if(ret < 0)
return ret;
/*动态申请设备结构体的内存*/
globalfifo_devp = kmalloc(sizeof(struct globalfifo_dev), GFP_KERNEL);
if(!globalfifo_devp){
ret = -ENOMEM;
goto fail_malloc;
}
memset(globalfifo_devp, 0, sizeof(struct globalfifo_dev));
globalfifo_setup_cdev(globalfifo_devp, 0);
init_MUTEX(&globalfifo_devp->sem); /*初始化信号量*/
init_waitqueue_head(&globalfifo_devp->r_wait); /*初始化读等待队列头*/
init_waitqueque_head(&globalfifo_devp->w_wait); /*初始化写等待队列头*/
return 0;
fail_malloc: unregister_chrdev_region(devno, 1);
return ret;
}
设备驱动读写操作需要被修改,在读函数中需增加等待globalfifo_devp->w_wait被唤醒的语句,而在写操作中唤醒globalfifo_devp->r_wait,如代码清单8.9所示。
代码清单8.9 增加等待队列后的globalfifo读写函数
/*globalfifo读函数*/
static ssize_t globalfifo_read(struct file *filp, char __user *buf, size_t count, loff_t *ppos){
int ret;
struct globalfifo_dev *dev = filp->private_data; //获得设备结构体指针
DECLARE_WAITQUEUE(wait, current); //定义等待队列
down(&dev->sem); //获得信号量
add_wait_queue(&dev->r_wait, &wait); //进入读等待队列头
/*等待FIFO非空*/
if(dev->current_len == 0){
if(filp->f_flags & O_NONBLOCK){
ret = -EAGAIN;
goto out;
}
__set_current_state(TASK_INTERRUTIBLE); //改变进程状态为睡眠
up(&dev->sem);
schedule(); //调度其他进程执行
if(signal_pending(current)){
//如果是因为信号唤醒
ret = -ERESTARTSYS;
goto out2;
}
down(&dev->sem);
}
/*复制到用户空间*/
if(count > dev->current_len)
count = dev->current_len;
if(copy_to_user(buf, dev->mem, count)){
ret = 0EFAULT;
goto out;
}
else{
memcpy(dev->mem, dev->mem + count, dev->current_len - count); //fifo数据前移
dev->current_len -= count; //有效数据长度减少
printk(KERN_INFO "read %d bytes(s),current_len:%d\n", count, dev->current_len);
wake_up_interruptible(&dev->w_wait); //唤醒写等待队列
ret = count;
}
out: up(&dev->sem); //释放信号量
out2: remove_wait_queue(&dev->w_wait, &wait); //从附属的等待队列头移除
set_current_state(TASK_RUNNING);
return ret;
}
/*globalfifo写操作*/
static ssize_t globalfifo_write(struct file *filp, const char __user *buf, size_t count, loff_t *ppos){
struct globalfifo_dev *dev = filp->private_data; //获得设备结构体指针
int ret;
DECLARE_WAITQUEUE(wait, current); //定义等待队列
down(&dev->sem); //获取信号量
add_wait_queue(&dev->w_wait, &wait); //进入写等待队列头
/*等待FIFO非满*/
if(dev->current_len == GLOBALFIFO_SIZE){
if(filp->f_flags & O_NONBLOCK){
//如果是阻塞访问
ret = -EAGAIN;
goto out;
}
__set_current_state(TASK_INTERRUPTIBLE); //改变进程状态为睡眠
up(&dev->sem);
schedule(); //调度其他进程执行
if(signal_pending(current)){
//如果是因为信号唤醒
ret = -ERESTARTSYS;
goto out2;
}
down(&dev->sem); //获得信号量
}
/*从用户空间复制到内核空间*/
if(count > GLOBALFIFO_SIZE - dev->current_len)
count = GLOBALFIFO - dev->current_len;
if(copy_from_user(dev->mem + dev->current_len, buf, count)){
ret = -EFAULT;
goto out;
}
else{
dev->current_len += count;
printk(KERN_INFO "written %d bytes(s), current_len:%d\n", count, dev->current_len);
wake_up_interruptible(&dev->r_wait); //唤醒读等待队列
ret = count;
}
out: up(&dev->sem); //释放信号量
out2: remove_wait_queue(&dev->w_wait, &wait); //从附属的等待队列头移除
set_current_state(TASK_RUNNING);
return ret;
}
代码清单8.9处理了等待队列进出和进程切换的过程。是否可以把读函数中一大段用于等待dev->current_len!=0的内容直接用wait_event_interruptible(dev->r_wait, dev->current_len!=0)替换,把写函数中一大段用于等待dev->current_len!= GLOBALFIFO_SIZE的代码用wai_event_interruptible(dev->w_wait, dev->current_len!=0)替换呢?
实际上,就控制等待队列非空和非满的角度而言,wait_event_interruptible(dev->r_wait, dev->current_len!=0)和上述代码功能完全一样,wait_event_interruptible(dev->w_wait, dev->current_len!=0)和上述代码功能完全一样。细微的区别体现在上述代码在进行schedule()即进程切换前,通过up(&dev->sem)释放了信号量。这一细微的动作意义重大,非如此,则死锁不可避免。
如图8.1(a)所示,假设目前的FIFO为空,即dev->current_len为0,此时如果有一个读进程,它会先获得信号量,因为条件不满足,它将因为wait_event_interruptible(dev->r_wait, dev->current_len!=0)而阻塞,而释放dev->r_wait 等待队列及让 dev->current_len != 0 的操作又需要在写进程中进行,写进程在执行写操作前又必须等待读进程释放信号量,造成互相等待对方资源的矛盾局面,从而死锁。
如图 8.1(b)所示,假设目前的 FIFO 为满即 dev->current_len 为 GLOBALFIFO_SIZE,此时如果有一个写 进 程 , 它 先 获 得 信 号 量 , 因 为 条 件 不 满 足 , 它 将 因 为 wait_event_interruptible (dev->w_wait,dev->current_len != GLOBALFIFO_SIZE)而阻塞,而释放 dev->w_wait 等待队列及让 dev->current_ len !=GLOBALFIFO_SIZE 的操作又需要在读进程中进行,读进程在执行读操作前又必须等待写进程释放信号量,造成互相等待对方资源的矛盾局面,从而死锁。
所谓死锁,就是多个进程循环等待它方占有的资源而无限期僵持下去的局面。如果没有外力的作用,那么死锁涉及的各个进程都将永远处于封锁状态。因此,驱动工程师一定要注意当多个等待队列、信号量等机制同时出现时,谨防死锁。
从代码清单可以发现在设备驱动的read()、write()等功能函数中,可以通过filp->f_flags标志获得用户空间是否要求非阻塞访问。驱动中可以根据此标志判断用户究竟要求阻塞还是非阻塞访问,从而进行不同的处理。
图8.1 等待队列、信号量等引起的死锁
8.2 轮询操作
一、轮询的概念和作用
在用户程序中,select()和poll()也是与设备阻塞和非阻塞访问息息相关的论题。使用非阻塞I/O的应用程序通常会使用select()和poll()系统调用查询是否可对设备进行无阻塞的访问。select()和poll()系统调用最终会引发设备驱动中的poll()函数被执行,在2.5.45内核中还引入了epoll(),即扩展的poll()。
select()和poll()系统调用的本质一样,前者在BSD UNIX中引入的,后者在System V中引入的。
二、应用程序中的轮询编程
应用程序中最广泛用到的是BSD UNIX中引入的select()系统调用,其原型如下:
int select(int numfds, fd_set *readfs, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
其中readfds、writefds、exceptfds分别是select()监视的读、写和异常处理的文件描述符集合,numfds的值是需要检查的号码最高的文件描述符加1。timeout参数是一个指向struct timeval类型的指针,它可以使select()在等待timeout时间后若没有文件描述符准备好则返回。struct timeval数据结构的定义如代码清单8.10所示。
代码清单8.10 timeval结构体定义
struct timeval{
int tv_sec;
int tv_usec;
};
下列操作用来设置、清除、判断文件描述符集合。
FD_ZERO(fd_set *set)
清除一个文件描述符集。
FD_SET(int fd, fd_set *set)
将一个文件描述符加入文件描述符集中。
FD_CLR(int fd, fd_set *set)
将一个文件描述符从文件描述符集中清除。
FD_ISSET(int fd, fd_set *set)
判断文件描述符是否被置位。
三、设备驱动中的轮询编程
设备驱动中poll()函数的原型如下:
unsigned int(*poll)(struct file *filp, struct poll_table *wait);
第一个参数为file结构体指针,第二个参数为轮询表指针。这个函数应该进行以下两项工作。
对可能引起设备文件状态变化的等待队列调用poll_wait()函数,将对应的等待队列头添加到poll_table()。
返回表示是否能对设备进行无阻塞读、写访问的掩码。
关键的用于向poll_table注册等待队列的poll_wait()函数的原型如下:
void poll_wait(struct file *filep, wait_queue_head_t *queue, poll_table *wait);
poll_wait()函数的名称非常容易让人产生误会,以为它和wait_event()等一样,会阻塞地等待某事件的发生,其实这个函数并不会引起阻塞。poll_wait()函数所做的工作是把当前进程添加到wait参数指定的等待列表(poll_table)中。
驱动程序poll()函数应该返回设备资源的可获取状态,即POLLIN、POLLOUT、POLLPRI、POLLERR、POLLNVAL等宏的位“或”结果。每个宏的定义都表明设备的一种状态,如POLLIN(定义为0x0001)意味着设备可以无阻塞地读,POLLOUT(定义为0x0004)意味着设备可以无阻塞地写。
通过以上分析,可得出设备驱动中poll()函数的典型模板,如代码清单8.11所示。
代码清单8.11 poll()函数典型模板
static unsigned int xxx_poll(struct file *filp, poll_table *wait){
unsigned int mask = 0;
struct xxx_dev *dev = filp->private_data; //获得设备结构体指针
...
poll_wait(filp, &dev->r_wait, wait); //加读等待队列头
poll_wait(filp, &dev->w_wait, wait); //加写等待队列头
/*可读*/
if(...){
mask |= POLLIN | POLLRDNORM; /*标识数据可获得*/
}
/*可写*/
if(...){
mask |= POLLOUT | POLLWRNORM; /*标识数据可写入*/
}
...
return mask;
}
8.3 支持轮询操作的globalfifo驱动
一、在globalfifo驱动中增加轮询操作
在globalfifo的poll()函数中,首先将设备结构体中的r_wait和w_wait等待队列头添加到等待列表,然后通过判断dev->current_len是否等于0来获得设备的可读状态,通过判断dev->current_len是否等于GLOBALFIFO_SIZE来获得设备的可写状态,如代码清单8.12所示。
代码清单8.12 globalfifo设备驱动的poll()函数
static unsigned int globalfifo_poll(struct file *filp, poll_table *wait){
unsigned int mask = 0;
struct globalfifo_dev *dev = filp->private_data; //获得设备指针结构体
down(&dev->sem);
poll_wait(filp, &dev->r_wait, wait);
poll_wait(filp, &dev->w_wait, wait);
/*fifo非空*/
if(dev->current_len != 0)
mask |= POLLIN | POLLRDRORM; /*标识数据可获得*/
/*fifo非满*/
if(dev->current_len != GLOBALFIFO_SIZE)
mask |= POLLOUT | POLLWRNORM; /*标识数据可写入*/
up(&dev->sem);
return mask;
}
注意要把globalfifo_poll赋给globalfifo_fops的poll成员,如下所示:
static const struct file_operations globalfifo_fops = {
...
.poll = globalfifo_poll,
...
};
二、在用户空间验证globalfifo设备的轮询
编写一个应用程序pollmonitor.c用于监控globalfifo的可读写状态,这个程序如代码清单8.13所示。
代码清单8.13 监控globalfifo是否可非阻塞读写的应用程序
#include ...
#define FIFO_CLEAR 0x1
#define BUFFER_LEN 20
main(){
int fd, num;
char rd_ch[BUFFER_LEN];
fd_set rfds, wfds; /*读、写文件描述符集*/
/*以非阻塞方式打开/dev/globalfifo设备文件*/
fd = open("/dev/globalfifo", O_RDONLY | O_NONBLOCK);
if(fd != -1){
/*FIFO清零*/
if(ioctl(fd, FIFO_CLEAR, 0) < 0){
printf("ioctl command failed\n");
}
while(1){
FD_ZERO(&rfds);
FD_ZERO(&wfds);
FD_SET(fd, &rfds);
FD_SET(fd, &wfds);
select(fd + 1, &rfds, &wfds, NULL, NULL);
/*数据可获得*/
if(FD_ISSET(fd, &rfds)){
printf("Poll monitor: can be read\n");
}
/*数据可写入*/
if(FD_ISSET(fd, &wfds)){
printf("Poll monitor: can be written\n");
}
}
}
else{
printf("Device open failure\n");
}
}
运行时看到,当没有任何输入,即FIFO为空时,程序不断地输出“Poll monitor: can be written”;当通过echo向/dev/globalfifo写入一些数据后,将输出“Poll monitor: can be read”和“Poll monitor: can be written”;如果不断地通过echo向/dev/globalfifo写入数据直到写满FIFO,发现pollmonitor程序将只输出“Poll monitor: can be read”。对于globalfifo而言,不会出现既不能读、又不能写的情况。
8.4 总结
阻塞与非阻塞访问是I/O操作的两种不同模式,前者在I/O操作暂时不可进行时会让进程睡眠。
在设备驱动中阻塞I/O一般基于等待队列来实现,等待队列可用于同步驱动中事件发生的先后顺序。使用非阻塞I/O的应用程序也可借助轮询函数来查询设备是否能立即被访问,用户空间调用select()和poll()接口,设备驱动提供poll()函数。设备驱动的 poll()本身不会阻塞,但是poll()和select()系统调用则会阻塞地等待文件描述符集合中的至少一个可访问或超时。