进程本地通信
目录本地通信的本质匿名管道基本原理相关系统调用匿名管道特点与shell关联起来进程池的应用命名管道SystemV IPC-共享内存共享内存特点SystemV IPC-消息队列消息队列特点SystemV IPC-信号量信号量特点SystemV IPC之间的联系shell查询IPC的命令本地通信的本质同一台主机同一个OS不同进程之间的通信本质是让不同的进程看到同一份资源。匿名管道基本原理在父进程中分别以读和写两种方式打开同一份文件。子进程继承打开的文件后关闭父进程以读方式打开的文件关闭子进程以写方式打开的文件这样父进程只能写子进程只能读实现单向通信。如果不关闭不需要的fd不会报错但是一来会导致读写混乱二来会浪费fd。匿名管道的通信原理大致就是这样子但是这里要澄清两点使用系统提供给我们的pipe调用来实现这种通信机制的话文件相关的内核级文件缓冲区不会也没必要和磁盘交互因为没有涉及到具体的文件只是利用了OS文件管理的一部分机制来实现父子进程看到同一份资源。也是因此我们打开的文件没有名字所以叫匿名管道当然如果你用手搓的方式实现的话那就另说了。一般是以读和写方式分别打开文件一次有两个struct_file而不是以读写方式只打开一次文件只有一个struct_file。相关系统调用用系统调用pipe打开的管道文件的内核级缓冲区不和磁盘有联系是纯内存级的缓冲区匿名管道特点与shell关联起来当我们执行类似与这样的命令ps -ajx | grep a | grep abshell会做如下处理分析字符串“ps -ajx | grep a | grep ab”发现有两个 “|”因此创建两个管道然后依次创建三个子进程第一个子进程的输出重定向为管道1并执行ps -ajx命令第二个子进程的输入重定向为管道1输出重定向为管道2并执行grep a 命令第三个子进程的输入重定向为管道2输出仍旧是显示器文件不变grep内部首先是read函数这样就可以预见我们执行了一串命令并把输出结果依次传递给后面的命令再由做后一个命令输出到显示器这样就实现了类似于过滤输出的效果这就是shell中使用“|”的本质下面是一个demo代码// 伪代码 function execute_pipeline(commands[]): int pipe_count len(commands) - 1 int pipes[pipe_count][2] // 每个管道有两个文件描述符 // 1. 创建所有管道 for i 0 to pipe_count-1: pipe(pipes[i]) // 2. 创建并执行每个命令 for i 0 to len(commands)-1: pid fork() if pid 0: // 子进程 // 设置输入除了第一个命令 if i 0: dup2(pipes[i-1][0], STDIN_FILENO) // 从前一个管道读 // 设置输出除了最后一个命令 if i len(commands)-1: dup2(pipes[i][1], STDOUT_FILENO) // 向后一个管道写 // 关闭所有管道描述符子进程不需要 for j 0 to pipe_count-1: close(pipes[j][0]) close(pipes[j][1]) // 执行命令 exec(commands[i]) exit(1) // exec 失败 else: // 父进程 // 继续创建下一个子进程 // 3. 父进程关闭所有管道描述符 for i 0 to pipe_count-1: close(pipes[i][0]) close(pipes[i][1]) // 4. 等待所有子进程结束 for i 0 to len(commands)-1: wait()进程池的应用创建多个子进程的同时为父进程与每个子进程建立一个管道并用例如vector数据结构管理所有的管道。每个子进程都在read函数阻塞等待命令。命名管道用系统接口 mkfifo 函数创建一个特殊的管道文件他是真实存在的文件唯一不同于其他文件的是它的文件类型是P并且OS规定只要是P类型的文件其内核缓冲区不会与磁盘进行交互。如此可以在不同的进程中用正常方式打开这个文件并读写实现通信。命名管道特点可以用于在无血缘关系的进程间进行通信以及匿名管道的所有特点SystemV IPC-共享内存由进程发起请求再由操作系统开创一块内存空间要通信的进程把这块内存空间挂接到自己的虚拟地址的共享空间上这样通信双方共享一块内存一但一个进程对内存写入数据另一个进程就可获知。共享内存可能会有很多进程要通信就要根据标识符找到对应的共享内存因此每个共享内存都有一个管理它的结构体里面包含了这块共享内存的标识符等信息OS也把所有的共享内存结构体组织起来统一管理先描述再组织。共享内存的标识符是由用户指定的因为如果由操作系统分配那么只有创建共享内存的进程可以拿到标识符而其他想要使用共享内存的进程是拿不到这个标识符的。不过系统也提供了一个接口给我们生成一个标识符下面介绍创建/获取共享内存的函数创建共享内存时候还需要把它挂载到进程的共享区两个进程都挂载上它们就可以互相通信了共享内存在语义上不属于某个进程因此进程退出他也不会销毁我们需要主动对其进行销毁也可以通过shell命令对共享内存进行销毁共享内存特点通信最速度快不调用系统调用并且拷贝次数比较少因为我们可以直接往共享内存写入而不是先拷贝给用户再拷贝给内核支持大块数据传递而管道最大64KB支持双向通信没有保护机制生命周期随内核不是随进程SystemV IPC-消息队列内核创建一个队列通信的双方通过在队列中添加节点和获取节点来实现通信。节点中有id属性不同进程添加节点时设置为自己的id这样可以区分数据是谁发出的。消息队列特点生命周期随内核本质上是创建了一个队列以存放发送的结构体消息SystemV IPC-信号量信号量可以表示一种资源的数量也可以用于进程间同步类似于“互斥同步”一节中的POSIX信号量的用法实际上信号量是一种进行资源预定的计数器他本身是共享资源用于进程间通知因此也算是一种本地通信方式信号量特点生命周期随内核不以传送数据为目的而是以通知或记录资源数目为目的count为2就是二元信号量可以充当一种简单的锁SystemV IPC之间的联系管理这三种通信的内核结构中都含有一个相同的结构体kern_ipc_perm且他们都是各自内核结构的第一个元素里面包含着他们共有的类型的信息编号权限拥有者等。通过kern_ipc_perm系统用一个柔性数组把所有类型所有数量的kern_ipc_perm管理在一个数组如果要访问的是完整的IPC结构只要把kern_ipc_perm类型转换成对应的内核指针类型即可实现对System V IPC 通信的同一管理多态。其中柔性数组的下标就是用户拿到的shmid等id号。这个柔性数组和它的大小也就是申请通信的数量组成的结构体ipc_id_ary本身被管理在ipc_ids中。而系统中有三个ipc_ids管理的是同一个柔性数组这样访问通信的时候只要进入对应的ipc_ids就天然的知道自己要访问的是哪种通信。由于所有IPC都被统一管理key和id当然也是统一的也就是说不同的通信也可能发生id或者key冲突。事实上共享内存就是文件缓冲区只不过把文件缓冲区映映射到了虚拟地址空间上因此可以通过地址直接访问匿名管道没有映射只能通过系统调用访问而共享内存直接在用户态中用指针访问shell查询IPC的命令