为什么会有进程间通信?
每一个进程都是拥有自己独立的虚拟地址空间和页表结构,促使了进程独立,同时带来了进程和进程之间相互协作之间的问题。所以就引入了进程间通信(最大的进程间通信叫做网络)。所以进程间通信可以理解为进程与进程之间交换数据。
学习进程间通信我们要了=了解掌握以下知识管道(数据传输)共享内存(数据共享)消息队列(数据传输)信号量(进程控制)
1.管道
管道:管道就是内核当中的一块内存,相当于内核为进程间通信创建的缓冲区
管道分为匿名管道和命名管道
匿名管道
1.创建匿名管道的接口 int pipe(int fd[2])
fd[2]:具有两个元素的整形数组数组当中的每一个元素都是一个文件描述符,fd[2】是一个出参,内核返回给用户两个文件描述符
fd[0]表示读端,操作fd[0]可以对匿名管道进行读
fd[1]表示写端,操作fd[1]可以对匿名管道进行写
返回值:成功返回0 失败返回-1
2.匿名管道的特性
(1)因为内核开辟的缓冲区并没有标识,所以只能用于具有亲缘关系的进程之间
(2)管道的数据流向是一个方向,数据流向只能从写端到读端,(管道是一个半双工)
(3)提供字节流服务,如果读端没有及时读走管道当中的数据,而写端写了很多次,每次写的数据之间是没有明确的数据边界;读端读的时候,可以一次性读走,也可以按照自己的想法读任意大小的数据
读:read(int fd,buf,size)读任意的字符
写:write(int fd,buf,size)往管道当中写,只要管道当中的容量空间足够,则可以写入
(4)管道的大小:PIPE_SIZE:64K
当读端不读,写端一直写的时候,当管道写满,写端就会进行阻塞
当写端不写,读端一直读的情况下,当管道为空的时候,读端就会进行阻塞
(5)创建匿名管道返回的文件描述符属性默认是阻塞的
(6)可以设置文件描述符的属性为非阻塞的 int fcntl(int fd,int cmd,…);
fd:文件描述符
cmd:想让fcntl函数执行什么操作 F_GETFL:获取当前文件描述符属性 F_SETFL:设置文件描述符的属性为非阻塞
…:可变参数列表:可以传递要改变文件描述符的属性数据
返回值:返回文件描述符的属性(使用的是位图的方式)
设置非阻塞属性的时候只需要给原来的属性按位或上O_NONBLOCK即可
(6)创建一个匿名管道,更改对应对写端的文件描述符为非阻塞属性
a.不进行读,但一直去写,设置写端的文件描述符为非阻塞属性,不用设置读端文件描述符,因为没有用到
读端不关闭,写端一直写,write会返回-1,报错当前资源不可用
读端直接关闭掉,写端还一直写,当前进程收到了SIGPIPE信号,写端程序被杀死,管道破裂
b。不写,但是一直进行读,只需要将读端设置为非阻塞,写端可以不用关心,因为没有用到写端
写端不关闭,读端进行读,read调用返回-1,报错当前资源不可用
所有写端关闭,读端进行读,read是正常调用的,read返回的是独到的字节数量
(7)PIPE_BUF:4K当我们读写的数据小于PIPE_BUF的时候,保证读写的原子性(当前操作不可被打断,当前操作要么完成了,要买没完成,不存在完成一半的情况)
(8)临界资源:同一时间时刻,当前的资源是能被一个进程所访问,如果多个进程同时去修改临界资源,可能会导致数据二义性
那么如何保证对临界资源的合理访问,不会造成二义性呢?
需要互斥与同步结合起来。互斥:同一时间,保证只能有一个进程访问临界资源。同步:保证了进程对临界资源访问的合理性。
命名管道
命名管道具有标识符,内核创建的内存是有标识符的,不同的进程可以通过标识访问命名管道。
1.如何创建一个命名管道?
使用命令创建: mkfifo [命名管道的文件名称] 文件类型是p,p为管道文件
使用函数创建:int mkfifo(const char* pathname,mode_t mode)
pathname:带路径的命名管道文件名称
mode:权限
2.用户可以通过操作命名管道文件来对内核当中的命名管道的内存区域进行读写操作
3.命名管道的特性
在匿名管道的基础上添加了具有标识符的特性,使它可以满足不同进程之间的进程间通信。
但不管是匿名管道还是命名管道,生命周期都是跟随进程的。
共享内存
共享内存的原理:
1.先在物理内存当中开辟一段空间
2.各个进程通过页表结构将物理内存映射到自己的虚拟地址空间当中的共享区
3.各个进程之间的通信是通过修改自己的虚拟地址空间当中的共享区的地址
2.共享内存的特性
不同进程对共享内存区域读的时候,并不会抹除物理内存当中的值
3.共享内存的接口
(1)创建共享内存 int shmget(key_t key,size_t size,int shmflg)
key:共享内存的标识符
size:共享内存的大小
shmflag:IPC_CREAT:如果想要获取的共享内存不存在,则创建共享内存,如果共享内存存在,返回共享内存的操作句柄 IPC_EXCL | IPC_CREAT如果想要获取的共享内存存在,则报错,按位或上权限(权限可以使用八进制数字来进行传参)
(2)将进程附加到共享内存上 void* shmat(int shmid,const void* shmaddr,int shmflg)
shmid:共享内存的操作句柄(shmget的返回值)
shmaddr:需要将物理内存映射到虚拟地址当中的哪一个地址,一般情况下我们传NULL值,让操作系统帮我们默认分配地址
shmflg:注定共享内存的读写权限 0:可读可写 IPC_RDONLY:只读
(3)从共享内存当中分离进程 int shmdt(const void* shmaddr)
shmaddr:共享内存当中映射的虚拟地址的首地址(shmat的返回值)
(4)共享内存的销毁 int shmctl(int shmid,int cmd,struct shmid_ds* buf)
shmid:共享内存的操作句柄
cmd:shmctl函数执行的操作 a.销毁:IPC_RMID删除共享内存,标记共享内存为删除状态 b.IPC_STAT 获取共享内存的状态信息,需要搭配shmid_ds* buf使用 buf是一个出参,用来返回共享内存的信息状态,一般在使用的时候传入struct shmid_ds 结构体对象的地址
共享内存的生命周期是跟随操作系统的
这里总结一下:key(共享内存标识符)shmid(共享内存操作句柄)owner(所属者)perms(权限)bytes(共享内存的大小)nattch(附加进程的数量)status(共享内存状态)
4.如何删除一个有附加进程的共享内存?
操作系统内核的做法是:
1.将该共享内存的状态标记为dest(destroy),将共享内存标识设置为0x00000000,标识当前共享内存不能被其他进程附加,同时会释放共享内存
2.带来风险,如果还有进程附加在被删除的共享内存上,有可能访问到非法的内存,从而导致程序越界,崩溃掉
3.当附加的程序退出掉,操作系统内核也会随之将描述共享内存的结构体释放掉
消息队列
队列的特性:先进先出,底层实现就是内核当中创建的链表,链表保证先进先出的特性,在队列中每一个元素都有自己的类型,类型之间有一个优先级的概念
(1)int msgget(key_t key,int msgflg)
key:消息队列的标识符
msgflg:IPC_CREAT IPC_CREAT | IPCEXCL 按位或上权限
(2)int msgsnd(int msqid,const void* msgp,size_t msgsz,int msgflg)
msqid:消息队列的操作句柄
msgp:发送的数据
msgsz:发送数据的大小
msgflg:0 阻塞发送。当队列满的时候,则阻塞 IPC_NOWAIT非阻塞
(3)ssize_t msgrcv(int msqid,void* msgp,size_t msgsz,long msgtyp,int msgflg)
msgtyp:接收什么样类型的数据
0:任意数据类型都是可以接受的
大于0:则返回队列当中消息类型为msgtype的第一个消息
小于0:则返回队列当中消息类型小于等于msgtype绝对值的消息,如果说,这种消息的类型比较多,则返回最小的那个消息
(4)int msgctl(int msqid,int cmd,struct msqid_ds* buf)
cmd:IPC_STAT IPC_RMID
2.消息队列的特性
消息队列的生命周期是跟随内核的
消息队列可以进行双工通信,由于数据有明显的数据边界了,克服了管道当中无格式的字节流的缺点
System V版本信号量
本质:计数器+PCB等待队列
计数器:用来保存资源数量的 PCB等待队列:存放被阻塞的进程的PCB
如上图,理论上进程A加1了,进程B也加1了,对于shm_cout理论上的值为12,但是由于进程A在++的时候,操作被打断了导致shm_cout的值为11了,所以我们需要获取信号量,对信号量当中的计数器进行预减操作,如果计数器的值大于等于0,也就意味着资源是有空闲的,对计数器进行减一操作后,进程可以去获取资源,如果计数器的值是小于0,也就意味着资源部是空闲的,进程不可以访问资源,如果这个时候还需要访问资源,则将该进程的PCB放到PCB等待队列当中去
归还临界资源:将计数器当中的值进行加1操作,唤醒PCB等待队列当中的进程
如何保证互斥:只需要将信号量当中的计数器设置为1,也就意味着同一时刻只有一个进程可以访问到临界资源
进程信号
1.信号的基本概念
信号是一种软件中断,当一个进程收到一个信号的时候,信号就会打断当前进程
信号的种类:Linux操作系统当中有62个信号,前31个(1-31):不可靠信号,非实时信号,信号有可能丢失,后31个(34-64):可靠信号,信号不会丢失
2.信号的产生方式:
硬件产生:
ctrl+c:给进程发送SIGINT信号,这个信号会导致进程退出
ctrl+z:给前台进程发送SIGINT信号,让一个前台进程放到后台,并且状态为T(暂停状态)
ctrk+|:发送一个SIGQUIT信号
软件产生:
kill函数 abort函数 alarm函数
3.信号的注册
(1)非可靠信号
第一次注册:更改sig位图当中对应的比特位(就是将对应的比特位置为1)在sigqueue队列当中添加响应信号的处理节点。
再次注册:(背景:还没有来得及处理已经注册的非可靠信号,进程有收到了同样的非可靠信号)先判断对应信号的比特位是否为1,如果为1,则不会添加sigqueue节点到sigqueue队列当中去,会丢弃新的信号
(2)可靠信号
第一次注册:更改sig位图当中信号对应的比特位(就是将对应的比特位置为1)在sigqueue队列当中添加响应信号的处理节点
再次注册:1.判断信号对应的bite为是否为1,如果为1,则不修改比特位,如果为0,则修改为12.判断为比特位之后,还需要添加对对应的sigqueue节点到sigqueue队列当中去,不管sigqueue当中是否有相同可靠信号的sigqueue节点
小结:非可靠信号,当进程还没有处理非可靠信号的时候,非可靠信号又多次注册,但是不会添加到sigqueue节点。可靠信号,当进程还没有处理可靠信号的时候,可靠信号又多次注册,会依次添加sigqueue节点。当进程在处理信号的时候,会根据sigqueue队列当中的节点,来依次处理的;非可靠信号注册多次,只会处理一次,可靠信号注册多次,会处理多次。
4.信号的注销过程
非可靠信号的注销:将sig位图当中对应的信号比特位置为0,并将sigqueue队列当中对应信号的sigqueue节点进行出队操作(内核执行响应信号的处理方式)
可靠信号的注销:
(1)判断对应信号在sigqueue队列当中的节点数量。信号节点数据量等于1,将对应信号的比特位置为0,并将sigqueue队列当中对应信号的sigqueue节点进行出队操作。信号节点数据量大于1,就不能将对应信号的比特位置为0(也就意味着还是1)将对应信号的一个sigqueue节点进行出队操作
5.信号捕捉流程
信号都有哪些处理方式:
默认处理方式:SIG_DEL
忽略处理方式:SIG_IGN(不做任何事)
自定义处理方式:程序员自己定义的信号的处理函数 *typedef void(sighandler_t)(int);sighander_t signal(int signum,sighandlel_t handler)
signum:需要重新定义哪一个信号
handler:sighandlel_t接收的是没有返回值(void)有一个int类型的参数的函数地址 int参数,表示说哪一个信号出发了内核调用该自定义处理函数,传递给自定义信号,一个信号的值。
int sigaction(int signum,const struct sigaction act,struct sigaction oldact)**这个函数也是可以自定义信号的处理函数
signum:需要自定义的信号
struct sigaction{
void (*sa_handler)(int)//操作系统为每一个信号定义的默认调用函数
void ( *sa_sigaction)(int,siginfo_t *,void *)//这个也是函数指针 ,但这个函数指针一般是预留的
,需要配合sa_flags进行使用
sigset_t sa_mask;//当一个进程在处理信号的时候,也有可能收到新的信号,将新的信号放在sa_mask当中,sa_mask的类型sigset_t,其实就是一个sig位图
int sa_flags;//当sa_flags为SA_SIGINFO的时候,配合sa_sigaction进行使用
void (*sa_restorer)//预留信心}
默认情况下,在处理一个信号的时候,使用sa_hander当中保存的函数地址,当sa_flags等于SA_SIGINFO的时候,在处理一个信号的时候,使用sa_sigaction当中保存的函数地址
signal函数是直接更改函数指针,而sigaction是直接更改结构体的
这里需要注意9号信号和19号信号是不可以自定义信号处理函数的
6.信号的阻塞
进程处理信号的时候,会先判断bock位图当中对应的比特位是否为1,如果为1,则暂时不处理,但是信号注册还是可以正常注册的(信号的阻塞并不是说信号不能被注册,不影响信号更改penging位图和增加sigqueue节点)
如何让让某一个信号变成阻塞的:int sigpromask(int how,const sigset_t *set,sigset_t *oldset)
how:通过不同的宏定义让sigprocmask函数执行不同的逻辑 SIG_BLOCK设置信号阻塞 SIG_UNBLOCK设置信号为非阻塞 SIG_SETMASK设置新的block位图
set:要设置新的block位图
oldset:老的block’位图
7.竞态条件
多个执行流访问同一个资源的情况下,会对程序结果产生一个二义性的结果,称之为竞态条件
重入:多个执行流访问同一个资源
多重入:多个执行流访问同一个资源,不会对程序结果产生影响
不可重入:多个执行流访问同一个资源,会对程序结果产生二义性