热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

深入解析Linux下的I/O多路转接epoll技术

本文深入解析了Linux下的I/O多路转接epoll技术,介绍了select和poll函数的问题,以及epoll函数的设计和优点。同时讲解了epoll函数的使用方法,包括epoll_create和epoll_ctl两个系统调用。
浅析I/O多路转接epoll技术

前面的两篇博客我们已经为大家介绍了select和poll函数,但是在学习中我们发现select和poll存在效率上的问题。而今天的主角epoll函数真的是让人惊艳的设计,它是在2.5.44内核中被引进的,它几乎具备了之前所说的一切优点,被公认为Linux2.6下性能最好的多路I/O就绪通知方法。包括现在最火的nginx服务器底层使用的也是epoll多路转接

epoll函数

要想在知道他为什么这么高效之前我们先来看看这个函数是如何使用的,所谓要学会跑就得先学会走就是这个道理
epoll函数有三个相关的系统调用:

int epoll_create(int size);

参数是一个int类型的整数,这个数字随便填,在2.6以后就被忽略了相当于是一个历史遗留问题。这里的返回值比较重要,epoll返回一个句柄,这个句柄能帮我们找到之后要使用的所有epoll机制。

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

参数介绍:

  • epfd:传入epoll_create函数的返回值,也就是文件句柄
  • fd:传入你所要关心的文件描述符
  • op:你想对要关心的文件描述符做什么操作。EPOLL_CTL_ADD选项注册新的fd到epfd中、EPOLL_CTL_MOD 选项修改已经注册的fd的监听事件、EPOLL_CTL_DEL选项从epfd中删除一个fd
  • event:可以看出这个参数和所关心的事件有关
    在这里插入图片描述
    events是一个位图,其中设置你所希望关心的事件。data中填充你所关心的fd

事件描述
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);

参数介绍:

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

浅谈epoll函数是如何做到高效的

ps:接下来可能说到的有些知识有的同学可能不太懂,不过现在搜索引擎这么强大,相信你能找到你不懂知识的答案的。

其实笔者在拿到epoll的这三个函数时是懵的,因为第一个create函数就让我产生了极大的困惑,那么我们就从第一个函数说起,看看创建这个句柄到底干了什么。

话说Linux下一切皆文件,在操作系统的内核中也同样如此。epoll向内核注册了一个虚拟文件系统,这个文件系统用来管理被监测的文件描述符,而epoll_create函数时为我们创建一个属于该文件系统的文件并返回。每个由epoll_create创建的文件都会得到一个struct eventpoll结构体,这个结构体被保存在file结构体的private_data中。这个结构体是用来干啥的呢?一起来看看他的成员

struct eventpoll { /* 用于维护自身的状态,可用于中断上下文 */ spinlock_t lock; /* * 用户进程上下文中 */ struct mutex mtx; /* 进程等待队列,由 sys_epoll_wait()使用,调用epoll_wait时,休眠在这里 */ wait_queue_head_t wq; /* 进程等待队列,由 file->poll()使用 ,epollfd本身被poll时,休眠在这里*/ wait_queue_head_t poll_wait; /* 就绪文件描述符链表 */ struct list_head rdllist; /* 红黑树头节点,该红黑树用于存储要监控的文件描述符 */ struct rb_root rbr; /* * ready事件的临时存放链表 */ struct epitem *ovflist; /* 创建eventpoll descriptor的用户 */ struct user_struct *user;
};

其余的东西不用太关心,但是有俩个东西非常重要。一个是红黑树的头节点,一个是就绪文件描述符链表。这俩个东西就是让epoll机制效率极大提升的神器。
在这里插入图片描述
我简单描叙一下这些部分都是用来干什么的。

  • struct eventpoll这个结构体在笔者看来他就像是一个事件管理器。之所以这么说是因为他管理着epoll系统中的红黑树,就绪队列和等待队列。
  • 红黑树:红黑树是一颗二叉搜索树,也叫次平衡树。他插入删除查找的效率都是nlogn。这颗红黑树中存储着所有添加到epoll中的需要监控的事件
  • rdllist:这个链表中存放的是已经就绪的事件
  • wait_queue:这个队列中存放着被检测的事件,一但有事件就绪,那么就通过回调机制告诉上级,并让上级将就绪的事件移动到rdllist中。

这里额外需要提到的是,在epoll中每个事件都被一个epitem结构体描述:

struct epitem{ struct rb_node rbn;//红黑树节点struct list_head rdllink;//双向链表节点struct epoll_filefd ffd; //事件句柄信息struct eventpoll *ep; //指向其所属的eventpoll对象struct epoll_event event; //期待发生的事件类型
}

ffd中存放所关心事件的fd和file结构体,其余的参数都比较好理解。

为什么epoll是高效的

现在我们来谈一谈为什么epoll是高效的,其实通过上面的模型你大体已经可以发现他高效的原因:

  • 查询就绪事件速度:想一想我们之前poll和select最大的瓶颈在哪里?没错,就是有事件就绪后一遍遍的遍历。而现在有了就绪队列之后呢,查询的速度变成了惊人的O(1),其实也就是说处于此队列中的事件一定就绪了,这归功于epoll的回调机制,这种机制与相应的文件描述符绑定在一起,当文件描述符就绪时就调用某个函数,将此事件添加到就绪队列中
  • 不在需要每次都拷贝数据到内核:select和poll中每次都要将位图或者数组进行拷贝,而epoll不是完全不拷贝,而是每次只拷贝少量数据。你所关心的事件从头到尾拷贝到内核并注册到红黑树只需要一次。就绪队列每次需要拷贝到用户空间一次,不过代价真的变得非常小了。

这是比较重要的两条原因,其实epoll已经解决了文件描述符有上限和接口设计不友好的等等问题。并且使用红黑树在进行不重复的插入和进行删除时都比数组查询的ON要快的多。

有的同学会说,epoll底层不是使用了内存映射么?这里为什么需要进行数据拷贝呢?注意注意注意!笔者之前点开b站有些自称为epoll深度解析的大佬张口就是内存映射,事实上epoll底层并没有使用映射这种机制,有的人也会质疑我凭什么你说没有就没有。质疑是种好习惯,为了找到事情的真相,下篇博客不如我们就来探究epoll的底层是如何实现的吧。

探究epoll的工作模式

在说epoll的工作模式之前我们先来举两个栗子来帮助我们更简单的理解epoll的工作模式。

  • 栗子一:你的妈妈是亲妈,你放假时特别喜欢玩游戏,而饭好时你妈妈就会叫你吃饭。叫了你一次之后,你没有去,你妈妈又来喊了你一次,你还是没有去,过了一会你妈妈又来喊了一次…
  • 栗子二:你的妈妈是后妈,你放假时特别喜欢玩游戏,而饭好时你妈妈就会叫你吃饭。叫了你一次之后,你没有去,全剧终。

上面俩个栗子其实对应了epoll的两张工作模式,前者称为水平触发Level Triggered 工作模式,后者称为边缘触发Edge Triggered工作模式,默认的情况下水平触发是epoll默认的工作模式,那么在epoll中怎么理解呢?

水平触发


  • 当epoll检测到socket上事件就绪的时候, 可以不立刻进行处理. 或者只处理一部分.
  • 就绪描述符中有2k数据,只读1K数据, 缓冲区中还剩1K数据, 在第二次调用 epoll_wait 时, epoll_wait仍然会立刻返回并通知socket读事件就绪,直到缓冲区上所有的数据都被处理完, epoll_wait 才不会立刻返回.
  • 支持阻塞读写和非阻塞读写

边缘触发


  • 当epoll检测到socket上的事件就绪的时候,必须立即进行处理
  • 上面的栗子中你先处理1k的数据,缓存区中还存在1k的数据,而你下次调用epoll_wait时epoll_wait就不会在返回了,也就是说在ET模式下文件描述符上的事件就绪后只有一次处理机会。
  • ET模式下比LT性能更高,因为epoll的返回次数变少了,nginx默认的模式就是ET模式
  • 只支持非阻塞的读写

对比LT和ET:其实假如LT模式下每次提示都立刻处理,且每次都将数据读完避免多次提示那么效率与ET也不会差太多

理解ET模式和非阻塞文件描述符

使用 ET 模式的 epoll,需要将文件描述设置为非阻塞.。这个不是接口上的要求,,而是 “工程实践” 上的要求。

假设场景,服务器接受到一个10k的请求,会向客户端返回一个应答数据.。如果客户端收不到应答,不会发送第二个10k请求

如果服务端写的代码是阻塞式的read,并且一次只 read 1k 数据的话(read不能保证一次就把所有的数据都读出来,参考 man 手册的说明,可能被信号打断),剩下的9k数据就会待在缓冲区中

在这里插入图片描述

所以, 为了解决上述问题(阻塞read不一定能一下把完整的请求读完), 于是就可以使用非阻塞轮训的方式来读缓冲区,保证一定能把完整的请求都读出来.
而如果是LT没这个问题. 只要缓冲区中的数据没读完, 就能够让 epoll_wait 返回文件描述符读就绪.

epoll的使用场景一般为:对于多连接, 且多连接中只有一部分连接比较活跃时, 比较适合使用epoll.

epoll的惊群问题

产生惊群问题的原因:

  • 在多线程或者多进程环境下,有些人为了提高程序的稳定性,往往会让多个线程或者多个进程同时在epoll_wait监听的socket描述符。当一个新的链接请求进来时,操作系统不知道选派那个线程或者进程处理此事件,则干脆将其中几个线程或者进程给唤醒,而实际上只有其中一个进程或者线程能够成功处理accept事件,其他线程都将失败,且errno错误码为EAGAIN。这种现象称为惊群效应,结果是肯定的,惊群效应肯定会带来资源的消耗和性能的影响。

如何解决:

  • 多线程:不建议让多个线程同时在epoll_wait监听的socket,而是让其中一个线程epoll_wait监听的socket,当有新的链接请求进来之后,由epoll_wait的线程调用accept,建立新的连接,然后交给其他工作线程处理后续的数据读写请求,这样就可以避免了由于多线程环境下的epoll_wait惊群效应问题
  • 多进程:在同一时刻,永远都只有一个子进程在监听的socket上epoll_wait,其做法是,创建一个全局的pthread_mutex_t,在子进程进行epoll_wait前,则先获取锁

总结

本节我们大概的介绍了epoll高效的原因,但是相信很多同学还是处于朦朦胧胧的状态,那么我们下节就从epoll的源码入手,深度刨析一下epoll的底层到底是怎么实现的。


推荐阅读
  • Nginx使用AWStats日志分析的步骤及注意事项
    本文介绍了在Centos7操作系统上使用Nginx和AWStats进行日志分析的步骤和注意事项。通过AWStats可以统计网站的访问量、IP地址、操作系统、浏览器等信息,并提供精确到每月、每日、每小时的数据。在部署AWStats之前需要确认服务器上已经安装了Perl环境,并进行DNS解析。 ... [详细]
  • 目录浏览漏洞与目录遍历漏洞的危害及修复方法
    本文讨论了目录浏览漏洞与目录遍历漏洞的危害,包括网站结构暴露、隐秘文件访问等。同时介绍了检测方法,如使用漏洞扫描器和搜索关键词。最后提供了针对常见中间件的修复方式,包括关闭目录浏览功能。对于保护网站安全具有一定的参考价值。 ... [详细]
  • linux进阶50——无锁CAS
    1.概念比较并交换(compareandswap,CAS),是原⼦操作的⼀种,可⽤于在多线程编程中实现不被打断的数据交换操作࿰ ... [详细]
  • 本文详细介绍了SQL日志收缩的方法,包括截断日志和删除不需要的旧日志记录。通过备份日志和使用DBCC SHRINKFILE命令可以实现日志的收缩。同时,还介绍了截断日志的原理和注意事项,包括不能截断事务日志的活动部分和MinLSN的确定方法。通过本文的方法,可以有效减小逻辑日志的大小,提高数据库的性能。 ... [详细]
  • 本文介绍了Python高级网络编程及TCP/IP协议簇的OSI七层模型。首先简单介绍了七层模型的各层及其封装解封装过程。然后讨论了程序开发中涉及到的网络通信内容,主要包括TCP协议、UDP协议和IPV4协议。最后还介绍了socket编程、聊天socket实现、远程执行命令、上传文件、socketserver及其源码分析等相关内容。 ... [详细]
  • Nginx使用(server参数配置)
    本文介绍了Nginx的使用,重点讲解了server参数配置,包括端口号、主机名、根目录等内容。同时,还介绍了Nginx的反向代理功能。 ... [详细]
  • 本文分享了一个关于在C#中使用异步代码的问题,作者在控制台中运行时代码正常工作,但在Windows窗体中却无法正常工作。作者尝试搜索局域网上的主机,但在窗体中计数器没有减少。文章提供了相关的代码和解决思路。 ... [详细]
  • JVM 学习总结(三)——对象存活判定算法的两种实现
    本文介绍了垃圾收集器在回收堆内存前确定对象存活的两种算法:引用计数算法和可达性分析算法。引用计数算法通过计数器判定对象是否存活,虽然简单高效,但无法解决循环引用的问题;可达性分析算法通过判断对象是否可达来确定存活对象,是主流的Java虚拟机内存管理算法。 ... [详细]
  • 本文介绍了在Mac上搭建php环境后无法使用localhost连接mysql的问题,并通过将localhost替换为127.0.0.1或本机IP解决了该问题。文章解释了localhost和127.0.0.1的区别,指出了使用socket方式连接导致连接失败的原因。此外,还提供了相关链接供读者深入了解。 ... [详细]
  • 31.项目部署
    目录1一些概念1.1项目部署1.2WSGI1.3uWSGI1.4Nginx2安装环境与迁移项目2.1项目内容2.2项目配置2.2.1DEBUG2.2.2STAT ... [详细]
  • Android工程师面试准备及设计模式使用场景
    本文介绍了Android工程师面试准备的经验,包括面试流程和重点准备内容。同时,还介绍了建造者模式的使用场景,以及在Android开发中的具体应用。 ... [详细]
  • 本文介绍了操作系统的定义和功能,包括操作系统的本质、用户界面以及系统调用的分类。同时还介绍了进程和线程的区别,包括进程和线程的定义和作用。 ... [详细]
  • 解决php错误信息不显示在浏览器上的方法
    本文介绍了解决php错误信息不显示在浏览器上的方法。作者发现php中的各种错误信息并不显示在浏览器上,而是需要在日志文件中查看。为了解决这个问题,作者提供了一种解决方式:通过修改php.ini文件中的display_errors参数为On,并重启服务。这样就可以在浏览器上直接显示php错误信息了。 ... [详细]
  • 本文介绍了在Android开发中使用软引用和弱引用的应用。如果一个对象只具有软引用,那么只有在内存不够的情况下才会被回收,可以用来实现内存敏感的高速缓存;而如果一个对象只具有弱引用,不管内存是否足够,都会被垃圾回收器回收。软引用和弱引用还可以与引用队列联合使用,当被引用的对象被回收时,会将引用加入到关联的引用队列中。软引用和弱引用的根本区别在于生命周期的长短,弱引用的对象可能随时被回收,而软引用的对象只有在内存不够时才会被回收。 ... [详细]
  • MySQL数据库锁机制及其应用(数据库锁的概念)
    本文介绍了MySQL数据库锁机制及其应用。数据库锁是计算机协调多个进程或线程并发访问某一资源的机制,在数据库中,数据是一种供许多用户共享的资源,如何保证数据并发访问的一致性和有效性是数据库必须解决的问题。MySQL的锁机制相对简单,不同的存储引擎支持不同的锁机制,主要包括表级锁、行级锁和页面锁。本文详细介绍了MySQL表级锁的锁模式和特点,以及行级锁和页面锁的特点和应用场景。同时还讨论了锁冲突对数据库并发访问性能的影响。 ... [详细]
author-avatar
谢俊荣1792
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有