IO多路复用及其机制(2) poll()

前文: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_setpollfd
类型位图(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_SETFD_CLR直接操作结构体,代码简洁易懂
文件描述符上限固定,通常为 1024(FD_SETSIZE无固定上限,取决于内存和数组大小
事件类型可读、可写、异常事件(有限)更多事件类型:如 POLLINPOLLOUTPOLLERR
性能遍历整个 fd_set,性能较低性能较好,适用于大规模文件描述符的场景
灵活性与扩展性FD_SETSIZE 限制,不适合高并发场景支持动态扩展,适合高并发场景
适用场景少量文件描述符和简单事件的场景高并发、大量文件描述符的场景

8、总结

Poll是一种用于多路复用I/O操作的系统调用,允许程序同时监控多个文件描述符的状态,以非阻塞方式处理多个I/O事件。其历史意义在于,它为解决传统I/O操作中的阻塞问题提供了一个基础方法,尤其在网络编程和高并发应用中得到了广泛应用。然而,Poll的主要缺陷在于它需要每次遍历所有文件描述符,这导致在大量连接的情况下性能严重下降。此外,Poll无法自动通知状态变化,程序仍需不断地轮询,造成不必要的资源浪费。因此,Poll的效率和可扩展性在大规模并发场景中受到限制。为了克服这些缺点,后续的epoll应运而生,提供了更加高效和灵活的事件驱动机制,成为了在高并发环境下对Poll的有力补充和改进。

9、资料参考

https://github.com/0voice

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值