📚 Linux 内核专栏 · 持续更新中
本系列从底层原理到源码实战,逐篇拆解内核机制。
👉 想系统追更,戳这里查看完整专栏:
👉 其他技术系列专栏,欢迎关注、交流、共同成长:
本篇定位 :Linux 内核专题 L2 补充篇 (进程间通信 IPC 上半部)。【linux内核专栏 03】进程管理与调度 讲了进程"是什么"、【linux内核专栏 07】同步与并发 讲了"内核态怎么加锁"------但两个进程怎么交换数据、怎么通知对方 ,这套机制叫 IPC(Inter-Process Communication) 。IPC 分两篇:本篇(上)讲用户态进程间的经典通信------管道(pipe/FIFO)、信号(signal)、System V IPC 三件套(消息队列/共享内存/信号量)、mmap 共享内存、文件锁;下篇(18)讲 Socket/Netlink/fd 三件套/futex/线程同步 + 内核↔用户态通信。读完你能选对 IPC、能讲清每种机制内核怎么实现、能对照 FreeRTOS 队列/任务通知。
嵌入式视角:IPC 是你从 RTOS 转过来最容易"会用不会选"的一块FreeRTOS 通信就两招:队列 (任务间传数据)+ 任务通知 (轻量事件)。到了 Linux,IPC 突然冒出十几种:管道、信号、消息队列、共享内存、信号量、socket、eventfd......选哪个 是 BSP 工程师天天要拍的板。
核心心法:
① 流式传数据 → 管道/socket;
② 事件通知 → 信号/eventfd;
③ 大数据共享 → 共享内存(mmap/shm);
④ 同步 → 信号量/mutex;
⑤ 内核↔用户 → Netlink/ioctl/procfs(下篇)。
本篇先吃透用户态间通信,下篇吃透内核交互。
目录
- [一、IPC 全景与选择决策树](#一、IPC 全景与选择决策树)
-
- [1.1 为什么需要 IPC](#1.1 为什么需要 IPC)
- [1.2 IPC 机制全景](#1.2 IPC 机制全景)
- [1.3 五大维度分类](#1.3 五大维度分类)
- [1.4 选择决策树](#1.4 选择决策树)
- [二、管道 pipe 与 FIFO](#二、管道 pipe 与 FIFO)
-
- [2.1 匿名管道 pipe](#2.1 匿名管道 pipe)
- [2.2 内核实现:pipe_buffer](#2.2 内核实现:pipe_buffer)
- [2.3 命名管道 FIFO](#2.3 命名管道 FIFO)
- [三、信号 signal](#三、信号 signal)
-
- [3.1 信号是什么](#3.1 信号是什么)
- [3.2 产生信号](#3.2 产生信号)
- [3.3 捕获与处理:sigaction](#3.3 捕获与处理:sigaction)
- [3.4 信号掩码(阻塞)](#3.4 信号掩码(阻塞))
- [3.5 信号中断系统调用:EINTR](#3.5 信号中断系统调用:EINTR)
- [3.6 内核信号结构(对接 【linux内核专栏 03】进程管理与调度(https://blog.csdn.net/peter_pan_study/article/details/166849786) task_struct)](#3.6 内核信号结构(对接 【linux内核专栏 03】进程管理与调度 task_struct))
- [四、System V IPC 三件套](#四、System V IPC 三件套)
-
- [4.1 共性:key / 对象 / ipcs](#4.1 共性:key / 对象 / ipcs)
- [4.2 消息队列 msgq](#4.2 消息队列 msgq)
- [4.3 共享内存 shm(最快 IPC)](#4.3 共享内存 shm(最快 IPC))
- [4.4 信号量 sem(SysV,跨进程同步)](#4.4 信号量 sem(SysV,跨进程同步))
- [4.5 System V IPC 内核实现共性](#4.5 System V IPC 内核实现共性)
- [五、POSIX IPC](#五、POSIX IPC)
- [六、mmap 共享内存](#六、mmap 共享内存)
-
- [6.1 mmap 做 IPC 的两种方式](#6.1 mmap 做 IPC 的两种方式)
- [6.2 MAP_SHARED vs MAP_PRIVATE](#6.2 MAP_SHARED vs MAP_PRIVATE)
- [6.3 共享内存的同步(必配)](#6.3 共享内存的同步(必配))
- [七、文件锁 flock 与 fcntl](#七、文件锁 flock 与 fcntl)
-
- [7.1 flock(建议锁,整文件)](#7.1 flock(建议锁,整文件))
- [7.2 fcntl(记录锁/租约锁,按字节区段)](#7.2 fcntl(记录锁/租约锁,按字节区段))
- [八、对照 FreeRTOS / RISC-V](#八、对照 FreeRTOS / RISC-V)
-
- [8.1 IPC 机制对照](#8.1 IPC 机制对照)
- [8.2 本质差异](#8.2 本质差异)
- [8.3 RISC-V 视角](#8.3 RISC-V 视角)
- [九、AEGIX BSP 实战](#九、AEGIX BSP 实战)
-
- [9.1 守护进程信号处理(SIGHUP 重载 / SIGCHLD 收尸)](#9.1 守护进程信号处理(SIGHUP 重载 / SIGCHLD 收尸))
- [9.2 共享内存 + 信号量:多进程配置共享](#9.2 共享内存 + 信号量:多进程配置共享)
- [9.3 管道拉起子进程读输出](#9.3 管道拉起子进程读输出)
- [9.4 选择实战(本篇机制)](#9.4 选择实战(本篇机制))
- 十、小结速查与决策树
-
- [10.1 机制速查](#10.1 机制速查)
- [10.2 选择决策树(精简)](#10.2 选择决策树(精简))
- [10.3 陷阱速查](#10.3 陷阱速查)
- [10.4 内核对象速查](#10.4 内核对象速查)
- 十一、相关笔记
一、IPC 全景与选择决策树
1.1 为什么需要 IPC
进程的根本隔离 :每个进程有独立虚拟地址空间(【linux内核专栏 04】内存管理 讲过 mm_struct),进程 A 的 0x4000 和进程 B 的 0x4000 是两块物理内存,默认谁也碰不到谁。这是保护,但也意味着"想交换数据必须经内核搭桥"------这个桥就是 IPC。
进程隔离 vs FreeRTOS 任务共享
FreeRTOS 所有任务共享一个地址空间 ,任务间传数据直接读写全局变量(配临界区)即可,根本不需要 IPC 。Linux 进程隔离,共享必须经内核------这是"多进程"对"单地址空间多任务"的核心代价。线程则共享地址空间(同进程的线程直接读写全局),所以"线程间通信"基本就是"共享变量 + 同步原语",不需要 IPC 机制本身(见下篇 pthread)。
1.2 IPC 机制全景
| 机制 | 通信模式 | 范围 | 内核对象 | 典型用途 |
|---|---|---|---|---|
| 管道 pipe | 字节流,单向,FIFO | 父子进程 | pipe_inode | shell ` |
| FIFO | 字节流,单向,有名 | 任意进程 | 文件系统 inode | mkfifo 命名管道 |
| 信号 signal | 事件通知(1 个信号号) | 任意进程 | signal_struct | kill/Ctrl+C/SIGCHLD |
| 消息队列 msgq | 消息(带类型),有序 | 任意进程(SysV key) | msqid_ds | 按类型分发消息 |
| 共享内存 shm | 共享物理页,最快 | 任意进程(SysV key) | shmid_kernel | 大数据零拷贝共享 |
| 信号量 sem(SysV) | 同步(资源计数) | 任意进程 | sem_array | 跨进程同步(非传输) |
| mmap 共享 | 共享物理页(文件/匿名) | 任意(有名)/亲缘(匿名) | 文件 mapping/匿名 | 大数据共享,替代 shm |
| 文件锁 flock/fcntl | 文件同步 | 同文件进程 | file_lock | 多进程写同一文件 |
| Unix socket(下篇) | 流/数据报,双向 | 同主机 | socket | 本机高效双向通信 |
| Netlink(下篇) | 内核↔用户 | 内核↔用户 | socket | 驱动事件/路由/配置 |
| eventfd/signalfd/timerfd(下篇) | 事件 fd | 任意进程 | eventfd_ctx | fd 化通知,配 epoll |
1.3 五大维度分类
按"传什么"分:
① 数据流 → 管道/FIFO/socket(字节流,边界消失)
② 消息(带边界)→ 消息队列/socket 数据报(保留消息边界)
③ 共享内存 → mmap/shm(直接共享页,最快,需自己同步)
④ 事件 → 信号/eventfd(只通知,不传数据)
⑤ 同步 → 信号量/mutex/文件锁(协调访问顺序)
按"有无边界"分:
① 流式(字节边界消失)→ 管道/FIFO/socket 流
② 消息(保留边界) → 消息队列/socket 数据报
按"内核是否拷贝"分:
① 拷贝(内核中转) → 管道/消息队列/socket(写者→内核缓冲→读者)
② 零拷贝(共享页) → mmap/shm(映射同一物理页,无拷贝,最快)
按"进程关系"分:
① 仅亲缘(父子) → 匿名 pipe/匿名 mmap
② 任意进程 → FIFO/SysV IPC/有名 mmap/socket
1.4 选择决策树
要传数据还是只通知?
├─ 只通知(无数据/极少)
│ ├─ 事件简单 → signal(kill, 1 个信号号)
│ └─ 要配 epoll → eventfd(下篇,推荐)
│
├─ 传数据
│ ├─ 数据量?
│ │ ├─ 小(<几百KB)/中等 → 管道 or 消息队列 or socket
│ │ │ ├─ 父子 + 单向流 → pipe
│ │ │ ├─ 任意进程 + 单向流 → FIFO
│ │ │ ├─ 要消息边界/按类型 → 消息队列(SysV msgq)
│ │ │ ├─ 双向/本机高效 → Unix socket(下篇)
│ │ │ └─ 跨主机 → INET socket(14 网络篇)
│ │ └─ 大(几MB+/持续共享)→ 共享内存
│ │ ├─ 简单 → mmap(MAP_SHARED,推荐现代写法)
│ │ └─ 兼容老代码 → SysV shm
│ └─ 需同步(协调访问)→ 信号量 / 文件锁
│ ├─ 跨进程资源计数 → SysV sem / POSIX sem
│ ├─ 同文件多进程 → flock/fcntl
│ └─ 同进程线程 → pthread mutex(下篇,不用 IPC)
│
└─ 内核 ↔ 用户态(驱动/内核事件)→ 下篇(Netlink/ioctl/procfs/sysfs)
现代选型倾向:fd 化 + epoll
老机制(管道/信号/SysV)各有坑:信号不可靠、SysV API 笨重、管道单向。现代 Linux 倾向一切皆 fd ------Unix socket / eventfd / signalfd / timerfd 都是 fd,能统一塞进 epoll 事件循环(下篇详讲)。新代码优先 fd-based IPC;SysV IPC 维护老代码/简单脚本用。
二、管道 pipe 与 FIFO
2.1 匿名管道 pipe
特点 :
① 单向(一端写、一端读);
② 无名字 (靠 fd 继承传递);
③ 只能亲缘进程 (父子,因为只有 fork 才能传 fd);
④ 字节流(无消息边界,write 两次的内容 read 可能一次读出);
⑤ 内核缓冲(默认 64KB,可调)。
c
#include <unistd.h>
int pipe(int fd[2]); // fd[0]=读端, fd[1]=写端
// 典型:父进程 pipe,fork,各关一端,形成单向通道
int fd[2];
pipe(fd); // 父先建管道
if (fork() == 0) { // 子
close(fd[0]); // 子关读端(子只写)
write(fd[1], "hello", 5);
close(fd[1]);
_exit(0);
}
close(fd[1]); // 父关写端(父只读)
char buf[16];
int n = read(fd[0], buf, sizeof(buf)); // n=5, "hello"
close(fd[0]);
关键行为:
| 情况 | 行为 |
|---|---|
| 读端读,所有写端关闭 | read 返回 0(EOF)------这是判断对方写完的标准 |
| 写端写,所有读端关闭 | write 触发 SIGPIPE (默认终止进程);忽略信号后返回 EPIPE |
| 缓冲区满(默认 64KB) | write 阻塞,等读者读走 |
| 缓冲区空 | read 阻塞,等写者写 |
"关掉不用的端"是管道正确性的关键
父子各持两端时,父想"只读"必须
close(fd[1]),否则父自己还持有写端,子退出后 read 永远不返回 0(EOF) ------因为还有一个写端(父自己)开着。管道 EOF = 所有写端关闭,漏关一个就死等。这是管道最经典的坑。
2.2 内核实现:pipe_buffer
管道在内核里是一个 struct pipe_inode_info(挂在 inode 上),含一组 struct pipe_buffer(环形缓冲页):
c
// 简化:内核 struct pipe_inode_info
struct pipe_inode_info {
struct mutex mutex; // 保护管道
unsigned int nrbufs; // 当前缓冲页数
unsigned int buffers; // 最大页数(默认 16 页 = 64KB)
unsigned int curbuf; // 读指针(环形)
struct pipe_buffer *bufs; // 环形缓冲页数组
...
};
// 每个 pipe_buffer 指向一页(page),记录 offset/len
struct pipe_buffer {
struct page *page;
unsigned int offset, len; // 数据在这页的偏移和长度
const struct pipe_buf_operations *ops;
};
- 写者
write:在bufs末尾分配页,copy_from_user写入,len++; - 读者
read:从curbuf读,copy_to_user,读完释放页,curbuf前移; - 环形:16 页轮转;
- 容量 =
buffers × PAGE_SIZE(默认 16×4KB = 64KB),fcntl(F_SETPIPE_SZ)可调,最大/proc/sys/fs/pipe-max-size(默认 1MB); - 零拷贝优化 :
splice()/tee()能在管道和文件/socket 间直接搬pipe_buffer的页指针,不拷数据(对接 【Linux 内核专栏 13】快IO层 / 【Linux内核专栏 14】网络协议栈)。
2.3 命名管道 FIFO
特点:① 有名字(文件系统里一个 inode);② 任意进程可 open(不必亲缘);③ 其余同 pipe(单向、字节流、内核缓冲)。
bash
mkfifo /tmp/myfifo # 创建命名管道(文件类型 p)
# 终端 A 写
echo "hi" > /tmp/myfifo # 阻塞,直到有读者
# 终端 B 读
cat < /tmp/myfifo # 读到 "hi"
c
// C 代码
mkfifo("/tmp/myfifo", 0666);
int fd = open("/tmp/myfifo", O_RDWR); // 读写都开(避免阻塞)
write(fd, "hi", 2);
char buf[16]; read(fd, buf, 2);
open默认阻塞 :只读 open 会等到有写者;只写 open 等到有读者;O_NONBLOCK不等;- FIFO 在 VFS 里就是个特殊文件(12 VFS 篇的 inode),
i_op指向 pipe 的实现; - shell 的
|就是匿名 pipe;mkfifo是命名版本(可跨任意进程)。
嵌入式视角:管道是"流式日志/命令管道"的命脉
写守护进程,子进程拉起工具:
popen("lsof", "r")读其输出 = 管道;syslog管道重定向;/dev/xconsole是 FIFO。凡"一条命令的输出喂给另一条",底层都是 pipe。
三、信号 signal
3.1 信号是什么
信号 = 内核投递给进程的"软件中断" 。内核在进程从内核态返回用户态前 ,检查是否有待处理信号,有则跳到进程注册的信号处理函数执行,完事再回原流程。这是"事件通知"型 IPC------只传一个信号号(1~31 标准 + 34~64 实时),不传数据(实时信号可带少量 siginfo)。
标准信号一览(常用):
| 信号 | 号 | 默认动作 | 触发场景 |
|---|---|---|---|
| SIGHUP | 1 | 终止 | 终端挂起/守护进程重载配置 |
| SIGINT | 2 | 终止 | Ctrl+C |
| SIGQUIT | 3 | 终止+core | Ctrl+\ |
| SIGKILL | 9 | 终止(不可捕/不可忽略) | kill -9 强杀 |
| SIGSEGV | 11 | 终止+core | 访问非法地址(你 06b MMU 缺页异常) |
| SIGPIPE | 13 | 终止 | 写已关闭的管道/socket |
| SIGALRM | 14 | 终止 | alarm() 超时 |
| SIGTERM | 15 | 终止 | kill 默认(优雅退出) |
| SIGCHLD | 17 | 忽略 | 子进程退出/暂停(父 wait 用) |
| SIGSTOP | 19 | 暂停(不可捕) | Ctrl+Z |
| SIGCONT | 18 | 继续 | 恢复暂停进程 |
3.2 产生信号
c
#include <signal.h>
kill(pid, SIGTERM); // 给指定进程发
raise(SIGTERM); // 给自己发
killpg(pgid, SIGTERM); // 给进程组发
alarm(5); // 5 秒后给自己发 SIGALRM(一次)
// sigqueue(pid, sig, siginfo) // 实时信号,可带数据
内核侧:kill(pid, sig) → 找到 task_struct → 在其 pending 队列挂一个 sigqueue → 设置 TIF_SIGPENDING → 进程从内核返回用户态时 do_signal() 处理。
3.3 捕获与处理:sigaction
c
#include <signal.h>
void handler(int sig) {
// 信号处理函数(异步上下文,只能调异步信号安全函数!)
write(1, "got SIGINT\n", 11); // write 是信号安全的
}
int main(void) {
struct sigaction sa;
sa.sa_handler = handler;
sigemptyset(&sa.sa_mask); // 处理时不额外屏蔽别的信号
sa.sa_flags = SA_RESTART; // 被中断的 syscall 自动重启(见 3.5)
sigaction(SIGINT, &sa, NULL); // 捕获 SIGINT
// 忽略:sa.sa_handler = SIG_IGN
// 默认:sa.sa_handler = SIG_DFL
for (;;) pause(); // 等信号
}
- SIGKILL(9) / SIGSTOP(19) 不可捕获、不可忽略------内核直接处理,这是"最后的杀手锏",保证总能杀掉失控进程;
- 处理函数执行期间,该信号被自动屏蔽(防递归),处理完再解除;
sa_mask额外屏蔽的信号,处理期间也屏蔽。
3.4 信号掩码(阻塞)
c
sigset_t set;
sigemptyset(&set);
sigaddset(&set, SIGINT);
sigprocmask(SIG_BLOCK, &set, NULL); // 屏蔽 SIGINT(不投递,但 pending)
// ... 临界区 ...
sigprocmask(SIG_UNBLOCK, &set, NULL); // 解除,期间若来了 SIGINT 现在投递
- "屏蔽(pending)"≠"忽略(丢弃)":屏蔽期间信号挂起,解除后投递;
- 标准信号(1~31)多次合并 :同一信号屏蔽期间来 5 次,解除后只投递 1 次(标准信号不排队);
- 实时信号(34~64,
SIGRTMIN~SIGRTMAX)不合并 :排队投递 ,可带siginfo(用sigqueue发)。
3.5 信号中断系统调用:EINTR
慢系统调用(read/write/wait/recv 等可能阻塞的)被信号打断时,返回 -1,errno=EINTR:
c
// 不加 SA_RESTART:
int n = read(fd, buf, 100);
if (n < 0 && errno == EINTR) {
// 被信号打断,要手动重试
n = read(fd, buf, 100);
}
// 加 SA_RESTART:内核自动重启,不返回 EINTR(部分 syscall 重启,如 read;有些不重启,如 select)
信号处理函数里只能调"异步信号安全"函数
信号是异步的------可能打断 malloc 持锁的中间状态。在 handler 里调
printf/malloc/fopen等非信号安全函数 = 死锁/数据破坏。只能调write/_exit/read/waitpid/信号安全函数 。要打印就用write(2, ...)。需要复杂处理:handler 只置标志位volatile sig_atomic_t flag=1,主循环检查 flag 再处理(自陷模式)。
3.6 内核信号结构(对接 【linux内核专栏 03】进程管理与调度 task_struct)
c
struct task_struct {
...
struct signal_struct *signal; // 进程级(共享):共享 pending、信号处理表
struct sighand_struct *sighand; // 信号处理表(k_sigaction 数组)
sigset_t blocked; // 本线程屏蔽
struct sigpending pending; // 本线程私有 pending 队列
struct sigpending *signal_pending; // 进程共享 pending
...
};
- 信号可进程级 也可线程级 :定向某线程用
tgkill/pthread_kill; - 内核投递点:每次从内核态返回用户态 (中断/异常/syscall 返回)检查
TIF_SIGPENDING; - 投递 = 跳到用户态 handler(设栈帧
rt_sigframe),handler 末尾sigreturnsyscall 回内核,内核恢复原上下文。
对照 FreeRTOS:信号 vs 任务通知
FreeRTOS 任务通知 (xTaskNotifyGive/ulTaskNotifyTake)是"轻量事件通知",类似信号------但 FreeRTOS 通知可带 32 位值、可计数、直接唤醒任务,比信号干净。Linux 信号古老且坑多(异步不安全、合并、EINTR),现代代码倾向 eventfd/signalfd 把信号 fd 化(下篇)。守护进程的 SIGHUP 重载配置、SIGCHLD 收尸子进程------这两类信号仍主流。
四、System V IPC 三件套
System V IPC 是 Unix SysV 加的一套 IPC:消息队列、共享内存、信号量。
三者共性 :
① 用 key_t(通常 ftok() 生成)标识;
② ipcs 查看、ipcrm 删除;
③ 内核持久(进程退出后对象还在,除非显式删);
④ API 笨重(老接口)。
4.1 共性:key / 对象 / ipcs
c
key_t key = ftok("/etc/profile", 'A'); // 路径+项目号生成 key(同机约定)
int id = msgget(key, IPC_CREAT | 0666); // 创建/打开对象,返回 id
// ... 用 id 操作 ...
// ipcs -q 查消息队列; ipcs -m 查共享内存; ipcs -s 查信号量
// ipcrm -q <id> 删
SysV IPC 的坑:不随进程退出自动删
SysV IPC 对象内核持久 ------进程 crash 也不删,泄漏会一直占内存/ID。必须显式
IPC_RMID或ipcrm。生产环境用要封装好生命周期管理。新代码优先 POSIX IPC 或 mmap。
4.2 消息队列 msgq
特点 :
① 消息带边界 (每条消息独立,不像管道字节流);
② 消息带类型 (long type),读者可按类型取;
③ 内核缓冲,有上限。
c
struct msgbuf {
long mtype; // 类型(>0,用户定)
char mtext[100]; // 数据
};
int qid = msgget(key, IPC_CREAT | 0666);
// 写(发送)
struct msgbuf msg = {.mtype = 1};
strcpy(msg.mtext, "hello");
msgsnd(qid, &msg, strlen(msg.mtext), 0); // 阻塞;IPC_NOWAIT 不阻塞
// 读(接收)
struct msgbuf rcv;
msgrcv(qid, &rcv, 100, 1, 0); // 收类型=1 的;0=任意类型;负值=类型<=|n| 最小
mtype让你"按类型分发":不同类型给不同消费者,一个队列多路复用;- 上限:
/proc/sys/kernel/msgmni(队列数)/msgmax(单条最大)/msgmnb(队列总字节),默认队列总 16KB; - 内核对象
struct msg_queue,挂msg_msg链表。
4.3 共享内存 shm(最快 IPC)
特点 :
① 多个进程映射同一物理页 到各自虚拟地址;
② 无内核中转拷贝 (读写直达),最快;
③ 但不带同步(共享了你要自己加信号量/mutex 保护)。
c
int shmid = shmget(key, 4096, IPC_CREAT | 0666); // 创建 4KB 共享段
char *addr = shmat(shmid, NULL, 0); // 映射到本进程地址,返回虚拟地址
// 现在 addr 指向共享内存,读写直达物理页
addr[0] = 'X'; // 另一进程 shmat 后能看到
shmdt(addr); // 解除映射
shmctl(shmid, IPC_RMID, NULL); // 删除(标记,所有人 dt 后才释放)
shmat返回的虚拟地址每进程不同(各自 VMA 分配),但物理页同一;- 写入不立即同步 到对方------同主机、同架构、cache 一致性硬件保证可见性,但仍可能需内存屏障 (【linux内核专栏 07】同步与并发 smp_mb)保证顺序;
- 同步必须自己做:配 SysV 信号量(4.4)或 pthread 进程间 mutex(下篇);
- 内核对象
struct shmid_kernel,实际映射靠shmem_file_setup(基于 tmpfs,对接 【linux内核专栏 04】内存管理 mmap)。
共享内存 = 最快 + 最危险
最快(零拷贝),但没有同步 ------两个进程同时写同一地址 = 数据破坏,且没有原子保证(你得自己加锁或用原子操作)。共享内存配同步原语是标配:shm(数据)+ sem(同步)。这也是"为什么 SysV 把 shm 和 sem 配成一对"。
4.4 信号量 sem(SysV,跨进程同步)
注意 :这是 SysV IPC 信号量 ,和 【linux内核专栏 07】同步与并发 篇的内核态 struct semaphore 不是一回事------07 那个是内核内同步,这个是用户态跨进程同步(经 syscall 进内核操作)。
特点 :
① 不是"传数据",是"同步/资源计数";
② 一个 sem array 可含多个信号量 (集合);
③ 操作是 semop 数组(可一次原子操作多个);
④ 永远是 IPC_NOWAIT / 阻塞 / SEM_UNDO(进程退出自动回滚)。
c
// 创建含 1 个信号量的集合,初值 1(二值互斥)
int semid = semget(key, 1, IPC_CREAT | 0666);
union semun { int val; struct semid_ds *buf; unsigned short *array; } arg;
arg.val = 1;
semctl(semid, 0, SETVAL, arg); // 0 号信号量设为 1
// P 操作(等/减)
struct sembuf p = {.sem_num = 0, .sem_op = -1, .sem_flg = 0};
semop(semid, &p, 1);
// 临界区(跨进程共享)
// V 操作(加/释放)
struct sembuf v = {.sem_num = 0, .sem_op = +1, .sem_flg = 0};
semop(semid, &v, 1);
sem_op负=P(减,不够则阻塞),正=V(加,唤醒等待者),0=等信号量变 0;SEM_UNDO:进程 crash 自动回滚其操作,防"持锁进程死了导致死锁";- 一次
semop操作多个信号量原子------要么全成功要么全不变,这是 SysV sem 的核心能力(可用来实现复杂的资源分配协议)。
4.5 System V IPC 内核实现共性
三类 SysV IPC 在内核里都是 ipc_ids 管理,对象类型:
| 机制 | 内核结构 | 关键操作 |
|---|---|---|
| 消息队列 | struct msg_queue |
msgsnd/msgrcv(挂 msg_msg 链) |
| 共享内存 | struct shmid_kernel |
shmat/shmdt(建 shmem mapping) |
| 信号量 | struct sem_array |
semop(改 sem 值,挂等待队列) |
- 都有
ipc_perm(权限 key/uid/gid/mode); ipcs命令读/proc/sysvipc/{msg,shm,sem};- 限制在
/proc/sys/kernel/{msg*,shm*,sem*}。
五、POSIX IPC
POSIX IPC 是 SysV IPC 的现代替代(IEEE Std 1003.1b),API 更干净:
① 用名字 而非 key;
② 引用计数随进程(fd 或 mmap);
③ 有超时/定时;
④ 一般更易用。
| SysV | POSIX | 说明 |
|---|---|---|
msgget/msgsnd/msgrcv |
mq_open/mq_send/mq_receive |
消息队列,带优先级 |
shmget/shmat/shmdt |
shm_open/mmap/munmap/shm_unlink |
共享内存(本质是 mmap 共享文件) |
semget/semop/semctl |
sem_open/sem_wait/sem_post/sem_unlink |
命名信号量 |
c
// POSIX 消息队列
#include <mqueue.h>
mqd_t mq = mq_open("/myqueue", O_CREAT | O_RDWR, 0666, NULL);
mq_send(mq, "hi", 2, 0); // 优先级 0
char buf[16]; unsigned prio;
mq_receive(mq, buf, 16, &prio);
// POSIX 共享内存(本质:开一个 tmpfs 文件,mmap)
int fd = shm_open("/myshm", O_CREAT | O_RDWR, 0666);
ftruncate(fd, 4096);
char *p = mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
p[0] = 'X';
munmap(p, 4096); shm_unlink("/myshm");
// POSIX 命名信号量
sem_t *s = sem_open("/mysem", O_CREAT, 0666, 1);
sem_wait(s); // P
// 临界区
sem_post(s); // V
sem_unlink("/mysem");
- POSIX 消息队列支持
mq_notify(信号或新线程通知,异步)、优先级; - POSIX 共享内存就是 mmap 一个
/dev/shm下的 tmpfs 文件 (Linux 实现)------所以"POSIX shm"和"mmap 共享文件"本质同源(见 【linux内核专栏 06】系统调用); - 编译加
-lrt(POSIX 实时扩展库); - 新代码优先 POSIX IPC,API 干净且更易集成(命名、fd 化)。
六、mmap 共享内存
6.1 mmap 做 IPC 的两种方式
【linux内核专栏 04】内存管理 篇讲 mmap 是内存映射(文件/匿名),这里聚焦 IPC 视角 :用 MAP_SHARED 让多进程映射同一物理页。
c
void *mmap(void *addr, size_t len, int prot, int flags, int fd, off_t off);
// flags 关键:MAP_SHARED(共享,写可见)/ MAP_PRIVATE(私写时复制)
// MAP_ANONYMOUS(匿名,无文件)/ 有 fd(文件映射)
方式 A:匿名共享(亲缘进程)
c
char *p = mmap(NULL, 4096, PROT_READ|PROT_WRITE,
MAP_SHARED | MAP_ANONYMOUS, -1, 0);
if (fork() == 0) {
p[0] = 'X'; // 子写
_exit(0);
}
wait(NULL);
printf("%c\n", p[0]); // 父读到 'X'(共享)
munmap(p, 4096);
MAP_SHARED | MAP_ANONYMOUS:内核建一个匿名 shmem 对象,fork 后父子继承同一物理页;- 只能亲缘(父子),因为匿名对象没法给"无关进程"找名字。
方式 B:文件共享(任意进程)
c
// 进程 A
int fd = open("/tmp/share", O_RDWR|O_CREAT, 0666);
ftruncate(fd, 4096);
char *p = mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
p[0] = 'X';
// 进程 B(独立)
int fd = open("/tmp/share", O_RDWR);
char *p = mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
printf("%c\n", p[0]); // 'X'
- 同一文件 +
MAP_SHARED→ 映射同一物理页(经 page cache); - 任意进程(只要能 open 该文件)即可共享;
- 这本质就是 POSIX shm(
/dev/shm下的 tmpfs 文件); - 写入会(按内核策略)回写 page cache → 文件(可见到其他映射者);
msync强制刷盘。
6.2 MAP_SHARED vs MAP_PRIVATE
| flag | 写入效果 | 用途 |
|---|---|---|
MAP_SHARED |
写入写回文件/page cache,其他映射者可见 | IPC 共享(本节)/共享文件 |
MAP_PRIVATE |
写入触发写时复制(COW),只影响本进程 | 私有映射(如加载 ELF .data,对接 02 启动) |
mmap 共享 = 现代 IPC 共享内存首选
mmap 文件共享 比 SysV
shmget更灵活:① 有文件名(可 open,自然命名);
② 配 page cache(对接 【linux内核专栏 04】内存管理);
③ 和 POSIX shm 同源;
④ 可
msync控制刷盘。新代码用 mmap,不用 SysV shm。SysV shm 只在维护老代码时见。
6.3 共享内存的同步(必配)
共享内存本身没同步,必须配:
c
// 进程间互斥:用 pthread 进程间 mutex(下篇详),放在共享内存首部
pthread_mutex_t *mtx = (void *)p; // p 是 mmap 共享内存
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_setpshared(&attr, PTHREAD_PROCESS_SHARED); // 关键:进程间共享
pthread_mutex_init(mtx, &attr);
pthread_mutex_lock(mtx);
// 临界区(操作 p + sizeof(mutex) 之后的数据)
pthread_mutex_unlock(mtx);
PTHREAD_PROCESS_SHARED让 mutex 可跨进程用(mutex 在共享内存里);- 或用 SysV 信号量(4.4)/ POSIX 信号量(5);
- 或用原子操作(atomic_t 的用户态版
__atomic)+ 自定义协议; - 没有同步的共享内存 = 定时炸弹。
七、文件锁 flock 与 fcntl
多进程访问同一文件时的协调,有两套锁:
7.1 flock(建议锁,整文件)
c
#include <sys/file.h>
int fd = open("/tmp/data", O_RDWR);
flock(fd, LOCK_EX); // 排他锁(写)
// 临界区
flock(fd, LOCK_UN); // 解锁
// LOCK_SH 共享锁(读);LOCK_NB 非阻塞
- 整文件级(不分区段);
- 建议锁 (advisory)------只有大家都 flock 才生效,不拦不锁的直接
write; - 只关联 fd (open file description),同进程内 dup 的 fd 共享锁,close 最后一个 fd 释放;
- 内核
struct file_lock,挂 inode 的锁列表。
7.2 fcntl(记录锁/租约锁,按字节区段)
c
struct flock fl = {
.l_type = F_WRLCK, // F_RDLCK/F_WRLCK/F_UNLCK
.l_whence = SEEK_SET,
.l_start = 0, // 起始偏移
.l_len = 100, // 锁 0~99 字节(区段)
};
fcntl(fd, F_SETLK, &fl); // 设锁(非阻塞);F_SETLKW 阻塞
// F_GETLK 查询是否被锁
- 按区段(字节范围),可锁文件的一部分;
- 关联 进程(PID),同进程重复加锁不冲突;
- Linux 上也是建议锁(强制锁需挂载
mand,基本废弃); - 租约锁(F_SETLEASE):给文件加租约,别人要打开/写时内核先发信号给你(F_LEASE),用于 Samba/NFS 之类。
| flock | fcntl | |
|---|---|---|
| 粒度 | 整文件 | 字节区段 |
| 关联 | open file(fd) | 进程(PID) |
| 类型 | 建议锁 | 建议(强制基本废弃) |
| 典型 | shell 脚本互斥 | 数据库按行锁 |
嵌入式视角:文件锁用得少,但知道有
嵌入式场景多进程写同一配置/日志文件时用 flock 做互斥(如多个采集进程写同一日志)。BSP 写驱动基本不碰文件锁,但用户态守护进程会。
八、对照 FreeRTOS / RISC-V
8.1 IPC 机制对照
| 通信需求 | FreeRTOS | Linux 用户态 IPC |
|---|---|---|
| 任务间传数据 | 队列(xQueueSend/Receive) | 管道/消息队列/socket |
| 轻量事件通知 | 任务通知(xTaskNotifyGive) | 信号/eventfd(下篇) |
| 共享大数据 | 共享全局数组(同地址空间) | mmap/shm(共享内存) |
| 跨任务同步 | 二值/计数信号量、mutex | SysV/POSIX 信号量、pthread mutex(下篇) |
| 多任务同步点 | 事件组(xEventGroup) | eventfd/futex(下篇) |
| 中断→任务通知 | xxxFromISR + 任务通知 | 内核↔用户:Netlink/kill(下篇) |
8.2 本质差异
| FreeRTOS | Linux | |
|---|---|---|
| 地址空间 | 所有任务共享一个 | 每进程独立(需 IPC 桥) |
| 默认通信 | 全局变量 + 队列 | 管道/IPC 机制(经内核) |
| 同步 | 信号量/mutex(单核,关中断) | 信号量/mutex(多核,futex/自旋) |
| 事件 | 任务通知/事件组 | 信号/eventfd |
| 零拷贝 | 自然(共享地址) | 共享内存(mmap/shm) |
8.3 RISC-V 视角
- 信号投递:RISC-V 上信号处理函数的栈帧
rt_sigframe、sigreturn由内核 setup,经ecall(06 syscall)进入; - 共享内存可见性:RISC-V RVWMO 弱内存模型(06b),多核共享内存可能需
fence rw,rw(对应smp_mb)保证顺序; - mmap:RISC-V Sv39/Sv48 页表(04/06b),
MAP_SHARED映射靠页表项共享同一物理页; - futex(下篇):RISC-V 上 futex 等待用内核
futex系统调用,唤醒靠futex_wake,底层是内核等待队列(非原子指令忙等)。
九、AEGIX BSP 实战
9.1 守护进程信号处理(SIGHUP 重载 / SIGCHLD 收尸)
AEGIX 守护进程标准信号用法:
c
#include <signal.h>
#include <sys/wait.h>
static volatile sig_atomic_t reload_flag = 0;
static volatile sig_atomic_t child_died = 0;
void sighup_handler(int sig) { reload_flag = 1; } // 只置标志
void sigchld_handler(int sig) { child_died = 1; }
int main(void) {
struct sigaction sa = {0};
sa.sa_handler = sighup_handler; sa.sa_flags = SA_RESTART;
sigaction(SIGHUP, &sa, NULL);
sa.sa_handler = sigchld_handler;
sigaction(SIGCHLD, &sa, NULL);
signal(SIGPIPE, SIG_IGN); // 写断管道不崩(返回 EPIPE)
for (;;) {
if (reload_flag) { reload_flag = 0; reload_config(); }
if (child_died) { child_died = 0;
while (waitpid(-1, NULL, WNOHANG) > 0) ; // 收尸所有子进程
}
// ... 主循环 ...
pause();
}
}
- SIGHUP:守护进程约定"重载配置"(因守护进程无终端,HUP 复用为重载);
- SIGCHLD :子进程退出时内核发,父
waitpid(WNOHANG)循环收尸,防僵尸; - SIGPIPE 忽略 :写断开的管道/socket 不致崩,改检查
EPIPE; - handler 只置
sig_atomic_t标志,主循环处理(自陷模式,避免信号不安全)。
9.2 共享内存 + 信号量:多进程配置共享
多采集进程共享一份传感器配置(避免各读各的):
c
// 用 mmap 文件共享 + pthread 进程间 mutex(放共享内存头)
typedef struct {
pthread_mutex_t lock; // 进程间 mutex,放共享内存首部
int sensor_count;
float values[64];
} shared_cfg_t;
shared_cfg_t *init_shared(const char *path) {
int fd = open(path, O_RDWR|O_CREAT, 0666);
ftruncate(fd, sizeof(shared_cfg_t));
shared_cfg_t *s = mmap(NULL, sizeof(shared_cfg_t),
PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
static int inited = 0;
if (!inited) { // 第一个映射者初始化 mutex
pthread_mutexattr_t a;
pthread_mutexattr_init(&a);
pthread_mutexattr_setpshared(&a, PTHREAD_PROCESS_SHARED);
pthread_mutex_init(&s->lock, &a);
inited = 1;
}
return s;
}
// 任意进程
shared_cfg_t *cfg = init_shared("/dev/shm/aegix_cfg");
pthread_mutex_lock(&cfg->lock);
cfg->values[0] = 23.5; // 写共享
float v = cfg->values[1]; // 读其他进程写的
pthread_mutex_unlock(&cfg->lock);
- mmap
/dev/shm(tmpfs,纯内存,不落盘)+PTHREAD_PROCESS_SHAREDmutex; - 比 SysV
shmget+semop更现代,API 更干净; - 注意 mutex 初始化只做一次(用文件大小判断是否首个)。
9.3 管道拉起子进程读输出
守护进程拉起 lsof 看打开文件:
c
FILE *fp = popen("lsof -p $(pidof aegix_daemon)", "r"); // popen 内部 pipe+fork+exec
char line[256];
while (fgets(line, sizeof(line), fp)) {
syslog(LOG_INFO, "%s", line); // 记到日志
}
pclose(fp);
popen= pipe + fork + exec + fdopen,封装好;- 等价手写:pipe → fork → 子 dup2(fd1,1) → exec → 父 read fd0。
9.4 选择实战(本篇机制)
| AEGIX 场景 | 选什么 | 理由 |
|---|---|---|
| 守护进程重载配置 | SIGHUP | 约定,kill -HUP 触发 |
| 收子进程尸体 | SIGCHLD + waitpid | 防僵尸 |
| 多进程共享传感器配置 | mmap /dev/shm + pthread mutex | 大数据 + 零拷贝 + 现代同步 |
| 拉起工具读输出 | popen(pipe) | 流式,父子单向 |
| 多进程写同一日志 | flock | 整文件互斥 |
| 简单消息分发 | SysV 消息队列(老代码)/ Unix socket(新,下篇) | 按类型/双向 |
十、小结速查与决策树
10.1 机制速查
| 机制 | 传什么 | 边界 | 拷贝 | 范围 | 典型 |
|---|---|---|---|---|---|
| pipe | 字节流 | 无 | 内核中转 | 父子 | shell ` |
| FIFO | 字节流 | 无 | 内核中转 | 任意 | 命名管道 |
| signal | 信号号 | --- | --- | 任意 | kill/Ctrl+C |
| msgq | 消息 | 有 | 内核中转 | 任意 | 按类型分发 |
| shm | 共享页 | --- | 零拷贝 | 任意 | 大数据共享 |
| SysV sem | 同步 | --- | --- | 任意 | 跨进程资源计数 |
| mmap 共享 | 共享页 | --- | 零拷贝 | 亲缘/任意 | 现代共享内存 |
| flock/fcntl | 文件锁 | --- | --- | 同文件 | 文件互斥 |
10.2 选择决策树(精简)
传数据?
├─ 否(只通知)→ signal / eventfd(下篇)
├─ 小/流式 → pipe(父子)/FIFO(任意)/socket(下篇)
├─ 消息带边界/按类型 → 消息队列(SysV)/socket 数据报(下篇)
├─ 大数据共享 → mmap(MAP_SHARED,首选)/SysV shm(老)
└─ 同步 → 信号量(SysV/POSIX)/文件锁(flock)/pthread mutex(下篇,进程间)
10.3 陷阱速查
| 陷阱 | 说明 |
|---|---|
| 管道漏关写端 | read 永不 EOF |
| 写断管道 | SIGPIPE 默认终止 → 先 signal(SIGPIPE, SIG_IGN) |
| 信号 handler 调不安全函数 | printf/malloc 死锁 → 只调信号安全函数 + 置标志 |
| 标准信号合并 | 同信号多次只投 1 次 → 用实时信号(SIGRTMIN+) |
| 共享内存无同步 | 数据破坏 → 必配 mutex/信号量 |
| SysV IPC 不自动删 | 泄漏 → 显式 IPC_RMID / ipcrm |
| 被信号打断 syscall | EINTR → SA_RESTART 或手动重试 |
10.4 内核对象速查
| 机制 | 内核结构 |
|---|---|
| pipe/FIFO | struct pipe_inode_info + pipe_buffer |
| signal | struct signal_struct/sighand_struct/sigpending |
| SysV msgq | struct msg_queue + msg_msg |
| SysV shm | struct shmid_kernel(基于 shmem/tmpfs) |
| SysV sem | struct sem_array + sem |
| mmap 共享 | VMA(vm_area_struct)+ page cache |
十一、相关笔记
- 【Linux 内核专栏 00】总目录 ------ 专题入口
- 【linux内核专栏 03】进程管理与调度 ------ task_struct/进程隔离(本篇前置)
- 【linux内核专栏 04】内存管理 ------ mmap/page cache/VMA(本篇 mmap 衔接)
- 【linux内核专栏 06】系统调用 ------ copy_to_user/信号投递 syscall 路径
- 【linux内核专栏 07】同步与并发 ------ 内核态 semaphore vs SysV IPC sem(易混,本篇 4.4 区分)
- 【Linux内核专栏 12】VFS 与文件系统 ------ pipe/FIFO 的 inode、文件锁
- \[18-进程间通信IPC下-Socket-Netlink-fd-futex-线程同步] ------ 下篇(本篇续)
- \[FreeRTOS_设计思想与功能模块讲解] ------ 队列/任务通知对照
- 【risc-v专栏 06b】内存架构深挖:PMA / ePMP / Cache / CMO / MMU 细节 / RVWMO 形式化 ------ RVWMO/共享内存可见性
- \[00-RISC-V架构专题总目录] ------ Sv39/MMU/ecall 对照
【Linux 内核专栏 00】总目录 · 上一篇:【Linux内核专栏 16】调试与性能 · 下一篇:\[18-进程间通信IPC下-Socket-Netlink-fd-futex-线程同步] · L2 补充篇(IPC 上)
✨ 感谢你的阅读 ✨
技术之路漫漫,分享是为了更好地交流。
如果本文的内容对你有启发,希望能得到你的支持:
👍 点赞 | ⭐ 收藏 | 👀 关注
如果你在调试过程中遇到了其他问题,
欢迎在 评论区 💬 留言,我们一起探讨。
关注我,一起交流底层开发的那些事儿!🚀