Linux: I/O多路转接之epoll(有图有代码有真相!!!)

来源:互联网 发布:少女前线淘宝 账号 编辑:程序博客网 时间:2024/06/03 17:58

一、基本概念

epoll是Linux内核为处理大批量文件描述符而作了改进的poll,是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。

另一点原因就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了。epoll除了提供select/poll那种IO事件的

水平触发(Level Triggered)外,还提供了边缘触发(Edge Triggered),这就使得用户空间程序有可能缓存IO状态,减少epoll_wait/epoll_pwait的调用,提高应用程序效率。

二、epoll函数解析

epoll过程分为三个接口

#include <sys/epoll.h>

int epoll_create(int size);

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

int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);


1、epoll_create(int size):

创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大。这个参数不同于select()中的第一个参数,给出最大监听的fd+1的值。需要注意的是,当创建好epoll句柄后,它就是会占用一个fd值,

在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。


2、epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)

epoll的事件注册函数,它不同与select()是在监听事件时告诉内核要监听什么类型的事件epoll的事件注册函数,它不同与select()是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。

第一个参数是epoll_create()的返回值;

第二个参数表示动作,用三个宏来表示:

EPOLL_CTL_ADD:注册新的fd到epfd中;

EPOLL_CTL_MOD:修改已经注册的fd的监听事件;

EPOLL_CTL_DEL:从epfd中删除一个fd;

第三个参数是需要监听的fd;

第四个参数是告诉内核需要监听什么事,struct epoll_event结构如下:

typedef  union epoll_data{       void *ptr;       int fd;       __uint32_t u32;       __uint64_t u64;}epoll_data_t;
 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、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 的工作原理

epoll同样只告知那些就绪的⽂文件描述符,⽽而且当我们调⽤用epoll_wait()获得就绪⽂文件描述符时,返回的不是实际的描述符,⽽而是⼀一个代表就绪描述符数量的值,

你只需要去epoll指定的⼀个数组中依次取得相应数量的⽂文件描述符即可,这⾥里也使⽤用了内存映射(mmap)技术,这样便彻底省掉了这些⽂文件描述符在系统调⽤用时复制的开销。

另⼀一个本质的改进在于epoll采⽤用基于事件的就绪通知⽅方式。在select/poll中,进程只有在调⽤用⼀一定的⽅方法后,内核才对所有监视的⽂文件描述符进⾏行扫描,

⽽而epoll事先通过epoll_ctl()来注册⼀一个⽂文件描述符,⼀一旦基于某个⽂文件描述符就绪时,内核会采⽤用类似callback的回调机制,迅速激活这个⽂文件描述符,

当进程调⽤用epoll_wait()时便得到通知。


Epoll的2种⼯工作⽅方式-⽔水平触发(LT)和边缘触发(ET):  

假如有这样一个例子:

1. 我们已经把一个用来从管道中读取数据的文件句柄(RFD)添加到epoll描述符

2. 这个时候从管道的另一端被写入了2KB的数据

3. 调用epoll_wait(2),并且它会返回RFD,说明它已经准备好读取操作

4. 然后我们读取了1KB的数据

5. 调用epoll_wait(2)......


Edge Triggered 工作模式:

如果我们在第1步将RFD添加到epoll描述符的时候使用了EPOLLET标志,那么在第5步调用epoll_wait(2)之后将有可能会挂

起,因为剩余的数据还存在于文件的输入缓冲区内,而且数据发出端还在等待一个针对已经发出数据的反馈信息。只有在监视

的文件句柄上发生了某个事件的时候 ET 工作模式才会汇报事件。因此在第5步的时候,调用者可能会放弃等待仍在存在于文

件输入缓冲区内的剩余数据。在上面的例子中,会有一个事件产生在RFD句柄上,因为在第2步执行了一个写操作,然后,

事件将会在第3步被销毁。因为第4步的读取操作没有读空文件输入缓冲区内的数据,因此我们在第5步调用 epoll_wait(2)完成

后,是否挂起是不确定的。epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操

作把处理多个文件描述符的任务饿死。最好以下面的方式调用ET模式的epoll接口,在后面会介绍避免可能的缺陷。

   i    基于非阻塞文件句柄

   ii   只有当read(2)或者write(2)返回EAGAIN时才需要挂起,等待。但这并不是说每次read()时都需要循环读,直到读到产生一

个EAGAIN才认为此次事件处理完成,当read()返回的读到的数据长度小于请求的数据长度时,就可以确定此时缓冲中已没有

数据了,也就可以认为此事读事件已处理完成。


Level Triggered 工作模式

相反的,以LT方式调用epoll接口的时候,它就相当于一个速度比较快的poll(2),并且无论后面的数据是否被使用,因此他们具

有同样的职能。因为即使使用ET模式的epoll,在收到多个chunk的数据的时候仍然会产生多个事件。调用者可以设定

EPOLLONESHOT标志,在 epoll_wait(2)收到事件后epoll会与事件关联的文件句柄从epoll描述符中禁止掉。因此当 

EPOLLONESHOT设定后,使用带有 EPOLL_CTL_MOD标志的epoll_ctl(2)处理文件句柄就成为调用者必须作的事情。

注:ET模式在很大程度上减少了epoll事件被重复触发的次数,因此效率要比LT模式高。epoll工作在ET模式的时候,必须使用

非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。


LT(level triggered)是epoll缺省的⼯工作⽅方式,并且同时⽀支持block和no-block socket.在这种做法中,内核告诉你⼀一个⽂

文件描述符是否就绪了,然后你可以对这个就绪的fd进⾏行IO操作。如果你不作任何操作,内核还是会继续通知你 的,所以,

这种模式编程出错误可能性要⼩小一点。传统的select/poll都是这种模型的代表。


ET (edge-triggered)是⾼高速⼯工作⽅方式,只⽀支持no-block socket,它效率要⽐比LT更⾼高。ET与LT的区别在于,当一个

新的事件到来时,ET模式下当然可以从epoll_wait调⽤用中获取到这个事件,可是如果这次没有把这个事件对应的套接字缓冲

区处理完,在这个套接字中没有新的事件再次到来时,在ET模式下是⽆无法再次从epoll_wait调⽤用中获取这个事件的。⽽而

LT模式正好相反,只要⼀一个事件对应的套接字缓冲区还有数据,就总能从epoll_wait中获取这个事件。因此,LT模式下开发

基于epoll的应⽤用要简单些,不太容易出错。⽽而在ET模式下事件发⽣生时,如果没有彻底地将缓冲区数据处理完,则会导

致缓冲区中的⽤用户请求得不到响应。Nginx默认采⽤用ET模式来使⽤用epoll。



四、epoll实现服务器

#include<stdio.h>#include<sys/epoll.h>#include<sys/types.h>#include<sys/socket.h>#include<netinet/in.h>#include<arpa/inet.h>#include<stdlib.h>#include<string.h>static void usage(const char *proc){  printf("usage:%s [local_ip] [local_port]",proc);}typedef struct fd_buf{  int fd;  char buf[10240];}fd_buf_t,*fd_buf_p;static void *alloc_fd_buf(int fd){ fd_buf_p tmp=(fd_buf_p)malloc(sizeof(fd_buf_t)); if(!tmp) {   perror("malloc");   return NULL; } tmp->fd=fd; return tmp;}int startup(const char *_ip,const int _port){ int sock=socket(AF_INET,SOCK_STREAM,0); if(sock<0) {   perror("socket");   return 2; } struct sockaddr_in local; local.sin_family=AF_INET; local.sin_port=htons(_port); local .sin_addr.s_addr=inet_addr(_ip); if(bind(sock,(struct sockaddr*)&local,sizeof(local))<0) {  perror("bind");  return 3; } if(listen(sock,10)<0) {  perror("listen");  return 4; } return sock;}int main(int argc,char *argv[]){  if(argc!=3)  {    usage(argv[0]);    return 1;  }  int listen_sock=startup(argv[1],atoi(argv[2]));    int epfd=epoll_create(256);  if(epfd<0)  {     printf("epoll_create");     close(listen_sock);     return 5;  }  struct epoll_event ev;  ev.events=EPOLLIN;  ev.data.ptr=alloc_fd_buf(listen_sock);  epoll_ctl(epfd,EPOLL_CTL_ADD,listen_sock,&ev);  int num=0;  struct epoll_event evs[64];  int timeout=-1;  while(1)  {   switch((num=epoll_wait(epfd,evs,64,timeout)))   {     //等待失败      case -1:         perror("epoll_wait");         break;     //超时         case 0:         perror("timeout...");         break;     //等待成功     default:         {           int i=0;           for(;i<num;i++)           {            fd_buf_p fp=(fd_buf_p)evs[i].data.ptr;            if(fp->fd==listen_sock && \                    (evs[i].events & EPOLLIN))            {            struct sockaddr_in client;            socklen_t len=sizeof(client);            int new_sock=accept(listen_sock,\                    (struct sockaddr*)&client,&len);            if(new_sock<0)            {                perror("accept");                continue;            }            printf("get a new client\n");            ev.events=EPOLLIN;            ev.data.ptr=alloc_fd_buf(new_sock);            epoll_ctl(epfd,EPOLL_CTL_ADD,\                    new_sock,&ev);            }            else if(fp->fd!=listen_sock)            {            //读事件             if(evs[i].events & EPOLLIN)             {               ssize_t s=read(fp->fd,fp->buf,sizeof(fp->buf));               if(s>0)               {                fp->buf[s]=0;                printf("client say:%s\n",fp->buf);                ev.events=EPOLLOUT;                ev.data.ptr=fp;                epoll_ctl(epfd,EPOLL_CTL_MOD,fp->fd,&ev);               }               else if(s<=0)               {                close(fp->fd);                epoll_ctl(epfd,EPOLL_CTL_DEL,fp->fd,NULL);                free(fp);               }                else{}             }             //写事件             else if(evs[i].events & EPOLLOUT)             {              const char *msg="HTTP/1.0 200 OK\r\n\r\n<html><h1>hello epoll</h1></html>";              write(fp->fd,msg,strlen(msg));              close(fp->fd);              epoll_ctl(epfd,EPOLL_CTL_DEL,fp->fd,NULL);              free(fp);             }             else{}            }            else{}           }         }   break;   }  }  return 0;}

五、epoll与select、poll比较


1 支持一个进程所能打开的最大连接数

 select                  单个进程所能打开的最大连接数有FD_SETSIZE宏定义,其大小是32个整数的大小(在32位的机器上,大小就是32*32,同理64位机器上 FD_SETSIZE为32*64),当然我们可以对它进行修改,然后重新编译内核,但是性能可能会受到影响,这需要进一步的测试。 poll poll本质上和select没有区别,但是它没有最大连接数的限制,原因是它是基于链表来存储的 

epoll

 虽然连接数有上限,但是很大,1G内存的机器上可以打开10万左右的连接,2G内存的机器可以打开20万左右的连接。

 

2 FD剧增后带来的IO效率问题

 select                   

 因为每次调用时都会对连接进行线性遍历,所以随着FD的增加会造成遍历速度慢的“线性下降性能问题”。 poll 同上 epoll 因为epoll内核中实现是根据每个fd上的callback函数来实现的,只有活跃的socket才会主动调用callback,所以在活跃socket较少的情况下,使用epoll没有前面两者的线性下降的性能问题,但是所有socket都很活跃的情况下,可能会有性能问题。

   

3 消息传递方式

 select 内核需要将消息传递到用户空间,都需要内核拷贝动作 poll 同上 epoll epoll通过内核和用户空间共享一块内存来实现的

   

综上,在选择select,poll,epoll时要根据具体的使用场合以及这三种方式的自身特点。表面上看epoll的性能最好,但是在连接数少并且连接都十

分活跃的情况下,select和poll的性能可能比epoll好,毕竟epoll的通知机制需要很多函数回调。



原创粉丝点击