C语言 多路复用 epoll

简介: 本文详细介绍了 Linux 下的非阻塞 IO 多路复用技术——`epoll`,对比 `select` 和 `poll`,`epoll` 使用红黑树结构存储文件描述符,支持动态增删节点,无数量限制,采用回调机制提高效率。文章通过示例代码展示了如何使用 `epoll_create()` 创建 `epoll` 实例,`epoll_ctl()` 管理文件描述符,以及 `epoll_wait()` 等待事件。最后简要分析了 `epoll` 的核心数据结构 `struct eventpoll` 和红黑树节点 `struct epitem`。

epoll

epoll 相对于 select 与 poll 有较⼤的不同,主要是针对前⾯两种多路复⽤ IO 接⼝的不⾜

  • 与 select/poll ⽅案对⽐
    • select ⽅案使⽤数组存储⽂件描述符,最⼤⽀持 1024
    • select 每次调⽤都需要将描述符集合拷⻉到内核中, ⾮常消耗资源
    • poll ⽅案解决⽂件描述符存储数量限制问题,但其他问题没有得到解决
    • select / poll 底层使⽤轮询的⽅式检测⽂件描述符是否就绪,⽂件描述符越多,则效率越低
    • epoll 底层使⽤红⿊树,没有⽂件描述符数量的限制, 并且可以动态增加与删除节点,不⽤重复拷⻉
    • epoll 底层使⽤ callback 机制, 没有采⽤遍历所有描述符的⽅式,效率较⾼

img_61.png

epoll

epoll创建需要调用epoll_create()函数

创建一个epool实例,分配相关的数据结构空间

函数头文件:

#include <sys/epoll.h>

函数原型:

int epoll_create(int size);

参数:

  • size: 填写大于0的整数,从Linux 2.6.8开始,size参数已经被废弃,可以不填

返回值:

  • 成功: 返回一个大于0的整数,表示epoll实例的句柄
  • 失败: 返回-1,并设置errno
创建epoll实例
//todo 创建一个epoll实例
#include <stdio.h>
#include <stdlib.h>
#include <poll.h>
#include <sys/epoll.h>
int main() {
   
   

    // 创建一个epoll实例,获取文件描述符
    int epoll_fd = epoll_create(1);
    if (epoll_fd == -1) {
   
   
        perror("epoll_create error");
        exit(1);
    }

    printf("epoll_fd = %d\n", epoll_fd);

    return 0;
}

epoll_ctl 控制函数

epoll 控制函数主要⽤于⽂件描述符集合的管理,包括增加、修改、删除等操作, 具体需要调⽤epoll_ctl 函数

函数头文件:

#include <sys/epoll.h>

函数原型:

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

参数:

epfd: epoll实例的句柄

op: 操作类型, 
      EPOLL_CTL_ADD(增加),在epoll实例中添加新的文件描述符,相当于添加到红黑树,并将事件链接到fd
      EPOLL_CTL_MOD(修改), 更改与目标文件描述符fd相关联的事件
      EPOLL_CTL_DEL(删除),从epoll实例中删除目标文件描述符fd,事件参数被忽略
      在系统中定义如下:
                    #define EPOLL_CTL_ADD 1
                    #define EPOLL_CTL_DEL 2
                    #define EPOLL_CTL_MOD 3

fd: 要操作的文件描述符

event: 指向epoll_event结构体的指针, 用于描述事件类型和事件掩码


struct epoll_event 结构体定义如下:
typedef union epoll_data {
   
   
        void        *ptr; // 指向用户空间的数据
        int          fd;// 文件描述符
        uint32_t     u32; // 32位无符号整数
        uint64_t     u64;// 64位无符号整数
} epoll_data_t;

struct epoll_event {
   
   
        uint32_t     events; // 事件类型掩码
        epoll_data_t data; // 事件数据
};

    epoll 事件,事件具体定义如下:
            EPOLLIN : 读事件有效
            EPOLLOUT : 写事件有效
            EPOLLET: 将 EPOLL 设为边缘触发 (Edge Triggered) 模式

epoll_wait 等待事件函数

epoll 等待文件描述符关联的事件发⽣ (关联的⽂件描述符就绪), 这⾥调⽤ epoll_wait 函数

函数头文件:

#include <sys/epoll.h>

函数原型:

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

参数:

epfd: epoll实例的句柄
events: 指向epoll_event结构体数组的指针, 用于接收就绪事件
maxevents: 最大就绪事件数, 即events数组的大小
timeout: 超时时间, 单位为毫秒, -1表示阻塞, 0表示不阻塞, >0表示超时时间

返回值:

  • 成功: 返回就绪事件数, 即events数组中填充的事件数
  • 超时: 返回0
  • 失败: 返回-1, 并设置errno

实例

//todo 创建一个epoll实例
#include <stdio.h>
#include <stdlib.h>
#include <poll.h>
#include <sys/epoll.h>
int main() {
   
   

    // 创建一个epoll实例,获取文件描述符
    int epoll_fd = epoll_create(1);
    if (epoll_fd == -1) {
   
   
        perror("epoll_create error");
        exit(1);
    }

    printf("epoll_fd = %d\n", epoll_fd);

    //定义事件结构体
    struct epoll_event event;//关联文件描述符和事件类型
    //设置事件类型为可读
    event.events = EPOLLIN;
    //设置文件描述符为0
    event.data.fd = 0;
    //注册事件
    // 注册事件,监控文件描述符0
    int ret = epoll_ctl(epoll_fd, EPOLL_CTL_ADD, 0, &event);
    if (ret == -1) {
   
   
        perror("epoll_ctl error");
        exit(1);
    }
    printf("epoll_ctl success\n");

    // 等待事件到来
    struct epoll_event events[10];
    while (1){
   
   
        int nfds = epoll_wait(epoll_fd, events, 10, 1000);
        if (nfds == -1) {
   
   
            perror("epoll_wait error");
            exit(1);
        }if (nfds == 0) {
   
   
            printf("超时\n");
            continue;
        }if (nfds > 0) {
   
    
            printf("有事件发生\n");

            char buffer[1024]={
   
   0};
            //遍历文件描述符集合
            for (int i = 0; i < nfds; i++) {
   
   
                //判断事件类型是否为可读
                if(events[i].data.fd==0){
   
   
                    fgets(buffer,1024,stdin);
                    printf("收到消息:%s",buffer);
                }
            }


        }
    }





    return 0;
}

epoll 源码分析

img_62.png

上述框架中核⼼的数据结构 struct eventpoll , 具体定义如下:


struct eventpoll {
   
   
    spinlock_t lock;//自旋锁,用于保护对 struct eventpoll 结构的访问。
    wait_queue_head_t wq;//等待队列头,用于 epoll 本身的等待队列。
    wait_queue_head_t poll_wait;//等待队列头,用于 poll 回调的等待队列。
    struct list_head rdllist;//就绪文件描述符的链表头
    struct rb_root_cached rbr;//红黑树根节点,用于存储被监视的文件描述符结构。
    struct user_struct *user;//创建 eventpoll 描述符的用户结构
    struct list_head f_ep_links;//链表头,用于将此结构链接到 struct file 的 f_ep_links 链表。
    struct list_head f_tfile_llink;//链表头,用于将此结构链接到 struct file 的 f_tfile_llink 链表。
    int visited;// 用于优化循环检测检查
    struct list_head visited_list_link;//链表头,用于链接到已访问的链表。
#ifdef CONFIG_NET_RX_BUSY_POLL
    unsigned int napi_id;  //用于跟踪 busy poll 的 napi_id(仅在配置了 CONFIG_NET_RX_BUSY_POLL 时存在)。
#endif
};

红⿊树的节点对应的数据结构为 struct epitem , 具体定义如下:

```c
struct epitem {
struct rb_node rbn; / 红黑树节点 /
struct list_head rdllink; / 就绪列表链表节点 /
struct epitem next; / 用于 epoll 实例的哈希表 /
struct epoll_filefd ffd; /
文件描述符和文件指针 /
int nwait; /
等待队列的数量 /
struct list_head pwqlist; /
等待队列链表 /
struct eventpoll
ep; / 指向所属的 epoll 实例 /
struct list_head fllink; / 文件链表节点 /
struct epoll_event event; / 用户感兴趣的事件 /
};

```

未完

相关文章
|
11天前
|
网络协议 数据处理 C语言
利用C语言基于poll实现TCP回声服务器的多路复用模型
此代码仅为示例,展示了如何基于 `poll`实现多路复用的TCP回声服务器的基本框架。在实际应用中,你可能需要对其进行扩展或修改,以满足具体的需求。
31 0
|
14天前
|
存储 监控 Linux
C语言 多路复用 select源码分析
本文详细介绍了阻塞IO与非阻塞IO的概念及其在Linux系统中的实现方式。首先阐述了常见的IO模型,包括阻塞型、非阻塞型及多路复用IO模型。阻塞IO模型会在IO请求未完成时阻塞进程,而非阻塞IO模型则允许在IO未完成时立即返回。非阻塞IO可通过设置`O_NONBLOCK`标志实现。接着介绍了多路复用IO模型,利用`select`、`poll`和`epoll`等系统调用监控多个文件描述符。`select`函数通过内核检测文件描述符是否就绪,并通知调用者。
|
30天前
|
C语言
【C语言】epoll函数
【C语言】epoll函数
16 0
|
1月前
|
Linux API C语言
C语言的TCPServer和select/poll/epoll并发探讨
C语言的TCPServer和select/poll/epoll并发探讨
|
10月前
|
Linux API C语言
C语言的TCPServer和select/poll/epoll并发探讨
C语言的TCPServer和select/poll/epoll并发探讨
|
6天前
|
存储 Serverless C语言
【C语言基础考研向】11 gets函数与puts函数及str系列字符串操作函数
本文介绍了C语言中的`gets`和`puts`函数,`gets`用于从标准输入读取字符串直至换行符,并自动添加字符串结束标志`\0`。`puts`则用于向标准输出打印字符串并自动换行。此外,文章还详细讲解了`str`系列字符串操作函数,包括统计字符串长度的`strlen`、复制字符串的`strcpy`、比较字符串的`strcmp`以及拼接字符串的`strcat`。通过示例代码展示了这些函数的具体应用及注意事项。
|
9天前
|
存储 C语言
C语言程序设计核心详解 第十章:位运算和c语言文件操作详解_文件操作函数
本文详细介绍了C语言中的位运算和文件操作。位运算包括按位与、或、异或、取反、左移和右移等六种运算符及其复合赋值运算符,每种运算符的功能和应用场景都有具体说明。文件操作部分则涵盖了文件的概念、分类、文件类型指针、文件的打开与关闭、读写操作及当前读写位置的调整等内容,提供了丰富的示例帮助理解。通过对本文的学习,读者可以全面掌握C语言中的位运算和文件处理技术。
|
9天前
|
存储 C语言
C语言程序设计核心详解 第七章 函数和预编译命令
本章介绍C语言中的函数定义与使用,以及预编译命令。主要内容包括函数的定义格式、调用方式和示例分析。C程序结构分为`main()`单框架或多子函数框架。函数不能嵌套定义但可互相调用。变量具有类型、作用范围和存储类别三种属性,其中作用范围分为局部和全局。预编译命令包括文件包含和宏定义,宏定义分为无参和带参两种形式。此外,还介绍了变量的存储类别及其特点。通过实例详细解析了函数调用过程及宏定义的应用。
|
14天前
|
Linux C语言
C语言 多进程编程(三)信号处理方式和自定义处理函数
本文详细介绍了Linux系统中进程间通信的关键机制——信号。首先解释了信号作为一种异步通知机制的特点及其主要来源,接着列举了常见的信号类型及其定义。文章进一步探讨了信号的处理流程和Linux中处理信号的方式,包括忽略信号、捕捉信号以及执行默认操作。此外,通过具体示例演示了如何创建子进程并通过信号进行控制。最后,讲解了如何通过`signal`函数自定义信号处理函数,并提供了完整的示例代码,展示了父子进程之间通过信号进行通信的过程。
|
14天前
|
C语言
C语言 字符串操作函数
本文档详细介绍了多个常用的字符串操作函数,包括 `strlen`、`strcpy`、`strncpy`、`strcat`、`strncat`、`strcmp`、`strncpy`、`sprintf`、`itoa`、`strchr`、`strspn`、`strcspn`、`strstr` 和 `strtok`。每个函数均提供了语法说明、参数解释、返回值描述及示例代码。此外,还给出了部分函数的自实现版本,帮助读者深入理解其工作原理。通过这些函数,可以轻松地进行字符串长度计算、复制、连接、比较等操作。