当前位置: 首页 > news >正文

epoll原理以及系统调用案例分析

一、epoll原理

  1. epoll 是 Linux 内核为处理大批量文件描述符,工作方式为水平触发(Level Triggered,LT)和边缘触发(Edge Triggered,ET),使用户空间程序可缓存 IO 状态,减少 epoll_wait/epoll_pwait 调用,提升应用程序效率。其中,LT 模式只要事件未处理就触发,ET 仅在高低电平变换时触发。
  2. 优势:
    a. 支持一个进程打开大数量目的 socket 描述符;
    b. IO 效率不随 FD 数目增加而线性下降;
    c. 未使用 mmap 加速内核与用户空间的消息传递。

二、epoll系统调用

        使用c库封装的3个epoll系统调用,具体源码如下:

此系统调用对文件描述符epfd引用的epoll实例执行控制操作,它要求操作op对目标文件描述符fd执行。op参数有效值为:EPOLL_CTL_ADD/EPOLL_CTL_MOD/EPOLL_CTL_DEL. 

等待 epoll 文件描述符上的 I/O 事件。epfd 引用的 epoll 实例上的事件,事件所指向的存储区域将包含供调用者使用的事件。

select 监控的句柄列表在用户态,每次调用都需要从用户态将句柄列表拷贝到内核态。epoll 中句柄建立在内核当中,减少内核和用户态拷贝,这也是其高效的原因。

 

三、用户api

1.epoll_create()

参数
  • size:旧版本 Linux 中曾用于预估监控文件描述符数量,现被忽略(Linux 2.6.8+),但需传入大于 0 的值,通常设为 1
返回值:
  • 成功:返回新创建的 epoll 实例文件描述符(非负整数)。
  • 失败:返回 -1,并通过全局变量 errno 设置错误类型(如内存不足)。
使用案例:
int epfd = epoll_create(1);  
if (epfd == -1) {  
    perror("epoll_create");  
    exit(EXIT_FAILURE);  
}  
// 后续通过 epfd 操作 epoll 实例  

二、epoll_ctl()

用于管理 epoll 实例监控的事件,如添加、修改、删除文件描述符的监控事件。

参数:
  • epfd:epoll 实例的文件描述符(由 epoll_create() 返回)。
  • op:操作类型,取值:
    • EPOLL_CTL_ADD:添加监控事件。
    • EPOLL_CTL_MOD:修改已有监控事件。
    • EPOLL_CTL_DEL:删除监控事件。
  • fd:需监控的文件描述符(如 socket)。
  • event:指向 struct epoll_event 的指针,定义监控事件类型(如 EPOLLIN 读事件、EPOLLOUT 写事件)。
返回值:
  • 成功:返回 0
  • 失败:返回 -1errno 记录错误(如非法文件描述符)。
使用案例:
struct epoll_event event;  
event.events = EPOLLIN;  
event.data.fd = sockfd; // 假设 sockfd 是待监控的 socket  
if (epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &event) == -1) {  
    perror("epoll_ctl add");  
    close(epfd);  
    exit(EXIT_FAILURE);  
}  

三、epoll_wait()

等待 epoll 实例监控的文件描述符上的事件就绪。

参数:
  • epfd:epoll 实例的文件描述符。
  • events:指向 struct epoll_event 数组的指针,用于存储就绪事件。
  • maxeventsevents 数组的最大容量,需大于 0。
  • timeout:等待超时时间(毫秒):
    • -1:永久阻塞,直到事件就绪。
    • 0:非阻塞,立即返回。
    • >0:等待指定毫秒,超时返回。
返回值:
  • 成功:返回就绪事件的数量。
  • 失败:返回 -1errno 记录错误(如中断信号)。
  • 超时:返回 0
使用案例:
struct epoll_event events[10]; // 最多处理 10 个事件  
int nfds = epoll_wait(epfd, events, 10, -1);  
if (nfds == -1) {  
    perror("epoll_wait");  
    close(epfd);  
    exit(EXIT_FAILURE);  
}  
for (int i = 0; i < nfds; i++) {  
    if (events[i].events & EPOLLIN) {  
        // 处理读事件  
    }  
}  

四、解读边缘触发和水平触发

        在Linux的epoll机制中,边缘触发(Edge-Triggered, ET)和水平触发(Level-Triggered, LT)是两种不同的事件通知模式,它们决定了epoll_wait如何向应用程序报告文件描述符的就绪状态。以下是两者的详细对比:

1. 水平触发(LT)模式

  • 工作原理

    • 只要文件描述符处于就绪状态(如读缓冲区有数据或写缓冲区有空闲空间),每次调用epoll_wait时都会通知应用程序。

    • 例如:若socket接收缓冲区中有数据未读完,epoll_wait会持续报告该描述符的读就绪事件,直到数据被完全读取。

  • 特点

    • 编程简单:无需一次性处理所有数据,未处理完的事件会被重复通知。

    • 效率较低:高并发时可能因频繁通知未处理的事件导致更多系统调用。

    • 兼容性:行为类似于select/poll,适合传统编程模型。

  • 代码示例

    // LT模式下读取数据(无需循环读)
    char buf[1024];
    int n = read(fd, buf, sizeof(buf));
    // 未读完的数据下次epoll_wait会再次通知

2. 边缘触发(ET)模式

  • 工作原理

    • 仅在文件描述符状态发生变化时通知一次(如从不可读到可读,或从不可写到可写)。

    • 例如:当socket接收缓冲区新数据到达时触发一次读事件;若未读完数据,后续epoll_wait不会重复通知,直到有新数据到来

  • 特点

    • 高性能:减少重复通知,适合高并发场景。

    • 编程复杂:需确保一次触发后处理所有数据,否则会丢失事件。

    • 必须使用非阻塞I/O:避免因未读完数据导致后续操作阻塞。

  • 代码示例

    // ET模式下必须循环读取,直到EAGAIN
    char buf[1024];
    while (1) {
        int n = read(fd, buf, sizeof(buf));
        if (n == -1) {
            if (errno == EAGAIN || errno == EWOULDBLOCK) {
                break; // 数据已读完
            }
            // 处理其他错误
        } else if (n == 0) {
            // 连接关闭
            break;
        }
        // 处理数据
    }

3. 核心区别

特性水平触发(LT)边缘触发(ET)
通知时机只要条件满足,重复通知。仅在状态变化时通知一次。
未处理事件持续通知,直到事件被处理。不通知,需应用自行处理。
性能可能因重复通知效率较低。减少通知次数,适合高并发。
编程复杂度简单,适合传统模型。复杂,需确保处理所有数据。
I/O模式阻塞或非阻塞均可。必须使用非阻塞I/O,避免阻塞。

4. 使用场景

  • LT模式

    • 适合对编程简单性要求高、事件处理可能分批的场景。

    • 例如:小型服务器、传统网络应用。

  • ET模式

    • 适合需要极致性能的高并发场景,如Web服务器(Nginx默认使用ET)。

    • 需结合非阻塞I/O和循环读写,确保完全处理数据。


5. 注意事项(ET模式)

  1. 必须使用非阻塞I/O

    • 避免因未读完数据导致后续操作阻塞。

    • 设置文件描述符为非阻塞:fcntl(fd, F_SETFL, O_NONBLOCK);

  2. 循环读写直到EAGAIN

    • 读:循环调用read直到返回EAGAINEWOULDBLOCK

    • 写:需监听EPOLLOUT事件,并在缓冲区可写时一次性发送数据。

  3. 单次触发处理全部数据

    • 若未处理完,可能永久丢失后续事件。

在服务器端使用epoll进行I/O多路转接的步骤如下:

1.创建监听的套接字:

int sockfd=socket(AF_INET,SOCK_STREAM,0);

2.设置端口复用: //可选

int opt=1;

setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));

3.使本地的ip与端口和监听的套接字进行绑定:

int ret=bind(sockfd,(struct sockaddr*)&serv_addr,sizeof(serv_addr));

4.给监听的套接字设置监听:

listen(sockfd,5);

5.创建epoll实例对象:

int epfd=epoll_create(1);

6.将用于监听的的套接字添加到epoll实例中:

struct epoll_event ev;

ev.events=EPOLLIN;

ev.data.fd=sockfd;   //相当于回调,当监听成功,通过这个可以知道是谁

int ret=epoll_ctl(epfd,EPOLL_CTL_ADD,sockfd,&ev);

7.检测添加到epoll实例中的文件描述符是否已经就绪,并将已经就绪的文件描述符进行处理

int nready=epoll_wait(epfd,events,size,-1);

        #1 如果是监听的文件描述符,则和新客户端建立连接,将得到的文件描述符添加到epoll实例当中

        struct sockaddr_in client_addr;

        memset(&client_addr,0,sizeof(struct sockaddr_in));

        socklen_t client_len=sizeof(client_addr);

        int clientfd=accept(sockfd,(struct  sockaddr*)&client_addr,&client_len);

        ev.events=EPOLLIN | EPOLLET    //边沿触发

        ev.data.fd=clientfd;

        epoll_ctl(epfd,EPOLL_CTL_ADD,clientfd,&ev);   //把新客户端节点加入红黑树

        #2 如果是通信文件的描述符,和对应的客户端进行通信,如果链接已断开,则从epoll树删除

        int clientfd=events[i].data.fd;

        char buffer[LENGTH];

        int len=recv(clientfd,buffer,LENGTH,0);

        if(len<0){

                close(clientfd)l;

                epoll_ctl(epfd,EPOLL_CTL_DEL,clientfd,NULL);

        }else if(len==0){

                close(clientfd)l;

                epoll_ctl(epfd,EPOLL_CTL_DEL,clientfd,NULL);

        }else{

                printf("Recv: %s, %d byte(s)\n", buffer, len);

        }

8.不断while循环重复第七步操作

        在边沿触发的模式中由于只有套接字的状态发生变化(如从“缓冲区空”变成“缓冲区不空”)才可以发送事件通知,因此如果当客户端的套接字不全部读完的时候,再有数据发送到服务器也不会引发状态的变化导致永远无法再次读取数据。因此必须在一次通知的处理中将数据全部读完。有两种方法:一种是设置缓冲区足够大,这样一次调用recv即可全部读出,但是往往不允许分配如此大的空间。因此第二中方法就需要循环不断读取,如下:

        但是如果正常读取完成recv就会阻塞,这个时候就必须设置非阻塞:

 五、案例分析

1.epolltest.c

//#include <sys/types.h>:提供了与套接字编程相关的基本类型和函数声明,像socket、bind、listen等函数,还有sockaddr结构体类型的定义
#include <sys/socket.h>
#include <sys/types.h>
//定义了 Internet 地址族相关的数据结构,例如sockaddr_in,用于存储 IPv4 地址和端口信息
#include <netinet/in.h>
// /包含了用于处理 IP 地址转换的函数,例如inet_pton和inet_ntop,可以在点分十进制字符串和二进制 IP 地址间转换
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <errno.h>
#include <fcntl.h>
#include <string.h>
#include <sys/epoll.h>
#include <stdlib.h>
#include <pthread.h>
//提供了 POSIX 操作系统 API 的基本功能,例如close、read、write等函数
#include <unistd.h>
#include <libgen.h>
#include <stdbool.h>  // 包含C99的布尔类型头文件


//定义了epoll_wait函数一次最多能返回的事件数量
#define MAX_EVENT_NUMBER 1024
//定义了接收数据缓冲区的大小
#define BUFFER_SIZE 5

// 设计一个函数:将文件描述符设备成为非阻塞方式
int SetNonBlocking(int fd){
    int oldoptions=fcntl(fd,F_GETFL);
    int newoptions=oldoptions|O_NONBLOCK;
    fcntl(fd,F_SETFL,newoptions);

    return oldoptions;
}

// 设计一个函数:将文件描述符fd上面的EPOLLIN注册到epollfd指示的epoll内核事件列表当中
void addfd(int epollfd,int fd,bool enable_et){
    struct epoll_event event;
    event.data.fd=fd;
    event.events=EPOLLIN;

    if(enable_et){
        // /若enable_et为true,则将EPOLLET标志添加到事件类型中,从而启用 ET 模式
        event.events|=EPOLLET;
    }

    epoll_ctl(epollfd,EPOLL_CTL_ADD,fd,&event);
    SetNonBlocking(fd);
}

// LT模式的工作流程
void lt(struct epoll_event *events,int number,int epollfd,int listenfd){
    //存储从套接字接收的数据
    char cbuffer[BUFFER_SIZE];
    //使用 for 循环遍历 epoll_wait 返回的 events 数组,number 表示事件的数量
    for(int i=0;i<number;i++){
        //从 epoll_event 结构体中获取当前事件对应的文件描述符
        int sockfd = events->data.fd;
        //如果当前文件描述符是监听套接字 listenfd,表示有新的客户端连接请求。
        //使用 accept 函数接受连接,得到新的连接套接字 connfd,并将其添加到 epoll 实例中,同时禁用 ET 模式。
        if(sockfd == listenfd){
            struct sockaddr_in client_address;
            socklen_t client_addrlength = sizeof(client_address);
            int connfd = accept(listenfd,(struct sockaddr*)&client_address,&client_addrlength);
            addfd(epollfd,connfd,false); // 对connfd禁用ET模式
        }else if(events[i].events & EPOLLIN){  //处理可读事件
            //只要socket读缓存中还有未读出的数据,这段代码就会被触发
            printf("触发一次事件!\n");
            memset(cbuffer,'\0',BUFFER_SIZE);
            int ret=recv(sockfd,cbuffer,BUFFER_SIZE-1,0);
            if(ret<=0){
                close(sockfd);
                continue;
            }
            printf("get %d bytes of content : %s\n",ret,cbuffer);
            send(sockfd, cbuffer, BUFFER_SIZE, 0);
        }else{
            printf("触发其它事件!\n");
        }       
    }
}

//ET模式的工作流程
void et(struct epoll_event *events,int number,int epollfd,int listenfd){
    char cbuffer[BUFFER_SIZE];
    for(int i = 0;i < number;i++){
        int sockfd = events[i].data.fd;
        if(sockfd == listenfd){
            struct sockaddr_in client_address;
            memset(&client_address, 0, sizeof(struct sockaddr_in));
            socklen_t client_addrlength = sizeof(client_address);
            while(1){
                int connfd = accept(listenfd,(struct sockaddr*)&client_address,&client_addrlength);
                if (connfd == -1){
                    if ((errno == EAGAIN) || (errno == EWOULDBLOCK)){
                        // 没有更多连接了
                        break;
                    }
                    perror("accept");
                    break;
                }
                addfd(epollfd, connfd, true); // 对connfd启用ET模式
            }
        }else if(events[i].events & EPOLLIN){
            // 只要有新数据到达,这段代码被触发,需要一次性读完所有数据
            while (1){
                memset(cbuffer, '\0', BUFFER_SIZE);
                int ret = recv(sockfd, cbuffer, BUFFER_SIZE - 1, 0);
                if (ret == -1){
                    if ((errno == EAGAIN) || (errno == EWOULDBLOCK)){
                        // 没有更多数据可读了
                        printf("数据已经接收完毕...\n");
                        break;
                    }else {
                        close(sockfd);
                        epoll_ctl(epollfd, EPOLL_CTL_DEL, sockfd, NULL);
                        exit(1);
                    }
                }
                else if (ret == 0){
                    // 客户端关闭连接
                    printf("客户端已经断开连接! \n");
                    close(sockfd);
                    epoll_ctl(epollfd, EPOLL_CTL_DEL, sockfd, NULL);
                }
                else{
                    printf("get %d bytes of content : %s\n", ret, cbuffer);
                    send(sockfd, cbuffer, BUFFER_SIZE, 0);
                }
            }
        }else{
            printf("触发其它事件!\n");
        }
    }
}

int main(int argc,char *argv[]){
    if(argc<=2){
        //basename函数是用来从路径名里提取文件名的,它定义在<libgen.h>头文件中
        printf("使用方法错误:%s缺少IP地址和端口?\n",basename(argv[0]));
        return -1;
    }

    const char *ip=argv[1]; // 参数IP地址
    int port=atoi(argv[2]); // 参数端口

    int ret = 0;
    struct sockaddr_in address;

    bzero(&address,sizeof(address));
    address.sin_family=AF_INET; // AF_INET(地址簇)PF_INET(协议簇)
    inet_pton(AF_INET,ip,&address.sin_addr);
    address.sin_port=htons(port);

    //创建套接字
    int listenfd=socket(PF_INET,SOCK_STREAM,0);
    assert(listenfd>=0);
    //绑定地址
    ret=bind(listenfd,(struct sockaddr*)&address,sizeof(address));
    assert(ret!=-1);
    //监听
    ret=listen(listenfd,5);
    assert(ret!=-1);

    struct epoll_event events[MAX_EVENT_NUMBER];

    int epollfd=epoll_create(5);
    assert(epollfd!=-1);

    addfd(epollfd,listenfd,true);

    while(1){
        int ret=epoll_wait(epollfd,events,MAX_EVENT_NUMBER,-1);
        if(ret == -1) continue;

        //lt(events,ret,epollfd,listenfd); // 使用LT模式
        et(events,ret,epollfd,listenfd); // 使用ET模式
    }
    
    close(listenfd);
    return 0;
}

2.client.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>

#define BUFFER_SIZE 10

int main(int argc, char *argv[]) {
    if (argc != 3) {
        printf("使用方法: %s <服务器 IP 地址> <服务器端口号>\n", argv[0]);
        return -1;
    }

    const char *server_ip = argv[1];
    int server_port = atoi(argv[2]);

    // 创建套接字
    int sockfd = socket(PF_INET, SOCK_STREAM, 0);
    if (sockfd == -1) {
        perror("socket");
        return -1;
    }

    // 配置服务器地址
    struct sockaddr_in server_addr;
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(server_port);
    if (inet_pton(AF_INET, server_ip, &server_addr.sin_addr) <= 0) {
        perror("inet_pton");
        close(sockfd);
        return -1;
    }

    // 连接服务器
    if (connect(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
        perror("connect");
        close(sockfd);
        return -1;
    }

    printf("已连接到服务器 %s:%d\n", server_ip, server_port);

    char send_buffer[BUFFER_SIZE];
    char recv_buffer[BUFFER_SIZE];

    while (1) {
        // 从标准输入读取数据
        printf("请输入要发送的数据(输入 'quit' 退出): ");
        fgets(send_buffer, BUFFER_SIZE, stdin);
        send_buffer[strcspn(send_buffer, "\n")] = 0; // 去除换行符

        if (strcmp(send_buffer, "quit") == 0) {
            break;
        }

        // 发送数据到服务器
        ssize_t send_len = send(sockfd, send_buffer, strlen(send_buffer), 0);
        if (send_len == -1) {
            perror("send");
            break;
        }

        // 接收服务器的响应
        ssize_t recv_len = recv(sockfd, recv_buffer, BUFFER_SIZE - 1, 0);
        if (recv_len == -1) {
            perror("recv");
            break;
        } else if (recv_len == 0) {
            printf("服务器关闭连接\n");
            break;
        }

        recv_buffer[recv_len] = '\0';
        printf("收到服务器响应: %s\n", recv_buffer);
    }

    // 关闭套接字
    close(sockfd);

    return 0;
}    

3.编译

gcc epolltest.c -o epolltest
gcc client.c -o client

4.运行

        本案例的代码将缓冲区大小仅设为5字节,因此左侧每次接收5个字节,客户端一次发送,服务器多次接收打印。

相关文章:

  • 动态规划——完全背包问题
  • 【中文翻译】第8章-The Algorithmic Foundations of Differential Privacy
  • [spring] Spring JPA - Hibernate 多表联查 3
  • 人形机器人科普
  • Unity Shader 的编程流程和结构
  • 现代前端质量保障与效能提升实战指南
  • Noteexpress插入参考文献无法对齐
  • Linux生产者消费者模型
  • 快速求出质数
  • 【算法训练】单向链表
  • pandas中新增的case_when()方法
  • c++ 命名空间 namespace
  • 【 <二> 丹方改良:Spring 时代的 JavaWeb】之 Spring Boot 中的数据验证:使用 Hibernate Validator
  • 数据建模流程: 概念模型>>逻辑模型>>物理模型
  • NSSCTF(MISC)——[NSSRound#4 SWPU]Type Message
  • 网络爬虫-2:基础与理论
  • 论文阅读笔记:Denoising Diffusion Probabilistic Models (3)
  • C语言中*a与a的区别和联系
  • 数据结构——B树、B+树、哈夫曼树
  • 安全测试理论
  • 15世纪以来中国文化如何向欧洲传播?《东学西传文献集成初编》发布
  • 青海省林业和草原局副局长旦增主动投案,正接受审查调查
  • 韩国下届大选执政党初选4进2结果揭晓,金文洙、韩东勋胜出
  • 建发股份:将于5月6日召开股东大会,审议提名林茂等为公司新一届董事等议案
  • 西湖大学独家回应本科招生走出浙江:经过三年试点,条件成熟
  • 法院为“外卖骑手”人身权益撑腰:依法认定实际投保人地位