golang,go,博客,开源,编程

认识epoll

Published on with 0 views and 0 comments

epoll 介绍

epoll 是 Linux 内核提供的一种 高效的 I/O 多路复用机制,用于处理大量的并发连接。它是 select()poll() 系列 I/O 多路复用函数的现代替代方案,旨在解决传统方法在高并发环境下的性能瓶颈问题。通过 epoll,一个进程可以同时监控多个文件描述符,以高效地处理大量的 I/O 事件(如网络连接、文件 I/O 等)。

为什么使用 epoll

  • 性能问题:在大量并发连接时,selectpoll 会遭遇性能瓶颈。例如,selectpoll 需要每次调用时遍历所有的文件描述符,随着文件描述符数量的增加,性能会显著下降。而 epoll 通过事件驱动的方式,避免了这种性能瓶颈。
  • 资源消耗epoll 使用了 内核级的事件通知机制,使得只有在有事件发生时才进行处理,减少了资源消耗,避免了不断轮询的浪费。

epoll 的优势

  1. 高效性epoll 在处理大量并发连接时比 selectpoll 更加高效,尤其在连接数目非常庞大的情况下,epoll 的性能优势尤为明显。
  2. 避免重复遍历selectpoll 需要每次遍历所有文件描述符,而 epoll 通过通知机制,避免了这种遍历,事件只有在发生时才会通知应用程序。
  3. 支持大规模文件描述符selectpoll 受到文件描述符数量的限制(如 select 一般最多支持 1024 个文件描述符),而 epoll 没有这样的限制,能够支持数万甚至更多的文件描述符。
  4. 内核与用户空间解耦epoll 在内核中维护了一个事件表,用户程序只需要对发生的事件进行处理,不需要重复操作内核数据结构,从而避免了不必要的上下文切换。
  5. 支持边缘触发和水平触发epoll 支持两种触发模式:
    • 水平触发(Level Triggered, LT):默认模式。当文件描述符有数据可读/可写时,会多次触发事件,直到数据被处理。
    • 边缘触发(Edge Triggered, ET):只会在事件从无到有的状态发生变化时触发一次事件。适合用于高效的 I/O 处理,但需要开发者更精细地控制读取和写入。

epoll 的工作原理

epoll 基本的工作流程如下:

  1. 创建 epoll 实例:通过 epoll_createepoll_create1 创建一个 epoll 实例,它会返回一个文件描述符,该文件描述符用于之后的事件通知和处理。
  2. 注册事件:通过 epoll_ctl 将需要监控的文件描述符和对应的事件类型(如可读、可写)注册到 epoll 实例中。当监控的文件描述符有事件发生时,内核会通知应用程序。
  3. 等待事件发生:调用 epoll_wait 来等待发生的事件。epoll_wait 会阻塞,直到有事件发生或者超时。
  4. 处理事件:当事件发生时,epoll_wait 会返回有事件的文件描述符,应用程序可以根据这些文件描述符进行读取、写入或者其他处理。
  5. 取消事件:通过 epoll_ctl 可以取消对文件描述符的监控,或者修改文件描述符的事件类型。

epoll 的核心 API

epoll 提供了三个主要的系统调用接口:

  1. epoll_create / epoll_create1: 创建一个 epoll 实例,返回一个文件描述符用于后续的事件操作。

    int epoll_create(int size);  // 已弃用
    int epoll_create1(int flags); // 推荐使用
    
    • size:为旧版 epoll_create 保留参数,用于设置 epoll 内部的事件表大小,通常可以忽略。
    • flags:指定创建时的一些标志,通常为 EPOLL_CLOEXEC
  2. epoll_ctl: 用于向 epoll 实例中添加、修改或删除文件描述符的监控事件。

    int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
    
    • epfd:由 epoll_createepoll_create1 返回的文件描述符。
    • op:操作类型,常用的有:
      • EPOLL_CTL_ADD:添加文件描述符。
      • EPOLL_CTL_MOD:修改文件描述符。
      • EPOLL_CTL_DEL:删除文件描述符。
    • fd:要监控的文件描述符。
    • event:包含事件类型的结构体 epoll_event,用于指定要监听的事件(如 EPOLLINEPOLLOUT 等)。
  3. epoll_wait: 用于等待并返回已经准备好的事件。

    int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
    
    • epfd:由 epoll_createepoll_create1 返回的文件描述符。
    • events:存储返回的事件。
    • maxevents:最大事件数,限制返回的事件数量。
    • timeout:阻塞时间,单位是毫秒。可以设置为:
      • -1:无限期阻塞,直到有事件发生。
      • 0:非阻塞,立即返回。
      • 正整数:设置超时时间。

epoll 事件类型

epoll 支持多种事件类型,可以通过 epoll_event 结构体来指定要监听的事件类型。常见的事件类型包括:

  • EPOLLIN:表示对应的文件描述符可以进行读取操作(如 TCP 连接可读)。
  • EPOLLOUT:表示对应的文件描述符可以进行写入操作(如 TCP 连接可写)。
  • EPOLLERR:表示文件描述符发生错误。
  • EPOLLHUP:表示文件描述符被挂起。
  • EPOLLET:启用边缘触发模式(Edge Triggered),与 EPOLLINEPOLLOUT 结合使用。
  • EPOLLONESHOT:每次事件触发后自动取消该事件的监控,需要重新注册。

使用示例

下面是一个使用 epoll 实现的简单 TCP 服务器:

#include <sys/epoll.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <arpa/inet.h>

#define MAX_EVENTS 10

int main() {
    int epoll_fd, nfds;
    struct epoll_event ev, events[MAX_EVENTS];
  
    // 创建 epoll 实例
    epoll_fd = epoll_create1(0);
    if (epoll_fd == -1) {
        perror("epoll_create1");
        exit(EXIT_FAILURE);
    }

    // 创建 TCP socket
    int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (listen_fd == -1) {
        perror("socket");
        exit(EXIT_FAILURE);
    }

    // 设置 socket 地址
    struct sockaddr_in server_addr;
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = INADDR_ANY;
    server_addr.sin_port = htons(8080);

    // 绑定和监听
    if (bind(listen_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
        perror("bind");
        exit(EXIT_FAILURE);
    }
    if (listen(listen_fd, 10) == -1) {
        perror("listen");
        exit(EXIT_FAILURE);
    }

    // 设置监听文件描述符为非阻塞
    fcntl(listen_fd, F_SETFL, O_NONBLOCK);

    // 注册监听事件
    ev.events = EPOLLIN;
    ev.data.fd = listen_fd;
    if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_fd, &ev) == -1) {
        perror("epoll_ctl: listen_fd");
        exit(EXIT_FAILURE);
    }

    // 事件循环
    while (1) {
        nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
        if (nfds == -1) {
        perror("epoll_wait");
        exit(EXIT_FAILURE);
    }

    for (int n = 0; n < nfds; n++) {
        if (events[n].data.fd == listen_fd) {
            // 新连接
            struct sockaddr_in client_addr;
            socklen_t client_len = sizeof(client_addr);
            int client_fd = accept(listen_fd, (struct sockaddr *)&client_addr, &client_len);
            if (client_fd == -1) {
                perror("accept");
                continue;
            }

            // 设置客户端 socket 为非阻塞
            fcntl(client_fd, F_SETFL, O_NONBLOCK);

            // 注册客户端的读取事件
            ev.events = EPOLLIN | EPOLLET;
            ev.data.fd = client_fd;
            if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &ev) == -1) {
                perror("epoll_ctl: client_fd");
                exit(EXIT_FAILURE);
            }
        } else if (events[n].events & EPOLLIN) {
            // 读取数据
            char buf[512];
            int len = read(events[n].data.fd, buf, sizeof(buf));
            if (len == -1) {
                perror("read");
                close(events[n].data.fd);
            } else if (len == 0) {
                // 客户端关闭连接
                close(events[n].data.fd);
            } else {
                printf("Received: %.*s", len, buf);
                write(events[n].data.fd, buf, len);  // Echo back
            }
        }
    }
}

close(listen_fd);
close(epoll_fd);

return 0;

}


### **总结**

`epoll` 是 Linux 提供的一个高效的事件驱动的 I/O 多路复用机制,它通过内核级的事件通知来替代传统的 `select` 和 `poll`,具有更高的性能,特别是在处理大量并发连接时。`epoll` 支持两种触发模式:水平触发和边缘触发,适用于高并发和高性能的网络应用。

标题:认识epoll
作者:mooncakeee
地址:http://blog.dd95828.com/articles/2025/01/06/1736154909488.html
联系:scotttu@163.com