1 进程间通讯概述因为进程有着独立性的特点所以进程间的通讯不能像线程那样方便所以为了满足进程间通讯才有这些方法的诞生1-1 进程间通信目的1.数据传输一个进程需要将它的数据发送给另一个进程2 资源共享多个进程之间共享同样的资源。3 通知事件一个进程需要向另一个或一组进程发送消息通知它它们发生了某种事件如进程终止时要通知父进程。4 进程控制有些进程希望完全控制另一个进程的执行如Debug进程此时控制进程希望能够拦截另⼀个进程的所有陷⼊和异常并能够及时知道它的状态改1-2 进程间通信发展1管道2 System V进程间通信3 POSIX进程间通信(这里是轻量级进程间通讯即进程在线程的文章中会详细讲解)1-3 进程间通信分类管道1 匿名管道pipe2 命名管道System V IPC1 System V 消息队列2 System V 共享内存(详细谈)3 System V 信号量POSIX IPC1 消息队列2 共享内存3 信号量4 互斥量5 条件变量6 读写锁1-4 什么是进程间通讯进程间通讯(IPC inter-Process Communication)是运行在同一台计算机或不同计算机上的多个进程间进行数据交换和通讯的技术这是因为每个进程都有自己的地址空间它们无法直接访问彼此的数据因此需要通过特定的机制来实现通讯1-5 进程间怎么通讯进程通讯的本质是先让不同的进程看到同一份资源(内存)。然后才有通讯的条件。通讯的资源不是由任何一个进程提供的而是由OS设置同一的接口提供的通过系统调用接口来体现。从上述的概述中我相信各位已经对于进程间通讯有什么和有什么用有了一个初步的了解但是在Linux多年的发展依以来有很多的进程间的通讯方式已经被不再被频繁使用而且有一些其实是在线程部分使用会更多点(为什么进程间通讯和线程有关我们放到我们线程部分再解答)所以此文只介绍一些常见和重要有意义的通讯。2 管道什么是管道管道就是Unix中最古老的进程间通讯的方式我们把从一个进程链接到另一个进程的一个数据流称为一个“管道”3匿名管道3-1 系统调用接口#include unistd.h 功能: 创建⼀⽆名管道 原型 int pipe(int fd[2]); 参数: fd⽂件描述符数组,其中fd[0]表⽰读端, fd[1]表⽰写端 返回值: 成功返回0失败返回错误代码demo:#include iostream #include unistd.h using namespace std; int main() { int fd[2]; pipe(fd); int pid fork(); if (pid 0) { close(fd[1]); while (true) { sleep(1); int buf 0; read(fd[0], buf, sizeof(buf)); cout 子进程通过管道受到父进程的数据 buf endl; } } int cnt 1; while (true) { close(fd[0]); write(fd[1], cnt, sizeof(cnt)); cnt; } return 0; }3-2 用 fork 来共享管道原理3-3 站在文件描述符角度-深度理解管道其实从这个原理就可以看到父子之间通过看到同一个资源就是基于文件系统特殊实现的管道文件因为文件自带文件缓冲区所以父子进程同时看到这个文件缓冲区从而实现通讯由此可以看出匿名管道是复用了文件系统的那一套操作并且配上单独的系统调用来实现的3-4 站在内核角度度-管道本质但是匿名管道的inode是不会保存该管道的名字的那我们是怎么保证两个进程打开的是同一个管道文件呢子进程会继承父进程文件描述符表这也在一定程度上体现了匿名管道的特性所以看待管道就如同看待文件⼀样管道的使用和文件⼀致迎合了“Linux一切皆文件思想”。注意如果递归创建多个子进程那么在关闭管道的时候要fd的引用计数问题因为子进程是拷贝的父进程的地址空间所以递归创建的时候进程的w端不只是有父进程指向也有可能被其他子进程指向所以此时关闭的时候你需要倒着关或者保证子进程的w端只能指向父进程3-6 管道读写规则1当没有数据可读时1-1 O_NONBLOCK disableread调用阻塞即进程暂停执行一直等到有数据来到为止。1-2 O_NONBLOCK enableread调用返回-1errno值为EAGAIN。2当管道满的时候2-1O_NONBLOCK disable write调用阻塞直到有进程读走数据2-2O_NONBLOCK enable调⽤返回-1errno值为EAGAIN3如果所有管道写端对应的文件描述符被关闭则read返回0表示文件结尾4如果所有管道读端对应的文件描述符被关闭则write操作会产生信号SIGPIPE,进而可能导致write进程退出否则一直写却没有人读会让这个写操作没有意义操作系统不做没有意义的事5当要写入的数据量不大于PIPE_BUF时Linux将保证写入的原子性。6当要写入的数据量大于PIPE_BUF时Linux将不再保证写入的原子性。3-7 管道特点1 只能用于具有共同祖先的进程具有亲缘关系的进程之间进行通信。通常一个管道由一个进程创建然后该进程调用 fork此后父、子进程之间就可应用该管道。2 管道提供流式服务自带同步机制(按照一定的顺序进行访问)所以才导致了管道的第 3 和第 4 条的管道特性3 一般而言进程退出管道释放所以管道的生命周期随进程4 一般而言内核会对管道操作进行同步与互斥5 管道是半双工的数据只能向一个方向流动需要双方通信时需要建立起两个管道注意1 任何一个时刻一个发一个收 --- 半双工2 任何一个时刻可以同时发收 --- 全双工3-8 管道的 4 种情况1 写满读快 ---读端就要阻塞(进程)2 写快读慢 --- 满了的时候写端就要阻塞3 写关继续读 --- read读到返回值为0表示文件结尾4 读关闭写继续 --- 写端写入没有任何意义OS不会做没有意义的事情 ---OS会杀掉进程4 命名管道管道应用的一个限制就是只能在具有共同祖先具有亲缘关系的进程间通信。 如果我们想在不相关的进程之间交换数据可以使用FIFO文件来做这项工作它经常被称为命名管道。命名管道是一种特殊类型的文件4-1 创建一个命名管道4-1-1 系统命令$ mkfifo filename4-1-2 系统调用#include sys/types.h #include sys/stat.h int mkfifo(const char *filename,mode_t mode); 参数 filename:管道文件的路径和名称 mode:创建管道文件的初始权限注意一定要是8进制输入否则会错乱 返回值 0创建成功 -1创建失败demo 1:#include iostream #include unistd.h #include sys/types.h #include sys/stat.h using namespace std; int main() { mkfifo(./pipe,0x666); return 0; }注意mkfifo最后一个参数设置的是文件初始权限一定要是0开头的3位数字即8进制数据传入否则就会权限错乱传入0666传入666这个时候pipe的文件权限就出现错乱了demo 2server#include iostream #include unistd.h #include sys/types.h #include sys/stat.h #include fcntl.h using namespace std; int main() { mkfifo(./pipe,0666); int fdopen(./pipe,O_WRONLY); int cnt 1; while(true) { sleep(1); write(fd,cnt,sizeof(cnt)); cout传输一个数据cntendl; cnt; } close(fd); return 0; }注意open的标志位不可以加O_TRUNC因为命名管道不支持而且也可以不加O_APPEND因为没有什么意义可以不加client#include iostream #include unistd.h #include sys/types.h #include sys/stat.h #include fcntl.h using namespace std; int main() { int fdopen(./pipe,O_RDONLY); int cnt 0; while(true) { sleep(1); read(fd,cnt,sizeof(cnt)); cout接收到一个数据cntendl; } close(fd); return 0; }4-2 理解命名管道命名管道是通过创建一个特殊的文件让这个文件的内容来作为进程间看到的同一份资源进行通信其实你会发现匿名管道和命名管道都是复用了文件系统来实现的只是他们一个是直接使用文件缓冲区不会创建磁盘文件但是命名管道是会创建磁盘文件的但是它们都不会将数据刷新到磁盘匿名管道是通过父子进程对于地址空间的拷贝找到同一个管道的那命名管道呢命名管道因为会在磁盘上创建文件并且它的属性中有它自己的名字所以这也可以复用文件系统路径唯一性来保证进程间访问的是同一个管道4-3匿名管道与命名管道的区别1 匿名管道由pipe函数创建并打开。2 命名管道由mkfifo函数创建打开用open3 FIFO命名管道与pipe匿名管道之间唯⼀的区别在它们创建与打开的方式不同一但这些工作完成之后它们具有相同的语义。4-3 命名管道的打开规则1 如果当前打开操作是为读而打开FIFO时1-1 O_NONBLOCK disable阻塞直到有相应进程为写而打开该FIFO1-2 O_NONBLOCK enable立刻返回成功2 如果当前打开操作是为写而打开FIFO时2-1 O_NONBLOCK disable阻塞直到有相应进程为读而打开该FIFO2-2 O_NONBLOCK enable立刻返回失败错误码为ENXIO4-4 命名管道的特性特性匿名管道一样但是有一点不同命名管道可以用来进行不相关的进程之间的通讯其他特性这里就不再赘述了。但是注意open再打开管道文件的时候都是默认阻塞的只有如果只有读写端一端开启的话就会阻塞再open只有都打开才会成功返回5 system V共享内存共享内存区是最快的IPC形式。一旦这样的内存映射到共享它的进程的地址空间这些进程间数据传递不再涉及到内核换句话说是进程不再通过执行进入内核的系统调用来传递彼此的数据5-1 什么是system Vsystem V是一种标准Linux内核支持了这种标准专门涉及了一个IPC通讯模块通信的接口设计和原理具有相似性。5-2 共享内存的原理因为IPC通信的本质是让不同的进程看见同一份资源而共享内存故名思意就是让两个进程看到同一个物理空间即共享内存5-3 理解共享内存1 这个物理内存通过平坦模式编址然后映射到各个进程的虚拟地址空间的用户共享区中所以我们访问这个共享的物理只需要通过使用虚拟地址就可以通过页表转换访问真实共享的物理内存2 开辟物理内存将物理内存映射到进程的虚拟地址空间中这些过程都是由OS来完成的就是系统调用我们用系统调用完成上述的操作3 当我们取消关联关系的时候由OS来释放内存4 可能同时存在由多组进程都在使用不同的共享内存来进行通信所以OS内有多个共享内存同时存在所以我们需要也需要通过先描述再组织这个方式来管理的。所以我们可以砍一下共享内存管理的数据结构来理解一下他5-4 共享内存的数据结构struct shmid_ds { struct ipc_perm shm_perm; /* operation perms */ int shm_segsz; /* size of segment(bytes) */ __kernel_time_t shm_atime; /* last attach time*/ __kernel_time_t shm_dtime; /* last detach time*/ __kernel_time_t shm_ctime; /* last change time*/ __kernel_ipc_pid_t shm_cpid; /* pid of creator */ __kernel_ipc_pid_t shm_lpid; /* pid of last operator */ unsigned short shm_nattch; /* no. of currentattaches */ unsigned short shm_unused; /* compatibility */ void shm_unused2; / ditto - used byDIPC */ void shm_unused3; / unused */ };5-5 共享内存函数shmget 函数功能⽤来创建共享内存 原型 int shmget(key_t key, size_t size, int shmflg); 参数 key:这个共享内存段名字通过ftok这个函数来获取 size:共享内存⼤⼩ shmflg:由九个权限标志构成它们的⽤法和创建⽂件时使⽤的mode模式标志是⼀样的 取值为IPC_CREAT共享内存不存在创建并返回共享内存已存在获取并返回。 取值为IPC_CREAT | IPC_EXCL共享内存不存在创建并返回共享内存已存在出错返回。 返回值成功返回⼀个⾮负整数即该共享内存段的标识码失败返回-1key值我们一般是通过ftok这个函数来让系统生成的。但是为什么要key呢因为不同的进程shm来进行通信所以需要标识内存的唯一性key来区分不是内核直接形成的而是在用户层构建并传入OS的。为什么要这样呢因为两个进程是相互独立的他们具有独立性这就意味着我们要访问同一个物理内存是需要标识但是由于独立性如果让OS来标识那么注定会有进程拿不到这个id所以让我们在用户层传入一个自定的key值然后两个进程传给操作系统相同的key经过运算shmget可以返回相同的id这样就让两个进程看到同一个物理内存了。这些都是共享内存函数中常见的标志位但是我们往往是组合使用的。IPC_CREAT创建共享内存如果目标共享内存不存在就创建否则打开这个已经存在的共享内存并且返回(这个进经常用来获取已经存在的目标共享内存)IPC_CREAT | IPC_EXCL | 权限位 :IPC_EXCL 单独使用往往没有意义所以经常是和IPC_CREAT一起使用的。如果你要创建的shm不存在就创建它如果已经存在shmget返回就会出错这个机制注定了用这个标志位shmget成功返回一定是一个全新的共享内存。因为共享内存也有权限并且在这个情况下只会返回全新的共享内存所以往往这些标志位都是和权限位例如0666 (0表示8进制详情请见上位对于权限位传入的理解)。所以这个参数常常是用来创建全新的共享内存的shmat函数功能将共享内存段连接到进程地址空间 原型 void *shmat(int shmid, const void *shmaddr, int shmflg); 参数 shmid: 共享内存标识 shmaddr:指定连接的地址如果不指定地址设为NULL就可以 shmflg:它的两个可能取值是SHM_RND和SHM_RDONLY,一般直接设置为0默认设置就可以了 返回值成功返回⼀个指针指向共享内存第⼀个节失败返回-1其实这个函数和malloc函数很像因为堆的使用其实和共享内存虚拟地址空间使用是类似的.因为读写共享内存并没有用系统调用共享区属于用户空间可以让用户直接使用说明shmaddr为NULL核心自动选择⼀个地址shmaddr不为NULL且shmflg无SHM_RND标记则以shmaddr为连接地址。shmaddr不为NULL且shmflg设置了SHM_RND标记则连接的地址会自动向下调整为SHMLBA的整数倍。公式shmaddr - (shmaddr % SHMLBA) shmflgSHM_RDONLY表示连接操作用来只读共享内存shmdt函数功能将共享内存段与当前进程脱离 原型 int shmdt(const void *shmaddr); 参数 shmaddr: 由shmat所返回的指针 返回值成功返回0失败返回-1 注意将共享内存段与当前进程脱离不等于删除共享内存段shmctl函数功能⽤于控制共享内存 原型 int shmctl(int shmid, int cmd, struct shmid_ds *buf); 参数 shmid:由shmget返回的共享内存标识码 cmd:将要采取的动作有三个可取值 buf:指向⼀个保存着共享内存的模式状态和访问权限的数据结构 返回值成功返回0失败返回-1注意进程结束了如果没有进行删除共享内存共享内存资源会一直存在因为共享内存的资源生命周期随内核如果没有显示的删除即使进程退出了IPC资源依旧不会被删除。5-6 共享内存的优缺点优点共享内存是进程间通信中 速度最快的方式1 映射之后读写直接被对方看到2 不需要进行系统调用获取或者写入内容缺点1 通信双方没有所谓的“同步机制”导致数据不一致2 共享内存没有保护机制