前面的内容中我们已经学习了解了Linux系统的底层相关的东西及其多进程的事情。既然有了多个进程,那么就非常有必要让这些进程互相联系起来。因此进程之间通信就十分重要。
相关代码已经上传至作者的个人gitee:楼田莉子/Linux学习喜欢请点个赞
目录
[System V IPC](#System V IPC)
[POSIX IPC](#POSIX IPC)
进程间通信介绍
前言
进程之前具有独立性,因此要让两个进程看到同一份"资源"。
进程通信的目的
数据传输:一个进程需要将它的数据发送给另一个进程。
资源共享:多个进程之间共享同样的资源。
通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
进程控制:有些进程希望完全控制另一个进程的执行(如调试进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
进程间通信的发展
管道
System V进程间通信
POSIX进程间通信
进程间通信的分类
管道
匿名管道(pipe)
命名管道
System V IPC
System V 消息队列
System V 共享内存
System V 信号量
POSIX IPC
消息队列
共享内存
信号量
互斥量
条件变量
读写锁
管道
通信原理
管道是unix系统最古老的通信方式。我们把从⼀个进程连接到另⼀个进程的⼀个数据流称为⼀个"管道"。管道是内核级基于文件的通信方式
fork代码执行的时候file要进行拷贝
管道是单向通信
管道是一个内存级的文件,不需要打开文件路径,不需要打开磁盘文件,没有路径,不需要文件名,是匿名通道

匿名管道
pipe函数
cpp
#include <unistd.h>
功能:创建⼀⽆名管道
原型:
int pipe(int fd[2]);
参数:
fd:⽂件描述符数组,其中fd[0]表⽰读端, fd[1]表⽰写端
返回值:成功返回0,失败返回错误代码

fork共享管道的原理

理解管道
文件标识符角度

内核角度

接下来我们写代码验证,要求实现:
1、创建管道文件
2、fork进程
3、关闭读写端,子进程w,父进程r
4、子进程合并到父进程
cpp
// 例子:从键盘读取数据,写入管道,读取管道,写到屏幕
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
//创建管道
int pipefd[2]={0};
int n=pipe(pipefd);
if(n<0)
{
perror("pipe failed");
return 1;
}
printf("pipefd[0]:%d,pipefd[1]:%d\n",pipefd[0],pipefd[1]);
//fork进程
pid_t id =fork();
if(id==0)
{
//child
close(pipefd[1]);//关闭写端
char *str="你好诗华";
// write(pipefd[0],str,strlen(str));//管道也是文件
int cnt=10;
char outbuffer[1024];
while(cnt--)
{
//sizeof(outbuffer)-1 留一个位置给字符串结束符\0
snprintf(outbuffer, sizeof(outbuffer)-1, "f->c# %s %d %d\n", str, getpid(), cnt);//父进程传到子进程的消息
write(pipefd[0],outbuffer,strlen(outbuffer));//管道也是文件
sleep(1);
}
exit(0);
}
//父进程
close(pipefd[1]);
char buffer[1024]={0};
char inbuffer[1024]={0};
while(1)
{
//sizeof(outbuffer)-1 留一个位置给字符串结束符\0
read(pipefd[0],inbuffer,sizeof(inbuffer)-1);
}
pid_t ret=waitpid(id,NULL,0);
(void)ret;//防止编译器报警告
return 0;
}
打开的文件生命周期是跟进程一样的。
匿名管道的特性
-
管道是只能单向通信,单工通信
-
匿名管道只能用来进行 具有血缘关系的进程之间 常用 父子通信(继承内核资源)
-
管道是面向字节流的。
-
管道的生命周期是随进程的!
-
管道通信,对于多进程而言,是自带互斥 与同步机制
匿名管道几种情况
管道通信,前提是让不同的进程,看到同一份资源。可以是文件、文件的内核缓冲区、内存块,属于父子进程共享的资源。但是这种并发就会导致互斥和同步的问题。
互斥就是任何时刻只允许一个进程访问资源
同步就是进程访问资源是有一定顺序性的
实际情况下会出现以下几种情况:
1、子进程写的慢父进程堵塞,要等管道有数据,父进程才能读
此时管道为空,读阻塞了,要给写机会。
2、子进程写的快父进程不读,管道被写满则父进程阻塞
此时管道满了,写阻塞了,要给读机会
3、读端在读写端的关闭,读端读完管道内剩余数据,接着就会读取空字符串,read函数返回值为0,表明管道读到结尾。
4、写端一直在写,读端不关闭fd。OS会直接杀掉写的进程。
我们可以利用进程池的方法来解决。
命名管道
进程间通信的本质是让不同进程间看到同一份资源。那么如果两个进程毫无关系,那么该如何通信呢?就依赖于命名管道。
如果多个进程打开同一份文件和资源,怎么保证他们打开的都是同一个呢?只需要让访问路径是一样的即可。因为文件名+路径=是唯一incode。而且文件一定有名字。
接下来我们就来尝试利用命名通道创建不同进程之间的通信手段。创建方式有mkfifo指令和mkfifo函数
bash
#创建名为filename的FIFO的命名管道指令
mkfifo filename
cpp
//创建名字为filename、权限为mode的FIFO文件
int mkfifo(const char *filename,mode_t mode);
我们先创建一个namepipe的FIFO文件(命名管道)。随后在多个进程下进程读和写操作。(图一写后必须等待图二读,否则会进程阻塞)


但是这样显得不够准确,所以我们接下来来编写一段代码,模拟一段通信服务,就像下面这张图一样。

代码内容如下:
Pipe.hpp
cpp
//.hpp是专属于C++的特殊编写模式文件,类似于.h文件。可以将头文件和源文件一起写
#pragma once
#include<iostream>
#include<string>
#include<cstring>
#include <sys/stat.h>
#include <fcntl.h>
#include <cerrno>
#include <sys/types.h>
#include<unistd.h>
#define ForRead 0
#define ForWrite 1
const std::string gcommfile="./fifo";
class FIFO
{
private:
std::string _commfile;
mode_t _mode;
int _fd;
bool IsExist()
{
//命名管道文件用open打开了必须要读写,如果只读就会阻塞
// int fd=open(_commfile.c_str(),O_RDONLY);
// if(fd<0)
// {
// return false;
// }
// else
// {
// close(fd);
// return true;
// }
struct stat buf;
int n=stat(_commfile.c_str(),&buf);
if(n<0)
{
return false;
}
else
{
return true;
}
}
public:
FIFO(const std::string &commfile=gcommfile)
:_commfile(commfile),_mode(0666),_fd(-1)
{}
//创建命名管道
void create()
{
if(IsExist())
{
return;
}
//设置文件权限掩码为0,确保创建的命名管道具有所需的权限
umask(0);
int n =mkfifo(_commfile.c_str(),_mode);
if(n<0)//创建失败
{
std::cerr<<"mkfifo error!"<<strerror(errno)\
<<"\terrno"<<errno<<std::endl;
//strerror用于返回错误码对应的错误信息
exit(1);
}
std::cerr<<"mkfifo success!"<<std::endl;
}
//获取命名管道
void Open(int mode)
{
//通信没有开始之前如果读端打开写端没有打开
//读端open就会阻塞,直到write打开
if(mode==ForRead)
{
_fd=open(_commfile.c_str(),O_RDONLY);
}
else if(mode ==ForWrite)
{
_fd=open(_commfile.c_str(),O_WRONLY);
}
if(_fd<0)
{
std::cerr<<"open error!"<<strerror(errno)\
<<"\terrno"<<errno<<std::endl;
exit(2);
}
else
{
std::cout<<"open "<<_commfile<<" success!"<<std::endl;
}
}
//发送用引用,接受用指针!
//发送数据
void Send(const std::string &data)
{
ssize_t n=write(_fd,data.c_str(),data.size());
(void)n;
}
//接收数据
int Recv(std::string *data)
{
char buffer[128];
ssize_t n = read(_fd, buffer, sizeof(buffer)-1); // 为什么要-1?
if(n > 0)
{
buffer[n] = 0;
*data = buffer;
return n;
}
else if(n == 0)
{
return 0;
}
else
{
return -1;
}
}
//销毁命名管道
void destroy()
{
if(!IsExist())
{
return;
}
int n=unlink(_commfile.c_str());
if(n<0)
{
std::cerr<<"unlink error!"<<strerror(errno)\
<<"\terrno"<<errno<<std::endl;
exit(2);
}
(void)n;
std::cerr<<"unlink "<<_commfile<<" success!"<<std::endl;
}
~FIFO()
{}
};
Server.cpp
cpp
#include"Pipe.hpp"
int main()
{
FIFO pipe;
pipe.create();
sleep(3);
pipe.Open(ForRead);
std::string data;
while(1)
{
int n=pipe.Recv(&data);
if(n<=0)
{
break;
}
else
{
std::cout<<"client send data:#"<<data<<std::endl;
}
}
pipe.destroy();
return 0;
}
client.cpp
cpp
#include"Pipe.hpp"
int main()
{
//客户端只需要打开命名管道进行读写操作
FIFO filename;
filename.Open(ForWrite);
while(1)
{
std::cout<<"Please Enter@ ";
std::string data;
std::getline(std::cin,data);
filename.Send(data);
}
return 0;
}
先运行./Server,然后运行./client,有以下结果:

除了管道外,system V也是进程间通信的重要部分。但是其内容量较大,这里就不展开说明了。后续会更新这部分内容。本期内容就到这里了,喜欢的话请点个赞谢谢
命名管道与匿名管道的对比
| 特性 | 命名管道(Named Pipe/FIFO) | 匿名管道(Anonymous Pipe) |
|---|---|---|
| 创建方式 | mkfifo() 函数或 mkfifo 命令 |
pipe() 系统调用 |
| 文件系统可见性 | ✅ 有实体文件(类型为p) |
❌ 仅存在于内存中 |
| 持久性 | 持久存在,直到被显式删除 | 随进程结束而销毁 |
| 进程关系限制 | ❌ 无亲缘关系要求 | ✅ 必须具有亲缘关系 |
| 通信进程范围 | 系统中任何有权限的进程 | 父子进程或兄弟进程 |
| 访问方式 | 通过文件路径名打开 | 通过继承的文件描述符 |
| Shell中使用 | 可作为普通文件操作 | 使用 ` |
| 创建示例 | mkfifo mypipe 或 mkfifo("path", mode) |
int pipe_fd[2]; pipe(pipe_fd); |
| 标识符类型 | 文件路径名(字符串) | 文件描述符(整数) |
| 阻塞行为 | 读写端相互阻塞等待 | 读写端相互阻塞等待 |
| 使用示例 | echo "data" > mypipe |
`cmd1 |
| 生命周期管理 | 需要手动创建和删除 | 随进程自动回收 |
| 权限控制 | 有文件权限位(rwx) | 无权限控制,继承父进程 |
| 打开次数限制 | 可被多个进程同时打开 | 只能被创建它的进程及其子进程使用 |
| 典型应用场景 | 1. 无亲缘关系进程通信 2. 持久化通信端点 3. Shell脚本间通信 | 1. 父子进程通信 2. Shell命令管道 3. 简单进程间数据传递 |
补充说明表
| 维度 | 命名管道 | 匿名管道 |
|---|---|---|
| 创建后表现 | 显示为文件,ls -l看到p类型 |
不显示,仅进程内部可见 |
| 通信方向 | 半双工(单向) | 半双工(单向) |
| 多进程使用 | 支持多个读者/写者(需协调) | 通常一对一 |
| 跨网络 | ❌ 仅本地 | ❌ 仅本地 |
| 缓冲区大小 | 系统默认(通常4K-64K) | 系统默认(通常4K-64K) |
| I/O多路复用 | ✅ 支持(select/poll/epoll) | ✅ 支持(select/poll/epoll) |
| 信号处理 | 可接收SIGPIPE等信号 | 可接收SIGPIPE等信号 |
| 阻塞模式 | 默认阻塞,可设为非阻塞 | 默认阻塞,可设为非阻塞 |
| 原子性 | 保证小于PIPE_BUF的写入是原子的 | 保证小于PIPE_BUF的写入是原子的 |
| POSIX标准 | ✅ 是 | ✅ 是 |
| Windows支持 | ✅ 有(命名管道) | ✅ 有(匿名管道) |
选择建议表
| 场景 | 推荐选择 | 理由 |
|---|---|---|
| Shell命令流水线 | 匿名管道 | 简单直接,`cmd1 |
| 无关进程间通信 | 命名管道 | 不需要亲缘关系 |
| 需要持久化通信端点 | 命名管道 | 文件系统可见,长期存在 |
| 父子/兄弟进程通信 | 匿名管道 | 简单高效,自动清理 |
| 需要权限控制的IPC | 命名管道 | 可利用文件权限机制 |
| 临时数据传递 | 匿名管道 | 自动管理生命周期 |
| 复杂IPC架构 | 命名管道 | 更适合复杂进程拓扑 |
封面图自取:
