16、poll模型:poll()函数详解、pollfd结构体、与select的对比

聊完了select,咱们来看看它的老大哥——poll。

说实话,我刚开始做网络编程那会儿,用的就是select。后来项目越做越大,连接数一上来,select那点家底就不够看了。这时候,poll就派上了用场。

poll和select干的是同一件事:帮你盯着多个文件描述符,看哪个准备好了。但实现方式上,poll要优雅得多。

pollfd结构体:核心数据结构

先看poll的核心数据结构。它比select的fd_set要直观不少。

#include <poll.h>

struct pollfd {
    int   fd;         /* 要监视的文件描述符 */
    short events;     /* 关心的事件类型 */
    short revents;    /* 实际发生的事件类型 */
};

你看,每个pollfd结构体就管一个描述符。fd就是你要监视的套接字或文件。events是你关心的事件,revents是内核返回给你的结果。

events和revents可以取这些值:

事件常量 含义 可用于events 可用于revents
POLLIN 有数据可读
POLLOUT 可以写入数据
POLLERR 发生错误
POLLHUP 对方关闭连接
POLLNVAL 文件描述符未打开

我个人习惯,每次初始化pollfd时,都把fd设成-1,events设成0。这样后面方便跳过无效的描述符。

poll()函数:用法详解

函数原型长这样:

#include <poll.h>

int poll(struct pollfd *fds, nfds_t nfds, int timeout);

参数说明:

  • fds:pollfd结构体数组的首地址
  • nfds:数组中有多少个有效的描述符
  • timeout:超时时间,单位毫秒

timeout的取值很有意思:

  • -1:永远等待,直到有事件发生
  • 0:立即返回,不阻塞
  • 大于0:等待指定的毫秒数

返回值:

  • 大于0:就绪的描述符数量
  • 等于0:超时,没有描述符就绪
  • -1:出错,errno被设置

小技巧:timeout设为0时,poll就变成了非阻塞模式。我经常用它来做一次性的状态检查,比如在游戏循环里快速扫一眼有没有新数据。

实战代码:一个简单的poll服务器

光说不练假把式。咱们写个简单的echo服务器,用poll来管理连接。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <poll.h>
#include <errno.h>

#define MAX_CLIENTS 1024
#define BUFFER_SIZE 1024

int main() {
    int listen_fd, client_fd;
    struct sockaddr_in server_addr, client_addr;
    socklen_t client_len;
    struct pollfd fds[MAX_CLIENTS];
    int nfds = 1, i;
    char buffer[BUFFER_SIZE];
    
    // 创建监听套接字
    listen_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (listen_fd < 0) {
        perror("socket");
        exit(1);
    }
    
    // 设置地址重用
    int opt = 1;
    setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
    
    // 绑定地址
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    server_addr.sin_port = htons(8888);
    
    if (bind(listen_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
        perror("bind");
        close(listen_fd);
        exit(1);
    }
    
    // 开始监听
    if (listen(listen_fd, 10) < 0) {
        perror("listen");
        close(listen_fd);
        exit(1);
    }
    
    // 初始化pollfd数组
    memset(fds, -1, sizeof(fds));
    fds[0].fd = listen_fd;
    fds[0].events = POLLIN;
    
    printf("Poll echo server started on port 8888...\n");
    
    while (1) {
        int ret = poll(fds, nfds, -1);
        if (ret < 0) {
            perror("poll");
            break;
        }
        
        // 检查监听套接字
        if (fds[0].revents & POLLIN) {
            client_len = sizeof(client_addr);
            client_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_len);
            if (client_fd < 0) {
                perror("accept");
                continue;
            }
            
            // 找个空位放新连接
            for (i = 1; i < MAX_CLIENTS; i++) {
                if (fds[i].fd == -1) {
                    fds[i].fd = client_fd;
                    fds[i].events = POLLIN;
                    if (i >= nfds) nfds = i + 1;
                    printf("New client connected, fd=%d\n", client_fd);
                    break;
                }
            }
            
            if (i == MAX_CLIENTS) {
                printf("Too many clients, rejecting\n");
                close(client_fd);
            }
        }
        
        // 检查客户端连接
        for (i = 1; i < nfds; i++) {
            if (fds[i].fd == -1) continue;
            
            if (fds[i].revents & POLLIN) {
                int n = read(fds[i].fd, buffer, sizeof(buffer) - 1);
                if (n <= 0) {
                    // 连接关闭或出错
                    if (n == 0) {
                        printf("Client fd=%d disconnected\n", fds[i].fd);
                    } else {
                        perror("read");
                    }
                    close(fds[i].fd);
                    fds[i].fd = -1;
                } else {
                    buffer[n] = '\0';
                    printf("Received from fd=%d: %s", fds[i].fd, buffer);
                    // 原样返回
                    write(fds[i].fd, buffer, n);
                }
            }
            
            if (fds[i].revents & POLLERR) {
                printf("Error on fd=%d\n", fds[i].fd);
                close(fds[i].fd);
                fds[i].fd = -1;
            }
        }
    }
    
    // 清理
    for (i = 0; i < nfds; i++) {
        if (fds[i].fd != -1) {
            close(fds[i].fd);
        }
    }
    
    return 0;
}

注意:poll返回后,revents字段会被内核修改。你必须在每次poll调用后重新检查所有描述符。我曾经犯过这个错——以为revents会自己清零,结果程序跑着跑着就乱了。

poll vs select:到底差在哪?

这两个家伙,说白了都是I/O多路复用的老前辈。但poll确实比select强不少。

对比项 select poll
描述符数量限制 FD_SETSIZE(通常1024) 无硬性限制(受内存限制)
数据结构 fd_set位图 pollfd结构体数组
事件分离 读、写、异常三个集合 统一在events/revents中
重新初始化 每次调用前都要重新设置 events字段保持不变
跨平台 几乎所有平台 POSIX系统,Windows不支持
性能(大量连接) O(n)遍历,n为最大描述符 O(n)遍历,n为实际描述符数

你看,poll最大的优势就是没有那个1024的限制。我记得有个项目,需要同时管理5000多个连接,用select根本搞不定,换成poll就轻松多了。

另外,poll不需要每次重新构造整个集合。select每次调用前都得把三个fd_set重新填一遍,poll的events字段会保留你上次的设置。这个细节,写代码时能省不少事。

但poll也不是完美的。它和select一样,都是线性扫描——每次返回后,你得遍历整个数组才能知道哪些描述符就绪了。连接数上万时,这个开销就很可观了。

核心区别一句话:select用位图,有上限;poll用结构体数组,无上限。但两者都是O(n)的遍历模型。

poll的避坑指南

用poll这些年,我踩过不少坑。分享几个典型的:

  • revents不清零:poll不会自动清零revents。每次调用前,你得手动把revents设成0。我习惯在循环开始前用memset清零整个数组。
  • fd重复:同一个fd不要出现在多个pollfd条目中。否则revents会被覆盖,结果不可预测。
  • timeout精度:poll的timeout是毫秒级。如果你需要微秒级的精度,得用ppoll或者epoll。
  • 信号中断:poll被信号中断时,会返回-1,errno设为EINTR。这时候要不要重试,得看你的业务逻辑。

嗯,说到信号中断,我有个亲身经历。有一次线上服务莫名其妙地超时,查了半天才发现是定时器信号打断了poll。后来加了EINTR重试逻辑,问题就解决了。

poll的工作流程

下面这张图,把poll的整个流程串起来了。你看一遍,应该就能理解它的工作方式。

poll模型工作流程 1. 创建pollfd数组 2. 设置events字段 3. 调用poll() 4. 内核检查描述符 5. 设置revents字段 6. 返回就绪描述符数 关键点 • events 由用户设置 • revents 由内核设置 • 返回后遍历数组 • 检查 revents 字段 • 处理就绪的描述符 • 下次调用前清零 循环处理

你看,整个流程其实很清晰。用户设置events,内核填充revents,然后你遍历数组处理就绪的描述符。循环往复,直到程序退出。

说实话,poll虽然比select先进,但在高并发场景下,它和select一样,都有O(n)的遍历开销。如果你要处理上万甚至十万级的连接,那还是得用epoll或者kqueue。不过对于中小型项目,poll完全够用了。

好了,poll就聊到这儿。记住它的核心思想:用结构体数组管理描述符,用events/revents分离关心的事件和发生的事件。这个设计思路,后来被很多框架借鉴了。


公众号:蓝海资料掘金营,微信deep3321