• 正文
  • 相关推荐
申请入驻 产业图谱

IO多路转接技术 | poll/epoll详解

05/06 10:04 来源:mindtechnist
1493
加入交流群
扫码加入
获取工程师必备礼包
参与热点资讯讨论

1. poll详解

函数原型

intpoll(struct?pollfd *fd,?nfds_t?nfds,?int?timeout);

函数参数

fd:数组的地址,struct pollfd all[120]; 其中struct pollfd结构体如下

structpollfd {
int? ?fd; ? ? ? ??/* 文件描述符 */
short?events; ? ??/* 等待的事件 */
short?revents; ? ?/* 实际发生的事件 */
? ? };

结构体红各项含义如下:

文件描述符fd:表示要坚持测的fd,通过 open("a.txt", O_wronly | O_append); 获得。

events:要等待的事件

revents:实际发生的事件,它是内核给的反馈,在select的时候,会有一个备份来供内核修改并传出。

nfds:数组的最大长度, 数组中最后一个使用的元素下标+1

内核会轮询检测fd数组的每个文件描述符

timeout:

1:永久阻塞

0:调用完成立即返回

>0:等待的时长毫秒

函数返回值:IO发生变化的文件描述符的个数。

2. epoll详解

(1)API介绍

intepoll_create(int?size);

函数功能:生成一个epoll专用的文件描述符,实际上就是生成一个epoll树的根结点。

函数参数:size,epoll树上能挂的最大文件描述符数量。表示我想在这个树节点上挂size个节点,假如实际上的节点大于size的话epoll会自动扩展,所以这个大小可以随便传,不用太在意。但是这个扩展也是有上限的,如果电脑内存是1G,那么扩展的上限是10万(2G就是20万。。。通过加内存可以扩大上限)。

函数返回值:函数返回值是树的根节点,在后面用到epft参数的时候,都是指这个返回值,也就是树的根节点。

intepoll_ctl(int?epfd,?int?op,?int?fd,?struct?epoll_event *event);

函数功能:用于控制某个epoll文件描述符事件,可以注册、修改、删除。

函数参数:

epfd:epoll_create()函数生成的专用文件描述符。

op:

EPOLL_CTL_ADD ? ? ? —— ?注册

EPOLL_CTL_MOD ? ? ?—— ?修改

EPOLL_CTL_DEL ? ? ? ?—— ?删除

fd:关联的文件描述符

event:告诉内核要监听什么事件

EPOLLIN ? ? —— 读

EPOLLOUT —— 写

EPOLLERR ?—— 异常

structepoll_event {/* 该结构体主要存放和fd有关的信息 */uint32_t? ? ?events; ? ? ? ? ? ? ? ? ? ? ? ? ?epoll_data_t?data;?? ? ? };
typedefunion epoll_data {void? ? ? ? ?*ptr;int? ? ? ? ? fd;uint32_t? ? ?u32;uint64_t? ? ?u64;? ? ? }?epoll_data_t;

epoll_data_t是一个联合体union,四个成员共用同一块内存,也就是说四个成员我们只能用一个,一般情况下我们用fd,这个fd实际上就是epoll_ctl()函数的第三个参数fd。

如果我们想在epoll树上挂载更多信息,而不仅仅是fd文件描述符的话,我们可以把更多信息封装在结构体中,并把该结构体传给epoll_data_t结构体的ptr指针,这样就可以在epoll树上挂载和fd有关的更多信息。

structsockInfo? ? ? ? {int? ? ? ? ?fd;structsockaddr_inaddr;? ? ? ? };

比如说,要获取发生变化的fd对应的client的IP和port,就可以利用指针ptr,这样的话联合epoll_data_t中的fd就不能用了,我们把文件描述符传给sockInfo的fd即可完成fd信息的挂载。

intepoll_wait(int?epfd,struct?epoll_event* events, ?/* 结构体数组 */int?maxevents,int?timeout);

函数功能:等待IO事件发生(可以设置阻塞),epoll_wait()函数相当于前面讲的select()或poll()函数,表示委托内核去进行检测。epoll_event通过返回值和传出参数events来实现把哪几个fd发生变化告诉server进程的目的。首先,每当有fd变化,就把这个fd对应的树节点拷贝到events数组中,最后,有几个fd变化,就返回几。这样只要根据返回值和参数events就可以遍历出所有变化的fd以及相关信息。

函数参数:

epfd:要检测的句柄

events:用于回传待处理事件的数组。它是一个传出参数,需要提前分配内存,哪个fd发生变化了,就把哪个fd的树节点(struct epoll_event)拷贝一份放到这个数组中。这样epoll就能返回是哪个fd发生了变化。

maxevents:告诉内核events的大小,因为内核要把发生变化的fd对应的树节点拷贝到数组中,所以要知道数组大小。

timeout:为超时时间

1:永久阻塞

0:立即返回

>0

函数返回值:有多少个fd发生了变化就返回几(变化的fd信息存在events数组中)。

(2)epoll树

(3)epoll模型

#include<stdio.h>#include<unistd.h>#include<stdlib.h>#include<sys/types.h>#include<string.h>#include<sys/socket.h>#include<arpa/inet.h>#include<ctype.h>#include<sys/epoll.h>
intmain(int?argc, constchar* argv[]){if(argc <?2)? ? {printf("eg: ./a.out portn");exit(1);? ? }structsockaddr_inserv_addr;socklen_t?serv_len =?sizeof(serv_addr);int?port =?atoi(argv[1]);?//字符串转整形值
// 创建套接字int?lfd =?socket(AF_INET, SOCK_STREAM,?0);// 初始化服务器 sockaddr_in?memset(&serv_addr,?0, serv_len);? ? serv_addr.sin_family = AF_INET;?// 地址族?? ? serv_addr.sin_addr.s_addr =?htonl(INADDR_ANY);?// 监听本机所有的IP? ? serv_addr.sin_port =?htons(port);?// 设置端口?// 绑定IP和端口? ??bind(lfd, (struct?sockaddr*)&serv_addr, serv_len);
// 设置同时监听的最大个数? ??listen(lfd,?36);printf("Start accept ......n");
structsockaddr_inclient_addr;socklen_t?cli_len =?sizeof(client_addr);
// 创建epoll树根节点int?epfd =?epoll_create(2000);// 初始化epoll树structepoll_eventev;? ? ev.events = EPOLLIN;? ? ev.data.fd = lfd;? ??epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);//存放发生变化的fd对应的树节点structepoll_eventall[2000];while(1)? ? {// 使用epoll通知内核fd 文件IO检测int?ret =?epoll_wait(epfd, all,?sizeof(all)/sizeof(all[0]),?-1);
// 遍历all数组中的前ret个元素 //ret表示有几个变化的fd,变化的fd都存在all数组中for(int?i=0; i<ret; ++i)? ? ? ? {int?fd = all[i].data.fd;// 判断是否有新连接if(fd == lfd)? ? ? ? ? ? {// 接受连接请求 // accept不阻塞,因为已经有连接int?cfd =?accept(lfd, (struct?sockaddr*)&client_addr, &cli_len);if(cfd ==?-1)? ? ? ? ? ? ? ? {? ? ? ? ? ? ? ? ? ??perror("accept error");exit(1);? ? ? ? ? ? ? ? }// 将新得到的cfd挂到树上structepoll_eventtemp;? ? ? ? ? ? ? ? temp.events = EPOLLIN;?//检测cfd对应的读缓冲区,是否有数据传入? ? ? ? ? ? ? ? temp.data.fd = cfd;? ? ? ? ? ? ? ??epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &temp);
// 打印客户端信息char?ip[64] = {0};printf("New Client IP: %s, Port: %dn",? ? ? ? ? ? ? ??inet_ntop(AF_INET, &client_addr.sin_addr.s_addr, ip,?sizeof(ip)),? ? ? ? ? ? ? ? ? ? ? ??ntohs(client_addr.sin_port));
? ? ? ? ? ? }else? ? ? ? ? ? {// 处理已经连接的客户端发送过来的数据if(!all[i].events & EPOLLIN)?//只处理读事件? ? ? ? ? ? ? ? {continue;? ? ? ? ? ? ? ? }/*? ? ? ? ? ? ? ? 假如说client发送过了100个数据,也就是serve的read缓冲区有100个数据,? ? ? ? ? ? ? ? 但是调用recv函数的时候只能读50个数据,而本次循环只调用了一次recv,? ? ? ? ? ? ? ? 那么只能下次循环再读剩余的50个数据,所以下次循环检测的时候,? ? ? ? ? ? ? ? epoll_wait还是会返回,因为缓冲区还是剩余数据。这就是水平触发模式。? ? ? ? ? ? ? ? 这样的话虽然client只发了1次,但是epoll_wait会通知两次server去读数据。? ? ? ? */
// 读数据char?buf[1024] = {0};int?len =?recv(fd, buf,?sizeof(buf),?0);if(len ==?-1)? ? ? ? ? ? ? ? {? ? ? ? ? ? ? ? ? ??perror("recv error");exit(1);? ? ? ? ? ? ? ? }elseif(len ==?0)? ? ? ? ? ? ? ? {printf("client disconnected ....n");//close(fd);// fd从epoll树上删除? ? ? ? ? ? ? ? ? ? ret =?epoll_ctl(epfd, EPOLL_CTL_DEL, fd,?NULL);// 挂树的时候需要ev,把ev挂在树上删除写NULL就行了if(ret ==?-1)? ? ? ? ? ? ? {? ? ? ? ? ? ? ? ? ??perror("epoll_ctl del error");exit(1);? ? ? ? ? ? ? }? ? ? ? ? ? ? ?close(fd);? ? ? ? ? ? ? ? }else? ? ? ? ? ? ? ? {printf(" recv buf: %sn", buf);? ? ? ? ? ? ? ? ? ??write(fd, buf, len);? ? ? ? ? ? ? ? }? ? ? ? ? ? }? ? ? ? }? ? }
? ??close(lfd);return0;}

epoll维护的红黑树是存在一个共享内存中,内核和用户都可以通过操作这个共享内存来操作树,不需要内核态和用户态的切换,也不需要两种状态之间的数据拷贝,所以效率更高。

(4)epoll的三种工作模式

水平触发模式 ? - (根据读来解释)

只要fd对应的缓冲区有数据,epoll_wait就会返回

返回的次数与发送数据的次数没有关系

epoll默认的工作模式

边沿触发模式 - ET

fd - 默认阻塞属性

客户端给server发数据:

发一次数据server 的 epoll_wait就返回一次

不在乎数据是否读完

如果读不完,如何把数据全部读出来?

while(recv());

数据读完之后recv会阻塞

解决阻塞问题 —— 设置非阻塞fd

对于epoll_wait()来说,epoll_wait 调用次数越多, 系统的开销越大。

水平触发模式会多次返回,只要server的read缓冲区有数据,epoll_wait就返回,也就会通知server去读数据,那么在循环检测的时候,只要server的read缓冲区有数据,epoll_wait就会多次调用,多次返回,并通知server去读数据;假如说client发送过了100个数据,也就是serve的read缓冲区有100个数据,但是调用recv函数的时候只能读50个数据,而本次循环只调用了一次recv,那么只能下次循环再读剩余的50个数据,所以下次循环检测的时候,epoll_wait还是会返回,因为缓冲区还是剩余数据。这就是水平触发模式。这样的话虽然client只发了1次,但是epoll_wait会通知两次server去读数据。

—— (printf函数是标准C库函数,C库函数都有一个默认缓冲区,printf的大小是8K。printf函数是行缓冲,使用printf函数的时候,如果不加 n 会默认等到写满的时候才打印内容,加 n 会强制把缓冲区的内容打印出来。另外 表示结束,不加 就会一直输出直到遇到 ,用write(STDOUT_FILENO)替代printf函数就可以解决这些问题。)

边沿触发模式,client发一次数据epoll_wait只返回一次,也就只读一次,这样的话server的read缓冲区可能会有很多数据堆积,server读数据的时候可能读到的是上一次剩余的数据,并且只有client发的时候,epoll_wait才会通知server去读数据,边沿触发模式尽可能减少了epoll_wait的调用次数,缺点是数据有可能读不完导致堆积;

边沿非阻塞触发

效率最高

如何设置非阻塞

open()

设置flags

必须 O_WDRW | O_NONBLOCK

终端文件: /dev/tty

 

fcntl

int flag = fcntl(fd, F_GETFL);

flag |= O_NONBLOCK;

fcntl(fd, F_SETFL, flag);

如何将缓冲区的全部数据都读出?

while(recv() >?0)? ? ?{? ? ??printf();? ? ?}

当缓冲区数据读完之后, 返回值是否为0?

阻塞状态

数据读完之后,recv阻塞

非阻塞状态

强行读了一个没有数据的缓冲区(fd),数据已经被读完了,因为是非阻塞,所以在while循环中recv还要继续读,导致返回-1

判断 errno == EAGAIN

示例

#include<stdio.h>#include<unistd.h>#include<stdlib.h>#include<sys/types.h>#include<string.h>#include<sys/socket.h>#include<arpa/inet.h>#include<ctype.h>#include<sys/epoll.h>#include<fcntl.h>#include<errno.h>
intmain(int?argc, constchar* argv[]){if(argc <?2)? ? {printf("eg: ./a.out portn");exit(1);? ? }structsockaddr_inserv_addr;socklen_t?serv_len =?sizeof(serv_addr);int?port =?atoi(argv[1]);
// 创建套接字int?lfd =?socket(AF_INET, SOCK_STREAM,?0);// 初始化服务器 sockaddr_in?memset(&serv_addr,?0, serv_len);? ? serv_addr.sin_family = AF_INET; ? ? ? ? ? ? ? ? ??// 地址族?? ? serv_addr.sin_addr.s_addr =?htonl(INADDR_ANY); ? ?// 监听本机所有的IP? ? serv_addr.sin_port =?htons(port); ? ? ? ? ? ?// 设置端口?// 绑定IP和端口? ??bind(lfd, (struct?sockaddr*)&serv_addr, serv_len);
// 设置同时监听的最大个数? ??listen(lfd,?36);printf("Start accept ......n");
structsockaddr_inclient_addr;socklen_t?cli_len =?sizeof(client_addr);
// 创建epoll树根节点int?epfd =?epoll_create(2000);// 初始化epoll树structepoll_eventev;
// 设置边沿触发? ? ev.events = EPOLLIN;?//监听的文件描述符没必要边沿触发,主要是通信的cfd? ? ev.data.fd = lfd;? ??epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);
structepoll_eventall[2000];while(1)? ? {// 使用epoll通知内核fd 文件IO检测int?ret =?epoll_wait(epfd, all,?sizeof(all)/sizeof(all[0]),?-1);printf("================== epoll_wait =============n");
// 遍历all数组中的前ret个元素for(int?i=0; i<ret; ++i)? ? ? ? {int?fd = all[i].data.fd;// 判断是否有新连接if(fd == lfd)? ? ? ? ? ? {// 接受连接请求int?cfd =?accept(lfd, (struct?sockaddr*)&client_addr, &cli_len);if(cfd ==?-1)? ? ? ? ? ? ? ? {? ? ? ? ? ? ? ? ? ??perror("accept error");exit(1);? ? ? ? ? ? ? ? }// 设置文件cfd为非阻塞模式int?flag =?fcntl(cfd, F_GETFL);? ? ? ? ? ? ? ? flag |= O_NONBLOCK;? ? ? ? ? ? ? ??fcntl(cfd, F_SETFL, flag);
// 将新得到的cfd挂到树上structepoll_eventtemp;// 设置边沿触发? ? ? ? ? ? ? ? temp.events = EPOLLIN | EPOLLET;? ? ? ? ? ? ? ? temp.data.fd = cfd;? ? ? ? ? ? ? ??epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &temp);
// 打印客户端信息char?ip[64] = {0};printf("New Client IP: %s, Port: %dn",? ? ? ? ? ? ? ??inet_ntop(AF_INET, &client_addr.sin_addr.s_addr, ip,?sizeof(ip)),? ? ? ? ? ? ? ??ntohs(client_addr.sin_port));
? ? ? ? ? ? }else? ? ? ? ? ? {// 处理已经连接的客户端发送过来的数据if(!all[i].events & EPOLLIN)?? ? ? ? ? ? ? ? {continue;? ? ? ? ? ? ? ? }
// 读数据char?buf[5] = {0};int?len;// 循环读数据while( (len =?recv(fd, buf,?sizeof(buf),?0)) >?0?)? ? ? ? ? ? ? ? {// 数据打印到终端//不要用printf,因为printf如果找不到  n 字符会出现乱码,打印不出来等问题? ? ? ? ? ? ? ? ? ??write(STDOUT_FILENO, buf, len);// 发送给客户端? ? ? ? ? ? ? ? ? ??send(fd, buf, len,?0);? ? ? ? ? ? ? ? }if(len ==?0)? ? ? ? ? ? ? ? {printf("客户端断开了连接n");? ? ? ? ? ? ? ? ? ? ret =?epoll_ctl(epfd, EPOLL_CTL_DEL, fd,?NULL);if(ret ==?-1)? ? ? ? ? ? ? ? ? ? {? ? ? ? ? ? ? ? ? ? ? ??perror("epoll_ctl - del error");exit(1);? ? ? ? ? ? ? ? ? ? }? ? ? ? ? ? ? ? ? ??close(fd);? ? ? ? ? ? ? ? }elseif(len ==?-1)? ? ? ? ? ? ? ? {//数据已经被读完了,因为是非阻塞,所以在while循环中recv还要继续读,导致返回-1if(errno == EAGAIN)? ? ? ? ? ? ? ? ? ? {printf("缓冲区数据已经读完n");? ? ? ? ? ? ? ? ? ? }else? ? ? ? ? ? ? ? ? ? {//这才是真正的recv错误printf("recv error----n");exit(1);? ? ? ? ? ? ? ? ? ? }? ? ? ? ? ? ? ? }#if?0if(len ==?-1)? ? ? ? ? ? ? ? {? ? ? ? ? ? ? ? ? ??perror("recv error");exit(1);? ? ? ? ? ? ? ? }elseif(len ==?0)? ? ? ? ? ? ? ? {printf("client disconnected ....n");// fd从epoll树上删除? ? ? ? ? ? ? ? ? ? ret =?epoll_ctl(epfd, EPOLL_CTL_DEL, fd,?NULL);if(ret ==?-1)? ? ? ? ? ? ? ? ? ? {? ? ? ? ? ? ? ? ? ? ? ??perror("epoll_ctl - del error");exit(1);? ? ? ? ? ? ? ? ? ? }? ? ? ? ? ? ? ? ? ??close(fd);
? ? ? ? ? ? ? ? }else? ? ? ? ? ? ? ? {// printf(" recv buf: %sn", buf);? ? ? ? ? ? ? ? ? ??write(STDOUT_FILENO, buf, len);? ? ? ? ? ? ? ? ? ??write(fd, buf, len);? ? ? ? ? ? ? ? }#endif? ? ? ? ? ? }? ? ? ? }? ? }
? ??close(lfd);return0;}

5)文件描述符1024限制

对于select来说,无法突破文件描述符1024上限,因为select是通过数组实现的。poll和epoll可以突破1024限制,poll是内部链表实现,而epoll是红黑树实现。

查看受计算机硬件限制的文件描述符上限可以通过下面命令

cat?/proc/sys/fs/file-max

同样,我们也可以通过修改配置文件来修改这个上限,但是,我们在程序中设置的时候不能超过硬件限制的上限

vim /etc/security/limits.conf

- soft ?nofile ? ?8000 ? ? ?—— 也可以通过命令ulimit -n 2000来修改为2000

- hard ?nofile ? 8000 ? ? ?—— 硬件资源限制

修改后重启系统即可起效。

相关推荐

登录即可解锁
  • 海量技术文章
  • 设计资源下载
  • 产业链客户资源
  • 写文章/发需求
立即登录

Linux、C、C++、Python、Matlab,机器人运动控制、多机器人协作,智能优化算法,贝叶斯滤波与卡尔曼滤波估计、多传感器信息融合,机器学习,人工智能。