前言
谈起epoll大家应该都不陌生,可以算是当前linux系统下支持高性能网络IO的底层基石了,我们常用的支持高并发的网络服务,比如Redis、
Nginx、Kafka、Netty,Swoole等等,底层无一例外使用epoll,所以我觉得每个程序员都应该对epoll有非常透彻的理解,借着疫情待岗期间,
我把我对epoll的理解从新梳理了一下,用C语言把几种常见的socket网络通信方式验证了一遍,包括同步阻塞方式,select方式和epoll方式,
从中查漏补缺,并且整理成一篇文章,以便日后温习,希望对大家也有些帮助。
我觉得理解一个东西至少要分四个层次,首先是看懂或听懂别人的讲解,这一步是被动的接受,表面上貌似看懂了,其实
还有大量细节不清楚,并且抓不住其中的重点,还很容易遗忘。
第二步需要自己尝试,必须亲自写一些demo验证,这时你会发现之前
的理解还很肤浅,有大量遗漏的知识点,很多疑问会自己涌现出来,然后再去查找资料,这样理解才能深刻也记得更牢。
第三步是讲给别人听或者自己写一篇技术博客,这一步的要求就更高了,因为你要避免出丑露怯或被人问住,更需要对一些边边角角的细节都能了解并准确描述,
还要自己组织语言,让别人能听懂。
最后一步就是正式应用于生产实践中了,这一步是可遇而不可求的,因为越是底层的技术(例如epoll)越不容易用于生产实践,而一旦用于生产实践
那一定是非常核心并且通用的服务。但是这并不意味着学习底层技术是没有意义的,正相反,理解计算机的底层逻辑对日常开发具有非常积极的意义,
这一点不必细说了,相信大家都能理解。
epoll并不是一项新技术,已经诞生了十几年,网上教程、博客、技术资料多如牛毛,水平也是参差不齐,
你要是再写,总得有点特别的地方,对此,我发现网上很多文章都讲了某一部分知识点,全面完整的描述不多,我的定位是尽可能的把一些知识点串起来,
加入我个人的理解,特别是有些细节在其他技术文章中容易忽略但又容易造成迷惑的地方,就重点聊一下,还有一些知识点,虽然也很重要,
但在网上或书中能够很容易找到答案,就不做过多介绍了。另外,前人为了便于理解画过很多优秀的图,我就直接拿来主义了(会给出原文出处),
在此对前人的工作表示感谢。我也只能说,个人水平有限,如果出现一些纰漏,欢迎大家指正。
硬件原理
首先,从硬件上说,A、B两台主机通信,数据通过网络从A主机传输到B主机时,必先经过B主机网卡,然后再从网卡写入到内存中。
注意这里写入的内存叫做内核缓冲区,属于内核空间,只有操作系统能够访问。
网卡把数据写到内存以后,会向CPU发出一个中断信号,CPU收到这个信号,会中断正在执行的程序,开始执行网卡中断处理函数。
网卡中断处理函数主要干两件事,首先要通知网卡数据收到了,第二是要把数据从网卡缓冲区中尽快写入内核缓冲区,这件事需要尽快完成
(及时处理中断),不然网卡缓冲区排在前面的数据就被覆盖了。
数据从网卡写入内核缓冲区这一步是需要花时间的,CPU不会干等着什么都不做,操作系统会控制当前进程出让CPU,让CPU去做别的事,
等数据都写好了,再唤醒当前进程继续执行。
此时这个进程就会处于阻塞等待状态,等数据拷贝完成后,系统会唤醒这个进程继续执行。
那么阻塞和唤醒的本质是什么呢?
首先,当进程执行到创建socket语句时,操作系统会创建一个socket对象,这个对象属于文件系统的一员(注意理解linux系统一切皆文件,
后面会提到 epfd 对象也是文件系统的一员),返回一个int型文件句柄 fd,这个socket对象主要包含3部分:
- 发送缓冲区
- 接收缓冲区
- 等待列表 - 注意这个等待列表是接下来要重点提及的
操作系统还维护了一个工作队列,凡是处于运行状态的进程都会加入到这个工作队列中,CPU通过轮询分时执行,同一时刻只会处理一个进程,
因为进程切换很快,给人的感觉就像多个进程在同时执行。
当进程需要阻塞时,操作系统就把这个进程从工作队列中删除,这样下次轮询就不会轮到这个进程了。
同时,操作系统会把这个进程加到这个进程监视的所有socket对象的等待列表中。
注意等待列表可能不是一个,而是多个,每个socket都有自己的等待列表,一个进程A可能同时监视了10个socket,进程进入阻塞状态时,
就得把进程A的指针插入到这10个socket的等待列表里(轮询一遍)。
当数据准备好以后(从网卡拷贝到内存中完成),就可以唤醒进程了,唤醒的具体操作是什么呢?
操作系统会再次轮询所有监视的socket,把进程A从它们各自的等待队列中删除,再把进程A加入当前工作队列中,下次CPU时间片切换时就会
继续执行这个进程了。
下面介绍一下几种常见的网络I/O模型,并贴出C语言版demo代码,代码都经过测试可以跑通。
网络I/O模型之阻塞模式
这是比较原始的I/O模型,也是最好理解的。
图,待补充。。。
客户端代码:(简单起见我后面都用这个客户端测试了)
服务器端代码:
以上阻塞模式要想实现并发响应用户请求,只能使用多线程或多进程,每一个socket都fork出一个进程出来,开销非常大,记得很久以前的web服务器Apche就是
这么干的,在高并发的场景下肯定不行,所以就出现了I/O多路复用技术,I/O多路复用又分3种,select、poll和epoll,select和poll差不多,本质上没有太大区别,
epoll是I/O多路复用的终极版本,适用于高并发场景,下面重点介绍一下select和epoll。
网络I/O模型之select
服务器端代码:
网络I/O模型之epoll
服务器端代码: