前文:IO多路复用及其机制(1) select()-优快云博客
poll(改进版的Select)
1、历史
poll() 是继 select() 之后引入的 I/O 多路复用机制,最早出现在 System V 系统中,并在 20 世纪 80 年代后期被广泛采用。
与 select() 相比,poll() 的设计更加灵活,取消了文件描述符数量的硬性限制,并提供了更直观的事件掩码机制。poll() 的目标是解决 select() 的一些固有缺陷,特别是在处理大量文件描述符时的性能问题。
2、相比于 select() 的优化
① 文件描述符数量无硬性限制:poll() 取消了 select() 的 1024 文件描述符限制,允许应用程序同时监控更多的文件描述符,更适合高并发场景。
② 更灵活的事件掩码:poll() 使用 pollfd 结构体,允许用户自定义关注的事件类型(如可读、可写、异常等),相比 select() 的位图机制更加直观和灵活。
③ 简化了文件描述符管理:poll() 不需要像 select() 那样在每次调用时重新设置文件描述符集合,减少了用户代码的复杂性和潜在的错误。
3、性能瓶颈
① 轮询机制:与 select() 类似,poll() 仍然采用轮询的方式检查每个文件描述符的状态。虽然它避免了 select() 的位图限制,但在高并发场景下,遍历大量文件描述符的开销仍然显著,导致性能瓶颈。
② 内存开销:poll() 使用 pollfd 结构体数组来管理文件描述符,随着文件描述符数量的增加,内存开销也会线性增长,进一步限制了其在高并发场景下的适用性。
4、意义
poll() 的出现是对 select() 的一次重要改进,它提供了更高的灵活性和扩展性,特别是在文件描述符数量和事件掩码方面。poll() 的引入为后续更高效的 I/O 多路复用机制(如 epoll)奠定了基础,推动了 I/O 多路复用技术的进一步发展。尽管 poll() 在高并发场景下仍然存在性能瓶颈,但它标志着 Unix/Linux 系统在 I/O 多路复用领域的一次重要进步。
5、poll()的组成
组成和select是相似的老三样。
函数:poll()
类型:pollfd
宏定义:POLLIN, POLLOUT, POLLERROR, POLLHUP, POLLNVAL
(1)poll()
poll() 是一种用于多路复用 I/O 的系统调用,主要用于检测多个文件描述符的事件。它的作用是等待一个或多个文件描述符上的事件发生,像是可读、可写、出错等,从而避免在程序中使用阻塞式的 I/O 操作。
下图是poll.h内的定义截图。
由上图可以看出,poll的参数数量和select相比明显减少(爽!)
三个参数简单来说就是pollfd结构体,fd的范围,阻塞时长。(最后两个和select一样)
应用一下会更直观。
struct pollfd fds[1024] = {0};
fds[sockfd].fd = sockfd;
fds[sockfd].events = POLLIN;
int maxfd = sockfd;
int nready = poll(fds, maxfd+1, -1);
timeout = -1跟select中的NULL是一个作用,使函数无限阻塞。
这里顺便说一下。poll() 的 timeout 参数允许你控制等待事件的时间。如果 timeout 设置为:
timeout > 0:程序会在指定的时间内等待事件。如果超时后仍没有事件发生,poll() 会返回 0,并且程序可以根据这个返回值来做其他处理。通过这种方式,程序不会一直等待某个事件发生,而是有机会继续执行其他任务。
timeout == 0:表示非阻塞模式,即立即返回。如果没有事件发生,poll() 会返回 0,程序可以根据返回值继续执行,而不会停留在等待状态。
timeout == -1:表示无限等待,直到至少有一个事件发生。这种方式会阻塞直到有感兴趣的事件,但它仍然是基于事件驱动的机制,并不是一直“盯着”一个文件描述符,而是会监控多个文件描述符。
P.S. 在使用 poll() 时,即使指定了 timeout == -1(无限等待),程序仍然不会因为一个文件描述符的 I/O 操作阻塞住其他文件描述符的操作。poll() 会并行监控多个文件描述符的状态,直到有事件发生,或者超时,从而避免了程序在一个文件描述符上等待时阻塞其他操作。
而maxfd+1表示监视的fd的最大范围,fds就是接下来要讲的pollfd。
至于返回值,和select类似,会返回就绪的fd数量。(另外如果返回0就是超时,-1是出错。)
(2)pollfd
pollfd 是一个结构体,每个结构体表示一个文件描述符以及它所关心的事件。
还是看头文件。
fd:需要监听的文件描述符。
events:应用程序关注的事件类型(如可读、可写、异常等),由用户设置。
revents:内核返回的实际发生的事件类型,由 poll() 函数填充。
对比:
特性 | fd_set | pollfd |
---|---|---|
类型 | 位图(bitset) | 结构体数组 |
表示内容 | 文件描述符集合,通过二进制位表示 | 每个结构体表示一个文件描述符及其事件状态 |
操作方式 | 通过宏(FD_SET , FD_CLR , FD_ISSET )操作 | 直接操作结构体字段(fd , events , revents ) |
文件描述符限制 | 固定上限,通常为 1024(FD_SETSIZE ) | 无固定上限,取决于内存和数组大小 |
事件类型 | 仅支持可读、可写、异常事件 | 支持更多事件类型(如 POLLIN , POLLOUT 等) |
灵活性 | 灵活性较差,受限于文件描述符数量和宏操作 | 更灵活,易于扩展,可以处理更多文件描述符和事件类型 |
性能 | 大量文件描述符时性能较差,需遍历整个集合 | 相对较好,适合大量文件描述符的场景 |
(3)宏定义
POLLIN:数据可读。
POLLOUT:数据可写。
POLLERR:发生错误。
POLLHUP:连接挂起。
POLLNVAL:文件描述符未打开。
6、案例
实现了一个简单的 TCP 服务器,使用 poll() 函数来监听多个客户端连接,并处理客户端的请求。
#include <sys/select.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <poll.h>
int main(){
// 创建 socket
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
// 设置服务器地址
struct sockaddr_in servaddr;
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY); //0.0.0.0
servaddr.sin_port = htons(2000); //0-1023
// 绑定 socket
if (-1 == bind(sockfd, (struct sockaddr*)&servaddr, sizeof(struct sockaddr)))
{
printf("bind failed:%s\n", strerror(errno));
close(sockfd);
return 1;
}
// 监听连接
listen(sockfd, 10);
printf("listen finished\n");
// 初始化 pollfd 数组
struct pollfd fds[1024] = {0};
fds[sockfd].fd = sockfd; // 监听 socket
fds[sockfd].events = POLLIN;
int maxfd = sockfd; // 当前最大文件描述符
struct sockaddr_in clientaddr;
socklen_t len = sizeof(clientaddr);
while (1) {
int nready = poll(fds, maxfd+1, -1);
if (fds[sockfd].revents & POLLIN) {
int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
printf("accept finshed: %d\n", clientfd);
//FD_SET(clientfd, &rfds); //
fds[clientfd].fd = clientfd;
fds[clientfd].events = POLLIN;
if (clientfd > maxfd) maxfd = clientfd;
}
int i = 0;
for (i = sockfd+1; i <= maxfd;i ++) { // i fd
if (fds[i].revents & POLLIN) {
char buffer[1024] = {0};
int count = recv(i, buffer, 1024, 0);
if (count == 0) { // disconnect
printf("client disconnect: %d\n", i);
close(i);
fds[i].fd = -1;
fds[i].events = 0;
continue;
}
printf("RECV: %s\n", buffer);
count = send(i, buffer, count, 0);
printf("SEND: %d\n", count);
}
}
}
printf("listen finished\n");
close(sockfd);
getchar();
printf("exit\n");
return 0;
}
7、与select()的简要对比
特性 | select() | poll() |
---|---|---|
数据结构 | fd_set 位图(固定大小,最大 1024) | pollfd 结构体数组(动态大小,支持更多文件描述符) |
操作复杂度 | 使用宏操作(如 FD_SET ,FD_CLR ) | 直接操作结构体,代码简洁易懂 |
文件描述符上限 | 固定,通常为 1024(FD_SETSIZE ) | 无固定上限,取决于内存和数组大小 |
事件类型 | 可读、可写、异常事件(有限) | 更多事件类型:如 POLLIN 、POLLOUT 、POLLERR 等 |
性能 | 遍历整个 fd_set ,性能较低 | 性能较好,适用于大规模文件描述符的场景 |
灵活性与扩展性 | 受 FD_SETSIZE 限制,不适合高并发场景 | 支持动态扩展,适合高并发场景 |
适用场景 | 少量文件描述符和简单事件的场景 | 高并发、大量文件描述符的场景 |
8、总结
Poll是一种用于多路复用I/O操作的系统调用,允许程序同时监控多个文件描述符的状态,以非阻塞方式处理多个I/O事件。其历史意义在于,它为解决传统I/O操作中的阻塞问题提供了一个基础方法,尤其在网络编程和高并发应用中得到了广泛应用。然而,Poll的主要缺陷在于它需要每次遍历所有文件描述符,这导致在大量连接的情况下性能严重下降。此外,Poll无法自动通知状态变化,程序仍需不断地轮询,造成不必要的资源浪费。因此,Poll的效率和可扩展性在大规模并发场景中受到限制。为了克服这些缺点,后续的epoll
应运而生,提供了更加高效和灵活的事件驱动机制,成为了在高并发环境下对Poll的有力补充和改进。
9、资料参考
https://github.com/0voice