阻塞与非阻塞是设备访问的两种方式。在写阻塞与非阻塞的驱动程序时,经常用到等待队列。
阻塞调用是指调用结果返回之前,当前线程会被挂起,函数只有在得到结果之后才会返回。
非阻塞指不能立刻得到结果之前,该函数不会阻塞当前进程,而会立刻返回。
函数是否处于阻塞模式和驱动对应函数中的实现机制是直接相关的,但并不是一一对应的,例如我们在应用层设置为阻塞模式,如果驱动中没有实现阻塞,函数仍然没有阻塞功能。
在linux设备驱动程序中,阻塞进程可以使用等待队列来实现。
在内核中,等待队列是有很多用处的,尤其是在中断处理,进程同步,定时等场合,可以使用等待队列实现阻塞进程的唤醒。它以队列为基础数据结构,与进程调度机制紧密结合,能够用于实现内核中的异步事件通知机制,同步对系统资源的访问。
等待队列的使用
(1)定义和初始化等待队列:
wait_queue_head_t wait;//定义等待队列 init_waitqueue_head(&wait);//初始化等待队列
定义并初始化等待队列:
#define DECLARE_WAIT_QUEUE_HEAD(name) wait_queue_head_t name = __WAIT_QUEUE_HEAD_INITIALIZER(name)
(2)添加或移除等待队列:
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);
(3)等待事件:
wait_event(wq, condition);//在等待队列中睡眠直到condition为真。 wait_event_timeout(wq, condition, timeout); wait_event_interruptible(wq, condition) ; wait_event_interruptible_timeout(wq, condition, timeout) ; /* *queue:作为等待队列头的等待队列被唤醒 * conditon:必须满足,否则阻塞 * timeout和conditon相比,有更高优先级 */
(4)睡眠:
sleep_on(wait_queue_head_t *q); interruptible_sleep_on(wait_queue_head_t *q); /* sleep_on作用是把目前进程的状态置成TASK_UNINTERRUPTIBLE,直到资源可用,q引导的等待队列被唤醒。 interruptible_sleep_on作用是一样的, 只不过它把进程状态置为TASK_INTERRUPTIBLE */
(5)唤醒等待队列:
//可唤醒处于TASK_INTERRUPTIBLE和TASK_UNINTERRUPTIBLE状态的进程; #define wake_up(x) __wake_up(x, TASK_NORMAL, 1, NULL)
//只能唤醒处于TASK_INTERRUPTIBLE状态的进程 #define wake_up_interruptible(x) __wake_up(x, TASK_INTERRUPTIBLE, 1, NULL)
挂起线程的意思是在线程中主动使用调度函数,将自己挂起,何时在执行需要等待系统调度实现,无法预知,可能在执行完调度函数后,内核马上又调度回来,继续运行。
使线程睡眠的意思是在线程中使用能够睡眠的语句,然后让线程进入睡眠状态,让出cpu,直到满足要求的事情发生了再唤醒线程继续执行,比如睡眠定时时间到达,或者睡眠被打断,或者睡眠等待的资源得到时。
线程阻塞有点是不一定会发生的意思,比如在运行函数的时候执行到摸一个位置需要一个资源,如果这个资源可用就继续执行,如果这个资源不可用程序会进行睡眠并等待资源可用时在唤醒。这与线程主动睡眠的区别就是是否阻塞睡眠需要根据一定的条件进行。
阻塞操作是指在执行设备操作时若不能获得资源则挂起进程,直到满足可操作的条件后在进行操作。
非阻塞操作的进程在不能进行设备操作时并不挂起,它或者被放弃,或者不停的查询,直到可以进行操作为止。
在简单字符设备驱动, 我们看到如何实现read和write方法。但是我们仅仅实现了阻塞的方式,也就是我们的应用程序不论如何设置,我们的驱动只支持阻塞方式。
在驱动中如何知道应用程序的设置呢?驱动中使用的是read或者write函数的参数struct file中的f_flags标志判断应用程序是否设置了非阻塞。
判断代码片段如下:
if (file->f_flags & O_NONBLOCK) /* 非 阻塞操作 */ { if (!ev_press) /* ev_press 为 1 表示有按键按下,为 0 if 成立 ,没有按键按下, */ return -EAGAIN; /* 返回 -EAGAIN 让再次来执行 */ } else /* 阻塞操作 */ { /* 如果没有按键动作, 休眠 */ wait_event_interruptible(button_waitq, ev_press); }
函数原型如下
static unsigned int poll(struct file *file, struct socket *sock, poll_table *wait)
第一个参数是file结构体指针,第三个参数是轮询表指针
这个函数应该进行两项工作
(1)对可能引起设备文件状态变化的等待队列调用poll_wait()函数,将对应的等待队列头添加到poll_table
(2)返回表示是否能对设备进行无阻塞读,写访问的掩码
poll_wait()函数原型如下
static inline void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p)
从中可以看出是将等待队列头wait_address添加到p所指向的结构体中(poll_table)
驱动函数中的poll()函数典型模板如下
static unsigned int xxx_poll(struct file *filp,struct socket *sock, poll_table *wait) { unsigned int mask = 0; struct xxx_dev *dev = filp->private_data;//获得设备结构体指针 ... poll_wait(filp,&dev->r_wait,wait);//加读等待队列头到poll_table poll_wait(filp,&dev->w_wait,wait);//加写等待队列头到poll_table ... if(...)//可读 mask |= POLLIN | POLLRDNORM; if(...)//可写 mask |= POLLOUT | POLLRDNORM; ... return mask; }
驱动程序中的poll函数,在应用程序中对应着select、poll、epoll函数。
1)将要监控的文件添加到文件描述符集
2)调用select开始监控
3)判断文件是否发生变化
系统提供了4个宏对描述符集进行操作:
#include<sys/select.h> Void FD_SET(int fd, fd_set *fdset) Void FD_CLR(int fd, fd_set *fdset) Void FD_ZERO(fd_set *fdset) Void FD_ISSET(int fd, fd_set *fdset)
宏FD_SET将文件描述符fd添加到文件描述符fdset中
宏FD_CLR从文件描述符集fdset中清除文件描述符fd
宏FD_ZERO清空文件描述符集fdset
在调用select后使用FD_ISSET来检测文件描述符集fdset中的文件fd发生了变化
使用例子(对两个文件进行读监控):
FD_ZERO(&fds);//清空集合 FD_SET(fd1,&fds);//设置描述符 FD_SET(fd2,&fds);//设置描述符 Maxfdp = fd1+1;//描述符最大值加1,假设fd1>fd2 Switch(select(maxfdp,&fds,NULL,NULL,&timeout))//读监控 Case -1: exit(-1);break;//select错误,退出程序 Case 0:break; Default: If(FD_ISSET(fd1,&fds)) //测试fd1是否可读
select的几大缺点:
(1)每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
(2)同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大
(3)select支持的文件描述符数量太小了,默认是1024
poll的实现和select非常相似,只是描述fd集合的方式不同,poll使用pollfd结构而不是select的fd_set结构,其他的都差不多。
epoll既然是对select和poll的改进,就应该能避免上述的三个缺点。那epoll都是怎么解决的呢?在此之前,我们先看一下epoll和select和poll的调用接口上的不同,select和poll都只提供了一个函数——select或者poll函数。而epoll提供了三个函数,epoll_create,epoll_ctl和epoll_wait,epoll_create是创建一个epoll句柄;epoll_ctl是注册要监听的事件类型;epoll_wait则是等待事件的产生。
对于第一个缺点,epoll的解决方案在epoll_ctl函数中。每次注册新的事件到epoll句柄中时(在epoll_ctl中指定EPOLL_CTL_ADD),会把所有的fd拷贝进内核,而不是在epoll_wait的时候重复拷贝。epoll保证了每个fd在整个过程中只会拷贝一次。
对于第二个缺点,epoll的解决方案不像select或poll一样每次都把current轮流加入fd对应的设备等待队列中,而只在epoll_ctl时把current挂一遍(这一遍必不可少)并为每个fd指定一个回调函数,当设备就绪,唤醒等待队列上的等待者时,就会调用这个回调函数,而这个回调函数会把就绪的fd加入一个就绪链表)。epoll_wait的工作实际上就是在这个就绪链表中查看有没有就绪的fd(利用schedule_timeout()实现睡一会,判断一会的效果,和select实现中的第7步是类似的)。
对于第三个缺点,epoll没有这个限制,它所支持的FD上限是最大可以打开文件的数目,这个数字一般远大于2048,举个例子,在1GB内存的机器上大约是10万左右,具体数目可以cat /proc/sys/fs/file-max察看,一般来说这个数目和系统内存关系很大。
(1)select,poll实现需要自己不断轮询所有fd集合,直到设备就绪,期间可能要睡眠和唤醒多次交替。而epoll其实也需要调用epoll_wait不断轮询就绪链表,期间也可能多次睡眠和唤醒交替,但是它是设备就绪时,调用回调函数,把就绪fd放入就绪链表中,并唤醒在epoll_wait中进入睡眠的进程。虽然都要睡眠和交替,但是select和poll在“醒着”的时候要遍历整个fd集合,而epoll在“醒着”的时候只要判断一下就绪链表是否为空就行了,这节省了大量的CPU时间。这就是回调机制带来的性能提升。
(2)select,poll每次调用都要把fd集合从用户态往内核态拷贝一次,并且要把current往设备等待队列中挂一次,而epoll只要一次拷贝,而且把current往等待队列上挂也只挂一次(在epoll_wait的开始,注意这里的等待队列并不是设备等待队列,只是一个epoll内部定义的等待队列)。这也能节省不少的开销。
参考资料:
http://www.cnblogs.com/apprentice89/archive/2013/05/09/3070051.html
http://www.linuxidc.com/Linux/2012-05/59873p3.htm
http://xingyunbaijunwei.blog.163.com/blog/static/76538067201241685556302/
http://blog.csdn.net/kkxgx/article/details/7717125
https://banu.com/blog/2/how-to-use-epoll-a-complete-example-in-c/epoll-example.c
异步通知是,一旦设备就绪,则主动向应用程序发送信号,应用程序根本就不需要查询设备状态,类似于中断的概念,一个进程收到一个信号与处理器收到一个中断请求可以说是一样的。信号是异步的,一个进程不必通过任何操作来等待信号的到达。
在linux中,异步通知是使用信号来实现的,而在linux,大概有30种信号,比如大家熟悉的ctrl+c的SIGINT信号,进程能够忽略或者捕获除过SIGSTOP和SIGKILL的全部信号,当信号背捕获以后,有相应的函数来处理它。
在驱动程序可以将特定信号发送到特定应用进程中。因此,应该在合适的时候让设备驱动发送信号,驱动中应实现fasync()函数。并在设备资源可获得时,调用kill_fasync()函数激发相应的信号。
驱动中实现异步通知机制很简单。首先说fasync()函数的实现。
static int xxx_fasync(int fd,struct file *filp,int mode) { struct xxx_dev *dev = filp->private_data; return fasync_helper(fd,filp,mode,&dev->async_queue); }
这个就是标准模板。
然后在需要发送信号的地方调用 kill_fasync()函数,释放信号。
释放信号的函数:
void kill_fasync(struct fasync_struct **fp, int sig, int band)
下面我们来看下支持异步通知的模板。
设备结构体:
struct xxx_dev{ struct cdev cdev; ...... struct fasync_struct *async_queue; }; fasync()函数: static int xxx_fasync(int fd,struct file *filp,int mode) { struct xxx_dev *dev = filp->private_data; return fasync_helper(fd,filp,mode,&dev->async_queue); }
在设备资源可以获得时,应该调用kill_fasync()释放SIGIO信号,可读时第三个参数是POLL_IN,可写时为POLL_OUT.
static ssize_t xxx_write(struct file *filp,const char __user *buf,size_t count,loff_t *ppos) { struct xxx_dev *dev = filp->private_data; ...... if(dev->async_queue) kill_fasync(&dev->async_queue,SIGIO,POLL_IN); ...... }
在release函数中,应调用fasync()函数将文件从异步通知的列表中删除。
int xxx_release(struct inode *inode,struct file *filp) { xxx_fasync(-1,filp,0); return 0; }
在应用程序中需要3步将信号与驱动绑定。
1、注册 SIGIO信号
signal(SIGIO, handler);
2、设置进程为文件的属主
fcntl(fd, F_SETOWN, getpid());
3、设置异步属性
int flags;
flags = fcntl(fd, F_GETFL);
flags |= FASYNC;
fcntl(fd, F_SETFL, flags);
然后当驱动发送信号的时候就会自动调用应用程序的信号处理函数。
应用程序模板:
void input_handler(int num) { …… } main() { int oflags; signal(SIGIO,input_handler); fcntl(STDIN_FILENO,F_SETOWN,getpid()); oflags=fcntl(STDIN_FILENO,F_GETFL); fcntl(STDIN_FILENO,F_SETFL,oflags|FASYNC); while(1); }
阻塞与非阻塞操作
(1)定义并初始化等待对列头;
(2)定义并初始化等待队列;
(3)把等待队列添加到等待队列头
(4)设置进程状态(TASK_INTERRUPTIBLE(可以被信号打断)和TASK_UNINTERRUPTIBLE(不能被信号打断))
(5)调用其它进程
poll机制
(1)把等待队列头加到poll_table
(2)返回表示是否能对设备进行无阻塞读,写访问的掩码
异步通知机制
(1)当发出 F_SETOWN,什么都没发生,除了一个值被赋值给filp->f_owner.
(2)当 F_SETFL被执行来打开FASYNC,驱动的fasync方法被调用.这个方法被调用无论何时FASYNC的值在filp->f_flags中被改变来通知驱动这个变化,因此它可正确地响应.这个标志在文件被打开时缺省地被清除.我们将看这个驱动方法的标准实现,在本节.
(3)当数据到达,所有的注册异步通知的进程必须被发出一个SIGIO信号.