1.select IO多路复用的执行原理
1.1select伪代码
#include <stdio.h>
#include <ctype.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <arpa/inet.h>
int main(int argc,const char* argv[])
{
//创建监听的套接字
int listenfd = socket(PF_INET,SOCK_STREAM,0);
if(listenfd == -1)
{
perror("socket error");
exit(1);
}
//绑定
struct sockadder_in serv_addr;
memset(&serv_addr,sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(9999);
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
//绑定端口
int ret = bind(listenfd,(struct sockadder*)&serv_addr,sizeof(serv_addr));
if(ret == -1){
perror("bind error");
exit(1);
}
//初始化fd_set数组
fd_set read_fds;//待检测的原始数据
FD_ZERO(&read_fds);
FD_SET(listenfd,&read_fds);
//while循环,不停的调select(),对文件描述符轮询
int maxfd = listenfd;
while(1){
fd_set tmp = read_fds;//保存原始集合
int ret = select(maxfd+1,&tmp,NULL,NULL,NULL);//select将用户态的文件描述符发送到内核修改,内核修改完毕后再返回给用户态(这个过程是阻塞操作)
//判断是不是监听的描述符
if(FD_ISSET(lfd,&tmp))
{
//接收客户端的连接
int client_fd = accept(listenfd,NULL,NULL);
FD_SET(cfd,&read_fds);
//判断clientfd文件描述符的值是否比之前的文件描述符的值更大,如果是,更新描述符
maxfd = cfd>maxfd?cfd:maxfd;
}
for(int i = 0;i<=maxfd;i++)
{
if(i!=listenfd && FD_ISSET(i,&tmp))
{
//接收数据
char buf[1024] = {0};
int len = recv(i,buf,sizeof(buf),0);
if(len == 1)
{
perror("recv error");
exit(1);
}
else if(len == 0)
{
printf("客户端已断开连接。。。\n");
FD_CLR(i,&read_fds);
close(i);
break;
}
printf("len: %d,buffer: %s\n",ret,buf);
ret = send(client_fd,buf,len,0);
if(ret == -1)
{
perror("send error");
exit(1);
}
}
}
}
close(listenfd);
return 0;
}
1.2select()参数说明
max+1:最大文件描述的编号+1,目的就是限定在内核遍历的范围
read_fds:表示可读的文件描述符集合,对应的要监听可读事件
write_fds:表示可写的文件描述符集合,对应的要监听可写事件
error_fds:表示错误的文件描述符集合,
NULL:表示超时时间,在指定的时间范围内,如果还没有检测到文件描述符被修改或已就绪,那么会结束阻塞,立即返回,设置成NULL表示只遍历一遍,不用等待
ret:已就绪的文件个数,具体哪个文件就绪了,还需要进行遍历,通过FD_ISSET()函数去判断哪个文件可读或可写
1.3select()io多路复用执行流程
用户态调用select()函数,当前进程A会阻塞,同时会将进程a的文件描述符fd_set以及回调函数拷贝传入内核,由内核把他们组成一个进程等待队列
此时内核不断遍历fd_set,哪个文件描述符准备就绪
当服务端网卡设备有数据到达,网卡设备就会通过中断信号告诉cpu
现在有数据到达,这个时候要执行中断程序,中断程序要执行6件事
1.通过DMA拷贝技术把网卡缓冲中的数据copy到内核环形缓冲区
2.将文件描述符信息从未就绪的状态改成已就绪的状态
3.根据文件描述符信息,把对应的数据copy到channel的数据接收队列
4.修改已就绪的文件描述信息,具体哪个socket队列已经就绪,进行标记
5.内核修改完毕,解除阻塞,把fd_set等信息返回用户态,用户态循环遍历调用FD_ISSET()判断哪个可读或可写,对于正真已就绪的文件描述符,就会把内核数据拷贝到用户态,然后进行处理
6.唤醒进程等待队列的进程A,告诉它数据已经到达,可以处理了,此时进程A会进入CPU运行队列
1.4多线程网络并发通信
#include <stdio.h>
#include <ctype.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <arpa/inet.h>
#include <pthread.h>
#include <sys/select.h>
pthread_mutex_t mutex;
//传给线程的参数
typedef struct fdinfo
{
int fd;//监听还是通信的
int *maxfd;
fd_set*rdset;
}FDInfo;
//accept线程处理函数
void* acceptConn(void *arg)
{
printf("子线程id:%ld\n",pthread_self());
FDInfo* info = (FDInfo*)arg;
//接收客户端的连接
int client_fd = accept(info->fd,NULL,NULL);
pthread_mutex_lock(&mutex);
FD_SET(cfd,info->rdset);
//判断clientfd文件描述符的值是否比之前的文件描述符的值更大,如果是,更新描述符
*info->maxfd = cfd> *info->maxfd?cfd: *info->maxfd;
pthread_mutex_unlock(&mutex);
free(info);
}
//收发数据处理函数
void* communication(void * arg)
{
char buf[1024] = {0};
FDInfo* info = (FDInfo*)arg;
int len = recv(info->fd,buf,sizeof(buf),0);
if(len == 1)
{
perror("recv error");
free(info);
return NULL;
}
else if(len == 0)
{
printf("客户端已断开连接。。。\n");
pthread_mutex_lock(&mutex);
FD_CLR(info->fd,info->rdset);
pthread_mutex_unlock(&mutex);
close(info->fd);
free(info);
return NULL;
}
printf("len: %d,buffer: %s\n",ret,buf);
int ret = send(client_fd,buf,len,0);
if(ret == -1)
{
perror("send error");
}
free(info);
return NULL;
}
int main(int argc,const char* argv[])
{
pthread_mutex_init(&mutex,NULL);
//创建监听的套接字
int listenfd = socket(PF_INET,SOCK_STREAM,0);
if(listenfd == -1)
{
perror("socket error");
exit(1);
}
//绑定
struct sockadder_in serv_addr;
memset(&serv_addr,sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(9999);
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
//绑定端口
int ret = bind(listenfd,(struct sockadder*)&serv_addr,sizeof(serv_addr));
if(ret == -1){
perror("bind error");
exit(1);
}
//初始化fd_set数组
fd_set read_fds;//待检测的原始数据
FD_ZERO(&read_fds);
FD_SET(listenfd,&read_fds);
//while循环,不停的调select(),对文件描述符轮询
int maxfd = listenfd;
while(1){
pthread_mutex_lock(&mutex);
fd_set tmp = read_fds;//保存原始集合
pthread_mutex_unlock(&mutex);
int ret = select(maxfd+1,&tmp,NULL,NULL,NULL);//select将用户态的文件描述符发送到内核修改,内核修改完毕后再返回给用户态(这个过程是阻塞操作)
//判断是不是监听的描述符
if(FD_ISSET(lfd,&tmp))
{
//创建子线程
pthread_t tid;
FDInfo* info = (FDInfo*)malloc(sizeof(FDInfo));
info->fd = lfd;
info->maxfd = &maxfd;
info->rdset = read_fds;
pthread_create(&tid,NULL,acceptConn,info);
}
for(int i = 0;i<=maxfd;i++)
{
if(i!=listenfd && FD_ISSET(i,&tmp))
{
//接收数据线程
pthread_t tid;
FDInfo* info = (FDInfo*)malloc(sizeof(FDInfo));
info->fd = i;
info->rdset = read_fds;
pthread_create(&tid,NULL,communication,info);
}
}
}
close(listenfd);
pthread_mutex_destroy(&mutex);
return 0;
}
1.5select()io多路复用缺点
1.fd_set或者说bitmap无法做到重复利用,每次循环都必须要重新创建,这个文件它会被上一次的内核已经给他修改过了所以要重新创建
2.调用select函数中需要把fd_set拷贝到内核态中,存在内核态和用户态的上下文切换,select()函数返回时不仅仅是拷贝已就绪二点描述符,是把所有的文件描述符信息拷贝到用户态
3.select()函数只是会返回已就绪的文件描述符个数,具体的哪个文件描述符需要我们自己遍历,遍历轮询的时间复杂度o(n)
4.它能够监听的文件描述符个数有限制,在32位操作系统中,可以监听1024个文件描述符(32个int的数组1024位,大多数Unix/Linux系统会限制单个进程可以打开的最大文件描述符数量,通常是1024或者2048,fd_set
通常被实现为位数组,每个位对应一个文件描述符。当FD_SETSIZE
被设置为1024时,意味着fd_set
需要128字节(因为1024位等于128字节,每个字节8位)。更大的FD_SETSIZE
值将需要更多的内存空间。)
2.poll IO多路复用的执行原理
2.1poll代码
#include <poll.h>
// 每个委托poll检测的fd都对应这样一个结构体
struct pollfd {
int fd; /* 委托内核检测的文件描述符 */
short events; /* 委托内核检测文件描述符的什么事件 */
short revents; /* 文件描述符实际发生的事件 -> 传出 */
};
struct pollfd myfd[100];
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/select.h>
#include <poll.h>
int main()
{
// 1.创建套接字
int lfd = socket(AF_INET, SOCK_STREAM, 0);
if(lfd == -1)
{
perror("socket");
exit(0);
}
// 2. 绑定 ip, port
struct sockaddr_in addr;
addr.sin_port = htons(9999);
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INADDR_ANY;
int ret = bind(lfd, (struct sockaddr*)&addr, sizeof(addr));
if(ret == -1)
{
perror("bind");
exit(0);
}
// 3. 监听
ret = listen(lfd, 100);
if(ret == -1)
{
perror("listen");
exit(0);
}
// 4. 等待连接 -> 循环
// 检测 -> 读缓冲区, 委托内核去处理
// 数据初始化, 创建自定义的文件描述符集
struct pollfd fds[1024];
// 初始化
for(int i=0; i<1024; ++i)
{
fds[i].fd = -1;
fds[i].events = POLLIN;
}
fds[0].fd = lfd;
int maxfd = 0;
while(1)
{
// 委托内核检测
ret = poll(fds, maxfd+1, -1);
if(ret == -1)
{
perror("select");
exit(0);
}
// 检测的度缓冲区有变化
// 有新连接
if(fds[0].revents & POLLIN)
{
// 接收连接请求
struct sockaddr_in sockcli;
int len = sizeof(sockcli);
// 这个accept是不会阻塞的
int connfd = accept(lfd, (struct sockaddr*)&sockcli, &len);
// 委托内核检测connfd的读缓冲区
int i;
for(i=0; i<1024; ++i)
{
if(fds[i].fd == -1)
{
fds[i].fd = connfd;
break;
}
}
maxfd = i > maxfd ? i : maxfd;
}
// 通信, 有客户端发送数据过来
for(int i=1; i<=maxfd; ++i)
{
// 如果在集合中, 说明读缓冲区有数据
if(fds[i].revents & POLLIN)
{
char buf[128];
int ret = read(fds[i].fd, buf, sizeof(buf));
if(ret == -1)
{
perror("read");
exit(0);
}
else if(ret == 0)
{
printf("对方已经关闭了连接...\n");
close(fds[i].fd);
fds[i].fd = -1;
}
else
{
printf("客户端say: %s\n", buf);
write(fds[i].fd, buf, strlen(buf)+1);
}
}
}
}
close(lfd);
return 0;
}
2.2poll()参数说明
struct pollfd {
int fd; /* 委托内核检测的文件描述符 */
short events; /* 委托内核检测文件描述符的什么事件 */
short revents; /* 文件描述符实际发生的事件 -> 内核填充并传出 */
};
struct pollfd myfd[100];
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
fds: 这是一个struct pollfd类型的数组, 里边存储了待检测的文件描述符的信息,这个数组中有三个成员:
fd:委托内核检测的文件描述符
events:委托内核检测的fd事件(输入、输出、错误),每一个事件有多个取值
revents:这是一个传出参数,数据由内核写入,存储内核检测之后的结果
nfds: 这是第一个参数数组中最后一个有效元素的下标 + 1(也可以指定参数1数组的元素总个数)
另外poll的第二个参数有两种赋值方式,但是都和第一个参数的数组有关系:
1.使用参数1数组的元素个数
2. 使用参数1数组中存储的最后一个有效元素对应的下标值 + 1
timeout: 指定poll函数的阻塞时长
-1:一直阻塞,直到检测的集合中有就绪的文件描述符(有事件产生)解除阻塞
0:不阻塞,不管检测集合中有没有已就绪的文件描述符,函数马上返回
大于0:阻塞指定的毫秒(ms)数之后,解除阻塞
函数返回值:
失败: 返回-1
成功:返回一个大于0的整数,表示检测的集合中已就绪的文件描述符的总个数
2.3poll()io多路复用执行流程
使用poll和select进行IO多路转接的处理思路是完全相同的,将需要监听或关注事件对应的文件描述符,她所执行的进程放到进程对等队列里面去等待,然后在内核里面循环遍历这些文件描述符,看那些文件米哦啊舒服已就绪,当有数据到达网卡设备上的时候 同样会利用DMA拷贝技术把数据拷贝到内核环形缓冲区,然后根据文件描述符的信息把数据拷贝到各自的数据接收队列里面去,内核在遍历这些文件描述符的数据接收队列有数据,就把他们的revents字段置1,接着poll函数返回,然后将文件描述符的结构体数组拷回到用户态。
2.4poll()和select()差别
select使用的位图的方式来标记要委托内核检测的文件描述符(每个比特位对应一个唯一的文件描述符),并且对这个fd_set类型的位图变量进行读写还需要借助一系列的宏函数,操作比较麻烦。而poll直接将要检测的文件描述符的相关信息封装到了一个结构体struct pollfd中,我们可以直接读写这个结构体变量。内核会根据第二个参数传递的值对参数1数组中的文件描述符进行线性遍历,这一点和select也是类似的。
poll的机制与select类似,与select在本质上没有多大差别,使用方法也类似,下面的是对于二者的对比:
1.内核对应文件描述符的检测也是以线性的方式进行轮询,根据描述符的状态进行处理
2.poll和select检测的文件描述符集合会在检测过程中频繁的进行用户区和内核区的拷贝,它的开销随着文件描述符数量的增加而线性增大,从而效率也会越来越低。
3.select检测的文件描述符个数上限是1024,poll没有最大文件描述符数量的限制
4.select可以跨平台使用,poll只能在Linux平台使用
3.epoll()多路复用的执行原理
当某一进程调用epoll_create方法时,Linux内核会创建一个eventpoll结构体,这个结构体中有两个成员与epoll的使用方式密切相关,如下所示:
struct eventpoll {
...
/*红黑树的根节点,这棵树中存储着所有添加到epoll中的事件,
也就是这个epoll监控的事件*/
struct rb_root rbr;
/*双向链表rdllist保存着将要通过epoll_wait返回给用户的、满足条件的事件*/
struct list_head rdllist;
...
};
我们在调用epoll_create时,内核除了帮我们在epoll文件系统里建了个file结点,在内核cache里建了个红黑树用于存储以后epoll_ctl传来的socket外,还会再建立一个rdllist双向链表,用于存储准备就绪的事件,当epoll_wait调用时,仅仅观察这个rdllist双向链表里有没有数据即可。有数据就返回,没有数据就sleep,等到timeout时间到后即使链表没数据也返回。所以,epoll_wait非常高效。
所有添加到epoll中的事件都会与设备(如网卡)驱动程序建立回调关系,也就是说相应事件的发生时会调用这里的回调方法。这个回调方法在内核中叫做ep_poll_callback,它会把这样的事件放到上面的rdllist双向链表中。
在epoll中对于每一个事件都会建立一个epitem结构体,如下所示:
struct epitem {
...
//红黑树节点
struct rb_node rbn;
//双向链表节点
struct list_head rdllink;
//事件句柄等信息
struct epoll_filefd ffd;
//指向其所属的eventepoll对象
struct eventpoll *ep;
//期待的事件类型
struct epoll_event event;
...
}; // 这里包含每一个事件对应着的信息。
当调用epoll_wait检查是否有发生事件的连接时,只是检查eventpoll对象中的rdllist双向链表是否有epitem元素而已,如果rdllist链表不为空,则这里的事件复制到用户态内存(使用共享内存提高效率)中,同时将事件数量返回给用户。因此epoll_waitx效率非常高。epoll_ctl在向epoll对象中添加、修改、删除事件时,从rbr红黑树中查找事件也非常快,也就是说epoll是非常高效的,它可以轻易地处理百万级别的并发连接。
3.1epoll代码
#include <stdio.h>
#include <ctype.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/epoll.h>
// server
int main(int argc, const char* argv[])
{
// 创建监听的套接字
int lfd = socket(AF_INET, SOCK_STREAM, 0);
if(lfd == -1)
{
perror("socket error");
exit(1);
}
// 绑定
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(9999);
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 本地多有的IP
// 设置端口复用
int opt = 1;
setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
// 绑定端口
int ret = bind(lfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
if(ret == -1)
{
perror("bind error");
exit(1);
}
// 监听
ret = listen(lfd, 64);
if(ret == -1)
{
perror("listen error");
exit(1);
}
// 现在只有监听的文件描述符
// 所有的文件描述符对应读写缓冲区状态都是委托内核进行检测的epoll
// 创建一个epoll模型
int epfd = epoll_create(100);
if(epfd == -1)
{
perror("epoll_create");
exit(0);
}
// 往epoll实例中添加需要检测的节点, 现在只有监听的文件描述符
struct epoll_event ev;
ev.events = EPOLLIN; // 检测lfd读读缓冲区是否有数据
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(struct epoll_event);
// 持续检测
while(1)
{
// 调用一次, 检测一次
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模型中, 下一轮循环的时候就可以被检测了
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);
}
}
else
{
// 处理通信的文件描述符
// 接收数据
char buf[1024];
memset(buf, 0, sizeof(buf));
int len = recv(curfd, buf, sizeof(buf), 0);
if(len == 0)
{
printf("客户端已经断开了连接\n");
// 将这个文件描述符从epoll模型中删除
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);
}
}
}
}
return 0;
}
3.2epoll操作函数参数说明
epoll_create()
函数的作用是创建一个红黑树模型的实例,用于管理待检测的文件描述符的集合。
// 创建epoll实例,通过一棵红黑树管理待检测集合
int epoll_create(int size);
size:在Linux内核2.6.8版本以后,这个参数是被忽略的,只需要指定一个大于0的数值就可以了。
函数返回值:
失败:返回-1
成功:返回一个有效的文件描述符,通过这个文件描述符就可以访问创建的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() 函数的返回值,通过这个参数找到epoll实例
op:这是一个枚举值,控制通过该函数执行什么操作
EPOLL_CTL_ADD:往epoll模型中添加新的节点
EPOLL_CTL_MOD:修改epoll模型中已经存在的节点
EPOLL_CTL_DEL:删除epoll模型中的指定的节点
fd:文件描述符,即要添加/修改/删除的文件描述符,需要监视的文件描述符
event:epoll事件,用来修饰第三个参数对应的文件描述符的,指定检测这个文件描述符的什么事件,需要监视该文件描述符上的哪些事件
events:委托epoll检测的事件
EPOLLIN:读事件, 接收数据, 检测读缓冲区,如果有数据该文件描述符就绪
EPOLLOUT:写事件, 发送数据, 检测写缓冲区,如果可写该文件描述符就绪
EPOLLERR:异常事件
data:用户数据变量,这是一个联合体类型,通常情况下使用里边的fd成员,用于存储待检测的文件描述符的值,在调用epoll_wait()函数的时候这个值会被传出。
函数返回值:
失败:返回-1
成功:返回0
//检测创建的epoll实例中有没有就绪的文件描述符
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
epfd:epoll_create() 函数的返回值, 通过这个参数找到epoll实例
events:传出参数, 这是一个结构体数组的地址, 里边存储了已就绪的文件描述符的信息
maxevents:修饰第二个参数, 结构体数组的容量(元素个数)
timeout:如果检测的epoll实例中没有已就绪的文件描述符,该函数阻塞的时长, 单位ms 毫秒
0:函数不阻塞,不管epoll实例中有没有就绪的文件描述符,函数被调用后都直接返回
大于0:如果epoll实例中没有已就绪的文件描述符,函数阻塞对应的毫秒数再返回
-1:函数一直阻塞,直到epoll实例中有已就绪的文件描述符之后才解除阻塞
函数返回值:
成功:
等于0:函数是阻塞被强制解除了, 没有检测到满足条件的文件描述符
大于0:检测到的已就绪的文件描述符的总个数
失败:返回-1
3.43epoll()io多路复用执行流程
对于每一个socket连接都会在调用epoll_ctl()函数的时候分配一个epitem,添加到红黑树里,每一个红黑树节点,都对应着epitem里边的一条记录
epoll_wait()首先是区检查已就绪的事件双链表中是否有就绪的事件,如果有就绪的事,epoll_wait()会立刻返回,不阻塞,但是当这个里边为空的时候,就会进行阻塞,等待有就绪的事件到达,比如进程A执行这个函数,那么进程A会让出CPU,进入阻塞的等待状态,同时会把进程A关联到阻塞进程队列中
比如客户端把数据发送到了socket的数据接收队列里面去,这个时候会已就绪的读事件,把他们插入到已就绪的链表里(通过回调函数,不是遍历),内核会检查现在是否有阻塞的进程存在,如果这个阻塞的进程恰好是执行7和9 两个文件描述符的事件,就把他唤醒,然后进程A进入运行队列,把7和9两个文件描述符返回给用户态,然后处理相应的读事件信息。
3.4epoll工作模式
水平触发LT
客户端把数据发送到网卡设备上,此时epoll_ctl()注册的回调函数就会将这些数据对应的文件文件描述符拷贝到已就绪的双向队列中,每个文件描述符每个客户端连接发送的消息的数量可以是不一样的,虽说消息数量不一样,但都表示这些数据对应的文件描述符或者说客户端连接有数据到达
如果采取默认的触发机制,只要底层有事件就绪,只要不被取走就一直通知上层,当调用epollwait()方法的时候,可以不立刻吧epoll()返回的数据都处理完
可以先处理一部分,然后再处理下一批数据,因为处理一部分数据之后内核任然是不断的通知你有数据到达,只有当你把所有数据都处理完了,他才不会通知
我们可以在下次调用epoll_wait()函数的时候把剩下这一批数据处理完,只有当内核中没有数据的时候,才会停止通知用户态有数据到达
边缘触发ET
只会通知一次有数据到达,用户态在调用epoll_wait()函数的时候,之处理一部分数据,并没有把所有的数据都处理完,边缘触发不会告知用户态,你还有数据需要处理的,但是当客户端再发送一批数据过来的时候,边缘触发会通知用户态进行处理,也仅仅是通知一次,只有底层就绪事件的数量,由无到有或由少到多,epoll才会通知用户,并且只通知一次。
3.5epoll为什么会比select/poll快,再哪些地方?
1. epoll模型只是在调用epoll_ctl方法的时候在监听事件的时候才会把数据从用户态拷贝到内核态而select和poll每次执行的时候都要重新将要监听的所有事件从用户态拷回内核态,,因此,把时间复杂度从O(n)降到O(1)
2.只需要在epoll_ctl()时传递一次文件描述符,epoll_wait()不需要再次传递文件描述符
3.epoll在调用epoll_wait()方法读取就绪事件的时候,它只会拷贝已就绪的这些事件,它不会拷贝那些没有就绪的事件
4.epoll基于红黑树+双链表储存事件,没有最大连接数的限制,不存在c10k问题()
5.epoll采用回调机制,epoll_ctl()函数中,为每个文件描述符都指定了回调函数,基于回调函数把就绪事件放到就绪队列中,而不是像select和poll那样主动用轮询的方式去轮询哪个文件描述符上的事件已就绪,epoll_wait()方法只需要访问已就绪的双端队列,就可以知道那些文件描述符已经就绪了,他的时间复杂度是O(1)
注:epoll并没有使用MMAP零拷贝技术
参考:epoll原理详解及epoll反应堆模型-CSDN博客
B站:码上加薪
爱编程的大丙: https://subingwen.cn/linux/epoll/