一. 阻塞与非阻塞IO概念
阻塞操作是指在执行设备操作时,若不能获取资源,则挂起进程进入休眠状态,等待可满足条件后进行操作。被挂起的进程从调度器队列移动到挂起队列(睡眠状态)。当操作驱动程序read、write操作时,应用程序希望以阻塞的方式访问设备,驱动程序需要提供响应的能力。在read、write中,当资源不可操作时,需要把进程挂起,直到资源 可用才获取资源并返回,整个过程仍然进行了正确的访问,应用层不可见,不能感知到这个挂起的过程。而非阻塞访问时,资源不可用,read、write操作会立即返回,并返回-EAGAIN。
在阻塞IO中,当进程进入到休眠状态时,需要一个地方将其唤醒,一般是在中断里面。若没有地方将其唤醒,则进程一直休眠。而非阻塞IO则不断尝试,直到可以获取资源。Linux系统中提供了多种方式解决阻塞与非阻塞的问题。本文介绍等待队列和轮询两种方式。
二、等待队列
在linux驱动程序中,可以使用等待队列来实现阻塞进程的唤醒。其以队列作为基础的数据结构,与进程调度机制紧密结合,可以用来同步对系统资源的访问。
1. 定义等待队列头
wait_queue_head_t m_queue;
2. 初始化
init_waitqueue_head(&m_queue);
或者使用宏DECLARE_WAIT_QUEUE_HEAD(name),定义并初始化一个等待队列。
DECLARE_WAIT_QUEUE_HEAD(name);
3. 定义等待队列元素
DECLARE_WAITQUEUE(name,tsk); //tsk最终是一个void*指针
定义并初始化一个名为name的等待队列元素。
4. 添加/移除等待队列
void add_wait_queue(wait_queue_head_t *q,wait_queue_t *wait); //队列元素添加到队列头部q
其中,q为双向链表
void remove_wait_queue(wait_queue_head_t *q,wait_queue_t *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必须被满足,否则继续等待。
xxx_interruptible表示可以被信号打断。xxx_timeout表示阻塞等待的超时事件,
以jiffy为单位时间,在timeout到达时,不论condition是否被满足均返回。
6. 唤醒队列
void wake_up(wait_queue_head_t *q);
void wake_up_interruptible(wait_queue_head_t *q);
唤醒以q为等待队列头部的所有进程。
wake_up与wait_event成对使用,wake_up_interruptible与wait_event_interruptible成对使用。
7. 在等待队列上睡眠
sleep_on(wait_queue_head_t *q);
interruptible_sleep_on(wait_queue_head_t *q);
sleep_on说将进程的状态转为休眠状态,将其挂到q对应的等待队列上,知道资源可用,
q上的进程被唤醒。sleep_on与wake_up成对使用。
示例代码
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);
//等待设备缓冲区可写
do{
avail = device_writable(...); //设备写入状态
if(avail < 0){ //设备不可写
if(file_f_flags & O_NONBLOCK){ //非阻塞
ret = -EAGAIN;
goto out;
}
__set_current_state(TASK_INTERRUPTIBLE); //设置进程状态
schedule(); //切换到其他进程
if(signal_pending(current)){ //进程切换回来时,判断是不是因为信号唤醒
ret = -ERESTARTSYS;
goto out;
}
}
}while(avail < 0);
device_writable(...);
out:
remove_wait_queue(&xxx_wait,&wait);
set_current_state(TASK_RUNNING);
return ret;
}
三、轮询操作
概念:在用户程序中,select和poll也是与设备阻塞与非阻塞访问信息息息相关的,使用非阻塞IO的应用程序通常使用select和poll系统调用查询是否对设备进行无阻塞的访问,select和poll系统调用最终会调用到驱动的poll函数。
int select(int numfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
readfds、writefds、exceptfds是被select监视的读、写和异常处理的文件描述符。numfds的值是需要检查的号码最高的fd加1。readfds文件集中任何一个文件变得可读,select返回。writefds文件集中任何一个文件变得可写,select返回。
接下来操作是设置、清除、判断文件描述符集合。
void FD_CLR(int fd, fd_set *set);将fd从set中清除出去(在集合中是1,清除出去就成0,set其实是位图)
int FD_ISSET(int fd, fd_set *set);判断fd是否在集合中;
void FD_SET(int fd, fd_set *set);将fd设置到集合中去;
void FD_ZERO(fd_set *set); 将set清空成0;
poll与select原理相似,原型为
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
当多路复用的文件数量庞大、IO操作频繁时,select和poll操作性能表现比较差,这时应该使用epoll,epoll不会随着文件数量的增大,性能下降明显。
在驱动程序中,poll函数的原型是
unsigned int (*poll)(struct file *filp,struct poll_table* wait);
filp:文件指针
wait:轮询表指针,
1)对可能引起设备文件状态变化的等待队列调用poll_wait函数,将对应的等待队列头部添加到poll_table中。
2)返回表示是否能对设备进行无阻塞读、写访问的掩码。
poll_wait函数原型如下
void poll_wait(struct file *filp, wait_queue_head_t *queue, poll_table *wait);
该函数并不会引起阻塞,它的工作是把当前进程添加到wait指定的等待列表中。实际作用是让唤醒参数queue对应的等待队列可唤醒因select()而休眠的进程。
驱动poll函数应该返回设备资源的可获取状态,即POLLIN、POLLOUT、POLLPRI、POLLERR、POLLNVAL等宏的位"或"结果。
每个宏表示一种设备状态,如下:
常量 说明
POLLIN 普通或优先级带数据可读
POLLRDNORM 普通数据可读
POLLRDBAND 优先级带数据可读
POLLPRI 高优先级数据可读
POLLOUT 普通数据可写
POLLWRNORM 普通数据可写
POLLWRBAND 优先级带数据可写
POLLERR 发生错误
POLLHUP 发生挂起
POLLNVAL 描述字不是一个打开的文件
poll驱动函数的示例
static unsigned int xxx_poll(struct file *filp, poll_table *wait)
{
unsigned int mask = 0;
struct xxx_dev *dev = filp->private_data; //获得设备结构指针
...
down(&dev->sem);
poll_wait(filp, &dev->r_wait, wait); //加读等待对列头
poll_wait(filp ,&dev->w_wait, wait); //加写等待队列头
if(...) //可读
{
mask |= POLLIN | POLLRDNORM; //标识数据可获得
}
if(...) //可写
{
mask |= POLLOUT | POLLRDNORM; //标识数据可写入
}
..
up(&dev->sem);
return mask;
}
四、总结
阻塞和非阻塞是两种不同的IO操作模式,阻塞在暂时不可进行IO操作时会让进程休眠,非阻塞则不然。在驱动程序中,阻塞IO一般基于等待队列或者基于等待队列的其他Linux内核API实现,等待队列可用于同步驱动中事件发生的先后顺序。使用非阻塞IO的应用程序可借助轮询函数查询设备是否能立即执行,调用驱动中的poll函数。驱动poll函数本身不会被阻塞,但是与poll、select、epoll相关的系统调用则会阻塞地等待至少一个文件描述符集合可访问或者超时。
参考书:Linux设备驱动开发详解(基于最新的Linux4.0内核) 宋宝华著
Linux设备驱动程序 J & G著