Linux IO 多路复用 :poll 用法实战笔记
·
前言
在传统单线程 TCP 服务端中,程序一次只能处理一个客户端连接,新连接或其他客户端请求会被阻塞,无法实现并发。 为了解决这个问题,Linux 提供了 IO 多路复用 技术:用单个线程同时监听多个文件描述符,哪个描述符有事件就绪,就处理哪个,以此实现多客户端并发通信。
本文主要学习 poll 多路复用,结合完整 TCP 服务端代码,从原理、代码实现、运行测试、优缺点一步步记录学习过程。
一、poll 基础概念
1. 作用
poll 是 Linux 下 IO 多路复用函数,统一监听多个文件描述符(套接字、普通文件等)的读写、异常事件。 当任意一个被监听的 fd 触发事件,poll 就会返回,程序再针对性处理。
2. 主要结构体 struct pollfd
poll 通过数组管理待监听的文件描述符,核心结构体定义:
struct pollfd {
int fd; // 待监听的文件描述符
short events; // 想要监听的事件(由用户设置)
short revents; // 实际触发的事件(由内核回填)
};
fd:文件描述符,赋值为-1表示当前位置空闲,不监听;events:我们主动设置要监听的事件,常用POLLIN(读事件就绪);revents:poll返回后,内核标记该 fd 实际发生了什么事件,只读不可手动修改。
3. 常用事件宏
POLLIN:读事件就绪,代表套接字有数据可读 / 有新客户端连接到来;POLLOUT:写事件就绪;POLLERR/POLLHUP:错误、连接断开事件。
4. poll 函数原型
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
参数说明:
fds:pollfd结构体数组,存放所有要监听的 fd;nfds:数组元素个数;timeout:超时时间,单位 毫秒>0:阻塞等待指定毫秒,超时无事件则返回 0;=0:非阻塞,立即返回;<0:永久阻塞,直到有事件触发。
返回值:
- 大于 0:就绪的文件描述符个数;
- 等于 0:等待超时,无事件;
- 小于 0:函数调用出错。
二、整体需求
编写基于 poll 的 TCP 并发服务端,实现功能:
- 监听本机
127.0.0.1:6000端口; - 支持多个客户端同时连接、并发收发数据;
- 客户端发送消息,服务端接收并回复
ok; - 客户端断开连接时,自动清理套接字、移出监听队列;
- 5 秒无事件则打印超时提示。
三、完整代码实现
服务端代码 poll_server.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <poll.h>
// 最大可监听的文件描述符数量
#define MAXFD 10
// 初始化 TCP 监听套接字
int socket_init();
// 初始化 pollfd 数组
void fds_init(struct pollfd fds[]);
// 向 poll 数组添加待监听 fd
void fds_add(struct pollfd fds[], int fd);
// 从 poll 数组移除 fd
void fds_del(struct pollfd fds[],int fd);
// 处理新客户端连接
void accept_client(struct pollfd fds[], int sockfd);
// 接收客户端数据并回复
void recv_data(struct pollfd fds[], int c);
void fds_init(struct pollfd fds[])
{
for(int i = 0; i < MAXFD; i++)
{
fds[i].fd = -1; // -1 代表位置空闲
fds[i].events = 0;
fds[i].revents = 0;
}
}
void fds_add(struct pollfd fds[], int fd)
{
for(int i = 0; i < MAXFD; i++)
{
if( fds[i].fd == -1)
{
fds[i].fd = fd;
fds[i].events = POLLIN; // 监听读事件
fds[i].revents = 0;
break;
}
}
}
void fds_del(struct pollfd fds[],int fd)
{
for(int i = 0; i < MAXFD; i++)
{
if( fds[i].fd == fd)
{
fds[i].fd = -1;
fds[i].revents = 0;
fds[i].events = 0;
break;
}
}
}
void accept_client(struct pollfd fds[], int sockfd)
{
int c = accept(sockfd,NULL,NULL);
if( c < 0)
{
return ;
}
printf("accept c=%d\n",c);
fds_add(fds,c); // 将客户端 fd 加入监听
}
void recv_data(struct pollfd fds[], int c)
{
char buff[128] = {0};
int n = recv(c,buff,127,0);
if( n <= 0 )
{
// 客户端断开/异常,回收资源
fds_del(fds,c);
close(c);
printf("client(%d) close\n",c);
return;
}
printf("recv(%d):%s\n",c,buff);
send(c,"ok\n",3,0); // 增加换行,优化终端排版
}
int main()
{
// 1. 创建 TCP 监听套接字
int sockfd = socket_init();
if( sockfd == -1)
{
exit(1);
}
// 2. 初始化 poll 数组
struct pollfd fds[MAXFD];
fds_init(fds);
fds_add(fds,sockfd); // 将监听套接字加入监听队列
// 3. 事件循环
while( 1 )
{
// 阻塞监听,超时 5000 毫秒(5秒)
int n = poll(fds,MAXFD,5000);
if( n == -1)
{
printf("poll err\n");
}
else if ( n == 0 )
{
printf("time out\n");
}
else
{
// 遍历所有 fd,处理就绪事件
for(int i = 0; i < MAXFD; i++ )
{
if( fds[i].fd == -1 )
{
continue;
}
// 判断是否是读事件就绪
if( fds[i].revents & POLLIN)
{
if( fds[i].fd == sockfd )
{
// 监听套接字就绪:有新客户端连接
accept_client(fds,sockfd);
}
else
{
// 客户端套接字就绪:有数据可读
recv_data(fds,fds[i].fd);
}
}
}
}
}
return 0;
}
// TCP 服务端初始化:socket -> bind -> listen
int socket_init()
{
int sockfd = socket(AF_INET,SOCK_STREAM,0);
if( sockfd == -1)
{
return -1;
}
struct sockaddr_in saddr;
memset(&saddr,0,sizeof(saddr));
saddr.sin_family = AF_INET;
saddr.sin_port = htons(6000);
saddr.sin_addr.s_addr = inet_addr("127.0.0.1");
int res = bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));
if( res == -1)
{
printf("bind err\n");
return -1;
}
res = listen(sockfd,5);
if( res == -1)
{
return -1;
}
return sockfd;
}
客户端代码 client.c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
int main() {
int sock = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(6000);
addr.sin_addr.s_addr = inet_addr("127.0.0.1");
connect(sock, (struct sockaddr*)&addr, sizeof(addr));
char buf[100];
while (1) {
printf("输入消息: ");
fgets(buf, 100, stdin);
send(sock, buf, strlen(buf), 0);
memset(buf, 0, 100);
recv(sock, buf, 99, 0);
printf("服务器返回: %s", buf);
}
close(sock);
return 0;
}
四、代码模块拆解讲解
1. 套接字初始化 socket_init
标准 TCP 服务端四步流程:
socket():创建 TCP 流式套接字;- 填充
sockaddr_in:配置 IPv4、端口 6000、本地回环地址127.0.0.1; bind():绑定 IP 和端口;listen():开启监听,等待客户端连接。
注意:
htons()作用是主机字节序转网络字节序,网络通信必须统一字节序。
2. poll 数组管理函数
fds_init:批量初始化数组,所有fd置为-1,标记为空闲;fds_add:遍历数组,找到空闲位置,添加新 fd 并设置监听POLLIN;fds_del:根据 fd 找到对应位置,重置为空闲,停止监听。
3. 业务处理函数
accept_client:监听 fd 触发读事件 → 调用accept获取客户端 fd → 加入 poll 监听;recv_data:客户端 fd 触发读事件 → 读取数据;n <= 0:客户端断开,移除监听、关闭套接字;- 正常读取:打印数据,回复
ok。
4. 主循环(核心逻辑)
- 初始化 poll 数组,把监听套接字先加入监听队列;
- 死循环调用
poll阻塞等待事件,超时 5 秒; - 根据
poll返回值分情况处理:- 返回 0:超时,打印提示;
- 返回 >0:有事件就绪,遍历数组区分「新连接」和「客户端数据」;
- 返回 -1:调用出错。
五、编译与运行测试
1. 编译代码
# 编译服务端
gcc poll_server.c -o poll_server
# 编译客户端
gcc client.c -o client
2. 启动程序
终端 1 启动服务端:
./poll_server
新开多个终端,启动客户端(模拟多并发):
./client
3. 运行现象
- 无客户端:服务端每 5 秒打印
time out; - 客户端连接:服务端打印
accept c=xxx(xxx 为客户端文件描述符); - 客户端发消息:服务端打印接收内容,客户端收到
ok回复; - 客户端断开:服务端打印
client(xxx) close,自动回收资源; - 多客户端:多个客户端可同时通信,互不影响,实现并发。
六、poll 优缺点总结
优点
- 接口简单易用,逻辑清晰,上手成本低;
- 跨平台支持,Linux、Windows 都可使用;
- 相比早期
select,解除了 fd 数量的硬编码上限;
缺点
- 每次事件触发后,必须遍历整个 poll 数组,连接数越多,遍历开销越大;
- 高并发场景下性能一般,海量连接时效率不如
epoll; - 依然存在数组遍历的性能损耗。
七、拓展:poll 与 epoll 简单对比
| 技术 | 平台 | 遍历方式 | 适用场景 |
|---|---|---|---|
| poll | 跨平台 | 全数组遍历 | 中小规模并发、跨平台程序 |
| epoll | 仅Linux | 只遍历就绪fd | 高并发服务端(主流生产方案) |
八、学习总结
poll是 IO 多路复用的经典实现,核心依靠pollfd数组管理监听对象;- 利用单个线程监听多个 fd,是实现 TCP 并发的基础方案;
- 完整流程:创建监听套接字 → 初始化 poll 数组 → 循环调用 poll 等待事件 → 分类处理连接与数据;
- poll 适合入门学习 IO 多路复用,理解原理后,可进一步学习 Linux 高性能方案
epoll。
更多推荐




所有评论(0)