epoll基本使用

epoll简介

epoll是Linux中的一种使用IO多路复用机制,即使用单个线程就可以监听多个文件描述符的读写事件,避免开启大量线程或者执行大量轮询的造成的不必要的性能开销。类似的机制还有select和poll,关于其区别参考。

高性能IO模型分析-浅析Select、Poll、Epoll机制(三) - 知乎 (zhihu.com)

头文件和API

#include <sys/epoll.h>
// 创建epoll,通常使用epoll_create即可,epoll_create1可以设置一些额外的配置
int epoll_create(int size);
int epoll_create1(int flags);

// 向epoll中添加、删除、修改fd
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

// 等待fd的事件
int epoll_wait(int epfd, struct epoll_event *events,
               int maxevents, int timeout);
// epoll_pwait相比epoll_wait多了屏蔽指定信号的功能,即通过指定sigmask可以在
// epoll_pwait函数中修改信号屏蔽设置,epoll_pwait返回后恢复原始信号屏蔽设置
int epoll_pwait(int epfd, struct epoll_event *events,
                int maxevents, int timeout,
                const sigset_t *sigmask);
// epoll_pwait2和epoll_wait区别在于timeout可以指定的精度不同,epoll_pwait2
// 可以指定纳秒级别的精度(实际内核调度精度达不到),上边函数的timeout精度为毫秒
int epoll_pwait2(int epfd, struct epoll_event *events,
                 int maxevents, const struct timespec *timeout,
                 const sigset_t *sigmask);
#include <unistd.h>
// 关闭并释放epoll
int close(int fd);

从Linux2.6.8开始,epoll_create中的size实际上没有作用了

epoll_create(2) - Linux manual page (man7.org)

epoll_ctl(2) - Linux manual page (man7.org)

关键数据结构

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 events */
    epoll_data_t data;        /* User data variable */
};

该结构有两个用途,第一是用于在epoll_ctl设置需要监听的fd的事件事件属性以及附带的额外数据,第二是在epoll_wait获取触发对应事件以及额外数据

events是需要监听的文件描述符事件以及事件属性,其为一个bitmask,可以是如下内容通过或运算(|)得到的值(仅包含常用部分,全部内容见参考文献):

events说明
EPOLLIN文件描述符中有数据可以读取(事件)
EPOLLOUT文件描述符可以写入数据(事件)
EPOLLRDHUP (since Linux 2.6.17)对端关闭连接(事件)
EPOLLET使用边沿触发(即仅在事件发生(例如有数据可读)时epoll_wait返回一次,即时数据没有读取完,第二次调用epoll_wait时仍然阻塞)。默认状态下epoll使用水平触发,即如果有文件描述符的数据没有读取完则每次调用epoll_wait均不会阻塞。

data通常用于设置对应的文件描述符(fd)。在epoll_ctl中设置的epoll_data可以在epoll_wait中获取到,因此通常将epoll_data设置为文件描述符以便于在epoll_wait返回后操作对应文件。

epoll_ctl中op的合法值

op描述
EPOLL_CTL_ADD将一个fd加入到epoll中
EPOLL_CTL_MOD使用新的epoll_event来修改已经加入到epoll中的fd的设置
EPOLL_CTL_DEL从epoll中删除一个fd,events可以设置为NULL

参考

epoll_ctl(2) - Linux manual page (man7.org)

epoll(7) - Linux manual page (man7.org)

基本使用

#define MAX_FD 3
#define LOG_ERROR(fmt, ...) printf("[ERROR]" fmt "\n", ##__VA_ARGS__)
void demo() {
    int ret;
    int in_fd;
    int epfd = -1;
    // 需要监听的socket
    int in_socks[MAX_FD] = {0, 1, 2};
    // 在添加socket到epoll时使用的的epoll_event结构
    struct epoll_event set_event;
    // epoll_event数组用于接收准备就绪的fd
    struct epoll_event ep_events[MAX_FD];
    
	// 创建epoll(从Linux2.6.8开始,epoll_create的参数已经没有作用,设置为任何大于等于0的数即可)
    epfd = epoll_create(MAX_FD);
    if (epfd < 0) {
        LOG_ERROR("Fail to create epoll, %s", strerror(epfd));
        goto out;
    }
	// 将需要监听的socket加入到epoll中
    for (i=0; i<MAX_FD; i++) {
        // 设置事件以及属性
        set_event.events = EPOLLIN;
        // 设置额外数据,通过额外数据可以直接是fd或者能够获取到fd的索引,这里直接设置为fd
       	set_event.data.fd = in_socks[i];
        // 将socket加入对应的epoll
        ret = epoll_ctl(epfd, EPOLL_CTL_ADD, in_socks[i], &set_event);
        if (ret < 0) {
            LOG_ERROR("Fail to add fd to epoll, %s", strerror(errno));
            goto out;
        }
    }

    while (1) {
        // 等待fd中的数据就绪,timeout设置为-1表示一直阻塞等待
        ready_fd_num = epoll_wait(epfd, ep_events, MAX_FD, -1);
        if (ready_fd_num < 0) {
            LOG_ERROR("Fail to wait for data, err: %s", strerror(errno));
            goto out;
        }
		// 处理就绪的fd
        for(int i=0; i<ready_fd_num; i++) {
            in_fd = ep_events[i].data.fd;
            // 处理数据
        }
    }
out:
    if (epfd != -1) {
        close(epfd);
    }
    return;
}

附录

下边是部分Linux中的API和其头文件

#include <errno.h>
// errno全局变量
#include <string.h>
char *strerror(int errnum);
#include <stdlib.h>

void exit(int status);
### 使用Epoll进行事件处理 #### 创建Epoll实例 为了启动基于`epoll`的事件处理流程,首先需要创建一个`epoll`实例。这可以通过调用`epoll_create1(0)`来实现,该函数返回一个新的`epoll`文件描述符用于后续的操作[^1]。 ```c int epfd = epoll_create1(0); if (epfd == -1) { perror("Failed to create epoll instance"); } ``` #### 控制操作——注册感兴趣的事件 一旦有了`epoll`实例,就可以利用`epoll_ctl()`向此实例中添加、修改或移除要监控的文件描述符及其对应的事件类型。比如想要监听某个套接字上的可读写状态变化,则可以这样做: ```c struct epoll_event event; event.events = EPOLLIN | EPOLLET; // 设置为边缘触发模式并关注输入就绪事件 event.data.fd = sockfd; // 将sockfd加入到epoll监视列表里 if (epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &event) == -1) { perror("Error adding socket to epoll set"); } ``` 这里选择了边沿触发(`EPOLLET`)的方式,意味着只有当首次检测到条件满足时才会收到通知;而水平触发则会在每次符合条件的时候都发出信号直到条件消失为止。选择哪种取决于具体的应用场景需求[^4]。 #### 等待并响应事件 最后一步是等待实际发生的事件,并作出相应的反应。通过循环调用`epoll_wait()`可以从之前设置好的`epoll`集合里面获取已经准备好了的数据流(即发生了指定类型的活动)。下面是一个简单的例子展示如何做这件事: ```c #define MAX_EVENTS 10 struct epoll_event events[MAX_EVENTS]; while(true){ int nfds = epoll_wait(epfd, events, MAX_EVENTS, -1); //-1表示无限期阻塞直至有事件发生 if(nfds == -1){ perror("epoll wait error"); break; } for(int i=0;i<nfds;++i){ if(events[i].events&EPOLLIN){ // 如果是读取准备好... handle_read(events[i].data.fd); } else if(events[i].events&EPOLLOUT){// 或者写出准备好... handle_write(events[i].data.fd); } } } void close_epoll(){ close(epfd); // 记得关闭epoll句柄以释放资源 } ``` 这段代码展示了基本框架:在一个无限循环内部不断查询是否有新的IO事件到来,并针对不同的情况执行特定的任务逻辑。需要注意的是,在程序结束前应当记得关闭由`epoll_create1()`所获得的那个特殊的文件描述符,以免造成系统资源泄露[^2]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值