epoll(2) 使用及源码分析的引子

本文代码取自内核版本 4.17

epoll(2) - I/O 事件通知设施。

epoll 是内核在2.6版本后实现的,是对 select(2)/poll(2) 更高效的改进,同时它自身也是一种文件,不恰当的比方可以看作 eventfd + poll。

多路复用也是一直在改进的,经历的几个阶段

  1. select(2) - 只能关注 1024 个文件描述符,并且范围固定在 0 - 1023,每次函数调用都需要把所有关注的数据复制进内核空间,再对所有的描述符集合进行遍历判断。
  2. poll(2) - 改进 select(2) 前面两个缺点,可以自定义关注的描述符,数量也不受限制(不超过系统的限制),每次调用同样需要复制所有的事件进内核空间,全部遍历。
  3. epoll(2) - 不需要每次调用时所有关注的文件描述符进行内核-用户空间的复制,而是直接将所有的文件描述符和事件常驻内核空间,同时也不需要每次遍历所有文件描述符。

提供的系统调用

#include <sys/epoll.h>

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 */
}; int epoll_create(int size);
int epoll_create1(int flags); 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);

epoll_create() - 用来创建一个 epoll 实例,返回一个新的文件描述符。第一个参数 @size 自 2.6.8 开始无意义,但必须大于 0。

epoll_create1() - 参数 @flags 为 0 则等效于 epoll_create(),flags 可以为 EPOLL_CLOEXEC, 在exec新程序时关闭文件描述符。

epoll_ctl() - epoll 的控制接口,用户调用该系统调用来控制监听的文件描述符。参数 @epfd 为 epoll_create() 返回的新文件描述符,参数 @op 为 epoll_ctl() 提供的控制操作:

  • EPOLL_CTL_ADD, 向 epoll 中注册一个新的文件描述符;
  • EPOLL_CTL_MOD, 修改关联文件描述符中的事件;
  • EPOLL_CTL_DEL, 移除 epoll 中的描述符,且无视 @event 参数;

    参数 @fd 为需要控制的文件描述符,参数 @event 为相关联的 struct epoll_event 结构。

epoll_wait() - 等待epoll中监听文件描述符就绪的 I/O 事件。参数 @epfd 为epoll实例对应的文件描述符,由 epoll_create() 创建,、

参数 @events 为就绪的事件集合的地址,参数 @maxevents 为需要就绪事件集合的大小,必须大于 0,参数 @timeout 为超时时间,单位为 微秒。

水平触发模式和边缘触发模式

epoll 默认使用水平触发模式,边缘触发模式需要设置 events |= EPOLLET

边缘触发模式的特点是边缘触发模式只在关注的文件描述符发生改变时才产生就绪的事件,考虑高低电平的图片,边缘是有一个瞬间的概念,而水平则有一个持续的状态。

这就导致了,边缘触发有可能会丢失需要通知的事件。分析如下

现有 5 个步骤:

  1. 管道读端的文件描述符 rfd 被注册到 epoll 实例中。
  2. 管道写端写入了 2 kB 数据。
  3. 调用 epoll_wait(2) 返回了 rfd 作为就绪的文件描述符。
  4. 管道读端读取了 1 kB 数据。
  5. 调用 epoll_wait(2)。

如果文件描述符 rfd 使用 EPOLLET 边缘触发模式注册到 epoll 中,那么在执行上面的 5 的时候,尽管管道的读端缓冲区还有数据,epoll_wait(2) 还是可能会挂起,

同时写端可能会基于其已发送的数据期望响应。产生这个情况的原因是边缘触发模式只在关注的文件描述符发生改变时才产生就绪的事件。在上面的步骤中,2 写入的数据,

因此在 rdf 上生成一个事件,由于在 4 中的读取操作不会消耗整个缓冲区数据,故在 5 对 epoll_wait(2) 调用可能发生阻塞。

使用边缘触发模式的程序应该使用非阻塞文件描述符来避免阻塞读写造成处理多个文件描述符时产生的饥饿问题。

所以建议使用的边缘触发模式时遵从一下两点:

  1. 文件描述符为非阻塞方式,并且
  2. 只在 read(2) / write(2) 返回 EAGAIN 后进行等待。

在使用边缘触发模式时,在接收到多个数据块的时候会产生多个事件,因此用户可以选择指定 EPOLLONESHOT 标志,在 epoll_wait(2) 收到事件后禁用相关的文件描述符。

而设置 EPOLLONESHOT 标志后,需要用户手动调用 epoll_ctl(2) 重新设置文件描述符。

在示例代码中可以看到边缘触发和水平触发的区别

示例代码

把 eventfd 注册到 epoll 中,进行两个线程间的通信。使用 eventfd 的 EFD_SEMAPHORE 的标志来模拟 read(2) 读取部分数据。

程序初始值设置 1000,在水平模式下,会先用 1000 次 read(2),把计数器的值消耗为 0,之后 write(2) 写入 cnt,就调用 cnt 次 read(2),总之只要水位(count)不为0就可读。

而 epoll 设置 EPOLLET 后只有发生了 write(2) 操作 epoll_wait(2) 才能产生一个可读事件,而计数器则是逐渐增大。

// 代码取自上一篇文章的 eventfd 示例

#include <unistd.h>
#include <pthread.h>
#include <poll.h>
#include <sys/epoll.h>
#include <sys/eventfd.h>
#include <stdio.h> int efd; void *run_eventfd_write(void *arg) {
uint64_t count = 1;
while (1) {
printf("write count: %zu\n", count);
write(efd, &count, sizeof(count));
count++;
sleep(2); // 将睡眠时间调成大于 timeout 时间,
}
} int main() {
unsigned int initval = 1000;
int flags = 0;
int timeout = 1000;
flags |= EFD_SEMAPHORE; // 使计数器器的值每一次减 1 而不清空,保持计数器的值不直接置为 0
efd = eventfd(initval, flags); int epfd = epoll_create(32);
struct epoll_event epfds;
struct epoll_event ev;
ev.data.fd = efd;
ev.events = EPOLLIN;
ev.events |= EPOLLET; // 对比注释这个行代码的打印输出
epoll_ctl(epfd, EPOLL_CTL_ADD, efd, &ev); pthread_t pid;
pthread_create(&pid, NULL, run_eventfd_write, NULL); while (1) {
int ret = epoll_wait(epfd, &epfds, 1, timeout);
if (ret > 0) {
uint64_t count;
read(efd, &count, sizeof(count));
printf("read count: %zu\n", count);
} else if (ret == 0) {
printf("not avaiable data\n");
}
}
}

特殊功能的标志

EPOLLONESHOT

单次命中,内核 2.6.2 引入,当事件就绪被 epoll_wait(2) 返回时,这个事件就不再被关注了。

EPOLLWAKEUP

内核 3.5 引入,如果 EPOLLONESHOTEPOLLET 标志被清除后,并且进程拥有CAP_BLOCK_SUSPEND(阻止系统挂起的特性)权限,这个标志能够保证事件在挂起或者处理的时候,系统不会挂起或休眠。

EPOLLEXCLUSIVE

排他的唤醒,内核 4.5 引入,解决惊群的问题,下一篇文章会分析到。

杂记

本文不准备把源码分析放在这里,由于是文件的原因 epoll(2) 对比 poll(2) 和 select(2) 来说要复杂很多,这里抛出几个点来引出源码分析的重点:

  1. epoll(2) 得到就绪事件的复杂度为何是 \(O(1)\)
  2. epoll(2) 和普通的文件相比的区别在哪里,比如和 eventfd(2) 比较
  3. epoll(2) 相对 poll(2)/select(2) 多提供了 EPOLLET 的触发模式,现象在上面可以看到区别,实现是如何做到的。
  4. epoll(2) 相互关注时,有就绪事件到来会产生相互唤醒的问题,为何会出现这样的问题
  5. 对于问题 4,内核是如何解决这种相互唤醒的问题。

刚开始把源码分析放了出来,但是发现写的过于混乱没有重点,代码贴的太多,所以想专门写一文来着分析上面的问题。

epoll(2) 使用及源码分析的引子的更多相关文章

  1. Epoll详解及源码分析【转】

    转自:http://blog.csdn.net/chen19870707/article/details/42525887 版权声明:本文为博主原创文章,未经博主允许不得转载.   目录(?)[-] ...

  2. [ipsec][strongswan]strongswan源码分析--(零)引子

    目录 strongswan sa 资料 编译 启动 进程信息 结构 架构图与插件 配置运行 传统配置方法 新的配置方法 其他配置方法 详细的配置文档 配置示例 用法 加密库 libgmp libcry ...

  3. epoll源码分析

    epoll源码分析 最近在使用libev过程中遇到一个场景:一个fd从一个ev_loop迁移到另一个ev_loop,会出现这个fd同时存在两个epoll的瞬间.不禁要问了,一个fd同时被两个epoll ...

  4. epoll(2) 源码分析

    epoll(2) 源码分析 文本内核代码取自 5.0.18 版本,和上一篇文章中的版本不同是因为另一个电脑出了问题,但是总体差异不大. 引子留下的问题 关键数据结构 提供的系统调用 就绪事件相关逻辑 ...

  5. epoll源码分析(基于linux-5.1.4)

    API epoll提供给用户进程的接口有如下四个,本文基于linux-5.1.4源码详细分析每个API具体做了啥工作,通过UML时序图理清内核内部的函数调用关系. int epoll_create1( ...

  6. 【转】libevent源码分析

    libevent源码分析 转自:http://www.cnblogs.com/hustcat/archive/2010/08/31/1814022.html 这两天没事,看了一下Memcached和l ...

  7. TeamTalk源码分析之login_server

    login_server是TeamTalk的登录服务器,负责分配一个负载较小的MsgServer给客户端使用,按照新版TeamTalk完整部署教程来配置的话,login_server的服务端口就是80 ...

  8. Redis学习——ae事件处理源码分析

    0. 前言 Redis在封装事件的处理采用了Reactor模式,添加了定时事件的处理.Redis处理事件是单进程单线程的,而经典Reator模式对事件是串行处理的.即如果有一个事件阻塞过久的话会导致整 ...

  9. Nginx学习笔记(四) 源码分析&socket/UDP/shmem

    源码分析 在茫茫的源码中,看到了几个好像挺熟悉的名字(socket/UDP/shmem).那就来看看这个文件吧!从简单的开始~~~ src/os/unix/Ngx_socket.h&Ngx_s ...

随机推荐

  1. 在Typora中使用Markdown

    在Typora中使用Markdown 记:准备正式开始在博客园写博客,故学习Markdown语法,为写博客做好准备.以前也在CSDN写过一些,但广告太多,个人更喜欢博客园的简洁. 1. 标题 (#)标 ...

  2. HashMap浅析

    一.概述 HashMap,基于哈希结构的Map接口的一个实现,无序,允许null键值对,线程不安全的.可以使用集合工具类Collections中的synchronizedMap方法,去创建一个线程安全 ...

  3. 搭建数据库galera集群

    galera集群 galera简介 galera集群又叫多主集群,用于数据库的同步,保证数据安全 最少3台,最好是奇数台数,当一台机器宕掉时,因为仲裁机制,这台机器就会被踢出集群. 通过wsrep协议 ...

  4. 常用的HDFS操作

    首先,把Hadoop命令加入到PATH环境变量中,直接通过start-dfs.sh开启Hadoop,也可以直接通过hdfs命令访问HDFS中的内容,方便平时的操作. 配置PATH环境变量 vim  ~ ...

  5. 读《深入理解Elasticsearch》点滴-对象类型、嵌套文档、父子关系

    一.对象类型 1.mapping定义文件 "title":{ "type":"text" }, "edition":{ ...

  6. CentOS 安装lsof命令

    1.在控制台上输入:# yum install lsof,安装过程中按y进行确认 2.使用lsof -i :port 可以产看端口的进程信息

  7. 夯实Java基础系列15:Java注解简介和最佳实践

    Java注解简介 注解如同标签 Java 注解概述 什么是注解? 注解的用处 注解的原理 元注解 JDK里的注解 注解处理器实战 不同类型的注解 类注解 方法注解 参数注解 变量注解 Java注解相关 ...

  8. TensorFlow2.0(四):填充与复制

    .caret, .dropup > .btn > .caret { border-top-color: #000 !important; } .label { border: 1px so ...

  9. ES(Elastic Search)update操作设置无 docment时进行insert

    最近使用一套数据加工中间工具,查看es操作中的update操作.其中方法命名为updateOrInsert.但是没发现代码中有ES的insert方法调用.于是仔细分析了代码逻辑. 经过一路追溯,直至E ...

  10. 继续学习freertos消息队列

    写在前面:杰杰这个月很忙~所以并没有时间更新,现在健身房闭馆装修,晚上有空就更新一下!其实在公众号没更新的这段日子,每天都有兄弟在来关注我的公众号,这让我受宠若惊,在这里谢谢大家的支持啦!!谢谢^ 在 ...