蕲春网站建设做外贸有哪些网站比较好
news/
2025/9/23 8:29:22/
文章来源:
蕲春网站建设,做外贸有哪些网站比较好,郑州大旗网站制作公司,句容网页定制epoll理解及应用
select复用方法其实由来已久#xff0c;因此#xff0c;利用该技术后#xff0c;无论如何优化程序性能也无法同时接入上百个客户端#xff08;当然#xff0c;硬件性能不同#xff0c;差别也很大)。这种select方式并不适合以Web服务器端开发为主流的现代…epoll理解及应用
select复用方法其实由来已久因此利用该技术后无论如何优化程序性能也无法同时接入上百个客户端当然硬件性能不同差别也很大)。这种select方式并不适合以Web服务器端开发为主流的现代开发环境所以要学习Linux平台下的epoll。
基于select的I/O复用技术速度慢的原因
第12章曾经实现过基于select的IO复用服务器端很容易从代码上分析出不合理的设计最 主要的两点如下。 □调用select函数后常见的针对所有文件描述符的循环语句。 □每次调用select函数时都需要向该函数传递监视对象信息。 上述两点可以从第12章回声服务器示例的第45,49行及第54行代码得到确认。调用select函数后并不是把发生变化的文件描述符单独集中到一起而是通过观察作为监视对象的fd_set变量的变化找出发生变化的文件描述符示例的第54、56行因此无法避免针对所有监视对象的循环语句。而且作为监视对象的fd_set变量会发生变化所以调用select函数前应复制并保存原有信息参考示例的第45行)并在每次调用select函数时传递新的监视对象信息。 各位认为哪些因素是提高性能的更大障碍是调用select函数后常见的针对所有文件描述符对象的循环语句?还是每次需要传递的监视对象信息?
在代码层面上思考很容易认为是循环。但相比于循环语句更大的障碍是每次传递监视对象信息。因为传递对象信息具有如下含义:每次调用select函数时向操作系统传递监视对象信息。
应用程序向操作系统传递数据将对程序造成很大负担而且无法通过优化代码解决因此将成为性能上的致命缺点。
select函数的这一缺点可以通过如下方式弥补:“仅向操作系统传递1次监视对象监视范围或内容发生变化时只通知发生变化的事项。”
这样就无需每次调用select函数时都向操作系统传递监视对象信息但前提是操作系统支持这种处理方式每种操作系统支持的程度和方式存在差异。Linux的支持方式是epollWindows的支持方式是IOCP。
select也有优点
知道这些内容后有些人可能对select函数感到失望但大家应当掌握select函数。本章的epoll方式只在Limux下提供支持也就是说改进的IO复用模型不具有兼容性。相反大部分操作系统都支持select函数。只要满足或要求如下两个条件即使在Linux平台也不应拘泥于epoll。 □服务器端接入者少 □程序应具有兼容性 实际并不存在适用于所有情况的模型。各位应理解好各种模型的优缺点。
实现epoll时必要的函数和结构体
能够克服select函数缺点的epoll函数具有如下优点这些优点正好与之前的select函数缺点相反。 □无需编写以监视状态变化为目的的针对所有文件描述符的循环语句。 □调用对应于select函数的epoll_wait函数时无需每次传递监视对象信息。
下面介绍epoll服务器端实现中需要的三个函数希望各位结合epoll函数的优点理解这些函数的功能。
□epoll_create:创建保存epoll文件描述符的空间。
□epoll_ctl:像空间注册或注销文件描述符。
□epoll_wait:于select函数类似等待文件描述符发生变化。
select方式中为了保存监视对象文件描述符直接声明了fd_set变量。但epoll方式下由操作系统负责保存监视对象文件描述符因此需要向操作系统请求创建保存文件描述符的空间此时使用的函数就是epoll_create。 此外为了添加和删除监视对象文件描述符select方式中需要FD_SET,FD_CLR函数。但在epoll方式中通过epoll_ctl函数请求操作系统完成。最后select方式下调用select函数等待文件描述符的变化而epoll中调用epoll_wait函数。还有select方式中通过fd_set变量查看监视对象的状态变化事件发生与否而epoll方式中通过如下结构体epoll_event将发生变化的(发生事件的文件描述符单独集中到一起。
struct epoll_event{__uint32_t events;epoll_data_t data;
}typedef union epoll_data{void *ptr;int fd;__uint32_t u32;__uint64_t u64;}epoll_data_t;
声明足够大的epoll_event结构体数组后传递给epoll_wait函数时发生变化的文件描述符信息被填入该数组。因此无需向select函数那样针对所有文件描述符进行循环。
epoll_create
epoll是从Linux2.5.44版内核开始引入的所以使用epoll前需要验证Linux内核版本。下面仔细观察epoll_create函数。
#includesys/epoll.h
int epoll_create(int size);//成功时返回epoll文件描述符失败时返回-1size //epoll示例的大小
调用epoll_create函数时创建的文件描述符保存空间称为epoll例程。还有一个注意点这个传入的size大小实际上并非用来决定epoll例程的大小而仅供操作系统参考。
epoll_create函数创建的资源与套接字相同也由操作系统管理。因此该函数和创建套接字的情况相同也会返回文件描述符。需要终止时与其他文件描述符相同也要调用close函数。
epoll_ctl
#includesys/epoll.h
int epoll_ctl(int epfd,int op,int fd,struct epoll_event* event);
//成功时返回0失败时返回-1epfd //用于注册监视对象epoll例程的文件描述符op //用于指定监视对象的添加删除或更改等操作 fd //需要注册监视对象文件描述符event //监视对象的事件类型
接下来是两个简单示例
epoll_ctl(A,EPOLL_CTL_ADD,B,C);
epoll_ctl(A,EPOLL_CTL_DEL,B,NULL);
第一句含义是:epoll例程A中注册文件描述符B主要目的是监视参数C中的事件
第二句含义是:从epoll例程A中删除文件描述符B
从上述调用语句中可以看到从监视对象中删除时不需要监视类型事件信息因此向第四个参数传递NULL。
接下来介绍可以向epoll_ctl第二个参数传递的常量及含义。 □ EPOLL_CTL_ADD:将文件描述符注册到epoll例程。
□ EPOLL_CTL DEL:从epoll例程中删除文件描述符。
□ EPOLL _CTL_MOD:更改注册的文件描述符的关注事件发生情况。 下面讲解各位不太熟悉的epoll_ctl函数的第四个参数其类型是之前讲过的epoll_event结构体指针。如前所述epoll_event结构体用于保存发生事件的文件描述符集合。但也可以在epoll例程中注册文件描述符时用于注册关注的事件!(有两个功能)函数中epoll_event结构体的定义并不显眼因此通过调用语句说明该结构体在epoll_ctl函数中的应用。
struct epoll_event event;
....
event.eventsEPOLLIN;//事件种类
event.data.fdsockfd;
epoll_ctl(epfd,EPOLL_CTL_ADD,sockfd,event);
....
上述代码将sockfd注册到epoll例程epfd中并在需要读取数据的情况下产生相应事件。接下来给出epoll_event的成员events中可以保存的常量及所指的事件类型。 □ EPOLLIN:需要读取数据的情况。 □ EPOLLOUT:输出缓冲为空可以立即发送数据的情况。 □ EPOLLPRI:收到OOB数据的情况。 □ EPOLLRDHUP:断开连接或半关闭的情况这在边缘触发方式下非常有用。 □ EPOLLERR:发生错误的情况。 □ EPOLLET:以边缘触发的方式得到事件通知。 □ EPOLLONESHOT:发生一次事件后相应文件描述符不再收到事件通知。因此需要向 epoll_ctl函数的第二个参数传递EPOLL_CTL_MOD,再次设置事件。
可以通过位或运算同时传递多个上述参数。关于“边缘触发”稍后将单独讲解目前只需记住EPOLLIN即可。
epoll_wait
最后介绍与select函数对应的epoll_wait函数,epoll相关函数中默认最后调用该函数。
#includesys/epoll.h
int epoll_wait(int epfd,struct epoll_event*events,int maxevent,int timeout);epfd //表示事件发生监视范围的epoll例程的文件描述符events //保存发生事件的文件描述符集合的结构体地址maxevents //表示第二个参数中可以保存的最大事件数timeout //以1/1000秒为单位的等待时间传递-1时一直等待直到发生事件 该函数的调用方式如下。需要注意的是第二个参数所指缓冲需要动态分配。
int event_cnt;
struct epoll_event* ep_events;
....
ep_eventsmalloc(sizeof(struct epoll_event)*EPOLL_SIZE);//EPOLL_SIZE是宏常量
....
event_cntepoll_wait(epfd,ep_event,EPOLL_SIZE,-1);
....
调用函数后返回发生事件的文件描述符数同时在第二个参数指向的缓冲中保存发生事件的文件描述符集合。因此无需像select那样插入针对所有文件描述符的循环。
基于epoll的回声服务端
#includestdio.h
#includestdlib.h
#includestring.h
#includeunistd.h
#includearpa/inet.h
#includesys/socket.h
#includesys/epoll.h#define BUF_SIZE 100
#define EPOLL_SIZE 50
void error_handling(char *message);int main(int argc,char *argv[]){int serv_sock,clnt_sock;struct sockaddr_in serv_addr,clnt_addr;socklen_t addr_sz;int str_len,i;char buf[BUF_SIZE];struct epoll_event *ep_events;struct epoll_event event;in epfd,event_cnt;if(argc!2){printf(Usage : %s port\n,argv[0]);exit(1);}serv_socksocket(PF_INET,SOCK_STREAM,0);memset(serv_addr,0,sizeof(serv_addr));serv_addr.sin_familyAF_INET;serv_addr.sin_addr.s_addrhtonl(INADDR_ANY);serv_addr.sin_porthtons(argv[1]);if(bind(serv_sock,(struct sockaddr*)serv_addr,sizeof(serv_addr))-1)error_handling(bind() error);if(listen(serv_sock,5)-1)error_handling(listen() error);epfdepoll_create(EPOLL_SIZE);ep_eventsmalloc(sizeof(struct epoll_event)*EOLL_SIZE);event.eventsEPOLLIN;event.data.fdserv_sock;epoll_ctl(epfd,EPOLL_CTL_ADD,serv_sock,event);while(1){event_cntepoll_wait(epfd,ep_events,EPOLL_SIZE,-1);if(event_cnt-1){puts(epoll_wait() error);break;}for(i0;ievent_cnt;;i){if(ep_events[i].data.fdserv_sock){addr_szsizeof(clnt_addr);clnt_sockaccept(serv_sock,(struct sockaddr*)clnt_addr,sizeof(addr_sz));event.eventsEPOLLIN;event.data.fdclnt_sock;epoll_ctl(epfd,EPOLL_CTL_ADD,clnt_sock,event);printf(connected client: %d \n,clnt_sock);}else{str_lenread(ep_events[i].data.fd,buf,BUF_SIZE);if(str_len0){ //传输完成printf(close client: %d \n,ep_events[i].data.fd);close(ep_events[i].data.fd);epoll_ctl(epfd,EPOLL_CTL_DEL,ep_events[i].data.fd,NULL);}else{write(ep_events[i].data.fd,buf,str_len);//回声}}}}close(serv_sock);close(epfd);return 0;
}void error_handling(char *message){fputs(buf,stderr);fputc(\n,stderr);exit(1);
}
上述代码的演示和之前章节中的select函数代码相似可以结合之前章节中的代码思路进行理解。
条件触发和边缘触发
有些人学习epoll时往往无法正确区分条件触发Level Trigger和边缘触发Edge Trigger, 但只有理解了二者区别才算完整掌握epoll。
条件触发和边缘触发的区别在于发生事件的时间点
首先给出示例帮助各位理解条件触发和边缘触发。观察如下对话可以通过对话内容理解条件触发事件的特点。
儿子:妈妈这次期末考试我全部都是A。
妈妈:好棒!
儿子:我的总分排全班第一。
妈妈:做的好!
儿子:但我的总分在年级中只排第十名
妈妈:不要灰心以及很好了!
从上述对话可以看出儿子从说期末考开始一直向妈妈报告这就是条件触发的原理。我将其整理如下“条件触发方式中只要输入缓冲有数据就会一直通知该事。
例如服务器端输入缓冲收到50字节的数据时服务器端操作系统将通知该事件注册到发生变化的文件描述符。但服务器端读取20字节后还剩30字节的情况下仍会注册事件。也就是说条件触发方式中只要输入缓冲中还剩有数据就将以事件方式再次注册。接下来通过如下对话介绍边缘触发的事件特性。
儿子:妈妈我期末考了。
妈妈:“考的怎么样。”
儿子:.........
妈妈:说话啊!是考差了吗?
从上述对话可以看出边缘触发中输人缓冲收到数据时仅注册1次该事件。即使输入缓冲中 还留有数据也不会再进行注册。
掌握条件触发的事件特性
接下来通过代码来了解条件触发的事件注册方式。epoll默认是以条件触发的方式工作因此可以通过该实例验证条件触发的特性。
#include 与之前示例的头文件声明一致故省略。
#define BUF_SIZE 4
#define EPOLL_SIZE 50
void error_handling(char *buf);int main(int argc, char *argv[]){int serv_sock, clnt_sock;struct sockaddr_in serv_adr, clnt_adr;socklen_t adr_sz;int str_len, i;char buf[BUF_SIZE];struct epoll_event *ep_events;struct epoll_event event;int epfd, event_cnt;if(argc!2){printf(Usage : %s port\n, argv[0]);exit(1);}serv_socksocket(PF_INET, SOCK_STREAM, 0);memset(serv_adr, 0, sizeof(serv_adr));serv_adr.sin_familyAF_INET;serv_adr.sin_addr.s_addrhtonl(INADDR_ANY)serv_adr.sin_porthtons(atoi(argv[1]));if(bind(serv_sock,(struct sockaddr*) serv_adr, sizeof(serv_adr))-1)error_handling(bind() error);if(listen(serv_sock,5)-1)error_handling(listen() error);epfdepoll_create(EPOLL_SIZE);ep_eventsmalloc(sizeof(struct epoll_event)*EPOLL_SIZE);event.eventsEPOLLIN;event.data.fdserv_sock;epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, event);while(1){event_cntepoll_wait(epfd,ep_eventsEPOLL_SIZE-1);if(event_cnt-1){puts(epoll_wait() error);break;}puts(return epoll_wait);for(i0; ievent_cnt; i){if(ep_events[i].data.fdserv_sock){adr_szsizeof(clnt_adr);clnt_sockaccept(serv_sock, (struct sockaddr*)clnt_adr, adr_sz);event.eventsEPOLLIN;event.data.fdclnt_sock;epoll_ctl(epfd, EPOLL_CTL_ADD, clnt_sock,event);printf(connected client: %d \n, clnt_sock);}else{str_lenread(ep_events[i].data.fd, buf, BUF_SIZE);if(str_len0){ //关闭套接字printf(closed client: %d \n, ep_events[i].data.fd);close(ep_events[i].data.fd);epoll_ctl(epfd, EPOLL_CTL_DEL, ep_events[i].data.fd, NULL);}else{write(ep_events[i].data.fd,buf,str_len);//回声}}}}close(serv_sock);close(epfd);return 0;
}void error_handling(char *buf){//与之前实例相同故省略
}
上述示例与之前的差异如下。
□将调用read函数时使用的缓冲大小缩减为4个字节第2行
□插入验证epoll_wait函数调用次数的语句第50行 减少缓冲大小是为了阻止服务器端一次性读取接收的数据。换言之调用read函数后输入缓冲中仍有数据需要读取。而且会因此注册新的事件并从epoll_wait函数返回时将循环输出”return epoll_wait”字符串。
从运行结果中可以看出每当收到客户端数据时都会注册该事件并因此多次调用epoll_ wait函数。下面将上述示例改成边缘触发方式需要做一些额外的工作。但我希望通过最小的改动验证边缘触发模型的事件注册方式。将上述示例的第57行改成如下形式运行服务器端和客户端:
event.events EPOLLIN|EPOLLET; 更改后可以验证如下事实“从客户端接收数据时仅输出1次return epoll_wait字符串这意味着仅注册1次事件。”虽然可以验证上述事实但客户端运行时将发生错误。大家是否遇到了这种问题能否自行分析原因虽然目前不必对此感到困惑但如果理解了边缘触发的特性应该可以分析出错误原因。
边缘触发的服务器端实现中必知的两点
如下两点是实现边缘触发的必知内容。
□通过errno变量验证错误原因。 □为了完成非阻塞(Non-blocking)I/O更改套接字特性。
Linux的套接字相关函数一般通过返回-1通知发生了错误。虽然知道发生了错误但仅凭这 些内容无法得知产生错误的原因。因此为了在发生错误时提供额外的信息Linux声明了如下 全局变量
int errno;
为了访问该变量需要引入error.h头文件。另外每种函数发生错误时保存到errno变量中的值都不同没必要记住所有可能的值。学习每种函数的过程中逐一掌握并能在必要时参考即可。本节只介绍如下类型的错误read函数发现输入缓冲中没有数据可读时返回-1同时在errno中保存EAGAIN常量。”
稍后通过示例给出errno的使用方法。下面讲解将套接字改为非阻塞方式的方法。Linux提供更改或读取文件属性的如下方法曾在第13章使用过。
#includefcntl.h
int fcntl(int filedes,int cmd,...);
//成功是返回cmd参数相关值失败时返回-1filedes //目标的文件描述符。cmd //表示函数调用的目的。
从上述声明中可以看到fcntl具有可变参数的形式。如果向第二个参数传递F_GETFL可以获得第一个参数所指的文件描述符属性int型。反之如果传递F_SETFL可以更改文件描述符属性。若希望将文件套接字改为非阻塞模式需要如下2条语句。
int flag fcntl(fd, F_GETFL, 0);
fcntl(fdF_SETFLflag|O_NONBLOCK);
通过第一条语句获取之前设置的属性信息通过第二条语句在此基础上添加非阻塞O_NONBLOCK标志。调用readwrite函数时无论是否存在数据都会形成非阻塞文件。fcntl函数的适用范围很广各位既可以在学习系统编程时一次性总结所有适用情况也可以每次需要时逐一掌握。
实现边缘触发的回声服务器端
之所以介绍读取错误原因的方法和非阻塞模式的套接字创建方法原因在于二者都与边缘触发的服务器端实现有密切联系。
首先说明为何需要通过errno确认错误原因:“边缘触发方式中接收数据时仅注册1次该事件。” 就因为这种特点一旦发生输入相关事件就应该读取输入缓冲中的全部数据。因此需要验 证输入缓冲是否为空。(不然套接字将无法注销)
既然如此为何还需要将套接字变成非阻塞模式边缘触发方式下以阻塞方式工作的readwrite函数有可能引起服务器端的长时间停顿(等待数据到来)。因此边缘触发方式中一定要采用非阻塞readwrite函数。接下来给出以边缘触发方式工作的回声服务器端示例。
#include“添加fcntl.h、errno.h其他与之前示例的头文件声明一致。“
#include fcntl.h
#include errno.h
#define BUF SIZE 4
#define EPOLL_SIZE 50
void setnonblockingmode(int fd);
void error_handling(char *buf);int main(int argc, char *argv[]){int serv_sock,clnt_sock;struct sockaddr_in serv_adr, clnt_adr;socklen_t adr_sz;int str_len, i;char buf[BUF_SIZE];struct epoll_event *ep_events;struct epoll_event event;int epfd, event_cnt;if(argc!2){printf(Usage : %s port\n,argv[0]);exit(1);}serv_socksocket(PF_INET, SOCK_STREAM, 0);memset(serv_adr, 0, sizeof(serv_adr));serv_adr.sin_familyAF_INET;serv_adr.sin_addr.s_addrhtonl(INADDR_ANY);serv_adr.sin_porthtons(atoi(argv[1]));if(bind(serv_sock,(struct sockaddr*) serv_adr, sizeof(serv_adr))-1)error_handling(bind() error);if(listen(serv_sock,5)-1)error_handling(listen()error);epfdepoll_create(EPOLL_SIZE);ep_eventsmalloc(sizeof(struct epoll_event)*EPOLL_SIZE);setnonblockingmode(serv_sock);event.eventsEPOLLIN;event.data.fdserv_sock;epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, event);while(1){event_cntepoll_wait(epfd, ep_events, EPOLL_SIZE,-1);if(event_cnt-1){puts(epoll_wait() error);break;}puts(return epoll_wait);for(i0; ievent_cnt; i){if(ep_events[i].data.fdserv_sock){adr_szsizeof(clnt_adr);clnt_sockaccept(serv_sock,(struct sockaddr*)clnt_adr,adr_sz);setnonblockingmode(clnt_sock);event.eventsEPOLLIN|EPOLLET;event.data.fdclnt_sock;epoll_ctl(epfdEPOLL_CTL_ADD, clnt_sock,event);printf(connected client: %d \n, clnt_sock);}else{while(1){str_lenread(ep_events[i].data.fd, buf, BUF_SIZE);if(str_len0){ // 客户端关闭连接printf(closed client: %d \n, ep_events[i].data.fd);close(ep_events[i].data.fd);epoll_ctl(epfd, EPOLL_CTL_DEL, ep_events[i].data.fd, NULL);break;}else if(str_len0){if(errnoEAGAIN)break;else {write(ep_events[i].data.fd, buf, str_len); // 回声}}}}}
close(serv_sock);
close(epfd);
return 0;
}void setnonblockingmode(int fd){int flagfcntl(fd,F_GETFL,0);fcntl(fd,F_SETFL,flag|O_NONBLOCK);
}void error_handling(char *buf){//示例和之前的示例相同故省略
}条件触发和边缘触发孰强孰弱
我们从理论和代码的角度充分理解了条件触发和边缘触发但仅凭这些还无法理解边缘触发 相对于条件触发的优点。边缘触发方式下可以做到如下这点:“可以分离接收数据和处理数据的时间点!”
虽然比较简单但非常准确有力地说明了边缘触发的优点。关于这句话的含义大家以后开 不同类型的程序时会有更深入的理解。现阶段给出如下情景帮助大家理解:有一个服务器端和三个客户端这三个客户端分别是A,B,C它们分别向服务器发送它们对应部分的数据服务器需要将这些数据组合以A,B,C的正向顺序排列发送给任意主机。
那么对应服务端的运行过程如下:
□服务器端分别从客户端A、B、C接收数据。 □服务器端按照A、B、C的顺序重新组合收到的数据。 □组合的数据将发送给任意主机。
为了完成该过程若能按如下流程运行程序服务器端的实现并不难。 □客户端按照A、B、C的顺序连接服务器端并依序向服务器端发送数据。
□需要接收数据的客户端应在客户端A、B、C之前连接到服务器端并等待。
但现实中可能频繁出现如下这些情况换言之如下情况出现更符合实际。
□客户端C和B正向服务器端发送数据但A尚未连接到服务器端。
□客户端A、B、C乱序发送数据。 □服务器端已收到数据但要接收数据的目标客户端还未连接到服务器端。
因此即使输入缓冲收到数据注册相应事件服务器端也能决定读取和处理这些数据的 时间点这样就给服务器端的实现带来巨大的灵活性。相比于条件触发如果尝试分离接受数据和处理数据的时间的话则每次调用epoll_wait函数时都会产生相应事件。而且事件数也会累加服务端能够接受吗?
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/911907.shtml
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!