🌌 进程间通信真正要解决的,其实从头到尾只有一个问题:

进程天生彼此独立,它们怎么才能看到同一份资源?

管道给它们一块共同访问的内核缓冲区;

共享内存直接把同一组物理页映射进多个进程;

消息队列让内核替进程保存一条条有边界的消息;

信号量甚至不负责传递数据,而是告诉多个进程:

“现在谁能动这份公共资源。”

把这条主线抓住以后,IPC 就不再是一堆零散接口,而是一套非常自然的设计。


📖 本文关键词

IPC · pipe · fork · FIFO · mkfifo · 共享内存 · ftok · shmget · shmat · 消息队列 · msgsnd · msgrcv · 信号量 · semget · semop · P/V操作

@TOC


1. 为什么需要进程间通信?

学习 IPC 之前,第一件事不是背:

pipe()
shmget()
msgget()
semget()

而是回答:

IPC 为什么会存在?

根本原因是:

1.1 进程具有独立性

Linux 中每个进程都有自己的:

进程地址空间
页表
代码区
数据区
堆
栈
...

例如两个进程:

Process A                 Process B

虚拟地址空间 A            虚拟地址空间 B
┌─────────────┐          ┌─────────────┐
│    Stack    │          │    Stack    │
│     Heap    │          │     Heap    │
│    Data     │          │    Data     │
│    Code     │          │    Code     │
└─────────────┘          └─────────────┘

A 修改自己的变量:

int x = 100;

B 不会突然看到:

x == 100;

因为:

两个进程拥有各自独立的虚拟地址空间。

这保证了进程之间的安全性和独立性,但同时带来了一个新的问题。


1.2 两个独立进程怎么交换数据?

既然:

进程 A 看不到进程 B 的地址空间

那么:

A 不能直接修改 B 的变量
B 也不能直接读取 A 的内存

所以如果我们希望:

Process A
    │
    │  "hello"
    ↓
Process B

必须想办法建立一个:

双方都能够访问的公共区域。

因此,进程间通信的核心可以压缩成一句话:

让不同进程看到同一份 OS 提供的公共资源

这份公共资源具体长什么样,就形成了不同的 IPC 方式。


2. IPC 的统一理解:公共资源不同,通信方式就不同

先不要急着研究接口,我们先看所有 IPC 的共同模型。

Process A
     \
      \
       ↓
   公共资源
       ↑
      /
     /
Process B

例如:

匿名管道
→ 内核管道缓冲区

命名管道
→ 通过文件系统名字找到同一个内核管道

共享内存
→ 多个进程映射同一组物理页

消息队列
→ 内核维护的消息队列

信号量
→ 内核维护的资源计数器

所以真正变化的不是 IPC 的本质。

变化的是:

操作系统到底给这两个进程提供了什么公共资源。

下面沿着这条主线展开。


3. 匿名管道:父子进程之间最经典的通信方式

管道是 Linux IPC 中最经典的一种方式。

先看模型:

父进程
  │
  │ pipe()
  ↓
┌─────────────────┐
│   内核管道缓冲区  │
└─────────────────┘
  ↑             ↓
读端 fd         写端 fd

它有两个文件描述符:

pipefd[0] → 读端
pipefd[1] → 写端

应用程序通过:

read()
write()

访问这个管道。


3.1 为什么叫“匿名管道”?

因为这种管道:

没有一个可以在文件系统中直接找到的路径名。

你执行:

ls

并不会看到:

my_pipe

这种文件。

它主要存在于内核中,并通过:

文件描述符

访问。

所以称为:

Anonymous Pipe —— 匿名管道。


3.2 pipe 接口

函数:

#include <unistd.h>

int pipe(int pipefd[2]);

成功:

返回 0

失败:

返回 -1

其中:

pipefd[0]
→ 管道读端

pipefd[1]
→ 管道写端

注意:

int pipefd[2];
pipe(pipefd);

调用之前:

pipefd 里面没有有效的管道 fd

调用之后,内核把两个 fd 写进去:

pipefd[0] = 某个 fd
pipefd[1] = 某个 fd

所以:

pipefd 是一个输出型参数。


3.3 父子进程为什么能通过匿名管道通信?

这才是匿名管道最重要的原理。

如果两个完全不相关的进程:

Process A
Process B

一个进程自己:

pipe();

另一个进程根本不知道它创建的管道 fd。

所以匿名管道通常借助:

fork 后子进程继承父进程文件描述符。

典型顺序一定是:

pipe
 ↓
fork
 ↓
父子进程共同持有管道 fd

而不是:

fork
 ↓
父进程自己 pipe

假设父进程首先:

int pipefd[2];
pipe(pipefd);

此时:

父进程 fd 表

pipefd[0] ──→ 管道读端
pipefd[1] ──→ 管道写端

随后:

fork();

子进程会继承父进程已经打开的文件描述符。

因此:

              内核管道
            ┌──────────┐
            │          │
            └──────────┘
             ↑        ↑
          读端        写端
          ↑             ↑
     ┌────┴────┐   ┌────┴────┐
     │ 父进程   │   │ 子进程   │
     └─────────┘   └─────────┘

父子进程因此:

共同引用了同一个内核管道。

这就是它们能够通信的根本原因。


3.4 为什么必须关闭不需要的管道端?

假设我们希望:

父进程写
子进程读

那么正确做法通常是:

父进程:
关闭读端
保留写端

子进程:
关闭写端
保留读端

也就是:

Parent                     Child

close(pipefd[0])           close(pipefd[1])

pipefd[1] ──────→ PIPE ──────→ pipefd[0]
     write()                   read()

这样就形成:

单向通信。


为什么不能两个端都留着?

不仅仅是“浪费两个 fd”。

后面的:

EOF
SIGPIPE

判断,都与:

还有没有进程持有管道某一端

密切相关。

这就引出了管道最重要的四种情况。


3.5 管道的四种经典情况

这四个一定要理解,面试非常喜欢问。


情况一:写端还在,但一直不写

读进程调用:

read(pipefd[0], ...);

如果:

管道没有数据
+
还有进程持有写端

那么:

read 阻塞

为什么?

因为内核知道:

还有写端存在,未来仍然可能有人写数据。

所以不能直接告诉读取者:

通信结束了

只能让它继续等待。


情况二:读端不读取,写端疯狂写

管道缓冲区不是无限大的。

假设:

PIPE

[xxxxxxxxxxxxxxxxxxxxxxxxxxxx]
            ↑
           满了

如果:

写进程继续 write
+
读进程一直不读取

最终:

管道被写满

此时默认阻塞模式下:

write 阻塞

直到:

读进程取走一些数据

腾出空间。


于是前两种情况实际上告诉我们:

没有数据
→ read 等待

没有空间
→ write 等待

因此管道天然带有:

一定程度的同步机制。


3.6 情况三:所有写端关闭

假设:

所有持有管道写端的进程

全部:

close(pipefd[1]);

此时读取者继续:

read();

如果管道缓冲区里还有数据:

先把剩余数据读完

等所有数据都读完后:

read()

返回:

0

也就是:

EOF。

这是一个非常关键的判断:

ssize_t n = read(fd, buf, sizeof(buf));

if (n == 0)
{
    // 对方所有写端都关闭了
}

为什么父子进程必须关闭多余的写端?

假设子进程负责读。

但子进程自己却一直没有关闭:

pipefd[1]

即使父进程退出:

系统依然发现:
还有一个写端存在!

这个写端恰好就在子进程自己手里。

于是:

read()

可能一直等不到 EOF。

所以:

关闭不需要的文件描述符不是“代码规范”,而是管道语义正确运行的必要条件。


3.7 情况四:所有读端关闭

反过来。

如果所有管道读端都被关闭:

没人能够读取这个管道

但是某个进程仍然执行:

write(pipefd[1], ...);

继续写数据已经没有意义。

所以内核会向写进程发送:

SIGPIPE

默认动作:

终止进程。

如果程序忽略或捕获了 SIGPIPE,对应的:

write()

通常会失败:

返回 -1
errno = EPIPE

所以:

读端全部关闭
        ↓
继续 write
        ↓
SIGPIPE

也是管道中非常经典的行为。


3.8 四种情况总结

① 管道为空,但仍存在写端
   ↓
   read 阻塞


② 管道满了,但读端不读取
   ↓
   write 阻塞


③ 所有写端关闭
   ↓
   剩余数据读完以后
   read 返回 0(EOF)


④ 所有读端关闭
   ↓
   write
   ↓
   SIGPIPE

前两个体现:

同步

后两个体现:

为什么必须正确关闭多余 fd

3.9 PIPE_BUF 和 PIPE_CAPACITY 千万不要混

管道还有两个经常被混为一谈的概念:

PIPE_BUF

和:

Pipe Capacity

它们不是一回事。


PIPE_CAPACITY

表示:

整个管道缓冲区能够保存多少数据。

也就是:

┌──────────────────────────────────┐
│          Pipe Buffer             │
└──────────────────────────────────┘
←----------- Capacity ------------→

当管道被写满以后:

阻塞 write

PIPE_BUF

它解决的不是“整个管道有多大”。

它主要与:

多个写进程同时向管道写数据时的原子性保证

有关。

POSIX 对不超过:

PIPE_BUF

大小的一次管道写入提供原子写保证。

例如:

Process A 写:"AAAA"

Process B 写:"BBBB"

在满足对应条件时,不希望得到:

AABABABB

而是希望一条写操作作为整体进入管道。

所以必须记住:

PIPE_BUF
→ 原子写保证有关


PIPE_CAPACITY
→ 整个管道缓冲区容量

不要把两个概念混在一起。


3.10 ls | grep cpp 本质上也是管道

我们每天可能都在使用:

ls | grep cpp

中间这个:

|

就是匿名管道。

Shell 大体上会完成这样的工作:

           PIPE
       ┌─────────┐
       │         │
       └─────────┘
        ↑       ↓
        │       │
      stdout   stdin
        ↑       ↓
       ls      grep

Shell:

  1. 创建管道;

  2. fork 出执行 ls 的子进程;

  3. fork 出执行 grep 的子进程;

  4. 把 ls 的标准输出重定向到管道写端;

  5. 把 grep 的标准输入重定向到管道读端。

其中就会用到我们前面文件系统学过的:

dup2();

大致关系:

ls
fd = 1
  ↓ dup2
管道写端


grep
fd = 0
  ↓ dup2
管道读端

于是:

ls 输出的数据
       ↓
     PIPE
       ↓
grep 作为输入读取

这就是 Shell 管道背后的核心原理。


3.11 匿名管道最小完整代码

#include <sys/wait.h>
#include <iostream>
#include <cstring>
#include <unistd.h>

int main()
{
    int pipefd[2];

    if (pipe(pipefd) == -1)
    {
        perror("pipe");
        return 1;
    }

    pid_t pid = fork();

    if (pid < 0)
    {
        perror("fork");
        return 2;
    }
    else if (pid == 0)
    {
        // 子进程负责读,所以关闭写端
        close(pipefd[1]);

        char buf[1024];

        ssize_t n =
            read(pipefd[0], buf, sizeof(buf) - 1);

        if (n > 0)
        {
            // read 读取的是裸字节,需要手动补 '\0'
            buf[n] = '\0';

            std::cout
                << "child get a message: "
                << buf
                << std::endl;
        }

        close(pipefd[0]);

        _exit(0);
    }
    else
    {
        // 父进程负责写,所以关闭读端
        close(pipefd[0]);

        const char* msg = "hello child";

        // write 不认识 C 字符串,
        // 所以要明确告诉它真正写多少字节
        write(pipefd[1], msg, strlen(msg));

        // 写完关闭写端
        close(pipefd[1]);

        waitpid(pid, nullptr, 0);
    }

    return 0;
}

整个过程就是:

pipe
 ↓
fork
 ↓
父子继承相同管道 fd
 ↓
父关闭读端
子关闭写端
 ↓
父 write
 ↓
内核管道
 ↓
子 read

4. 命名管道 FIFO:没有血缘关系怎么办?

匿名管道存在一个明显问题:

pipe 创建以后
↓
双方得能够拿到对应 fd

父子进程可以利用:

fork 的 fd 继承

解决。

但是如果是两个完全独立启动的程序:

server

client

它们之间没有:

fork

关系。

那怎么办?

核心问题仍然是:

双方怎样找到同一份公共资源?


4.1 给管道一个名字

解决办法非常自然:

在文件系统中创建一个有路径的管道对象。

例如:

./myfifo

于是:

Server
   ↓
./myfifo
   ↑
Client

两个互不相关的进程只要约定:

大家都访问 "./myfifo"

就能够找到同一个管道。

这就是:

FIFO / Named Pipe —— 命名管道。


一个非常重要的区别

虽然 FIFO 在文件系统中有:

路径

但这并不意味着通信数据像普通文件那样被不断写入磁盘。

真正用于进程通信的数据仍然走:

内核中的管道机制

文件系统中的路径主要承担:

让两个无血缘关系的进程能够找到同一个管道对象。

所以可以理解成:

FIFO 路径
   ↓
负责“找到它”

真正通信数据
   ↓
内核管道

4.2 创建 FIFO

Shell 可以直接:

mkfifo myfifo

删除:

rm myfifo

程序中则可以使用:

#include <sys/types.h>
#include <sys/stat.h>

int mkfifo(const char* pathname, mode_t mode);

例如:

mkfifo("./myfifo", 0666);

成功:

0

失败:

-1

这里:

0666

同样会受到:

umask

影响。

最终权限仍然遵循:

mode & ~umask

这一套规则。


4.3 FIFO 的生命周期

这一点和匿名管道区别很明显。

程序退出以后:

FIFO 路径不会自动从文件系统消失

所以如果创建:

./myfifo

之后不删除:

ls

仍然能够看到它。

可以使用:

unlink("./myfifo");

或者:

rm myfifo

删除路径。

因此常见服务器流程:

mkfifo
 ↓
open
 ↓
通信
 ↓
close
 ↓
unlink

4.4 open FIFO 时的阻塞规则

FIFO 一个非常值得记住的地方,就是:

open();

本身也可能阻塞。


O_RDONLY

open(path, O_RDONLY);

默认情况下:

等待某个写端打开 FIFO。

也就是:

读者来了
 ↓
没有写者
 ↓
等待

O_WRONLY

open(path, O_WRONLY);

默认情况下:

等待某个读端出现。

写者来了
 ↓
没有读者
 ↓
等待

所以:

FIFO 默认情况下
一端会等待另一端

这本身就是一种同步。


4.5 O_NONBLOCK 下有什么变化?

如果:

O_RDONLY | O_NONBLOCK

即使当前没有写端:

open 也可以立即成功

而:

O_WRONLY | O_NONBLOCK

如果当前根本没有读端:

open 立即失败

通常:

errno = ENXIO

还有一种比较特别的用法:

O_RDWR

Linux 上一个进程可以同时以读写方式打开 FIFO。

这往往可以:

立即成功

因为它自己同时拥有:

读端 + 写端

不过这更偏 Linux 行为,写实际程序时仍然要清楚自己为什么这么做,而不是为了“逃避阻塞”随手使用。


4.6 FIFO 打开以后,剩下和匿名管道非常像

一旦双方成功:

open()

并拿到 fd:

Server fd
Client fd

后面的:

read()
write()

本质上仍然是管道语义。

因此匿名管道中的:

四种读写情况
PIPE_BUF
Pipe Capacity

在 FIFO 中仍然非常重要。

这也说明:

匿名管道和命名管道最核心的区别,不在数据怎么传,而在双方怎么找到同一个管道。

匿名管道:

靠 fork 继承 fd

命名管道:

靠文件系统 pathname

4.7 FIFO Server

// server.cpp

#include <iostream>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <cerrno>

int main()
{
    const char* path = "./myfifo";

    if (mkfifo(path, 0666) == -1)
    {
        // 已经存在则可以继续使用
        if (errno != EEXIST)
        {
            perror("mkfifo");
            return 1;
        }
    }

    // 默认情况下会等待写端
    int fd = open(path, O_RDONLY);

    if (fd == -1)
    {
        perror("open");
        return 2;
    }

    char buf[1024];

    while (true)
    {
        ssize_t n =
            read(fd, buf, sizeof(buf) - 1);

        if (n > 0)
        {
            buf[n] = '\0';

            std::cout
                << "server recv: "
                << buf
                << std::endl;
        }
        else if (n == 0)
        {
            // 所有写端关闭并且数据已经读完
            break;
        }
        else
        {
            perror("read");
            break;
        }
    }

    close(fd);

    // 删除 FIFO 路径
    unlink(path);

    return 0;
}

4.8 FIFO Client

// client.cpp

#include <iostream>
#include <string>
#include <unistd.h>
#include <fcntl.h>

int main()
{
    const char* path = "./myfifo";

    // 默认情况下会等待 FIFO 出现读端
    int fd = open(path, O_WRONLY);

    if (fd == -1)
    {
        perror("open");
        return 1;
    }

    std::string msg;

    while (std::getline(std::cin, msg))
    {
        write(fd, msg.c_str(), msg.size());
    }

    close(fd);

    return 0;
}

现在即使两个程序分别从不同终端启动:

Terminal 1
./server

Terminal 2
./client

它们也可以借助:

./myfifo

找到同一个 IPC 资源。


5. 共享内存:把“搬数据”这件事直接省掉

接下来是 IPC 中非常重要的一种方式:

Shared Memory —— 共享内存。

先思考管道通信发生了什么。

大体可以理解为:

Process A 用户空间
      ↓
    write
      ↓
内核中的管道缓冲区
      ↓
     read
      ↓
Process B 用户空间

数据存在:

搬移过程

那么有没有更加直接的方法?

当然有。


5.1 直接让两个进程操作同一组物理页

我们以前学进程地址空间时已经知道:

虚拟地址
  ↓
页表
  ↓
物理内存

不同进程完全可以拥有不同的虚拟地址:

Process A               Process B

0x7000                   0x9000
   │                        │
   └─────────┐   ┌──────────┘
             ↓   ↓
          同一组物理页

于是:

Process A
写物理页

Process B 立刻就能通过自己的映射看到这份数据。

不需要:

A → 内核管道 → B

来回搬。

所以共享内存的核心就是:

把同一组物理页映射进多个进程的虚拟地址空间。

最终:

A 操作自己的虚拟地址
B 操作自己的虚拟地址

但实际上
      ↓

它们操作的是同一块物理内存

5.2 这也是为什么共享内存非常快

共享内存建立完成以后:

char* p = ...;

后续甚至可以直接:

strcpy(p, "hello");

另一个进程:

std::cout << p;

它不需要每次通信都重新调用:

write()
read()

完成数据搬运。

所以共享内存最大的特点就是:

数据交换非常直接,效率很高。

但是别急。

这种“太自由”的设计马上又会带来新的问题。

我们等接口讲完再回来。


6. System V IPC:key 和 id 到底是什么?

共享内存这里第一次遇到 System V IPC。

后面的:

共享内存
消息队列
System V 信号量

都有一个非常类似的模型。

它们需要解决:

两个完全独立的进程,怎样找到同一个内核 IPC 对象?

常用方式就是:

key

6.1 ftok:生成 key

接口:

#include <sys/ipc.h>

key_t ftok(const char* pathname, int proj_id);

例如:

key_t key = ftok("/tmp", 'S');

双方只要约定:

相同 pathname
+
相同 proj_id

就可以得到相同的 key,用它寻找同一份 IPC 资源。

需要注意:

ftok 生成的 key 并不是“绝对不会碰撞的全球唯一 ID”。

它主要承担:

双方约定资源身份

的作用。


6.2 key 和 shmid 不是一个东西

共享内存中会同时遇到:

key

和:

shmid

不要混。

可以这样理解:

key
↓
查找 / 创建共享内存时使用

找到以后
↓
得到 shmid

shmid
↓
后续操作这个共享内存对象

也就是:

key
→ 找对象

shmid
→ 操作已经找到的对象

7. 共享内存四个核心接口

整个流程可以压成:

shmget
 ↓
得到 shmid
 ↓
shmat
 ↓
映射进入进程地址空间
 ↓
直接用指针访问
 ↓
shmdt
 ↓
解除映射
 ↓
shmctl IPC_RMID
 ↓
删除共享内存对象

下面一个一个看。


7.1 shmget:创建 / 获取共享内存

接口:

#include <sys/shm.h>

int shmget(key_t key,
           size_t size,
           int shmflg);

成功:

返回 shmid

失败:

返回 -1

例如:

int shmid =
    shmget(key,
           4096,
           IPC_CREAT | 0666);

表示:

如果不存在
→ 创建

如果已经存在
→ 获取

IPC_CREAT | IPC_EXCL

如果使用:

IPC_CREAT | IPC_EXCL

则语义变成:

不存在
→ 创建

已经存在
→ 报错

这在服务器创建资源时非常常见。

例如:

int shmid =
    shmget(key,
           4096,
           IPC_CREAT | IPC_EXCL | 0666);

这样服务器就能知道:

这块资源到底是不是我刚创建出来的。


7.2 size 为什么经常看到 4096?

例如:

shmget(key, 4096, ...);

因为内存底层以:

页

为基本管理单位。

很多常见 Linux 环境中页大小是:

4KB

也就是:

4096 Bytes

但不要把:

Linux 页一定等于 4096

当成永远成立的规则。

实际页大小应以具体系统为准。

例如可以查看:

getconf PAGESIZE

7.3 shmat:把共享内存映射到当前进程

有了:

shmid

还不够。

当前进程必须把共享内存:

挂接 / attach 到自己的虚拟地址空间。

接口:

void* shmat(int shmid,
            const void* shmaddr,
            int shmflg);

例如:

void* addr =
    shmat(shmid, nullptr, 0);

成功:

返回映射后的虚拟地址

失败:

(void*)-1

所以判断失败时不要写:

if (addr == nullptr)

而应该判断:

if (addr == (void*)-1)

shmaddr = nullptr

表示:

让操作系统帮我们选择合适的虚拟地址。

一般直接这么使用即可。


shmflg = 0

表示正常读写挂接。

如果:

SHM_RDONLY

则以只读方式挂接。


7.4 shmat 以后就可以直接用指针操作

例如:

char* p = static_cast<char*>(addr);

之后:

strcpy(p, "hello client");

就已经在写共享内存。

另一个进程映射同一个共享内存以后:

std::cout << p;

就可以直接看到。

这就是共享内存与管道一个非常明显的区别:

管道
→ read / write

共享内存建立映射以后
→ 直接指针访问

7.5 shmdt:解除当前进程的映射

接口:

int shmdt(const void* shmaddr);

例如:

shmdt(addr);

这里最容易产生一个误解:

shmdt 是不是把共享内存删掉了?

不是。

它只是:

当前进程
      X
共享内存

把当前进程与共享内存之间的映射解除。

也就是:

detach。

共享内存对象本身依然可能存在于内核中。


7.6 shmctl:真正控制共享内存对象

接口:

int shmctl(int shmid,
           int cmd,
           struct shmid_ds* buf);

最常见的删除:

shmctl(shmid,
       IPC_RMID,
       nullptr);

表示:

将共享内存对象标记删除。

这里注意“标记”两个字。

如果还有进程挂接在这块共享内存上:

shm_nattch > 0

Linux 不会粗暴地立即让这些进程手里的地址失效。

而是:

IPC_RMID
 ↓
标记删除
 ↓
等待所有进程 detach
 ↓
shm_nattch == 0
 ↓
真正销毁

这和文件系统中:

unlink
+
仍有进程持有打开文件

其实有一点相似的设计思想。


8. System V 共享内存的生命周期

共享内存还有一个必须掌握的特点:

它的生命周期随内核,而不是简单随创建它的进程。

也就是说:

Server 创建共享内存
 ↓
Server 崩了

共享内存不一定自动消失。

所以你重新运行程序可能看到:

shmget(... IPC_CREAT | IPC_EXCL ...)

失败:

File exists

因为上一次留下的共享内存对象还存在。


可以查看:

ipcs -m

删除:

ipcrm -m shmid

或者程序:

shmctl(shmid,
       IPC_RMID,
       nullptr);

因此 System V IPC 学习中要建立一个非常重要的意识:

程序退出
≠
IPC 对象一定销毁

9. 共享内存完整示例

Server

// server.cpp

#include <iostream>
#include <cstring>
#include <sys/ipc.h>
#include <sys/shm.h>

int main()
{
    key_t key = ftok("/tmp", 'S');

    if (key == -1)
    {
        perror("ftok");
        return 1;
    }

    int shmid =
        shmget(key,
               4096,
               IPC_CREAT | IPC_EXCL | 0666);

    if (shmid == -1)
    {
        perror("shmget");
        return 2;
    }

    void* addr =
        shmat(shmid, nullptr, 0);

    if (addr == (void*)-1)
    {
        perror("shmat");
        return 3;
    }

    char* p =
        static_cast<char*>(addr);

    strcpy(p, "hello client");

    std::cout
        << "server write: "
        << p
        << std::endl;

    // 暂停一下,给 client 读取
    std::cin.get();

    shmdt(addr);

    shmctl(shmid,
           IPC_RMID,
           nullptr);

    return 0;
}

Client

// client.cpp

#include <iostream>
#include <sys/ipc.h>
#include <sys/shm.h>

int main()
{
    key_t key = ftok("/tmp", 'S');

    if (key == -1)
    {
        perror("ftok");
        return 1;
    }

    // 获取已有共享内存
    int shmid =
        shmget(key, 4096, 0666);

    if (shmid == -1)
    {
        perror("shmget");
        return 2;
    }

    void* addr =
        shmat(shmid, nullptr, 0);

    if (addr == (void*)-1)
    {
        perror("shmat");
        return 3;
    }

    char* p =
        static_cast<char*>(addr);

    std::cout
        << "client read: "
        << p
        << std::endl;

    shmdt(addr);

    return 0;
}

整个链路:

Server                         Client
  │                              │
 ftok                           ftok
  │                              │
  └────────── same key ──────────┘
                 ↓
           Shared Memory
                 ↑
        ┌────────┴────────┐
        │                 │
      shmat             shmat
        │                 │
        ↓                 ↓
   Virtual Addr A    Virtual Addr B
        │                 │
        └──── same physical pages ────┘

10. 共享内存这么爽,缺点是什么?

看起来共享内存非常完美:

不用反复 read/write
不用经过管道来回搬
直接指针操作

但是它恰好因为:

太自由了

导致了最大的问题。

假设:

Process A
Process B

同时操作:

*shared_data

A 正写到一半:

HEL...

B 就开始读取。

或者:

A 在修改结构
B 同时也在修改

那就可能产生竞争。

因此共享内存本身:

不提供同步和互斥。

也就是说:

Shared Memory
→ 解决“数据放哪里”

Semaphore
→ 解决“谁什么时候可以操作”

这就是为什么共享内存经常和:

信号量

一起出现。

但在研究信号量之前,我们还有一种非常重要的 IPC。


11. 消息队列:我想按“一条消息一条消息”通信

管道有什么特点?

本质是一串字节流

例如:

helloABC123...

数据本身没有天然告诉你:

hello 是一条消息
ABC 是下一条消息
123 又是一条消息

应用层通常还需要设计自己的:

消息协议 / 消息边界

共享内存则更加“裸”:

给你一块内存
自己随便操作

同步互斥都得自己解决。

于是就产生了另一种思路:

干脆让内核直接保存一条条消息。

这就是:

Message Queue —— 消息队列。


11.1 消息队列的本质

内核维护:

Message Queue
┌───────────────┐
│ Message 1     │
├───────────────┤
│ Message 2     │
├───────────────┤
│ Message 3     │
└───────────────┘

多个进程通过同一个队列:

Process A
    ↓
消息队列
    ↓
Process B

于是:

不同进程再次通过“看到同一份 OS 公共资源”实现通信。


11.2 消息队列相比管道最大的特点:消息有边界

管道:

byte byte byte byte byte...

消息队列:

Message 1

Message 2

Message 3

因此应用程序天然是:

发一条消息
收一条消息

而不是:

从一片连续字节流里自己重新切消息

11.3 消息还可以带 type

System V 消息队列中的消息通常长这样:

struct Message
{
    long mtype;
    char mtext[128];
};

第一个成员必须是:

long mtype;

而且:

mtype > 0

正文:

mtext

为什么要有 type?

因为它可以用于:

对消息分类。

例如:

type = 1
→ Client 发给 Server

type = 2
→ Server 发给 Client

于是同一个消息队列甚至可以实现:

双向通信

或者:

type = 1 → 用户 A
type = 2 → 用户 B
type = 3 → 用户 C

接收方可以只取:

自己感兴趣的消息类型

12. 消息队列核心接口

流程:

ftok
 ↓
msgget
 ↓
msgsnd / msgrcv
 ↓
msgctl

12.1 msgget:创建 / 获取消息队列

#include <sys/msg.h>

int msgget(key_t key,
           int msgflg);

例如:

int msqid =
    msgget(key,
           IPC_CREAT | 0666);

成功:

返回 msqid

失败:

-1

这里与共享内存非常像:

key
→ 找对象

msqid
→ 操作对象

12.2 msgsnd:发送消息

接口:

int msgsnd(int msqid,
           const void* msgp,
           size_t msgsz,
           int msgflg);

例如:

struct Message
{
    long mtype;
    char mtext[128];
};

Message msg;

msg.mtype = 1;
strcpy(msg.mtext, "hello");

msgsnd(msqid,
       &msg,
       strlen(msg.mtext) + 1,
       0);

这里有一个非常非常容易错的点:

msgsz 不包含 mtype 的大小。

假设结构:

struct Message
{
    long mtype;
    char mtext[128];
};

不能简单写:

sizeof(Message)

然后认为这就是正文大小。

msgsz 描述的是:

mtype 后面的消息正文大小

12.3 队列满了怎么办?

如果消息队列已经无法继续容纳消息:

Queue Full

默认情况下:

msgsnd()

会:

阻塞

直到有空间。

如果使用:

IPC_NOWAIT

则:

不等待
立即失败

12.4 msgrcv:接收消息

接口:

ssize_t msgrcv(int msqid,
               void* msgp,
               size_t msgsz,
               long msgtyp,
               int msgflg);

其中最值得学的是:

msgtyp

它决定:

到底取哪一种消息。


msgtyp > 0

例如:

msgtyp = 5;

表示:

找第一条 mtype == 5 的消息。


msgtyp == 0

表示:

直接读取当前队列中的第一条消息。

也就是不关心类型。


msgtyp < 0

这个最容易记错。

例如:

msgtyp = -5;

此时会在:

mtype <= 5

的消息中:

找到满足条件的最小类型值,然后读取这一类型中最靠前的消息。

例如当前有:

mtype = 4

mtype = 2

mtype = 5

mtype = 2

调用:

msgrcv(..., -5, ...);

满足:

mtype <= 5

的都有。

其中最小类型为:

2

因此读取最前面的:

mtype = 2

消息。


12.5 没有符合条件的消息怎么办?

默认:

阻塞

直到对应消息到来。

如果使用:

IPC_NOWAIT

则立即失败。


还有一个非常重要的特点:

消息被成功 msgrcv
        ↓
从消息队列中移除

也就是说:

消息默认是“消费掉”的。

不是简单复制一份之后仍然留在那里。


12.6 msgctl:控制 / 删除消息队列

接口:

int msgctl(int msqid,
           int cmd,
           struct msqid_ds* buf);

删除:

msgctl(msqid,
       IPC_RMID,
       nullptr);

同样:

System V Message Queue

生命周期随内核。

因此创建者退出:

消息队列可能依然存在

可以查看:

ipcs -q

删除:

ipcrm -q msqid

12.7 消息队列还能实现时间上的解耦

假设通信要求是:

发送方发消息时
接收方必须正在那里等着

那么双方耦合会很严重。

消息队列提供了一定程度的:

时间解耦。

例如:

Process A
 ↓
send Message
 ↓
内核消息队列保存
 ↓
Process A 可以继续干别的事


过了一会儿


Process B
 ↓
从消息队列读取

发送者与接收者不必时时刻刻同时执行到同一个地方。

这也是队列模型在各种系统中如此常见的原因之一。


13. 终于来到信号量:它到底“通信”了什么?

很多人第一次学 IPC 时都有一个疑问:

管道传字符串,我理解。

共享内存传数据,我理解。

消息队列传消息,我也理解。

信号量连一串数据都不传,它凭什么算 IPC?

这个问题问得非常好。

因为:

进程间通信并不只意味着交换字符串。

不同执行流之间还需要:

协调
同步
互斥

也就是:

告诉另一个进程什么时候能够干什么。

这同样属于进程之间的信息交流。


13.1 为什么共享内存尤其需要信号量?

回到共享内存。

假设:

共享内存中有一个账户余额:

balance = 100

两个进程同时:

Process A
读取 100

Process B
读取 100

A:

+ 50

写回:

150

B:

- 30

写回:

70

最终:

balance = 70

但正确结果应该是:

120

问题就在于:

两个执行流同时操作了同一个共享资源。

因此我们需要某种机制告诉它们:

这块资源现在有人用了
你先等等

这就是信号量的用途之一。


13.2 信号量的本质:资源计数器

可以把信号量简单理解成:

资源数量的计数器。

例如:

sem = 1

表示:

当前有一个资源可以使用

某进程申请:

sem--

变成:

0

意味着:

资源被占用了

其他进程再申请:

没有资源
↓
等待

释放资源:

sem++

回到:

1

于是可以唤醒等待者。


13.3 P 操作和 V 操作

信号量中最经典的两个操作:

P
V

P 操作

含义:

申请资源。

可以粗略理解成:

sem--

如果没有资源:

等待

V 操作

含义:

归还资源。

粗略理解:

sem++

并且:

可能唤醒等待资源的进程

特别注意:

P/V 不是让程序自己 sem--、sem++。

如果是普通变量:

sem--;

操作可能被其他执行流打断。

那么信号量就失去意义了。

因此:

对信号量值的修改必须由内核提供原子操作。

这就是:

semop()

存在的原因。


14. 临界资源与临界区

信号量正式使用之前,还需要明确两个概念。


临界资源

多个执行流都能够访问,但是:

必须控制访问过程的共享资源。

例如:

共享内存
全局共享数据
共享文件
某个硬件资源

都可能成为临界资源。


临界区

程序中:

访问临界资源的那段代码。

例如:

// P

shared_data++;

// V

其中:

shared_data++;

就是临界区中的操作。


15. 互斥和同步不是一回事

这两个词非常容易被一起念成:

同步互斥

最后反而不知道区别。


互斥

强调的是:

同一时刻只能有一个执行流访问临界资源。

例如:

Process A
进入临界区

Process B
必须等

典型模型:

sem = 1

同步

强调的是:

控制多个执行流之间的执行顺序或者执行条件。

例如:

生产者先产生数据
        ↓
消费者才能消费

重点不是:

永远只有一个人进去

而是:

事情必须按照某种条件 / 顺序推进

所以:

互斥
→ 防止同时操作

同步
→ 控制先后关系 / 条件

16. System V 信号量

System V 中创建的其实不是单纯一个 semaphore,而是:

Semaphore Set —— 信号量集合。

因此接口:

semget()

里面需要告诉操作系统:

我要几个信号量

16.1 semget:创建 / 获取信号量集合

#include <sys/sem.h>

int semget(key_t key,
           int nsems,
           int semflg);

例如:

int semid =
    semget(key,
           1,
           IPC_CREAT | 0666);

这里:

nsems = 1

表示:

这个集合中有一个信号量。

成功:

返回 semid

16.2 semctl:设置、查询和删除

接口:

int semctl(int semid,
           int semnum,
           int cmd,
           ...);

其中:

semid
→ 哪个信号量集合

semnum
→ 集合中的第几个信号量

注意:

semnum 从 0 开始

SETVAL:初始化

例如我们希望:

sem[0] = 1

严谨的 System V 写法一般会准备:

union semun
{
    int val;
    struct semid_ds* buf;
    unsigned short* array;
};

然后:

union semun arg;
arg.val = 1;

semctl(semid,
       0,
       SETVAL,
       arg);

这样完成初始化。

很多教学代码为了突出含义,会把它简写理解为:

SETVAL → 把信号量设为 1

真正写程序时应按照系统接口要求准备第四个参数。


GETVAL:查询

int value =
    semctl(semid,
           0,
           GETVAL);

得到当前信号量值。


IPC_RMID:删除

semctl(semid,
       0,
       IPC_RMID);

删除整个信号量集合。


16.3 semop:真正执行 P / V

接口:

int semop(int semid,
          struct sembuf* sops,
          size_t nsops);

关键结构:

struct sembuf
{
    unsigned short sem_num;
    short          sem_op;
    short          sem_flg;
};

sem_num

表示:

操作集合中的第几个信号量

例如:

sem_num = 0;

sem_op

这是核心。

sem_op < 0

表示:

申请资源。

例如:

sem_op = -1;

就是最典型的:

P 操作

如果资源不足:

默认阻塞等待

sem_op > 0

表示:

释放资源。

例如:

sem_op = +1;

也就是:

V 操作

sem_op == 0

这个比较特殊。

表示:

等待对应信号量的值变为 0。

不是 P,也不是 V。


16.4 sem_flg

最常见:

sem_flg = 0;

代表:

默认阻塞行为

也可以设置:

IPC_NOWAIT

表示:

条件不满足时不要等待
直接返回失败

还有:

SEM_UNDO

后面单独解释。


16.5 nsops 到底是什么意思?

semop(semid,
      &op,
      1);

最后这个:

1

就是:

sops 数组中有几个操作。

例如:

struct sembuf ops[2];

那么:

semop(semid,
      ops,
      2);

表示一次提交两个信号量操作。

更重要的是:

System V 可以把这一组操作作为一个整体进行协调处理。

这也是它比简单地:

value--;

强很多的地方。


17. P/V 操作代码

P 操作

bool P(int semid)
{
    struct sembuf op;

    op.sem_num = 0;
    op.sem_op  = -1;
    op.sem_flg = 0;

    return semop(semid,
                 &op,
                 1) == 0;
}

这里:

sem_op = -1

表示:

申请一份资源

如果资源不存在:

阻塞

V 操作

bool V(int semid)
{
    struct sembuf op;

    op.sem_num = 0;
    op.sem_op  = +1;
    op.sem_flg = 0;

    return semop(semid,
                 &op,
                 1) == 0;
}

这里:

sem_op = +1

表示:

释放一份资源

于是保护共享内存时可以形成:

P
↓
进入临界区
↓
操作共享内存
↓
离开临界区
↓
V

代码思路:

P(semid);

// 临界区
shared_data->value++;

V(semid);

18. SEM_UNDO 是干什么的?

考虑一个问题。

进程:

P
↓
成功拿到资源
↓
sem = 0

然后还没来得及:

V

程序突然:

崩溃 / 异常退出

那怎么办?

信号量可能永远停留在一个不正确的状态。

所以 System V 提供:

SEM_UNDO

让内核为相关操作记录调整信息。

当进程退出时:

内核尝试对这些信号量变化进行恢复

从而降低:

进程异常死亡
→ 信号量永久丢失

的问题。

不过必须注意:

SEM_UNDO 不是“打开以后系统就百分之百永远不会出问题”的万能保险。

它存在:

系统限制
复杂语义
资源限制

等问题。

所以正确理解是:

它为进程退出场景提供一种内核辅助恢复机制,而不是替代整个并发正确性设计。


19. System V 信号量同样具有内核生命周期

创建:

Semaphore Set

以后:

创建者退出

对象并不会因此自动消失。

查看:

ipcs -s

删除:

ipcrm -s semid

程序中:

semctl(semid,
       0,
       IPC_RMID);

所以现在可以发现一个共同特点:

System V Shared Memory
System V Message Queue
System V Semaphore

基本都有:

key
 ↓
创建 / 获取
 ↓
得到 id
 ↓
操作
 ↓
显式 IPC_RMID

这一整套风格。


20. 为什么信号量也算 IPC?

现在可以正式回答这个问题。

IPC 全称:

Inter-Process Communication

这里的:

Communication

不能狭义理解为:

只有把 "hello world" 从 A 发给 B 才算通信。

进程之间还可能需要传递:

资源是否可用
某件事情是否已经完成
什么时候可以继续
谁应该先执行

这些本质上也是:

进程之间的信息与状态协调。

因此:

管道
→ 传数据

共享内存
→ 共享数据

消息队列
→ 交换消息

信号量
→ 传递资源状态 / 实现协同

都属于 IPC。


21. 五种 IPC 放在一起看,就突然很清楚了

IPC公共资源是什么数据特点是否天然有消息边界同步特点生命周期
匿名管道内核管道缓冲区字节流❌read/write 有阻塞语义随引用关系
FIFO有名字的管道对象字节流❌与管道类似路径需显式删除
共享内存多进程映射同一组物理页直接内存访问自己定义❌ 本身不提供System V 对象随内核
消息队列内核消息队列一条条 message✅队列空/满可阻塞System V 对象随内核
信号量内核信号量集合不主要传业务数据—✅ 专门解决同步/互斥System V 对象随内核

这张表不要死背。

真正要抓的是:

进程彼此独立
       ↓
必须找到一份公共资源
       ↓
公共资源形式不同
       ↓
产生不同 IPC

22. 再从“数据到底怎么走”看一遍

匿名管道 / FIFO

Process A
用户空间
   ↓
 write
   ↓
内核管道
   ↓
 read
   ↓
Process B
用户空间

共享内存

Process A VA ───┐
                │
                ↓
           Physical Pages
                ↑
                │
Process B VA ───┘

双方直接访问同一组物理页。


消息队列

Process A
   ↓
 msgsnd
   ↓
┌─────────────────┐
│ Kernel MsgQueue │
│                 │
│ type + message  │
│ type + message  │
└─────────────────┘
   ↓
 msgrcv
   ↓
Process B

信号量

Process A
   ↓
   P
   ↓
Semaphore
   ↓
临界资源被占用
   ↑
Process B
   ↓
   P
   ↓
等待


Process A
   ↓
   V
   ↓
释放资源
   ↓
唤醒等待者

IPC 到这里已经变成一个完整体系,而不是几个 API 名字。


23. System V IPC 为什么总是长得这么像?

现在把三套 System V 接口放一起:

Shared Memory

ftok
 ↓
shmget
 ↓
shmat
 ↓
shmdt
 ↓
shmctl

Message Queue

ftok
 ↓
msgget
 ↓
msgsnd / msgrcv
 ↓
msgctl

Semaphore

ftok
 ↓
semget
 ↓
semop
 ↓
semctl

规律非常明显:

key
 ↓
xxxget
 ↓
得到 xxxid
 ↓
执行具体操作
 ↓
xxxctl

所以面试准备时,真没必要把所有 System V API 当成几十个毫无关系的函数死背。

它们本来就是同一种设计风格。


24. IPC 最值得记住的面试问题

最后把真正容易问的地方收一下。


① IPC 为什么存在?

因为:

进程具有独立性,各进程拥有自己的虚拟地址空间,因此需要借助操作系统提供的公共资源完成数据交换或协同。


② 匿名管道为什么通常用于父子进程?

因为:

pipe
 ↓
fork

之后子进程可以继承父进程的文件描述符,从而让父子进程共同引用同一个内核管道。


③ 为什么 pipe 一般要在 fork 之前创建?

因为需要:

fork 时
把已经创建好的管道 fd
继承给子进程

这样双方才能找到同一个管道。


④ 为什么管道必须关闭不用的端?

不仅为了节省 fd。

更重要的是:

所有写端是否关闭
→ 决定 read 能不能看到 EOF

所有读端是否关闭
→ 决定 write 是否触发 SIGPIPE

所以关闭多余描述符直接影响管道通信语义。


⑤ 管道的四种情况?

有写端但没数据
→ read 阻塞

有读端但管道满
→ write 阻塞

所有写端关闭
→ 数据读完后 read 返回 0

所有读端关闭
→ write 触发 SIGPIPE

⑥ PIPE_BUF 和管道容量区别?

PIPE_BUF
→ 原子写保证

Pipe Capacity
→ 整个管道缓冲区容量

⑦ 匿名管道和命名管道最大区别?

真正数据传输机制都属于管道模型。

主要区别在于:

匿名管道
→ 通常靠 fork 继承 fd 找到同一管道

FIFO
→ 靠文件系统 pathname 找到同一管道

⑧ FIFO 的数据真的写进磁盘文件了吗?

不是普通磁盘文件那种意义。

FIFO 的:

pathname

存在于文件系统中,主要用于:

定位管道对象。

真正通信的数据仍通过内核管道机制传输。


⑨ 共享内存为什么快?

因为把:

同一组物理页

映射进多个进程地址空间。

建立映射后,进程直接通过指针访问共享区域,避免了管道式的重复数据搬移。


⑩ 共享内存最大的缺陷?

本身不提供同步和互斥。

多个进程可以同时修改共享数据,所以通常还需要:

信号量
互斥锁
其他同步机制

配合。


⑪ key 和 shmid / msqid / semid 区别?

key
→ 查找 / 创建 IPC 对象时使用

id
→ 找到对象后,由内核返回,后续直接用它操作对象

⑫ shmdt 会删除共享内存吗?

不会。

shmdt
→ 当前进程解除映射

shmctl(... IPC_RMID ...)
→ 删除共享内存对象

是两件不同的事。


⑬ IPC_RMID 后为什么共享内存可能还存在?

如果仍然存在进程挂接:

shm_nattch > 0

Linux 会先标记删除。

等:

最后一个进程 detach

之后再真正销毁。


⑭ 消息队列和管道最大的区别?

管道主要是:

字节流

而消息队列:

以 message 为单位

天然保留消息边界,并且消息还可以拥有:

mtype

用于分类。


⑮ msgsnd 的 msgsz 包不包含 mtype?

不包含。

msgsz 表示:

mtype 后面正文部分的大小

⑯ msgrcv 的 msgtyp < 0 怎么理解?

如果:

msgtyp = -N;

则寻找:

mtype <= N

范围内:

类型值最小的消息类型中的第一条消息。

这个点非常容易记反。


⑰ 信号量的本质是什么?

可以理解为:

内核维护的资源计数器。

用于在多个执行流之间实现:

同步
互斥

⑱ P/V 分别是什么?

P
→ 申请资源
→ sem_op < 0

V
→ 释放资源
→ sem_op > 0

⑲ 为什么信号量操作必须是原子的?

因为信号量本身就是用于解决:

并发竞争

的。

如果修改信号量自身:

都会被并发破坏

那整个同步机制就失去意义了。

所以信号量核心操作必须由内核原子完成。


⑳ 为什么信号量也属于 IPC?

因为 IPC 不仅是:

传业务数据

还包括:

进程之间交换状态
控制执行顺序
完成同步与互斥

信号量正是在帮助不同进程进行协同。


25. 最后一张图,收掉整个 IPC

                    进程具有独立性
                          │
                          ↓
               不能直接访问对方地址空间
                          │
                          ↓
              怎么让不同进程发生联系?
                          │
                          ↓
         让不同进程看到同一份 OS 公共资源
                          │
        ┌─────────────────┼──────────────────┐
        │                 │                  │
        ↓                 ↓                  ↓
      管道             System V           其他 IPC
        │                 │
   ┌────┴────┐      ┌─────┼────────┐
   ↓         ↓      ↓     ↓        ↓
匿名管道    FIFO  共享内存 消息队列 信号量
   │         │      │      │       │
   │         │      │      │       └─同步 / 互斥
   │         │      │      │
   │         │      │      └─按消息交换
   │         │      │
   │         │      └─映射同一组物理页
   │         │
   │         └─文件系统路径找到管道
   │
   └─fork 继承 fd 找到管道

如果继续再压缩:

匿名管道
→ fork 继承 fd

FIFO
→ pathname

共享内存
→ shared physical pages

消息队列
→ message + type

信号量
→ resource counter

但这五句话背后的共同本质只有一句:

进程本来彼此独立,IPC 就是操作系统替它们创造一块双方都能认识的公共资源。


🌙 写在最后

IPC 这一章第一次看的时候特别容易产生一种感觉:

pipe 一个接口
mkfifo 又一个接口

shmget
shmat
shmdt
shmctl

msgget
msgsnd
msgrcv
msgctl

semget
semctl
semop
...

越背越多。

最后:

函数名全眼熟
实际啥关系不知道

但真正把底层逻辑串起来以后,会发现它根本没这么乱。


管道在解决:

我给你一条共同的字节通道

FIFO 只是进一步解决:

咱俩没有父子关系
怎么找到这条通道?

共享内存在解决:

干脆别搬数据了
咱俩直接看同一块物理内存

然后马上产生新问题:

那咱俩同时改怎么办?

于是信号量出现:

先告诉我谁有资格操作

消息队列则走了另一条路:

别给我一串没有边界的字节
我直接一条消息一条消息地传

所以 IPC 真正值得记住的不是:

十几个函数原型

而是:

进程独立
   ↓
需要公共资源
   ↓
公共资源如何被双方找到
   ↓
数据如何通过它交换
   ↓
多个进程同时访问时怎么协调

只要这条链通了,后面的接口只是实现手段。


🧭 操作系统真正有意思的地方,就是你以为自己在学几个函数,最后却发现它们全都是同一个设计思想的不同答案。

今天这一块,IPC 收官。

👀 关注:继续沿着 Linux 系统这棵大树往下挖
❤️ 点赞:如果这篇真的把 IPC 从“接口地狱”变成一条完整主线
⭐ 收藏:面试前重新顺一遍五种 IPC 的共同本质
💬 评论:如果还有哪个 IPC 机制感觉没彻底通,欢迎一起讨论

不要背散装知识点。真正建立体系以后,很多东西根本不需要硬记。


🐾 今日份 Linux 练级完成

IPC 核心思想                 ✅

匿名管道
├── pipe                    ✅
├── fork + fd 继承          ✅
├── 四种读写情况            ✅
├── SIGPIPE / EOF           ✅
├── PIPE_BUF                ✅
└── Shell 管道              ✅

命名管道 FIFO
├── mkfifo                  ✅
├── open 阻塞规则           ✅
├── O_NONBLOCK              ✅
└── Server / Client         ✅

共享内存
├── ftok                    ✅
├── shmget                  ✅
├── shmat                   ✅
├── shmdt                   ✅
├── shmctl                  ✅
└── 物理页共享原理          ✅

消息队列
├── msgget                  ✅
├── msgsnd                  ✅
├── msgrcv                  ✅
├── mtype                   ✅
└── msgctl                  ✅

System V 信号量
├── 临界资源 / 临界区       ✅
├── 同步 / 互斥             ✅
├── semget                  ✅
├── semctl                  ✅
├── semop                   ✅
├── P / V                   ✅
└── SEM_UNDO                ✅

下一次看到 IPC,不应该再想到一堆 API,而应该先想到:如何让两个独立进程看到同一份公共资源?

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐