I/O复用三 : epoll

epoll_create
#include <sys/epoll.h> 

int epoll_create(int size);

返回值:
  success:返回一个非0 的未使用过的最小的文件描述符
  error:-1 errno被设置

参数 size 从 Linux 2.6.8 以后就不再使用,但是必须设置一个大于 0 的值。

需要注意的是,当创建好epoll句柄后,它就是会占用一个fd值,在linux下查看/proc/进程id/fd/,是能够看到这个fd的( eg: ls /proc/$(ps -aux | grep './main' | awk 'NR==1 { print $2 }')/fd ),所以在使用完 epoll 后,必须调用close()关闭,否则可能导致 fd 被耗尽。

  • epoll_create1
    int epoll_create1(int flags);
    
    - flags:
     - 如果这个参数是0,这个函数等价于epoll_create(0)
     - EPOLL_CLOEXEC:这是这个参数唯一的有效值,如果这个参数设置为这个。
       那么当进程替换映像的时候会关闭这个文件描述符,这样新的映像中就无法对这个文件描述符操作,
       适用于多进程编程+映像替换的环境里
    
epoll_ctl
#include <sys/epoll.h> 

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

epoll的事件注册函数
第一个参数是 epoll_create() 的创建的 epoll 实例。
第二个参数表示动作,用3个宏表示:

EPOLL_CTL_ADD:注册新的fd到epfd中
EPOLL_CTL_MOD:修改已经注册的fd的监听事件
EPOLL_CTL_DEL:从epfd中删除一个fd

第三个参数是需要监听的fd
第四个参数是内核需要监听什么事件,struct epoll_event 结构如下:

#include <sys/epoll.h> 

struct epoll_event {
      __uint32_t events;
      epoll_data_t data;
}

typedef union epoll_data {
    void        *ptr;
    int          fd;
    __uint32_t   u32;
    __uint64_t   u64;
} epoll_data_t;
  • EPOLLIN
    表示对应的文件描述符可以读(包括对端SOCKET正常关闭)
  • EPOLLOUT
    表示对应的文件描述符可以写
  • EPOLLPRI
    表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来)
  • EPOLLERR
    表示对应的文件描述符发生错误
  • EPOLLHUP
    表示对应的文件描述符被挂断
  • EPOLLET
    将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的
epoll_wait
#include <sys/epoll.h> 

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

等待事件的产生。

参数 events 用来从内核得到事件的集合。
参数 maxevents 告之内核这个events有多大(数组成员的个数)。
参数 timeout 是超时时间,单位毫秒(0会立即返回,-1将是永久阻塞)。

该函数返回需要处理的事件数目,返回的事件集合在 events 数组中,如返回 0 表示已超时。

  • maxevents 参数设置多少合适?
    muduo 中的思路是动态扩张:开始是 n 个,当发现有事件的 fd 数量已经到达 n 个后,将 struct epoll_event 数量调整成 2n 个,下次如果还不够,则变成 4n 个,以此类推。
    // 初始化代码  
    std::vector<struct epoll_event> events_(16);  
    
    // 线程循环里面的代码  
    while (m_bExit)  
    {  
        int numEvents = ::epoll_wait(epollfd_, &*events_.begin(), static_cast<int>(events_.size()), 1);  
        if (numEvents > 0)  
        {  
            if (static_cast<size_t>(numEvents) == events_.size())  
            {  
                events_.resize(events_.size() * 2);  
            }  
        }  
    }
    
  • timeout 超时时间设置多少合适?
    muduo 中epoll_wait 的超时事件设置为 1 毫秒。
代码示例

服务器

#include "lib/common.h"

#define MAXEVENTS 128

char rot13_char(char c) 
{
    if ((c >= 'a' && c <= 'm') || (c >= 'A' && c <= 'M'))
        return c + 13;
    else if ((c >= 'n' && c <= 'z') || (c >= 'N' && c <= 'Z'))
        return c - 13;
    else
        return c;
}

int main(int argc, char **argv) 
{
    int listen_fd, socket_fd;
    int n, i;
    int efd;
    struct epoll_event event;
    struct epoll_event *events;

    listen_fd = tcp_nonblocking_server_listen(SERV_PORT);

    efd = epoll_create1(0);
    if (efd == -1) {
        error(1, errno, "epoll create failed");
    }

    event.data.fd = listen_fd;
    event.events = EPOLLIN | EPOLLET;
    if (epoll_ctl(efd, EPOLL_CTL_ADD, listen_fd, &event) == -1) {
        error(1, errno, "epoll_ctl add listen fd failed");
    }

    /* Buffer where events are returned */
    events = calloc(MAXEVENTS, sizeof(event));

    while (1) {
        n = epoll_wait(efd, events, MAXEVENTS, -1);
        printf("epoll_wait wakeup\n");
        for (i = 0; i < n; i++) {
            if ((events[i].events & EPOLLERR) ||
                (events[i].events & EPOLLHUP) ||
                (!(events[i].events & EPOLLIN))) {
                fprintf(stderr, "epoll error\n");
                close(events[i].data.fd);
                continue;
            } else if (listen_fd == events[i].data.fd) {
                struct sockaddr_storage ss;
                socklen_t slen = sizeof(ss);
                int fd = accept(listen_fd, (struct sockaddr *) &ss, &slen);
                if (fd < 0) {
                    error(1, errno, "accept failed");
                } else {
                    make_nonblocking(fd);
                    event.data.fd = fd;
                    event.events = EPOLLIN | EPOLLET; //edge-triggered
                    if (epoll_ctl(efd, EPOLL_CTL_ADD, fd, &event) == -1) {
                        error(1, errno, "epoll_ctl add connection fd failed");
                    }
                }
                continue;
            } else {
                socket_fd = events[i].data.fd;
                printf("get event on socket fd == %d \n", socket_fd);
                while (1) {
                    char buf[512];
                    if ((n = read(socket_fd, buf, sizeof(buf))) < 0) {
                        if (errno != EAGAIN) {
                            error(1, errno, "read error");
                            close(socket_fd);
                        }
                        break;
                    } else if (n == 0) {
                        close(socket_fd);
                        break;
                    } else {
                        for (i = 0; i < n; ++i) {
                            buf[i] = rot13_char(buf[i]);
                        }
                        if (write(socket_fd, buf, n) < 0) {
                            error(1, errno, "write error");
                        }
                    }
                }
            }
        }
    }

    free(events);
    close(listen_fd);
}
LT和ET 工作模式

epoll对文件描述符的操作有2种模式:LT和ET。

  • LT模式(水平触发):
    LT(水平触发)是默认的工作模式,并且同时支持阻塞和非阻塞socket。
    对于采用LT工作模式的文件描述符,当epoll_wait检测到其上有时间发生并将此事件通知应用程序后,应用程序可以不立即处理该事件,这样,当应用程序下一次调用epoll_wait时,epoll_wait还会再次相应应用程序通告此事件,直到该事件被处理。

  • ET模式(边沿触发):
    ET是一种高效的模式,只支持非阻塞socket。
    对于采用ET模式的文件描述符,当epoll_wait检测到其上有事件发生并将此时间通知应用程序以后,应用程序可以不立即处理该事件,但是后续的epoll_wait将不再向应用程序通知这一事件。

    • 为什么将ET称为高效的工作方式了?
      因为ET不用多次触发,减少了每次epoll_wait可能需要返回的fd数量,在并发event数量极多的情况下能够加快epoll_wait的处理速度。
      注意 epoll 工作在 ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的 阻塞读/阻塞写 操作把处理多个文件描述符的任务饿死。

如果对于一个非阻塞 socket,如果使用 epoll 边缘模式去检测数据是否可读,触发可读事件以后,一定要一次性把 socket 上的数据收取干净才行,也就是一定要循环调用 recv 函数直到 recv 出错,错误码是 EWOULDBLOCK 或者 EAGAIN

如果使用水平模式,则不用,你可以根据业务一次性收取固定的字节数,或者收完为止。边缘模式下收取数据的代码示例如下:


参考资料
[1]《UNIX 网络编程》3th [美] W.Richard Stevens,Bill Fenner,Andrew M. Rudoff
[2] http://www.cnblogs.com/ajianbeyourself/p/5859989.html
[3] https://blog.csdn.net/hnlyyk/article/details/50946194

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容

  • I/O复用基本概念 I/O多路复用技术通过把多个I/O的阻塞复用到同一个select的阻塞上,从而使得系统在单线程...
    Ycres阅读 938评论 0 0
  • 看到网上有不少讨论epoll,但大多不够详细准确,以前面试有被问到这个问题。不去更深入的了解,只能停留在知其然...
    电台_Fang阅读 11,441评论 0 8
  • epoll概述 epoll是linux中IO多路复用的一种机制,I/O多路复用就是通过一种机制,一个进程可以监视多...
    发仔很忙阅读 10,858评论 4 35
  • 同步、异步、阻塞、非阻塞 同步 & 异步 同步与异步是针对多个事件(线程/进程)来说的。 如果事件A需要等待事件B...
    rainybowe阅读 2,875评论 0 9
  • 一、概述 I/O复用使得程序能同时监听多个文件描述符,这对提高程序的性能至关重要。 I/O复用虽然能同时监听多个文...
    saviochen阅读 1,037评论 0 4