浅析epoll

浅析epoll-为何多路复用I/O要使用epoll

现如今,网络通讯中用epoll(linux)和IOCP(windows)几乎是大家津津乐道的东西,不为别的,就因为高效,所以大家喜欢用。IOCP的基础东西已经讲过了,可翻阅《IOCP浅析》 《IOCP浅析[二]——IOCP出现的意义和函数接口》.

什么是epoll

epoll是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率,因为它会复用文件描述符集 合来传递结果而不用迫使开发者每次等待事件之前都必须重新准备要被侦听的文件描述符集合,另一点原因就是获取事件的时候,它无须遍历整个被侦听的描述符 集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了。epoll除了提供select/poll那种IO事件的水平触发 (Level Triggered)外,还提供了边沿触发(Edge Triggered),这就使得用户空间程序有可能缓存IO状态,减少epoll_wait/epoll_pwait的调用,提高应用程序效率。Linux2.6内核中对/dev/epoll设备的访问的封装(system epoll)。

这个使我们开发网络应用程序更加简单,并且更加高效。

为什么要使用epoll?

同样,我们在linux系统下,影响效率的依然是I/O操作,linux提供给我们select/poll/epoll等多路复用I/O方式(kqueue暂时没研究过),为什么我们对epoll情有独钟呢?原因如下:

1.文件描述符数量的对比。

epoll并没有fd(文件描述符)的上限,它只跟系统内存有关,我的2G的ubuntu下查看是20480个,轻松支持20W个fd。可使用如下命令查看:

cat/proc/sys/fs/file-max

再来看select/poll,有一个限定的fd的数量,linux/posix_types.h头文件中

#define __FD_SETSIZE    1024

2.效率对比。

当然了,你可以修改上述值,然后重新编译内核,然后再次写代码,这也是没问题的,不过我先说说select/poll的机制,估计你马上会作废上面修改枚举值的想法。

select/poll会因为监听fd的数量而导致效率低下,因为它是轮询所有fd,有数据就处理,没数据就跳过,所以fd的数量会降低效率;而epoll只处理就绪的fd,它有一个就绪设备的队列,每次只轮询该队列的数据,然后进行处理。(先简单讲一下,第二篇还会详细讲解)

3.内存处理方式对比。

不管是哪种I/O机制,都无法避免fd在操作过程中拷贝的问题,而epoll使用了mmap(是指文件/对象的内存映射,被映射到多个内存页上),所以同一块内存就可以避免这个问题。

btw:TCP/IP协议栈使用内存池管理sk_buff结构,你还可以通过修改内存池pool的大小,毕竟linux支持各种微调内核。

epoll的工作方式

epoll分为两种工作方式LT和ET。

LT(level triggered) 是默认/缺省的工作方式,同时支持 block和no_block socket。这种工作方式下,内核会通知你一个fd是否就绪,然后才可以对这个就绪的fd进行I/O操作。就算你没有任何操作,系统还是会继续提示fd已经就绪,不过这种工作方式出错会比较小,传统的select/poll就是这种工作方式的代表。

ET(edge-triggered) 是高速工作方式,仅支持no_block socket,这种工作方式下,当fd从未就绪变为就绪时,内核会通知fd已经就绪,并且内核认为你知道该fd已经就绪,不会再次通知了,除非因为某些操作导致fd就绪状态发生变化。如果一直不对这个fd进行I/O操作,导致fd变为未就绪时,内核同样不会发送更多的通知,因为only once。所以这种方式下,出错率比较高,需要增加一些检测程序。

LT可以理解为水平触发,只要有数据可以读,不管怎样都会通知。而ET为边缘触发,只有状态发生变化时才会通知,可以理解为电平变化。

如何使用epoll?

使用epoll很简单,只需要

#include <sys/epoll.h>

有三个关键函数:

int epoll_create(int size);

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

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

当然了,不要忘记关闭函数.

 

浅析epoll – epoll函数深入讲解

前一篇大致讲了一下epoll是个什么东西,优点等内容,这篇延续上一篇的内容,主要是分析epoll的函数,epoll高性能的深入分析。
epoll的三大函数
1.创建epoll fd函数
int epoll_create(int size);
epoll_create()创建一个epoll的事例,通知内核需要监听size个fd。size指的并不是最大的后备存储设备,而是衡量内核内部结构大小的一个提示。当创建成功后,会占用一个fd,所以记得在使用完之后调用close(),否则fd可能会被耗尽。
Note:自从Linux2.6.8版本以后,size值其实是没什么用的,不过要大于0,因为内核可以动态的分配大小,所以不需要size这个提示了。
创建还有另外一个函数
int epoll_create1(int flag);
这个函数是在linux 2.6.27中加入的,当你在看陈硕的muduo时可以看到这个函数,其实它和epoll_create差不多,不同的是epoll_create1函数的参数是flag,当flag是0时,表示和epoll_create函数完全一样,不需要size的提示了。
当flag = EPOLL_CLOEXEC,创建的epfd会设置FD_CLOEXEC
当flag = EPOLL_NONBLOCK,创建的epfd会设置为非阻塞
一般用法都是使用EPOLL_CLOEXEC.
Note:关于FD_CLOEXEC,现在网上好多都说的有点问题,我翻阅了一些资料,请教了一些人,大约明白它的意思了。
它是fd的一个标识说明,用来设置文件close-on-exec状态的。当close-on-exec状态为0时,调用exec时,fd不会被关闭;状态非零时则会被关闭,这样做可以防止fd泄露给执行exec后的进程。关于exec的用法,大家可以去自己查阅下,或者直接man exec。

2.epoll事件的注册函数
int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event);
select是在监听时告诉内核要监听的事件,而epoll_ctl是先注册需要监听的事件。
第一个参数epfd,为epoll_create返回的的epoll fd。
第二个参数op表示操作值。有三个操作类型,
EPOLL_CTL_ADD  // 注册目标fd到epfd中,同时关联内部event到fd上
EPOLL_CTL_MOD // 修改已经注册到fd的监听事件
EPOLL_CTL_DEL // 从epfd中删除/移除已注册的fd,event可以被忽略,也可以为NULL
第三个参数fd表示需要监听的fd。
第四个参数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 */
};
event参数是一个枚举的集合,可以用” | “来增加事件类型,枚举如下:
EPOLLIN:表示关联的fd可以进行读操作了。
EPOLLOUT:表示关联的fd可以进行写操作了。
EPOLLRDHUP(since Linux 2.6.17):表示套接字关闭了连接,或者关闭了正写一半的连接。
EPOLLPRI:表示关联的fd有紧急优先事件可以进行读操作了。
EPOLLERR:表示关联的fd发生了错误,epoll_wait会一直等待这个事件,所以一般没必要设置这个属性。
EPOLLHUP:表示关联的fd挂起了,epoll_wait会一直等待这个事件,所以一般没必要设置这个属性。
EPOLLET:设置关联的fd为ET的工作方式,epoll的默认工作方式是LT。
EPOLLONESHOT (since Linux 2.6.2):设置关联的fd为one-shot的工作方式。表示只监听一次事件,如果要再次监听,需要把socket放入到epoll队列中。


3.epoll等待事件函数

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
int epoll_pwait(int epfd, struct epoll_event *events, int maxevents, int timeout,  const sigset_t *sigmask);

上面两个函数的参数含义:
第一个参数:表示epoll_wait等待epfd上的事件
第二个参数:events指针携带有epoll_data_t数据
第三个参数:maxevents告诉内核events有多大,该值必须大于0
第四个参数:timeout表示超时时间(单位:毫秒)
epoll_pwait(since linux 2.6.19)允许一个应用程序安全的等待,直到fd设备准备就绪,或者捕获到一个信号量。其中sigmask表示要捕获的信号量。
函数如果等待成功,则返回fd的数字;0表示等待fd超时,其他错误号请查看errno
函数到这里就讲完了,下一篇会写一个例子给大家看下这些函数是如何使用的。

浅析epoll – epoll例子以及分析

上篇我们讲到epoll的函数和性能。这一篇用用这些个函数,给出一个最简单的epoll例子

//   
// a simple echo server using epoll in linux  
//   
// 2009-11-05  
// by sparkling  
//   
#include <sys/socket.h>  
#include <sys/epoll.h>  
#include <netinet/in.h>  
#include <arpa/inet.h>  
#include <fcntl.h>  
#include <unistd.h>  
#include <stdio.h>  
#include <errno.h>  
#include <iostream> 

using namespace std;  
#define MAX_EVENTS 500  

struct myevent_s  
{  
    int fd;  
    void (*call_back)(int fd, int events, void *arg);  
    int events;  
    void *arg;  
    int status; // 1: in epoll wait list, 0 not in  
    char buff[128]; // recv data buffer  
    int len;  
    long last_active; // last active time  
}; 

// set event  
void EventSet(myevent_s *ev, int fd, void (*call_back)(int, int, void*), void *arg)  
{  
    ev->fd = fd;  
    ev->call_back = call_back;  
    ev->events = 0;  
    ev->arg = arg;  
    ev->status = 0;  
    ev->last_active = time(NULL);  
}  

// add/mod an event to epoll  
void EventAdd(int epollFd, int events, myevent_s *ev)  
{  
    struct epoll_event epv = {0, {0}};  
    int op;  
    epv.data.ptr = ev;  
    epv.events = ev->events = events;  
    if(ev->status == 1){  
        op = EPOLL_CTL_MOD;  
    }  
    else{  
        op = EPOLL_CTL_ADD;  
        ev->status = 1;  
    }  
    if(epoll_ctl(epollFd, op, ev->fd, &epv) < 0)  
        printf("Event Add failed[fd=%d]\n", ev->fd);  
    else  
        printf("Event Add OK[fd=%d]\n", ev->fd);  
}  

// delete an event from epoll  
void EventDel(int epollFd, myevent_s *ev)  
{  
    struct epoll_event epv = {0, {0}};  
    if(ev->status != 1) return;  
    epv.data.ptr = ev;  
    ev->status = 0;  
    epoll_ctl(epollFd, EPOLL_CTL_DEL, ev->fd, &epv);  
}  

int g_epollFd;  
myevent_s g_Events[MAX_EVENTS+1]; // g_Events[MAX_EVENTS] is used by listen fd  
void RecvData(int fd, int events, void *arg);  
void SendData(int fd, int events, void *arg);  

// accept new connections from clients  
void AcceptConn(int fd, int events, void *arg)  
{  
    struct sockaddr_in sin;  
    socklen_t len = sizeof(struct sockaddr_in);  
    int nfd, i;  
    // accept  
    if((nfd = accept(fd, (struct sockaddr*)&sin, &len)) == -1)  
    {  
        if(errno != EAGAIN && errno != EINTR)  
        {  
            printf("%s: bad accept", __func__);  
        }  
        return;  
    }  
    do  
    {  
        for(i = 0; i < MAX_EVENTS; i++)  
        {  
            if(g_Events[i].status == 0)  
            {  
                break;  
            }  
        }  
        if(i == MAX_EVENTS)  
        {  
            printf("%s:max connection limit[%d].", __func__, MAX_EVENTS);  
            break;  
        }  
        // set nonblocking  
        if(fcntl(nfd, F_SETFL, O_NONBLOCK) < 0) break;  
        // add a read event for receive data  
        EventSet(&g_Events[i], nfd, RecvData, &g_Events[i]);  
        EventAdd(g_epollFd, EPOLLIN|EPOLLET, &g_Events[i]);  
        printf("new conn[%s:%d][time:%d]\n", inet_ntoa(sin.sin_addr), ntohs(sin.sin_port), g_Events[i].last_active);  
    }while(0);  
}  

// receive data  
void RecvData(int fd, int events, void *arg)  
{  
    struct myevent_s *ev = (struct myevent_s*)arg;  
    int len;  
    // receive data  
    len = recv(fd, ev->buff, sizeof(ev->buff)-1, 0);    
    EventDel(g_epollFd, ev);  
    if(len > 0)  
    {  
        ev->len = len;  
        ev->buff[len] = '\0';  
        printf("C[%d]:%s\n", fd, ev->buff);  
        // change to send event  
        EventSet(ev, fd, SendData, ev);  
        EventAdd(g_epollFd, EPOLLOUT|EPOLLET, ev);  
    }  
    else if(len == 0)  
    {  
        close(ev->fd);  
        printf("[fd=%d] closed gracefully.\n", fd);  
    }  
    else  
    {  
        close(ev->fd);  
        printf("recv[fd=%d] error[%d]:%s\n", fd, errno, strerror(errno));  
    }  
}  

// send data  
void SendData(int fd, int events, void *arg)  
{  
    struct myevent_s *ev = (struct myevent_s*)arg;  
    int len;  
    // send data  
    len = send(fd, ev->buff, ev->len, 0);  
    ev->len = 0;  
    EventDel(g_epollFd, ev);  
    if(len > 0)  
    {  
        // change to receive event  
        EventSet(ev, fd, RecvData, ev);  
        EventAdd(g_epollFd, EPOLLIN|EPOLLET, ev);  
    }  
    else  
    {  
        close(ev->fd);  
        printf("recv[fd=%d] error[%d]\n", fd, errno);  
    }  
}  

void InitListenSocket(int epollFd, short port)  
{  
    int listenFd = socket(AF_INET, SOCK_STREAM, 0);  
    fcntl(listenFd, F_SETFL, O_NONBLOCK); // set non-blocking  
    printf("server listen fd=%d\n", listenFd);  
    EventSet(&g_Events[MAX_EVENTS], listenFd, AcceptConn, &g_Events[MAX_EVENTS]);  
    // add listen socket  
    EventAdd(epollFd, EPOLLIN|EPOLLET, &g_Events[MAX_EVENTS]);  
    // bind & listen  
    sockaddr_in sin;  
    bzero(&sin, sizeof(sin));  
    sin.sin_family = AF_INET;  
    sin.sin_addr.s_addr = INADDR_ANY;  
    sin.sin_port = htons(port);  
    bind(listenFd, (const sockaddr*)&sin, sizeof(sin));  
    listen(listenFd, 5);  
}  

int main(int argc, char **argv)  
{  
    short port = 12345; // default port  
    if(argc == 2){  
        port = atoi(argv[1]);  
    }  
    // create epoll  
    g_epollFd = epoll_create(MAX_EVENTS);  
    if(g_epollFd <= 0) printf("create epoll failed.%d\n", g_epollFd);  
    // create & bind listen socket, and add to epoll, set non-blocking  
    InitListenSocket(g_epollFd, port);  
    // event loop  
    struct epoll_event events[MAX_EVENTS];  
    printf("server running:port[%d]\n", port);  
    int checkPos = 0;  
    while(1){  
        // a simple timeout check here, every time 100, better to use a mini-heap, and add timer event  
        long now = time(NULL);  
        for(int i = 0; i < 100; i++, checkPos++) // doesn't check listen fd  
        {  
            if(checkPos == MAX_EVENTS) checkPos = 0; // recycle  
            if(g_Events[checkPos].status != 1) continue;  
            long duration = now - g_Events[checkPos].last_active;  
            if(duration >= 60) // 60s timeout  
            {  
                close(g_Events[checkPos].fd);  
                printf("[fd=%d] timeout[%d--%d].\n", g_Events[checkPos].fd, g_Events[checkPos].last_active, now);  
                EventDel(g_epollFd, &g_Events[checkPos]);  
            }  
        }  
        // wait for events to happen  
        int fds = epoll_wait(g_epollFd, events, MAX_EVENTS, 1000);  
        if(fds < 0){  
            printf("epoll_wait error, exit\n");  
            break;  
        }  
        for(int i = 0; i < fds; i++){  
            myevent_s *ev = (struct myevent_s*)events[i].data.ptr;  
            if((events[i].events&EPOLLIN)&&(ev->events&EPOLLIN)) // read event  
            {  
                ev->call_back(ev->fd, events[i].events, ev->arg);  
            }  
            if((events[i].events&EPOLLOUT)&&(ev->events&EPOLLOUT)) // write event  
            {  
                ev->call_back(ev->fd, events[i].events, ev->arg);  
            }  
        }  
    }  
    // free resource  
    return 0;  
}


上面是一个echo server的例子,下来我们看下echo client的例子

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <netdb.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
 
#define MAXDATASIZE 100 // max number of bytes we can get at once
 
int main(int argc, char *argv[])
{
    int sockfd, numbytes;
    char buf[MAXDATASIZE];
    struct hostent *he;
    struct sockaddr_in their_addr; // connector'saddress information
 
    if (( argc == 1) || (argc==2) ) 
    {
        fprintf(stderr,"usage: client hostname\nEx:\n$./client01 ip port\n");
        exit(1);
    }
 
    if ((he=gethostbyname(argv[1])) == NULL) 
    { // get the host info
        herror("gethostbyname");
        exit(1);
    }
 
    if ((sockfd = socket(PF_INET, SOCK_STREAM, 0)) == -1) 
    {
        perror("socket");
        exit(1);
    }
 
    their_addr.sin_family = AF_INET; // host byte order
    their_addr.sin_port = htons(atoi(argv[2])); // short, network byte order
    their_addr.sin_addr = *((struct in_addr *)he->h_addr);
    memset(their_addr.sin_zero, ' ', sizeof their_addr.sin_zero);
 
    if (connect(sockfd, (struct sockaddr *)&their_addr, sizeof their_addr) == -1) 
    {
        perror("connect");
        exit(1);
    }
 
    while( 1 )
    {
    	char* strTest = "hello, this is client message!";
        if(send(sockfd, strTest, strlen(strTest), 0 ) == -1)
        {
            perror("send");
        }
 
        if ((numbytes=recv(sockfd, buf, MAXDATASIZE-1, 0)) == -1)
        {
            perror("recv");
        }
        else if( numbytes == 0 )
        {
            printf("Remote server has shutdown!\n");
            break;
        }
 
        buf[numbytes] = ' ';
 
        printf("Received: %s \n",buf);
        sleep(1);
    }
 
    close(sockfd);
    return 0;
}

以上客户端和服务器例子均来自网络,自己没有单独写,因为觉得自己写了跟着个差不多还浪费时间,大家有兴趣可以去研究下libevent和libev,这都是很好用的C风格的epoll网络库,或者之际看ASIO吧。



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值