linux设备驱动程序中的阻塞、IO多路复用与异步通知机制

一、阻塞与非阻塞

阻塞与非阻塞是设备访问的两种方式。在写阻塞与非阻塞的驱动程序时,经常用到等待队列。

阻塞调用是指调用结果返回之前,当前线程会被挂起,函数只有在得到结果之后才会返回。

非阻塞指不能立刻得到结果之前,该函数不会阻塞当前进程,而会立刻返回。

函数是否处于阻塞模式和驱动对应函数中的实现机制是直接相关的,但并不是一一对应的,例如我们在应用层设置为阻塞模式,如果驱动中没有实现阻塞,函数仍然没有阻塞功能。

二、等待队列

在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);
} 

五、IO多路复用poll

函数原型如下

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;
}

六、应用程序实现IO复用

驱动程序中的poll函数,在应用程序中对应着select、poll、epoll函数。

Select使用方法

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使用方法

poll的实现和select非常相似,只是描述fd集合的方式不同,poll使用pollfd结构而不是select的fd_set结构,其他的都差不多。

epoll使用方法

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信号.

时间: 2024-08-03 17:10:22

linux设备驱动程序中的阻塞、IO多路复用与异步通知机制的相关文章

Linux设备驱动中的阻塞与非阻塞IO与并发控制

Linux设备驱动中的阻塞与非阻塞IO: 1.Linux设备驱动中的阻塞与非阻塞总结:http://m.blog.csdn.net/blog/dongteen/17264501 2.Linux设备驱动中的阻塞与非阻塞IO:http://m.blog.csdn.net/blog/dongteen/17264501 3.Linux设备驱动中的阻塞与非阻塞I/O实例:http://blog.csdn.net/wenhui_/article/details/6817659 linux内核中等待队列: 1

深入浅出~Linux设备驱动中的阻塞和非阻塞I/O

今天意外收到一个消息,真是惊呆我了,博客轩给我发了信息,说是俺的博客文章有特色可以出本书,,这简直让我受宠若惊,俺只是个大三的技术宅,写的博客也是自己所学的一些见解和在网上看到我一些博文以及帖子里综合起来写的,,总之这又给了额外的动力,让自己继续前进,,希望和大家能够分享一些自己的经验,,在最需要奋斗的年级以及在技术的领域踽踽独行的过程中有共同的伙伴继续前进~ 今天写的是Linux设备驱动中的阻塞和非阻塞I/0,何谓阻塞与非阻塞I/O?简单来说就是对I/O操作的两种不同的方式,驱动程序可以灵活的

Linux设备驱动中的阻塞和非阻塞I/O

[基本概念] 1.阻塞 阻塞操作是指在执行设备操作时,托不能获得资源,则挂起进程直到满足操作所需的条件后再进行操作.被挂起的进程进入休眠状态(不占用cpu资源),从调度器的运行队列转移到等待队列,直到条件满足. 2.非阻塞 非阻塞操作是指在进行设备操作是,若操作条件不满足并不会挂起,而是直接返回或重新查询(一直占用CPU资源)直到操作条件满足为止. 当用户空间的应用程序调用read(),write()等方法时,若设备的资源不能被获取,而用户又希望以阻塞的方式来访问设备,驱动程序应当在设备驱动层的

Linux设备驱动程序学习 高级字符驱动程序操作[阻塞型I/O和非阻塞I/O]【转】

转自:http://blog.csdn.net/jacobywu/article/details/7475432 阻塞型I/O和非阻塞I/O 阻塞:休眠 非阻塞:异步通知 一 休眠 安全地进入休眠的两条规则: (1) 永远不要在原子上下文中进入休眠,即当驱动在持有一个自旋锁.seqlock或者 RCU锁时不能睡眠:关闭中断也不能睡眠.持有一个信号量时休眠是合法的,但你应当仔细查看代码:如果代码在持有一个信号量时睡眠,任何其他的等待这个信号量的线程也会休眠.因此发生在持有信号量时的休眠必须短暂,而

《Linux4.0设备驱动开发详解》笔记--第九章:Linux设备驱动中的异步通知与同步I/O

在设备驱动中使用异步通知可以使得对设备的访问可进行时,由驱动主动通知应用程序进行访问.因此,使用无阻塞I/O的应用程序无需轮询设备是否可访问,而阻塞访问也可以被类似"中断"的异步通知所取代.异步通知类似于硬件上的"中断"概念,比较准确的称谓是"信号驱动的异步I/O". 9.1 异步通知的概念和作用 异步通知:一旦设备就绪,则主动通知应用程序,该应用程序无需查询设备状态 几种通知方式比较: 阻塞I/O :一直等待设备可访问后开始访问 非阻塞I/O:

20150518 Linux设备驱动中的并发控制

20150518 Linux设备驱动中的并发控制 2015-05-18 Lover雪儿 总结一下并发控制的相关知识: 本文参考:华清远见<Linux 设备驱动开发详解>—第7章 Linux 设备驱动中的并发控制,更多详细内容请看原书 一.并发与竞态 并发(concurrency)指的是多个执行单元同时.并行被执行,而并发的执行单元对共享资源(硬件资源和软件上的全局变量.静态变量等)的访问则很容易导致竞态(race conditions). 在 Linux 内核中,主要的竞态发生于如下几种情况:

python(十)下:事件驱动与 阻塞IO、非阻塞IO、IO多路复用、异步IO

上节的问题: 协程:遇到IO操作就切换. 但什么时候切回去呢?怎么确定IO操作完了? 一.事件驱动模型介绍 通常,我们写服务器处理模型的程序时,有以下几种模型: (1)每收到一个请求,创建一个新的进程,来处理该请求: (2)每收到一个请求,创建一个新的线程,来处理该请求: (3)每收到一个请求,放入一个事件列表,让主进程通过非阻塞I/O方式来处理请求 第三种就是协程.时间驱动的方式,一般普遍认为第(3)种方式是大多数网络服务器采用的方式 论事件驱动模型 在UI编程中,,常常要对鼠标点击进行相应,

linux设备驱动程序该添加哪些头文件以及驱动常用头文件介绍(转)

原文链接:http://blog.chinaunix.net/uid-22609852-id-3506475.html 驱动常用头文件介绍 #include <linux/***.h> 是在linux-2.6.29/include/linux下面寻找源文件.#include <asm/***.h> 是在linux-2.6.29/arch/arm/include/asm下面寻找源文件.#include <mach/***.h> 是在linux-2.6.29/arch/ar

linux设备驱动中的并发控制

并发指的是多个执行单元同时.并行被执行,而并发的执行单元对共享资源的访问则很容易导致竞态 linux内核中主要竞态1.多对称处理器的多个CPU  2.单CPU内进程与抢占它的进程 3.中断(硬中断.软中断.Tasklet.下半部)与进程之间访问共享内存资源的代码区称为“临界区”,临界区需要被以某种互斥机制加以保护,中断屏蔽.原子操作.自旋锁和信号量等是linux设备驱动中可采用的互斥途径. 这几个互斥的介绍: 1.中断屏蔽,这个主要用于单CPU,中断屏蔽将使得中断和进程之间的并发不再发生.使用方