跟 select、poll 的对比
epoll 性能更高,Nginx、redis 等流行的软件,都是基于 epoll 实现的。epoll 优点有:
- 监听描述符数量大于 1024
- 只返回准备好的描述符,不需要浪费时间遍历
- 描述符集合基于红黑树实现,高效
使用步骤
epoll 有 3 个函数:
- epoll_create 指定 epoll 对应的红黑树的大概节点数,并返回 epoll 描述符
- epoll_ctl 控制 epoll 描述符,增加、删除、修改节点
- epoll_wait 开始监听
epoll_create
#include <sys/epoll.h>
int epoll_create(int size);
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_CTL_MOD:修改描述符的 event
- EPOLL_CTL_DEL:取消监听指定的描述符
- fd:要操作的描述符
- event:struct epoll_event 结构体类型,是跟 fd 描述符相关联的信息。其中 data 字段是 union epoll_data 联合体类型,可以存放 fd 用于回调,或存放回调函数的结构体指针
- events:要监听的事件。
- EPOLLIN:是否满足 read 操作
- EPOLLOUT:是否满足 write 操作
- EPOLLERR:是否出错
- EPOLLRDHUP:socket 对方关闭连接,或对方关闭写端
- events:要监听的事件。
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_wait
#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event *events,
int maxevents, int timeout);
int epoll_pwait(int epfd, struct epoll_event *events,
int maxevents, int timeout,
const sigset_t *sigmask);
demo
struct epoll_event evt;
struct epoll_event cbevt[10];
epfd = epoll_create(10);
evt.events = EPOLLIN | EPOLLET;
evt.data.fd = lfd; /* 假设 lfd 是要监听的描述符 */
epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &evt);
while(1) {
ret = epoll_wait(epfd, cbevt, 10, -1);
if (ret < 0) {
perror("epoll_wait");
return -1;
} else if (ret == 0) {
perror("peer closed");
return 0;
} else {
for (i = 0; i < ret; i++) {
if (cbevt[i].data.fd == lfd) {
/* 业务代码 */
}
}
}
}
水平触发、边沿触发
epoll 提供了两种触发模式,目的是减少对 epoll_wait 的调用次数,从而减少阻塞,减少在内核态和用户态之间切换的频率,提高效率。
- 边沿触发:EPOLL_ET(Edge Trigger),只有用户端发送数据过来才会触发
- 水平触发:EPOLL_LT(Level Trigger),缓冲区只要还是数据,就不停触发
假设客户端发送 1000B 数据,服务器首次取出 500B,此时,对于这两种触发模式有不同的表现:
- 边沿触发:不再从 epoll_wait 返回,直到用户下一次数据到达
- 水平触发:进入 epoll_wait 后立刻返回,直到服务器把所有数据都取出,才会阻塞
非阻塞 IO
Linux 文件 IO 操作时,除了普通文件和块设备文件外,都可以设置非阻塞 IO。有两种方式可以设置:
- 通过 open 系统调用打开文件时,指定 O_NONBLOCK 参数
- 对于已经打开的文件,通过 fcntl 设置 O_NONBLOCK 参数
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
open(path, O_NONBLOCK);
flag = fcntl(fd, F_GETFL);
flag |= O_NONBLOCK;
fcntl(fd, F_SETFL, O_NONBLOCK);
代码示例
下面代码创建了十个套接字,并在父进程中监听可读状态,在子进程中随机写入。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/epoll.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#define MAX_FILE 10
void err_exit(const char* str) {
perror(str);
exit(EXIT_FAILURE);
}
int main() {
int i, ret;
int epfd;
int pipefds[10][2];
struct epoll_event cbevt[100];
struct epoll_event evt;
for (i = 0; i < 10; i++) {
ret = pipe(pipefds[i]);
if (ret < 0)
err_exit("pipe");
}
ret = fork();
if (ret < 0) {
err_exit("fork");
} else if (ret == 0) {
char buf[] = "hello world\n";
for (i = 0; i < 10; i++) {
close(pipefds[i][0]);
}
while(1) {
ret = rand() % 10;
printf("child write pipe %d \n", ret);
write(pipefds[ret][1], buf, sizeof(buf));
sleep(1);
}
} else {
char rdbuf[BUFSIZ];
for (i = 0; i < 10; i++) {
close(pipefds[i][1]);
}
epfd = epoll_create(MAX_FILE);
if (epfd < 0)
err_exit("epoll_create");
for (i = 0; i < 10; i++) {
evt.events = EPOLLIN;
evt.data.fd = pipefds[i][0];
epoll_ctl(epfd, EPOLL_CTL_ADD, pipefds[i][0], &evt);
}
while(1) {
ret = epoll_wait(epfd, cbevt, 100, -1);
printf("ret of epoll_wait is %d\n", ret);
if (ret < 0) {
err_exit("epoll_wait");
} else if (ret == 0) {
puts("time out");
} else {
for (i = 0; i < ret; i++) {
printf("ret is:%d\n", ret);
read(cbevt[i].data.fd, rdbuf, sizeof(rdbuf));
printf("read res is:%s\n", rdbuf);
}
}
}
}
return 0;
}