IO多路复用、 Epoll(二)

您所在的位置:网站首页 操作系统epoll IO多路复用、 Epoll(二)

IO多路复用、 Epoll(二)

2024-07-12 08:55| 来源: 网络整理| 查看: 265

先看这几个问题

进程阻塞为什么不占用CPU资源?

  网络数据流通过硬件传输,网卡接收的数据存放到内存中。操作系统就可以去读取它们。一般而言,由硬件产生的信号需要CPU立马做

出回应(不然数据可能就丢失),所以它的优先级很高。CPU理应中断掉正在执行的程序,去做出响应;当CPU完成对硬件CPU的响应后,

再重新执行用户程序。

  当网卡把数据写入到内存后,网卡向cpu发出一个中断信号,操作系统便能得知有新数据到来,再通过网卡中断程序去处理数据。

  比如服务器端运行到 recv() 函数的时候,如果对端没有数据发送过来,那么程序会一直阻塞在这里,其实操作系统为了支持多任务,实

现了进程调度的功能,会把进程分为 运行 和 等待 。。。几种状态,行状态是进程获得cpu使用权,正在执行代码的状态;等待状态是阻

塞状态,比如上述程序运行到 recv 时,程序会从运行状态变为等待状态,接收到数据后又变回运行状态。操作系统会分时执行各个运行状

态的进程,由于速度很快,看上去就像是同时执行多个任务。      

等待队列

(1)创建socket :当进程A执行到创建socket的语句时,操作系统会创建一个由文件系统管理的 socket对象(如下图)。这个socket对象包

含了发送缓冲区、接收缓冲区、等待队列等成员。等待队列是个非常重要的结构,它指向所有需要等待该socket事件的进程。 在这里插入图片描述

(2) socket的等待队列:

  当程序执行到recv时,操作系统会将进程A从工作队列移动到该socket的等待队列中(如下图)。由于工作队列只剩下了进程B和C,依

据进程调度,cpu会轮流执行这两个进程的程序,不会执行进程A的程序。所以进程A被阻塞,不会往下执行代码,也不会占用cpu资源。 在这里插入图片描述

  操作系统添加等待队列只是添加了对这个“等待中”进程的引用,以便在接收到数据时获取进程对象、将其唤醒,而非直接将进程管理纳

入自己之下。

     

唤醒进程

  当socket接收到数据后,操作系统将该socket等待队列上的进程重新放回到工作队列,该进程变成运行状态(当socket事件触发是,也就

是有数据到来,会取下一个进程结构调用其回调,将其挂到工作队列中),继续执行代码。同时也由于socket的接收缓冲区已经有了数据,recv

可以返回接收到的数据。

这一步,贯穿网卡、中断、进程调度的知识,叙述阻塞recv下,内核接收数据全过程。

进程在recv阻塞期间,计算机收到了对端传送的数据(步骤①)。           数据经由网卡传送到内存(步骤②),           然后网卡通过中断信号通知cpu有数据到达,cpu执行中断程序(步骤③)。           此处的中断程序主要有两项功能,先将网络数据写入到对应socket的接收缓冲区里面(步骤④),           再唤醒进程A(步骤⑤),重新将进程A放入工作队列中。 在这里插入图片描述 唤醒进程的过程如下图所示 在这里插入图片描述        客户端 服务器端 靠socket连接,监听socket相当于监听文件描述符,这时候就回到 如何监听多个文件描述符的问题 ? ,服务端需要

管理多个客户端连接,而recv只能监视单个socket,使用IO多路复用,就可以监听多个 socket

在这里插入图片描述

  IO多路复用的方法用三种:select、poll、epoll,是逐步发展的,现在主流是 epoll ,select的实现思路很直接。假如程序同时监视如下

图的sock1、sock2和sock3三个socket,那么在调用select之后,操作系统把进程A(包括了select逻辑)分别加入这三个socket的等待队列

中(前文说过,放的其实是进程A的引用)。

   

select的流程

   假如能够预先传入一个socket列表,如果列表中的socket都没有数据,挂起进程,直到有一个socket收到数据,唤醒进程。这种方法很

直接,也是select的设计思想。

   为方便理解,我们先复习select的用法。在如下的代码中,先准备一个数组(下面代码中的fds),让fds存放着所有需要监视的

socket。然后调用select,如果fds中的所有socket都没有数据,select会阻塞,直到有一个socket接收到数据,select返回,唤醒进程。用户

可以遍历fds,通过FD_ISSET判断具体哪个socket收到数据,然后做出处理。

int s = socket(AF_INET, SOCK_STREAM, 0); bind(s, ...) listen(s, ...) int fds[] = 存放需要监听的socket while(1){ int n = select(..., fds, ...) for(int i=0; i 原理概述:

  select 的核心功能是调用tcp文件系统的poll函数,不停的查询,如果没有想要的数据,主动执行一次调度(防止一直占用cpu),直到有一个连接有想要的消息为止。从这里可以看出select的执行方式基本就是不同的调用poll,直到有需要的消息为止。 缺点:

1、每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大;

2、同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大;

3、select支持的文件描述符数量太小了,默认是1024。

优点:

1、select的可移植性更好,在某些Unix系统上不支持poll()。

2、select对于超时值提供了更好的精度:微秒,而poll是毫秒。

poll——>

原理概述:

  poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态,如果设备就绪则在设备等待队列中加入一项并继续遍历,如果遍历完所有fd后没有发现就绪设备,则挂起当前进程,直到设备就绪或者主动超时,被唤醒后它又要再次遍历fd。这个过程经历了多次无谓的遍历。poll还有一个特点是“水平触发”,如果报告了fd后,没有被处理,那么下次poll时会再次报告该fd。

缺点:

1、大量的fd的数组被整体复制于用户态和内核地址空间之间,而不管这样的复制是不是有意义;

2、与select一样,poll返回后,需要轮询pollfd来获取就绪的描述符。

优点:

1、poll() 不要求开发者计算最大文件描述符加一的大小。

2、poll() 在应付大数目的文件描述符的时候速度更快,相比于select。

3、它没有最大连接数的限制,原因是它是基于链表来存储的。    

epoll——> 原理概述:

  epoll同样只告知那些就绪的文件描述符,而且当我们调用epoll_wait()获得就绪文件描述符时, 返回的不是实际的描述符,而是一个代表就绪描述符数量的值,你只需要去epoll指定的一 个数组中依次取得相应数量的文件描述符即可,这里也使用了内存映射技术,这 样便彻底省掉了这些文件描述符在系统调用时复制的开销。

epoll的优点就是改进了前面所说缺点:

1、支持一个进程打开大数目的socket描述符:相比select,epoll则没有对FD的限制,它所支持的FD上限是最大可以打开文件的数目,这个数字一般远大于2048,举个例子,在1GB内存的机器上大约是10万左右,具体数目可以cat /proc/sys/fs/file-max察看,一般来说这个数目和系统内存关系很大。

2、IO效率不随FD数目增加而线性下降:epoll不存在这个问题,它只会对"活跃"的socket进行操作— 这是因为在内核实现中epoll是根据每个fd上面的callback函数实现的。那么,只有"活跃"的socket才会主动的去调用 callback函数,其他idle状态socket则不会,在这点上,epoll实现了一个"伪"AIO,因为这时候推动力在os内核。在一些 benchmark中,如果所有的socket基本上都是活跃的—比如一个高速LAN环境,epoll并不比select/poll有什么效率,相 反,如果过多使用epoll_ctl,效率相比还有稍微的下降。但是一旦使用idle connections模拟WAN环境,epoll的效率就远在select/poll之上了。

3、使用mmap加速内核与用户空间的消息传递:这点实际上涉及到epoll的具体实现了。无论是select,poll还是epoll都需要内核把FD消息通知给用户空间,如何避免不必要的内存拷贝就 很重要,在这点上,epoll是通过内核于用户空间mmap同一块内存实现的。

三者对比与区别: 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内部定义的等待队列)。这也能节省不少的开销。           等待列表中的进程,我的理解是系统每次有空闲进程,就将其放入等待列表中阻塞,epoll_wait有一个time_out参数,在这个等待时间time里,如果就绪队列有socket需要处理,就调用阻塞的进程运行,如果一直为空,当计时器到了,进程变为非阻塞继续去干活。   其实还要区分的就是哪些事情是操作系统做的(进程调度、epoll功能的实现),哪些事情是用户自己写是(调用epoll_wait…)。像“中断系统里注册一个监听回调函数”都是操作系统做的事情。      

向内核中断处理注册回调,一旦关心的事件触发,回调自动将socket对应的epitem添加到rdlist中          

参考资料 如果这篇文章说不清epoll的本质,那就过来掐死我吧! (1) 硬核分享,(最全)TCP/IP协议栈,epoll的内部实现原理 Epoll的实现原理,了解epoll高效背后的魔法



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3