IO多路复用之select、poll、epoll详解
目前支持I/O多路复用的系统调用有
select,pselect,poll,epoll
,I/O多路复用就是
经过一种机制,一个进程能够监视多个描述符,一旦某个描述符就绪(通常是读就绪或者写就绪),可以通知程序进行相应的读写操做
。
但select,pselect,poll,epoll本质上都是同步I/O
,由于他们都须要在读写事件就绪后本身负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需本身负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。
与多进程和多线程技术相比,
I/O多路复用技术的最大优点是系统开销小,系统没必要建立进程/线程
,也没必要维护这些进程/线程,从而大大减少了系统的开销。
1、使用场景
IO多路复用是指内核一旦发现进程指定的一个或者多个IO条件准备读取,它就通知该进程。IO多路复用适用以下场合:
1)当客户处理多个描述符时(通常是交互式输入和网络套接口),必须使用I/O复用。
2)当一个客户同时处理多个套接口时,这种状况是可能的,但不多出现。
3)若是一个TCP服务器既要处理监听套接口,又要处理已链接套接口,通常也要用到I/O复用。
4)若是一个服务器即要处理TCP,又要处理UDP,通常要使用I/O复用。
5)若是一个服务器要处理多个服务或多个协议,通常要使用I/O复用。
2、select、poll、epoll简介
epoll跟select都能提供多路I/O复用的解决方案。在如今的Linux内核里有都可以支持,
其中epoll是Linux所特有,而select则应该是POSIX所规定
,通常操做系统均有实现。
一、select
基本原理:select 函数监视的文件描述符分3类,分别是writefds、readfds、和exceptfds。调用后select函数会阻塞,直到有描述符就绪(有数据 可读、可写、或者有except),或者超时(timeout指定等待时间,若是当即返回设为null便可),函数返回。当select函数返回后,能够经过遍历fdset,来找到就绪的描述符。
基本流程,如图所示:
select目前几乎在全部的平台上支持,
其良好跨平台支持也是它的一个优势
。select的一个缺点在于单个进程可以监视的文件描述符的数量存在最大限制
,在Linux上通常为1024,能够经过修改宏定义甚至从新编译内核的方式提高这一限制
,可是这样也会形成效率的下降。
select本质上是经过设置或者检查存放fd标志位的数据结构来进行下一步处理
。这样所带来的缺点是:
一、select最大的缺陷就是单个进程所打开的FD是有必定限制的,它由FD_SETSIZE设置,默认值是1024。
通常来讲这个数目和系统内存关系很大,具体数目能够cat /proc/sys/fs/file-max察看
。32位机默认是1024个。64位机默认是2048.
二、对socket进行扫描时是线性扫描,即采用轮询的方法,效率较低。
当套接字比较多的时候,每次select()都要经过遍历FD_SETSIZE个Socket来完成调度,无论哪一个Socket是活跃的,都遍历一遍。这会浪费不少CPU时间。若是能给套接字注册某个回调函数,当他们活跃时,自动完成相关操做,那就避免了轮询
,这正是epoll与kqueue作的。
三、须要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大。
二、poll
基本原理:
poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间
,而后查询每一个fd对应的设备状态,若是设备就绪则在设备等待队列中加入一项并继续遍历,若是遍历完全部fd后没有发现就绪设备,则挂起当前进程,直到设备就绪或者主动超时,被唤醒后它又要再次遍历fd。这个过程经历了屡次无谓的遍历。
它没有最大链接数的限制,缘由是它是基于链表来存储的
,可是一样有一个缺点:
1)大量的fd的数组被总体复制于用户态和内核地址空间之间
,而无论这样的复制是否是有意义。
2)poll还有一个特色是“水平触发”
,若是报告了fd后,没有被处理,那么下次poll时会再次报告该fd。
注意:从上面看,select和poll都须要在返回后,
经过遍历文件描述符来获取已经就绪的socket
。事实上,
同时链接的大量客户端在一时刻可能只有不多的处于就绪状态
,所以随着监视的描述符数量的增加,其效率也会线性降低。
三、epoll
epoll是在2.6内核中提出的,是以前的select和poll的加强版本。相对于select和poll来讲,epoll更加灵活,没有描述符限制。
epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次
。
基本原理:
epoll支持水平触发和边缘触发,最大的特色在于边缘触发,它只告诉进程哪些fd刚刚变为就绪态,而且只会通知一次
。还有一个特色是,
epoll使用“事件”的就绪通知方式
,经过epoll_ctl注册fd,
一旦该fd就绪,内核就会采用相似callback的回调机制来激活该fd
,epoll_wait即可以收到通知。
epoll的优势:
一、没有最大并发链接的限制
,能打开的FD的上限远大于1024(1G的内存上能监听约10万个端口)。
二、效率提高,不是轮询的方式,不会随着FD数目的增长效率降低
。
只有活跃可用的FD才会调用callback函数;
即Epoll最大的优势就在于它只管你“活跃”的链接,而跟链接总数无关
,所以在实际的网络环境中,Epoll的效率就会远远高于select和poll。
三、内存拷贝
,利用mmap()文件映射内存加速与内核空间的消息传递;即epoll使用mmap减小复制开销
。
epoll对文件描述符的操做有两种模式:
LT(level trigger)和ET(edge trigger)
。LT模式是默认模式,LT模式与ET模式的区别以下:
LT模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,
应用程序能够不当即处理该事件
。下次调用epoll_wait时,会再次响应应用程序并通知此事件。
ET模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,
应用程序必须当即处理该事件
。若是不处理,下次调用epoll_wait时,不会再次响应应用程序并通知此事件。
一、LT模式
LT(level triggered)是缺省的工做方式,而且同时支持block和no-block socket
。在这种作法中,内核告诉你一个文件描述符是否就绪了,而后你能够对这个就绪的fd进行IO操做。
若是你不做任何操做,内核仍是会继续通知你的
。
二、ET模式
ET(edge-triggered)是高速工做方式,只支持no-block socket
。在这种模式下,当描述符从未就绪变为就绪时,内核经过epoll告诉你。而后它会假设你知道文件描述符已经就绪,而且不会再为那个文件描述符发送更多的就绪通知,直到你作了某些操做致使那个文件描述符再也不为就绪状态了(好比,你在发送,接收或者接收请求,或者发送接收的数据少于必定量时致使了一个EWOULDBLOCK 错误)。
可是请注意,若是一直不对这个fd做IO操做(从而致使它再次变成未就绪),内核不会发送更多的通知(only once)
。
ET模式在很大程度上减小了epoll事件被重复触发的次数,所以效率要比LT模式高
。epoll工做在ET模式的时候,
必须使用非阻塞套接口
,以免因为一个文件句柄的阻塞读/阻塞写操做把处理多个文件描述符的任务饿死。
三、在select/poll中,
进程只有在调用必定的方法后,内核才对全部监视的文件描述符进行扫描
,而epoll事先经过epoll_ctl()来注册一个文件描述符,
一旦基于某个文件描述符就绪时,内核会采用相似callback的回调机制
,迅速激活这个文件描述符,当进程调用epoll_wait()时便获得通知。(
此处去掉了遍历文件描述符,而是经过监听回调的的机制。这正是epoll的魅力所在。
)
注意:若是没有大量的idle-connection或者dead-connection,epoll的效率并不会比select/poll高不少,可是当遇到大量的idle-connection,就会发现epoll的效率大大高于select/poll。
3、select、poll、epoll区别
一、支持一个进程所能打开的最大链接数
二、FD剧增后带来的IO效率问题
三、消息传递方式
综上,在选择select,poll,epoll时要根据具体的使用场合以及这三种方式的自身特色:
一、表面上看epoll的性能最好,
可是在链接数少而且链接都十分活跃的状况下,select和poll的性能可能比epoll好
,毕竟epoll的通知机制须要不少函数回调。
二、select低效是由于每次它都须要轮询
。但低效也是相对的,视状况而定,也可经过良好的设计改善。
select()函数和poll()函数均是主要用来处理多路I/O复用的状况。好比一个服务器既想等待输入终端到来,又想等待若干个套接字有客户请求到达,这时候就须要借助select或者poll函数了。
(一)select()函数
原型以下:
css
1 int select(int fdsp1, fd_set *readfds, fd_set *writefds, fd_set *errorfds, const struct timeval *timeout);
各个参数含义以下:html
- int fdsp1:最大描述符值 + 1
- fd_set *readfds:对可读感兴趣的描述符集
- fd_set *writefds:对可写感兴趣的描述符集
- fd_set *errorfds:对出错感兴趣的描述符集
- struct timeval *timeout:超时时间(注意:对于linux系统,此参数没有const限制,每次select调用完毕timeout的值都被修改成剩余时间,而unix系统则不会改变timeout值)
select函数会在发生如下状况时返回:node
- readfds集合中有描述符可读
- writefds集合中有描述符可写
- errorfds集合中有描述符遇到错误条件
- 指定的超时时间timeout到了
当select返回时,描述符集合将被修改以指示哪些个描述符正处于可读、可写或有错误状态。能够用FD_ISSET宏对描述符进行测试以找到状态变化的描述符。若是select由于超时而返回的话,全部的描述符集合都将被清空。
select函数返回状态发生变化的描述符总数。返回0意味着超时。失败则返回-1并设置errno。可能出现的错误有:EBADF(无效描述符)、EINTR(因终端而返回)、EINVAL(nfds或timeout取值错误)。
设置描述符集合一般用以下几个宏定义:
linux
1 FD_ZERO(fd_set *fdset); /* clear all bits in fdset */
2 FD_SET(int fd, fd_set *fdset); /* turn on the bit for fd in fd_set */
3 FD_CLR(int fd, fd_set *fdset); /* turn off the bit for fd in fd_set */
4 int FD_ISSET(int fd, fd_set *fdset); /* is the bit for fd on in fdset? */
如:
ios
1 fd_set rset;
2 FD_ZERO(&rset); /* initialize the set: all bits off */
3 FD_SET(1, &rset); /* turn on bit for fd 1 */
4 FD_SET(4, &rset); /* turn on bit for fd 4 */
5 FD_SET(5, &rset); /* turn on bit for fd 5 */
当select返回的时候,rset位都将被置0,除了那些有变化的fd位。
当发生以下状况时认为是可读的:程序员
- socket的receive buffer中的字节数大于socket的receive buffer的low-water mark属性值。(low-water mark值相似于分水岭,当receive buffer中的字节数小于low-water mark值的时候,认为socket还不可读,只有当receive buffer中的字节数达到必定量的时候才认为socket可读)
- 链接半关闭(读关闭,即收到对端发来的FIN包)
- 发生变化的描述符是被动套接字,而链接的三路握手完成的数量大于0,即有新的TCP链接创建
- 描述符发生错误,若是调用read系统调用读套接字的话会返回-1。
当发生以下状况时认为是可写的:web
- socket的send buffer中的字节数大于socket的send buffer的low-water mark属性值以及socket已经链接或者不须要链接(如UDP)。
- 写半链接关闭,调用write函数将产生SIGPIPE
- 描述符发生错误,若是调用write系统调用写套接字的话会返回-1。
注意:
select默认能处理的描述符数量是有上限的,为FD_SETSIZE的大小。
对于timeout参数,若是置为NULL,则表示wait forever;若timeout->tv_sec = timeout->tv_usec = 0,则表示do not wait at all;不然指定等待时间。
若是使用select处理多个套接字,那么须要使用一个数组(也能够是其余结构)来记录各个描述符的状态。而使用poll则不须要,下面看poll函数。
(二)poll()函数
原型以下:
算法
1 int poll(struct pollfd *fdarray, unsigned long nfds, int timeout);
各参数含义以下:sql
- struct pollfd *fdarray:一个结构体,用来保存各个描述符的相关状态。
- unsigned long nfds:fdarray数组的大小,即里面包含有效成员的数量。
- int timeout:设定的超时时间。(以毫秒为单位)
poll函数返回值及含义以下:编程
- -1:有错误产生
- 0:超时时间到,并且没有描述符有状态变化
- >0:有状态变化的描述符个数
着重讲fdarray数组,由于这是它和select()函数主要的不一样的地方:
pollfd的结构以下:
1 struct pollfd {
2 int fd; /* descriptor to check */
3 short events; /* events of interest on fd */
4 short revents; /* events that occured on fd */
5 };
其实poll()和select()函数要处理的问题是相同的,只不过是不一样组织在几乎相同时刻同时推出的,所以才同时保留了下来。select()函数把可读描述符、可写描述符、错误描述符分在了三个集合里,这三个集合都是用bit位来标记一个描述符,一旦有若干个描述符状态发生变化,那么它将被置位,而其余没有发生变化的描述符的bit位将被clear,也就是说select()的readset、writeset、errorset是一个value-result类型,经过它们传值,而也经过它们返回结果。这样的一个坏处是每次从新select 的时候对集合必须从新赋值。而poll()函数则与select()采用的方式不一样,它经过一个结构数组保存各个描述符的状态,每一个结构体第一项fd表明描述符,第二项表明要监听的事件,也就是感兴趣的事件,而第三项表明poll()返回时描述符的返回状态。合法状态以下:
- POLLIN: 有普通数据或者优先数据可读
- POLLRDNORM: 有普通数据可读
- POLLRDBAND: 有优先数据可读
- POLLPRI: 有紧急数据可读
- POLLOUT: 有普通数据可写
- POLLWRNORM: 有普通数据可写
- POLLWRBAND: 有紧急数据可写
- POLLERR: 有错误发生
- POLLHUP: 有描述符挂起事件发生
- POLLNVAL: 描述符非法
对于POLLIN | POLLPRI等价与select()的可读事件;POLLOUT | POLLWRBAND等价与select()的可写事件;POLLIN 等价与POLLRDNORM | POLLRDBAND,而POLLOUT等价于POLLWRBAND。若是你对一个描述符的可读事件和可写事件以及错误等事件均感兴趣那么你应该都进行相应的设置。
对于timeout的设置以下:
- INFTIM: wait forever
- 0: return immediately, do not block
- >0: wait specified number of milliseconds
同步IO和异步IO,阻塞IO和非阻塞IO分别是什么,到底有什么区别?不一样的人在不一样的上下文下给出的答案是不一样的。因此先限定一下本文的上下文。
本文讨论的背景是Linux环境下的network IO。
一 概念说明
在进行解释以前,首先要说明几个概念:
- 用户空间和内核空间
- 进程切换
- 进程的阻塞
- 文件描述符
- 缓存 I/O
用户空间与内核空间
如今操做系统都是采用虚拟存储器,那么对32位操做系统而言,它的寻址空间(虚拟存储空间)为4G(2的32次方)。操做系统的核心是内核,独立于普通的应用程序,能够访问受保护的内存空间,也有访问底层硬件设备的全部权限。为了保证用户进程不能直接操做内核(kernel),保证内核的安全,操心系统将虚拟空间划分为两部分,一部分为内核空间,一部分为用户空间。针对linux操做系统而言,将最高的1G字节(从虚拟地址0xC0000000到0xFFFFFFFF),供内核使用,称为内核空间,而将较低的3G字节(从虚拟地址0x00000000到0xBFFFFFFF),供各个进程使用,称为用户空间。
进程切换
为了控制进程的执行,内核必须有能力挂起正在CPU上运行的进程,并恢复之前挂起的某个进程的执行。这种行为被称为进程切换。所以能够说,任何进程都是在操做系统内核的支持下运行的,是与内核紧密相关的。
从一个进程的运行转到另外一个进程上运行,这个过程当中通过下面这些变化:
1. 保存处理机上下文,包括程序计数器和其余寄存器。
2. 更新PCB信息。
3. 把进程的PCB移入相应的队列,如就绪、在某事件阻塞等队列。
4. 选择另外一个进程执行,并更新其PCB。
5. 更新内存管理的数据结构。
6. 恢复处理机上下文。
注:总而言之就是很耗资源,具体的能够参考这篇文章:进程切换
进程的阻塞
正在执行的进程,因为期待的某些事件未发生,如请求系统资源失败、等待某种操做的完成、新数据还没有到达或无新工做作等,则由系统自动执行阻塞原语(Block),使本身由运行状态变为阻塞状态。可见,进程的阻塞是进程自身的一种主动行为,也所以只有处于运行态的进程(得到CPU),才可能将其转为阻塞状态。当进程进入阻塞状态,是不占用CPU资源的
。
文件描述符fd
文件描述符(File descriptor)是计算机科学中的一个术语,是一个用于表述指向文件的引用的抽象化概念。
文件描述符在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者建立一个新文件时,内核向进程返回一个文件描述符。在程序设计中,一些涉及底层的程序编写每每会围绕着文件描述符展开。可是文件描述符这一律念每每只适用于UNIX、Linux这样的操做系统。
缓存 I/O
缓存 I/O 又被称做标准 I/O,大多数文件系统的默认 I/O 操做都是缓存 I/O。在 Linux 的缓存 I/O 机制中,操做系统会将 I/O 的数据缓存在文件系统的页缓存( page cache )中,也就是说,数据会先被拷贝到操做系统内核的缓冲区中,而后才会从操做系统内核的缓冲区拷贝到应用程序的地址空间。
缓存 I/O 的缺点:
数据在传输过程当中须要在应用程序地址空间和内核进行屡次数据拷贝操做,这些数据拷贝操做所带来的 CPU 以及内存开销是很是大的。
二 IO模式
刚才说了,对于一次IO访问(以read举例),数据会先被拷贝到操做系统内核的缓冲区中,而后才会从操做系统内核的缓冲区拷贝到应用程序的地址空间。因此说,当一个read操做发生时,它会经历两个阶段:
1. 等待数据准备 (Waiting for the data to be ready)
2. 将数据从内核拷贝到进程中 (Copying the data from the kernel to the process)
正式由于这两个阶段,linux系统产生了下面五种网络模式的方案。
- 阻塞 I/O(blocking IO)
- 非阻塞 I/O(nonblocking IO)
- I/O 多路复用( IO multiplexing)
- 信号驱动 I/O( signal driven IO)
- 异步 I/O(asynchronous IO)
注:因为signal driven IO在实际中并不经常使用,因此我这只说起剩下的四种IO Model。
阻塞 I/O(blocking IO)
在linux中,默认状况下全部的socket都是blocking,一个典型的读操做流程大概是这样:

当用户进程调用了recvfrom这个系统调用,kernel就开始了IO的第一个阶段:准备数据(对于网络IO来讲,不少时候数据在一开始尚未到达。好比,尚未收到一个完整的UDP包。这个时候kernel就要等待足够的数据到来)。这个过程须要等待,也就是说数据被拷贝到操做系统内核的缓冲区中是须要一个过程的。而在用户进程这边,整个进程会被阻塞(固然,是进程本身选择的阻塞)。当kernel一直等到数据准备好了,它就会将数据从kernel中拷贝到用户内存,而后kernel返回结果,用户进程才解除block的状态,从新运行起来。
因此,blocking IO的特色就是在IO执行的两个阶段都被block了。
非阻塞 I/O(nonblocking IO)
linux下,能够经过设置socket使其变为non-blocking。当对一个non-blocking socket执行读操做时,流程是这个样子:

当用户进程发出read操做时,若是kernel中的数据尚未准备好,那么它并不会block用户进程,而是马上返回一个error。从用户进程角度讲 ,它发起一个read操做后,并不须要等待,而是立刻就获得了一个结果。用户进程判断结果是一个error时,它就知道数据尚未准备好,因而它能够再次发送read操做。一旦kernel中的数据准备好了,而且又再次收到了用户进程的system call,那么它立刻就将数据拷贝到了用户内存,而后返回。
因此,nonblocking IO的特色是用户进程须要不断的主动询问kernel数据好了没有。
I/O 多路复用( IO multiplexing)
IO multiplexing就是咱们说的select,poll,epoll,有些地方也称这种IO方式为event driven IO。select/epoll的好处就在于单个process就能够同时处理多个网络链接的IO。它的基本原理就是select,poll,epoll这个function会不断的轮询所负责的全部socket,当某个socket有数据到达了,就通知用户进程。

当用户进程调用了select,那么整个进程会被block
,而同时,kernel会“监视”全部select负责的socket,当任何一个socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操做,将数据从kernel拷贝到用户进程。
因此,I/O 多路复用的特色是经过一种机制一个进程能同时等待多个文件描述符,而这些文件描述符(套接字描述符)其中的任意一个进入读就绪状态,select()函数就能够返回。
这个图和blocking IO的图其实并无太大的不一样,事实上,还更差一些。由于这里须要使用两个system call (select 和 recvfrom),而blocking IO只调用了一个system call (recvfrom)。可是,用select的优点在于它能够同时处理多个connection。
因此,若是处理的链接数不是很高的话,使用select/epoll的web server不必定比使用multi-threading + blocking IO的web server性能更好,可能延迟还更大。select/epoll的优点并非对于单个链接能处理得更快,而是在于能处理更多的链接。)
在IO multiplexing Model中,实际中,对于每个socket,通常都设置成为non-blocking,可是,如上图所示,整个用户的process实际上是一直被block的。只不过process是被select这个函数block,而不是被socket IO给block。
异步 I/O(asynchronous IO)
inux下的asynchronous IO其实用得不多。先看一下它的流程:

用户进程发起read操做以后,马上就能够开始去作其它的事。而另外一方面,从kernel的角度,当它受到一个asynchronous read以后,首先它会马上返回,因此不会对用户进程产生任何block。而后,kernel会等待数据准备完成,而后将数据拷贝到用户内存,当这一切都完成以后,kernel会给用户进程发送一个signal,告诉它read操做完成了。
总结
blocking和non-blocking的区别
调用blocking IO会一直block住对应的进程直到操做完成,而non-blocking IO在kernel还准备数据的状况下会马上返回。
synchronous IO和asynchronous IO的区别
在说明synchronous IO和asynchronous IO的区别以前,须要先给出二者的定义。POSIX的定义是这样子的:
- A synchronous I/O operation causes the requesting process to be blocked until that I/O operation completes;
- An asynchronous I/O operation does not cause the requesting process to be blocked;
二者的区别就在于synchronous IO作”IO operation”的时候会将process阻塞。按照这个定义,以前所述的blocking IO,non-blocking IO,IO multiplexing都属于synchronous IO。
有人会说,non-blocking IO并无被block啊。这里有个很是“狡猾”的地方,定义中所指的”IO operation”是指真实的IO操做,就是例子中的recvfrom这个system call。non-blocking IO在执行recvfrom这个system call的时候,若是kernel的数据没有准备好,这时候不会block进程。可是,当kernel中数据准备好的时候,recvfrom会将数据从kernel拷贝到用户内存中,这个时候进程是被block了,在这段时间内,进程是被block的。
而asynchronous IO则不同,当进程发起IO 操做以后,就直接返回不再理睬了,直到kernel发送一个信号,告诉进程说IO完成。在这整个过程当中,进程彻底没有被block。
各个IO Model的比较如图所示:

经过上面的图片,能够发现non-blocking IO和asynchronous IO的区别仍是很明显的。在non-blocking IO中,虽然进程大部分时间都不会被block,可是它仍然要求进程去主动的check,而且当数据准备完成之后,也须要进程主动的再次调用recvfrom来将数据拷贝到用户内存。而asynchronous IO则彻底不一样。它就像是用户进程将整个IO操做交给了他人(kernel)完成,而后他人作完后发信号通知。在此期间,用户进程不须要去检查IO操做的状态,也不须要主动的去拷贝数据。
三 I/O 多路复用之select、poll、epoll详解
select,poll,epoll都是IO多路复用的机制。I/O多路复用就是经过一种机制,一个进程能够监视多个描述符,一旦某个描述符就绪(通常是读就绪或者写就绪),可以通知程序进行相应的读写操做。但select,poll,epoll本质上都是同步I/O,由于他们都须要在读写事件就绪后本身负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需本身负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。(这里啰嗦下)
select
int select (int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
select 函数监视的文件描述符分3类,分别是writefds、readfds、和exceptfds。调用后select函数会阻塞,直到有描述副就绪(有数据 可读、可写、或者有except),或者超时(timeout指定等待时间,若是当即返回设为null便可),函数返回。当select函数返回后,能够 经过遍历fdset,来找到就绪的描述符。
select目前几乎在全部的平台上支持,其良好跨平台支持也是它的一个优势。select的一 个缺点在于单个进程可以监视的文件描述符的数量存在最大限制,在Linux上通常为1024,能够经过修改宏定义甚至从新编译内核的方式提高这一限制,但 是这样也会形成效率的下降。
poll
int poll (struct pollfd *fds, unsigned int nfds, int timeout);
不一样与select使用三个位图来表示三个fdset的方式,poll使用一个 pollfd的指针实现。
struct pollfd {
int fd;
pollfd结构包含了要监视的event和发生的event,再也不使用select“参数-值”传递的方式。同时,pollfd并无最大数量限制(可是数量过大后性能也是会降低)。 和select函数同样,poll返回后,须要轮询pollfd来获取就绪的描述符。
从上面看,select和poll都须要在返回后,经过遍历文件描述符来获取已经就绪的socket
。事实上,同时链接的大量客户端在一时刻可能只有不多的处于就绪状态,所以随着监视的描述符数量的增加,其效率也会线性降低。
epoll
epoll是在2.6内核中提出的,是以前的select和poll的加强版本。相对于select和poll来讲,epoll更加灵活,没有描述符限制。epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。
一 epoll操做过程
epoll操做过程须要三个接口,分别以下:
int epoll_create(int size);
1. int epoll_create(int size);
建立一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大,这个参数不一样于select()中的第一个参数,给出最大监听的fd+1的值,参数size并非限制了epoll所能监听的描述符最大个数,只是对内核初始分配内部数据结构的一个建议
。
当建立好epoll句柄后,它就会占用一个fd值,在linux下若是查看/proc/进程id/fd/,是可以看到这个fd的,因此在使用完epoll后,必须调用close()关闭,不然可能致使fd被耗尽。
2. int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
函数是对指定描述符fd执行op操做。
- epfd:是epoll_create()的返回值。
- op:表示op操做,用三个宏来表示:添加EPOLL_CTL_ADD,删除EPOLL_CTL_DEL,修改EPOLL_CTL_MOD。分别添加、删除和修改对fd的监听事件。
- fd:是须要监听的fd(文件描述符)
- epoll_event:是告诉内核须要监听什么事,struct epoll_event结构以下:
struct epoll_event {
__uint32_t events;
3. int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
等待epfd上的io事件,最多返回maxevents个事件。
参数events用来从内核获得事件的集合,maxevents告以内核这个events有多大,这个maxevents的值不能大于建立epoll_create()时的size,参数timeout是超时时间(毫秒,0会当即返回,-1将不肯定,也有说法说是永久阻塞)。该函数返回须要处理的事件数目,如返回0表示已超时。
二 工做模式
epoll对文件描述符的操做有两种模式:LT(level trigger)和ET(edge trigger)。LT模式是默认模式,LT模式与ET模式的区别以下:
LT模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序能够不当即处理该事件
。下次调用epoll_wait时,会再次响应应用程序并通知此事件。
ET模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序必须当即处理该事件
。若是不处理,下次调用epoll_wait时,不会再次响应应用程序并通知此事件。
1. LT模式
LT(level triggered)是缺省的工做方式,而且同时支持block和no-block socket.在这种作法中,内核告诉你一个文件描述符是否就绪了,而后你能够对这个就绪的fd进行IO操做。若是你不做任何操做,内核仍是会继续通知你的。
2. ET模式
ET(edge-triggered)是高速工做方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核经过epoll告诉你。而后它会假设你知道文件描述符已经就绪,而且不会再为那个文件描述符发送更多的就绪通知,直到你作了某些操做致使那个文件描述符再也不为就绪状态了(好比,你在发送,接收或者接收请求,或者发送接收的数据少于必定量时致使了一个EWOULDBLOCK 错误)。可是请注意,若是一直不对这个fd做IO操做(从而致使它再次变成未就绪),内核不会发送更多的通知(only once)
ET模式在很大程度上减小了epoll事件被重复触发的次数,所以效率要比LT模式高。epoll工做在ET模式的时候,必须使用非阻塞套接口,以免因为一个文件句柄的阻塞读/阻塞写操做把处理多个文件描述符的任务饿死。
3. 总结
假若有这样一个例子:
1. 咱们已经把一个用来从管道中读取数据的文件句柄(RFD)添加到epoll描述符
2. 这个时候从管道的另外一端被写入了2KB的数据
3. 调用epoll_wait(2),而且它会返回RFD,说明它已经准备好读取操做
4. 而后咱们读取了1KB的数据
5. 调用epoll_wait(2)......
LT模式:
若是是LT模式,那么在第5步调用epoll_wait(2)以后,仍然能受到通知。
ET模式:
若是咱们在第1步将RFD添加到epoll描述符的时候使用了EPOLLET标志,那么在第5步调用epoll_wait(2)以后将有可能会挂起,由于剩余的数据还存在于文件的输入缓冲区内,并且数据发出端还在等待一个针对已经发出数据的反馈信息。只有在监视的文件句柄上发生了某个事件的时候 ET 工做模式才会汇报事件。所以在第5步的时候,调用者可能会放弃等待仍在存在于文件输入缓冲区内的剩余数据。
当使用epoll的ET模型来工做时,当产生了一个EPOLLIN事件后,
读数据的时候须要考虑的是当recv()返回的大小若是等于请求的大小,那么颇有多是缓冲区还有数据未读完,也意味着该次事件尚未处理完,因此还须要再次读取:
while(rs){
buflen = recv(activeevents[i].data.fd, buf, sizeof(buf), 0);
if(buflen < 0){
Linux中的EAGAIN含义
Linux环境下开发常常会碰到不少错误(设置errno),其中EAGAIN是其中比较常见的一个错误(好比用在非阻塞操做中)。
从字面上来看,是提示再试一次。这个错误常常出如今当应用程序进行一些非阻塞(non-blocking)操做(对文件或socket)的时候。
例如,以 O_NONBLOCK的标志打开文件/socket/FIFO,若是你连续作read操做而没有数据可读。此时程序不会阻塞起来等待数据准备就绪返回,read函数会返回一个错误EAGAIN,提示你的应用程序如今没有数据可读请稍后再试。
又例如,当一个系统调用(好比fork)由于没有足够的资源(好比虚拟内存)而执行失败,返回EAGAIN提示其再调用一次(也许下次就能成功)。
三 代码演示
下面是一段不完整的代码且格式不对,意在表述上面的过程,去掉了一些模板代码。
#define IPADDRESS "127.0.0.1"
#define PORT 8787 #define MAXSIZE 1024 #define LISTENQ 5 #define FDSIZE 1000 #define EPOLLEVENTS 100 listenfd = socket_bind(IPADDRESS,PORT); struct epoll_event events[EPOLLEVENTS];
四 epoll总结
在 select/poll中,进程只有在调用必定的方法后,内核才对全部监视的文件描述符进行扫描,而epoll事先经过epoll_ctl()来注册一 个文件描述符,一旦基于某个文件描述符就绪时,内核会采用相似callback的回调机制,迅速激活这个文件描述符,当进程调用epoll_wait() 时便获得通知。(此处去掉了遍历文件描述符,而是经过监听回调的的机制
。这正是epoll的魅力所在。)
epoll的优势主要是一下几个方面:
1. 监视的描述符数量不受限制,它所支持的FD上限是最大能够打开文件的数目,这个数字通常远大于2048,举个例子,在1GB内存的机器上大约是10万左 右,具体数目能够cat /proc/sys/fs/file-max察看,通常来讲这个数目和系统内存关系很大。select的最大缺点就是进程打开的fd是有数量限制的。这对 于链接数量比较大的服务器来讲根本不能知足。虽然也能够选择多进程的解决方案( Apache就是这样实现的),不过虽然linux上面建立进程的代价比较小,但仍旧是不可忽视的,加上进程间数据同步远比不上线程间同步的高效,因此也不是一种完美的方案。
- IO的效率不会随着监视fd的数量的增加而降低。epoll不一样于select和poll轮询的方式,而是经过每一个fd定义的回调函数来实现的。只有就绪的fd才会执行回调函数。
若是没有大量的idle -connection或者dead-connection,epoll的效率并不会比select/poll高不少,可是当遇到大量的idle- connection,就会发现epoll的效率大大高于select/poll。
参考
用户空间与内核空间,进程上下文与中断上下文[总结]
进程切换
维基百科-文件描述符
Linux 中直接 I/O 机制的介绍
IO - 同步,异步,阻塞,非阻塞 (亡羊补牢篇)
Linux中select poll和epoll的区别
IO多路复用之select总结
IO多路复用之poll总结
IO多路复用之epoll总结
select,poll,epoll都是IO多路复用的机制。I/O多路复用就经过一种机制,能够监视多个描述符,一旦某个描述符就绪(通常是读就绪或者写就绪),可以通知程序进行相应的读写操做。但select,poll,epoll本质上都是同步I/O,由于他们都须要在读写事件就绪后本身负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需本身负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。关于这三种IO多路复用的用法,前面三篇总结写的很清楚,并用服务器回射echo程序进行了测试。链接以下所示:
select:http://www.cnblogs.com/Anker/archive/2013/08/14/3258674.html
poll:http://www.cnblogs.com/Anker/archive/2013/08/15/3261006.html
epoll:http://www.cnblogs.com/Anker/archive/2013/08/17/3263780.html
今天对这三种IO多路复用进行对比,参考网上和书上面的资料,整理以下:
一、select实现
select的调用过程以下所示:

(1)使用copy_from_user从用户空间拷贝fd_set到内核空间
(2)注册回调函数__pollwait
(3)遍历全部fd,调用其对应的poll方法(对于socket,这个poll方法是sock_poll,sock_poll根据状况会调用到tcp_poll,udp_poll或者datagram_poll)
(4)以tcp_poll为例,其核心实现就是__pollwait,也就是上面注册的回调函数。
(5)__pollwait的主要工做就是把current(当前进程)挂到设备的等待队列中,不一样的设备有不一样的等待队列,对于tcp_poll来讲,其等待队列是sk->sk_sleep(注意把进程挂到等待队列中并不表明进程已经睡眠了)。在设备收到一条消息(网络设备)或填写完文件数据(磁盘设备)后,会唤醒设备等待队列上睡眠的进程,这时current便被唤醒了。
(6)poll方法返回时会返回一个描述读写操做是否就绪的mask掩码,根据这个mask掩码给fd_set赋值。
(7)若是遍历完全部的fd,尚未返回一个可读写的mask掩码,则会调用schedule_timeout是调用select的进程(也就是current)进入睡眠。当设备驱动发生自身资源可读写后,会唤醒其等待队列上睡眠的进程。若是超过必定的超时时间(schedule_timeout指定),仍是没人唤醒,则调用select的进程会从新被唤醒得到CPU,进而从新遍历fd,判断有没有就绪的fd。
(8)把fd_set从内核空间拷贝到用户空间。
总结:
select的几大缺点:
(1)每次调用select,都须要把fd集合从用户态拷贝到内核态,这个开销在fd不少时会很大
(2)同时每次调用select都须要在内核遍历传递进来的全部fd,这个开销在fd不少时也很大
(3)select支持的文件描述符数量过小了,默认是1024
2 poll实现
poll的实现和select很是类似,只是描述fd集合的方式不一样,poll使用pollfd结构而不是select的fd_set结构,其余的都差很少。
关于select和poll的实现分析,能够参考下面几篇博文:
http://blog.csdn.net/lizhiguo0532/article/details/6568964#comments
http://blog.csdn.net/lizhiguo0532/article/details/6568968
http://blog.csdn.net/lizhiguo0532/article/details/6568969
http://www.ibm.com/developerworks/cn/linux/l-cn-edntwk/index.html?ca=drs-
http://linux.chinaunix.net/techdoc/net/2009/05/03/1109887.shtml
三、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 I/O复用中select poll epoll模型的介绍及其优缺点的比较
关于I/O多路复用:
I/O多路复用(又被称为“事件驱动”),首先要理解的是,操做系统为你提供了一个功能,当你的某个socket可读或者可写的时候,它能够给你一个通知。这样当配合非阻塞的socket使用时,只有当系统通知我哪一个描述符可读了,我才去执行read操做,能够保证每次read都能读到有效数据而不作纯返回-1和EAGAIN的无用功。写操做相似。操做系统的这个功能经过select/poll/epoll之类的系统调用来实现,这些函数均可以同时监视多个描述符的读写就绪情况,这样,**多个描述符的I/O操做都能在一个线程内并发交替地顺序完成,这就叫I/O多路复用,这里的“复用”指的是复用同一个线程。
1、I/O复用之select
一、介绍:
select系统调用的目的是:在一段指定时间内,监听用户感兴趣的文件描述符上的可读、可写和异常事件。poll和select应该被归类为这样的系统调用,它们能够阻塞地同时探测一组支持非阻塞的IO设备,直至某一个设备触发了事件或者超过了指定的等待时间——也就是说它们的职责不是作IO,而是帮助调用者寻找当前就绪的设备。
下面是select的原理图:

二、select系统调用API以下:
fd_set结构体是文件描述符集,该结构体其实是一个整型数组,数组中的每一个元素的每一位标记一个文件描述符。fd_set能容纳的文件描述符数量由FD_SETSIZE指定,通常状况下,FD_SETSIZE等于1024,这就限制了select能同时处理的文件描述符的总量。
三、下面介绍一下各个参数的含义:
1)nfds参数指定被监听的文件描述符的总数。一般被设置为select监听的全部文件描述符中最大值加1;
2)readfds、writefds、exceptfds分别指向可读、可写和异常等事件对应的文件描述符集合。这三个参数都是传入传出型参数,指的是在调用select以前,用户把关心的可读、可写、或异常的文件描述符经过FD_SET(下面介绍)函数分别添加进readfds、writefds、exceptfds文件描述符集,select将对这些文件描述符集中的文件描述符进行监听,若是有就绪文件描述符,select会重置readfds、writefds、exceptfds文件描述符集来通知应用程序哪些文件描述符就绪。这个特性将致使select函数返回后,再次调用select以前,必须重置咱们关心的文件描述符,也就是三个文件描述符集已经不是咱们以前传入 的了。
3)timeout参数用来指定select函数的超时时间(下面讲select返回值时还会谈及)。
struct timeval
{
long tv_sec;
四、下面几个函数(宏实现)用来操纵文件描述符集:
void FD_SET(int fd, fd_set *set);
五、select的返回状况:
1)若是指定timeout为NULL,select会永远等待下去,直到有一个文件描述符就绪,select返回;
2)若是timeout的指定时间为0,select根本不等待,当即返回;
3)若是指定一段固定时间,则在这一段时间内,若是有指定的文件描述符就绪,select函数返回,若是超过指定时间,select一样返回。
4)返回值状况:
a)超时时间内,若是文件描述符就绪,select返回就绪的文件描述符总数(包括可读、可写和异常),若是没有文件描述符就绪,select返回0;
b)select调用失败时,返回 -1并设置errno,若是收到信号,select返回 -1并设置errno为EINTR。
六、文件描述符的就绪条件:
在网络编程中,
1)下列状况下socket可读:
a) socket内核接收缓冲区的字节数大于或等于其低水位标记SO_RCVLOWAT;
b) socket通讯的对方关闭链接,此时该socket可读,可是一旦读该socket,会当即返回0(能够用这个方法判断client端是否断开链接);
c) 监听socket上有新的链接请求;
d) socket上有未处理的错误。
2)下列状况下socket可写:
a) socket内核发送缓冲区的可用字节数大于或等于其低水位标记SO_SNDLOWAT;
b) socket的读端关闭,此时该socket可写,一旦对该socket进行操做,该进程会收到SIGPIPE信号;
c) socket使用connect链接成功以后;
d) socket上有未处理的错误。
2、I/O复用之poll
一、poll系统调用的原理与原型和select基本相似,也是在指定时间内轮询必定数量的文件描述符,以测试其中是否有就绪者。
二、poll系统调用API以下:
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
三、下面介绍一下各个参数的含义:
1)第一个参数是指向一个结构数组的第一个元素的指针,每一个元素都是一个pollfd结构,用于指定测试某个给定描述符的条件。
struct pollfd
{
int fd;
待监听的事件由events成员指定,函数在相应的revents成员中返回该描述符的状态(每一个文件描述符都有两个事件,一个是传入型的events,一个是传出型的revents,从而避免使用传入传出型参数,注意与select的区别),从而告知应用程序fd上实际发生了哪些事件。events和revents均可以是多个事件的按位或。
2)第二个参数是要监听的文件描述符的个数,也就是数组fds的元素个数;
3)第三个参数意义与select相同。
四、poll的事件类型:
在使用POLLRDHUP时,要在代码开始处定义_GNU_SOURCE
五、poll的返回状况:
与select相同。
3、I/O复用之epoll
一、介绍:
epoll 与select和poll在使用和实现上有很大区别。首先,epoll使用一组函数来完成,而不是单独的一个函数;其次,epoll把用户关心的文件描述符上的事件放在内核里的一个事件表中,无须向select和poll那样每次调用都要重复传入文件描述符集合事件集。
二、建立一个文件描述符,指定内核中的事件表:
#include<sys/epoll.h>
int epoll_create(int size);
size参数并不起做用,只是给内核一个提示,告诉它事件表须要多大。该函数返回的文件描述符指定要访问的内核事件表,是其余全部epoll系统调用的句柄。
三、操做内核事件表:
#include<sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epfd是epoll_create返回的文件句柄,标识事件表,op指定操做类型。操做类型有如下3种:
a)EPOLL_CTL_ADD, 往事件表中注册fd上的事件;
b)EPOLL_CTL_MOD, 修改fd上注册的事件;
c)EPOLL_CTL_DEL, 删除fd上注册的事件。
event参数指定事件,epoll_event的定义以下:
struct epoll_event
{
__int32_t events;
在使用epoll_ctl时,是把fd添加、修改到内核事件表中,或从内核事件表中删除fd的事件。若是是添加事件到事件表中,能够往data中的fd上添加事件events,或者不用data中的fd,而把fd放到用户数据ptr所指的内存中(由于epoll_data是一个联合体,只能使用其中一个数据),再设置events。
三、epoll_wait函数
epoll系统调用的最关键的一个函数epoll_wait,它在一段时间内等待一个组文件描述符上的事件。
#include<sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
timeout参数和select与poll相同,指定一个超时时间;maxevents指定最多监听多少个事件;events是一个传出型参数,epoll_wait函数若是检测到事件就绪,就将全部就绪的事件从内核事件表(epfd所指的文件)中复制到events指定的数组中。这个数组用来输出epoll_wait检测到的就绪事件,而不像select与poll那样,这也是epoll与前者最大的区别,下文在比较三者之间的区别时还会说到。
4、三组I/O复用函数的比较
相同点:
1)三者都须要在fd上注册用户关心的事件;
2)三者都要一个timeout参数指定超时时间;
不一样点:
1)select:
a)select指定三个文件描述符集,分别是可读、可写和异常事件,因此不能更加细致地区分全部可能发生的事件;
b)select若是检测到就绪事件,会在原来的文件描述符上改动,以告知应用程序,文件描述符上发生了什么时间,因此再次调用select时,必须先重置文件描述符;
c)select采用对全部注册的文件描述符集轮询的方式,会返回整个用户注册的事件集合,因此应用程序索引就绪文件的时间复杂度为O(n);
d)select容许监听的最大文件描述符个数一般有限制,通常是1024,若是大于1024,select的性能会急剧降低;
e)只能工做在LT模式。
2)poll:
a)poll把文件描述符和事件绑定,事件不但能够单独指定,并且能够是多个事件的按位或,这样更加细化了事件的注册,并且poll单独采用一个元素用来保存就绪返回时的结果,这样在下次调用poll时,就不用重置以前注册的事件;
b)poll采用对全部注册的文件描述符集轮询的方式,会返回整个用户注册的事件集合,因此应用程序索引就绪文件的时间复杂度为O(n)。
c)poll用nfds参数指定最多监听多少个文件描述符和事件,这个数能达到系统容许打开的最大文件描述符数目,即65535。
d)只能工做在LT模式。
3)epoll:
a)epoll把用户注册的文件描述符和事件放到内核当中的事件表中,提供了一个独立的系统调用epoll_ctl来管理用户的事件,并且epoll采用回调的方式,一旦有注册的文件描述符就绪,讲触发回调函数,该回调函数将就绪的文件描述符和事件拷贝到用户空间events所管理的内存,这样应用程序索引就绪文件的时间复杂度达到O(1)。
b)epoll_wait使用maxevents来制定最多监听多少个文件描述符和事件,这个数能达到系统容许打开的最大文件描述符数目,即65535;
c)不只能工做在LT模式,并且还支持ET高效模式(即EPOLLONESHOT事件,读者能够本身查一下这个事件类型,对于epoll的线程安全有很好的帮助)。
select/poll/epoll总结:

深度理解select、poll和epoll
在linux 没有实现epoll事件驱动机制以前,咱们通常选择用select或者poll等IO多路复用的方法来实现并发服务程序。在大数据、高并发、集群等一些名词唱得火热之年代,select和poll的用武之地愈来愈有限,风头已经被epoll占尽。
本文便来介绍epoll的实现机制,并附带讲解一下select和poll。经过对比其不一样的实现机制,真正理解为什么epoll能实现高并发。
select()和poll() IO多路复用模型
select的缺点:
- 单个进程可以监视的文件描述符的数量存在最大限制,一般是1024,固然能够更改数量,但因为select采用轮询的方式扫描文件描述符,文件描述符数量越多,性能越差;(在linux内核头文件中,有这样的定义:#define __FD_SETSIZE 1024)
- 内核 / 用户空间内存拷贝问题,select须要复制大量的句柄数据结构,产生巨大的开销;
- select返回的是含有整个句柄的数组,应用程序须要遍历整个数组才能发现哪些句柄发生了事件;
- select的触发方式是水平触发,应用程序若是没有完成对一个已经就绪的文件描述符进行IO操做,那么以后每次select调用仍是会将这些文件描述符通知进程。
相比select模型,poll使用链表保存文件描述符,所以没有了监视文件数量的限制,但其余三个缺点依然存在。
拿select模型为例,假设咱们的服务器须要支持100万的并发链接,则在__FD_SETSIZE 为1024的状况下,则咱们至少须要开辟1k个进程才能实现100万的并发链接。除了进程间上下文切换的时间消耗外,从内核/用户空间大量的无脑内存拷贝、数组轮询等,是系统难以承受的。所以,基于select模型的服务器程序,要达到10万级别的并发访问,是一个很难完成的任务。
所以,该epoll上场了。
epoll IO多路复用模型实现机制
因为epoll的实现机制与select/poll机制彻底不一样,上面所说的 select的缺点在epoll上不复存在。
设想一下以下场景:有100万个客户端同时与一个服务器进程保持着TCP链接。而每一时刻,一般只有几百上千个TCP链接是活跃的(事实上大部分场景都是这种状况)。如何实现这样的高并发?
在select/poll时代,服务器进程每次都把这100万个链接告诉操做系统(从用户态复制句柄数据结构到内核态),让操做系统内核去查询这些套接字上是否有事件发生,轮询完后,再将句柄数据复制到用户态,让服务器应用程序轮询处理已发生的网络事件,这一过程资源消耗较大,所以,select/poll通常只能处理几千的并发链接。
epoll的设计和实现与select彻底不一样。epoll经过在Linux内核中申请一个简易的文件系统(文件系统通常用什么数据结构实现?B+树)。把原先的select/poll调用分红了3个部分:
1)调用epoll_create()创建一个epoll对象(在epoll文件系统中为这个句柄对象分配资源)
2)调用epoll_ctl向epoll对象中添加这100万个链接的套接字
3)调用epoll_wait收集发生的事件的链接
如此一来,要实现上面说是的场景,只须要在进程启动时创建一个epoll对象,而后在须要的时候向这个epoll对象中添加或者删除链接。同时,epoll_wait的效率也很是高,由于调用epoll_wait时,并无一股脑的向操做系统复制这100万个链接的句柄数据,内核也不须要去遍历所有的链接。
下面来看看Linux内核具体的epoll机制实现思路。
当某一进程调用epoll_create方法时,Linux内核会建立一个eventpoll结构体,这个结构体中有两个成员与epoll的使用方式密切相关。eventpoll结构体以下所示:
- struct eventpoll{
- ....
-
- struct rb_root rbr;
-
- struct list_head rdlist;
- ....
- };
每个epoll对象都有一个独立的eventpoll结构体,用于存放经过epoll_ctl方法向epoll对象中添加进来的事件。这些事件都会挂载在红黑树中,如此,重复添加的事件就能够经过红黑树而高效的识别出来(红黑树的插入时间效率是lgn,其中n为树的高度)。
而全部添加到epoll中的事件都会与设备(网卡)驱动程序创建回调关系,也就是说,当相应的事件发生时会调用这个回调方法。这个回调方法在内核中叫ep_poll_callback,它会将发生的事件添加到rdlist双链表中。
在epoll中,对于每个事件,都会创建一个epitem结构体,以下所示:
- struct epitem{
- struct rb_node rbn;
- struct list_head rdllink;
- struct epoll_filefd ffd;
- struct eventpoll *ep;
- struct epoll_event event;
- }
当调用epoll_wait检查是否有事件发生时,只须要检查eventpoll对象中的rdlist双链表中是否有epitem元素便可。若是rdlist不为空,则把发生的事件复制到用户态,同时将事件数量返回给用户。

epoll数据结构示意图
从上面的讲解可知:经过红黑树和双链表数据结构,并结合回调机制,造就了epoll的高效。
OK,讲解完了Epoll的机理,咱们便能很容易掌握epoll的用法了。一句话描述就是:三步曲。
第一步:epoll_create()系统调用。此调用返回一个句柄,以后全部的使用都依靠这个句柄来标识。
第二步:epoll_ctl()系统调用。经过此调用向epoll对象中添加、删除、修改感兴趣的事件,返回0标识成功,返回-1表示失败。
第三部:epoll_wait()系统调用。经过此调用收集收集在epoll监控中已经发生的事件。
最后,附上一个epoll编程实例。(做者为sparkliang)
- #include <sys/socket.h>
- #include <sys/epoll.h>
- #include <netinet/in.h>
- #include <arpa/inet.h>
- #include <fcntl.h>
- #include <unistd.h>
- #include <stdio.h>
- #include <errno.h>
- #include <iostream>
- using namespace std;
- #define MAX_EVENTS 500
- struct myevent_s
- {
- int fd;
- void (*call_back)(int fd, int events, void *arg);
- int events;
- void *arg;
- int status;
- char buff[128];
- int len, s_offset;
- long last_active;
- };
- void EventSet(myevent_s *ev, int fd, void (*call_back)(int, int, void*), void *arg)
- {
- ev->fd = fd;
- ev->call_back = call_back;
- ev->events = 0;
- ev->arg = arg;
- ev->status = 0;
- bzero(ev->buff, sizeof(ev->buff));
- ev->s_offset = 0;
- ev->len = 0;
- ev->last_active = time(NULL);
- }
- void EventAdd(int epollFd, int events, myevent_s *ev)
- {
- struct epoll_event epv = {0, {0}};
- int op;
- epv.data.ptr = ev;
- epv.events = ev->events = events;
- if(ev->status == 1){
- op = EPOLL_CTL_MOD;
- }
- else{
- op = EPOLL_CTL_ADD;
- ev->status = 1;
- }
- if(epoll_ctl(epollFd, op, ev->fd, &epv) < 0)
- printf("Event Add failed[fd=%d], evnets[%d]\n", ev->fd, events);
- else
- printf("Event Add OK[fd=%d], op=%d, evnets[%0X]\n", ev->fd, op, events);
- }
- void EventDel(int epollFd, myevent_s *ev)
- {
- struct epoll_event epv = {0, {0}};
- if(ev->status != 1) return;
- epv.data.ptr = ev;
- ev->status = 0;
- epoll_ctl(epollFd, EPOLL_CTL_DEL, ev->fd, &epv);
- }
- int g_epollFd;
- myevent_s g_Events[MAX_EVENTS+1];
- void RecvData(int fd, int events, void *arg);
- void SendData(int fd, int events, void *arg);
- void AcceptConn(int fd, int events, void *arg)
- {
- struct sockaddr_in sin;
- socklen_t len = sizeof(struct sockaddr_in);
- int nfd, i;
-
- if((nfd = accept(fd, (struct sockaddr*)&sin, &len)) == -1)
- {
- if(errno != EAGAIN && errno != EINTR)
- {
- }
- printf("%s: accept, %d", __func__, errno);
- return;
- }
- do
- {
- for(i = 0; i < MAX_EVENTS; i++)
- {
- if(g_Events[i].status == 0)
- {
- break;
- }
- }
- if(i == MAX_EVENTS)
- {
- printf("%s:max connection limit[%d].", __func__, MAX_EVENTS);
- break;
- }
-
- int iret = 0;
- if((iret = fcntl(nfd, F_SETFL, O_NONBLOCK)) < 0)
- {
- printf("%s: fcntl nonblocking failed:%d", __func__, iret);
- break;
- }
-
- EventSet(&g_Events[i], nfd, RecvData, &g_Events[i]);
- EventAdd(g_epollFd, EPOLLIN, &g_Events[i]);
- }while(0);
- printf("new conn[%s:%d][time:%d], pos[%d]\n", inet_ntoa(sin.sin_addr),
- ntohs(sin.sin_port), g_Events[i].last_active, i);
- }
- void RecvData(int fd, int events, void *arg)
- {
- struct myevent_s *ev = (struct myevent_s*)arg;
- int len;
-
- len = recv(fd, ev->buff+ev->len, sizeof(ev->buff)-1-ev->len, 0);
- EventDel(g_epollFd, ev);
- if(len > 0)
- {
- ev->len += len;
- ev->buff[len] = '\0';
- printf("C[%d]:%s\n", fd, ev->buff);
-
- EventSet(ev, fd, SendData, ev);
- EventAdd(g_epollFd, EPOLLOUT, ev);
- }
- else if(len == 0)
- {
- close(ev->fd);
- printf("[fd=%d] pos[%d], closed gracefully.\n", fd, ev-g_Events);
- }
- else
- {
- close(ev->fd);
- printf("recv[fd=%d] error[%d]:%s\n", fd, errno, strerror(errno));
- }
- }
- void SendData(int fd, int events, void *arg)
- {
- struct myevent_s *ev = (struct myevent_s*)arg;
- int len;
-
- len = send(fd, ev->buff + ev->s_offset, ev->len - ev->s_offset, 0);
- if(len > 0)
- {
- printf("send[fd=%d], [%d<->%d]%s\n", fd, len, ev->len, ev->buff);
- ev->s_offset += len;
- if(ev->s_offset == ev->len)
- {
-
- EventDel(g_epollFd, ev);
- EventSet(ev, fd, RecvData, ev);
- EventAdd(g_epollFd, EPOLLIN, ev);
- }
- }
- else
- {
- close(ev->fd);
- EventDel(g_epollFd, ev);
- printf("send[fd=%d] error[%d]\n", fd, errno);
- }
- }
- void InitListenSocket(int epollFd, short port)
- {
- int listenFd = socket(AF_INET, SOCK_STREAM, 0);
- fcntl(listenFd, F_SETFL, O_NONBLOCK);
- printf("server listen fd=%d\n", listenFd);
- EventSet(&g_Events[MAX_EVENTS], listenFd, AcceptConn, &g_Events[MAX_EVENTS]);
-
- EventAdd(epollFd, EPOLLIN, &g_Events[MAX_EVENTS]);
-
- sockaddr_in sin;
- bzero(&sin, sizeof(sin));
- sin.sin_family = AF_INET;
- sin.sin_addr.s_addr = INADDR_ANY;
- sin.sin_port = htons(port);
- bind(listenFd, (const sockaddr*)&sin, sizeof(sin));
- listen(listenFd, 5);
- }
- int main(int argc, char **argv)
- {
- unsigned short port = 12345;
- if(argc == 2){
- port = atoi(argv[1]);
- }
-
- g_epollFd = epoll_create(MAX_EVENTS);
- if(g_epollFd <= 0) printf("create epoll failed.%d\n", g_epollFd);
-
- InitListenSocket(g_epollFd, port);
-
- struct epoll_event events[MAX_EVENTS];
- printf("server running:port[%d]\n", port);
- int checkPos = 0;
- while(1){
-
- long now = time(NULL);
- for(int i = 0; i < 100; i++, checkPos++)
- {
- if(checkPos == MAX_EVENTS) checkPos = 0;
- if(g_Events[checkPos].status != 1) continue;
- long duration = now - g_Events[checkPos].last_active;
- if(duration >= 60)
- {
- close(g_Events[checkPos].fd);
- printf("[fd=%d] timeout[%d--%d].\n", g_Events[checkPos].fd, g_Events[checkPos].last_active, now);
- EventDel(g_epollFd, &g_Events[checkPos]);
- }
- }
-
- int fds = epoll_wait(g_epollFd, events, MAX_EVENTS, 1000);
- if(fds < 0){
- printf("epoll_wait error, exit\n");
- break;
- }
- for(int i = 0; i < fds; i++){
- myevent_s *ev = (struct myevent_s*)events[i].data.ptr;
- if((events[i].events&EPOLLIN)&&(ev->events&EPOLLIN))
- {
- ev->call_back(ev->fd, events[i].events, ev->arg);
- }
- if((events[i].events&EPOLLOUT)&&(ev->events&EPOLLOUT))
- {
- ev->call_back(ev->fd, events[i].events, ev->arg);
- }
- }
- }
-
- return 0;
- }
Mmap的实现原理和应用
不少文章分析了mmap的实现原理。从代码的逻辑来分析,老是觉没有把mmap后读写映射区域和普通的read/write联系起来。不得不产生疑问:
1,普通的read/write和mmap后的映射区域的读写到底有什么区别。
2, 为何有时候会选择mmap而放弃普通的read/write。
3,若是文章中的内容有不对是或者是不妥的地方,欢迎你们指正。
围绕着这两个问题分析一下,其实在考虑这些问题的同时难免和其余的不少系统机制产生交互。虽然是讲解mmap,可是不少知识仍是为了阐明问题作必要的铺垫。这些知识也正是linux的繁琐所在。一个应用每每和系统中的多种机制交互。这篇文章中尽可能减小对源代码的引用和分析。把这样的工做留到之后的细节分析中。可是不少分析的理论依据仍是来自于源代码。可见源代码的重要地位。
基础知识:
1, 进程每次切换后,都会在tlb base寄存器中从新load属于每个进程本身的地址转换基地址。在cpu当前运行的进程中都会有current宏来表示当前的进程的信息。应为这个代码实现涉及到硬件架构的问题,为了不差别的存在在文章中用到硬件知识的时候仍是指明是x86的架构,毕竟x86的资料和分析的研究人员也比较多。其实arm还有其余相似的RISC的芯片,只要有mmu支持的,都会有相似的基地址寄存器。
2, 在系统运行进程以前都会为每个进程分配属于它本身的运行空间。而且这个空间的有效性依赖于tlb base中的内容。32位的系统中访问的空间大小为4G。在这个空间中进程是“自由”的。所谓“自由”不是说对于4G的任何一个地址或者一段空间均可以访问。若是要访问,仍是要遵循地址有效性,就是tlb base中所指向的任何页表转换后的物理地址。其中的有效性有越界,权限等等检查。
3, 任何一个用户进程的运行在系统分配的空间中。这个空间能够有
vma:struct vm_area_struct来表示。全部的运行空间能够有这个结构体描述。用户进程能够分为text data 段。这些段的具体在4G中的位置有不一样的vma来描述。Vma的管理又有其余机制保证,这些机制涉及到了算法和物理内存管理等。请看一下两个图片:
图 一:

图 二:

系统调用中的write和read:
这里没有指定确切的文件系统类型做为分析的对象。找到系统调用号,而后肯定具体的文件系统所带的file operation。在特定的file operation中有属于每一种文件系统本身的操做函数集合。其中就有read和write。
图 三:

在真正的把用户数据读写到磁盘或者是存储设备前,内核还会在page cache中管理这些数据。这些page的存在有效的管理了用户数据和读写的效率。用户数据不是直接来自于应用层,读(read)或者是写入(write)磁盘和存储介质,而是被一层一层的应用所划分,在每一层次中都会有不一样的功能对应。最后发生交互时,在最恰当的时机触发磁盘的操做。经过IO驱动写入磁盘和存储介质。这里主要强调page cache的管理。应为page的管理设计到了缓存,这些缓存以page的单位管理。在没有IO操做以前,暂时存放在系统空间中,而并未直接写入磁盘或者存贮介质。
系统调用中的mmap:
当建立一个或者切换一个进程的同时,会把属于这个当前进程的系统信息载入。这些系统信息中包含了当前进程的运行空间。当用户程序调用mmap后。函数会在当前进程的空间中找到适合的vma来描述本身将要映射的区域。这个区域的做用就是将mmap函数中文件描述符所指向的具体文件中内容映射过来。
原理是:mmap的执行,仅仅是在内核中创建了文件与虚拟内存空间的对应关系。用户访问这些虚拟内存空间时,页面表里面是没有这些空间的表项的。当用户程序试图访问这些映射的空间时,因而产生缺页异常。内核捕捉这些异常,逐渐将文件载入。所谓的载入过程,具体的操做就是read和write在管理pagecache。Vma的结构体中有很文件操做集。vma操做集中会有本身关于page cache的操做集合。这样,虽然是两种不一样的系统调用,因为操做和调用触发的路径不一样。可是最后仍是落实到了page cache的管理。实现了文件内容的操做。
Ps:
文件的page cache管理也是很好的内容。涉及到了address space的操做。其中不少的内容和文件操做相关。
效率对比:
这里应用了网上一篇文章。发现较好的分析,着这里引用一下。
Mmap:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <sys/mman.h>
void main()
{
int fd = open("test.file", 0);
struct stat statbuf;
char *start;
char buf[2] = {0};
int ret = 0;
fstat(fd, &statbuf);
start = mmap(NULL, statbuf.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
do {
*buf = start[ret++];
}while(ret < statbuf.st_size);
}
Read:
#include <stdio.h>
#include <stdlib.h>
void main()
{
FILE *pf = fopen("test.file", "r");
char buf[2] = {0};
int ret = 0;
do {
ret = fread(buf, 1, 1, pf);
}while(ret);
}
运行结果:
[xiangy@compiling-server test_read]$ time ./fread
real 0m0.901s
user 0m0.892s
sys 0m0.010s
[xiangy@compiling-server test_read]$ time ./mmap
real 0m0.112s
user 0m0.106s
sys 0m0.006s
[xiangy@compiling-server test_read]$ time ./read
real 0m15.549s
user 0m3.933s
sys 0m11.566s
[xiangy@compiling-server test_read]$ ll test.file
-rw-r--r-- 1 xiangy svx8004 23955531 Sep 24 17:17 test.file
能够看出使用mmap后发现,系统调用所消耗的时间远远比普通的read少不少。
共享内存:mmap函数实现
内存映射的应用:
- 以页面为单位,将一个普通文件映射到内存中,一般在须要对文件进行频繁读写时使用,这样用内存读写取代I/O读写,以得到较高的性能;
- 将特殊文件进行匿名内存映射,能够为关联进程提供共享内存空间;
- 为无关联的进程提供共享内存空间,通常也是将一个普通文件映射到内存中。
相关API
#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset); void *mmap64(void *addr, size_t length, int prot, int flags, int fd, off64_t offset); int munmap(void *addr, size_t length); int msync(void *addr, size_t length, int flags);
mmap函数说明:
-
参数 addr 指明文件描述字fd指定的文件在进程地址空间内的映射区的开始地址,必须是页面对齐的地址,一般设为 NULL,让内核去选择开始地址。任何状况下,mmap 的返回值为内存映射区的开始地址。
-
参数 length 指明文件须要被映射的字节长度。off 指明文件的偏移量。一般 off 设为 0 。
- 若是 len 不是页面的倍数,它将被扩大为页面的倍数。扩充的部分一般被系统置为 0 ,并且对其修改并不影响到文件。
- off 一样必须是页面的倍数。经过 sysconf(_SC_PAGE_SIZE) 能够得到页面的大小。
-
参数 prot 指明映射区的保护权限。一般有如下 4 种。一般是 PROT_READ | PROT_WRITE 。
- PROT_READ 可读
- PROT_WRITE 可写
- PROT_EXEC 可执行
- PROT_NONE 不能被访问
-
参数 flag 指明映射区的属性。取值有如下几种。MAP_PRIVATE 与 MAP_SHARED 必选其一,MAP_FIXED 为可选项。
- MAP_PRIVATE 指明对映射区数据的修改不会影响到真正的文件。
- MAP_SHARED 指明对映射区数据的修改,多个共享该映射区的进程均可以看见,并且会反映到实际的文件。
- MAP_FIXED 要求 mmap 的返回值必须等于 addr 。若是不指定 MAP_FIXED 而且 addr 不为 NULL ,则对 addr 的处理取决于具体实现。考虑到可移植性,addr 一般设为 NULL ,不指定 MAP_FIXED。
-
当 mmap 成功返回时,fd 就能够关闭,这并不影响建立的映射区。
munmap函数说明:
进程退出的时候,映射区会自动删除。不过当再也不须要映射区时,能够调用 munmap 显式删除。当映射区删除后,后续对映射区的引用会生成 SIGSEGV 信号。
msync函数说明:
文件一旦被映射后,调用mmap()的进程对返回地址的访问是对某一内存区域的访问,暂时脱离了磁盘上文件的影响。全部对mmap()返回地址空间的操做只在内存中有意义,只有在调用了munmap()后或者msync()时,才把内存中的相应内容写回磁盘文件。
代码实例:
两个进程经过映射普通文件实现共享内存通讯
map_normalfile1.c及map_normalfile2.c。编译两个程序,可执行文件分别为map_normalfile1及map_normalfile2。两个程序经过命令行参数指定同一个文件来实现共享内存方式的进程间通讯。map_normalfile2试图打开命令行参数指定的一个普通文件,把该文件映射到进程的地址空间,并对映射后的地址空间进行写操做。map_normalfile1把命令行参数指定的文件映射到进程地址空间,而后对映射后的地址空间执行读操做。这样,两个进程经过命令行参数指定同一个文件来实现共享内存方式的进程间通讯。
map_normalfile1首先打开或建立一个文件,并把文件的长度设置为5个people结构大小.mmap映射10个people结构大小的内存,利用返回的地址开始设置15个people结构。而后睡眠10S,等待其余进程映射同一个文件,而后解除映射。
经过实验,在map_normalfile1输出initialize over 以后,输出umap ok以前,运行map_normalfile2 file,能够输出设置好的15个people结构
在map_normalfile1 输出umap ok后,运行map_normalfile2则输出结构,前5个people是已设置的,后10结构为0。
1) 最终被映射文件的内容的长度不会超过文件自己的初始大小,即映射不能改变文件的大小.
2) 能够用于进程通讯的有效地址空间大小大致上受限于被映射文件的大小,但不彻底受限于文件大小.打开文件的大小为5个people结构,映射长度为10个people结构长度,共享内存通讯用15个people结构大小。
在linux中,内存的保护是以页为基本单位的,即便被映射文件只有一个字节大小,内核也会为映射分配一个页面大小的内存。当被映射文件小于一个页面大小时,进程能够对从mmap()返回地址开始的一个页面大小进行访问,而不会出错;可是,若是对一个页面之外的地址空间进行访问,则致使错误发生,后面将进一步描述。所以,可用于进程间通讯的有效地址空间大小不会超过文件大小及一个页面大小的和。
3) 文件一旦被映射后,调用mmap()的进程对返回地址的访问是对某一内存区域的访问,暂时脱离了磁盘上文件的影响。全部对mmap()返回地址空间的操做只在内存中有意义,只有在调用了munmap()后或者msync()时,才把内存中的相应内容写回磁盘文件,所写内容仍然不能超过文件的大小
技巧:
生成固定大小的文件的两种方式:
父子进程经过匿名映射实现共享内存
- 匿名内存映射 与 使用 /dev/zero 类型,都不须要真实的文件。要使用匿名映射之须要向 mmap 传入 MAP_ANON 标志,而且 fd 参数 置为 -1 。
- 所谓匿名,指的是映射区并无经过 fd 与 文件路径名相关联。匿名内存映射用在有血缘关系的进程间。
#include <sys/mman.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
typedef struct{ char name[4]; int age; }people; main(int argc, char** argv) { int i; people *p_map; char temp; p_map=(people*)mmap(NULL,sizeof(people)*10,PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS,-1,0); if(fork() == 0) { sleep(2); for(i = 0;i<5;i++) printf("child read: the %d people's age is %d\n",i+1,(*(p_map+i)).age); (*p_map).age = 100; munmap(p_map,sizeof(people)*10);
参考:
驱动总结之mmap函数实现
mmap做为struct file_operations的重要一个元素,mmap主要是实现物理内存到虚拟内存的映射关系,这样能够实现直接访问虚拟内存,而不用使用设备相关的read、write操做,mmap的基本过程是将文件映射到虚拟内存中。在以前的一篇博客中谈到了mmap实现文件复制的操做。
关于linux中的mmap调用以下,最好的办法查看命令,man mmap:
必要的头文件
#include<sys/mman.h>
函数声明
void * mmap(void *addr,size_t length,int prot,
int flags,int fd,off_t offset);
关于各个参数的意义以下:
一、返回值是一个通用型指针,这样就保证了各类类型的申请方式。
二、void *addr 是程序员所但愿的虚拟地址做为起始映射地址,一般为NULL,内核自动分配。
三、size_t length固然是指须要映射的区域大小。
四、int flags是指对这段区域的保护方式。具体的能够参看内核源码的linux/mm.h。经常使用的是PROT_EXEC,PROT_READ,PROT_WRITE,PROT_NONE。
五、int flags主要是指对这段区域的映射方式,主要分为两种方式MAP_SHARE,MAP_PRIVATE.其中的MAP_SHARE是指对映射区域的写操做会更新到文件中,这样就至关于直接操做文件。而MAP_PRIVATE一般采用一种称为"写时保护的机制"实现映射,对映射区的写操做不会更新到文件中,实现方法是将须要被写操做的页复制到从新分配的新页中,而后再对新的页进行写操做。原来的映射并无改变,可是读操做并不会从新分配物理内存空间。具体的参考深刻理解计算机系统。
六、int fd是指将被映射的文件描述符,映射须要保证文件描述符的正确性。
七、off_t offset是指从文件的具体位置开始映射,一般状况下能够设置为0,即从开头映射。
基本的映射关系以下图:
设备驱动的mmap实现主要是将一个物理设备的可操做区域(设备空间)映射到一个进程的虚拟地址空间。这样就能够直接采用指针的方式像访问内存的方式访问设备。在驱动中的mmap实现主要是完成一件事,就是实际物理设备的操做区域到进程虚拟空间地址的映射过程。同时也须要保证这段映射的虚拟存储器区域不会被进程当作通常的空间使用,所以须要添加一系列的保护方式。
具体的实现过程以下:
- /*主要是创建虚拟地址到物理地址的页表关系,其余的过程又内核本身完成*/
- static int mem_mmap(struct file* filp,struct vm_area_struct *vma)
- {
- /*间接的控制设备*/
- struct mem_dev *dev = filp->private_data;
-
- /*标记这段虚拟内存映射为IO区域,并阻止系统将该区域包含在进程的存放转存中*/
- vma->vm_flags |= VM_IO;
- /*标记这段区域不能被换出*/
- vma->vm_flags |= VM_RESERVED;
- /**/
- if(remap_pfn_range(vma,/*虚拟内存区域*/
- vma->vm_start, /*虚拟地址的起始地址*/
- virt_to_phys(dev->data)>>PAGE_SHIFT, /*物理存储区的物理页号*/
- dev->size, /*映射区域大小*/
- vma->vm_page_prot /*虚拟区域保护属性*/
- ))
- return -EAGAIN;
- return 0;
- }
具体的实现分析以下:
- vma->vm_flags |= VM_IO;
- vma->vm_flags |= VM_RESERVED;
上面的两个保护机制就说明了被映射的这段区域具备映射IO的类似性,同时保证这段区域不能随便的换出。就是创建一个物理页与虚拟页之间的关联性。具体原理是虚拟页和物理页之间是以页表的方式关联起来,虚拟内存一般大于物理内存,在使用过程当中虚拟页经过页表关联一切对应的物理页,当物理页不够时,会选择性的牺牲一些页,也就是将物理页与虚拟页之间切断,重现关联其余的虚拟页,保证物理内存够用。在设备驱动中应该具体的虚拟页和物理页之间的关系应该是长期的,应该保护起来,不能随便被别的虚拟页所替换。具体也可参看关于虚拟存储器的文章。
接下来就是创建物理页与虚拟页之间的关系,即采用函数remap_pfn_range(),具体的参数以下:
int remap_pfn_range(structvm_area_struct *vma, unsigned long addr,unsigned long pfn, unsigned long size, pgprot_t prot)
一、struct vm_area_struct是一个虚拟内存区域结构体,表示虚拟存储器中的一个内存区域。其中的元素vm_start是指虚拟存储器中的起始地址。
二、addr也就是虚拟存储器中的起始地址,一般能够选择addr = vma->vm_start。
三、pfn是指物理存储器的具体页号,一般经过物理地址获得对应的物理页号,具体采用virt_to_phys(dev->data)>>PAGE_SHIFT.首先将虚拟内存转换到物理内存,而后获得页号。>>PAGE_SHIFT一般为12,这是由于每一页的大小恰好是4K,这样右移12至关于除以4096,获得页号。
四、size区域大小
五、区域保护机制。
返回值,若是成功返回0,不然正数。
测试代码能够直接经过对虚拟内存区域操做,实现不一样的操做,以下:
- #include<fcntl.h>
- #include<unistd.h>
- #include<stdio.h>
- #include<stdlib.h>
- #include<sys/types.h>
- #include<sys/stat.h>
- #include<sys/mman.h>
- #include<string.h>
- int main()
- {
- int fd;
- char *start;
-
- char buf[2048];
- strcpy(buf,"This is a test!!!!");
- fd = open("/dev/memdev0",O_RDWR);
-
- if(fd == -1)
- {
- printf("Error!!\n");
- exit(-1);
- }
- /*建立映射*/
- start = mmap(NULL,2048,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
- /*必须检测是否成功*/
- if(start == -1)
- {
- printf("mmap error!!!\n");
- exit(-1);
- }
- strcpy(start,buf);
- printf("start = %s,buf = %s\n",start,buf);
- strcpy(start,"Test is Test!!!\n");
- printf("start = %s,buf = %s\n",start,buf);
- /**/
- strcpy(buf,start);
-
- printf("start = %s,buf=%s\n",start,buf);
- /*取消映射关系*/
- munmap(start,2048);
- /*关闭文件*/
- close(fd);
- exit(0);
- }
通过测试,成功获得了驱动。
Linux 内存映射函数 mmap()函数详解
mmap将一个文件或者其它对象映射进内存。文件被映射到多个页上,若是文件的大小不是全部页的大小之和,最后一个页不被使用的空间将会清零。mmap在用户空间映射调用系统中做用很大。
头文件 <sys/mman.h>
函数原型
void* mmap(void* start,size_t length,int prot,int flags,int fd,off_t offset);
int munmap(void* start,size_t length);
mmap()[1] 必须以PAGE_SIZE为单位进行映射,而内存也只能以页为单位进行映射,若要映射非PAGE_SIZE整数倍的地址范围,要先进行内存对齐,强行以PAGE_SIZE的倍数大小进行映射。
用法:
下面说一下内存映射的步骤:
用open系统调用打开文件, 并返回描述符fd.
用mmap创建内存映射, 并返回映射首地址指针start.
对映射(文件)进行各类操做, 显示(printf), 修改(sprintf).
用munmap(void *start, size_t lenght)关闭内存映射.
用close系统调用关闭文件fd.
UNIX网络编程第二卷进程间通讯对mmap函数进行了说明。该函数主要用途有三个:
一、将一个普通文件映射到内存中,一般在须要对文件进行频繁读写时使用,这样用内存读写取代I/O读写,以得到较高的性能;
二、将特殊文件进行匿名内存映射,能够为关联进程提供共享内存空间;
三、为无关联的进程提供共享内存空间,通常也是将一个普通文件映射到内存中。
函数:void *mmap(void *start,size_t length,int prot,int flags,int fd,off_t offsize);
参数start:指向欲映射的内存起始地址,一般设为 NULL,表明让系统自动选定地址,映射成功后返回该地址。
参数length:表明将文件中多大的部分映射到内存。
参数prot:映射区域的保护方式。能够为如下几种方式的组合:
PROT_EXEC 映射区域可被执行
PROT_READ 映射区域可被读取
PROT_WRITE 映射区域可被写入
PROT_NONE 映射区域不能存取
参数flags:影响映射区域的各类特性。在调用mmap()时必需要指定MAP_SHARED 或MAP_PRIVATE。
MAP_FIXED 若是参数start所指的地址没法成功创建映射时,则放弃映射,不对地址作修正。一般不鼓励用此旗标。
MAP_SHARED对映射区域的写入数据会复制回文件内,并且容许其余映射该文件的进程共享。
MAP_PRIVATE 对映射区域的写入操做会产生一个映射文件的复制,即私人的“写入时复制”(copy on write)对此区域做的任何修改都不会写回原来的文件内容。
MAP_ANONYMOUS创建匿名映射。此时会忽略参数fd,不涉及文件,并且映射区域没法和其余进程共享。
MAP_DENYWRITE只容许对映射区域的写入操做,其余对文件直接写入的操做将会被拒绝。
MAP_LOCKED 将映射区域锁定住,这表示该区域不会被置换(swap)。
参数fd:要映射到内存中的文件描述符。若是使用匿名内存映射时,即flags中设置了MAP_ANONYMOUS,fd设为-1。有些系统不支持匿名内存映射,则能够使用fopen打开/dev/zero文件,而后对该文件进行映射,能够一样达到匿名内存映射的效果。
参数offset:文件映射的偏移量,一般设置为0,表明从文件最前方开始对应,offset必须是分页大小的整数倍。
返回值:
若映射成功则返回映射区的内存起始地址,不然返回MAP_FAILED(-1),错误缘由存于errno 中。
错误代码:
EBADF 参数fd 不是有效的文件描述词
EACCES 存取权限有误。若是是MAP_PRIVATE 状况下文件必须可读,使用MAP_SHARED则要有PROT_WRITE以及该文件要能写入。
EINVAL 参数start、length 或offset有一个不合法。
EAGAIN 文件被锁住,或是有太多内存被锁住。
ENOMEM 内存不足。
系统调用mmap()用于共享内存的两种方式:
(1)使用普通文件提供的内存映射:
适用于任何进程之间。此时,须要打开或建立一个文件,而后再调用mmap()
典型调用代码以下:
fd=open(name, flag, mode); if(fd<0) ...
ptr=mmap(NULL, len , PROT_READ|PROT_WRITE, MAP_SHARED , fd , 0);
经过mmap()实现共享内存的通讯方式有许多特色和要注意的地方,能够参看UNIX网络编程第二卷。
(2)使用特殊文件提供匿名内存映射:
适用于具备亲缘关系的进程之间。因为父子进程特殊的亲缘关系,在父进程中先调用mmap(),而后调用 fork()。那么在调用fork()以后,子进程继承父进程匿名映射后的地址空间,一样也继承mmap()返回的地址,这样,父子进程就能够经过映射区 域进行通讯了。注意,这里不是通常的继承关系。通常来讲,子进程单独维护从父进程继承下来的一些变量。而mmap()返回的地址,却由父子进程共同维护。 对于具备亲缘关系的进程实现共享内存最好的方式应该是采用匿名内存映射的方式。此时,没必要指定具体的文件,只要设置相应的标志便可。
1、概述
内存映射,简而言之就是将用户空间的一段内存区域映射到内核空间,映射成功后,用户对这段内存区域的修改能够直接反映到内核空间,一样,内核空间对这段区域的修改也直接反映用户空间。那么对于内核空间<---->用户空间二者之间须要大量数据传输等操做的话效率是很是高的。
如下是一个把广泛文件映射到用户空间的内存区域的示意图。
图一:

2、基本函数
mmap函数是unix/linux下的系统调用,详细内容可参考《Unix Netword programming》卷二12.2节。
mmap系统调用并非彻底为了用于共享内存而设计的。它自己提供了不一样于通常对普通文件的访问方式,进程能够像读写内存同样对普通文件的操做。而Posix或系统V的共享内存IPC则纯粹用于共享目的,固然mmap()实现共享内存也是其主要应用之一。
mmap系统调用使得进程之间经过映射同一个普通文件实现共享内存。普通文件被映射到进程地址空间后,进程能够像访问普通内存同样对文件进行访问,没必要再调用read(),write()等操做。mmap并不分配空间, 只是将文件映射到调用进程的地址空间里(可是会占掉你的 virutal memory), 而后你就能够用memcpy等操做写文件, 而不用write()了.写完后,内存中的内容并不会当即更新到文件中,而是有一段时间的延迟,你能够调用msync()来显式同步一下, 这样你所写的内容就能当即保存到文件里了.这点应该和驱动相关。 不过经过mmap来写文件这种方式没办法增长文件的长度, 由于要映射的长度在调用mmap()的时候就决定了.若是想取消内存映射,能够调用munmap()来取消内存映射
- void * mmap(void *start, size_t length, int prot , int flags, int fd, off_t offset)
mmap用于把文件映射到内存空间中,简单说mmap就是把一个文件的内容在内存里面作一个映像。映射成功后,用户对这段内存区域的修改能够直接反映到内核空间,一样,内核空间对这段区域的修改也直接反映用户空间。那么对于内核空间<---->用户空间二者之间须要大量数据传输等操做的话效率是很是高的。
start:要映射到的内存区域的起始地址,一般都是用NULL(NULL即为0)。NULL表示由内核来指定该内存地址
length:要映射的内存区域的大小
prot:指望的内存保护标志,不能与文件的打开模式冲突。是如下的某个值,能够经过or运算合理地组合在一块儿
PROT_EXEC //页内容能够被执行
PROT_READ //页内容能够被读取
PROT_WRITE //页能够被写入
PROT_NONE //页不可访问
flags:指定映射对象的类型,映射选项和映射页是否能够共享。它的值能够是一个或者多个如下位的组合体
MAP_FIXED :使用指定的映射起始地址,若是由start和len参数指定的内存区重叠于现存的映射空间,重叠部分将会被丢弃。若是指定的起始地址不可用,操做将会失败。而且起始地址必须落在页的边界上。
MAP_SHARED :对映射区域的写入数据会复制回文件内, 并且容许其余映射该文件的进程共享。
MAP_PRIVATE :创建一个写入时拷贝的私有映射。内存区域的写入不会影响到原文件。这个标志和以上标志是互斥的,只能使用其中一个。
MAP_DENYWRITE :这个标志被忽略。
MAP_EXECUTABLE :同上
MAP_NORESERVE :不要为这个映射保留交换空间。当交换空间被保留,对映射区修改的可能会获得保证。当交换空间不被保留,同时内存不足,对映射区的修改会引发段违例信号。
MAP_LOCKED :锁定映射区的页面,从而防止页面被交换出内存。
MAP_GROWSDOWN :用于堆栈,告诉内核VM系统,映射区能够向下扩展。
MAP_ANONYMOUS :匿名映射,映射区不与任何文件关联。
MAP_ANON :MAP_ANONYMOUS的别称,再也不被使用。
MAP_FILE :兼容标志,被忽略。
MAP_32BIT :将映射区放在进程地址空间的低2GB,MAP_FIXED指定时会被忽略。当前这个标志只在x86-64平台上获得支持。
MAP_POPULATE :为文件映射经过预读的方式准备好页表。随后对映射区的访问不会被页违例阻塞。
MAP_NONBLOCK :仅和MAP_POPULATE一块儿使用时才有意义。不执行预读,只为已存在于内存中的页面创建页表入口。
fd:文件描述符(由open函数返回)
offset:表示被映射对象(即文件)从那里开始对映,一般都是用0。 该值应该为大小为PAGE_SIZE的整数倍
返回说明
成功执行时,mmap()返回被映射区的指针,munmap()返回0。失败时,mmap()返回MAP_FAILED[其值为(void *)-1],munmap返回-1。errno被设为如下的某个值
EACCES:访问出错
EAGAIN:文件已被锁定,或者太多的内存已被锁定
EBADF:fd不是有效的文件描述词
EINVAL:一个或者多个参数无效
ENFILE:已达到系统对打开文件的限制
ENODEV:指定文件所在的文件系统不支持内存映射
ENOMEM:内存不足,或者进程已超出最大内存映射数量
EPERM:权能不足,操做不容许
ETXTBSY:已写的方式打开文件,同时指定MAP_DENYWRITE标志
SIGSEGV:试着向只读区写入
SIGBUS:试着访问不属于进程的内存区
- int munmap(void *start, size_t length)
start:要取消映射的内存区域的起始地址
length:要取消映射的内存区域的大小。
返回说明
成功执行时munmap()返回0。失败时munmap返回-1.
int msync(const void *start, size_t length, int flags);
对映射内存的内容的更改并不会当即更新到文件中,而是有一段时间的延迟,你能够调用msync()来显式同步一下, 这样你内存的更新就能当即保存到文件里
start:要进行同步的映射的内存区域的起始地址。
length:要同步的内存区域的大小
flag:flags能够为如下三个值之一:
MS_ASYNC : 请Kernel快将资料写入。
MS_SYNC : 在msync结束返回前,将资料写入。
MS_INVALIDATE : 让核心自行决定是否写入,仅在特殊情况下使用
3、用户空间和驱动程序的内存映射
3.一、基本过程
首先,驱动程序先分配好一段内存,接着用户进程经过库函数mmap()来告诉内核要将多大的内存映射到内核空间,内核通过一系列函数调用后调用对应的驱动程序的file_operation中指定的mmap函数,在该函数中调用remap_pfn_range()来创建映射关系。
3.二、映射的实现
首先在驱动程序分配一页大小的内存,而后用户进程经过mmap()将用户空间中大小也为一页的内存映射到内核空间这页内存上。映射完成后,驱动程序往这段内存写10个字节数据,用户进程将这些数据显示出来。
驱动程序:
- #include <linux/miscdevice.h>
- #include <linux/delay.h>
- #include <linux/kernel.h>
- #include <linux/module.h>
- #include <linux/init.h>
- #include <linux/mm.h>
- #include <linux/fs.h>
- #include <linux/types.h>
- #include <linux/delay.h>
- #include <linux/moduleparam.h>
- #include <linux/slab.h>
- #include <linux/errno.h>
- #include <linux/ioctl.h>
- #include <linux/cdev.h>
- #include <linux/string.h>
- #include <linux/list.h>
- #include <linux/pci.h>
- #include <linux/gpio.h>
-
-
- #define DEVICE_NAME "mymap"
-
-
- static unsigned char array[10]={0,1,2,3,4,5,6,7,8,9};
- static unsigned char *buffer;
-
-
- static int my_open(struct inode *inode, struct file *file)
- {
- return 0;
- }
-
-
- static int my_map(struct file *filp, struct vm_area_struct *vma)
- {
- unsigned long page;
- unsigned char i;
- unsigned long start = (unsigned long)vma->vm_start;
- unsigned long size = (unsigned long)(vma->vm_end - vma->vm_start);
-
- page = virt_to_phys(buffer);
- if(remap_pfn_range(vma,start,page>>PAGE_SHIFT,size,PAGE_SHARED))
- return -1;
-
- for(i=0;i<10;i++)
- buffer[i] = array[i];
-
- return 0;
- }
-
-
- static struct file_operations dev_fops = {
- .owner = THIS_MODULE,
- .open = my_open,
- .mmap = my_map,
- };
-
- static struct miscdevice misc = {
- .minor = MISC_DYNAMIC_MINOR,
- .name = DEVICE_NAME,
- .fops = &dev_fops,
- };
-
-
- static int __init dev_init(void)
- {
- int ret;
-
- ret = misc_register(&misc);
- buffer = (unsigned char *)kmalloc(PAGE_SIZE,GFP_KERNEL);
- SetPageReserved(virt_to_page(buffer));
-
- return ret;
- }
-
-
- static void __exit dev_exit(void)
- {
- misc_deregister(&misc);
- ClearPageReserved(virt_to_page(buffer));
- kfree(buffer);
- }
-
-
- module_init(dev_init);
- module_exit(dev_exit);
- MODULE_LICENSE("GPL");
- MODULE_AUTHOR("LKN@SCUT");
应用程序:
- #include <unistd.h>
- #include <stdio.h>
- #include <stdlib.h>
- #include <string.h>
- #include <fcntl.h>
- #include <linux/fb.h>
- #include <sys/mman.h>
- #include <sys/ioctl.h>
-
- #define PAGE_SIZE 4096
-
-
- int main(int argc , char *argv[])
- {
- int fd;
- int i;
- unsigned char *p_map;
-
- fd = open("/dev/mymap",O_RDWR);
- if(fd < 0)
- {
- printf("open fail\n");
- exit(1);
- }
-
- p_map = (unsigned char *)mmap(0, PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED,fd, 0);
- if(p_map == MAP_FAILED)
- {
- printf("mmap fail\n");
- goto here;
- }
-
- for(i=0;i<10;i++)
- printf("%d\n",p_map[i]);
-
-
- here:
- munmap(p_map, PAGE_SIZE);
- return 0;
- }
先加载驱动后执行应用程序,用户空间打印以下:

linux内存映射mmap原理分析
一直都对内存映射文件这个概念很模糊,不知道它和虚拟内存有什么区别,并且映射这个词也很让人迷茫,今天终于搞清楚了。。。下面,我先解释一下我对映射这个词的理解,再区分一下几个容易混淆的概念,以后,什么是内存映射就很明朗了。
原理
首先,“映射”这个词,就和数学课上说的“一一映射”是一个意思,就是创建一种一一对应关系,在这里主要是只 硬盘上文件 的位置与进程 逻辑地址空间 中一块大小相同的区域之间的一一对应,如图1中过程1所示。这种对应关系纯属是逻辑上的概念,物理上是不存在的,缘由是进程的逻辑地址空间自己就是不存在的。在内存映射的过程当中,并无实际的数据拷贝,文件没有被载入内存,只是逻辑上被放入了内存,具体到代码,就是创建并初始化了相关的数据结构(struct address_space),这个过程有系统调用mmap()实现,因此创建内存映射的效率很高。

图1.内存映射原理
既然创建内存映射没有进行实际的数据拷贝,那么进程又怎么能最终直接经过内存操做访问到硬盘上的文件呢?那就要看内存映射以后的几个相关的过程了。
mmap()会返回一个指针ptr,它指向进程逻辑地址空间中的一个地址,这样之后,进程无需再调用read或write对文件进行读写,而只须要经过ptr就可以操做文件。可是ptr所指向的是一个逻辑地址,要操做其中的数据,必须经过MMU将逻辑地址转换成物理地址,如图1中过程2所示。这个过程与内存映射无关。
前面讲过,创建内存映射并无实际拷贝数据,这时,MMU在地址映射表中是没法找到与ptr相对应的物理地址的,也就是MMU失败,将产生一个缺页中断,缺页中断的中断响应函数会在swap中寻找相对应的页面,若是找不到(也就是该文件历来没有被读入内存的状况),则会经过mmap()创建的映射关系,从硬盘上将文件读取到物理内存中,如图1中过程3所示。这个过程与内存映射无关。
若是在拷贝数据时,发现物理内存不够用,则会经过虚拟内存机制(swap)将暂时不用的物理页面交换到硬盘上,如图1中过程4所示。这个过程也与内存映射无关。
效率
从代码层面上看,从硬盘上将文件读入内存,都要通过文件系统进行数据拷贝,而且数据拷贝操做是由文件系统和硬件驱动实现的,理论上来讲,拷贝数据的效率是同样的。可是经过内存映射的方法访问硬盘上的文件,效率要比read和write系统调用高,这是为何呢?缘由是read()是系统调用,其中进行了数据拷贝,它首先将文件内容从硬盘拷贝到内核空间的一个缓冲区,如图2中过程1,而后再将这些数据拷贝到用户空间,如图2中过程2,在这个过程当中,实际上完成了 两次数据拷贝 ;而mmap()也是系统调用,如前所述,mmap()中没有进行数据拷贝,真正的数据拷贝是在缺页中断处理时进行的,因为mmap()将文件直接映射到用户空间,因此中断处理函数根据这个映射关系,直接将文件从硬盘拷贝到用户空间,只进行了 一次数据拷贝 。所以,内存映射的效率要比read/write效率高。

图2.read系统调用原理
下面这个程序,经过read和mmap两种方法分别对硬盘上一个名为“mmap_test”的文件进行操做,文件中存有10000个整数,程序两次使用不一样的方法将它们读出,加1,再写回硬盘。经过对比能够看出,read消耗的时间将近是mmap的两到三倍。
- #include<unistd.h>
- #include<stdio.h>
- #include<stdlib.h>
- #include<string.h>
- #include<sys/types.h>
- #include<sys/stat.h>
- #include<sys/time.h>
- #include<fcntl.h>
- #include<sys/mman.h>
-
- #define MAX 10000
-
- int main()
- {
- int i=0;
- int count=0, fd=0;
- struct timeval tv1, tv2;
- int *array = (int *)malloc( sizeof(int)*MAX );
-
-
- gettimeofday( &tv1, NULL );
- fd = open( "mmap_test", O_RDWR );
- if( sizeof(int)*MAX != read( fd, (void *)array, sizeof(int)*MAX ) )
- {
- printf( "Reading data failed.../n" );
- return -1;
- }
- for( i=0; i<MAX; ++i )
-
- ++array[ i ];
- if( sizeof(int)*MAX != write( fd, (void *)array, sizeof(int)*MAX ) )
- {
- printf( "Writing data failed.../n" );
- return -1;
- }
- free( array );
- close( fd );
- gettimeofday( &tv2, NULL );
- printf( "Time of read/write: %dms/n", tv2.tv_usec-tv1.tv_usec );
-
-
- gettimeofday( &tv1, NULL );
- fd = open( "mmap_test", O_RDWR );
- array = mmap( NULL, sizeof(int)*MAX, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0 );
- for( i=0; i<MAX; ++i )
-
- ++array[ i ];
- munmap( array, sizeof(int)*MAX );
- msync( array, sizeof(int)*MAX, MS_SYNC );
- free( array );
- close( fd );
- gettimeofday( &tv2, NULL );
- printf( "Time of mmap: %dms/n", tv2.tv_usec-tv1.tv_usec );
-
- return 0;
- }
输出结果:
Time of read/write: 154ms
Time of mmap: 68ms
Linux的mmap内存映射机制解析
在讲述文件映射的概念时,不可避免的要牵涉到虚存(SVR 4的VM).实际上,文件映射是虚存的中心概念, 文件映射一方面给用户提供了一组措施,好似用户将文件映射到本身地址空间的某个部分,使用简单的内存访问指令读写文件;另外一方面,它也能够用于内核的基本组织模式,在这种模式种,内核将整个地址空间视为诸如文件之类的一组不一样对象的映射.中的传统文件访问方式是,首先用open系统调用打开文件,而后使用read, write以及lseek等调用进行顺序或者随即的I/O.这种方式是很是低效的,每一次I/O操做都须要一次系统调用.另外,若是若干个进程访问同一个文件,每一个进程都要在本身的地址空间维护一个副本,浪费了内存空间.而若是可以经过必定的机制将页面映射到进程的地址空间中,也就是说首先经过简单的产生某些内存管理数据结构完成映射的建立.当进程访问页面时产生一个缺页中断,内核将页面读入内存而且更新页表指向该页面.并且这种方式很是方便于同一副本的共享.
VM是面向对象的方法设计的,这里的对象是指内存对象:内存对象是一个软件抽象的概念,它描述内存区与后备存储之间的映射.系统能够使用多种类型的后备存储,好比交换空间,本地或者远程文件以及帧缓存等等. VM系统对它们统一处理,采用同一操做集操做,好比读取页面或者回写页面等.每种不一样的后备存储均可以用不一样的方法实现这些操做.这样,系统定义了一套统一的接口,每种后备存储给出本身的实现方法.这样,进程的地址空间就被视为一组映射到不一样数据对象上的的映射组成.全部的有效地址就是那些映射到数据对象上的地址.这些对象为映射它的页面提供了持久性的后备存储.映射使得用户能够直接寻址这些对象.
值得提出的是, VM体系结构独立于Unix系统,全部的Unix系统语义,如正文,数据及堆栈区均可以建构在基本VM系统之上.同时, VM体系结构也是独立于存储管理的,存储管理是由操做系统实施的,如:究竟采起什么样的对换和请求调页算法,到底是采起分段仍是分页机制进行存储管理,到底是如何将虚拟地址转换成为物理地址等等(Linux中是一种叫Three Level Page Table的机制),这些都与内存对象的概念无关.
1、Linux中VM的实现.
一个进程应该包括一个mm_struct(memory manage struct), 该结构是进程虚拟地址空间的抽象描述,里面包括了进程虚拟空间的一些管理信息: start_code, end_code, start_data, end_data, start_brk, end_brk等等信息.另外,也有一个指向进程虚存区表(vm_area_struct: virtual memory area)的指针,该链是按照虚拟地址的增加顺序排列的.在Linux进程的地址空间被分做许多区(vma),每一个区(vma)都对应虚拟地址空间上一段连续的区域, vma是能够被共享和保护的独立实体,这里的vma就是前面提到的内存对象.
下面是vm_area_struct的结构,其中,前半部分是公共的,与类型无关的一些数据成员,如:指向mm_struct的指针,地址范围等等,后半部分则是与类型相关的成员,其中最重要的是一个指向vm_operation_struct向量表的指针vm_ops, vm_pos向量表是一组虚函数,定义了与vma类型无关的接口.每个特定的子类,即每种vma类型都必须在向量表中实现这些操做.这里包括了: open, close, unmap, protect, sync, nopage, wppage, swapout这些操做.
- struct vm_area_struct {
-
- struct mm_struct * vm_mm;
- unsigned long vm_start;
- unsigned long vm_end;
- struct vm_area_struct *vm_next;
- pgprot_t vm_page_prot;
- unsigned long vm_flags;
- short vm_avl_height;
- struct vm_area_struct * vm_avl_left;
- struct vm_area_struct * vm_avl_right;
- struct vm_area_struct *vm_next_share;
- struct vm_area_struct **vm_pprev_share;
-
- struct vm_operations_struct * vm_ops;
- unsigned long vm_pgoff;
- struct file * vm_file;
- unsigned long vm_raend;
- void * vm_private_data;
- };
vm_ops: open, close, no_page, swapin, swapout……
2、驱动中的mmap()函数解析
设备驱动的mmap实现主要是将一个物理设备的可操做区域(设备空间)映射到一个进程的虚拟地址空间。这样就能够直接采用指针的方式像访问内存的方式访问设备。在驱动中的mmap实现主要是完成一件事,就是实际物理设备的操做区域到进程虚拟空间地址的映射过程。同时也须要保证这段映射的虚拟存储器区域不会被进程当作通常的空间使用,所以须要添加一系列的保护方式。
- static int mem_mmap(struct file* filp,struct vm_area_struct *vma)
- {
-
- struct mem_dev *dev = filp->private_data;
-
-
- vma->vm_flags |= VM_IO;
-
- vma->vm_flags |= VM_RESERVED;
-
-
- if(remap_pfn_range(vma,
- vma->vm_start,
- virt_to_phys(dev->data)>>PAGE_SHIFT,
- dev->size,
- vma->vm_page_prot
- ))
- return -EAGAIN;
-
- return 0;
- }
具体的实现分析以下:
vma->vm_flags |= VM_IO;
vma->vm_flags |= VM_RESERVED;
上面的两个保护机制就说明了被映射的这段区域具备映射IO的类似性,同时保证这段区域不能随便的换出。就是创建一个物理页与虚拟页之间的关联性。具体原理是虚拟页和物理页之间是以页表的方式关联起来,虚拟内存一般大于物理内存,在使用过程当中虚拟页经过页表关联一切对应的物理页,当物理页不够时,会选择性的牺牲一些页,也就是将物理页与虚拟页之间切断,重现关联其余的虚拟页,保证物理内存够用。在设备驱动中应该具体的虚拟页和物理页之间的关系应该是长期的,应该保护起来,不能随便被别的虚拟页所替换。具体也可参看关于虚拟存储器的文章。
接下来就是创建物理页与虚拟页之间的关系,即采用函数remap_pfn_range(),具体的参数以下:
int remap_pfn_range(structvm_area_struct *vma, unsigned long addr,unsigned long pfn, unsigned long size, pgprot_t prot)
一、struct vm_area_struct是一个虚拟内存区域结构体,表示虚拟存储器中的一个内存区域。其中的元素vm_start是指虚拟存储器中的起始地址。
二、addr也就是虚拟存储器中的起始地址,一般能够选择addr = vma->vm_start。
三、pfn是指物理存储器的具体页号,一般经过物理地址获得对应的物理页号,具体采用virt_to_phys(dev->data)>>PAGE_SHIFT.首先将虚拟内存转换到物理内存,而后获得页号。>>PAGE_SHIFT一般为12,这是由于每一页的大小恰好是4K,这样右移12至关于除以4096,获得页号。
四、size区域大小
五、区域保护机制。
返回值,若是成功返回0,不然正数。
3、系统调用mmap函数解析
介绍完VM的基本概念后,咱们能够讲述mmap和munmap系统调用了.mmap调用实际上就是一个内存对象vma的建立过程,
一、mmap函数
Linux提供了内存映射函数mmap,它把文件内容映射到一段内存上(准确说是虚拟内存上),经过对这段内存的读取和修改,实现对文件的读取和修改 。普通文件被映射到进程地址空间后,进程能够向访问普通内存同样对文件进行访问,没必要再调用read(),write()等操做。
先来看一下mmap的函数声明:
- 头文件:
- <unistd.h>
- <sys/mman.h>
-
- 原型: void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offsize);
-
mmap的做用是映射文件描述符fd指定文件的 [off,off + len]区域至调用进程的[addr, addr + len]的内存区域, 以下图所示:

mmap系统调用的实现过程是
1.先经过文件系统定位要映射的文件;
2.权限检查,映射的权限不会超过文件打开的方式,也就是说若是文件是以只读方式打开,那么则不容许创建一个可写映射;
3.建立一个vma对象,并对之进行初始化;
4.调用映射文件的mmap函数,其主要工做是给vm_ops向量表赋值;
5.把该vma链入该进程的vma链表中,若是能够和先后的vma合并则合并;
6.若是是要求VM_LOCKED(映射区不被换出)方式映射,则发出缺页请求,把映射页面读入内存中.
二、munmap函数
munmap(void * start, size_t length):
该调用能够看做是mmap的一个逆过程.它将进程中从start开始length长度的一段区域的映射关闭,若是该区域不是刚好对应一个vma,则有可能会分割几个或几个vma.
msync(void * start, size_t length, int flags):
把映射区域的修改回写到后备存储中.由于munmap时并不保证页面回写,若是不调用msync,那么有可能在munmap后丢失对映射区的修改.其中flags能够是MS_SYNC, MS_ASYNC, MS_INVALIDATE, MS_SYNC要求回写完成后才返回, MS_ASYNC发出回写请求后当即返回, MS_INVALIDATE使用回写的内容更新该文件的其它映射.该系统调用是经过调用映射文件的sync函数来完成工做的.
brk(void * end_data_segement):
将进程的数据段扩展到end_data_segement指定的地址,该系统调用和mmap的实现方式十分类似,一样是产生一个vma,而后指定其属性.不过在此以前须要作一些合法性检查,好比该地址是否大于mm->end_code, end_data_segement和mm->brk之间是否还存在其它vma等等.经过brk产生的vma映射的文件为空,这和匿名映射产生的vma类似,关于匿名映射不作进一步介绍.库函数malloc就是经过brk实现的.
4、实例解析
下面这个例子显示了把文件映射到内存的方法,源代码是:
- #include <sys/mman.h> /* for mmap and munmap */
- #include <sys/types.h> /* for open */
- #include <sys/stat.h> /* for open */
- #include <fcntl.h> /* for open */
- #include <unistd.h> /* for lseek and write */
- #include <stdio.h>
-
- int main(int argc, char **argv)
- {
- int fd;
- char *mapped_mem, * p;
- int flength = 1024;
- void * start_addr = 0;
-
- fd = open(argv[1], O_RDWR | O_CREAT, S_IRUSR | S_IWUSR);
- flength = lseek(fd, 1, SEEK_END);
- write(fd, "\0", 1);
- lseek(fd, 0, SEEK_SET);
- mapped_mem = mmap(start_addr, flength, PROT_READ,
- MAP_PRIVATE,
- fd, 0);
-
-
- printf("%s\n", mapped_mem);
- close(fd);
- munmap(mapped_mem, flength);
- return 0;
- }
编译运行此程序:
gcc -Wall mmap.c
./a.out text_filename
上面的方法由于用了PROT_READ,因此只能读取文件里的内容,不能修改,若是换成PROT_WRITE就能够修改文件的内容了。又因为 用了MAAP_PRIVATE因此只能此进程使用此内存区域,若是换成MAP_SHARED,则能够被其它进程访问,好比下面的
- #include <sys/mman.h> /* for mmap and munmap */
- #include <sys/types.h> /* for open */
- #include <sys/stat.h> /* for open */
- #include <fcntl.h> /* for open */
- #include <unistd.h> /* for lseek and write */
- #include <stdio.h>
- #include <string.h> /* for memcpy */
-
- int main(int argc, char **argv)
- {
- int fd;
- char *mapped_mem, * p;
- int flength = 1024;
- void * start_addr = 0;
-
- fd = open(argv[1], O_RDWR | O_CREAT, S_IRUSR | S_IWUSR);
- flength = lseek(fd, 1, SEEK_END);
- write(fd, "\0", 1);
- lseek(fd, 0, SEEK_SET);
- start_addr = 0x80000;
- mapped_mem = mmap(start_addr, flength, PROT_READ|PROT_WRITE,
- MAP_SHARED,
- fd, 0);
-
- * 使用映射区域. */
- printf("%s\n", mapped_mem);
- while((p = strstr(mapped_mem, "Hello"))) {
- memcpy(p, "Linux", 5);
- p += 5;
- }
-
- close(fd);
- munmap(mapped_mem, flength);
- return 0;
- }
5、mmap和共享内存对比
共享内存容许两个或多个进程共享一给定的存储区,由于数据不须要来回复制,因此是最快的一种进程间通讯机制。共享内存能够经过mmap()映射普通文件(特殊状况下还能够采用匿名映射)机制实现,也能够经过系统V共享内存机制实现。应用接口和原理很简单,内部机制复杂。为了实现更安全通讯,每每还与信号灯等同步机制共同使用。
对好比下:
mmap机制:就是在磁盘上创建一个文件,每一个进程存储器里面,单独开辟一个空间来进行映射。若是多进程的话,那么不会对实际的物理存储器(主存)消耗太大。
shm机制:每一个进程的共享内存都直接映射到实际物理存储器里面。
一、mmap保存到实际硬盘,实际存储并无反映到主存上。优势:储存量能够很大(多于主存);缺点:进程间读取和写入速度要比主存的要慢。
二、shm保存到物理存储器(主存),实际的储存量直接反映到主存上。优势,进程间访问速度(读写)比磁盘要快;缺点,储存量不能很是大(多于主存)
使用上看:若是分配的存储量不大,那么使用shm;若是存储量大,那么使用mmap。
mmap - 用户空间与内核空间
mmap概述
共享内存能够说是最有用的进程间通讯方式,也是最快的IPC形式, 由于进程能够直接读写内存,而不须要任何数据的拷贝。对于像管道和消息队列等通讯方式,则须要在内核和用户空间进行四次的数据拷贝,而共享内存则只拷贝两次数据: 一次从输入文件到共享内存区,另外一次从共享内存区到输出文件。实际上,进程之间在共享内存时,并不老是读写少许数据后就解除映射,有新的通讯时,再从新创建共享内存区域。而是保持共享区域,直到通讯完毕为止,这样,数据内容一直保存在共享内存中,并无写回文件。共享内存中的内容每每是在解除映射时才写回文件的。所以,采用共享内存的通讯方式效率是很是高的。
传统文件访问
UNIX访问文件的传统方法是用open打开它们, 若是有多个进程访问同一个文件, 则每个进程在本身的地址空间都包含有该文件的副本,这没必要要地浪费了存储空间. 下图说明了两个进程同时读一个文件的同一页的情形. 系统要将该页从磁盘读到高速缓冲区中, 每一个进程再执行一个存储器内的复制操做将数据从高速缓冲区读到本身的地址空间.

共享存储映射
如今考虑另外一种处理方法: 进程A和进程B都将该页映射到本身的地址空间, 当进程A第一次访问该页中的数据时, 它生成一个缺页中断. 内核此时读入这一页到内存并更新页表使之指向它.之后, 当进程B访问同一页面而出现缺页中断时, 该页已经在内存, 内核只须要将进程B的页表登记项指向次页便可. 以下图所示:

mmap系统调用使得进程之间经过映射同一个普通文件实现共享内存,普通文件被映射到进程地址空间后,进程能够像访问普通内存同样对文件进行访问,没必要再调用read和write等。

mmap用户空间
用户空间mmap函数原型
头文件 sys/mman.h
void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);
int munmap(void *start, size_t length);
int msync ( void * addr , size_t len, int flags) 经过调用msync()实现磁盘上文件内容与共享内存区的内容一致
做用:
mmap将一个文件或者其余对象映射进内存,当文件映射到进程后,就能够直接操做这段虚拟地址进行文件的读写等操做。
参数说明:
start:映射区的开始地址
length:映射区的长度
prot:指望的内存保护标志
—-PROT_EXEC //页内容能够被执行
—-PROT_READ //页内容能够被读取
—-PROT_WRITE //页能够被写入
—-PROT_NONE //页不可访问
flags:指定映射对象的类型
—-MAP_FIXED
—-MAP_SHARED 与其它全部映射这个对象的进程共享映射空间
—-MAP_PRIVATE 创建一个写入时拷贝的私有映射。内存区域的写入不会影响到原文件
—-MAP_ANONYMOUS 匿名映射,映射区不与任何文件关联
fd:若是MAP_ANONYMOUS被设定,为了兼容问题,其值应为-1
offset:被映射对象内容的起点
经过共享映射的方式修改文件
系统调用mmap能够将文件映射至内存(进程空间),如此能够把对文件的操做转为对内存的操做,以此避免更多的lseek()、read()、write()等系统调用,这点对于大文件或者频繁访问的文件尤为有用,提升了I/O效率。
下面例子中测试所需的data.txt文件内容以下:
aaaaaaaaa
bbbbbbbbb
ccccccccc
ddddddddd
经过共享映射实现两个进程之间的通讯
两个程序映射同一个文件到本身的地址空间, 进程A先运行, 每隔两秒读取映射区域, 看是否发生变化.
进程B后运行, 它修改映射区域, 而后推出, 此时进程A可以观察到存储映射区的变化
进程A的代码:
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h> #include <unistd.h> #include <error.h> #define BUF_SIZE 100 int main(int argc, char **argv) { int fd, nread, i; struct stat sb; char *mapped, buf[BUF_SIZE]; for (i = 0; i < BUF_SIZE; i++) { buf[i] = '#'; }
进程B的代码:
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h> #include <unistd.h> #include <error.h> #define BUF_SIZE 100 int main(int argc, char **argv) { int fd, nread, i; struct stat sb; char *mapped, buf[BUF_SIZE]; for (i = 0; i < BUF_SIZE; i++) { buf[i] = '#'; }
经过匿名映射实现父子进程通讯
#include <sys/mman.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#define BUF_SIZE 100 int main(int argc, char** argv) { char *p_map;
对mmap返回地址的访问
linux采用的是页式管理机制。对于用mmap()映射普通文件来讲,进程会在本身的地址空间新增一块空间,空间大
小由mmap()的len参数指定,注意,进程并不必定可以对所有新增空间都能进行有效访问。进程可以访问的有效地址大小取决于文件被映射部分的大小。简单的说,可以容纳文件被映射部分大小的最少页面个数决定了进程从mmap()返回的地址开始,可以有效访问的地址空间大小。超过这个空间大小,内核会根据超过的严重程度返回发送不一样的信号给进程。可用以下图示说明:
总结一下就是, 文件大小, mmap的参数 len 都不能决定进程能访问的大小, 而是容纳文件被映射部分的最小页面数决定进程能访问的大小. 下面看一个实例:
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h> #include <stdio.h> int main(int argc, char** argv) { int fd,i; int pagesize,offset; char *p_map; struct stat sb;
mmap内核空间
内核空间mmap函数原型
内核空间的mmap函数原型为:int (*map)(struct file *filp, struct vm_area_struct *vma);
做用是实现用户进程中的地址与内核中物理页面的映射
mmap函数实现步骤
内核空间mmap函数具体实现步骤以下:
1. 经过kmalloc, get_free_pages, vmalloc等分配一段虚拟地址
2. 若是是使用kmalloc, get_free_pages分配的虚拟地址,那么使用virt_to_phys()将其转化为物理地址,再将获得的物理地址经过”phys>>PAGE_SHIFT”获取其对应的物理页面帧号。或者直接使用virt_to_page从虚拟地址获取获得对应的物理页面帧号。
若是是使用vmalloc分配的虚拟地址,那么使用vmalloc_to_pfn获取虚拟地址对应的物理页面的帧号。
3. 对每一个页面调用SetPageReserved()标记为保留才能够。
4. 经过remap_pfn_range为物理页面的帧号创建页表,并映射到用户空间。
说明:kmalloc, get_free_pages, vmalloc分配的物理内存页面最好仍是不要用remap_pfn_range,建议使用VMA的nopage方法。
说明:
若共享小块连续内存,上面所说的get_free_pages就能够分配多达几M的连续空间,
若共享大块连续内存,就得靠uboot帮忙,给linux kernel传递参数的时候指定”mem=”,而后在内核中使用下面两个函数来预留和释放内存。
void *alloc_bootmem(unsigned long size);
void free_bootmem(unsigned long addr, unsigned long size);
mmap函数实现例子
在字符设备驱动中,有一个struct file_operation结构提,其中fops->mmap指向你本身的mmap钩子函数,用户空间对一个字符设备文件进行mmap系统调用后,最终会调用驱动模块里的mmap钩子函数。在mmap钩子函数中须要调用下面这个API:
int remap_pfn_range(struct vm_area_struct *vma,
在mmap钩子函数中,像下面这样就能够了
int my_mmap(struct file *filp, struct vm_area_struct *vma){
来看一个例子:
内核空间代码mymap.c
#include <linux/miscdevice.h>
#include <linux/delay.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/init.h> #include <linux/mm.h> #include <linux/fs.h> #include <linux/types.h> #include <linux/delay.h> #include <linux/moduleparam.h> #include <linux/slab.h> #include <linux/errno.h> #include <linux/ioctl.h> #include <linux/cdev.h> #include <linux/string.h> #include <linux/list.h> #include <linux/pci.h> #include <linux/gpio.h> #define DEVICE_NAME "mymap" static unsigned char array[10]={0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; static unsigned char *buffer; static int my_open(struct inode *inode, struct file *file) { return 0; } static int my_map(struct file *filp, struct vm_area_struct *vma) { unsigned long phys;
用户空间代码mymap_app.c
从上面这张图能够看出:
当系统开机,driver起来的时候会将内存前10个字节初始化,经过cat /sys/devices/virtual/misc/mymap/rng_current,能够看出此时内存中的值。
当执行mymap_app时会将前10个字节的内容加上10再写进内存,再经过cat /sys/devices/virtual/misc/mymap/rng_current,能够看出修改后的内存中的值。
参考文章
- linux 内存映射 remap_pfn_range操做
- mmap详解
资源下载
- mmap内核驱动与应用程序
Linux设备驱动之mmap设备操做
1.mmap系统调用
void *mmap(void *addr, size_t len, int prot, int flags, int fd, off_t offset);
功能:负责把文件内容映射到进程的虚拟地址空间,经过对这段内存的读取和修改来实现对文件的读取和修改,而不须要再调用read和write;
参数:addr:映射的起始地址,设为NULL由系统指定;
len:映射到内存的文件长度;
prot:指望的内存保护标志,不能与文件的打开模式冲突。PROT_EXEC,PROT_READ,PROT_WRITE等;
flags:指定映射对象的类型,映射选项和映射页是否能够共享。MAP_SHARED,MAP_PRIVATE等;
fd:由open返回的文件描述符,表明要映射的文件;
offset:开始映射的文件的偏移。
返回值:成功执行时,mmap()返回被映射区的指针。失败时,mmap()返回MAP_FAILED。
mmap映射图:

2.解除映射:
int munmap(void *start, size_t length);
3.虚拟内存区域:
虚拟内存区域是进程的虚拟地址空间中的一个同质区间,即具备一样特性的连续地址范围。一个进程的内存映象由下面几个部分组成:程序代码、数据、BSS和栈区域,以及内存映射的区域。
linux内核使用vm_area_struct结构来描述虚拟内存区。其主要成员:
unsigned long vm_start; /* Our start address within vm_mm. */
unsigned long vm_end; /* The first byte after our end address within vm_mm. */ unsigned long vm_flags; /* Flags, see mm.h. 该区域的标记。如VM_IO(该VMA标记为内存映射的IO区域,会阻止系统将该区域包含在进程的存放转存中)和VM_RESERVED(标志内存区域不能被换出)。*/
4.mmap设备操做:
映射一个设备是指把用户空间的一段地址(虚拟地址区间)关联到设备内存上,当程序读写这段用户空间的地址时,它其实是在访问设备。
mmap方法是file_operations结构的成员,在mmap系统调用的发出时被调用。在此以前,内核已经完成了不少工做。
mmap设备方法所须要作的就是创建虚拟地址到物理地址的页表(虚拟地址和设备的物理地址的关联经过页表)。
static int mmap(struct file *file, struct vm_area_struct *vma);
mmap如何完成页表的创建?(两种方法)
(1)使用remap_pfn_range一次创建全部页表。
int remap_pfn_range(struct vm_area_struct *vma, unsigned long addr, unsigned long pfn, unsigned long size, pgprot_t prot);
/** * remap_pfn_range - remap kernel memory to userspace * @vma: user vma to map to:内核找到的虚拟地址区间 * @addr: target user address to start at:要关联的虚拟地址 * @pfn: physical address of kernel memory:要关联的设备的物理地址,也即要映射的物理地址所在的物理帧号,可将物理地址>>PAGE_SHIFT * @size: size of map area * @prot: page protection flags for this mapping * * Note: this is only safe if the mm semaphore is held when called. */
(2)使用nopage VMA方法每次创建一个页表;
5.源码分析:
(1)memdev.h
View Code
/*mem设备描述结构体*/
struct mem_dev
{
char *data;
unsigned long size; }; #endif /* _MEMDEV_H_ */
(2)memdev.c
View Code
static int mem_major = MEMDEV_MAJOR;
module_param(mem_major, int, S_IRUGO);
struct mem_dev *mem_devp; /*设备结构体指针*/ struct cdev cdev; /*文件打开函数*/ int mem_open(struct inode *inode, struct file *filp) { struct mem_dev *dev; /*获取次设备号*/ int num = MINOR(inode->i_rdev); if (num >= MEMDEV_NR_DEVS) return -ENODEV; dev = &mem_devp[num]; /*将设备描述结构指针赋值给文件私有数据指针*/ filp->private_data = dev; return 0; } /*文件释放函数*/ int mem_release(struct inode *inode, struct file *filp) { return 0; } static int memdev_mmap(struct file*filp, struct vm_area_struct *vma) { struct mem_dev *dev = filp->private_data; /*得到设备结构体指针*/ vma->vm_flags |= VM_IO; vma->vm_flags |= VM_RESERVED; if (remap_pfn_range(vma,vma->vm_start,virt_to_phys(dev->data)>>PAGE_SHIFT, vma->vm_end - vma->vm_start, vma->vm_page_prot)) return -EAGAIN; return 0; } /*文件操做结构体*/ static const struct file_operations mem_fops = { .owner = THIS_MODULE, .open = mem_open, .release = mem_release, .mmap = memdev_mmap, }; /*设备驱动模块加载函数*/ static int memdev_init(void) { int result; int i; dev_t devno = MKDEV(mem_major, 0); /* 静态申请设备号*/ if (mem_major) result = register_chrdev_region(devno, 2, "memdev"); else /* 动态分配设备号 */ { result = alloc_chrdev_region(&devno, 0, 2, "memdev"); mem_major = MAJOR(devno); } if (result < 0) return result; /*初始化cdev结构*/ cdev_init(&cdev, &mem_fops); cdev.owner = THIS_MODULE; cdev.ops = &mem_fops; /* 注册字符设备 */ cdev_add(&cdev, MKDEV(mem_major, 0), MEMDEV_NR_DEVS); /* 为设备描述结构分配内存*/ mem_devp = kmalloc(MEMDEV_NR_DEVS * sizeof(struct mem_dev), GFP_KERNEL); if (!mem_devp) /*申请失败*/ { result = - ENOMEM; goto fail_malloc; } memset(mem_devp, 0, sizeof(struct mem_dev)); /*为设备分配内存*/ for (i=0; i < MEMDEV_NR_DEVS; i++) { mem_devp[i].size = MEMDEV_SIZE; mem_devp[i].data = kmalloc(MEMDEV_SIZE, GFP_KERNEL); memset(mem_devp[i].data, 0, MEMDEV_SIZE); } return 0; fail_malloc: unregister_chrdev_region(devno, 1); return result; } /*模块卸载函数*/ static void memdev_exit(void) { cdev_del(&cdev); /*注销设备*/ kfree(mem_devp); /*释放设备结构体内存*/ unregister_chrdev_region(MKDEV(mem_major, 0), 2); /*释放设备号*/ } MODULE_AUTHOR("David Xie"); MODULE_LICENSE("GPL"); module_init(memdev_init); module_exit(memdev_exit);
(3)app-mmap.c
#include <stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#include<sys/mman.h>
int main()
{
int fd;
char *start;
//char buf[100]; char *buf; /*打开文件*/ fd = open("/dev/memdev0",O_RDWR); buf = (char *)malloc(100); memset(buf, 0, 100); start=mmap(NULL,100,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0); /* 读出数据 */ strcpy(buf,start); sleep (1); printf("buf 1 = %s\n",buf); /* 写入数据 */ strcpy(start,"Buf Is Not Null!"); memset(buf, 0, 100); strcpy(buf,start); sleep (1); printf("buf 2 = %s\n",buf); munmap(start,100); /*解除映射*/ free(buf); close(fd); return 0; }
测试步骤:
(1)编译安装内核模块:insmod memdev.ko
(2)查看设备名、主设备号:cat /proc/devices
(3)手工建立设备节点:mknod /dev/memdev0 c *** 0
查看设备文件是否存在:ls -l /dev/* | grep memdev
(4)编译下载运行应用程序:./app-mmap
结果:buf 1 =
buf 2 = Buf Is Not Null!
总结:mmap设备方法实现将用户空间的一段内存关联到设备内存上,对用户空间的读写就至关于对字符设备的读写;不是全部的设备都能进行mmap抽象,好比像串口和其余面向流的设备就不能作mmap抽象。
细说linux IPC(三):mmap系统调用共享内存
前面讲到socket的进程间通讯方式,这种方式在进程间传递数据时首先须要从进程1地址空间中把数据拷贝到内核,内核再将数据拷贝到进程2的地址空间中,也就是数据传递须要通过内核传递。这样在处理较多数据时效率不是很高,而让多个进程共享一片内存区则解决了以前socket进程通讯的问题。共享内存是最快的进程间通讯 ,将一片内存映射到多个进程地址空间中,那么进程间的数据传递将不在涉及内核。
共享内存并非从某一进程拥有的内存中划分出来的;进程的内存老是私有的。共享内存是从系统的空闲内存池中分配的,但愿访问它的每一个进程链接它。这个链接过程称为映射,它给共享内存段分配每一个进程的地址空间中的本地地址。
mmap()系统调用使得进程之间经过映射同一个普通文件实现共享内存。普通文件被映射到进程地址空间后,进程能够向访问普通内存同样对文件进行访问,没必要再调用read(),write()等操做。函数原型为:
- #include <sys/mman.h>
- void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
其中参数addr为描述符fd应该被映射到进程空间的起始地址,当指定为NULL时内核将本身去选择起始地址,不管addr是为NULL,函数返回值都是fd所映射到内存的起始地址;
len是映射到调用进程地址空间的字节数,它 从被映射文件开头offset个字节开始算起,offset一般设置为0;
prot 参数指定共享内存的访问权限。可取以下几个值的或:PROT_READ(可读) , PROT_WRITE (可写), PROT_EXEC (可执行), PROT_NONE(不可访问),该值常设置为PROT_READ | PROT_WRITE 。
flags由如下几个常值指定:MAP_SHARED , MAP_PRIVATE , MAP_FIXED,其中,MAP_SHARED(变更是共享的,对共享内存的修改全部进程可见) , MAP_PRIVATE(变更是私有的,对共享内存修改只对该进程可见) 必选其一,而MAP_FIXED则不推荐使用 。
munmp() 删除地址映射关系,函数原型以下:
- #include <sys/mman.h>
- int munmap(void *addr, size_t length);
参数addr是由mmap返回的地址,len是映射区大小。
进程在映射空间的对共享内容的改变并不直接写回到磁盘文件中,每每在调用munmap()后才执行该操做。能够经过调用msync()实现磁盘上文件内容与共享内存区的内容一致。 msync()函数原型为:
- #include <sys/mman.h>
- int msync(void *addr, size_t length, int flags);
参数addr和len表明内存区,flags有如下值指定,MS_ASYNC(执行异步写), MS_SYNC(执行同步写),MS_INVALIDATE(使高速缓存失效)。其中MS_ASYNC和MS_SYNC两个值必须且只能指定一个,一旦写操做排入内核,MS_ASYNC当即返回,MS_SYNC要等到写操做完成后才返回。若是还指定了MS_INVALIDATE,那么与其最终拷贝不一致的文件数据的全部内存中拷贝都失效。
在使用open函数打开一个文件以后调用mmap把文件内容映射到调用进程的地址空间,这样咱们操做文件内容只须要对映射的地址空间进行操做,而无需再使用open,write等函数。
使用共享内存的步骤基本是:
open()建立内存段;
用 ftruncate()设置它的大小;
用mmap() 把它映射到进程内存,执行其余参与者须要的操做;
当使用完时,原来的进程调用 munmap()而后退出。
下面来看一个实现:
server程序建立内存并向共享内存写入数据:
- int sln_shm_get(char *shm_file, void **shm, int mem_len)
- {
- int fd;
- fd = open(shm_file, O_RDWR | O_CREAT, 0644);
- if (fd < 0) {
- printf("open <%s> failed: %s\n", shm_file, strerror(errno));
- return -1;
- }
- ftruncate(fd, mem_len);
- *shm = mmap(NULL, mem_len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
- if (MAP_FAILED == *shm) {
- printf("mmap: %s\n", strerror(errno));
- return -1;
- }
- close(fd);
- return 0;
- }
- int main(int argc, const char *argv[])
- {
- char *shm_buf = NULL;
- sln_shm_get(SHM_IPC_FILENAME, (void **)&shm_buf, SHM_IPC_MAX_LEN);
- snprintf(shm_buf, SHM_IPC_MAX_LEN, "hello share memory ipc! i'm server.");
- return 0;
- }
client程序映射共享内存并读取其中数据:
- int main(int argc, const char *argv[])
- {
- char *shm_buf = NULL;
- sln_shm_get(SHM_IPC_FILENAME, (void **)&shm_buf, SHM_IPC_MAX_LEN);
- printf("ipc client get: %s\n", shm_buf);
- munmap(shm_buf, SHM_IPC_MAX_LEN);
- return 0;
- }
先执行server程序向共享内存写入数据,再运行客户程序,运行结果以下:
- # ./server
- # ./client
- ipc client get: hello share memory ipc! i'm server.
- #
共享内存不像socket那样自己具备同步机制,它须要经过增长其余同步操做来实现同步,好比信号量等。同步相关操做在后面会有相关专栏详细叙述。
存储器的层次结构
高速缓冲存储器 cache
读cache操做
cache若是包含数据就直接从cache中读出来,由于cache速度要比内存快
若是没有包含的话,就从内存中找,找到后就放到cache中去,之后再读的话就直接从cache读了,下次访问不少次的时候就会快不少,至关于提升了命中率,cpu的访问速度就大大提升了
cache能大大提升cpu的访问速率
cache的设计
不能太大,也不能过小
太大的话,由于程序在查看数据的时候须要把cache走一遍,若是cache太大,
那么走一遍的时间就太长,就不能达到提高速率的效果
若是过小的话,存放在内存的块数就少了,命中率下降,访问内存的次数就增长了,cpu性能一样下降了
块大小
Cache与memory的的数据交换单位
由小变大时, 由局部性原理,命中增长
变得更大时, 新近取得数据被用到的可能性,小于那些必须被移出Cache的数据再次用到的可能性,命中率大大下降
DMA
大量移动数据
cpu性能快,外设的速度比较慢,cpu就要一直等待外设
所以诞生了DMA
之前没有DMA,就把数据从内存通过cpu到外设
有了DMA的话,cpu把指令告诉DMA,把内存从某个地址开始,多大的数据写给外设,这样的话DMA就去作了,cpu就不用作了,DMA就把内存中取出数据,而后再到外设,这种机制就是DMA机制,cpu就解放了,作更重要的事,所以这种内存拷贝、数据大量移动拷贝的时候就用DMA来解放cpu,记住cpu只在开始于结束时参与
开始时:
结束时:
中断处理例程
直接内存访问(DMA)
1. 什么是DMA
直接内存访问是一种硬件机制,它容许外围设备和主内存之间直接传输它们的I/O数据,而不须要系统处理器的参与。使用这种机制能够大大提升与设备通讯的吞吐量。
2. DMA数据传输
有两种方式引起数据传输:
第一种状况:软件对数据的请求
1. 当进程调用read,驱动程序函数分配一个DMA缓冲区,并让硬件将数据传输到这个缓冲区中。进程处于睡眠状态。
2. 硬件将数据写入到DMA缓冲区中,当写入完毕,产生一个中断
3. 中断处理程序获取输入的数据,应答中断,并唤起进程,该进程如今便可读取数据
第二种状况发生在异步使用DMA时。
1. 硬件产生中断,宣告新数据的到来
2. 中断处理程序分配一个缓冲区,而且告诉硬件向哪里传输数据
3. 外围设备将数据写入数据区,完成后,产生另一个中断
4.处理程序分发新数据,唤醒任何相关进程,而后执行清理工做
高效的DMA处理依赖于中断报告。
3. 分配DMA缓冲区
使用DMA缓冲区的主要问题是:当大于一页时,它们必须占据连续的物理页,由于设备使用ISA或PCI系统总线传输数据,而这两种方式使用的都是物理地址。
使用get_free_pasges能够分配多大几M字节的内存(MAX_ORDER是11),可是对于较大数量(即便是远小于128KB)的请求,一般会失败,这是由于系统内存充满了内存碎片。
解决方法之一就是在引导时分配内存,或者为缓冲区保留顶部物理内存。
例子:在系统引导时,向内核传递参数“mem=value”的方法保留顶部的RAM。好比系统有256内存,参数“mem=255M”,使内核不能使用顶部的1M字节。随后,模块能够使用下面代码得到该内存的访问权:
dmabuf=ioremap(0XFF00000/**255M/, 0X100000/*1M/*);
解决方法之二是使用GPF_NOFAIL分配标志为缓冲区分配内存,可是该方法为内存管理子系统带来了至关大的压力。
解决方法之三十设备支持分散/汇集I/O,这能够将缓冲区分配成多个小块,设备会很好地处理它们。
4. 通用DMA层
DMA操做最终会分配缓冲区,并将总线地址传递给设备。内核提升了一个与总线——体系结构无关的DMA层。强烈建议在编写驱动程序时,为DMA操做使用该层。使用这些函数的头文件是。
int dma_set_mask(struct device *dev, u64 mask);
该掩码显示该设备能寻址能力对应的位。好比说,设备受限于24位寻址,则mask应该是0x0FFFFFF。
5. DMA映射
IOMMU在设备可访问的地址范围内规划了物理内存,使得物理上分散的缓冲区对设备来讲成连续的。对IOMMU的运用须要使用到通用DMA层,而vir_to_bus函数不能完成这个任务。可是,x86平台没有对IOMMU的支持。
解决之道就是创建回弹缓冲区,而后,必要时会将数据写入或者读出回弹缓冲区。缺点是下降系统性能。
根据DMA缓冲区指望保留的时间长短,PCI代码区分两种类型的DMA映射:
一是一致性DMA映射,存在于驱动程序生命周期中,一致性映射的缓冲区必须可同时被CPU和外围设备访问。一致性映射必须保存在一致性缓存中。创建和使用一致性映射的开销是很大的。
二是流式DMA映射,内核开发者建议尽可能使用流式映射,缘由:一是在支持映射寄存器的系统中,每一个DMA映射使用总线上的一个或多个映射寄存器,而一致性映射生命周期很长,长时间占用这些这些寄存器,甚至在不使用他们的时候也不释放全部权;二是在一些硬件中,流式映射能够被优化,但优化的方法对一致性映射无效。
6. 创建一致性映射
驱动程序可调用pci_alloc_consistent函数创建一致性映射:
void *dma_alloc_coherent(struct device *dev, size_t size, dma_addr_t *dma_handle, int falg);
该函数处理了缓冲区的分配和映射,前两个参数是device结构和所需的缓冲区的大小。函数在两处返回DMA映射的结果:函数的返回值是缓冲区的内核虚拟地址,能够被驱动程序使用;而与其相关的总线地址保存在dma_handle中。
当再也不须要缓冲区时,调用下函数:
void dma_free_conherent(struct device *dev, size_t size, void *vaddr, dma_addr_t *dma_handle);
7. DMA池
DMA池是一个生成小型,一致性DMA映射的机制。调用dma_alloc_coherent函数得到的映射,可能其最小大小为单个页。若是设备须要的DMA区域比这还小,就是用DMA池。在中定义了DMA池函数:
struct dma_pool *dma_pool_create(const char *name, struct device *dev, size_t size, size_t align, size_t allocation);
void dma_pool_destroy(struct dma_pool *pool);
name是DMA池的名字,dev是device结构,size是从该池中分配的缓冲区的大小,align是该池分配操做所必须遵照的硬件对齐原则(用字节表示),若是allocation不为零,表示内存边界不能超越allocation。好比说传入的allocation是4K,表示从该池分配的缓冲区不能跨越4KB的界限。
在销毁以前必须向DMA池返回全部分配的内存。
void * dma_pool_alloc(sturct dma_pool *pool, int mem_flags, dma_addr_t *handle);
void dma_pool_free(struct dma_pool *pool, void *addr, dma_addr_t addr);
8. 创建流式DMA映射
在某些体系结构中,流式映射也可以拥有多个不连续的页和多个“分散/汇集”缓冲区。创建流式映射时,必须告诉内核数据流动的方向。
DMA_TO_DEVICE
DEVICE_TO_DMA
若是数据被发送到设备,使用DMA_TO_DEVICE;而若是数据被发送到CPU,则使用DEVICE_TO_DMA。
DMA_BIDIRECTTONAL
若是数据可双向移动,则使用该值
DMA_NONE
该符号只是出于调试目的。
当只有一个缓冲区要被传输的时候,使用下函数映射它:
dma_addr_t dma_map_single(struct device *dev, void *buffer, size_t size, enum dma_data_direction direction);
返回值是总线地址,能够把它传递给设备;若是执行错误,返回NULL。
当传输完毕后,使用下函数删除映射:
void dma_unmap_single(struct device *dev, dma_addr_t dma_addr, size_t size, enum dma-data_direction direction);
使用流式DMA的原则:
一是缓冲区只能用于这样的传送,即其传送方向匹配与映射时给定的方向值;
二是一旦缓冲区被映射,它将属于设备,不是处理器。直到缓冲区被撤销映射前,驱动程序不能以任何方式访问其中的内容。只用当dma_unmap_single函数被调用后,显示刷新处理器缓存中的数据,驱动程序才能安全访问其中的内容。
三是在DMA出于活动期间内,不能撤销对缓冲区的映射,不然会严重破坏系统的稳定性。
若是要映射的缓冲区位于设备不能访问的内存区段(高端内存),怎么办?一些体系结构只产生一个错误,可是其余一些系统结构件建立一个回弹缓冲区。回弹缓冲区就是内存中的独立区域,它可被设备访问。若是使用DMA_TO_DEVICE标志映射缓冲区,而且须要使用回弹缓冲区,则在最初缓冲区中的内容做为映射操做的一部分被拷贝。很明显,在拷贝后,最初缓冲区内容的改变对设备不可见。一样DEVICE_TO_DMA回弹缓冲区被dma_unmap_single函数拷贝回最初的缓冲区中,也就是说,直到拷贝操做完成,来自设备的数据才可用。
有时候,驱动程序须要不通过撤销映射就访问流式DMA缓冲区的内容,为此内核提供了以下调用:
void dma_sync_single_for_cpu(struct device *dev, dma_handle_t bus_addr, size_t size, enum dma_data_directction direction);
应该在处理器访问流式DMA缓冲区前调用该函数。一旦调用了该函数,处理器将“拥有”DMA缓冲区,并可根据须要对它进行访问。而后在设备访问缓冲区前,应该调用下面的函数将全部权交还给设备:
void dma_sync_single_for_device(struct device *dev, dma_handle_t bus_addr, size_t size, enum dma_data_direction direction);
再次强调,处理器在调用该函数后,不能再访问DMA缓冲区了。
DMA原理
DMA原理:DMA 是全部现代电脑的重要特点,他容许不一样速度的硬件装置来沟通,而不须要依于 CPU 的大量 中断 负载。不然,CPU 须要从 来源 把每一片断的资料复制到 暂存器,而后把他们再次写回到新的地方。在这个时间中,CPU 对于其余的工做来讲就没法使用。 DMA 传输重要地将一个内存区从一个装置复制到另一个。当 CPU 初始化这个传输动做,传输动做自己是由 DMA 控制器 来实行和完成。典型的例子就是移动一个外部内存的区块到芯片内部更快的内存去。像是这样的操做并无让处理器工做拖延,反而能够被从新排程去处理其余的工做。DMA 传输对于高效能 嵌入式系统 算法和网络是很重要的。
在实现DMA传输时,是由DMA控制器直接掌管总线,所以,存在着一个总线控制权转移问题。即DMA传输前,CPU要把总线控制权交给DMA控制器,而在结束DMA传输后,DMA控制器应当即把总线控制权再交回给CPU。
DMA
一个完整的DMA传输过程必须通过下面的4个步骤。
1.DMA请求
CPU对DMA控制器初始化,并向I/O接口发出操做命令,I/O接口提出DMA请求。
2.DMA响应
DMA控制器对DMA请求判别优先级及屏蔽,向总线裁决逻辑提出总线请求。当CPU执行完当前总线周期便可释放总线控制权。此时,总线裁决逻辑输出总线应答,表示DMA已经响应,经过DMA控制器通知I/O接口开始DMA传输。
3.DMA传输
DMA控制器得到总线控制权后,CPU即刻挂起或只执行内部操做,由DMA控制器输出读写命令,直接控制RAM与I/O接口进行DMA传输。
4.DMA结束
当完成规定的成批数据传送后,DMA控制器即释放总线控制权,并向I/O接口发出结束信号。当I/O接口收到结束信号后,一方面停 止I/O设备的工做,另外一方面向CPU提出中断请求,使CPU从不介入的状态解脱,并执行一段检查本次DMA传输操做正确性的代码。最后,带着本次操做结果及状态继续执行原来的程序。
因而可知,DMA传输方式无需CPU直接控制传输,也没有中断处理方式那样保留现场和恢复现场的过程,经过硬件为RAM与I/O设备开辟一条直接传送数据的通路,使CPU的效率大为提升。
DMA操做模式
DMA用于无需CPU的介入而直接由专用控制器创建源与目的传输的应用,所以,在大量数据传输中解放了CPU。PIC32微控制器中的DMA可用于映射到内存空间中的不一样外设,如从存储区到SPI,UART或I2C等设备。DMA特性详见器件参考手册,这里仅对一些基本原理与功能作一个简析。
PIC32中DMA的传输涉及到几个基本的术语。
Event:事件,触发控制器启动或中止DMA传输的操做;
Transaction:事务,单字传输(最多能够到4个字节),由读/写组成;
Cell transfer:元传输,单次共DCHXCSIZE个字节的数据传输。元传输由单个或多个事务组成。
Block transfer:块传输,块传输总的字节数由DCHXSSIZ或DCHXDSIZ决定。块传输由单个或多个元传输组成。
事件是触发DMA控制器产生动做的方式,分为,START EVENT->启动传输;ABORT EVENT->取消传输;STOP EVENT->中止传输;为了有一个完整的概念认识,能够把用户软件的操做,如置位启动传输位等也包含在事件范围内。由此,能够看出,任何一个DMA动做都是由事件触发完成的。用户在使用DMA控制器时只需设计好事件与DMA操做的关联便可。要充分的使用DMA控制器,熟悉DMA各类工做模式的原理是颇有必要的。
传输模式二:字符匹配终止模式
字符匹配模式用于传输不定长字节,而又有传输终止标识字节的应用环境中,Uart是这种模式的应用案例。
DMA通道的自动使能模式
DMA每一个通道在正常的块传输、终结字符匹配后或者因异常ABORT后,通道自动禁能。若是该通道有屡次的块传输,须要手动的使能通道;为了省却该操做,DCHXCON寄存器提供了容许自动使能通道的位CHAEN(channel auto enable)。通道使能位CHEN在取消传输或ABORT事件发生时会被置为0。
注:
一、通道起始/终止/中止中断事件独立于中断控制器,所以相应的中断无需使能,也无需在DMA传输后清除相应的位;
二、通道优先级和选择
DMA控制器每一个通道有一个天然的优先级,CH0默认为最高,CH4默认为最低;通道寄存器DCHXCON中提供了修改优先级的控制位。优先级控制了通道的传输顺序。
三、DMA传输中的字节对齐
PIC32采用的数据总线是32位,4字节;无疑访问地址为4字节对齐的访问效率最高,可是,若是把全部的常量或变量存储地址都限制在4字节对齐显然是不可能的;DMA中在处理这个问题上采用的字节对齐方法(存储方式为LSB)。举例来讲,若是当前物理地址与4的模为0,则取4字节;模为1,则取高3字节;模为2,则取高2字节;模为3,则取高1字节。
物理地址为0x1230,模为0,则取从0x1230处4字节数据;
物理地址为0x1231,模为1,则取从0x1231处3字节数据;
物理地址为0x1232,模为2,则取从0x1232处2字节数据;
物理地址为0x1233,模为3,则取从0x1233处1字节数据;
读/写过程均采起相同的字节对齐机制。DMA传输中的字节对齐过程如图2.
直接存储器存取(DMA)控制器是一种在系统内部转移数据的独特外设,能够将其视为一种可以经过一组专用总线将内部和外部存储器与每一个具备DMA能力的外设链接起来的控制器。它之因此属于外设,是由于它是在处理器的编程控制下来执行传输的。值得注意的是,一般只有数据流量较大(kBps或者更高)的外设才须要支持DMA能力,这些应用方面典型的例子包括视频、音频和网络接口。
通常而言,DMA控制器将包括一条地址总线、一条数据总线和控制寄存器。高效率的DMA控制器将具备访问其所须要的任意资源的能力,而无须处理器自己的介入,它必须能产生中断。最后,它必须能在控制器内部计算出地址。
一个处理器能够包含多个DMA控制器。每一个控制器有多个DMA通道,以及多条直接与存储器站(memory bank)和外设链接的总线,如图1所示。在不少高性能处理器中集成了两种类型的DMA控制器。第一类一般称为“系统DMA控制器”,能够实现对任何资源(外设和存储器)的访问,对于这种类型的控制器来讲,信号周期数是以系统时钟(SCLK)来计数的,以ADI的Blackfin处理器为例,频率最高可达133MHz。第二类称为内部存储器DMA控制器(IMDMA),专门用于内部存储器所处位置之间的相互存取操做。由于存取都发生在内部(L1-L一、L1-L2,或者L2-L2),周期数的计数则以内核时钟(CCLK)为基准来进行,该时钟的速度能够超过600MHz。
每一个DMA控制器有一组FIFO,起到DMA子系统和外设或存储器之间的缓冲器的做用。对于MemDMA(Memory DMA)来讲,传输的源端和目标端都有一组FIFO存在。当资源紧张而不能完成数据传输的话,则FIFO能够提供数据的暂存区,从而提升性能。
由于一般会在代码初始化过程当中对DMA控制器进行配置,内核就只须要在数据传输完成后对中断作出响应便可。你能够对DMA控制进行编程,让其与内核并行地移动数据,而同时让内核执行其基本的处理任务—那些应该让它专一完成的工做。

图1:系统和存储器DMA架构。
在一个优化的应用中,内核永远不用参与任何数据的移动,而仅仅对L1存储器中的数据进行读写。因而,内核不须要等待数据的到来,由于DMA引擎会在内核准备读取数据以前将数据准备好。图2给出了处理器和DMA控制器间的交互关系。由处理器完成的操做步骤包括:创建传输,启用中断,生成中断时执行代码。返回处处理器的中断输入能够用来指示“数据已经准备好,可进行处理”。

图2:DMA控制器。
数据除了往来外设以外,还须要从一个存储器空间转移到另外一个空间中。例如,视频源能够从一个视频端口直接流入L3存储器,由于工做缓冲区规模太大,没法放入到存储器中。咱们并不但愿让处理器在每次须要执行计算时都从外部存储读取像素信息,所以为了提升存取的效率,能够用一个存储器到存储器的DMA(MemDMA)来将像素转移到L1或者L2存储器中。
到目前为之,咱们还仅专一于数据的移动,可是DMA的传送能力并不老是用来移动数据。
在最简单的MemDMA状况中,咱们须要告诉DMA控制器源端地址、目标端地址和待传送的字的个数。每次传输的字的大小能够是八、16或者12位。 咱们只须要改变数据传输每次的数据大小,就能够简单地增长DMA的灵活性。例如,采用非单一大小的传输方式时,咱们以传输数据块的大小的倍数来做为地址增量。也就是说,若规定32位的传输和4个采样的跨度,则每次传输结束后,地址的增量为16字节(4个32位字)。
DMA的设置
目前有两类主要的DMA传输结构:寄存器模式和描述符模式。不管属于哪一类DMA,表1所描述的几类信息都会在DMA控制器中出现。当DMA以寄存器模式工做时,DMA控制器只是简单地利用寄存器中所存储的参数值。在描述符模式中,DMA控制器在存储器中查找本身的配置参数。

表1:DMA寄存器
基于寄存器的DMA
在基于寄存器的DMA内部,处理器直接对DMA控制寄存器进行编程,来启动传输。基于寄存器的DMA提供了最佳的DMA控制器性能,由于寄存器并不须要不断地从存储器中的描述符上载入数据,而内核也不须要保持描述符。
基于寄存器的DMA由两种子模式组成:自动缓冲(Autobuffer)模式和中止模式。在自动缓冲DMA中,当一个传输块传输完毕,控制寄存器就自动从新载入其最初的设定值,同一个DMA进程从新启动,开销为零。
正如咱们在图3中所看到的那样,若是将一个自动缓冲DMA设定为从外设传输必定数量的字到L1数据存储器的缓冲器上,则DMA控制器将会在最后一个字传输完成的时刻就迅速从新载入初始的参数。这构成了一个“循环缓冲器”,由于当一个量值被写入到缓冲器的最后一个位置上时,下一个值将被写入到缓冲器的第一个位置上。

图3:用DMA实现循环缓冲器。
自动缓冲DMA特别适合于对性能敏感的、存在持续数据流的应用。DMA控制器能够在独立于处理器其余活动的状况下读入数据流,而后在每次传输结束时,向内核发出中断。
中止模式的工做方式与自动缓冲DMA相似,区别在于各寄存器在DMA结束后不会从新载入,所以整个DMA传输只发生一次。中止模式对于基于某种事件的一次性传输来讲十分有用。例如,非按期地将数据块从一个位置转移到另外一个位置。当你须要对事件进行同步时,这种模式也很是有用。例如,若是一个任务必须在下一次传输前完成的话,则中止模式能够确保各事件发生的前后顺序。此外,中止模式对于缓冲器的初始化来讲很是有用。
描述符模型
基于描述符(descriptor)的DMA要求在存储器中存入一组参数,以启动DMA的系列操做。该描述符所包含的参数与那些一般经过编程写入DMA控制寄存器组的全部参数相同。不过,描述符还能够允许多个DMA操做序列串在一块儿。在基于描述符的DMA操做中,咱们能够对一个DMA通道进行编程,在当前的操做序列完成后,自动设置并启动另外一次DMA传输。基于描述符的方式为管理系统中的DMA传输提供了最大的灵活性。
ADI 的Blackfin处理器上有两种主要的描述符方式—描述符阵列和描述符列表,这两种操做方式所要实现的目标是在灵活性和性能之间实现一种折中平衡。
DMA 方式, 即外设在专用的接口电路DMA 控制器的控制下直接和存储器进行高速数据传送。采用DMA 方式时,如外设
须要进行数据传输, 首先向DMA 控制器发出请求,DMA 再向CPU 发出总线请求,要求控制系统总线。CPU 响应DMA 控制器
的总线请求并把总线控制权交给DMA, 而后在DMA 的控制下开始利用系统总线进行数据传输。数据传输结束后,DMA 并回
总线控制权。DMA 操做步骤:
(1) DMA 控制器的初始化
(2) DMA 数据传送
(3) DMA 结束
DMA 初始化预置以下信息:一是指定I/O 设备对外设"读"仍是"写",即指定其控制/状态寄存器中相应的控制位;二是数据应传送至何处,指定其地址的首地址;三是有多少数据字须要传送。
DMA原理解析
DMA概念
DMA(Direct Memory Access,直接内存存取) ,DMA 传输将数据从一个地址空间复制到另一个地址空间。采用CPU来初始化这个传输动做,可是传输动做自己是由 DMA 控制器来实行和完成,不须要占用CPU。
DMA控制器(以2440为例)
2440芯片手册第8章为DMA控制器。
2440的DMA控制器支持4个通道。
请求源:
上图是2440中DMA控制器支持的请求源。
基本时序
在请求信号有效以后,通过2个周期DACK信号有效,再通过3个周期,DMA控制器才可得到总线的控制权,开始读写操做。
工做模式
Demond模式:
若是DMA完成一次请求后若是Request仍然有效,那么DMA就认为这是下一次DMA请求,并当即开始下一次的传输。
Handshake模式:
DMA完成一次请求后等待Request信号无效,若是Request 无效,DMA会无效ACK两个时钟周期,再等待下一次Request。
6410芯片的DMA控制器在芯片手册的第11章。
DMA程序设计(2440芯片)
char *buf = "Hello World!";
#define DISRC0 (*(volatile unsigned long*)0x4B000000)
#define DISRCC0 (*(volatile unsigned long*)0x4B000004) #define DIDST0 (*(volatile unsigned long*)0x4B000008) #define DIDSTC0 (*(volatile unsigned long*)0x4B00000C) #define DCON0 (*(volatile unsigned long*)0x4B000010) #define DMASKTRIG0 (*(volatile unsigned long*)0x4B000020) #define UTXH0 (volatile unsigned long*)0x50000020 void dma_init() {
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
DMA程序设计(6410芯片)
Linux内核DMA机制
DMA控制器硬件结构
DMA容许外围设备和主内存之间直接传输 I/O 数据, DMA 依赖于系统。每一种体系结构DMA传输不一样,编程接口也不一样。
数据传输能够以两种方式触发:一种软件请求数据,另外一种由硬件异步传输。
在第一种状况下,调用的步骤能够归纳以下(以read为例):
(1)在进程调用 read 时,驱动程序的方法分配一个 DMA 缓冲区,随后指示硬件传送它的数据。进程进入睡眠。
(2)硬件将数据写入 DMA 缓冲区并在完成时产生一个中断。
(3)中断处理程序得到输入数据,应答中断,最后唤醒进程,该进程如今能够读取数据了。
第二种情形是在 DMA 被异步使用时发生的。以数据采集设备为例:
(1)硬件发出中断来通知新的数据已经到达。
(2)中断处理程序分配一个DMA缓冲区。
(3)外围设备将数据写入缓冲区,而后在完成时发出另外一个中断。
(4)处理程序利用DMA分发新的数据,唤醒任何相关进程。
网卡传输也是如此,网卡有一个循环缓冲区(一般叫作 DMA 环形缓冲区)创建在与处理器共享的内存中。每个输入数据包被放置在环形缓冲区中下一个可用缓冲区,而且发出中断。而后驱动程序将网络数据包传给内核的其它部分处理,并在环形缓冲区中放置一个新的 DMA 缓冲区。
驱动程序在初始化时分配DMA缓冲区,并使用它们直到中止运行。
DMA控制器依赖于平台硬件,这里只对i386的8237 DMA控制器作简单的说明,它有两个控制器,8个通道,具体说明以下:
控制器1: 通道0-3,字节操做, 端口为 00-1F
控制器2: 通道 4-7, 字操做, 端口咪 C0-DF
- 全部寄存器是8 bit,与传输大小无关。
- 通道 4 被用来将控制器1与控制器2级联起来。
- 通道 0-3 是字节操做,地址/计数都是字节的。
- 通道 5-7 是字操做,地址/计数都是以字为单位的。
- 传输器对于(0-3通道)必须不超过64K的物理边界,对于5-7必须不超过128K边界。
- 对于5-7通道page registers 不用数据 bit 0, 表明128K页
- 对于0-3通道page registers 使用 bit 0, 表示 64K页
DMA 传输器限制在低于16M物理内存里。装入寄存器的地址必须是物理地址,而不是逻辑地址。
对于0-3通道来讲地址对寄存器的映射以下:
A23 ... A16 A15 ... A8 A7 ... A0 (物理地址)
| ... | | ... | | ... | | ... | | ... | | ... | | ... | | ... | | ... | P7 ... P0 A7 ... A0 A7 ... A0 | Page | Addr MSB | Addr LSB | (DMA 地址寄存器)
对于5-7通道来讲地址对寄存器的映射以下:
A23 ... A17 A16 A15 ... A9 A8 A7 ... A1 A0 (物理地址)
| ... | \ \ ... \ \ \ ... \ \
| ... | \ \ ... \ \ \ ... \ (没用) | ... | \ \ ... \ \ \ ... \ P7 ... P1 (0) A7 A6 ... A0 A7 A6 ... A0
| Page | Addr MSB | Addr LSB | (DMA 地址寄存器)
通道 5-7 传输以字为单位, 地址和计数都必须是以字对齐的。
在include/asm-i386/dma.h中有i386平台的8237 DMA控制器的各处寄存器的地址及寄存器的定义,这里只对控制寄存器加以说明:
DMA Channel Control/Status Register (DCSRX)
第31位 代表是否开始
第30位 选定Descriptor和Non-Descriptor模式
第29位 判断有无中断
第8位 请求处理 (Request Pending)
第3位 Channel是否运行
第2位 当前数据交换是否完成
第1位 是否由Descriptor产生中断
第0位 是否由总线错误引发中断
Linux 内核DMA机制
DMA控制器硬件结构
DMA容许外围设备和主内存之间直接传输 I/O 数据, DMA 依赖于系统。每一种体系结构DMA传输不一样,编程接口也不一样。
数据传输能够以两种方式触发:一种软件请求数据,另外一种由硬件异步传输。
在第一种状况下,调用的步骤能够归纳以下(以read为例):
(1)在进程调用 read 时,驱动程序的方法分配一个 DMA 缓冲区,随后指示硬件传送它的数据。进程进入睡眠。
(2)硬件将数据写入 DMA 缓冲区并在完成时产生一个中断。
(3)中断处理程序得到输入数据,应答中断,最后唤醒进程,该进程如今能够读取数据了。
第二种情形是在 DMA 被异步使用时发生的。以数据采集设备为例:
(1)硬件发出中断来通知新的数据已经到达。
(2)中断处理程序分配一个DMA缓冲区。
(3)外围设备将数据写入缓冲区,而后在完成时发出另外一个中断。
(4)处理程序利用DMA分发新的数据,唤醒任何相关进程。
网卡传输也是如此,网卡有一个循环缓冲区(一般叫作 DMA 环形缓冲区)创建在与处理器共享的内存中。每个输入数据包被放置在环形缓冲区中下一个可用缓冲区,而且发出中断。而后驱动程序将网络数据包传给内核的其它部分处理,并在环形缓冲区中放置一个新的 DMA 缓冲区。
驱动程序在初始化时分配DMA缓冲区,并使用它们直到中止运行。
DMA控制器依赖于平台硬件,这里只对i386的8237 DMA控制器作简单的说明,它有两个控制器,8个通道,具体说明以下:
控制器1: 通道0-3,字节操做, 端口为 00-1F
控制器2: 通道 4-7, 字操做, 端口咪 C0-DF
- 全部寄存器是8 bit,与传输大小无关。
- 通道 4 被用来将控制器1与控制器2级联起来。
- 通道 0-3 是字节操做,地址/计数都是字节的。
- 通道 5-7 是字操做,地址/计数都是以字为单位的。
- 传输器对于(0-3通道)必须不超过64K的物理边界,对于5-7必须不超过128K边界。
- 对于5-7通道page registers 不用数据 bit 0, 表明128K页
- 对于0-3通道page registers 使用 bit 0, 表示 64K页
DMA 传输器限制在低于16M物理内存里。装入寄存器的地址必须是物理地址,而不是逻辑地址。
在include/asm-i386/dma.h中有i386平台的8237 DMA控制器的各处寄存器的地址及寄存器的定义,这里只对控制寄存器加以说明:
DMA Channel Control/Status Register (DCSRX)
第31位 代表是否开始
第30位选定Descriptor和Non-Descriptor模式
第29位 判断有无中断
第8位 请求处理 (Request Pending)
第3位 Channel是否运行
第2位 当前数据交换是否完成
第1位是否由Descriptor产生中断
第0位 是否由总线错误引发中断
DMA通道使用的地址
DMA通道用dma_chan结构数组表示,这个结构在kernel/dma.c中,列出以下:
- struct dma_chan {
- int lock;
- const char *device_id;
- };
-
- static struct dma_chan dma_chan_busy[MAX_DMA_CHANNELS] = {
- [4] = { 1, "cascade" },
- };
若是dma_chan_busy[n].lock != 0表示忙,DMA0保留为DRAM更新用,DMA4用做级联。DMA 缓冲区的主要问题是,当它大于一页时,它必须占据物理内存中的连续页。
因为DMA须要连续的内存,于是在引导时分配内存或者为缓冲区保留物理 RAM 的顶部。在引导时给内核传递一个"mem="参数能够保留 RAM 的顶部。例如,若是系统有 32MB 内存,参数"mem=31M"阻止内核使用最顶部的一兆字节。稍后,模块能够使用下面的代码来访问这些保留的内存:
- dmabuf = ioremap( 0x1F00000
分配 DMA 空间的方法,代码调用 kmalloc(GFP_ATOMIC) 直到失败为止,而后它等待内核释放若干页面,接下来再一次进行分配。最终会发现由连续页面组成的DMA 缓冲区的出现。
一个使用 DMA 的设备驱动程序一般会与链接到接口总线上的硬件通信,这些硬件使用物理地址,而程序代码使用虚拟地址。基于 DMA 的硬件使用总线地址而不是物理地址,有时,接口总线是经过将 I/O 地址映射到不一样物理地址的桥接电路链接的。甚至某些系统有一个页面映射方案,可以使任意页面在外围总线上表现为连续的。
当驱动程序须要向一个 I/O 设备(例如扩展板或者DMA控制器)发送地址信息时,必须使用 virt_to_bus 转换,在接受到来自链接到总线上硬件的地址信息时,必须使用 bus_to_virt 了。
DMA操做函数
由于 DMA 控制器是一个系统级的资源,因此内核协助处理这一资源。内核使用 DMA 注册表为 DMA 通道提供了请求/释放机制,而且提供了一组函数在 DMA 控制器中配置通道信息。
DMA 控制器使用函数request_dma和free_dma来获取和释放 DMA 通道的全部权,请求 DMA 通道应在请求了中断线以后,而且在释放中断线以前释放它。每个使用 DMA 的设备也必须使用中断信号线,不然就没法发出数据传输完成的通知。这两个函数的声明列出以下(在kernel/dma.c中):
- int request_dma(unsigned int channel, const char *name);
- void free_dma(unsigned int channel);
DMA 控制器被dma_spin_lock 的自旋锁所保护。使用函数claim_dma_lock和release_dma_lock对得到和释放自旋锁。这两个函数的声明列出以下(在kernel/dma.c中):
unsigned long claim_dma_lock(); 获取 DMA 自旋锁,该函数会阻塞本地处理器上的中断,所以,其返回值是"标志"值,在从新打开中断时必须使用该值。
void release_dma_lock(unsigned long flags); 释放 DMA 自旋锁,而且恢复之前的中断状态。
DMA 控制器的控制设置信息由RAM 地址、传输的数据(以字节或字为单位),以及传输的方向三部分组成。下面是i386平台的8237 DMA控制器的操做函数说明(在include/asm-i386/dma.h中),使用这些函数设置DMA控制器时,应该持有自旋锁。但在驱动程序作I/O 操做时,不能持有自旋锁。
void set_dma_mode(unsigned int channel, char mode); 该函数指出通道从设备读(DMA_MODE_WRITE)或写(DMA_MODE_READ)数据方式,当mode设置为 DMA_MODE_CASCADE时,表示释放对总线的控制。
void set_dma_addr(unsigned int channel, unsigned int addr); 函数给 DMA 缓冲区的地址赋值。该函数将 addr 的最低 24 位存储到控制器中。参数 addr 是总线地址。
void set_dma_count(unsigned int channel, unsigned int count);该函数对传输的字节数赋值。参数 count 也表明 16 位通道的字节数,在此状况下,这个数字必须是偶数。
除了这些操做函数外,还有些对DMA状态进行控制的工具函数:
void disable_dma(unsigned int channel); 该函数设置禁止使用DMA 通道。这应该在配置 DMA 控制器以前设置。
void enable_dma(unsigned int channel); 在DMA 通道中包含了合法的数据时,该函数激活DMA 控制器。
int get_dma_residue(unsigned int channel); 该函数查询一个 DMA 传输还有多少字节还没传输完。函数返回没传完的字节数。当传输成功时,函数返回值是0。
void clear_dma_ff(unsigned int channel) 该函数清除 DMA 触发器(flip-flop),该触发器用来控制对 16 位寄存器的访问。能够经过两个连续的 8 位操做来访问这些寄存器,触发器被清除时用来选择低字节,触发器被置位时用来选择高字节。在传输 8 位后,触发器会自动反转;在访问 DMA 寄存器以前,程序员必须清除触发器(将它设置为某个已知状态)。
DMA映射
一个DMA映射就是分配一个 DMA 缓冲区并为该缓冲区生成一个可以被设备访问的地址的组合操做。通常状况下,简单地调用函数virt_to_bus 就设备总线上的地址,但有些硬件映射寄存器也被设置在总线硬件中。映射寄存器(mapping register)是一个相似于外围设备的虚拟内存等价物。在使用这些寄存器的系统上,外围设备有一个相对较小的、专用的地址区段,能够在此区段执行 DMA。经过映射寄存器,这些地址被重映射到系统 RAM。映射寄存器具备一些好的特性,包括使分散的页面在设备地址空间看起来是连续的。但不是全部的体系结构都有映射寄存器,特别地,PC 平台没有映射寄存器。
在某些状况下,为设备设置有用的地址也意味着须要构造一个反弹(bounce)缓冲区。例如,当驱动程序试图在一个不能被外围设备访问的地址(一个高端内存地址)上执行 DMA 时,反弹缓冲区被建立。而后,按照须要,数据被复制到反弹缓冲区,或者从反弹缓冲区复制。
根据 DMA 缓冲区指望保留的时间长短,PCI 代码区分两种类型的 DMA 映射:
一致 DMA 映射 它们存在于驱动程序的生命周期内。一个被一致映射的缓冲区必须同时可被 CPU 和外围设备访问,这个缓冲区被处理器写时,可当即被设备读取而没有cache效应,反之亦然,使用函数pci_alloc_consistent创建一致映射。
流式 DMA映射 流式DMA映射是为单个操做进行的设置。它映射处理器虚拟空间的一块地址,以至它能被设备访问。应尽量使用流式映射,而不是一致映射。这是由于在支持一致映射的系统上,每一个 DMA 映射会使用总线上一个或多个映射寄存器。具备较长生命周期的一致映射,会独占这些寄存器很长时间――即便它们没有被使用。使用函数dma_map_single创建流式映射。
(1)创建一致 DMA 映射
函数pci_alloc_consistent处理缓冲区的分配和映射,函数分析以下(在include/asm-generic/pci-dma-compat.h中):
- static inline void *pci_alloc_consistent(struct pci_dev *hwdev, size_t size, dma_addr_t *dma_handle)
- {
- return dma_alloc_coherent(hwdev == NULL ? NULL : &hwdev->dev, size, dma_handle, GFP_ATOMIC);
- }
结构dma_coherent_mem定义了DMA一致性映射的内存的地址、大小和标识等。结构dma_coherent_mem列出以下(在arch/i386/kernel/pci-dma.c中):
- struct dma_coherent_mem {
- void *virt_base;
- u32 device_base;
- int size;
- int flags;
- unsigned long *bitmap;
- };
函数dma_alloc_coherent分配size字节的区域的一致内存,获得的dma_handle是指向分配的区域的地址指针,这个地址做为区域的物理基地址。dma_handle是与总线同样的位宽的无符号整数。 函数dma_alloc_coherent分析以下(在arch/i386/kernel/pci-dma.c中):
- void *dma_alloc_coherent(struct device *dev, size_t size, dma_addr_t *dma_handle, int gfp)
- {
- void *ret;
- struct dma_coherent_mem *mem = dev ? dev->dma_mem : NULL;
- int order = get_order(size);
- gfp &= ~(__GFP_DMA | __GFP_HIGHMEM);
-
- if (mem) {
-
- int page = bitmap_find_free_region(mem->bitmap, mem->size, order);
- if (page >= 0) {
- *dma_handle = mem->device_base + (page << PAGE_SHIFT);
- ret = mem->virt_base + (page << PAGE_SHIFT);
- memset(ret, 0, size);
- return ret;
- }
- if (mem->flags & DMA_MEMORY_EXCLUSIVE)
- return NULL;
- }
-
- if (dev == NULL || (dev->coherent_dma_mask < 0xffffffff))
- gfp |= GFP_DMA;
-
- ret = (void *)__get_free_pages(gfp, order);
-
- if (ret != NULL) {
- memset(ret, 0, size);
- *dma_handle = virt_to_phys(ret);
- }
- return ret;
- }
当再也不须要缓冲区时(一般在模块卸载时),应该调用函数 pci_free_consitent 将它返还给系统。
(2)创建流式 DMA 映射
在流式 DMA 映射的操做中,缓冲区传送方向应匹配于映射时给定的方向值。缓冲区被映射后,它就属于设备而再也不属于处理器了。在缓冲区调用函数pci_unmap_single撤销映射以前,驱动程序不该该触及其内容。
在缓冲区为 DMA 映射时,内核必须确保缓冲区中全部的数据已经被实际写到内存。可能有些数据还会保留在处理器的高速缓冲存储器中,所以必须显式刷新。在刷新以后,由处理器写入缓冲区的数据对设备来讲也许是不可见的。
若是欲映射的缓冲区位于设备不能访问的内存区段时,某些体系结构仅仅会操做失败,而其它的体系结构会建立一个反弹缓冲区。反弹缓冲区是被设备访问的独立内存区域,反弹缓冲区复制原始缓冲区的内容。
函数pci_map_single映射单个用于传送的缓冲区,返回值是能够传递给设备的总线地址,若是出错的话就为 NULL。一旦传送完成,应该使用函数pci_unmap_single 删除映射。其中,参数direction为传输的方向,取值以下:
PCI_DMA_TODEVICE 数据被发送到设备。
PCI_DMA_FROMDEVICE若是数据将发送到 CPU。
PCI_DMA_BIDIRECTIONAL数据进行两个方向的移动。
PCI_DMA_NONE 这个符号只是为帮助调试而提供。
函数pci_map_single分析以下(在arch/i386/kernel/pci-dma.c中):
- static inline dma_addr_t pci_map_single(struct pci_dev *hwdev, void *ptr, size_t size, int direction)
- {
- return dma_map_single(hwdev == NULL ? NULL : &hwdev->dev, ptr, size, (enum ma_data_direction)direction);
- }
函数dma_map_single映射一块处理器虚拟内存,这块虚拟内存能被设备访问,返回内存的物理地址,函数dma_map_single分析以下(在include/asm-i386/dma-mapping.h中):
- static inline dma_addr_t dma_map_single(struct device *dev, void *ptr, size_t size, enum dma_data_direction direction)
- {
- BUG_ON(direction == DMA_NONE);
-
- flush_write_buffers();
- return virt_to_phys(ptr);
- }
(3)分散/集中映射
分散/集中映射是流式 DMA 映射的一个特例。它将几个缓冲区集中到一块儿进行一次映射,并在一个 DMA 操做中传送全部数据。这些分散的缓冲区由分散表结构scatterlist来描述,多个分散的缓冲区的分散表结构组成缓冲区的struct scatterlist数组。
分散表结构列出以下(在include/asm-i386/scatterlist.h):
- struct scatterlist {
- struct page *page;
- unsigned int offset;
- dma_addr_t dma_address;
- unsigned int length;
- };
每个缓冲区的地址和长度会被存储在 struct scatterlist 项中,但在不一样的体系结构中它们在结构中的位置是不一样的。下面的两个宏定义来解决平台移植性问题,这些宏定义应该在一个pci_map_sg 被调用后使用:
- #define sg_dma_address(sg) ((sg)->dma_address) //从该分散表项中返回总线地址
- #define sg_dma_len(sg) ((sg)->length) //返回该缓冲区的长度
函数pci_map_sg完成分散/集中映射,其返回值是要传送的 DMA 缓冲区数;它可能会小于 nents(也就是传入的分散表项的数量),由于可能有的缓冲区地址上是相邻的。一旦传输完成,分散/集中映射经过调用函数pci_unmap_sg 来撤销映射。 函数pci_map_sg分析以下(在include/asm-generic/pci-dma-compat.h中):
- static inline int pci_map_sg(struct pci_dev *hwdev, struct scatterlist *sg, int nents, int direction)
- {
- return dma_map_sg(hwdev == NULL ? NULL : &hwdev->dev, sg, nents, (enum dma_data_direction)direction);
- }
-
- static inline int dma_map_sg(struct device *dev, struct scatterlist *sg, int nents, enum dma_data_direction direction)
- {
- int i;
- BUG_ON(direction == DMA_NONE);
- for (i = 0; i < nents; i++ ) {
- BUG_ON(!sg[i].page);
-
- sg[i].dma_address = page_to_phys(sg[i].page) + sg[i].offset;
- }
-
- flush_write_buffers();
- return nents;
- }
DMA池
许多驱动程序须要又多又小的一致映射内存区域给DMA描述子或I/O缓存buffer,这使用DMA池比用dma_alloc_coherent分配的一页或多页内存区域好,DMA池用函数dma_pool_create建立,用函数dma_pool_alloc从DMA池中分配一块一致内存,用函数dmp_pool_free放内存回到DMA池中,使用函数dma_pool_destory释放DMA池的资源。
结构dma_pool是DMA池描述结构,列出以下:
- struct dma_pool {
- struct list_head page_list;
- spinlock_t lock;
- size_t blocks_per_page;
- size_t size;
- struct device *dev;
- size_t allocation;
- char name [32];
- wait_queue_head_t waitq;
- struct list_head pools;
- };
函数dma_pool_create给DMA建立一个一致内存块池,其参数name是DMA池的名字,用于诊断用,参数dev是将作DMA的设备,参数size是DMA池里的块的大小,参数align是块的对齐要求,是2的幂,参数allocation返回没有跨越边界的块数(或0)。
函数dma_pool_create返回建立的带有要求字符串的DMA池,若建立失败返回null。对被给的DMA池,函数dma_pool_alloc被用来分配内存,这些内存都是一致DMA映射,可被设备访问,且没有使用缓存刷新机制,由于对齐缘由,分配的块的实际尺寸比请求的大。若是分配非0的内存,从函数dma_pool_alloc返回的对象将不跨越size边界(如不跨越4K字节边界)。这对在个体的DMA传输上有地址限制的设备来讲是有利的。
函数dma_pool_create分析以下(在drivers/base/dmapool.c中):
- struct dma_pool *dma_pool_create (const char *name, struct device *dev, size_t size, size_t align, size_t allocation)
- {
- struct dma_pool *retval;
- if (align == 0)
- align = 1;
- if (size == 0)
- return NULL;
- else if (size < align)
- size = align;
- else if ((size % align) != 0) {
- size += align + 1;
- size &= ~(align - 1);
- }
-
- if (allocation == 0) {
- if (PAGE_SIZE < size)
- allocation = size;
- else
- allocation = PAGE_SIZE;
-
- } else if (allocation < size)
- return NULL;
-
- if (!(retval = kmalloc (sizeof *retval, SLAB_KERNEL)))
- return retval;
-
- strlcpy (retval->name, name, sizeof retval->name);
-
- retval->dev = dev;
-
- INIT_LIST_HEAD (&retval->page_list);
- spin_lock_init (&retval->lock);
- retval->size = size;
- retval->allocation = allocation;
- retval->blocks_per_page = allocation / size;
- init_waitqueue_head (&retval->waitq);
-
- if (dev) {
- down (&pools_lock);
- if (list_empty (&dev->dma_pools))
-
- device_create_file (dev, &dev_attr_pools);
-
- list_add (&retval->pools, &dev->dma_pools);
- up (&pools_lock);
- } else
- INIT_LIST_HEAD (&retval->pools);
- return retval;
- }
函数dma_pool_alloc从DMA池中分配一块一致内存,其参数pool是将产生块的DMA池,参数mem_flags是GFP_*位掩码,参数handle是指向块的DMA地址,函数dma_pool_alloc返回当前没用的块的内核虚拟地址,并经过handle给出它的DMA地址,若是内存块不能被分配,返回null。
函数dma_pool_alloc包裹了dma_alloc_coherent页分配器,这样小块更容易被总线的主控制器使用。这可能共享slab分配器的内容。
函数dma_pool_alloc分析以下(在drivers/base/dmapool.c中):
- void *dma_pool_alloc (struct dma_pool *pool, int mem_flags, dma_addr_t *handle)
- {
- unsigned long flags;
- struct dma_page *page;
- int map, block;
- size_t offset;
- void *retval;
-
- restart:
- spin_lock_irqsave (&pool->lock, flags);
- list_for_each_entry(page, &pool->page_list, page_list) {
- int i;
-
-
- for (map = 0, i = 0; i < pool->blocks_per_page;
- if (page->bitmap [map] == 0)
- continue;
- block = ffz (~ page->bitmap [map]);
- if ((i + block) < pool->blocks_per_page) {
- clear_bit (block, &page->bitmap [map]);
-
- offset = (BITS_PER_LONG * map) + block;
- offset *= pool->size;
- goto ready;
- }
- }
- }
-
-
- if (!(page = pool_alloc_page (pool, SLAB_ATOMIC))) {
- if (mem_flags & __GFP_WAIT) {
- DECLARE_WAITQUEUE (wait, current);
- current->state = TASK_INTERRUPTIBLE;
- add_wait_queue (&pool->waitq, &wait);
- spin_unlock_irqrestore (&pool->lock, flags);
- schedule_timeout (POOL_TIMEOUT_JIFFIES);
- remove_wait_queue (&pool->waitq, &wait);
- goto restart;
- }
- retval = NULL;
- goto done;
- }
- clear_bit (0, &page->bitmap [0]);
- offset = 0;
-
- ready:
- page->in_use++;
- retval = offset + page->vaddr;
- *handle = offset + page->dma;
- #ifdef CONFIG_DEBUG_SLAB
- memset (retval, POOL_POISON_ALLOCATED, pool->size);
- #endif
-
- done:
- spin_unlock_irqrestore (&pool->lock, flags);
- return retval;
- }
一个简单的使用DMA 例子
示例:下面是一个简单的使用DMA进行传输的驱动程序,它是一个假想的设备,只列出DMA相关的部分来讲明驱动程序中如何使用DMA的。
函数dad_transfer是设置DMA对内存buffer的传输操做函数,它使用流式映射将buffer的虚拟地址转换到物理地址,设置好DMA控制器,而后开始传输数据。
- int dad_transfer(struct dad_dev *dev, int write, void *buffer, size_t count)
- {
- dma_addr_t bus_addr;
- unsigned long flags;
-
-
- dev->dma_dir = (write ? PCI_DMA_TODEVICE : PCI_DMA_FROMDEVICE);
- dev->dma_size = count;
-
- bus_addr = pci_map_single(dev->pci_dev, buffer, count, dev->dma_dir);
- dev->dma_addr = bus_addr;
-
-
- writeb(dev->registers.command, DAD_CMD_DISABLEDMA);
-
- writeb(dev->registers.command, write ? DAD_CMD_WR : DAD_CMD_RD);
- writel(dev->registers.addr, cpu_to_le32(bus_addr));
- writel(dev->registers.len, cpu_to_le32(count));
-
-
- writeb(dev->registers.command, DAD_CMD_ENABLEDMA);
- return 0;
- }
-
- void dad_interrupt(int irq, void *dev_id, struct pt_regs *regs)
- {
- struct dad_dev *dev = (struct dad_dev *) dev_id;
-
-
- pci_unmap_single(dev->pci_dev, dev->dma_addr, dev->dma_size, dev->dma_dir);
-
-
- ...
- }
-
- int dad_open (struct inode *inode, struct file *filp)
- {
- struct dad_device *my_device;
-
- if ( (error = request_irq(my_device.irq, dad_interrupt, SA_INTERRUPT, "dad", NULL)) )
- return error;
-
- if ( (error = request_dma(my_device.dma, "dad")) ) {
- free_irq(my_device.irq, NULL);
- return error;
- }
- return 0;
- }
-
- void dad_close (struct inode *inode, struct file *filp)
- {
- struct dad_device *my_device;
- free_dma(my_device.dma);
- free_irq(my_device.irq, NULL);
- ……
- }
-
- int dad_dma_prepare(int channel, int mode, unsigned int buf, unsigned int count)
- {
- unsigned long flags;
- flags = claim_dma_lock();
- disable_dma(channel);
- clear_dma_ff(channel);
- set_dma_mode(channel, mode);
- set_dma_addr(channel, virt_to_bus(buf));
- set_dma_count(channel, count);
- enable_dma(channel);
- release_dma_lock(flags);
- return 0;
- }
-
- int dad_dma_isdone(int channel)
- {
- int residue;
- unsigned long flags = claim_dma_lock ();
- residue = get_dma_residue(channel);
- release_dma_lock(flags);
- return (residue == 0);
- }
方法之三:以数据结构为基点,举一反三
结构化程序设计思想认为:程序 =数据结构 +算法。数据结构体现了整个系统的构架,因此数据结构一般都是代码分析的很好的着手点,对Linux内核分析尤为如此。好比,把进程控制块结构分析清楚 了,就对进程有了基本的把握;再好比,把页目录结构和页表结构弄懂了,两级虚存映射和内存管理也就掌握得差很少了。为了体现按部就班的思想,在这我就以 Linux对中断机制的处理来介绍这种方法。
首先,必须指出的是:在此处,中断指广义的中断概义,它指全部经过idt进行的控制转移的机制和处理;它覆盖如下几个经常使用的概义:中断、异常、可屏蔽中断、不可屏蔽中断、硬中断、软中断 … … …
I、硬件提供的中断机制和约定
一.中断向量寻址:
硬件提供可供256个服务程序中断进入的入口,即中断向量;
中断向量在保护模式下的实现机制是中断描述符表idt,idt的位置由idtr肯定,idtr是个48位的寄存器,高32位是idt的基址,低16位为idt的界限(一般为2k=256*8);
idt中包含256个中断描述符,对应256个中断向量;每一个中断描述符8位,其结构如图一:

中断进入过程如图二所示。
当中断是由低特权级转到高特权级(即当前特权级CPL>DPL)时,将进行堆栈的转移;内层堆栈的选择由当前tss的相应字段肯定,并且内层堆栈将依次被压入以下数据:外层SS,外层ESP,EFLAGS,外层CS,外层EIP; 中断返回过程为一逆过程;

二.异常处理机制:
Intel公司保留0-31号中断向量用来处理异常事件:当产生一个异常时,处理机就会自动把控制转移到相应的处理程序的入口,异常的处理程序由操做系统提供,中断向量和异常事件对应如表一:
表1、中断向量和异常事件对应表
中断向量号 |
异常事件 |
Linux的处理程序 |
0 |
除法错误 |
Divide_error |
1 |
调试异常 |
Debug |
2 |
NMI中断 |
Nmi |
3 |
单字节,int 3 |
Int3 |
4 |
溢出 |
Overflow |
5 |
边界监测中断 |
Bounds |
6 |
无效操做码 |
Invalid_op |
7 |
设备不可用 |
Device_not_available |
8 |
双重故障 |
Double_fault |
9 |
协处理器段溢出 |
Coprocessor_segment_overrun |
10 |
无效TSS |
Incalid_tss |
11 |
缺段中断 |
Segment_not_present |
12 |
堆栈异常 |
Stack_segment |
13 |
通常保护异常 |
General_protection |
14 |
页异常 |
Page_fault |
15 |
|
Spurious_interrupt_bug |
16 |
协处理器出错 |
Coprocessor_error |
17 |
对齐检查中断 |
Alignment_check |
三.可编程中断控制器8259A:
为更好的处理外部设备,x86微机提供了两片可编程中断控制器,用来辅助cpu接受外部的中断信号;对于中断,cpu只提供两个外接引线:NMI和INTR;
NMI只能经过端口操做来屏蔽,它一般用于:电源掉电和物理存储器奇偶验错;
INTR可经过直接设置中断屏蔽位来屏蔽,它可用来接受外部中断信号,但只有一个引线,不够用;因此它经过外接两片级链了的8259A,以接受更多的外部中断信号。8259A主要完成这样一些任务:
- 中断优先级排队管理,
- 接受外部中断请求
- 向cpu提供中断类型号
外部设备产生的中断信号在IRQ(中断请求)管脚上首先由中断控制器处理。中断控制器可 以响应多个中断输入,它的输出链接到 CPU 的 INT 管脚,信号可经过INT 管脚,通知处理器产生了中断。若是 CPU 这时能够处理中断,CPU 会经过 INTA(中断确认)管脚上的信号通知中断控制器已接受中断,这时,中断控制器可将一个 8 位数据放置在数据总线上,这一 8 位数据也称为中断向量号,CPU 依据中断向量号和中断描述符表(IDT)中的信息自动调用相应的中断服务程序。图三中,两个中断控制器级联了起来,从属中断控制器的输出链接到了主中断控 制器的第 3 个中断信号输入,这样,该系统可处理的外部中断数量最多可达 15 个,图的右边是 i386 PC 中各中断输入管脚的通常分配。可经过对8259A的初始化,使这15个外接引脚对应256个中断向量的任何15个连续的向量;因为intel公司保留0- 31号中断向量用来处理异常事件(而默认状况下,IBM bios把硬中断设在0x08-0x0f),因此,硬中断必须设在31之后,linux则在实模式下初始化时把其设在0x20-0x2F,对此下面还将具 体说明。
图3、i386 PC 可编程中断控制器8259A级链示意图

II、Linux的中断处理
硬件中断机制提供了256个入口,即idt中包含的256个中断描述符(对应256个中断向量)。
而0-31号中断向量被intel公司保留用来处理异常事件,不能另做它用。对这 0-31号中断向量,操做系统只需提供异常的处理程序,当产生一个异常时,处理机就会自动把控制转移到相应的处理程序的入口,运行相应的处理程序;而事实 上,对于这32个处理异常的中断向量,此版本(2.2.5)的 Linux只提供了0-17号中断向量的处理程序,其对应处理程序参见表1、中断向量和异常事件对应表;也就是说,17-31号中断向量是空着未用的。
既然0-31号中断向量已被保留,那么,就是剩下32-255共224个中断向量可用。 这224个中断向量又是怎么分配的呢?在此版本(2.2.5)的Linux中,除了0x80 (SYSCALL_VECTOR)用做系统调用总入口以外,其余都用在外部硬件中断源上,其中包括可编程中断控制器8259A的15个irq;事实上,当 没有定义CONFIG_X86_IO_APIC时,其余223(除0x80外)个中断向量,只利用了从32号开始的15个,其它208个空着未用。
这些中断服务程序入口的设置将在下面有详细说明。
一.相关数据结构
- 中断描述符表idt: 也就是中断向量表,至关如一个数组,保存着各中断服务例程的入口。(详细描述参见图1、中断描述符格式)
- 与硬中断相关数据结构:
与硬中断相关数据结构主要有三个:
一:定义在/arch/i386/kernel/irq.h中的
struct hw_interrupt_type {
const char * typename;
void (*startup)(unsigned int irq);
void (*shutdown)(unsigned int irq);
void (*handle)(unsigned int irq, struct pt_regs * regs);
void (*enable)(unsigned int irq);
void (*disable)(unsigned int irq);
};
二:定义在/arch/i386/kernel/irq.h中的
typedef struct {
unsigned int status; /* IRQ status - IRQ_INPROGRESS, IRQ_DISABLED */
struct hw_interrupt_type *handler; /* handle/enable/disable functions */
struct irqaction *action; /* IRQ action list */
unsigned int depth; /* Disable depth for nested irq disables */
} irq_desc_t;
三:定义在include/linux/ interrupt.h中的
struct irqaction {
void (*handler)(int, void *, struct pt_regs *);
unsigned long flags;
unsigned long mask;
const char *name;
void *dev_id;
struct irqaction *next;
};
三者关系以下:

图4、与硬中断相关的几个数据结构各关系
各结构成员详述以下:
- struct irqaction结构,它包含了内核接收到特定IRQ以后应该采起的操做,其成员以下:
- handler:是一指向某个函数的指针。该函数就是所在结构对相应中断的处理函数。
- flags:取值只有SA_INTERRUPT(中断可嵌套),SA_SAMPLE_RANDOM(这个中断是源于物理随机性的),和SA_SHIRQ(这个IRQ和其它struct irqaction共享)。
- mask:在x86或者体系结构无关的代码中不会使用(除非将其设置为0);只有在SPARC64的移植版本中要跟踪有关软盘的信息时才会使用它。
- name:产生中断的硬件设备的名字。由于不止一个硬件能够共享一个IRQ。
- dev_id:标识硬件类型的一个惟一的ID。Linux支持的全部硬件设备的每一种类型,都有一个由制造厂商定义的在此成员中记录的设备ID。
- next:若是IRQ是共享的,那么这就是指向队列中下一个struct irqaction结构的指针。一般状况下,IRQ不是共享的,所以这个成员就为空。
- struct hw_interrupt_type结构,它是一个抽象的中断控制器。这包含一系列的指向函数的指针,这些函数处理控制器特有的操做:
- typename:控制器的名字。
- startup:容许从给定的控制器的IRQ所产生的事件。
- shutdown:禁止从给定的控制器的IRQ所产生的事件。
- handle:根据提供给该函数的IRQ,处理惟一的中断。
- enable和disable:这两个函数基本上和startup和shutdown相同;
- 另一个数据结构是irq_desc_t,它具备以下成员:
- status:一个整数。表明IRQ的状态:IRQ是否被禁止了,有关IRQ的设备当前是否正被自动检测,等等。
- handler:指向hw_interrupt_type的指针。
- action:指向irqaction结构组成的队列的头。正常状况下每一个IRQ只有一个操做,所以连接列表的正常长度是1(或者0)。可是,若是IRQ被两个或者多个设备所共享,那么这个队列中就有多个操做。
- depth:irq_desc_t的当前用户的个数。主要是用来保证在中断处理过程当中IRQ不会被禁止。
- irq_desc是irq_desc_t 类型的数组。对于每个IRQ都有一个数组入口,即数组把每个IRQ映射到和它相关的处理程序和irq_desc_t中的其它信息。
- 与Bottom_half相关的数据结构:
图5、底半处理数据结构示意图
- bh_mask_count:计数器。对每一个enable/disable请求嵌套对进行计数。这些请求经过调用enable_bh和 disable_bh实现。每一个禁止请求都增长计数器;每一个使能请求都减少计数器。当计数器达到0时,全部未完成的禁止语句都已经被使能语句所匹配了,因 此下半部分最终被从新使能。(定义在kernel/softirq.c中)
- bh_mask和bh_active:它们共同决定下半部分是否运行。它们两个都有32位,而每个下半部分都占用一位。当一个上半部 分(或者一些其它代码)决定其下半部分须要运行时,就经过设置bh_active中的一位来标记下半部分。无论是否作这样的标记,下半部分均可以经过清空 bh_mask中的相关位来使之失效。所以,对bh_mask和bh_active进行位AND运算就可以代表应该运行哪个下半部分。特别是若是位与运 算的结果是0,就没有下半部分须要运行。
- bh_base:是一组简单的指向下半部分处理函数的指针。
bh_base表明的指针数组中可包含 32 个不一样的底半处理程序。bh_mask 和 bh_active 的数据位分别表明对应的底半处理过程是否安装和激活。若是 bh_mask 的第 N 位为 1,则说明 bh_base 数组的第 N 个元素包含某个底半处理过程的地址;若是 bh_active 的第 N 位为 1,则说明必须由调度程序在适当的时候调用第 N 个底半处理过程。
二. 向量的设置和相关数据的初始化:
- 在实模式下的初始化过程当中,经过对中断控制器8259A-1,9259A-2从新编程,把硬中断设到0x20-0x2F。即把IRQ0& #0;IRQ15分别与0x20-0x2F号中断向量对应起来;当对应的IRQ发生了时,处理机就会经过相应的中断向量,把控制转到对应的中断服务例 程。(源码在Arch/i386/boot/setup.S文件中;相关内容可参见 实模式下的初始化 部分)
- 在保护模式下的初始化过程当中,设置并初始化idt,共256个入口,服务程序均为ignore_int, 该服务程序仅打印“Unknown interruptn”。(源码参见Arch/i386/KERNEL/head.S文件;相关内容可参见 保护模式下的初始化 部分)
- 在系统初始化完成后运行的第一个内核程序asmlinkage void __init start_kernel(void) (源码在文件init/main.c中) 中,经过调用void __init trap_init(void)函数,把各自陷和中断服务程序的入口地址设置到 idt 表中,即将表一中对应的处理程序入口设置到相应的中断向量表项中;在此版本(2.2.5)的Linux只设置0-17号中断向量。(trap_init (void)函数定义在arch/i386/kernel/traps.c 中; 相关内容可参见 详解系统调用 部分)
- 在同一个函数void __init trap_init(void)中,经过调用函数set_system_gate(SYSCALL_VECTOR,&system_call); 把系统调用总控程序的入口挂在中断0x80上。其中SYSCALL_VECTOR是定义在 linux/arch/i386/kernel/irq.h中的一个常量0x80; 而 system_call 即为中断总控程序的入口地址;中断总控程序用汇编语言定义在arch/i386/kernel/entry.S中。(相关内容可参见 详解系统调用 部分)
- 在系统初始化完成后运行的第一个内核程序asmlinkage void __init start_kernel(void) (源码在文件init/main.c中) 中,经过调用void init_IRQ(void)函数,把地址标号interrupt[i](i从1-223)设置到 idt 表中的的32-255号中断向量(0x80除外),外部硬件IRQ的触发,将经过这些地址标号最终进入到各自相应的处理程序。(init_IRQ (void)函数定义在arch/i386/kernel/IRQ.c 中;)
- interrupt[i](i从1-223),是在arch/i386/kernel/IRQ.c文件中,经过一系列嵌套的相似如 BUILD_16_IRQS(0x0)的宏,定义的一系列地址标号;(这些定义interrupt[i]的宏,所有定义在文件 arch/i386/kernel/IRQ.c和arch/i386/kernel/IRQ.H中。这些嵌套的宏的使用,原理很简单,但很烦,限于篇幅, 在此省略)
- 各以interrupt[i]为入口的代码,在进行一些简单的处理后,最后都会调用函数asmlinkage void do_IRQ(struct pt_regs regs),do_IRQ函数调用static void do_8259A_IRQ(unsigned int irq, struct pt_regs * regs) 而do_8259A_IRQ在进行必要的处理后,将调用已与此IRQ创建联系irqaction中的处理函数,以进行相应的中断处理。最后处理机将跳转到 ret_from_intr进行必要处理后,整个中断处理结束返回。(相关源码都在文件arch/i386/kernel/IRQ.c和 arch/i386/kernel/IRQ.H中。Irqaction结构参见上面的数据结构说明)
三. Bottom_half处理机制
在此版本(2.2.5)的Linux中,中断处理程序从概念上被分为上半部分(top half)和下半部分(bottom half);在中断发生时上半部分的处理过程当即执行,可是下半部分(若是有的话)却推迟执行。内核把上半部分和下半部分做为独立的函数来处理,上半部分 决定其相关的下半部分是否须要执行。必须当即执行的部分必须位于上半部分,而能够推迟的部分可能属于下半部分。
那么为何这样划分红两个部分呢?
- 一个缘由是要把中断的总延迟时间最小化。Linux内核定义了两种类型的中断,快速的和慢速的,这二者之间的一个区别是慢速中断自身还能够被中 断,而快速中断则不能。所以,当处理快速中断时,若是有其它中断到达;无论是快速中断仍是慢速中断,它们都必须等待。为了尽量快地处理这些其它的中断, 内核就须要尽量地将处理延迟到下半部分执行。
- 另一个缘由是,当内核执行上半部分时,正在服务的这个特殊IRQ将会被可编程中断控制器禁止,因而,链接在同一个IRQ上的其它设备 就只有等到该该中断处理被处理完毕后果才能发出IRQ请求。而采用Bottom_half机制后,不须要当即处理的部分就能够放在下半部分处理,从而,加 快了处理机对外部设备的中断请求的响应速度。
- 还有一个缘由就是,处理程序的下半部分还能够包含一些并不是每次中断都必须处理的操做;对这些操做,内核能够在一系列设备中断以后集中处 理一次就能够了。即在这种状况下,每次都执行并不是必要的操做彻底是一种浪费,而采用Bottom_half机制后,能够稍稍延迟并在后来只执行一次就行 了。
因而可知,没有必要每次中断都调用下半部分;只有bh_mask 和 bh_active的对应位的与为1时,才必须执行下半部分(do_botoom_half)。因此,若是在上半部分中(也可能在其余地方)决定必须执行 对应的半部分,那么能够经过设置bh_active的对应位,来指明下半部分必须执行。固然,若是bh_active的对应位被置位,也不必定会立刻执行 下半部分,由于还必须具有另外两个条件:首先是bh_mask的相应位也必须被置位,另外,就是处理的时机,若是下半部分已经标记过须要执行了,如今又再 次标记,那么内核就简单地保持这个标记;当状况容许的时候,内核就对它进行处理。若是在内核有机会运行其下半部分以前给定的设备就已经发生了100次中 断,那么内核的上半部分就运行100次,下半部分运行1次。
bh_base数组的索引是静态定义的,定时器底半处理过程的地址保存在第 0 个元素中,控制台底半处理过程的地址保存在第 1 个元素中,等等。当 bh_mask 和 bh_active 代表第 N 个底半处理过程已被安装且处于活动状态,则调度程序会调用第 N 个底半处理过程,该底半处理过程最终会处理与之相关的任务队列中的各个任务。由于调度程序从第 0 个元素开始依次检查每一个底半处理过程,所以,第 0 个底半处理过程具备最高的优先级,第 31 个底半处理过程的优先级最低。
内核中的某些底半处理过程是和特定设备相关的,而其余一些则更通常一些。表二列出了内核中通用的底半处理过程。
表2、Linux 中通用的底半处理过程
TIMER_BH(定时器) |
在每次系统的周期性定时器中断中,该底半处理过程被标记为活动状态,并用来驱动内核的定时器队列机制。 |
CONSOLE_BH(控制台) |
该处理过程用来处理控制台消息。 |
TQUEUE_BH(TTY 消息队列) |
该处理过程用来处理 tty 消息。 |
NET_BH(网络) |
用于通常网络处理,做为网络层的一部分 |
IMMEDIATE_BH(当即) |
这是一个通常性处理过程,许多设备驱动程序利用该过程对本身要在随后处理的任务进行排队。 |
当某个设备驱动程序,或内核的其余部分须要将任务排队进行处理时,它将任务添加到适当的 系统队列中(例如,添加到系统的定时器队列中),而后通知内核,代表须要进行底半处理。为了通知内核,只需将 bh_active 的相应数据位置为 1。例如,若是驱动程序在 immediate 队列中将某任务排队,并但愿运行 IMMEDIATE 底半处理过程来处理排队任务,则只需将 bh_active 的第 8 位置为 1。在每一个系统调用结束并返回调用进程以前,调度程序要检验 bh_active 中的每一个位,若是有任何一位为 1,则相应的底半处理过程被调用。每一个底半处理过程被调用时,bh_active 中的相应为被清除。bh_active 中的置位只是暂时的,在两次调用调度程序之间 bh_active 的值才有意义,若是 bh_active 中没有置位,则不须要调用任何底半处理过程。
四.中断处理全过程
由前面的分析可知,对于0-31号中断向量,被保留用来处理异常事件;0x80中断向量用来做为系统调用的总入口点;而其余中断向量,则用来处理外部设备中断;这三者的处理过程都是不同的。
- 异常的处理全过程
对这0-31号中断向量,保留用来处理异常事件;操做系统提供相应的异常的处理程序,并在初 始化时把处理程序的入口等级在对应的中断向量表项中。当产生一个异常时,处理机就会自动把控制转移到相应的处理程序的入口,运行相应的处理程序,进行相应 的处理后,返回原中断处。固然,在前面已经提到,此版本(2.2.5)的Linux只提供了0-17号中断向量的处理程序。
- 中断的处理全过程
对于0-31号和0x80以外的中断向量,主要用来处理外部设备中断;在系统完成初始化后,其中断处理过程以下:
当外部设备须要处理机进行中断服务时,它就会经过中断控制器要求处理机进行中断服务。如 果 CPU 这时能够处理中断,CPU将根据中断控制器提供的中断向量号和中断描述符表(IDT)中的登记的地址信息,自动跳转到相应的interrupt[i]地 址;在进行一些简单的但必要的处理后,最后都会调用函数do_IRQ , do_IRQ函数调用 do_8259A_IRQ 而do_8259A_IRQ在进行必要的处理后,将调用已与此IRQ创建联系irqaction中的处理函数,以进行相应的中断处理。最后处理机将跳转到 ret_from_intr进行必要处理后,整个中断处理结束返回。
从数据结构入手,应该说是分析操做系统源码最经常使用的和最主要的方法。由于操做系统的几大功能部件,如进程管理,设备管理,内存管理等等,均可以经过对其相应的数据结构的分析来弄懂其实现机制。很好的掌握这种方法,对分析Linux内核大有裨益。
方法之四:以功能为中心,各个击破
从功能上看,整个Linux系统可看做有一下几个部分组成:
- 进程管理机制部分;
- 内存管理机制部分;
- 文件系统部分;
- 硬件驱动部分;
- 系统调用部分等;
以功能为中心、各个击破,就是指从这五个功能入手,经过源码分析,找出Linux是怎样实现这些功能的。
在这五个功能部件中,系统调用是用户程序或操做调用核心所提供的功能的接口;也是分析 Linux内核源码几个很好的入口点之一。对于那些在dos或 Uinx、Linux下有过C编程经验的高手尤为如此。又因为系统调用相对其它功能而言,较为简单,因此,我就以它为例,但愿经过对系统调用的分析,能使 读者体会到这一方法。
与系统调用相关的内容主要有:系统调用总控程序,系统调用向量表sys_call_table,以及各系统调用服务程序。下面将对此一一介绍:
- 保护模式下的初始化过程当中,设置并初始化idt,共256个入口,服务程序均为ignore_int, 该服务程序仅打印“Unknown interruptn”。(源码参见/Arch/i386/KERNEL/head.S文件;相关内容可参见 保护模式下的初始化 部分)
- 在系统初始化完成后运行的第一个内核程序start_kernel中,经过调用 trap_init函数,把各自陷和中断服务程序的入口地址设置到 idt 表中;同时,此函数还经过调用函数set_system_gate 把系统调用总控程序的入口地址挂在中断0x80上。其中:
- start_kernel的原型为void __init start_kernel(void) ,其源码在文件 init/main.c中;
- trap_init函数的原型为void __init trap_init(void),定义在arch/i386/kernel/traps.c 中
- 函数set_system_gate一样定义在arch/i386/kernel/traps.c 中,调用原型为set_system_gate(SYSCALL_VECTOR,&system_call);
- 其中,SYSCALL_VECTOR是定义在 linux/arch/i386/kernel/irq.h中的一个常量0x80;
- 而 system_call 即为系统调用总控程序的入口地址;中断总控程序用汇编语言定义在arch/i386/kernel/entry.S中。
(其它相关内容可参见 中断和中断处理 部分)
- 系统调用向量表sys_call_table, 是一个含有NR_syscalls=256个单元的数组。它的每一个单元存放着一个系统调用服务程序的入口地址。该数组定义在 /arch/i386/kernel/entry.S中;而NR_syscalls则是一个等于256的宏,定义在 include/linux/sys.h中。
- 各系统调用服务程序则分别定义在各个模块的相应文件中;例如asmlinkage int sys_time(int * tloc)就定义在kerneltime.c中;另外,在kernelsys.c中也有很多服务程序;
II、系统调用过程
∥颐侵溃低车饔檬怯没С绦蚧虿僮鞯饔煤诵乃峁┑墓δ艿慕涌冢凰韵低车粲玫墓叹褪谴佑没С绦虻较低衬诤耍缓笥只氐接没С绦虻墓蹋辉贚inux中,此过程大致过程可描述以下:
系统调用过程示意图:

整个系统调用进入过程客表示以下:
用户程序 系统调用总控程序(system_call) 各个服务程序
可见,系统调用的进入课分为“用户程序 系统调用总控程序”和“系统调用总控程序各个服务程序”两部分;下边将分别对这两个部分进行详细说明:
以程序流程为线索,适合于分析系统的初始化过程:系统引导、实模式下的初始化、保护模式下的初始化三个部分,和分析应用程序的执行流程:从程序的装载,到运行,一直到程序的退出。而流程图则是这种分析方法最合适的表达工具。
- “用户程序 系统调用总控程序”的实现:在前面已经说过,Linux的系统调用使用第0x80号中断向量项做为总的入口,也即,系统调用总控程序的入口地址 system_call就挂在中断0x80上。也就是说,只要用户程序执行0x80中断 ( int 0x80 ),就可实现“用户程序 系统调用总控程序”的进入;事实上,在Linux中,也是这么作的。只是0x80中断的执行语句int 0x80 被封装在标准C库中,用户程序只需用标准系统调用函数就能够了,而不须要在用户程序中直接写0x80中断的执行语句int 0x80。至于中断的进入的详细过程可参见前面的“中断和中断处理”部分。
- “系统调用总控程序 各个服务程序” 的实现:在系统调用总控程序中经过语句“call * SYMBOL_NAME(sys_call_table)(,%eax,4)”来调用各个服务程序(SYMBOL_NAME是定义在 /include/linux/linkage.h中的宏:#define SYMBOL_NAME_LABEL(X) X),能够忽略)。当系统调用总控程序执行到此语句时,eax中的内容便是相应系统调用的编号,此编号即为相应服务程序在系统调用向量表 sys_call_table中的编号(关于系统调用的编号说明在/linux/include/asm/unistd.h中)。又由于系统调用向量表 sys_call_table每项占4个字节,因此由%eax 乘上4造成偏移地址,而sys_call_table则为基址;基址加上偏移所指向的内容就是相应系统调用服务程序的入口地址。因此此call语句就至关 于直接调用对应的系统调用服务程序。
- 参数传递的实现:在Linux中全部系统调用服务例程都使用了asmlinkage标志。此标志是一个定义在/include/linux/linkage.h 中的一个宏:
#if defined __i386__ && (__GNUC__ > 2 || __GNUC_MINOR__ > 7)
#define asmlinkage CPP_ASMLINKAGE__attribute__((regparm(0)))
#else
#define asmlinkage CPP_ASMLINKAGE
#endif
其中涉及到了gcc的一些约定,总之,这个标志它能够告诉编译器该函数不须要从寄存器中得到任何参数,而是从堆栈中取得参数;即参数在堆栈中传递,而不是直接经过寄存器;
堆栈参数以下:
EBX = 0x00
ECX = 0x04
EDX = 0x08
ESI = 0x0C
EDI = 0x10
EBP = 0x14
EAX = 0x18
DS = 0x1C
ES = 0x20
ORIG_EAX = 0x24
EIP = 0x28
CS = 0x2C
EFLAGS = 0x30
在进入系统调用总控程序前,用户按照以上的对应顺序将参数放到对应寄存器中,在系统调用 总控程序一开始就将这些寄存器压入堆栈;在退出总控程序前又按如上顺序堆栈;用户程序则能够直接从寄存器中复得被服务程序加工过了的参数。而对于系统调用 服务程序而言,参数就能够直接从总控程序压入的堆栈中复得;对参数的修改一能够直接在堆栈中进行;其实,这就是asmlinkage标志的做用。因此在进 入和退出系统调用总控程序时,“保护现场”和“恢复现场”的内容并不必定会相同。
- 特殊的服务程序:在此版本(2.2.5)的linux内核中,有好几个系统调用的服务程序都是定义在/usr/src/linux/kernel/sys.c 中的同一个函数:
asmlinkage int sys_ni_syscall(void)
{
return -ENOSYS;
}
此函数除了返回错误号以外,什么都没干。那他有什么做用呢?归结起来有以下三种可能:
1.处理边界错误,0号系统调用就是用的此特殊的服务程序;
2.用来替换旧的已淘汰了的系统调用,如: Nr 17, Nr 31, Nr 32, Nr 35, Nr 44, Nr 53, Nr 56, Nr58, Nr 98;
3. 用于将要扩展的系统调用,如: Nr 137, Nr 188, Nr 189;
III、系统调用总控程序(system_call)
系统调用总控程序(system_call)可参见arch/i386/kernel/entry.S其执行流程以下图:

IV、实例:增长一个系统调用
由以上的分析可知,增长系统调用因为下两种方法:
i.编一个新的服务例程,将它的入口地址加入到sys_call_table的某一项,只要该项的原服务例程是sys_ni_syscall,而且是sys_ni_syscall的做用属于第三种的项,也即Nr 137, Nr 188, Nr 189。
ii.直接增长:
- 编一个新的服务例程;
- 在sys_call_table中添加一个新项, 并把的新增长的服务例程的入口地址加到sys_call_table表中的新项中;
- 把增长的 sys_call_table 表项所对应的向量, 在include/asm-386/unistd.h 中进行必要申明,以供用户进程和其余系统进程查询或调用。
- 因为在标准的c语言库中没有新系统调用的承接段,因此,在测试程序中,除了要#include ,还要申明以下 _syscall1(int,additionSysCall,int, num)。
下面将对第ii种状况列举一个我曾经实现过了的一个增长系统调用的实例:
1.)在kernel/sys.c中增长新的系统服务例程以下:
asmlinkage int sys_addtotal(int numdata)
{
int i=0,enddata=0;
while(i<=numdata)
enddata+=i++;
return enddata;
}
该函数有一个 int 型入口参数 numdata , 并返回从 0 到 numdata 的累加值; 固然也能够把系统服务例程放在一个本身定义的文件或其余文件中,只是要在相应文件中做必要的说明;
2.)把 asmlinkage int sys_addtotal( int) 的入口地址加到sys_call_table表中:
arch/i386/kernel/entry.S 中的最后几行源代码修改前为:
... ...
.long SYMBOL_NAME(sys_sendfile)
.long SYMBOL_NAME(sys_ni_syscall) /* streams1 */
.long SYMBOL_NAME(sys_ni_syscall) /* streams2 */
.long SYMBOL_NAME(sys_vfork) /* 190 */
.rept NR_syscalls-190
.long SYMBOL_NAME(sys_ni_syscall)
.endr
修改后为: ... ...
.long SYMBOL_NAME(sys_sendfile)
.long SYMBOL_NAME(sys_ni_syscall) /* streams1 */
.long SYMBOL_NAME(sys_ni_syscall) /* streams2 */
.long SYMBOL_NAME(sys_vfork) /* 190 */
/* add by I */
.long SYMBOL_NAME(sys_addtotal)
.rept NR_syscalls-191
.long SYMBOL_NAME(sys_ni_syscall)
.endr
3.) 把增长的 sys_call_table 表项所对应的向量,在include/asm-386/unistd.h 中进行必要申明,以供用户进程和其余系统进程查询或调用:
增长后的部分 /usr/src/linux/include/asm-386/unistd.h 文件以下:
... ...
#define __NR_sendfile 187
#define __NR_getpmsg 188
#define __NR_putpmsg 189
#define __NR_vfork 190
/* add by I */
#define __NR_addtotal 191
4.测试程序(test.c)以下:
#include
#include
_syscall1(int,addtotal,int, num)
main()
{
int i,j;
do
printf("Please input a numbern");
while(scanf("%d",&i)==EOF);
if((j=addtotal(i))==-1)
printf("Error occurred in syscall-addtotal();n");
printf("Total from 0 to %d is %d n",i,j);
}
对修改后的新的内核进行编译,并引导它做为新的操做系统,运行几个程序后能够发现一切正常;在新的系统下对测试程序进行编译(*注:因为原内核并未提供此系统调用,因此只有在编译后的新内核下,此测试程序才能可能被编译经过),运行状况以下:
$gcc �o test test.c
$./test
Please input a number
36
Total from 0 to 36 is 666
综述
可见,修改为功。
因为操做系统内核源码的特殊性:体系庞大,结构复杂,代码冗长,代码间联系错综复杂。因此要把内核源码分析清楚,也是一个很艰难,很须要毅力的事。尤为须要交流和讲究方法;只有方法正确,才能事半功倍。
在上面的论述中,一共列举了两个内核分析的入口、和三种分析源码的方法:以程序流程为线索,一线串珠;以数据结构为基点,举一反三;以功能为中心,各个击破。三种方法各有特色,适合于分析不一样部分的代码:
- 以数据结构为基点、举一反三,这种方法是分析操做系统源码最经常使用的和最主要的方法。对分析进程管理,设备管理,内存管理等等都是颇有效的。
- 以功能为中心、各个击破,是把整个系统分红几个相对独立的功能模块,而后分别对各个功能进行分析。这样带来的一个好处就是,每次只以一 个功能为中心,涉及到其余部分的内容,能够看做是其它功能提供的服务,而无需急着追究这种服务的实现细节;这样,在很大程度上减轻了分析的复杂度。
三种方法,各有其长,只要合理的综合运用这些方法,相信对减轻分析的复杂度仍是有所帮组的。
LINUX中断机制
【主要内容】
Linux设备驱动编程中的中断与定时器处理
【正文】
1、基础知识
一、中断
所谓中断是指CPU在执行程序的过程当中,出现了某些突发事件急待处理,CPU必须暂停执行当前的程序,转去处理突发事件,处理完毕后CPU又返回程序被中断的位置并继续执行。
二、中断的分类
1)根据中断来源分为:内部中断和外部中断。内部中断来源于CPU内部(软中断指令、溢出、语法错误等),外部中断来自CPU外部,由设备提出请求。
2)根据是否可被屏蔽分为:可屏蔽中断和不可屏蔽中断(NMI),被屏蔽的中断将不会获得响应。
3)根据中断入口跳转方法分为:向量中断和非向量中断。向量中断为不一样的中断分配不一样的中断号,非向量中断多个中断共享一个中断号,在软件中判断具体是哪一个中断(非向量中断由软件提供中断服务程序入口地址)。
2、Linux中断处理程序架构
设备的中断会打断内核中正常调度和运行,系统对更高吞吐率的追求势必要求中断服务程序尽量的短小(时间短),可是在大多数实际使用中,要完成的工做都是复杂的,它可能须要进行大量的耗时工做。
一、Linux中断处理中的顶半部和底半部机制
因为中断服务程序的执行并不存在于进程上下文,所以,要求中断服务程序的时间尽量的短。 为了在中断执行事件尽量短和中断处理需完成大量耗时工做之间找到一个平衡点,Linux将中断处理分为两个部分:顶半部(top half)和底半部(bottom half)。
Linux中断处理机制
顶半部完成尽量少的比较紧急的功能,它每每只是简单地读取寄存器中的中断状态并清除中断标志后进行“登记中断”的工做。“登记”意味着将底半部的处理程序挂载到该设备的底半部指向队列中去。底半部做为工做重心,完成中断事件的绝大多数任务。
a. 底半部能够被新的中断事件打断,这是和顶半部最大的不一样,顶半部一般被设计成不可被打断
b. 底半部相对来讲不是很是紧急的,并且相对比较耗时,不在硬件中断服务程序中执行。
c. 若是中断要处理的工做自己不多,全部的工做可在顶半部所有完成
3、中断编程
一、申请和释放中断
在Linux设备驱动中,使用中断的设备须要申请和释放相对应的中断,分别使用内核提供的 request_irq() 和 free_irq() 函数
a. 申请IRQ
typedef irqreturn_t (*irq_handler_t)(int irq, void *dev_id);
int request_irq(unsigned int irq, irq_handler_t handler, unsigned long irqflags, const char *devname, void *dev_id) /* 参数: ** irq:要申请的硬件中断号 ** handler:中断处理函数(顶半部) ** irqflags:触发方式及工做方式 ** 触发:IRQF_TRIGGER_RISING 上升沿触发 ** IRQF_TRIGGER_FALLING 降低沿触发 ** IRQF_TRIGGER_HIGH 高电平触发 ** IRQF_TRIGGER_LOW 低电平触发 ** 工做:不写:快速中断(一个设备占用,且中断例程回调过程当中会屏蔽中断) ** IRQF_SHARED:共享中断 ** dev_id:在共享中断时会用到(中断注销与中断注册的此参数应保持一致) ** 返回值:成功返回 - 0 失败返回 - 负值(绝对值为错误码) */
b. 释放IRQ
void free_irq(unsigned int irq, void *dev_id);
/* 参数参见申请IRQ */
二、屏蔽和使能中断
void disable_irq(int irq); //屏蔽中短、当即返回
void disable_irq_nosync(int irq); //屏蔽中断、等待当前中断处理结束后返回 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ void enable_irq(int irq); //使能中断
全局中断使能和屏蔽函数(或宏)
屏蔽:
#define local_irq_save(flags) ...
void local irq_disable(void );
使能:
#define local_irq_restore(flags) ...
void local_irq_enable(void);
三、底半部机制
Linux实现底半部机制的的主要方式有 Tasklet、工做队列和软中断
a. Tasklet
Tasklet使用简单,只须要定义tasklet及其处理函数并将两者关联便可,例如:
void my_tasklet_func(unsigned long); /* 定义一个处理函数 */ DECLARE_TASKLET(my_tasklet, my_tasklet_func, data);
/* 定义一个名为 my_tasklet 的 struct tasklet 并将其与 my_tasklet_func 绑定,data为传入 my_tasklet_func的参数 */
只须要在顶半部中电泳 tasklet_schedule()函数就能使系统在适当的时候进行调度运行
tasklet_schedule(struct tasklet *xxx_tasklet);
tasklet使用模版
/* 定义 tasklet 和底半部函数并关联 */
void xxx_do_tasklet(unsigned long data); DECLARE_TASKLET(xxx_tasklet, xxx_tasklet_func, data); /* 中断处理底半部 */ void xxx_tasklet_func() { /* 中断处理具体操做 */ } /* 中断处理顶半部 */ irqreturn xxx_interrupt(int irq, void *dev_id) { //do something task_schedule(&xxx_tasklet); //do something
return IRQ_HANDLED; } /* 设备驱动模块 init */ int __init xxx_init(void) { ... /* 申请设备中断 */ result = request_irq(xxx_irq, xxx_interrupt, IRQF_DISABLED, "xxx", NULL); ... return 0; } module_init(xxx_init); /* 设备驱动模块exit */ void __exit xxx_exit(void) { ... /* 释放中断 */ free_irq(xxx_irq, NULL); }
module_exit(xxx_exit);
b. 工做队列 workqueue
工做队列与tasklet方法很是相似,使用一个结构体定义一个工做队列和一个底半部执行函数:
struct work_struct {
atomic_long_t data;
struct list_head entry;
work_func_t func;
#ifdef CONFIG_LOCKDEP
struct lockdep_map lockdep_map; #endif };
struct work_struct my_wq; /* 定义一个工做队列 */
void my_wq_func(unsigned long); /*定义一个处理函数 */
经过INIT_WORK()能够初始化这个工做队列并将工做队列与处理函数绑定(通常在模块初始化中使用):
void INIT_WORK(struct work_struct *my_wq, work_func_t);
/* my_wq 工做队列地址
** work_func_t 处理函数
*/
与tasklet_schedule_work ()对应的用于调度工做队列执行的函数为schedule_work()
工做队列使用模版
/* 定义工做队列和关联函数 */
struct work_struct xxx_wq;
void xxx_do_work(unsigned long); /* 中断处理底半部 */ void xxx_work(unsigned long) { /* do something */ } /* 中断处理顶半部 */ irqreturn_t xxx_interrupt(int irq, void *dev_id) { ... schedule_work(&xxx_wq); ... return IRQ_HANDLED; } /* 设备驱动模块 init */ int __init xxx_init(void) { ... /* 申请设备中断 */ result = request_irq(xxx_irq, xxx_interrupt, IRQF_DISABLED, "xxx", NULL); /* 初始化工做队列 */ INIT_WORK(&xxx_wq, xxx_do_work); ... return 0; } module_init(xxx_init); /* 设备驱动模块exit */ void __exit xxx_exit(void) { ... /* 释放中断 */ free_irq(xxx_irq, NULL); } module_exit(xxx_exit);
c. 软中断
软中断(softirq)也是一种传统的底半部处理机制,它的执行时机一般是顶半部返回的时候,tasklet的基于软中断实现的,所以也运行于软中断上下文。
在Linux内核中,用softirq_action结构体表征一个软中断,这个结构体中包含软中断处理函数指针和传递给该函数的参数。使用open_softirq()函数能够注册软中断对应的处理函数,而raise_softirq()函数能够触发一个软中断。
struct softirq_action
{
void (*action)(struct softirq_action *);
};
void open_softirq(int nr, void (*action)(struct softirq_action *)); /* 注册软中断 */ void raise_softirq(unsigned int nr); /* 触发软中断 */
local_bh_disable() 和 local_bh_enable() 是内核中用于禁止和使能软中断和tasklet底半部机制的函数。
linux中断处理原理分析
Tasklet做为一种新机制,显然能够承担更多的优势。正好这时候SMP愈来愈火了,所以又在tasklet中加入了SMP机制,保证同种中断只能在一个cpu上执行。在软中断时代,显然没有这种考虑。所以同一种中断能够在两个cpu上同时执行,极可能形成冲突。
Linux中断下半部处理有三种方式:软中断、tasklet、工做队列。
曾经有人问我为何要分这几种,该怎么用。当时用书上的东西蒙混了过去,可是本身明白本身其实是不懂的。最近有时间了,因而试着整理一下linux的中断处理机制,目的是起码从原理上可以说得通。
1、最简单的中断机制
最简单的中断机制就是像芯片手册上讲的那样,在中断向量表中填入跳转到对应处理函数的指令,而后在处理函数中实现须要的功能。相似下图:
这种方式在原来的单片机课程中经常用到,一些简单的单片机系统也是这样用。
它的好处很明显,简单,直接。
2、下半部
中断处理函数所做的第一件事情是什么?答案是屏蔽中断(或者是什么都不作,由于经常是若是不清除IF位,就等于屏蔽中断了),固然只屏蔽同一种中断。之因此要屏蔽中断,是由于新的中断会再次调用中断处理函数,致使原来中断处理现场的破坏。即,破坏了 interrupt context。
随着系统的不断复杂,中断处理函数要作的事情也愈来愈多,多到都来不及接收新的中断了。因而发生了中断丢失,这显然不行,因而产生了新的机制:分离中断接收与中断处理过程。中断接收在屏蔽中断的状况下完成;中断处理在时能中断的状况下完成,这部分被称为中断下半部。
从上图中看,只看int0的处理。Func0为中断接收函数。中断只能简单的触发func0,而func0则能作更多的事情,它与funcA之间能够使用队列等缓存机制。当又有中断发生时,func0被触发,而后发送一个中断请求到缓存队列,而后让funcA去处理。
因为func0作的事情是很简单的,因此不会影响int0的再次接收。并且在func0返回时就会使能int0,所以funcA执行时间再长也不会影响int0的接收。
3、软中断
下面看看linux中断处理。做为一个操做系统显然不能任由每一个中断都各自为政,统一管理是必须的。
咱们不可中断部分的共同部分放在函数do_IRQ中,须要添加中断处理函数时,经过request_irq实现。下半部放在do_softirq中,也就是软中断,经过open_softirq添加对应的处理函数。
4、tasklet
旧事物跟不上历史的发展时,总会有新事物出现。
随着中断数的不停增长,软中断不够用了,因而下半部又作了进化。
软中断用轮询的方式处理。假如正好是最后一种中断,则必须循环完全部的中断类型,才能最终执行对应的处理函数。显然当年开发人员为了保证轮询的效率,因而限制中断个数为32个。
为了提升中断处理数量,顺道改进处理效率,因而产生了tasklet机制。
Tasklet采用无差异的队列机制,有中断时才执行,免去了循环查表之苦。
总结下tasklet的优势:
(1)无类型数量限制;
(2)效率高,无需循环查表;
(3)支持SMP机制;
5、工做队列
前面的机制不论如何折腾,有一点是不会变的。它们都在中断上下文中。什么意思?说明它们不可挂起。并且因为是串行执行,所以只要有一个处理时间较长,则会致使其余中断响应的延迟。为了完成这些不可能完成的任务,因而出现了工做队列。工做队列说白了就是一组内核线程,做为中断守护线程来使用。多个中断能够放在一个线程中,也能够每一个中断分配一个线程。
工做队列对线程做了封装,使用起来更方便。
由于工做队列是线程,因此咱们能够使用全部能够在线程中使用的方法。
Tasklet其实也不必定是在中断上下文中执行,它也有可能在线程中执行。
假如中断数量不少,并且这些中断都是自启动型的(中断处理函数会致使新的中断产生),则有可能cpu一直在这里执行中断处理函数,会致使用户进程永远得不到调度时间。
为了不这种状况,linux发现中断数量过多时,会把多余的中断处理放到一个单独的线程中去作,就是ksoftirqd线程。这样又保证了中断很少时的响应速度,又保证了中断过多时不会把用户进程饿死。
问题是咱们不能保证咱们的tasklet或软中断处理函数必定会在线程中执行,因此仍是不能使用进程才能用的一些方法,如放弃调度、长延时等。
6、使用方式总结
Request_irq挂的中断函数要尽可能简单,只作必须在屏蔽中断状况下要作的事情。
中断的其余部分都在下半部中完成。
软中断的使用原则很简单,永远不用。它甚至都不算是一种正是的中断处理机制,而只是tasklet的实现基础。
工做队列也要少用,若是不是必需要用到线程才能用的某些机制,就不要使用工做队列。其实对于中断来讲,只是对中断进行简单的处理,大部分工做是在驱动程序中完成的。因此有什么必要非使用工做队列呢?
除了上述状况,就要使用tasklet。
即便是下半部,也只是做必须在中断中要作的事情,如保存数据等,其余都交给驱动程序去作。
linux 中断机制的处理过程
1、中断的概念
中断是指在CPU正常运行期间,因为内外部事件或由程序预先安排的事件引发的CPU暂时中止正在运行的程序,转而为该内部或外部事件或预先安排的事件服务的程序中去,服务完毕后再返回去继续运行被暂时中断的程序。Linux中一般分为外部中断(又叫硬件中断)和内部中断(又叫异常)。
在实地址模式中,CPU把内存中从0开始的1KB空间做为一个中断向量表。表中的每一项占4个字节。可是在保护模式中,有这4个字节的表项构成的中断向量表不知足实际需求,因而根据反映模式切换的信息和偏移量的足够使得中断向量表的表项由8个字节组成,而中断向量表也叫作了中断描述符表(IDT)。在CPU中增长了一个用来描述中断描述符表寄存器(IDTR),用来保存中断描述符表的起始地址。
2、中断的请求过程
外部设备当须要操做系统作相关的事情的时候,会产生相应的中断。设备经过相应的中断线向中断控制器发送高电平以产生中断信号,而操做系统则会从中断控制器的状态位取得那根中断线上产生的中断。并且只有在设备在对某一条中断线拥有控制权,才能够向这条中断线上发送信号。也因为如今的外设愈来愈多,中断线又是很宝贵的资源不可能被一一对应。所以在使用中断线前,就得对相应的中断线进行申请。不管采用共享中断方式仍是独占一个中断,申请过程都是先讲全部的中断线进行扫描,得出哪些没有别占用,从其中选择一个做为该设备的IRQ。其次,经过中断申请函数申请相应的IRQ。最后,根据申请结果查看中断是否可以被执行。
中断机制的核心数据结构是 irq_desc, 它完整地描述了一条中断线 (或称为 “中断通道” )。如下程序源码版本为linux-2.6.32.2。
其中irq_desc 结构在 include/linux/irq.h 中定义:
typedef void (*irq_flow_handler_t)(unsigned int irq,
struct irq_desc *desc);
struct irq_desc {
unsigned int irq;
struct timer_rand_state *timer_rand_state;
unsigned int *kstat_irqs;
#ifdef CONFIG_INTR_REMAP
struct irq_2_iommu *irq_2_iommu;
#endif
irq_flow_handler_t handle_irq; /* 高层次的中断事件处理函数 */
struct irq_chip *chip; /* 低层次的硬件操做 */
struct msi_desc *msi_desc;
void *handler_data; /* chip 方法使用的数据*/
void *chip_data; /* chip 私有数据 */
struct irqaction *action; /* 行为链表(action list) */
unsigned int status; /* 状态 */
unsigned int depth; /* 关中断次数 */
unsigned int wake_depth; /* 唤醒次数 */
unsigned int irq_count; /* 发生的中断次数 */
unsigned long last_unhandled; /*滞留时间 */
unsigned int irqs_unhandled;
spinlock_t lock; /*自选锁*/
#ifdef CONFIG_SMP
cpumask_var_t affinity;
unsigned int node;
#ifdef CONFIG_GENERIC_PENDING_IRQ
cpumask_var_t pending_mask;
#endif
#endif
atomic_t threads_active;
wait_queue_head_t wait_for_threads;
#ifdef CONFIG_PROC_FS
struct proc_dir_entry *dir; /* 在 proc 文件系统中的目录 */
#endif
const char *name;/*名称*/
} ____cacheline_internodealigned_in_smp;
I、Linux中断的申请与释放:在<linux/interrupt.h>, , 实现中断申请接口:
request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,const char *name, void *dev);
函数参数说明
unsigned int irq:所要申请的硬件中断号
irq_handler_t handler:中断服务程序的入口地址,中断发生时,系统调用handler这个函数。irq_handler_t为自定义类型,其原型为:
typedef irqreturn_t (*irq_handler_t)(int, void *);
而irqreturn_t的原型为:typedef enum irqreturn irqreturn_t;
enum irqreturn {
IRQ_NONE,/*此设备没有产生中断*/
IRQ_HANDLED,/*中断被处理*/
IRQ_WAKE_THREAD,/*唤醒中断*/
};
在枚举类型irqreturn定义在include/linux/irqreturn.h文件中。
unsigned long flags:中断处理的属性,与中断管理有关的位掩码选项,有一下几组值:
#define IRQF_DISABLED 0x00000020 /*中断禁止*/
#define IRQF_SAMPLE_RANDOM 0x00000040 /*供系统产生随机数使用*/
#define IRQF_SHARED 0x00000080 /*在设备之间可共享*/
#define IRQF_PROBE_SHARED 0x00000100/*探测共享中断*/
#define IRQF_TIMER 0x00000200/*专用于时钟中断*/
#define IRQF_PERCPU 0x00000400/*每CPU周期执行中断*/
#define IRQF_NOBALANCING 0x00000800/*复位中断*/
#define IRQF_IRQPOLL 0x00001000/*共享中断中根据注册时间判断*/
#define IRQF_ONESHOT 0x00002000/*硬件中断处理完后触发*/
#define IRQF_TRIGGER_NONE 0x00000000/*无触发中断*/
#define IRQF_TRIGGER_RISING 0x00000001/*指定中断触发类型:上升沿有效*/
#define IRQF_TRIGGER_FALLING 0x00000002/*中断触发类型:降低沿有效*/
#define IRQF_TRIGGER_HIGH 0x00000004/*指定中断触发类型:高电平有效*/
#define IRQF_TRIGGER_LOW 0x00000008/*指定中断触发类型:低电平有效*/
#define IRQF_TRIGGER_MASK (IRQF_TRIGGER_HIGH | IRQF_TRIGGER_LOW | /
IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING)
#define IRQF_TRIGGER_PROBE 0x00000010/*触发式检测中断*/
const char *dev_name:设备描述,表示那一个设备在使用这个中断。
void *dev_id:用做共享中断线的指针.。通常设置为这个设备的设备结构体或者NULL。它是一个独特的标识, 用在当释放中断线时以及可能还被驱动用来指向它本身的私有数据区,来标识哪一个设备在中断 。这个参数在真正的驱动程序中通常是指向设备数据结构的指针.在调用中断处理程序的时候它就会传递给中断处理程序的void *dev_id。若是中断没有被共享, dev_id 能够设置为 NULL。
II、释放IRQ
void free_irq(unsigned int irq, void *dev_id);
III、中断线共享的数据结构
struct irqaction {
irq_handler_t handler; /* 具体的中断处理程序 */
unsigned long flags;/*中断处理属性*/
const char *name; /* 名称,会显示在/proc/interreupts 中 */
void *dev_id; /* 设备ID,用于区分共享一条中断线的多个处理程序 */
struct irqaction *next; /* 指向下一个irq_action 结构 */
int irq; /* 中断通道号 */
struct proc_dir_entry *dir; /* 指向proc/irq/NN/name 的入口*/
irq_handler_t thread_fn;/*线程中断处理函数*/
struct task_struct *thread;/*线程中断指针*/
unsigned long thread_flags;/*与线程有关的中断标记属性*/
};
thread_flags参见枚举型
enum {
IRQTF_RUNTHREAD,/*线程中断处理*/
IRQTF_DIED,/*线程中断死亡*/
IRQTF_WARNED,/*警告信息*/
IRQTF_AFFINITY,/*调整线程中断的关系*/
};
多个中断处理程序能够共享同一条中断线,irqaction 结构中的 next 成员用来把共享同一条中断线的全部中断处理程序组成一个单向链表,dev_id 成员用于区分各个中断处理程序。
根据以上内容能够得出中断机制各个数据结构之间的联系以下图所示:

三.中断的处理过程
Linux中断分为两个半部:上半部(tophalf)和下半部(bottom half)。上半部的功能是"登记中断",当一个中断发生时,它进行相应地硬件读写后就把中断例程的下半部挂到该设备的下半部执行队列中去。所以,上半部执行的速度就会很快,能够服务更多的中断请求。可是,仅有"登记中断"是远远不够的,由于中断的事件可能很复杂。所以,Linux引入了一个下半部,来完成中断事件的绝大多数使命。下半部和上半部最大的不一样是下半部是可中断的,而上半部是不可中断的,下半部几乎作了中断处理程序全部的事情,并且能够被新的中断打断!下半部则相对来讲并非很是紧急的,一般仍是比较耗时的,所以由系统自行安排运行时机,不在中断服务上下文中执行。
中断号的查看能够使用下面的命令:“cat /proc/interrupts”。
Linux实现下半部的机制主要有tasklet和工做队列。
小任务tasklet的实现
其数据结构为struct tasklet_struct,每个结构体表明一个独立的小任务,定义以下
struct tasklet_struct
{
struct tasklet_struct *next;/*指向下一个链表结构*/
unsigned long state;/*小任务状态*/
atomic_t count;/*引用计数器*/
void (*func)(unsigned long);/*小任务的处理函数*/
unsigned long data;/*传递小任务函数的参数*/
};
state的取值参照下边的枚举型:
enum
{
TASKLET_STATE_SCHED, /* 小任务已被调用执行*/
TASKLET_STATE_RUN /*仅在多处理器上使用*/
};
count域是小任务的引用计数器。只有当它的值为0的时候才能被激活,并其被设置为挂起状态时,才可以被执行,不然为禁止状态。
I、声明和使用小任务tasklet
静态的建立一个小任务的宏有一下两个:
#define DECLARE_TASKLET(name, func, data) /
struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(0), func, data }
#define DECLARE_TASKLET_DISABLED(name, func, data) /
struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(1), func, data }
这两个宏的区别在于计数器设置的初始值不一样,前者能够看出为0,后者为1。为0的表示激活状态,为1的表示禁止状态。其中ATOMIC_INIT宏为:
#define ATOMIC_INIT(i) { (i) }
便可看出就是设置的数字。此宏在include/asm-generic/atomic.h中定义。这样就建立了一个名为name的小任务,其处理函数为func。当该函数被调用的时候,data参数就被传递给它。
II、小任务处理函数程序
处理函数的的形式为:void my_tasklet_func(unsigned long data)。这样DECLARE_TASKLET(my_tasklet, my_tasklet_func, data)实现了小任务名和处理函数的绑定,而data就是函数参数。
III、调度编写的tasklet
调度小任务时引用tasklet_schedule(&my_tasklet)函数就能使系统在合适的时候进行调度。函数原型为:
static inline void tasklet_schedule(struct tasklet_struct *t)
{
if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state))
__tasklet_schedule(t);
}
这个调度函数放在中断处理的上半部处理函数中,这样中断申请的时候调用处理函数(即irq_handler_t handler)后,转去执行下半部的小任务。
若是但愿使用DECLARE_TASKLET_DISABLED(name,function,data)建立小任务,那么在激活的时候也得调用相应的函数被使能
tasklet_enable(struct tasklet_struct *); //使能tasklet
tasklet_disble(struct tasklet_struct *); //禁用tasklet
tasklet_init(struct tasklet_struct *,void (*func)(unsigned long),unsigned long);
固然也能够调用tasklet_kill(struct tasklet_struct *)从挂起队列中删除一个小任务。清除指定tasklet的可调度位,即不容许调度该tasklet 。
使用tasklet做为下半部的处理中断的设备驱动程序模板以下:
/*定义tasklet和下半部函数并关联*/
void my_do_tasklet(unsigned long);
DECLARE_TASKLET(my_tasklet, my_tasklet_func, 0);
/*中断处理下半部*/
void my_do_tasklet(unsigned long)
{
……/*编写本身的处理事件内容*/
}
/*中断处理上半部*/
irpreturn_t my_interrupt(unsigned int irq,void *dev_id)
{
……
tasklet_schedule(&my_tasklet)/*调度my_tasklet函数,根据声明将去执行my_tasklet_func函数*/
……
}
/*设备驱动的加载函数*/
int __init xxx_init(void)
{
……
/*申请中断, 转去执行my_interrupt函数并传入参数*/
result=request_irq(my_irq,my_interrupt,IRQF_DISABLED,"xxx",NULL);
……
}
/*设备驱动模块的卸载函数*/
void __exit xxx_exit(void)
{
……
/*释放中断*/
free_irq(my_irq,my_interrupt);
……
}
工做队列的实现
工做队列work_struct结构体,位于/include/linux/workqueue.h
typedef void (*work_func_t)(struct work_struct *work);
struct work_struct {
atomic_long_t data; /*传递给处理函数的参数*/
#define WORK_STRUCT_PENDING 0/*这个工做是否正在等待处理标志*/
#define WORK_STRUCT_FLAG_MASK (3UL)
#define WORK_STRUCT_WQ_DATA_MASK (~WORK_STRUCT_FLAG_MASK)
struct list_head entry; /* 链接全部工做的链表*/
work_func_t func; /* 要执行的函数*/
#ifdef CONFIG_LOCKDEP
struct lockdep_map lockdep_map;
#endif
};
这些结构被链接成链表。当一个工做者线程被唤醒时,它会执行它的链表上的全部工做。工做被执行完毕,它就将相应的work_struct对象从链表上移去。当链表上再也不有对象的时候,它就会继续休眠。能够经过DECLARE_WORK在编译时静态地建立该结构,以完成推后的工做。
#define DECLARE_WORK(n, f) /
struct work_struct n = __WORK_INITIALIZER(n, f)
然后边这个宏为一下内容:
#define __WORK_INITIALIZER(n, f) { /
.data = WORK_DATA_INIT(), /
.entry = { &(n).entry, &(n).entry }, /
.func = (f), /
__WORK_INIT_LOCKDEP_MAP(#n, &(n)) /
}
其为参数data赋值的宏定义为:
#define WORK_DATA_INIT() ATOMIC_LONG_INIT(0)
这样就会静态地建立一个名为n,待执行函数为f,参数为data的work_struct结构。一样,也能够在运行时经过指针建立一个工做:
INIT_WORK(struct work_struct *work, void(*func) (void *));
这会动态地初始化一个由work指向的工做队列,并将其与处理函数绑定。宏原型为:
#define INIT_WORK(_work, _func) /
do { /
static struct lock_class_key __key; /
/
(_work)->data = (atomic_long_t) WORK_DATA_INIT(); /
lockdep_init_map(&(_work)->lockdep_map, #_work, &__key, 0);/
INIT_LIST_HEAD(&(_work)->entry); /
PREPARE_WORK((_work), (_func)); /
} while (0)
在须要调度的时候引用相似tasklet_schedule()函数的相应调度工做队列执行的函数schedule_work(),如:
schedule_work(&work);/*调度工做队列执行*/
若是有时候并不但愿工做立刻就被执行,而是但愿它通过一段延迟之后再执行。在这种状况下,能够调度指定的时间后执行函数:
schedule_delayed_work(&work,delay);函数原型为:
int schedule_delayed_work(struct delayed_work *work, unsigned long delay);
其中是以delayed_work为结构体的指针,而这个结构体的定义是在work_struct结构体的基础上增长了一项timer_list结构体。
struct delayed_work {
struct work_struct work;
struct timer_list timer; /* 延迟的工做队列所用到的定时器,当不须要延迟时初始化为NULL*/
};
这样,便使预设的工做队列直到delay指定的时钟节拍用完之后才会执行。
使用工做队列处理中断下半部的设备驱动程序模板以下:
/*定义工做队列和下半部函数并关联*/
struct work_struct my_wq;
void my_do_work(unsigned long);
/*中断处理下半部*/
void my_do_work(unsigned long)
{
……/*编写本身的处理事件内容*/
}
/*中断处理上半部*/
irpreturn_t my_interrupt(unsigned int irq,void *dev_id)
{
……
schedule_work(&my_wq)/*调度my_wq函数,根据工做队列初始化函数将去执行my_do_work函数*/
……
}
/*设备驱动的加载函数*/
int __init xxx_init(void)
{
……
/*申请中断,转去执行my_interrupt函数并传入参数*/
result=request_irq(my_irq,my_interrupt,IRQF_DISABLED,"xxx",NULL);
……
/*初始化工做队列函数,并与自定义处理函数关联*/
INIT_WORK(&my_irq,(void (*)(void *))my_do_work);
……
}
/*设备驱动模块的卸载函数*/
void __exit xxx_exit(void)
{
……
/*释放中断*/
free_irq(my_irq,my_interrupt);
……
}
深刻剖析Linux中断机制之三---Linux对异常和中断的处理
【摘要】本文详解了Linux内核的中断实现机制。首先介绍了中断的一些基本概念,而后分析了面向对象的Linux中断的组织形式、三种主要数据结构及其之间的关系。随后介绍了Linux处理异常和中断的基本流程,在此基础上分析了中断处理的详细流程,包括保存现场、中断处理、中断退出时的软中断执行及中断返回时的进程切换等问题。最后介绍了中断相关的API,包括中断注册和释放、中断关闭和使能、如何编写中断ISR、共享中断、中断上下文中断状态等。
【关键字】中断,异常,hw_interrupt_type,irq_desc_t,irqaction,asm_do_IRQ,软中断,进程切换,中断注册释放request_irq,free_irq,共享中断,可重入,中断上下文
1 Linux对异常和中断的处理
1.1 异常处理
Linux利用异常来达到两个大相径庭的目的:
² 给进程发送一个信号以通报一个反常状况
² 管理硬件资源
对于第一种状况,例如,若是进程执行了一个被0除的操做,CPU则会产生一个“除法错误”异常,并由相应的异常处理程序向当前进程发送一个SIGFPE信号。当前进程接收到这个信号后,就要采起若干必要的步骤,或者从错误中恢复,或者终止执行(若是这个信号没有相应的信号处理程序)。
内核对异常处理程序的调用有一个标准的结构,它由如下三部分组成:
² 在内核栈中保存大多数寄存器的内容(由汇编语言实现)
² 调用C编写的异常处理函数
² 经过ret_from_exception()函数从异常退出。
1.2 中断处理
当一个中断发生时,并非全部的操做都具备相同的急迫性。事实上,把全部的操做都放进中断处理程序自己并不合适。须要时间长的、非重要的操做应该推后,由于当一个中断处理程序正在运行时,相应的IRQ中断线上再发出的信号就会被忽略。另外中断处理程序不能执行任何阻塞过程,如I/O设备操做。所以,Linux把一个中断要执行的操做分为下面的三类:
² 紧急的(Critical)
这样的操做诸如:中断到来时中断控制器作出应答,对中断控制器或设备控制器从新编程,或者对设备和处理器同时访问的数据结构进行修改。这些操做都是紧急的,应该被很快地执行,也就是说,紧急操做应该在一个中断处理程序内当即执行,并且是在禁用中断的状态下。
² 非紧急的(Noncritical)
这样的操做如修改那些只有处理器才会访问的数据结构(例如,按下一个键后,读扫描码)。这些操做也要很快地完成,所以,它们由中断处理程序当即执行,但在启用中断的状态下。
² 非紧急可延迟的(Noncritical deferrable)
这样的操做如,把一个缓冲区的内容拷贝到一些进程的地址空间(例如,把键盘行缓冲区的内容发送到终端处理程序的进程)。这些操做可能被延迟较长的时间间隔而不影响内核操做,有兴趣的进程会等待须要的数据。
全部的中断处理程序都执行四个基本的操做:
² 在内核栈中保存IRQ的值和寄存器的内容。
² 给与IRQ中断线相连的中断控制器发送一个应答,这将容许在这条中断线上进一步发出中断请求。
² 执行共享这个IRQ的全部设备的中断服务例程(ISR)。
² 跳到ret_to_usr( )的地址后终止。
1.3 中断处理程序的执行流程
1.3.1 流程概述
如今,咱们能够从中断请求的发生到CPU的响应,再到中断处理程序的调用和返回,沿着这一思路走一遍,以体会Linux内核对中断的响应及处理。
假定外设的驱动程序都已完成了初始化工做,而且已把相应的中断服务例程挂入到特定的中断请求队列。又假定当前进程正在用户空间运行(随时能够接受中断),且外设已产生了一次中断请求,CPU就在执行完当前指令后来响应该中断。
中断处理系统在Linux中的实现是很是依赖于体系结构的,实现依赖于处理器、所使用的中断控制器的类型、体系结构的设计及机器自己。
设备产生中断,经过总线把电信号发送给中断控制器。若是中断线是激活的,那么中断控制器就会把中断发往处理器。在大多数体系结构中,这个工做就是经过电信号给处理器的特定管脚发送一个信号。除非在处理器上禁止该中断,不然,处理器会当即中止它正在作的事,关闭中断系统,而后跳到内存中预约义的位置开始执行那里的代码。这个预约义的位置是由内核设置的,是中断处理程序的入口点。
对于ARM系统来讲,有个专用的IRQ运行模式,有一个统一的入口地址。假定中断发生时CPU运行在用户空间,而中断处理程序属于内核空间,所以,要进行堆栈的切换。也就是说,CPU从TSS中取出内核栈指针,并切换到内核栈(此时栈还为空)。
若当前处于内核空间时,对于ARM系统来讲是处于SVC模式,此时产生中断,中断处理完毕后,如果可剥夺内核,则检查是否须要进行进程调度,不然直接返回到被中断的内核空间;若须要进行进程调度,则svc_preempt,进程切换。
190 .align 5
191__irq_svc:
192 svc_entry
197#ifdef CONFIG_PREEMPT
198 get_thread_info tsk
199 ldr r8, [tsk, #TI_PREEMPT] @ get preempt count
200 add r7, r8, #1 @ increment it
201 str r7, [tsk, #TI_PREEMPT]
202#endif
203
204 irq_handler
205#ifdef CONFIG_PREEMPT
206 ldr r0, [tsk, #TI_FLAGS] @ get flags
207 tst r0, #_TIF_NEED_RESCHED
208 blne svc_preempt
209preempt_return:
210 ldr r0, [tsk, #TI_PREEMPT] @ read preempt value
211 str r8, [tsk, #TI_PREEMPT] @ restore preempt count
212 teq r0, r7
213 strne r0, [r0, -r0] @ bug()
214#endif
215 ldr r0, [sp, #S_PSR] @ irqs are already disabled
216 msr spsr_cxsf, r0
221 ldmia sp, {r0 - pc}^ @ load r0 - pc, cpsr
222
223 .ltorg
当前处于用户空间时,对于ARM系统来讲是处于USR模式,此时产生中断,中断处理完毕后,不管是不是可剥夺内核,都调转到统一的用户模式出口ret_to_user,其检查是否须要进行进程调度,若须要进行进程调度,则进程切换,不然直接返回到被中断的用户空间。
404 .align 5
405__irq_usr:
406 usr_entry
407
411 get_thread_info tsk
412#ifdef CONFIG_PREEMPT
413 ldr r8, [tsk, #TI_PREEMPT] @ get preempt count
414 add r7, r8, #1 @ increment it
415 str r7, [tsk, #TI_PREEMPT]
416#endif
417
418 irq_handler
419#ifdef CONFIG_PREEMPT
420 ldr r0, [tsk, #TI_PREEMPT]
421 str r8, [tsk, #TI_PREEMPT]
422 teq r0, r7
423 strne r0, [r0, -r0] @ bug()
424#endif
428
429 mov why, #0
430 b ret_to_user
432 .ltorg
1.3.2 保存现场
105/*
106 * SVC mode handlers
107 */
108
115 .macro svc_entry
116 sub sp, sp, #S_FRAME_SIZE
117 SPFIX( tst sp, #4 )
118 SPFIX( bicne sp, sp, #4 )
119 stmib sp, {r1 - r12}
120
121 ldmia r0, {r1 - r3}
122 add r5, sp, #S_SP @ here for interlock avoidance
123 mov r4, #-1 @ "" "" "" ""
124 add r0, sp, #S_FRAME_SIZE @ "" "" "" ""
125 SPFIX( addne r0, r0, #4 )
126 str r1, [sp] @ save the "real" r0 copied
127 @ from the exception stack
128
129 mov r1, lr
130
131 @
132 @ We are now ready to fill in the remaining blanks on the stack:
133 @
134 @ r0 - sp_svc
135 @ r1 - lr_svc
136 @ r2 - lr_<exception>, already fixed up for correct return/restart
137 @ r3 - spsr_<exception>
138 @ r4 - orig_r0 (see pt_regs definition in ptrace.h)
139 @
140 stmia r5, {r0 - r4}
141 .endm
1.3.3 中断处理
由于C的调用惯例是要把函数参数放在栈的顶部,所以pt- regs结构包含原始寄存器的值,这些值是之前在汇编入口例程svc_entry中保存在栈中的。
linux+v2.6.19/include/asm-arm/arch-at91rm9200/entry-macro.S
18 .macro get_irqnr_and_base, irqnr, irqstat, base, tmp
19 ldr /base, =(AT91_VA_BASE_SYS) @ base virtual address of SYS peripherals
20 ldr /irqnr, [/base, #AT91_AIC_IVR] @ read IRQ vector register: de-asserts nIRQ to processor (and clears interrupt)
21 ldr /irqstat, [/base, #AT91_AIC_ISR] @ read interrupt source number
22 teq /irqstat, #0 @ ISR is 0 when no current interrupt, or spurious interrupt
23 streq /tmp, [/base, #AT91_AIC_EOICR] @ not going to be handled further, then ACK it now.
24 .endm
26/*
27 * Interrupt handling. Preserves r7, r8, r9
28 */
29 .macro irq_handler
301: get_irqnr_and_base r0, r6, r5, lr
31 movne r1, sp
32 @
33 @ routine called with r0 = irq number, r1 = struct pt_regs *
34 @
35 adrne lr, 1b
36 bne asm_do_IRQ
58 .endm
中断号的值也在irq_handler初期得以保存,因此,asm_do_IRQ能够将它提取出来。这个中断处理程序实际上要调用do_IRQ(),而do_IRQ()要调用handle_IRQ_event()函数,最后这个函数才真正地执行中断服务例程(ISR)。下图给出它们的调用关系:
中断处理函数的调用关系
1.3.3.1 asm_do_IRQ
112asmlinkage void asm_do_IRQ(unsigned int irq, struct pt_regs *regs)
113{
114 struct pt_regs *old_regs = set_irq_regs(regs);
115 struct irqdesc *desc = irq_desc + irq;
116
121 if (irq >= NR_IRQS)
122 desc = &bad_irq_desc;
123
124 irq_enter(); //记录硬件中断状态,便于跟踪中断状况肯定是不是中断上下文
125
126 desc_handle_irq(irq, desc);
///////////////////desc_handle_irq
33static inline void desc_handle_irq(unsigned int irq, struct irq_desc *desc)
34{
35 desc->handle_irq(irq, desc); //一般handle_irq指向__do_IRQ
36}
///////////////////desc_handle_irq
130
131 irq_exit(); //中断退出前执行可能的软中断,被中断前是在中断上下文中则直接退出,这保证了软中断不会嵌套
132 set_irq_regs(old_regs);
133}
1.3.3.2 __do_IRQ
157 * __do_IRQ - original all in one highlevel IRQ handler
167fastcall unsigned int __do_IRQ(unsigned int irq)
168{
169 struct irq_desc *desc = irq_desc + irq;
170 struct irqaction *action;
171 unsigned int status;
172
173 kstat_this_cpu.irqs[irq]++;
186
187 spin_lock(&desc->lock);
188 if (desc->chip->ack) //首先响应中断,一般实现为关闭本中断线
189 desc->chip->ack(irq);
190
194 status = desc->status & ~(IRQ_REPLAY | IRQ_WAITING);
195 status |= IRQ_PENDING; /* we _want_ to handle it */
196
201 action = NULL;
202 if (likely(!(status & (IRQ_DISABLED | IRQ_INPROGRESS)))) {
203 action = desc->action;
204 status &= ~IRQ_PENDING; /* we commit to handling */
205 status |= IRQ_INPROGRESS; /* we are handling it */
206 }
207 desc->status = status;
208
215 if (unlikely(!action))
216 goto out;
217
218 /*
219 * Edge triggered interrupts need to remember
220 * pending events.
227 */
228 for (;;) {
229 irqreturn_t action_ret;
230
231 spin_unlock(&desc->lock);//解锁,中断处理期间能够响应其余中断,不然再次进入__do_IRQ时会死锁
233 action_ret = handle_IRQ_event(irq, action);
237 spin_lock(&desc->lock);
238 if (likely(!(desc->status & IRQ_PENDING)))
239 break;
240 desc->status &= ~IRQ_PENDING;
241 }
242 desc->status &= ~IRQ_INPROGRESS;
243
244out:
249 desc->chip->end(irq);
250 spin_unlock(&desc->lock);
251
252 return 1;
253}
该函数的实现用到中断线的状态,下面给予具体说明:
#define IRQ_INPROGRESS 1 /* 正在执行这个IRQ的一个处理程序*/
#define IRQ_DISABLED 2 /* 由设备驱动程序已经禁用了这条IRQ中断线 */
#define IRQ_PENDING 4 /* 一个IRQ已经出如今中断线上,且被应答,但尚未为它提供服务 */
#define IRQ_REPLAY 8 /* 当Linux从新发送一个已被删除的IRQ时 */
#define IRQ_WAITING 32 /*当对硬件设备进行探测时,设置这个状态以标记正在被测试的irq */
#define IRQ_LEVEL 64 /* IRQ level triggered */
#define IRQ_MASKED 128 /* IRQ masked - shouldn't be seen again */
#define IRQ_PER_CPU 256 /* IRQ is per CPU */
这8个状态的前5个状态比较经常使用,所以咱们给出了具体解释。
经验代表,应该避免在同一条中断线上的中断嵌套,内核经过IRQ_PENDING标志位的应用保证了这一点。当do_IRQ()执行到for (;;)循环时,desc->status 中的IRQ_PENDING的标志位确定为0。当CPU执行完handle_IRQ_event()函数返回时,若是这个标志位仍然为0,那么循环就此结束。若是这个标志位变为1,那就说明这条中断线上又有中断产生(对单CPU而言),因此循环又执行一次。经过这种循环方式,就把可能发生在同一中断线上的嵌套循环化解为“串行”。
在循环结束后调用desc->handler->end()函数,具体来讲,若是没有设置IRQ_DISABLED标志位,就启用这条中断线。
1.3.3.3 handle_IRQ_event
当执行到for (;;)这个无限循环时,就准备对中断请求队列进行处理,这是由handle_IRQ_event()函数完成的。由于中断请求队列为一临界资源,所以在进入这个函数前要加锁。
handle_IRQ_event执行全部的irqaction链表:
130irqreturn_t handle_IRQ_event(unsigned int irq, struct irqaction *action)
131{
132 irqreturn_t ret, retval = IRQ_NONE;
133 unsigned int status = 0;
134
135 handle_dynamic_tick(action);
136 // 若是没有设置IRQF_DISABLED,则中断处理过程当中,打开中断
137 if (!(action->flags & IRQF_DISABLED))
138 local_irq_enable_in_hardirq();
139
140 do {
141 ret = action->handler(irq, action->dev_id);
142 if (ret == IRQ_HANDLED)
143 status |= action->flags;
144 retval |= ret;
145 action = action->next;
146 } while (action);
147
150 local_irq_disable();
151
152 return retval;
153}
这个循环依次调用请求队列中的每一个中断服务例程。这里要说明的是,若是设置了IRQF_DISABLED,则中断服务例程在关中断的条件下进行(不包括非屏蔽中断),但一般CPU在穿过中断门时自动关闭中断。可是,关中断时间毫不能太长,不然就可能丢失其它重要的中断。也就是说,中断服务例程应该处理最紧急的事情,而把剩下的事情交给另一部分来处理。即后半部分(bottom half)来处理,这一部份内容将在下一节进行讨论。
不一样的CPU不容许并发地进入同一中断服务例程,不然,那就要求全部的中断服务例程必须是“可重入”的纯代码。可重入代码的设计和实现就复杂多了,所以,Linux在设计内核时巧妙地“避难就易”,以解决问题为主要目标。
中断退出前执行可能的软中断,被中断前是在中断上下文中则直接退出,这保证了软中断不会嵌套
////////////////////////////////////////////////////////////
linux+v2.6.19/kernel/softirq.c
285void irq_exit(void)
286{
287 account_system_vtime(current);
288 trace_hardirq_exit();
289 sub_preempt_count(IRQ_EXIT_OFFSET);
290 if (!in_interrupt() && local_softirq_pending())
291 invoke_softirq();
////////////
276#ifdef __ARCH_IRQ_EXIT_IRQS_DISABLED
277# defineinvoke_softirq() __do_softirq()
278#else
279# defineinvoke_softirq() do_softirq()
280#endif
////////////
292 preempt_enable_no_resched();
293}
////////////////////////////////////////////////////////////
1.3.4 从中断返回
asm_do_IRQ()这个函数处理全部外设的中断请求后就要返回。返回状况取决于中断前程序是内核态仍是用户态以及是不是可剥夺内核。
² 内核态可剥夺内核,只有在preempt_count为0时,schedule()才会被调用,其检查是否须要进行进程切换,须要的话就切换。在schedule()返回以后,或者若是没有挂起的工做,那么原来的寄存器被恢复,内核恢复到被中断的内核代码。
² 内核态不可剥夺内核,则直接返回至被中断的内核代码。
² 中断前处于用户态时,不管是不是可剥夺内核,统一跳转到ret_to_user。
虽然咱们这里讨论的是中断的返回,但实际上中断、异常及系统调用的返回是放在一块儿实现的,所以,咱们经常以函数的形式提到下面这三个入口点:
ret_to_user()
终止中断处理程序
ret_slow_syscall ( ) 或者ret_fast_syscall
终止系统调用,即由0x80引发的异常
ret_from_exception( )
终止除了0x80的全部异常
565/*
566 * This is the return code to user mode for abort handlers
567 */
568ENTRY(ret_from_exception)
569 get_thread_info tsk
570 mov why, #0
571 b ret_to_user
57ENTRY(ret_to_user)
58ret_slow_syscall:
由上可知,中断和异常须要返回用户空间时以及系统调用完毕后都须要通过统一的出口ret_slow_syscall,以此决定是否进行进程调度切换等。
linux+v2.6.19/arch/arm/kernel/entry-common.S
16 .align 5
17/*
18 * This is the fast syscall return path. We do as little as
19 * possible here, and this includes saving r0 back into the SVC
20 * stack.
21 */
22ret_fast_syscall:
23 disable_irq @ disable interrupts
24 ldr r1, [tsk, #TI_FLAGS]
25 tst r1, #_TIF_WORK_MASK
26 bne fast_work_pending
27
28 @ fast_restore_user_regs
29 ldr r1, [sp, #S_OFF + S_PSR] @ get calling cpsr
30 ldr lr, [sp, #S_OFF + S_PC]! @ get pc
31 msr spsr_cxsf, r1 @ save in spsr_svc
32 ldmdb sp, {r1 - lr}^ @ get calling r1 - lr
33 mov r0, r0
34 add sp, sp, #S_FRAME_SIZE - S_PC
35 movs pc, lr @ return & move spsr_svc into cpsr
36
37/*
38 * Ok, we need to do extra processing, enter the slow path.
39 */
40fast_work_pending:
41 str r0, [sp, #S_R0+S_OFF]! @ returned r0
42work_pending:
43 tst r1, #_TIF_NEED_RESCHED
44 bne work_resched
45 tst r1, #_TIF_NOTIFY_RESUME | _TIF_SIGPENDING
46 beq no_work_pending
47 mov r0, sp @ 'regs'
48 mov r2, why @ 'syscall'
49 bl do_notify_resume
50 b ret_slow_syscall @ Check work again
51
52work_resched:
53 bl schedule
54/*
55 * "slow" syscall return path. "why" tells us if this was a real syscall.
56 */
57ENTRY(ret_to_user)
58ret_slow_syscall:
59 disable_irq @ disable interrupts
60 ldr r1, [tsk, #TI_FLAGS]
61 tst r1, #_TIF_WORK_MASK
62 bne work_pending
63no_work_pending:
64 @ slow_restore_user_regs
65 ldr r1, [sp, #S_PSR] @ get calling cpsr
66 ldr lr, [sp, #S_PC]! @ get pc
67 msr spsr_cxsf, r1 @ save in spsr_svc
68 ldmdb sp, {r0 - lr}^ @ get calling r1 - lr
69 mov r0, r0
70 add sp, sp, #S_FRAME_SIZE - S_PC
71 movs pc, lr @ return & move spsr_svc into cpsr
进入ret_slow_syscall后,首先关中断,也就是说,执行这段代码时CPU不接受任何中断请求。而后,看调度标志是否为非0(tst r1, #_TIF_NEED_RESCHED),若是调度标志为非0,说明须要进行调度,则去调用schedule()函数进行进程调度。
【嵌入式Linux学习七步曲之第五篇 Linux内核及驱动编程】中断服务下半部之工做队列详解
【摘要】本文详解了中断服务下半部之工做队列实现机制。介绍了工做队列的特色、其与tasklet和softirq的区别以及其使用场合。接着分析了工做队列的三种数据结构的组织形式,在此基础之上分析了工做队列执行流程。最后介绍了工做队列相关的API,如何编写本身的工做队列处理程序及定义一个work对象并向内核提交等待调度运行。
【关键字】中断下半部,工做队列,workqueue_struct,work_struct,DECLARE_WORK,schedule_work,schedule_delayed_work ,flush_workqueue,create_workqueue,destroy_workqueue
1 工做队列概述
工做队列(work queue)是另一种将工做推后执行的形式,它和咱们前面讨论的全部其余形式都不相同。工做队列能够把工做推后,交由一个内核线程去执行—这个下半部分老是会在进程上下文执行,但因为是内核线程,其不能访问用户空间。最重要特色的就是工做队列容许从新调度甚至是睡眠。
一般,在工做队列和软中断/tasklet中做出选择很是容易。可以使用如下规则:
² 若是推后执行的任务须要睡眠,那么只能选择工做队列;
² 若是推后执行的任务须要延时指定的时间再触发,那么使用工做队列,由于其能够利用timer延时;
² 若是推后执行的任务须要在一个tick以内处理,则使用软中断或tasklet,由于其能够抢占普通进程和内核线程;
² 若是推后执行的任务对延迟的时间没有任何要求,则使用工做队列,此时一般为可有可无的任务。
另外若是你须要用一个能够从新调度的实体来执行你的下半部处理,你应该使用工做队列。它是唯一能在进程上下文运行的下半部实现的机制,也只有它才能够睡眠。这意味着在你须要得到大量的内存时、在你须要获取信号量时,在你须要执行阻塞式的I/O操做时,它都会很是有用。
实际上,工做队列的本质就是将工做交给内核线程处理,所以其能够用内核线程替换。可是内核线程的建立和销毁对编程者的要求较高,而工做队列实现了内核线程的封装,不易出错,因此咱们也推荐使用工做队列。
2 工做队列的实现
2.1 工做者线程
工做队列子系统是一个用于建立内核线程的接口,经过它建立的进程负责执行由内核其余部分排到队列里的任务。它建立的这些内核线程被称做工做者线程(worker thread)。工做队列可让你的驱动程序建立一个专门的工做者线程来处理须要推后的工做。不过,工做队列子系统提供了一个默认的工做者线程来处理这些工做。所以,工做队列最基本的表现形式就转变成了一个把须要推后执行的任务交给特定的通用线程这样一种接口。
默认的工做者线程叫作events/n,这里n是处理器的编号,每一个处理器对应一个线程。好比,单处理器的系统只有events/0这样一个线程。而双处理器的系统就会多一个events/1线程。
默认的工做者线程会从多个地方获得被推后的工做。许多内核驱动程序都把它们的下半部交给默认的工做者线程去作。除非一个驱动程序或者子系统必须创建一个属于它本身的内核线程,不然最好使用默认线程。不过并不存在什么东西可以阻止代码建立属于本身的工做者线程。若是你须要在工做者线程中执行大量的处理操做,这样作或许会带来好处。处理器密集型和性能要求严格的任务会由于拥有本身的工做者线程而得到好处。
2.2 工做队列的组织结构
外部可见的工做队列抽象,用户接口,是由每一个CPU的工做队列组成的链表
64struct workqueue_struct {
65 struct cpu_workqueue_struct *cpu_wq;
66 const char *name;
67 struct list_head list; /* Empty if single thread */
68};
² cpu_wq:本队列包含的工做者线程;
² name:全部本队列包含的线程的公共名称部分,建立工做队列时的惟一用户标识;
² list:连接本队列的各个工做线程。
在早期的版本中,cpu_wq是用数组维护的,即对每一个工做队列,每一个CPU包含一个此线程。改为链表的优点在于,建立工做队列的时候能够指定只建立一个内核线程,这样消耗的资源较少。
在该结构体里面,给每一个线程分配一个cpu_workqueue_struct,于是也就是给每一个处理器分配一个,由于每一个处理器都有一个该类型的工做者线程。
这个结构是针对每一个CPU的,属于内核维护的结构,用户不可见。
43struct cpu_workqueue_struct {
44
45 spinlock_t lock;
46
47 long remove_sequence; /* Least-recently added (next to run) */
48 long insert_sequence; /* Next to add */
49
50 struct list_head worklist;
51 wait_queue_head_t more_work;
52 wait_queue_head_t work_done;
53
54 struct workqueue_struct *wq;
55 struct task_struct *thread;
56
57 int run_depth; /* Detect run_workqueue() recursion depth */
58} ____cacheline_aligned;
² lock:操做该数据结构的互斥锁
² remove_sequence:下一个要执行的工做序号,用于flush
² insert_sequence:下一个要插入工做的序号
² worklist:待处理的工做的链表头
² more_work:标识有工做待处理的等待队列,插入新工做后唤醒对应的内核线程
² work_done:处理完的等待队列,没完成一个工做后,唤醒可能等待通知处理完成通知的线程
² wq:所属的工做队列节点
² thread:关联的内核线程指针
² run_depth:run_workqueue()循环深度,多处可能调用此函数
全部的工做者线程都是用普通的内核线程实现的,它们都要执行worker thread()函数。在它初始化完之后,这个函数执行一个死循环并开始休眠。当有操做被插入到队列里的时候,线程就会被唤醒,以便执行这些操做。当没有剩余的操做时,它又会继续休眠。
工做用work_struct结构体表示:
linux+v2.6.19/include/linux/workqueue.h
14struct work_struct {
15 unsigned long pending;
16 struct list_head entry;
17 void (*func)(void *);
18 void *data;
19 void *wq_data;
20 struct timer_list timer;
21};
² Pending:这个工做是否正在等待处理标志,加入到工做队列后置此标志
² Entry:该工做在链表中的入口点,链接全部工做
² Func:该工做执行的回调函数
² Data:传递给处理函数的参数
² wq_data:本工做所挂接的cpu_workqueue_struct;若须要使用定时器,则其为工做队列传递给timer
² timer:延迟的工做队列所用到的定时器,无需延迟是初始化为NULL
2.2.4 三者的关系
位于最高一层的是工做队列。系统容许有多种类型的工做队列存在。每个工做队列具有一个workqueue_struct,而SMP机器上每一个CPU都具有一个该类的工做者线程cpu_workqueue_struct,系统经过CPU号和workqueue_struct 的链表指针及第一个成员cpu_wq能够获得每一个CPU的cpu_workqueue_struct结构。
而每一个工做提交时,将连接在当前CPU的cpu_workqueue_struct结构的worklist链表中。一般状况下由当前所注册的CPU执行此工做,但在flush_work中可能由其余CPU来执行。或者CPU热插拔后也将进行工做的转移。
内核中有些部分能够根据须要来建立工做队列。而在默认状况下内核只有events这一种类型的工做队列。大部分驱动程序都使用的是现存的默认工做者线程。它们使用起来简单、方便。但是,在有些要求更严格的状况下,驱动程序须要本身的工做者线程。
2.3 工做队列执行的细节
工做结构体被链接成链表,对于某个工做队列,在每一个处理器上都存在这样一个链表。当一个工做者线程被唤醒时,它会执行它的链表上的全部工做。工做被执行完毕,它就将相应的work_struct对象从链表上移去。当链表上再也不有对象的时候,它就会继续休眠。
此为工做者线程的标准模板,因此工做者线程都使用此函数。对于用户自定义的内核线程能够参考此函数。
233static int worker_thread(void *__cwq)
234{
235 struct cpu_workqueue_struct *cwq = __cwq;
// 与该工做者线程关联的cpu_workqueue_struct结构
236 DECLARE_WAITQUEUE(wait, current);
// 声明一个等待节点,若无工做,则睡眠
237 struct k_sigaction sa;
238 sigset_t blocked;
239
240 current->flags |= PF_NOFREEZE;
241
242 set_user_nice(current, -5);
// 设定较低的进程优先级, 工做进程不是个很紧急的进程,不和其余进程抢占CPU,一般在系统空闲时运行
244 /* 禁止并清除全部信号 */
245 sigfillset(&blocked);
246 sigprocmask(SIG_BLOCK, &blocked, NULL);
247 flush_signals(current);
248
255 /* SIG_IGN makes children autoreap: see do_notify_parent(). */
// 容许SIGCHLD信号,并设置处理函数
256 sa.sa.sa_handler = SIG_IGN;
257 sa.sa.sa_flags = 0;
258 siginitset(&sa.sa.sa_mask, sigmask(SIGCHLD));
259 do_sigaction(SIGCHLD, &sa, (struct k_sigaction *)0);
260
261 set_current_state(TASK_INTERRUPTIBLE);
// 可被信号中断,适当的时刻可被杀死,若收到中止命令则退出返回,不然进程就一直运行,无工做可执行时,主动休眠
262 while (!kthread_should_stop()) {
// 为了便于remove_wait_queue的统一处理,将当前内核线程添加到cpu_workqueue_struct的more_work等待队列中,当有新work结构链入队列中时会激活此等待队列
263 add_wait_queue(&cwq->more_work, &wait);
// 判断是否有工做须要做,无则调度让出CPU等待唤醒
264 if (list_empty(&cwq->worklist))
265 schedule();
266 else
267 __set_current_state(TASK_RUNNING);
268 remove_wait_queue(&cwq->more_work, &wait);
// 至此,线程确定处于TASK_RUNNING,从等待队列中移出
//须要再次判断是由于可能从schedule中被唤醒的。若是有工做作,则执行
270 if (!list_empty(&cwq->worklist))
271 run_workqueue(cwq);
// 无工做或者所有执行完毕了,循环整个过程,接着通常会休眠
272 set_current_state(TASK_INTERRUPTIBLE);
273 }
274 __set_current_state(TASK_RUNNING);
275 return 0;
276}
该函数在死循环中完成了如下功能:
² 线程将本身设置为休眠状态TASK_INTERRUPTIBLE并把本身加人到等待队列上。
² 若是工做链表是空的,线程调用schedule()函数进入睡眠状态。
² 若是链表中有对象,线程不会睡眠。相反,它将本身设置成TASK_RUNNING,脱离等待队列。
² 若是链表非空,调用run_workqueue函数执行被推后的工做。
run_workqueue执行具体的工做,多处会调用此函数。在调用Flush_work时为防止死锁,主动调用run_workqueue,此时可能致使多层次递归。
196static void run_workqueue(struct cpu_workqueue_struct *cwq)
197{
198 unsigned long flags;
199
204 spin_lock_irqsave(&cwq->lock, flags);
// 统计已经递归调用了多少次了
205 cwq->run_depth++;
206 if (cwq->run_depth > 3) {
207 /* morton gets to eat his hat */
208 printk("%s: recursion depth exceeded: %d/n",
209 __FUNCTION__, cwq->run_depth);
210 dump_stack();
211 }
212 while (!list_empty(&cwq->worklist)) {
213 struct work_struct *work = list_entry(cwq->worklist.next,
214 struct work_struct, entry);
215 void (*f) (void *) = work->func;
216 void *data = work->data;
217 //将当前节点从链表中删除并初始化其entry
218 list_del_init(cwq->worklist.next);
219 spin_unlock_irqrestore(&cwq->lock, flags);
220
221 BUG_ON(work->wq_data != cwq);
222 clear_bit(0, &work->pending); //清除pengding位,标示已经执行
223 f(data);
224
225 spin_lock_irqsave(&cwq->lock, flags);
226 cwq->remove_sequence++;
// // 唤醒可能等待的进程,通知其工做已经执行完毕
227 wake_up(&cwq->work_done);
228 }
229 cwq->run_depth--;
230 spin_unlock_irqrestore(&cwq->lock, flags);
231}
3 工做队列的API
3.1 API列表
功能描述 |
对应API函数 |
附注 |
静态定义一个工做 |
DECLARE_WORK(n, f, d) |
|
动态建立一个工做 |
INIT_WORK(_work, _func, _data) |
|
工做原型 |
void work_handler(void *data) |
|
将工做添加到指定的工做队列中 |
queue_work(struct workqueue_struct *wq, struct work_struct *work) |
|
将工做添加到keventd_wq队列中 |
schedule_work(struct work_struct *work) |
|
延迟delay个tick后将工做添加到指定的工做队列中 |
queue_delayed_work(struct workqueue_struct *wq, struct work_struct *work, unsigned long delay) |
|
延迟delay个tick后将工做添加到keventd_wq队列中 |
schedule_delayed_work(struct work_struct *work, unsigned long delay) |
|
刷新等待指定队列中的全部工做完成 |
flush_workqueue(struct workqueue_struct *wq) |
|
刷新等待keventd_wq中的全部工做完成 |
flush_scheduled_work(void) |
|
取消指定队列中全部延迟工做 |
cancel_delayed_work(struct work_struct *work) |
|
建立一个工做队列 |
create_workqueue(name) |
|
建立一个单线程的工做队列 |
create_singlethread_workqueue(name) |
|
销毁指定的工做队列 |
destroy_workqueue(struct workqueue_struct *wq) |
|
3.2 如何建立工做
首先要作的是实际建立一些须要推后完成的工做。能够经过DECLARE_WORK在编译时静态地建立该结构体:
27#define __WORK_INITIALIZER(n, f, d) { /
28 .entry = { &(n).entry, &(n).entry }, /
29 .func = (f), /
30 .data = (d), /
31 .timer = TIMER_INITIALIZER(NULL, 0, 0), /
32 }
33
34#define DECLARE_WORK(n, f, d) /
35 struct work_struct n = __WORK_INITIALIZER(n, f, d)
这样就会静态地建立一个名为name,处理函数为func,参数为data的work_struct结构体。
一样,也能够在运行时经过指针建立一个工做:
40#define PREPARE_WORK(_work, _func, _data) /
41 do { /
42 (_work)->func = _func; /
43 (_work)->data = _data; /
44 } while (0)
45
49#define INIT_WORK(_work, _func, _data) /
50 do { /
51 INIT_LIST_HEAD(&(_work)->entry); /
52 (_work)->pending = 0; /
53 PREPARE_WORK((_work), (_func), (_data)); /
54 init_timer(&(_work)->timer); /
55 } while (0)
这会动态地初始化一个由work指向的工做,处理函数为func,参数为data。
不管是动态仍是静态建立,默认定时器初始化为0,即不进行延时调度。
3.3 工做队列处理函数
工做队列处理函数的原型是:
void work_handler(void *data)
这个函数会由一个工做者线程执行,所以,函数会运行在进程上下文中。默认状况下,容许响应中断,而且不持有任何锁。若是须要,函数能够睡眠。须要注意的是,尽管操做处理函数运行在进程上下文中,但它不能访问用户空间,由于内核线程在用户空间没有相关的内存映射。一般在系统调用发生时,内核会表明用户空间的进程运行,此时它才能访问用户空间,也只有在此时它才会映射用户空间的内存。
在工做队列和内核其余部分之间使用锁机制就像在其余的进程上下文中使用锁机制同样方便。这使编写处理函数变得相对容易。
3.4 调度工做
3.4.1 queue_work
建立一个工做的时候无须考虑工做队列的类型。在建立以后,能够调用下面列举的函数。这些函数与schedule-work()以及schedule-delayed-Work()相近,唯一的区别就在于它们针对给定的工做队列而不是默认的event队列进行操做。
将工做添加到当前处理器对应的链表中,但并不能保证此工做由提交该工做的CPU执行。Flushwork时可能执行全部CPU上的工做或者CPU热插拔后将进行工做的转移
107int fastcall queue_work(struct workqueue_struct *wq, struct work_struct *work)
108{
109 int ret = 0, cpu = get_cpu();
// 工做结构还没在队列, 设置pending标志表示把工做结构挂接到队列中
111 if (!test_and_set_bit(0, &work->pending)) {
112 if (unlikely(is_single_threaded(wq)))
113 cpu = singlethread_cpu;
114 BUG_ON(!list_empty(&work->entry));
115 __queue_work(per_cpu_ptr(wq->cpu_wq, cpu), work);
////////////////////////////////
84static void __queue_work(struct cpu_workqueue_struct *cwq,
85 struct work_struct *work)
86{
87 unsigned long flags;
88
89 spin_lock_irqsave(&cwq->lock, flags);
//// 指向CPU工做队列
90 work->wq_data = cwq;
// 加到队列尾部
91 list_add_tail(&work->entry, &cwq->worklist);
92 cwq->insert_sequence++;
// 唤醒工做队列的内核处理线程
93 wake_up(&cwq->more_work);
94 spin_unlock_irqrestore(&cwq->lock, flags);
95}
////////////////////////////////////
116 ret = 1;
117 }
118 put_cpu();
119 return ret;
120}
121EXPORT_SYMBOL_GPL(queue_work);
一旦其所在的处理器上的工做者线程被唤醒,它就会被执行。
3.4.2 schedule_work
在大多数状况下, 并不须要本身创建工做队列,而是只定义工做, 将工做结构挂接到内核预约义的事件工做队列中调度, 在kernel/workqueue.c中定义了一个静态全局量的工做队列static struct workqueue_struct *keventd_wq;
调度工做结构, 将工做结构添加到全局的事件工做队列keventd_wq,调用了queue_work通用模块。对外屏蔽了keventd_wq的接口,用户无需知道此参数,至关于使用了默认参数。keventd_wq由内核本身维护,建立,销毁。
455static struct workqueue_struct *keventd_wq;
463int fastcall schedule_work(struct work_struct *work)
464{
465 return queue_work(keventd_wq, work);
466}
467EXPORT_SYMBOL(schedule_work);
3.4.3 queue_delayed_work
有时候并不但愿工做立刻就被执行,而是但愿它通过一段延迟之后再执行。在这种状况下,
同时也能够利用timer来进行延时调度,到期后才由默认的定时器回调函数进行工做注册。
延迟delay后,被定时器唤醒,将work添加到工做队列wq中。
143int fastcall queue_delayed_work(struct workqueue_struct *wq,
144 struct work_struct *work, unsigned long delay)
145{
146 int ret = 0;
147 struct timer_list *timer = &work->timer;
148
149 if (!test_and_set_bit(0, &work->pending)) {
150 BUG_ON(timer_pending(timer));
151 BUG_ON(!list_empty(&work->entry));
152
153 /* This stores wq for the moment, for the timer_fn */
154 work->wq_data = wq;
155 timer->expires = jiffies + delay;
156 timer->data = (unsigned long)work;
157 timer->function = delayed_work_timer_fn;
////////////////////////////////////
定时器到期后执行的默认函数,其将某个work添加到一个工做队列中,需两个重要信息:
Work:__data定时器的惟一参数
待添加至的队列:由work->wq_data提供
123static void delayed_work_timer_fn(unsigned long __data)
124{
125 struct work_struct *work = (struct work_struct *)__data;
126 struct workqueue_struct *wq = work->wq_data;
127 int cpu = smp_processor_id();
128
129 if (unlikely(is_single_threaded(wq)))
130 cpu = singlethread_cpu;
131
132 __queue_work(per_cpu_ptr(wq->cpu_wq, cpu), work);
133}
////////////////////////////////////
158 add_timer(timer);
159 ret = 1;
160 }
161 return ret;
162}
163EXPORT_SYMBOL_GPL(queue_delayed_work);
3.4.4 schedule_delayed_work
其利用queue_delayed_work实现了默认线程keventd_wq中工做的调度。
477int fastcall schedule_delayed_work(struct work_struct *work, unsigned long delay)
478{
479 return queue_delayed_work(keventd_wq, work, delay);
480}
481EXPORT_SYMBOL(schedule_delayed_work);
3.5 刷新工做
3.5.1 flush_workqueue
排入队列的工做会在工做者线程下一次被唤醒的时候执行。有时,在继续下一步工做以前,你必须保证一些操做已经执行完毕了。这一点对模块来讲就很重要,在卸载以前,它就有可能须要调用下面的函数。而在内核的其余部分,为了防止竟争条件的出现,也可能须要确保再也不有待处理的工做。
出于以上目的,内核准备了一个用于刷新指定工做队列的函数flush_workqueue。其确保全部已经调度的工做已经完成了,不然阻塞直到其执行完毕,一般用于驱动模块的关闭处理。其检查已经每一个CPU上执行完的序号是否大于此时已经待插入的序号。对于新的之后插入的工做,其不受影响。
320void fastcall flush_workqueue(struct workqueue_struct *wq)
321{
322 might_sleep();
323
324 if (is_single_threaded(wq)) {
325 /* Always use first cpu's area. */
326 flush_cpu_workqueue(per_cpu_ptr(wq->cpu_wq, singlethread_cpu));
327 } else {
328 int cpu;
// 被保护的代码可能休眠,故此处使用内核互斥锁而非自旋锁
330 mutex_lock(&workqueue_mutex);
// 将同时调度其余CPU上的工做,这说明了工做并不是在其注册的CPU上执行
331 for_each_online_cpu(cpu)
332 flush_cpu_workqueue(per_cpu_ptr(wq->cpu_wq, cpu));
//////////////////////////
278static void flush_cpu_workqueue(struct cpu_workqueue_struct *cwq)
279{
280 if (cwq->thread == current) {
// keventd自己须要刷新全部工做时,手动调用run_workqueue,不然将形成死锁。
285 run_workqueue(cwq);
286 } else {
287 DEFINE_WAIT(wait);
288 long sequence_needed;
289
290 spin_lock_irq(&cwq->lock);
// 保存队列中当前已有的工做所处的位置,不用等待新插入的工做执行完毕
291 sequence_needed = cwq->insert_sequence;
292
293 while (sequence_needed - cwq->remove_sequence > 0) {
// 若是队列中还有未执行完的工做,则休眠
294 prepare_to_wait(&cwq->work_done, &wait,
295 TASK_UNINTERRUPTIBLE);
296 spin_unlock_irq(&cwq->lock);
297 schedule();
298 spin_lock_irq(&cwq->lock);
299 }
300 finish_wait(&cwq->work_done, &wait);
301 spin_unlock_irq(&cwq->lock);
302 }
303}
//////////////////////////
333 mutex_unlock(&workqueue_mutex);
334 }
335}
336EXPORT_SYMBOL_GPL(flush_workqueue);
函数会一直等待,直到队列中全部对象都被执行之后才返回。在等待全部待处理的工做执行的时候,该函数会进入休眠状态,因此只能在进程上下文中使用它。
注意,该函数并不取消任何延迟执行的工做。就是说,任何经过schedule_delayed_work调度的工做,若是其延迟时间未结束,它并不会由于调用flush_scheduled_work()而被刷新掉。
3.5.2 flush_scheduled_work
刷新系统默认工做线程的函数为flush_scheduled_work,其调用了上面通用的函数
532void flush_scheduled_work(void)
533{
534 flush_workqueue(keventd_wq);
535}
536EXPORT_SYMBOL(flush_scheduled_work);
3.5.3 cancel_delayed_work
取消延迟执行的工做应该调用:
int cancel_delayed_work(struct work_struct *work);
这个函数能够取消任何与work_struct相关的挂起工做。
3.6 建立新的工做队列
若是默认的队列不能知足你的须要,你应该建立一个新的工做队列和与之相应的工做者线程。因为这么作会在每一个处理器上都建立一个工做者线程,因此只有在你明确了必需要靠本身的一套线程来提升性能的状况下,再建立本身的工做队列。
建立一个新的任务队列和与之相关的工做者线程,只需调用一个简单的函数:create_workqueue。这个函数会建立全部的工做者线程(系统中的每一个处理器都有一个)而且作好全部开始处理工做以前的准备工做。name参数用于该内核线程的命名。对于具体的线程会更加CPU号添加上序号。
create_workqueue和create_singlethread_workqueue都是建立一个工做队列,可是差异在于create_singlethread_workqueue能够指定为此工做队列只建立一个内核线程,这样能够节省资源,无需发挥SMP的并行处理优点。
create_singlethread_workqueue对外进行了封装,至关于使用了默认参数。两者同时调用了统一的处理函数__create_workqueue,其对外不可见。
59#define create_workqueue(name) __create_workqueue((name), 0)
60#define create_singlethread_workqueue(name) __create_workqueue((name), 1)
363struct workqueue_struct *__create_workqueue(const char *name,
364 int singlethread)
365{
366 int cpu, destroy = 0;
367 struct workqueue_struct *wq;
368 struct task_struct *p;
369
370 wq = kzalloc(sizeof(*wq), GFP_KERNEL);
371 if (!wq)
372 return NULL;
373
374 wq->cpu_wq = alloc_percpu(struct cpu_workqueue_struct);
375 if (!wq->cpu_wq) {
376 kfree(wq);
377 return NULL;
378 }
379
380 wq->name = name;
381 mutex_lock(&workqueue_mutex);
382 if (singlethread) {
383 INIT_LIST_HEAD(&wq->list); //终止链表
384 p = create_workqueue_thread(wq, singlethread_cpu);
385 if (!p)
386 destroy = 1;
387 else
388 wake_up_process(p);
389 } else {
390 list_add(&wq->list, &workqueues);
391 for_each_online_cpu(cpu) {
392 p = create_workqueue_thread(wq, cpu);
/////////////////////////////////
338static struct task_struct *create_workqueue_thread(struct workqueue_struct *wq,
339 int cpu)
340{
341 struct cpu_workqueue_struct *cwq = per_cpu_ptr(wq->cpu_wq, cpu);
342 struct task_struct *p;
343
344 spin_lock_init(&cwq->lock);
345 cwq->wq = wq;
346 cwq->thread = NULL;
347 cwq->insert_sequence = 0;
348 cwq->remove_sequence = 0;
349 INIT_LIST_HEAD(&cwq->worklist);
350 init_waitqueue_head(&cwq->more_work);
351 init_waitqueue_head(&cwq->work_done);
352
353 if (is_single_threaded(wq))
354 p = kthread_create(worker_thread, cwq, "%s", wq->name);
355 else
356 p = kthread_create(worker_thread, cwq, "%s/%d", wq->name, cpu);
357 if (IS_ERR(p))
358 return NULL;
359 cwq->thread = p;
360 return p;
361}
/////////////////////////////////
393 if (p) {
394 kthread_bind(p, cpu);
395 wake_up_process(p);
396 } else
397 destroy = 1;
398 }
399 }
400 mutex_unlock(&workqueue_mutex);
401
405 if (destroy) {//若是启动任意一个线程失败,则销毁整个工做队列
406 destroy_workqueue(wq);
407 wq = NULL;
408 }
409 return wq;
410}
411EXPORT_SYMBOL_GPL(__create_workqueue);
3.7 销毁工做队列
销毁一个工做队列,如有未完成的工做,则阻塞等待其完成。而后销毁对应的内核线程。
434void destroy_workqueue(struct workqueue_struct *wq)
435{
436 int cpu;
437
438 flush_workqueue(wq); //等待全部工做完成
439/// 利用全局的互斥锁锁定全部工做队列的操做
441 mutex_lock(&workqueue_mutex);
// 清除相关的内核线程
442 if (is_single_threaded(wq))
443 cleanup_workqueue_thread(wq, singlethread_cpu);
444 else {
445 for_each_online_cpu(cpu)
446 cleanup_workqueue_thread(wq, cpu);
/////////////////////////////////
413static void cleanup_workqueue_thread(struct workqueue_struct *wq, int cpu)
414{
415 struct cpu_workqueue_struct *cwq;
416 unsigned long flags;
417 struct task_struct *p;
418
419 cwq = per_cpu_ptr(wq->cpu_wq, cpu);
420 spin_lock_irqsave(&cwq->lock, flags);
421 p = cwq->thread;
422 cwq->thread = NULL;
423 spin_unlock_irqrestore(&cwq->lock, flags);
424 if (p)
425 kthread_stop(p); //销毁该线程,此处可能休眠
426}
/////////////////////////////////
447 list_del(&wq->list);
448 }
449 mutex_unlock(&workqueue_mutex);
450 free_percpu(wq->cpu_wq);
451 kfree(wq);
452}
453EXPORT_SYMBOL_GPL(destroy_workqueue);
【嵌入式Linux学习七步曲之第五篇 Linux内核及驱动编程】中断服务下半部之七姑八姨
【摘要】本文分析了中断服务下半部存在的必要性,接着介绍了上下半部的分配原则,最后分析了各类下半部机制的历史渊源,简单介绍了各类机制的特色。
【关键字】下半部,bottom half,BH,tasklet,softirq,工做队列,内核定时器
1 下半部,我思故我在
中断处理程序是内核中颇有用的—实际上也是必不可少的—部分。可是,因为自己存在一些局限,因此它只能完成整个中断处理流程的上半部分。这些局限包括:
² 中断处理程序以异步方式执行而且它有可能会打断其余重要代码(甚至包括其余中断处理程序)的执行。所以,为了不被打断的代码中止时间过长,中断处理程序应该执行得越快越好。
² 若是当前有一个中断处理程序正在执行,在最好的状况下与该中断同级的其余中断会被屏蔽,在最坏的状况下,当前处理器上全部其余中断都会被屏蔽。所以,仍应该让它们执行得越快越好。
² 因为中断处理程序每每须要对硬件进行操做,因此它们一般有很高的时限要求。
² 中断处理程序不在进程上下文中运行,因此它们不能阻塞。这限制了它们所作的事情。
如今,为何中断处理程序只能做为整个硬件中断处理流程一部分的缘由就很明显了。咱们必须有一个快速、异步、简单的处理程序负责对硬件作出迅速响应并完成那些时间要求很严格的操做。中断处理程序很适合于实现这些功能,但是,对于那些其余的、对时间要求相对宽松的任务,就应该推后到中断被激活之后再去运行。
这样,整个中断处理流程就被分为了两个部分,或叫两半。第一个部分是中断处理程序(上半部),内核经过对它的异步执行完成对硬件中断的即时响应。下半部(bottom half)负责其余响应。
2 上下半部分家产的原则
下半部的任务就是执行与中断处理密切相关但中断处理程序自己不执行的工做。在理想的状况下,最好是中断处理程序将全部工做都交给下半部分执行,由于咱们但愿在中断处理程序中完成的工做越少越好(也就是越快越好)。咱们指望中断处理程序可以尽量快地返回。
可是,中断处理程序注定要完成一部分工做。例如,中断处理程序几乎都须要经过操做硬件对中断的到达进行确认。有时它还会从硬件拷贝数据。由于这些工做对时间很是敏感,因此只能靠中断处理程序本身去完成。
剩下的几乎全部其余工做都是下半部执行的目标。例如,若是你在上半部中把数据从硬件拷贝到了内存,那么固然应该在下半部中处理它们。遗憾的是,并不存在严格明确的规定来讲明到底什么任务应该在哪一个部分中完成—如何作决定彻底取决于驱动程序开发者本身的判断。记住,中断处理程序会异步执行,而且即便在最好的状况下它也会锁定当前的中断线。所以将中断处理程序持续执行的时间缩短到最小很是重要。上半部和下半部之间划分应大体遵循如下规则:
² 若是一个任务对时间很是敏感,将其放在中断处理程序中执行;
² 若是一个任务和硬件相关,将其放在中断处理程序中执行;
² 若是一个任务要保证不被其余中断(特别是相同的中断)打断,将其放在中断处理程序中执行;
² 其余全部任务,考虑放置在下半部执行。
在决定怎样把你的中断处理流程中的工做划分到上半部和下半部中去的时候,问问本身什么必须放进上半部而什么能够放进下半部。一般,中断处理程序要执行得越快越好。
理解为何要让工做推后执行以及在何时推后执行很是关键。咱们但愿尽可能减小中断处理程序中须要完成的工做量,由于在它运行的时候当前的中断线在全部处理器上都会被屏蔽。更糟糕的是若是一个处理程序是SA_ INTERRUPT类型,它执行的时候会禁止全部本地中断。而缩短中断被屏蔽的时间对系统的响应能力和性能都相当重要。解决的方法就是把一些工做放到之后去作。
但具体放到之后的何时去作呢?在这里,之后仅仅用来强调不是立刻而已,理解这一点至关重要。下半部并不须要指明一个确切时间,只要把这些任务推迟一点,让它们在系统不太繁忙而且中断恢复后执行就能够了。一般下半部在中断处理程序一返回就会立刻运行。下半部执行的关键在于当它们运行的时候,容许响应全部的中断。
不只仅是Linux,许多操做系统也把处理硬件中断的过程分为两个部分。上半部分简单快速,执行的时候禁止一些或者所有中断。下半部分稍后执行,并且执行期间能够响应全部的中断。这种设计可以使系统处干中断屏蔽状态的时间尽量的短,以此来提升系统的响应能力。
3 下半部之七姑八姨
和上半部分只能经过中断处理程序实现不一样,下半部能够经过多种机制实现。这些用来实现下半部的机制分别由不一样的接口和子系统组成。实际上,在Linux发展的过程当中曾经出现过多种下半部机制。让人倍受困扰的是,其中很多机制名字起得很相像,甚至还有一些机制名字起得辞不达意。
最先的Linux只提供“bottom half”这种机制用于实现下半部。这个名字在那个时候毫无异义,由于当时它是将工做推后的唯一方法。这种机制也被称为“BH",咱们如今也这么叫它,以免和“下半部”这个通用词汇混淆。
BH接口也很是简单。它提供了一个静态建立、由32个bottom half组成的数组。上半部经过一个32位整数中的一位来标识出哪一个bottom half能够执行。每一个BH都在全局范围内进行同步。对于本地CPU,严格的串行执行,当被中断重入后,若发现中断前已经在执行BH则退出。即便分属于不一样的处理器,也不容许任何两个bottom half同时执行。若发现另外一CPU正在执行,则退出。这种机制使用方便却不够灵活,简单却有性能瓶颈。
不久,内核开发者们就引入了任务队列(task queue)机制来实现工做的推后执行,并用它来代替BH机制。内核为此定义了一组队列,其中每一个队列都包含一个由等待调用的函数组成链表,这样就至关于实现了二级链表,扩展了BH32个的限制。根据其所处队列的位置,这些函数会在某个时刻被执行。驱动程序能够把它们本身的下半部注册到合适的队列上去。这种机制表现得还不错,但仍不够灵活,无法代替整个BH接口。对于一些性能要求较高的子系统,像网络部分,它也不能胜任。
在2.3这个开发版本中,内核开发者引入了tasklet和软中断softirq。若是无须考虑和过去开发的驱动程序兼容的话,软中断和tasklet能够彻底代替BH接口。
软中断是一组静态定义的下半部接口,有32个,能够在全部处理器上同时执行—即便两个类型相同也能够。
tasklet是一种基于软中断实现的灵活性强、动态建立的下半部实现机制。两个不一样类型的tasklet能够在不一样的处理器上同时执行,但类型相同的tasklet不能同时执行。tasklet实际上是一种在性能和易用性之间寻求平衡的产物。对于大部分下半部处理来讲,用tasklet就足够了。像网络这样对性能要求很是高的状况才须要使用软中断。但是,使用软中断须要特别当心,由于两个相同的软中断在SMP上有可能同时被执行。此外,软中断由数组组织,还必须在编译期间就进行静态注册,即与某个软中断号关联。与此相反,tasklet为某个固定的软中断号,通过二级扩展,维护了一个链表,所以能够动态注册删除。
在开发2.5版本的内核时,BH接口最终被弃置了,全部的BH使用者必须转而使用其余下半部接口。此外,任务队列接口也被工做队列接口取代了。工做队列是一种简单但颇有用的方法,它们先对要推后执行的工做排队,稍后在进程上下文中执行它们。
另一个能够用于将工做推后执行的机制是内核定时器。不像其余下半部机制,内核定时器把操做推迟到某个肯定的时间段以后执行。也就是说,尽管本章讨论的其余机制能够把操做推后到除了如今之外的任什么时候间进行,可是当你必须保证在一个肯定的时间段过去之后再运行时,你应该使用内核定时器。可是执行定时器注册的函数时,仍然须要使用软中断机制,即定时器引入了一个固定延时和一个软中断的可变延时。
把BH转换为软中断或者tasklet并非垂手可得的事,由于BH是全局同步的,所以,在其执行期间假定没有其余BH在执行。可是,这种转换最终仍是在内核2.5中实现了。
“下半部(bottom half)”是一个操做系统通用词汇,用于指代中断处理流程中推后执行的那一部分,之因此这样命名是由于它表示中断处理方案一半的第二部分或者下半部。全部用于实现将工做推后执行的内核机制都被称为“下半部机制”。
综上所述,在2.6这个当前版本中,内核提供了三种不一样形式的下半部实现机制:软中断、tasklet和工做队列。tasklet经过软中断实现,而工做队列与它们彻底不一样。下半部机制的演化历程以下:
【嵌入式Linux学习七步曲之第五篇 Linux内核及驱动编程】中断服务下半部之老大-软中断softirq
【摘要】本文详解了中断服务下半部机制的基础softirq。首先介绍了其数据结构,分析了softirq的执行时机及流程。接着介绍了软中断的API及如何添加本身的软中断程序,注册及其触发。最后了介绍了用于处理过多软中断的内核线程ksoftirqd,分析了触发ksoftirqd的原则及其执行流程。
【关键字】中断服务下半部,软中断softirq,softirq_action,open_softirq(),raise_softirq,ksoftirqd
1 软中断结构softirq_action
2 执行软中断
3 软中断的API
3.1 分配索引号
3.2 软中断处理程序
3.3 注册软中断处理程序
3.4 触发软中断
4 ksoftirqd
4.1 Ksoftirqd的诞生
4.2 启用Ksoftirqd的准则
4.3 Ksoftirqd的实现
1 软中断结构softirq_action
软中断使用得比较少,但其是tasklet实现的基础。而tasklet是下半部更经常使用的一种形式。软中断是在编译期间静态分配的。它不像tasklet那样能被动态地注册或去除。软中断由softirq_action结构表示,它定义在<linux/interrupt.h>中:
246struct softirq_action
247{
248 void (*action)(struct softirq_action *);
249 void *data;
250};
Action: 待执行的函数;
Data: 传给函数的参数,任意类型的指针,在action内部转化
kernel/softirq.c中定义了一个包含有32个该结构体的数组。
static struct softirq_actionsoftirq_vec[32] __cacheline_aligned_in_smp;
每一个被注册的软中断都占据该数组的一项。所以最多可能有32个软中断,由于系统靠一个32位字的各个位来标识是否须要执行某个软中断。注意,这是注册的软中断数目的最大值无法动态改变。在当前版本的内核中,这个项中只用到6个。。
2 执行软中断
一个注册的软中断必须在被标记后才会执行。这被称做触发软中断(raising the softirq )。一般,中断处理程序会在返回前标记它的软中断,使其在稍后被执行。因而,在合适的时刻,该软中断就会运行。在下列地方,待处理的软中断会被检查和执行:
² 从一个硬件中断代码处返回时。
² 在ksoftirqd内核线程中。
² 在那些显式检查和执行待处理的软中断的代码中,如网络子系统中。
无论是用什么办法唤起,软中断都要在do_softirq()中执行。若是有待处理的软中断,do_softirq()会循环遍历每个,调用它们的处理程序。
252#ifndef __ARCH_HAS_DO_SOFTIRQ
253
254asmlinkage void do_softirq(void)
255{
256 __u32 pending;
257 unsigned long flags;
258
259 if (in_interrupt()) //中断函数中不能执行软中断
260 return;
261
262 local_irq_save(flags);
263
264 pending = local_softirq_pending();
265
266 if (pending) //只有有软中断须要处理时才进入__do_softirq
267 __do_softirq();
/////////////////////////
195/*
196 * 最多循环执行MAX_SOFTIRQ_RESTART 次若中断,若仍然有未处理完的,则交由softirqd 在适当的时机处理。须要协调的是延迟和公平性。尽快处理完软中断,但不能过渡影响用户进程的运行。
203 */
204#define MAX_SOFTIRQ_RESTART 10
206asmlinkage void __do_softirq(void)
207{
208 struct softirq_action *h;
209 __u32 pending;
210 int max_restart = MAX_SOFTIRQ_RESTART;
211 int cpu;
212
213 pending = local_softirq_pending();
214 account_system_vtime(current);
215
216 __local_bh_disable((unsigned long)__builtin_return_address(0));
217 trace_softirq_enter();
218
219 cpu = smp_processor_id();
220restart:
221 /* Reset the pending bitmask before enabling irqs */
222 set_softirq_pending(0);
223
224 local_irq_enable();
225
226 h = softirq_vec;
227
228 do {
229 if (pending & 1) {
230 h->action(h);
231 rcu_bh_qsctr_inc(cpu);
232 }
233 h++;
234 pending >>= 1;
235 } while (pending);
236
237 local_irq_disable();
238
239 pending = local_softirq_pending();
240 if (pending && --max_restart)