在Linux操作系统中为了保证系统的安全稳定每个进程都有自己独立的虚拟地址空间。你可以把每个进程想象成在一个完全隔音、独立的办公室里工作的员工。他们各自处理自己的文件互不干扰。但这带来了一个问题如果一项复杂的工作需要多个员工协同完成比如员工A负责获取数据员工B负责处理数据他们被锁在各自的办公室里该怎么交流呢这就是进程间通信Inter-Process Communication, IPC存在的意义。它是操作系统为这些隔离的员工提供的沟通渠道主要目的是为了实现数据传输、资源共享、通知事件以及进程控制文章目录一、 匿名管道(Anonymous Pipe)二、 命名管道(Named Pipe)三、 共享内存(Shared Memory) 终篇总结 (Conclusion)我们先从最古老、最经典的通信方式开始管道 (Pipes)。想象一下你在 Linux 终端输入了一行最常见的命令who | wc -l。这里的|其实就是一个管道它把who进程的标准输出像水流一样直接灌进了wc -l进程的标准输入里 。在代码里我们最常用的是匿名管道 (Anonymous Pipe)。一、 匿名管道(Anonymous Pipe) 匿名管道的诞生与共享要建造这样一根水管我们需要用到一个系统调用函数pipe()。intpipe(intfd[2]);调用成功后系统会给你一个包含两个整数的数组它们就像是水管的两头fd[0]读端 (Read end) —— 相当于水管的出水口 。fd[1]写端 (Write end) —— 相当于水管的进水口 。但是如果只有父进程一个人拿着水管的两头自己给自己灌水是没意义的。我们怎么让另一个进程也拿到这根水管呢秘诀就是fork()按照文件里的原理解析管道通信分为巧妙的三步建水管父进程调用pipe()创建管道拿到了fd[0]和fd[1]影分身父进程调用fork()产生子进程。因为子进程会继承父进程的文件描述符所以子进程也拿到了这根水管的fd[0]和fd[1]定方向管道是半双工的数据只能单向流动。为了防止混乱比如假设是父进程写、子进程读那么父进程就必须关闭读端fd[0]子进程必须关闭写端fd[1]这样一条干净的、从父进程流向子进程的单向数据通道就建好了 动动脑筋既然“匿名管道”是依靠fork()的继承机制来让两个进程共享这根水管的那你觉得这种通信方式有什么天生的局限性假设系统里有两个你昨天分别独立启动的程序比如进程 A 和进程 B它们能用这种“匿名管道”来聊天吗匿名管道的致命局限性在于它只能用于具有共同祖先具有亲缘关系的进程之间进行通信。因为匿名管道在系统中没有名字完全依赖fork()时子进程对父进程文件描述符表的拷贝。如果是昨天独立启动的进程 A 和进程 B它们之间没有任何血缘关系自然也就无法共享到这根“水管”的进出口。 深入水管管道的读写四大规则在使用管道水管时Linux 内核帮我们处理了同步与互斥 。你可以把下面四种情况当成日常用水的常识来记忆水管没水了写正常读空如果写端还没写数据读端来读读进程会被阻塞挂起等待直到水管里有水 。水管塞满了读正常写满如果读端不读写端一直写当管道写满时写进程会被阻塞直到有人把水读走腾出空间 。供水站下班了写关闭读正常如果所有写端进水口都关闭了读端把管道里剩下的数据读完后read函数会直接返回 0明确告诉你“数据到此结束” 。没人接水了读关闭写正常极其重要如果所有的读端出水口都关闭了此时写端再去写数据是毫无意义的。操作系统会非常严厉地直接发送SIGPIPE信号杀掉这个写进程 。 实战应用基于管道的“进程池” (Process Pool)每次有任务都去创建一个子进程开销太大。我们可以提前雇佣一批“打工人”子进程让它们待命这就是进程池的思想。场景比喻你主进程/包工头提前招了 5 个工人子进程并给每个工人都单独拉了一根单向水管匿名管道。工人每天的工作就是死死盯着水管的出口处阻塞式read。当有新任务比如任务编号1代表处理网页2代表查数据库时你挑一根没那么忙的水管把任务编号扔进去 。对应的工人拿到编号立刻开始干活。干完继续盯着水管。C 核心代码逻辑演示#includeiostream#includevector#includeunistd.h#includesys/wait.h// 描述通信通道structChannel{int_wfd;// 包工头掌握的写端pid_t _worker_id;// 打工人的进程号Channel(intfd,pid_t id):_wfd(fd),_worker_id(id){}};voidCreatePool(intnum,std::vectorChannelchannels){for(inti0;inum;i){intpipefd[2];pipe(pipefd);// 1. 建水管pid_t idfork();// 2. 招工人if(id0){// 子进程打工人close(pipefd[1]);// 关闭写端// ... 循环从 pipefd[0] 读取任务并执行 ...exit(0);}// 父进程包工头close(pipefd[0]);// 关闭读端channels.emplace_back(pipefd[1],id);// 把写端和工人ID记录在名册上}} 核心考点自测❓ 动动脑筋在进程池退出时包工头父进程应该如何优雅地让所有打工人子进程下班并回收它们的资源而不至于产生僵尸进程✅ 答案解析根据上面讲的“管道读写四大规则”的第 3 条写关闭读正常。包工头只需要遍历自己的channels记录依次关闭所有管道的写端 (_wfd)。打工人们一直阻塞在读端一旦写端全关它们的read就会返回 0这就相当于收到了“下班指令”。子进程内部判断read 0后直接break退出死循环。随后包工头再调用waitpid()就能顺利回收子进程资源实现安全清理 。我们继续往下走。刚才提到的匿名管道虽然好用但有一个致命弱点必须要有血缘关系。那么如果两个完全不相干的进程比如你昨天写的一个服务端程序和今天刚写的一个客户端程序想要通信该怎么办呢这就轮到我们的第二个沟通渠道出场了二、 命名管道(Named Pipe) “街角的公共邮筒”命名管道 FIFO场景比喻为了让两个互不认识的员工也能交换文件系统在走廊的公共区域设立了一个有具体地址的“邮筒”命名管道。只要员工 A 知道这个邮筒的名字就可以往里面投递文件员工 B 只要知道同一个名字就可以去那里取文件 。如何建造这个“邮筒”命名管道是一种特殊类型的文件 。在命令行里你可以直接用指令创建$ mkfifo mypipe在 C 代码里我们用同名的系统调用#includesys/types.h#includesys/stat.h// 创建一个权限为 0644 的命名管道文件intnmkfifo(mypipe,0644);一旦创建好它就会像普通文件一样出现在磁盘的目录里但这只是一个“入口”真正的数据依然像流水一样在内存里穿梭。如何使用匿名管道和命名管道最大的区别在于创建和打开的方式。匿名管道由pipe()凭空变出而命名管道需要用对待普通文件的方式去open()它进程 A写进程open(mypipe, O_WRONLY);然后用write()塞入数据。进程 B读进程open(mypipe, O_RDONLY);然后用read()拿走数据。一旦打开工作完成它们底层的通信规则和匿名管道是一模一样的 。 动动脑筋因为管道是用来通信的必然需要读和写双方配合。假设现在走廊上建好了一个邮筒mypipe写进程员工A跑过去执行了open(mypipe, O_WRONLY)准备往里塞数据但是读进程员工B还没上班还没调用open准备读。在默认阻塞模式下你觉得操作系统会对这个时候正在执行open的写进程员工A做什么操作系统为什么要这么设计关于刚才“邮筒”命名管道的开门规则如果写进程调用open准备写但读进程还没打开写进程会被操作系统阻塞一直卡在open函数那里等待直到有读进程也打开了这个管道 。为什么系统要这么干因为管道存在的唯一意义就是通信如果“收件人”都没到场你把信塞进邮筒不仅没意义还可能造成数据的无意义堆积。所以系统强制要求双方“同时到场”才能打通通道。接下来我们进入下一个重量级沟通渠道。三、 共享内存(Shared Memory) “高效的公共大白板”System V 共享内存场景比喻不管是匿名管道还是命名管道数据都像是在水管里流动本质上是把数据从一个员工的办公室用户空间拷贝到操作系统那里内核空间再由操作系统拷贝到另一个员工的办公室。这个过程涉及到多次的数据搬运。为了追求极致的速度操作系统直接在两个办公室中间的走廊上挂了一块“大白板”物理内存。员工 A 和员工 B 只要一抬头映射到自己的虚拟地址空间就能直接看到并在上面写字 。数据再也不用经过内核来回拷贝了 核心系统调用函数操作系统为这块白板提供了一套标准的操作流程shmget申请白板去后勤部申请一块指定大小的白板。如果已经存在就直接获取它的使用权 。shmat搬进办公室把这块白板的视野拉进自己的虚拟地址空间Attach函数会返回这块内存的起始指针 。shmdt移出视线用完了把白板移出自己的地址空间Detach 。注意这只是你不再看了白板本身还在。shmctl销毁白板彻底把这块白板砸烂回收IPC_RMID命令。System V 的 IPC 资源生命周期是随内核的如果进程退出了但没有执行销毁操作这块共享内存会一直存在直到重启 。C 核心代码演示#includeiostream#includesys/ipc.h#includesys/shm.h#includeunistd.hintmain(){// 1. 生成一个唯一的 key就像是白板的资产编号key_t keyftok(.,0x66);// 2. 申请一块 4096 字节的共享内存 (IPC_CREAT 代表没有就创建)intshmidshmget(key,4096,IPC_CREAT|0666);if(shmid0)return-1;// 3. 挂接共享内存获取指针char*shmaddr(char*)shmat(shmid,nullptr,0);// 4. 直接像操作普通数组一样使用它std::cout写入数据...std::endl;// shmaddr[0] A; // 读写操作完全在用户态进行// 5. 去关联shmdt(shmaddr);// 6. 销毁共享内存 (通常由服务端/主进程来执行)shmctl(shmid,IPC_RMID,nullptr);return0;} 高频面试题与知识点拓展题目共享内存是速度最快的 IPC 方式那它有什么致命的缺点解析共享内存没有任何内置的同步与互斥保护机制缺乏访问控制。想象一下员工 A 正在白板上画一幅复杂的架构图画了一半员工 B 就跑过来拍照读取数据那 B 拿到的就是一个残次品。这就是典型的并发问题 。为了解决这个问题我们必须配合使用其他机制比如信号量或管道来约束他们的行为。 信号量与临界区概念铺垫为了解决上面“白板打架”的问题我们需要明白几个极其关键的基础概念 临界资源像大白板这种多个进程都能看到但一次只应该让一个人去修改的公共资源 。临界区你代码里真正去修改白板、读取白板的那几行代码。保护资源本质上就是保护这几行代码不被同时执行 。信号量 (Semaphore)本质上是一个计数器是对资源的预订机制 。你可以把它当成白板旁边挂着的一把锁。用白板前先申请加锁P 操作计数器减一 用完释放锁V 操作计数器加一 。 内核是怎么管理这些资源的C 语言实现多态最后一个硬核知识点Linux 内核是如何在底层把管道、共享内存、消息队列管理得井井有条的场景比喻系统里有各种各样的 IPC 资源就像公司里有白板、邮筒、保险箱。为了方便登记行政部内核做了一个统一的“资产清单”一个柔性数组ipc_id_ary。这里藏着一个极度优雅的设计不管是共享内存的结构体shmid_kernel还是消息队列的msg_queue亦或是信号量的sem_array它们的第一个成员毫无例外都是一个叫做kern_ipc_perm的基础权限结构体 这意味着内核只需要维护一个存放kern_ipc_perm*指针的数组。当需要操作具体资源时拿出这个通用指针直接做一个强制类型转换就能访问到该资源特有的属性。这其实就是用 C 语言实现了面向对象编程里的“多态”特性 终篇总结 (Conclusion) Linux IPC 核心技术大比拼IPC 通信方式场景比喻亲缘关系限制底层关键函数/指令核心优缺点核心考点/注意点匿名管道 (Pipe)办公室单向水管 必须有父子/兄弟pipe(),fork(),read(),write()优点内置同步与互斥自带锁安全缺点只能用于亲缘关系进程间通信读写四大规则1. 写正常/读空-阻塞2. 读正常/写满-阻塞3. 写关闭/读正常-读完返回04. 读关闭/写正常-异常崩溃(SIGPIPE)命名管道 (FIFO)街角公共邮筒 无限制任意进程mkfifo(),open(),read(),write()优点打破亲缘限制像操作文件一样简单缺点数据传输仍需在内核与用户态间来回拷贝默认阻塞打开规则必须读写双方同时open才会继续执行共享内存 (Shm)走廊公共大白板 无限制ftok(),shmget(),shmat(),shmdt(),shmctl()优点速度最快数据不经过内核来回拷贝缺点没有任何内置同步与互斥机制1. 缺乏访问控制易带来并发问题 。2. 生命周期随内核进程退出后资源不释放须手动销毁️ 核心架构与底层思维升华从工具到设计进程池我们不仅学习了单条管道还通过进程池Process Pool的架构理解了包工头主进程如何通过多路管道实现任务的分发。在关闭进程池时我们利用“写端关闭读端返回0”的天然特性优雅地实现了子进程的退出与资源回收告别了僵尸进程。理解临界三要素为了防止“公共大白板”被乱涂乱画我们引入了临界资源白板本身、临界区操作白板的代码以及信号量资源预订计数器的概念。这是后续学习多线程、并发编程的绝对基石。内核的艺术C 语言实现多态Linux 内核在管理 System V 资源共享内存、消息队列、信号量时展现了极高的代码美学。通过将通用权限结构体kern_ipc_perm放在各自定义结构体的首位内核用一个柔性指针数组ipc_id_ary统一了天下在 C 语言中完美复现了面向对象的“多态”思想。
深入浅出 Linux 进程间通信:从匿名管道到内核 System V 对象
在Linux操作系统中为了保证系统的安全稳定每个进程都有自己独立的虚拟地址空间。你可以把每个进程想象成在一个完全隔音、独立的办公室里工作的员工。他们各自处理自己的文件互不干扰。但这带来了一个问题如果一项复杂的工作需要多个员工协同完成比如员工A负责获取数据员工B负责处理数据他们被锁在各自的办公室里该怎么交流呢这就是进程间通信Inter-Process Communication, IPC存在的意义。它是操作系统为这些隔离的员工提供的沟通渠道主要目的是为了实现数据传输、资源共享、通知事件以及进程控制文章目录一、 匿名管道(Anonymous Pipe)二、 命名管道(Named Pipe)三、 共享内存(Shared Memory) 终篇总结 (Conclusion)我们先从最古老、最经典的通信方式开始管道 (Pipes)。想象一下你在 Linux 终端输入了一行最常见的命令who | wc -l。这里的|其实就是一个管道它把who进程的标准输出像水流一样直接灌进了wc -l进程的标准输入里 。在代码里我们最常用的是匿名管道 (Anonymous Pipe)。一、 匿名管道(Anonymous Pipe) 匿名管道的诞生与共享要建造这样一根水管我们需要用到一个系统调用函数pipe()。intpipe(intfd[2]);调用成功后系统会给你一个包含两个整数的数组它们就像是水管的两头fd[0]读端 (Read end) —— 相当于水管的出水口 。fd[1]写端 (Write end) —— 相当于水管的进水口 。但是如果只有父进程一个人拿着水管的两头自己给自己灌水是没意义的。我们怎么让另一个进程也拿到这根水管呢秘诀就是fork()按照文件里的原理解析管道通信分为巧妙的三步建水管父进程调用pipe()创建管道拿到了fd[0]和fd[1]影分身父进程调用fork()产生子进程。因为子进程会继承父进程的文件描述符所以子进程也拿到了这根水管的fd[0]和fd[1]定方向管道是半双工的数据只能单向流动。为了防止混乱比如假设是父进程写、子进程读那么父进程就必须关闭读端fd[0]子进程必须关闭写端fd[1]这样一条干净的、从父进程流向子进程的单向数据通道就建好了 动动脑筋既然“匿名管道”是依靠fork()的继承机制来让两个进程共享这根水管的那你觉得这种通信方式有什么天生的局限性假设系统里有两个你昨天分别独立启动的程序比如进程 A 和进程 B它们能用这种“匿名管道”来聊天吗匿名管道的致命局限性在于它只能用于具有共同祖先具有亲缘关系的进程之间进行通信。因为匿名管道在系统中没有名字完全依赖fork()时子进程对父进程文件描述符表的拷贝。如果是昨天独立启动的进程 A 和进程 B它们之间没有任何血缘关系自然也就无法共享到这根“水管”的进出口。 深入水管管道的读写四大规则在使用管道水管时Linux 内核帮我们处理了同步与互斥 。你可以把下面四种情况当成日常用水的常识来记忆水管没水了写正常读空如果写端还没写数据读端来读读进程会被阻塞挂起等待直到水管里有水 。水管塞满了读正常写满如果读端不读写端一直写当管道写满时写进程会被阻塞直到有人把水读走腾出空间 。供水站下班了写关闭读正常如果所有写端进水口都关闭了读端把管道里剩下的数据读完后read函数会直接返回 0明确告诉你“数据到此结束” 。没人接水了读关闭写正常极其重要如果所有的读端出水口都关闭了此时写端再去写数据是毫无意义的。操作系统会非常严厉地直接发送SIGPIPE信号杀掉这个写进程 。 实战应用基于管道的“进程池” (Process Pool)每次有任务都去创建一个子进程开销太大。我们可以提前雇佣一批“打工人”子进程让它们待命这就是进程池的思想。场景比喻你主进程/包工头提前招了 5 个工人子进程并给每个工人都单独拉了一根单向水管匿名管道。工人每天的工作就是死死盯着水管的出口处阻塞式read。当有新任务比如任务编号1代表处理网页2代表查数据库时你挑一根没那么忙的水管把任务编号扔进去 。对应的工人拿到编号立刻开始干活。干完继续盯着水管。C 核心代码逻辑演示#includeiostream#includevector#includeunistd.h#includesys/wait.h// 描述通信通道structChannel{int_wfd;// 包工头掌握的写端pid_t _worker_id;// 打工人的进程号Channel(intfd,pid_t id):_wfd(fd),_worker_id(id){}};voidCreatePool(intnum,std::vectorChannelchannels){for(inti0;inum;i){intpipefd[2];pipe(pipefd);// 1. 建水管pid_t idfork();// 2. 招工人if(id0){// 子进程打工人close(pipefd[1]);// 关闭写端// ... 循环从 pipefd[0] 读取任务并执行 ...exit(0);}// 父进程包工头close(pipefd[0]);// 关闭读端channels.emplace_back(pipefd[1],id);// 把写端和工人ID记录在名册上}} 核心考点自测❓ 动动脑筋在进程池退出时包工头父进程应该如何优雅地让所有打工人子进程下班并回收它们的资源而不至于产生僵尸进程✅ 答案解析根据上面讲的“管道读写四大规则”的第 3 条写关闭读正常。包工头只需要遍历自己的channels记录依次关闭所有管道的写端 (_wfd)。打工人们一直阻塞在读端一旦写端全关它们的read就会返回 0这就相当于收到了“下班指令”。子进程内部判断read 0后直接break退出死循环。随后包工头再调用waitpid()就能顺利回收子进程资源实现安全清理 。我们继续往下走。刚才提到的匿名管道虽然好用但有一个致命弱点必须要有血缘关系。那么如果两个完全不相干的进程比如你昨天写的一个服务端程序和今天刚写的一个客户端程序想要通信该怎么办呢这就轮到我们的第二个沟通渠道出场了二、 命名管道(Named Pipe) “街角的公共邮筒”命名管道 FIFO场景比喻为了让两个互不认识的员工也能交换文件系统在走廊的公共区域设立了一个有具体地址的“邮筒”命名管道。只要员工 A 知道这个邮筒的名字就可以往里面投递文件员工 B 只要知道同一个名字就可以去那里取文件 。如何建造这个“邮筒”命名管道是一种特殊类型的文件 。在命令行里你可以直接用指令创建$ mkfifo mypipe在 C 代码里我们用同名的系统调用#includesys/types.h#includesys/stat.h// 创建一个权限为 0644 的命名管道文件intnmkfifo(mypipe,0644);一旦创建好它就会像普通文件一样出现在磁盘的目录里但这只是一个“入口”真正的数据依然像流水一样在内存里穿梭。如何使用匿名管道和命名管道最大的区别在于创建和打开的方式。匿名管道由pipe()凭空变出而命名管道需要用对待普通文件的方式去open()它进程 A写进程open(mypipe, O_WRONLY);然后用write()塞入数据。进程 B读进程open(mypipe, O_RDONLY);然后用read()拿走数据。一旦打开工作完成它们底层的通信规则和匿名管道是一模一样的 。 动动脑筋因为管道是用来通信的必然需要读和写双方配合。假设现在走廊上建好了一个邮筒mypipe写进程员工A跑过去执行了open(mypipe, O_WRONLY)准备往里塞数据但是读进程员工B还没上班还没调用open准备读。在默认阻塞模式下你觉得操作系统会对这个时候正在执行open的写进程员工A做什么操作系统为什么要这么设计关于刚才“邮筒”命名管道的开门规则如果写进程调用open准备写但读进程还没打开写进程会被操作系统阻塞一直卡在open函数那里等待直到有读进程也打开了这个管道 。为什么系统要这么干因为管道存在的唯一意义就是通信如果“收件人”都没到场你把信塞进邮筒不仅没意义还可能造成数据的无意义堆积。所以系统强制要求双方“同时到场”才能打通通道。接下来我们进入下一个重量级沟通渠道。三、 共享内存(Shared Memory) “高效的公共大白板”System V 共享内存场景比喻不管是匿名管道还是命名管道数据都像是在水管里流动本质上是把数据从一个员工的办公室用户空间拷贝到操作系统那里内核空间再由操作系统拷贝到另一个员工的办公室。这个过程涉及到多次的数据搬运。为了追求极致的速度操作系统直接在两个办公室中间的走廊上挂了一块“大白板”物理内存。员工 A 和员工 B 只要一抬头映射到自己的虚拟地址空间就能直接看到并在上面写字 。数据再也不用经过内核来回拷贝了 核心系统调用函数操作系统为这块白板提供了一套标准的操作流程shmget申请白板去后勤部申请一块指定大小的白板。如果已经存在就直接获取它的使用权 。shmat搬进办公室把这块白板的视野拉进自己的虚拟地址空间Attach函数会返回这块内存的起始指针 。shmdt移出视线用完了把白板移出自己的地址空间Detach 。注意这只是你不再看了白板本身还在。shmctl销毁白板彻底把这块白板砸烂回收IPC_RMID命令。System V 的 IPC 资源生命周期是随内核的如果进程退出了但没有执行销毁操作这块共享内存会一直存在直到重启 。C 核心代码演示#includeiostream#includesys/ipc.h#includesys/shm.h#includeunistd.hintmain(){// 1. 生成一个唯一的 key就像是白板的资产编号key_t keyftok(.,0x66);// 2. 申请一块 4096 字节的共享内存 (IPC_CREAT 代表没有就创建)intshmidshmget(key,4096,IPC_CREAT|0666);if(shmid0)return-1;// 3. 挂接共享内存获取指针char*shmaddr(char*)shmat(shmid,nullptr,0);// 4. 直接像操作普通数组一样使用它std::cout写入数据...std::endl;// shmaddr[0] A; // 读写操作完全在用户态进行// 5. 去关联shmdt(shmaddr);// 6. 销毁共享内存 (通常由服务端/主进程来执行)shmctl(shmid,IPC_RMID,nullptr);return0;} 高频面试题与知识点拓展题目共享内存是速度最快的 IPC 方式那它有什么致命的缺点解析共享内存没有任何内置的同步与互斥保护机制缺乏访问控制。想象一下员工 A 正在白板上画一幅复杂的架构图画了一半员工 B 就跑过来拍照读取数据那 B 拿到的就是一个残次品。这就是典型的并发问题 。为了解决这个问题我们必须配合使用其他机制比如信号量或管道来约束他们的行为。 信号量与临界区概念铺垫为了解决上面“白板打架”的问题我们需要明白几个极其关键的基础概念 临界资源像大白板这种多个进程都能看到但一次只应该让一个人去修改的公共资源 。临界区你代码里真正去修改白板、读取白板的那几行代码。保护资源本质上就是保护这几行代码不被同时执行 。信号量 (Semaphore)本质上是一个计数器是对资源的预订机制 。你可以把它当成白板旁边挂着的一把锁。用白板前先申请加锁P 操作计数器减一 用完释放锁V 操作计数器加一 。 内核是怎么管理这些资源的C 语言实现多态最后一个硬核知识点Linux 内核是如何在底层把管道、共享内存、消息队列管理得井井有条的场景比喻系统里有各种各样的 IPC 资源就像公司里有白板、邮筒、保险箱。为了方便登记行政部内核做了一个统一的“资产清单”一个柔性数组ipc_id_ary。这里藏着一个极度优雅的设计不管是共享内存的结构体shmid_kernel还是消息队列的msg_queue亦或是信号量的sem_array它们的第一个成员毫无例外都是一个叫做kern_ipc_perm的基础权限结构体 这意味着内核只需要维护一个存放kern_ipc_perm*指针的数组。当需要操作具体资源时拿出这个通用指针直接做一个强制类型转换就能访问到该资源特有的属性。这其实就是用 C 语言实现了面向对象编程里的“多态”特性 终篇总结 (Conclusion) Linux IPC 核心技术大比拼IPC 通信方式场景比喻亲缘关系限制底层关键函数/指令核心优缺点核心考点/注意点匿名管道 (Pipe)办公室单向水管 必须有父子/兄弟pipe(),fork(),read(),write()优点内置同步与互斥自带锁安全缺点只能用于亲缘关系进程间通信读写四大规则1. 写正常/读空-阻塞2. 读正常/写满-阻塞3. 写关闭/读正常-读完返回04. 读关闭/写正常-异常崩溃(SIGPIPE)命名管道 (FIFO)街角公共邮筒 无限制任意进程mkfifo(),open(),read(),write()优点打破亲缘限制像操作文件一样简单缺点数据传输仍需在内核与用户态间来回拷贝默认阻塞打开规则必须读写双方同时open才会继续执行共享内存 (Shm)走廊公共大白板 无限制ftok(),shmget(),shmat(),shmdt(),shmctl()优点速度最快数据不经过内核来回拷贝缺点没有任何内置同步与互斥机制1. 缺乏访问控制易带来并发问题 。2. 生命周期随内核进程退出后资源不释放须手动销毁️ 核心架构与底层思维升华从工具到设计进程池我们不仅学习了单条管道还通过进程池Process Pool的架构理解了包工头主进程如何通过多路管道实现任务的分发。在关闭进程池时我们利用“写端关闭读端返回0”的天然特性优雅地实现了子进程的退出与资源回收告别了僵尸进程。理解临界三要素为了防止“公共大白板”被乱涂乱画我们引入了临界资源白板本身、临界区操作白板的代码以及信号量资源预订计数器的概念。这是后续学习多线程、并发编程的绝对基石。内核的艺术C 语言实现多态Linux 内核在管理 System V 资源共享内存、消息队列、信号量时展现了极高的代码美学。通过将通用权限结构体kern_ipc_perm放在各自定义结构体的首位内核用一个柔性指针数组ipc_id_ary统一了天下在 C 语言中完美复现了面向对象的“多态”思想。