linux下的I/O复用模型之epoll详解
- epoll模型函数原理和使用
epoll的监听集合实质是一棵红黑树
头文件:
#include <sys/epoll.h>
(1)int epfd = epll_create(int size)
参数:
size:树节点的数量
可以用命令查看进程能够打开的最大数目的文件描述符
xw@ubuntu:~$ cat /proc/sys/fs/file-max
394030
返回值:成功返回一个非负的指向树的文件描述符,失败返回-1。
(2)epoll_ctl(int epfd,int op,int fd,struct epoll_event *event)
参数:
epfd:函数 epll_create(int size)的返回值
op:系统指定的操作类型,负责树节点的增删改
(a)添加:EPOLL_CTL_ADD
(b)修改:EPOLL_CTL_MOD
(c)删除:EPOLL_CTL_DEL
fd:操作的文件描述符
event:指定内核要监听事件,它是struct epoll_event结构类型的指针
struct epoll_event {
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
其中events可以是以下几个宏的集合:
EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
EPOLLOUT:表示对应的文件描述符可以写;
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,默认的是水平触发(Level Triggered)。
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听
这个socket的话,需要再次把这个socket加入到EPOLL队列里
返回值:成功返回0,失败返回-1。
(3)int epoll_wait(int epfd, struct epoll_event *events,
int maxevents, int timeout);
参数:
(1)监听的树地址
(2)就绪队列的首地址
就绪队列中传出就绪的socket集合,用户只需要遍历这些并处理,
而不用像select那样只知道就绪socket的个数去自己校验。
这也是epoll模型不需要传出参数的原因
(3)就绪最大数量
(4)超时事件(工作方式)
(a)-1:阻塞
(b)0:立即返回(轮询)非阻塞,
(3)>0:定时等待
返回值:成功返回就绪的文件描述符个数,失败返回-1,时间超时返回0
epoll工作模式:
epoll对文件描述符的操作有两种模式:LT(level trigger)和ET(edge trigger)。
修改工作模式:
struct epoll_event ev;
ev.data.fd = serverfd;
ev.events |= EPOLLET;
修改套接字属性为非阻塞
(a)可以在创建的时候初始化
socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, IPPROTO_TCP);
(b)可以自己通过函数修改
fcntl(sockfd, F_SETFL, fcntl(sockfd, F_GETFL, 0) | O_NONBLOCK);
或者
ioctl(sockfd, FIONBIO, 1);
LT模式是默认模式,LT模式与ET模式的区别如下:
LT模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,
应用程序可以不立即处理该事件。下次调用epoll_wait时,
会再次响应应用程序并通知此事件。
只要这个fd还有数据可读,每次 epoll_wait都会返回它的事件,
提醒用户程序去操作。
ET模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,
应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,
不会再次响应应用程序并通知此事件。
所以在ET模式下,read一个fd的时候一定要把它的buffer读光,
也就是说一直读到read的返回值小于请求值,或者遇到EAGAIN错误。
LT模式:有较大的系统开销,但是可以保证数据的完整性
LT(level triggered)是缺省的工作方式,并且同时支持block和no-block socket.在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的。
ET模式:开销更小,但是需要用户在操作读取事件数据时自己保障数据的读取完整性
ET(edge-triggered)是高速工作方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了(比如,你在发送,接收或者接收请求,或者发送接收的数据少于一定量时导致了一个EWOULDBLOCK 错误)。但是请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once)
ET模式在很大程度上减少了epoll事件被重复触发的次数,因此效率要比LT模式高。epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。
- epoll服务器代码
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<sys/select.h>
#include<arpa/inet.h>
#include<sys/epoll.h>
#define MAXEVENTS 5000
int main()
{
//网络初始化
char recvbuf[1024];
char ip[16];
int ret;
int epfd;
int nfds;
int clientfd;
struct epoll_event ev,ready[MAXEVENTS];
struct sockaddr_in addrserver,addrclient;
bzero(&addrserver,sizeof(addrserver));
bzero(&recvbuf,sizeof(recvbuf));
bzero(&ip,sizeof(ip));
addrserver.sin_family = AF_INET;
addrserver.sin_port = htons(27015);
addrserver.sin_addr.s_addr = htonl(INADDR_ANY);
//创建socket
int serverfd = socket(AF_INET,SOCK_STREAM,0);
if(serverfd == -1){
printf("create socket error\n");
return 1;
}
//绑定
ret = bind(serverfd,(struct sockaddr*)&addrserver,sizeof(addrserver));
if(ret == -1)
{
printf("bind error\n");
close(serverfd);
return 1;
}
//监听
ret = listen(serverfd,128);
if(ret == -1)
{
printf("listen error\n");
close(serverfd);
return 1;
}
//创建epoll监听树
epfd = epoll_create(MAXEVENTS);
if(epfd == -1)
{
printf("epoll_create error\n");
return -1;
}
//准备树节点结构
ev.data.fd = serverfd;
ev.events = EPOLLIN;
//添加监听节点
ret = epoll_ctl(epfd,EPOLL_CTL_ADD,serverfd,&ev);
if(ret == -1)
{
printf("epoll_ctl: add serverfd error\n");
return -1;
}
printf("epoll Server Runing.....\n");
while(1)
{
nfds = epoll_wait(epfd,ready,MAXEVENTS,-1);
if(nfds == -1)
{
printf("epoll_wait failed\n");
return -1;
}
for(int i = 0; i < nfds; ++i)
{
//监听客户端建立连接
if(ready[i].data.fd == serverfd)
{
socklen_t size = sizeof(addrclient);
clientfd = accept(serverfd,(struct sockaddr*)&addrclient,&size);
if(clientfd == -1)
{
printf("accept error\n");
return -1;
}
//添加到树中
ev.data.fd = clientfd;
ev.events = EPOLLIN;
ret = epoll_ctl(epfd,EPOLL_CTL_ADD,clientfd,&ev);
if(ret == -1)
{
printf("epoll_ctl: add clientfd error\n");
return -1;
}
}
else //数据传输
{
ret = recv(ready[i].data.fd,recvbuf,sizeof(recvbuf),0);
if(ret > 0)
{
printf("recv data is:%s\n",recvbuf);
continue;
}
else if(ret == 0)
{
printf("client normal closed...\n");
close(ready[i].data.fd);
epoll_ctl(epfd,EPOLL_CTL_DEL,ready[i].data.fd,NULL);
break;
}
else{
printf("recv error\n");
close(ready[i].data.fd);
epoll_ctl(epfd,EPOLL_CTL_DEL,ready[i].data.fd,NULL);
break;
}
}
}
}
close(serverfd);
return 0;
}
-
测试结果
-
模型评价
优点:
(1) 没有最大并发连接的限制,能打开的文件描述符的上限远大于1024(1G的内存上能监听约10万个端口),真正意义上突破了文件描述符的个数限制。上限是最大可以打开文件的数目,一般来说这个数目和系统内存关系很大
(2)epoll并采用轮询的方式,也就是没有使用内核IO设备队列,而是自己创建一个监听队列,使用的是异步抛出手段(事件/通知/回调),而是通过每个fd定义的回调函数来实现的,只有就绪的fd才会执行回调函数。 所以 epoll 不会随着FD数目的增加效率下降。只有活跃可用的fd才会调用callback函数。即 Epoll 最大的优点就在于它只管你“活跃”的连接,而跟连接总数无关,因此在实际的网络环境中,epoll的效率就会远远高于select和poll。
(3) 为了避免epoll大量的就绪拷贝开销,采用mmap内存共享映射的技术。利用mmap()文件映射内存加速与内核空间的消息传递;即epoll使用mmap减少复制开销。
(4)epoll的监听集合不会产生重复拷贝挂载现象,每一个监听的socket只会拷贝挂载一次
缺点:如果对epoll频繁的插入,删除操作,可能会导致维护数据结构的开销越来越大,处理效率降低(甚至会低于select和poll)。
参考资料:
1.Linux IO模式及 select、poll、epoll详解
2.Linux网络编程—I/O复用模型之epoll
3.服务器编程心得(四)—— 如何将socket设置为非阻塞模式
linux学习笔记