一、What:异步IO Epoll是什么? 

1. 一句话总结

允许进程发起很多I/O操作,而不用阻塞或等待任何操作完成。Epoll是Linux下的网络异步IO库函数。

 

2. 详细说说

 一般来说,服务器端的I/O主要有两种情况:一是来自网络的I/O;二是对文件(设备)的I/O。Windows的异步I/O模型能很好的适用于这两种情况。而Linux针对前者提供了epoll模型,针对后者提供了AIO模型(关于是否把两者统一起来争论了很久)。今天我们聊聊前者。


epoll相关函数
1)epoll_create函数
NAME
       epoll_create, epoll_create1 - open an epoll file descriptor

SYNOPSIS
       #include <sys/epoll.h>

       int epoll_create(int size);
       int epoll_create1(int flags);
DESCRIPTION
       epoll_create() creates an epoll "instance", requesting the kernel to
       allocate an event backing store dimensioned  for  size  descriptors.
       The  size  is  not  the maximum size of the backing store but just a
       hint to the kernel  about  how  to  dimension  internal  structures.
       (Nowadays, size is ignored; see NOTES below.)

       epoll_create()  returns a file descriptor referring to the new epoll
       instance.  This file descriptor is used for all the subsequent calls
       to  the epoll interface.  When no longer required, the file descrip‐
       tor returned by epoll_create() should be closed by  using  close。
epoll_create()创建一个epoll句柄,内核会分配一个空间用来存放你想关注的文件描述符上是否发生以及发生了什么事件。
epoll_create()返回一个代指新epoll句柄的fd。这个fd作为epoll的接口,会在接下来的所有epoll调用中使用这个fd。

2)epoll_ctl函数
NAME
       epoll_ctl - control interface for an epoll descriptor

SYNOPSIS
       #include <sys/epoll.h>

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

DESCRIPTION
       This  system  call  performs  control  operations on the epoll instance  referred to by the file descriptor epfd. 

1)int op
 It requests that  the  operation op be performed for the target file descriptor fd.
epoll_ctl执行在epfd代表的epoll句柄上的控制操作。操作op将会在目标文件描述符fd上进行操作。
       Valid values for the op argument are :

       EPOLL_CTL_ADD
              Register  the  target  file  descriptor fd on the epoll instance
              referred to by the file descriptor epfd and associate the  event
              event with the internal file linked to fd.
EPOLL_CTL_ADD:将目标文件描述符fd注册到epoll句柄上,并使事件类型event与文件描述符fd相关联。
       EPOLL_CTL_MOD
              Change  the event event associated with the target file descrip‐
              tor fd.
EPOLL_CTL_MOD:改变目标文件描述符fd的事件类型。
       EPOLL_CTL_DEL
              Remove (deregister) the target file descriptor fd from the epoll
              instance  referred  to by epfd.  The event is ignored and can be
              NULL (but see BUGS below).
EPOLL_CTL_DEL:从epoll句柄epfd中删除目标文件描述符fd。fd对应的事件将会被忽略。
2) struct epoll_event *event
The event argument describes the object linked to the  file  descriptor fd. 
与文件描述符fd相对应的事件类型event,其结构体如下:
 The struct epoll_event is defined as :

           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 */
           };

        The  events  member is a bit set composed using the following available event types:
    
常用的事件类型: 
EPOLLIN :表示对应的文件描述符可以读; 
EPOLLOUT:表示对应的文件描述符可以写; 
EPOLLPRI:表示对应的文件描述符有紧急的数据可读 
EPOLLERR:表示对应的文件描述符发生错误; 
EPOLLHUP:表示对应的文件描述符被挂断; 
EPOLLET:表示对应的文件描述符有事件发生;   
3)举例说明其使用方式
struct epoll_event ev; 
//设置与要处理的事件相关的文件描述符 
ev.data.fd=listenfd; 
//设置要处理的事件类型 
ev.events=EPOLLIN|EPOLLET; 
//注册epoll事件 
epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&ev); 
3)epoll_wait函数
  epoll_wait,  epoll_pwait  -  wait  for an I/O event on an epoll file  descriptor
作用:等待在epoll 文件描述符上的一个I/O事件。

SYNOPSIS
       #include <sys/epoll.h>

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

DESCRIPTION
       The epoll_wait() system call waits for events on the epoll instance referred to by the file descriptor epfd.  The memory area pointed to by events will contain the events that will be available for the caller.  Up to maxevents are returned by epoll_wait().  The maxevents argument must be greater than zero.
       epoll_wait系统调用等待epoll句柄中events事件。由events指向的内存区域包含了对于调用者而言可选的事件,即如果注册在epoll句柄上的fd的事件发生,那么就会将发生的fd以及事件类型放入events数组中。当到了最大事件maxevents时,epoll_wait会返回。maxevents必须大于0.
       The call waits for a maximum time of timeout milliseconds.  Specifying a timeout of -1 makes epoll_wait() wait  indefinitely,  while  specifying  a  timeout    equal to zero makes epoll_wait() to return immediately even if no events are available (return code equal to zero).
       该系统调用epoll_wait等待timeout ms。如果timeout=-1,则是无限等待;如果timeout=0,即使没有事件可供选择,epoll_wait也会立即返回。
The struct epoll_event is defined as :

           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 */
           };

       The  data of each returned structure will contain the same data the user set with an epoll_ctl(2) (EPOLL_CTL_ADD,EPOLL_CTL_MOD) while the events member will  contain the returned event bit field.
注意:epoll_wait会将注册在epfd上的已经发生事件的fd的事件类型清空,所以如果下一个循环还要关注这个fd的话,就必须通过epoll_ctl(epfd,EPOLL_CTL_MOD,xx,xxxx)来重新设置fd的事件类型。这时不用EPOLL_CTL_ADD,因为fd没有被清空,只是事件类型被清空。


二、Why:为什么Epoll,它能干啥?


Epoll与select和poll模式相比有很多优势。

1.为什么要采用epoll事件处理机制呢?也就是Epoll的优点。
     一句话,并发性高。因为select和poll事件处理机制都是采用轮询IO的方式。采用轮询IO是最耗时的操作之一。
     可以参考文章 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网 卡驱动架构。

     
2.epoll事件处理机制有两种触发方式:ET和LT。有何区别?
     ET:Edge Trigger;LT:Level Trigger。
     作为一名通信专业的学生,当然不能把所学的通信方面的知识忘记了呀,那咱就用信号方面的东西讲讲水平触发和边缘触发。




三、How:异步IO Epoll怎么玩


先上代码



示例代码

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <openssl/ssl.h>
#include <openssl/err.h>
#include <fcntl.h>
#include <sys/epoll.h>
#include <sys/time.h>
#include <sys/resource.h>
#define MAXBUF 1024
#define MAXEPOLLSIZE 10000
/*
   setnonblocking - 设置句柄为非阻塞方式
   */
int setnonblocking(int sockfd)
{
    if (fcntl(sockfd, F_SETFL, fcntl(sockfd, F_GETFD, 0)|O_NONBLOCK) == -1)
    {
if (fcntl(sockfd, F_SETFL, fcntl(sockfd, F_GETFD, 0)|O_NONBLOCK) == -1)
    {
        return -1;
    }
    return 0;
}
/*
   handle_message - 处理每个 socket 上的消息收发
   */
int handle_message(int new_fd)
{
    char buf[MAXBUF + 1];
    int len;
    /* 开始处理每个新连接上的数据收发 */
    bzero(buf, MAXBUF + 1);
    /* 接收客户端的消息 */
    len = recv(new_fd, buf, MAXBUF, 0);
    if (len > 0)
    {
        printf("%d接收消息成功:'%s',共%d个字节的数据\n",
             new_fd, buf, len);
    }
    else
    {
        if (len < 0)
                        printf("消息接收失败!错误代码是%d,错误信息是'%s'\n",
                 errno, strerror(errno));
        close(new_fd);
        return -1;
    }
    /* 处理每个新连接上的数据收发结束 */
    return len;
}
int main(int argc, char **argv)
{
    int listener, new_fd, kdpfd, nfds, n, ret, curfds;
    socklen_t len;
    struct sockaddr_in my_addr, their_addr;
    unsigned int myport, lisnum;
    struct epoll_event ev;
    struct epoll_event events[MAXEPOLLSIZE];
    struct rlimit rt;
    myport = 5000;
    lisnum = 2;
    /* 设置每个进程允许打开的最大文件数 */
    rt.rlim_max = rt.rlim_cur = MAXEPOLLSIZE;
    printf("The MaxEpollSize is %d", rt.rlim_max);
    if (setrlimit(RLIMIT_NOFILE, &rt) == -1)
    {
        perror("setrlimit");
        exit(1);
    }
    else
    {
        printf("设置系统资源参数成功!\n");
        /*
        if (execl("/bin/echo","echo  'setrlimit success' >>/tmp/testlog",NULL)<0){
                perror("exec");
                exit(1);
        }*/
    }
    /* 开启 socket 监听 */
    if ((listener = socket(PF_INET, SOCK_STREAM, 0)) == -1)
    {
        perror("socket");
        exit(1);
    }
    else
    {
        printf("socket 创建成功!\n");
    }
    setnonblocking(listener);
    bzero(&my_addr, sizeof(my_addr));
    my_addr.sin_family = PF_INET;
    my_addr.sin_port = htons(myport);
    my_addr.sin_addr.s_addr = INADDR_ANY;
    if (bind(listener, (struct sockaddr *) &my_addr, sizeof(struct sockaddr)) == -1)
    {
        perror("bind");
        exit(1);
    }
    else
    {
        printf("IP 地址和端口绑定成功\n");
    }
    if (listen(listener, lisnum) == -1)
    {
        perror("listen");
        exit(1);
    }
    else
    {
        printf("开启服务成功!\n");
    }
    //exit(1);
    /* 创建 epoll 句柄,把监听 socket 加入到 epoll 集合里 */
    kdpfd = epoll_create(MAXEPOLLSIZE);
    len = sizeof(struct sockaddr_in);
    ev.events = EPOLLIN | EPOLLET;
    ev.data.fd = listener;//listen socket
    if (epoll_ctl(kdpfd, EPOLL_CTL_ADD, listener, &ev) < 0)
    {
        fprintf(stderr, "epoll set insertion error: fd=%d\n", listener);
        return -1;
    }
    else
    {
        printf("监听 socket 加入 epoll 成功!\n");
    }
    curfds = 1;
    while (1)
    {
        /* 等待有事件发生 */
        nfds = epoll_wait(kdpfd, events, curfds, -1);//nfds is the num of happened events
        if (nfds == -1)
        {
            perror("epoll_wait");
            break;
        }
        /* 处理所有事件 */
        for (n = 0; n < nfds; ++n)
        {
            if (events[n].data.fd == listener)
            {
                new_fd = accept(listener, (struct sockaddr *) &their_addr,&len);//new_fd: client fd with detailed info
                if (new_fd < 0)
                {
                    perror("accept");
                    continue;
                }
                else
                {
                    printf("有连接来自于: %d:%d, 分配的 socket 为:%d\n",
                            inet_ntoa(their_addr.sin_addr), ntohs(their_addr.sin_port), new_fd);
                }
                setnonblocking(new_fd);
                ev.events = EPOLLIN | EPOLLET;
                ev.data.fd = new_fd;
                if (epoll_ctl(kdpfd, EPOLL_CTL_ADD, new_fd, &ev) < 0)
                {
                    fprintf(stderr, "把 socket '%d' 加入 epoll 失败!%s\n",
                            new_fd, strerror(errno));
                    return -1;
                }
                curfds++;
            }
            else
            {
                ret = handle_message(events[n].data.fd);
                if (ret < 1 && errno != 11)
                {
                    epoll_ctl(kdpfd, EPOLL_CTL_DEL, events[n].data.fd,&ev);
                    curfds--;
                }
            }
        }
    }
    close(listener);
    return 0;
}

示例程序结果:

Server端:

test@spark1:~/xuezh/linux/libfunc$ ./epoll2 

The MaxEpollSize is 10000设置系统资源参数成功!
socket 创建成功!
IP 地址和端口绑定成功
开启服务成功!
监听 socket 加入 epoll 成功!
有连接来自于: -1929214248:52688, 分配的 socket 为:5
有连接来自于: -1929214248:52691, 分配的 socket 为:5
有连接来自于: -1929214248:52693, 分配的 socket 为:5
5接收消息成功:'hello, spark1, I'm from spark2
',共31个字节的数据


简单测一测:Client端:

test@spark2:~$ nc -v -z 172.18.8.239 4999-5001

nc: connect to 172.18.8.239 port 4999 (tcp) failed: Connection refused
Connection to 172.18.8.239 5000 port [tcp/*] succeeded!
nc: connect to 172.18.8.239 port 5001 (tcp) failed: Connection refused
test@spark2:~$ echo "hello, spark1, I'm from spark2" |nc 172.18.8.239 5000


稍微正规点测试:Client端

客户端代码:

#include <stdlib.h>
#include <sys/types.h>
#include <stdio.h>
#include <sys/socket.h>
#include <linux/in.h>
#include <string.h>

int main(int argc, char* argv[] )
{
int cfd; /* 文件描述符 */
int recbytes;
int sendbytes;
int sin_size;
char buffer[1024]={0};    /* 接受缓冲区 */
struct sockaddr_in s_add,c_add; /* 存储服务端和本端的ip、端口等信息结构体 */
//unsigned short portnum=0x8888;  /* 服务端使用的通信端口,可以更改,需和服务端相同 */
unsigned short portnum=atoi( argv[2] );  /* 服务端使用的通信端口,可以更改,需和服务端相同 */
printf("Hello,welcome to client !\r\n");
/* 建立socket 使用因特网,TCP流传输 */
cfd = socket(AF_INET, SOCK_STREAM, 0);
if(-1 == cfd)
{
    printf("socket fail ! \r\n");
    return -1;
}
printf("socket ok !\r\n");
printf("Address is %s\n", argv[1]);
/* 构造服务器端的ip和端口信息,具体结构体可以查资料 */
bzero(&s_add,sizeof(struct sockaddr_in));
s_add.sin_family=AF_INET;
s_add.sin_addr.s_addr= inet_addr(argv[1]); /* ip转换为4字节整形,使用时需要根据服务端ip进行更改 */
s_add.sin_port=htons(portnum); /* 这里htons是将short型数据字节序由主机型转换为网络型,其实就是
                                  将2字节数据的前后两个字节倒换,和对应的ntohs效果、实质相同,只不过名字不同。htonl和ntohl是
				  操作的4字节整形。将0x12345678变为0x78563412,名字不同,内容两两相同,一般情况下网络为大端,
				  PPC的cpu为大端,x86的cpu为小端,arm的可以配置大小端,需要保证接收时字节序正确。
				*/
printf("s_addr = %#x ,port : %#x\r\n",s_add.sin_addr.s_addr,s_add.sin_port); /* 这里打印出的是小端和我们平时看到的是相反的。 */

/* 客户端连接服务器,参数依次为socket文件描述符,地址信息,地址结构大小 */
if(-1 == connect(cfd,(struct sockaddr *)(&s_add), sizeof(struct sockaddr)))
{
    perror("connect fail");
    return -1;
}
printf("connect ok !\r\n");
/*连接成功,从服务端接收字符*/
char *buff = "hi, i'm from client,haaaaaaaaaaaaaaaaaaaaaaaa\n";
if ( sendbytes = send(cfd, buff, 8*sizeof(buff), MSG_DONTWAIT) == -1) {
    perror("send");
    return -1;
}
printf("send ok\r\nREC:\r\n");



buffer[sendbytes]='\0';
printf("%s\r\n",buffer);

close(cfd); /* 关闭连接,本次通信完成 */
return 0;
}

调用客户端用脚本压:

#!/bin/bash
for((i=1;i<100;i++));
do
./epollclient  172.18.8.239 5000
done;




四、参考





epoll函数  

http://blog.csdn.net/penzo/article/details/5986574 (非常不错,例子简单易懂。原帖无法访问了

http://www.cnblogs.com/OnlyXP/archive/2007/08/10/851222.html)

事件处理机制之epoll: http://blog.csdn.net/yankai0219/article/details/8453345

linux epoll模型:http://yjtjh.blog.51cto.com/1060831/294119

Epoll 程序示例: http://blog.csdn.net/vonzhoufz/article/details/20864011

Epoll精髓:http://www.cnblogs.com/OnlyXP/archive/2007/08/10/851222.html


GitHub 加速计划 / li / linux-dash
10.39 K
1.2 K
下载
A beautiful web dashboard for Linux
最近提交(Master分支:2 个月前 )
186a802e added ecosystem file for PM2 4 年前
5def40a3 Add host customization support for the NodeJS version 4 年前
Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐