阻塞和非阻塞I/O是用户空间对设备访问的两种方式,驱动程序应支持这两种方式。
阻塞操作指在执行设备操作时,若不能获得资源,则挂起该进程直至资源可获得后,再唤醒该进程去操作资源。
非阻塞操作指在执行设备操作时,若不能获得资源,则该进程要么放弃进行设备操作,要么不停查询直至资源可获得,该进程不会被挂起。
驱动程序需要完成的工作:
- 当用户程序以阻塞方式进行
read()
、write()
等系统调用时,若资源不可获得,驱动程序应在设备驱动的xxx_read()
、xxx_write()
等操作中将进程阻塞,直到资源可以获取时再唤醒该进程。 - 当用户程序以非阻塞方式进行
read()
、write()
等系统调用时,若资源不可获得,驱动程序的xxx_read()
、xxx_write()
等操作应立即返回-EAGAIN
。
等待队列
在Linux驱动程序中,可使用等待队列实现阻塞进程的唤醒。
等待队列以队列为基础数据结构,与进程调度机制紧密结合,可用来同步对系统资源的访问,如信号量就是依赖等待队列实现的。
等待队列的相关操作:
/* 定义等待队列头部 */
wait_queue_head_t my_queue;
/* 初始化等待队列头部 */
init_waitqueue_head(&my_queue);
/* 定义并初始化名为name的等待队列头部 */
DECLARE_WAIT_QUEUE_HEAD(name);
/* 定义并初始化名为name的等待队列元素 */
DECLARE_WAITQUEUE(name, tsk);/* tsk为当前任务的指针,如current */
/* 添加或移除等待队列元素 */
void add_wait_queue(wait_queue_head_t *q, wait_queue_t *wait);/* 添加等待队列元素wait至等待队列头部q所指向的双向链表中 */
void remove_wait_queue(wait_queue_head_t *q, wait_queue_t *wait);/* 从等待队列头部q所指向的双向链表中移除等待队列元素wait */
/* 等待事件 */
/* 等待以queue作为等待队列头部的队列被唤醒,queue为等待队列头部,condition条件为true时该函数会直接返回 */
/* 应与wake_up函数成对使用 */
wait_event(queue, condition); /* 无法被信号打断 */
wait_event_timeout(queue, condition, timeout); /* 允许超时返回,超时时间以jiffy为单位 */
/* 应与wake_up_interruptible函数成对使用 */
wait_event_interruptible(queue, condition); /* 可被信号打断 */
wait_event_interruptible_timeout(queue, condition, timeout);
/* 唤醒队列 */
/* 可唤醒以queue作为等待队列头部的队列中所有的进程 */
void wake_up(wait_queue_head_t *queue);
void wake_up_interruptible(wait_queue_head_t *queue);
/* 在等待队列上睡眠 */
/* 将当前进程的状态设置为TASK_UNINTERRUPTIBLE,并定义一个等待队列元素,再将该元素挂到等待队列头部q指向的双向链表,然后将当前进程调度出去,直至被信号或wake_up函数唤醒 */
sleep_on(wait_queue_head_t *q);
/* 将当前进程的状态设置为TASK_INTERRUPTIBLE,并定义一个等待队列元素,再将该元素挂到等待队列头部q指向的双向链表,然后将当前进程调度出去,直至被wake_up_interruptible函数唤醒 */
interruptible_sleep_on(wait_queue_head_t *q);
等待队列头部wait_queue_head_t
、等待队列元素wait_queue_t
和任务task_struct
之间的关系如下:
宏定义wait_event(queue, condition)
展开如下:
prepare_to_wait
函数将等待队列元素__wait
加入到等待队列头部wq
指向的双向链表中,并将当前任务current
状态置为TASK_UNINTERRUPTIBLE
。当条件condition
仍不满足时就将当前任务调度出去。
#define wait_event(wq, condition) \
do { \
if (condition) \
break; \
do { \
wait_queue_t __wait = { \
.private = current, \
.func = autoremove_wake_function, \
.task_list = {&__wait.task_list, &__wait.task_list}, \
}
\
for (;;) { \
prepare_to_wait(&wq, &__wait, TASK_UNINTERRUPTIBLE); \
if (condition) \
break; \
schedule(); \
} \
finish_wait(&wq, &__wait); \
} while (0);
} while (0);
由此可以看出,等待队列最基本的用法是:
- 定义等待队列头部
wait_queue_head_t
- 定义等待队列元素
wait_queue_t
,并将当前任务挂到该元素上,设置该元素的回调函数func
- 将等待队列元素添加到等待队列头部,可使用
add_wait_queue
函数 - 设置当前任务的状态
set_current_state(state)
- 调用
schedule()
函数调度其他任务执行 - 等待被唤醒
- 将等待队列元素从等待队列头部指向的双向链表中移除
- 设置当前任务状态为
TASK_RUNNING
轮询操作
使用非阻塞I/O的应用程序通常会使用select()
和poll()
系统调用查询是否可对设备进行无阻塞的访问。这两个系统调用最终会使设备驱动中的poll()
函数被执行。
select()
:
该系统调用是应用程序中使用最广泛的,原型为:
int select(int numfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
//numfds:需要检查的号码最高的fd加1
//readfds:被监视的读文件描述符
//writefds:被监视的写文件描述符
//exceptfds:被监视的异常处理文件描述符
//timeout:超时时间
struct timeval{
int tv_sec;/* 秒 */
int tv_usec;/* 毫秒 */
};
任何一个文件满足要求时,select()
直接返回。否则调用select()
的进程阻塞且睡眠。
select()
底层调用驱动的poll
接口,阻塞的进程会被挂到驱动的等待队列上。
timeout
参数是一个指向struct timeval
类型的指针,可以使select()
在等待timeout
时间后若仍然没有文件描述符准备好就超时返回。
对文件描述符集合的各种操作:
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()
:
该函数功能和实现原理与select()
相似。
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
select()
和poll()
适用于文件数量较少的场景。当多路复用的文件数量庞大、I/O流量频繁的时候,不适合使用select()
和poll()
,它们的效率会下降。此时应该使用epoll()
,它不会随着文件数量增加而降低效率。
epoll()
:
与epoll()
相关的用户空间编程接口:
/*
创建一个epoll句柄,size用于告诉内核需要监听多少个fd。
创建好epoll句柄后,句柄本身也会占用一个fd,因此需要使用close关闭该fd。
*/
int epoll_create(int size);
/*
告诉内核要监听什么类型的事件。
第1个参数是epoll句柄,即epoll_create函数的返回值。
第2个参数表示动作:
EPOLL_CTL_ADD:注册新的fd到epfd中
EPOLL_CTL_MOD:修改已经注册的fd的监听事件
EPOLL_CTL_DEL:从epfd中删除一个fd
第3个参数是需要监听的fd。
第4个参数是告诉内核需要监听的事件类型。
*/
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
struct epoll_event{
__uint32_t events;/* Epoll events */
epoll_data_t data;/* User data variable */
};
/*
等待事件的产生。
events参数是输出参数,用来从内核得到事件的集合。
maxevents告诉内核本次最多收多少个事件,该值不能大于epoll_create时的size。
timeout是超时时间,单位是毫秒,0表示立即返回,-1表示永久等待。
函数返回值表示需要处理的事件数目,返回0表示超时。
*/
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
事件类型events
可以是以下几个宏的或:
- EPOLLIN:表示对应的文件描述符可以读
- EPOLLOUT:表示对应的文件描述符可以写
- EPOLLPRI:表示对应的文件描述符有紧急的数据可读(socket带外数据到来)
- EPOLLERR:表示对应的文件描述符发生错误
- EPOLLHUP:表示对应的文件描述符被挂断
- EPOLLET:并将epoll设为边缘触发模式(ET),该模式属于高速工作方式,当fd从未就绪变为就绪时,内核通过epoll告诉用户,然后它会假设用户知道fd已经就绪,并且不会再为那个fd发送更多的就绪通知;默认模式是水平触发模式(LT),该模式下内核告诉用户一个fd是否就绪,如果用户不进行任何操作,该事件也不会丢失。
- EPOLLONESHOT:一次性监听,即监听完这次事件之后,若还需要继续监听这个fd,需要再次把这个fd加入到epoll队列里。
设备驱动中的轮询编程
设备驱动中poll函数原型:
/*
第1个参数为file结构体指针
第2个参数为轮询表指针
该函数需要完成两项工作:
1、对可能引起设备文件状态变化的等待队列调用poll_wait函数,将对应的等待队列头部添加到poll_table中
2、返回表示是否能对设备进行无阻塞读、写访问的掩码
*/
unsigned int (*poll)(struct file *filp, struct poll_table *wait);
用于向poll_table
注册等待队列的关键poll_wait
函数原型如下:
void poll_wait(struct file *filp, wait_queue_head_t *queue, struct poll_table *wait);
该函数不会引起阻塞,只是把当前进程添加到wait参数指定的等待列表,实际作用是让唤醒参数queue对应的等待队列可以唤醒因select()而睡眠的进程。
驱动程序poll函数应该返回设备资源的可获取状态,即:
- POLLIN:设备可无阻塞地读
- POLLOUT:设备可无阻塞地写
- POLLPRI
- POLLERR
- POLLNVAL
在设备驱动中,阻塞I/O一般基于等待队列或者基于等待队列的其他内核API来实现。等待队列可用于同步驱动中事件发生的先后顺序。