计算机由CPU、内存、网卡等设备硬件设备组成。
计算机接收网络数据的处理过程是:
硬中断:
外部设备(磁盘、网卡、键盘等)对CPU的中断;
软中断:
中断服务程序对内核的中断;
软中断是Linux中断机制的“下半部分”(bottom-half),在软件上模拟硬件中断,达到异步调用下半部分服务函数的功能。
信号:
内核(或其他进程)对某个进程的中断。
软件上模拟硬件中断,中断某个用户进程的运行,转而去处理相应的“中断”。这一系列动作包含:
信号产生—>信号响应—>信号处理—>信号返回。
当进程创建一个socket套接字时,操作系统会相应的创建出一个由文件系统管理的socket对象,这个socket对象中包含:
发送缓冲区、接收缓冲区、等待队列、 等成员。
其中,等待队列是非常重要的成员。
操作系统会将调动recv阻塞的进程挂在对应socket的等待队列中(只是进程的引用,不是进程本身),此时进程进入阻塞态,不会占用CPU资源。
当socket上收到数据后,操作系统会将该socket从等待队列中重新放回到工作队列,继续执行。
每个socket对应的是一个网络连接(TCP或UDP),即一个socket对应一组IP地址和端口号,而网络数据中包含了IP地址和端口号信息,操作系统会维护端口号到socket的索引结构,根据收到的网络数据中的端口号信息就可以快速找到对应的socket套接字。
服务端需要管理多个客户端连接,而 recv()
系统调用只能监视单个socket。
这种情况下,如果要管理多个客户端连接,就需要多开进程或线程,每个进程维护一个socket套接字,没有网络数据时进程阻塞在recv()系统调用上,当网络数据到达时,操作系统环境对应socket等待队列上的进程。
此时面临的问题是维护进程或线程带来的系统开销(每个线程的栈空间8M,由于系统的内存资源有限,1K个线程就已经需要消耗8G内存,不可能无限制的多开线程,且进程、线程间的频繁切换也会带来较大的开销)。
这种矛盾下,人们开始寻找监视多个socket的方法。
最先想到的办法是使用一个进程监视多个socket,预先传入一个socket列表,如果列表中的socket都没有数据,则进程继续挂起;直到有一个或以上的socket接收到网络数据,再唤醒进程。
这种方法很直接,这也是select的设计思想。
如下图,假设进程A同时监听 sock1、sock2、sock3(通过fd_set传入),那么,在调用select之后,操作系统会把进程A分别加入到这三个socket的等待队列中:
当任何一个socket上收到数据时,中断程序将唤起进程。
所谓唤起进程,就是将其从所有的socket对象的等待队列中移除,并插入到就绪队列中。
经过这两步之后,当进程A被唤醒时,它知道它所检测的socket列表中至少有个socket已经接收到数据了。此时程序遍历一遍socket列表,就可以得到就绪的socket。
当进程调用 epoll_create
方法时,内核会创建一个 eventpoll
对象,也就是应用程序中的 epfd 所代表的对象。
eventpoll
对象也是文件系统中的一员(Linux中一切设备皆文件),和socket一样也拥有一个“等待队列”。
创建epoll对象 eventpoll
之后,可以使用 epoll_ctl
添加或者删除所要监听的socket。
以添加socket为例,如果要对sock1、sock2、sock3进行监视,内核会将eventpoll添加到这三个socket的等待队列中。
当socket收到数据后,中断回调程序会操作eventpoll对象,而不是直接操作进程。
select的低效原因之一在于应用程序不知道哪些socket收到数据,只能一个个的遍历。如果内核维护一个“就绪列表”,在就绪列表中引用收到数据的socket,就能避免遍历。
在 eventpoll
对象中就实现了这样的一个“就绪列表” ---- rdlist
。
当socket收到数据,中断回调程序会给eventpoll的“就绪列表”添加socket的引用,如下图所示:
eventpoll对象相当于是socket和进程之间的中介,socket的数据接收并不直接影响进程,而是通过改变eventpoll的就绪列表来改变进程状态。
epoll_wait的返回条件也是根据rdlist的状态进行判断:
如果rdlist已经引用了socket,那么epoll_wait直接返回;
如果rdlist为空,阻塞进程。
如下图,假设当进程A运行到epoll_wait()时,操作系统会将进程A放入到eventpoll对象的等待队列中,阻塞进程。
(对于epoll,操作系统只需要将进程A放入eventpoll这一个对象的等待队列中;而对于select,操作系统则需要将进程A放入到socket列表中的所有socket对象的等待队列中。)
当socket接收到数据时,中断回调程序一方面修改rdlist“就绪列表”,另一方面唤醒eventpoll等待队列中的进程A。
也因为rdlist就绪列表的存在,进程A可以在重新进入运行态后准确知道哪些socket上发生了变化。
相较于select,epoll实现高效主要基于以下两点:
select低效的原因之一是将“维护等待队列”和“阻塞进程”两个步骤合二为一。
每次调用select都需要这两步操作,然而大多数应用场景中,需要监视的socket相对固定,并不需要每次修改。
epoll将这两个操作分开,先用 epoll_ctl
维护等待队列,再调用 epoll_wait
阻塞进程,以此来提高效率。
select低效的另一个原因在于程序不知道哪些socket收到数据,只能一个一个的遍历。如果内核维护一个“就绪列表”,引用收到的数据的socket,就能避免遍历。
eventpoll对象包含了:lock、mtx、wq(等待队列)、rdlist 等成员。
epoll使用双向链表来实现就绪队列rdlist。
epoll使用红黑树作为索引结构,以便于快速的插入和删除要监视的socket套接字。
红黑树时一种自平衡的二叉查找树,搜索、插入、删除的时间复杂度都是O(logN)。
参考内容:
添加链接描述