IO多路复用--epoll

一、概述

IO 多路转接的意思是在一个操作里同时监听多个输入输出源,在其中一个或多个输入输出源可用的时候返回,然后对其的进行读写操作。epoll 是 select 和 poll 的升级版,相较于这两个前辈,epoll 改进了工作方式,因此它更加高效。

epoll相对于select/poll,有以下优点:

  • 对于待检测集合select和poll是基于线性方式处理的,epoll是基于红黑树来管理待检测集合的
  • select和poll每次都会线性扫描整个待检测集合,集合越大速度越慢,epoll使用的是回调机制,效率高,处理效率也不会随着检测集合的变大而下降
  • select和poll工作过程中存在内核/用户空间数据的频繁拷贝问题,在epoll中内核和用户区使用的是共享内存(基于mmap内存映射区实现),省去了不必要的内存拷贝。
  • 程序猿需要对select和poll返回的集合进行判断才能知道哪些文件描述符是就绪的,通过epoll可以直接得到已就绪的文件描述符集合,无需再次检测
  • 使用 epoll 没有最大文件描述符的限制,仅受系统中进程能打开的最大文件数目限制

二、epoll的应用

linux提供如下三个epoll函数,分别处理不同的操作,函数原型如下:

c 复制代码
#include <sys/epoll.h>
// 创建epoll实例,通过一棵红黑树管理待检测集合
int epoll_create(int size);
// 管理红黑树上的文件描述符(添加、修改、删除)
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
// 检测epoll树中是否有就绪的文件描述符
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

epoll_create() 函数的作用是创建一个红黑树模型的实例,用于管理待检测的文件描述符的集合。

c 复制代码
int epoll_create(int size);
  • 函数参数 size:在 Linux 内核 2.6.8 版本以后,这个参数是被忽略的,只需要指定一个大于 0 的数值就可以了。
  • 函数返回值:
    • 失败:返回 - 1
    • 成功:返回一个有效的文件描述符,通过这个文件描述符就可以访问创建的 epoll 实例了

epoll_ctl() 函数的作用是管理红黑树实例上的节点,可以进行添加、删除、修改操作。

c 复制代码
// 联合体, 多个变量共用同一块内存        
typedef union epoll_data {
 	void        *ptr;
	int          fd;	// 通常情况下使用这个成员, 和epoll_ctl的第三个参数相同即可
	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_ctl(int epfd, int op, int fd, struct epoll_event *event);
  • 函数参数
    • epfd:epoll_create () 函数的返回值,通过这个参数找到 epoll 实例
    • op:这是一个枚举值,控制通过该函数执行什么操作
      • EPOLL_CTL_ADD:往 epoll 模型中添加新的节点
      • EPOLL_CTL_MOD:修改 epoll 模型中已经存在的节点
      • EPOLL_CTL_DEL:删除 epoll 模型中的指定的节点
    • fd:文件描述符,即要添加 / 修改 / 删除的文件描述符
    • event:epoll 事件,用来修饰第三个参数对应的文件描述符的,指定检测这个文件描述符的什么事件
      • events:委托 epoll 检测的事件
        • EPOLLIN:读事件,接收数据,检测读缓冲区,如果有数据该文件描述符就绪
        • EPOLLOUT:写事件,发送数据,检测写缓冲区,如果可写该文件描述符就绪
        • EPOLLERR:异常事件
      • data:用户数据变量,这是一个联合体类型,通常情况下使用里边的 fd 成员,用于存储待检测的文件描述符的值,在调用 epoll_wait() 函数的时候这个值会被传出。
  • 函数返回值:
    • 失败:返回 - 1
    • 成功:返回 0

epoll_wait() 函数的作用是检测创建的 epoll 实例中有没有就绪的文件描述符。

c 复制代码
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
  • 函数参数:
    • epfd:epoll_create () 函数的返回值,通过这个参数找到 epoll 实例
    • events:传出参数,这是一个结构体数组的地址,里边存储了已就绪的文件描述符的信息
    • maxevents:修饰第二个参数,结构体数组的容量(元素个数)
    • timeout:如果检测的 epoll 实例中没有已就绪的文件描述符,该函数阻塞的时长,单位 ms 毫秒
      • 0:函数不阻塞,不管 epoll 实例中有没有就绪的文件描述符,函数被调用后都直接返回
      • 大于 0:如果 epoll 实例中没有已就绪的文件描述符,函数阻塞对应的毫秒数再返回
      • -1:函数一直阻塞,直到 epoll 实例中有已就绪的文件描述符之后才解除阻塞
  • 函数返回值 :
    • 成功:
      • 等于 0:函数是阻塞被强制解除了,没有检测到满足条件的文件描述符
      • 大于 0:检测到的已就绪的文件描述符的总个数
    • 失败:返回 - 1

基于epoll的回声服务器代码如下:

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/epoll.h>

#define BUF_SIZE 100
#define EPOLL_SIZE 50
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 adr_sz;
    int str_len, i;
    char buf[BUF_SIZE];

    struct epoll_event *ep_events;
    struct epoll_event event;
    int epfd, event_cnt;

    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_create(EPOLL_SIZE);
    ep_events = malloc(sizeof(struct epoll_event) * EPOLL_SIZE);

    event.events = EPOLLIN;
    event.data.fd = serv_sock;
    // 在epoll例程内部注册监视对象文件描述符
    epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, &event);

    while (1) {
        event_cnt = epoll_wait(epfd, ep_events, EPOLL_SIZE, -1);
        if (event_cnt == -1) {
            puts("epoll_wait() error");
            break;
        }

        for (i = 0; i < event_cnt; i++) {
            if (ep_events[i].data.fd == serv_sock) {
                adr_sz = sizeof(clnt_adr);
                clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_adr, &adr_sz);
                event.events = EPOLLIN;
                event.data.fd = clnt_sock;
                epoll_ctl(epfd, EPOLL_CTL_ADD, clnt_sock, &event);
                printf("connected client: %d \n", clnt_sock);
            }
            else {
                str_len = read(ep_events[i].data.fd, buf, BUF_SIZE);
                if (str_len == 0) {
                    epoll_ctl(epfd, EPOLL_CTL_DEL, ep_events[i].data.fd, NULL);
                    close(ep_events[i].data.fd);
                    printf("closed client: %d\n", ep_events[i].data.fd);
                }
                else {
                    write(ep_events[i].data.fd, buf, str_len);
                }
            }
        }

    }
    close(serv_sock);
    close(epfd);
    return 0;
}

void error_handling(char *message) {
    fputs(message, stderr);
    fputs("\n", stderr);
    exit(1);
}
相关推荐
jjkkzzzz39 分钟前
Linux下的C/C++开发之操作Zookeeper
linux·zookeeper·c/c++
二当家的素材网40 分钟前
Centos和麒麟系统如何每天晚上2点10分定时备份达梦数据库
linux·数据库·centos
挑战者66688840 分钟前
CentOS 系统高效部署 Dify 全攻略
linux·运维·centos
DD_陈东东1 小时前
gdbus 常用数据结构与库函数
linux
DIY机器人工房2 小时前
代码详细注释:ARM-Linux字符设备驱动开发案例:LCD汉字输出改进建议开发板断电重启还能显示汉字,显示汉字位置自定义
linux·嵌入式·文件io·diy机器人工房
得物技术2 小时前
eBPF 助力 NAS 分钟级别 Pod 实例溯源|得物技术
linux
Rudon滨海渔村2 小时前
解决阿里云ubuntu内存溢出导致vps死机无法访问 - 永久性增加ubuntu的swap空间 - 阿里云Linux实例内存溢出(OOM)问题修复方案
linux·运维·ubuntu
A-刘晨阳3 小时前
【Linux】Redis 6.2.6 的二进制部署【适用于多版本】
linux·运维·redis
cat_with_cat3 小时前
Linux网络:UDP socket创建流程与简单通信
linux·网络·udp
2401_861615284 小时前
跨平台的ARM 和 x86 Docker 镜像:汇编语言实验环境搭建
linux·汇编·ubuntu·docker·容器