epoll的本质是什么
epoll很重要,但是epoll和select有什么区别呢?是什么让epoll 高效?
虽然网上解释epoll的文章很多,但要么过于简单,要么停留在源码分析上,容易理解的很少。作者决定写这篇文章,让缺乏专业背景知识的读者也能了解epoll的原理。
文章的核心思想是:让读者清楚地理解为什么epoll有好的性能。
一、从网卡接收数据说起
下面是典型的计算机结构图。计算机由CPU、存储器(存储器)和网络接口组成。了解epoll本质的第一步,就是从硬件角度看计算机是如何接收网络数据的。
下图为网卡接收数据的流程。
第阶段,网卡接收来自网线的数据;在阶段的硬件电路传输之后;最后在阶段,数据被写入内存中的某个地址。这个过程涉及到DMA传输、IO通道选择等硬件相关知识,但我们只需要知道网卡会将接收到的数据写入内存即可。
网卡接收数据的流程
通过硬件传输,网卡接收到的数据存储在内存中,操作系统可以读取它们。
二、如何知道接收了数据?
了解epoll本质的第二步是从CPU的角度来看数据接收。要了解这个问题,首先要了解——中断的概念。
当计算机执行程序时,它有优先级要求。例如,当计算机收到断电信号时,应立即保存数据。保存数据的程序优先级较高(电容可以节省少量电量供CPU短时间运行)。
一般来说,硬件产生的信号需要CPU立即响应,否则数据可能会丢失,因此其优先级非常高。 CPU应中断正在执行的程序来响应;当CPU完成对硬件的响应后,应重新执行用户程序。中断过程如下图所示。它与函数调用类似,只不过函数调用有预定的位置,而中断位置是由“信号”决定的。
中断例程调用
以键盘为例,当用户按下键盘上的按键时,键盘会向CPU的中断引脚发送高电平。 CPU可以捕获该信号,然后执行键盘中断程序。下图展示了各种硬件通过中断与CPU交互的过程。
现在可以回答“怎么知道数据已经收到了?”的问题:当网卡将数据写入内存时,网卡向CPU发送中断信号。然后操作系统就可以知道有新的数据到达,然后通过网卡中断程序。处理数据。
三、进程阻塞为什么不占用 CPU 资源?
了解epoll本质的第三步,从操作系统进程调度的角度来看数据接收。阻塞是进程调度的关键部分。指的是进程在等待某个事件(如接收网络数据)发生之前的等待状态。 receive、select和epoll都是阻塞方法。我们来分析一下为什么进程阻塞不占用CPU资源?
为了简单起见,我们从普通的recv接收开始分析。我们先看下面的代码:
//创建套接字int s=socket(AF_INET, SOCK_STREAM, 0); //绑定bind(s,) //监听listen(s,) //接受客户端连接int c=accept(s ,)//接收客户端数据recv(c, );//打印数据printf(.) 这是最基本的网络编程代码。首先创建一个socket对象并依次调用bind。监听并接受,最后调用recv接收数据。 recv 是一种阻塞方法。当程序运行到recv时,会等待接收到数据才继续。
那么阻塞的原理是什么呢?
工作队列
为了支持多任务,操作系统实现了进程调度功能,将进程划分为“运行”和“等待”等几种状态。运行状态是进程已经获得CPU的使用权,正在执行代码的状态;等待状态是阻塞状态。比如上面的程序运行到recv时,程序会从运行状态转为等待状态,接收到数据后又转回运行状态。操作系统会以分时的方式执行各个运行状态下的进程。由于其速度很快,看起来就像是在同时执行多个任务。
下图中的计算机正在运行三个进程A、B和C。进程A执行上述基本网络程序。一开始,这三个进程被操作系统的工作队列引用,并处于运行状态。他们将分时进行。实施。
工作队列中有3个进程A、B、C
等待队列
当进程A执行创建socket的语句时,操作系统会创建一个由文件系统管理的socket对象(如下所示)。这个socket对象包含发送缓冲区、接收缓冲区和等待队列等成员。等待队列是一个非常重要的结构,它指向所有需要等待socket事件的进程。
创建套接字
当程序执行recv时,操作系统会将进程A从工作队列移动到socket的等待队列中(如下图)。由于工作队列中只剩下进程B和C,根据进程调度,CPU会依次执行这两个进程的程序,而不会执行进程A的程序。因此,进程A被阻塞,不会执行执行代码或占用CPU资源。
套接字等待队列
注意:操作系统在添加等待队列时,只是添加了对这个“等待”进程的引用,以便获取进程对象并在接收到数据时将其唤醒,而不是直接将进程管理纳入自身之下。上图中,为了说明方便,直接将进程放入等待队列中。
唤醒过程
当socket接收到数据时,操作系统将socket等待队列上的进程放回到工作队列中,该进程变为运行状态,继续执行代码。同时,由于socket的接收缓冲区已经有数据,因此recv可以返回接收到的数据。
四、内核接收网络数据全过程
这一步涵盖了网卡、中断和进程调度的知识,描述了阻塞recv下内核接收数据的整个过程。
如下图,进程在recv阻塞的同时,计算机接收对端传输的数据(步骤),数据通过网卡传输到内存(步骤),然后网卡通过中断信号通知CPU数据到达,CPU执行中断程序。 (步骤)。
这里的中断程序主要有两个功能。首先将网络数据写入相应套接字的接收缓冲区(步骤),然后唤醒进程A(步骤),并将进程A再次放入工作队列。
内核接收数据的全过程
唤醒进程的流程如下图所示:
唤醒过程
以上就是内核接收数据的整个过程。这里我们可以思考两个问题:
首先,操作系统如何知道网络数据对应哪个socket?二、如何同时监听多个socket的数据?第一个问题:因为一个socket对应一个端口号,而网络数据包中包含IP和端口信息,因此内核可以通过端口号找到对应的socket。当然,为了提高处理速度,操作系统会给socket维护一个端口号的索引结构,以便快速读取。
五、同时监视多个 socket 的简单方法
服务器需要管理多个客户端连接,但recv只能监视单个套接字。在这种矛盾之下,人们开始寻找监控多个socket的方法。 epoll的目的是高效地监控多个socket。
从历史发展的角度来看,首先必须出现效率较低的方法,然后人们才能对其进行改进,就像select 对epoll 所做的那样。
只有先了解效率较低的select,才能更好地理解epoll的本质。
如果可以提前传入一个socket列表,如果列表中没有socket有数据,进程就会被挂起,直到有socket接收到数据,进程才会被唤醒。这个方法非常简单,也是select的设计思想。
为了方便理解,我们先回顾一下select的用法。下面的代码中,首先准备一个数组fds,让fds存放所有需要监控的socket。然后调用选择。如果fds中所有socket都没有数据,select会阻塞,直到有一个socket接收到数据,select返回,唤醒进程。用户可以遍历fds,使用FD_ISSET来判断哪个socket收到了数据,然后进行处理。
int s=套接字(AF_INET, SOCK_STREAM, 0); bind(s,);listen(s,);int fds[]=存储需要监听的socket; while(1){ int n=select (. fds,) for(int i=0; i fds.count; i++){ if(FD_ISSET(fds[i],)){ //fds[i]的数据处理} }}select过程
select的实现思路非常简单。如果程序同时监听如下所示的三个套接字sock1、sock2、sock3,那么调用select后,操作系统将进程A添加到这三个套接字的等待队列中。
操作系统将进程A添加到这三个套接字的等待队列中。
当任何套接字接收到数据时,中断例程将唤醒该进程。下图为sock2接收数据的处理流程:
注意:recv和select的中断回调可以设置不同的内容。
sock2接收到数据并中断程序唤醒进程A
所谓唤醒进程,就是将进程从所有等待队列中移除,添加到工作队列中,如下图所示:
将进程A从所有等待队列中移除并添加到工作队列中
经过这些步骤,当进程A醒来时,它知道至少有一个套接字已经接收到数据。程序只需要遍历一次socket链表即可获得就绪的socket。
这种简单的方法是有效的,并且在几乎所有操作系统中都有相应的实现。
然而,简单的方法往往存在缺点,主要是:
首先,每次调用select都需要将进程添加到所有监控套接字的等待队列中,每次唤醒都需要从每个队列中移除。这就涉及到两次遍历,每次都要将整个fds链表传递给内核,有一定的开销。正是因为遍历操作的开销较大以及效率方面的考虑,才规定了select的最大监听数。默认情况下,只能监控1024 个套接字。
其次,进程被唤醒后,程序并不知道哪些套接字收到了数据,需要遍历一次。
那么,有什么办法可以减少遍历呢?有没有办法保存就绪的套接字?这两个问题正是epoll技术想要解决的。
附加说明:本节仅说明select的一种情况。当程序调用select时,内核会首先遍历socket。如果多个socket接收缓冲区中有数据,select会直接返回,不会阻塞。这就是select的返回值可能大于1的原因之一。如果没有socket有数据,进程就会阻塞。
六、epoll 的设计思路
epoll是在select出现N年后发明的。它是select 和poll 的增强版本(poll 和select 基本相同,有一些改进)。 epoll通过以下措施提高效率:
措施一:功能分离
select效率低下的原因之一就是将“维护等待队列”和“阻塞进程”两个步骤合二为一。如下图所示,每次调用select都需要这两步。但在大多数应用场景中,需要监控的socket都是相对固定的,不需要每次都修改。 epoll将这两个操作分开,首先使用epoll_ctl维护等待队列,然后调用epoll_wait阻塞进程。显然,效率是可以提高的。
epoll与select相比,功能拆分
为了方便理解下面的内容,我们先来了解一下epoll的用法。下面的代码中,首先使用epoll_create创建一个epoll对象epfd,然后使用epoll_ctl将需要监听的socket添加到epfd中,最后调用epoll_wait等待数据:
int s=套接字(AF_INET, SOCK_STREAM, 0);绑定(s,)听(s,)int epfd=epoll_create(.); epoll_ctl(epfd,); //将所有需要监听的socket添加到epfd中while(1){ int n=epoll_wait(.) for (接收数据的socket){ //处理}} 功能分离使得epoll可以优化。
措施2:准备清单
select效率低下的另一个原因是程序不知道哪些socket收到了数据,只能一一遍历。如果内核维护一个引用接收数据的套接字的“就绪列表”,则可以避免这种遍历。如下图所示,计算机有3个socket,接收数据的sock2和sock3被就绪链表rdlist引用。当进程被唤醒时,只要获取到rdlist的内容,就可以知道哪些socket收到了数据。
就绪栏表示意图
七、epoll 的原理与工作流程
本节将通过示例和图表来讲解epoll的原理和工作流程。
创建epoll对象
如下图所示,当进程调用epoll_create方法时,内核会创建一个eventpoll对象(即程序中epfd代表的对象)。 eventpoll 对象也是文件系统的成员。和socket一样,它也有一个等待队列。
内核创建eventpoll对象
创建代表epoll的eventpoll对象是必要的,因为内核维护着“就绪列表”等数据,这些数据可以作为eventpoll的成员。
维护关注列表
创建epoll对象后,可以使用epoll_ctl来添加或删除想要监听的socket。以添加socket为例,如下图所示。如果通过epoll_ctl添加对sock1、sock2、sock3的监控,内核会将eventpoll添加到这三个socket的等待队列中。
添加需要监听的socket
当socket接收到数据时,中断程序会操作eventpoll对象,而不是直接操作进程。
接收数据
当套接字接收到数据时,中断程序将套接字引用添加到eventpoll的“就绪列表”中。如下图所示,sock2和sock3接收到数据后,中断程序允许rdlist引用这两个socket。
添加对就绪列表的引用
eventpoll对象相当于socket和进程之间的中介。 socket的数据接收并不直接影响进程,而是通过改变eventpoll的就绪列表来改变进程状态。
当程序执行epoll_wait时,如果rdlist已经引用了该socket,则epoll_wait直接返回。如果rdlist 为空,则进程被阻塞。
阻塞和唤醒进程
假设计算机上正在运行进程A和进程B。在某个时刻,进程A运行epoll_wait语句。如下图所示,内核会将进程A放入eventpoll的等待队列中,阻塞该进程。
epoll_wait 阻塞进程
当socket接收到数据时,中断程序一方面修改rdlist,另一方面唤醒eventpoll等待队列中的进程。进程A再次进入运行状态(如下图)。也因为rdlist的存在,进程A可以知道哪些socket发生了变化。
epoll 唤醒进程
八、epoll 的实现细节
至此,相信读者已经对epoll的本质有了一定的了解。但我们还需要知道eventpoll的数据结构是什么样的?
另外,就绪队列应该使用什么数据结构? eventpoll应该使用什么数据结构来管理通过epoll_ctl添加或删除的套接字?
如下图所示,eventpoll包括lock、mtx、wq(等待队列)和rdlist等成员,其中rdlist和rbr是我们关心的。
就绪列表数据结构
就绪列表指的是就绪的套接字,因此它应该能够快速插入数据。
程序可以随时调用epoll_ctl添加监控套接字,也可以随时删除监控套接字。删除时,如果socket已经在就绪链表中,也应该将其删除。所以就绪链表应该是一个可以快速插入和删除的数据结构。
双向链表就是这样一种数据结构。 epoll使用双向链表来实现就绪队列(对应上图中的rdllist)。
指数结构
既然epoll将“维护监控队列”和“进程阻塞”分开,那么也意味着需要有一个数据结构来保存监控的socket,至少方便添加和删除,也方便查找避免重复添加。红黑树是一种自平衡二叉搜索树。查找、插入、删除的时间复杂度都是O(log(N)),效率不错。 epoll使用红黑树作为索引结构(对应上图中的rbr)。
九、小结
epoll在select和poll的基础上引入eventpoll作为中间层,采用先进的数据结构,是一种高效的复用技术。这里我们也以表格的形式简单比较一下select、poll和epoll来结束本文。希望读者能够有所收获。
罗培宇是一名程序员,致力于创造有趣的游戏。
作为游戏行业从业者,参与过《卡布西游》、《卡布仙踪》、《卡布魔镜》、《坦克射击》、《海陆大战》等多个项目的研发;作为独立游戏开发者,曾主导过《仙剑5前传之心愿》和:0 10-30000等项目的研发,具有丰富的实践经验。
自2009年发布第一本视频教程《蚀梦》以来,已出版专业书籍《教你用VB制作RPG游戏》和《手把手教你用C#制作RPG游戏》。
目前专注于手游、AI技术等领域,从第三方角度记录普通开发者的心路历程。
简介:《Unity3D网络游戏实战》基于Unity 3D的跨平台基础Mono及其游戏脚本语言C#进行讲解。全面系统地分析了Unity 3D的跨平台原理以及游戏脚本开发的特点。
用户评论
作为游戏行业的爱好者,在接触“Epoll 的本质是什么”这一体类游戏时,我第一感觉就是被这个名字深深吸引了!它不仅富有探索精神,而且名字中蕴含着科技感和复杂性,让玩家产生一种想要深入了解的欲望。
有9位网友表示赞同!
玩了一段时间后,我发现这个游戏在深入探讨 epoll 机制的同时也融入了策略元素。通过模拟网络环境下的数据读写处理过程,让我体验到技术学习的乐趣。
有16位网友表示赞同!
游戏中的动态操作和实时反馈系统,使得我在面对大量并发连接时的操作逻辑清晰了不少。尤其对于前端开发者来说,这简直是一次难得的学习机会!
有16位网友表示赞同!
"Epoll 的本质是什么"不仅仅是一款游戏,更像是一种教育工具,让人在轻松的游戏氛围中学习并理解 epoll 机制的精妙之处。
有14位网友表示赞同!
对细节的关注和优化处理在这款游戏的设计上展现得淋漓尽致。从网络事件的管理和多路复用技术的应用都能看出开发者用心良苦。
有18位网友表示赞同!
在游戏中不断调整参数,观察系统的响应速度变化时,我体验到了解决问题的乐趣和成就感,这让我更加深爱这个领域。
有15位网友表示赞同!
我认为“Epoll 的本质是什么”的设计者很巧妙地将复杂的技术概念拆解重组为易于理解的游戏形式。这对于我这样的技术新手来说真的是太友好了!
有9位网友表示赞同!
游戏中的帮助文档和教程系统做得也十分贴心,随着游戏难度的提升,相关的知识逐步递进,让我能够边玩边学。
有8位网友表示赞同!
"Epoll 的本质是什么"这款游戏不仅提升了我的专业知识水平,还增强了我对问题分析和解决问题的能力。真的很推荐给对计算机网络有兴趣的朋友!
有12位网友表示赞同!
我非常喜欢这个游戏中的历史背景和故事线索,它巧妙地将技术学习与冒险故事结合在一起,让人在探索科技的同时还不忘享受剧情。
有8位网友表示赞同!
"Epoll 的本质是什么"不只是关于技术的学习工具,更是一个激发好奇心、培养逻辑思维的游戏。每个环节都充满了挑战和惊喜。
有20位网友表示赞同!
这款游戏让我对 epoll 算法有了更深的理解。通过游戏化的学习方式,我能够在实践中掌握理论知识的精髓。
有17位网友表示赞同!
我一直期待“Epoll 的本质是什么”能有更多的进阶内容,比如更深层的网络设计、更高级的技术挑战等,让玩家有持续探索的动力。
有20位网友表示赞同!
我在玩这个游戏时经常发现自己的代码编写有了新的灵感。这款游戏鼓励创新思维和实践操作,真的很推荐给需要灵感的开发者。
有12位网友表示赞同!
"Epoll 的本质是什么"在技术与娱乐之间的平衡做得非常好。通过它,我不仅学到了知识,还能享受游戏的乐趣。
有16位网友表示赞同!
对于那些对 epoll机制感兴趣但又不想淹没在枯燥的学习资料中的开发者来说,“Epoll 的本质是什么”是一个完美的选择。
有18位网友表示赞同!
如果能有更多的类似“Epoll 的本质是什么”的学习型游戏出现,将极大地提高我们学习专业知识的效率和乐趣。
有11位网友表示赞同!
"Epoll 的本质是什么"不仅拓宽了我的技术视野,还让我在面对复杂问题时有了更为直观的理解方式。太赞了!
有16位网友表示赞同!
这款游戏真的改变了我对专业技能学习的看法,我认为它应该被推荐给更多对网络编程感兴趣的学生们。
有18位网友表示赞同!