进程间通信
操作系统提供的进程间通信方式,因为进程间具有独立性,无法直接通信,因此操作系统根据应用场景不同提供了不同的进程间通信方式。其中包含:管道 ,共享内存 ,消息对列 ,信号量
**1.**管道:就是内核中的一块缓冲区,多个进程通过访问同一个管道实现数据传输。
分类:匿名管道 / 命名管道
匿名管道:只能用于具有亲缘关系的进程间通信
内核中管道缓冲区没有标识符,其他进程无法获取,只能通过子进程复制父进程的方式获取到同一管道的操作句柄进行通信
int pipe(int pipefd[2]); pipefd[0]–用于从管道读取数据/ pipefd[1]–用于向管道写入数据
1.2 命名管道:可以用于同一主机上任意进程间通信
内核中管道缓冲区具有标识符,标识符是一个可见于文件系统的管道文件,所有进程通过打开同一管道文件访问同一管道实现通信
int mkfifo(char *filename, mode_t mode);
管道特性:
管道是半双工通信
管道生命周期随进程
管道自带同步与互斥
同步:通过一些条件判断,查看进程是否符合访问资源的条件,若不符合则让进程阻塞,符合的时候再唤醒,通过这种方式保证资源访问合理若管道中没有数据,则read会阻塞;管道中数据满了,则write会阻塞;
互斥:通过保证同一时间只有一个进程能访问临界资源,访问期间其他进程不能访问,通过这种方式保证资源访问的安全性
若管道操作数据的大小不超过PIPE_BUF大小,默认4096字节,保证操作原子性。
原子操作:指的是一个操作要不然不做,要不就一次性完成,中间不会被打断
管道提供字节流传输服务(面向连接的,有序的,以字节为最小传输单元的传输方式)
若管道所有读端被关闭,则继续write会触发异常;/若管道所有写端被关闭,则继续read读完数据将返回0,不再阻塞。
其他特性:命名管道的打开特性:只读打开则会阻塞直到管道被以写打开 / 只写打开则会阻塞直到管道被以读打开
2.共享内存:用于进程间的数据共享,最快的进程间通信方式
本质原理:本质上说就是一块物理内存,多个进程通过将同一块物理内存都映射到自己的虚拟地址空间,通过自己的虚拟地址进行访问,数据共享,共享内存创建以后,在内核中是有标识符的,多个进程可以通过同一个标识符打开相同的共享内存,进行映射。
共享内存直接通过虚拟地址访问共享内存的数据,不像管道以及其他方式那样需要将数据先拷贝到内核态,然后获取数据的时候再从内核态拷出,少了两次用户态与内核态的之间的数据拷贝,因此通信速度最快。
操作流程:
1.创建共享内存(开辟一块物理内存,并在内核中进行描述)
2.进程将共享内存映射到自己的虚拟地址空间,并且获取映射后的首地址。
3.对这块内存进行各式各样的内存操作
4.不使用共享内存了,则解除映射(只是解除共享内存与当前进程之间的关系)
5.删除共享内存(共享内存有映射计数,只有为0时才会真的删除)
接口介绍:
int shmget(key_t key,int size, int shmflag)-----开辟物理内存并且在内核中进行描述
key:共享内存的标识符,是一个整数;可以通过这个函数生成:key_t ftok(const char *pathname , int proj_id);
size:共享内存的大小
shmflag:IPC_CREAT----共享内存不存在则创建,存在则打开 / IPC_EXCL----与IPC_CREAT同时使用, 若共享内存存在则报错/
mode
mode:共享内存的访问权限–0664
返回值:共享内存在代码中的操作句柄,是一个数字,通过这个数字可以在内核中找到共享内存描述
void *shmat(int shmid, void *addr, int shmflag)
shmid: 第-步shmget接口返回的操作句柄
addr: 指定映射到虚拟地址空间的首地址,推荐置NuUL,由操作系统进行映射地址选择
shmflag: SHM RDONLU-只读/0-可读可写(前提对共享内存具有相应的操作校限)
返回值:映射到虚拟地址空间的首地址,通过这个首地址访问这块共享内存
int shmdt(void *shm_start);
shm. start:映射首地址,传入进来进行解除映射关系
int shmctl, (int shmid ,int cmd , struct shmid_ds *buf)
shmid:操作句柄
cmd:具体要进行操作IPC _RMID…删除共享内存
buf:用于设置/获取共享内存信息,用不到置NULL即可
特性:
共享内存生命周期随内核
共享内存是最快的进程间通信方式
注意事项:
共享内存并不自带同步与互斥, 他的操作需要程序员额外进行保护
ipcs -查看进程间通信资源
ipcrm -删除进程间通信资源
-m:共享内存 数据共享
-q:消息队列 数据传输
-s:信号量 进程间的同步与互斥
特性:
生命周期随内核
自带同步与互斥
信号量:主要用于实现进程间的同步与互斥
同步:通过条件判断决定进程是否能够获取资源,实现资源访问的合理性
互斥:通过同一时间只有一个进程能够访问资源,实现资源访问的合理性
本质:内核中的一个计数器+pcb队列,对资源进行计数,通过计数判断进程是否满足访问资源的条件,如果不满足则使进程阻塞,满足后再唤醒
实现同步的原理:通过自身的计数器对资源进行计数,在进程访问资源之前先访问信号量,通过计数判断是否是否符合访问条件同步并不保证安全
实现互斥的原理:保证自身计数为1,表示资源只有一份,只有一个进程能够获取,一个进程获取期间其他进程就需要等待,直到这个进程返还资源(计数+1),其他进程才能进行访问这样就能保证同一时间只有一个进程能够访问资源
如果进程不能访问资源则需要等待:将进程pcb设置为阻塞状态(可中断休眠状态),等到条件满足了,则再重新设置为运行状态
3.消息队列
消息队列:用于实现进程间的数据传输
实际是内核中的一 个优先级队列, 多个进程访问同一个队列, 通过添加节点和获取节点实现通信
特性:
生命周期随内核
自带同步与互斥
信号量:主要用于实现进程间的同步与互斥
同步:通过条件判断决定进程是否能够获取资源,实现资源访问的合理性
互斥:通过同一时间只有一个进程能够访问资源,实现资源访问的安全性
本质:内核中的一个计数器+pcb队列,对资源进行计数,通过计数判断进程是否满足访问资源的条件,如果不满足则使进程阻塞,满足后再唤醒
实现同步的原理:通过自身的计数器对资源进行计数,在进程访问资源之前先访问信号量,通过计数判断是否符合访问条件
同步并不保证安全
实现互斥的原理: 保证自身计数为1, 表示资源只有一份。只有一个进程能够获取,一个进程获取期间其它进程就需要等持,直到这个进程返还资源(计数+1),其它进程才能进行访问这样就能保证同一时间只有一个进程能够访问资源
如果进程不 的访问资源则需要等待:将进程pcb设置为相塞状态同中断休展状志) ,等到条件满足了,则两重新设置为运行状志
4.进程信号
信号就是一种中断,通知我们发生了某件事情,打断我们当前的操作,让我们去处理这个事情
信号能够同通知时间的发生,进程能够识别这个信号,并且信号与事件是一一对应的
信号的种类:在linux中使用kill-l 命令查看信号的种类:62种信号
非可靠信号:1~31号信号,从Unix就有的信号,每个信号在系统中都有对应的事件,不可靠的,有可能丢失事件
可靠信号:34~64号信号,后来扩充的32种信号,扩充的时候并没有具体对应的事件,可靠的不会丢失事件
信号的生命周期:信号的产生-》信号在进程中注册-》信号在进程中注销-》信号的处理
信号的产生:
硬件:ctrl+c/ \ /z
软件:kill命令/ kill(int pid ,int signum)/ raise(int signum)/abort()/alarm(int seconds)/sigqueue()
kill杀死一个进程的原理:就是发送一个终止信号给指定的进程,进程收到信号处理事件,事件处理的结果就是退出进程
信号在进程中注册:在进程pcb中标记,这时候有一个信号待处理
在进程pcb中有一个信号的pending位图-----未决信号集合。以0/1标记进程当前进程是否收到了哪些信号
未决::一种信号所处的状态,这种状态指的是收到了信号但是还没有处理的状态
信号的注册就是在pending位图中指定信号的相应比特位置1 ,并且为每个信号封装一个sigqueue节点,添加到进程的等待处理信号链表中
非可靠信号的注册:当信号到来,检测是否有相同信号已经注册,若未注册,则注册一个,若已经注册,则什么都不干
可靠信号注册:当信号到来,不管是否有相同的信号已经注册,都会封装一个sigqueue节点添加到链表中,并且进行为位图标记
信号在进程中注销:在进程pcb中,消除一个指定信号存在的痕迹—删除节点,修改位图
非可靠信号的注销:删除节点,直接将位图相应位置0(因为非可信号相同信号最多只有一个相同节点)
可靠信号的注销:删除一个节点,根据是否还有相同节点,决定是否修改位图
可靠信号删除一个节点后,因为有可能有多个相同节点,因此需要判断是否有相同节点,没有了才会修改位图,否则位图依然置1, 表示有这个信号待处理
信号的处理:就是调用信号对应的处理函数,通过这个函数完成信号时间对应的功能
处理方式:
默认处理方式–系统中定义好的处理函数
忽略处理–系统中定义好的什么都不干的函数
自定义处理–用户自己定义信号回调函数,使用这个函数地址替换内核中信号对应的处理函数地址
如何修改信号的处理方式:sighanler_t signal(int signum,sighander_t hander);
signum:要修改那个信号的处理方式
hander:新的处理方式SIG_DFL-默认处理方式/ SIG_IGN-忽略处理方式 /void sigcb(int signo)–字定义处理方式
自定义处理方式信号的捕捉流程:
1.信号是程序运行从内核态切换到用户态之前进行处理的
2.进程如何从用户态切换到内核态:中断/ 异常/ 系统调用
信号的阻塞:在整个pcb中,标记那些信号到来之后,暂时不去处理,直到解除标记(阻塞)之后才会去处理
在pcb中有个block信号的阻塞集合–位图—用于标记要阻塞哪些信号,要阻塞哪些信号就把那些信号添加到这个集合中
如何阻塞:int sigprocmask(int how,sig_*set,sigset_t *old)—
how:要对pcb中的block阻塞集合进行的动作
SIG_BLOCK—将set集合中的信息,添加到阻塞集合中,将原来阻塞集合中的信号放到old,便于还原。block=block|set
SIG_UNBLOCK—将set集合中的信号,从阻塞集合中移除(解除阻塞)block=block&(~set)
SIG_SETMASK—将阻塞集合中的信号,设置为set集合中的信号
block=set
int sigemptyset(sigset_t *set);—清空set集合–初始化集合
int sigfillset(sigset_t *set); --将所有信号添加到set集合
int sigaddset(sigset_t *set,int signum);----将signum指定信号添加到set集合中
int sigdelset(sigset_t *set,int signum);—将signum指定信号从set集合中移除
int sigismember(const sigset_t *set, intsignum);—判断signum指定信号量是否在set集合中
**在所有信号中,有两个信号比较特殊:SIGKILL-9/ SIGSTOP-19,这两个信号不能被阻塞,不能被定义,不能被忽略
**
关键字:volatile—修饰一个变量,保持变量内存可见性,防止编译器过度优化
正常情况下,CPU对每个变量数据的访问,都需鸭从内存中将数据加载到CPU寄存器上进行处理
但是若编译器在进行代码优化的时候,认为这个变量没有改变,切访问频率很高,这时候就会直接将值放到CPU寄存器中
CPU寄存器:离CPU更近的用来缓存的数据存储单元
CPU原本每次访问数据,都会从内存中获取,但是代码优化后,在指令中是直接将内存中值直接进行寄存器赋值,造成当变量内存中的数据发生改变的时候,CPU不会从内存中重新获取
函数的可重入与不可重入:
函数的可重入:在多个执行流,同时进入某一个函数运行
原子操作:一个操作要么不做,要么就一次做完,中间不会被打断—最小的不可分割性
可重入函数:函数在多个执行流中重入,而不会造成异常,不会带来不确定性
不可重入函数:函数在多个执行流中重入,有可能带来程序运行的不确定性,造成异常
判断标准:一个函数中是否对全局数据进行了不受保护的非原子操作
以后我们自己写函数/或者使用别人的函数在多执行流中的时候,就要注意函数的可重入与不可重入问题
信号的应用:
1.管道–若管道所有读端被关闭,则写端会触发异常,进程退出
进程退出,如果不是正常退出,那么进程的退出其实都是因为信号的处理而中途退出的
进程继续向关闭了所有读端的管道写入数据,触发异常,操作系统给进程发送这个管道坏了的信号,处理方式就是退出进程SIGPIPE信号
2.僵尸进程—子进程先于父进程退出,父进程没有关注子进程的退出状态进行等待处理,则子进程称为僵尸进程
因为父进程也不知道子进程什么时候才会退出,因此也不知道什么时候去进行等待处理
在避免僵尸子进程的时候,都是创建之后直接等待,但是这种方式导致父进程在子进程退出之前会阻塞
子进程退出的时候,操作系统就会给父进程发送子进程的退出通知—SIGCHLD信号
因为SIGCHLD信号的默认处理方式就是忽略处理,因此无法关注到子进程的退出状态
学了信号——修改SIGCHLD信号的处理方式,在信号的回调函数重进行进程等待—在子进程退出的时候再去调用wait/ waitpid,父进程不用阻塞
但是SIGCHLD信号是一个非可靠信号,若有两个子进程同时退出,两个信号同时只会注册一次,也就是只能调用一次wait处理一个子进程,另一个子进程的退出的事件通知就丢失了,也就意味着另一个进程就会成为僵尸进程
因此应该在一次信号的处理回调函数中将所有的退出的子进程处理完毕—while(waitpid(-1,&status, WNOHANG)>0);