阻塞操作是指在执行设备操作时,若不能获得资源,则挂起进程直到满足可操作的条件后再进行操作。被挂起的进程进入睡眠状态,被从调度器的运行队列移走,直到等待的条件被满足。 非阻塞操作的进程在不能进行设备操作时, 并不挂起,它要么放弃,要么不停地查询,直至可以进行操作为止。
在阻塞访问时, 不能获取资源的进程将进入休眠, 它将CPU资源“礼让”给其他进程。因为阻塞的进程会进入休眠状态, 所以必须确保有一个地方能够唤醒休眠的进程, 否则, 进程就真的“寿终正寝”了。 唤醒进程的地方最大可能发生在中断里面, 因为在硬件资源获得的同时往往伴随着一个中断。 而非阻塞的进程则不断尝试, 直到可以进行I/O
如何设置文件为阻塞/非阻塞:
// open open("file_name", O_RDWR | O_NONBLOCK); //或 fcntl fcntl(fd,F_SETFL, O_NONBLOCK)
阻塞访问最大的好处是当设备不可操作时进程可以挂起进入睡眠,让出CPU。
在Linux驱动程序中, 可以使用等待队列(Wait Queue) 来实现阻塞进程的唤醒。等待队列作为Linux内核中的一个基本单位,以队列为数据结构,与调度机制紧密结合,可以用来同步对系统资源的访问。
定义在:include/linux/wait.h
struct __wait_queue_head {
spinlock_t lock;
struct list_head task_list;
};
typedef struct __wait_queue_head wait_queue_head_t;
//定义等待队列头部
wait_queue_head_t my_queue;
//初始化等待队列头部
void init_waitqueue_head(&my_queue);
或使用宏DECLARE_WAIT_QUEUE_HEAD定义+初始化:
//或使用宏 定义且初始化 等待队列头
#define DECLARE_WAIT_QUEUE_HEAD(my_queue) \
wait_queue_head_t my_queue= __WAIT_QUEUE_HEAD_INITIALIZER(my_queue)
struct __wait_queue {
unsigned int flags;
void *private;
wait_queue_func_t func;
struct list_head task_list;
};
typedef struct __wait_queue wait_queue_t;
//定义并初始化一个名为name的等待队列元素
/* 参数name:等待队列项的名字
* 参数tsk:表示等待队列项属于哪个任务,一般设置为current(表示当前进程)
*/
#define DECLARE_WAITQUEUE(name, tsk) \
wait_queue_t name = __WAITQUEUE_INITIALIZER(name, tsk)
//添加/删除等待队列中的元素
/* 参数q:等待队列项要加入的等待队列头
* 参数wait:要加入的等待队列项
*/
void add_wait_queue(wait_queue_head_t *q, wait_queue_t *wait);
/* 参数q:要删除的等待队列项所在的等待队列头
* 参数wait:要删除的等待队列项
*/
void remove_wait_queue(wait_queue_head_t *q, wait_queue_t *wait);
除了主动唤醒,也可以设置等待队列等待某个事件,当事件满足就自动唤醒等待队列中的进程。
函数 | 说明 |
wait_event(wq, condition) | 等待以wq为等待队列头的等待队列被唤醒,前提是condition条件满足(为真),否则一直阻塞。函数会将进程设置为TASK_UNINTERRUPTIBLE。 |
wait_event_timeout(wq, condition, timeout) | 和wait_event类似,但可以添加超时时间,以jiffies为单位。返回值为0表示超时时间到而且condition为假;如果返回值为1表示condition为真,条件满足。 |
wait_event_interruptible(wq, condition) | 和wait_event类似,但设置进程状态为TASK_INTERRUPTIBLE,可以被信号打断。 |
wait_event_interruptible_timeout(wq, condition, timeout) | 和wait_event_timeout类似,但设置进程状态为TASK_INTERRUPTIBLE,可以被信号打断。 |
唤醒以queue作为等待队列头部的队列中所有的进程。
void wake_up(wait_queue_head_t *queue);
void wake_up_interruptible(wait_queue_head_t *queue);
能唤醒的进程类型 | 配对使用 | |
wake_up() | TASK_INTERRUPTIBLE TASK_UNINTERRUPTIBLE |
wait_event() wait_event_timeout() |
wake_up_interruptible() | TASK_INTERRUPTIBLE | wait_event_interruptible() wait_event_interruptible_timeout() |
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()成对使用。
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_write(...);
out:
remove_wait_queue(&xxx_wait, &wait); /* 将元素移出xxx_wait指引的队列 */
set_current_state(TASK_RUNNING); /* 设置进程状态为TASK_RUNNING */
return ret;
}
1)如果是非阻塞访问(O_NONBLOCK被设备),设备忙时,直接返回-EAGAIN。
2)如果是阻塞访问,调用__set_current_state(TASK_INTERRUPTIBLE)进行进程状态切换并通过schedule()调度其它进程执行。
3)醒来的时候,由于调度出去的时候进程状态是TASK_INTERRUPTIBLE(浅度睡眠),所以唤醒的可能是信号,所以先通过signal_pending判断是否为信号唤醒,如果是,立即返回-ERESTARTSYS。
DECLARE_WAITQUEUE和add_wait_queue这两个动作的效果如下图所示:
在wait_queue_head_t指向的链表上,新定义的wait_queue元素被插入,这个新元素绑定了一个task_struct数据结构(当前做xxx_write的current,也是DECLARE_WAITQUEUE使用"current"作为参数的原因)。
在用户程序中, select() 和 poll() 也是与设备阻塞与非阻塞访问息息相关的论题。 使用非阻塞I/O的应用程序通常会使用select() 和poll() 系统调用查询是否可对设备进行无阻塞的访问。 select() 和 poll() 系统调用最终会使设备驱动中的poll() 函数被执行, 在Linux2.5.45内核中还引入了epoll() , 即扩展的poll() 。
select() 和poll() 系统调用的本质一样, 前者在BSD UNIX中引入, 后者在System V中引入。
/* 参数numfds:等待队列项要加入的等待队列头
* 参数readfds:要监视的读事件
* 参数writefds:要监视的写事件
* 参数exceptfds:要监视的异常事件
* 参数timeout:超时时间
*/
int select(int numfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds,
struct timeval *timeout);
readfds文件集中的任何一个文件变得可读, select() 返回;同理, writefds文件集中的任何一个文件变得可写, select也返回。
如图8.3所示, 第一次对n个文件进行select() 的时候, 若任何一个文件满足要求, select() 就直接返回; 第2次再进行select() 的时候, 没有文件满足读写要求, select() 的进程阻塞且睡眠。 由于调用select() 的时候, 每个驱动的poll() 接口都会被调用到, 实际上执行select() 的进程被挂到了每个驱动的等待队列上, 可以被任何一个驱动唤醒。 如果FDn变得可读写, select() 返回。
timeout参数是一个指向struct timeval类型的指针, 它可以使select() 在等待timeout时间后若仍然没有文件描述符准备好则超时返回。 struct timeval数据结构的定义如代码清单8.7所示。
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)
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
当多路复用的文件数量庞大、 I/O流量频繁的时候, 一般不太适合使用select() 和poll() , 此种情况下, select() 和poll() 的性能表现较差, 我们宜使用epoll。 epoll的最大好处是不会随着fd的数目增长而降低效率, select() 则会随着fd的数量增大性能下降明显。
/* 参数size:要监听多少个fd */
int epoll_create(int size);
/* 参数epfd:要监听的事件类型epoll_create返回值。
* 参数op:动作。
EPOLL_CTL_ADD: 注册新的fd到epfd中。
EPOLL_CTL_MOD: 修改已经注册的fd的监听事件。
EPOLL_CTL_DEL: 从epfd中删除一个fd。
* 参数fd:需要监听的fd。
* 参数event:告诉内核需要监听的事件类型
EPOLLIN: 表示对应的文件描述符可以读。
EPOLLOUT: 表示对应的文件描述符可以写。
EPOLLPRI: 表示对应的文件描述符有紧急的数据可读( 这里应该表示的是有socket带外数据到
来) 。
EPOLLERR: 表示对应的文件描述符发生错误。
EPOLLHUP: 表示对应的文件描述符被挂断。
EPOLLET: 将epoll设为边缘触发( Edge Triggered) 模式, 这是相对于水平触发( Level Triggered) 来说的。 LT( Level Triggered) 是缺省的工作方式, 在LT情况下, 内核告诉用户一个fd是否就绪了, 之后用户可以对这个就绪的fd进行I/O操作。 但是如果用户不进行任何操作, 该事件并不会丢失, 而ET( EdgeTriggered) 是高速工作方式, 在这种模式下, 当fd从未就绪变为就绪时, 内核通过epoll告诉用户, 然后它会假设用户知道fd已经就绪, 并且不会再为那个fd发送更多的就绪通知。
EPOLLONESHOT: 意味着一次性监听, 当监听完这次事件之后, 如果还需要继续监听这个fd的话,
需要再次把这个fd加入到epoll队列里。
*/
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
/* 参数epfd:要监听的事件类型epoll_create返回值。
* 输出参数events:用来从内核得到事件的集合。
* 参数maxevents:告诉内核本次最多收多少事件。
* 参数timeout:超时时间( 以毫秒为单位, 0意味着立即返回, -1意味着永久等待) 。 该函数的返回值是需要处理的事件数目, 如返回0, 则表示已超时。
*/
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
设备驱动中poll() 函数的原型是:
unsigned int(*poll)(struct file * filp, struct poll_table* wait);
第1个参数为file结构体指针, 第2个参数为轮询表指针。 这个函数应该进行两项工作。
1) 对可能引起设备文件状态变化的等待队列调用poll_wait() 函数, 将对应的等待队列头部添加到poll_table中。
2) 返回表示是否能对设备进行无阻塞读、 写访问的掩码。
用于向poll_table注册等待队列的关键poll_wait() 函数的原型如下:
void poll_wait(struct file *filp, wait_queue_heat_t *queue, poll_table * wait);
poll_wait() 函数的名称非常容易让人产生误会, 以为它和wait_event() 等一样, 会阻塞地等待某事件的发生, 其实这个函数并不会引起阻塞。
poll_wait() 函数所做的工作是把当前进程添加到wait参数指定的等待列表(poll_table) 中, 实际作用是让唤醒参数queue对应的等待队列可以唤醒因select() 而睡眠的进程。
驱动程序poll() 函数应该返回设备资源的可获取状态, 即POLLIN、 POLLOUT、 POLLPRI、POLLERR、 POLLNVAL等宏的位“或”结果。 每个宏的含义都表明设备的一种状态, 如POLLIN(定义为0x0001) 意味着设备可以无阻塞地读, POLLOUT(定义为0x0004) 意味着设备可以无阻塞地写。
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;
}