进程间通信(IPC -> Inter Process Communication)是指不同进程之间交换信息或同步其行为的机制。它的主要目的是使进程之间能够相互协作、共享数据,以实现更加复杂的功能。
进程间通信的本质是运行不同进程之间进行数据交换和共享,从而协作完成特定任务。由于每个进程都是独立的执行体,其运行环境和数据空间都是相互隔离的,因此进程间通信需要借助操作系统提供的通信机制。
为了实现进程间通信,操作系统提供了一些机制,这些机制允许进程在共享的内存区域或是操作系统内核中的数据结构上进行读写操作,以便实现数据的传输和共享。在使用这些机制时,需要注意进程同步和互斥的问题,以保证数据的正确性和完整性。

因此,IPC 的本质是通过操作系统提供的通信机制,让不同的进程之间进行数据交换和共享,从而协同完成任务。
管道
System V IPC
POSIX IPC
常见的 IPC 机制如上所示,它们有各自不同的特点和适用场景,根据实际的需求选择合适的机制可以有效的提高进程间通信的效率和可靠性。
在 IPC 中,管道是一种半双工的通信方式,用于在两个进程之间传递数据。管道哦通常用于在父子进程之间进行数据传输。
例如,统计我们当前使用的云服务器上登录的用户个数:

其中,who 命令和 wc 命令是两个不同的程序,当他们运行起来后就变成了两个进程。who 进程通过标准输出将数据写入 ‘‘管道’’ 中,wc 进行通过标准输出从 ‘‘管道’’ 中读取数据,命令中的 | 就表示管道,它们以此来实现两个进程之间的数据共享。

匿名管道是一种进程间通信方式,它是一种半双工通信方式,即数据只能单向流动。匿名管道只能在具有情缘关系的进程之间使用,如父子进程之间的通信。
匿名管道在创建时,操作系统会在内存中开辟一个缓冲区作为数据传输的通道,这个缓冲区的大小是有限制的。管道只能顺序进行读写,先写入的数据先被读出,因此匿名管道具有一定的局限性,仅适用于较小的数据量传输。
pipe 函数用于创建匿名管道,pipe 函数的语法如下:
#include
int pipe(int pipefd[2]);
返回值:成功则返回 0。失败则返回 -1,并设置对应的错误码。
说明:

创建匿名管道实现父子进程间通信的过程中,需要 pipe 函数和 fork 函数配合使用,如下所示:父进程是写端,子进程是读端。
step1: 父进程调用 pipe 函数创建管道。

step2:父进程调用 frok 创建子进程。

step3:父进程关闭读端,子进程关闭写端。

step1:父进程创建管道。

step2:父进程调用 fork 创建子进程。

step3:父进程关闭 fd[0],子进程关闭 fd[1]。

示例:该代码使用匿名管道实现父子进程之间的通信。父进程向管道中写入数据,子进程从管道中读取数据。代码主要过程如下:
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
// 演示pipe通信的基本过程 -- 匿名管道
#define NUM 1024
int main()
{
// 1.创建管道
int pipefd[2] = {0};
if (pipe(pipefd) != 0)
{
cerr << "pipe error" << endl;
return 1;
}
// 2.创建子进程
pid_t id = fork();
if (id < 0)
{
cerr << "fork error" << endl;
return 1;
}
else if (id == 0)
{
// child process
// 子进程用来进行读取,因此,子进程应该关闭写端 -> pipefd[1]
close(pipefd[1]);
char buffer[NUM] = {0};
ssize_t s;
while ((s = read(pipefd[0], buffer, sizeof(buffer) - 1)) > 0)
{
cout << "时间戳:" << (uint64_t)time(nullptr) << endl; // 打印子进程读取数据的时间
// 读取成功
buffer[s] = '\0';
cout << "子进程读取数据成功,数据是:" << buffer << endl;
}
close(pipefd[0]);
exit(0);
}
else
{
// parent process
// 父进程用来写入数据,因此,父进程应该关闭读端
close(pipefd[0]);
const char *msg = "这是一条由父进程发出的数据->";
int cnt = 1;
while (cnt <= 5)
{
char sendBuffer[NUM];
sprintf(sendBuffer, "%s : %d", msg, cnt);
write(pipefd[1], sendBuffer, strlen(sendBuffer));
cnt++;
sleep(1);
}
close(pipefd[1]);
cout << "父进程数据已写完!!!" << endl;
}
pid_t ret = waitpid(id, nullptr, 0);
if (ret > 0)
cout << "等待子进程成功!" << endl;
return 0;
}
运行结果如下所示:

以下代码实现了一个父子进程之间通过匿名管道进行通信,实现了父进程向子进程派发任务的功能。具体实现过程如下:
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
typedef void (*functor)(); // 定义一个名为functor的函数指针
vector<functor> functors; // 方法集合
// for debug
unordered_map<uint32_t, string> info;
void f1()
{
cout << "执行处理日志的任务,进程ID -> " << getpid() << " , "
<< "执行时间:" << (uint64_t)time(nullptr) << endl;
}
void f2()
{
cout << "执行备份数据的任务,进程ID -> " << getpid() << " , "
<< "执行时间:" << (uint64_t)time(nullptr) << endl;
}
void f3()
{
cout << "处理网路连接的任务,进程ID -> " << getpid() << " , "
<< "执行时间:" << (uint64_t)time(nullptr) << endl;
}
void loadFunctor()
{
info.insert({functors.size(), "执行处理日志的任务"});
functors.push_back(f1);
info.insert({functors.size(), "执行备份数据的任务"});
functors.push_back(f2);
info.insert({functors.size(), "处理网路连接的任务"});
functors.push_back(f3);
}
void errno_exit(const char *msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
// 父进程控制子进程
int main()
{
// 1.加载任务列表
loadFunctor();
// 2.创建管道
int pipefd[2] = {0};
if (pipe(pipefd) != 0)
errno_exit("pipe");
// 3.创建子进程
pid_t id = fork();
if (id < 0)
errno_exit("fork");
else if (id == 0)
{
// 子进程用于执行父进程派发的任务
// 3.关闭不需要的文件描述符
close(pipefd[1]);
// 4.处理任务
uint32_t operatorType = 0;
ssize_t s;
while ((s = read(pipefd[0], &operatorType, sizeof(uint32_t))) > 0)
{
if (operatorType < functors.size())
functors[operatorType]();
else
cerr << "bug? operatorType = " << operatorType << endl;
}
if (s == 0)
cout << "子进程退出了..." << endl;
else
errno_exit("read");
// 关闭文件描述符
close(pipefd[0]);
exit(0);
}
else
{
// 父进程用于向派发任务
// 3.关闭不需要的文件描述符
close(pipefd[0]);
// 4.指派任务
srand((long long)time(nullptr));
int num = functors.size();
int cnt = 7;
while (cnt--)
{
// 5.形成任务码
uint32_t commandCode = rand() % num;
cout << "父进程指派任务:" << info[commandCode] << " 任务编号为:" << cnt << endl;
// 向指定的进程下达执行任务的操作
ssize_t s = write(pipefd[1], &commandCode, sizeof(uint32_t));
if (s != sizeof(uint32_t))
errno_exit("write");
sleep(1);
}
close(pipefd[1]);
pid_t res = waitpid(id, nullptr, 0);
if (res)
cout << "wait success!" << endl;
}
return 0;
}
运行结果如下:

以下代码实现了一个基于进程间通信的任务调度器。其中,父进程负责派发任务,子进程负责处理任务。子进程通过管道从父进程获取任务,然后执行。具体实现过程如下:
代码如下:
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
using functor = void (*)(); // 定义一个名为functor的函数指针
vector<functor> functors; // 方法集合
// for debug -> 将编号与任务对应
unordered_map<uint32_t, string> info;
void f1()
{
cout << "执行处理日志的任务,进程ID -> " << getpid() << " , "
<< "执行时间:" << chrono::system_clock::now().time_since_epoch().count() << endl;
}
void f2()
{
cout << "执行备份数据的任务,进程ID -> " << getpid() << " , "
<< "执行时间:" << chrono::system_clock::now().time_since_epoch().count() << endl;
}
void f3()
{
cout << "处理网路连接的任务,进程ID -> " << getpid() << " , "
<< "执行时间:" << chrono::system_clock::now().time_since_epoch().count() << endl;
}
void loadFunctor()
{
// 插入函数时使用 emplace_back() 函数,效率更高
info.emplace(functors.size(), "执行处理日志的任务");
functors.emplace_back(f1);
info.emplace(functors.size(), "执行备份数据的任务");
functors.emplace_back(f2);
info.emplace(functors.size(), "处理网路连接的任务");
functors.emplace_back(f3);
}
void errno_exit(const char *msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
// int32_t:进程pid,该进程对应的管道写端fd
typedef pair<int32_t, int32_t> elem;
const int32_t processNum = 5;
void work(int32_t blockFd)
{
cout << "进程 [" << getpid() << "] 开始工作" << endl;
// 子进程核心工作代码
while (true)
{
// a.阻塞等待 b.获取任务
uint32_t operatorCode = 0;
ssize_t s = read(blockFd, &operatorCode, sizeof(uint32_t));
if (s == 0)
break;
assert(s == sizeof(uint32_t));
(void)s;
// c.处理任务
if (operatorCode < functors.size())
functors[operatorCode]();
sleep(1);
}
cout << "进程 [" << getpid() << "] 结束工作" << endl;
}
// [子进程的pid,子进程的管道fd] -> 父进程负责派发任务
void dispatchTask(const vector<elem> &processFds)
{
random_device rand;
mt19937 gen(rand());
while (true)
{
// 随机选择一个进程
uniform_int_distribution<int32_t> dist(0, processFds.size() - 1);
int32_t pick = dist(gen);
// 选择一个任务
uniform_int_distribution<int32_t> taskDist(0, functors.size() - 1);
uint32_t task = taskDist(gen);
// 将任务派发给一个指定的进程
ssize_t s = write(processFds[pick].second, &task, sizeof(task));
// 打印对应的提示信息
cout << "父进程指派任务:" << info[task] << "到进程:" << processFds[pick].first << " 编号:" << pick << endl;
(void)s;
sleep(1);
}
}
int main()
{
// 加载任务列表
loadFunctor();
vector<elem> assignMap;
// 创建processNum个进程
for (int i = 0; i < processNum; ++i)
{
// 定义保存管道fd的对象
int pipefd[2] = {0};
// 创建管道
if (pipe(pipefd) != 0)
errno_exit("pipe");
// 创建子进程
pid_t id = fork();
if (id < 0)
errno_exit("fork");
else if (id == 0)
{
// 子进程读取
close(pipefd[1]);
// 子进程执行
work(pipefd[0]);
close(pipefd[0]);
exit(0);
}
close(pipefd[0]);
elem e(id, pipefd[1]);
assignMap.push_back(e);
}
cout << "create all process success!" << endl;
// 父进程派发任务
dispatchTask(assignMap);
// 回收资源
for (int i = 0; i < processNum; ++i)
{
if (waitpid(assignMap[i].first, nullptr, 0) > 0)
cout << "wait for: " << assignMap[i].first << " success!"
<< "number:" << i << endl;
close(assignMap[i].second);
}
return 0;
}
运行结果如下:

pipe2 与 pipe 函数的功能类似,与 pipe 函数不同的是,pipe2 函数提供了额外的选项参数。
#include
#include
int pipe2(int pipefd[2],int flags);

pipe2 函数可以设置管道的阻塞或非阻塞模式,或者可以指定管道在进程终止时是否自动关闭。
1️⃣ 当没有数据可读时:
2️⃣ 当管道满的时候:
3️⃣ 如果所有管道写端对应的文件描述符被关闭,read 调用会返回 0,表示管道已被关闭。
4️⃣ 如果所有管道读端对应的文件描述符被关闭,write 调用会产生 SIGPIPE 信号,进而可能导致 write 进程退出。
5️⃣ 当要写入的数据量不大于 PIPE_BUF 时,Linux 会保证写入操作的原子性,即写入操作要么完整地成功执行,要么完全不执行。
6️⃣ 当要写入的数据量大于 PIPE_BUF 时,Linux 不再保证写入操作的原子性,即写入操作可能会被中断或分成多个部分执行。
管道是一种常用的 IPC 机制,它的特点如下:
匿名管道只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信;通常,一个管道由一个进程创建,然后该进程调用 fork ,此后,父子进程就可以使用该管道了。 🎯
管道的生命周期随进程。 🎯
当一个进程创建了一个管道后,该管道就存在了,并可以被多个相关进程使用。这些进程中,只要有一个进程仍在使用管道,管道就会一直存在,直到所有相关进程都关闭了管道的读端和写端,管道才会被系统回收和释放。需要注意的是,若管道的创建进程异常终止或被终止,管道也会被自动关闭和回收。
管道是一种基于字节流的通信机制,进程无法控制消息的大小和边界。 🎯
进程 A 写入管道的数据,进程 B 每次从管道中读取的数据多少是任意的 ,这种被称为流式服务,与之相对应的是数据报服务。
注意:由于进程无法控制数据的大小和边界,使用管道进行通信时需要注意缓冲区的大小和数据的完整性。若管道的缓冲区大小不足以容纳将要发送的数据,写入进程会被阻塞,直到有足够的空间可用为止。若管道中的数据被拆分成多个字节流,进程可能需要自己处理字节流的拆分和组合,以确保数据的完整性。
管道内部自带同步互斥机制。 🎯
管道内部自带同步机制,是因为管道在内部实现了一个缓冲区,当进程向管道写入数据时,数据会被先存在管道的缓冲区中,直到缓冲区满或者达到一定的阈值才会被发送到管道的读端,这个过程中,若有多个进程同时向管道写入数据,管道会自动对写入的数据进行排队和同步操作,避免了进程间的竞争条件。
管道内部的互斥机制,是因为管道本身只有一个读端和一个写端,进程需要先获得管道的读写权限,才能进行读写操作。若多个进程同时向管道写入数据,写入的数据顺序会被自动排队,避免了数据冲突和互相覆盖的情况。同样的,若多个进程同时从管道中读取数据,读取的顺序也会被自动排队,避免了数据的丢失和混淆。
管道是一种半双工的通信机制,即同一时间只能有一个进程进行读操作,另一个进程进行写操作。 🎯
在数据通信中,数据在线路上的传送方式分为以下三种:
管道是半双工的,数据只能向一个方向流动,需要双方通信时,需要建立起两个管道。

在 IPC 中,管道的大小一般是指管道缓冲区的大小,也可以称为管道的容量。管道缓冲区是用来暂存数据的区域,当管道写满时,继续写入数据会阻塞写入进程;当管道读空时,继续读取数据会阻塞读取进程。
那么我们怎么才能直到管道的最大容量呢?
方法一:man 手册
使用命令 man 7 pipe ,可以查看 pipe 的容量信息,如下所示:在 2.6.11 之前的 Linux 中,管道的最大容量与系统页面大小相同;在 2.6.11 之后的 Linux 中,管道的最大容量为 65536 字节。

可以使用 uname -r 查看当前的 Linux 版本:

方法二:ulimit 命令
可以使用 ulimit -a 来查看当前资源的限制设置。

管道缓冲区的大小由内核自动分配,一般为一页大小(4KB 或 8KB),也可以在创建管道时通过 fcntl() 函数来设置缓冲区大小。在实际使用中,可以通过减小缓冲区大小来降低管道的延迟,但也可能导致管道写满时频繁阻塞的问题。
管道的应用限制是由于管道只存在于父子进程之间的共享内存中,而不是存在于文件系统中,因此只能在具有共同祖先的进程之间通信。若想要在没有亲缘关系的进程之间交换数据,可以使用 FIFO 文件,也被称为命名管道。
命名管道是一种特殊类型的文件,它可以被多个进程同时打开,从而实现不相关进程之间的通信。与管道不同的是,命名管道存在于文件系统中,具有文件名,多个进程可以通过打开相同的文件名来访问同一个命名管道,从而实现数据的交换。因此,命名管道不仅可以用于不相关的进程之间的通信,还可以用于相对独立的进程之间的通信,例如:在不同的终端或远程系统之间交换数据。
命名管道可以从命令行上创建,可以使用以下命令:
$ mkfifo filename
其中,filename 是创建的命名管道的名称。这个命令用于创建一个新的 FIFO 文件,即命名管道。并将其添加到文件系统中。

从创建出的 fifo 文件可看出,其文件类型为 p ,表示该文件是一个管道文件。
如下所示:在一个进程中向使用 shell 脚本向命名管道每隔 1 秒写入一个字符串,在另外一个进程中通过 fifo 就可以进程读取管道中的数据了。

当管道的读端进程退出之后,写端的进程再向管道中写入数据就没有意义了,因此读端退出,写端就会被操作系统杀掉。如下,我们可以进行验证:因为写端执行的循环脚本是由命令行解释器 bash 执行的,当终止读端进程时,bash 就会被操作系统杀掉。

在程序中可以通过 mkfifo 函数在文件系统中创建一个命名管道,它通过文件路径名来标识一种特殊类型的文件。
函数如下:
#include
#include
int mkfifo(const char *pathname, mode_t mode);
说明:
返回值:
如果调用成功,mkfifo() 返回 0 。如果出现错误,将返回 -1,对应的错误码将被设置。
示例:使用 mkfifo() 在代码所在的路径下,创建一个名为 myfifo 的命名管道。

// 使用mkfifo函数创建一个名为myfifo的命令管道
#include
#include
#include
#include
#include
using namespace std;
#define FILE_NAME "myfifo"
int main()
{
// 判断是否已经存在同名的命名管道
if (access(FILE_NAME, F_OK) != -1)
{
cout << "Named pipe " << FILE_NAME << " already exists!" << endl;
return 0;
}
// 将文件默认的权限掩码设置为0,使得创建的文件权限与给出的权限对应
umask(0);
if (mkfifo(FILE_NAME, 0666) < 0)
{
perror("mkfifo");
return 1;
}
cout << "Named pipe created successfully!" << endl;
return 0;
}
运行程序,myfifo 命令管道创建成功:

命名管道的打开规则会根据打开操作的类型不同而有不同的行为。
如果当前打开操作是为读而打开 FIFO 时:
如果当前打开操作是为写而打开 FIFO 时:
以下两份代码实现了一个基于管道的数据传输功能。第一份代码 read.cc 打开了一个名为 “test” 的文件,将其读入到缓冲区,然后将数据写入一个名为 “tp” 的管道文件。第二份代码 write.cc 从 “tp” 管道文件读入数据,并将数据写入到 “test.bak” 文件中,最后删除 “tp” 管道文件。
read.cc 代码如下:
#include
#include
#include
#include
#include
#include
void error_exit(const char *msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
#define NUM 1024
int main()
{
// 对FIFO文件是否存在进行检测,如果存在则删除
if (access("tp", F_OK) == 0)
{
if (unlink("tp") == -1)
error_exit("unlink");
}
mkfifo("tp", 0664);
int infd = open("test", O_RDONLY);
if (infd == -1)
error_exit("open");
int outfd = open("tp", O_WRONLY);
if (outfd == -1)
error_exit("open");
char buffer[NUM];
int n;
// 从test中读取数据到buffer中
while ((n = read(infd, buffer, sizeof(buffer)-1)) > 0)
{
// 将读到的buffer中的数据写入管道tp中
buffer[n]='\0';
write(outfd, buffer, n);
}
close(infd);
close(outfd);
return 0;
}
write.cc 代码如下:
#include
#include
#include
#include
#include
#include
#include
#include
void errno_exit(const char *msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
#define NUM 1024
int main()
{
// 检测拷贝的目标文件是否存在,存在则删除
if (access("test.bak", F_OK) == 0)
{
if (unlink("test.bak") == -1)
errno_exit("unlink");
}
int outfd = open("test.bak", O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (outfd == -1)
errno_exit("open");
int infd = open("tp", O_RDONLY);
if (infd == -1)
errno_exit("open");
char buffer[NUM];
int n, m;
while ((n = read(infd, buffer, sizeof(buffer))) > 0)
{
m = write(outfd, buffer, n);
if (m != n)
{
errno_exit("write");
}
}
if (n == -1)
errno_exit("read");
close(infd);
close(outfd);
if (unlink("tp") == -1)
errno_exit("unlink");
return 0;
}
运行结果如下:

使用管道实现的文件拷贝有什么意义?
在这里使用管道在本地进行文件拷贝,似乎没有什么意义。但是你可以将管道比作 ”网络“,将客户端比作 ”Windows Xshell“ ,将服务端看作 ”centos 服务器“。那么实现的就是文件的上传过程,反过来则是文件的下载功能。

以下代码实现了基于命名管道的进程间通信,即一个进程作为服务端,另一个进程作为客户端,客户端可以向服务端发送消息,服务端可以接收客户端发送的消息,并将其打印到输出终端上。
公共头文件 comm.h 代码:
#pragma once
#include
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
#define IPC_PATH "./.fifo" // 命名管道路径
#define NUM 1024 // 缓冲区大小
// 错误处理函数,输出错误信息并退出程序
void errno_exit(const char *msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
server.cpp 代码如下:
#include "comm.h"
int main()
{
// 设置当前进程的文件模式创建屏蔽字,即将屏蔽掉文件权限位中的某些权限
umask(0);
// 创建命名管道
if (mkfifo(IPC_PATH, 0666) != 0)
errno_exit("mkfifo");
// 打开管道,获取文件描述符
int pipefd = open(IPC_PATH, O_RDONLY);
if (pipefd < 0)
errno_exit("open");
// 正常通信过程
char buffer[NUM];
ssize_t s;
while ((s = read(pipefd, buffer, sizeof(buffer) - 1)) > 0)
{
// 在读取的数据后加上字符串结束符
buffer[s] = '\0';
cout << "client -> server# " << buffer << endl;
}
if (s == 0)
cout << "client exit!" << endl;
else
cout << "read:" << strerror(errno) << endl;
// 关闭文件描述符
close(pipefd);
cout << "server exit!" << endl;
// 删除命名管道
unlink(IPC_PATH);
return 0;
}
client.cpp 代码如下:
#include "comm.h"
int main()
{
int pipefd = open(IPC_PATH, O_WRONLY);
if (pipefd < 0)
errno_exit("open");
char line[NUM] = {0};
while (true)
{
printf("请输入你的消息# ");
fflush(stdout);
// fgets -> c -> line结尾自动添加\0
if (fgets(line, sizeof(line), stdin) != nullptr)
{
line[strlen(line) - 1] = '\0'; //去掉fgets读入的换行符
write(pipefd, line, strlen(line));
}
else
break;
}
close(pipefd);
cout << "client exit!" << endl;
return 0;
}
代码编写完毕之后,先将 server.cpp 生成的可执行程序运行起来,接下来在客户端就可以看见服务端创建的命名管道:

接下来运行 client.cpp 生成的可执行程序,此时,我们在客户端输入信息,服务端就会将输入的信息打印到终端上:

匿名管道和命名管道都是进程间通信的一种方式,它们的主要区别就在于创建和打开的方式不同。
匿名管道通过 pipe 函数创建,只能在有亲缘关系的进程间使用,并且生命周期也是在进程间通信期间,通信结束后自动销毁。
命名管道(FIFO)通过 mkfifo 函数创建,需要指定管道的名字,并且可以被不相关的进程所共享。命名管道在创建之后,可以被不同的进程打开,并通过读写管道中的数据进行通信。
在命令行中输入以下命令:
$ ls -l | wc -l
该命令将 ls 命令的输出通过管道传递给 wc 命令。ls -l 命令的输出是当前目录下所有文件和文件夹的列表,wc -l 命令将其作为输入并计算行数。如果管道工作正常,则输出结果应该是当前目录下文件和文件夹的总数。

那么命令行中的管道 “|” 属于什么类型的管道呢?
匿名管道只能用于具有亲缘关系的进程之间通信,而命名管道可以用于两个不相关的进程之间的通信。因此,我们可以查看命令行中用管道 “|” 连接起来的进程之间是否具有亲缘关系来判断 “|” 管道的类型。
如下所示:使用管道 “|” 连接了三个进程,通过 ps 命令查看这三个进程发现,这三个进程的 PPID 都是相同的,即它们是由同一个父进程创建的子进程。

查看它们的父进程 bash :

由此看出,管道 “|” 连接起来的各个进程之间是具有亲缘关系的。若两个进程之间使用的是命名管道,那么在磁盘上一定有一个对应的命名管道文件名,但实际上,我们在命令行使用管道 “|” 时不存在类似的命名管道文件名,因此命令行中的管道实际上是匿名管道。
共享内存是 Linux 和其他类 Unix 系统中可用三种进程间通信机制之一。对于共享内存,内核创建共享内存段,并将其映射到请求进程的地址空间的数据段。进程可以像使用其他地址空间的任何其它全局变量一样使用共享内存。
共享内存区是最快的 IPC 形式,一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到内核,换句话说是进程不再指向进入内核的系统调用来传递彼此的数据。
共享内存其基本原理是让多个进程共享同一块物理内存,这样这些进程就可以在这块共享内存中读写数据,从而实现数据共享。为了实现共享内存,操作系统会在物理内存中分配一块连续的内存空间,并在各个进程的虚拟地址空间中开辟与之对应的地址空间。各个进程可以通过这些虚拟地址访问共享内存,操作系统负责将虚拟地址翻译成物理地址,从而实现共享内存的访问。

在操作系统中,可能存在大量的进程进行通信,因此也会有大量的共享内存。为了对这些共享内存进行管理,操作系统会为每个共享内存维护一个共享内存数据结构。在这个数据结构中,可以获取到该共享内存的各种元信息,并进行管理。
共享内存的数据结构如下:
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 */
};
共享内存是用于进程间通信的一种方式,它需要一个唯一的标识符来标识系统中的共享内存。这个标识符就是我们在创建共享内存时传递的 key 值,它的作用类似于文件系统中的文件名。不同的进程可以通过这个 key 值来访问同一块共享内存,实现进程间通信。
在共享内存的数据结构中,shm_perm 成员变量储存了共享内存的权限信息,包括创建者的 UID 和 GID,访问权限等信息。同时,shm_perm 中包含了用于标识共享内存的 IPC 键值(即 key),其中 ipc_perm 结构体的定义如下:
struct ipc_perm {
key_t key; /* Key supplied to shmget(2) */
uid_t uid; /* Effective UID of owner */
gid_t gid; /* Effective GID of owner */
uid_t cuid; /* Effective UID of creator */
gid_t cgid; /* Effective GID of creator */
mode_t mode; /* Permissions */
unsigned short seq; /* Sequence number */
};
💫 想要使用 System V IPC 机制,我们需要创建一个 System V IPC key。 我们使用 ftok 函数来进行创建。
ftok() : 将路径名和项目标识符转换为 System V IPC key。
函数定义:
#include
#include
key_t ftok(const char *pathname, int proj_id);
说明:
fork() 函数使用给定的路径名命名的文件的标识(必须引用一个现有的、可访问的文件)和 proj_id 的最低有效 8 位(必须是非零的)来生成一个 key_t 类型的 System V IPC key。
返回值:
调用成功时,返回生成的 key 值。如果失败,返回 -1,errno 标识 stat(2) 系统调用的错误。
示例:
#include
#include
#include
#include
#include
#include
#include
#include
#define PATH_NAME "/home/hyr/linux_code/linux19"
#define PROJ_ID 0x16
key_t CreateKey()
{
key_t key = ftok(PATH_NAME, PROJ_ID);
if (key < 0)
{
std::cerr << "ftok:" << strerror(errno) << std::endl;
exit(-1);
}
return key;
}
int main()
{
std::cout << CreateKey() << std::endl;
return 0;
}
运行结果:

系统提供了一些函数用于创建和操作共享内存,如:shmget、shmat、shmflg、shmctl 。下面进行介绍。
shmget - 分配一个 System V 共享内存。
函数定义:
#include
#include
int shmget(key_t key, size_t size, int shmflg);
参数说明:
key:共享内存段的标识符,可以是任意整数值,通常使用 ftok 函数将一个路径名和项目 ID 转换为一个唯一个 key 值。size:共享内存段的大小,以字节为单位。shmflg:共享内存段的访问权限和其它选项的标志,由九个权限标志构成,包括:IPC_CREAT、IPC_EXCL、mode_flags 和 SHM_HUGETLB 等。说明:
shmget() 函数返回一个标识符,该标识符与参数 key 的值相关联,表示 System V 共享内存段。如果 key 的值为 IPC_PRIVATE 或 key 不是IPC_PRIVATE,但是 shmflg 指定了 IPC_CREAT 并且没有与 key 相对应的共享内存段,则将创建一个新的共享内存段,其大小等于 size 的值向上舍入到 PAGE_SIZE 的倍数。
如果 shmflg 同时指定了 IPC_CREAT 和 IPC_EXCL ,并且 key 已经存在一个共享内存段,则 shmget() 会失败,并将 errno 设置为 EEXIST。
返回值: 调用成功,返回共享内存的标识符。调用失败,则返回 -1。
变量 shmflg 常用的值如下:
IPC_CREAT:用于创建一个新的共享内存段。如果不使用此标志,则 shmget() 将查找与关键字 key 相关联的段,并检查用户是否有访问该段的权限。
IPC_EXCL:与 IPC_CREAT 一起使用,以确保如果段已经存在,则操作失败。
接下来,我们使用 ftok 函数和 shmget 函数来创建一块共享内存,然后打印出共享内存的相关值,以便于进行观察:
#include
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
#define PATH_NAME "/home/hyr/linux_code/linux19" // 路径名
#define PROJ_ID 0x16 // 整数标识符
#define SIZE 4096 // 共享内存大小
// 创建一个全新的共享内存
const int flag = IPC_CREAT | IPC_EXCL | 0666;
key_t CreateKey()
{
key_t key = ftok(PATH_NAME, PROJ_ID);
if (key < 0)
{
std::cerr << "ftok:" << strerror(errno) << std::endl;
exit(-1);
}
return key;
}
int main()
{
key_t key = CreateKey();
int shmid = shmget(key, SIZE, flag);
if (shmid < 0)
{
perror("shmget");
return 1;
}
printf("key: %x\n", key);
printf("shmid: %d\n", shmid);
return 0;
}
运行结果如下所示:

ipcs 是一个 Unix/Linux 下的命名行工具,用于查询当前系统中的 IPC 资源的使用情况,以及列出系统上 IPC 对象的信息。

使用 ipcs 命令时,若只想查看特定类型的 IPC 相关信息,可以携带以下选项:
-q:列出消息队列的相关信息。-m:列出共享内存相关信息。-s:列出信号量相关信息。ipcs -m 命令输出的每列信息的含义如下:
| key | 共享内存对应的键值 |
|---|---|
| shmid | 共享内存标识符 |
| owner | 创建共享内存的用户的用户名 |
| perms | 共享内存的访问权限 |
| bytes | 共享内存的大小,以字节为单位 |
| nattch | 当前连接到该共享内存的进程数 |
| status | 共享内存状态,通常 0 表示共享内存已被删除,1 表示共享内存还存在 |
shmat() - 将共享内存段连接到进程地址空间
函数定义:
#include
#include
void *shmat(int shmid,const void *shmaddr, int shmflg);
参数说明:
说明:
返回值: 函数执行成功返回共享内存段的首地址,执行失败返回 -1 并设置 errno 变量。
使用 shmat 函数对共享内存进行连接:
#include
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
#define PATH_NAME "/home/hyr/linux_code/linux19" // 路径名
#define PROJ_ID 0x16 // 整数标识符
#define SIZE 4096 // 共享内存大小
// 创建一个全新的共享内存
const int flag = IPC_CREAT | IPC_EXCL | 0666;
key_t CreateKey()
{
key_t key = ftok(PATH_NAME, PROJ_ID);
if (key < 0)
{
std::cerr << "ftok:" << strerror(errno) << std::endl;
exit(-1);
}
return key;
}
int main()
{
key_t key = CreateKey();
// 创建共享内存
int shmid = shmget(key, SIZE, flag);
if (shmid < 0)
{
perror("shmget");
return 1;
}
printf("key: %x\n", key);
printf("shmid: %d\n", shmid);
cout << "attach begin!" << endl;
sleep(2);
// 将共享内和自己的进程产生关联(attach)
char *str = (char *)shmat(shmid, nullptr, 0);
if (str == (char *)-1)
{
perror("shmat");
return 1;
}
sleep(2);
cout << "attach end!" << endl;
return 0;
}
运行程序,使用监控脚本观测发现共享内存挂接成功。

shmdt() - 用于解除当前进程与共享内存之间的映射关系,将共享内存从当前进程的虚拟地址空间中分离出来。
函数定义:
#include
#include
int shmdt(const void *shmaddr);
说明: 其中,参数 shmaddr 是共享内存区域的起始地址。调用 shmdt 函数后,进程就不再拥有该共享区域的访问权限,若后续需要再次访问该共享内存区域,需要通过 shmat 函数重新将其附加到进程的地址空间。
返回值: shmdt 函数调用成功则返回 0 ,调用失败则返回 -1 ,并设置 errno。
如下,我们调用 shmdt 函数来取消进程与共享内存之间的关联:

注意:shmdt 函数只是将共享内存区域与进程的地址空间分离,并没有释放该共享内存区域的物理空间。
通过上面创建共享内存来看,当进程运行完毕之后,申请的共享内存依旧存在,并没有被操作系统释放。实际上,管道的生命周期是随进程的,而共享内存的生命周期是随内核的,也就是说进程虽然已经退出,但是曾经创建的共享内存依旧存在。
因此,若进程不主动删除创建的共享内存,那么共享内存就会一直存在,直到关机重启。因此 IPC 资源是由内核提供并维护的。
这里我们介绍两种方法来释放共享内存:1、命令行释放 2、调用函数进行释放。
命令行释放
可以使用命令 ipcs -m shmid 来释放指定 shmid 的共享内存。
# ipcs -m shmid

shmctl 函数
shmctl() - 用于控制共享内存。
#include
#include
int shmctl(int shmid,int cmd, struct shmid_ds *buf);
说明: shmctl() 函数使用 shmid 标识符标识的共享内存段执行 cmd 指定的控制操作。
参数说明:
返回值: 执行成功时,返回 0;执行失败时,返回 -1,并设置 errno 错误码。
函数第二个参数 cmd 的常用选项如下:
| 命令 | 说明 |
|---|---|
| IPC_STAT | 读取共享内存的状态信息,并将其存储在 shmid_ds 结构体中 |
| IPC_SET | 在进程权限足够的情况下,修改共享内存的状态信息,修改的内存包含在 shmid_ds 结构体中 |
| IPC_RMID | 删除共享内存段 |
| SHM_LOCK | 锁定共享内存 |
| SHM_UNLOCK | 解除共享内存锁定 |
示例代码如下:在进程退出前,将创建的共享内存调用函数删除。
#include
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
#define PATH_NAME "/home/hyr/linux_code/linux19" // 路径名
#define PROJ_ID 0x16 // 整数标识符
#define SIZE 4096 // 共享内存大小
// 创建一个全新的共享内存
const int flag = IPC_CREAT | IPC_EXCL | 0666;
key_t CreateKey()
{
key_t key = ftok(PATH_NAME, PROJ_ID);
if (key < 0)
{
std::cerr << "ftok:" << strerror(errno) << std::endl;
exit(-1);
}
return key;
}
int main()
{
key_t key = CreateKey();
// 创建共享内存
int shmid = shmget(key, SIZE, flag);
if (shmid < 0)
{
perror("shmget");
return 1;
}
printf("key: %x\n", key);
printf("shmid: %d\n", shmid);
cout << "attach begin!" << endl;
sleep(1);
// 将共享内和自己的进程产生关联(attach)
char *str = (char *)shmat(shmid, nullptr, 0);
if (str == (char *)-1)
{
perror("shmat");
return 1;
}
sleep(1);
cout << "attach end!" << endl;
// 将共享内存与进程去关联
cout << "detach begin!" << endl;
sleep(1);
shmdt(str);
cout << "detach end!" << endl;
sleep(1);
// 释放共享内存
sleep(1);
shmctl(shmid,IPC_RMID,nullptr);
sleep(1);
return 0;
}
当程序运行时,我们可以通过监控脚本 while :; do ipcs -m;echo "-----------------------------"; sleep 1; done 来实时观测共享内存的资源分配情况:

以下两端代码实现了一个简单的共享内存通信机制,server.cc 代码用于读取共享内存的内容。client.cc 代码用于向共享内存中写入数据。通过共享内存,这两个程序实现了进程间的数据共享和通信。
两个程序的头文件和共享函数定义在 comm.h,如下:
#pragma once
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
#define PATH_NAME "/home/hyr/linux_code"
#define PROJ_ID 0x14
#define MEM_SIZE 4096
#define FIFO_FIEL ".fifo"
// 创建一个全新的共享内存
const int flag = IPC_CREAT | IPC_EXCL | 0666;
key_t CreateKey()
{
key_t key = ftok(PATH_NAME, PROJ_ID);
if (key < 0)
{
cerr << "ftok : " << strerror(errno) << endl;
exit(-1);
}
return key;
}
server.cc 代码如下:
#include "comm.hpp"
using namespace std;
int main()
{
// 创建共享内存
key_t key = CreateKey();
int shmid = shmget(key, MEM_SIZE, flag);
if (shmid == -1)
{
perror("shmget");
return 1;
}
// 使共享内存与进程进行关联
char *str = static_cast<char *>(shmat(shmid, nullptr, 0));
if (str == reinterpret_cast<char *>(-1))
{
perror("shmat");
return 1;
}
// 使用共享内存
while (true)
{
printf("%s\n", str);
sleep(1);
}
// 解除共享内存关联
if (shmdt(str) == -1)
{
perror("shmdt");
return 1;
}
// 删除共享内存
if (shmctl(shmid, IPC_RMID, nullptr) == -1)
{
perror("shmctl");
return 1;
}
return 0;
}
client.cc 代码如下:
#include "comm.hpp"
// 使用共享内存
int main()
{
// 创建相同的key值
key_t key = CreateKey();
// 获取共享内存
int shmid = shmget(key, MEM_SIZE, IPC_CREAT);
if (shmid <= 0)
{
perror("shmid");
return 1;
}
// 关联共享内存
char *str = static_cast<char *>(shmat(shmid, nullptr, 0));
if (str == reinterpret_cast<char *>(-1))
{
perror("shmat");
return 1;
}
// 使用共享内存
while (true)
{
printf("Please Enter# ");
fflush(stdout);
ssize_t s = read(0, str, MEM_SIZE);
if (s > 0)
str[s] = '\0';
}
// 解除共享内存关联
if (shmdt(str) == -1)
{
perror("shmdt");
return 1;
}
return 0;
}
先后运行服务端和客户端,通过监控脚本 while :; do ipcs -m;echo "-----------------------------"; sleep 1; done 来验证它们是否关联到同一块内存,并且该共享内存当前关联的进程数为 2 ,则表名服务端和客户端成功地建立了与共享内存的连接。

共享内存和管道是两种不同的进行间通信机制,它们的区别和特点如下:
数据交互方式:
进程关系:
通信方式:
适用场景:
因此,共享内存适用于需要高效数据共享和实时交互的场景,管道适用于简单的单向通信需求。