【Linux通信篇】深入理解进程间通信——管道
---------------------------------------------------------------------------------------------------------------------------------
每日鸡汤:找一个对的人,然后好好去爱。一个你跟他在一起,然后又可以舒舒服服做自己的人。
---------------------------------------------------------------------------------------------------------------------------------
目录
一:进程间通信介绍
1.1、进程间通信是什么?
1.2、为什么要进程间通信?【目的】
1.3、如何实现进程间通信?【怎么办】
二:匿名管道
2.1 管道原理
2.2 管道接口使用
l2.3 管道编码实现
2.4 管道特征
2.5 管道的四种情况
2.6 管道的使用场景
2.6.1 命令行管道
2.6.2 基于管道的简易进程池
三:命名管道
3.1 mkfifo 函数接口
3.2 命名管道编码实现
四:结语
一:进程间通信介绍
1.1、进程间通信是什么?
进程间通信是两个或者多个进程实现数据层面的交互。因为进程独立性的存在,就导致了进程通信的成本比较高。
1.2、为什么要进程间通信?【目的】
一个进程向另一个进程发送基本数据、发送命令、一些协同、通知等等。就是要让不同的进程通信起来。
即目的:
- 数据传输:一个进程需要将它的数据发送给另一个进程。
- 资源共享:多个进程之间共享同样的资源。
- 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
- 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
1.3、如何实现进程间通信?【怎么办】
因为两个进程天然都是独立的。那么要让它们通信起来,该怎么办呢?首先进程间通信的本质是必须要让不同的进程看到同一份资源。所谓资源,就是特定形式的内存空间。为了保证进程的独立性,一般就是操作系统来提供这个资源。进程要访问这个空间,进行通信,本质就是访问操作系统。进程代表的是用户,但是操作系统并不相信用户,这就使得我们在写代码的过程中不能直接去访问操作系统提供的资源,而是通过系统调用接口去创建、释放这个资源。因为操作系统内部可能会存在多个进程之间需要通信,所以这些资源页一定是很多分的。故操作系统需要对这些资源管理起来——"先描述,再组织",一般的操作系统,会有一个独立的通信模块,它隶属于文件系统——IPC模块。进程间通信是有标准的(system V标准和 posix 标准),system V 标准主要针对的是本机内部通信,而 posix 标准主要针对的是网络通信,但是凡是发展都是有一个过程的,在这两个标准没有出来之前,进程间通信的方式都是通过基于文件级别的通信方式【管道通信】。
- 管道:匿名管道pipe、命名管道。
- System V IPC:System V 消息队列、System V 共享内存、System V 信号量。
- POSIX IPC:消息队列、共享内存、信号量、互斥量、条件变量、读写锁。
管道是 Unix 中最古老的进程间通信的方式。把一个进程连接到另一个进程的一个数据流称作管道。
二:匿名管道
2.1 管道原理

如图,父进程创建子进程来演示管道的原理。因为进程间通信的本质前提就是需要先将不同的进程看到同一份资源,而管道本质就是一个内存级别的文件。即父进程创建一个子进程,那么父子进程就能看到同一份文件了,父进程通过往缓冲区内写数据,之后子进程再读取缓冲区中的数据,即可实现进程之间的通信。
小Dis:一个文件内部含有 int cnt 引用计数这一变量,所以当两个进程指针指向同一份文件时,一个进程关闭,并不会影响另一个进程交互该文件。
这只是管道通信的大致情况,那么实际上的管道通信是怎样的呢?


首先,父进程使用读写方式分别打开同一个文件,如图,3号文件描述符以读的方式打开该文件,而4号文件描述符以写的方式打开该文件。之后父进程创建子进程,子进程继承了父进程一系列的文件资源。因为都是同一个文件,那么也就只有一个文件缓冲区,之后在该唯一的缓冲区内进行读写文件数据。父进程可以通过4号文件描述符向文件中进行写入,之后子进程再通过3号文件描述符向文件中进行读取数据。此时父子进程就实现了数据的传输交互。一般在进行通信之前,需要把不需要的文件描述符关闭,上图中,将父进程的不用的3号文件描述符关闭,将子进程的不用的4号文件描述符关闭。但是我们发现这种通信方式只适用于单向通信,所以我们就将它命名为管道。
注意:这种通信方式只适用于具具备血缘关系的进程之间的通信。
2.2 管道接口使用
使用 pipe 函数系统调用接口来创建管道。
int pipe(int fd[2])
参数:输出型参数,具有两个元素的整形数组。将 两个文件描述符数字返回给用户。即 fd:文件描述符数组,其中fd[0]表示读端,fd[1]表示写端。
返回值:管道创建成功返回 0,失败返回 -1,设置错误码。
l2.3 管道编码实现
#include <iostream>
#include <cstdlib>
#include <unistd.h>static const int N=2;int main()
{int pipefd[N] = {0};int n = pipe(pipefd);if(n < 0) return errno;std::cout << "pipefd[0]: " << pipefd[0] << ", pipefd[1]: " << pipefd[1] << std::endl;return 0;
}

说明 3 号文件描述符代表的是读端,4号文件描述符代表的是写端。
接下来让父进程创建子进程来进行父子进程通信。
#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>static const int N=2;// child
void Writer(int wfd)
{std::string s = "Hello, I am child";pid_t self = getpid();int number = 0;char wbuffer[1024];while (true){// 构建发送字符串wbuffer[0] = 0;snprintf(wbuffer, sizeof(wbuffer), "%s-%d-%d", s.c_str(), self, number++);// 发送给父进程write(wfd, wbuffer, strlen(wbuffer));sleep(1);}
}// father
void Reader(int rfd)
{char rbuffer[1024];while (true){rbuffer[0] = 0;ssize_t n = read(rfd, rbuffer, sizeof(rbuffer));if(n > 0){rbuffer[n] = 0;std::cout << "father get a message from child: [" << getpid() << "]@ " << rbuffer << std::endl;}else if(n == 0){std::cout << "father read file done" << std::endl;break;}elsebreak;}}int main()
{int pipefd[N] = {0};int n = pipe(pipefd);if(n < 0) return errno;// std::cout << "pipefd[0]: " << pipefd[0] << ", pipefd[1]: " << pipefd[1] << std::endl;// 让child 写,father 读pid_t id = fork();if(id < 0){perror("fork");return errno;}else if(id == 0){// childclose(pipefd[0]); // 子进程关闭读端Writer(pipefd[1]); // 子进程的写操作close(pipefd[1]);exit(0);}// fatherclose(pipefd[1]); // 父进程关闭写端Reader(pipefd[0]); // 父进程的读操作pid_t rid = waitpid(id, nullptr, 0); // 阻塞等待if(rid < 0){perror("waitpid");return errno;}close(pipefd[0]); // 通信结束,关闭读端return 0;
}

我们在代码中只让子进程 sleep,父进程没有 sleep,并且发现父进程并没有一直在读取管道中的数据,即没有出现子进程正在写的同时,父进程读的情况。而是子进程写完之后父进程再来读取数据。所以,父子进程是会进行协同的,会有同步和互斥来保证管道文件的数据安全。
小Dis:管道是面向字节流的。
2.4 管道特征
- 具有血缘关系的进程进行进程间通信。
- 管道只能单向通信。
- 父子进程是会进程协同的,进行同步与互斥,以保证管道文件中数据的安全。
- 管道是面向字节流的。
- 管道是基于文件的,而文件的生命周期是随进程的,所以进程如果退出了,管道文件也会被自动关闭释放掉。
2.5 管道的四种情况
- 读写端正常,管道如果为空,读端就要堵塞。
- 读写端正常,管道如果被写满,写端就要被阻塞。
- 读端正常,写端关闭,读端就会读到0,表示读到了文件(pipe)结尾,不会被阻塞。
- 写端正常,读端关闭,操作系统会通过 13 号信号把正在写入的进程杀掉。
2.6 管道的使用场景
2.6.1 命令行管道
这个管道与我们以前学习的 | 管道有什么关系呢?其实命令行中的 | 就是通过 pipe 来创建的。
2.6.2 基于管道的简易进程池
所谓进程池,就是提前将一个个的进程当作资源准备好,在需要的时候直接分配而不需要再调用 fork 函数创建子进程了,因为每次的调用fork函数创建子进程的工作量是很大的。
#include <iostream>
#include <vector>
#include <string>
#include <ctime>
#include <errno.h>#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>#include "Task.hpp"static const int processnum = 10; // 要创建的进程数量
std::vector<task_t> tasks;// 先描述
class Channel
{
public:Channel(int cmdfd, pid_t slaverid, const std::string &processname): _cmdfd(cmdfd), _slaverid(slaverid), _processname(processname){}public:int _cmdfd; // 发布任务的文件描述符(负责写)pid_t _slaverid; // 子进程的pid(负责读)std::string _processname; // 子进程的名字
};// 子进程执行的功能,做的任务
void slaver()
{while (true){int cmdcode = 0;int n = read(0, &cmdcode, sizeof(int));if(n == sizeof(int)){// 执行cmdcode对应的任务列表std::cout << "child-[" << getpid() << "]-slaver say@ get a cmdcode: " << cmdcode << std::endl;if(cmdcode >= 0 && cmdcode < tasks.size())tasks[cmdcode]();}else if(n == 0)break;}}// 初始化
void InitProcessPool(std::vector<Channel>* channels)
{for (size_t i = 0; i < processnum; i++){// 创建管道int pipefd[2];int n = pipe(pipefd);if(n < 0){perror("pipe");return;}// 此时 pipefd[0] = 3, pipefd[1] = 4// 创建子进程pid_t id = fork();if(id == 0){// childclose(pipefd[1]); // 关闭写端dup2(pipefd[0], 0); //标准输入0号文件描述符指向 读端管道文件slaver(); // 子进程执行的函数功能exit(0);}// fatherclose(pipefd[0]); // 关闭读端//添加channel字段std::string name = "process-" + std::to_string(i);channels->push_back(Channel(pipefd[1], id, name));}
}// 控制子进程,给自己才能发布任务
void Menu()
{std::cout << "################################################" << std::endl;std::cout << "# 1. 刷新日志 2. 更新野区 #" << std::endl;std::cout << "# 3. 刷新金币 4. 获取点Juana #" << std::endl;std::cout << "# 5.释放技能 0. 退出 #" << std::endl;std::cout << "################################################" << std::endl;
}
void CtrlSlaver(std::vector<Channel>& channels)
{int which = 0;int cnt = 10;while (cnt--) // 轮询10次{// int select = 0;// Menu();// std::cout << "Please Enter@: ";// std::cin >> select;// if(select <= 0 || select > 5) break;// // 选择任务// int cmdcode = select-1;// 选择任务int cmdcode = rand() % tasks.size();// 轮询进程std::cout << "father say: cmdcode: " << cmdcode << ", already sendto: "<< channels[which]._slaverid << ", processname: " << channels[which]._processname << std::endl;// 发送任务write(channels[which]._cmdfd, &cmdcode, sizeof(cmdcode));which++;which %= channels.size();sleep(1);}
}// 清理收尾
void QuitProcess(std::vector<Channel>& channels)
{for(auto& ch : channels){close(ch._cmdfd);}sleep(3);for(auto& ch : channels){waitpid(ch._slaverid, nullptr, 0); // 等待子进程}
}int main()
{LoadTask(&tasks);srand(time(0) ^ getpid() ^ 1023);// 再组织std::vector<Channel> channels;// 初始化InitProcessPool(&channels);// 控制子进程,给自己才能发布任务CtrlSlaver(channels);// 清理收尾QuitProcess(channels);return 0;
}
三:命名管道
管道应用(匿名管道)只能是具有血缘关系的进程才可进行进程间通信,那么如果是两个毫不相关的进程进行进程间通信呢?——命名管道。
进程间通信的前提就是先让两个不同的进程看到同一份资源。即看到同一份文件。
所以,命名管道通信是通过使用路径+文件名的方案让不同的进程看到同一份文件资源,进而实现不同进程间的通信的。故有路径+文件名。即叫做命名管道。
3.1 mkfifo 函数接口
#include <sys/types.h>
#include <sys/stat.h>int mkfifo(const char *pathname, mode_t mode);
- 参数 pathname:文件路径
- 参数 mode:命名管道的文件权限
3.2 命名管道编码实现
// comm.hpp#pragma once#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>#define FILE_FIFO "./myfifo"
#define MODE 0666enum{FIFO_CREATE_ERR = 1,FIFO_DELETE_ERR,FIFO_OPEN_ERR
};class Init
{
public:Init(){// 创建管道int n = mkfifo(FILE_FIFO, MODE);if(n < 0){perror("mkfifo");exit(FIFO_CREATE_ERR);}}~Init(){// 删除管道int m = unlink(FILE_FIFO);if(m < 0){perror("unlink");exit(FIFO_DELETE_ERR);}}
};
// client.cc
#include <iostream>
#include "comm.hpp"using namespace std;int main()
{int fd = open(FILE_FIFO, O_WRONLY); // 以写方式打开管道文件if(fd < 0){perror("open");exit(FIFO_OPEN_ERR);}cout << "client open fifo_file done" << endl;string line;while (true){cout << "Please Enter@ ";getline(cin, line);write(fd, line.c_str(), line.size());}close(fd);return 0;
}
客户端主要完成的工作是:打开信道——开始通信——关闭信道。
#include "comm.hpp"using namespace std;int main()
{// 创建信道Init init;// 打开信道int fd = open(FILE_FIFO, O_RDONLY); // 只读的方式打开if(fd < 0){cout << "server open fifo_file error" << endl;exit(FIFO_OPEN_ERR);}cout << "server open fifo_file success" << endl;// 开始通信while (true){char buffer[1024] = {0};int x = read(fd, buffer, sizeof(buffer));if(x > 0){buffer[x] = 0;cout << "I am server, client say# " << buffer << endl;}else if(x == 0){cout << "client quit, me too" << endl;break;}elsebreak;}close(fd);// 结束之后自动退出管道return 0;
}
服务端主要完成的工作是:创建信道——打开信道——开始通信——关闭信道——删除信道。

四:结语
今天的分享到这里就结束了,如果觉得文章还可以的话,就一键三连支持一下欧。各位的支持就是捣蛋鬼前进的动力。

相关文章:
【Linux通信篇】深入理解进程间通信——管道
--------------------------------------------------------------------------------------------------------------------------------- 每日鸡汤:找一个对的人,然后好好去爱。一个你跟他在一起,然后又可以舒舒服服做自己的人。 -------…...
「 DelegateUI 」Ant-d 风格的 Qt Qml UI 套件
写在前面:关于为什么要写一套新的UI框架 一方面,Qt Qml 生态中缺乏一套既遵循现代设计规范(自带的功能少且丑,懂得都懂),又能深度整合 Qt 生态的开源组件库。 另一方面,Qt Qml 中也有一些其他方案,例如 FluentUI Qml…...
Redis--Set类型
目录 一、引言 二、介绍 三、命令 1.sadd,smembers,sismember 2.spop,srandmember 3.smove,srem 4.sinter,sinterstore 5.sunion,sunionstore,sdiff,sdiffstore 四、内部编码 1.intset 2.hashtable 五、应用场景 1.使用Set保存用…...
【0013】Python数据类型-列表类型详解
如果你觉得我的文章写的不错,请关注我哟,请点赞、评论,收藏此文章,谢谢! 本文内容体系结构如下: Python列表,作为编程中的基础数据结构,扮演着至关重要的角色。它不仅能够存储一系…...
文件上传靶场(10--20)
目录 实验环境: 具体内容实现: 第十关(双写绕过): 第十一关:(%00截断,此漏洞在5.2版本中) 正确用法 错误用法 思路: 操作过程: 第十二关…...
C# 检查系统是否开启 Hyper - V
C# 检查系统是否开启 Hyper - V 在使用 C# 开发应用程序时,有时需要判断系统是否开启了 Hyper - V 功能。Hyper - V 是 Windows 系统提供的一款虚拟化技术,以下为你介绍几种在 C# 中检查系统是否开启 Hyper - V 的方法。 方法一:通过查询系…...
【前端】BOM DOM
两天更新完毕,建议关注收藏点赞 友情链接: HTML&CSS&LESS&Bootstrap&Emmet Axios & AJAX & Fetch BOM DOM 待整理 js2 Web API 是浏览器提供的一套操作浏览器功能和页面元素的 API ( BOM 和 DOM)。官方文档点击跳转 目录 BOMDOM…...
K8s 1.27.1 实战系列(十一)ConfigMap
ConfigMap 是 Kubernetes 中管理非敏感配置的核心资源,通过解耦应用与配置实现灵活性和可维护性。 一、ConfigMap 的核心功能及优势 1、配置解耦 将配置文件(如数据库地址、日志级别)与容器镜像分离,支持动态更新而无需重建镜像。 2、多形式注入 环境变量:将键值…...
计算机网络——IP、MAC、ARP
一、IP地址 1. 什么是IP地址? IP地址(Internet Protocol Address)是互联网中设备的唯一逻辑标识符,类似于现实生活中的“门牌号”。它分为 IPv4(32位,如 192.168.1.1)和 IPv6(128位…...
代码优化——基于element-plus封装组件:表单封装
前言 今天实现一个基于element-plus表单组件的二次封装,什么是二次封装?查看以下表单,传统表单组件是不是用<el-form>嵌套几个<el-form-item>即可实现,那么一个表单可不可以实现,传入一个对象给封装组件&a…...
C/C++中使用CopyFile、CopyFileEx原理、用法、区别及分别在哪些场景使用
文章目录 1. CopyFile原理函数原型返回值用法示例适用场景 2. CopyFileEx原理函数原型返回值用法示例适用场景 3. 核心区别4. 选择建议5. 常见问题6.区别 在Windows系统编程中,CopyFile和CopyFileEx是用于文件复制的两个API函数。它们的核心区别在于功能扩展性和控制…...
qt 多进程使用共享内存 ,加速数据读写,进程间通信 共享内存
Summary: 项目中我们有时需要使用共享内存共享数据,这样,数据不用进程IO读写,加进数据加载和落地; 程序退出时,再保存到本地;速度提升数十倍; Part1:QSharedMemory Windows平台下进程间通信…...
HTML左右分页2【搬代码】
HTML左右分页2 html <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>左右分页布局</title>&l…...
【鸿蒙开发】OpenHarmony调测工具hdc使用教程(设备开发者)
00. 目录 文章目录 00. 目录01. OpenHarmony概述02. hdc简介03. hdc获取04. option相关的命令05. 查询设备列表的命令06. 服务进程相关命令07. 网络相关的命令08. 文件相关的命令09. 应用相关的命令10. 调试相关的命令11. 常见问题12. 附录 01. OpenHarmony概述 OpenHarmony是…...
【贪心算法】简介
1.贪心算法 贪心策略:解决问题的策略,局部最优----》全局最优 (1)把解决问题的过程分成若干步 (2)解决每一步的时候,都选择当前看起来的“最优”的算法 (3)“希望”得…...
transformer模型介绍——大语言模型 LLMBook 学习(二)
1. transformer模型 1.1 注意力机制 **注意力机制(Attention Mechanism)**在人工智能中的应用,实际上是对人类认知系统中的注意力机制的一种模拟。它主要模仿了人类在处理信息时的选择性注意(Selective Attention)&a…...
【GPT入门】第11课 FunctionCall调用本地代码入门
【GPT入门】第11课 FunctionCall调用代码入门 1. 手撕FunctionCall2.代码3.functionCall的结果 1. 手撕FunctionCall 为了了解,funcationCall底层,手写一个functionCall多方法,并调用,体验 思路: 任务:让…...
LangChain教程 - Agent -之 ZERO_SHOT_REACT_DESCRIPTION
在构建智能 AI 助手时,我们希望模型能够智能地调用工具,以便提供准确的信息。LangChain 提供了 AgentType.ZERO_SHOT_REACT_DESCRIPTION,它结合了 ReAct(Reasoning Acting)策略,使得 LLM 可以基于工具的描…...
GStreamer —— 2.17、Windows下Qt加载GStreamer库后运行 - “播放教程 5:色彩平衡“(附:完整源码)
运行效果 介绍 亮度、对比度、色相和饱和度是常见的视频调整, 在 GStreamer 中统称为 Color Balance 设置。 本教程展示了: • 如何找出可用的色彩平衡通道 • 如何更改它们 允许访问颜色平衡设置。如果 元素支持这个接口,只需将其转发给应用…...
串口通信ASCII码转16进制及C#串口编程完整源码下载
在工业自动化、嵌入式系统及物联网以行业中,串口编程非常重要。 串口编程,重点在于串口数据通信和数据处理。 在C#中,System.IO.Ports命名空间提供了SerialPort类,用于实现串口通信。 串口程序的开发主要包括以下几点 1.引用命…...
解决vscode中出现“无法将pip项识别...“问题
问题 遇见问题如下: 查看pip 通过 winR ,输入 cmd,进入终端,搜索 where pip。 发现 pip 查不出来,然后进入文件资源管理器,搜索 Scripts 文件夹,如果没有找到可能是电脑没有下载 python。 点击…...
nacos下载及安装
下载官方最新稳定版 github下载较慢,推荐下面的下载链接 Nacos Server 下载 | Nacos 官网 点击下载和试用下载最新稳定版 Nacos Server 下载 | Nacos 官网 配置检查(可选) 默认情况下,Nacos 使用内置的 Derby 数据库&#x…...
C++从零实现Json-Rpc框架
文章目录 一、项目介绍1. 基本原理2. 涉及到的技术栈3. 最终实现的效果 二、 第三方库的介绍与使用1. JsonCpp库Json的数据格式JsonCpp介绍封装Json工具类 2. muduo库muduo库是什么Muduo库常见接口介绍 3. C11异步操作std::future 三、框架设计1. 服务端模块划分NetworkProtoco…...
rom定制系列------小米note3 原生安卓15 批量线刷 默认开启usb功能选项 插电自启等
小米Note 3搭载骁龙660处理器,1200万像素广角镜头、俗称大号版的小米6,官方最终版为12.0.1稳定版安卓9的固件。客户需要运行在安卓15的rom。根据原生官网的rom修改一些功能选项。以便客户操作需求。 定制资源说明 根据客户需求采用安卓15原生系统为底包…...
使用jest测试用例之入门篇
Jest使用 Jest 是由 Facebook 开发的一个 js 测试框架,jest 主要侧重于被用于做单元测试和集成测试 安装 npm i jest -D运行 **package.json**里面配置命令 // scripts添加测试脚本 {"test": "jest" /* 运行后便会使用 jest 执行所有的 .t…...
大数据学习(59)-DataX执行机制
&&大数据学习&& 🔥系列专栏: 👑哲学语录: 承认自己的无知,乃是开启智慧的大门 💖如果觉得博主的文章还不错的话,请点赞👍收藏⭐️留言📝支持一下博主哦ᾑ…...
YashanDB认证,YCA证书认证教程,免费证书,内含真题考试题库及答案——五分钟速成
目录 一.账号及平台注册登录流程 二.登录进行设备调试核验 三.考试(考完获取分数) 四.获取证书 五.题库及答案 一.账号及平台注册登录流程 1-点击这里进行账号注册(首次学习必须先注册,有账号之后可以直接在2号链接登录&#…...
消防行业如何借助 TDengine 打造高效的数据监控与分析系统
小T导读:本篇文章来自“2024,我想和 TDengine 谈谈”征文活动的优秀投稿,深入探讨了如何在消防行业中运用 TDengine 进行业务建模。文章重点介绍了如何通过 TDengine 的超级表、标签设计和高效查询功能,有效管理消防监控系统中的时…...
自然语言处理中的语音识别技术:从声波到语义的智能解码
引言 语音识别(Automatic Speech Recognition, ASR)是自然语言处理(NLP)的关键分支,旨在将人类语音信号转化为可处理的文本信息。随着深度学习技术的突破,语音识别已从实验室走向日常生活,赋能…...
010-Catch2
Catch2 一、框架简介 Catch2 是一个基于 C 的现代化单元测试框架,支持 TDD(测试驱动开发)和 BDD(行为驱动开发)模式。其核心优势在于: 单头文件设计:v2.x 版本仅需包含 catch.hpp 即可使用自然…...

