Linux中I/O复用机制epoll

1. 为什么会出现 epoll

在早期的网络编程中,select 是一个非常常用的 I/O 复用机制,用于在多个文件描述符(如套接字)上进行 I/O 操作的检测。select 会将多个文件描述符传入,轮询检查它们的状态,看哪些是可以读取、写入或者异常的。然而,select 存在以下几个问题,特别是在需要处理大量文件描述符时,epoll 的出现就是为了解决这些问题:

Linux中的 I/O 复用机制 select-CSDN博客

select 存在的问题:
  1. 文件描述符限制select 的一个关键限制是它最大支持的文件描述符数量,通常是 1024(这个数量在 Linux 中默认定义为 FD_SETSIZE)。如果需要监控更多的文件描述符,select 就不适用了。

  2. 每次调用 select 都要传入整个文件描述符集合 :在每次调用 select 时,系统需要扫描整个文件描述符集合,检查每个文件描述符的状态,即使其中很多文件描述符并未发生变化。这种轮询会带来较高的开销,尤其是在大量文件描述符的情况下。

  3. 性能低下select 的机制是阻塞 式的,当有大量文件描述符时,性能会下降,特别是当许多文件描述符没有变化时,select 还是需要遍历所有文件描述符集合。

select示意图

为了应对这些问题,Linux 引入了 epoll,它是一种更加高效的 I/O 复用机制,专门用于处理大量的文件描述符。

2. epoll 的作用

epoll 是一种 事件驱动的 I/O 复用 机制,主要解决了传统的 selectpoll 的性能瓶颈。它的出现,主要是为了更高效地处理成千上万的连接。epoll 的优势包括:

  • 高效的事件通知机制 :与 selectpoll 不同,epoll 采用基于内核的事件通知机制,只有当文件描述符的状态发生变化时,才会通知应用程序。这避免了不必要的遍历和检查,从而提高了效率。

  • 支持大规模文件描述符epoll 可以支持数万个文件描述符,不再受 select 文件描述符限制(1024个)。

  • 单次注册,持续监控:通过一次注册文件描述符,就可以持续地对其进行监控,避免每次调用时都要传递整个文件描述符集合。

epoll示意图

3. epoll 的工作原理

epoll 的工作原理基于 事件通知,它的基本流程包括以下几个步骤:

  1. 创建 epoll 实例 : 使用 epoll_create() 创建一个 epoll 实例,返回一个 epoll 文件描述符。

  2. 注册文件描述符 : 使用 epoll_ctl() 向 epoll 实例注册需要监控的文件描述符,指定这些文件描述符上的事件(如读事件、写事件、异常事件)。

  3. 等待事件发生 : 使用 epoll_wait() 阻塞或非阻塞地等待文件描述符上的事件发生。当某个文件描述符的事件发生时,epoll_wait() 返回。

  4. 事件处理: 事件发生后,应用程序处理相关文件描述符的数据传输或者其他操作。

epoll 的 API 解释
  1. epoll_create() 用于创建一个 epoll 实例。

    复制代码
    int epoll_create(int size);
    • size 参数用于指定内核为该 epoll 实例分配多少空间,这个值在现代 Linux 中并没有实际作用。

    • 返回值:成功时返回一个 epoll 实例的文件描述符,失败时返回 -1。

  2. epoll_ctl() 用于添加、修改或删除文件描述符的监控事件。

    复制代码
    int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
    • epfd:epoll 实例的文件描述符。

    • op:操作类型(EPOLL_CTL_ADDEPOLL_CTL_MODEPOLL_CTL_DEL)。

    • fd:要监控的文件描述符。

    • event:指定要监控的事件类型(例如 EPOLLINEPOLLOUT 等)。

  3. epoll_wait() 用于等待并获取已经就绪的文件描述符事件。

    复制代码
    int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
    • epfd:epoll 实例的文件描述符。

    • events:保存就绪事件的数组。

    • maxevents:最多返回的事件数。

    • timeout:等待时间,单位为毫秒,0 为不阻塞,-1 为无限等待。

4. epollselect 的区别
特性 select epoll
文件描述符数目 有最大限制(一般为1024) 无限制,支持成千上万的文件描述符
性能 文件描述符多时性能差 只有状态变化的文件描述符会被通知,性能优越
事件通知方式 每次调用都需要遍历所有文件描述符 基于事件的通知机制,只有状态变化才通知
内存开销 需要传递整个文件描述符集合 只需要传递事件队列,开销较小
5. 示例代码

下面是一个简单的使用 epoll 的服务器端示例,它可以同时处理多个客户端连接:

cpp 复制代码
// 服务器端 (`epoll` 示例)
​
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/epoll.h>
#include <arpa/inet.h>
#include <sys/socket.h>
​
#define BUF_SIZE 1024
#define MAX_EVENTS 10
​
void error_handling(char *message);
​
int main(int argc, char *argv[]) {
    int serv_sock, clnt_sock;
    struct sockaddr_in serv_adr, clnt_adr;
    socklen_t clnt_adr_sz;
    char buf[BUF_SIZE];
    int str_len;
    struct epoll_event ev, events[MAX_EVENTS];
    int epfd, event_count;
​
    if (argc != 2) {
        printf("Usage : %s <port>\n", argv[0]);
        exit(1);
    }
    
    // 创建服务端套接字
    serv_sock = socket(PF_INET, SOCK_STREAM, 0);
    memset(&serv_adr, 0, sizeof(serv_adr));
    serv_adr.sin_family = AF_INET;
    serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
    serv_adr.sin_port = htons(atoi(argv[1]));
    
    // 绑定地址
    if (bind(serv_sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr)) == -1)
        error_handling("bind() error");
    
    // 开始监听
    if (listen(serv_sock, 5) == -1)
        error_handling("listen() error");
    
    // 创建 epoll 实例
    epfd = epoll_create1(0);
    if (epfd == -1)
        error_handling("epoll_create1() error");
    
    // 注册监听套接字到 epoll
    ev.events = EPOLLIN;
    ev.data.fd = serv_sock;
    if (epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, &ev) == -1)
        error_handling("epoll_ctl() error");
    
    while (1) {
        // 等待事件发生
        event_count = epoll_wait(epfd, events, MAX_EVENTS, -1);
        if (event_count == -1)
            error_handling("epoll_wait() error");
    
        // 处理就绪的事件
        for (int i = 0; i < event_count; i++) {
            if (events[i].data.fd == serv_sock) {
                // 新客户端连接
                clnt_adr_sz = sizeof(clnt_adr);
                clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_adr, &clnt_adr_sz);
                if (clnt_sock == -1)
                    error_handling("accept() error");
    
                // 将客户端套接字添加到 epoll 中
                ev.events = EPOLLIN;
                ev.data.fd = clnt_sock;
                if (epoll_ctl(epfd, EPOLL_CTL_ADD, clnt_sock, &ev) == -1)
                    error_handling("epoll_ctl() error");
                printf("New client connected: %d\n", clnt_sock);
            } else {
                // 处理客户端数据
                str_len = read(events[i].data.fd, buf, BUF_SIZE);
                if (str_len == 0) {
                    // 客户端关闭连接
                    epoll_ctl(epfd, EPOLL_CTL_DEL, events[i].data.fd, NULL);
                    close(events[i].data.fd);
                    printf("Client disconnected: %d\n", events[i].data.fd);
                } else {
                    // 回显数据
                    write(events[i].data.fd, buf, str_len);
                }
            }
        }
    }
    
    close(serv_sock);
    return 0;
​
}
​
void error_handling(char *message) {
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}
6. 总结
  • epoll 是一种比 select 更高效的 I/O 复用机制,特别适用于需要处理大量并发连接的服务器。

  • 它通过事件驱动的方式来提高性能,避免了每次调用时遍历所有文件描述符的开销。

  • epoll 具有高效的性能,并且不受文件描述符数量的限制,适合大规模的并发处理场景。

epoll 对比 select 的优势使得它成为高并发网络编程中的主流选择,尤其是在 Linux 系统中。

相关推荐
云盾安全防护2 小时前
医疗行业网络安全的综合防护策略
网络·安全·web安全
ZZH1120KQ3 小时前
Linux 进程和计划任务管理
linux
小猪写代码4 小时前
大白话解释蓝牙的RPC机制
网络·网络协议·rpc
想躺在地上晒成地瓜干5 小时前
树莓派超全系列教程文档--(57)如何设置 Apache web 服务器
服务器·apache·树莓派·raspberrypi·树莓派教程
Linux运维技术栈5 小时前
Vim 命令大全:从入门到精通
linux·编辑器·vim
云云3215 小时前
亚矩阵云手机针对AdMob广告平台怎么进行多账号的广告风控
大数据·网络·线性代数·游戏·智能手机·矩阵
c7_ln5 小时前
Linux基本指令(包含vim,用户,文件等方面)超详细
linux·操作系统·vim
晨曦backend6 小时前
Vim 撤销 / 重做 / 操作历史命令汇总
linux·编辑器·vim
网安INF6 小时前
CVE-2020-1938源码分析与漏洞复现(Tomcat 文件包含/读取)
java·网络·web安全·网络安全·tomcat·漏洞复现
晨曦backend6 小时前
Vim 插件管理:MiniBufExplorer 使用指南
linux·编辑器·vim