1. epoll
epoll是为克服select、poll每次监听都需要在用户、内核空间反复拷贝,以及需要用户程序自己遍历发现有变化的文件描述符的缺点的多路IO复用技术。
epoll原理
创建内核空间的红黑树;
将需要监听的文件描述符上树;
内核监听红黑树上文件描述符的变化;
返回有变化的文件描述符。
epoll优点
① 无需在用户、内核空间反复拷贝数据;
② 内核返回发生变化的文件描述符,无需用户遍历所有文件描述符。
2. epoll API
(1)epoll_create 创建红黑树
#include<sys/epoll.h>
int epoll_create(int size);
/*
功能:
创建内核中的epoll红黑树;
参数:
size:监听的文件描述符上限,kernel 2.6版本后写1即可,会自动扩展。
返回值:
成功:返回红黑树的句柄(相当于操作树的入口)。
失败:-1,会设置errno。
*/
(2)epoll_ctl 上树、下树、修改节点的监听事件
#include<sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event);
/*
功能:
上树、下树、修改节点。
参数:
epfd:红黑树的句柄,epoll_create的返回值。
op:对文件描述符fd的操作
EPOLL_CTL_ADD:将文件描述符fd上树
EPOLL_CTL_MOD:修改文件描述符fd的事件
EPOLL_CTL_DEL:将文件描述符fd下树
fd:op要操作的文件描述符
event:用于对特定的文件描述符事件进行设置。
返回值:
成功:
失败:
*/
struct epoll_event {
uint32_t events; // 监听的事件
epoll_data_t data; // 需要监听的文件描述符(共用体中的fd)
}
/*
参数 events:
EPOLLIN:读事件
EPOLLOUT:写事件
*/
typedef union epoll_data {
void* ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
(2)epoll_wait 监听
#include<sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout);
/*
功能:
监听红黑树上文件描述符的变化;
参数:
epfd:红黑树的句柄(epoll_create的返回值);
events:接收发送变化的文件描述符的数组地址
maxevents:数组元素的个数
timeout:
> 0:监听超时时间(多久监听一次);
0:无文件描述符变化则立即返回;
-1:阻塞监听到有文件描述符变化才返回
返回值:
成功:0表示没有文件描述符发生变化;否则返回发生变化的文件描述符的个数
失败:-1,调用错误,会设置errno
*/
3. epoll使用示例
(1)监听管道
子进程每3s向管道写数据,父进程使用epoll监听管道,有数据可读则读出管道中的数据。
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/epoll.h>
int main(int argc, const char* argv[]) {
int fd[2];
pipe(fd);
pid_t pid;
pid = fork();
if (0 == pid) { // 子进程
close(fd[0]);
char buf[5];
char ch = 'a';
while (1) {
memset(buf, ch++, 5);
write(fd[1], buf, 5);
sleep(3);
}
} else { // 父进程
close(fd[1]);
// 创建红黑树
int epfd = epoll_create(1);
// 上树
struct epoll_event ev;
ev.data.fd = fd[0];
ev.events = EPOLLIN;
epoll_ctl(epfd, EPOLL_CTL_ADD, fd[0], &ev); // 将fd[0]上树,同时使用ev结构体来设置监听fd[0]的读事件。
// 监听
struct epoll_event evs[1]; // 接收从内核返回的有变化的文件描述符的数组。
while (1) {
int n = epoll_wait(epfd, evs, 1, -1);
if (1 == n) {
char buf[64] = "";
n = read(fd[0], buf, 64);
if (n <= 0) {
printf("子进程关闭了写端");
close(fd[0]);
epoll_ctl(epfd, EPOLL_CTL_DEL, fd[0], &ev); // 下树
break;
} else {
printf("读到子进程写的内容:%s\n", buf);
}
}
}
}
return 0;
}
运行结果:
(2)epoll实现简单并发服务器示例:
#include<stdio.h>
#include<sys/epoll.h>
#include"wrap.h"
int main(int argc, const char* argv[]) {
// 1.创建socket,绑定
int lfd = tcp4bind(8888, NULL);
// 2.监听
Listen(lfd, 128);
// 3.创建树
int epfd = epoll_create(1);
// 5.将lfd上树
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = lfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);
// 4.while epoll_wait监听
struct epoll_event evs[1024];
while (1) {
int n = epoll_wait(epfd, evs, 1024, -1); // 阻塞监听到有文件描述符变化才返回
if (n < 0) { // 调用出错
perror("epoll_wait");
break;
} else if (0 == n) {
continue;
} else { // 有文件描述符变化
for (int i = 0;i < n;i++) {
// 若lfd有读事件
if (evs[i].data.fd == lfd && evs[i].events & EPOLLIN) {
struct sockaddr_in cliaddr;
char ip[16] = "";
socklen_t len = sizeof(cliaddr);
int cfd = Accept(lfd, (struct sockaddr*)&cliaddr, &len); // 提取
printf("新连接到来:IP = %s, port = %d\n",
inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr, ip, 16),
ntohs(cliaddr.sin_port));
ev.data.fd = cfd;
ev.events = EPOLLIN;
epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev); // cfd上树;监听cfd的读事件
} else if (evs[i].events & EPOLLIN) { // 若cfd有读事件
char buf[1024] = "";
int n = read(evs[i].data.fd, buf, 1024);
if (n < 0) { // 出错
perror("read");
close(evs[i].data.fd);
epoll_ctl(epfd, EPOLL_CTL_DEL, evs[i].data.fd, &evs[i]); // 下树
} else if (0 == n) { // 客户端关闭
printf("客户端关闭.\n");
close(evs[i].data.fd);
epoll_ctl(epfd, EPOLL_CTL_DEL, evs[i].data.fd, &evs[i]); // 下树
} else {
write(STDOUT_FILENO, buf, 1024); / printf依赖于字符串终止符'\0',而write输出指定长度的字符串。
}
}
}
}
}
return 0;
}
运行结果:
4. epoll的两种工作方式
epoll有两种工作方式:水平触发(LT)、边缘触发(ET)。
(1)水平触发
如监听文件描述符的读缓冲区时,只要读缓冲区有数据就会触发epoll_wait。例如读缓冲区有数据,只要没读干净,就会触发epoll_wait。
如监听文件描述符的写缓冲区时,只要可写就会触发epoll_wait,因此监听写缓冲区时推荐使用边缘触发。
(2)边缘触发
如监听文件描述符的读缓冲区时,读缓冲区有数据到来才会触发epoll_wait;与水平触发不一样,缓冲区数据没读干净且无数据到来,则下次不会再触发epoll_wait,因此要求一次性将读缓冲区数据读干净。
如监听文件描述符的写缓冲区时,写缓冲区数据从有到无才会触发epoll_wait。
epoll默认工作方式为水平触发,但推荐使用边缘触发,以减少epoll_wait系统调用次数。
5. epoll的边缘触发使用示例
使用边缘触发,主要两点:1. 监听的事件加上边缘触发的属性;2. 只要触发就一次性将事情处理完。
1. 监听的事件加上边缘触发的属性
无需将监听的文件描述符设置为边缘触发,而是将与客户端通信的文件描述符设置为边缘触发,需将上面的 “epoll实现简单并发服务器示例” 代码第44行:
ev.events = EPOLLIN;
改为如下,即加上边缘触发的属性。
ev.events = EPOLLIN | EPOLLET;
2. 只要触发就一次性将事情处理完
以读事件为例,将上面的 “epoll实现简单并发服务器示例” 一次性读取字节数由1024B变为4B,则大多数情况下无法一次read调用就读完缓冲区中所有数据,因此循环读取缓冲区,直至读完。
由于是循环读取,直至读完,因此文件描述符cfd需要设置为非阻塞,否则循环到最后一次无数据可读时,read函数将阻塞,无法返回继续监听。
而水平触发时,结合上面代码,read是阻塞的,但通常不会阻塞住。因为是只要有数据可读就会触发epoll_wait,无数据就不会触发,因此不会阻塞住。
上述 "epoll实现简单并发服务器示例" 改为边缘触发:
#include<stdio.h>
#include<sys/epoll.h>
#include<fcntl.h>
#include"wrap.h"
int main(int argc, const char* argv[]) {
// 1.创建socket,绑定
int lfd = tcp4bind(8888, NULL);
// 2.监听
Listen(lfd, 128);
// 3.创建树
int epfd = epoll_create(1);
// 5.将lfd上树
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = lfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);
// 4.while epoll_wait监听
struct epoll_event evs[1024];
while (1) {
int n = epoll_wait(epfd, evs, 1024, -1); // 阻塞监听到有文件描述符变化才返回
if (n < 0) { // 调用出错
perror("epoll_wait");
break;
} else if (0 == n) {
continue;
} else { // 有文件描述符变化
for (int i = 0;i < n;i++) {
// 若lfd有读事件
if (evs[i].data.fd == lfd && evs[i].events & EPOLLIN) {
struct sockaddr_in cliaddr;
char ip[16] = "";
socklen_t len = sizeof(cliaddr);
int cfd = Accept(lfd, (struct sockaddr*)&cliaddr, &len); // 提取
/* 设置cfd非阻塞 */
int flag = fcntl(cfd, F_GETFL);
flag |= O_NONBLOCK;
fcntl(cfd, F_SETFL, flag);
printf("新连接到来:IP = %s, port = %d\n",
inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr, ip, 16),
ntohs(cliaddr.sin_port));
ev.data.fd = cfd;
ev.events = EPOLLIN | EPOLLET;
epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev); // cfd上树;监听cfd的读事件
} else if (evs[i].events & EPOLLIN) { // 若cfd有读事件
while (1) { // 循环读取
char buf[4] = "";
/*缓冲区无数据时,以阻塞的方式读取,则会阻塞等待;若以非阻塞的方式读取,则返回-1,并且设置errno为EAGAIN*/
int n = read(evs[i].data.fd, buf, 4);
if (n < 0) { // 出错
if (EAGAIN == errno) { // 缓冲区被读干净,则继续下一次监听
break;
}
perror("read");
close(evs[i].data.fd);
epoll_ctl(epfd, EPOLL_CTL_DEL, evs[i].data.fd, &evs[i]); // 下树
break;
} else if (0 == n) { // 客户端关闭
printf("客户端关闭.\n");
close(evs[i].data.fd);
epoll_ctl(epfd, EPOLL_CTL_DEL, evs[i].data.fd, &evs[i]); // 下树
break;
} else {
write(STDOUT_FILENO, buf, 4); // printf依赖于字符串终止符'\0',而write输出指定长度的字符串。
}
}
}
}
}
}
return 0;
}
运行结果: