Linux设备驱动中的阻塞与非阻塞总结
阻塞与非阻塞访问是I/O操作的两种不同模式,前者在I/O操作暂时不可进行时会让进程睡眠。
在设备驱动中阻塞I/O一般基于等待队列来实现,等待队列可用于同步驱动中事件发生的先后顺序。
使用非阻塞I/O的应用程序也可借助轮询函数来查询设备是否能立即被访问。
阻塞操作是指在设备操作时若不能获得资源则挂起进程,直到满足可操作的条件后再进行操作。被挂起的进程进入休眠状态,被从调度器的运行队列移走,直到等待的条件满足。
非阻塞操作的进程在不能进行设备操作时并不挂起,它或者放弃,或者不停地查询,直到可进行操作为止。
等待队列
等待队列的操作:
// 定义“等待队列头” wait_queue_head_t my_queue; // 初始化“等待队列头” init_wait_queue_head(&my_queue); // 定义并初始化等待队列头的快捷方式(宏) DECLARE_WAIT_QUEUE_HEAD(name) // 定义等待队列 DECLARE_WAITQUEUE(name, tsk) // 该宏用于定义并初始化一个名为name的等待队列 //添加/移除等待队列 void fastcall add_wait_queue(wait_queue_head_t *q, wait_queue_t *wait); // 将等待队列wait添加到等待队列头q指向的等待队列链表中 void fastcall remove_wait_queue(wait_queue_head_t *q, wait_queue_t *wait); // 将等待队列wait从附属的等待队列头q指向的等待队列链表中移除 // 等待事件 wait_event(queue, condition) // queue作为等待队列头的等待队列被唤醒,而且condition必须满足,否则阻塞。不能被信号打断 wait_event_interruptible(queue, condition) // 可以被信号打断 wait_event_timeout(queue, condition) // 加上_timeout后意味着阻塞等待的超时时间,以jiffy为单位。在timeout到达时不论condition是否满足,均返回 wait_event_interruptible_timeout(queue, condition) // 唤醒队列 // 唤醒以queue作为等待队列头的所有等待队列中所有属于该等待队列头的等待队列对应的进程 void wake_up(wait_queue_head_t *queue); void wake_up_interruptible(wait_queue_head_t *queue); // wake_up()应与wait_event()或wait_event_timeout()成对使用 // 而wake_up_intterruptible()则应与wait_event_interruptible()或wait_envent_interruptible_timeout()成对使用 // wake_up()可唤醒处于TASK_INTERRUPTIBLE和TASK_UNINTERRUPTIBLE的进程,而wake_up_interruptible()只能唤醒处于TASK_INTERRUPTIBLE的进程 // 在等待队列上睡眠 sleep_on(wait_queue_head_t *q); // 将目前进程的状态设置成TASK_UNINTERRUPTIBLE,并定义一个等待队列,之后把它附属到等待队列头q, // 直到资源可获得,q引导的等待队列被唤醒。 interruptible_seep_on(wait_queue_head_t *q); // 将目前进程的状态设置成TASK_INTERRUPTIBLE,并定义一个等待队列,之后把它附属到等待队列头q, // 直到资源可获得,q引导的等待队列被唤醒或者进程收到信号。
轮询操作
使用非阻塞I/O的应用程序通常使用select()和poll()系统调用查询是否可对设备进行无阻塞的访问。select()和poll()系统调最终会引发设备中的poll()函数执行(xxx_poll()).
应用程序中的轮询编程
select()系统调用:
int select(int numfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout); 其中readfds、writefds、exceptfds分别是被select()监视的读、写和异常处理的文件描述符集合,numfds的值是需要检查的号码最高的文件描述符加1. timeout参数是一个指向struct timeval类型的指针,它可以使select()在等待timeout时间后若没有文件描述符准备好则返回。
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 *flip, struct poll_table *wait);
第一个参数为file结构指针,第二个参数为轮训表指针。这个函数进行一下两项工作:
● 对可能引起设备文件状态变化的等待队列调用poll_wait()函数,将对应的等待队列头添加到poll_table.
● 返回表示是否能对设备进行无阻塞读、写访问的掩码。
关键的用于向poll_table注册等待队列的poll_wait()函数的原型如下:
void poll_wait(struct file *flip, wait_queue_head_t *queue, poll_table *wait);
这个函数不会引起阻塞。poll_wait()函数所做的工作是把当前进程添加到wait参数指定的等待列表(poll_table)中。
poll()函数应该返回设备资源的可获取状态,即POLLIN、POLLOUT、POLLPRI、POLLERR、POLLNVAL等宏的位“或”结果。
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; }
用户空间调用select()和poll()接口,设备驱动提供poll()函数。设备驱动的poll()本身不会阻塞,但是poll()和select()系统调用则会阻塞地等待文件描述符集合中的至少一个可访问或超时。