linux c上特有的事件驱动模型 - epoll

2018-06-29 11:16:14

在Linux上开发超高并发的服务器一般都会使用 epoll 来代替传统的单纯的 多进程和多线程(一个连接一个进程或一个线程),以及IO多路复用 select 和 poll,还有就是信号驱动 I/O。

epoll API的主要优点如下。 

1.当检查大量的文件描述符时, epoll 的性能延展性比 select() 和 poll() 高很多。
2.epoll API既支持水平触发也支持边缘触发。与之相反, select() 和 poll() 只支持水平触发,而信号驱动IO只支持边缘触发。

性能表现上, epoll 同信号驱动 I/O 相似。但是,epoll() 有一些胜过信号驱动IO的优点。

1.可以避免复杂的信号处理流程(比如信号队列溢出时的处理)。
2.灵活性高,可以指定我们希望检査的事件类型(例如,检査套接字文件描述符的读就绪、写就绪或者两者同时指定)。

epoll API 是 Linux系统专有的,在2.6版中新增。


创建 epoll 实例:epoll_create()

#include <sys/epoll.h>

int epoll_create(int size);

//Returns file descriptor on success, or –1 on error

参数 size 指定了我们想要通过 epoll 实例来检查的文件描述符个数。该参数并不是一 个上限,而是告诉内核应该如何为内部数据结构划分初始大小。(从 Linux2.6.8版以来, size 参数被忽略不用,因为内核实现做了修改意味着该参数之前提供的信息已经不再需要了。)

 作为函数返回值, epoll_ create() 返回了代表新创建的 epoll实例的文件描述符。这个文件描述符在其他几个 epoll 系统调用中用来表示 epoll 实例。当这个文件描述符不再需要时,应该通过 close() 来关闭。当所有与 epoll 实例相关的文件描述符都被关闭时,实例被销毁,相关的资源都返还给系统。(多个文件描述符可能引用到相同的 epoll 实例,这是由于调用了 fork() 或者 dup() 这样类似的函数所致。) 


修改 epoll 的兴趣列表:epoll_ctl()

#include <sys/epoll.h>

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

//Returns 0 on success, or –1 on error

参数 fd 指明了要修改兴趣列表中的哪一个文件描述符的设定。该参数可以是代表管道 FIFO、套接字、 POSIX消息队列、 inotify实例、终端、设备,甚至是另一个epoll 实例的文件描述符(例如,我们可以为受检查的描述符建立起一种层次关系)。但是,这里 fd 不能作为普通文件或目录的文件描述符(会出现 EPERM错误)。 

参数op用来指定需要执行的操作,它可以是如下几种值。

EPOLL_CTL_ADD 将描述符 fd 添加到 epoll实例 epfd 中的兴趣列表中去。对于fd上我们感兴趣的事件,都指定在ev所指向的结构体中,下面会详细介绍。如果我们试图向兴趣列表中添加一个已存在的文件描述符, epoll_ctI() 将出现 EEXIST 错误。

EPOLL_CTL_MOD 修改描述符fd上设定的事件,需要用到由ev所指向的结构体中的信息。如果我们试图修改不在兴趣列表中的文件描述符, epoll_ctI() 将出现 ENOENT错误。

EPOLL_CTL_DEL 将文件描述符fd从epfd的兴趣列表中移除。该操作忽略参数ev。如果我们试图移除一个不在epfd的兴趣列表中的文件描述符, epoll_ctl() 将出现 ENOENT错误。关闭一个文件描述符会自动将其从所有的epoll 实例的兴趣列表中移除。

参数ev是指向结构体 epoll_event的指针,结构体的定义如下。

struct epoll_event {
    uint32_t events; /* epoll events (bit mask) */
    epoll_data_t data; /* User data */
};
typedef union epoll_data {
    void *ptr;     /* Pointer to user-defined data */
    int fd;     /* File descriptor */
    uint32_t u32;     /* 32-bit integer */
    uint64_t u64;     /* 64-bit integer */
} epoll_data_t;
int epfd;
struct epoll_event ev;

epfd = epoll_create(5);
if (epfd == -1)
    errExit("epoll_create");

ev.data.fd = fd;
ev.events = EPOLLIN;
if (epoll_ctl(epfd, EPOLL_CTL_ADD, fd, ev) == -1)
    errExit("epoll_ctl");


max_user_watches 上限

因为每个注册到 epoll 实例上的文件描述符需要占用一小段不能被交换的内核内存空间, 因此内核提供了一个接口用来定义每个用户可以注册到 epoll 实例上的文件描述符总数。这个上限值可以通过 max_user_watches来査看和修改。 max_user_watches 是专属于 Linux系统的 /proc/sys/fs/epoll 目录下的一个文件。默认的上限值根据可用的系统内存来计算得出。

[root@izj6cfw9yi1iqoik31tqbgz ~]# cat /proc/sys/fs/epoll/max_user_watches
204206


事件等待:epoll_wait()

#include <sys/epoll.h>

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

//Returns number of ready file descriptors, 0 on timeout, or –1 on error

系统调用 epoll_wait() 返回 epoll 实例中处于就绪的文件描述符信息。单个 epoll_wait() 调用能返回多个就绪文件描述符的信息。

参数 evlist 所指向的结构体数组中返回的是有关就绪态文件描述符的信息。数组 evlist 的空间由调用者负责申请,所包含的元素个数在参数 maxevents 中指定。 

在数组 evlist 中,每个元素返回的都是单个就绪态文件描述符的信息。 events字段返回了在该描述符上已经发生的事件掩码。data字段返回的是我们在描述符上使用 poll_ctI() 注册感兴趣的事件时在 ev.data中所指定的值。注意,data字段是唯一可获知同这个事件相关的文件描述符号的途径。因此,当我们调用 epoll_ctl() 将文件描述符添加到兴趣列表中时,应该要么将 ev.data.fd设为文件描述符号,要么将 ev.data.ptr设为指向包含文件描述符号的结构体。

参数 timeout用来确定 epoll_wait() 的阻塞行为,有如下几种。

1.如果 timeout 等于-1,调用将一直阻塞,直到兴趣列表中的文件描述符上有事件产生, 或者直到捕获到一个信号为止。

2.如果 timeout等于0,执行一次非阻塞式的检查,看兴趣列表中的文件描述符上产生了哪个事件。

3.如果 timeout 大于0,调用将阻塞至多 timeout 毫秒,直到文件描述符上有事件发生, 或者直到捕获到一个信号为止。

调用成功后, epoll_wait() 返回数组 evlist 中的元素个数。如果在 timeout超时间隔内没有任何文件描述符处于就绪态的话,返回0。出错时返回-1,并在 errno中设定错误码以表示错误原因。

在多线程程序中,可以在一个线程中使用 epoll_ctI() 将文件描述符添加到另一个线程中由 epoll_wait() 所监视的 epoll 实例的兴趣列表中去。这些对兴趣列表的修改将立刻得到处理,而 epoll_wait() 调用将返回有关新添加的文件描述符的就绪信息。

epoll 事件

当我们调用 epoll_ctl() 时可以在 ev.events 中指定的位掩码以及由 epoll_wait() 返回的 enlist[].events 中的值在表63-8中给出。

 位掩码epoll_ctl()的输入  epoll_wait()返回 描述
 EPOLLIN 是 是 可读取非高优先级
 EPOLLPRI 是 是 可读取高优先级
 EPOLLRDHUP 是 是 套接字对端关闭
 EPOLLOUT 是 是 普通数据可写
 EPOLLET 是  采用边缘触发事件通知
 EPOLLONESHOT 是  在完成事件通知后后禁用检查
 EPOLLERR  是 有错误发生
 EPOLLHUP  是 出现挂断

EPOLLONESHOT 标志

默认情况下,一旦通过 epoll_ctI() 的 EPOLL_CTL_ADD 操作将文件描述符添加到epoll 实例的兴趣列表中后,它会保持激活状态(即,之后对 epoll_wait() 的调用会在描述符处于就绪态时通知我们)直到我们显式地通过 epoll_ctI() 的 EPOLL_CTL_DEL 操作将其从列表中移除.如果我们希望在某个特定的文件描述符上只得到一次通知,那么可以在传给 epoll_ctI() 的 ev.events 中指定 EPOLLONESHOT(从 Linux2.6.2版开始支持)标志。如果指定了这个标志, 那么在下一个 epoll_wait() 调用通知我们对应的文件描述符处于就绪态之后,这个描述符就会在兴趣列表中被标记为非激活态,之后的 epoll_wait() 调用都不会再通知我们有关这个描述符的状态了。如果需要,我们可以稍后通过 epoll_ctI() 的 EPOLL_CTL_MOD 操作重新激活对这 个文件描述符的检查。(这种情况下不能用 EPOLL_CTL_ADD操作,因为非激活态的文件描 述符仍然还在 epoll实例的兴趣列表中。)

例子

#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <ctype.h>
#include <limits.h>
#include <signal.h>
#include <sys/stat.h>
#include <sys/file.h>
#include <syslog.h>
#include <sys/wait.h>

#include <sys/epoll.h>

#define MAX_BUF     1000
#define MAX_EVENTS     5

void errExit(char *msg){
    perror(msg);
    exit(1);
}
void fatal(char *msg){
    printf(msg);
    printf("\n");
    exit(1);
}

int main(int argc, char *argv[]){
    int epfd, ready, fd, s, j, numOpenFds;
    struct epoll_event ev;
    struct epoll_event evlist[MAX_EVENTS];
    char buf[MAX_BUF];

    if (argc < 2 || strcmp(argv[1], "--help") == 0){
        printf("%s file...\n", argv[0]);
        return 0;
    }
        

    epfd = epoll_create(argc - 1);
    if (epfd == -1)
        errExit("epoll_create");

    /* Open each file on command line, and add it to the "interest
       list" for the epoll instance */

    for (j = 1; j < argc; j++) {
        fd = open(argv[j], O_RDONLY);
        if (fd == -1)
            errExit("open");
        printf("Opened \"%s\" on fd %d\n", argv[j], fd);

        ev.events = EPOLLIN;            /* Only interested in input events */
        ev.data.fd = fd;
        if (epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev) == -1)
            errExit("epoll_ctl");
    }

    numOpenFds = argc - 1;

    while (numOpenFds > 0) {

        /* Fetch up to MAX_EVENTS items from the ready list of the
           epoll instance */

        printf("About to epoll_wait()\n");
        ready = epoll_wait(epfd, evlist, MAX_EVENTS, -1);
        if (ready == -1) {
            if (errno == EINTR)
                continue;               /* Restart if interrupted by signal */
            else
                errExit("epoll_wait");
        }

        printf("Ready: %d\n", ready);

        /* Deal with returned list of events */

        for (j = 0; j < ready; j++) {
            printf("  fd=%d; events: %s%s%s\n", evlist[j].data.fd,
                    (evlist[j].events & EPOLLIN)  ? "EPOLLIN "  : "",
                    (evlist[j].events & EPOLLHUP) ? "EPOLLHUP " : "",
                    (evlist[j].events & EPOLLERR) ? "EPOLLERR " : "");

            if (evlist[j].events & EPOLLIN) {
                s = read(evlist[j].data.fd, buf, MAX_BUF);
                if (s == -1)
                    errExit("read");
                printf("    read %d bytes: %.*s\n", s, s, buf);

            } else if (evlist[j].events & (EPOLLHUP | EPOLLERR)) {

                /* After the epoll_wait(), EPOLLIN and EPOLLHUP may both have
                   been set. But we'll only get here, and thus close the file
                   descriptor, if EPOLLIN was not set. This ensures that all
                   outstanding input (possibly more than MAX_BUF bytes) is
                   consumed (by further loop iterations) before the file
                   descriptor is closed. */

                printf("    closing fd %d\n", evlist[j].data.fd);
                if (close(evlist[j].data.fd) == -1)
                    errExit("close");
                numOpenFds--;
            }
        }
    }

    printf("All file descriptors closed; bye\n");
    exit(EXIT_SUCCESS);
}
[root@izj6cfw9yi1iqoik31tqbgz c]# mkfifo p
[root@izj6cfw9yi1iqoik31tqbgz c]# ./a.out p
Opened "p" on fd 4
About to epoll_wait()

Ready: 1
  fd=4; events: EPOLLIN 
    read 8 bytes: freecls

About to epoll_wait()

Ready: 1
  fd=4; events: EPOLLIN 
    read 16 bytes: www.freecls.com

About to epoll_wait()

Ready: 1
  fd=4; events: EPOLLHUP 
    closing fd 4
All file descriptors closed; bye
[root@izj6cfw9yi1iqoik31tqbgz c]# cat > p
freecls
www.freecls.com
//ctrl+c

epoll 同 I/O 多路复用的性能对比

从上表可以看出,一旦监控的文件描述符变多,性能差距将越来越大。下面解释下为啥epoll 的性能表现会更好:

1.每次调用 select() 和 poll() 时,内核必须检查所有在调用中指定的文件描述符。与之相反,当通过 epoll_ctI() 指定了需要监视的文件描述符时,内核会在与打开的文件描述上下文相关联的列表中记录该描述符。之后每当执行IO操作使得文件描述符成为就绪态时,内核就在 epoll 描述符的就绪列表中添加一个元素。(单个打开的文件描述上下文中的一次IO事件可能导致与之相关的多个文件描述符成为就绪态。)之后的 epoll_wait() 调用从就绪列表中简单地取出这些元素。

2.每次调用 select() 或 poll() 时,我们传递一个标记了所有待监视的文件描述符的数据结构给内核,调用返回时,内核将所有标记为就绪态的文件描述符的数据结构再传回给我们。与之相反,在 epoll中我们使用 epoll_ctI() 在内核空间中建立一个数据结构,该数据结构会将待监视的文件描述符都记录下来。一旦这个数据结构建立完成,稍后每次调用 epoll_wait() 时就不需要再传递任何与文件描述符有关的信息给内核了,而调用返回的信息中只包含那些已经处于就绪态的描述符。

边缘触发和水平触发

默认情况下 epoll 提供的是水平触发通知。要使用边缘触发通知,可以用如下代码来修改。

struct epoll_event ev;
ev.data.fd = fd
ev.events = EPOLLIN | EPOLLET;
if (epoll_ctl(epfd, EPOLL_CTL_ADD, fd, ev) == -1)
    errExit("epoll_ctl");

我们通过一个例子来说明 epoll 的水平触发和边缘触发通知之间的区别。假设我们使用 epoll 来监视一个套接字上的输入( EPOLLIN),接下来会发生如下的事件。

1.套接字上有输入到来。
2.我们调用一次 epoll_wait()。无论我们采用的是水平触发还是边缘触发通知,该调用都会告诉我们套接字已经处于就绪态了。
3.再次调用 epoll_wait( 如果我们采用的是水平触发通知,那么第二个 epoll_wait() 调用将告诉我们套接字处于就绪态。而如果我们采用边缘触发通知,那么第二个 epoll_wait(调用将阻塞,因为自从上一次调用 epoll_wait() 以来并没有新的输入到来。

边缘触发通知通常和非阻塞的文件描述符结合使用。因而,采用 epoll的边缘触发通知机制的程序基本框架如下。

1.让所有待监视的文件描述符都成为非阻塞的。
2.通过 epoll_ctI() 构建 epoll 的兴趣列表。
3.通过如下的循环处理IO事件。
    (a)通过 epoll_ wait() 取得处于就绪态的描述符列表。
    (b)针对每一个处于就绪态的文件描述符,不断进行IO处理直到相关的系统调用(例如 read()、 write()、 recv()、send() 或 accept() )返回 EAGAIN 或 EWOULDBLOCK 错误。

也就是边缘触发只通知一次,如果只处理了一半,那么下次epoll_wait()会阻塞(假设以阻塞模式),而水平触发模式如果只处理了一般, epoll_wait() 会立马返回。

采用边缘触发通知时避免出现文件描述符饥饿现象

假设我们采用边缘触发通知监视多个文件描述符,其中一个处于就绪态的文件描述符上有着大量的输入存在(可能是一个不间断的输入流)。如果在检测到该文件描述符处于就绪态后,我们将尝试通过非阻塞式的读操作将所有的输入都读取,那么此时就会有使其他的文件描述符处于饥饿状态的风险存在(即,在我们再次检查这些文件描述符是否处于就绪态并执行IO操作前会有很长的一段处理时间)。该问题的一种解决方案是让应用程序维护一个列表,列表中存放着已经被通知为就绪态的文件描述符。通过一个循环按照如下 方式不断处理。

1.调用 epoll_wait() 监视文件描述符,并将处于就绪态的描述符添加到应用程序维护的列表中。如果这个文件描述符已经注册到应用程序维护的列表中了,那么这次监视操作的超时时间应该设为较小的值或者是0。这样如果没有新的文件描述符成为就绪态,应用程序就可以迅速进行到下一步,去处理那些已经处于就绪态的文件描述符了。

2.在应用程序维护的列表中,只在那些已经注册为就绪态的文件描述符上进行一定限度的I/O操作(可能是以轮转调度( round- robin)方式循环处理,而不是每次 epoll_wait() 调用后都从列表头开始处理)。当相关的非阻塞I/O系统调用出现 EAGAIN 或 EWOULDBLOCK 错误时,文件描述符就可以从应用程序维护的列表中移除了。


 备注

1.编译器版本gcc4.8,运行环境centos7 64位
2.原文地址http://www.freecls.com/a/2712/66 

 

©著作权归作者所有
收藏
推荐阅读
简介
天降大任于斯人也,必先苦其心志。