Linux 中 epoll 的使用 | AI生成和翻译

Home 2025.03

epoll 是 Linux 系统调用,专为高效监控多个文件描述符的事件而设计,例如当数据可读或套接字可写时。它在网络编程中特别有用,因为服务器可能需要同时处理许多连接。与 selectpoll 等旧机制相比,epoll 在大量文件描述符的情况下扩展性更好,因为它避免了重复扫描所有描述符以检查就绪状态。

以下是使用 epoll 的分步指南,随后是一个简单的服务器示例,该服务器接受连接并将数据回显给客户端。


使用 epoll 的步骤

1. 创建 Epoll 实例

要开始使用 epoll,您需要创建一个 epoll 实例,该实例由文件描述符表示。使用 epoll_create1 系统调用:

int epoll_fd = epoll_create1(0);

较旧的 epoll_create 函数类似,但接受一个大小提示(现在被忽略),因此更推荐使用 epoll_create1

2. 添加要监控的文件描述符

使用 epoll_ctl 向 epoll 实例注册文件描述符(例如套接字),并指定要监控的事件:

struct epoll_event ev;
ev.events = EPOLLIN;  // 监控可读性
ev.data.fd = some_fd; // 要监控的文件描述符
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, some_fd, &ev);

3. 等待事件

使用 epoll_wait 阻塞并等待受监控文件描述符上的事件:

struct epoll_event events[MAX_EVENTS];
int nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);

4. 处理事件

循环遍历 epoll_wait 返回的事件并处理它们:

for (int i = 0; i < nfds; i++) {
    if (events[i].events & EPOLLIN) {
        // 文件描述符 events[i].data.fd 可读
    }
}

5. 管理文件描述符(可选)


关键概念

水平触发与边缘触发

非阻塞 I/O

epoll 通常与非阻塞文件描述符配对使用,以防止在 I/O 操作上阻塞。使用以下命令将套接字设置为非阻塞模式:

fcntl(fd, F_SETFL, fcntl(fd, F_GETFL) | O_NONBLOCK);

示例:简单回显服务器

以下是一个基本示例,展示了一个使用 epoll 接受连接并将数据回显给客户端的服务器。为简单起见,它使用水平触发模式。

#include <sys/epoll.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>

#define MAX_EVENTS 10
#define PORT 8080

int main() {
    // 创建监听套接字
    int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (listen_fd == -1) { perror("socket"); exit(1); }

    struct sockaddr_in addr = { .sin_family = AF_INET, .sin_addr.s_addr = INADDR_ANY, .sin_port = htons(PORT) };
    if (bind(listen_fd, (struct sockaddr*)&addr, sizeof(addr)) == -1) { perror("bind"); exit(1); }
    if (listen(listen_fd, 5) == -1) { perror("listen"); exit(1); }

    // 将监听套接字设置为非阻塞
    fcntl(listen_fd, F_SETFL, fcntl(listen_fd, F_GETFL) | O_NONBLOCK);

    // 创建 epoll 实例
    int epoll_fd = epoll_create1(0);
    if (epoll_fd == -1) { perror("epoll_create1"); exit(1); }

    // 将监听套接字添加到 epoll
    struct epoll_event ev, events[MAX_EVENTS];
    ev.events = EPOLLIN; // 水平触发
    ev.data.fd = listen_fd;
    if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_fd, &ev) == -1) { perror("epoll_ctl"); exit(1); }

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

        for (int i = 0; i < nfds; i++) {
            int fd = events[i].data.fd;

            if (fd == listen_fd) {
                // 接受新连接
                int client_fd = accept(listen_fd, NULL, NULL);
                if (client_fd == -1) { perror("accept"); continue; }

                // 将客户端套接字设置为非阻塞
                fcntl(client_fd, F_SETFL, fcntl(client_fd, F_GETFL) | O_NONBLOCK);

                // 将客户端套接字添加到 epoll
                ev.events = EPOLLIN;
                ev.data.fd = client_fd;
                if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &ev) == -1) {
                    perror("epoll_ctl: client_fd");
                    close(client_fd);
                }
            } else {
                // 处理客户端数据
                char buf[1024];
                ssize_t n = read(fd, buf, sizeof(buf));
                if (n <= 0) {
                    // 错误或 EOF:关闭并从 epoll 中移除
                    if (n == -1 && (errno == EAGAIN || errno == EWOULDBLOCK)) continue;
                    close(fd);
                    epoll_ctl(epoll_fd, EPOLL_CTL_DEL, fd, NULL);
                } else {
                    // 将数据回显
                    write(fd, buf, n); // 注意:在实际应用中,处理部分写入
                }
            }
        }
    }

    close(epoll_fd);
    close(listen_fd);
    return 0;
}

关于示例的说明


总结

epoll 提供了一种在 Linux 中处理多个文件描述符的高效方法:

  1. 使用 epoll_create1 创建实例。
  2. 使用 epoll_ctl 注册文件描述符和事件。
  3. 使用 epoll_wait 等待事件。
  4. 在循环中处理事件,根据需要调整监控的事件或移除描述符。

对于简单的应用程序,推荐使用水平触发模式。对于高性能需求,可以考虑边缘触发模式,并仔细处理所有可用数据。始终将 epoll 与非阻塞 I/O 配对使用以获得最佳效果。


Back Donate