linux epoll的使用

举报
ZhjDayDayUp 发表于 2022/01/17 21:03:10 2022/01/17
【摘要】 epoll是一种I/O多路复用技术,支持高并发

epoll是一种I/O多路复用技术,支持高并发。考虑如下场景:当进程存在大量的tcp连接时(如100万个),但是某一时刻只有少量的活跃连接(如100个),在这种情况下,如何高效地处理呢?

传统的poll和select方法:当事件发生时,会去遍历所有的连接,因为它并不知道哪些连接是真正有事件发生的。

epoll方法:会维护一个红黑表用于存储需要监听的连接,维护一个双向链表用于存储真正发生事件的连接。当事件发生时,只需要遍历此链表,可以提高处理效率。

epoll的API

使用时调用如下三个主要函数:

1、调用epoll_create建立一个epoll对象

int epoll_create(int size)
// size参数(大于0)表示epoll对象会处理的事件的大致数量,而不是能够处理的事件的最大数。
// 成功,则返回epoll对象句柄;如果出错,则返回-1,并且将errno设置为指示错误。

这一步,会创建一个eventpoll结构的对象,主要结构如下:rbr成员指向一颗红黑树,存储了需要监听的连接;rdllist是一个双向链表,存储满足条件的事件

struct eventpoll {
// ...
struct rb_root rbr; // 管理 epoll 监听的事件
struct list_head rdllist; // 保存着 epoll_wait 返回满⾜条件的事件
// ...
};

// 每个红黑树节点的结构为
struct epitem {
// ...
struct rb_node rbn; // 红⿊树节点
struct list_head rdllist; // 双向链表节点
struct epoll_filefd ffd; // 事件句柄信息
struct eventpoll *ep; // 指向所属的eventpoll对象
struct epoll_event event; // 注册的事件类型
// ...
};

需要注意的是,当创建好epoll对象后,它会占用一个fd值,在linux下查看/proc/进程id/fd可以看到该fd,因此在使用完后需要close关闭,负责可能导致fd资源被耗尽。

2、调用epoll_ctl向epoll对象中添加需要监听的连接,也可以修改或者删除epoll对象中的特定连接。

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表示连接的句柄,也就是需要监听的fd
// epoll_event表示需要监听的事件,结构如下:
typedef union epoll_data {
void *ptr; // 指向用户自定义的数据
int fd; // 注册的文件描述符
uint32_t u32;
uint64_t u64;
}epoll_data_t;

struct epoll_event {
__uint32_t events; // 表示监听的事件
epoll_data_t data;
};
// events的取值:
EPOLLIN 表示该连接上有数据可读(tcp连接远端主动关闭连接,也是可读事件,因为需要处理发送来的FIN包; FIN包就是read 返回 0EPOLLOUT 表示该连接上可写发送(主动向上游服务器发起⾮阻塞tcp连接,连接建⽴成功事件相当于可写事件)
EPOLLRDHUP 表示tcp连接的远端关闭或半关闭连接
EPOLLPRI 表示连接上有紧急数据需要读
EPOLLERR 表示连接发⽣错误
EPOLLHUP 表示连接被挂起
EPOLLET 将触发⽅式设置为边缘触发,系统默认为⽔平触发
EPOLLONESHOT 表示该事件只处理⼀次,下次需要处理时需重新加⼊epoll

// 返回值:0表示成功,-1表示错误。
// 错误码:
EBADF:epfd或fd不是有效的文件描述符。
EEXIST:op为EPOLL_CTL_ADD,并且提供的文件描述符fd已在该epoll实例中注册。
EINVAL:epfd不是epoll文件描述符,或者fd与epfd相同,或者此接口不支持请求的操作op。
ENOENT:op是EPOLL_CTL_MODEPOLL_CTL_DEL,并且fd未在该epoll实例中注册。
ENOMEM:没有足够的内存来处理请求的操作控制操作。
ENOSPC:尝试在主机上注册(EPOLL_CTL_ADD)新文件描述符时遇到了/proc/sys/fs/epoll/max_user_watches施加的限制。
EPERM:目标文件fd不支持epoll。

3、调用epoll_wait等待事件的发生,并返回事件集合(即events),如果没有时间发生,则等待timieout毫秒

int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout)
// epfd表示epoll对象句柄
// events表示分配好的epoll_event结构体数组,epoll将会把发生的事件复制到events数组中(events不可以是空指针,内核只负责把数据复制到这个 events数组中,不会去帮助我们在用户态中分配内存。内核这种做法效率很高)
// maxevents表示本次可以返回的最大事件数目,通常maxevents参数与预分配的events数组的大小是相等的。该值不能大于创建epoll_create时的size
// timeout表示超时时间,单位为毫秒。0表示立即返回,-1表示一直等待

// 返回值:实际发生事件的连接数,0表示没有事件发生,-1表示错误

epoll的触发方式

分别为水平触发和边缘触发

1、水平触发时机

对于读操作,只要缓冲区内容不为空,就会返回读就绪;

对于写操作,只要换缓冲区还不满,就会返回写就绪。

当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。如果这次没有把数据一次性全部读写完,那么下次调用epoll_wait()时,它还会通知你在上次没读写完的文件描述符上继续读写。如果一直不去读写,它会一直通知。如果系统中有大量不需要读写的就绪文件描述符,而它们每次都会返回,这样会大大降低处理就绪文件描述符的效率。

水平触发模式适合一次性读大数据。

2、边缘触发时机

对于读操作,当缓冲区由不可读变为可读时;或者有新数据到达时(即缓冲区中的待读数据变多时);或者缓冲区由数据可读,且应用进程对相应的描述符进行EPOLL_CTL_MOD修改EPOLLIN事件时。

当被监控的文件描述符上有可读写事件发生时,epoll_wait会通知处理程序去读写。如果这次没有把数据全部读写完,那么下次调用epoll_wait时,它不会再通知你。这种模式比水平模式的效率高,适合读取少量数据的场景。

具体实例

#include <stdio.h>
#include <unistd.h>
#include <sys/time.h>
#include <stdint.h>
#include <pthread.h>
#include <sys/eventfd.h>
#include <sys/epoll.h>

int efd = -1;
pthread_t pid = 0;

#define POST_PROCESS(PID, EFD)\
do\
{\
    if ((PID) != 0)\
    {\
        pthread_join((PID), nullptr);\
        (PID) = 0;\
    }\
\
    if ((EFD) >= 0)\
    {\
        close((EFD));\
        (EFD) = -1;\
    }\
\
}while(0)

void *read_thread(void *)
{
    printf("read_func begin\n");

    uint64_t data;
    int ret;

    int epfd = epoll_create(10);
    if (epfd < 0)
    {
        printf("epoll_create fail\n");
        return nullptr;
    }

    struct epoll_event event;
    event.events = EPOLLET | EPOLLIN;
    event.data.fd = efd;
    ret = epoll_ctl(epfd, EPOLL_CTL_ADD, efd, &event);
    if (ret < 0)
    {
        printf("epoll_ctl fail\n");
        POST_PROCESS(pid, efd);
        return nullptr;
    }

    struct epoll_event events[10];
    while(true)
    {
        ret = epoll_wait(epfd, &events[0], 10, 5000);
        if (ret > 0)
        {
            for (int i = 0; i < ret; i++)
            {
                if (events[i].events & EPOLLIN)
                {
                    int event_fd = events[i].data.fd;
                    ret = read(event_fd, &data, sizeof(data));
                    if (ret < 0)
                    {
                        printf("read fail\n");
                    }
                    printf("success read from efd, read %d bytes, data is %d\n", ret, data);
                }
            }
        }
        else if (ret == 0)
        {
            printf("epoll wait timeout\n");
            break;
        }
        else
        {
            printf("epoll wait error\n");
            POST_PROCESS(pid, efd);
            return nullptr;
        }
    }
    return nullptr;
}

int main()
{
    efd = eventfd(0, 0);
    if (efd < 0)
    {
        printf("eventfd fail\n");
        return -1;
    }

    pthread_t pid = 0;
    int ret = pthread_create(&pid, nullptr, read_thread, nullptr);
    if (ret < 0)
    {
        printf("pthread_create fail\n");
        POST_PROCESS(pid, efd);
        return -1;
    }

    uint64_t data;
    for (int i = 1; i <= 5; i++)
    {
        data = i;
        ret = write(efd, &data, sizeof(data));
        if (ret < 0)
        {
            printf("write fail\n");
            POST_PROCESS(pid, efd);

            return -1;
        }
        else
        {
            printf("write success, data = %d\n", data);
        }

        sleep(1);
    }

    POST_PROCESS(pid, efd);

    return 0;
}

参考:
https://blog.csdn.net/u014183456/article/details/121441877

https://zhuanlan.zhihu.com/p/63179839

https://blog.csdn.net/baidu_41388533/article/details/110134366

https://blog.csdn.net/daaikuaichuan/article/details/83862311

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。