文章目录
一、概念介绍
在操作系统中,I/O 多路复用是高性能网络服务的关键技术。它允许单个进程或线程同时监听多个文件描述符(FD),在其中任意一个就绪时立即响应,不必为每个连接分配独立的线程或阻塞等待。
在 Linux 中,常见的 I/O 多路复用机制包括:
- Select
- Poll
- Epoll
而本文将重点讲解第二种实现方式 —— Poll。
二、Poll 的核心特点
| 特性 | 描述 |
|---|---|
| 非阻塞 | 当某个文件描述符未就绪时不会阻塞整个进程 |
| 轮询机制 | 核心通过轮询检查所有文件描述符状态 |
| 事件驱动 | 当状态变化时才返回就绪结果 |
| 可扩展性 | 相较于 select,无文件描述符数量上限 |
三、Poll 的工作流程
四、工作流程详解
4.1 初始化阶段
- 创建 pollfd 数组
struct pollfd fds[MAX_CLIENT]; memset(fds, 0, sizeof(fds)); - 设置监听参数
fds[i].fd = socket_fd; // 要监听的文件描述符 fds[i].events = POLLIN | POLLOUT; // 监听读写事件 fds[i].revents = 0; // 由内核填充
4.2 监听阶段
调用内核的 poll() 系统调用:
int ret = poll(fds, nfds, timeout);
if (ret > 0) {
// 有fd就绪
}
🔍 内核处理逻辑:
- 检查每个文件描述符状态是否可读、可写或异常;
- 就绪的 FD 被标记在
revents字段; - 返回就绪 FD 的数量。
4.3 事件处理阶段
for (int i = 0; i < nfds; i++) {
if (fds[i].revents & POLLIN) {
handle_read(fds[i].fd);
}
if (fds[i].revents & POLLOUT) {
handle_write(fds[i].fd);
}
if (fds[i].revents & POLLERR) {
handle_error(fds[i].fd);
}
}
🔁 处理完毕后再次调用 poll() 进入事件循环。
五、思维导图:Poll机制核心模块
六、Poll 与 Select 的原理对比
6.1 系统调用接口差异
| 机制 | 系统调用 |
|---|---|
| Select | int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout); |
| Poll | int poll(struct pollfd *fds, nfds_t nfds, int timeout); |
6.2 核心数据结构
Select:
typedef struct {
long fds_bits[FD_SETSIZE / NFDBITS];
} fd_set;
每个 bit 表示一个文件描述符状态,固定大小,最大1024个 fd。
Poll:
struct pollfd {
int fd; // 文件描述符
short events; // 请求的事件
short revents; // 返回的事件
};
支持动态数量、扩展性更强。
6.3 内核实际处理流程对比
6.4 内核伪代码示例
Select机制:
int do_select(int nfds, fd_set *readfds) {
for (int fd = 0; fd < nfds; fd++) {
if (FD_ISSET(fd, readfds) && is_readable(fd))
FD_SET(fd, readfds);
else
FD_CLR(fd, readfds);
}
return ready_count;
}
Poll机制:
int do_poll(struct pollfd *fds, nfds_t nfds) {
for (int i = 0; i < nfds; i++) {
fds[i].revents = 0;
if (fds[i].events & POLLIN && is_readable(fds[i].fd))
fds[i].revents |= POLLIN;
if (fds[i].events & POLLOUT && is_writable(fds[i].fd))
fds[i].revents |= POLLOUT;
}
return ready_count;
}
七、性能对比分析
| 项目 | Select | Poll |
|---|---|---|
| 文件描述符上限 | 1024 | 无限制 |
| 数据结构 | 位图 | 数组 |
| 用户空间重新设置 | 每次必须重置 | 可复用 pollfd 数组 |
| 时间复杂度 | O(n) (遍历全部fd) | O(n) (遍历实际fd) |
| 典型场景 | 低并发、小网络应用 | 中高并发服务,例如代理服务器 |
poll 对 select 的核心优化是突破文件描述符数量限制、优化就绪事件存储结构,其用户态 - 内核态的内存拷贝同样为2 次。
八、时序模型:用户调用与内核事件交互
九、示例程序:基于 Poll 的多客户端服务
#include <stdio.h>
#include <poll.h>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#define MAX_CLIENT 100
int main() {
struct pollfd fds[MAX_CLIENT];
int nfds = 1;
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
// 初始化服务器socket
struct sockaddr_in addr = {0};
addr.sin_family = AF_INET;
addr.sin_port = htons(8080);
addr.sin_addr.s_addr = INADDR_ANY;
bind(server_fd, (struct sockaddr *)&addr, sizeof(addr));
listen(server_fd, 5);
fds[0].fd = server_fd;
fds[0].events = POLLIN;
while (1) {
int ret = poll(fds, nfds, 1000); // 1秒超时
if (ret < 0) perror("poll error");
for (int i = 0; i < nfds; i++) {
if (fds[i].revents & POLLIN) {
if (fds[i].fd == server_fd) {
int client_fd = accept(server_fd, NULL, NULL);
fds[nfds].fd = client_fd;
fds[nfds].events = POLLIN;
nfds++;
printf("新连接建立: %d\n", client_fd);
} else {
char buf[1024];
int n = read(fds[i].fd, buf, sizeof(buf));
if (n <= 0) {
close(fds[i].fd);
printf("客户端断开: %d\n", fds[i].fd);
fds[i] = fds[--nfds]; // 删除fd
} else {
printf("收到数据: %s\n", buf);
}
}
}
}
}
}
十、总结与扩展
| 特性维度 | Poll | Select |
|---|---|---|
| 易用性 | 简洁结构体配置 | 需要位图操作 |
| 扩展性 | 动态数组,无上限 | 固定大小(FD_SETSIZE) |
| 性能 | 中高并发表现更好 | 适合少量fd |
| 推荐场景 | Web服务器、代理服务、消息队列监听 | 轻量级工具或脚本 |
- Poll 是 I/O 多路复用的重要阶段性技术;
- 它解决了 select 的固定位图和限制;
- 对高并发网络服务而言,是向 epoll、IO_uring 过渡的关键基础。

6298

被折叠的 条评论
为什么被折叠?



