写点什么

linux 网络编程—7 层网络以及 5 种 Linux IO 模型以及相应 IO 基础

发布于: 2021 年 07 月 06 日

一、七层网络模型

  OSI 是 Open System Interconnection 的缩写,意为开放式系统互联。国际标准化组织(ISO)制定了 OSI 模型,该模型定义了不同计算机互联的标准,它是一个七层的、抽象的模型体。

文章相关视频讲解:

详解网络编程细节点击:90分钟详解网络编程相关的细节处理

详解底层网络 IO 模型点击: 底层网络IO模型,必须要懂得10种模型

腾讯认证 T9 后端开发岗位,linux 服务器开发高级架构师系统学习视频点击:C/C++Linux服务器开发高级架构师/Linux后台架构师

1、物理层

  并不是物理媒体本身,它只是开放系统中利用物理媒体实现物理连接的功能描述和执行连接的规程,建立、维护、断开物理连接,传输单位是比特(bit)。

  物理层的媒体包括架空明线、平衡电缆、光纤、无线信道等。通信用的互连设备指 DTE(Data Terminal Equipment)和 DCE(Data Communications Equipment)间的互连设备。DTE 即数据终端设备,又称物理设备,如计算机、终端等都包括在内。而 DCE 则是数据通信设备或电路连接设备,如调制解调器等。数据传输通常是经过 DTE-DCE,再经过 DCE-DTE 的路径。互连设备指将 DTE、DCE 连接起来的装置,如各种插头、插座。LAN 中的各种粗、细同轴电缆、T 型接头、插头、接收器、发送器、中继器等都属物理层的媒体和连接器。

  物理层的主要功能是:

  ① 为数据端设备提供传送数据的通路,数据通路可以是一个物理媒体,也可以是多个物理媒体连接而成。一次完整的数据传输,包括激活物理连接、传送数据和终止物理连接。所谓激活,就是不管有多少物理媒体参与,都要在通信的两个数据终端设备间连接起来,形成一条通路。

  ② 传输数据。物理层要形成适合数据传输需要的实体,为数据传送服务。一是要保证数据能在其上正确通过,二是要提供足够的带宽(带宽是指每秒钟内能通过的比特(Bit)数),以减少信道上的拥塞。传输数据的方式能满足点到点,一点到多点,串行或并行,半双工或全双工,同步或异步传输的需要。

  2、数据链路层

  可以粗略地理解为数据通道,传输单位是帧(Frame)。物理层要为终端设备间的数据通信提供传输介质及其连接。介质是长期的,连接是有生存期的。在连接生存期内,收发两端可以进行不等的一次或多次数据通信。每次通信都要经过建立通信联络和拆除通信联络两个过程。这种建立起来的数据收发关系就叫做数据链路。而在物理媒体上传输的数据难免受到各种不可靠因素的影响而产生差错,为了弥补物理层上的不足,为上层提供无差错的数据传输,就要能对数据进行检错和纠错。链路层应具备如下功能:

  ① 链路连接的建立、拆除和分离;

  ② 差错检测和恢复。还有链路标识,流量控制等等。

  独立的链路产品中最常见的当属网卡、网桥、二路交换机等。

  3、网络层

  在网络层: 有 IP (IPV4、IPV6)协议、ICMP 协议、ARP 协议、RARP 协议和 BOOTP 协议,负责建立“主机”到“主机”的通讯,传输单位是分组(数据包 Packet)。

  当数据终端增多时。它们之间有中继设备相连,此时会出现一台终端要求不只是与惟一的一台而是能和多台终端通信的情况,这就产生了把任意两台数据终端设备的数据链接起来的问题,也就是路由或者叫寻径。另外,当一条物理信道建立之后,被一对用户使用,往往有许多空闲时间被浪费掉。人们自然会希望让多对用户共用一条链路,为解决这一问题就出现了逻辑信道技术和虚拟电路技术。

  4、传输层

  在传输层: 有 TCP 协议与 UDP 协议,负责建立“端口”到“端口”的通信,传输单位是数据段(Segment)。

  有一个既存事实,即世界上各种通信子网在性能上存在着很大差异。例如电话交换网,分组交换网,公用数据交换网,局域网等通信子网都可互连,但它们提供的吞吐量,传输速率,数据延迟通信费用各不相同。对于会话层来说,却要求有一性能恒定的界面。传输层就承担了这一功能。

  5、会话层

  会话单位的控制层,其主要功能是按照在应用进程之间约定的原则,按照正确的顺序收、发数据,进行各种形态的对话。会话层规定了会话服务用户间会话连接的建立和拆除规程以及数据传送规程。

  会话层提供的服务是应用建立和维持会话,并能使会话获得同步。会话层使用校验点可使通信会话在通信失效时从校验点继续恢复通信。这种能力对于传送大的文件极为重要。

  6、表示层

  其主要功能是把应用层提供的信息变换为能够共同理解的形式,提供字符代码、数据格式、控制信息格式、加密等的统一表示。表示层的作用之一是为异种机通信提供一种公共语言,以便能进行互操作。这种类型的服务之所以需要,是因为不同的计算机体系结构使用的数据表示法不同。例如,IBM 主机使用 EBCDIC 编码,而大部分 PC 机使用的是 ASCII 码。在这种情况下,便需要表示层来完成这种转换。

  7、应用层

  向应用程序提供服务,这些服务按其向应用程序提供的特性分成组,并称为服务元素。有些可为多种应用程序共同使用,有些则为较少的一类应用程序使用。应用层是开放系统的最高层,是直接为应用进程提供服务的。其作用是在实现多个系统应用进程相互通信的同时,完成一系列业务处理所需的服务。

  在应用层: 有 FTP、HTTP、TELNET、SMTP、DNS 等协议。

二、七层网络模型传输过程


TCP/IP 中的数据包传输过程如下:

每个分层中,都会对所发送的数据附加一个首部,在这个首部中包含了该层必要的信息,如发送的目标地址以及协议相关信息。通常,为协议提供的信息为包首部,所要发送的内容为数据。在下一层的角度看,从上一层收到的包全部都被认为是本层的数据。

  网络中传输的数据包由两部分组成:一部分是协议所要用到的首部,另一部分是上一层传过来的数据。首部的结构由协议的具体规范详细定义。在数据包的首部,明确标明了协议应该如何读取数据。反过来说,看到首部,也就能够了解该协议必要的信息以及所要处理的数据。

  ① 应用程序处理

  首先应用程序会进行编码处理,这些编码相当于 OSI 的表示层功能;编码转化后,邮件不一定马上被发送出去,这种何时建立通信连接何时发送数据的管理功能,相当于 OSI 的会话层功能。

  ② TCP 模块的处理

  TCP 根据应用的指示,负责建立连接、发送数据以及断开连接。TCP 提供将应用层发来的数据顺利发送至对端的可靠传输。为了实现这一功能,需要在应用层数据的前端附加一个 TCP 首部。

  ③ IP 模块的处理

  IP 将 TCP 传过来的 TCP 首部和 TCP 数据合起来当做自己的数据,并在 TCP 首部的前端加上自己的 IP 首部。IP 包生成后,参考路由控制表决定接受此 IP 包的路由或主机。

  ④ 网络接口(以太网驱动)的处理

  从 IP 传过来的 IP 包对于以太网来说就是数据。给这些数据附加上以太网首部并进行发送处理,生成的以太网数据包将通过物理层传输给接收端。

  ⑤ 网络接口(以太网驱动)的处理

  主机收到以太网包后,首先从以太网包首部找到 MAC 地址判断是否为发送给自己的包,若不是则丢弃数据。

  如果是发送给自己的包,则从以太网包首部中的类型确定数据类型,再传给相应的模块,如 IP、ARP 等。这里的例子则是 IP 。

  ⑥ IP 模块的处理

  IP 模块接收到 数据后也做类似的处理。从包首部中判断此 IP 地址是否与自己的 IP 地址匹配,如果匹配则根据首部的协议类型将数据发送给对应的模块,如 TCP、UDP。这里的例子则是 TCP。  另外,对于有路由器的情况,接收端地址往往不是自己的地址,此时,需要借助路由控制表,在调查应该送往的主机或路由器之后再进行转发数据。

  ⑦ TCP 模块的处理

  在 TCP 模块中,首先会计算一下校验和,判断数据是否被破坏。然后检查是否在按照序号接收数据。最后检查端口号,确定具体的应用程序。数据被完整地接收以后,会传给由端口号识别的应用程序。

  ⑧ 应用程序的处理

  接收端应用程序会直接接收发送端发送的数据。通过解析数据,展示相应的内容。

  传输过程中协议如下:

C/C++Linux 服务器开发高级架构师学习视频 点击 linux服务器学习资料 获取,内容知识点包括 Linux,Nginx,ZeroMQ,MySQL,Redis,线程池,MongoDB,ZK,Linux 内核,CDN,P2P,epoll,Docker,TCP/IP,协程,DPDK 等等。

三、什么是 SOCKET 

  Socket 是应用层与 TCP/IP 协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket 其实就是一个门面模式,它把复杂的 TCP/IP 协议族隐藏在 Socket 接口后面,对用户来说,一组简单的接口就是全部,让 Socket 去组织数据,以符合指定的协议。

  Socket 接口是 TCP/IP 网络的 API,Socket 接口定义了许多函数或例程,用以开发 TCP/IP 网络上的应用程序。

  Socket 为了实现以上的通信过程而建立成来的通信管道,其真实的代表是客户端和服务器端的一个通信进程,双方进程通过 socket 进行通信,而通信的规则采用指定的协议。socket 只是一种连接模式,不是协议,tcp,udp,简单的说(虽然不准确)是两个最基本的协议,很多其它协议都是基于这两个协议如,http 就是基于 tcp 的,用 socket 可以创建 tcp 连接,也可以创建 udp 连接,这意味着,用 socket 可以创建任何协议的连接,因为其它协议都是基于此的。

  综上所述:需要 IP 协议来连接网络;TCP 是一种允许我们安全传输数据的机制,使用 TCP 协议来传输数据的 HTTP 是 Web 服务器和客户端使用的特殊协议。HTTP 基于 TCP 协议,但是却可以使用 socket 去建立一个 TCP 连接。

  如图:

四、长短连接

  短连接:连接->传输数据->关闭连接

  也可以这样说:短连接是指 SOCKET 连接后发送后接收完数据后马上断开连接。

  长连接:连接->传输数据->保持连接 -> 传输数据-> 。。。 ->关闭连接。

  长连接指建立 SOCKET 连接后不管是否使用都保持连接,但安全性较差。

什么时候用长连接,短连接?

  长连接多用于操作频繁,点对点的通讯,而且连接数不能太多情况。每个 TCP 连接都需要三步握手,这需要时间,如果每个操作都是先连接,再操作的话那么处理速度会降低很多,所以每个操作完后都不断开,下次处理时直接发送数据包就 OK 了,不用建立 TCP 连接。例如:数据库的连接用长连接, 如果用短连接频繁的通信会造成 socket 错误,而且频繁的 socket 创建也是对资源的浪费。

  而像 WEB 网站的 http 服务一般都用短链接,因为长连接对于服务端来说会耗费一定的资源,而像 WEB 网站这么频繁的成千上万甚至上亿客户端的连接用短连接会更省一些资源,如果用长连接,而且同时有成千上万的用户,如果每个用户都占用一个连接的话,那可想而知吧。所以并发量大,但每个用户无需频繁操作情况下需用短连好。总之,长连接和短连接的选择要视情况而定。

五、三次握手四次分手

  SYN,ACK,FIN 存放在 TCP 的标志位,一共有 6 个字符,这里就介绍这三个:

SYN:代表请求创建连接,所以在三次握手中前两次要 SYN=1,表示这两次用于建立连接,至于第三次什么用,在疑问三里解答。FIN:表示请求关闭连接,在四次分手时,我们发现 FIN 发了两遍。这是因为 TCP 的连接是双向的,所以一次 FIN 只能关闭一个方向。ACK:代表确认接受,从上面可以发现,不管是三次握手还是四次分手,在回应的时候都会加上 ACK=1,表示消息接收到了,并且在建立连接以后的发送数据时,都需加上 ACK=1,来表示数据接收成功。seq: 序列号,什么意思呢?当发送一个数据时,数据是被拆成多个数据包来发送,序列号就是对每个数据包进行编号,这样接受方才能对数据包进行再次拼接。初始序列号是随机生成的,这样不一样的数据拆包解包就不会连接错了。(例如:两个数据都被拆成 1,2,3 和一个数据是 1,2,3 一个是 101,102,103,很明显后者不会连接错误)ack: 这个代表下一个数据包的编号,这也就是为什么第二请求时,ack 是 seq+1

  TCP 是双向的,所以需要在两个方向分别关闭,每个方向的关闭又需要请求和确认,所以一共就 4 次分手。

六、文件描述符

  在 UNIX、Linux 的系统调用中,内核系统把应用程序可以操作的资源都抽象成了文件概念,比如说硬件设备,socket,流,磁盘,进程,线程;文件描述符就是索引(指针)。

  文件描述符就是内核为了高效管理已被打开的文件所创建的索引,用于指向被打开的文件,所有执行 I/O 操作的系统调用都通过文件描述符;文件描述符是一个简单的非负整数,用以表明每个被进程打开的文件。程序刚刚启动时,第一个打开的文件是 0,第二个是 1,以此类推。也可以理解为文件的身份 ID。如:

  标准输入输出说明

  stdin,标准输入,默认设备是键盘,文件编号为 0

  stdout,标准输出,默认设备是显示器,文件编号为 1,也可以重定向到文件

  stderr,标准错误,默认设备是显示器,文件编号为 2,也可以重定向到文件

  /proc/[进程 ID]/fd 这个目录专门用于存放文件描述符,可以到目录下查看文件描述符使用情况,同时也可以通过 ulimit 查看文件描述符限制,如:

192:~ XXX$ ulimit -n  //-n打开文件描述符的最大个数256192:~ XXX$ ulimit -Sn  //-S是软性限额256192:~ XXX$ ulimit -Hn  //-H是硬性限额unlimited
复制代码

  Linux 中最大文件描述符的限制有两个方面,一个是用户级限制,一个是系统级限制,文件描述符限制均可进行修改,但是也有一个限制,规则如下:

  a. 所有进程打开的文件描述符数不能超过/proc/sys/fs/file-max

  b. 单个进程打开的文件描述符数不能超过 user limit 中 nofile 的 soft limit

  c. nofile 的 soft limit 不能超过其 hard limit

  d. nofile 的 hard limit 不能超过/proc/sys/fs/nr_open

七、零拷贝

  应用程序获取数据的两个阶段:

  数据准备:应用程序无法直接操作我们的硬件资源,需要操作系统资源时,先通知我们的内核,内核检查是否有就绪的资源,如果有则先把对应数据加载到内核空间。

  数据拷贝:把数据资源从内核空间复制到应用程序的用户空间。

  补充知识 -> 零拷贝

  现代操作系统都使用虚拟内存,使用虚拟的地址取代物理地址,这样做的好处是:

  1.一个以上的虚拟地址可以指向同一个物理内存地址,

  2.虚拟内存空间可大于实际可用的物理地址;

  利用第一条特性可以把内核空间地址和用户空间的虚拟地址映射到同一个物理地址,这样 DMA 就可以填充对内核和用户空间进程同时可见的缓冲区了,大致如下图所示:

  关于 mmap 以及 sendfile 零拷贝,可以参考:如何实现高性能的 IO 及其原理?

C/C++Linux 服务器开发高级架构师学习视频 点击 linux服务器学习资料 获取,内容知识点包括 Linux,Nginx,ZeroMQ,MySQL,Redis,线程池,MongoDB,ZK,Linux 内核,CDN,P2P,epoll,Docker,TCP/IP,协程,DPDK 等等。

八、Linux 网络 IO 模型

  什么是同步和异步,阻塞和非阻塞?

  同步和异步关注的是结果消息的通信机制

  同步:同步的意思就是调用方需要主动等待结果的返回

  异步:异步的意思就是不需要主动等待结果的返回,而是通过其他手段比如,状态通知,回调函数等。

  阻塞和非阻塞主要关注的是等待结果返回时调用方的状态

  阻塞:是指结果返回之前,当前线程被挂起,不做任何事

  非阻塞:是指结果在返回之前,线程可以做一些其他事,不会被挂起。

  Linux 有 5 种 IO 模型,如下图所示:

  1、阻塞 I/O 模型

  应用程序调用一个 IO 函数,导致应用程序阻塞,等待数据准备好。 如果数据没有准备好,一直等待….数据准备好了,从内核拷贝到用户空间,IO 函数返回成功指示。

  当调用 recv()函数时,系统首先查是否有准备好的数据。如果数据没有准备好,那么系统就处于等待状态。当数据准备好后,将数据从系统缓冲区复制到用户空间,然后该函数返回。在套接应用程序中,当调用 recv()函数时,未必用户空间就已经存在数据,那么此时 recv()函数就会处于等待状态。

  2、非阻塞 IO 模型

  我们把一个 SOCKET 接口设置为非阻塞就是告诉内核,当所请求的 I/O 操作无法完成时,不要将进程睡眠,而是返回一个错误。这样我们的 I/O 操作函数将不断的测试数据是否已经准备好,如果没有准备好,继续测试,直到数据准备好为止。在这个不断测试的过程中,会大量的占用 CPU 的时间。上述模型绝不被推荐。

  把 SOCKET 设置为非阻塞模式,即通知系统内核:在调用 Windows Sockets API 时,不要让线程睡眠,而应该让函数立即返回。在返回时,该函数返回一个错误代码。如图所示,一个非阻塞模式套接字多次调用 recv()函数的过程。前三次调用 recv()函数时,内核数据还没有准备好。因此,该函数立即返回 WSAEWOULDBLOCK 错误代码。第四次调用 recv()函数时,数据已经准备好,被复制到应用程序的缓冲区中,recv()函数返回成功指示,应用程序开始处理数据。

  3、IO 复用模型

  简介:主要是 select 和 epoll;对一个 IO 端口,两次调用,两次返回,比阻塞 IO 并没有什么优越性;关键是能实现同时对多个 IO 端口进行监听;

  I/O 复用模型会用到 select、poll、epoll 函数,这几个函数也会使进程阻塞,但是和阻塞 I/O 所不同的的,这两个函数可以同时阻塞多个 I/O 操作。而且可以同时对多个读操作,多个写操作的 I/O 函数进行检测,直到有数据可读或可写时,才真正调用 I/O 操作函数。

  当用户进程调用了 select,那么整个进程会被 block;而同时,kernel 会“监视”所有 select 负责的 socket;当任何一个 socket 中的数据准备好了,select 就会返回。这个时候,用户进程再调用 read 操作,将数据从 kernel 拷贝到用户进程。

  这个图和 blocking IO 的图其实并没有太大的不同,事实上还更差一些。因为这里需要使用两个系统调用(select 和 recvfrom),而 blocking IO 只调用了一个系统调用(recvfrom)。但是,用 select 的优势在于它可以同时处理多个 connection。(select/epoll 的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。)

  在这种模型中,这时候并不是进程直接发起资源请求的系统调用去请求资源,进程不会被“全程阻塞”,进程是调用 select 或 poll 函数。进程不是被阻塞在真正 IO 上了,而是阻塞在 select 或者 poll 上了。Select 或者 poll 帮助用户进程去轮询那些 IO 操作是否完成。

  不过你可以看到之前都只使用一个系统调用,在 IO 复用中反而是用了两个系统调用,但是使用 IO 复用你就可以等待多个描述符也就是通过单进程单线程实现并发处理,同时还可以兼顾处理套接字描述符和其他描述符。

   4、信号驱动 IO

  简介:两次调用,两次返回;

  首先我们允许套接口进行信号驱动 I/O,并安装一个信号处理函数,进程继续运行并不阻塞。当数据准备好时,进程会收到一个 SIGIO 信号,可以在信号处理函数中调用 I/O 操作函数处理数据。

  5、异步 IO 模型

  当一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者的输入输出操作。

  在 linux 的异步 IO 模型中,并没有真正实现异步通道,最终的实现还是等同于调用 Epoll。

  LInux IO 模型总结如图所示:

九、多路复用 IO 原理详解

  在 linux 没有实现 epoll 事件驱动机制之前,我们一般选择用 select 或者 poll 等 IO 多路复用的方法来实现并发服务程序。但在大数据、高并发、集群出现后,select 和 poll 的性能瓶颈无法在支撑,于是 epoll 出现了。

  1、select

  首先来说说 select,select 函数监视的文件描述符分 3 类,分别是 writefds、readfds、和 exceptfds。调用后 select 函数会阻塞,直到有描述符就绪(有数据 可读、可写、或者有 except),或者超时(timeout 指定等待时间,如果立即返回设为 null 即可),函数返回。当 select 函数返回后,可以通过遍历 fd_set,来找到就绪的描述符。

int select(int nfds, fd_set *restrict readfds, fd_set *restrict writefds, fd_set *restrict errorfds, struct timeval *restrict timeout);
复制代码

  具体 select 步骤如图所示:

  1. 使用 copy_from_user 从用户空间拷贝 fd_set 到内核空间。

  2. 注册回调函数__pollwait。

  3. 遍历所有 FD,调用其对应的 poll 方法(对于 socket,这个 poll 方法是 sock_poll,sock_poll 根据情况会调用到 tcp_poll, udp_poll 或者 datagram_poll)

  4. 以 tcp_poll 为例,其核心实现就是__pollwait,也就是上面注册的回调函数。

  5. __pollwait 的主要工作就是把当前进程挂到设备的等待队列中,不同的设备有不同的等待队列,对于 tcp_poll 来说,其等待队列是 sk->sk_sleep(注意把进程挂到等待队列中并不代表进程已经睡眠了)。在设备收到一条消息(网络 IO)或填写完文件数据(磁盘 IO)后,会唤醒设备等待队列上睡眠的进程,这时当前进程便被唤醒了。

  6. poll 方法返回时会返回一个描述读写操作是否就绪的 mask 掩码,根据这个 mask 掩码给 fd_set 赋值。

  7. 如果遍历完所有的 FD,还没有返回一个可读写的 mask 掩码,则会调用 schedule_timeout 让调用 select 的当前进程进入睡眠。当设备驱动发生自身资源可读写后,会唤醒其等待队列上睡眠的进程。如果超过设定的超时时间,还是没人唤醒,则调用 select 的进程会重新被唤醒获得 CPU,进而重新遍历 FD,判断有没有就绪的 FD。

  8. 把 fd_set 从内核空间拷贝到用户空间。

  9. select 的触发方式是水平触发,应用程序如果没有完成对一个已经就绪的文件描述符进行 IO 操作,那么之后每次 select 调用还是会将这些文件描述符通知进程。

注意:select 的实现依赖于文件的驱动函数 poll,在 unix 中无论是调用 select、poll 还是 epoll,最终都会调用该函数。

  2、poll

int poll(struct pollfd fds[], nfds_t nfds, int timeout);
复制代码

  不同与 select 使用三个位图来表示三个 fdset 的方式,poll 使用一个 pollfd 的指针实现。

struct pollfd {    int fd; /* file descriptor */    short events; /* requested events to watch */    short revents; /* returned events witnessed */};
复制代码

  和 select 函数一样,poll 返回后,需要轮询 pollfd 来获取就绪的描述符。select 和 poll 都需要在返回后,通过遍历文件描述符来获取已经就绪的 socket。事实上,同时连接的大量客户端在一时刻可能只有很少的处于就绪状态,因此随着监视的描述符数量的增长,其效率也会线性下降。

  3、epoll

  在 select/poll 时代,服务器进程每次都把这 100 万个连接告诉操作系统(从用户态复制句柄数据结构到内核态),让操作系统内核去查询这些套接字上是否有事件发生,轮询完后,再将句柄数据复制到用户态,让服务器应用程序轮询处理已发生的网络事件,这一过程资源消耗较大,因此,select/poll 一般只能处理几千的并发连接。

  epoll 的设计和实现与 select 完全不同。epoll 通过在 Linux 内核中申请一个简易的文件系统(文件系统一般用什么数据结构实现?红黑树)。epoll 提供了三个函数,epoll_create, epoll_ctl 和 epoll_wait,epoll_create 是创建一个 epoll 句柄;epoll_ctl 是注册要监听的事件类型;epoll_wait 则是等待事件的产生。

int epoll_create(int size);//创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);struct epoll_event {  __uint32_t events;  /* Epoll events */  epoll_data_t data;  /* User data variable */};// events可以是以下几个宏的集合:// EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);// EPOLLOUT:表示对应的文件描述符可以写;// EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);// EPOLLERR:表示对应的文件描述符发生错误;// EPOLLHUP:表示对应的文件描述符被挂断;// EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。// EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
复制代码

  一棵红黑树,一张准备就绪句柄链表,少量的内核 cache,就帮我们解决了大并发下的 socket 处理问题。

  ① 执行 epoll_create    内核在 epoll 文件系统中建了个 file 结点,(使用完,必须调用 close()关闭,否则导致 fd 被耗尽)       在内核 cache 里建了红黑树存储 epoll_ctl 传来的 socket,       在内核 cache 里建了 rdllist 双向链表存储准备就绪的事件。  ② 执行 epoll_ctl    如果增加 socket 句柄,检查红黑树中是否存在,存在立即返回,不存在则添加到树干上,然后向内核注册回调函数,告诉内核如果这个句柄的中断到了,就把它放到准备就绪 list 链表里。所有添加到 epoll 中的事件都会与设备(如网卡)驱动程序建立回调关系,相应的事件发生时,会调用回调方法。

  ③ 执行 epoll_wait

    立刻返回准备就绪表里的数据即可(将内核 cache 里双向列表中存储的准备就绪的事件 复制到用户态内存),当调用 epoll_wait 检查是否有事件发生时,只需要检查 eventpoll 对象中的 rdlist 双链表中是否有 epitem 元素即可。如果 rdlist 不为空,则把发生的事件复制到用户态,同时将事件数量返回给用户。

  对于 select 的三个缺点以及 epoll 的解决方案:

  (1)每次调用 select,都需要把 fd 集合从用户态拷贝到内核态,这个开销在 fd 很多时会很大。

  (2)同时每次调用 select 都需要在内核遍历传递进来的所有 fd,这个开销在 fd 很多时也很大。

  (3)select 支持的文件描述符数量太小了,默认是 1024。

  对于第一个缺点,epoll 的解决方案在 epoll_ctl 函数中。每次注册新的事件到 epoll 句柄中时(在 epoll_ctl 中指定 EPOLL_CTL_ADD),会把所有的 fd 拷贝进内核,而不是在 epoll_wait 的时候重复拷贝。epoll 保证了每个 fd 在整个过程中只会拷贝一次。

  对于第二个缺点,epoll 的解决方案不像 select 或 poll 一样每次都把 current 轮流加入 fd 对应的设备等待队列中,而只在 epoll_ctl 时把 current 挂一遍(这一遍必不可少)并为每个 fd 指定一个回调函数,当设备就绪,唤醒等待队列上的等待者时,就会调用这个回调函数,而这个回调函数会把就绪的 fd 加入一个就绪链表)。epoll_wait 的工作实际上就是在这个就绪链表中查看有没有就绪的 fd(利用 schedule_timeout()实现睡一会,判断一会的效果,和 select 实现中的第 7 步是类似的)。

  对于第三个缺点,epoll 没有这个限制,它所支持的 FD 上限是最大可以打开文件的数目,这个数字一般远大于 2048,举个例子,在 1GB 内存的机器上大约是 10 万左右,具体数目可以 cat /proc/sys/fs/file-max 察看,一般来说这个数目和系统内存关系很大。

  4、select、poll、epoll 优缺点对比


  5、epoll 的水平触发与边缘触发

  Level_triggered(水平触发):

  当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。如果这次没有把数据一次性全部读写完(如读写缓冲区太小),那么下次调用 epoll_wait()时,它还会通知你在上没读写完的文件描述符上继续读写,当然如果你一直不去读写,它会一直通知你!!!如果系统中有大量你不需要读写的就绪文件描述符,而它们每次都会返回,这样会大大降低处理程序检索自己关心的就绪文件描述符的效率!!!

  Edge_triggered(边缘触发):

  当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。如果这次没有把数据全部读写完(如读写缓冲区太小),那么下次调用 epoll_wait()时,它不会通知你,也就是它只会通知你一次,直到该文件描述符上出现第二次可读写事件才会通知你!!!这种模式比水平触发效率高,系统不会充斥大量你不关心的就绪文件描述符!!

select(),poll()模型都是水平触发模式,信号驱动 IO 是边缘触发模式,epoll()模型即支持水平触发,也支持边缘触发,默认是水平触发。

用户头像

Linux服务器开发qun720209036,欢迎来交流 2020.11.26 加入

专注C/C++ Linux后台服务器开发。

评论

发布
暂无评论
linux网络编程—7层网络以及5种Linux IO模型以及相应IO基础