I/O多路复用-epoll
epoll
epoll是Linux内核为处理大批量文件描述符而作了改进的poll,是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。另一点原因就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了。epoll除了提供select/poll那种IO事件的水平触发(Level Triggered)外,还提供了边缘触发(Edge Triggered),这就使得用户空间程序有可能缓存IO状态,减少epoll_wait/epoll_pwait的调用,提高应用程序效率[1]。剩下的部分是对man手册的翻译.
简介
I/O event notification facility I/O 事件通知工具
epoll API监视多个文件描述符以查看是否可以在其中任何一个上进行 I/O操作。 epoll API 可以用作边缘触发或水平触发的接口,并且可以很好地扩展到大量监视的文件描述符。 提供了以下系统调用来创建和管理 epoll 实例:
epoll_create(2)创建一个新的 epoll 实例并返回一个引用该实例的文件描述符。- 通过
epoll_ctl(2)注册需要被监视的文件描述符。 注册在 epoll 实例上的文件描述符的集合有时称为 epoll 集。 epoll_wait(2)等待 I/O 事件,如果当前没有可用的事件,则阻塞调用线程。
水平触发和边缘触发
epoll 事件分发接口既可以作为边缘触发(edge-triggered, ET),也可以作为级别触发(level-triggered , LT)。
边缘触发模式仅在受监视的文件描述符发生更改时才传递事件。向被监听文件描述符对应的输入缓冲区写入数据后会产生可读事件, epoll_wait(2) 会返回,如果读操作没有消耗整个缓冲区数据, 那么下次epoll_wait(2)可能会永久阻塞. 使用 EPOLLET(边缘触发)标志将rfd文件描述符添加到 epoll 接口.
使用 epoll 作为边缘触发接口的建议方法:使用非阻塞文件描述符和仅在 read(2) 或 write(2) 返回 EAGAIN 后等待事件
水平触发模式是只要读缓冲区有内容或写缓冲区有空间epoll_wait(2) 会返回. 默认使用这种模式.
epoll_create(2)
#include <sys/epoll.h>
int epoll_create(int size);
方法介绍
epoll_create()创建一个新的 epoll(7) 实例。 从 Linux 2.6.8 开始,size 参数被忽略,但必须大于零;
epoll_create() 返回一个引用新 epoll实例的文件描述符。 此文件描述符用于所有后续对 epoll 接口的调用。 当不再需要时,应使用 close(2) 关闭 epoll_create() 返回的文件描述符。 当所有引用 epoll 实例的文件描述符都已关闭时,内核会销毁该实例并释放相关资源以供重用。
返回值
成功时,这些系统调用返回一个非负的文件描述符。 出错时,返回 -1,并设置 errno 以指示错误。
epoll_ctl(2)
#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
方法介绍
这是一个系统调用. 该系统调用对文件描述符 epfd 引用的 epoll(7) 实例执行控制操作。控制操作是在 epoll(7) 实例上对目标文件描述符 fd 执行操作op。
参数
epfd: epoll(7) 实例对应的文件描述符
op: 表示被操作的文件描述符fd是添加到epoll, 还是从epoll实例上移除, 还是修改epoll实例对文件描述符fd监听的事件. 分别对应下面三个
//在文件描述符epfd所引用的epoll实例上注册目标文件描述符fd,并将事件事件与链接到 fd 的内部文件相关联。
EPOLL_CTL_ADD
//更改与目标文件描述符 fd 关联的事件事件。
EPOLL_CTL_MOD
//从 epfd 引用的 epoll 实例中移除(注销)目标文件描述符 fd。该事件被忽略并且可以为 NULL。
EPOLL_CTL_DEL
struct epoll_event *event : epoll实例要监听文件描述符fd的哪些事件
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_event结构体中的events 成员是一个位掩码,通过位的或运算or(|) 将以下零个或多个可用事件类型组合在一起:
EPOLLIN 关联文件fd可用于 read(2) 操作。
EPOLLOUT 关联文件fd可用于 write(2) 操作。
EPOLLPRI 文件描述符存在异常情况。请参阅 poll(2) 中对 POLLPRI 的讨论。
epoll_wait(2)
等待epoll实例的I/O事件
#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);
方法介绍
epoll_wait() 系统调用等待文件描述符 epfd 引用的 epoll(7) 实例上的事件。事件指向的内存区域将包含可供调用者使用的事件。
参数
epfd : epoll实例的文件描述符
maxevents : epoll_wait() 最多返回 maxevents。 maxevents 参数必须大于零。
timeout : 指定epoll_wait()将阻塞的毫秒数。传入 -1 epoll_wait()无限期阻塞直到epoll管理的fd可读或可写,而指定超时时间为零会导致epoll_wait()立即返回,即使没有可用的事件。传入其他值该方法将阻塞,直到:
* 一个文件描述符传递一个事件;
* 调用被信号处理程序中断;或者
* 超时到期。
events : I/O准备好的文件描述符将在方法返回时添加到这个数组中
返回值
成功时,epoll_wait() 返回为I/O准备好的文件描述符的数量,如果在请求的超时毫秒内没有文件描述符准备好,则返回零。 发生错误时,epoll_wait() 返回 -1 并正确设置 errno。
代码实例
/*************************************************************************
使用epoll实现的服务端
> File Name: epoll_server.c
> Author:
> Mail:
> Created Time: Sat 31 Jul 2021 09:55:45 AM CST
************************************************************************/
#include <stdio.h>
#include <sys/select.h>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <unistd.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <errno.h>
#include <fcntl.h>
#include "../log/log.h"
#define MAX_EVENTS 50
#define BUF_SIZE 1024
//创建接受链接的socket
int createListenSocket() {
//创建socket
int lfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in addr;
bzero(&addr, sizeof addr);
addr.sin_port = htons(8000);
addr.sin_addr.s_addr = INADDR_ANY;
addr.sin_family = AF_INET;
//绑定
int restBind = bind(lfd, (const struct sockaddr *) &addr, sizeof(addr));
if (restBind < 0) {
close(lfd);
perror(strerror(errno));
exit(EXIT_FAILURE);
}
int opt = 1;
//设置为可复用
setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
socklen_t len = sizeof(addr);
//监听
int restListen = listen(lfd, 128);
if (restListen < 0) {
close(lfd);
perror(strerror(errno));
exit(EXIT_FAILURE);
}
return lfd;
}
int main() {
LOG(GREEN"Debug\n");
int lfd = createListenSocket();
struct epoll_event ev, events[MAX_EVENTS];
//创建epoll实例
int efd = epoll_create(1);
if (efd < 0) {
exit(EXIT_FAILURE);
}
//设置为非阻塞
int old_option = fcntl(lfd, F_GETFL);
int new_option = old_option | O_NONBLOCK;
fcntl(lfd, F_SETFL, new_option);
//添加到epoll实例
ev.events = EPOLLIN | EPOLLET;
ev.data.fd = lfd;
epoll_ctl(efd, EPOLL_CTL_ADD, lfd, &ev);
while (1) {
int ret = epoll_wait(efd, events, MAX_EVENTS, -1);
if (ret < 0) {
exit(EXIT_FAILURE);
}
for (int i = 0; i < ret; ++i) {
struct epoll_event event = events[i];
int fd = event.data.fd;
if (event.data.fd == lfd) {
//有新的链接
struct sockaddr_in cliaddr;
bzero(&cliaddr, sizeof cliaddr);
socklen_t len = sizeof(cliaddr);
//获取到为新链接创建的socket的文件描述符
int cfd = accept(event.data.fd, (struct sockaddr *) &cliaddr, &len);
char buf[16] = {0};
//printf("accept new connect, ip:%s, port:%d\n", inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr, buf, 16),ntohs(cliaddr.sin_port));
//设为非阻塞
int oldOpt = fcntl(cfd, F_GETFL);
int newOpt = oldOpt | O_NONBLOCK;
fcntl(cfd, F_SETFL, newOpt);
struct epoll_event ev;
ev.data.fd = cfd;
ev.events = EPOLLIN | EPOLLET;
//添加epoll实例
epoll_ctl(efd, EPOLL_CTL_ADD, cfd, &ev);
} else {
if(!event.events & EPOLLIN){
continue;
}
char buf[BUF_SIZE];
while (1) {
memset(buf, 0, BUF_SIZE);
int len = recv(event.data.fd, buf, BUF_SIZE - 1, 0);
if (len < 0) {
//在linux进行非阻塞的socket接收数据时经常出现Resource temporarily unavailable
//EAGAIN,提示你的应用程序现在没有数据可读请稍后再试。
if ((errno == EAGAIN) || errno == EWOULDBLOCK) {
//printf("-------\n\n");
break;
}
perror("recv ");
exit(EXIT_FAILURE);
} else if (len == 0) {
if(epoll_ctl(efd, EPOLL_CTL_DEL, fd, NULL)==-1){
printf("file descriptor:%d\n",event.data.fd);
perror("EPOLL_CTL_DEL ");
exit(1);
}
close(event.data.fd);
break;
} else {
printf("%s", buf);
write(fd,buf,len);
//printf("file descriptor %d get %d bytes of content: %s", event.data.fd, len, buf);
}
}
}
}
}
return 0;
}
参考文章:
[1]百度百科

浙公网安备 33010602011771号