Theory · OS · Inter-Process Communication

进程间通信 IPC

隔离的代价与补全:管道 · 信号 · 消息队列 · 共享内存 · socket —— 按拷贝次数和同步方式建立坐标系

为什么需要

地址空间隔离换来了安全,通信就必须经过内核这座"桥"——桥的过路费就是拷贝与切换

坐标系

两个轴定一切:数据要不要过内核(拷贝次数)+ 谁来保证时序(同步模型)——六大方式都能落进这张图

Go 视角

channel 是进程内的"IPC";真正跨进程时 Go 生态的标准答案是 exec + pipe 与 UDS

定位:OS 系列第四篇。叙事主线:"隔离 → 通信要过内核 → 内核里的三种过法(缓冲中转 / 直接映射 / 通知)"。

Why IPC Exists

先看现象:把指针交给另一个进程,它读出来是垃圾

// 进程 A:算好了一块数据,想把"地址"给 B
struct Result *r = compute();
printf("%p\n", r);   // 0x7f8a4c001000
send_to_B((unsigned long)r);

// 进程 B:拿到同一个数字
struct Result *r = (struct Result*)0x7f8a4c001000;
r->count++;          // SIGSEGV,或者读到一堆垃圾
关键观察:同一个数字 0x7f8a4c001000 在进程 A 和进程 B 里指向完全不同的物理内存。这不是 bug,而是操作系统刻意的设计——每个进程都有独立的虚拟地址空间,A 崩溃、A 被杀、A 越界,都伤不到 B。

没有 IPC 会怎样

隔离换来了安全,但也把进程变成了互不相通的孤岛ls | wc -l 这种流水线根本写不出来,Nginx 无法把请求交给 PHP-FPM,docker 命令行也没法指挥守护进程。要协作,必须有一条受控的通道

它能换成什么(粗糙的替代方案及代价)

最朴素的办法是"写文件,另一个进程去读":能通信,但慢(要落盘或至少过文件系统)、没有边界(读到一半怎么办)、没有通知(只能轮询)。IPC 换掉的就是这三点——内核提供有边界、有通知、可控权限的通道。

代价:过桥要付过路费

既然通道必须由内核这个可信第三方来做,那每一次传递都可能要付三笔成本:数据拷贝(用户 ↔ 内核)、系统调用上下文切换。本篇所有机制的差异,本质就是"这笔过路费怎么付、付几次"。

本 deck 的路线

先给六种方式定坐标(两张地图)→ 再逐个拆机制(管道 / 信号 / 消息队列与共享内存 / socket)→ 补上同步配套 → 最后给选型总表与 Go 视角。读完你应该能回答"这个场景该选谁、为什么"。

动机页:用"同一地址在两个进程里指向不同物理内存"这个能立刻在脑子里跑起来的现象,讲清隔离带来的通信难题,再引出"过桥要付过路费"这条贯穿全篇的成本主线。

Prerequisites & Glossary

先把词认全:下面每一页都会用到它们

术语一句话理解(先记住这个,细节后面展开)
进程隔离每个进程有自己独立的虚拟地址空间,看不到也碰不到别的进程的内存
IPC 进程间通信进程之间交换数据或事件的所有机制的总称,本篇的主角
文件描述符 fd内核给"一个打开的对象"编的整数号,进程用它指代这个对象
管道 pipe / FIFO内核里的一段环形缓冲区:一头写、一头读,字节流单向流动
信号 signal内核发给进程的异步事件通知:只有编号,不携带数据
消息队列内核维护的消息链表:消息有边界、带类型,生命周期独立于进程
共享内存同一块物理内存映射进多个进程的地址空间,读写就是普通访存
信号量 semaphore内核维护的原子计数器:P(取许可)/ V(还许可),只传许可不传数据
socket / UDS一套统一的读写接口,既能本机(UDS)也能跨主机(TCP)
同步约定"什么时候能动":谁先谁后、能不能同时改,与"数据怎么到"是两回事

如果你还不确定下面两件事

先读这两篇再回来,本 deck 默认你已经知道它们:
进程 · 线程 · 协程 → 进程是什么、fork 怎么复制
虚拟内存 → "地址空间"到底是怎么隔离出来的

最小心智模型(后面所有变体都是它的展开)

通信 = 数据怎么到;同步 = 什么时候能动。每一种 IPC 都是这两个问题的一种答案组合:管道把同步内置在收发里(读空就睡、写满就睡),共享内存只管""不管"",信号量干脆只回答""。

一个提前建立的直觉

给六种方式定坐标只需要两个轴① 数据要过几次内核(拷贝次数)② 同步由谁负责(内核还是你自己)。凡是"最快的方案",一定是在第一个轴上省了钱、在第二个轴上欠了债——没有免费午餐。

阅读提示:术语不用背,忘了回来查这一页;真正要背的是"通信 vs 同步"这个二分,以及最后那张速查页。
前置页:十个术语先定义再使用。最小心智模型"通信 vs 同步"把六种方式统一成一张两轴坐标图;提前点出"最快的方案一定欠了同步的债"。

Why IPC · The Map

六大方式总览:三种过内核的姿势

开场那个"指针传过去就失效"的问题,内核给的答案是:既然你们无法共享地址,那就由我当中间人。中间人只有三种做法。

进程间通信方式全景图:三种过内核的姿势 全景图分三列:缓冲中转类包括匿名管道、命名管道、消息队列,数据从用户空间拷贝进内核缓冲再拷贝出去;直接映射类是共享内存,两个进程映射同一块物理内存,不经过内核拷贝但需要信号量同步;通知与套接字类包括信号(只传事件不传数据)和 socket(通用,跨主机也能用)。 ① 缓冲中转 · 内核拷贝 ×2 管道 pipe / FIFO 字节流 · 亲缘(匿名)/ 路径(命名) 消息队列 有边界消息 · 可按类型读取 · 独立生命周期 信号量( semaphore 组) 内核计数器 · 传"许可"不传数据 ② 直接映射 · 零拷贝 共享内存 shmget/shmat 或 mmap 同一块物理页 读写就是普通内存访问 最快,但同步自己负责 共享内存 + 信号量 = System V 时代的"快速通道组合拳" 内存映射文件 mmap 特殊形态:以文件为后备的共享区 ③ 通知与套接字 信号 signal 异步通知 · 只传事件号不传数据 socket 本机 UDS / 跨主机 TCP · 唯一通用解 文件锁 flock / fcntl 以文件为媒介的互斥协调 拷贝 2 次 拷贝 0 次(但要自己同步) 通用性最强 / 开销居中
地图页:三大类各管一列。信号量严格说是同步原语不是数据通道,放在"传许可"的位置最准确。这页的坐标系(拷贝次数 × 同步责任)贯穿后面每一页。

pipe / FIFO

管道:内核里的一段环形缓冲

本质

管道是内核内存里的一段环形缓冲区(Linux 默认 64KB = 16 页,可用 F_SETPIPE_SZ 调整):写端写入、读端读出,半双工(要双向就开两根),数据一旦读走就没了——不是文件。

匿名管道 pipe()

pipe(fd[2]) 返回读/写两个 fd。没有名字、只能靠 fork 继承 fd 传给亲缘进程——这就是"匿名管道只能用于有血缘关系的进程"的原理。shell 的 cmd1 | cmd2 就是 pipe + dup2 把子进程的 stdout/stderr 接到管道上。

命名管道 FIFO

mkfifo /tmp/x 在文件系统里挂一个名字(inode 类型为 FIFO),无亲缘关系的进程按路径打开同一管道。注意:名字只是"接头暗号",数据仍在内核缓冲,不落盘。

阻塞语义

缓冲满 → write 阻塞;缓冲空 → read 阻塞;写端全关 → read 返回 0(EOF,shell 管道据此收尾);读端全关 → write 触发 SIGPIPE(Go 里体现为 broken pipe)。

// shell 里 ls | wc -l 的等效 C 代码骨架
int fd[2];
pipe(fd);                       // fd[0] 读, fd[1] 写
if (fork() == 0) {              // 子进程 1: ls
    dup2(fd[1], STDOUT_FILENO); // stdout 接到写端
    close(fd[0]); close(fd[1]);
    execvp("ls", ...);
}
if (fork() == 0) {              // 子进程 2: wc
    dup2(fd[0], STDIN_FILENO);  // stdin 接到读端
    close(fd[0]); close(fd[1]);
    execvp("wc", ...);
}
close(fd[0]); close(fd[1]);     // 父进程关两端!
常见坑:父进程忘记关自己的管道两端 → 读端永远等不到 EOF,下游进程挂住。Go 的 cmd.StdoutPipe() 帮你管理了这套 fd 生命周期。
面试金句:"管道的'单向字节流'不是限制而是定位——UNIX 哲学的组合器:每个进程只做好一件事,管道负责把它们串成流水线。"
四个必背点:内核环形缓冲 64KB、半双工、匿名靠 fork 继承 fd、EOF/SIGPIPE 语义。右侧代码是 shell 管道的展开,父进程关两端是最常见 bug。

Signals

信号:传"事件"不传数据的异步通知

定位

信号是内核发给进程(或进程发给进程)的异步事件通知:只有编号没有载荷(SIGCHLD 可附带一点信息)。本质是"软件层面的中断"——响应点在指令边界,由内核在返回用户态前检查 pending 位图。

进程的三种处置

默认(SIG_DFL):终止 / 终止+core / 忽略 / 停止;② 捕获(注册 handler):注册处理函数,收到时跳去执行;③ 忽略(SIG_IGN)。例外:SIGKILL(9) 和 SIGSTOP 永远不可捕获、不可忽略——这是内核保留的"最后处分权"。

高频信号表

SIGHUP 挂断(常被 daemon 当重载)· SIGINT Ctrl-C · SIGTERM 优雅退出请求 · SIGKILL 强杀 · SIGSEGV 非法内存 · SIGPIPE 写无读端管道 · SIGCHLD 子进程退出 · SIGURG(Go runtime 拿它做异步抢占)。

// Go 的优雅退出(几乎每个服务都有)
func main() {
    srv := &http.Server{Addr: ":8080"}
    go srv.ListenAndServe()

    quit := make(chan os.Signal, 1)
    // KILL 是收不到的——没有机会
    signal.Notify(quit,
        syscall.SIGTERM, syscall.SIGINT)
    <-quit

    ctx, cancel := context.WithTimeout(
        context.Background(), 10*time.Second)
    defer cancel()
    srv.Shutdown(ctx) // 排空存量请求
}
SIGTERM vs SIGKILL(必考):SIGTERM 可捕获 → 进程有机会排空请求、刷盘、释放锁;SIGKILL 直接由内核终止,进程没有任何执行机会。所以 K8s 先 SIGTERM、宽限期后才 SIGKILL;应用必须把"优雅退出"挂在 SIGTERM 上。
不可靠与可靠:1–31 号传统信号不支持排队(连发多次可能只收一次),实时信号 SIGRTMIN 起(34+)排队可靠——低频知识,答出"信号不排队"即加分。
信号页三件事:只有事件号没有数据、三种处置方式、SIGKILL/SIGSTOP 不可捕获。Go 代码是"SIGTERM 优雅退出"的标准实现;SIGURG 抢占是 Go 岗彩蛋(1.14+ 异步抢占)。

Message Queue · Shared Memory

消息队列 vs 共享内存:两条过内核的路

消息队列(System V / POSIX)

内核维护的消息链表msgget 创建、msgsnd/msgrcv 收发。三个特点:① 消息有类型标记,可按类型随机读取(不像管道必须顺序消费);② 生命周期独立于进程(显式删除才消失);③ 内核保证消息边界与原子性。

消息队列的代价

每次收发都是用户 ↔ 内核两次拷贝;有单消息/队列容量上限(msgmnb 默认 16KB/队列)。适合"小消息、需要按类型分发、跨无亲缘进程"的场景;大吞吐场景让位给共享内存。

共享内存(最快的 IPC)

shmget/shmatshm_open + mmap:把同一块物理内存映射进两个进程的地址空间。读写就是普通访存——零拷贝、零系统调用(数据面)。快不是魔法,只是把"过内核"省掉了。

代价:同步完全裸奔

内核不管时序:读写竞争、可见性全靠用户态自己解决——信号量、互斥锁(PTHREAD_PROCESS_SHARED)、或自己设计协议。共享内存 + 信号量是组合出现的经典搭配。

维度消息队列共享内存
数据拷贝2 次(进内核 + 出内核)0 次
消息边界有(格式化消息)无(自己定协议)
同步内核收发天然同步用户态自行同步
生命周期独立于进程,持久随引用计数 / 显式删除
数据量小消息为主大批量数据首选
现实中的赢家:高性能本机通信(GPU 驱动、数据库 WAL 共享、音视频帧传递)都用共享内存;但工程上更多场景直接用 socket/UDS——省下的拷贝不如省下的心智
面试金句:"共享内存快在数据面零拷贝,贵在控制面自己造——把内核的同步工作搬到用户态,没有免费午餐。"
两栏对照:消息队列"内核管边界管时序但收两次过路费",共享内存"零过路费但裸奔"。工程观点(省心智 > 省拷贝)让答案不落教科书窠臼。

Copy Count Anatomy

一张图看清开销:数据怎么过内核

管道、消息队列与共享内存的数据拷贝路径对比 三行对比图:第一行管道,进程 A 的用户内存拷贝进内核缓冲区,再从内核缓冲区拷贝到进程 B 的用户内存,共两次拷贝;第二行消息队列同样两次拷贝但内核按消息组织;第三行共享内存,两个进程的页表各自映射到同一块物理内存,读写不经过内核拷贝,为零次。 管道 / 消息队列 进程 A 用户内存 write() 系统调用 内核缓冲区 环形页 / 消息链表 进程 B 用户内存 read() 系统调用 拷贝 ① 拷贝 ② 共 2 次拷贝 + 2 次系统调用 + 上下文切换 共享内存 进程 A 地址空间 映射页 va_A 进程 B 地址空间 映射页 va_B 同一块物理内存 物理页 frame 页表映射 页表映射 数据面 0 拷贝 0 系统调用 —— A 写入即 B 可见(同步自理) 本机 socket(TCP loopback)≈ 2 次拷贝 + 完整协议栈处理 —— 所以 UDS 更快、共享内存最快
机制收口页:管道/mq 的 2 次拷贝 vs 共享内存的 0 拷贝。"数据面 vs 控制面"的分工在这里最直观。为第 12 篇零拷贝 deck 埋钩子(那边是把"用户↔内核↔设备"的拷贝省掉)。

Socket · Unix Domain Socket

socket:唯一的"通吃"方案,UDS 是本机加速版

为什么 socket 是通用解

其他 IPC 都绑定"本机 + 特定内核对象",socket 的 API 抽象统一了:同一套 read/write 语义,既能本机(AF_UNIX)也能跨主机(AF_INET)。进程从本机通信迁移到分布式,代码几乎不改——微服务架构的通信底座。

Unix Domain Socket(UDS)

本机进程间走 socket 语义,但数据不经过 TCP/IP 协议栈:直接在内核 socket 缓冲之间搬运,少了三次握手、头封装、校验和与拥塞控制。同一个进程的 API,性能显著高于 TCP loopback。

UDS 的独门能力:fd 传递

通过辅助消息(SCM_RIGHTS)可以把打开的文件描述符发给另一个进程——权限分离的经典手法:特权父进程打开受控资源,把 fd 交给沙箱子进程使用。

在哪见得到

Docker daemon(/var/run/docker.sock)、MySQL 本机连接、Nginx 与 FastCGI/PHP-FPM、容器 sidecar 的 gRPC over UDS、PostgreSQL unix socket——"本机高频 RPC 用 UDS"是通用工程实践。

维度TCP localhostUDS
协议栈完整 TCP/IP(含校验、拥塞控制)内核内直接搬运,无协议栈
连接建立三次握手内核直接配对
寻址IP + 端口(可能被抢)文件系统路径(权限随文件)
延迟 / 吞吐较低延迟、受 loopback 限更高吞吐、更低延迟
fd 传递不支持支持(SCM_RIGHTS)
面试金句:"socket 的价值在语义统一:本机和分布式一套 API。UDS 的价值在只省协议栈不省语义——需要跨主机时零成本迁移。"
Go 实践:net.Listen("unix", "/tmp/app.sock") 与 TCP 用法完全一致(net.Conn 抽象),gRPC 直接 grpc.NewServer(grpc.Creds(...)) 挂 UDS——netpoller 对 UDS 同样生效。
UDS 快的原因要答准:"省协议栈"而不是"省拷贝"(数据面仍要过内核缓冲,拷贝次数与 TCP loopback 同级,省的是封包、校验、握手、拥塞控制)。fd 传递是差异化细节。

Sync Primitives for IPC

通信之外还要同步:信号量、事件与文件锁

信号量 semaphore(IPC 版)

内核维护的原子计数器:P(减 1,为 0 则睡)与 V(加 1,唤醒等待者)。它不传数据、只传"许可"——把"资源有几个"与"谁在等"都交给内核仲裁。二元信号量(初值 1)即进程间互斥锁。

典型组合:共享内存 + 信号量

数据走共享内存(快),时序走信号量(稳):写者 V 通知、读者 P 等待——生产者消费者模型的进程间版本。POSIX 匿名信号量可放进共享内存(sem_init 带 pshared),比 System V 信号量集更轻。

文件锁 flock / fcntl

以文件为仲裁媒介:flock(fd, LOCK_EX) 独占锁、LOCK_SH 共享锁。常用于"防多实例"(daemon 单例)、日志轮转协调。fcntl 的 POSIX 记录锁还能锁字节区间——数据库存储引擎的锁边界就借鉴这套语义。

原语作用域典型用法
System V 信号量集内核对象,key 标识多进程生产者消费者
POSIX 信号量(pshared)可驻共享内存shm 内的互斥/计数
进程共享互斥锁PTHREAD_PROCESS_SHARED + 共享内存shm 临界区保护
flock打开的文件单实例守护、crontab 互斥
fcntl 记录锁文件的字节区间细粒度资源仲裁
信号量 vs 互斥锁(高频辨析):互斥锁语义是"谁拿谁放"(所有权),信号量是"计数许可"(无所有权,A 的 V 可以唤醒 B 的 P)。信号量能表达"资源池",互斥锁只能表达"临界区"。
面试金句:"通信解决'数据怎么到',同步解决'什么时候能动'——管道和消息队列的同步内置在收发里,共享内存的同步是另一道题。"
把"传数据"与"传许可"分开是这页的心智模型。信号量 vs 互斥锁的所有权辨析是高频追问;下一册 thread-sync 会把这套对比在用户态重讲一遍。

IPC Cheat Sheet

六大方式对比总表

方式数据形态拷贝次数适用进程典型场景 / 短板
匿名管道无边界字节流2仅亲缘(fork 继承)shell 流水线 / 单向小流量;半双工
命名管道 FIFO无边界字节流2任意(路径寻址)无亲缘的本机流;仍半双工
消息队列有边界、带类型2任意按类型分发、小消息;容量受限
共享内存裸内存(自定义)0任意大数据量最高吞吐;同步自理
信号仅事件编号任意(有权限)通知/控制流;不能承载数据
socket / UDS字节流或报文2任意 + 跨主机通用性天花板;本机用 UDS 提速
选型口诀:本机流水线 → 管道;本机大流量 → 共享内存(配同步);本机 RPC/控制面 → UDS;跨主机 → TCP socket;事件通知 → 信号;"按类型取消息"的遗留系统 → 消息队列。
答法框架:先分类(缓冲中转 / 直接映射 / 通知与套接字),再对单方式讲机制,最后给场景选型——比背 isolated 定义高一档。
汇总页六行三列。讲表时横着念"数据形态→拷贝→适用",竖着对比"谁最快谁最通用"。选型口诀直接背。

Go × IPC

Go 与 IPC:channel 是理念,socket 是实践

CSP:把 IPC 的心智搬进进程内

"Don't communicate by sharing memory; share memory by communicating"——channel 本质是进程内自带同步的队列:通信即同步(发送/接收就是会合点)。OS 层的管道哲学(字节流组合)与 CSP 的消息传递哲学在此汇合。

Go 进程间真实用什么

exec + pipe:os/exec 的 StdoutPipe/StdinPipe 包装匿名管道,跑子进程抓输出;② UDS:net.Listen("unix", …),sidecar、docker.sock、本机 gRPC 的标准解;③ TCP:跨主机兜底,同一套 net.Conn;④ 信号:os/signal 做 SIGTERM 优雅退出。

为什么 Go 生态少用 System V IPC

shm/消息队列绕开 runtime 的调度与 GC 视野,跨进程共享内存还与 Go 的内存模型有坑(无 happens-before 保证);生态共识:跨进程直接上 UDS/TCP,把序列化成本当可读性保险。

// 子进程输出走匿名管道
cmd := exec.Command("ls", "-l")
stdout, _ := cmd.StdoutPipe()
cmd.Start()
out, _ := io.ReadAll(stdout)
cmd.Wait()

// 本机 gRPC 挂 UDS
lis, _ := net.Listen("unix",
    "/tmp/app.sock")
grpcSrv.Serve(lis)
-runtime 联动:Go runtime 自己也大量使用"IPC 思维"的系统设施:SIGURG 做异步抢占(信号页)、futex 做锁睡眠(thread-sync 页)、epoll 做 netpoller(io-model 页)——进程内与进程间的边界由 runtime 统一调度。
面试金句:"Go 把'通信即同步'内建成了 channel;跨进程时又诚实地退回内核通道——进程内信 CSP,进程间信 socket。"
CSP 哲学页把 channel 与管道/消息队列串成一条思想线。"进程内信 CSP,进程间信 socket"是本 deck 的 Go 岗落点;System V 少用的理由(内存模型/非惯用)是差异化答案。

Cheat Sheet

一页带走:两轴坐标、选型路径、易混辨析

① 两轴坐标(先分类,再讲机制)

缓冲中转管道 / FIFO / 消息队列 —— 拷贝 2 次,同步内核包办
直接映射共享内存 / mmap —— 拷贝 0 次同步自己负责
通知与套接字信号(只传事件)/ socket(可跨主机)/ 文件锁(只做协调)

② 选型决策路径

· 要跨主机? → 只能 socket(TCP)
· 本机 + 大数据量高频? → 共享内存 + 信号量(先想清楚同步协议)
· 本机 + RPC / 控制面?UDS(语义与 TCP 一致,省协议栈)
· 本机 + 单向流水线? → 管道(shell 组合器)
· 只是通知"出事了"? → 信号
· 要求不高就别上共享内存:省心智常常比省拷贝值钱

③ 拷贝次数速记

管道 / 消息队列2 次拷贝 + 2 次系统调用 + 上下文切换
共享内存数据面 0 次拷贝、0 次系统调用
UDS vs TCP loopback拷贝同级,UDS 省的是协议栈处理

④ 易混辨析(高频踩坑)

匿名管道 vs FIFO匿名靠 fork 继承 fd(必须亲缘);FIFO 有路径,谁都能 open
管道 vs 消息队列管道无边界、必须顺序消费;消息队列有边界、可按类型读、生命周期独立
信号量 vs 互斥锁互斥锁有所有权(谁拿谁放);信号量是计数许可(A 的 V 能唤醒 B 的 P)
SIGTERM vs SIGKILLSIGTERM 可捕获(能优雅退出);SIGKILL 内核直杀,没有执行机会
UDS 快在哪不是零拷贝,是省了握手/封包/校验/拥塞控制

⑤ 排障与命令

· 管道下游不退出 → 查父进程有没有关掉自己那两端(读端等不到 EOF)
· 单实例 daemon → flock(LOCK_EX|LOCK_NB)(进程死了内核自动放锁)
· 本机通信延迟高 → 先确认是不是走了 TCP loopback,换 UDS
· Go 写无读端管道 → SIGPIPE 被 runtime 改写成 EPIPE 错误(不杀进程)
一句话背下来:通信解决"数据怎么到",同步解决"什么时候能动";最快的共享内存把同步的债留给了你
速查页:五块按使用顺序排——两轴坐标、选型决策路径、拷贝次数、易混辨析、排障命令。回看时只看这一页。

Interview QA · Part 1

高频追问:机制辨析

先盖住答案自己答一遍,再展开对照——想不起来比看得顺眼记得牢;答不出的直接翻回速查页。

1 · IPC 方式有哪些?怎么分类记忆?

三类坐标

缓冲中转(管道/命名管道/消息队列)、直接映射(共享内存 + mmap)、通知与套接字(信号/socket/文件锁)。两个轴:数据拷贝几次、同步谁负责。

2 · 管道的原理?为什么匿名管道只能亲缘进程用?

内核环形缓冲

内核里 64KB 环形缓冲 + 一对 fd。匿名管道的 fd 只存在于打开它的进程,唯一传递方式是 fork 继承——所以必须亲缘。FIFO 有文件系统路径,谁都能 open。

3 · 共享内存为什么最快?快了之后少了什么?

0 拷贝同步裸奔

同一物理页映射进双方地址空间,读写就是普通访存:数据面零拷贝、零系统调用。少掉的是内核的同步与边界服务——时序、竞争、可见性全要自己用信号量/互斥锁解决。

4 · SIGTERM 和 SIGKILL 的区别?优雅退出怎么做?

可捕获性

SIGTERM 可捕获可忽略,SIGKILL 由内核直接终止、进程无执行机会。优雅退出:捕获 SIGTERM → 停止接新请求 → 带超时排空存量请求 → 刷盘/释放资源 → 退出。K8s 终止宽限期结束时才 SIGKILL。

5 · 消息队列和管道的区别?

边界 / 类型 / 生命周期

消息队列有消息边界、可带类型按类型读取(不必 FIFO 消费)、生命周期独立于进程;管道是无边界字节流、必须顺序消费、读走即逝。数据面两者都是 2 次拷贝。

6 · UDS 为什么比 TCP localhost 快?

省协议栈

UDS 走 socket API 但不进 TCP/IP 协议栈:无三次握手、无包头封装/校验/拥塞控制,内核缓冲直接对搬;地址是文件路径,还能借文件权限做访问控制、传 fd。数据面拷贝次数与 loopback 同级,省的是协议处理。

六题覆盖机制主线。第 6 题注意别答成"UDS 零拷贝"——它省的是协议栈处理,不是数据拷贝。

Interview QA · Part 2

高频追问:工程与排障

同样建议先自答。这一页的题都需要"结论 + 一句代价/边界"才完整——只答结论会被追问。

7 · Go 的 channel 和 OS 管道像吗?

通信即同步

理念同源(消息传递 + 内建同步),层次不同:channel 在进程内由 runtime 实现(hchan + 锁 + 等待队列),管道在进程间由内核实现(环形缓冲 + 睡醒队列)。channel 有类型有边界,更像"类型化消息队列"。

8 · 管道读端没了还写,会怎样?

SIGPIPE

write 触发 SIGPIPE,默认终止进程。Go 特殊处理:stdin/stdout/stderr 之外的 fd 写出 SIGPIPE 转为 EPIPE 错误返回(不然一个 panic 的下游会杀掉整个 Go 服务)——这是 runtime 对信号的改写,面试答出即亮点。

9 · 大文件要在两个进程间高频共享,怎么设计?

mmap 共享序号协议

共享内存(mmap 同一文件或 shm)+ 自己定协议:定长环形缓冲、写指针/读指针放原子变量、用信号量或 futex 通知。参考性能上限:内存带宽。工程上若吞吐要求不极端,UDS + 应用层协议更可维护。

10 · 怎么保证一个 daemon 只跑一个实例?

flock

启动时对锁文件 flock(LOCK_EX | LOCK_NB),拿不到即退出;进程死锁自动释放(内核随 fd 回收),比"check pid 文件再写"的竞态安全。systemd 内部同样是这套思路。

11 · 信号处理函数里能干什么、不能干什么?

async-signal-safe

处理函数在任意指令边界异步打断主流程:只能调 async-signal-safe 函数(write 等),不能 malloc、不能拿非重入锁——否则可能死锁在"被打断时正持有的锁"上。Go 用 masked 线程收信号再转 channel,绕开了这套雷区。

12 · 为什么 K8s 里 preStop 和宽限期都要配?

SIGTERM 链路

preStop 钩子在 SIGTERM 前执行(摘流),SIGTERM 触发应用优雅退出,terminationGracePeriodSeconds 是总预算,超时 SIGKILL 强杀。三段齐全才能做到"不丢请求的滚动发布"——SIGKILL 一到什么优雅都来不及。

后六题偏工程:channel 对照、SIGPIPE 的 Go 特殊行为(真亮点)、共享内存协议设计、flock 单实例、信号安全、K8s 优雅退出链路。第 8 题的 Go SIGPIPE 改写是差异化知识点。

Related & References

相关知识点与参考

OS 系列(本分类)

同步与互斥 →(信号量/互斥的用户态版深度)
进程线程协程 →(隔离是 IPC 存在的理由)
I/O 模型与 epoll →(socket 就绪事件怎么等)
零拷贝 →(把"过内核"的拷贝继续省下去)

跨领域联动

Channel 底层原理 →(CSP 的 runtime 实现)
TCP 连接管理 →(socket 跨主机侧的成本明细)

参考来源(本 deck 结论可溯源至下列一手材料)

man 7 pipe / fifo / signal / sem_overview / unix管道缓冲 64KB、信号处置语义、UDS 与 SCM_RIGHTS 的一手定义
OSTEP ch.26(Concurrency)+ TLPI ch.43–53IPC 与同步原语的系统化梳理(TLPI = Linux/UNIX 系统编程手册)
CSAPP ch.8(信号)/ ch.12信号语义与进程级并发
Go src/os/exec、src/net、src/runtime/signal_unix.goStdoutPipe、UDS 监听、SIGPIPE 改写与 SIGURG 抢占
xiaolincoding.com《图解系统》process_commu.html六种 IPC 的中文对比叙述
Kubernetes docs: container lifecycleSIGTERM / preStop / 宽限期 / SIGKILL 链路
收尾:TLPI(Linux/UNIX 系统编程手册)是 IPC 语义的最权威出处,man 7 系列可日常自查。总页数 13。下一篇:同步与互斥。