
Linux 核心 IO 模型深析:CMake 构建与 Poll 多路转接实现
Linux IO 模型中的多路转接机制通过 poll 接口实现高效文件描述符监听。文章介绍了 poll 函数原型、参数及返回值,对比了其与 select 的差异。结合 CMake 构建工具,展示了如何在 C++ 项目中初始化 pollfd 数组、设置监听事件、处理就绪状态及读写数据。提供了完整的服务器端示例代码,涵盖新连接接受、数据接收及断开处理逻辑,适用于高并发场景下的网络编程开发。

Linux IO 模型中的多路转接机制通过 poll 接口实现高效文件描述符监听。文章介绍了 poll 函数原型、参数及返回值,对比了其与 select 的差异。结合 CMake 构建工具,展示了如何在 C++ 项目中初始化 pollfd 数组、设置监听事件、处理就绪状态及读写数据。提供了完整的服务器端示例代码,涵盖新连接接受、数据接收及断开处理逻辑,适用于高并发场景下的网络编程开发。



**引言:**IO 是 Linux 系统性能的核心瓶颈之一,所有 IO 操作本质上都离不开'等待'与'拷贝'两个关键步骤。在五种经典 IO 模型中,非阻塞 IO 以'轮询'打破传统阻塞限制,多路转接 IO 凭'多文件描述符监听'实现高效等待,二者凭借独特的工作逻辑,成为高并发、低延迟场景的核心选择。
理解:CMake 中输入目标和源文件,可以自己调用 make 生成,更加简化,主流。
使用方法:
# Ubuntu/Debian sudo apt update && sudo apt install cmake -y
# 验证安装(显示版本即成功)
cmake --version
因为 CMake 会产生一堆副文件,避免污染重要目录的源码,比如创建一个名为 build 的目录。
在需要生成的源码同目录下创建 CMakeLists.txt 文件,添加下面的代码内容:
第一行直接复制,第二行和第三行按照对应情况修改即可。
# 最低 CMake 版本要求(根据自己安装的版本调整,比如 3.10)
cmake_minimum_required(VERSION 3.10)
# 项目名(随便取,比如 myapp)
project(myapp)
# 生成可执行文件:可执行文件名为 myapp,编译的源文件是 main.c
add_executable(myapp main.c)
例如:用 cmake 指令调用 CMakeLists.txt 文件会直接生成 makefile 文件,再手动 make 指令。


**如果要生成多个可执行程序:**如果修改了源码,再重新 make /make clean 即可,和之前一样。

#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
参数描述:指向结构体 pollfd 类型的指针(管理多个文件描述符可以是 struct pollfd 类型的数组)。
struct pollfd {
int fd; // 要监听的文件描述符(-1 表示忽略此结构体)
short events; // 要监听的事件(输入参数,由程序设置)
short revents; // 实际发生的事件(输出参数,由内核填充)
};
监听事件选项:即 events 由你设置选项,revents 由 poll 调用之后由操作系统给你填写。
| 事件标识 | 含义(events 输入) | 含义(revents 输出) |
|---|---|---|
| POLLIN | 监听'可读'事件 | 该 fd 有数据可读 |
| POLLOUT | 监听'可写'事件 | 该 fd 可写入数据 |
| POLLERR | 无需主动设置 | 该 fd 发生错误 |
| POLLHUP | 无需主动设置 | 该 fd 对应的连接关闭(如 socket 断开) |
| POLLNVAL | 无需主动设置 | fd 无效(如未打开) |
参数描述:数组中有效结构体的数量(必须大于 0),可理解为监听的文件描述符个数。
参数描述:超时时间(单位:毫秒)。>0 最大返回时间;=0 非阻塞立刻返回;=-1 阻塞使用。
(1)只要有事件就绪就会一直通知你,这和 select 通知特点一样
(2)监听个数由用户决定,受限于系统资源,而 select 受限于自身数组
(3)输入和输出分离,而 select 输入输出是合并的
(4)每次调用不需要重新设置监听集合
#define max_num_size 10
struct pollfd fds[max_num_size];
void Initialize_struct() {
for(int i=0; i<max_num_size; i++) {
fds[i].fd = -1;
}
}
先将 listen 套接字添加到关心数组,再根据 poll 的返回值判断是否需要处理新链接。
void Deal() {
// 初始化结构体
Initialize_struct();
// 将 listen 套接字添加到关心结构体
fds[0].fd = _V.Fd();
fds[0].events = POLLIN;
for(;;) {
// 计算关心的个数
int nods = 0;
for(nods=0; nods<max_num_size; nods++) {
if(fds[nods].fd == -1) continue;
else nods++;
}
// 调用 poll
int po = poll(fds, nods, 1000);
// 判断事件
switch (po) {
case 0: {
std::cout << "等待超时" << std::endl;
break;
}
case -1: {
// log_message(LOG_LEVEL_ERROR,__FILE__,__LINE__,"错误码:%d,错误信息:%s",errno,strerror(errno));
break;
}
default: {
// 处理新链接
Handle();
break;
}
}
}
}
遍历关心数组: 如果是 listen 套接字 并且 该套接字准备就绪,就将 accept 返回的文件描述符重新添加到关心数组。 如果不是 listen 套接字 并且 该套接字又准备就绪,说明是读端(只关心了读),可以直接 recv。
void Handle() {
for(int i=0; i<max_num_size; i++) {
// 如果是 listen 套接字且 listen 套接字准备就绪
if(fds[i].fd == _V.Fd() && fds[i].revents & POLLIN) {
Accept();
} else if(fds[i].revents & POLLIN) { // 需要判断该文件描述符的读端是否就绪
Recv(i);
}
}
}
找到下标为 -1 的空余结构体位置,将 accept 返回的进行添加。
void Accept() {
// 获取 accept 文件描述符
int fd = _V.Accept();
// 添加到关心结构体
int i;
for(i=0; i<max_num_size; i++) {
if(fds[i].fd == -1) break;
}
if(i == max_num_size) {
std::cout << "连接数已满" << std::endl;
close(fd);
}
// 添加
fds[i].fd = fd;
fds[i].events = POLLIN;
}
读取对应文件描述符即可。
void Recv(int i) {
char buffer[1024] = {0};
ssize_t d = recv(fds[i].fd, buffer, sizeof(buffer) - 1, 0);
if (d > 0) {
buffer[d] = 0;
std::cout << "客户端发送了数据 : ";
std::cout << buffer << std::endl;
} else if (d == 0) {
// 对方断开了连接
close(fds[i].fd);
fds[i].fd = -1; // 关闭当前的文件描述符,并且从数组中删掉
} else {
// 读取错误
close(fds[i].fd);
fds[i].fd = -1;
}
}
首先我们创建 Cmake 文件:

避免垃圾信息干扰当前目录,我们新建一个目录,执行 cmake .. 指令,再执行 make,运行程序。

运行效果:

注意:以下类中,Accept() 为服务器 accept 函数。
// 辅助数组大小
#define max_num_size 10
class Media {
public:
void Install() {
_V.Socket(); // 绑定
_V.Bind(); // 发起连接
_V.Listen();
}
void Initialize_struct() {
for(int i=0; i<max_num_size; i++) {
fds[i].fd = -1;
}
}
void Accept() {
// 获取 accept 文件描述符
int fd = _V.Accept();
// 添加到关心结构体
int i;
for(i=0; i<max_num_size; i++) {
if(fds[i].fd == -1) break;
}
if(i == max_num_size) {
std::cout << "连接数已满" << std::endl;
close(fd);
}
// 添加
fds[i].fd = fd;
fds[i].events = POLLIN;
}
void Recv(int i) {
char buffer[1024] = {0};
ssize_t d = recv(fds[i].fd, buffer, sizeof(buffer) - 1, 0);
if (d > 0) {
buffer[d] = 0;
std::cout << "客户端发送了数据 : ";
std::cout << buffer << std::endl;
} else if (d == 0) {
// 对方断开了连接
close(fds[i].fd);
fds[i].fd = -1; // 关闭当前的文件描述符,并且从数组中删掉
} else {
// 读取错误
close(fds[i].fd);
fds[i].fd = -1;
}
}
void Handle() {
for(int i=0; i<max_num_size; i++) {
// 如果是 listen 套接字且 listen 套接字准备就绪
if(fds[i].fd == _V.Fd() && fds[i].revents & POLLIN) {
Accept();
} else if(fds[i].revents & POLLIN) { // 需要判断该文件描述符的读端是否就绪
Recv(i);
}
}
}
void Deal() {
// 初始化结构体
Initialize_struct();
// 将 listen 套接字添加到关心结构体
fds[0].fd = _V.Fd();
fds[0].events = POLLIN;
for(;;) {
// 计算关心的个数
int nods = 0;
for(nods=0; nods<max_num_size; nods++) {
if(fds[nods].fd == -1) continue;
else nods++;
}
// 调用 poll
int po = poll(fds, nods, 1000);
// 判断事件
switch (po) {
case 0: {
std::cout << "等待超时" << std::endl;
break;
}
case -1: {
// log_message(LOG_LEVEL_ERROR,__FILE__,__LINE__,"错误码:%d,错误信息:%s",errno,strerror(errno));
break;
}
default: {
// 处理新链接
Handle();
break;
}
}
}
}
private:
Server _V;
struct pollfd fds[max_num_size];
};

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online