深入理解网络 I/O 多路复用:Epoll

这篇具有很好参考价值的文章主要介绍了深入理解网络 I/O 多路复用:Epoll。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

🔭 嗨,您好 👋 我是 vnjohn,在互联网企业担任 Java 开发,CSDN 优质创作者
📖 推荐专栏:Spring、MySQL、Nacos、Java,后续其他专栏会持续优化更新迭代
🌲文章所在专栏:网络 I/O
🤔 我当前正在学习微服务领域、云原生领域、消息中间件等架构、原理知识
💬 向我询问任何您想要的东西,ID:vnjohn
🔥觉得博主文章写的还 OK,能够帮助到您的,感谢三连支持博客🙏
😄 代词: vnjohn
⚡ 有趣的事实:音乐、跑步、电影、游戏

目录

前言

Unix/Linux 下可用的 I/O 模型有以下五种:

  1. 阻塞式 I/O
  2. 非阻塞式 I/O
  3. I/O 复用(select、poll)
  4. 信号驱动式 I/O(SIGIO)
  5. 异步 I/O

在 Linux 中操作内核时,所有的无非三种操作,分别是输入、输出、报错输出

0-输入
1-输出
2-报错输出

一个输入操作通常包括两个不同的阶段:

  • 等待数据准备好
  • 从内核向进程复制数据

对于一个套接字(Socket)的输入操作,第一步通常涉及等待数据从网络中;当所等待分组到达时,它被复制到内核中的某个缓冲区,第二步就是把数据从内核缓冲区复制到应用进程缓冲区

Epoll 函数

在 Epoll 多路复用模型中,最主要的是涉及到了三个系统函数指令,分别是:epoll_create、epoll_ctl、epoll_wait

EPOLL_CREATE

借助:man 2 epoll_create 帮助文档来学习该函数

通过 epoll_create、epoll_create1 函数,打开 epoll 文件描述符

int epoll_create(int size);
int epoll_create1(int flags);

epoll_create 返回指向新的 Epoll 实例的文件描述符,该文件描述符用于对 epoll 接口的所有后续调用,当不再需要时,将调用 close 函数关闭 epoll_create 返回的文件描述符,当引用 epoll 实例的所有文件描述符都关闭时,内核将销毁该实例并释放相关资源以供资源重用

epoll_create1:若 flags 为 0,除了删除过时的 size 参数之外,epoll_create1 与 epoll_create 是相同的

如果指向新的 epoll 实例描述符成功的话,这些系统调用函数将会返回一个非负数的文件描述符,若出现错误,将返回 -1,并设置 errno 来指示错误.

EPOLL_CTL

借助:man 2 epoll_ctl 帮助文档来学习该函数

通过 epoll_ctl 来承担 epoll 描述符的控制接口

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

这个系统调用对文件描述符 epfd 引用的 epoll 实例执行控制操作,它请求对目前文件描述符 fd 执行 op 操作

op 参数可选值如下:

  1. EPOLL_CTL_ADD:在文件描述符 epfd 引用的 epoll 实例上注册目标文件描述符 fd,并关联事件,内部文件链接到 fd
  2. EPOLL_CTL_MOD:更改与目标文件描符 fd 关联的事件
  3. EPOLL_CTL_DEL:从 epfd 引用的 epoll 实例中删除或注销目标文件描述符 fd,该事件被忽略,并且可以为空

EPOLL_WAIT

借助:man 2 epoll_wait 帮助文档来学习该函数

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

epoll_wait 系统调用等待文件描述符 epfd 引用的 epoll 实例,事件指向的内存区域将包含调用者可用的事件,epoll_wait 最多返回 maxevents 个事件,其参数必须大于 0

timeout 函数指定 epoll_wait 将阻塞的最小毫秒数,若指定 timeout 为 -1 会导致 epoll_wait 无限期阻塞,而指定 timeout 为 0 会导致 epoll_wait 立即返回,即使没有事件可用

在 epoll_wait 调用时,在给定的 timeout 时间内,当在监控的所有 fd 中有事件发生时,就返回用户态的进程

epoll_event 数据结构

typedef union epoll_data {
    void    *ptr;
    int      fd;
    uint32_t u32;
    uint64_t u64;
} epoll_data_t;

struct epoll_event {
    uint32_t     events;    /* Epoll events */
    epoll_data_t data;      /* User data variable */
};

作为 epoll 中重要返回的数据结构,每个返回结构的数据将包含用户使用:epoll_ctl「EPOLL_CTL_ADD、EPOLL_CTL_MOD」设置的相同数据,而 events 成员将包含返回的事件位字段

边沿触发、水平触发

Epoll 提供两种事件接口:边沿触发(ET:edge-triggered)、水平触发(LT:level-triggered)

边沿触发

  • socket 接收缓冲区状态变化时触发读事件,即空的接收缓冲区刚接收到数据时触发读事件
  • socket 发送缓冲区状态变化时触发写事件,即满的缓冲区刚空出空间时触发读事件

水平触发

  • socket 接收缓冲区不为空时,有数据可读,读事件一直触发
  • socket 发送缓冲区不满,可以继续写入数据,写事件一直触发

使用 EPOLLET 标志的应用程序应该使用非阻塞文件描述符,以避免在处理多个文件描述符的任务中出现阻塞读写。建议使用 epoll 作为边缘触发(EPOLLET)接口的方法如下:

  1. 使用非阻塞文件描述符
  2. 在 read 或 write 之后等待事件返回 EAGAIN

例如,两条线分别有数据 ABC、DEF,水平触发的处理顺序:ADBECF,边缘触发的处理顺序:ABCDEF

Nginx、Redis 都使用了 Epoll 多路复用模型

Nginx 使用的是边缘触发 ET、Redis 使用的是水平触发 LT

再举例而言说明两者的区别:你有急事打电话找人,如果对方一直不接,那你只有一直打,直到他接电话为止,这就是 LT 模式;如果不急,电话打过去对方不接,那就等有空再打,这就是 ET 模式

小结

从调用方式可以看出 epoll 对比 select/poll 优越之处,因为 每次调用时都要传递你所要监控的所有 socket 给 select/poll 进行系统调用,这也就意味着需要将用户态的 socket 列表 copy 到内核态,若以万计数的 socket 会导致每次都要 copy 几十或几百 KB 的内存到达内核态,非常的低效,而当我们调用 epoll_wait 时就相当于以往调用 select/poll,但这此时却不用传递 socketfd 给到内核,因为内核通过 epoll_ctl 函数已经拿到了所要监控的 socketfd 列表

实际在你调用 epoll_create 后,内核就已经在开始准备帮你存储要监控的 socket 了,每次调用 epoll_ctl 只是在往内核的数据结构 > 红黑树,塞入新的 socketfd 罢了

Epoll 内核源码

#define MAX_EVENTS 10
struct epoll_event ev, events[MAX_EVENTS];
int listen_sock, conn_sock, nfds, epollfd;

/* Set up listening socket, 'listen_sock' (socket(),
   bind(), listen()) */

epollfd = epoll_create(10);
if (epollfd == -1) {
    perror("epoll_create");
    exit(EXIT_FAILURE);
}

ev.events = EPOLLIN;
ev.data.fd = listen_sock;
if (epoll_ctl(epollfd, EPOLL_CTL_ADD, listen_sock, &ev) == -1) {
    perror("epoll_ctl: listen_sock");
    exit(EXIT_FAILURE);
}

for (;;) {
    nfds = epoll_wait(epollfd, events, MAX_EVENTS, -1);
    if (nfds == -1) {
        perror("epoll_pwait");
        exit(EXIT_FAILURE);
    }

    for (n = 0; n < nfds; ++n) {
        if (events[n].data.fd == listen_sock) {
            conn_sock = accept(listen_sock,
                            (struct sockaddr *) &local, &addrlen);
            if (conn_sock == -1) {
                perror("accept");
                exit(EXIT_FAILURE);
            }
            setnonblocking(conn_sock);
            ev.events = EPOLLIN | EPOLLET;
            ev.data.fd = conn_sock;
            if (epoll_ctl(epollfd, EPOLL_CTL_ADD, conn_sock,
                        &ev) == -1) {
                perror("epoll_ctl: conn_sock");
                exit(EXIT_FAILURE);
            }
        } else {
            do_use_fd(events[n].data.fd);
        }
	}
}

socket()、bind()、listen() 是所有的 I/O 模型都必须要经过的操作

  • epollfd=epoll_create(10):创建一个 epoll 文件描述符 > epfd,用于执行后续所有的 epoll 操作
  • 若 epoll_create 函数返回 -1,代表操作失败,失败的原因可能是内核中文件描述符的大小超出了限制
  • 通过 epoll_ctl 函数将新获取的 socket 套接字放入到 epoll 红黑树中,在内核会单独为 epoll 开辟一些数据结构,存放这些 socket 信息
  • EPOLL_CTL_ADD 代表新增 op 操作,成功返回 0,失败则返回 -1
  • 第一个死循环进行 wait 阻塞等待,主要是调用 epoll_wait 函数,类似 select/poll 的操作,从红黑树中复制出来的链表有状态的文件描述符,将拿到的结果存放在 events 数组中

epoll_wait(epollfd, events, MAX_EVENTS, -1):第四个参数的 -1 代表不超时

  • 当拿到有结果的 events 数组以后,对这些有状态的文件描述符进行遍历,若当前文件描述符等于传入的文件描述符,那么则对当前描述符进行 accept 函数调用,将套接字对应的 IP、Port 进行绑定,成功则返回文件描述符,否则返回 -1

此时,代表有新的客户端连接了,需要进行 accept 监听,生成一个新的 socketfd,并且设置为非阻塞运行的方式,调用 epoll_ctl 将新的 socketfd 放入到红黑树中

  • 若当前文件描述符不等于传入的文件描述符,那么就使用已被监听的文件描述符中的数据

通过命令:cat /proc/sys/fs/epoll/max_user_watches,可以查看系统上所有 epoll 实例注册的文件描述符总数的最大限制

深入理解网络 I/O 多路复用:Epoll,网络 I/O,网络,Epoll

图解分析

其实 Epoll 的模型大致上和 select/poll 模型大致上是一样的,只不过它们额外做的处理工作不一样而已,下面具体来介绍

epoll VS select/poll 工作原理

深入理解网络 I/O 多路复用:Epoll,网络 I/O,网络,Epoll

如上图,所有的 I/O 模型,都会经过三个函数的调用:socket -> bind -> listen,然后 accept 等待客户端建立连接再分配新的 fd 文件描述符!

经历过三个函数调用以后,epoll、select/poll 做以下对比:

  1. 经过通用的函数调用以后,在 Epoll 中会调用 epoll_create 函数生成 fd7,再调用 epoll_ctl 将应用程序与客户端之间新创建的文件描述符放入到内核中维护的红黑树数据结构中,当有客户端有数据 Send-Queue 发送到网卡,然后会到达对应文件描述符 fd4「socket 函数生成的文件描述符」Buffer 缓冲区中,在此时,epoll 对比 select/poll 多做了一下延伸处理工作:将红黑树里有状态的文件描述符 fds 拷贝到链表中,随即在调用 epoll_wait 函数就可以从链表中取出所有有状态(R/W)的 fds
  2. 经过通用的函数调用以后,在 select/poll 中会通过 socket 生成的文件描述符 fd 到内核循环遍历全量的文件描述符以后,返回哪些有状态(R/W)的 fds,当我们在应用程序什么时候调用 select 方法就会触发一次全量的 fds 遍历
  3. 无论是 epoll 还是 select/poll,即使不调用内核,内核也会随着中断的处理机制完成所有 fd 文件描述符的状态设置,而 Epoll 就是在中断处理时多做了一件事情:将红黑树里有状态的 fd 拷贝到了链表中,当应用程序要取来进行处理时,直接取链表里面的 fds 即可(O(1)),而不需要像 select/poll 那样去循环遍历(O(n)

epoll VS select/poll 日志追踪

可以通过 strace 命令来追踪 epoll、select/poll 内核底层的源码是如何处理的,Java 代码与上一篇讲解的 select/poll 多路复用是一致的。

select/poll

运行命令:

strace -ff -o poll java -Djava.nio.channels.spi.SelectorProvider=sun.nio.ch.PollSelectorProvider SelectMultiplexingSocketThread

JVM native 分配了数组来保存 fd 信息,strace 源码追踪:

socket(AF_INET6, SOCK_STREAM, IPPROTO_IP) = 4
# 如 Java 代码:server.configureBlocking(false)
fcntl(4, F_SETFL, O_RDWR|O_NONBLOCK)    = 0

bind(4, {sa_family=AF_INET6, sin6_port=htons(8090), inet_pton(AF_INET6, "::", &sin6_addr), ...) = 0

listen(4, 50)
# 返回 = 1,代表一个 fd 有事件到来
# 返回 = -1,代表非阻塞情况下,没有事件
# 返回 = 0,代表调用超时且没有事件返回
ppoll([{fd=5, events=POLLIN}, {fd=4, events=POLLIN}], 2, NULL, NULL, 0) = 1 ([{fd=4, revents=POLLIN}])
# 新的客户端连接进来,分配的是 fd7
accept(4, {sa_family=AF_INET6, sin6_port=htons(60292), inet_pton(AF_INET6, "::1", ...) = 7
fcntl(7, F_SETFL, O_RDWR|O_NONBLOCK)    = 0

epoll

strace -ff -o epoll java -Djava.nio.channels.spi.SelectorProvider=sun.nio.ch.EPollSelectorProvider SelectMultiplexingSocketThread

运行命令:

socket(AF_UNIX, SOCK_STREAM, 0)         = 4
fcntl(4, F_SETFL, O_RDWR|O_NONBLOCK)    = 0
bind(4, {sa_family=AF_INET6, sin6_port=htons(8090), inet_pton(AF_INET6, "::",...) = 0
listen(4, 50) 
# 创建了 epfd 7
epoll_create1(0)                        = 7
# 将 socket 返回的文件描述符放入红黑树中
epoll_ctl(7, EPOLL_CTL_ADD, 4, {EPOLLIN, {u32=4, u64=545460846596}}) = 0
# 未设置超时时间会一直阻塞,设置了超时时间,在时间内无事件会返回 0
epoll_pwait(7,
# 新的客户端连接进来,分配 fd8
accept(4, {sa_family=AF_INET6, sin6_port=htons(60294), inet_pton(AF_INET6, "::1", &sin6_addr), ...) = 8
fcntl(8, F_SETFL, O_RDWR|O_NONBLOCK)    = 0
# 将新客户端 fd8 天假到红黑树中
epoll_ctl(7, EPOLL_CTL_ADD, 8, {EPOLLIN, {u32=8, u64=545460846600}}) = 0
# 继续循环 epoll_wait
epoll_pwait(7,

在 Java NIO 包下 Selector 通过一套代码在底层实现了 select/poll、epoll 两种 I/O 模型,对应的实现类分别是:sun.nio.ch.PollSelectorProvider、sun.nio.ch.EPollSelectorProvider

Epoll 优势之处

Epoll 高效在于:当我们调用 epoll_ctl 往内核塞入百万个 socket 时,epoll_wait 仍然可以飞快的返回,并会有效的将有发生事件的 socket 给到应用程序;这主要是在调用 epoll_create 时,内核除了在文件系统里建了 epfd,还在内核中建立了一个红黑树结构用于存储以后 epoll_ctl 传来的 socket 以外,还会再建立一个链表,用于存储哪些准备就绪的事件,当 epoll_wait 调用时,只需要仅仅观察这个链表有没有数据即可,有数据就返回,无数据就 sleep 等待 timeout 时间到,所以,epoll_wait 非常快.

每次都是 O(1) 的操作,不会在内核中发生循环遍历寻找的动作,以及也会减少用户态、内核态之间的大额数据交互,减少了资源的浪费及无效时间的行为.

总结

该篇博文主要介绍的就是比较重要比较核心的多路复用模型 Epoll,先简略说明 Epoll 重要的三大函数:epoll_create、epoll_ctl、epoll_wait,在其中说到了 Epoll 事件接口:边沿触发(ET:edge-triggered)、水平触发(LT:level-triggered),提及到了 Epoll 内核中关键的源码部分,使用三大函数巧妙结合起来实现 epoll 高效的多路复用,在底层采用红黑树结构存储所有的 socket fd 信息,采用链表结构存储所有有事件状态的 socket fd 信息,最后图解+日志追踪分析了 Epoll 与 select/poll 之间的区别以及介绍 Epoll 优势之处,希望您能够喜欢,感谢三连支持!

参考文献:

  1. 《UNIX网络编程 卷1:套接字联网API(第3版)》— [美] W. Richard Stevens Bill Fenner Andrew M. Rudoff
  2. Epoll原理 — 千寻
  3. Epoll — dreamgoing

学习帮助文档:

  • man pages:yum install man
  • pthread man pages:yum -y install man-pages

🌟🌟🌟愿你我都能够在寒冬中相互取暖,互相成长,只有不断积累、沉淀自己,后面有机会自然能破冰而行!

博文放在 网络 I/O 专栏里,欢迎订阅,会持续更新!

如果觉得博文不错,关注我 vnjohn,后续会有更多实战、源码、架构干货分享!

推荐专栏:Spring、MySQL,订阅一波不再迷路

大家的「关注❤️ + 点赞👍 + 收藏⭐」就是我创作的最大动力!谢谢大家的支持,我们下文见!文章来源地址https://www.toymoban.com/news/detail-753352.html

到了这里,关于深入理解网络 I/O 多路复用:Epoll的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Linux多路IO复用:epoll

            epoll是为克服select、poll每次监听都需要在用户、内核空间反复拷贝,以及需要用户程序自己遍历发现有变化的文件描述符的缺点的多路IO复用技术。 epoll原理 创建内核空间的红黑树; 将需要监听的文件描述符上树; 内核监听红黑树上文件描述符的变化; 返回有变化

    2024年02月04日
    浏览(48)
  • epoll多路复用_并发服务器

    应用程序: 驱动程序:

    2024年02月15日
    浏览(58)
  • epoll() 多路复用 和 两种工作模式

    epoll 是 Linux 内核中的一个 事件驱动I/O机制 ,用于处理多个文件描述符上的事件。它是一个高效且强大的I/O多路复用工具,可以用于处理大量文件描述符的I/O操作。 epoll 的主要优点是它只占用较少的资源,并且比传统的 select 和 poll 更易于使用。 epoll 的工作原理 是通过一个

    2024年02月11日
    浏览(37)
  • 驱动开发,IO多路复用实现过程,epoll方式

    被称为当前时代最好用的io多路复用方式; 核心操作:一棵树(红黑树)、一张表(内核链表)以及三个接口;  思想:(fd代表文件描述符)         epoll要把检测的事件fd挂载到内核空间红黑树上,遍历红黑树,调用每个fd对应的操作方法,找到发生事件的fd,如果没有发

    2024年02月07日
    浏览(54)
  • IO多路复用之select/poll/epoll

    掌握select编程模型,能够实现select版本的TCP服务器. 掌握poll编程模型,能够实现poll版本的TCP服务器. 掌握epoll的编程模型,能够实现epoll版本的TCP服务器. epoll的LT模式和ET模式. 理解select和epoll的优缺点对比. 提示:以下是本篇文章正文内容,下面案例可供参考 多路转接天然的是让我

    2023年04月09日
    浏览(76)
  • Linux多路IO复用技术——epoll详解与一对多服务器实现

    本文详细介绍了Linux中epoll模型的优化原理和使用方法,以及如何利用epoll模型实现简易的一对多服务器。通过对epoll模型的优化和相关接口的解释,帮助读者理解epoll模型的工作原理和优缺点,同时附带代码实现和图解说明。

    2024年02月05日
    浏览(44)
  • 02-Linux-IO多路复用之select、poll和epoll详解

    前言: 在linux系统中,实际上所有的 I/O 设备都被抽象为了文件这个概念,一切皆文件,磁盘、网络数据、终端,甚至进程间通信工具管道 pipe 等都被当做文件对待。 在了解多路复用 select、poll、epoll 实现之前,我们先简单回忆复习以下两个概念: 一、什么是多路复用: 多路

    2024年02月10日
    浏览(57)
  • 【TCP服务器的演变过程】使用IO多路复用器epoll实现TCP服务器

    手把手教你从0开始编写TCP服务器程序,体验开局一块砖,大厦全靠垒。 为了避免篇幅过长使读者感到乏味,对【TCP服务器的开发】进行分阶段实现,一步步进行优化升级。 本节,在上一章节的基础上,将IO多路复用机制select改为更高效的IO多路复用机制epoll,使用epoll管理每

    2024年01月17日
    浏览(70)
  • 网络通信基础 - 多路复用技术(频分多路复用、时分多路复用、波分多路复用)

    多路复用技术:把多个低速信道组合成一个高速信道的技术 这种技术要用到两个设备,统称为 多路器(MUX) 多路复用器(Multiplexer) :在发送端根据某种约定的规则把多个低带宽的信号复合成一个高带宽的信号 多路分配器(Demultiplexer) :在接收端根据同一规则把高带宽信

    2023年04月23日
    浏览(45)
  • 【网络】多路转接——poll | epoll

    🐱作者:一只大喵咪1201 🐱专栏:《网络》 🔥格言: 你只管努力,剩下的交给时间! 书接上文五种IO模型 | select。 poll 也是一种多路转接的方案,它专门用来解决 select 的两个问题: 等待fd有上限的问题。 每次调用都需要重新设置 fd_set 的问题。 如上图所示便是 poll 系统调

    2024年02月10日
    浏览(41)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包