poll系统调用及示例

poll系统调用及示例

我们继续学习 Linux 系统编程中的重要函数。这次我们介绍 poll 函数,它是一种高效的 I/O 多路复用机制,允许一个进程同时监视多个文件描述符,等待其中任何一个或多个文件描述符变我们继续学习 Linux 系统编程中的重要函数。这次我们介绍 poll 函数,它是一种高效的 I/O 多路复用机制,允许一个进程同时监视多个文件描述符,等待其中任何一个或多个文件描述符变为“就绪”状态(例如可读、可写或发生异常)。

相关文章:ppoll系统调用及示例 epoll_create1系统调用及示例 epoll_ctl系统调用及示例


1. 函数介绍

poll 是一个 Linux 系统调用,用于实现 I/O 多路复用 (I/O multiplexing)。它的核心思想是让进程能够同时检查多个文件描述符(如套接字、管道、终端等)的状态,看它们是否准备好进行 I/O 操作(例如读取、写入),而无需对每个文件描述符都进行阻塞式等待。

在没有 poll(或 selectepoll)的情况下,如果一个程序需要同时处理多个网络连接或文件,它可能需要创建多个线程或进程,或者在一个文件描述符上阻塞等待,这会非常低效或复杂。poll 允许一个线程/进程在一个调用中“监听”所有感兴趣的文件描述符,当其中任何一个准备好时,poll 返回,程序就可以处理那个就绪的文件描述符。

你可以把它想象成一个“服务员”,同时照看多张餐桌(文件描述符)。服务员不需要一直站在某一张餐桌旁等客人点菜(数据),而是可以走一圈看看哪张餐桌的客人举手了(数据就绪),然后去为那张餐桌服务。


2. 函数原型

#include <poll.h> // 必需

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

3. 功能

  • 监视文件描述符集合poll 会检查 fds 数组中列出的 nfds 个文件描述符的状态。
  • 等待就绪: 调用 poll 的进程会阻塞(挂起),直到以下情况之一发生:
    1. fds 数组中的至少一个文件描述符变为“就绪”状态(根据 events 字段指定的条件)。
    2. 调用被信号中断(返回 -1,并设置 errno 为 EINTR)。
    3. 达到指定的超时时间 timeout(如果 timeout >= 0)。
  • 返回就绪数量: 当 poll 返回时,它会报告有多少个文件描述符已就绪。
  • 更新状态poll 会修改 fds 数组中每个元素的 revents 字段,以指示该文件描述符上实际发生的事件。

4. 参数

  • struct pollfd *fds: 这是一个指向 struct pollfd 类型数组的指针。这个数组包含了所有需要监视的文件描述符及其感兴趣的事件。
    struct pollfd 的定义如下:struct pollfd { int fd; // 要监视的文件描述符 short events; // 程序关心的事件 (输入) short revents; // 实际发生的事件 (输出) };
    • fd: 要监视的文件描述符。如果 fd 为负数,则忽略该数组元素。
    • events: 这是一个位掩码,指定了应用程序对这个 fd 感兴趣的事件。常用的值包括:
      • POLLIN: 数据可读(对于普通文件,通常总是可读的)。
      • POLLOUT: 数据可写(对于普通文件,通常总是可写的)。
      • POLLPRI: 高优先级数据可读(例如 TCP 带外数据)。
      • POLLERR: 发生错误(作为 revents 返回,不能在 events 中设置)。
      • POLLHUP: 挂起(例如对端套接字关闭)(作为 revents 返回)。
      • POLLNVAL: 文件描述符无效(作为 revents 返回)。
    • revents: 这个字段由 poll 调用填充,返回该 fd 上实际发生的事件。程序需要检查这个字段来确定 fd 是否就绪以及发生了什么事件。
  • nfds_t nfds: 这是 fds 数组中的元素个数,即要监视的文件描述符总数。
  • int timeout: 指定 poll 调用阻塞等待的超时时间(以毫秒为单位)。
    • timeout == -1poll 会无限期阻塞,直到至少一个文件描述符就绪或被信号中断。
    • timeout == 0poll 执行非阻塞检查,立即返回,报告当前有多少文件描述符已就绪。
    • timeout > 0poll 最多阻塞 timeout 毫秒。如果在超时前没有文件描述符就绪,则返回 0。

5. 返回值

  • 成功时:
    • 返回 就绪的文件描述符的数量(即 revents 非零的 fds 元素个数)。这个数字可以是 0(表示超时)。
  • 失败时:
    • 返回 -1,并设置全局变量 errno 来指示具体的错误原因(例如 EFAULT fds 指针无效,EINTR 调用被信号中断,EINVAL nfds 负数等)。
  • 超时:
    • 如果在 timeout 指定的时间内没有任何文件描述符就绪,返回 0。

6. 相似函数,或关联函数

  • select: 一个更老的 I/O 多路复用函数,功能与 poll 类似,但在处理大量文件描述符时效率较低,且文件描述符集合有大小限制 (FD_SETSIZE)。
  • epoll_wait / epoll_ctl / epoll_create: Linux 特有的、更高效的 I/O 多路复用机制,特别适合处理大量的并发连接。它使用一个内核事件表来管理监视的文件描述符,避免了 poll/select 每次调用都需要传递整个文件描述符集合的开销。
  • readwrite: 在 poll 返回某个文件描述符就绪后,通常会调用 read 或 write 来执行实际的 I/O 操作。

7. 示例代码

示例 1:监视标准输入和一个管道

这个例子演示如何使用 poll 同时监视标准输入(键盘)和一个管道的读端,看哪个先有数据可读。

#include <poll.h>     // poll, struct pollfd
#include <unistd.h>   // pipe, read, write, close, STDIN_FILENO
#include <stdio.h>    // perror, printf, fprintf
#include <stdlib.h>   // exit
#include <string.h>   // strlen

int main() {
    int pipefd[2];
    struct pollfd fds[2];
    int num_fds = 2;
    int timeout_ms = 5000; // 5 秒超时
    int ret;
    char buffer[100];
    ssize_t bytes_read;

    // 1. 创建管道
    if (pipe(pipefd) == -1) {
        perror("pipe");
        exit(EXIT_FAILURE);
    }

    // 2. 设置要监视的文件描述符数组
    // 监视标准输入 (stdin)
    fds[0].fd = STDIN_FILENO; // 通常是 0
    fds[0].events = POLLIN;   // 关心可读事件
    fds[0].revents = 0;       // 内核会填充

    // 监视管道的读端
    fds[1].fd = pipefd[0];
    fds[1].events = POLLIN;   // 关心可读事件
    fds[1].revents = 0;       // 内核会填充

    printf("Waiting up to %d ms for input from stdin or data in pipe...\n", timeout_ms);
    printf("Type something in the terminal, or run 'echo hello > /proc/%d/fd/%d' in another terminal.\n",
           getpid(), pipefd[1]); // 提示用户如何向管道写入

    // 3. 调用 poll 进行等待
    ret = poll(fds, num_fds, timeout_ms);

    // 4. 检查 poll 的返回值
    if (ret == -1) {
        perror("poll");
        close(pipefd[0]);
        close(pipefd[1]);
        exit(EXIT_FAILURE);
    } else if (ret == 0) {
        printf("Timeout occurred! No data within %d ms.\n", timeout_ms);
    } else {
        printf("%d file descriptor(s) became ready.\n", ret);

        // 5. 检查哪个文件描述符就绪了
        for (int i = 0; i < num_fds; ++i) {
            if (fds[i].revents != 0) {
                printf("fd %d (originally fd %d) is ready. revents = 0x%04x\n",
                       fds[i].fd, fds[i].fd, fds[i].revents);

                if (fds[i].revents & POLLIN) {
                    printf("  -> POLLIN event on fd %d\n", fds[i].fd);
                    if (fds[i].fd == STDIN_FILENO) {
                        printf("  -> Reading from standard input:\n");
                        bytes_read = read(STDIN_FILENO, buffer, sizeof(buffer) - 1);
                        if (bytes_read > 0) {
                            buffer[bytes_read] = '\0'; // 确保字符串结束
                            printf("  -> Read from stdin: %s", buffer); // buffer 可能已包含 \n
                        }
                    } else if (fds[i].fd == pipefd[0]) {
                        printf("  -> Reading from pipe:\n");
                        bytes_read = read(pipefd[0], buffer, sizeof(buffer) - 1);
                        if (bytes_read > 0) {
                            buffer[bytes_read] = '\0';
                            printf("  -> Read from pipe: %s", buffer);
                        }
                    }
                }
                // 可以检查其他 revents,如 POLLERR, POLLHUP 等
                if (fds[i].revents & (POLLERR | POLLHUP | POLLNVAL)) {
                     printf("  -> Error or hangup or invalid fd on fd %d\n", fds[i].fd);
                }
            }
        }
    }

    // 6. 清理资源
    close(pipefd[0]);
    close(pipefd[1]);

    return 0;
}

代码解释:

  1. 使用 pipe() 创建一个管道,得到读端 pipefd[0] 和写端 pipefd[1]
  2. 定义一个 struct pollfd 数组 fds,包含两个元素。
  3. 第一个元素监视 STDIN_FILENO(标准输入),关心 POLLIN 事件。
  4. 第二个元素监视管道的读端 pipefd[0],也关心 POLLIN 事件。
  5. 调用 poll(fds, 2, 5000),等待最多 5 秒钟。
  6. 检查 poll 的返回值:
    • -1:错误。
    • 0:超时。
    • 0:就绪的文件描述符数量。
  7. 如果有文件描述符就绪(ret > 0),遍历 fds 数组,检查每个元素的 revents 字段。
  8. 如果 revents 包含 POLLIN,则调用 read 从对应的文件描述符读取数据。
  9. 最后关闭管道的两端。

示例 2:简单的 TCP 服务器(非阻塞 accept 和客户端 socket)

这个例子演示如何在 TCP 服务器中使用 poll 来同时监听监听套接字(用于接受新连接)和已建立连接的客户端套接字(用于接收数据)。

#include <poll.h>      // poll, struct pollfd
#include <sys/socket.h> // socket, bind, listen, accept, recv, send
#include <netinet/in.h> // sockaddr_in
#include <arpa/inet.h>  // inet_addr, inet_ntoa (简化版,非线程安全)
#include <unistd.h>     // close, read, write
#include <stdio.h>      // perror, printf, fprintf
#include <stdlib.h>     // exit
#include <string.h>     // memset, strlen

#define PORT 8080
#define MAX_CLIENTS 10
#define BUFFER_SIZE 1024

int main() {
    int server_fd, new_socket;
    struct sockaddr_in address;
    int addrlen = sizeof(address);
    struct pollfd fds[MAX_CLIENTS + 1]; // +1 for the listening socket
    int nfds = 1; // Initially, only the listening socket
    int timeout_ms = -1; // Block indefinitely
    int activity;
    char buffer[BUFFER_SIZE] = {0};
    char *hello = "Hello from server";

    // 1. 创建服务器套接字
    if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }

    // 2. 配置服务器地址
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY; // 绑定到所有本地接口
    address.sin_port = htons(PORT);

    // 3. 绑定套接字
    if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
        perror("bind failed");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    // 4. 监听连接
    if (listen(server_fd, 3) < 0) { // backlog=3
        perror("listen");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    printf("Server listening on port %d\n", PORT);

    // 5. 设置 poll 监视的初始文件描述符:监听套接字
    fds[0].fd = server_fd;
    fds[0].events = POLLIN; // 关心可读事件 (有新连接)
    fds[0].revents = 0;

    // 初始化其他客户端槽位
    for(int i = 1; i < MAX_CLIENTS + 1; i++) {
        fds[i].fd = -1; // -1 表示槽位空闲
        fds[i].events = POLLIN;
        fds[i].revents = 0;
    }

    // 6. 主循环
    while(1) {
        // 7. 调用 poll 等待事件
        activity = poll(fds, nfds, timeout_ms);

        if (activity < 0) {
            perror("poll error");
            break; // 或 exit(EXIT_FAILURE);
        }

        if (activity == 0) {
             // 不应该发生,因为 timeout_ms = -1
             printf("poll timeout (unexpected)\n");
             continue;
        }

        // 8. 检查监听套接字 (fds[0]) 是否有活动
        if (fds[0].revents & POLLIN) {
            // 有新的客户端连接请求
            if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {
                perror("accept");
                continue; // 继续处理其他事件
            }

            printf("New connection, socket fd is %d, ip is : %s, port : %d\n",
                   new_socket, inet_ntoa(address.sin_addr), ntohs(address.sin_port));

            // 将新连接的套接字添加到 poll 监视集合中
            int i;
            for (i = 1; i < MAX_CLIENTS + 1; i++) {
                if (fds[i].fd == -1) {
                    fds[i].fd = new_socket;
                    fds[i].events = POLLIN;
                    fds[i].revents = 0;
                    if (i >= nfds) nfds = i + 1; // 更新监视的 fd 数量
                    // 发送欢迎信息
                    send(new_socket, hello, strlen(hello), 0);
                    printf("Welcome message sent\n");
                    break;
                }
            }
            if (i == MAX_CLIENTS + 1) {
                printf("Too many clients, connection rejected\n");
                close(new_socket);
            }
        }

        // 9. 检查已连接的客户端套接字是否有活动
        for (int i = 1; i < nfds; i++) {
            if (fds[i].fd != -1 && (fds[i].revents & POLLIN)) {
                // 有数据从客户端发来
                int sd = fds[i].fd;
                ssize_t valread = read(sd, buffer, BUFFER_SIZE - 1);
                if (valread == 0) {
                    // 客户端断开连接
                    getpeername(sd, (struct sockaddr*)&address, (socklen_t*)&addrlen);
                    printf("Host disconnected, ip %s, port %d\n",
                           inet_ntoa(address.sin_addr), ntohs(address.sin_port));
                    close(sd);
                    fds[i].fd = -1; // 标记槽位为空闲
                } else {
                    // 处理收到的数据
                    buffer[valread] = '\0';
                    printf("Received message from socket %d: %s", sd, buffer);
                    // Echo 回去
                    send(sd, buffer, strlen(buffer), 0);
                }
            }
            // 可以检查 POLLERR, POLLHUP 等错误事件
            if (fds[i].fd != -1 && (fds[i].revents & (POLLERR | POLLHUP))) {
                 printf("Error or hangup on client socket %d\n", fds[i].fd);
                 close(fds[i].fd);
                 fds[i].fd = -1;
            }
        }
    }

    // 10. 清理 (在真实应用中,需要更优雅的退出机制)
    for(int i = 0; i < nfds; i++) {
        if(fds[i].fd != -1) {
            close(fds[i].fd);
        }
    }
    close(server_fd);
    printf("Server closed.\n");
    return 0;
}

代码解释:

  1. 创建、绑定、监听 TCP 套接字。
  2. 初始化 struct pollfd 数组 fds。第一个元素 (fds[0]) 用于监视监听套接字 server_fd,关心 POLLIN 事件(表示有新的连接请求)。
  3. 数组的其余元素(fds[1] 到 fds[MAX_CLIENTS])用于监视已建立连接的客户端套接字。初始时,它们的 fd 被设置为 -1,表示空闲槽位。
  4. 进入主循环,调用 poll(fds, nfds, -1)nfds 跟踪当前需要监视的 fds 数组元素个数(通常是已使用的槽位数)。
  5. poll 返回后,检查返回值 activity
  6. 如果 fds[0].revents & POLLIN 为真,说明监听套接字就绪,调用 accept 接受新连接。
  7. 将新获得的客户端套接字 new_socket 放入 fds 数组的一个空闲槽位中(fd 为 -1 的位置),并更新 nfds
  8. 遍历 fds 数组中用于客户端的槽位(从索引 1 开始),检查 revents
  9. 如果某个客户端套接字的 revents & POLLIN 为真,说明该客户端有数据可读,调用 read 读取数据。
  10. 如果 read 返回 0,表示客户端关闭了连接,关闭该套接字,并将 fds 中对应的 fd 设置回 -1。
  11. 如果 read 返回正数,表示读到了数据,这里简单地将其 echo 回客户端。
  12. 同样检查 POLLERR 和 POLLHUP 等错误事件。

这个例子展示了 poll 如何在一个单线程服务器中高效地管理多个并发连接。与为每个连接创建一个线程或进程相比,poll(以及更高效的 epoll)是构建高性能网络服务器的基础技术之一。

理解 poll 的关键是掌握 struct pollfd 数组的使用、events 和 revents 的含义,以及如何根据返回的就绪文件描述符数量和状态来处理相应的 I/O 操作。

poll系统调用详解, poll函数使用示例, linux poll系统调用, poll函数原理与应用, linux系统编程poll函数, poll函数如何工作, poll系统调用教程, poll函数在Linux中的应用, poll系统调用实例解析, poll函数编程指南

此条目发表在linux文章分类目录。将固定链接加入收藏夹。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注