epoll事件驱动

Linux系统下,Nginx会默认使用epoll作为事件驱动机制,使用epoll是Nginx性能高的一个重要原因。
本文对epoll工作机制稍作整理。

事件驱动

事件驱动又称作IO多路复用,epoll是Linux内核实现一种IO多路复用机制

  • IO: 在Linux系统下,一切皆文件。文件可读/可写即称作IO,比如:普通文件、socket套接字、进程之间通信的管道pipe,都是可以IO的对象。Linux系统统一用文件描述符fd表示。
  • 事件:
    • 可读事件:当fd关联的内核读缓冲区可读时,则触发可读事件 (可写:内核缓冲区非空)
    • 可写事件:当fd关联的内科写缓冲区可写时,则触发可写事件 (可读:内核缓冲区不满)
  • 事件通知方式
    • 主动通知:仅当有fd有事件发生时,内核主动通知上层应用有事件发生的fd集合
    • 被动通知:上层应用向内核轮询检查fd集合中的fd是否有事件发生

epoll API介绍

epoll_create

  • 原型:int epoll_create(int size)
    • 功能:创建一个epoll实例,内核会返回一个表示epoll实例的fd
    • 入参
      • size: 原本表示所要监视fd的最多个数。现在Linux版中无意义,只要传值大于0的整数即可
    • 返回值: epoll实例的fd
    • 说明:调用该API,内核会产生一个epoll实例数据结构,然后返回一个文件描述符

epoll_ctl

  • 原型: int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
    • 功能:添加/删除/修改要监听的事件
    • 入参
      • epfd: 即epoll_create创建的epoll实例描述符
      • op: 添加、删除或修改
        • EPOLL_CTL_ADD: 添加一个需要监视的fd
        • EPOLL_CTL_DEL: 删除一个fd
        • EPOLL_CTL_MOD: 修改一个fd
      • fd: IO对象描述符
      • event结构体
        struct epoll_event {
            __uint32_t events;
            epoll_data_t data;
        };
        
        • events字段:感兴趣的事件描述,可多选
          • EPOLLIN:fd处于可读状态
          • EPOLLOUT:fd处于可写状态
          • EPOLLET:通知模式为边缘触发模式,相对于水平触发模式而言
          • EPOLLONESHOT:第一次进行通知,之后不再检测
          • EPOLLHUP: 本端产生一个挂断事件,默认检测事件
          • EPOLLDHUP:对端产生一个挂断事件
          • EPOLLPRI:由带外数据触发
          • EPOLLERR:产生错误时触发,默认检测事件
        • data字段:是一个union结构
          typedef union epoll_data {
              void *ptr;
              int fd;
              __uint32_t u32;
              __uint64_t u64;
          };
          
          • 如果不需要额外的信息,只需简单的赋值: epoll_data.fd = fd
          • 如果需要额外信息,则可以使用void *ptr, 这样epoll_wait返回时可以使用
    • 返回值: 如果返回-1表示调用失败

epoll_wait

  • 原型:int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout)
    • 功能:阻塞等待已注册的事件发生,返回事件的数目,并将触发的事件写入events数组中
    • 入参:
      • epfd: 即epoll_create创建的epoll实例描述符
      • events:用来记录被触发的事件数组,其大小应该和maxevents一致
      • maxevents: 返回的events的最大个数
      • timeout: 阻塞超时(以ms为单位)
        • 如果传参-1, 则调用一直阻塞,直到有事件发生或捕获其他信号
        • 如果传参0,则非阻塞检测是否有事件发生,无论有事件还是没事件,调用立即返回
        • 如果传参大于0, 则最多等待timeout时间返回,如果等待期间有事件发生则立即返回

水平触发和边缘触发

水平触发

  • 只要缓冲区有数据,则返回可读事件
  • 只要缓冲区还不满,则返回可写事件

边缘触发

  • 可读事件

    • 当缓冲区数据为空变为非空时,则返回可读事件
    • 当缓冲区接收到新数据时,即缓冲区待读数据变多时,返回可读事件
    • 当缓冲区有数据可读,且应用进程对相应的描述符进行EPOLL_CTL_MOD 修改EPOLLIN事件时,返回可读事件
  • 可写事件

    • 当缓冲区由不可写变为可写时,则返回可写事件
    • 当有旧数据被发送走,即缓冲区中的内容变少的时候,返回可写事件
    • 当缓冲区有空间可写,且应用进程对相应的描述符进行EPOLL_CTL_MOD 修改EPOLLOUT事件时,返回可写事件

代码示例

#include <stdio.h>
#include <unistd.h>
#include <sys/epoll.h>
#include <string.h>

#define MAX_EVENTS 5
#define READ_SIZE 10

int main() {
    int running = 1, event_count, i;
    size_t bytes_read;
    char read_buffer[READ_SIZE + 1];
    struct epoll_event event, events[MAX_EVENTS];

    int epoll_fd = epoll_create1(0);
    if (epoll_fd == -1) {
        fprintf(stderr, "Failed to create epoll fd\n");
        return 1;
    }

    event.events = EPOLLIN;
    event.data.fd = 0;

    if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, 0, &event) == -1) {
        fprintf(stderr, "Failed to add fd to epoll\n");
        return 1;
    }

    while (running) {
        printf("\nPolling for input...\n");
        event_count = epoll_wait(epoll_fd, events, MAX_EVENTS, 30000);
        printf("%d ready events\n", event_count);
        for (i = 0; i < event_count; i++) {
            printf("Reading fd '%d' -- ", events[i].data.fd);
            bytes_read = read(events[i].data.fd, read_buffer, READ_SIZE);
            printf("%zd bytes read.\n", bytes_read);
            read_buffer[bytes_read] = '\0';
            printf("Read '%s'\n", read_buffer);

            if (!strncmp(read_buffer, "stop\n", 5)) {
                running = 0;
            }
        }
    }

    if (close(epoll_fd)) {
        fprintf(stderr, "Failed to close epoll fd\n");
        return 1;
    }

    return 0;
}