linux编程:多路转接IO之epoll模型
epoll模型:linux下最好用的多路转接模型
- epoll是Linux内核为处理大批量文件描述符而作了改进的poll,是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。
- 另一点原因就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了。
- epoll除了提供select/poll那种IO事件的水平触发(Level Triggered)外,还提供了边缘触发(Edge Triggered),这就使得用户空间程序有可能缓存IO状态,减少epoll_wait/epoll_pwait的调用,提高应用程序效率。
操作流程
- 程序中创建epoll句柄。内核中对应struct eventpoll结构体(epol的各项描述符的监控信息都是存放在内核中的,不需要重复添加)
int epoll_create(int size);//size决定了所能监控的描述符数量上限,但是在linux内核2.6之后弃用,使用了动态增长
- 为每一个需要监控的描述符组织事件结构,添加到内核的eventpoll结构中(采用的数据结构是红黑树)
struct epollevent{
uint32_t events;//需要监控的事件,以及监控返回后保存实际就绪的事件(EPOLLIN / EPOLLOUT)
typedef union epoll_data{//这个信息是描述符就绪后返回的信息,通过这个信息决定就绪后对哪个描述符进行操作
int fd; //通常会被设置为想要监控的描述符
void *ptr; //有时候也会自己组织一个结构,将其地址赋值过来,结构中必须包含有操作的描述符
}data;
}
int epoll ctl(int epfd--句柄,int cmd--操作类型, int fd--要监控的描述符,struct epollevent *ev--描述符对应的事件结构);
cmd: EPOLL_CTL_ADD / EPOLL_CTL_DEL/ EPOLL_CTL_MOD
- 调用epoll_wait开始监控,等到有描述符就绪或者等待超时则调用返回
int epoll_wait(int epfd,struct epollevent *evs,int maxevents,int timeout)
evs:是一个事件结构体数组的首地址,用于接收就绪的描述符事件结构信息
maxevents:每次想要获取的就绪事件最大数量,不大于数组的节点个数
- 监控返回后,根据epoll_wait返回值遍历evs数组,其中放置的都是就绪的描述符对应事件节点,直接对其中的描述符进行操作即可
epoll的IO状态触发模式
1. 水平触发 EPOLLLT : epoll和select以及poll的默认触发方式,并且select和poll只有水平触发
- 可读事件:接收缓冲区中有数据,就会触发就绪事件
- 可写事件:发送缓冲区中有空闲空间,就会触发就绪事件
2. 边缘触发:EPOLLET
- 可读事件:只有在新数据到来的时候才会触发一次事件(注意:若一次数据到来没有一次性读取完毕,则不会触发第二次)
- 可写事件:只有缓冲区剩余空间从无到有的时候才会触发一次事件
- 边缘触发,要求程序中最好能够一次将缓冲区中的数据完全读取,进行处理。但是因为我们不知道缓冲区中有多少数据,因此通常需要循环读取数据,但若是循环读取数据,若读取到缓冲区中没有数据,则recv会阻塞。
- 解决方案:将套接字描述符的非阻塞属性开启(没有数据的时候立即报错返回–EAGAIN)
- 设置方法:int fcntl(int fd, int cmd,…/* arg */); cmd: F_GETFL-获取属性,F_SETFL-设置属性–O_NONBLOCK(非阻塞属性)
边缘触发与水平触发的使用场景
- 大多数的情况下都使用的是水平触发,只有在特定情况下会使用边缘触发,比如想要一次从缓冲区中读取到一条完整的数据
- 边缘触发只是为了在特定情况下使用,避免水平触发一直触发就绪条件,但是不符合操作条件的空判断的情况,降低了程序处理效率
epoll的优缺点分析
- 缺点:跨平台移植性差
- 优点:
- 所能监控的描述符数量没有上限;
- 所有的描述符事件只需要向内核拷贝一次, 不需要重复拷贝
- 监控原理是异步阻塞操作(不是轮询遍历判断,而是监控由系统完成,系统为事件进行回调处理,将就绪事件添加到就绪链表中,程序只判断就绪事件链表是否为NULL就可以判断是否有就绪),性能不会随着描述符的增多而下降
- 直接返回的就是就绪的描述符以及事件信息,程序中进行遍历操作即可,没有空遍历
多路转接模型适用场景
- 有大量描述符需要进行事件监控:适用epoll
- 有单个描述符需要进行读写超时控制:适用select或poll
- 多路转接模型在大量描述符监控的情况下,只适用于有大量监控,但是同一时间只有少量描述符活跃的场景,因为多路转接模型,在流程中是对就绪的描述符进行轮询操作,如果活跃的描述符比较多,则最后一个描述符等待的时间会很长。
- 如果真的是大量活跃,这时候搭配线程池处理(将就绪的描述符放到线程池中,让线程池中的线程进行处理),多线程的均衡调度是系统负责。
epoll编程
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<netinet/in.h>//地址结构
#include<arpa/inet.h>//字节序转换接口
#include<sys/socket.h>//套接字接口
#include<sys/epoll.h>
#define MAX_LISTEN_NUM 5
int main()
{
//1.创建套接字
int sockfd=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
if(sockfd<0){
perror("socket error");
return -1;
}
//2.绑定地址信息
struct sockaddr_in addr;
addr.sin_family=AF_INET;
addr.sin_port=htons(9000);
addr.sin_addr.s_addr=inet_addr("0.0.0.0");
socklen_t len=sizeof(addr);
int ret = bind(sockfd,(struct sockaddr*)&addr,len);
if(ret<0){
perror("bind error");
return -1;
}
//3.开始监听
ret =listen(sockfd,MAX_LISTEN_NUM);
if(ret<0){
perror("listen error");
return -1;
}
int epfd=epoll_create(1);//创建epoll句柄
struct epoll_event ev;//为每个需要监控的描述符组织事件节点信息
ev.events=EPOLLIN;
ev.data.fd=sockfd;
epoll_ctl(epfd,EPOLL_CTL_ADD,sockfd,&ev);//添加监控
//4.获取新连接
while(1){
struct epoll_event evs[10]; //定义事件结构数组,同于获取就绪的事件节点
int nfds=epoll_wait(epfd,evs,10,3000);//开始监控
if(nfds<0){
perror("epoll_wait error");
continue;
}else if(nfds==0){
printf("No descriptor ready,timeout...\n");
continue;
}
for(int i=0;i<nfds;i++){
if(evs[i].events&EPOLLIN){//可读事件
if(evs[i].data.fd==sockfd){//监听描述符的就绪事件
struct sockaddr_in cliaddr;
int newfd=accept(sockfd,(struct sockaddr*)&cliaddr,&len);
if(newfd<0){
perror("accept error");
continue;
}
struct epoll_event ev;
ev.events=EPOLLIN;
ev.data.fd=newfd;
epoll_ctl(epfd,EPOLL_CTL_ADD,newfd,&ev);//将新建连接也添加进添加监控
}else{
//普通通信描述符的就绪事件
char buf[1024]={0};
//接收数据
ret=recv(evs[i].data.fd,buf,1023,0);
if(ret<=0){
perror("recv error");
epoll_ctl(epfd,EPOLL_CTL_DEL,evs[i].data.fd,NULL);//移除监控
close(evs[i].data.fd);
continue;
}
printf("client say:%s\n",buf);
//发送数据
ret=send(evs[i].data.fd,buf,strlen(buf),0);
if(ret<0){
perror("send error");
epoll_ctl(epfd,EPOLL_CTL_DEL,evs[i].data.fd,NULL);
close(evs[i].data.fd);
continue;
}
}
}else if(evs[i].events&EPOLLOUT){//可写事件
}
}
}
//6.关闭套接字
close(sockfd);
return 0;
}
通信演示
epoll_server
client客户端