做微商童装网站,网页制作培训机构好不好,楚雄市住房和城乡建设局门户网站,郑州网站建设 新浪博客目录
I/O多路复用的介绍
多进程/多线程模型的弊端
网络多路复用如何解决问题#xff1f;
网络多路复用的常见实现方式
常见的开源网络库
select详细介绍
select函数介绍
套接字可读事件,可写事件,异常事件
fd_set类型介绍
select的两次拷贝#xff0c;两次遍历
se…目录
I/O多路复用的介绍
多进程/多线程模型的弊端
网络多路复用如何解决问题
网络多路复用的常见实现方式
常见的开源网络库
select详细介绍
select函数介绍
套接字可读事件,可写事件,异常事件
fd_set类型介绍
select的两次拷贝两次遍历
select使用示例介绍
select服务器示例代码
poll函数详细介绍
poll函数介绍
pollfd类型介绍
poll的工作原理
poll的优缺点 I/O多路复用的介绍
多进程/多线程模型的弊端
在上一篇文章中我们详细介绍了Linux中的网络编程使用相关API实现了多进程/多线程模型即
Linux网络编程-CSDN博客
之前的客户端—服务器端连接处理思路每当有一个新的客户端连接服务器就创建一个新的进程或线程来处理它。我们之前的示例中在新创建的进程中还会使用fork来进行进一步创建一个进程用来实现读写分离。
这样就相当于每一个客户端连接服务器就需要多创建两个进程来实现客户端与服务器端的通信。
弊端 资源消耗大每个进程或线程都需要独立的内存空间栈、堆等并维护自己的上下文信息。大量的进程/线程会迅速耗尽系统内存。 上下文切换开销操作系统在这么多进程/线程之间切换 CPU 时会产生大量的上下文切换开销这会严重降低 CPU 的有效工作时间。 文件描述符限制每个进程/线程都会占用一个文件描述符。系统对单个进程或整个系统的文件描述符数量有上限容易达到瓶颈。 网络多路复用如何解决问题 网络多路复用允许单个进程/线程同时监控多个文件描述符包括套接字。当任何一个文件描述符准备好进行 I/O 操作例如有数据可读或可以写入数据时多路复用机制会通知应用程序。
这样你的服务器就不需要为每个客户端都创建一个独立的进程或线程了。一个工作进程/线程就能高效地管理数百甚至上万个并发连接。 网络多路复用的常见实现方式
网络多路复用的常见实现方式主要有三种select、poll 和 epoll (Linux 特有)。它们都允许一个进程或线程同时监控多个文件描述符包括网络套接字但具体机制和性能特点有所不同。 常见的开源网络库
在实际开发中我们经常会选择使用成熟的开源网络库或框架来构建高性能的并发服务器而并不是会选择使用selectpollepoll这些来进行构建。 在 C 中有很多优秀的开源网络库可以帮助你高效地开发网络应用程序。这些库封装了底层操作系统的网络 API如 Linux 上的 epollmacOS 上的 kqueueWindows 上的 IOCP提供了更高级、更易用的接口并且通常具备高性能、跨平台和丰富的功能。
下面介绍几个 C 中常用的开源网络库 Boost.Asio Boost.Asio 是一个功能强大、设计精良的 C 异步 I/O 库是现代 C 网络编程的首选。 它提供了一套统一的接口来处理各种异步 I/O 操作包括网络套接字、定时器、串口等。其核心是 io_context (或 io_service)一个事件循环用于分发 I/O 事件。 主要特点 C 风格与 C 标准库和现代 C 特性如模板、协程高度融合代码更符合 C 习惯。 功能全面不仅处理网络通信还支持定时器、信号等多种 I/O。 跨平台底层自动适配不同操作系统的高性能 I/O 多路复用机制如 Linux 的 epoll。 灵活性高支持同步和异步编程模型以及多种并发模式。 libevent 和 libev libevent 和 libev 是轻量级、事件驱动的 C 语言网络库专注于高性能的事件通知。 libev 是 libevent 的一个更小、更快的替代品设计理念类似。 它们的核心是事件循环 (event loop)通过注册回调函数来处理文件描述符上的 I/O 事件、定时器事件和信号事件。 事件驱动基于事件循环当 I/O 事件发生时通过回调函数通知应用程序避免了阻塞。 轻量和高效库本身的代码量较小运行效率高资源占用低。 跨平台支持 epoll、kqueue、IOCP、poll、select 等多种 I/O 复用机制。 多种事件支持不仅支持网络 I/O 事件还支持定时器、信号、文件 I/O 等事件。 适用场景 适用于需要极致性能、资源受限或嵌入式环境下的网络应用开发如高性能代理服务器、聊天服务器、游戏服务器等。它们是构建自己的高性能网络框架的理想基石。 使用开源网络库的好处
简化开发提供了抽象层你不需要直接操作 epoll_create、epoll_ctl、epoll_wait 等底层函数。提高效率这些库通常由经验丰富的开发者优化过性能经过严格测试并解决了许多难以发现的 bug 和边界条件。跨平台支持许多流行的库支持跨平台底层会自动根据操作系统选择合适的 I/O 多路复用机制epoll、kqueue、IOCP 等。丰富的功能除了基本的 I/O 封装它们往往还集成了定时器、线程池、内存管理、日志、协议编解码等常用功能。 select详细介绍
select函数介绍
select通过轮询的方式检查一组文件描述符的状态判断它们是否准备好进行 I/O 操作。
函数原型
#include sys/select.h
int select(int nfds, fd_set *restrict readfds, fd_set *restrict writefds,fd_set *restrict exceptfds, struct timeval *restrict timeout
);参数介绍
nfds所有文件描述符集合中最大的文件描述符值加 1。select 内部会从 0 到 nfds-1 遍历这些文件描述符。
readfds指向一个 fd_set 结构体的指针用于监听可读事件的文件描述符集合。如果不需要监听可读事件可以设置为 NULL。
writefds指向一个 fd_set 结构体的指针用于监听可写事件的文件描述符集合。如果不需要监听可写事件可以设置为 NULL。
exceptfds指向一个 fd_set 结构体的指针用于监听异常事件的文件描述符集合。如果不需要监听异常事件可以设置为 NULL。
timeout指向一个 struct timeval 结构体的指针用于设置 select 的超时时间。
如果为 NULLselect 将一直阻塞直到有文件描述符就绪。如果指向一个 struct timeval 结构体且其成员 tv_sec 和 tv_usec 都为 0select 将立即返回不阻塞非阻塞轮询。如果指向一个 struct timeval 结构体且其成员 tv_sec 或 tv_usec 大于 0select 将阻塞直到超时时间到达或有文件描述符就绪。 返回值
成功时返回就绪的文件描述符的数量。超时时返回 0。失败时返回 -1并设置 errno。 select返回值表示设置了多少位这个位数是可读事件可写事件异常事件的总和 select 的返回值是 所有就绪的文件描述符的总数无论是可读、可写还是异常事件就绪。它不会区分这些事件的类型只是告诉你“有这么多 FD 就绪了”。 例如如果 sock1 可读sock2 可写sock3 有异常那么 select 将返回 3。 套接字可读事件,可写事件,异常事件
上面我们说到了select可以用来监控socket套接字集合的可读事件可写事件异常事件。
那么这三种事件究竟是什么呢套接字在什么情况下会产生这些事件呢
我们必须要搞清楚这三种事件理解 select 何时会认为一个socket套接字的文件描述符是“可读”或“可写”是正确使用它的必要前提。 select中的可读事件(检测可读事件最常见) 监听套接字 (Listening Socket) 如果监听套接字上发生了新的连接请求即有客户端尝试连接服务器它就会变得可读。此时你可以调用 accept() 来接受新的连接。 连接套接字 (Connected Socket) 接收缓冲区中有数据可读。此时你可以调用 read() 或 recv() 来读取数据并且这些操作通常会立即返回而不会阻塞除非缓冲区的数据量小于你请求读取的量在阻塞模式下仍可能阻塞但通常会与非阻塞模式结合使用。 连接被对端关闭发送了 FIN 包。此时read() 会返回 0表示连接已正常关闭。 连接发生错误导致数据不再可读。 select中的可写事件 发送缓冲区有空间 套接字的发送缓冲区send buffer有足够的空间可以容纳你要发送的数据。此时你可以调用 write() 或 send() 来写入数据并且这些操作通常会立即返回而不会阻塞。 connect() 完成 对于非阻塞的 connect() 调用当连接建立成功或失败时套接字会变得可写或在 exceptfds 中报告错误。你需要通过 getsockopt() 结合 SO_ERROR 选项来获取连接的结果。 select 中的异常事件
exceptfds 用于监听“异常事件”。在实际的网络编程中最常见几乎是唯一的异常事件是
TCP 带外数据 (Out-of-Band Data, OOB)
当 TCP 套接字接收到带外数据时它会触发一个异常事件。带外数据是一种特殊的、优先级更高的数据流它可以绕过正常的 TCP 缓冲区用于传输紧急信息例如发送“紧急”信号来中断远程操作。
接收带外数据需要使用 recv() 并指定 MSG_OOB 标志。 fd_set类型介绍
fd_set 是一个位图bitmap用来表示一组文件描述符file descriptor简称 FD。每个位bit对应一个文件描述符如果对应的位被设置为 1就表示这个文件描述符在这个集合中。 fd_set 集合的操作宏
为了方便用户程序操作 fd_set 集合标准库提供了一组宏。这些宏实际上是对底层位图操作的封装
FD_ZERO(fd_set *set)将 fd_set 集合中所有的位清零即清空集合。FD_SET(int fd, fd_set *set)将文件描述符 fd 加入到 fd_set 集合中。FD_CLR(int fd, fd_set *set)将文件描述符 fd 从 fd_set 集合中移除。FD_ISSET(int fd, fd_set *set)检查文件描述符 fd 是否在 fd_set 集合中即是否就绪。 fd_set的限制
fd_set 最主要的限制是它能够容纳的文件描述符数量。这个上限由系统宏 FD_SETSIZE 定义在大多数 Linux 系统上其默认值通常是 1024。这意味着一个 fd_set 实例最多只能同时监听 1024 个文件描述符。
这个限制对于处理高并发连接的服务器来说是一个严重的瓶颈。当需要处理超过 1024 个客户端连接时select 就不再适用需要考虑使用 poll 或 epoll 等其他 I/O 多路复用机制。 fd_set 的内存与性能开销 内存开销 fd_set 的大小是固定的通常是 FD_SETSIZE / 8 字节。例如如果 FD_SETSIZE 是 1024那么 fd_set 大约占用 128 字节 (1024 / 8 128)。这部分内存开销通常不大。 性能开销与 select 相关 用户空间到内核空间的拷贝 每次调用 select都需要将完整的 fd_set 集合从用户空间复制到内核空间。当 FD_SETSIZE 较大时即使实际活跃的 FD 很少也需要复制整个 fd_set这会带来不必要的开销。 内核遍历 内核需要遍历 fd_set 中所有的 FD_SETSIZE 个位以检查哪些 FD 已经就绪。这个过程是 O(N) 的其中 N 是 FD_SETSIZE 的值或 nfds 的值。 内核空间到用户空间的拷贝select 返回时内核需要将包含就绪文件描述符的 fd_set 集合经过内核修改后的从内核空间复制回用户空间。这同样是一次完整的 fd_set 结构体的拷贝带来了额外的开销。 用户空间遍历 select 返回后用户程序也需要遍历整个 fd_set 来找出是哪个 FD 就绪这也是一个 O(N) 的操作。 select的两次拷贝两次遍历
从用户程序调用一次 select 系统调用通常涉及到两次数据拷贝和两次遍历操作。我们来详细分解一下 1. 第一次拷贝用户空间到内核空间
当你调用 select(nfds, readfds, writefds, exceptfds, timeout) 时 拷贝内容 readfds、writefds 和 exceptfds 这三个 fd_set 结构体以及 timeout 结构体的完整内容会从用户空间复制到内核空间注意这里是整个位图都会被拷贝过去并不是根据nfds来选择部分进行拷贝。 原因 内核需要知道你对哪些文件描述符的哪些事件感兴趣以便进行监控。 2. 第一次遍历内核空间遍历
在内核空间 遍历过程 内核会从 0 到 nfds-1 遍历每一个文件描述符。对于每个文件描述符它会检查其是否在你传入的 readfds、writefds 或 exceptfds 的副本中被设置了位。 检查状态 如果被设置了位内核就会去检查这个文件描述符的实际状态例如网络缓冲区是否有数据或者发送缓冲区是否有空间。 结果记录 如果文件描述符就绪内核会在其内部的一个临时就绪 fd_set 集合中标记对应的位。 3. 第二次拷贝内核空间到用户空间
当 select 返回时有就绪 FD、超时或出错 拷贝内容 内核会将其内部维护的、只包含就绪文件描述符的临时就绪 fd_set 集合复制回用户空间覆盖掉你传入的 readfds、writefds 和 exceptfds。 原因 这是 select 返回就绪信息给用户程序的方式。 4. 第二次遍历用户空间遍历
select 返回后在用户空间 遍历过程 用户程序需要再次从 0 到 nfds-1 (或你实际感兴趣的 FD 范围) 遍历 readfds、writefds 和 exceptfds 这三个被修改过的 fd_set 集合。 检查状态 使用 FD_ISSET(fd, set) 宏来逐个检查是哪些文件描述符就绪了。 执行操作 根据 FD_ISSET 的结果对就绪的文件描述符执行相应的 I/O 操作read(), write(), accept() 等。 大家应该会好奇一个问题为什么不能让程序将用户空间中的套接字位图fd_set的地址传递给内核空间呢这样不是可以避免拷贝吗为什么要拷贝一份数据过去内核设置好了之后再将设置好的数据拷贝回用户空间 主要原因有如下两点 1.内存保护角度隔离用户空间和内核空间 这是更重要的隔离。内核拥有最高的权限负责管理所有硬件资源和系统核心功能。如果用户程序能直接通过一个指针访问内核内存或者内核能随意访问用户内存那么 安全性风险 恶意用户程序可以修改内核数据结构从而获得特权甚至破坏整个系统。稳定性风险 用户程序的错误比如空指针解引用、越界访问可能会直接导致内核崩溃从而引发整个系统宕机。一致性问题 如果内核直接操作用户数据而用户程序同时也在修改这些数据会带来复杂的数据同步和一致性问题。 2.虚拟内存差异角度 现代操作系统都采用虚拟内存技术。 虚拟地址 vs. 物理地址 用户程序中使用的地址都是虚拟地址。这些虚拟地址需要通过内存管理单元MMU映射到实际的物理地址。每个进程都有自己的页表负责将本进程的虚拟地址映射到物理地址。 不同的地址空间 内核运行在它自己的虚拟地址空间中用户进程运行在它们各自的虚拟地址空间中。即使一个用户进程传递给内核一个它自己虚拟地址空间中的指针对于内核来说这个指针指向的虚拟地址是无效的因为它不属于内核自己的地址空间。内核需要一套机制来“翻译”或“安全地访问”这些用户空间的地址。 内核访问用户空间数据的正确方式 既然不能直接操作那内核如何安全地访问用户空间数据呢答案是通过特定的安全机制和系统调用。 拷贝Copy_From_User / Copy_To_User 这是最常见且最安全的方式。当用户程序调用 select 或 poll 这样的系统调用并传递数据如 fd_set 或 pollfd 数组时内核会使用专门的函数例如 Linux 内核中的 copy_from_user() 和 copy_to_user()来 验证地址 首先内核会验证用户提供的地址是否合法是否在用户进程的有效虚拟地址范围内以及是否有足够的权限访问。 安全拷贝 验证通过后内核会将用户空间的数据完整地拷贝到内核空间的一块临时缓冲区中进行操作。操作完成后再将结果拷贝回用户空间。 这种拷贝虽然有性能开销但它确保了内核不会因为用户空间的错误而崩溃也避免了用户程序的恶意篡改。 内存映射Memory Mapping 对于一些需要高性能、大量数据传输的场景例如文件I/O、共享内存操作系统提供了内存映射机制如 mmap()。这允许用户空间和内核空间或多个用户进程共享同一块物理内存区域。但即使是 mmap也需要通过系统调用来建立映射关系并且内核会设置适当的权限和保护确保安全。这种方式并非直接的指针传递而是建立了一种受控的共享访问机制。 select使用示例介绍
举个例子假设你正在编写一个服务器程序需要同时监听客户端连接请求通过监听套接字 listen_sock以及已经建立的客户端连接上的数据通过连接套接字 client_sock1, client_sock2 等。
这个示例将创建一个简单的服务器为了实现简单这个示例中select只检测了socket套接字的可读事件集合 (read_fds) 在这种情况下你需要
创建一个 fd_set read_fds; 在每次循环开始时调用 FD_ZERO(read_fds); 将 listen_sock 和所有活动的 client_sock 使用 FD_SET 添加到 read_fds 中。 调用 select(max_fd 1, read_fds, NULL, NULL, timeout); select 返回后首先检查 FD_ISSET(listen_sock, read_fds)。如果是说明有新的连接请求可以调用 accept()。 然后遍历所有 client_sock检查 FD_ISSET(client_sock_i, read_fds)。如果是说明这个客户端有数据可读可以调用 read()。 如果某个客户端连接关闭了就使用 FD_CLR 将其从 fd_set 中移除。 fd_set 是 select I/O 多路复用机制的核心数据结构它以位图的形式高效地管理文件描述符集合。尽管它使用简单且具有良好的跨平台性但其固定的 FD_SETSIZE 限制和线性扫描的效率问题使其在高并发场景下表现不佳。理解 fd_set 的工作原理对于掌握 select 的使用至关重要 select服务器示例代码
#include stdio.h // For printf, perror
#include stdlib.h // For exit, EXIT_FAILURE
#include string.h // For memset, strlen
#include unistd.h // For close, read, write
#include arpa/inet.h // For sockaddr_in, inet_ntop
#include sys/socket.h // For socket, bind, listen, accept
#include sys/select.h // For select, FD_ZERO, FD_SET, FD_CLR, FD_ISSET
#include errno.h // For errno, EWOULDBLOCK#define PORT 8080 // 服务器监听端口
#define MAX_CLIENTS 5 // 最大支持的客户端连接数
#define BUFFER_SIZE 1024 // 数据缓冲区大小int main() {int listen_fd; // 监听套接字文件描述符int client_fds[MAX_CLIENTS]; // 存储已连接客户端的套接字文件描述符int max_fd; // select 监听的最大文件描述符 1int i; // 循环变量fd_set read_fds; // select 用来监听可读事件的文件描述符集合// 初始化客户端文件描述符数组设为 -1 表示空闲for (i 0; i MAX_CLIENTS; i) {client_fds[i] -1;}// --- 1. 创建监听套接字 ---// AF_INET: IPv4协议族// SOCK_STREAM: TCP流式套接字// 0: 默认协议 (TCP)if ((listen_fd socket(AF_INET, SOCK_STREAM, 0)) -1) {perror(socket error);exit(EXIT_FAILURE);}printf(Listening socket created: %d\n, listen_fd);// 设置套接字选项允许地址重用防止 TIME_WAIT 状态导致端口不能立即重用int opt 1;if (setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, opt, sizeof(opt)) -1) {perror(setsockopt error);close(listen_fd);exit(EXIT_FAILURE);}// --- 2. 绑定地址和端口 ---struct sockaddr_in server_addr;memset(server_addr, 0, sizeof(server_addr)); // 清零server_addr.sin_family AF_INET; // IPv4server_addr.sin_addr.s_addr INADDR_ANY; // 监听所有可用网络接口server_addr.sin_port htons(PORT); // 端口号htons 将主机字节序转为网络字节序if (bind(listen_fd, (struct sockaddr *)server_addr, sizeof(server_addr)) -1) {perror(bind error);close(listen_fd);exit(EXIT_FAILURE);}printf(Socket bound to port %d\n, PORT);// --- 3. 开启监听 ---// 10: 允许的最大等待连接队列长度if (listen(listen_fd, 10) -1) {perror(listen error);close(listen_fd);exit(EXIT_FAILURE);}printf(Server listening on port %d...\n, PORT);// --- 4. select 循环处理事件 ---while (1) {FD_ZERO(read_fds); // 每次循环前清空文件描述符集合FD_SET(listen_fd, read_fds); // 将监听套接字加入可读集合 (因为它可能接收新连接)// 确定当前需要监听的最大文件描述符 1max_fd listen_fd;for (i 0; i MAX_CLIENTS; i) {if (client_fds[i] ! -1) {FD_SET(client_fds[i], read_fds); // 将每个活跃的客户端套接字加入可读集合if (client_fds[i] max_fd) {max_fd client_fds[i];}}}// 调用 select 进行 I/O 多路复用阻塞等待事件// 第一个参数是所有要监听的 FD 中的最大值加 1// 后三个参数分别代表监听可读、可写、异常事件的 FD 集合// 最后一个参数是超时时间NULL 表示永远阻塞直到有事件发生printf(\nWaiting for events (max_fd %d)...\n, max_fd);int activity select(max_fd 1, read_fds, NULL, NULL, NULL);if ((activity 0) (errno ! EINTR)) { // 检查 select 返回值perror(select error);break; // 出现错误则退出循环}// --- 5. 处理就绪事件 ---// (1) 检查监听套接字是否可读表示有新的客户端连接请求if (FD_ISSET(listen_fd, read_fds)) {struct sockaddr_in client_addr;socklen_t client_addr_len sizeof(client_addr);// 接受新连接int new_socket accept(listen_fd, (struct sockaddr *)client_addr, client_addr_len);if (new_socket -1) {perror(accept error);continue; // 继续下一轮循环}char client_ip[INET_ADDRSTRLEN];inet_ntop(AF_INET, client_addr.sin_addr, client_ip, INET_ADDRSTRLEN);printf(New connection accepted. Socket FD: %d, IP: %s, Port: %d\n,new_socket, client_ip, ntohs(client_addr.sin_port));// 将新连接的套接字加入到 client_fds 数组中int found_slot 0;for (i 0; i MAX_CLIENTS; i) {if (client_fds[i] -1) { // 找到一个空闲位置client_fds[i] new_socket;found_slot 1;printf(Adding client socket %d to array slot %d\n, new_socket, i);break;}}if (!found_slot) {printf(Max clients reached. Rejecting new connection %d\n, new_socket);close(new_socket); // 如果没有空闲位置关闭新连接}activity--; // 减少一个已处理的活动事件}// (2) 检查已连接客户端套接字是否可读表示有数据到来或连接关闭for (i 0; i MAX_CLIENTS; i) {int client_fd client_fds[i];if (client_fd ! -1 FD_ISSET(client_fd, read_fds)) {char buffer[BUFFER_SIZE];memset(buffer, 0, BUFFER_SIZE); // 清空缓冲区// 从客户端读取数据ssize_t bytes_read read(client_fd, buffer, BUFFER_SIZE - 1);if (bytes_read 0) {// 对端关闭了连接printf(Client %d disconnected.\n, client_fd);close(client_fd); // 关闭套接字client_fds[i] -1; // 将数组中的位置标记为空闲} else if (bytes_read -1) {// 读取错误perror(read error);close(client_fd);client_fds[i] -1;} else {// 成功读取到数据buffer[bytes_read] \0; // 确保字符串以 null 结尾printf(Received from client %d: %s\n, client_fd, buffer);// 可选将收到的数据回显给客户端if (write(client_fd, buffer, bytes_read) -1) {perror(write error);}}activity--; // 减少一个已处理的活动事件}// 如果所有活动事件都已处理可以提前退出循环if (activity 0) {break;}}}// --- 6. 清理资源 (通常不会到达这里除非发生严重错误) ---close(listen_fd);for (i 0; i MAX_CLIENTS; i) {if (client_fds[i] ! -1) {close(client_fds[i]);}}return 0;
} poll函数详细介绍
poll函数介绍
函数原型
#include poll.hint poll(struct pollfd *fds, nfds_t nfds, int timeout); 参数介绍
fds这是一个指向 struct pollfd 结构体数组的指针。每个 struct pollfd 结构体都代表一个我们希望监视的文件描述符及其感兴趣的事件。
nfds这是 fds 数组中元素的个数即我们要监视的文件描述符的总数。
timeout这是一个整数指定 poll 函数的等待时间毫秒。
大于0的整数 poll 将等待指定毫秒数如果在此期间没有事件发生poll 将返回0。0 poll 不会等待立即返回。它会检查当前文件描述符的状态并返回已经准备好的文件描述符的数量。-1 poll 将无限期等待直到有事件发生或被信号中断。 返回值
poll 函数的返回值表示就绪的文件描述符的数量即 revents 字段非零的 struct pollfd 结构体的数量。 大于0 表示有指定数量的文件描述符就绪。 0 表示在 timeout 期间没有文件描述符就绪。 -1 表示 poll 函数调用失败此时可以通过 errno 变量获取具体的错误信息。 pollfd类型介绍
struct pollfd {int fd; /* 文件描述符 */short events; /* 监视的事件 */short revents; /* 实际发生的事件 */
}; fd要监视的文件描述符。 events这是一个位掩码表示我们感兴趣的事件。可以是一个或多个事件的按位或组合。常用的事件标志包括 POLLIN文件描述符上有数据可读。 POLLOUT文件描述符上可以写入数据。 POLLERR文件描述符上发生错误。 POLLHUP对端挂断连接通常是EOF。 POLLNVAL无效的文件描述符请求。 revents这是一个位掩码由 poll 函数返回表示在文件描述符上实际发生的事件。它的取值与 events 类似可以包含上述事件标志。 poll的工作原理
当调用 poll 函数时内核会遍历 fds 数组中的每个 struct pollfd 结构体检查其对应的文件描述符上是否发生了 events 中指定的事件。
如果事件发生内核会在该 struct pollfd 的 revents 字段中设置相应的位并将其标记为就绪。如果没有事件发生并且 timeout 尚未到期poll 函数会进入睡眠状态直到事件发生或 timeout 到期。当 poll 返回时程序可以遍历 fds 数组检查每个 struct pollfd 的 revents 字段以确定哪些文件描述符已经就绪然后对这些文件描述符进行相应的I/O操作。 poll的优缺点
poll 的优点 没有文件描述符数量限制 解决了 select 的 FD_SETSIZE 限制问题可以监视任意数量的文件描述符只受限于系统内存。 更清晰的事件表示 struct pollfd 结构体使得事件的设置和检查更加直观。 更好的性能 尤其在文件描述符数量较多时poll 的性能优于 select。 可重用性 fds 数组可以在多次 poll 调用中重用而 select 的 fd_set 每次调用后都需要重新初始化。 只拷贝实际监视的 nfds 个 struct pollfd 结构体数据更紧凑。 poll 的缺点 仍然需要遍历 尽管 poll 没有文件描述符数量限制但在 poll 返回后仍然需要遍历整个 fds 数组来查找哪些文件描述符就绪当文件描述符数量非常庞大时这会成为一个性能瓶颈。