进程间通信
- 通信的本质:互相传递数据
- 进程间不能直接相互传递数据,因为进程具有独立性,所有的数据操作都会发生写时拷贝
- 进程间通信一定通过中间媒介(OS提供的内存空间)的方式来进行通信的
- 进程间通信的本质:让不同的进程能看到同一份系统资源(系统通过某种方式(方式是有差别的,决定了通信策略是有差异的)提供的系统内存)
进程间通信目的
- 数据传输:一个进程需要将它的数据发送给另一个进程
- 资源共享:多个进程之间共享同样的资源。
- 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
- 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变
进程间通信发展
- 管道
- System V进程间通信
- POSIX进程间通信
进程间通信分类
管道
- 匿名管道pipe
- 命名管道
System V IPC
- System V 消息队列
- System V 共享内存
- System V 信号量
POSIX IPC
- 消息队列
- 共享内存
- 信号量
- 互斥量
- 条件变量
- 读写锁
从一个进程连接到另一个进程的一个数据流称为一个“管道”
注:
- 管道只能进行单向数据通信
- 实现双向数据通信必须建立多个管道(由OS本身决定的)
- 并不是所有文件都能充当管道,管道是一种文件
补充:
-
管道是半双工通信
-
管道的本质是内核中的缓冲区
-
管道自带同步(没有数据读阻塞,缓冲区写满写阻塞)与互斥
-
多个进程只要能够访问同一管道就可以实现通信,不限于读写个数
-
管道的生命周期随进程,管道文件只是标识,删除后依然可以通信
匿名管道
- 匿名管道供具有血缘关系的进程,进行进程间通信(常见于父子进程)
#include 功能:创建一无名管道原型int pipe(int fd[2]);参数fd:文件描述符数组,其中fd[0]表示读端, fd[1]表示写端返回值:成功返回0,失败返回错误代码RETURN VALUE On success, zero is returned. On error, -1 is returned, and errno is set appropriately.
用fork来共享管道原理
注:
- fork创建子进程之后,子进程会继承父进程的大部分数据(包括父进程中的fd_array[]),此外父子进程的文件描述符指向同一个文件的r、w。
- 匿名管道(保证不同进程看到同一份资源),同一分资源指的是系统中fork之后父子进程所指向同一份文件(struct file)下的’系统级"文件缓冲区
- 父子进程关闭不需要的文件操作符,从而达到构建单向通信的管道的目的
- 父进程必须都要打开文件的读写权限,不打开读写,子进程拿到的文件打开方式必定和父进程一样,将无法通信
- 父子进程打开文件的读写权限之后,必须要关闭相应的其中一个权限,原因是防止误操作
- int pipe(int pipefd[2]);中的参数为输出型参数,拿到打开的管道文件描述符是2个fd,分别为read,write
站在文件描述符角度-深度理解管道
站在内核角度-管道本质
代码实列:
#include#include#include#include #include int main(){ int pipe_fd[2]={0}; if(pipe(pipe_fd)<0) { perror("pipe"); return 1; } printf("%d,%d\n",pipe_fd[0],pipe_fd[1]); pid_t id=fork(); if(id<0) { perror("fork"); return 2; } else if(id==0) { close(pipe_fd[0]); const char* msg="hello parent,i am child"; int count=5; while(count) { write(pipe_fd[1],msg,strlen(msg)); sleep(1); count--; } close(pipe_fd[1]); exit(0); } else { close(pipe_fd[1]); char buffer[64]; while(1) { ssize_t size=read(pipe_fd[0],buffer,sizeof(buffer)-1); if(size>0) { buffer[size]=0; printf("parent get messge from child# %s\n",buffer); } else if(size==0) { printf("pipe file close,child quit!\n"); break; } else { perror("read"); break; } } int status=0; if(waitpid(id,&status,0)>0) { printf("child quit,wait success!\n"); } close(pipe_fd[0]); } return 0;}
匿名管道特点
- 管道自带同步机制:
-
- 如果管道里面没有信息,父进程(读端)在等待,等管道内部有数据就绪(子进程写入)
-
- 如果管道里面写端已经写满了,不能继续写入,写端就必需等待,等管道内部有空闲空间(父进程读走)
- 管道是单向通信的
- 管道是面向字节流的
- 管道只能保证是具有血缘关系的进程间的进程通信,常用于父子
- 管道可以保证一定程度的数据读取的原子性
注意:读端从管道成功读取数据之后管道里面的数据会设置成无效
补充:
- 只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信;通常,一个管道由一个进程创建,然后该进程调用fork,此后父、子进程之间就可应用该管道。
- 管道提供流式服务
- 一般而言,进程退出,管道释放,所以管道的生命周期随进程
- 一般而言,内核会对管道操作进行同步与互斥
- 管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道
管道自带同步机制检验:
匿名管道读写规则
补充:
注:读取端关闭,一直写是毫无意义的,本质就是在浪费系统资源,写进程会立刻被OS终止掉(通过发送信号的方式),获取终止信号可以由waitpid获取
管道大小的检验:
结论:管道大小为64KB
注:用man 7 pipe查看PIPE_BUF
总结:
- 当没有数据可读时:
-
- O_NONBLOCK disable:read调用阻塞,即进程暂停执行,一直等到有数据来到为止。
-
- O_NONBLOCK enable:read调用返回-1,errno值为EAGAIN。
- 当管道满的时候:
-
- O_NONBLOCK disable: write调用阻塞,直到有进程读走数据
-
- O_NONBLOCK enable:调用返回-1,errno值为EAGAIN
- 如果所有管道写端对应的文件描述符被关闭,则read返回0
- 如果所有管道读端对应的文件描述符被关闭,则write操作会产生信号SIGPIPE,进而可能导致write进程退出
- 当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。
- 当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。
补充:
进程退出,曾经打开的文件也会被关掉。管道也是文件,管道的生命周期是随进程的
命名管道
- 不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,它经常被称为命名管道。
- 命名管道是一种特殊类型的文件
注:
- 普通文件是需要将数据刷新到磁盘的,持久化存储的
- 并不是所有的文件都能充当命名管道,命名管道是一种特殊的文件
- 命名管道并不会将写读的数据刷新到磁盘中的该文件里,只是将数据存在缓冲区里
创建一个命名管道
- 命令行中创建
mkfifo filename
- 程序里创建
int mkfifo(const char *filename,mode_t mode);
注:用man mkfifo查看命令行中的mkfifo;用man 3 mkfifo查看程序中的mkfifo
匿名管道与命名管道的区别:
- 匿名管道由pipe函数创建并打开。
- 命名管道由mkfifo函数创建,打开用open
- FIFO(命名管道)与pipe(匿名管道)之间唯一的区别在它们创建与打开的方式不同,一但这些工作完成之后,它们具有相同的语义。
总结:匿名管道与命名管道只是进程间的关系的问题(匿名管道进程间有血缘关系、命名管道进程间毫无关系),此外匿名管道有的管道特点命名管道也都有
命名管道的打开规则
- 如果当前打开操作是为读而打开FIFO时
-
- O_NONBLOCK disable:阻塞直到有相应进程为写而打开该FIFO
-
- O_NONBLOCK enable:立刻返回成功
- 如果当前打开操作是为写而打开FIFO时
-
- O_NONBLOCK disable:阻塞直到有相应进程为读而打开该FIFO
-
- O_NONBLOCK enable:立刻返回失败,错误码为ENXIO
用命名管道实现server&client通信
总结:
- 进程间通信的本质:
-
- 要先让不同的进程看到同一份资源(文件资源)
-
- 通信的过程
- 通信的方式:
-
- 匿名管道:父子共享文件的特征
-
- 命名管道:文件路径具有唯一性,让进程看到同一个文件
共享内存区是最快的IPC形式。一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到内核,换句话说是进程不再通过执行进入内核的系统调用来传递彼此的数据
共享内存
-
- OS申请一块物理内存空间
-
- OS将该内存映射进对应进程的共享区中(堆、栈之间)
-
- OS可以将映射之后的虚拟地址空间返回给用户
注:
- 申请是进程的需求,进程让OS申请的
- 操作系统内部提供了通信机制(IPC)——ipc模块
- OS内可以提供大量的共享内存(OS要对这些共享内存进行管理)
- system V共享内存达到通信的步骤:
- 申请共享内存
- 进程A和进程B分别挂接对应的共享内存到自己的地址空间(共享内存)
- 双方就看到了同一份资源,既可以进行正常通信了
注:上述的这些步骤,有对应的系统调用接口,给提供服务
共享内存函数
shmget函数
功能:用来创建共享内存原型 int shmget(key_t key, size_t size, int shmflg);参数 key:这个共享内存段名字 size:共享内存大小 shmflg:由九个权限标志构成,它们的用法和创建文件时使用的mode模式标志是一样的返回值:成功返回一个非负整数,即该共享内存段的标识码;失败返回-1
注:
- shmget中的参数size建议是4KB的倍数
- shmflg中的IPC_CREAT和IPC_EXCL 同时使用时,如果目标共享内存不存在,创建之;如果已经存在,则出错返回。如果此时调用shmget成功,一定得到的是全新的共享内存;否则返回-1
- 如果只使用shmflg中的IPC_CREAT,没有共享内存创建之;有就获取之
- 如果只使用shmflg中的IPC_EXCL 是没有任何意义的
- shmflg使用0默认是IPC_CREAT
- 多个进程如何看到同一块共享内存——通过key来进行唯一性区分的
- 通过使用ftok函数来保证AB进程获取的是同一个key值
补充:
注:nattch指的是与当前共享内存关联的进程的个数
补充:
注意:“dest”状态
Linux下删除任何内容,都会先检查一下这个内容的引用计数(就是文件的使用数,n个进程使用,引用计数为n)。若引用计数为0,就会真正的删除该内容(这里就是删除共享内存)。不为0,表示仍有进程使用,则正在使用的进程可以正常使用,直至引用计数降为0后,系统才会将该内容真正意义上的删除掉。
对这里用共享内存来说同理,显示“dest”是表示该共享内存已经被删除但有进程还在使用它。这时操作系统将共享内存的mode标记为SHM_DEST,key标记为0x00000000,并对外显示status为“dest”。当用户调用shmctl的IPC_RMID时,系统会先查看这个共享内存的引用计数,如果引用计数为0,就会销毁这段共享内存,否者设置这段内存的mod的mode位为SHM_DEST,如果所有进程都不用则删除这段共享内存。
- 释放ipc资源的方式:
-
- 用指令删除
-
- 进程退出时使用函数调用释放
-
- 操作系统重启
shmctl函数
功能:用于控制共享内存原型 int shmctl(int shmid, int cmd, struct shmid_ds *buf);参数 shmid:由shmget返回的共享内存标识码 cmd:将要采取的动作(有三个可取值) buf:指向一个保存着共享内存的模式状态和访问权限的数据结构返回值:成功返回0;失败返回-1
shmat函数
功能:将共享内存段连接到进程地址空间原型 void *shmat(int shmid, const void *shmaddr, int shmflg);参数 shmid: 共享内存标识 shmaddr:指定连接的地址 shmflg:它的两个可能取值是SHM_RND和SHM_RDONLY返回值:成功返回一个指针,指向共享内存第一个节;失败返回-1
说明:
- shmaddr为NULL,核心自动选择一个地址
- shmaddr不为NULL且shmflg无SHM_RND标记,则以shmaddr为连接地址。
- shmaddr不为NULL且shmflg设置了SHM_RND标记,则连接的地址会自动向下调整为SHMLBA的整数倍。公式:shmaddr - (shmaddr % SHMLBA)
- shmflg=SHM_RDONLY,表示连接操作用来只读共享内存
shmdt函数
功能:将共享内存段与当前进程脱离原型 int shmdt(const void *shmaddr);参数 shmaddr: 由shmat所返回的指针返回值:成功返回0;失败返回-1注意:将共享内存段与当前进程脱离不等于删除共享内存段
注:System V进程间通信不需要用系统调用函数read、write的方式来实现通信
system V共享内存的特点
总结:
- 共享内存的生命周期随操作系统(OS)(进程退出,共享内存不销毁)
- 共享内存不提供任何同步与互斥的操作,双方彼此独立(进程间通信不考虑读写端问题)
- 共享内存是所有的进程间通信中,速度最快的(因为进程间通信不需要使用read、write等函数调用,以及写时拷贝次数少(不需要拷贝到缓冲区))
补充:
- 共享内存的大小,系统在分配shm的时候,是按照4KB为基本单位的(下图中的共享内存大小为4097,在OS中分配为4096+4096其中4095大小的空间会浪费)
- key:是一个用户层的唯一键值,核心作用是为了区分“唯一性”,不能用来进行IPC资源的操作(key类比于inode号)
- shmid:是一个系统给我们返回的IPC资源标识符,用来进行操作IPC资源(shmid类比于文件的fd)
- 用make和makefile编译多个源文件
共享内存数据结构
struct shmid_ds { struct ipc_perm shm_perm; int shm_segsz; __kernel_time_t shm_atime; __kernel_time_t shm_dtime; __kernel_time_t shm_ctime; __kernel_ipc_pid_t shm_cpid; __kernel_ipc_pid_t shm_lpid; unsigned short shm_nattch; unsigned short shm_unused; void *shm_unused2; void *shm_unused3; };
注:
IPC资源在内核中是数组维护的(是通过类似于C++中的切片的方式用ipc_perm来找到共享内存、消息队列、信号量)
补充:
-
共享内存实现通信的原理是因为所有进程操作映射同一块物理内存
-
共享内存的操作是非进程安全的
-
共享内存只有在当前映射链接数为0时,才会真正被删除
-
共享内存生命周期随内核,只要不删除,就一直存在于内核中,除非重启系统
-
共享内存是将同一块物理内存映射到各个进程虚拟地址空间,可以直接通过虚拟地址访问,相较于其它方式少了两步内核态与用户态之间的数据拷贝因此速度最快
-
ipcs 查看进程间通信资源/ipcrm 删除进程间通信资源:
-
- -m 针对共享内存的操作
-
- -q 针对消息队列的操作
-
- -s 针对消息队列的操作
-
- -a 针对所有资源的操作
- 消息队列提供了一个从一个进程向另外一个进程发送一块数据的方法
- 每个数据块都被认为是有一个类型,接收者进程接收的数据块可以有不同的类型值
- 特性方面:IPC资源必须删除,否则不会自动清除,除非重启,所以system V IPC资源的生命周期随内核
消息队列数据结构
- 让进程看到同一份资源(内存空间)——临界资源
- 进程内的所有的代码,不是全部的代码都是访问临界资源,而是只有一部分在访问,可能造成数据不一致问题的是这部分少量的代码——临界区
- 为了避免数据不一致,保护临界资源,需要对临界区代码进行某种保护(互斥)
- 互斥:一部分空间任何时候,有且只有一个进程在进行访问,串行化的执行(eg:锁、二元信号量)
- 加锁和解锁是有对应的代码的。本质是对临界去进行加锁和解锁,完成互斥操作
- 进程互斥:
-
- 由于各进程要求共享资源,而且有些资源需要互斥使用,因此各进程间竞争使用这些资源,进程的这种关系为进程的互斥
-
- 系统中某些资源一次只允许一个进程使用,称这样的资源为临界资源或互斥资源。
-
- 在进程中涉及到互斥资源的程序段叫临界区
-
- 特性方面:IPC资源必须删除,否则不会自动清除,除非重启,所以system V IPC资源的生命周期随内核
信号量的数据结构
补充:
- 原子性(感性认识):要么做了,要么没做
- 信号量:本质是一个计数器。用来描述临界资源中资源数目的计数器
- PV原语:
- P操作:申请信号量(成功:一定有一个临界资源给你使用)
- V操作:释放信号量
- 多个信号量不能操作同一个count值(信号量不等于count)
- 信号量可以保护临界资源的安全性(操作计数器时是有多条语句构成,有可能多份配资源出去,不是原子性的)
- 信号量本身就是一个临界资源,要保护其他临界资源,先得保护自己的安全(PV操作是原子的)
- 信号量在多进程环境下,可以通过semget,semctl,ftok()等系统接口来保证信号量被多个进程看到
- 如果信号量的计数器的值是:1 (1、0:二元信号量(互斥语义))