C语言中通过 poll
实现IO多路复用
在C语言中,IO多路复用是一种常用的技术,用于提高程序的并发能力,尤其在处理多个文件描述符(如网络连接、设备等)时。poll
是Linux系统提供的一个用于IO多路复用的系统调用,它能够监听多个文件描述符的状态,帮助程序处理多个输入输出事件。
一、什么是IO多路复用?
IO多路复用允许单个进程或线程同时监听多个文件描述符上的事件(如数据的可读、可写等),从而避免阻塞等待,提升程序的并发性能。传统的阻塞IO模型下,每个IO操作会阻塞当前线程,直到操作完成,效率较低。IO多路复用通过非阻塞IO和事件驱动机制,允许程序在一个线程中同时处理多个IO事件。
常见的IO多路复用技术有:
select
:最早的IO多路复用技术,支持多个文件描述符的监听。poll
:改进版的select
,可以处理更多的文件描述符。epoll
:Linux特有的IO多路复用机制,处理大规模文件描述符时效率更高。
二、poll
的工作原理
poll
系统调用允许程序同时监控多个文件描述符的状态,类似于 select
,但它比 select
更加高效,特别是在文件描述符数量较多时。
poll
的基本工作原理是:通过一个结构体 pollfd
数组传递待监控的文件描述符,并设置需要监听的事件类型。poll
会阻塞直到某个事件发生,或在指定的超时时间内返回。
三、poll
的使用
poll
的原型如下:
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
fds
:指向一个pollfd
结构体数组,数组中每个元素对应一个待监控的文件描述符。nfds
:fds
数组的大小,表示待监控的文件描述符的个数。timeout
:等待的时间,单位为毫秒。timeout
为0
时,poll
立即返回,timeout
为负数时,表示无限期等待。
pollfd
结构体定义如下:
struct pollfd {
int fd; // 文件描述符
short events; // 监控的事件类型
short revents; // 返回发生的事件
};
events
:指定我们希望监听的事件类型(例如,POLLIN
表示文件描述符可读,POLLOUT
表示文件描述符可写等)。revents
:返回发生的事件类型。
四、常见事件类型
poll
支持以下几种事件类型:
POLLIN
:表示文件描述符可读,即数据可以被读取。POLLOUT
:表示文件描述符可写,即可以向其写入数据。POLLERR
:表示文件描述符发生错误。POLLHUP
:表示文件描述符被挂起。
五、通过 poll
实现多路复用
下面通过一个具体的示例来说明如何使用 poll
来实现IO多路复用。
假设我们有两个文件描述符,一个用于标准输入(stdin
),另一个用于网络套接字。我们希望同时监听标准输入和套接字是否可读。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <poll.h>
#include <string.h>
#include <fcntl.h>
#define TIMEOUT 5000 // 5秒超时
int main() {
// 创建pollfd数组
struct pollfd fds[2];
// 设置标准输入(fd=0)用于监听输入事件
fds[0].fd = STDIN_FILENO; // 标准输入
fds[0].events = POLLIN; // 监听可读事件
// 创建一个模拟的套接字文件描述符(这里使用标准输出,实际应用中可以是套接字文件描述符)
fds[1].fd = STDOUT_FILENO; // 假设为网络套接字
fds[1].events = POLLIN; // 监听可读事件
int ret = poll(fds, 2, TIMEOUT); // 监听两个文件描述符,超时5秒
if (ret == -1) {
perror("poll");
exit(1);
} else if (ret == 0) {
printf("Timeout occurred! No events happened.\n");
} else {
if (fds[0].revents & POLLIN) {
char buf[128];
ssize_t bytes_read = read(STDIN_FILENO, buf, sizeof(buf) - 1);
if (bytes_read > 0) {
buf[bytes_read] = '\0';
printf("Data from stdin: %s\n", buf);
}
}
if (fds[1].revents & POLLIN) {
printf("Data is ready to be read from socket (simulated output).\n");
}
}
return 0;
}
六、代码解析
fds[0]
和fds[1]
初始化:首先,我们为标准输入(stdin
)和模拟的套接字(这里使用stdout
)创建了两个pollfd
结构体。fds[0].fd = STDIN_FILENO
:将标准输入的文件描述符设置为监听的对象。fds[0].events = POLLIN
:监听标准输入是否有数据可读。
- 调用
poll
:我们使用poll(fds, 2, TIMEOUT)
来监听这两个文件描述符的事件,2
表示我们要监听两个文件描述符,TIMEOUT
是等待的时间,单位是毫秒。 处理返回事件:
poll
会阻塞,直到某个事件发生,或者超时返回。- 如果
fds[0].revents & POLLIN
为真,说明标准输入上有可读数据,程序会从标准输入读取数据。 - 如果
fds[1].revents & POLLIN
为真,说明模拟的套接字有可读数据,程序会处理该事件。
- 如果
七、poll
的优缺点
优点 | 缺点 |
---|---|
相比 select 支持更多的文件描述符 | poll 仍然需要扫描整个文件描述符集合,效率较低 |
支持文件描述符的动态增加 | 不适用于大规模的文件描述符集合 |
简单易用,API清晰 | 当有大量文件描述符时,性能会降低 |
八、总结
通过 poll
实现IO多路复用能够有效提高程序的并发性能,尤其适用于监听多个文件描述符的可读、可写事件。虽然 poll
比 select
更高效,但在需要处理大量文件描述符时,epoll
会提供更好的性能。poll
的使用方法简单直观,但在性能方面有所限制。适用于中小规模的IO多路复用任务。