IPC管道 一台OS写出来的时候是没有进程通信的是后续有了进程通信的需求首先复用文件系统搞出管道后来为了更好地满足需求搞出了System V和POSIX进程是具有独立性的但是进程间也要进行通信进程通信Inter-Process Communication比如一个做网络服务的进程负责从网络中读取数据而另一个进程不管数据的读取只对数据进行处理比如加密解密存储访问数据库读取文件访问Redis比如在线OJ写好的代码是字符集合就是文本文件传到服务端的时候服务器通过一个进程拿到代码再让另一个文件处理数据比如编译运行进程之间协同工作进程各司其职那么研究进程间如何通信也就是研究进程如何协同工作1.进程通信目的(why)进程间通信目的数据传输一个进程需要将其数据发送给另一个进程资源共享多个进程共享相同的资源通知事件一个进程需要向另一个/组进程发送消息通知它们发生了某种事件比如进程终止时要通知父进程有些进程希望完全控制另一个进程的执行如Debug进程此时控制进程希望拦截另一个进程的所有陷入和异常并能够及时知道其状态变化比如gdb调试被调试的进程是子进程用户输入r等方法gdb控制子进程跑起来或者停下来2.进程通信方式(what)我们知道父子进程之间是可以数据共享的但是会发生写时拷贝也就是父子进程只能共享数据中相同的内容不同的内容就要通信而非父子进程也要通信但是在面临一个新需求的时候我们想到的不是从零到一搓一个说实话全新的东西谁知道怎么搓那就就考虑复用原有的某些数据结构来实现新的需求并复用其基操进程间通信方式包括2.1 分类管道就是内核级文件的复用改造System V IPC用于同一台机器内部单机但兼容性不强比如要把软件分布式部署到十台机器上本来写的System V就要重新写但是如果是跨网络的POSIX就能直接用所以System V有些边缘化POSIX IPC跨网络通信2.2 发展System V IPC和POSIX IPC都是通信标准的名称类似华为的5G标准新能源一些电池的标准标准定下来了USB口大小内存条宽度HDMI有多少个口标准定好之后USB线的制造商笔记本电脑的制造商USB口要遵守同一套标准这样制造出来的USB线才能插在电脑的USB接口通信方式有哪些函数怎么定参数怎么传每个参数什么含义返回值什么含义内核当中对这种技术的实现是什么标准定下来之后在windows/linux下写的通信技术就可以相互迁移把方法拿过来更高屋建瓴的话说是协议各个厂商、OS的制造商程序员约定好一套标准在Linux/windows/macos下无论哪一平台写一份符合标准的协议就可以在各个平台下跑一流的公司是做标准的我们国家在10年前对高科技产品是没有定价权的标准定不了造的产品没有定价权用别人的技术/标准赚钱还要分别人一杯羹后来以华为为代表提出的5G包括未来潜在的物联网通信标准华为鸿蒙系统把手机卖向全球一夜之间用釜底抽薪的方式把OS换成鸿蒙所以现在是让大家把鸿蒙用起来占领未来物联网标准的高地当所有硬件用的都是鸿蒙纯血OS时我的物理网设备之间互相通信的标准就由我来定因为OS时我定的就像windows/linux时西方国家定的而西方国家规定网络必须用TCPIP全世界只要用windows/linux就得用TCP-IP有点像网络效应就比如小明用了微信小明身边的大部分人用了微信那小明的父母未来也要用微信因为要和小明进行通信一流公司做标准可以出卖专利费给其它公司做授权如果是免费专利和技术不谈但如果是收费谁用了专利和技术赚的钱就要给你分一杯羹二流公司做产品腾讯做产品在国内属于第一梯队linux内核或其它技术二流公司最近的高科技创新比如AI模型脑机接口物联网新能源汽车可控核聚变具身智能机器人制造/AI产业大家都在做全球范围内不同国家不同团队之间进行赛马没有一/几家公司能够超出别人一个或十几个身位技术上还没有碾压别人如果真出现了后进者就不会在做这个后面标准才能定区分做的技术是很稳定很完整的技术比如OS依据很稳定再怎么搞也搞不出花来一流标准二流产品三流技术但是高科技技术一切乾坤未定一流的公司还在做技术目标就是定标准有话语权一旦标准是它定的专利是它出的所有人用的工具技术都是它的那么这个时候定价权都在它手上比如中国要出linux课程就要听专利拥有者的他规定必须出的内容目录出标准定价权全球linux课程的定价可以把专利费收的很贵POSIX PIC和System V PIC是老的标准学的时候会有一些规律比如函数命名返回值参数会有一定的规则对历史的了解对于标准、协议的了解还是重要的标准在计算机领域很常见比如组装机在希捷买的磁盘买的内存条主板可能是不同厂家的能够互相组装2.3 通信方式单工通信向听广播不能向广播输出也就是只能从a端向b端发送信号还有两种通信方式半双工通信像正常交流每次只有一人说话另一人在听每一时刻只允许一端发送消息但是不限制是a端还是b端也就是a端可以发送消息b端可以接收也可以是b端发送消息a端接收但每一时刻只允许一端发送单工通信其实是半双工通信的一种特殊形式全双工通信像俩人吵架输出的同时还听着对方输出允许任意时刻任意一端发送消息2.4 管道管道是类Unix系统最古老但应用场景挺多的进程间通信方式Linux和Macos都是脱胎于UNix系统命令行和系统调用很像先来看管道而如果要进行通信就像人和人聊天要同时看到消息信息同步那么我们就要想办法让两个进程看到同一份资源在内存中而进程间是独立的如果在进程的虚拟地址空间比如栈/堆/数据区开辟的变量对于另一个进程是不可见的所以这时候就需要操作系统就需要提供系统调用来实现因为进程是没有办法通过指针或者其它工具来获取进程内核数据结构内容比如PID,PPID只能通过系统调用而两个进程通信用到的资源肯定是隶属于操作系统的要获取OS中的资源必须通过系统调用而系统调用本质就是函数返回值是什么呢函数名是什么呢是大驼峰、小驼峰还是下划线命名呢OS内部通信怎么实现由标准定所以进程间通信原则上是要通过OS管道是基于文件进行内核级进程通信2.4.1 匿名管道2.4.1.1 原理先看现象who显示登录系统的用户信息wc -l 统计文件行数who和wc都是C语言写的程序bash解释成两个进程通过 |形成管道who的stdout重定向到管道文件管道文件重定向到wc的stdinwc进行读取我们先来看父子进程要知道我们刚才分析父子进程可以共享部分数据从这里是不是可以找一找思路为什么父子进程可以共享因为父进程在创建子进程的时候子承父业子进程拷贝父进程的PCBpidppid优先级状态和一些内容会被修改、struct files_struct内核数据结构直接初始化但是inode和文件缓冲区和父进程共用一个因为系统只加载一次而且如果拷贝给子进程如果属性/内容不一样听谁的也就是像下面这样所以本质其实是父子进程共用一个文件而读写的是同一个文件自然能够通信正因为是基于文件的单向通信所以叫管道自然界管道比如自来水天然气管道都是单向的bash会默认打开显示器为标准输入标准输出以及标准错误后续执行的指令或者可执行程序都是bash的子进程都会浅拷贝父进程的文件描述符表所以创建任何进程都是默认打开标准输入标准输出标准错误那么父子间非相同数据以及非父子进程的通信怎么做呢我们可以仿照上面为父子进程创建一个struct file占用一个文件描述符表但是没有必要打开磁盘中的文件因为只是进程间通信用没必要刷新到磁盘所以直接分配文件描述符即可读写内存中的文件缓冲区所以不需要通过路径文件名读磁盘文件因为没有路文件名所以是匿名管道接着我们要实现管道以子w父r为例具体谁读谁写由通信场景决定为父进程创建一个struct file为该结构体对象分配两个文件描述符假设为3和43是用于读4是用于写都指向同一个文件缓冲区因为子进程会拷贝父进程的files_struct中的文件描述符表一个要写一个要读就要求父进程以读和写两种方式也就是分配两个文件描述符给同一个文件缓冲区这样子进程继承之后父子进程根据需求关闭不需要的一端创建子进程子进程会继承父进程的文件描述符表接下来把父进程用于写的fd4关了子进程用于r的fd3关了子进程只能通过fd4向文件缓冲区写入父进程只能通过fd3从文件缓冲区读那父进程的fd4子进程的fd3不关会怎么样呢可能会误写/误读比如子进程刚写子进程就读了父进程啥也没读到导致操作出现错误浪费资源关掉用不到的文件描述符可以用于其他文件因此打开同一个文件仿照文件设计了一套通信标准因为管道就是仿照文件系统所以功能上做的比较简单就是单向通信一般管道文件是4K8或者8/16KB具体和OS有关可以看作队列先进先出一个进程写字符另一个进程读操作系统提供了系统调用pipeopen打开的是磁盘文件需要输路径而pipe是创建struct file因此不是打开一个已经存在的文件因此没有说打开但是关闭文件描述符都是close给匿名管道文件inode和文件缓冲区的目的是对内存页进行读写复用文件系统操作下面的进程A和B就是通信的父子进程所以管道操作就是文件操作把管道看作文件就是Linux下一切皆文件的思想·address_apce是用文件偏移量把文件内容以radix基数树的形式组织起来所以一个进程分别以读和写操作struct file对应的文件缓冲区本质是f_mapping浅拷贝接下来写代码验证以子w父r为例1.创建管道文件父进程以读和写两种方式分配两个文件描述符给同一个文件缓冲区/inode2.fork创建子进程继承父进程的文件描述符表3.父子进程分别关闭不需要的读/写端父关w子关r4.子w父rCtrl~ 唤起命令行在命令行选中单击右键可以进行复制Ctrl /- 放大或者放小字体Ctrl/ 对选中的代码进行批量注释或者取消在VS或者VS code下CtrlC复制鼠标所在行不需要选中CtrlV进行粘贴实验一 创建一个空文件进行写入之后读实验二 打开上面写入hello的文件读两次而read最多读count字节是说一方面如果buf太小或者文件内容过少比如文件只有5B的内容而count为10或者buf只有5B大小count为10那两种情况都是只能拷贝5B一是地方不够二是内容不够当然也有系统的限制比如count大于某一个值等write也是一样如果给的buf大小比count小包括系统的限制文件大小的限制2.4.1.2 特性单工通信如果要父子都能发信需要两次pipe是全双工匿名管道只能用于有血缘关系的进程进行通信最常用的父子通信原理是继承内核资源管道文件是面向字节流的管道不care用户写的是字符还是二进制程序还是视频、音频等管道只关心写了多少字节而比如说写x次写入的数据读取y次读完y与x的关系可能或者或者而如果必须那是数据报字节流的读取是按需读取语言级、内核级缓冲区都是面向字节流xx流都是上述特征比如语言级缓冲区刷新到文件文件内核缓冲区刷新到磁盘叫文件流管道的生命周期是随进程的管道的struct file中f_count(也就是有多少个进程指向这个struct file比如上面测试父进程的pipefd[0]和子进程的pipefd[1]存储的都是管道struct file结构体对象的地址所以f_count为2每关闭一个f_count就–直到减为0struct file就会被OS回收区分struct file和inodestruct file是OS维护的是指有多少进程中fd_array[x]file有多少f_count就为几随着进程结束struct files_struct被系统回收f_count–直到减为0回收struct file而inode是磁盘层面的概念如果文件打开就加载文件的inode到内存修改的是内存中的inode由OS将修改内容写回到磁盘但不完全是因为管道的inode从始至终只在内存是指有多少目录文件下包含文件名和inode的映射关系也就是inode中i_nlink硬链接数如果目录文件下删除文件也就是删掉目录文件中文件名和inode的映射关系则i_nlink- -直到inode的i_nlink为0该inode被系统置为可用及文件占用的磁盘数据块也可以被其它文件占用structinode{structhlist_nodei_hash;structlist_headi_list;structlist_headi_sb_list;structlist_headi_dentry;unsignedlongi_ino;atomic_t i_count;unsignedinti_nlink;//...};管道通信对于多进程而言自带同步机制访问数据具有一定的顺序性如果要实现互斥需要互斥锁或信号量实验一父进程可以创建多个子进程多个子进程就可以通信下面需要两个管道首先父进程创建管道P1用于sleep 2000和sleep 3000通信得到读端fd[3]和写端fd[4]接着创建管道P2用于sleep 3000和sleep 4000通信得到读端fd[5]和写端fd[6]sleep 2000的stdout(fd[1])重定向到fd[4]关闭fd[3]、fd[5]和fd[6]sleep 3000的stdin重定向到fd[3]关闭fd[4]和fd[5]stdout重定向到fd[6]sleep 4000的stdin重定向到fd[5]关闭fd[3]、fd[4]和fd[6]以此类推由于子进程可以继承父进程的文件描述符表所以父进程在fork之前创建的管道其所有子孙进程都可以通过继承来的fd进行通信只要不主动关闭实验二面向字节流可以看到每次读取的时候是把文件中的所有字节读出来有多少就读多少实验三每次让父进程sleep(10)再写同步子进程只有在父进程写入之后才能读取在此之前一直阻塞在写入小于PIPE_BUF时保证写入的原子性也就是写入不会出现字节级交错在写入数据小于等于PIPE_BUF时没有读端可以看到已写入的部分数据其它写端也没办法写入但这不是互斥互斥是指每次只有一个进程可以使用资源通过加锁等方式但是如果写入数据PIPE_BUF时不保证原子性管道的本质是让不同进程看到同一份资源OS通过文件实现准确说是文件的内核缓冲区本质是内存块属于父子进程共享的资源就会存在并发访问但并不互斥下面可以看到程序运行结果是不固定的取决于调度器说明并不是互斥如果互斥会强制串行结果一定是AAAABBBB2.4.1.3 四种情况2.4.1.3.1 写快读慢管道被写满后write所在进程就阻塞read所在进程每次读到缓冲区通过观察发现在read5次后父进程继续写入再read4次后父进程继续写入就是缓冲区有空的话就会唤醒写进程从参数也可以看出来buf是void不关注数据类型outbuffer在父进程的栈上虚拟地址空间read本质是把内核中的数据拷贝到栈上2.4.1.3.2 写的慢读得快read所在进程就阻塞本质是没有读到内容但是有写端就阻塞read所在进程等write所在进程慢慢写入等到缓冲区有数据了就唤醒read所在进程可以读了和调用scanf但是键盘没有输入scanf所在进程阻塞一个原理键盘也被抽象为文件当然已经读到末尾再读没有意义上面是为了对比试验下面对n0做了处理如果先close(pipefd[1])子进程那关闭写端父进程读完再读就是空直接打印read end of file之后子进程打印quit但如果子进程先打印quit那就是父进程后打印2.4.1.3.3 关了写端下面其实是子进程写了一条接着关闭了写端读端读完管道中数据再读类似于读到文件末尾后续子进程每次read返回值都是0一直while(1)死循环2.4.1.3.4 关了读端我们看到没有读端子进程再写就被OS干掉了因为管道就是为了进行进程间通信都没读端了你写啥写占着内存CPUOS直接干掉进程通过查询可以看到退出信号是13是SIGPIPE管道破裂被OS杀死一般exit_sig非零都是被杀掉的上面两种情况对比着看OS允许没了写端继续读但是不允许没了读端继续写2.4.1.4 应用场景进程池池化技术是对资源的预先创建进程池一个进程控制多个进程通过向管道中写入不同的数字来控制通信子进程要完成的任务就像stl中的内存池先申请大量空间接着用户使用的时候不需要频繁系统调用直接从池中拿就行先创建一堆进程接着有任务的时候就分配进程去做任务子进程在read处阻塞父进程向管道写入数字几对应子进程被唤醒读到之后直接执行对应的任务本质是提高效率在有任务的时候不再创建进程我们创建多个进程如果每次都选某一个进程就会导致其它进程空闲但还是忙不过来所以要负载均衡进程调度的方式1.轮询2.随机3.权重出现第二种情况是因为我们先来分析首先第一次pipe打开的是父进程的3和4文件描述符父w子r子进程关4父进程关3接着父进程创建第二个子进程pipe打开3和5但是此时父进程fd_array[4]是指向第一个管道w端所以fork创建第二个子进程子进程会拷贝父进程的文件描述符表父进程关3子进程关5此时子进程的3指向第二个管道的r端但是4指向第一个管道的w端所以此时父进程的4和第二个子进程的4都指向第一个管道的w端当最后父进程关闭写端但是二号进程的4还指向第一个管道的w端第一个子进程读完管道但是还有写端就会在read阻塞所以一直回收不了继续找规律那么创建第三个子进程的时候父进程的4和5都会被拷贝所以子进程3的fd_array[4]指向第一个管道的w端fd_array[5]指向第二个管道的w端或者可以这么说在子进程1后面创建的进程都会拷贝父进程的fd_arrray[4]所以假设有X个子进程那么有X-1个子进程的写端指向第一个管道的w端再加上父进程的fd_arrray[4]一共是X个进程指向写端第一个子进程只有3被占用第二个子进程3是第二个管道的读端4是管道1的w端…第X个子进程3是管道X的r端4是管道1的w端…X2是管道X-1的w端那么我们应该怎么做我们可以看到最后一个进程对应管道只有父进程指向其w端可以先关最后一个进程的w端先回收最后一个子进程之后倒数第二个进程的w端只会被倒数第一个进程继承所以倒数第一个进程退出后倒数第二个进程的管道w端只有父进程指向父进程关闭w端子进程退出以此类推本质是最后一个进程先退接着依次往前退口说无凭下图是通过查看/proc/进程pid/fd -l详细信息结合权限位可以看到确实如此其实上面是专病专治但是我们可以从源头上解决问因为父进程创建子进程谁先被调度取决于调度算法也就是说可能父进程会先被调度那就会出现父进程先把此次打开的w端创建的Channel插入到channels子进程会不会因此把自己管道的w端也关了呢不会因为子进程拷贝父进程的资源发生在fork的时候之后父子进程各自写入就会引发写时拷贝互不影响包括子进程关闭的这些写端也不会影响父进程C/C混编时stdlib.h变成 cstdlib但是unistd.h还是.h因为后者属于系统调用的头文件不属于C语言库函数原理首先进程间通信的本质是让通信的进程看到同一份资源匿名管道通过子进程继承父进程资源的方式来实现而命名管道依然是对文件系统的复用本质还是依赖文件进行让要通信的进程打开同一个文件这样就能看到同一个文件缓冲区通过读写进行通信但是文件相关的inode操作表文件缓冲区只会加载一份到内存但是和打开文件不同的是管道文件的内容无需同步加载到内存而命名管道顾名思义是有名称的相较于匿名管道也就是子进程继承父进程资源后父子进程根据应用场景分别关闭r和w端完成通信也就是由OS创建struct file结构体等内核数据结构但在磁盘上不存在这个文件所以没有名字称为匿名对于命名管道打开的是磁盘上的文件有路径文件名唯一标识所以有名字称为命名2.4.2 命名管道2.4.2.1 原理对于命名管道和匿名管道的区别之一·是可以用于不具有亲缘关系进程之间的通信而且生命周期不随进程创建命名管道之后需要unlink删除1.单工通信2.面向字节流3.自带同步机制通信数据容量PIPE_BUF时数据的写入具有原子性不会出现字节级交错而对于可能出现的四种情况1.读慢写快直到写满write所在进程就会在write阻塞等待read所在进程进行读取直到可以写入2.写慢读快直到读完read所在进程会在read处阻塞等待write所在进程进行写入直到有内容可读3.关闭读端写端还写会被OS杀掉4.关闭写端读端读完管道中的数据接着读每次读到的都是空但是OS不会主动处理读端所在进程通过mkfifo可以创建管道文件可以看到命名管道的大小是0B有自己的inode文件类型是p也就是pipe管道文件执行echo “aaaaaa” pipe1的时候终端1阻塞因为此时没有读端在终端2cat pipe1把pipe1的内容重定向到显示器之后终端1不再阻塞进程结束终端2打印aaaaaa2.4.2.2 应用场景2.4.2.2.1 Client和Server通信默认make只会执行第一条使用伪目标.hpp和.cc一般用于开源项目库文件本质是为了闭源源代码也就是方法实现直接编译成.o打包成库只给使用者库头文件这样程序员可以使用但是看不到方法实现而.hpp是方法和声明都在一个文件.h和.cpp的结合既可以直接提供.hpp(header only)也可以.hpp库本质还是要编译出.o然而如果在程序执行前pipe就已经存在了就没有必要创建了直接使用所以需要一个函数来判断管道是否存在可以使用open打开文件如果已经存在返回文件描述符0如果不存在返回-1或者用stat如果pipe已经存在stat有创建就有删除先启动ServerServer卡住了卡在了Open里的open假设此时open没有卡住Server在Recv的时候直接n0并且没有写端那读完管道内容就break结束了那这不符合逻辑正常情况应该是写端所在进程确实启动了不想写了关闭写端这时候读端把管道文件内容也读完了返回值是0读端进程也结束所以OS为了区分是写端进程还没启动还是写端写完不想写了这两种情况所以就在open的时候规定当且仅当管道的读端和写端都open打开管道文件时才往后走否则任何一端先open都会阻塞在open处这也是为什么IsExist并没有用open的原因之后Client每Send一条内容OS就把内容从栈上拷贝到pipe的文件缓冲区中再从文件缓冲区中读到Server的栈上通过CtrlC结束Client进程此时Server读完管道文件内容就退出了总体思路Client是写Server是读Server先Bulid管道文件接着通过Open以读的方式打开Pipe类有返回的文件描述符用于写入和读取因为是同一个目录下的同一个文件所以Client也能获取到管道文件接着Client以w的方式打开Client从键盘读入Send给ServerServer打印Recv的内容Client不再发了Server也不读了接着Delete关闭管道文件有点像社交软件的聊天框但是还差的远至少加上网络2.4.2.2.2 拷贝文件实现文件拷贝先运行Copy会阻塞运行Paste两者很快结束有个bugPaste用完管道应该unlink(“./pipe”)不用就删了它牛刀小试以下描述正确的有A.进程之间可以直接通过地址访问进行相互通信B.进程之间不可以直接通过地址访问进行相互通信C.所有的进程间通信都是通过内核中的缓冲区实现的D.以上都是错误的B以下选项属于进程间通信的是【多选】A.管道B.套接字C.内存D.消息队列ABD下列关于管道(Pipe)通信的叙述中正确的是A.一个管道可以实现双向数据传输B.管道的容量仅受磁盘容量大小限制C.进程对管道进行读操作和写操作都可能被阻塞D.一个管道只能有一个读进程或一个写进程对其操作C以下关于管道的描述中正确的是【多选】A.匿名管道可以用于任意进程间通信B.匿名管道只能用于具有亲缘关系的进程间通信C.在创建子进程之后也可以通过创建匿名管道实现父子进程间通信D.必须在创建子进程之前创建匿名管道才能实现父子进程间通信BD以下关于管道的描述中错误的是【多选】A.可以通过int pipe(int pipefd[2])接口创建匿名管道其中pipefd[0]用于从管道中读取数据B.可以通过int pipe(int pipefd[2])接口创建匿名管道其中pipefd[0]用于向管道中写入数据C.若在所有进程中将管道的写端关闭则从管道中读取数据时会返回-1D.管道的本质是内核中的一块缓冲区BC以下关于管道描述正确的有A.命名管道可以用于同一主机上的任意进程间通信B.向命名管道中写入的数据越多则管道文件越大C.若以只读的方式打开命名管道时则打开操作会报错D.命名管道可以实现双向通信A以下关于管道描述正确的有A.命名管道和匿名管道的区别在于命名管道是通过普通文件实现的B.命名管道在磁盘空间足够的情况下可以持续写入数据C.多个进程在通过管道通信时删除管道文件则无法继续通信D.命名管道的本质和匿名管道的本质相同都是内核中的一块缓冲区D