linux网络编程之epoll模型

epoll的系统调用函数:

epoll_create、epoll_ctl、epoll_wait 三个系统调用。

1. int epoll_create(int size);

函数:创建一个epoll的句柄,即占有一个fd值。

参数:size指定epoll所支持的最大句柄数。

所有的操作符都是通过这个句柄数来进行操作,用完句柄后要用close关闭。

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

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

参数:epfd 是epoll_create()的返回值。

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

EPOLL_CTL_ADD:注册新的fd到epfd中(就像一个池一样)

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

EPOLL_CTL_DEL:从epfd中删除一个fd

参数: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. int epoll_wait(int epfd,struct epoll_event * events,int maxevents,int timeout);

收集在epoll监控的事件中已经发生了的事件。

参数:epfd是epoll_create()的返回值。

参数:events是分配好的struct epoll_event结构体数组

参数:maxevents是告知内核这个events有多大,这个 maxevents的值不能大于创建epoll_create()时的size,

参数:timeout是超时时间(毫秒,0会立即返回,-1将永远等待)

返回值代表这个池中有多少个socket返回数据了。

epoll工作原理


    epoll同样只告知那些就绪的文件描述符,而且当我们调用epoll_wait()获得就绪文件描述符时,返回的不是实际的描述符,而是一个代表就绪描述符数量的值,你只需要去epoll指定的一个数组中依次取得相应数量的文件描述符即可,这里也使用了内存映射(mmap)技术,这样便彻底省掉了这些文件描述符在系统调用时复制的开销。

 

    另一个本质的改进在于epoll采用基于事件的就绪通知方式。在select/poll中,进程只有在调用一定的方法后,内核才对所有监视的文件描述符进行扫描,而epoll事先通过epoll_ctl()来注册一个文件描述符,一旦基于某个文件描述符就绪时,内核会采用类似callback的回调机制,迅速激活这个文件描述符,当进程调用epoll_wait()时便得到通知。

 

Epoll2种工作方式-水平触发(LT)和边缘触发(ET

假如有这样一个例子:

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

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

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

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

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

 

EdgeTriggered工作模式:

    如果我们在第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()返回的读到的数据长度小于请求的数据长度时,就可以确定此时缓冲中已没有数据了,也就可以认为此事读事件已处理完成。

 

LevelTriggered工作模式

相反的,以LT方式调用epoll接口的时候,它就相当于一个速度比较快的poll(2),并且无论后面的数据是否被使用,因此他们具有同样的职能。因为即使使用ET模式的epoll,在收到多个chunk的数据的时候仍然会产生多个事件。调用者可以设定EPOLLONESHOT标志,在 epoll_wait(2)收到事件后epoll会与事件关联的文件句柄从epoll描述符中禁止掉。因此当EPOLLONESHOT设定后,使用带有EPOLL_CTL_MOD标志的epoll_ctl(2)处理文件句柄就成为调用者必须作的事情。

 

LT(level triggered)epoll缺省的工作方式,并且同时支持blockno-block socket.在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的,所以这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的代表.

 

ET (edge-triggered)是高速工作方式,只支持no-block socket,它效率要比LT更高。ETLT的区别在于,当一个新的事件到来时,ET模式下当然可以从epoll_wait调用中获取到这个事件,可是如果这次没有把这个事件对应的套接字缓冲区处理完,在这个套接字中没有新的事件再次到来时,在ET模式下是无法再次从epoll_wait调用中获取这个事件的。而LT模式正好相反,只要一个事件对应的套接字缓冲区还有数据,就总能从epoll_wait中获取这个事件。

 

因此,LT模式下开发基于epoll的应用要简单些,不太容易出错。而在ET模式下事件发生时,如果没有彻底地将缓冲区数据处理完,则会导致缓冲区中的用户请求得不到响应。

图示说明:


注:Nginx默认采用ET模式来使用epoll

 

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)的大小--- 通过echoXXXX>/proc/sys/net/core/hot_list_length完成。再比如listen函数的第2个参数(TCP完成3次握手的数据包队列长度),也可以根据你平台内存大小动态调整。更甚至在一个数据包面数目巨大但同时每个数据包本身大小却很小的特殊系统上尝试最新的NAPI网卡驱动架构

 

linuxepoll如何实现高效处理百万句柄的

开发高性能网络程序时,windows开发者们言必称iocplinux开发者们则言必称epoll。大家都明白epoll是一种IO多路复用技术,可以非常高效的处理数以百万计的socket句柄,比起以前的selectpoll效率高大发了。我们用起epoll来都感觉挺爽,确实快,那么,它到底为什么可以高速处理这么多并发连接呢?

 

使用起来很清晰,首先要调用epoll_create建立一个epoll对象。参数size是内核保证能够正确处理的最大句柄数,多于这个最大数时内核可不保证效果。

 

epoll_ctl可以操作上面建立的epoll,例如,将刚建立的socket加入到epoll中让其监控,或者把 epoll正在监控的某个socket句柄移出epoll,不再监控它等等。

 

epoll_wait在调用时,在给定的timeout时间内,当在监控的所有句柄中有事件发生时,就返回用户态的进程。

 

从上面的调用方式就可以看到epollselect/poll的优越之处:因为后者每次调用时都要传递你所要监控的所有socketselect/poll系统调用,这意味着需要将用户态的socket列表copy到内核态,如果以万计的句柄会导致每次都要copy几十几百KB的内存到内核态,非常低效。而我们调用epoll_wait时就相当于以往调用select/poll,但是这时却不用传递socket句柄给内核,因为内核已经在epoll_ctl中拿到了要监控的句柄列表。

 

所以,实际上在你调用epoll_create后,内核就已经在内核态开始准备帮你存储要监控的句柄了,每次调用epoll_ctl只是在往内核的数据结构里塞入新的socket句柄。



epoll的使用方法

那么究竟如何来使用epoll呢?其实非常简单。

 

通过在包含一个头文件#include <sys/epoll.h> 以及几个简单的API将可以大大的提高你的网络服务器的支持人数。

 

首先通过create_epoll(intmaxfds)来创建一个epoll的句柄。这个函数会返回一个新的epoll句柄,之后的所有操作将通过这个句柄来进行操作。在用完之后,记得用close()来关闭这个创建出来的epoll句柄。

 

之后在你的网络主循环里面,每一帧的调用epoll_wait(int epfd, epoll_event events, int max events, inttimeout)来查询所有的网络接口,看哪一个可以读,哪一个可以写了。基本的语法为:

nfds = epoll_wait(kdpfd, events, maxevents, -1);

 

其中kdpfd为用epoll_create创建之后的句柄,events是一个epoll_event*的指针,当epoll_wait这个函数操作成功之后,epoll_events里面将储存所有的读写事件。max_events是当前需要监听的所有socket句柄数。最后一个timeout epoll_wait的超时,为0的时候表示马上返回,为-1的时候表示一直等下去,直到有事件返回,为任意正整数的时候表示等这么长的时间,如果一直没有事件,则返回。一般如果网络主循环是单独的线程的话,可以用-1来等,这样可以保证一些效率,如果是和主逻辑在同一个线程的话,则可以用0来保证主循环的效率。

 

epoll_wait返回之后应该是一个循环,遍历所有的事件。


for( ; ; )
    {
        nfds = epoll_wait(epfd,events,20,500);
        for(i=0;i<nfds;++i)
        {
            if(events[i].data.fd==listenfd)//有新的连接
            {
                connfd = accept(listenfd,(sockaddr*)&clientaddr,&clilen);//accept这个连接
                ev.data.fd=connfd;
                ev.events=EPOLLIN|EPOLLET;
                epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev);//将新的fd添加到epoll的监听队列中
            }
            else if( events[i].events&EPOLLIN )//接收到数据,读socket
            {
                n = read(sockfd, line, MAXLINE))<0    //读
                ev.data.ptr = md;     //md为自定义类型,添加数据
                ev.events=EPOLLOUT|EPOLLET;
                epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);//修改标识符,等待下一个循环时发送数据,异步处理的精髓
            }
            else if(events[i].events&EPOLLOUT)//有数据待发送,写socket
            {
                struct myepoll_data* md= (myepoll_data*)events[i].data.ptr;    //取数据
                sockfd = md->fd;
                send( sockfd, md->ptr, strlen((char*)md->ptr),0 );        //发送数据
                ev.data.fd=sockfd;
                ev.events=EPOLLIN|EPOLLET;
                epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);//修改标识符,等待下一个循环时接收数据
            }
            else
            {
                //其他的处理
            }
        }
    }

epoll代码实例:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/epoll.h>
#include <netinet/in.h>
#include <arpa/inet.h>

ssize_t socket_recv(int st)
{
    char buf[1024];
    memset(buf, 0, sizeof(buf));
    ssize_t rc = recv(st, buf, sizeof(buf), 0);
    if (rc <= 0)
    {
        printf("recv failed %s\n", strerror(errno));
    } else
    {
        printf("recv %s\n", buf);
        send(st, buf, rc, 0);
    }
    return rc;
}

int socket_accept(int listen_st)
{
    struct sockaddr_in client_addr;
    socklen_t len = sizeof(client_addr);
    memset(&client_addr, 0, sizeof(client_addr));
    int client_st = accept(listen_st, (struct sockaddr *) &client_addr, &len);
    if (client_st < 0)
        printf("accept failed %s\n", strerror(errno));
    else
        printf("accept by %s\n", inet_ntoa(client_addr.sin_addr));
    return client_st;
}

void setnonblocking(int st) //将socket设置为非阻塞
{
    int opts = fcntl(st, F_GETFL);
    if (opts < 0)
    {
        printf("fcntl failed %s\n", strerror(errno));
    }
    opts = opts | O_NONBLOCK;
    if (fcntl(st, F_SETFL, opts) < 0)
    {
        printf("fcntl failed %s\n", strerror(errno));
    }
}

int socket_create(int port)
{
    int st = socket(AF_INET, SOCK_STREAM, 0);
    int on = 1;
    if (setsockopt(st, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) == -1)
    {
        printf("setsockopt failed %s\n", strerror(errno));
        return 0;
    }
    struct sockaddr_in addr;
    memset(&addr, 0, sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_port = htons(port);
    addr.sin_addr.s_addr = htonl(INADDR_ANY);
    if (bind(st, (struct sockaddr *) &addr, sizeof(addr)) == -1)
    {
        printf("bind port %d failed %s\n", port, strerror(errno));
        return 0;
    }
    if (listen(st, 300) == -1)
    {
        printf("listen failed %s\n", strerror(errno));
        return 0;
    }
    return st;
}

int main(int arg, char *args[])
{
    if (arg < 2)
        return -1;
    int iport = atoi(args[1]);
    int listen_st = socket_create(iport);
    if (listen_st == 0)
        return -1;

    struct epoll_event ev, events[100]; //声明epoll_event结构体的变量,ev用于注册事件,数组用于回传要处理的事件
    int epfd = epoll_create(100); //生成用于处理accept的epoll专用的文件描述符
    setnonblocking(listen_st); //把socket设置为非阻塞方式
    ev.data.fd = listen_st; //设置与要处理的事件相关的文件描述符
    ev.events = EPOLLIN | EPOLLERR | EPOLLHUP; //设置要处理的事件类型
    epoll_ctl(epfd, EPOLL_CTL_ADD, listen_st, &ev); //注册epoll事件

    int st = 0;
    while (1)
    {
        int nfds = epoll_wait(epfd, events, 100, -1); //等待epoll事件的发生
        if (nfds == -1)
        {
            printf("epoll_wait failed %s\n", strerror(errno));
            break;
        }

        int i;
        for (i = 0; i < nfds; i++)
        {
            if (events[i].data.fd < 0)
                continue;

            if (events[i].data.fd == listen_st) //监测到一个SOCKET用户连接到了绑定的SOCKET端口,建立新的连接。
            {
                st = socket_accept(listen_st);
                if (st >= 0)
                {
                    setnonblocking(st);
                    ev.data.fd = st;
                    ev.events = EPOLLIN | EPOLLERR | EPOLLHUP; //设置要处理的事件类型
                    epoll_ctl(epfd, EPOLL_CTL_ADD, st, &ev);
                    continue;
                }
            }
            //client端的socket有数据到达
            if (events[i].events & EPOLLIN) //socket收到数据
            {
                st = events[i].data.fd;
                if (socket_recv(st) <= 0)
                {
                    close(st);
                    events[i].data.fd = -1;
                }
            }
            if (events[i].events & EPOLLERR) //socket错误
            {
            	 st = events[i].data.fd;
                close(st);
                events[i].data.fd = -1;
            }

            if (events[i].events & EPOLLHUP) //socket错误
            {
            	 st = events[i].data.fd;
                close(st);
                events[i].data.fd = -1;
            }
        }
    }
    close(epfd);
    return 0;
}

















在这里最重要的莫过于select模型和Asynchronous I/O模型。从理论上说,AIO似乎是最高效的,你的IO操作可以立即返回,然后等待os告诉你IO操作完成。但是一直以来,如何实现就没有一个完美的方案。最著名的windows完成端口实现的AIO,实际上也只是内部用线程池实现的罢了,最后的结果是IO有个线程池,你的应用程序也需要一个线程池...... 很多文档其实已经指出了这引发的线程context-switch所带来的代价。在linux 平台上,关于网络AIO一直是改动最多的地方,2.4的年代就有很多AIO内核patch,最著名的应该算是SGI。但是一直到2.6内核发布,网络模块的AIO一直没有进入稳定内核版本(大部分都是使用用户线程模拟方法,在使用了NPTL的linux上面其实和windows的完成端口基本上差不多了)。2.6内核所支持的AIO特指磁盘的AIO---支持io_submit(),io_getevents()以及对Direct IO的支持(即:就是绕过VFS系统buffer直接写硬盘,对于流服务器在内存平稳性上有相当的帮助)。 所以,剩下的select模型基本上就成为我们在linux上面的唯一选择,其实,如果加上no-block socket的配置,可以完成一个"伪"AIO的实现,只不过推动力在于你而不是os而已。不过传统的select/poll函数有着一些无法忍受的缺点,所以改进一直是2.4-2.5开发版本内核的任务,包括/dev/poll,realtime signal等等。 最终,Davide Libenzi开发的epoll进入2.6内核成为正式的解决方案。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值