Linux编程-epoll多路IO复用

poll

epollLinux 专属的 IO 多路复用 ,用来代替 select / poll,高并发场景下性能远优于 select,是 Linux 高并发网络编程核心。

接口 时间复杂度 最大 fd 上限 内核实现 触发方式
select O(n) 1024(默认) 轮询扫描所有 fd 水平触发 LT
poll O(n) 无硬性上限 轮询扫描所有 fd 水平触发 LT
epoll O (1) 就绪回调 无硬性上限 回调 + 就绪链表 LT / ET 二选一

1. epoll_create

  • 成功:返回一个 epoll 实例的文件描述符 epfd

  • 失败:‑1,设置 errno

    #include <sys/epoll.h>

    int epoll_create(int size);

    Linux 2.6.8 之后,size 不再用来限定 epoll 能监听的 fd 最大数量
    但是size 必须传一个大于 0 的整数,比如 1

2. epoll_ctl

向内核 epoll 实例添加、修改、删除要监听的文件描述符。epoll 唯一的管理接口。

复制代码
#include <sys/epoll.h>

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

op 参数三个选项:

  • EPOLL_CTL_ADD:添加 fd 到 epoll 监听
  • EPOLL_CTL_MOD:修改 fd 监听事件
  • EPOLL_CTL_DEL:从 epoll 移除 fd

struct epoll_event

一般定义为结构体数组,数组的长度一般定义为,一次调用epoll_wait可以响应多少个就绪事件

  • epoll 里面一共监听了 5000 个客户端 fd(红黑树 5000 个节点)
  • 你定义:struct epoll_event events[256];,maxevents=256

含义:

某一瞬间最多一次返回 256 个就绪事件。 如果同一时刻有300 个 fd 同时就绪 : epoll_wait 先返回前 256 个;剩下 44 个就绪的还留在内核就绪链表,下一次调用 epoll_wait 再拿剩下的

复制代码
struct epoll_event {
    uint32_t events;      // 监听事件
    epoll_data_t data;    // 用户数据,一般存fd
};

events 常用宏:

  • EPOLLIN:可读事件
  • EPOLLOUT:可写事件
  • EPOLLERR:出错,自动监听,不用手动加
  • EPOLLHUP:挂起断开
  • EPOLLET:边缘触发 ET

3. epoll_wait

  1. epfd epoll 实例的文件描述符,来自 epoll_create1()

  2. events(传出参数) 结构体数组首地址,内核会把已经就绪的事件拷贝到这个数组里。

  3. maxevents events 数组最多能存放多少个事件,不能填 0

例:struct epoll_event evs[1024]; → maxevents = 1024

  1. timeout 超时时间,单位:毫秒
  • -1:永久阻塞,直到有事件到来
  • 0:非阻塞,立刻返回,没有就绪事件就返回 0
  • >0:等待指定毫秒,超时后返回 0运行代码

返回值

  • > 0:就绪 fd 的数量;返回多少,就遍历 events 0 \~ ret‑1

  • 0:超时,没有任何事件就绪

  • -1:出错,perror 查看原因

    #include <sys/epoll.h>

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

练习:使用epoll实现TCP服务器非阻塞接收后回传

复制代码
/**
 * @brief  将指定文件描述符添加到epoll的监听列表
 * @param  epfd  epoll实例的文件描述符
 * @param  fd    需要被监听的文件描述符
 * @return 成功返回0,失败返回‑1
 */
int epoll_add(int epfd,int fd)
{
	// 定义epoll事件结构体,存放待监听的事件信息
	struct epoll_event ev;
	// 设置监听事件类型:EPOLLIN → 可读事件
	ev.events = EPOLLIN;
	// 绑定要监听的文件描述符,事件就绪后可通过data.fd取出
	ev.data.fd = fd;

	// epoll_ctl:对epoll实例执行控制操作
	// EPOLL_CTL_ADD:添加fd到epoll监听树上
	if(epoll_ctl(epfd,EPOLL_CTL_ADD,fd,&ev) < 0)
	{
		perror("epoll ctl fail");
		return -1;
	}

	return 0;
}


// 创建epoll实例,参数2为历史遗留参数,大于0即可
int epfd = epoll_create(2);
// 判断epoll创建是否失败
if(epfd < 0)
{
    perror("epoll_create fail");
    return -1;
}

// 将标准输入fd=0添加到epoll监听树上
epoll_add(epfd,0);
// 将通信套接字connfd添加到epoll监听树上
epoll_add(epfd,connfd);

// 获取标准输入原来的文件状态标志
int flag = fcntl(0,F_GETFL);
// 设置标准输入为非阻塞模式
fcntl(0,F_SETFL,flag | O_NONBLOCK);

// 获取通信套接字原来的文件状态标志
flag = fcntl(connfd,F_GETFL);
// 设置connfd套接字为非阻塞模式
fcntl(connfd,F_SETFL,flag | O_NONBLOCK);


// 事件循环,持续等待epoll事件
while(1)
{
    // 存放就绪事件的数组,最多2个事件
    struct epoll_event rev[2];
    // 阻塞等待就绪事件,-1代表无限阻塞
    int ret = epoll_wait(epfd,rev,2,-1);
    // epoll_wait出错判断
    if(ret < 0)
    {
        perror("epoll_wait fail");
        return -1;
    }

    // 打印本次就绪的文件描述符数量
    printf("ret = %d\n",ret);

    // 接收数据缓冲区,初始清零
    char buf[1024] = {'\0'};
    int i = 0;
    // 遍历本次所有就绪的文件描述符
    for(i = 0;i<ret;i++)
    {
        // 如果就绪的是标准输入(键盘)
        if(rev[i].data.fd == 0)
        {
            // 从键盘读取一行输入
            fgets(buf,sizeof(buf),stdin);
            printf("buf = %s\n",buf);
        }
        // 如果就绪的是网络通信套接字
        else if(rev[i].data.fd == connfd)
        {
            // 从套接字读取客户端发来的数据
            int ret = read(connfd,buf,sizeof(buf));
            // 判断收到quit指令,退出程序
            if(strncmp(buf,"quit",4) == 0)
            {
                close(connfd);
                exit(0);
            }

            // 打印读到的字节数和接收的数据
            printf("ret = %d buf = %s \n",ret,buf);

            // 组装回显应答字符串
            char sbuf[1024];
            sprintf(sbuf,"server+%s\n",buf);
            // 将处理后的数据写回客户端
            write(connfd,sbuf,strlen(sbuf)+1);

        }
    }
}

LT 水平触发 & ET 边缘触发

  • LT (水平触发):只要数据还在缓冲区,就一直通知你
  • ET (边缘触发):状态一变,只通知一次;剩下的数据不再提醒

客户端给服务器发 2KB 数据,服务器一次只 read 了 1KB,缓冲区还剩 1KB。

LT 水平触发(默认,不加 EPOLLET)

  1. epoll_wait 返回可读事件
  2. read 读到 1KB,缓冲区还剩 1KB
  3. 下一轮 epoll_wait 立刻又会通知可读

只要缓冲区有数据,事件就一直 "处于就绪状态",反复唤醒你。

ET 边缘触发(加 EPOLLET)

  1. epoll_wait 返回可读事件
  2. read 读到 1KB,缓冲区还剩 1KB
  3. 不会再通知你了!剩下 1KB 永远留在缓冲区等下一次新数据到来才会再触发

只有从无数据 → 有数据这个跳变瞬间通知一次。

对比项 LT 水平触发 Level‑Triggered ET 边缘触发 Edge‑Triggered
触发条件 缓冲区有未读数据 状态发生变化 (无→有)
通知次数 只要有剩余数据,每次 epoll_wait 都通知 变化瞬间仅通知一次
fd 阻塞要求 阻塞 / 非阻塞都可以 必须非阻塞 O_NONBLOCK
读数据策略 一次读一点,剩下下次再处理 收到事件后循环读直到 read 返回 EAGAIN
性能开销 唤醒次数多 唤醒次数少,效率更高
难度 简单,不容易丢数据 容易踩坑,难度高
默认模式 epoll 默认开启 需要手动 ` EPOLLET

使用边缘触发,必须手动设置,添加fd监听的时候,设置为边缘触发模式EPOLLET,并且将文件描述符设置为非阻塞状态O_NONBLOCK

复制代码
int epoll_add(int epfd,int fd)
{
	struct epoll_event ev;
	ev.events = EPOLLIN | EPOLLET;
	ev.data.fd = fd;

	if(epoll_ctl(epfd,EPOLL_CTL_ADD,fd,&ev) < 0)
	{
		perror("epoll ctl fail");
		return -1;
	}

	return 0;
}


int set_nonblock(int fd)
{
	int flag = fcntl(fd,F_GETFL,0);
	fcntl(fd,F_SETFL,flag | O_NONBLOCK);

	return 0;
}

什么时候选 LT,什么时候选 ET

LT(水平触发)适用场景

  • 初学 epoll,快速开发
  • 简单服务器,单线程,不想写循环读写
  • 大部分工程里的简单网络程序

ET(边缘触发)适用场景

  • 高并发、上万连接,想要减少 epoll 唤醒次数
  • 高性能 Reactor 服务器,如 libevent、nginx (可选 ET)
相关推荐
wtblszn19 分钟前
城市二供泵站远程监控运维系统物联网方案
运维·物联网
kmomo..27 分钟前
Linux 进程学习笔记
linux·笔记·学习
白猫不黑29 分钟前
运维如何转安全(个人经验篇)
运维·学习·安全·web安全·网络安全·信息安全
Tanner_SL37 分钟前
Linux笔记之PATH, LD_LIBRARY_PATH和LIBRARY_PATH的区别
linux·运维·笔记
是稻香啊1 小时前
HarmonyOS7 国际化适配:i18n 让你的 App 走向全球
运维·服务器·国际化·arkts·arkui·harmonyos7
無法複制1 小时前
Windows10安装配置Docker Desktop教程
运维·docker·容器
AI智图坊1 小时前
宠物用品电商视觉内容生产的技术难点与自动化方案分析
大数据·运维·人工智能·ai作画·自动化·aigc
xiaoxiangsiyan1 小时前
网络智能化转型核心模块全解析
运维·服务器·网络·数据库·学习·架构·php