携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第11天,点击查看活动详情 >> 本文根据罗培羽的知乎系列文章内容进行整理:
epoll作为linux下高性能网络服务器的必备技术至关重要,nginx、redis、skynet和大部分游戏服务器都使用到这一多路复用技术
1.硬件-网卡接收数据
网卡会把接收到的数据写入内存。通过硬件传输,网卡接收的数据存放到内存中。操作系统就可以去读取它们。
2.cpu角度-数据接收
当网卡把数据写入到内存后,网卡向cpu发出一个中断信号,操作系统便能得知有新数据到来,再通过网卡中断程序去处理数据。
3. 操作系统进程调度角度-进程阻塞为什么不占用CPU资源
阻塞是进程调度的关键一环,指的是进程在等待某事件(如接收到网络数据)发生之前的等待状态,recv、select和epoll都是阻塞方法。
- 工作队列
当程序运行行到recv阻塞函数时,会一直等待,直到接收到数据才会向下执行。
- 等待队列
此时该进程A就会从工作队列移到该socket的等待队列中。不影响工作队列的进程调度,所以进程A被阻塞,不会往下执行代码,也不会占用CPU资源。
- 唤醒进程
当socket接收到数据后,操作系统将该socket等待队列上的进程重新放回工作队列,进程继续执行
4. 内核接收网络数据全过程
其一,操作系统如何知道网络数据对应于哪个socket?
因为一个socket对应着一个端口号,而网络数据包中包含了ip和端口的信息,内核可以通过端口号找到对应的socket。当然,为了提高处理速度,操作系统会维护端口号到socket的索引结构,以快速读取。
其二,如何同时监视多个socket的数据?
多路复用的重中之重
5. 同时监视多个socket的简单方法
epoll的要义是高效的监视多个socket
预先传入一个socket列表,如果列表中的socket都没有数据,挂起进程,直到有一个socket收到数据,唤醒进程。这种方法很直接,也是select的设计思想。
select方法会对每一个socket都记录进程A,然后任意一个socket结束,将进程A从所有socket里面删除,加入到工作队列中。当进程A被唤醒之后,遍历一遍socket列表得到就绪socket。
- 两次遍历所有的socket,导致开销大,规定select最大的监视数量1024
- 进程被唤醒还要再遍历一次
6. epoll的设计思路
epoll在select和poll的基础进行了改进和增强。主要表现在
- 将维护等待队列和阻塞进程两个步骤分开。用epoll_ctl维护等待队列,再调用epoll_wait阻塞进程
- 同时内核维护了一个就绪列表,获知哪些socket收到数据。当进程被唤醒后,只要获取rdlist的内容,就能够知道哪些socket收到数据。
7. epoll的原理和流程
创建epoll对象
当某个进程调用epoll_create方法时,内核会创建一个eventpoll对象
维护监视列表
用epoll_ctl添加或删除所要监听的socket,终端程序操作eventpoll对象,而不是直接操作进程
接收数据
中断程序给eventpoll的就绪列表添加socket引用
阻塞和唤醒进程
8. epoll的实现细节
nignx 如何快速从kernel中找到等待处理的事件?Epoll
- 链表:仅仅存储活跃链接
- 红黑树:添加事件
适合做大并发连接的事件,随着句柄树的增加,性能变化不大
就绪列表的数据结构
就绪列表引用着就绪的socket,所以它应能够快速的插入数据。
程序可能随时调用epoll_ctl添加监视socket,也可能随时删除。当删除时,若该socket已经存放在就绪列表中,它也应该被移除。
所以就绪列表应是一种能够快速插入和删除的数据结构。双向链表就是这样一种数据结构,epoll使用双向链表来实现就绪队列(对应上图的rdllist)。
索引结构
既然epoll将“维护监视队列”和“进程阻塞”分离,也意味着需要有个数据结构来保存监视的socket。至少要方便的添加和移除,还要便于搜索,以避免重复添加。红黑树是一种自平衡二叉查找树,搜索、插入和删除时间复杂度都是O(log(N)),效率较好。epoll使用了红黑树作为索引结构(对应上图的rbr)。
9.结论
epoll在select和poll(poll和select基本一样,有少量改进)的基础引入了eventpoll作为中间层,使用了先进的数据结构,是一种高效的多路复用技术。
10.评论
首先epoll初始化用epoll_create创建一个event_poll对象,这个对象有就绪列表,红黑树,等待列表。就绪列表存放就绪的socket,红黑树存放所有正在监听的socket引用,等待列表放正在等待的进程。
每次accept到一个新连接,调用中断在文件系统中创建fd,这个fd里有接收缓存区,发送缓存区,等待列表,(ps:如果已有这个fd,则直接将这个socket加入这个fd的等待列表中,这里我理解为fd对应的端口,计算机有65535个端口,所以epoll最多支持65535,而一个端口可能会有许多的连接,因为socket是IP:端口,IP不同,端口一样也是不同的socket,但是对应的端口fd是一个),同时在中断系统里注册一个监听回调函数(这个回调函数到底在哪里,没搞明白)。一旦某个socket发生了读写操作,中断程序会调用这个socket的回调函数,将这个socket的引用加入到event_poll的就绪队列中,在while程序里,一直都会有epoll_wait(A进程),一旦A进程会一直轮询就绪队列,一旦就绪队列非空,A进程获得其中的socket数据进入系统运行队列,由等待列表的下一个进程继续使用epoll_wait来轮询。
等待列表中的进程,我的理解是系统每次有空闲进程,就将其放入等待列表中阻塞,epoll_wait有一个time_out参数,在这个等待时间time里,如果就绪队列有socket需要处理,就调用阻塞的进程运行,如果一直为空,当计时器到了,进程变为非阻塞继续去干活。