资讯详情

【Linux】进程间通信:两个互相看不见的进程,怎么看到同一份资源

📅 2026/10/15 19:03:57 | 华诺云谱 👁 阅读
【Linux】进程间通信:两个互相看不见的进程,怎么看到同一份资源
自律是普通人最大的武器系列专栏直达链接 Linux进入专栏 → C语言进入专栏 → C进入专栏 → MySQL进入专栏 → 算法刷题进入专栏 → 博客主页虚无的纽扣 作者仓库github | gitee目录引言1、为什么需要进程间通信2、怎么通信3、管道匿名管道的原理管道的读写与特性4、命名管道5、System V 共享内存共享内存的原理与唯一标识创建、挂接与删除为什么最快以及和动态库的关系6、System V 信号量7、拓展IPC 的内核结构结语引言在前面几篇博客里我们从进程的概念和状态讲起聊完了进程优先级与环境变量又用一整篇的篇幅拆开了进程地址空间最后还自己动手实现了一个简易的 Shell。走到这里我们对一个进程自身的运作已经不算陌生了。不过仔细想想前面所有内容都有一个隐含的前提——我们观察的始终是单独的一个进程可真实的操作系统里同时跑着成百上千个进程它们各自拥有独立的地址空间页表把虚拟地址映射到属于自己的那一份物理内存上也就是说进程 A 看不见进程 B 的数据进程 B 也看不见进程 A 的数据这份“看不见”正是操作系统给每个进程的安全隔离它的来龙去脉我们在《每个进程都拥有“假内存” | 进程地址空间详解》里已经拆解过了。隔离本身是件好事但它也带来了一个很现实的问题如果两个进程想交换数据、共享一份资源该怎么办这就是我们今天要聊的进程间通信。这篇文章里我们会从最古老的管道讲起一路走到 System V 标准的共享内存与信号量最后再钻进内核看看这几种 IPC 资源在操作系统内部是怎么被统一管理起来的。1、为什么需要进程间通信为什么要进程间通信把进程放进一个真实的场景里想一想答案就很清楚了。首先是数据传输多个进程之间可能要进行数据传输比如一个进程负责采集、另一个进程负责处理数据就得从前者流到后者其次是资源共享多个进程之间可能共享同样的资源比如同一台硬件设备、同一块内存区域再往后是通知事件一个进程需要向另一个或一组进程发送消息通知它发生了某种事件。最后是进程控制有些进程希望完全控制另一个进程的执行如 debug 进程此时控制进程希望能够拦截另一个进程的所有陷入和异常并能够及时知道它的状态改变。这四种需求看起来各不相同但它们最终的落脚点是一样的。2、怎么通信那么两个进程要怎么才能通信呢前面说过进程之间之所以看不见彼此是因为它们的页表指向的是不同的物理内存那反过来想只要能让两个进程的页表指向同一块物理内存它们不就等于看到了同一份数据了吗所以进程间通信的本质是先让不同的进程看到同一份资源然后才有通信的条件而这一份资源是由操作系统提供的。顺着这个思路往下走你会发现接下来的每一种通信方式其实都是在回答“怎么让两个进程看到同一份资源”这一个问题。3、管道匿名管道的原理管道是最古老的进程间通信的形式我们把从一个进程连接到另一个进程的数据流称之为管道我们最熟悉的匿名管道通常就用来做父子之间的通信。要理解它的原理得先回到进程创建的那一刻当父进程创建子进程时父进程内的所有数据都会给子进程拷贝一份但是文件并不会打开第二次而是子进程拷贝下来的页表中会有同样的映射关系指向打开的那个文件struct file也拷贝了一份但 inode 和数据页只进行了一次浅拷贝只发生了指针拷贝。所以父子进程就可以同时访问同一个文件可以同时看到文件中的缓冲区里的数据——这不就满足了我们上面说的进程间通信的本质让不同的进程看到同一份资源吗这就是所谓的管道。具体来说父进程先分别以读写方式打开一个内存级的文件作为管道然后创建子进程子进程经过拷贝也拥有同样的文件描述符然后一般都是实现父子单向通信将父子进程不需要的文件描述符关掉就可以实现父子间的单向通信。创建管道文件用的是int pipe(int pipefd[2]);返回值表示是否创建成功并将文件描述符保存在pipefd这个数组中。这种文件不需要文件路径因为它是内存级的只存在内存里不会刷新到磁盘上它只有 inode 和内容也就是缓冲区其他的文件结构都是不存在的所以也不需要文件名这种管道就叫匿名管道。那没有文件名字又是怎么保证两个进程打开的是同一个管道文件呢因为子进程继承父进程的文件描述符表。管道的读写与特性管道创建好之后读写两端的行为会出现四种典型情况1、写慢读快读端阻塞等待。2、写快读慢管道文件写满后写端就要阻塞等待。3、写端关闭读端继续read 就会读到返回值为 0表示文件结尾。4、写端继续读端关闭写端再写入没有任何意义操作系统会杀掉写端进程发送 13 号信号。另外管道的容量在 Ubuntu 系统里为 64KB。把管道的特点归纳一下1、匿名管道只能用来进行具有血缘关系的进程进行进程间通信常用于父子。2、管道文件自带同步机制。3、管道是面向字节流的。4、管道是单向通信的任何一个时刻只能一个发、一个收就叫半双工如果任何一个时刻都可以同时发收就叫全双工管道属于半双工的一种特殊情况。5、文件的生命周期是随进程的。4、命名管道匿名管道只能用来进行具有血缘关系的进程进行进程间通信那如果两个进程不相关该如何进行通信呢当两个独立的进程访问同一份文件时第一个进程在访问时操作系统会将该文件的属性和内容加载到内存中然后进程的struct file中就会有两个指针分别指向存储属性的 inode 和存储数据的缓冲区当第二个进程也加载这个文件时虽然操作系统也会创建一个独立的struct file但因为指向的文件相同所以它也会指向同一份 inode 和缓冲区。所以上面父子进程看到同一份资源的方式是通过继承而这里则是通过打开同一个路径下的同一个文件来让两个进程看到同一份资源而这个文件是有名字的所以就叫命名管道。这样一来这个文件就不是我们之前说过的单纯是内存级的文件而是一个磁盘级的文件当一个文件是磁盘级文件时它里面的数据是要被刷新到磁盘上的可是今天我们不想它刷新到磁盘上所以 Linux 系统中会支持一种特殊的文件叫管道文件它的特点就是只会被打开不需要刷新。当写入方没有执行 open 的时候read 方就要在 open 内部进行阻塞直到有人把管道文件打开了open 才会返回因为只打开 read 方没有意义没有 write 方往里面写信息所以 read 方会阻塞直到有 write 方启动。总的来说命名管道的特性和匿名管道是一致的只有一点不同——命名管道可以用来进行不相关的进程间通信。5、System V 共享内存共享内存的原理与唯一标识System V 是一种标准Linux 内核支持了这种标准专门设计了一个 IPC 通信模块IPC 就是 Inter-Process Communication进程间通信的缩写。先说共享内存的原理进程 A 在物理内存上申请一块空间并在自己的虚拟地址空间中也申请一块空间用页表建立映射关系进程 B 也在自己的虚拟空间内部申请一块空间用自己的页表与物理内存的空间也建立映射关系这样进程 A 和 B 就都可以访问同一块物理内存空间了。上述操作都是操作系统完成的操作系统给我们提供系统调用我们可以用系统调用完成上述操作释放这块物理空间只要先取消关联关系然后由操作系统释放内存就行。在操作系统内部可能同时存在多组进程都在使用不同的共享内存来进行通信操作系统内就同时存在多个共享内存所以操作系统要管理共享内存既然操作系统要管理共享内存说明共享内存一定要有对应的描述共享内存的内核结构体对象进程和共享内存的关系就是内核数据结构之间的关系。那么我们怎么知道共享内存存在还是不存在又怎么保证两个不同的进程拿到的是同一个共享内存呢这两个问题的答案都需要用到系统调用的第一个参数 key。不同的进程要使用共享内存来进行通信我们需要一种方法来标识共享内存的唯一性这个唯一性就由 key 来区分key 不是内核直接形成的而是在用户层构建并传入操作系统的。为什么 key 要这么设计呢如果 key 值是由操作系统在创建共享内存后返回给其中一个进程的值的话那么其他想要使用这个共享内存的进程无法获取这个 key 值也就无法找到对应的共享内存那如果我们现在让这个 key 值在进程内设定然后在创建共享内存时将其也传入进去其他的进程是不是就可以用这个 key 值去寻找对应的共享内存了那其他的进程又是怎么拿到这个 key 值的呢我们可以在每一个进程里都设置一个同样的方法通过这个方法就可以计算出一个同样的 key 值不就可以做到获得同一个 key 值了吗这就和我们上面讲过的命名管道类似命名管道就相当于是把管道文件的路径作为这个 key 值用同样的路径就可以找到同一个管道文件key 值不过是换了一种方法而已。这个 key 值可以用一个函数来获得就是 ftok它的用法如图 3 所示。pathname 就是文件路径proj_id 随便输入任意值都行这样 ftok 算法就会结合这两个数据构建出一个独特的 key 值如果重复了就更改传入的数据就行。创建、挂接与删除和曾经在基础 IO 时讲过的类似shmget的标记位shmflg常见的只有两个IPC_CREAT创建共享内存如果目标共享内存不存在就创建否则打开已经存在的共享内存并返回。IPC_EXCL单独使用无意义必须与上一个标记位组合使用才有意义如果要创建的共享内存不存在就创建如果已经存在shmget就会出错返回。这听起来好像没什么用实际上它的用途是只要shmget成功返回一定是一个全新的共享内存。在创建共享内存时这个共享内存也会被系统看作一个文件所以在创建时我们也要给共享内存设置权限在标记位的后面用按位或符号|或上0666就行返回值为一个整型从 0 开始可以记作 shmid。共享内存有一点和管道很不一样进程结束如果没有进行删除共享内存共享内存资源就会一直存在。共享内存的资源生命周期随内核如果没有显式的删除即便进程退出了IPC 资源依旧被占用。删除或控制共享内存在用户层我们不能使用 keykey 可以只给内核来区分唯一性在用户层我们需要用 shmid 进行管理共享内存shmctl的 cmd 需要传入对应的命令删除共享内存的命令为IPC_RMID第三个参数是传入共享内存的属性这里我们还没学到直接传入nullptr就行。我们现在已经知道了怎么创建和删除共享内存那我们怎么建立进程与共享内存间的映射呢shmat的返回值是映射完成的起始虚拟地址空间地址第一个参数是共享内存的标识符表明你要链接哪一个共享内存第二个参数传一个虚拟地址可以固定地址进行挂接正常我们用不到设置为nullptr就行第三个参数也是标志位我们在使用时一般都是既读又写的所以设置为 0 就行使用共享内存的默认设置。shmat就是将共享内存挂接到当前进程的地址空间中。关于删除共享内存还漏了一个知识点在删除共享内存时我们不能直接的删除我们需要先进行去关联。假设我们现在有进程挂载在共享内存上我们需要先解除挂载才能进行删除。为什么最快以及和动态库的关系接下来是读写。读写共享内存并没有使用系统调用因为我们一旦把共享内存映射成功了共享内存的映射位置在堆和栈之间属于共享区共享区属于用户空间可以让用户直接使用。共享内存是进程间通信中速度最快的方式原因有两点1、映射之后读写可以直接被对方看到2、不需要进行系统调用获取或者写入内容。但它的缺点也很明显通信双方没有同步机制可能导致写入端才输入了一部分读取端就已经读走了共享内存没有保护机制。另外如果共享内存的另一端不是进程而是动态库动态库可以将自己的代码和数据加载到共享内存中进程建立对应的映射关系然后对进程的数据做地址空间级的重定向这样就能以用户地址的方式访问库中的方法所以共享内存不仅是实现进程间通信的一种方式也是动态库加载到内存、让进程看到的底层原理。最后补充一个细节在内核中共享内存在创建的时候它的大小必须是 4KB4096的整数倍但是在用户级你申请用多少就给你分配多少意思就是你如果申请 4097 个字节虽然共享内存实际申请了 8KB但是你能使用的还是只有 4097因为你只申请了 4097 个字节。6、System V 信号量信号量的本质是一个计数器一块内存空间里划分出很多个小内存空间每有一个空间正在被访问计数器就减一每有一个空间结束访问计数器就加一。在这里要先介绍一个概念——临界资源临界资源指的是一次只允许一个进程访问的共享资源像我们上面讲的共享内存就是非临界资源。要想访问临界资源都需要申请而我们所说的信号量就相当于管理着一块块的临界资源描述的是临界资源中资源数量的多少。所有进程想访问临界资源中的一小块就必须先申请信号量申请信号量本身是一种对资源的预定机制只要某个进程申请了一块临界资源哪怕没有使用这块资源也无法再分配给其他进程直到结束使用为止所以信号量本质上是一种对资源的预定机制。为了防止在申请资源时出现两个进程同时申请资源的情况申请资源时的操作必须为原子性操作。原子性是什么呢简单来说就是不是 0 就是 1没有中间态要么申请了要么没申请不会有申请过程这种东西换成更严格的说法原子操作是不可被分割的执行到一半不会被打断也正因为如此它才不会出现“申请到一半被别人插一脚”的情况。只要某进程申请资源如果申请成功就立马获得资源不会出现一个进程申请过程中另一个进程也申请导致的两个进程申请到同一块资源的问题。信号量这种 IPC 资源最重要的两个问题是1、不要让两个进程访问同一个位置2、不要放入过多的进程进来。这两个问题都可以通过保证资源申请的原子性来解决当资源已经被申请完还有进程要继续申请信号量就会将这些进程进行阻塞挂起并管理起来直到有资源结束使用后再按照顺序进行分配。如果我们将一整块资源整体使用就叫做二元信号量如果分块使用就是多元信号量。7、拓展IPC 的内核结构聊完了共享内存和信号量最后我们钻到内核里看看操作系统是怎么把这些 IPC 资源统一管理起来的。这一节还会带出一个前面没提过的成员——消息队列它和共享内存、信号量一样都属于 System V 标准我们这里不展开它怎么用只看看内核是把它们放在一起管的。IPC 资源在操作系统中会存在一个全局的数据结构ipc_ids这个数据结构里有一个指针叫 entries指向一个叫做ipc_id_ary的结构体结构体里有一个数组是一个柔性数组。内核里还有个管理消息队列的结构叫msg_queue管理信号量的sem_array管理共享内存的shmid_kernel它们三个数据结构里的第一个成员是q_perm、sem_perm、shm_perm这种结构在内核中叫做kern_ipc_perm这三种 IPC 的 key 值都存在这个数据结构里包括用户信息和权限等级等都存在这里面而上面讲到的那个柔性数组里存放的数据类型就是kern_ipc_perm*。所以虽然共享内存、消息队列、信号量这三种 IPC 资源都不一样但是它们的开头第一个元素都一样操作系统就可以将这三种 IPC 资源维护在一个柔性数组里。所以我们之前说的 shmid其余还有 msgid、semid 其实拿到的就是数组下标在使用时用这个数组下标就可以索引到对应的 IPC索引到了怎么访问呢索引到了对应的下标后将整个数组下标位置使用强制类型转换就可以将该位置的资源转化成对应的 IPC 资源就可以整体访问了这就是用 C 语言实现多态的方式。结语这节课我们学习了进程间通信的几种方式从最古老的管道到 System V 标准的共享内存与信号量它们看似各不相同但本质上都在回答同一个问题——怎么让两个互相看不见的进程看到同一份资源。匿名管道靠继承命名管道靠同一个路径共享内存靠把两块虚拟地址映射到同一块物理内存上而信号量则在这之上补了一层对资源数量的管理。如果有哪里讲得不清楚也欢迎各位大佬在评论区指正我们一起把这些底层的东西啃透。以后我也会继续创作 Linux 方向的博客如果觉得有帮助的话希望各位大佬能三联关注支持一下看到后我会进行回关和回访。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑