深入理解 `poll` 函数:详细解析与实际应用

文章目录

  • [深入理解 `poll` 函数:详细解析与实际应用](#深入理解 poll 函数:详细解析与实际应用)
    • [1. `poll` 函数简介](#1. poll 函数简介)
    • [2. `poll` 的底层机制与工作原理](#2. poll 的底层机制与工作原理)
      • [1. `poll` 与 `select` 的区别](#1. pollselect 的区别)
      • [2. 工作流程](#2. 工作流程)
    • [3. `poll` 使用场景](#3. poll 使用场景)
    • [4. `poll` 的典型代码示例](#4. poll 的典型代码示例)
    • [5. 常见问题解答](#5. 常见问题解答)
      • [5.1 `poll` 和 `select` 的区别是什么?](#5.1 pollselect 的区别是什么?)
      • [5.2 为什么使用 `pollfd` 数组时需要将未使用的套接字初始化为 `-1`?](#5.2 为什么使用 pollfd 数组时需要将未使用的套接字初始化为 -1?)
    • 总结

深入理解 poll 函数:详细解析与实际应用

在网络编程中,处理多个客户端并发连接是一项常见且重要的任务。传统上,使用 select 函数来实现 多路复用。然而,随着对高性能和可扩展性的要求越来越高,poll 函数逐渐成为更常用的选择。本篇博客将详细介绍 poll 函数,并结合你遇到的难点,逐一解释其使用方法、底层机制及常见应用场景。

1. poll 函数简介

poll 是一种多路输入输出复用机制,常用于同时监听多个文件描述符的状态变化。与 select 函数类似,poll 通过阻塞等待多个文件描述符中的一个或多个事件发生,减少了在多客户端情况下需要处理的阻塞和轮询问题。

poll 函数原型

c 复制代码
#include <poll.h>

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

参数解析:

fds:是一个指向 pollfd 结构体数组的指针。每个 pollfd 结构体表示一个需要监听的文件描述符。该结构体有以下几个字段:

fd:需要监听的文件描述符(通常是套接字)。
events:要监听的事件类型(如 POLLINPOLLERRPOLLOUT 等)。
revents:实际发生的事件,poll 调用返回后,revents 会被填充,指示哪些事件已经发生。

nfdsfds 数组的大小,即需要监听的文件描述符的个数。

timeout:设置等待事件发生的超时时间,单位为毫秒。常用值有:

-1:无限等待,直到至少有一个事件发生。
0:立即返回(非阻塞模式)。

大于 0 的整数:指定最大等待时间,超时后返回。

返回值:

> 0:表示有文件描述符发生了事件,返回值是发生事件的文件描述符数量。
0:表示超时,没有事件发生。
< 0:表示出错,errno 会设置为相应的错误代码。


2. poll 的底层机制与工作原理

select 类似,poll 也是通过轮询来检查多个文件描述符的状态。但是,poll 在一些方面比 select 更加高效,尤其在文件描述符数量较多时。

1. pollselect 的区别

fd_set 结构:select 使用 fd_set 结构来管理文件描述符集,最大文件描述符的数量是固定的(通常为 1024)。而 poll 使用 pollfd 结构,它没有文件描述符数量的硬性限制,适合更大规模的文件描述符集合。

性能:由于 poll 不需要手动设置和清理 fd_set 结构(不像 select 那样需要每次修改),它的使用更简便。poll 通过直接操作 pollfd 数组,并在轮询时返回具体哪个文件描述符发生了事件。

超时机制:poll 支持精确的超时管理,可以精确设置等待时间,而 select 中只能粗略设置超时。

2. 工作流程

poll 会执行以下几个步骤:

  1. 初始化 pollfd 数组:首先,设置一个 pollfd 数组,配置每个文件描述符的监听事件。
  2. 调用 poll() 阻塞等待事件:poll 会阻塞直到监听的文件描述符上有一个或多个事件发生,或者超时。
  3. 检查返回的事件:poll 返回后,检查每个文件描述符的 revents 字段,查看哪些文件描述符发生了预期的事件。

3. poll 使用场景

poll 适用于 高并发连接 的场景,尤其是以下几种情况:

  1. 监听多个客户端连接:poll 可以同时监听多个客户端套接字,避免在高并发情况下的阻塞问题。
  2. 网络服务器:在构建 高性能的网络服务器 时,poll 可以帮助处理大量并发连接,减少线程和进程的创建和销毁,提高系统的资源利用效率。
  3. I/O 多路复用:在需要监听多个输入输出流(如文件、网络套接字等)时,poll 提供了一个有效的解决方案。

4. poll 的典型代码示例

我们来看一个简单的 poll 示例,来展示如何使用它来处理多个客户端的连接。

c 复制代码
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <poll.h>

#define LISTEN_MAX 5
#define CLIENT_MAX 5

int main(void) {
    char buff[1024];
    int sock_fd, ret;

    // 创建套接字
    sock_fd = socket(PF_INET, SOCK_STREAM, 0);
    if (sock_fd == -1) {
        perror("socket failed");
        exit(1);
    }

    struct sockaddr_in server_address;
    server_address.sin_family = AF_INET;
    server_address.sin_addr.s_addr = htonl(INADDR_ANY);
    server_address.sin_port = htons(8888);
    
    if (bind(sock_fd, (struct sockaddr)&server_address, sizeof(server_address)) != 0) {
        perror("bind failed");
        exit(1);
    }

    listen(sock_fd, LISTEN_MAX);

    // 设置 pollfd
    struct pollfd fds[CLIENT_MAX + 1];
    fds[0].fd = sock_fd;
    fds[0].events = POLLIN;

    // 初始化客户端套接字
    for (int i = 1; i <= CLIENT_MAX; i++) {
        fds[i].fd = -1;  // 不监听,避免浪费资源
        fds[i].events = 0;
    }

    while (1) {
        ret = poll(fds, CLIENT_MAX + 1, -1);
        if (ret < 0) {
            perror("poll failed");
            exit(1);
        }

        // 处理 server 套接字
        if (fds[0].revents & POLLIN) {
            struct sockaddr_in client_address;
            socklen_t client_len = sizeof(client_address);
            int client_fd = accept(sock_fd, (struct sockaddr)&client_address, &client_len);
            if (client_fd < 0) {
                perror("accept failed");
                continue;
            }

            // 寻找空闲的槽位存储客户端套接字
            for (int i = 1; i <= CLIENT_MAX; i++) {
                if (fds[i].fd == -1) {
                    fds[i].fd = client_fd;
                    fds[i].events = POLLIN;
                    printf("New client connected: %d\n", client_fd);
                    break;
                }
            }
        }

        // 处理客户端请求
        for (int i = 1; i <= CLIENT_MAX; i++) {
            if (fds[i].fd != -1 && (fds[i].revents & POLLIN)) {
                int client_fd = fds[i].fd;
                ret = read(client_fd, buff, sizeof(buff));
                if (ret <= 0) {
                    close(client_fd);
                    fds[i].fd = -1;  // 清空客户端套接字
                    printf("Client %d disconnected\n", client_fd);
                } else {
                    printf("Received from client %d: %s\n", client_fd, buff);
                    write(client_fd, buff, ret);
                }
            }
        }
    }

    close(sock_fd);
    return 0;
}

代码说明:

  1. 套接字初始化:首先创建套接字,绑定到指定的 IP 和端口。

  2. pollfd 数组初始化:为每个客户端套接字分配一个 pollfd 结构,监听可读事件(POLLIN)。

  3. poll 调用:在 while 循环中,调用 poll() 来等待客户端连接或数据。

  4. 处理事件:

    服务端套接字:当服务端套接字可读时,接受新连接。

    客户端套接字:当客户端套接字可读时,读取数据并回传。


5. 常见问题解答

5.1 pollselect 的区别是什么?

pollselect 都用于多路复用,但有以下几个区别:

select 的限制:select 使用一个最大文件描述符数(通常为 1024),无法处理更大的文件描述符集合。而 poll 没有这个限制,可以处理更多的文件描述符。
select 的性能问题:select 在文件描述符较多时,性能较差,因为每次调用时都需要检查所有的文件描述符。
poll 的优越性:poll 在大文件描述符集合中表现得更高效,因为它只需要处理传入的文件描述符数组。

5.2 为什么使用 pollfd 数组时需要将未使用的套接字初始化为 -1

pollfd 数组中的每个元素都代表一个文件描述符。通过将未使用的套接字设置为 -1,可以确保 poll 只监听有效的文件描述符。这样做可以节省资源并避免监听无效的套接字。


总结

poll 是一种多路复用技术,适用于监听多个套接字事件。与 select 相比,poll 没有文件描述符数量的限制,且更高效。
poll 使用 pollfd 数组 来管理文件描述符,每个套接字的状态(如可读、错误等)都会存储在该结构中。
poll 适用于多客户端服务器,可以高效地处理多个客户端的并发请求。

相关推荐
倔强的石头1062 小时前
Linux 进程深度解析(二):进程状态、fork 创建与特殊进程(僵尸 与 孤儿)
linux·运维·服务器
小李小李无与伦比2 小时前
使用Simiki,部署.md文档
linux·运维·服务器
做人不要太理性2 小时前
【Linux系统】ELF 文件格式的硬核揭秘
java·linux·服务器
仰泳的熊猫3 小时前
1140 Look-and-say Sequence
数据结构·c++·算法·pat考试
Hard but lovely3 小时前
C/C++ ---条件编译#ifdef
c语言·开发语言·c++
草根站起来3 小时前
局域网内网IP能不能申请SSL证书
服务器·tcp/ip·ssl
闻缺陷则喜何志丹3 小时前
【计算几何】P12144 [蓝桥杯 2025 省 A] 地雷阵|普及+
c++·数学·蓝桥杯·计算几何
怀旧,3 小时前
【Linux系统编程】12. 基础IO(下)
linux·运维·服务器
EXtreme353 小时前
栈与队列的“跨界”对话:如何用双队列完美模拟栈的LIFO特性?
c语言·数据结构·leetcode·双队列模拟栈·算法思维