03-进程间通信.md 33 KB


title: 进程间通信 tags: [嵌入式Linux, Linux应用编程, IPC, 管道, FIFO, 消息队列, 信号量, 共享内存, Socket, IMX6ULL] created: 2026-09-18 updated: 2026-09-18

pdf_ref: "《I.MX6U嵌入式Linux C应用编程指南V1.6》第十章 进程间通信简介"

进程间通信

💡 关联知识:[[02-进程线程与IPC/01-信号机制]]、[[02-进程线程与IPC/02-进程管理]]、[[02-进程线程与IPC/04-线程与线程同步]];延伸阅读:[[Linux+C+C++技术体系梳理/2. Linux系统编程/19. 管道]]、[[Linux+C+C++技术体系梳理/2. Linux系统编程/20. 共享内存与mmap]]、[[Linux+C+C++技术体系梳理/2. Linux系统编程/21. 消息队列]]、[[Linux+C+C++技术体系梳理/2. Linux系统编程/22. 信号量]]

所谓进程间通信(interprocess communication,IPC),指的是系统中两个进程之间的通信。不同进程处于各自独立、隔离的地址空间中,因此相互通信比较难,Linux 内核提供了多种 IPC 机制。

本篇定位说明:教材第十章以了解为主,系统介绍了 IPC 的分类与各种机制的概念,未展开各机制的 API 细节。因此本篇结构为:第 1~3 章与第 5 章为教材内容(IPC 概念、机制分类、机制总览);具体的 pipe/mkfifo/msgget/semget/shmget 等 API 与完整源码为扩展知识,各扩展小节均带来源说明,便于你真正上手查询与使用。


1. 进程间通信简介

系统的每一个进程都有各自的地址空间,并且相互独立、隔离,每个进程都处于自己的地址空间中。所以:

  • 同一进程内部的模块(譬如不同函数)之间通信很简单,用全局变量即可;
  • 两个不同进程之间通信通常比较难,因为它们处于不同的地址空间中。

大多数程序是单进程程序(可以有多线程),不需要考虑进程间通信;复杂、大型的应用程序(如 GUI、服务器应用)则会根据实际需要设计成多进程程序,这时才需要 IPC。


2. 内核提供了哪些 IPC 机制

Linux 内核提供的多种 IPC 机制基本从 UNIX 系统继承而来。对 UNIX 发展做出重大贡献的两大主力——AT&T 贝尔实验室与 BSD(加州大学伯克利分校的伯克利软件发布中心)——在进程间通信方面侧重点不同:

  • 前者对早期 UNIX 的进程间通信手段进行了系统的改进和扩充,形成 System V IPC,通信进程局限在单个计算机内;
  • 后者跳过了该限制,形成了基于套接字(Socket,即网络)的进程间通信机制。

Linux 把两者都继承了下来:

flowchart TD
    root["Linux IPC 机制"]
    root --> u["早期 UNIX IPC"]
    root --> sv["System V IPC"]
    root --> posix["POSIX IPC"]
    root --> sock["Socket IPC"]

    u --> u1["管道 pipe"]
    u --> u2["FIFO 有名管道"]
    u --> u3["信号 signal"]

    sv --> s1["System V 信号量"]
    sv --> s2["System V 消息队列"]
    sv --> s3["System V 共享内存"]

    posix --> p1["POSIX 信号量"]
    posix --> p2["POSIX 消息队列"]
    posix --> p3["POSIX 共享内存"]

    sock --> k1["基于 Socket 的进程间通信"]

总结:

  • UNIX IPC:管道、FIFO、信号;
  • System V IPC:信号量、消息队列、共享内存;
  • POSIX IPC:信号量、消息队列、共享内存;
  • Socket IPC:基于 Socket 的进程间通信。

较早的 System V IPC 存在一些不足之处,而 POSIX IPC 是在 System V IPC 的基础上改进形成的,弥补了它的一些不足。

2.1 机制总览对比

⚠️ 来源说明:下表"适用场景与限制"的细节为扩展知识,机制分类本身来自教材。

机制 类别 通信方向 能否跨非亲缘进程 数据形式 典型适用场景 主要限制
管道 pipe UNIX IPC 单工 否,仅父子/兄弟 无格式字节流 shell 管道、父子进程简单数据流 只能亲缘进程、单向
FIFO 有名管道 UNIX IPC 半双工/双向 无格式字节流 无亲缘关系进程间简单数据流 字节流无消息边界
信号 signal UNIX IPC 单向通知 编号 + 少量伴随数据 异步事件通知、进程同步 传递信息少
System V / POSIX 消息队列 System V/POSIX 双向 有格式的消息 需要消息边界、异步收发 内核中受系统限制
System V / POSIX 信号量 System V/POSIX 不传数据 计数器 访问共享资源的同步/互斥 只做同步,不传数据
System V / POSIX 共享内存 System V/POSIX 双向 裸内存 大数据量、高性能共享 需自行配合同步机制
Socket Socket IPC 全双工 是(可跨主机) 字节流/数据报 网络通信、跨主机、跨语言 协议栈开销

3. 管道和 FIFO

管道是 UNIX 系统上最古老的 IPC 方法,它在 20 世纪 70 年代早期 UNIX 的第三个版本上就出现了。把一个进程连接到另一个进程的数据流称为管道,管道被抽象成一个文件(pipe 文件类型)。

管道包括三种:

类型 单工/双工 适用范围
普通管道 pipe 单工,数据只能单向传输 只能在父子或兄弟进程间使用
流管道 s_pipe 半双工,可以双向传输 只能在父子或兄弟进程间使用
有名管道 name_pipe(FIFO) 双向 允许在不相关(非父子/兄弟)的进程间通信
  • 普通管道:用于具有亲缘关系的进程间通信,数据只能单向传输;要实现双向传输必须使用两个管道
  • 流管道:去除了普通管道的单向限制,以半双工方式双向传输,但仍只能在亲缘进程间通信。
  • 有名管道 FIFO:同时突破普通管道的两种限制,既可双向传输,又能在非亲缘关系进程间通信。

3.1 管道(pipe)读写细节

⚠️ 来源说明:本节 API 原型与阻塞规则不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。

#include <unistd.h>

int pipe(int pipefd[2]);
参数 含义
pipefd 输出参数,pipefd[0] 为读端,pipefd[1] 为写端
返回值 含义
成功 返回 0
失败 返回 -1,并设置 errno

典型用法:父进程 pipe()fork(),子进程继承两个 fd;关闭不用的一端后即可单向通信(父写 pipefd[1],子读 pipefd[0],或反之)。

flowchart LR
    P["父进程"] -->|"write(pipefd[1], buf, n)"| PIPE[("管道<br/>内核缓冲区")]
    PIPE -->|"read(pipefd[0], buf, n)"| C["子进程"]

读写阻塞规则

情况 行为
管道为空,仍有写端打开 read() 阻塞,直到有数据写入
所有写端都已关闭 read() 不再阻塞,返回 0(读到 EOF)
管道缓冲区已满 write() 阻塞,直到有空间
所有读端都已关闭 write() 触发 SIGPIPE 信号,默认终止进程(write 返回 -1,errnoEPIPE
写入量 ≤ PIPE_BUF(Linux 默认 4096) 写入是原子的,不会与其它进程的写交错

要点:管道是无格式字节流,没有消息边界;读端关闭后写端继续写会收到 SIGPIPE,因此健壮的程序应处理或忽略 SIGPIPE

3.2 有名管道 FIFO

⚠️ 来源说明:本节 API 原型与打开规则不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。

FIFO 在文件系统中以一个文件存在(文件类型为 p),因此无亲缘关系的进程只要知道路径就能打开它通信。

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

int mkfifo(const char *pathname, mode_t mode);
参数 含义
pathname FIFO 文件路径
mode 权限位(会与 umask 作用)
返回值 含义
成功 返回 0
失败 返回 -1,并设置 errno(如 EEXIST

创建后,用 open()read()write()close() 像普通文件一样操作,规则与 pipe 一致。打开阻塞规则

打开方式 行为
open(path, O_RDONLY) 阻塞,直到有进程以写方式打开该 FIFO
open(path, O_WRONLY) 阻塞,直到有进程以读方式打开该 FIFO
O_NONBLOCK 只读打开 立即成功返回
O_NONBLOCK 只写打开 若无读者,返回 -1,errnoENXIO

3.3 shell 中的管道

shell 的 | 就是 pipe 的经典应用:ls -l | grep .c 中,shell 创建管道并让 ls 的标准输出接到管道写端、grep 的标准输入接到读端。


4. 信号

关于信号的内容在第八章已经介绍过:信号用于通知接收信号的进程有某种事件发生,因此可用于进程间通信;除了用于进程间通信之外,进程还可以发送信号给进程本身。

信号作为 IPC 手段的特点:

  • 传递的信息量少(一个信号编号,加实时信号的少量伴随数据);
  • 主要作为异步事件通知与进程同步手段,而不是数据传输手段;
  • 可靠信号支持排队与伴随数据。

详细用法见 [[02-进程线程与IPC/01-信号机制]]。


5. 消息队列

消息队列是消息的链表,存放在内核中并由消息队列标识符标识。它克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺陷。消息队列包括 POSIX 消息队列和 System V 消息队列。

消息队列是 UNIX 下不同进程之间实现共享资源的一种机制:UNIX 允许不同进程将格式化的数据流以消息队列形式发送给任意进程;有足够权限的进程可以向队列中添加消息,被赋予读权限的进程则可以读走队列中的消息

5.1 System V 消息队列 API

⚠️ 来源说明:本节 API 原型、结构体与示例不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

key_t ftok(const char *pathname, int proj_id);              /* 生成 IPC key */

int msgget(key_t key, int msgflg);                          /* 创建/获取消息队列 */
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);   /* 发送消息 */
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg); /* 接收 */
int msgctl(int msqid, int cmd, struct msqid_ds *buf);       /* 控制 */
函数 参数 返回值
msgget key IPC 键;msgflgIPC_CREAT \| 0666 成功返回消息队列 ID;失败 -1 并设 errno
msgsnd msqid 队列 ID;msgp 消息指针;msgsz 消息正文长度(不含 mtype);msgflgIPC_NOWAIT 成功 0;失败 -1 并设 errno
msgrcv msqidmsgp 接收缓冲区;msgsz 缓冲区正文容量;msgtyp 指定接收哪类消息;msgflg 成功返回实际接收字节数;失败 -1 并设 errno
msgctl msqidcmdIPC_RMID(删除队列)、IPC_STATIPC_SETbuf 成功 0;失败 -1 并设 errno

消息结构(用户自定义,mtype 必须为正):

struct msgbuf {
    long mtype;       /* 消息类型,必须 > 0 */
    char mtext[1];    /* 消息正文,长度由 msgsz 指定 */
};

msgtyp 取值规则:

msgtyp 含义
0 接收队列中的第一条消息
> 0 接收第一条类型为 msgtyp 的消息
< 0 接收类型值 ≤ `

msgsnd / msgrcv 在队列满 / 为空时默认阻塞,加 IPC_NOWAIT 则非阻塞返回(errnoEAGAIN / ENOMSG)。


6. 信号量

信号量是一个计数器,与其它 IPC 方式不同:它主要用于控制多个进程间或一个进程内多个线程间对共享资源的访问,相当于内存中的标志。进程可根据它判定是否能够访问某些共享资源,同时也可以修改该标志。除了用于共享资源的访问控制外,还可用于进程同步

它常作为一种锁机制,防止某进程访问资源时其它进程也访问该资源,因此主要作为进程间以及同一进程内不同线程之间的同步手段。Linux 提供了一组接口来操作信号量,声明在头文件 sys/sem.h 中。

6.1 System V 信号量 API 与 PV 操作

⚠️ 来源说明:本节 API 原型、结构体与示例不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

int semget(key_t key, int nsems, int semflg);                  /* 创建/获取信号量集 */
int semop(int semid, struct sembuf *sops, size_t nsops);       /* PV 操作 */
int semctl(int semid, int semnum, int cmd, ...);               /* 控制 */
函数 参数 返回值
semget key IPC 键;nsems 信号量个数;semflgIPC_CREAT \| 0666 成功返回信号量集 ID;失败 -1 并设 errno
semop semidsops 操作数组;nsops 操作个数 成功 0;失败 -1 并设 errno
semctl semidsemnum 第几个信号量;cmdSETVAL/GETVAL/IPC_RMID;第四个参数为 union semun cmd 而定
struct sembuf {
    unsigned short sem_num;   /* 信号量在集合中的下标 */
    short          sem_op;    /* 操作值:>0 表示 V 操作(释放);<0 表示 P 操作(申请);0 表示等待为 0 */
    short          sem_flg;   /* 如 IPC_NOWAIT、SEM_UNDO */
};

P / V 语义(扩展):

  • P 操作(申请资源)sem_op = -1,信号量减 1;若当前值为 0,则阻塞等待,直到有进程释放。
  • V 操作(释放资源)sem_op = +1,信号量加 1,唤醒等待者。
  • 等待为 0sem_op = 0,阻塞直到信号量值为 0,用于同步等待。

semctl 的常用 cmd

cmd 含义
SETVAL 设置某个信号量的初值(需传 union semun.val
GETVAL 获取某个信号量的当前值
IPC_RMID 删除整个信号量集
IPC_STAT / IPC_SET 获取/设置状态信息

union semun 需由用户自行定义(部分系统在头文件中提供):

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

7. 共享内存

共享内存就是映射一段能被其它进程所访问的内存:这段共享内存由一个进程创建,但其它多个进程都可以访问,使得多个进程可以访问同一块内存空间

共享内存是最快的 IPC 方式,它是针对其它进程间通信方式运行效率低而专门设计的。它往往与其它通信机制(譬如结合信号量)来使用,以实现进程间的同步和通信——因为共享内存本身不提供互斥,多个进程同时写会产生竞态。

7.1 System V 共享内存 API

⚠️ 来源说明:本节 API 原型与示例不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。

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

int shmget(key_t key, size_t size, int shmflg);                        /* 创建/获取 */
void *shmat(int shmid, const void *shmaddr, int shmflg);              /* 挂载到进程地址空间 */
int shmdt(const void *shmaddr);                                       /* 脱离 */
int shmctl(int shmid, int cmd, struct shmid_ds *buf);                 /* 控制 */
函数 参数 返回值
shmget key IPC 键;size 共享内存大小;shmflgIPC_CREAT \| 0666 成功返回共享内存 ID;失败 -1 并设 errno
shmat shmidshmaddr 一般为 NULL(由内核选择地址);shmflg0 / SHM_RDONLY 成功返回映射后的虚拟地址;失败返回 (void *)-1
shmdt shmaddrshmat 返回的地址 成功 0;失败 -1 并设 errno
shmctl shmidcmdIPC_RMIDIPC_STATbuf 成功 0;失败 -1 并设 errno

使用流程:

flowchart LR
    A["进程A: shmget 创建"] --> B["shmat 挂载"]
    B --> C["读写共享内存"]
    D["进程B: shmget 以同一 key 获取"] --> E["shmat 挂载"]
    E --> C
    C --> F["shmdt 脱离"]
    F --> G["shmctl IPC_RMID 删除"]

7.2 与 mmap 对比

⚠️ 来源说明:本节为扩展知识。

mmap() 也能实现内存映射,常用于共享内存与文件映射。两者定位不同:

维度 System V 共享内存 mmap
载体 匿名内核共享内存段 文件或匿名映射(MAP_SHARED
持久化 直到 IPC_RMID,不随进程退出消失 文件映射随文件持久化;匿名映射随进程组消失
生命周期管理 shmctl(IPC_RMID) unlink 文件 / 自动回收
接口 shmget/shmat/shmdt/shmctl mmap/munmap/msync
与文件 IO 关系 无文件对象 可与文件内容直接关联,读写即文件读写
常见用途 纯进程间大块数据共享 文件映射、设备映射(如 FrameBuffer)、匿名共享内存

二者都需要自行配合同步机制(信号量、互斥锁、信号)使用。


8. 套接字(Socket)

Socket 是一种 IPC 方法,是基于网络的 IPC 方法,允许位于同一主机或使用网络连接起来的不同主机上的应用程序之间交换数据,说白了就是网络通信。

在一个典型的客户端/服务器场景中,应用使用 socket 通信的方式如下:

  • 各个应用程序创建一个 socket。socket 是一个允许通信的"设备",两个应用程序都需要用到它;
  • 服务器将自己的 socket 绑定到一个众所周知的地址(名称)上,使得客户端能够定位到它的位置。

Socket 的具体内容在网络编程章节介绍。其最大优势是全双工、可跨主机、语言无关;代价是协议栈开销。


9. 完整可复制源码

9.1 管道:父子进程通信

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

int main(void)
{
    int pipefd[2];
    pid_t pid;
    char buf[128] = {0};

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

    pid = fork();
    if (-1 == pid) {
        perror("fork error");
        exit(-1);
    }

    if (0 == pid) {
        /* 子进程:只读,关闭写端 */
        close(pipefd[1]);
        ssize_t n = read(pipefd[0], buf, sizeof(buf) - 1);
        if (n > 0) {
            buf[n] = '\0';
            printf("子进程读到: %s\n", buf);
        }
        close(pipefd[0]);
        _exit(0);
    }

    /* 父进程:只写,关闭读端 */
    close(pipefd[0]);
    const char *msg = "Hello from parent via pipe";
    write(pipefd[1], msg, strlen(msg));
    close(pipefd[1]);

    wait(NULL);
    exit(0);
}

逐段解释

  • pipe(pipefd) 创建管道,pipefd[0] 读、pipefd[1] 写。
  • fork() 后子进程继承两个 fd;子进程关闭写端、父进程关闭读端,形成单向数据流。
  • 父进程 write 后关闭写端;子进程 read 到数据后打印。若父进程关闭写端时子进程还没读,read 仍能读到缓冲数据,读完再次 read 会返回 0(EOF)。

9.2 FIFO:两个独立进程通信

写端 fifo_write.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>

#define FIFO_PATH "./my_fifo"

int main(void)
{
    int fd;
    const char *msg = "Hello from FIFO writer";

    /* 不存在则创建,EEXIST 忽略 */
    if (-1 == mkfifo(FIFO_PATH, 0666) && errno != EEXIST) {
        perror("mkfifo error");
        exit(-1);
    }

    /* 写方式打开会阻塞,直到有进程以读方式打开 */
    fd = open(FIFO_PATH, O_WRONLY);
    if (-1 == fd) {
        perror("open error");
        exit(-1);
    }

    write(fd, msg, strlen(msg));
    close(fd);

    return 0;
}

读端 fifo_read.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>

#define FIFO_PATH "./my_fifo"

int main(void)
{
    int fd;
    char buf[128] = {0};

    if (-1 == mkfifo(FIFO_PATH, 0666) && errno != EEXIST) {
        perror("mkfifo error");
        exit(-1);
    }

    fd = open(FIFO_PATH, O_RDONLY);
    if (-1 == fd) {
        perror("open error");
        exit(-1);
    }

    ssize_t n = read(fd, buf, sizeof(buf) - 1);
    if (n > 0) {
        buf[n] = '\0';
        printf("FIFO 读端收到: %s\n", buf);
    }
    close(fd);

    return 0;
}

编译与运行(需两个终端,先运行读端再运行写端):

gcc -Wall -o fifo_write fifo_write.c
gcc -Wall -o fifo_read  fifo_read.c
# 终端1
./fifo_read
# 终端2
./fifo_write

9.3 System V 消息队列:发送与接收

发送端 msg_send.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

#define MSG_KEY  0x1234

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

int main(void)
{
    int msqid;
    struct msgbuf msg;

    /* 打开或创建消息队列 */
    msqid = msgget(MSG_KEY, IPC_CREAT | 0666);
    if (-1 == msqid) {
        perror("msgget error");
        exit(-1);
    }

    msg.mtype = 1;
    strcpy(msg.mtext, "Hello System V message queue");

    if (-1 == msgsnd(msqid, &msg, strlen(msg.mtext) + 1, 0)) {
        perror("msgsnd error");
        exit(-1);
    }

    puts("消息发送成功");
    return 0;
}

接收端 msg_recv.c

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

#define MSG_KEY  0x1234

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

int main(void)
{
    int msqid;
    struct msgbuf msg;

    msqid = msgget(MSG_KEY, IPC_CREAT | 0666);
    if (-1 == msqid) {
        perror("msgget error");
        exit(-1);
    }

    /* msgtyp = 0:接收第一条消息 */
    if (-1 == msgrcv(msqid, &msg, sizeof(msg.mtext), 0, 0)) {
        perror("msgrcv error");
        exit(-1);
    }

    printf("收到消息(type=%ld): %s\n", msg.mtype, msg.mtext);

    /* 删除消息队列 */
    msgctl(msqid, IPC_RMID, NULL);
    return 0;
}

逐段解释

  • msgget(MSG_KEY, IPC_CREAT | 0666):以固定 key 创建/获取队列,双方用同一 key 找到同一队列。
  • msgsndmsgsz正文长度,不含 mtypemtype 必须大于 0。
  • msgrcvmsgtyp = 0 表示取第一条消息;msgsz 是接收缓冲区正文容量。
  • 接收端 msgctl(msqid, IPC_RMID, NULL) 删除队列,释放内核资源。

9.4 System V 信号量:PV 互斥

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <unistd.h>

#define SEM_KEY 0x5678

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

/* P 操作:申请资源,信号量 -1 */
static int sem_p(int semid)
{
    struct sembuf op = { .sem_num = 0, .sem_op = -1, .sem_flg = 0 };
    return semop(semid, &op, 1);
}

/* V 操作:释放资源,信号量 +1 */
static int sem_v(int semid)
{
    struct sembuf op = { .sem_num = 0, .sem_op = 1, .sem_flg = 0 };
    return semop(semid, &op, 1);
}

int main(void)
{
    int semid;
    union semun arg;

    /* 创建含 1 个信号量的集合 */
    semid = semget(SEM_KEY, 1, IPC_CREAT | 0666);
    if (-1 == semid) {
        perror("semget error");
        exit(-1);
    }

    /* 初值设为 1(当作互斥锁) */
    arg.val = 1;
    if (-1 == semctl(semid, 0, SETVAL, arg)) {
        perror("semctl error");
        exit(-1);
    }

    sem_p(semid);       /* 进入临界区 */
    puts("进入临界区,持有信号量...");
    sleep(2);
    sem_v(semid);       /* 退出临界区 */

    puts("退出临界区");
    semctl(semid, 0, IPC_RMID);   /* 删除信号量集 */
    return 0;
}

9.5 System V 共享内存

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>

#define SHM_KEY 0x9abc
#define SHM_SIZE 4096

int main(void)
{
    int shmid;
    char *addr;

    /* 创建共享内存段 */
    shmid = shmget(SHM_KEY, SHM_SIZE, IPC_CREAT | 0666);
    if (-1 == shmid) {
        perror("shmget error");
        exit(-1);
    }

    /* 挂载到当前进程地址空间(shmaddr 为 NULL) */
    addr = shmat(shmid, NULL, 0);
    if ((void *)-1 == addr) {
        perror("shmat error");
        exit(-1);
    }

    /* 直接像普通内存一样读写 */
    strcpy(addr, "Hello shared memory");
    printf("写入共享内存: %s\n", addr);

    /* 脱离 */
    if (-1 == shmdt(addr)) {
        perror("shmdt error");
        exit(-1);
    }

    /* 删除共享内存段 */
    shmctl(shmid, IPC_RMID, NULL);
    return 0;
}

逐段解释

  • shmget 以 key 创建共享内存,其它进程用同一 key + shmget 即可获取同一段内存。
  • shmat 返回映射后的虚拟地址,之后直接当普通内存读写(无需 read/write),这是共享内存高效的原因。
  • shmdt 脱离映射;shmctl(IPC_RMID) 删除共享内存段。注意删除后已挂载的进程仍可继续访问,直到最后脱离。

10. 实验步骤与调试方法

10.1 编译运行

gcc -Wall -o pipe_demo  pipe_demo.c
gcc -Wall -o msg_send   msg_send.c
gcc -Wall -o msg_recv   msg_recv.c
gcc -Wall -o sem_demo   sem_demo.c
gcc -Wall -o shm_demo   shm_demo.c

./pipe_demo
./msg_send && ./msg_recv
./sem_demo
./shm_demo

10.2 常用调试命令

命令 用途
ls -l <fifo> 查看 FIFO 文件,类型为 p
ipcs 查看所有 System V IPC 对象(共享内存/消息队列/信号量)
ipcs -q 只看消息队列
ipcs -m 只看共享内存
ipcs -s 只看信号量
ipcrm -q <msqid> 删除消息队列
ipcrm -m <shmid> 删除共享内存
ipcrm -s <semid> 删除信号量集
strace ./app 跟踪 pipe/read/write/msgget 等系统调用

10.3 实验一:验证管道 EOF 与 SIGPIPE

  • 父进程写完立即 close(pipefd[1]),子进程 read 完数据后再次 read 会返回 0,说明读到 EOF;
  • 反过来,让读端提前关闭,写端继续 write,进程会收到 SIGPIPE 默认终止。用 signal(SIGPIPE, SIG_IGN) 忽略后,write 返回 -1、errnoEPIPE

10.4 实验二:验证消息队列持久化

先运行 ./msg_send,再在另一个时刻运行 ./msg_recv:消息仍能收到,因为消息队列存在于内核中,不随发送进程退出而消失。用 ipcs -q 可看到队列,ipcrm -q <id> 清理。

10.5 实验三:观察 FIFO 打开阻塞

先运行 ./fifo_read(只读打开阻塞等待写者),此时进程挂起;再运行 ./fifo_write,两端同时被唤醒完成通信。用 ps 可看到读端处于睡眠状态。

10.6 实验四:共享内存 + 信号量配合

共享内存本身不互斥。可写一个程序模拟"多个进程同时写同一块共享内存",无信号量保护时观察数据错乱;再用信号量 P/V 包住临界区,数据保持一致。


11. 跨平台对比:IMX6ULL vs STM32 vs RK3568

维度 I.MX6ULL(本教程) STM32(裸机/RTOS) RK3568(Linux)
进程隔离 完整 MMU 隔离,需 IPC 通常单地址空间,全局变量即可 同 I.MX6ULL
管道/FIFO 支持 无(可用环形缓冲区替代) 支持
消息队列 System V / POSIX RTOS 消息队列(如 FreeRTOS xQueueSend 同 I.MX6ULL
信号量 System V / POSIX RTOS 信号量/互斥量 同 I.MX6ULL
共享内存 shmget 系列、mmap 直接访问全局内存/双缓冲 同 I.MX6ULL
跨主机通信 Socket 需外接网络模块 + 协议栈 Socket
调试工具 ipcs/ipcrm/ls -l 调试器观察内存 同 I.MX6ULL

要点:

  • STM32 裸机上"进程间通信"概念基本不成立,因为没有进程隔离,全局变量、RTOS 队列/信号量就够用;只有到 Linux 这类带 MMU 的系统,IPC 才是刚需。
  • I.MX6ULL 与 RK3568 都是 Linux,System V IPC 接口通用(RK3568 通常内核配置更全,POSIX IPC、mq_*sem_* 都可用)。
  • 嵌入式 Linux 上使用 System V IPC 要注意:内核资源有限msgmnishmmaxsemmni 等参数),对象用完要及时 IPC_RMID,否则会泄漏内核资源。

12. 面试精选(5 题)

Q1:Linux 有哪些进程间通信机制?各自适合什么场景?

答案:分四类。UNIX IPC:管道(亲缘进程、单向字节流)、FIFO 有名管道(无亲缘进程、可双向)、信号(异步事件通知、信息量少)。System V IPC:消息队列(有格式消息、异步收发、有消息边界)、信号量(计数器,做同步/互斥,不传数据)、共享内存(大块数据、最快的 IPC,需自配同步)。POSIX IPC:与 System V 对应,接口更现代(sem_open/mq_open/shm_open)。Socket IPC:全双工、可跨主机、跨语言,适合网络与复杂场景。选型原则:小数据同步用信号/信号量;简单字节流用管道/FIFO;需要消息边界用消息队列;大数据量高性能用共享内存 + 信号量;跨主机用 Socket。

Q2:管道的读写分别在什么情况下阻塞?读端关闭后写端继续写会怎样?

答案read:管道为空且仍有写端打开时阻塞;所有写端关闭后不再阻塞,返回 0(EOF)。write:管道缓冲区满时阻塞,直到有空间。若所有读端都已关闭,写端继续 write 会触发 SIGPIPE 信号,默认终止进程,write 返回 -1 且 errno = EPIPE。因此健壮程序应忽略或处理 SIGPIPE,例如 signal(SIGPIPE, SIG_IGN) 后再判断返回值。此外管道写入量不超过 PIPE_BUF(Linux 默认 4096 字节)时是原子写,多进程写不会交错。

Q3:消息队列相对管道有什么优势?msgrcvmsgtyp 参数怎么用?

答案:消息队列存放在内核中,有消息边界(保留消息长度),支持按类型选择性接收,还能突破管道"缓冲区大小受限"和"只能承载无格式字节流"的缺陷;且不要求收发进程有亲缘关系,发送方发完即可退出(消息保留在内核)。msgrcvmsgtyp:等于 0 接收队列中第一条消息;大于 0 接收第一条类型等于 msgtyp 的消息;小于 0 接收类型值 ≤ |msgtyp| 的消息中类型最小的那一条。配合 IPC_NOWAIT 可非阻塞读取。

Q4:共享内存为什么是最快的 IPC?为什么它通常要和信号量配合使用?

答案:共享内存把同一块物理内存映射到多个进程各自的虚拟地址空间,进程读写它就像读写自己的内存——没有任何内核数据拷贝。管道、消息队列等都要经历"用户空间→内核缓冲区→用户空间"的两次拷贝,因此共享内存最快。但它本身不提供任何同步或互斥:多个进程同时读写会产生竞态、读到不一致数据。所以通常配一个信号量(或互斥锁)来保证同一时刻只有一个进程进入临界区,实现"高效数据传输 + 正确同步"的组合。

Q5:fork() 之后父子进程如何通信?请描述一个基于管道的实现,并说明为什么要关闭不用的文件描述符。

答案:先 pipe(pipefd) 创建管道,再 fork()。由于子进程继承父进程的文件描述符,父子都能访问 pipefd[0](读端)和 pipefd[1](写端)。实现单向通信:若要父写子读,父进程关闭 pipefd[0]、子进程关闭 pipefd[1],父进程 write(pipefd[1], ...)、子进程 read(pipefd[0], ...)必须关闭不用的端,否则会有两个问题:① 无法正确产生 EOF——只有当所有写端都关闭后,读端的 read 才返回 0,如果不关闭子进程里继承的写端,父进程关闭了自己的写端,管道仍被视为有写者打开,读端会一直阻塞;② 无谓地占用文件描述符、影响资源回收。这正是"每个进程只保留自己需要的一端"的原因。


内容来源:《I.MX6U嵌入式Linux C应用编程指南》第十章 进程间通信简介