关于poll和epoll

171 阅读9分钟

poll 和 epoll 是 Linux 系统中用于实现 I/O 多路复用 的系统调用,用于高效管理多个文件描述符(如网络套接字)的 I/O 事件(可读、可写、错误等),是高并发网络编程的核心技术。两者解决的问题相似,但在性能和设计上有显著差异,以下是详细对比:

一、I/O 多路复用的核心问题

在网络编程中,单个进程 / 线程需要同时处理多个网络连接(如服务器同时应对成百上千个客户端)。传统的 “一连接一线程” 模型会导致资源耗尽(线程栈、上下文切换开销),而 I/O 多路复用 允许单个进程 / 线程通过一个系统调用同时监控多个文件描述符(fd),仅当某个 fd 有 I/O 事件发生时才进行处理,大幅提升效率。

poll 和 epoll 都是实现这一机制的工具,替代了更早的 select,但 epoll 是 Linux 特有的优化版本。

二、poll 机制

poll 是 POSIX 标准定义的系统调用,在 Linux、Unix 等系统中通用,功能上是 select 的改进版。

1. 核心原理

  • 数据结构:使用 struct pollfd 数组描述需要监控的文件描述符及事件:

    struct pollfd {
        int   fd;         // 要监控的文件描述符
        short events;     // 关注的事件(如 POLLIN 表示可读,POLLOUT 表示可写)
        short revents;    // 实际发生的事件(由内核填充)
    };
    
  • 调用流程

    1. 应用程序初始化 pollfd 数组,设置需要监控的 fd 和事件。

    2. 调用 poll 系统调用,阻塞等待事件发生:

      int poll(struct pollfd *fds, nfds_t nfds, int timeout);
      
      • fds:监控的 fd 数组;nfds:数组长度;timeout:超时时间(毫秒,-1 表示永久阻塞)。
    3. 内核遍历所有 fd,检查是否有事件发生,将结果写入 revents 并返回就绪的 fd 数量。

    4. 应用程序遍历 pollfd 数组,根据 revents 处理就绪的 fd。

2. 优缺点

  • 优点

    • 突破 select 对 fd 数量的限制(select 受 FD_SETSIZE 限制,通常为 1024),poll 仅受系统文件描述符上限限制。
    • 无需每次调用都重置监控事件(select 的 fd_set 会被内核修改,需重新初始化)。
  • 缺点

    • 效率低:每次调用 poll 时,内核需遍历整个 pollfd 数组检查事件,当 fd 数量庞大(如上万)时,遍历开销显著。
    • 无事件就绪通知机制:应用程序需遍历整个数组才能找到就绪的 fd,进一步增加开销。
    • 水平触发(LT)模式:只要 fd 有未处理的数据,就会持续触发事件(可能导致不必要的调用)。

三、epoll 机制

epoll 是 Linux 2.6 内核引入的 I/O 多路复用机制,专为高并发场景设计,性能远超 poll 和 select

1. 核心原理

epoll 通过三个系统调用实现,引入了 “事件表” 和 “就绪队列” 的设计,避免了 poll 的遍历开销:

  • epoll_create:创建一个 epoll 实例(事件表),返回一个管理 fd。
  • epoll_ctl:向事件表中添加、修改或删除需要监控的 fd 及事件。
  • epoll_wait:等待事件发生,返回就绪的 fd 列表。

关键设计

  • 事件表:内核维护一个红黑树存储所有注册的 fd 和事件,支持高效的增删改操作(O (log n))。
  • 就绪队列:内核维护一个双向链表,当 fd 有事件发生时,自动加入该队列,epoll_wait 直接返回就绪队列中的 fd,无需遍历所有注册的 fd。

2. 触发模式

epoll 支持两种事件触发模式,可根据场景选择:

  • 水平触发(Level Trigger,LT)
    只要 fd 中还有未处理的数据(如可读缓冲区非空),就会持续触发事件。优点是编程简单(无需一次性处理完所有数据),缺点是可能有冗余通知。
  • 边缘触发(Edge Trigger,ET)
    仅在 fd 状态发生变化时触发一次(如从不可读变为可读)。优点是通知次数少,效率高;缺点是必须一次性处理完所有数据(否则可能遗漏事件),编程复杂(需配合非阻塞 I/O)。

3. 调用流程

// 1. 创建 epoll 实例
int epfd = epoll_create1(0);

// 2. 注册需要监控的 fd 和事件(如监听可读事件)
struct epoll_event ev;
ev.events = EPOLLIN | EPOLLET;  // 边缘触发模式的可读事件
ev.data.fd = sockfd;            // 关联的文件描述符
epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev);

// 3. 等待事件发生
struct epoll_event events[1024];
int n = epoll_wait(epfd, events, 1024, -1);

// 4. 处理就绪事件
for (int i = 0; i < n; i++) {
    if (events[i].events & EPOLLIN) {
        // 处理可读事件(如读取客户端数据)
    }
}

4. 优缺点

  • 优点

    • 高效性:内核通过红黑树管理注册的 fd,通过就绪队列直接返回就绪事件,避免遍历所有 fd,适合大量 fd 场景(万级以上)。
    • 灵活的触发模式:支持 LT 和 ET,ET 模式可减少系统调用次数,提升性能。
    • 无 fd 数量限制:仅受系统内存和文件描述符上限限制。
  • 缺点

    • Linux 特有:不支持 Windows、BSD 等其他系统(移植性差)。
    • ET 模式编程复杂:需确保一次性处理完所有数据,否则会丢失事件。

四、poll 与 epoll 的核心差异

对比维度pollepoll
适用场景连接数较少(千级以下)的场景高并发(万级以上连接)场景
事件查找方式每次调用遍历所有注册的 fd直接返回就绪队列中的 fd,无需遍历
时间复杂度O (n)(n 为注册的 fd 总数)O (1)(获取就绪事件)+ O (log n)(增删改)
触发模式仅支持水平触发(LT)支持 LT 和边缘触发(ET)
系统调用次数每次等待事件都需传入完整的 fd 列表注册 / 修改 fd 时调用 epoll_ctl,等待时无需重复传入
移植性跨平台(Linux、Unix、BSD 等)仅 Linux 支持

五、如何选择?

  • 中小规模连接(<1000)poll 足够用,且移植性更好(如需要跨平台)。
  • 高并发场景(>10000) :优先用 epoll,尤其是 ET 模式,可显著降低系统开销(如 Nginx、Redis 等高性能服务器均采用 epoll)。
  • 跨平台需求:若需支持 Windows 或 macOS,可考虑 kqueue(BSD/macOS)或 IOCP(Windows),或使用封装库(如 libevent、libuv)屏蔽底层差异。

总结

poll 是对 select 的改进,解决了 fd 数量限制,但仍存在遍历开销;epoll 是 Linux 为高并发设计的优化方案,通过事件表和就绪队列实现高效 I/O 多路复用,是高性能网络服务器的首选。理解两者的差异,有助于在实际开发中根据场景选择合适的技术,平衡性能和移植性

select和poll本质时轮询,将set_fd 数据从用户空间拷贝到内核空间,一旦有哪个fd活跃了,则将整个数组拷贝到用户空间,逐个检查时那个fd活跃。

epoll是基于事件驱动的,将新增加的连接,(epoll_ctr)注册到内核提供的红黑树节点(epoll_create创建红黑树),节点注册了回调函数,一旦i/o就绪,则触发回调函数,将fd节点加入就绪队列。直接处理就绪队列便可。epoll_wait就是从就绪链表中拿这些fd。

如果连接数不多,epoll没有优势。

#include <iostream>
#include <vector>
#include <string>
#include <cstring>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <unistd.h>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
#include <functional>
#include <algorithm>

// 线程池类
class ThreadPool {
public:
    ThreadPool(size_t numThreads) : stop(false) {
        for (size_t i = 0; i < numThreads; ++i) {
            workers.emplace_back([this] {
                while (true) {
                    std::function<void()> task;
                    {
                        std::unique_lock<std::mutex> lock(this->queueMutex);
                        this->condition.wait(lock, [this] {
                            return this->stop || !this->tasks.empty();
                        });
                        
                        if (this->stop && this->tasks.empty())
                            return;
                            
                        task = std::move(this->tasks.front());
                        this->tasks.pop();
                    }
                    task();
                }
            });
        }
    }
    
    ~ThreadPool() {
        {
            std::unique_lock<std::mutex> lock(queueMutex);
            stop = true;
        }
        condition.notify_all();
        for (std::thread &worker : workers)
            worker.join();
    }
    
    template<class F>
    void enqueue(F &&f) {
        {
            std::unique_lock<std::mutex> lock(queueMutex);
            tasks.emplace(std::forward<F>(f));
        }
        condition.notify_one();
    }

private:
    std::vector<std::thread> workers;
    std::queue<std::function<void()>> tasks;
    std::mutex queueMutex;
    std::condition_variable condition;
    bool stop;
};

// 设置文件描述符为非阻塞模式
void setNonBlocking(int fd) {
    int flags = fcntl(fd, F_GETFL, 0);
    if (flags == -1) {
        perror("fcntl F_GETFL failed");
        exit(EXIT_FAILURE);
    }
    if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) == -1) {
        perror("fcntl F_SETFL failed");
        exit(EXIT_FAILURE);
    }
}

// 读取数据
ssize_t readn(int fd, char *buf, size_t n) {
    size_t nleft = n;
    ssize_t nread;
    char *ptr = buf;
    
    while (nleft > 0) {
        if ((nread = read(fd, ptr, nleft)) < 0) {
            if (errno == EINTR)
                nread = 0;
            else if (errno == EAGAIN)
                return n - nleft;
            else
                return -1;
        } else if (nread == 0)
            break;
            
        nleft -= nread;
        ptr += nread;
    }
    return n - nleft;
}

// 发送数据
ssize_t writen(int fd, const char *buf, size_t n) {
    size_t nleft = n;
    ssize_t nwritten;
    const char *ptr = buf;
    
    while (nleft > 0) {
        if ((nwritten = write(fd, ptr, nleft)) < 0) {
            if (errno == EINTR)
                nwritten = 0;
            else if (errno == EAGAIN)
                return n - nleft;
            else
                return -1;
        }
        
        nleft -= nwritten;
        ptr += nwritten;
    }
    return n;
}

// 处理客户端请求
void handleClient(int clientFd, ThreadPool &threadPool, int epollFd) {
    char buf[1024] = {0};
    ssize_t n = readn(clientFd, buf, sizeof(buf) - 1);
    
    if (n <= 0) {
        if (n < 0) perror("read error");
        close(clientFd);
        epoll_ctl(epollFd, EPOLL_CTL_DEL, clientFd, nullptr);
        return;
    }
    
    std::cout << "Received from client " << clientFd << ": " << buf << std::endl;
    
    // 构造响应
    std::string response = "HTTP/1.1 200 OK\r\n";
    response += "Content-Length: " + std::to_string(n) + "\r\n";
    response += "Content-Type: text/plain\r\n\r\n";
    response += std::string(buf, n);
    
    // 发送响应
    n = writen(clientFd, response.c_str(), response.size());
    if (n < 0) {
        perror("write error");
        close(clientFd);
        epoll_ctl(epollFd, EPOLL_CTL_DEL, clientFd, nullptr);
        return;
    }
    
    // 对于HTTP长连接,可以保持连接,这里简单处理为关闭
    close(clientFd);
    epoll_ctl(epollFd, EPOLL_CTL_DEL, clientFd, nullptr);
}

int main(int argc, char *argv[]) {
    if (argc != 2) {
        std::cerr << "Usage: " << argv[0] << " <port>" << std::endl;
        return 1;
    }
    
    int port = std::stoi(argv[1]);
    
    // 创建监听socket
    int listenFd = socket(AF_INET, SOCK_STREAM, 0);
    if (listenFd < 0) {
        perror("socket creation failed");
        return 1;
    }
    
    // 设置SO_REUSEADDR选项
    int opt = 1;
    if (setsockopt(listenFd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) < 0) {
        perror("setsockopt failed");
        return 1;
    }
    
    // 绑定地址
    struct sockaddr_in serverAddr;
    std::memset(&serverAddr, 0, sizeof(serverAddr));
    serverAddr.sin_family = AF_INET;
    serverAddr.sin_addr.s_addr = INADDR_ANY;
    serverAddr.sin_port = htons(port);
    
    if (bind(listenFd, (struct sockaddr *)&serverAddr, sizeof(serverAddr)) < 0) {
        perror("bind failed");
        close(listenFd);
        return 1;
    }
    
    // 开始监听
    if (listen(listenFd, SOMAXCONN) < 0) {
        perror("listen failed");
        close(listenFd);
        return 1;
    }
    
    // 设置监听socket为非阻塞
    setNonBlocking(listenFd);
    
    // 创建epoll实例
    int epollFd = epoll_create1(0);
    if (epollFd < 0) {
        perror("epoll_create1 failed");
        close(listenFd);
        return 1;
    }
    
    // 注册监听socket到epoll
    struct epoll_event event;
    std::memset(&event, 0, sizeof(event));
    event.data.fd = listenFd;
    event.events = EPOLLIN | EPOLLET;  // 边缘触发模式
    
    if (epoll_ctl(epollFd, EPOLL_CTL_ADD, listenFd, &event) < 0) {
        perror("epoll_ctl add listenFd failed");
        close(listenFd);
        close(epollFd);
        return 1;
    }
    
    // 创建线程池,线程数为CPU核心数
    int numThreads = std::thread::hardware_concurrency();
    if (numThreads == 0) numThreads = 4;
    ThreadPool threadPool(numThreads);
    
    std::cout << "Server started on port " << port << ", using " << numThreads << " threads" << std::endl;
    
    // 事件循环
    const int MAX_EVENTS = 1024;
    struct epoll_event events[MAX_EVENTS];
    
    while (true) {
        int nfds = epoll_wait(epollFd, events, MAX_EVENTS, -1);
        if (nfds < 0) {
            perror("epoll_wait failed");
            break;
        }
        
        for (int i = 0; i < nfds; ++i) {
            if (events[i].data.fd == listenFd) {
                // 处理新连接
                while (true) {
                    struct sockaddr_in clientAddr;
                    socklen_t clientLen = sizeof(clientAddr);
                    int clientFd = accept(listenFd, (struct sockaddr *)&clientAddr, &clientLen);
                    
                    if (clientFd < 0) {
                        if (errno == EAGAIN || errno == EWOULDBLOCK)
                            break;  // 没有更多连接
                        else {
                            perror("accept failed");
                            break;
                        }
                    }
                    
                    std::cout << "New connection from " << inet_ntoa(clientAddr.sin_addr) 
                              << ":" << ntohs(clientAddr.sin_port) << std::endl;
                    
                    // 设置客户端socket为非阻塞
                    setNonBlocking(clientFd);
                    
                    // 注册客户端socket到epoll
                    struct epoll_event clientEvent;
                    std::memset(&clientEvent, 0, sizeof(clientEvent));
                    clientEvent.data.fd = clientFd;
                    clientEvent.events = EPOLLIN | EPOLLET | EPOLLONESHOT;  // 一次性触发
                    
                    if (epoll_ctl(epollFd, EPOLL_CTL_ADD, clientFd, &clientEvent) < 0) {
                        perror("epoll_ctl add clientFd failed");
                        close(clientFd);
                    }
                }
            } else if (events[i].events & EPOLLIN) {
                // 处理可读事件
                int clientFd = events[i].data.fd;
                threadPool.enqueue([clientFd, &threadPool, epollFd]() {
                    handleClient(clientFd, threadPool, epollFd);
                });
            }
        }
    }
    
    // 清理资源
    close(listenFd);
    close(epollFd);
    return 0;
}