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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

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