linux IO复用之epoll
这篇文章是我查看网上各种文章来总结的,为自己学习来做个笔记!!!
大多数来源于:https://www.cnblogs.com/lojunren/p/3856290.html
首先,什么事是epoll?
epoll是Linux内核为处理大批句柄而作改进的poll,是Linux下多路复用IO接口select/poll的增强版本,它能显著的减少程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。因为它会复用文件描述符集合来传递结果而不是迫使开发者每次等待事件之前都必须重新准备要被侦听的文件描述符集合,另一个原因就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了。epoll除了提供select\poll那种IO事件的电平触发(Level Triggered)外,还提供了边沿触发(Edge Triggered),这就使得用户空间程序有可能缓存IO状态,减少epoll_wait/epoll_pwait的调用,提供应用程序的效率。
两种工作方式:
LT(level triggered):水平触发,缺省方式,同时支持block和no-block socket,在这种做法中,内核告诉我们一个文件描述符是否被就绪了,如果就绪了,你就可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的,所以,这种模式编程出错的可能性较小。传统的select\poll都是这种模型的代表。
ET(edge-triggered):边沿触发,高速工作方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪状态时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了(比如:你在发送、接受或者接受请求,或者发送接受的数据少于一定量时导致了一个EWOULDBLOCK错误)。但是请注意,如果一直不对这个fs做IO操作(从而导致它再次变成未就绪状态),内核不会发送更多的通知。
区别:LT事件不会丢弃,而是只要读buffer里面有数据可以让用户读取,则不断的通知你。而ET则只在事件发生之时通知。
epoll的优点:
1.支持一个进程打开大数目的socket描述符(FD)
select 最不能忍受的是一个进程所打开的FD是有一定限制的,由FD_SETSIZE设置,默认值是2048。对于那些需要支持的上万连接数目的IM服务器来说显然太少了。这时候你一是可以选择修改这个宏然后重新编译内核,不过资料也同时指出这样会带来网络效率的下降,二是可以选择多进程的解决方案(传统的 Apache方案),不过虽然linux上面创建进程的代价比较小,但仍旧是不可忽视的,加上进程间数据同步远比不上线程间同步的高效,所以也不是一种完美的方案。不过 epoll则没有这个限制,它所支持的FD上限是最大可以打开文件的数目,这个数字一般远大于2048,举个例子,在1GB内存的机器上大约是10万左右,具体数目可以cat /proc/sys/fs/file-max察看,一般来说这个数目和系统内存关系很大。
2.IO效率不随FD数目增加而线性下降
传统的select/poll另一个致命弱点就是当你拥有一个很大的socket集合,不过由于网络延时,任一时间只有部分的socket是"活跃"的,但是select/poll每次调用都会线性扫描全部的集合,导致效率呈现线性下降。但是epoll不存在这个问题,它只会对"活跃"的socket进行操作---这是因为在内核实现中epoll是根据每个fd上面的callback函数实现的。那么,只有"活跃"的socket才会主动的去调用 callback函数,其他idle状态socket则不会,在这点上,epoll实现了一个"伪"AIO,因为这时候推动力在os内核。在一些 benchmark中,如果所有的socket基本上都是活跃的---比如一个高速LAN环境,epoll并不比select/poll有什么效率,相反,如果过多使用epoll_ctl,效率相比还有稍微的下降。但是一旦使用idle connections模拟WAN环境,epoll的效率就远在select/poll之上了。
3.使用mmap加速内核与用户空间的消息传递
这点实际上涉及到epoll的具体实现了。无论是select,poll还是epoll都需要内核把FD消息通知给用户空间,如何避免不必要的内存拷贝就很重要,在这点上,epoll是通过内核于用户空间mmap同一块内存实现的。而如果你想我一样从2.5内核就关注epoll的话,一定不会忘记手工 mmap这一步的。
4.内核微调
这一点其实不算epoll的优点了,而是整个linux平台的优点。也许你可以怀疑linux平台,但是你无法回避linux平台赋予你微调内核的能力。比如,内核TCP/IP协议栈使用内存池管理sk_buff结构,那么可以在运行时期动态调整这个内存pool(skb_head_pool)的大小— 通过echo XXXX>/proc/sys/net/core/hot_list_length完成。再比如listen函数的第2个参数(TCP完成3次握手的数据包队列长度),也可以根据你平台内存大小动态调整。更甚至在一个数据包面数目巨大但同时每个数据包本身大小却很小的特殊系统上尝试最新的NAPI网卡驱动架构。
linux下epoll怎么实现高效的功能的
首先,我们现来介绍一下epoll中所使用的函数:
1、int epoll_create(int size)
创建一个epoll句柄,参数size用来告诉内核监听的数目。
2、int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
epoll事件注册函数,
参数epfd为epoll的句柄;
参数op表示动作,用3个宏来表示:EPOLL_CTL_ADD(注册新的fd到epfd),EPOLL_CTL_MOD(修改已经注册的fd的监听事件),EPOLL_CTL_DEL(从epfd删除一个fd);
参数fd为需要监听的标示符;
参数event告诉内核需要监听的事件,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队列里
3、 int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout)
等待事件的产生,类似于select()调用。参数events用来从内核得到事件的集合,maxevents告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size,参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。该函数返回需要处理的事件数目,如返回0表示已超时。
由此可知, epoll精巧的使用了3个方法来实现select方法要做的事:
- 新建epoll描述符==epoll_create()
- epoll_ctrl(epoll描述符,添加或者删除所有待监控的连接)
返回的活跃连接 ==epoll_wait( epoll描述符 )
与select相比,epoll分清了频繁调用和不频繁调用的操作。例如,epoll_ctrl是不太频繁调用的,而epoll_wait是非常频繁调用的。这时,epoll_wait却几乎没有入参,这比select的效率高出一大截,而且,它也不会随着并发连接的增加使得入参越发多起来,导致内核执行效率下降。
我们继续深入看看epoll怎么实现的高效:
epoll的三大关键要素:mmap、红黑树、链表。
epoll是通过内核与用户空间mmap同一块内存实现的。mmap将用户空间的一块地址和内核空间的一块地址同时映射到相同的一块物理内存地址(不管是用户空间还是内核空间都是虚拟地址,最终要通过地址映射映射到物理地址),使得这块物理内存对内核和对用户均可见,减少用户态和内核态之间的数据交换。内核可以直接看到epoll监听的句柄,效率高。
红黑树将存储epoll所监听的套接字。上面mmap出来的内存如何保存epoll所监听的套接字,必然也得有一套数据结构,epoll在实现上采用红黑树去存储所有套接字,当添加或者删除一个套接字时(epoll_ctl),都在红黑树上去处理,红黑树本身插入和删除性能比较好,时间复杂度O(logN)。
通过epoll_ctl函数添加进来的事件都会被放在红黑树的某个节点内,所以,重复添加是没有用的。当把事件添加进来的时候时候会完成关键的一步,那就是该事件都会与相应的设备(网卡)驱动程序建立回调关系,当相应的事件发生后,就会调用这个回调函数,该回调函数在内核中被称为:ep_poll_callback,**这个回调函数其实就所把这个事件添加到rdllist这个双向链表中**。一旦有事件发生,epoll就会将该事件添加到双向链表中。那么当我们调用epoll_wait时,epoll_wait只需要检查rdlist双向链表中是否有存在注册的事件,效率非常可观。这里也需要将发生了的事件复制到用户态内存中即可。
下面几个关键数据结构的定义
1 struct epitem
2 {
3 struct rb_node rbn; //用于主结构管理的红黑树
4 struct list_head rdllink; //事件就绪队列
5 struct epitem *next; //用于主结构体中的链表
6 struct epoll_filefd ffd; //每个fd生成的一个结构
7 int nwait;
8 struct list_head pwqlist; //poll等待队列
9 struct eventpoll *ep; //该项属于哪个主结构体
10 struct list_head fllink; //链接fd对应的file链表
11 struct epoll_event event; //注册的感兴趣的事件,也就是用户空间的epoll_event
12 }
1 struct eventpoll
2 {
3 spin_lock_t lock; //对本数据结构的访问
4 struct mutex mtx; //防止使用时被删除
5 wait_queue_head_t wq; //sys_epoll_wait() 使用的等待队列
6 wait_queue_head_t poll_wait; //file->poll()使用的等待队列
7 struct list_head rdllist; //事件满足条件的链表
8 struct rb_root rbr; //用于管理所有fd的红黑树
9 struct epitem *ovflist; //将事件到达的fd进行链接起来发送至用户空间
10 }
epoll_wait的工作流程:
epoll_wait调用ep_poll,当rdlist为空(无就绪fd)时挂起当前进程,直到rdlist不空时进程才被唤醒。
文件fd状态改变(buffer由不可读变为可读或由不可写变为可写),导致相应fd上的回调函数ep_poll_callback()被调用。
ep_poll_callback将相应fd对应epitem加入rdlist,导致rdlist不空,进程被唤醒,epoll_wait得以继续执行。
ep_events_transfer函数将rdlist中的epitem拷贝到txlist中,并将rdlist清空。
ep_send_events函数(很关键),它扫描txlist中的每个epitem,调用其关联fd对用的poll方法。此时对poll的调用仅仅是取得fd上较新的events(防止之前events被更新),之后将取得的events和相应的fd发送到用户空间(封装在struct epoll_event,从epoll_wait返回)。
有了这些理论基础,我们开始实现一个小例子:
https://www.cnblogs.com/lojunren/p/3856290.html
这篇博客中的实例很有参考价值。
代码:摘自:https://github.com/shineyr/Socket/tree/master/epoll_socket
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <time.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <fcntl.h>
#ifndef CONNECT_SIZE
#define CONNECT_SIZE 256
#endif
#define PORT 7777
#define MAX_LINE 2048
#define LISTENQ 20
void setNonblocking(int sockfd)
{
int opts;
opts=fcntl(sockfd,F_GETFL);
if(opts<0)
{
perror("fcntl(sock,GETFL)");
return;
}//if
opts = opts|O_NONBLOCK;
if(fcntl(sockfd,F_SETFL,opts)<0)
{
perror("fcntl(sock,SETFL,opts)");
return;
}//if
}
int main(int argc , char **argv)
{
int i, listenfd, connfd, sockfd, epfd, nfds;
ssize_t n, ret;
char buf[MAX_LINE];
socklen_t clilen;
struct sockaddr_in servaddr , cliaddr;
/*声明epoll_event结构体变量,ev用于注册事件,数组用于回传要处理的事件*/
struct epoll_event ev, events[20];
/*(1) 得到监听描述符*/
listenfd = socket(AF_INET , SOCK_STREAM , 0);
setNonblocking(listenfd);
/*生成用于处理accept的epoll专用文件描述符*/
epfd = epoll_create(CONNECT_SIZE);
/*设置监听描述符*/
ev.data.fd = listenfd;
/*设置处理事件类型*/
ev.events = EPOLLIN | EPOLLET;
/*注册事件*/
epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev);
/*(2) 绑定套接字*/
bzero(&servaddr , sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(PORT);
bind(listenfd , (struct sockaddr *)&servaddr , sizeof(servaddr));
/*(3) 监听*/
listen(listenfd , LISTENQ);
/*(4) 进入服务器接收请求死循环*/
while(1)
{
/*等待事件发生*/
nfds = epoll_wait(epfd , events , CONNECT_SIZE , -1);
if(nfds <= 0)
continue;
printf("nfds = %d\n" , nfds);
/*处理发生的事件*/
for(i=0 ; i<nfds ; ++i)
{
/*检测到用户链接*/
if(events[i].data.fd == listenfd)
{
/*接收客户端的请求*/
clilen = sizeof(cliaddr);
if((connfd = accept(listenfd , (struct sockaddr *)&cliaddr , &clilen)) < 0)
{
perror("accept error.\n");
exit(1);
}//if
printf("accpet a new client: %s:%d\n", inet_ntoa(cliaddr.sin_addr) , cliaddr.sin_port);
/*设置为非阻塞*/
setNonblocking(connfd);
ev.data.fd = connfd;
ev.events = EPOLLIN | EPOLLET;
epoll_ctl(epfd , EPOLL_CTL_ADD , connfd , &ev);
}//if
/*如果是已链接用户,并且收到数据,进行读入*/
else if(events[i].events & EPOLLIN){
if((sockfd = events[i].data.fd) < 0)
continue;
bzero(buf , MAX_LINE);
printf("reading the socket~~~\n");
if((n = read(sockfd , buf , MAX_LINE)) <= 0)
{
close(sockfd);
events[i].data.fd = -1;
}//if
else{
buf[n] = '\0';
printf("clint[%d] send message: %s\n", i , buf);
/*设置用于注册写操作文件描述符和事件*/
ev.data.fd = sockfd;
ev.events = EPOLLOUT| EPOLLET;
epoll_ctl(epfd , EPOLL_CTL_MOD , sockfd , &ev);
}//else
}//else
else if(events[i].events & EPOLLOUT)
{
if((sockfd = events[i].data.fd) < 0)
continue;
if((ret = write(sockfd , buf , n)) != n)
{
printf("error writing to the sockfd!\n");
break;
}//if
/*设置用于读的文件描述符和事件*/
ev.data.fd = sockfd;
ev.events = EPOLLIN | EPOLLET;
/*修改*/
epoll_ctl(epfd , EPOLL_CTL_MOD , sockfd , &ev);
}//else
}//for
}//while
free(events);
close(epfd);
exit(0);
}
cli.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <time.h>
#include <sys/socket.h>
#include <sys/select.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <fcntl.h>
#define PORT 7777
#define MAX_LINE 2048
int max(int a , int b)
{
return a > b ? a : b;
}
/*readline函数实现*/
ssize_t readline(int fd, char *vptr, size_t maxlen)
{
ssize_t n, rc;
char c, *ptr;
ptr = vptr;
for (n = 1; n < maxlen; n++) {
if ( (rc = read(fd, &c,1)) == 1) {
*ptr++ = c;
if (c == '\n')
break; /* newline is stored, like fgets() */
} else if (rc == 0) {
*ptr = 0;
return(n - 1); /* EOF, n - 1 bytes were read */
} else
return(-1); /* error, errno set by read() */
}
*ptr = 0; /* null terminate like fgets() */
return(n);
}
/*普通客户端消息处理函数*/
void str_cli(int sockfd)
{
/*发送和接收缓冲区*/
char sendline[MAX_LINE] , recvline[MAX_LINE];
while(fgets(sendline , MAX_LINE , stdin) != NULL)
{
write(sockfd , sendline , strlen(sendline));
bzero(recvline , MAX_LINE);
if(readline(sockfd , recvline , MAX_LINE) == 0)
{
perror("server terminated prematurely");
exit(1);
}//if
if(fputs(recvline , stdout) == EOF)
{
perror("fputs error");
exit(1);
}//if
bzero(sendline , MAX_LINE);
}//while
}
int main(int argc , char **argv)
{
/*声明套接字和链接服务器地址*/
int sockfd;
struct sockaddr_in servaddr;
/*判断是否为合法输入*/
if(argc != 2)
{
perror("usage:tcpcli <IPaddress>");
exit(1);
}//if
/*(1) 创建套接字*/
if((sockfd = socket(AF_INET , SOCK_STREAM , 0)) == -1)
{
perror("socket error");
exit(1);
}//if
/*(2) 设置链接服务器地址结构*/
bzero(&servaddr , sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(PORT);
if(inet_pton(AF_INET , argv[1] , &servaddr.sin_addr) < 0)
{
printf("inet_pton error for %s\n",argv[1]);
exit(1);
}//if
/*(3) 发送链接服务器请求*/
if(connect(sockfd , (struct sockaddr *)&servaddr , sizeof(servaddr)) < 0)
{
perror("connect error");
exit(1);
}//if
/*调用消息处理函数*/
str_cli(sockfd);
exit(0);