一.阻塞
阻塞操作是指在执行设备操作时,若不能获得资源,则挂起进程直到满足可操作的条件后再进行操作。被挂起的进程进入休眠状态,被从调度器的运行队列移走,直到等待的条件被满足。通过读写函数中内嵌阻塞代码(信号量,等待队列)来实现不满足条件时的睡眠,等到满足条件了应用程序从睡眠中唤醒,继续下面的操作。
阻塞从字面上听起来似乎意味着低效率,实则不然,如果设备驱动不阻塞,则用户想获取设备资源只能不停地查询,这反而会无谓地耗费CPU资源。而阻塞访问时,不能获取资源的进程将进入休眠,它将CPU资源“礼让”给其他进程。因为阻塞的进程会进入休眠状态,因此,必须确保有一个地方能够唤醒休眠的进程,否则,进程就真的“寿终正寝”了。唤醒进程的地方最大可能发生在中断里面,因为硬件资源获得的同时往往伴随着一个中断。在设备驱动中阻塞I/O一般给予等待队列来实现:
1.定义“等待队列头”
wait_queue_head_t my_queue;
2.初始化“等待队列头”
init_waitqueue_head(&my_queue);
而下面的DECLARE_WAIT_QUEUE_HEAD()宏可以作为定义并初始化等待队列头“快捷方 式”。
DECLARE_WAIT_QUEUE_HEAD (name)
3.定义等待队列
DECLARE_WAITQUEUE (name, tsk)
该宏用于定义并初始化一个名为name的等待队列。
4.等待事件
wait_event(queue,condition)
wait_event_interruptible(queue, condition)
wait_event_timeout(queue, condition, timeout)
wait_event_interruptible_timeout(queue, condition, timeout)
等待第 1 个参数 queue 作为等待队列头的等待队列被唤醒,而且第 2 个参数condition必须满足否则继续阻塞。
wait_event()和wait_event_interruptible()的区别在于后者可以被信号打断,而前者不能。加上_timeout 后的宏意味着阻塞等待的超时时间,以jiffy 为单位,在第 3 个参数的 timeout到达时,不论 condition是否满足,均返回。当 condition 满足时,wait_event()会立即返回,否则,阻塞等待condition 满足。
5. 唤醒队列
void wake_up(wait_queue_head_t *queue);
void wake_up_interruptible(wait_queue_head_t *queue);
上述操作会唤醒以 queue 作为等待队列头的所有等待队列中所有属于该等待队列头的等待队列对应的进程。
(1) wake_up()应该与wait_event()或wait_event_timeout()成对使用,wake_up_interruptible()则应与wait_event_interruptible()或wait_event_interruptible_timeout()成对使用。
(2) wake_up()可唤醒处于TASK_INTERRUPTIBLE 和TASK_UNINTERRUPTIBLE 的进程,而wake_up_interruptible()只能唤醒处于 TASK_INTERRUPTIBLE 的进程。
二.非阻塞
非阻塞就是应用程序即使得不到硬件数据也不会睡眠,而是直接返回。当然真实的操作不会就这样返回推出了,而是先通过轮询的方式,也就是上一篇文章的select,(虽然这里说是非阻塞,但是select本身就有点阻塞的味道,如果加入的文件描述符都不满足,select会休眠,一旦下层驱动有变化则会通知select再次调用poll函数,这其实是一种改良的轮询,非常好的一种机制),通过select来实现轮询的方式,只要select能通过则表明可以进行读或者写了。
int select (intnumfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds,
struct timeval *timeout);
其中 readfds、writefds、exceptfds 分别是被 select()监视的读、写和异常处理的文件描述符集合,numfds 的值是需要检查的号码最高的文件描述符加 1。timeout 参数是一个指向 struct timeval类型的指针,它可以使select()在等待 timeout 时间后若没有文件描述符准备好则返回。
在超时或者其中一个或多个文件描述符发生变化时,此函数都将返回。在实际应用中,可以针对select的返回值进行选择性的执行处理:
1) 如果函数执行出错,将返回-1;
2) 如果因为超时而返回,即在timeout所描述的时间范围内没有任何文件描述符有需要的操 作,则返回0,并且将该时间结构体清0,因此如果在循环体中使用select时一定要个给这个结构体重新赋值;
3) 如果因一个或者多个文件描述符需要处理而返回,其返回值为产生异常的文件描述符数,并在相应的文件描述符集合中清除不需要处理的文件描述符,因此,返回后可以根据文件描述符集合的记录值判断哪些文件描述符需要处理。
文件描述符集合的操作:
fd_set rfds; //定义可读、可写、可执行的文件描述符集合
FD_ZERO(&rfds) //清除一个文件描述符集
FD_SET(int fd, &rfds) //将一个文件描述符加入文件描述符集中
FD_CLR(int fd, &rfds) //将一个文件描述符从文件描述符集中清除
FD_ISSET(int fd,&rfds) //判断文件描述符是否被置位
select()超时结构体 pselect()超时结构体
struct timeval struct timespec
{ {
inttv_sec; /* 秒 */ int ts_sec /* 秒 */
inttv_usec; /* 微秒 */ int ts_nsec /* 纳秒*/
}; }
pselect()函数和select()函数完成几乎相同的功能,只是时间精度更高。
三.异步IO
一旦实现异步IO,底层的一旦有数据变化,就会像产生一个中断一样通知上层应用.异步通知的意思是:一旦设备就绪,则主动通知应用程序,这样应用程序就根本不需要查询设备状态。
驱动程序运行在内核空间中,应用程序运行在用户空间中,两者是不能直接通信的。但在实际应用中,在设备已经准备好的时候,我们希望通知用户程序设备已经就绪,用户程序可以读取了,这样应用程序就不需要一直查询该设备的状态,从而节约了资源,这就是异步通知。
驱动程序异步通知应用程序需要完成两方面的工作。
1、驱动程序:
(1) 在设备抽象的数据结构中增加一个struct fasync_struct*async_queue;的指针
struct fasync_struct
{
int magic;
int fa_fd;
struct fasync_struct * fa_next; /*singly linked list 链表*/
struct file *fa_file;
};
(2) 实现设备操作中的fasync函数,这个函数很简单,主体就是调用内核的fasync_helper函数。
int xxx_fasync (int fd, struct file *filp,int mode)
{
struct_test_t * dev = filp->private_data;
// fasync_helper 函数自动初始化结构体fasync_struct
return fasync_helper(fd, filp, mode, &dev->async_queue);
}
(3) 当设备可写时,调用函数 kill_fasync 发送信号 SIGIO 给内核。
kill_fasync(&dev->async_queue,SIGIO, POLL_IN);
讲解一下这个函数:
void kill_fasync(struct fasync_struct **fp,int sig, int band)
sig 就是我们要发送的信号。
band(带宽),一般都是使用 POLL_IN,表示设备可读,如果设备可写,使用 POLL_OUT
(4). 在驱动的release方法中调用前面定义的fasync函数,需要将 fasync_struct 从异步队列中删除,删除函数中也是调用 xxx_fasync,不过改了一下参数而已。
xxx_fasync(-1, filp, 0);
其中fasync_helper和kill_fasync都是内核函数,我们只需要调用就可以了。在(1)中定义的指针是一个重要参数,fasync_helper和kill_fasync会使用这个参数。
2、应用程序
(1) 利用signal或者sigaction设置SIGIO信号的处理函数
(2) fcntl的F_SETOWN命令设置当前进程为设备文件owner,这样设备驱动发出的信号才能被本进程接收;
(3) fcntl的F_SETF命令设置设备文件支持FASYNC,即异步通知模式。
完成了以上的工作的话,当内核执行到kill_fasync函数,用户空间SIGIO函数的处理函数就会被调用了。
总结:阻塞采用等待队列实现,非阻塞采用select轮询,区别是轮询程序阻塞于 select, 而不是真正的系统调用接口API(open),和阻塞IO模式相比,把阻塞操作提前,即在调用select或poll时阻塞,后面的真正数据操作并不阻塞。select的高明之处在于它能同时等待多个文件描述符,当文件描述符集中有一个进入就绪状态时,select函数就可以返回。异步通知则意味着设备通知自身可访问,实现了异步I/O。