《Linux 核心 IO 模型深析(中篇):探索Cmake与多路转接的高效实现poll》

**前引:**IO 是 Linux 系统性能的核心瓶颈之一,所有 IO 操作本质上都离不开 "等待" 与 "拷贝" 两个关键步骤。在五种经典 IO 模型中,非阻塞 IO 以 "轮询" 打破传统阻塞限制,多路转接 IO 凭 "多文件描述符监听" 实现高效等待,二者凭借独特的工作逻辑,成为高并发、低延迟场景的核心选择。本文将深入剖析两种模型的底层原理、工作流程、优劣势差异,以及实际开发中的落地要点,帮助开发者真正理解其设计思想并灵活运用!

目录

【一】Cmake替代make

(1)先安装:需要sudo权限

(2)准备一个空目录

(3)三步上篮

(4)效果

【二】poll接口介绍

(1)函数原型

(2)参数说明

(1)第一个参数

(2)第二个参数

(3)第三个参数

(3)返回值

(4)特点

(5)函数使用

(1)建立关心数组

(2)初始化关心数组

(3)设置poll

(4)任务处理

(5)添加新链接

(6)读取数据

(6)效果演示

(7)完整代码


【一】Cmake替代make

理解:Cmake中输入目标和源文件,可以自己调用make生成,更加简化,主流

使用方法:

(1)先安装:需要sudo权限
cpp 复制代码
# Ubuntu/Debian
sudo apt update && sudo apt install cmake -y

# 验证安装(显示版本即成功)
cmake --version
(2)准备一个空目录

因为Cmake会产生一堆副文件,避免污染重要目录的源码,比如我创建了一个名为 build 目录

(3)三步上篮

在需要生成的源码同目录下创建CMakeLists.txt文件,添加下面的代码内容:

第一行直接复制,第二行和第三行按照对应情况修改即可

cpp 复制代码
# 最低CMake版本要求(根据自己安装的版本调整,比如3.10)
cmake_minimum_required(VERSION 3.10)

# 项目名(随便取,比如myapp)
project(myapp)

# 生成可执行文件:可执行文件名为myapp,编译的源文件是main.c
add_executable(myapp main.c)
(4)效果

例如:用cmake 指令调用 CMakeLists.txt文件会直接生成 makefile 文件,再手动 make 指令

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

【二】poll接口介绍

(1)函数原型
cpp 复制代码
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
(2)参数说明
(1)第一个参数

参数描述:指向结构体 pollfd 类型的指针(管理多个文件描述符可以是 struct polled 类型的数组)

cpp 复制代码
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 无效(如未打开)
(2)第二个参数

参数描述:数组中有效结构体的数量(必须大于0),可理解为监听的文件描述符个数

(3)第三个参数

参数描述:超时时间(单位:毫秒)(>0 最大返回时间 =0 非阻塞立刻返回 =-1 阻塞使用)

(3)返回值
  • 成功:返回就绪的文件描述符总数(可能为 0,即超时)
  • 失败:返回 -1,且设置 errno(如 EINTR 表示被信号中断,可重试)
(4)特点

(1)只要有事件就绪就会一直通知你,这和select通知特点一样

(2)监听个数由用户决定,受限于系统资源,而select受限于自身数组

(3)输入和输出分离,而select输入输出是合并的

(4)每次调用不需要重新设置监听集合

(5)函数使用
(1)建立关心数组
cpp 复制代码
#define max_num_size 10

struct pollfd fds[max_num_size];
(2)初始化关心数组
cpp 复制代码
    void Initialize_struct()
    {
        for(int i=0;i<max_num_size;i++)
        {
            fds[i].fd=-1;
        }
    }
(3)设置poll

先将listen套接字添加到关心数组,再根据poll的返回值判断是否需要处理新链接

cpp 复制代码
    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;
            }
            case -1:
            {
                // log_message(LOG_LEVEL_ERROR,__FILE__,__LINE__,"错误码:%d,错误信息:%s",errno,strerror(errno));
                break;
            }
            default:
            {
                // 处理新链接
                Handle();
            }
            }
        }
    }
(4)任务处理

遍历关心数组:

如果是listen套接字 并且 该套接字准备就绪,就将accept返回的文件描述符重新添加到关心数组

如果不是listen套接字 并且 该套接字又准备就绪,说明是读端(只关心了读),可以直接recv

cpp 复制代码
    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);
            }
        }
    }
(5)添加新链接

找到下标为 -1 的空余结构体位置,将accept返回的进行添加

cpp 复制代码
    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<<"满了.....hhhhhhhh"<<std::endl;
            close(fd);
        }
        //添加
        fds[i].fd=fd;
        fds[i].events=POLLIN;
    }
(6)读取数据

读取对应文件描述符即可

cpp 复制代码
    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;
        }
    }
(6)效果演示

首先我们创建Cmake文件:

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

运行效果:

(7)完整代码

注意:以下类中,Accept()为服务器 accept 函数

cpp 复制代码
// 辅助数组大小
#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<<"满了.....hhhhhhhh"<<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;
            }
            case -1:
            {
                // log_message(LOG_LEVEL_ERROR,__FILE__,__LINE__,"错误码:%d,错误信息:%s",errno,strerror(errno));
                break;
            }
            default:
            {
                // 处理新链接
                Handle();
            }
            }
        }
    }

private:
    Server _V;
    struct pollfd fds[max_num_size];
};
相关推荐
DBA小马哥17 小时前
MongoDB迁移全解析:国产多模融合下的平滑替代实践
数据库·mongodb·dba
进阶小白猿17 小时前
Java技术八股学习Day14
java·数据库·学习
EveryPossible17 小时前
cpu展示示例
服务器
韦东东17 小时前
行业资讯日报自动化:从采集到 LLM 生成的全链路拆解(以政务网站为例)
运维·人工智能·自动化·大模型·llm·政务·行业资讯
jnrjian17 小时前
Oracle username 集成 AD
数据库·oracle
tianyuanwo17 小时前
TERM变量迷思:从Jenkins节点连接差异看终端仿真与构建系统的微妙关系
运维·ssh·jenkins·java web·term
一勺菠萝丶17 小时前
Jenkins 打包显示 SUCCESS 但产物不全?日志出现 Killed 的排查与解决(小白版)
运维·jenkins
Java 码农17 小时前
RabbitMQ集群部署方案及配置指南01
linux·服务器·rabbitmq
施嘉伟17 小时前
Oracle重建控制文件技术总结
数据库·oracle