深入理解Linux内核之IO多路复用下

本文详细解析了epoll的高效原理,涉及Linux中断处理、内核收包过程、进程阻塞机制以及epoll的数据结构,重点介绍了epoll如何通过功能分离和就绪列表优化网络数据监控效率。
摘要由CSDN通过智能技术生成

目录

epoll高效原理和底层机制分析

linux中的中断

linux内核收包

linux进程阻塞

linux内核接收网络数据

linux中同时监视多个socket方法

epoll 的设计思路

epoll 的原理和流程

epoll 的数据结构


epoll高效原理和底层机制分析

          首先,我们需要知道,数据是通过网线传输到网卡上的,网卡收到网线传来的数据,经过硬件电路的传输,最终将数据写入到内存中的某个地址上。CPU 和操作系统是通过中断来知道网络上有数据要接收的。

linux中的中断

       Linux 中断处理函数是分上半部和下半部的。上半部是只进行最简单的工作,快速处理然后释放 CPU,接着 CPU 就可以允许其它中断进来。剩下将绝大部分的工作都放到下半部中,可以慢慢从容处理。2.4 以后的内核版本采用的下半部实现方式是软中断,由 ksoftirqd 内核线程全权处理。和硬中断不同的是,硬中断是通过给 CPU 物理引脚施加电压变化,而软中断是通过给内存中的一个变量的二进制值以通知软中断处理程序。


linux内核收包

              

       当网卡上收到数据以后,Linux 中第一个工作的模块是网络驱动。网络驱动会以 DMA 的方式把网卡上收到的帧写到内存里。再向 CPU 发起一个中断,以通知 CPU 有数据到达。第二,当 CPU 收到中断请求后,会去调用网络驱动注册的中断处理函数。网卡的中断处理函数并不做过多工作,发出软中断请求,然后尽快释放 CPU。ksoftirqd 检测到有软中断请求到达,调用 poll 开始轮询收包,收到后交由各级协议栈处理。最后会被放到用户 socket 的接收队列中。


linux进程阻塞

         epoll的本质要从操作系统进程调度的角度来看数据接收。。阻塞是进程调度的关键一环,指的是进程在等待某事件(如接收到网络数据)发生之前的等待状态,recv、select 和 epoll 都是阻塞方法。

        阻塞的原理是:操作系统为了支持多任务,实现了进程调度的功能,会把进程分为“运行”和“等待”等几种状态。运行状态是进程获得 cpu 使用权,正在执行代码的状态。等待状态是阻塞状态,比如程序运行到 recv 时,程序会从运行状态变为等待状态,接收到数据后又变回运行状态。操作系统会分时执行各个运行状态的进程,由于速度很快,看上去就像是同时执行多个任务。

举例说明:

              

        计算机中运行着 A、B、C 三个进程,其中进程 A 执行着上述基础网络程序,一开始,这 3 个进程都被操作系统的工作队列所引用,处于运行状态,会分时执行。当进程 A 执行到创建 socket 的语句时,操作系统会创建一个由文件系统管理的 socket 对象。这个 socket 对象包含了发送缓冲区、接收缓冲区、等待队列等成员。等待队列是个非常重要的结构,它指向所有需要等待该 socket 事件的进程。

                 

        当程序执行到 recv 时,操作系统会将进程 A 从工作队列移动到该 socket 的等待队列中。由于工作队列只剩下了进程 B 和 C,依据进程调度,cpu 会轮流执行这两个进程的程序,不会执行进程 A 的程序。所以进程 A 被阻塞,不会往下执行代码,也不会占用 cpu 资源。

       操作系统添加等待队列只是添加了对这个“等待中”进程的引用,以便在接收到数据时获取进程对象、将其唤醒,而非直接将进程管理纳入自己之下。上图为了方便说明,直接将进程挂到等待队列之下。

       当 socket 接收到数据后,操作系统将该 socket 等待队列上的进程重新放回到工作队列,该进程变成运行状态,继续执行代码。也由于 socket 的接收缓冲区已经有了数据,recv 可以返回接收到的数据。


linux内核接收网络数据

            

        进程在 recv 阻塞期间,计算机收到了对端传送的数据(步骤①)。数据经由网卡传送到内存(步骤②),然后网卡通过中断信号通知 cpu 有数据到达,cpu 执行中断程序(步骤 ③)。此处的中断程序主要有两项功能,先将网络数据写入到对应 socket 的接收缓冲区里面(步骤④),再唤醒进程 A(步骤⑤),重新将进程 A 放入工作队列中。

操作系统如何知道网络数据对应于哪个socket?

       因为一个 socket 对应着一个端口号,而网络数据包中包含了 ip 和端口的信息,内核可以通过端口号找到对应的 socket。当然,为了提高处理速度,操作系统会维护端口号到 socket 的索引结构,以快速读取。(其实也就是通过五元组来确定的)


linux中同时监视多个socket方法

       为了更好的理解epoll,我们先看select是如何做的,服务端需要管理多个客户端连接,而 recv 只能监视单个 socket,假如能够预先传入一个 socket 列表,如果列表中的 socket 都没有数据,挂起进程,直到有一个 socket 收到数据,唤醒进程。这种方法很直接,也是 select 的设计思想。

       select 的实现思路很直接。假如程序同时监视 sock1、sock2 和 sock3 三个 socket,那么在调用 select 之后,操作系统把进程 A 分别加入这三个 socket 的等待队列中。

                   

       当任何一个 socket 收到数据后,中断程序将唤起进程。所谓唤起进程,就是将进程从所有的等待队列中移除,加入到工作队列里面。经由这些步骤,当进程 A 被唤醒后,它知道至少有一个 socket 接收了数据。程序只需遍历一遍 socket 列表,就可以得到就绪的 socket。

       这种简单方式行之有效,在几乎所有操作系统都有对应的实现。但是简单的方法往往有缺点,主要是:

1. 每次调用 select 都需要将进程加入到所有被监视 socket 的等待队列,每次唤醒都需要从每个队列中移除,都必须要进行遍历。而且每次都要将整个 fds 列表传递给内核,有一定的开销。正是因为遍历操作开销大,出于效率的考量,才会规定 select 的最大监视数量,默认只能监视 1024 个 socket。

2. 进程被唤醒后,程序并不知道哪些 socket 收到数据,还需要遍历一次。

为了解决这些缺点,epoll应运而生。


epoll 的设计思路

       epoll 是在 select 出现 N 多年后才被发明的,是 select 和 poll 的增强版本。epoll 通过以下一些措施来改进效率。

措施一:功能分离

        select 低效的原因之一是将“维护等待队列”和“阻塞进程”两个步骤合二为一。每次调用 select 都需要这两步操作,然而大多数应用场景中,需要监视的 socket 相对固定,并不需要每次都修改。epoll 将这两个操作分开,先用 epoll_ctl 维护等待队列,再调用 epoll_wait 阻塞进程。显而易见的,效率就能得到提升。 相比 select,epoll 拆分了功能,先用 epoll_create 创建一个 epoll 对象 epfd,再通过 epoll_ctl 将需要监视的 socket 添加到 epfd 中,最后调用 epoll_wait 等待数据。功能分离,使得 epoll 有了优化的可能。

措施二:就绪列表

       select 低效的另一个原因在于程序不知道哪些 socket 收到数据,只能一个个遍历。如果内核维护一个“就绪列表”,引用收到数据的 socket,就能避免遍历。


epoll 的原理和流程

       当某个进程调用 epoll_create 方法时,内核会创建一个 eventpoll 对象(也就是程序中 epfd 所代表的对象)。eventpoll 对象也是文件系统中的一员,和 socket 一样,它也会有等待队列。

       创建 epoll 对象后,可以用 epoll_ctl 添加或删除所要监听的 socket。以添加 socket 为例,如下图,如果通过 epoll_ctl 添加 sock1、sock2 和 sock3 的监视,内核会将 eventpoll 添加到这三个 socket 的等待队列中。

               

      当socket收到数据后,中断程序会操作 eventpoll 对象,而不是直接操作进程。中断程序会给 eventpoll 的“就绪列表”添加 socket 引用。如下图展示的是 sock2 和 sock3 收到数据后,中断程序让 rdlist 引用这两个 socket。

                    

        eventpoll 对象相当于是 socket 和进程之间的中介,socket 的数据接收并不直接影响进程,而是通过改变 eventpoll 的就绪列表来改变进程状态。

        当程序执行到 epoll_wait 时,如果 rdlist 已经引用了 socket,那么 epoll_wait 直接返回,如果 rdlist 为空,阻塞进程。

       假设计算机中正在运行进程 A 和进程 B,在某时刻进程 A 运行到了 epoll_wait 语句。如下图所示,内核会将进程 A 放入 eventpoll 的等待队列中,阻塞进程。

           

       当 socket 接收到数据,中断程序一方面修改 rdlist,另一方面唤醒 eventpoll 等待队列中的进程,进程 A 再次进入运行状态。也因为 rdlist 的存在,进程 A 可以知道哪些 socket 发生了变化。


epoll 的数据结构

       就绪列表是一种能够快速插入和删除的数据结构。双向链表就是这样一种数据结构,所以epoll使用双向链表来实现就绪队列。Linux 源码如下:

       epoll 将“维护监视队列”和“进程阻塞”分离,也意味着需要有个数据结构来保存监视的 socket。至少要方便的添加和移除,还要便于搜索,以避免重复添加。红黑树是一种自平衡二叉查找树,搜索、插入和删除时间复杂度都是 O(log(N)),效率较好。epoll 使用了红黑树作为索引结构,Linux 源码如下:

  • 16
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值