本序列涉及的 Linux 源码都是基于 linux-4.14.143 。
1.1 文件抽象
在 Linux 内核里,文件是一个抽象,设备是个文件,网络套接字也是个文件。
文件抽象必须支持的能力定义在 file_operations 结构体里。
在 Linux 里,一个打开的文件对应一个文件描述符 file descriptor/FD,FD 其实是一个整数,内核把进程打开的文件维护在一个数组里,FD 对应的是数组的下标。
文件抽象的能力定义:
1.2 文件 poll *** 作
poll 函数的原型:
文件抽象 poll 函数的具体实现必须完成两件事(这两点算是规范了):
1. 在 poll 函数敢兴趣的等待队列上调用 poll_wait 函数,以接收到唤醒;具体的实现必须把 poll_table 类型的参数作为透明对象来使用,不需要知道它的具体结构。
2. 返回比特掩码,表示当前可立即执行而不会阻塞的 *** 作。
下面是某个驱动的 poll 实现示例,来自:https://www.oreilly.com/library/view/linux-device-drivers/0596000081/ch05s03.html:
poll 函数接收的 poll_table 只有一个队列处理函数 _qproc 和感兴趣的事件属性 _key。
文件抽象的具体实现在构建时会初始化一个或多个 wait_queue_head_t 类型的事件等待队列 。
poll 等待的过程:
事件发生时的唤醒过程:
一个小困惑:
随着2.6内核对epoll的完全支持,网络上很多的文章和示例代码都提供了这样一个信息:使用epoll代替传统的poll能给网络服务应用带来性能上的提升。但大多文章里关于性能提升的原因解释的较少,这里我将试分析一下内核(2.6.21.1)代码中poll与epoll的工作原理,然后再通过一些测试数据来对比具体效果。
POLL:
先说poll,poll或select为大部分Unix/Linux程序员所熟悉,这俩个东西原理类似,性能上也不存在明显差异,但select对所监控的文件描述符数量有限制,所以这里选用poll做说明。
poll是一个系统调用,其内核入口函数为sys_poll,sys_poll几乎不做任何处理直接调用do_sys_poll,do_sys_poll的执行过程可以分为三个部分:
1,将用户传入的pollfd数组拷贝到内核空间,因为拷贝 *** 作和数组长度相关,时间上这是一个O(n) *** 作,这一步的代码在do_sys_poll中包括从函数开始到调用do_poll前的部分。
2,查询每个文件描述符对应设备的状态,如果该设备尚未就绪,则在该设备的等待队列中加入一项并继续查询下一设备的状态。查询完所有设备后如果没有一个设备就绪,这时则需要挂起当前进程等待,直到设备就绪或者超时,挂起 *** 作是通过调用schedule_timeout执行的。设备就绪后进程被通知继续运行,这时再次遍历所有设备,以查找就绪设备。这一步因为两次遍历所有设备,时间复杂度也是O(n),这里面不包括等待时间。相关代码在do_poll函数中。
3,将获得的数据传送到用户空间并执行释放内存和剥离等待队列等善后工作,向用户空间拷贝数据与剥离等待队列等 *** 作的的时间复杂度同样是O(n),具体代码包括do_sys_poll函数中调用do_poll后到结束的部分。
EPOLL:
接下来分析epoll,与poll/select不同,epoll不再是一个单独的系统调用,而是由epoll_create/epoll_ctl/epoll_wait三个系统调用组成,后面将会看到这样做的好处。
先来看sys_epoll_create(epoll_create对应的内核函数),这个函数主要是做一些准备工作,比如创建数据结构,初始化数据并最终返回一个文件描述符(表示新创建的虚拟epoll文件),这个 *** 作可以认为是一个固定时间的 *** 作。
epoll是做为一个虚拟文件系统来实现的,这样做至少有以下两个好处:
1,可以在内核里维护一些信息,这些信息在多次epoll_wait间是保持的,比如所有受监控的文件描述符。
2, epoll本身也可以被poll/epoll
具体epoll的虚拟文件系统的实现和性能分析无关,不再赘述。
在sys_epoll_create中还能看到一个细节,就是epoll_create的参数size在现阶段是没有意义的,只要大于零就行。
接着是sys_epoll_ctl(epoll_ctl对应的内核函数),需要明确的是每次调用sys_epoll_ctl只处理一个文件描述符,这里主要描述当op为EPOLL_CTL_ADD时的执行过程,sys_epoll_ctl做一些安全性检查后进入ep_insert,ep_insert里将 ep_poll_callback做为回掉函数加入设备的等待队列(假定这时设备尚未就绪),由于每次poll_ctl只 *** 作一个文件描述符,因此也可以认为这是一个O(1) *** 作
ep_poll_callback函数很关键,它在所等待的设备就绪后被系统回掉,执行两个 *** 作:
1,将就绪设备加入就绪队列,这一步避免了像poll那样在设备就绪后再次轮询所有设备找就绪者,降低了时间复杂度,由O(n)到O(1)
2,唤醒虚拟的epoll文件
最后是sys_epoll_wait,这里实际执行 *** 作的是ep_poll函数。该函数等待将进程自身插入虚拟epoll文件的等待队列,直到被唤醒(见上面ep_poll_callback函数描述),最后执行ep_events_transfer将结果拷贝到用户空间。由于只拷贝就绪设备信息,所以这里的拷贝是一个O(1) *** 作。
还有一个让人关心的问题就是epoll对EPOLLET的处理,即边沿触发的处理,粗略看代码就是把一部分水平触发模式下内核做的工作交给用户来处理,直觉上不会对性能有太大影响,感兴趣的朋友欢迎讨论。
POLL/EPOLL对比:
表面上poll的过程可以看作是由一次epoll_create/若干次epoll_ctl/一次epoll_wait/一次close等系统调用构成,实际上epoll将poll分成若干部分实现的原因正是因为服务器软件中使用poll的特点(比如Web服务器):
1,需要同时poll大量文件描述符
2,每次poll完成后就绪的文件描述符只占所有被poll的描述符的很少一部分。
3,前后多次poll调用对文件描述符数组(ufds)的修改只是很小
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)