
一、进程间通信介绍
1.1进程间通信目的
数据传输:⼀个进程需要将它的数据发送给另⼀个进程。
资源共享:多个进程之间共享同样的资源。
通知事件:⼀个进程需要向另⼀个或⼀组进程发送消息,通知它(它们)发生了某种事件(如进 程终止时要通知父进程)。
进程控制:有些进程希望完全控制另⼀个进程的执行(如Debug进程),此时控制进程希望能够 拦截另⼀个进程的所有陷入和异常,并能够及时知道它的状态改变。
**怎么通信?**进程间通信的本质是先让不同的进程看到同一份"资源",然后才有通信条件。
1.2进程间通信发展
- 萌芽奠基阶段(1969-1980 年):早期 UNIX 推出匿名管道、命名管道、信号,实现了亲缘 / 非亲缘进程的基础数据传输与异步事件通知,奠定了 UNIX「一切皆文件」的 IPC 核心设计思想。
- 标准化扩展阶段(1980-1990 年):System V 推出消息队列、共享内存、信号量三大核心 IPC 机制,补齐了单主机通用进程通信的能力;BSD 套接字突破单主机限制,实现了跨网络的进程通信,形成了 Linux IPC 的两大核心基础体系。
- 可移植性统一阶段(1990-2000 年):POSIX IPC 标准发布,解决了 UNIX 分支 IPC 碎片化的问题,统一了接口规范,大幅提升了跨平台可移植性,成为现代 Linux 的标准推荐 IPC 方案。
- 场景化优化阶段(2000-2015 年):针对桌面、移动嵌入式、高性能服务端等细分场景,衍生出 D-Bus、Binder、eventfd 等定制化 IPC 机制,同时内核零拷贝技术进一步优化了 IPC 的传输性能。
- 云原生分布式阶段(2015 年至今):容器技术通过 IPC Namespace 实现了容器间的 IPC 隔离与共享;RPC 框架、消息中间件成为分布式场景的主流 IPC 方案,eBPF、RDMA 等技术持续优化分布式 IPC 的性能与灵活性。
1.3进程间通信分类
管道:匿名管道pipe,命名管道
System V IPC:System V 消息队列,System V 共享内存,System V 信号量
POSIX IPC:消息队列,共享内存,信号量 , 互斥量 , 条件变量 , 读写锁
二、管道
什么是管道?
管道是Unix中最古老的进程间通信的形式。我们把从⼀个进程连接到另⼀个进程的⼀个数据流称为一个"管道"。

三、匿名管道
我们先看下面的图片深入理解匿名管道!!!



示例代码:
cpp
#include <iostream>
#include <unistd.h>
#include <cstring>
#include <sys/types.h>
#include <sys/wait.h>
void ChildWrite(int wfd)
{
char buffer[1024];
int cnt = 0;
while(1)
{
snprintf(buffer,sizeof(buffer),"I am child,pid:%d,cnt:%d",getpid(),cnt++);
write(wfd,buffer,strlen(buffer));
sleep(1);
}
}
void FatherRead(int rfd)
{
char buffer[1024];
while(1)
{
buffer[0] = 0;
ssize_t n = read(rfd,buffer,sizeof(buffer)-1);
if (n > 0)
{
buffer[n] = 0;
std::cout << "child say : " << buffer << std::endl;
}
}
}
int main()
{
//创建管道
int fds[2] = {0};
int n = pipe(fds);
if (n < 0)
{
std::cout << "pipe error" << std::endl;
return 1;
}
std::cout << fds[0] << std::endl;//读端
std::cout << fds[1] << std::endl;//写端
//创建子进程
pid_t id = fork();
if (id == 0)
{
close(fds[0]);
ChildWrite(fds[1]);
close(fds[1]);
exit(0);
}
//关闭不需要的读写端,形成通信通道
//f -> r,c -> w
close(fds[1]);
FatherRead(fds[0]);
waitpid(id, nullptr, 0);
close(fds[0]);
return 0;
}
管道的特点:
1.匿名管道只用来进行具有血缘关系的进程进行进程间通信(常用于父子)
2.管道文件自带同步机制
3.管道是面向字节流的
4.管道是单向通信的
5.(管道)文件的生命周期是随进程的

管道通信的四种情况:
- 写慢,读快------>读端就要阻塞进程(就是在等待写入)
- 写快,读慢------>写满了的时候就要阻塞等待(等待读端)
- 写关闭,读正常------>read就会读到返回值为0,表示文件结尾
- 读关闭,写正常------->只是在写端写入没有任何意义,OS不会做没有意义的事,所以OS会杀掉(发送异常信号 13) SIGPIPE)写端进程
管道容量测试代码:
cpp
void ChildWrite(int wfd)
{
char c = 0;
int cnt = 0;
while(1)
{
//snprintf(buffer,sizeof(buffer),"I am child,pid:%d,cnt:%d",getpid(),cnt++);
write(wfd,&c,1);
printf("child: %d\n",cnt);
cnt++;
//sleep(2);
}
}
void FatherRead(int rfd)
{
char buffer[1024];
while(1)
{
sleep(100);
buffer[0] = 0;
ssize_t n = read(rfd,buffer,sizeof(buffer)-1);
if (n > 0)
{
buffer[n] = 0;
std::cout << "child say : " << buffer << std::endl;
}
else if (n == 0)
{
std::cout << "n : " << n << std::endl;
std::cout << "child退出,我也退出" ;
break;
}
else
{
break;
}
}
}

我们这里先抛出一个概念,在后续的学习中会有答案

基于匿名管道---进程池
四、命名管道
管道应用的⼀个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信。如果我们想在不相关的进程之间交换数据,可以使用FIFO文件 来做这项工作,它经常被称为命名管道 。命名管道是⼀种特殊类型的文件。

4.1创建一个命名管道
命名管道可以从命令行上创建,命令行方法是使用下面这个命令:
cpp
mkfifo filename

命名管道也可以从程序里创建,相关函数有:
cpp
int mkfifo(const char *filename,mode_t mode);
4.2匿名管道与命名管道的区别
- 匿名管道由pipe函数创建并打开
- 命名管道由mkfifo函数创建,打开用open
- FIFO(命名管道)与pipe(匿名管道)之间唯⼀的区别在它们创建与打开的方式不同,⼀但这些 工作完成之后,它们具有相同的语义
4.3命名管道的打开规则
如果当前打开操作是为读而打开FIFO时:
- O_NONBLOCK disable:阻塞直到有相应进程为写而打开该FIFO
- O_NONBLOCK enable:立刻返回成功
如果当前打开操作是为写而打开FIFO时:
- O_NONBLOCK disable:阻塞直到有相应进程为读而打开该FIFO
- O_NONBLOCK enable:立刻返回失败,错误码为ENXIO
4.4实例:用命名管道实现server&client通信
comm.hpp
#pragma once
#include<iostream>
#include <string>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#define PATH "."
#define FIFONAME "fifo"
class NamedFifo
{
public:
NamedFifo(const std::string& path, const std::string& name)
:_path(path),_name(name)
{
_fifoname = _path + "/" +_name;
umask(0);
//新建管道
int n = mkfifo( _fifoname.c_str(),0666);
if(n != 0)
{
std::cerr << " FIFO error " << std::endl;
}
else
{
std::cout << "FIFO created" << std::endl;
}
}
~NamedFifo()
{
//删除管道文件
int n = unlink(_fifoname.c_str());
if(n == 0)
{
std::cout << "remove FIFO success" << std::endl;
}
else
{
std::cout << "remove FIFO failed" << std::endl;
}
}
private:
std::string _path;
std::string _name;
std::string _fifoname;
};
//操作类
class FileOper
{
public:
FileOper(const std::string& path, const std::string& name)
:_path(path),_name(name)
{
_fifoname = _path + "/" + _name;
}
void OpenForRead()
{
//打开文件
_fd = open(_fifoname.c_str(),O_RDONLY);
if(_fd < 0)
{
std::cerr << "open FIFO error " << std::endl;
return;
}
std::cout << "open FIFO success" << std::endl;
}
void OpenForWrite()
{
//打开文件
_fd = open(_fifoname.c_str(), O_WRONLY);
if(_fd < 0)
{
std::cerr << "open fifo error" << std::endl;
return;
}
std::cout << "open fifo success" << std::endl;
}
void Write()
{
//写入
std::string message;
int cnt = 1;
pid_t pid = getpid();
while(true)
{
std::cout << "Please Enter:";
std::getline(std::cin,message);
message += (",message number:" + std::to_string(cnt++) + ",[" + std::to_string(pid) + "]");
write(_fd,message.c_str(),message.size());
}
}
void Read()
{
//读文件
while(true)
{
char buffer[1024];
int number = read(_fd,buffer,sizeof(buffer)-1);
if(number > 0)
{
buffer[number] = 0;
std::cout << "Client Say:" << buffer << std::endl;
}
else if(number == 0)
{
std::cout << "Client close!me too!" << std::endl;
}
else
{
std::cout << "read error" << std::endl;
break;
}
}
}
void Close()
{
if(_fd > 0)
{
close(_fd);
}
}
~FileOper()
{}
private:
std::string _path;
std::string _name;
std::string _fifoname;
int _fd;
};
#include "comm.hpp"
int main()
{
NamedFifo fifo(PATH,FIFONAME);
FileOper readerfile(PATH,FIFONAME);
readerfile.OpenForRead();
readerfile.Read();
readerfile.Close();
return 0;
}
#include "comm.hpp"
int main()
{
FileOper writerfile(PATH,FIFONAME);
writerfile.OpenForWrite();
writerfile.Write();
writerfile.Close();
return 0;
}

五、system V------共享内存
共享内存区是最快的IPC形式。⼀旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到内核,换句话说是进程不再通过执行进入内核的系统调用来传递彼此的数据。
所以,system V是一种标准,Linux内核支持了这种标准,专门设计了一个IPC通信模块,通信的接口设计,原理,接口,相识性。IPC的本质是让不同的进程先看到同一份资源。
5.1共享内存示意图

5.2共享内存数据结构
struct shmid_ds {
struct ipc_perm shm_perm; /* operation perms */
int shm_segsz; /* size of segment (bytes) */
__kernel_time_t shm_atime; /* last attach time */
__kernel_time_t shm_dtime; /* last detach time */
__kernel_time_t shm_ctime; /* last change time */
__kernel_ipc_pid_t shm_cpid; /* pid of creator */
__kernel_ipc_pid_t shm_lpid; /* pid of last operator */
unsigned short shm_nattch; /* no. of current attaches */
unsigned short shm_unused; /* compatibility */
void shm_unused2; / ditto - used by DIPC */
void shm_unused3; / unused */
};
5.3共享内存函数
shmget函数
**功能:**用来创建共享内存
int shmget(key_t key, size_t size, int shmflg);
参数:
**key:**这个共享内存段名字
**size:**共享内存大小
**shmflg:**由九个权限标志构成,它们的用法和创建文件时使用的mode模式标志是⼀样的
取值为IPC_CREAT:共享内存不存在,创建并返回;共享内存已存在,获取并返回。
取值为IPC_CREAT | IPC_EXCL:共享内存不存在,创建并返回;共享内存已存在, 出错返回。(这样只要shmflg成功返回就一定是一个全新的共享内存)
**返回值:**成功返回⼀个非负整数,即该共享内存段的标识码;失败返回-1

shmctl函数
**功能:**用于控制共享内存
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数:
**shmid:**由shmget返回的共享内存标识码
**cmd:**将要采取的动作(有三个可取值)
**buf:**指向⼀个保存着共享内存的模式状态和访问权限的数据结构


shmat函数
**功能:**将共享内存段连接到进程地址空间
void *shmat(int shmid, const void *shmaddr, int shmflg);
参数:
**shmid:**共享内存标识
**shmaddr:**指定连接的地址
- shmaddr为NULL,核心自动选择⼀个地址
- shmaddr不为NULL且shmflg无SHM_RND标记,则以shmaddr为连接地址。
- shmaddr不为NULL且shmflg设置了SHM_RND标记,则连接的地址会自动向下调整为SHMLBA的整数倍。公式:shmaddr - (shmaddr % SHMLBA)
- shmflg=SHM_RDONLY,表示连接操作用来只读共享内存
**shmflg:**它的两个可能取值是SHM_RND和SHM_RDONLY
返回值:成功返回⼀个指针(起始虚拟地址),指向共享内存第⼀个节;失败返回-1。这有些像C语言中的malloc
shmdt函数
**功能:**将共享内存段与当前进程脱离
int shmdt(const void *shmaddr);
参数:
**shmaddr:**由shmat所返回的指针
**返回值:**成功返回0;失败返回-1
**注意:**将共享内存段与当前进程脱离不等于删除共享内存段

六、system V消息队列
消息队列提供了⼀个从⼀个进程向另外⼀个进程发送⼀块数据的方法。每个数据块都被认为是有⼀个类型,接收者进程接收的数据块可以有不同的类型值。
特性:IPC资源必须删除,否则不会自动清除,除非重启,所以system V IPC资源的生命周期随内核。

七、system V信号量

一句话总结:所谓的对共享资源进行保护,本质是对访问共享资源的代码进行保护
我们这里再次解释一下原子性的概念

信号量:
- 特性方面:IPC资源必须删除,否则不会自动清除,除非重启,所以system V IPC资源的生命周期随内核
- 理解方面:信号量是一个计数器
- 作用方面:保护临界区
- 本质方面:信号量本质是对资源的预订机制
- 操作方面:申请资源,计数器--,P操作;释放资源,计数器++,V操作。

信号量和通信有什么关系?
1.先访问信号量P,每个进程都要先看到同一个信号量。system V 就解决这个问题。
2.不是传递数据才是通信IPC,通知,同步互斥也是
