为了保护操作系统中进程互不干扰,需要使用进程隔离技术,以防不同进程能够修改其他进程数据。但进程之间又不能完全隔离,需要一定的通信手段,于是开发出了**进程间通信(IPC, InterProcess Communication)**技术。
管道(Pipe)
管道是最基本的通信机制,实质是一个半双工的通信方式,即数据只能单向流通,每次从头读取,向尾写入,并且遵循 FIFO(First In First Out) 原则,以字节流的方式进行传输,就仿佛一个单向队列。并且管道双方必须均处于「在线」状态,一旦一方退出,另一方也自动断开。
如果双方相互通信时,则需要建立两个管道。
根据是否存在于文件系统中,又可将管道分为匿名管道和命名管道两种。
「名」指的就是路径名啦。
匿名管道
Linux 中可以通过函数 int pipe(int fd[2]) 建立匿名管道,其中 fd[0] 为读取端,fd[1] 为写入端,两个都是文件描述符,从而可以用 read()/write() 系统调用进行数据传输。比如下面这段代码:
1#include <stdio.h>
2#include <unistd.h>
3#include <string.h>
4
5int main() {
6 int fd[2];
7 int ret = pipe(fd); // ret>=0 for success, ret<0 for fail
8 if (ret < 0) {
9 printf("Pipe Create Failed!\n");
10 }
11
12 pid_t id = fork();
13 if (id < 0) {
14 printf("Fork Failed\n");
15 } else if(id == 0) {
16 close(fd[0]); // 关闭读取才能写入
17 int i = 0;
18 char* msg = "I'm child";
19 while(i++ < 100) {
20 write(fd[1], msg, strlen(msg));
21 sleep(1);
22 }
23 } else {
24 close(fd[1]); // 关闭写入才能读取
25 int i = 0;
26 char msg[100];
27 while (i++ < 100) {
28 memset(msg, '\0', sizeof(msg));
29 read(fd[0], msg, sizeof(msg));
30 printf("%s\n", msg);
31 }
32 }
33
34 return 0;
35}
事实上,匿名管道并不位于文件系统中,它本质上是一个位于内核空间中的缓冲区,只不过将两端以「文件」的形式提供了数据读写的渠道。如果缓冲区已满,则阻塞写入;如果缓冲区为空,则阻塞读取。

同时,读写端的文件描述符仅仅位于通信双方公共祖先进程的地址空间内部,也是因为如此,匿名管道仅适用于父子进程这种有「亲缘关系」(或称相关)的进程之间的通信,而无法跨进程通信。
毕竟调用
fork()会进行一次完整的拷贝操作,也就会复制该fd[]。
同样,一旦进程销毁,该管道也随之被销毁。也就是说,匿名管道的生命周期完全由进程决定。
命名管道
那么为了实现跨进程通信,不如就把上文中的缓冲区改为实际的文件,令其位于文件系统中,这样一来只要知道该管道文件的路径,就可以在任何进程之间进行通信了。
Linux 中可以通过函数 int mkfifo(const char *path, mode_t mode) 建立命名管道,其中 path 指明文件路径,mode 指明管道文件的存取权限。比如下面这段代码:
1#include <stdio.h>
2#include <string.h>
3#include <unistd.h>
4#define _PATH_NAME_ "/tmp/file.tmp"
5#define _SIZE_ 100
6
7int main() {
8 int ret = mkfifo(_PATH_NAME_, S_IFIFO | 0666);
9 if (ret == -1) {
10 printf("Make File Error\n");
11 return -1;
12 }
13
14 char buf[_SIZE_] = {0};
15 int fd = open(_PATH_NAME_, O_WRONLY);
16 while (true) {
17 fget(buf, sizeof(buf) - 1, stdin);
18 int ret = write(fd, buf, strlen(buf) + 1);
19 if (ret < 0) {
20 printf("Write Error\n");
21 break;
22 }
23 }
24 close(fd);
25
26 return 0;
27}
1#include <stdio.h>
2#include <string.h>
3#include <unistd.h>
4#define _PATH_NAME_ "/tmp/file.tmp"
5#define _SIZE_ 100
6
7int main() {
8 int fd = open(_PATH_NAME_, O_RDONLY);
9 if (fd < 0) {
10 printf("Open File Error\n");
11 return 1;
12 }
13
14 char buf[_SIZE_] = {0};
15 while (true) {
16 int ret = read(fd, buf, sizeof(buf));
17 if (ret < 0) {
18 printf("Read Error\n");
19 break;
20 }
21 printf("%s\n", buf);
22 }
23 close(fd);
24
25 return 0;
26}
服务端利用系统函数 mkfifo() 创建一个名为 file.tmp 的文件作为管道并写入。客户端同样也能打开该文件进行读取。而它们却是无关进程。
信号(Signal)
信号是一种软中断,采用异步通信方式,内核可以利用信号来通知用户进程发生了哪些系统事件,一般源于硬件方式(用户按下 Ctrl+C 键)或软件方式(系统调用产生信号)
Linux 中共定义了 64 种信号,分为以下两种:
- 不可靠信号:不支持排队,信号可能会丢失,发送多次相同的信号,进程只能收到一次. 信号值取值区间为 1~31;
- 可靠信号:支持排队,信号不会丢失,发多少次,就可以收到多少次,信号值取值区间为 32~64;
常见信号
| 信号 | 描述 |
|---|---|
| SIGHUP | 用户从终端注销,所有已启动进程都将收到该进程。系统缺省状态下对该信号的处理是终止进程 |
| SIGINT | 程序终止信号。程序运行过程中,按 Ctrl+C 键将产生该信号 |
| SIGQUIT | 程序退出信号。程序运行过程中,按 Ctrl+\ 键将产生该信号 |
| SIGBUS / SIGSEGV | 进程访问非法地址 |
| SIGFPE | 运算中出现致命错误,如除零操作、数据溢出等 |
| SIGKILL | 用户终止进程执行信号。执行 kill -9 发送该信号 |
| SIGTERM | 结束进程信号。执行 kill pid 发送该信号 |
| SIGALRM | 定时器信号 |
| SIGCHLD | 子进程退出信号。如果其父进程没有忽略该信号也没有处理该信号,则子进程退出后将形成僵尸进程 |
Linux 信号相关数据结构
Linux 在进程管理结构 task_struct 中定义了信号相关的变量。
1struct task_struct {
2 ...
3 int sigpending; // 是否有待处理信号
4 ...
5 struct signal_struct *sig; // 定义了信号值对应的处理方法
6 sigset_t blocked; // 被屏蔽的信息,以 bit 表示
7 struct sigpending pending; // 待处理信号队列
8 ...
9}
1#define _NSIG 64
2
3struct signal_struct {
4 atomic_t count;
5 struct k_sigaction action[_NSIG]; // 信号处理函数表
6 spinlock_t siglock;
7};
8
9typedef void (*__sighandler_t)(int);
10
11struct sigaction {
12 __sighandler_t sa_handler; // 处理函数
13 unsigned long sa_flags;
14 void (*sa_restorer)(void);
15 sigset_t sa_mask;
16};
17
18struct k_sigaction {
19 struct sigaction sa;
20};
21
22struct sigqueue {
23 struct sigqueue *next;
24 siginfo_t info;
25};
26
27struct sigpending {
28 struct sigqueue *head, **tail;
29 sigset_t signal;
30};
信号发送流程
以 kill() 为例,其函数原型为 int kill(pid_t pid, int sig)。
当源进程(src)调用 kill() 打算将目标进程(dst)给杀掉时,该系统调用会进一步调用内核函数 sys_kill():
1asmlinkage long
2sys_kill(int pid, int sig)
3{
4 struct siginfo info;
5
6 info.si_signo = sig;
7 info.si_errno = 0;
8 info.si_code = SI_USER;
9 info.si_pid = current->pid;
10 info.si_uid = current->uid;
11
12 return kill_something_info(sig, &info, pid);
13}
14
15static int kill_something_info(int sig, struct siginfo *info, int pid)
16{
17 if (!pid) {
18 return kill_pg_info(sig, info, current->pgrp);
19 } else if (pid == -1) {
20 int retval = 0, count = 0;
21 struct task_struct * p;
22 read_lock(&tasklist_lock);
23 for_each_task(p) {
24 if (p->pid > 1 && p != current) {
25 int err = send_sig_info(sig, info, p);
26 ++count;
27 if (err != -EPERM)
28 retval = err;
29 }
30 }
31 read_unlock(&tasklist_lock);
32 return count ? retval : -ESRCH;
33 } else if (pid < 0) {
34 return kill_pg_info(sig, info, -pid);
35 } else {
36 return kill_proc_info(sig, info, pid);
37 }
38}
从函数 kill_something_info() 可以得知,信号发送后具体的处理流程与 dst 的 pid 有很大关系,具体为:
pid > 0时:直接发送给pid对应的进程;pid = 0时:发送给所有与 src 属同一个使用组的进程;pid = -1时:发送给 src 有权给其发送信号的所有进程,除了进程 1(init);pid < -1时:发送给 -pid 为组标识的进程;
这里只讨论第一种情况。当 pid>0 时,调用 kill_proc_info() 函数,通过 send_sig_info() 发送信号。
1inline int
2kill_proc_info(int sig, struct siginfo *info, pid_t pid)
3{
4 int error;
5 struct task_struct *p;
6
7 read_lock(&tasklist_lock);
8 p = find_task_by_pid(pid); // 获得 pid 对应的 task_struct
9 error = -ESRCH;
10 if (p) {
11 error = send_sig_info(sig, info, p);
12 }
13 read_unlock(&tasklist_lock);
14 return error;
15}
16
17int
18send_sig_info(int sig, struct siginfo *info, struct task_struct *t)
19{
20 unsigned long flags;
21 int ret;
22
23 ret = -EINVAL;
24 if (sig < 0 || sig > _NSIG)
25 goto out_nolock;
26
27 ret = -EPERM;
28 if (bad_signal(sig, info, t)) // 检查是否有权限发送,没有就直接返回
29 goto out_nolock;
30
31 ret = 0;
32 if (!sig || !t->sig)
33 goto out_nolock;
34
35 spin_lock_irqsave(&t->sigmask_lock, flags);
36 handle_stop_signal(sig, t)
37
38 if (ignored_signal(sig, t)) // 是否忽略该信号,是就不发送
39 goto out;
40
41 if (sig < SIGRTMIN && sigismember(&t->pending.signal, sig)) // 不可靠信号如果已经在等待队列中,就不会再次入队
42 goto out;
43
44 ret = deliver_signal(sig, info, t); // 发送信号,并将 task_struct.pending 置为 1
45out:
46 spin_unlock_irqrestore(&t->sigmask_lock, flags);
47 if ((t->state & TASK_INTERRUPTIBLE)&& signal_pending(t)) // 如果处于睡眠可中断状态且有信号待处理,将其唤醒
48 wake_up_process(t);
49
50out_nolock:
51 return ret;
52}
至此,发送信号的流程已完成。
信号处理流程
此时此刻信号已经发送给目标进程,为了尽快让信号得到处理,Linux 把信号处理过程放置在进程从内核态返回到用户态前,也就是操作系统决定调度目标进程的时候,然后检查 sigpending 是否为 1,若是,就调用 do_signal() 函数进行处理。
1int do_signal(struct pt_regs *regs, sigset_t *oldset)
2{
3 siginfo_t info;
4 struct k_sigaction *ka;
5
6 if ((regs->xcs & 3)!= 3)
7 return 1;
8
9 if (!oldset)
10 oldset = ¤t->blocked;
11
12 for (;;) {
13 unsigned long signr;
14
15 // 不断对信号队列进行 dequeue 取出头部,如果没有可取的就直接退出
16 spin_lock_irq(¤t->sigmask_lock);
17 signr = dequeue_signal(¤t->blocked, &info);
18 spin_unlock_irq(¤t->sigmask_lock);
19 if (!signr)
20 break;
21
22 ka = ¤t->sig->action[signr-1];
23 if (ka->sa.sa_handler == SIG_IGN) { // ignore. 即如果忽视就直接跳过
24 if (signr != SIGCHLD)
25 continue;
26
27 /* Check for SIGCHLD: it's special. */
28 while (sys_wait4(-1, NULL, WNOHANG, NULL)> 0)
29 /* nothing */;
30
31 continue;
32 }
33
34 if (ka->sa.sa_handler == SIG_DFL) { // default. 即指定为默认就用系统的默认处理方法
35 int exit_code = signr;
36
37 /* Init gets no signals it doesn't want. */
38 if (current->pid == 1)
39 continue;
40
41 switch (signr) {
42 case SIGCONT: case SIGCHLD: case SIGWINCH:
43 continue;
44
45 case SIGTSTP: case SIGTTIN: case SIGTTOU:
46 if (is_orphaned_pgrp(current->pgrp))
47 continue;
48 /* FALLTHRU */
49
50 case SIGSTOP:
51 current->state = TASK_STOPPED;
52 current->exit_code = signr;
53 if (!(current->p_pptr->sig->action[SIGCHLD-1].sa.sa_flags & SA_NOCLDSTOP))
54 notify_parent(current, SIGCHLD);
55 schedule();
56 continue;
57
58 case SIGQUIT: case SIGILL: case SIGTRAP:
59 case SIGABRT: case SIGFPE: case SIGSEGV:
60 case SIGBUS: case SIGSYS: case SIGXCPU: case SIGXFSZ:
61 if (do_coredump(signr, regs))
62 exit_code |= 0x80;
63 /* FALLTHRU */
64
65 default:
66 sigaddset(¤t->pending.signal, signr);
67 recalc_sigpending(current);
68 current->flags |= PF_SIGNALED;
69 do_exit(exit_code);
70 /* NOTREACHED */
71 }
72 }
73 ...
74 handle_signal(signr, ka, &info, oldset, regs); // 如果通过 signal() 注册了自定义处理方法,就用 handle_signal() 去处理
75 return 1;
76 }
77 ...
78 return 0;
79}
值得注意的是,用户定义的处理方法代码位于用户态,而在运行这段系统函数期间 CPU 还处于内核态,只能先返回用户态执行用户态代码,再回到内核态做后续工作。所以信号处理的开销还是蛮大的。
总结
发送进程通过 sys_*() 系统调用,将信号信息打包放到目标进程的 task_struct 中,下次操作系统调度进程时,检查这个进程是否有待处理的信号,如果有则先进行信号处理,之后在回到用户态继续执行之前的指令。
消息队列(Msg Queue)
管道的一个劣势在于,数据都是以字节流进行传输,没有格式一说,这就要求管道的读取方和写入方必须事先约定好数据的格式与长度,比如多少字节算作一个消息/命令/记录等等。
为了打破这一限制,诞生了消息队列。其本质上是内核中的链表,每个节点是一个具有特定格式的消息,和 C++ 中的 std::list<Message> 很像。不同的队列以不同的**队列标识符(qid)**来区分,只有内核重启或显式删除后相关队列才会被移除。
和管道相同之处在于,如果管道/队列存放的数据/消息满了,写会被阻塞,读同理。
不同之处在于,虽然写都是写到末尾,但是消息队列可以根据消息字段随机读取,而不一定要从队首。并且,消息队列允许多个进程对其读写。
消息队列使用
1#include <stdio.h>
2#include <stdlib.h>
3#include <string.h>
4#include <sys/ipc.h>
5#include <sys/msg.h>
6#include <sys/types.h>
7
8struct msgbuf { // 待写入消息节点数据结构,必须以 long 类型变量开始
9 long mtype;
10 char mtext[255];
11};
12
13int main() {
14 int mq_id = msgget(123, IPC_CREAT | 0666); // 创建一个消息队列
15 if (mq_id != -1) {
16 // 初始化要发生的消息
17 struct msgbuf mybuf;
18 mybuf.mtype = 1;
19 strcpy(mybuf.mtext, "I'm send process.\n");
20
21 // 发送消息
22 if (msgsnd(mq_id, &mybuf, sizeof(mybuf.mtext), 0)) {
23 printf("success\n");
24 } else {
25 printf("msgsnd Fail\n");
26 }
27 } else {
28 printf("msgget Fail\n");
29 }
30
31 return 0;
32}
1#include <stdio.h>
2#include <stdlib.h>
3#include <string.h>
4#include <sys/ipc.h>
5#include <sys/msg.h>
6#include <sys/types.h>
7
8struct msgbuf { // 待读取消息节点数据结构,必须与消息队列节点格式一致
9 long mtype;
10 char mtext[255];
11};
12
13int main() {
14 int mq_id = msgget(123, IPC_CREAT | 0666); // 获取消息队列
15 if (mq_id != -1) {
16 struct msgbuf mybuf;
17 // 接收第一条消息,存到 mybuf 中
18 if (msgrcv(mq_id, &mybuf, sizeof(mybuf.mtext), 0, IPC_NOWAIT) != -1) {
19 printf("read success: %s\n", mybuf.mtext);
20 // 删除这个消息队列
21 if (msgctl(mq_id, IPC_RMID, 0) != -1) {
22 printf("delete msg success\n");
23 }
24 } else {
25 printf("msgsnd Fail\n");
26 }
27
28 } else {
29 printf("msgget Fail\n");
30 }
31
32 return 0;
33}
共享内存(Shared Memory)
共享内存是指将同一片内存映射到多个进程的虚拟地址空间,进程就可以直接读写这一块内存而不需要进行数据的拷贝,从而大大提高效率。
当然,建立共享内存的过程无需进行数据复制,直接在内存中操作,是最快的可用通信机制。然而还需要一些信号量提供同步/互斥手段,否则会出现一致性的问题。
管道、消息队列都需要将进程内部数据复制到内核空间/磁盘
目前 Linux 系统有以下三种共享内存方案:
- mmap 内存共享映射;
- XSI 共享内存;
- POSIX 共享内存;
mmap 共享内存
mmap 本来的是存储映射功能。它可以将一个文件映射到内存中,在程序里就可以直接使用内存地址对文件内容进行访问,这可以让程序对文件访问更方便。
1#include <sys/mman.h>
2
3// 通过设置 fd,就可以映射到文件,返回值是相应的内存地址。
4void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
5
6// 移除地址 addr 上的共享内存
7int munmap(void *addr, size_t length);
由于这个系统调用的特性可以用在很多场合,所以 Linux 系统用它实现了很多功能,并不仅局限于存储映射,还可以进行共享内存,只需令 mmap() 中 flags = MAP_SHARED|MAP_ANONYMOUS, fd = -1。这样一来,配合 fork(),父子进程都可以获取相同的 mmap() 返回的共享内存地址,从而实现通信。样例代码如下:
1#include <errno.h>
2#include <fcntl.h>
3#include <stdio.h>
4#include <stdlib.h>
5#include <string.h>
6#include <sys/file.h>
7#include <sys/mman.h>
8#include <unistd.h>
9#include <wait.h>
10
11#define COUNT 100
12
13int do_child(int* count) {
14 int interval;
15
16 /* critical section */
17 interval = *count;
18 interval++;
19 usleep(1);
20 *count = interval;
21 /* critical section */
22
23 exit(0);
24}
25
26int main() {
27 pid_t pid;
28 int count;
29 int* shm_p;
30
31 shm_p = (int*)mmap(NULL, sizeof(int), PROT_WRITE | PROT_READ,
32 MAP_SHARED | MAP_ANONYMOUS, -1, 0);
33 if (MAP_FAILED == shm_p) {
34 perror("mmap()");
35 exit(1);
36 }
37
38 *shm_p = 0;
39
40 for (count = 0; count < COUNT; count++) {
41 pid = fork();
42 if (pid < 0) {
43 perror("fork()");
44 exit(1);
45 }
46
47 if (pid == 0) {
48 do_child(shm_p);
49 }
50 }
51
52 for (count = 0; count < COUNT; count++) {
53 wait(NULL);
54 }
55
56 printf("shm_p: %d\n", *shm_p);
57 munmap(shm_p, sizeof(int));
58 exit(0);
59}
XSI 共享内存
mmap 实现共享内存的方式需要 fork() 配合,只能支持相关进程。XSI 采取更为通用的手段,能够支持无关进程。
1#include <sys/ipc.h>
2#include <sys/shm.h>
3#include <sys/types.h>
4
5// 根据指定文件的 inode 编号和文件所在设备的设备编号来生成 key
6key_t ftok(const char *pathname, int proj_id);
7
8// 根据 key 生成/获取共享内存标识符 shmid
9// Linux 限制了系统最大能创建的共享内存为 128 个
10int shmget(key_t key, size_t size, int shmflg);
11
12// 将 {虚拟地址: 物理地址} 的映射绑定到进程内,并令该共享内存的 `shm_nattach`
13// 计数器加一
14void *shmat(int shmid, const void *shmaddr, int shmflg);
15
16// 移除映射,并令该共享内存的 `shm_nattach` 计数器减一
17int shmdt(const void *shmaddr);
18
19// 执行控制命令,如进行删除
20int shmctl(int shmid, int cmd, struct shmid_ds *buf);
但根据管道的经验,无关进程之间的通信能且仅能通过文件来实现,每次根据 path 打开一个文件,进程内部都有一个文件描述符 fd 来指向该文件,从而进行操作。XSI 就是根据这个思路来设计的,key 对应了 path,shmid 对应了 fd。之所以说对应,是因为并不是真正的文件描述符,无法使用 open()/write() 这样的系统调用对其执行操作。事实上,内核里有一张名为 shm_segs[] 的表来存放共享内存相关信息,并根据 shmid 来索引。只要 ftok 的参数是一样的,那么生成的 key 也必然是一样且唯一的,根据 shmget() 得到的 shmid 也是唯一的,这就实现了任意进程能够对共享内存进行访问。样例代码如下:
1#include <errno.h>
2#include <fcntl.h>
3#include <stdio.h>
4#include <stdlib.h>
5#include <string.h>
6#include <sys/file.h>
7#include <sys/ipc.h>
8#include <sys/mman.h>
9#include <sys/shm.h>
10#include <sys/types.h>
11#include <unistd.h>
12#include <wait.h>
13
14#define COUNT 100
15#define PATHNAME "/etc/passwd"
16
17int do_child(int proj_id) {
18 int interval;
19 int *shm_p, shm_id;
20 key_t shm_key;
21 // 使用 ftok() 产生shmkey
22 if ((shm_key = ftok(PATHNAME, proj_id)) == -1) {
23 perror("ftok()");
24 exit(1);
25 }
26
27 // 在子进程中获取到已经在父进程中创建好的 shmid
28 shm_id = shmget(shm_key, sizeof(int), 0);
29 if (shm_id < 0) {
30 perror("shmget()");
31 exit(1);
32 }
33
34 // 将相关共享内存段映射到本进程的内存地址
35 shm_p = (int *)shmat(shm_id, NULL, 0);
36 if ((void *)shm_p == (void *)-1) {
37 perror("shmat()");
38 exit(1);
39 }
40
41 /* critical section */
42 interval = *shm_p;
43 interval++;
44 usleep(1);
45 *shm_p = interval;
46 /* critical section */
47
48 // 解除本进程内对共享内存的地址映射
49 if (shmdt(shm_p) < 0) {
50 perror("shmdt()");
51 exit(1);
52 }
53
54 exit(0);
55}
56
57int main() {
58 pid_t pid;
59 int count;
60 int *shm_p;
61 int shm_id, proj_id;
62 key_t shm_key;
63
64 proj_id = 1234;
65
66 // 使用约定好的文件路径和 proj_id 产生 shm_key
67 if ((shm_key = ftok(PATHNAME, proj_id)) == -1) {
68 perror("ftok()");
69 exit(1);
70 }
71
72 // 创建一个共享内存,如果系统中已经存在此共享内存则报错退出,创建出来的共享内存权限为
73 // 0600(-rw-------)
74 shm_id = shmget(shm_key, sizeof(int), IPC_CREAT | IPC_EXCL | 0600);
75 if (shm_id < 0) {
76 perror("shmget()");
77 exit(1);
78 }
79
80 // 绑定映射
81 shm_p = (int *)shmat(shm_id, NULL, 0);
82 if ((void *)shm_p == (void *)-1) {
83 perror("shmat()");
84 exit(1);
85 }
86
87 *shm_p = 0;
88
89 // 并发读写
90 for (count = 0; count < COUNT; count++) {
91 pid = fork();
92 if (pid < 0) {
93 perror("fork()");
94 exit(1);
95 }
96
97 if (pid == 0) {
98 do_child(proj_id);
99 }
100 }
101
102 // 等待所有子进程执行完毕
103 for (count = 0; count < COUNT; count++) {
104 wait(NULL);
105 }
106
107 // 解除映射
108 if (shmdt(shm_p) < 0) {
109 perror("shmdt()");
110 exit(1);
111 }
112
113 // 删除共享内存
114 if (shmctl(shm_id, IPC_RMID, NULL) < 0) {
115 perror("shmctl()");
116 exit(1);
117 }
118
119 exit(0);
120}
POSIX 共享内存
POSIX 共享内存本质上就是在 /dev/shm 上创建一个文件,并将其 mmap 之后映射其内存地址。