一、概念
epoll
全称 eventpoll
,是 linux 内核实现IO多路复用的一个实现。
epoll
是 select
和 poll
的升级版,相较于这两个,epoll
改进了工作方式,因此它更加高效。
- 对于待检测集合
select
和poll
是基于线性方式处理的,需要线性遍历;epoll
是基于红黑树来管理待检测集合的; select
和poll
每次都会线性扫描整个待检测集合,集合越大速度越慢;epoll
使用的是回调机制,效率高,处理效率也不会随着检测集合的变大而下降;- 我们需要对
select
和poll
返回的集合进行判断才能知道哪些文件描述符是就绪的;而通过epoll
可以直接得到已就绪的文件描述符集合,无需再次检测; - 使用
epoll
没有最大文件描述符的限制,仅受系统中进程能打开的最大文件数目限制;
当连接数量比较大,IO处理比较频繁的时候,此时再使用 select
和 poll
效率就比较低了。这时最好使用 epoll
。
二、函数原型
epoll
常用的 API 函数一共有三个:
#include <sys/epoll.h>
// 创建epoll实例,通过一棵红黑树管理待检测集合
int epoll_create(int size);
// 管理红黑树上的文件描述符(添加、修改、删除)
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
// 检测epoll树中是否有就绪的文件描述符
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
1.epoll_create
是创建一个红黑树的实例,用来管理待检测的文件描述符的集合。
int epoll_create(int size);
size
:在Linux 2.6.8
版本以后,这个参数是被忽略的,只需要指定一个大于 0 0 0 的数即可;
函数返回值:-
- 失败,返回 − 1 -1 −1;
-
- 成功,返回一个有效的文件描述符
epfd
,通过这个文件描述符就能访问epoll
实例了;
- 成功,返回一个有效的文件描述符
2.epoll_ctl
是管理 epoll
实例上的节点,可以对其进行增加、修改、删除操作。
// 联合体, 多个变量共用同一块内存
typedef union epoll_data {
void *ptr;
int fd; // 通常情况下使用这个成员, 和epoll_ctl的第三个参数相同即可
uint32_t u32;
uint64_t u64;
} epoll_data_t;
struct epoll_event {
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epfd
:epoll_create()
的返回值,通过epfd
就能访问epoll
实例模型;op
:这是一个枚举量,通过它控制epoll_ctl
进行对应的处理:-
EPOLL_CTL_ADD
:往epoll模型中添加新的节点
-
EPOLL_CTL_MOD
:修改epoll模型中已经存在的节点
-
EPOLL_CTL_DEL
:删除epoll模型中的指定的节点
fd
:即要添加、修改、删除的文件描述符;event
:epoll
事件,用来指定给这个文件描述符fd
对应的事件;-
events
:委托epoll
检测的事件。
-
-
EPOLLIN
:读事件,检测读缓冲区是否有数据;
-
-
-
EPOLLOUT
:写事件,检测写缓冲区是否有容量;
-
-
-
EPOLLERR
:异常事件;
-
-
data
:用户数据变量。这是一个联合体类型,我们通常使用里面的fd
,用于存储待检测的文件描述符的值。在调用epoll_wait()
的时候,这个值会被传出来;
函数返回值:
- 成功,返回 0 0 0;
- 失败,返回 − 1 -1 −1;
epoll_wait
是检测被创建的epoll
实例中有没有就绪的文件描述符。
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
epfd
:epoll_create()
的返回值,通过epfd
就能访问epoll
实例模型;events
:这是一个传出参数,它是一个结构体数组的地址,里面存储了已经就绪的文件描述的信息;maxevents
:修饰events
,表示其结构体数组的容量;timeout
:如果待检测的epoll
实例中没有就绪的文件描述符,那么就会阻塞timeout
毫秒。-
-
0
0
0 ,函数不阻塞。不管
epoll
实例中有没有已经就绪的文件描述符,函数执行完毕之后都直接返回;
-
0
0
0 ,函数不阻塞。不管
-
-
>
0
> 0
>0,如果
epoll
实例中没有就绪的文件描述符,函数就会阻塞timeout
毫秒再返回;
-
>
0
> 0
>0,如果
-
-
−
1
-1
−1,函数一直阻塞,直到
epoll
实例中有文件描述符就绪就解除阻塞;
-
−
1
-1
−1,函数一直阻塞,直到
函数返回值:
- 成功:
-
- 0 0 0,函数是被强制解除阻塞了,没有检测到就绪的文件描述符;
-
- > 0 > 0 >0,检测到的已经就绪的文件描述符的总数量;
- 失败,返回 − 1 -1 −1;
三、epoll的使用
在服务端使用 epoll
的步骤如下:
1.创建用于监听的文件描述符 lfd
。
//使用 ipv4 , TCP协议
int lfd = socket(AF_INET, SOCK_STREAM, 0);
2.设置端口复用(可选可不选)。
int opt = 1;
setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
3.绑定 ip
与 端口号。
int ret = bind(lfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
4.设置监听。
listen(lfd, 128);
5.创建 epoll
实例。
int epfd = epoll_create(100);
6.将用于监听的文件描述符 lfd
添加到 epoll
实例中去。
struct epoll_event ev;
ev.events = EPOLLIN; // 检测lfd读读缓冲区是否有数据
ev.data.fd = lfd;
int ret = epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);
7.检测 epoll
实例中是否有文件描述符就绪了,并对这些就绪的文件描述符做相应的处理。
//cnt 是 epoll 实例中已经就绪的文件描述符的数量
int cnt= epoll_wait(epfd, evs, size, -1);
8.如果是用于监听的文件描述符 lfd
就绪了,那么就和客户端建立连接,将得到的用于通信的文件描述符 cfd
添加到 epoll
实例中。
int cfd = accept(lfd, NULL, NULL);
ev.events = EPOLLIN;
ev.data.fd = cfd;
// 新得到的文件描述符添加到epoll模型中, 下一轮循环的时候就可以被检测了
epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);
9.如果是用于通信的文件描述符 cfd
就绪了,那么就和客户端进行通信。如果连接已经断开,就将用于通信的文件描述符 cfd
从 epoll
实例中删除,并且关闭这个文件描述符。
int len = recv(curfd, buf, sizeof(buf), 0);
if(len == 0)
{
// 将这个文件描述符从epoll模型中删除
epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL);
close(curfd);
}
else if(len > 0)
{
send(curfd, buf, len, 0);
}
10.重复第 7 7 7 步。
1.代码
客户端的代码:
#include <stdio.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
int main(){
//1.创建用于通信的文件描述符 cfd
int cfd = socket(AF_INET,SOCK_STREAM,0);
if(cfd == -1){
perror("socket");
return -1;
}
printf("1.成功创建了用于通信的文件描述符 : %d\n",cfd);
//2.连接服务器
unsigned short port = 10000;
const char* ip = "10.0.8.14";
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
inet_pton(AF_INET,ip,&addr.sin_addr.s_addr);
int ret = connect(cfd,(struct sockaddr*)&addr,sizeof(addr));
if(ret == -1){
perror("connet");
return -1;
}
printf("2.成功连接了服务器 , ip : %s , port : %d\n",ip,port);
//3.开始通信
char send_buf[1024];
char recv_buf[1024];
int cnt = 0;
while(1){
memset(send_buf,0,sizeof send_buf);
memset(recv_buf,0,sizeof recv_buf);
sprintf(send_buf,"hello i love you : %d",cnt++);
//发送数据
send(cfd,send_buf,strlen(send_buf) + 1,0);
//接收数据
int len = recv(cfd,recv_buf,sizeof(recv_buf),0);
if(len > 0){
printf("服务端 : %s\n",recv_buf);
}
else if(len == 0){
printf("服务端已经断开了连接...\n");
break;
}
else{
perror("recv");
break;
}
sleep(1);
}
close(cfd);
return 0;
}
服务端的代码:
#include <stdio.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <pthread.h>
#include <strings.h>
#include <string.h>
#include <stdlib.h>
#include <sys/epoll.h>
int main()
{
// 1.创建用于监听的文件描述符
int lfd = socket(AF_INET, SOCK_STREAM, 0);
if (lfd == -1)
{
perror("socket");
return -1;
}
// 2.绑定 ip 和 端口号
unsigned short port = 10000;
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(port);
saddr.sin_addr.s_addr = INADDR_ANY;
int ret = bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));
if (ret == -1)
{
perror("bind");
return -1;
}
// 3.设置监听
ret = listen(lfd, 128);
if (ret == -1)
{
perror("listen");
return -1;
}
printf("设置监听成功...\n");
// 4.获取连接
// 创建一个 epoll 模型
int epfd = epoll_create(1);
if (epfd == -1)
{
perror("epoll_create");
exit(0);
}
// 往 epoll 模型中添加节点 , 目前只有用于监听的文件描述符
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = lfd;
ret = epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);
if (ret == -1)
{
perror("epoll_ctl");
exit(0);
}
struct epoll_event evs[1024];
int len = sizeof(evs) / sizeof(evs[0]);
int addr_len = sizeof(struct sockaddr_in);
char buf[1024];
char *str = "ok";
while (1)
{
int cnt = epoll_wait(epfd, evs, len, -1);
for (int i = 0; i < cnt; i++)
{
// 取出当前文件描述符 cur_fd
int cur_fd = evs[i].data.fd;
// 这个文件描述符 是用于监听的文件描述符
if (cur_fd == lfd)
{
struct sockaddr_in addr;
// 获取连接 , 返回用于通信的文件描述符 cfd
int cfd = accept(cur_fd, (struct sockaddr *)&addr, &addr_len);
// 把用于通信的文件描述符 cfd 添加到 epoll 模型中
ev.events = EPOLLIN;
ev.data.fd = cfd;
ret = epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);
if (ret == -1)
{
perror("epoll_ctl ... accept");
exit(0);
}
printf("获取连接成功 , 客户端 ip : %s , port : %d\n", inet_ntoa(addr.sin_addr), ntohs(addr.sin_port));
}
else
{
// 开始通信
memset(buf, 0, sizeof buf);
int len = read(cur_fd, buf, sizeof buf);
printf("客户端 : %s\n", buf);
if (len > 0)
{
write(cur_fd, str, strlen(str) + 1);
}
else if (len == 0)
{
// 客户端已经关闭了连接
printf("客户端已经关闭了连接...\n");
epoll_ctl(epfd,EPOLL_CTL_DEL,cur_fd,NULL);
close(cur_fd);
}
else
{
perror("read");
exit(0);
}
}
}
}
close(lfd);
return 0;
}
四、epoll的工作模式
1.水平工作模式
水平模式,即 LT(level triggered)
,是默认的工作模式,并且同时支持 block socket
和 no block socket
。在这种工作模式下,内核会通知调用者那些文件描述符已经就绪了,我们就可以的对这些已经就绪的文件描述符做操作。
水平模式的特点:
读事件: 如果文件描述符对应的读缓冲区还有数据,读事件就会被触发,epoll_wait()
解除阻塞。
- 当读事件被触发,
epoll_wait()
解除阻塞,之后就可以接收数据了; - 如果接收数据的缓冲区很小,不能全部将缓冲区数据读出,那么读事件会继续被触发,直到数据被全部读出;如果接收数据的缓冲区相对较大,读数据的效率也会相对较高(减少了读数据的次数);
- 因为读数据是被动的,必须要通过读事件才能知道有数据到达了,因此对于读事件的检测是必须的;
写事件: 如果文件描述符对应的写缓冲区可写,写事件就会被触发,epoll_wait()
解除阻塞。
- 当写事件被触发,
epoll_wait()
解除阻塞,之后就可以将数据写入到写缓冲区了; - 写事件的触发发生在写数据之前而不是之后,被写入到写缓冲区中的数据是由内核自动发送出去的;
- 如果写缓冲区没有被写满,写事件会一直被触发;
- 因为写数据是主动的,并且写缓冲区一般情况下都是可写的(缓冲区不满),因此对于写事件的检测不是必须的;
2.边沿工作模式
边沿模式,即 ET(edge-triggered)
是高速工作方式,只支持 no-block socket
。
在这种模式下,当文件描述符从 未就绪 变为 就绪 时,内核会通过 epoll
通知调用者。然后它会假设调用者知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知(只会发送一次)。如果我们对这个文件描述符做IO操作,从而导致它再次变成未就绪,当这个未就绪的文件描述符再次变成就绪状态,内核会再次进行通知,并且还是只通知一次。
ET模式在很大程度上减少了 epoll
事件被重复触发的次数,因此效率要比LT模式高。
边沿模式的特点:
读事件: 当读缓冲区有新的数据进入,读事件被触发一次,没有新数据不会触发该事件
- 如果有新数据进入到读缓冲区,读事件被触发,
epoll_wait()
解除阻塞; - 读事件被触发,可以通过调用
read()/recv()
函数将缓冲区数据读出; - 如果数据没有被全部读走,并且没有新数据进入,读事件不会再次触发,只通知一次;
- 如果数据被全部读走或者只读走一部分,此时有新数据进入,读事件被触发,并且只通知一次;
写事件: 当写缓冲区状态可写,写事件只会触发一次;
- 如果写缓冲区被检测到可写,写事件被触发,
epoll_wait()
解除阻塞; - 写事件被触发,就可以通过调用
write()/send()
函数,将数据写入到写缓冲区中; - 写缓冲区从 不满 到 被写满,期间写事件只会被触发一次;
- 写缓冲区从 满 到 不满,状态变为可写,写事件只会被触发一次;
综上所述:epoll
的边沿模式下 epoll_wait()
检测到文件描述符有新事件才会通知,如果不是新的事件就不通知,通知的次数比水平模式少,效率比水平模式要高。
1.ET工作模式的设置
边沿模式不是默认的 epoll
模式,需要额外进行设置。
epoll
设置边沿模式是非常简单的,epoll
管理的红黑树示例中每个节点都是 struct epoll_event
类型,只需要将 EPOLLET
添加到结构体的 events
成员中即可:
struct epoll_event ev;
ev.events = EPOLLIN | EPOLLET; // 设置边沿模式
代码如下:
int num = epoll_wait(epfd, evs, size, -1);
for(int i=0; i<num; ++i)
{
// 取出当前的文件描述符
int curfd = evs[i].data.fd;
// 判断这个文件描述符是不是用于监听的
if(curfd == lfd)
{
// 建立新的连接
int cfd = accept(curfd, NULL, NULL);
// 新得到的文件描述符添加到epoll模型中, 下一轮循环的时候就可以被检测了
// 读缓冲区是否有数据, 并且将文件描述符设置为边沿模式
struct epoll_event ev;
ev.events = EPOLLIN | EPOLLET;
ev.data.fd = cfd;
ret = epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);
if(ret == -1)
{
perror("epoll_ctl-accept");
exit(0);
}
}
}
2.设置非阻塞
对于写事件的触发一般情况下是不需要进行检测的,因为写缓冲区大部分情况下都是有足够的空间可以进行数据的写入。
对于读事件的触发就必须要检测了,因为服务器也不知道客户端什么时候发送数据,如果使用 epoll
的 边沿模式 进行读事件的检测,有新数据达到只会通知一次,那么必须要保证得到通知后将数据全部从读缓冲区中读出。那么,应该如何读这些数据呢?
方式1:准备一块特别大的内存,用于存储从读缓冲区中读出的数据,但是这种方式有很大的弊端:
- 内存的大小没有办法界定,太大浪费内存,太小又不够用;
- 系统能够分配的最大堆内存也是有上限的,栈内存就更小;
方式2:循环接收数据
int len = 0;
while((len = recv(curfd, buf, sizeof(buf), 0)) > 0)
{
// 数据处理...
}
这样做也是有弊端的,因为套接字操作默认是阻塞的,当读缓冲区数据被读完之后,读操作就阻塞了也就是调用的 read()/recv()
函数被阻塞了,当前进程/线程 被阻塞之后就无法处理其他操作了。
要解决阻塞问题,就需要将套接字默认的阻塞行为修改为非阻塞,需 要使用 fcntl()
函数进行处理:
// 设置完成之后, 读写都变成了非阻塞模式
int flag = fcntl(cfd, F_GETFL);
flag |= O_NONBLOCK;
fcntl(cfd, F_SETFL, flag);
通过上述分析就可以得出一个结论:epoll
在 边沿模式 下,必须要将套接字设置为非阻塞模式。
但是,这样就会引发另外的一个bug,在非阻塞模式下,循环地将读缓冲区数据读到本地内存中,当缓冲区数据被读完了,调用的 read()/recv()
函数还会继续从缓冲区中读数据,此时函数调用就失败了,返回
−
1
-1
−1,对应的全局变量 errno
值为 EAGAIN
或者 EWOULDBLOCK
。如果打印错误信息会得到如下的信息:Resource temporarily unavailable
// 非阻塞模式下recv() / read()函数返回值 len == -1
int len = recv(curfd, buf, sizeof(buf), 0);
if(len == -1)
{
if(errno == EAGAIN)
{
printf("数据读完了...\n");
}
else
{
perror("recv");
exit(0);
}
}
3.代码
#include <stdio.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <pthread.h>
#include <strings.h>
#include <string.h>
#include <stdlib.h>
#include <sys/epoll.h>
#include <ctype.h>
#include <fcntl.h>
#include <errno.h>
int main()
{
// 1.创建用于监听的文件描述符
int lfd = socket(AF_INET, SOCK_STREAM, 0);
if (lfd == -1)
{
perror("socket");
return -1;
}
// 2.绑定 ip 和 端口号
unsigned short port = 10000;
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(port);
saddr.sin_addr.s_addr = INADDR_ANY;
// 设置端口复用
int opt = 1;
setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
int ret = bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));
if (ret == -1)
{
perror("bind");
return -1;
}
// 3.设置监听
ret = listen(lfd, 128);
if (ret == -1)
{
perror("listen");
return -1;
}
printf("设置监听成功...\n");
// 4.获取连接
// 创建一个 epoll 模型
int epfd = epoll_create(1);
if (epfd == -1)
{
perror("epoll_create");
exit(0);
}
// 往 epoll 模型中添加节点 , 目前只有用于监听的文件描述符
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = lfd;
ret = epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);
if (ret == -1)
{
perror("epoll_ctl");
exit(0);
}
struct epoll_event evs[1024];
int size = sizeof(evs) / sizeof(evs[0]);
int addr_len = sizeof(struct sockaddr_in);
char buf[5];
while (1)
{
int cnt = epoll_wait(epfd, evs, size, -1);
for (int i = 0; i < cnt; i++)
{
// 取出当前文件描述符 cur_fd
int cur_fd = evs[i].data.fd;
// 这个文件描述符 是用于监听的文件描述符
if (cur_fd == lfd)
{
struct sockaddr_in addr;
// 获取连接 , 返回用于通信的文件描述符 cfd
int cfd = accept(cur_fd, (struct sockaddr *)&addr, &addr_len);
// 设置用于通信的文件描述符 cfd 的属性为非阻塞
int flag = fcntl(cfd, F_GETFL);
flag |= O_NONBLOCK;
fcntl(cfd, F_SETFL, flag);
// 把用于通信的文件描述符 cfd 添加到 epoll 模型中 , 设置为边沿模式
ev.events = EPOLLIN | EPOLLET;
ev.data.fd = cfd;
ret = epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);
if (ret == -1)
{
perror("epoll_ctl ... accept");
exit(0);
}
printf("获取连接成功 , 客户端 ip : %s , port : %d\n", inet_ntoa(addr.sin_addr), ntohs(addr.sin_port));
}
else
{
// 开始通信
memset(buf, 0, sizeof buf);
while (1)
{
int len = read(cur_fd, buf, sizeof buf);
if (len > 0)
{
printf("客户端 : %s\n", buf);
for (int i = 0; i < len; i++)
buf[i] = toupper(buf[i]);
write(cur_fd, buf, strlen(buf) + 1);
}
else if (len == 0)
{
// 客户端已经关闭了连接
printf("客户端已经关闭了连接...\n");
epoll_ctl(epfd, EPOLL_CTL_DEL, cur_fd, NULL);
close(cur_fd);
break;
}
else
{
if(errno == EAGAIN){
printf("数据读完了...\n");
break;
}
else{
perror("read");
exit(0);
}
}
}
}
}
}
close(lfd);
return 0;
}