对epoll机制的学习理解v1
epoll机制
wrk用非阻塞多路复用IO技术创造出大量的连接,从而达到很好的压力测试效果。epoll就是实现IO多路复用的关键。
本节是对epoll的本质的学习总结,进一步的参考资料为:
《深入理解Nginx:模块开发与架构解析(第二版)》,陶辉
首先分析网络数据接收模型。
计算机分为硬件中断和软件中断,硬件中断是由外接设备产生的,比如网卡,键盘,鼠标等这些都是硬件设备。硬件设备向CPU发出中断信号,高电平信号到达CPU引脚,触发CPU立即执行中断。软中断就是由程序产生的中断。
当网卡收到外部网络发送过来的数据,网卡会做相应的处理,然后网卡发送数据到计算机内存中,之后向CPU发出硬件中断信号。CPU得到信号后立即中断当前任务,去处理网络数据,将内存中的网络数据写入socket对象中,同时唤醒等待该数据的进程。
socket对象用于收发网络数据,socket对象由进程创建后被文件描述符指向,即fd指针。socket对象指定了“端口号”,而网络数据包里包含了端口号,这使得CPU可以准确将数据写入对应的socket中。socket对象里有三个数据结构: 发送缓冲区,接收缓冲区和等待队列。接收缓冲区就是负责接收内存中的数据,并且等待被进程处理。等待队列实际上是指针,该指针指向进程时,表示进程处于等待状态,于是CPU不会处理该进程,而是处理其他进程,直到该进程被中断程序唤醒,同时中断程序移除被监听的socket上的等待队列,这样该进程重新加入进程的运行状态,被CPU处理,这样,进程拿到了socket缓冲区中的数据,recv这一环顺利通过,可以执行下一步。
在早期,互联网用户少,因此一台服务器每当被一个客户端连接,就建立一个进程,该进程只监听一个socket, 服务器能够承受住负载。当用户越来越多时,一台服务器仍然起大量进程已不现实。因此一个进程监听多个连接的技术应运而生,这就是IO多路复用技术。
最早的IO多路复用技术的思路较为简单,这就是select方法。
进程创建并监听多个socket对象,这些socket对象的描述符被写到数组fds中,进程执行系统调用select时,操作系统将进程放入每个socket的等待队列,此时进程被阻塞。其中任意一个socket被写入数据(实际上,唤醒工作是中断程序做的),进程就会被唤醒,并遍历fds中的socket对象,并读取缓冲数据,从而继续执行下去,此时进程处于运行态。
select方法让一个进程等待再唤醒执行它的过程中,一共有3次遍历,2次内核传递。让进程处于等待状态时,等待即阻塞,因为CPU执行其他进程去了,所以等待状态下的进程不消耗CPU资源,该进程会被操作系统放入被监听的所有的socket的等待队列中,因此需要遍历fds,遍历之前需要把fds整个列表传递到内核去。等到设备接收到网络数据,进程被唤醒的时候,操作系统要将fds中每个socket的等待队列中的进程指针清空,因此再一次遍历,遍历前仍然要传递fds到内核。涉及到进程的操作必然由内核执行,进程内部的执行则是用户空间权限,不需要内存干涉。最后一次遍历,是进程遍历fds上的socket(fds本来就在用户态),直到找到有缓冲数据的。
这样会带来两个问题,1.频繁的内核传递,2.频繁的遍历。问题的根源在于,进程的每一次状态更新就要重新传递fds以及遍历(fds的状态更新)。传递fds的原因显而易见,每一次调用select都是一次独立的监听一群socket的行为,在实际场景下,fds中的socket并不会较大规模地变化,因此fds最好整个列表只传一次,如果有修改,也只是对整个小增小减。遍历既源于fds传递至内核后要让fds中的每个socket和进程建立联系,也源于进程唤醒后要寻找到有缓冲数据的socket, 所以最好能进程和fds一次建立联系,然后进程能一次就找到需要的socket.
fds的状态变化和进程状态的变化是一起发生的,能不能让它们分开发生?即进程的状态变化不需要和fds重新建立连接?此外,进程也不知道fds哪个socket发生了变化,因为fds不存储发生变化的信息。进程既然要和fds每个socket发生关系,为什么fds不派一个管理者代表来和进程沟通呢?这个管理者,就是event poll。
epoll就是在fds的基础上,增加了一个eventpoll数据结构,进程创建fds之后,其中的socket都为空时(如果不为空,recv直接拿到socket数据,就不阻塞了),进入阻塞状态,此时fds列表整体传入内核。所有socket与eventpoll对象建立关系,即将eventpoll对象放入所有socket的等待列表里。然后eventpoll对象的等待列表中放入进程。这是epoll方法下的进程阻塞模型,eventpoll不会频繁地改变状态,所以fds列表只传一次。eventpoll还维护一个rdlist数组,当多个socket收到数据,内核中断程序拿到了网络数据包中的五元组信息,拿到了端口号,找到了socket对象,同时知道了socket对象的地址,于是在rdlist数组中写入这些socket对象的地址。进程被唤醒时,被从eventpoll的等待列表里移除,进程又读取rdlist中的socket对象地址,直接找到收到数据的socket.
epoll的核心在于操作eventpoll管理进程状态改变,只要传递一次fds,遍历1次fds就可以阻塞进程,唤醒进程则只需操作一次eventpoll。极大降低了开销。epoll的根本原理还是中间层原理。
参考
注1:等待队列的真正意思是,该socket有个列表,里面存储了所有监听该socket的进程的fd描述符。所以,可以有多个进程监听同一个端口。
注2:网卡将数据写入内存,中断程序将内存中的数据写入socket对象中。唤起进程的是中断程序,中断程序是硬中断发起后,被CPU执行的。唤起进程的同时,将所有等待队列清空,清空后便可以CPU执行该进程,执行中,遍历socket,如果哪个socket收到了数据,便处理哪一个recv. select,就是选择,就是遍历式地选择。
注3:进程是被内核管理的,所以,操作进程,就必须将所涉及到的数据传递给内核。内核和应用空间的关系,理解成包围和被包围的关系更为合适。
其他小问题
什么是事件?
事件是被进程所等待的数据。1个事件可以让多个进程等待。
为什么说等待了,就会阻塞呢?
因为进程A创建完socket之后,下一步到了recv方法,此时进程A被丢入(其实是生成一个等待中的引用)socket对象的等待队列中去(内存),CPU就去执行其他的进程了。直到有socket事件被硬中断传入,CPU将其写入内存,进程A才再次被唤醒。
为什么阻塞是进程调度关键的一环?
阻塞又叫做等待状态,等待什么? 进程在等待某一个事件的发生,在等待时,无法进入下一步状态。对于处理网络的进程,就是等待接收网络数据包。
eventpoll对象的数据结构是怎样的?
eventpoll用到了红黑树。就绪列表需要快速地被加入和删除,所以,就绪列表是红黑树。
为什么select监视的最大socket数量是1024个?
因为select在每次进程状态改变时候,要3次遍历fds列表,2次将fds列表传递到内核,fds列表变大,即提升了遍历时间,又因为复制更大的数据传递至内核,用户空间到内核空间的复制传输开销较大。所以限制了fds的大小。默认最大是1024.