从事服务器端开发,少不了接触网络编程。epoll是Linux下高性能web服务器必不可少的技术。Nginx、Redis、Skynet和大多数游戏服务器都使用了这种多路复用技术。epoll很重要,但是epoll和select有什么区别呢?epoll效率高的原因是什么?虽然网上讲解Epoll的文章很多,但要么过于简单,要么陷入源码分析,通俗易懂的文章很少。.所以笔者决定写这篇文章,让缺乏专业背景知识的读者也能了解epoll的原理。这篇文章的核心思想是:让读者清楚地了解为什么epoll有好的性能。文章将从网卡接收数据的过程开始,串联CPU中断、操作系统进程调度等知识;然后分析阻塞接收数据的演化过程,一步步选择到Epoll;最后探究一下epoll的实现细节。下面是一个典型的计算机结构图,从网卡接收数据开始。计算机由CPU、内存(memory)和网络接口组成。了解Epoll本质的第一步,就是从硬件的角度来看计算机是如何接收网络数据的。计算机结构图(图片来源:Linux内核全注释微机组成结构)下图为网卡接收数据的过程:第一阶段,网卡从网线接收数据。通过2级硬件电路传输。最后3个阶段将数据写入内存中的地址。这个过程涉及到DMA传输、IO通道选择等硬件相关知识,但我们只需要知道网卡会将接收到的数据写入内存即可。网卡接收数据的过程是通过硬件传输的,网卡接收到的数据存储在内存中,操作系统可以读取它们。你怎么知道数据已经收到了?理解Epoll本质的第二步,就是从CPU的角度来看数据接收。要理解这个问题,首先要理解一个概念:中断。计算机在执行程序时,会有优先级要求。例如,当计算机收到断电信号时,应立即保存数据,保存数据的程序具有更高的优先级(电容器可以为CPU短时间运行节省少量电量)时间)。一般来说,硬件产生的信号需要CPU立即响应,否则可能会丢失数据,因此具有较高的优先级。CPU应该中断正在执行的程序来响应;当CPU完成对硬件的响应后,会重新执行用户程序。中断流程如下图所示,与函数调用类似,只是函数调用的位置是事先确定的,而中断位置是由“信号”决定的。中断程序调用以键盘为例。当用户按下键盘上的某个键时,键盘会向CPU的中断引脚发送一个高电平。CPU可以捕捉到这个信号,然后执行键盘中断程序。下图是各种硬件通过中断与CPU交互的过程:网卡写入数据到达内存后,网卡向CPU发送中断信号,操作系统就可以知道有新的数据到来,然后通过网卡中断程序对数据进行处理。为什么进程阻塞不占用CPU资源?理解Epoll本质的第三步,就是从操作系统进程调度的角度来看数据接收。阻塞是进程调度的关键部分。它是指进程在事件(如接收网络数据)发生之前的等待状态。Recv、Select、Epoll都是阻塞方法。下面分析一下为什么进程阻塞不占用CPU资源?为了简单起见,我们从普通的Recv接收开始,先看下面的代码://createsocketints=socket(AF_INET,SOCK_STREAM,0);//bindbind(s,...)//listenlisten(s,...)//接受客户端连接intc=accept(s,...)//接收客户端数据recv(c,...);//打印数据printf(...)这是最基本的网络编程代码,首先创建一个Socket对象,依次调用Bind、Listen和Accept,最后调用Recv接收数据。Recv是一种阻塞方法。当程序运行到Recv时,会等到接收到数据再继续。那么阻塞的原理是什么?工作队列操作系统为了支持多任务,实现了进程调度的功能,将进程划分为“运行”、“等待”等几种状态。运行态是进程获得CPU使用权,正在执行代码的状态;等待状态是阻塞状态。比如上面的程序运行到Recv时,程序会从运行态变为等待态,接收到数据后又回到运行态。操作系统会分时执行各个运行状态下的进程。由于速度快,看起来像是在同时执行多个任务。下图中的计算机运行了三个进程A、B、C,其中进程A执行的是上述的基本网络程序。一开始,这三个进程被操作系统的工作队列引用,处于运行状态,将被分时执行。工作队列中有A、B、C三个进程在等待。当进程A执行创建Socket的语句时,操作系统会创建一个由文件系统管理的Socket对象(如下图)。创建套接字。这个Socket对象包含发送缓冲区、接收缓冲区和等待队列等成员。等待队列是一个非常重要的结构体,它指向所有需要等待Socket事件的进程。当程序执行到Recv时,操作系统会将进程A从工作队列中移到Socket的等待队列中(如下图)。Socket的等待队列只有进程B和C留在工作队列中。根据进程调度,CPU会依次执行这两个进程的程序,不会执行进程A的程序。所以进程A被阻塞,不会往下执行代码,不会占用CPU资源。注意:操作系统增加等待队列只是给这个“等待”的进程增加了一个引用,这样当接收到数据的时候,就可以获取并唤醒进程对象,而不是直接把进程管理归于自己。为了图解方便,上图直接把进程挂在了等待队列下面。唤醒进程当Socket接收到数据时,操作系统将Socket等待队列中的进程放回工作队列中,进程变为运行状态,继续执行代码。同时,由于Socket的接收缓冲区已经有数据,Recv可以返回接收到的数据。内核接收网络数据的全过程通过网卡、中断和进程调度的知识,描述内核在blockingRecv下接收数据的全过程。内核接收数据的整个过程如上图所示,在Recv期间进程阻塞:计算机接收对端发送的数据(步骤①)数据通过网卡传输到内存(步骤②))然后网卡通过中断信号通知CPU有数据到达,CPU执行中断程序(步骤③)这里的中断程序主要有两个作用,首先将网络数据写入对应的接收缓冲区Socket(步骤④),然后唤醒进程A(步骤⑤),将进程A重新写入工作队列。进程唤醒过程如下图所示:以上就是内核接收数据的全过程。这里我们可能会思考两个问题:操作系统如何知道网络数据对应于哪个Socket?如何同时监听多个Socket的数据?第一个问题:因为一个Socket对应一个端口号,而网络包中包含IP和端口信息,内核可以通过端口号找到对应的Socket。当然,为了提高处理速度,操作系统会维护从端口号到Socket的索引结构,以便快速读取。第二个问题是复用的症结所在,也是本文后半部分的重点。同时监控多个Sockets的简单方法。服务器需要管理多个客户端连接,而Recv只能监听单个Socket。在这种矛盾下,人们开始寻找监控多个Sockets的方法。Epoll的本质是高效地监听多个Socket。从历史发展来看,效率较低的方法必然先出现,人们才会对其进行改进,就像Selectforepoll一样。只有了解了效率较低的Select,才能更好地理解Epoll的本质。如果可以提前传入一个Socket列表,如果列表中的Socket都没有数据,则挂起进程,直到有一个Socket收到数据,唤醒进程。这个方法很直接,也是Select的设计思路。为了方便理解,我们先回顾一下Select的用法。在下面的代码中,首先准备一个数组FDS,让FDS存放所有需要监听的Socket。然后调用Select,如果FDS中的所有Socket都没有数据,Select会阻塞,直到某个Socket收到数据,Select返回并唤醒进程。用户可以遍历FDS,通过FD_ISSET判断是哪个Socket接收到了数据,然后进行处理。ints=socket(AF_INET,SOCK_STREAM,0);bind(s,...)listen(s,...)intfds[]=要监视的存储套接字while(1){intn=select(...,fds,...)for(inti=0;i
