IO 多路复用之 poll 总结

在网络编程中,I/O 操作是非常常见且重要的。当需要处理多个 I/O 流时,如果采用传统的阻塞 I/O 方式,程序会在一个 I/O 操作上阻塞,无法同时处理其他 I/O 事件,这显然效率低下。为了解决这个问题,出现了 I/O 多路复用技术。poll 就是其中一种常用的 I/O 多路复用机制,它允许程序同时监视多个文件描述符的 I/O 事件,从而提高程序的并发处理能力。本文将详细介绍 poll 的工作原理、使用方法、优缺点等内容。

目录#

  1. poll 简介
  2. poll 函数的原型和参数
  3. poll 的工作原理
  4. poll 的使用示例
  5. poll 的优缺点
  6. 常见实践和最佳实践
  7. 总结
  8. 参考资料

poll 简介#

poll 是 Unix 系统下常用的 I/O 多路复用机制之一,它和 select 类似,但在某些方面进行了改进。poll 可以处理的文件描述符数量没有 select 的限制(select 一般受限于 FD_SETSIZE),并且使用更方便。poll 可以同时监视多个文件描述符上的读、写和异常事件,当这些事件发生时,poll 函数会返回,通知程序进行相应的处理。

poll 函数的原型和参数#

poll 函数的原型如下:

#include <poll.h>
 
int poll(struct pollfd *fds, nfds_t nfds, int timeout);

参数说明:#

  • fds:一个指向 struct pollfd 数组的指针,struct pollfd 结构体定义如下:
struct pollfd {
    int   fd;         /* 文件描述符 */
    short events;     /* 监视的事件 */
    short revents;    /* 发生的事件 */
};
- `fd`:需要监视的文件描述符。
- `events`:要监视的事件集合,可以是以下值的按位或组合:
    - `POLLIN`:有数据可读。
    - `POLLOUT`:可以进行写操作。
    - `POLLERR`:发生错误。
    - `POLLHUP`:发生挂起。
- `revents`:由内核填充,指示实际发生的事件。
  • nfdsfds 数组中元素的个数。

  • timeout:超时时间,以毫秒为单位。

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

返回值:#

  • 大于 0:表示有事件发生的文件描述符的数量。
  • 0:表示超时,没有事件发生。
  • -1:表示出错,错误信息存储在 errno 中。

poll 的工作原理#

poll 的工作原理可以简单概括为:程序将需要监视的文件描述符和对应的事件信息填充到 struct pollfd 数组中,然后调用 poll 函数。poll 函数会将这个数组传递给内核,内核会对这些文件描述符进行监视。当有文件描述符上的指定事件发生时,内核会将发生的事件信息填充到 struct pollfd 数组的 revents 字段中,并返回发生事件的文件描述符的数量。程序通过检查 revents 字段来确定具体发生了哪些事件,并进行相应的处理。

poll 的使用示例#

以下是一个简单的使用 poll 实现的 TCP 服务器示例:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <poll.h>
#include <unistd.h>
 
#define MAX_EVENTS 10
#define BUFFER_SIZE 1024
 
int main() {
    int listen_fd, client_fd;
    struct sockaddr_in server_addr, client_addr;
    socklen_t client_addr_len = sizeof(client_addr);
 
    // 创建监听套接字
    if ((listen_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
        perror("socket");
        return 1;
    }
 
    // 初始化服务器地址
    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(8888);
 
    // 绑定套接字
    if (bind(listen_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
        perror("bind");
        close(listen_fd);
        return 1;
    }
 
    // 监听连接
    if (listen(listen_fd, 5) == -1) {
        perror("listen");
        close(listen_fd);
        return 1;
    }
 
    struct pollfd fds[MAX_EVENTS];
    int nfds = 1;
 
    // 初始化 pollfd 数组
    fds[0].fd = listen_fd;
    fds[0].events = POLLIN;
    fds[0].revents = 0;
 
    while (1) {
        int ready = poll(fds, nfds, -1);
        if (ready == -1) {
            perror("poll");
            break;
        }
 
        for (int i = 0; i < nfds; i++) {
            if (fds[i].revents & POLLIN) {
                if (fds[i].fd == listen_fd) {
                    // 有新的连接
                    if ((client_fd = accept(listen_fd, (struct sockaddr *)&client_addr, &client_addr_len)) == -1) {
                        perror("accept");
                        continue;
                    }
                    // 将新的客户端套接字添加到 pollfd 数组中
                    if (nfds < MAX_EVENTS) {
                        fds[nfds].fd = client_fd;
                        fds[nfds].events = POLLIN;
                        fds[nfds].revents = 0;
                        nfds++;
                    } else {
                        close(client_fd);
                    }
                } else {
                    // 有数据可读
                    char buffer[BUFFER_SIZE];
                    ssize_t n = recv(fds[i].fd, buffer, sizeof(buffer), 0);
                    if (n <= 0) {
                        // 客户端关闭连接
                        close(fds[i].fd);
                        // 移除该文件描述符
                        for (int j = i; j < nfds - 1; j++) {
                            fds[j] = fds[j + 1];
                        }
                        nfds--;
                        i--;
                    } else {
                        // 处理数据
                        buffer[n] = '\0';
                        printf("Received: %s\n", buffer);
                        // 回显数据
                        send(fds[i].fd, buffer, n, 0);
                    }
                }
            }
        }
    }
 
    // 关闭监听套接字
    close(listen_fd);
    return 0;
}

代码说明:#

  1. 创建监听套接字并绑定地址,开始监听连接。
  2. 初始化 struct pollfd 数组,将监听套接字添加到数组中,并设置要监视的事件为 POLLIN
  3. 进入循环,调用 poll 函数等待事件发生。
  4. 当有事件发生时,遍历 struct pollfd 数组,检查 revents 字段。
    • 如果是监听套接字有事件发生,说明有新的连接,接受连接并将新的客户端套接字添加到 pollfd 数组中。
    • 如果是客户端套接字有事件发生,说明有数据可读,读取数据并进行处理。如果客户端关闭连接,关闭对应的套接字并从 pollfd 数组中移除。
  5. 最后关闭监听套接字。

poll 的优缺点#

优点#

  • 没有文件描述符数量限制poll 不像 select 那样受限于 FD_SETSIZE,可以处理更多的文件描述符。
  • 使用方便poll 使用 struct pollfd 数组来管理文件描述符和事件,比 select 的位操作更直观。

缺点#

  • 性能问题:和 select 一样,poll 每次调用都需要将 struct pollfd 数组从用户空间复制到内核空间,处理大量文件描述符时会有性能开销。
  • 内核循环遍历:内核需要遍历 struct pollfd 数组来检查哪些文件描述符有事件发生,时间复杂度为 O(n)O(n),当文件描述符数量很大时,性能会下降。

常见实践和最佳实践#

常见实践#

  • 合理设置超时时间:根据实际需求设置 poll 函数的超时时间,避免长时间阻塞。
  • 及时处理事件:当 poll 函数返回后,应及时处理发生的事件,避免事件积压。

最佳实践#

  • 结合其他技术:可以将 poll 与多线程或多进程结合使用,提高程序的并发处理能力。
  • 动态管理文件描述符:根据实际情况动态添加或移除 struct pollfd 数组中的元素,避免不必要的资源浪费。

总结#

poll 是一种常用的 I/O 多路复用机制,它在处理多个文件描述符的 I/O 事件时比传统的阻塞 I/O 方式更高效。poll 没有文件描述符数量的限制,使用也比较方便。但它也存在一些性能问题,特别是在处理大量文件描述符时。在实际应用中,我们可以根据具体情况选择合适的 I/O 多路复用机制,同时结合一些最佳实践来提高程序的性能和可靠性。

参考资料#

  1. 《Unix 网络编程》(第 3 版)
  2. Linux 系统手册:man poll
  3. The Linux Documentation Project - poll