深入理解NIO(四)—— epoll的实现原理

本文链接:https://www.cnblogs.com/fatmanhappycode/p/12362423.html

终于来到最后了,万里长征只差最后一步 ( `д´) !

简单流程梳理

我们先从只监听一个socket开始讲起:

首先我们有一个程序A,他运行这下面这样一段代码:

//创建socket
int s = socket(AF_INET, SOCK_STREAM, );
//绑定端口
bind(s, ...)
//监听
listen(s, ...)
//接受客户端连接
int c = accept(s, ...)
//接收客户端数据,没有数据就先阻塞在这里
recv(c, ...);
//将数据打印出来
printf(...)

当程序A运行到recv()的时候它阻塞了,

之后就挂起在等待队列中,等待被唤醒之后继续执行,

而在Linux中,万事万物皆为文件,我们的socket也占用了一个fd

我们的A就挂在socket中的等待队列中。

接下来我们来看一个简单的流程:

  1. 首先第一步是通过网线发送数据到网卡
  2. 网卡将数据存到内存中
  3. 网卡对cpu发出中断信号,提醒cpu过来处理网卡的任务
  4. cpu接到信号后暂时中断自己的任务,过来运行网卡准备的中断程序
  5. 中断程序的内容是:将网卡写到内存中的网络数据写入socket的输入缓冲区
  6. 然后中断程序再唤醒处于阻塞状态的A唤醒,并挂到工作队列中让cpu运行它,而cpu就会运行刚刚代码的最后一段:打印
//将数据打印出来
printf(...)

当然,实际应用中我们不可能只监听一个socket,接下来我们就直接来看一下,能监听多个socket的select是怎么实现的:

select

select:上世纪 80 年代就实现了,它支持注册 FD_SETSIZE(1024) 个 socket,在那个年代肯定是够用的,不过现在嘛,肯定是不行了。

当然,这里的select不是指nio里的select()方法,而是指操作系统中的一个实现,它的改进版本是pollepoll,请大家注意不要混淆。

我们先从select说起:

int s = socket(AF_INET, SOCK_STREAM, );
bind(s, ...);
listen(s, ...);
int fds[] = // 存放需要监听的socket;
while(){
// 这里就是我们的select
int n = select(..., fds, ...)
for(int i=; i < fds.count; i++){
if(FD_ISSET(fds[i], ...)){
//fds[i]的数据处理
}
}
}

这段代码中,先准备了一个数组 fds,让 fds 存放着所有需要监视的 socket。然后调用 select,如果 fds 中的所有 socket 都没有数据,select 会阻塞,直到有一个 socket 接收到数据,select 返回,唤醒进程。用户可以遍历 fds,通过 FD_ISSET 判断具体哪个 socket 收到数据,然后做出处理。

接下来我们看一下流程图:

  1. 流程基本都一样,A却存在于多个socket的等待队列中
  2. 当某个socket被写入数据时,A也被唤醒并从多个socket的等待队列中移除后加入工作队列
  3. 但是此时A并不知道是哪个socket被写入了数据,所以只能遍历所有socket
  4. 在A处理完任务后移出工作队列,但是此时却需要遍历所有socket并加入它们的等待队列中

select的缺点在于:

  1. 每次调用 select 都需要将进程加入到所有监视 socket 的等待队列,每次唤醒都需要从每个队列中移除。这里涉及了两次遍历,而且每次都要将整个 fds 列表传递给内核,有一定的开销。正是因为遍历操作开销大,出于效率的考量,才会规定 select 的最大监视数量,默认只能监视 1024 个 socket。
  2. 进程被唤醒后,程序并不知道哪些 socket 收到数据,还需要遍历一次。

poll

因此人们先提出了poll

poll:1997 年,出现了 poll 作为 select 的替代者,最大的区别就是,poll 不再限制 socket 数量。

但是poll并没有解决刚刚提到的select的问题,所以就有了epoll

epoll

select 低效的原因之一是将“维护等待队列”和“阻塞进程”两个步骤合二为一。如下图所示,每次调用 select 都需要这两步操作,然而大多数应用场景中,需要监视的 socket 相对固定,并不需要每次都修改。epoll 将这两个操作分开,先用 epoll_ctl 维护等待队列,再调用 epoll_wait 阻塞进程。显而易见地,效率就能得到提升。

还记得我们的NIO源码分析吗,里面就调用了epoll_create、epoll_ctl和epoll_wait

epoll_create

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

epoll_ctl

如果通过 epoll_ctl 添加 sock1、sock2 和 sock3 的监视,内核会将 eventpoll 添加到这三个 socket 的等待队列中。

epoll_wait

进程 A 运行到了 epoll_wait 语句。如下图所示,内核会将进程 A 放入 eventpoll 的等待队列中,阻塞进程。(这里的rdlist和等待队列都是eventpoll对象的属性一样的东西,都是属于它的)

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

数据结构

监听所有socket的等待队列(不是存放进程A的)的结构是红黑树,红黑树是一种自平衡二叉查找树,搜索、插入和删除时间复杂度都是O(log(N)),效率较好

而rdlist是一个双向链表,进程A应该是放在poll_wait里(我猜的,错了不怪我)

Linux epoll API

之前说Java NIO封装了epoll的api,那么我们就来看一下它的api吧(虽然并不重要,但是还是看看吧):

int epoll_create(int size)

创建一个epoll的句柄。自从linux2.6.8之后,size参数是被忽略的。需要注意的是,当创建好epoll句柄后,它就是会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)

1. 第一个参数是epoll_create()的返回值。

2. 第二个参数表示动作,用三个宏来表示:

  • EPOLL_CTL_ADD:注册新的fd到epfd中;
  • EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
  • EPOLL_CTL_DEL:从epfd中删除一个fd;

3. 第三个参数是需要监听的fd。

4. 第四个参数是告诉内核需要监听什么事:

  • EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
  • EPOLLOUT:表示对应的文件描述符可以写;
  • EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
  • EPOLLERR:表示对应的文件描述符发生错误;
  • EPOLLHUP:表示对应的文件描述符被挂断;
  • EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
  • EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里

int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout)

  1. 收集在epoll监控的事件中已经发送的事件。
  2. 参数events是分配好的epoll_event结构体数组,epoll将会把发生的事件赋值到events数组中(events不可以是空指针,内核只负责把数据复制到这个events数组中,不会去帮助我们在用户态中分配内存)。
  3. maxevents告之内核这个events有多大,这个 maxevents的值不能大于创建epoll_create()时的size。
  4. 参数timeout是超时时间(毫秒,0会立即返回,-1是永久阻塞)。如果函数调用成功,返回对应I/O上已准备好的文件描述符数目,如返回0表示已超时。

epoll的两种触发模式

epoll有EPOLLLT和EPOLLET两种触发模式,LT是默认的模式,ET是“高速”模式。

LT(水平触发)模式下,只要这个文件描述符还有数据可读,每次 epoll_wait都会返回它的事件,提醒用户程序去操作;

ET(边缘触发)模式下,在它检测到有 I/O 事件时,通过 epoll_wait 调用会得到有事件通知的文件描述符,对于每一个被通知的文件描述符,如可读,则必须将该文件描述符一直读到空,让 errno 返回 EAGAIN 为止,否则下次的 epoll_wait 不会返回余下的数据,会丢掉事件。如果ET模式不是非阻塞的,那这个一直读或一直写势必会在最后一次阻塞。

采用ET这种边缘触发模式的话,当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。如果这次没有把数据全部读写完(如读写缓冲区太小),那么下次调用epoll_wait()时,它不会通知你,也就是它只会通知你一次,直到该文件描述符上出现第二次可读写事件才会通知你

如果你能看到这里

终于结束了 ( `д´) !经过四篇深入理解NIO系列博文,你已经是个成熟Java调包师了,你将和我一样找不到工作,おめでとう !


本文参考自 : https://my.oschina.net/editorial-story/blog/3052308#comments,我很喜欢这种讲解风格,不过原博主好像主要写游戏开发类的,可能很难再有我想看的博文了。

Linux epoll API小节搬运自https://www.cnblogs.com/yangang92/p/7469970.html

两种触发模式搬运自https://blog.csdn.net/daaikuaichuan/article/details/83862311?utm_source=distribute.pc_relevant.none-task

深入理解NIO(四)—— epoll的实现原理的更多相关文章

  1. 深入理解NIO(三)—— NIO原理及部分源码的解析

    深入理解NIO(三)—— NIO原理及部分源码的解析 欢迎回到淦™的源码看爆系列 在看完前面两个系列之后,相信大家对NIO也有了一定的理解,接下来我们就来深入源码去解读它,我这里的是OpenJDK-8 ...

  2. 深入理解NIO(一)—— NIO的简单使用及其三大组件介绍

    深入理解NIO(一)—— NIO的简单使用及其三大组件介绍 深入理解NIO系列分为四个部分 第一个部分也就是本节为NIO的简单使用(我很少写这种新手教程,所以如果你是复习还好,应该不难理解这篇,但如果 ...

  3. TCP协议—三次握手四次挥手的原理<转>

    三次握手四次挥手的原理   TCP是面向连接的,无论哪一方向另一方发送数据之前,都必须先在双方之间建立一条连接.在TCP/IP协议中,TCP 协议提供可靠的连接服务,连接是通过三次握手进行初始化的.三 ...

  4. 深入理解Java并发之synchronized实现原理

    深入理解Java类型信息(Class对象)与反射机制 深入理解Java枚举类型(enum) 深入理解Java注解类型(@Annotation) 深入理解Java类加载器(ClassLoader) 深入 ...

  5. 图解ARP协议(四)代理ARP原理与实践(“善意的欺骗”)

    一.代理ARP概述 我:当电脑要访问互联网上的服务器,目标MAC是什么? 很多小伙伴在刚学习网络协议的时候,经常这样直接回应:不就是服务器的MAC嘛! 这时我会反问:那电脑怎么拿到这个服务器的MAC地 ...

  6. 深入理解并发编程之----synchronized实现原理

    版权声明:本文为博主原创文章,请尊重原创,未经博主允许禁止转载,保留追究权 https://blog.csdn.net/javazejian/article/details/72828483 [版权申 ...

  7. 深入理解NIO(二)—— Tomcat中对NIO的应用

    深入理解NIO(二)—— Tomcat中对NIO的应用 老哥行行好,转载和我说一声好吗,我不介意转载的,但是请把原文链接贴大点好吗 Tomcat大致架构 先贴两张图大致看一眼Tomcat的架构 Tom ...

  8. linux基础-第十四单元 Linux网络原理及基础设置

    第十四单元 Linux网络原理及基础设置 三种网卡模式图 使用ifconfig命令来维护网络 ifconfig命令的功能 ifconfig命令的用法举例 使用ifup和ifdown命令启动和停止网卡 ...

  9. 理解Promise简单实现的背后原理

    在写javascript时我们往往离不开异步操作,过去我们往往通过回调函数多层嵌套来解决后一个异步操作依赖前一个异步操作,然后为了解决回调地域的痛点,出现了一些解决方案比如事件订阅/发布的.事件监听的 ...

随机推荐

  1. SpringBoot入门系列(二)如何返回统一的数据格式

    前面介绍了Spring Boot的优点,然后介绍了如何快速创建Spring Boot 项目.不清楚的朋友可以看看之前的文章:https://www.cnblogs.com/zhangweizhong/ ...

  2. vue組件自学

    Vue组件 什么是组件? 组件 (Component) 是 Vue.js 最强大的功能之一.组件可以扩展 HTML 元素,封装可重用的代码.在较高层面上,组件是自定义元素,Vue.js 的编译器为它添 ...

  3. Object-Oriented Programming Summary Ⅲ

    目录 JML单元作业博客 1.1 梳理JML语言的理论基础 0. 前言 1. 形式 2. 作用域 3. 前置条件 (requires) 4. 后置条件 (ensures) 5. 模型域 (model) ...

  4. day07深浅copy与流程控制

    目录:流程控制 0:补充(了解) 短路运算:偷懒原则,偷懒到哪个位置,就把当前位置的值返回 深浅copy 1.循环之while循环 循环的语法与基本使用 死循环与效率问题 循环的应用 退出循环的两种方 ...

  5. JavaScript面向对象class

    JavaScript面向对象class 本周逆战班学习的主题是“面向对象”,很多人觉得面向对象很难理解,但其实我们早就在面向对象的思想之中了,今天就让我们再重新认识一下他,主要介绍一下ES6中新增的c ...

  6. Drf 序列化 ModelSerializer跨表取数据

    1.对于OneToOne.Foreignkey.choices字段可以使用source取出相关信息: class CourseSerializer(serializers.ModelSerialize ...

  7. 你会无聊到把Administrator用户禁用,并且自己创建的用户搞到消失掉还有Administrator用户被禁吗。。。。。

    2020年3月17日20:07:00 如文章标题哈,就是这么任性,无奈 事件起因:因为要部署项目,并且需要将线上的Oracle数据库导入到本地Oracle数据库中突然发现使用 sqlplus 连接数据 ...

  8. 通过locust进行性能测试

    首先我们需要准备好python环境 接口 安装python 插件 locust,网上有很多文章而且都很错,这里不再赘述 我是通过pycharm 编写的代码  导入 HttpLocust,TaskSet ...

  9. (转)浅析epoll-为何多路复用I/O要使用epoll

    原文地址:http://www.cppfans.org/1417.html 浅析epoll-为何多路复用I/O要使用epoll 现如今,网络通讯中用epoll(linux)和IOCP(windows) ...

  10. 2020年IOS超级签最新实现原理详解

    相信2019年最火的应该就是这个东西了,我也是摸着石头过河,勉强混进了这个行业! 超级签这个东西吧,说白了就是用个人账号分发应用,大致分成以下几个步骤吧 一.使用配置文件获取UDID 苹果公司允许开发 ...