在 Linux 世界中,进程是资源分配和调度的基本单位。通常情况下,每个进程都拥有自己独立的虚拟地址空间,这意味着一进程无法直接访问另一个进程的数据。但现实中的复杂任务往往需要多个进程协同工作,这就引出了一个核心需求------进程间通信。
本文将深入探讨 Linux 下的几种经典 IPC 机制,理解它们的目的、发展历程以及工作原理。
1. 进程间通信的目的
• 数据传输:一个进程需要将它的数据发送给另一个进程
• 资源共享:多个进程之间共享同样的资源。
• 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进 程终止时要通知父进程)。
• 进程控制:有些进程希望完全控制另⼀个进程的执行(如Debug进程),此时控制进程希望能够 拦截另⼀个进程的所有陷入和异常,并能够及时知道它的状态改变。
2. 进程间通信的发展
Linux 进程间通信(IPC,Inter-Process Communication)的经典机制主要包括管道、System V IPC 和 POSIX IPC。
管道是最简单的单向数据流,用于父子进程等有亲缘关系的进程间通信,分为匿名管道和有名管道。
System V IPC 是一组功能更强的机制,包括消息队列(用于传递格式化的消息)、信号量(用于进程同步)和共享内存(最快的数据共享方式),它们通过系统唯一的键值来标识。
POSIX IPC 是更现代、接口更统一的标准,同样提供了消息队列、信号量和共享内存,但它使用类似文件路径的名字来标识对象,比 System V IPC 更易用、更清晰。
接下来我们主要介绍前两种机制
3. 管道
管道的本质是一个在内核中维护的缓冲区。它就像一个单向的水管,数据从一端流入,从另一端流出。这个缓冲区被抽象成一个特殊的"文件",进程通过文件描述符来对其进行读写操作。
管道主要分为匿名/无名管道和命名/有名管道:
3.1 匿名管道
匿名管道是Unix/Linux系统中最基础的进程间通信形式,它提供了一个单向的、内存中的字节流通道,专门用于具有亲缘关系的进程(如父子进程、兄弟进程)之间的通信。
匿名管道通过pipe系统调用创建管道
#include<unistd.h>
int pipe(int pipefd[2]);
参数pipefd是输出型参数,该函数调用成功后(成功返回值为0,失败返回-1),内核会返回两个文件描述符:
pipefd[0]:用于从管道读取数据。
pipefd[1]:用于向管道写入数据。
一个进程(通常是父进程)创建管道后,通过 fork() 创建子进程。子进程会继承这两个文件描述符。这样,父子进程就拥有了指向同一个内核缓冲区的通道。
shell命令中的 " | "就是匿名管道
案例
我们来写一个匿名管道通信的案例:

运行结果:

3.3 命名管道
命名管道是一种允许无亲缘关系的进程通过文件系统路径进行通信的先进先出(FIFO)通信机制。
创建命名管道函数mkfifo
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode); //成功返回0,失败返回-1
pathname:要创建的命名管道在文件系统中的路径名
mode:管道的访问权限(与普通文件的权限位相同),通常使用八进制表示,如 0666
案例
cpp
//Common.h
#ifndef __COMMON__
#define __COMMON__
#include<iostream>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
char pipename[] = "FIFO";
#endif
cpp
//client.cpp
#include"Common.h"
//向管道写
int main()
{
//创建管道FIFO文件
mkfifo(pipename,0664);//默认在当前路径下建立
char buf[1024];
//以只写方式打开管道FIFO文件,会阻塞知道有读进程打开另一端
int fd = open(pipename,O_WRONLY);
while(fgets(buf,sizeof(buf),stdin))
{
write(fd,buf,sizeof(buf)-1);//留一个\0的位置
//输入end结束
if(strncmp(buf,"end",3)==0)
break;
}
//关闭管道
close(fd);
return 0;
}
cpp
//server.cpp
#include"Common.h"
//从管道读
int main()
{
char buf[1024] = {0};
//以只读方式打开管道文件
int fd = open(pipename,O_RDONLY);
//读取管道内容
while(strncmp(buf,"end",3))
{
ssize_t r = read(fd,buf,sizeof(buf));
if(r>0)
{
buf[r] = '\0';
printf("read:%s",buf);
}
}
//关闭管道
close(fd);
return 0;
}
两进程运行起来后,server端就可以接收到client端发送的信息了

这是不是很像我们平时发消息?只不过我们没法回复信息,那么再使用另一个管道让client端接收server端接受信息后的回应就可以初步实现进程相互通信了,大家可以自己试一试哦!
之后server进程和client进程就可以通过管道FIFO管道进行通信了,我们当前文件下也会有一个管道文件FIFO,这个管道文件的路径是可以改变的,默认是在当前路径下
4. System V 共享内存
共享内存块结构体(struct shmid_ds):用于内核中,来记录和管理每一个共享内存段的状态信息。
第一个成员变量:
我们重点注意这个_key变量,是用来区分每一块共享内存的,相当于每个共享内存块的身份证号,每个共享内存块的_key值都不同,进程进行通信时通过使用同一个_key值来达到使用同一块共享内存的目的
4.1 相关函数和指令
下面函数所需头文件
#include <sys/shm.h>
1. shmget - 创建/获取共享内存段
int shmget(key_t key, size_t size, int shmflg);
参数:
key:共享内存的键值,通常使用 ftok() 生成或使用 IPC_PRIVATE

proj_id可以是任意值,所以ftok生成的值可能会有冲突,如果冲突,那么就换一个proj_id就可以了
size:共享内存段的大小(字节)
shmflg:权限标志,如 IPC_CREAT | 0666
返回值: 成功返回共享内存标识符,失败返回 -1
2. shmat - 附加共享内存段
进程要使用共享内存就必须使用shmat将共享内存挂接到对应的虚拟地址空间
void *shmat(int shmid, const void *shmaddr, int shmflg);
参数:
shmid:shmget 返回的标识符
shmaddr:指定附加地址,通常为 NULL(让系统自动选择)
shmflg:附加标志,如 SHM_RDONLY(只读模式),设置为0的话就可以直接使用它的缺省值(即共享内存的权限)
返回值: 成功返回共享内存段的起始地址,失败返回 (void *) -1
3. shmdt - 分离共享内存段
解除共享内存和进程之间的映射
int shmdt(const void *shmaddr);
shmaddr:shmat 返回的地址指针
返回值: 成功返回 0,失败返回 -1
4. shmctl - 控制共享内存段
shmctl主要是控制设置的是共享内存段的属性,甚至删除共享内存
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
cmd常用命令:
IPC_RMID:删除共享内存段
IPC_STAT:获取状态信息
IPC_SET:设置参数
5. 常用指令
// 查看共享内存资源
ipcs -m
// 释放/删除共享内存
ipcrm -m 共享内存shmid


用ipcs查到的信息中我们可以看到key,shmid,owner,bytes这几个就不用多说了,我们就解释一下perms和nattch是什么:
perms:共享内存的访问权限
nattch:当前有多少个进程链接到了这个共享内存段上
6. 验证这些函数和命令
cpp
//Shm.h
#ifndef __SHM_HPP__
#define __SHM_HPP__
#include<iostream>
#include<unistd.h>
#include<sys/ipc.h>
#include<sys/shm.h>
#include<sys/types.h>
std::string pathname = ".";
int proj_id = 0x123;
size_t defaultsize = 4096;//一般是4096的整数倍
int shmflg = IPC_CREAT|IPC_EXCL|0666;//创建全新的共享内存(带上权限0666)
//共享内存
class SharedMemory
{
public:
SharedMemory(size_t size = defaultsize)
:_size(size)
,_shmid(-1)
{
_key = ftok(pathname.c_str(),proj_id);
if(_key<0)
{
perror("ftok");
}
else
printf("键值key创建成功:0x%x\n",_key);
}
//创建共享内存
bool Create()
{
_shmid = shmget(_key,_size,shmflg);
if(_shmid<0)
{
perror("shmget");
return false;
}
printf("创建共享内存成功,shmid:%d\n",_shmid);
return true;
}
//建立进程和共享内存间的映射
bool Attach()
{
_atchaddr = shmat(_shmid,nullptr,0);
if((long long)_atchaddr<0)
{
perror("shmat");
return false;
}
printf("链接成功,起始地址为:%p\n",_atchaddr);
return true;
}
//解除进程和共享内存间的映射
bool Detach()
{
int n = shmdt(_atchaddr);
if(n<0)
{
perror("shmdt");
return false;
}
printf("成功解除进程虚拟地址空间和共享内存间的映射\n");
return true;
}
//释放掉共享内存
bool Remove()
{
int n = shmctl(_shmid,IPC_RMID,nullptr);
if(n<0)
{
perror("shmctl");
return false;
}
printf("删除共享内存成功!\n");
return true;
}
~SharedMemory(){}
private:
int _shmid;
key_t _key;
size_t _size;
void* _atchaddr;
};
#endif
cpp
//test.cpp
#include"Shm.hpp"
int main()
{
SharedMemory sm;
sm.Create();
sleep(3);
sm.Attach();
sleep(3);
sm.Detach();
sleep(3);
sm.Remove();
return 0;
}
每两秒检测一下共享内存信息:

共享内存的size一般是4096bytes(4KB)的整数倍,如果我们设置一个不是4096整数倍的size,OS会进行内核层面的4KB对齐,但是我们用ipcs -m查询共享内存信息的时候查到的bytes还是你设置的大小,你所能用的空间也只有你设置得内存大小,只是操作系统在底层为了对齐规则会多给你空间,但你没法使用多余的空间。
4.2 共享内存通信案例
前面的步骤其实就是在建立通信信道,下面我们就可以利用共享内存开始进行真正的通信了。
cpp
//Shm.hpp
#ifndef __SHM_HPP__
#define __SHM_HPP__
#include<iostream>
#include<functional>
#include<unistd.h>
#include<sys/ipc.h>
#include<sys/shm.h>
#include<sys/types.h>
std::string pathname = ".";
int proj_id = 0x123;
size_t defaultsize = 4096;//共享内存的大小一般是4096的整数倍
//共享内存
class SharedMemory
{
private:
bool comm(int shmflg)
{
_shmid = shmget(_key,_size,shmflg);
if(_shmid<0)
{
perror("shmget");
return false;
}
printf("创建共享内存成功,shmid:%d\n",_shmid);
return true;
}
public:
SharedMemory(size_t size = defaultsize)
:_size(size)
,_shmid(-1)
,_windex(0)
,_rindex(0)
{
_key = ftok(pathname.c_str(),proj_id);
if(_key<0)
{
perror("ftok");
}
else
printf("键值key创建成功:0x%x\n",_key);
}
//向共享内存里写不同类型字符
void AddChar(char c)
{
((char*)_atchaddr)[_windex++] = c;
((char*)_atchaddr)[_windex] = '\0';
_windex%=_size;
printf("写入字符:%c\n",c);
}
void AddInt(int x)
{
((int*)_atchaddr)[_windex++] = x;
_windex %= _size;
}
void AddString(std::string s)
{
((std::string*)_atchaddr)[_windex++] = s;
_windex %= _size;
}
//从共享内存里读
void PopAddr()
{
if(*((char*)_atchaddr) == '\0')
return;
printf("获取共享内存中的数据:%s\n",(char*)_atchaddr);
}
//创建全新的共享内存(带上权限0666)
bool Create()
{
return comm(IPC_CREAT|0666);
}
//获取共享内存
bool Get()
{
return comm(IPC_CREAT);
}
//建立进程和共享内存间的映射
bool Attach()
{
_atchaddr = shmat(_shmid,nullptr,0);
if((long long)_atchaddr<0)
{
perror("shmat");
return false;
}
printf("链接成功,起始地址为:%p\n",_atchaddr);
return true;
}
//解除进程和共享内存间的映射
bool Detach()
{
int n = shmdt(_atchaddr);
if(n<0)
{
perror("shmdt");
return false;
}
printf("成功解除进程虚拟地址空间和共享内存间的映射\n");
return true;
}
//释放掉共享内存
bool Remove()
{
int n = shmctl(_shmid,IPC_RMID,nullptr);
if(n<0)
{
perror("shmctl");
return false;
}
printf("删除共享内存成功!\n");
return true;
}
~SharedMemory(){}
private:
int _shmid;
key_t _key;
size_t _size;
void* _atchaddr;
int _windex;
int _rindex;
};
#endif
cpp
//client.cpp------向共享内存中写入数据
#include"Shm.hpp"
int main()
{
SharedMemory sm;
sm.Get();//获取共享内存
sleep(3);
sm.Attach();//链接
for(char c = 'A';c<='Z';c++)
{
sm.AddChar(c);
sleep(1);
}
sleep(3);
sm.Detach();//解除链接
return 0;
}
//server.cpp------从共享内存中读取数据
#include"Shm.hpp"
int main()
{
SharedMemory sm;
sm.Create();//创建
sleep(3);
sm.Attach();//链接
while(1)
{
sm.PopAddr();
sleep(1);
}
sm.Detach();//解除链接
sleep(3);
sm.Remove();//删除
return 0;
}
4.3 共享内存的特点
共享内存如果不释放就会导致系统级别的内存泄漏问题,
1. 最高效的IPC方式
直接内存访问:进程直接读写同一块物理内存
零拷贝:数据不需要在内核和用户空间之间来回拷贝
速度最快:相比管道、消息队列等IPC方式性能最优
2. 进程间通信机制
多进程共享:多个进程可以映射到同一块物理内存
双向通信:支持进程间的双向数据交换
实时性高:数据立即可见,无需额外同步机制(需要配合同步原语)
3. 内存管理特性
持久性:创建后一直存在,直到显式删除或系统重启,所以我们创建共享内存后要记得及时删除
固定大小:创建时指定大小,无法动态调整
内核管理:由操作系统内核统一分配和管理


