epoll
本质上红黑树 :节点多或少不影响epoll的效率。
epoll 使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的 copy 只需一次。epoll可以理解为event poll,不同于忙轮询和无差别轮询,epoll会把哪个流发生了怎样的I/O事件通知我们。所以我们说epoll实际上是 事件驱动(每个事件关联上fd)的,此时我们对这些流的操作都是有意义的。(复杂度降低到了O(1))
优点
1、select和poll工作过程中存在内核/用户空间数据的频繁拷贝问题,在epoll中内核和用户区使用的是共享内存(基于mmap内存映射区实现),省去了不必要的内存拷贝。
2、程序猿需要对select和poll返回的集合进行判断才能知道哪些文件描述符是就绪的,通过epoll可以 直接得到已就绪的文件描述符集合 ,无需再次检测。
3、使用 epoll 没有最大文件描述符的限制,仅受系统中进程能打开的最大文件数目限制。
epoll的操作函数
#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);
任务的添加和检测是拆分开的。
epoll_create()
int epoll_create(int size);
epoll_create() 函数的作用是创建一个红黑树模型的实例,用于管理待检测的文件描述符的集合。
失败:返回 - 1
成功:返回一个有效的文件描述符,通过这个文件描述符就可以访问创建的 epoll 实例
epoll_ctl
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
它完成的工作就是把fd和epoll_event 结构体一并记录在epoll树上;
-
epfd:epoll树的实例,epoll_create()的返回值
-
op:对epfd的操作
EPOLL_CTL_ADD:往 epoll 模型中添加新的节点
EPOLL_CTL_MOD:修改 epoll 模型中已经存在的节点
EPOLL_CTL_DEL:删除 epoll 模型中的指定的节点 -
fd:文件描述符,即要添加 / 修改 / 删除的文件描述符
-
event:epoll 事件,用来修饰第三个参数对应的文件描述符的,指定检测这个文件描述符的什么事件;
EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
EPOLLOUT:表示对应的文件描述符可以写;
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
data:用户数据变量,需要自己初始化,这是一个联合体类型,通常情况下使用里边的 fd 成员,用于存储待检测的文件描述符的值,在调用 epoll_wait() 函数的时候这个值会被传出。
/ 联合体, 多个变量共用同一块内存
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 */
};
epoll_data_t data:用户数据,对内核没有作用;
每一个epoll实例都对应一个文件描述符fd,他们之间的对应关系通过epoll里面的data实现的;而events表示这个文件描述符fd需要被检测什么事件。
当把epoll_event传递给内核,内核检测到这个文件描述符对应的事件触发了,会修改并传出这个epoll_event(通过epoll_wait的传参是采用指针实现的);这个epoll_event里面的对应的data数据就是我们传进去而指定的数据。
一般data使用int fd;相当于这个data是储存的就是对这个事件的备注信息:这个事件属于哪一个文件描述符;
当数据较多的时候,可以创建一块堆内存,由ptr指向data。
epoll_wait
epoll_wait() 检测所有添加到epoll树上的节点是否已经处于就绪状态;如果有就绪状态的fd,它就会返回有哪些fd变成就绪。
如果没有会一直保持阻塞。
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
-
epfd:epoll_create () 函数的返回值,通过这个参数找到 epoll 实例;
-
events:传出参数;events则是分配好的 epoll_event结构体数组,epoll将会把发生的事件 复制(所以这些被复制的epoll_event是之前就写好的) 到 events数组中(events不可以是空指针,内核只负责把数据复制到这个 events数组中,不会去帮助我们在用户态中分配内存;内核这种做法效率很高)。
所以我们可以通过读epoll_event 中的data知道这个事件对应的是哪一个文件描述符。
返回值等于数组的大小。 -
maxevents:修饰第二个参数,结构体数组的容量(元素个数) ;他是一个数组的首地址。
-
timeout:实例中没有已就绪的文件描述符,该函数阻塞的时长,单位 ms 毫秒
0:函数不阻塞,不管 epoll 实例中有没有就绪的文件描述符,函数被调用后都直接返回;
大于 0:如果 epoll 实例中没有已就绪的文件描述符,函数阻塞对应的毫秒数再返回;
-1:函数一直阻塞,直到 epoll 实例中有已就绪的文件描述符之后才解除阻塞 -
函数返回值:
成功:
等于 0:函数是阻塞被强制解除了,没有检测到满足条件的文件描述符;
大于 0:检测到的已就绪的文件描述符的总个数,且第二个参数有若干有效的元素,且等于总个数。
失败:返回 - 1
epoll的使用
操作步骤
1、设置服务器性质:设置套接字、端口复用、绑定;
int lfd = socket(AF_INET, SOCK_STREAM, 0);
int opt = 1;
setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
int ret = bind(lfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
2、注册监听用lfd
listen(lfd, 128);
3、创建epoll树
int epfd = epoll_create(100);
epoll的实例是一个int型数据,也是一个文件描述符。
4、将用于监听的套接字添加到epoll实例中
epoll_event :epoll的实例;
epfd :epoll树。
struct epoll_event ev;
ev.events = EPOLLIN; // 检测lfd读读缓冲区是否有数据
ev.data.fd = lfd;
int ret = epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);
ev.data.fd = lfd:每一个文件描述符都需要一个epoll_event 去表述它,所以给每一个fd创建一个epoll_event 实例并完成初始化;
ev.events = EPOLLIN:初始化决定对这个文件描述符需要监视它的什么事件。 ;
注意:ev.data.fd的值要和第四个参数文件描述符保持一致。
检测添加到epoll实例中的文件描述符是否已就绪,并将这些已就绪的文件描述符进行处理
5、创建epoll实例的数组,这个数组作为参数传入内核,经过修改再传出;这个数组的大小等于被检测出的就绪文件描述符个数。
epoll_ctl会把编写好的epoll_event放入epoll树中,以供之后提取;
epoll_wait会把就绪的epoll_event提取出来,放入数组中。
struct epoll_event evs[1024];
int size = sizeof(evs) / sizeof(struct epoll_event);
需要先创建一个 struct epoll_event evs[1024]用于存放epoll实例;由于每一个epoll_event 都存放有对应的fd,可以通过fd的对比来判断他是不是lfd。
6、进入while循环,使用epoll_wait进行检测;根据检测结果进行其他处理。
建立新的链接
while(1)
{
//epoll_wait检测文件描述符状态;timeout设置为-1(不阻塞);
int num = epoll_wait(epfd, evs, size, -1);
for(int i=0; i<num; ++i)
{
// 取出当前的文件描述符
int curfd = evs[i].data.fd;
epoll_wait返回最大的文件描述符+1;
被检测到数据将被存放在数组evs[]下;我们使用一个 中间量int curfd = evs[i].data.fd来依次遍历整个epoll_event数组;
// 判断这个文件描述符是不是用于监听的
if(curfd == lfd)
{
// 建立新的连接,得到新连接的cfd;
int cfd = accept(curfd, NULL, NULL);
// 给新cfd创建对应的epoll_event,并添加到epoll模型中, 下一轮循环的时候就可以被检测了
ev.events = EPOLLIN; // 设置事件类型
ev.data.fd = cfd; // 设置fd
//加入epoll实例中
ret = epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);
if(ret == -1)
{
perror("epoll_ctl-accept");
exit(0);
}
}
struct epoll_event ev;ev是早早被定义可以重复使用;
我们只需要把epoll_event加入epoll实例中就可以了,这是由于拷贝进入epoll实例中;所以即使我们使用同一个ev往epoll实例中加入,但依旧可以构件这个树。
读和取
else
{
// 处理通信的文件描述符
// 接收数据
char buf[1024];
memset(buf, 0, sizeof(buf));
int len = recv(curfd, buf, sizeof(buf), 0);
if(len == 0)
{
printf("客户端已经断开了连接\n");
// 数据读完了,这里认为是客户端断开连接,所以紧接着就close了
epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL);
close(curfd);
//也可以选择暂时无数据
}
else if(len > 0)
{
printf("客户端say: %s\n", buf);
send(curfd, buf, len, 0);
}
else
{
perror("recv");
exit(0);
}
}
memset(buf, 0, sizeof(buf)): 由于每一次读的长度不一样长,如果每一次buf不置为0很有可能导致二次读的和第一次读的混在一起;这是由于第二次读是从头开始覆盖第一次的值;
关于 epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL):
在这里可能存在两种状态:
1、服务器真的断开连接了,那我们就应该关闭该连接的文件描述符;
2、服务器暂时不来数据了;我们不关闭fd,让它人在epoll树中,等待下一次检测。
但是我们不通过判断的话并不清楚是哪一种情况,索性直接关闭连接并且从epoll数中剔除,下次再来请重新连接。
epoll的工作模式
工作模式是针对每一个文件描述符而言的;
修改是通过修改epoll_event:
struct epoll_evebt ev;
ev.events = EPOLLIN | EPOLLET;
ev.data.fd = fd;
水平模式(默认工作模式)
如果有一个文件描述符就绪,如果不对它进行处理,每次处理时epoll都会对用户进行通知。
特点:以读写事件为例
读事件
1、当读事件就绪时,如果一次性读完,那么缓存区里就没有数据了,下一次检测时不会再通知;因为该文件描述符从就绪状态转为未就绪状态(缓存区无数据)。
如果没有读完,下一次检测仍然会通知;只要读缓存区有数据就是就绪状态。
写事件
1、实际上epoll检测的写缓存区是否可写,如果文件描述符对应的空间都被写满那就是未就绪状态;
2、一般情况下写缓冲区都是有空间的,所以可以不用检测直接往里面写。
边沿模式
如果有一个文件描述符就绪,有且只会通知一次。所以占用资源少,但用起来复杂。
读事件
1、如果缓冲区读了一半,但下一次检测不会通知的,这个时候epoll_wait处于阻塞状态;当下一条信息来到是才会再次通知。
2、如何保证读完了呢?
使用while循环去read,但是read时阻塞的,当读完所有数据就会阻塞进程。
所以要修改为非阻塞的;读完了缓存区读不到数据返回-1,退出循环。
决定是否阻塞时取决于文件描述符,而不是read函数。
//得到fd的flags属性
int flag = fcntl(cfd, F_GETFL);
//添加新属性
flag |= O_NONBLOCK;
fcntl(cfd, F_SETFL, flag);
int len = 0;
while((len = recv(cfd, buf, sizeof(buf), 0)) > 0)
{
// 数据处理...
}
实例修改
注意需要 在添加入epoll树之前完成修改!
if(curfd == lfd)
{
// 建立新的连接,得到新连接的cfd;
int cfd = accept(curfd, NULL, NULL);
// 给新cfd创建对应的epoll_event,并添加到epoll模型中, 下一轮循环的时候就可以被检测了
ev.events = EPOLLIN | EPOLLET; // 设置事件类型
ev.data.fd = cfd; // 设置fd
//修改cfd的属性
int flag = fcntl(cfd, F_GETFL);
flag |= O_NONBLOCK;
fcntl(cfd, F_SETFL, flag);
//将修改后的cfd加入epoll实例中
ret = epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);
.........
读取数据
while(1){
int num = epoll_wait(epfd, evs, size, -1);
for(int i = 0;i < num; i++)
{
if(evs[i].data.fd = lfd)
........
else
{
while(1)
{
int len = recv(cfd, buf, sizeof(buf), 0);
if(len == -1)
{
if(errno = EAGAIN) //头文件error.h
{
printf("数据读取完毕");
epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL);
close(curfd);
break;
}
else
{
perror("recv error");
exit(1);
}
}
}
}
}
}
写事件
1、检测写缓冲区是否可写,如果时可写的,epoll在检测时可写会通知一次;如果下一次还可写就不会通知;直到写缓冲区被写满了,且写缓冲区又能写了,这时候才会通知一次。