目录
为什么需要进程间通信
每一个进程的数据都是存储在物理内存当中,进程通过各自的进程虚拟地址空间进行访问,访问的时候通过各自的页表的映射关系,访问物理内存。从进程的角度看,每个进程都认为自己拥有4G(32位操作系统)的空间。至于物理内存当中如何存储,页表如何映射,进程是不清楚的。这也造就了进程的独立性。
好处:让每个进程运行的时候都是独立运行的,数据不会乱窜。
坏处:如果两个数据之间需要数据交换,由于进程的独立性,就没有那么方便了。
常见的进程间通信方式:管道、共享内存、消息队列、信号量、信号、网络
管道
匿名管道
1、管道符号:
2、管道的本质:
管道在内核当中是一块缓冲区,供进程进行读写,交换数据。
3、管道的接口
参数:参数为出参,也就是pipefd的值是pipe函数进行填充,调用之进行使用
pipefd[0]:管道的读端
pipefd[1]:管道的写端
返回值:创建成功,返回0;创建失败,返回-1。
#include<stdio.h>
2 #include<unistd.h>
3 int main(){
4 int fd[2];
5 int ret = pipe(fd);
6 if(ret < 0){
7 perror("pipe");
8 return 0;
9 }
10 printf("fd[0] = %d,fd[1] = %d\n",fd[0],fd[1]);
11 while(1){
12
13 }
14 return 0;
15 }
要让不同的进程通过匿名管道进行交换数据(进程间通信),进程应该具备什么条件呢?
不同的进程要用同一个匿名管道进行通信。则需要进程拥有该管道的读写两端的文件描述符。
4、从PCB的角度理解管道
下面让父子进程间进行通信:
#include<stdio.h>
2 #include<unistd.h>
3 #include<string.h>
4 int main(){
5 int fd[2];
6 int ret = pipe(fd);
7 if(ret < 0){
8 perror("pipe");
9 return 0;
10 }
11 pid_t pid = fork();
12 if(pid < 0){
13 perror("fork");
14 return 0;
15 }else if(pid == 0){
16 //child,写数据
17 const char* buf = "linux is so easy\n";
18 write(fd[1],buf,strlen(buf));
19 }else{
20 //father,读数据
21 char buf[1024] = {0};
22 ssize_t read_size = read(fd[0],buf,sizeof(buf) - 1);
23 printf("read_size = %ld,%s\n",read_size,buf);
24 }
25 while(1){
26
27 }
28 return 0;
可以看到父子进程同时拥有该管道读写两端的文件描述符。
如图:
5、匿名管道的特性:
①半双工
数据只能从管道的写端流向管道的读端,并不支持双向通信。
②没有标识符,只能 具有亲缘性关系的进程进行进程间通信。
创建的匿名管道,在内核当中是没有任何表示符的。其他的进程是没有办法通过标识符找到这个匿名管道对应的缓冲区。(只能具有亲缘性关系的进程进行进程间通信)具体步骤如下:
第一步:父进程先创建匿名管道。
第二步:父进程再创建子进程。(为了保证子进程的文件描述符表当中有匿名管道的读写两端的文件描述符)
③管道的声明周期跟随进程
进程退出后,管道就随之被销毁了
④管道的大小为64k,也就是65536个字节(可以无脑的向管道当中写来验证)
⑤管道提供字节流服务
从读端进行读的时候,是将数据从管道当中读走了
读端可以自定义读多少内容
⑥原子性:
pipe_size:4096字节
pipe_size是保证读写原子性的阈值:当读/写小于pipe_size的时候,管道保证读写的原子性。
当读写得数据小于pize_size得时候,管道保证读写的原子性。
什么是原子性: 一个操作要么不间断的全部被执行,要么一个也没有执行,非黑即白
即读写操作在同一时刻只有一个进程在操作,保证进程在操作的时候,读写操作不会被其他进程打扰。
⑦阻塞属性:读写两端的文件描述符初始的属性为阻塞属性。
阻塞:
6、设置非阻塞属性
fd:待要操作的文件描述符
cmd:告知fcntl函数做什么操作
F_GETFL:获取文件描述符的属性信息
F_SETFL:设置文件描述符的属性信息,设置新的属性放到可变参数列表当中
返回值:
F_GETFL:返回文件描述符的属性信息
F_SETFL:
0:设置成功
-1:设置失败
设置非阻塞属性的时候,宏的名字为0_NONBLOCK
eg:
第一步:先获取文件描述符原来的属性
int flag = fcntl(fd[0], F_GETFL)
第二步:设置文件描述符新的属性的时候,不要忘记将原来的属性也带上。新的属性 = 老的 属性 | 增加的属性
fcntl(fd[fd], F_SETFL, flag | O_NONBLOCK);
读设置成为非阻塞属性:
1、写不关闭,让子进程来读
#include<stdio.h>
2 #include<unistd.h>
3 #include<fcntl.h>
4
5 int main(){
6 int fd[2];
7 int ret = pipe(fd);
8 if(ret < 0){
9 perror("pipe");
10 return 0;
11 }
12 pid_t pid = fork();
13 if(pid < 0){
14 perror("fork");
15 return 0;
16 }
17 if(pid == 0){
18 //child
19 //1、关闭写端
20 //2、设置读端为非阻塞属性
21 //3、读
22 close(fd[1]);
23 int flag = fcntl(fd[0], F_GETFL);
24 fcntl(fd[0], F_SETFL, flag | O_NONBLOCK);
25 char buf[1024] = {0};
26 ssize_t read_size = read(fd[0], buf, sizeof(buf) - 1);
27 printf("read_size = %ld\n",read_size);
28 perror("read");
29 }else{
30 //father
31 //1、关闭读端
32 //2、写关闭/写不关闭
33 //3、写
34 close(fd[0]);
35 while(1){
36
37 }
38 }
结果:此时read的返回值为-1,而errno被设置为EAGAIN,读失败
2、写端关闭,子进程读
结果:read读成功了,读到了一个字节。
写设置为非阻塞属性
1、读不关闭,一直写
#include<stdio.h>
2 #include<unistd.h>
3 #include<fcntl.h>
4 #include<string.h>
5 int main(){
6 int fd[2];
7 int ret = pipe(fd);
8 if(ret < 0){
9 perror("pipe");
10 return 0;
11 }
12 pid_t pid = fork();
13 if(pid < 0){
14 perror("fork");
15 return 0;
16 }
17 if(pid == 0){
18 //child
19 //1、关闭读端
20 //2、设置写端为非阻塞属性
21 //3、写
22 sleep(2); //让父进程先运行,关闭发的fd[0]
23 close(fd[0]);
24 int flag = fcntl(fd[1], F_GETFL);
25 fcntl(fd[1], F_SETFL, flag | O_NONBLOCK);
26 ssize_t write_size;
27 while(1){
28 write_size = write(fd[1],"a",1);
29 if(write_size < 0){
30 break;
31 }
32 }
33 printf("write_size = %ld\n",write_size);
34 perror("write");
35 }else{
36 //father
37 //1、关闭写端
38 //2、读关闭/读不关闭
//3、读
40 close(fd[1]);
41 while(1){
42 sleep(1);
43 }
44 }
45 return 0;
46 }
当把管道写满以后,再调用write函数,返回-1,errno设置为EAGAIN。
2、读关闭,一直写
代码加上close(fd[0])
写端调用write进行写的时候,就会发生崩溃。(本质原因是因为写端的进程,收到了SIGPIPE信号,导致写端进程崩溃,这种现象称为管道破碎)
扩展(位图)
系统接口当中,文件打开方式的宏,再内核当中使用的方式是位图(比如O_RDONLY、O_CREAT、O_NONBLOCK)
这些属性值得二进制序列只有其中一个比特位是1,这样在进行按位或运算的时候,就会重新组合,组合后的值每个比特位都有自己得含义所在。
命名管道
1、创建
①mkfifo命令
这里的fifo是命名管道文件,相当于内核缓冲区的标识符。不同的进程通过这个文件,都可以找到内核的缓冲区。
②函数创建
pathname:要创建的命名管道的路径以及文件名
mode_t:命名管道的文件的权限,八进制数组(如0664)
返回值:
成功:0
失败:-1
2、特性
支持不同的进程进行进程间通信,不依赖于亲缘关系
3、代码验证
创建一个管道文件fifo,一个进程往管道中写,一个进程读。
共享内存
原理:
共享内存是在物理内存当中的一段空间。
不同的进程通过各自的页表将该物理内存空间映射到自己进程的虚拟地址空间当中。
不同的进程通过操作自己的进程虚拟地址空间当中的虚拟地址来操作共享内存。
如下图所示 :
接口:
1、创建或者获取共享内存接口:
key:共享内存的标识符
size:共享内存大小
shmflg:获取/创建共享内存时,传递的属性信息
IPC_CREAT:如果获取的共享内存不存在,则创建;若存在,返回值就是该共享内 存的操作句柄
IPC_EXCL | IPC_CREAT:
如果获取的共享内存存在,则函数报错。
如果获取的共享内存不存在,则创建
本质上:该组合是要获取是重新创建的共享内存
返回值:
成功:返回共享内存的操作句柄
失败:-1
2、将共享内存附加到进程的虚拟地址空间
shmat:共享内存操作句柄,就是shmget函数调用成功的返回值。
shmaddr:将共享内存附加到进程的哪一个地址上,一般让OS自己分配,所以传递NULL
shmflg:以什么权限将共享内存附加到进程当中(约束进程对共享内存由什么样的权限)
SHM_RDONLY:只读
0:可读可写
返回值:
成功:返回附加的虚拟地址
失败:返回NULL
3、分离
shmaddr:shmat函数的返回值
返回值:
成功:0
失败:-1
4、操作共享内存接口
shmid:共享内存的操作句柄
cmd:告诉shmctl函数需要完成什么功能
IPC_SET:设置共享内存属性
IPC_STAT:获取共享内存属性信息
IPC_RMID:删除共享内存,第三个参数传递NULL
buf:共享内存数据结构buf;是一个输出型参数,用户提供空间,函数填充内容后返回
第三个参数指向的结构体如下:
特性:
1、生命周期跟随OS
2、共享内存是覆盖写的方式,读的时候是访问地址
写:覆盖上一次的数据(可以理解为清空上一次的数据重新写)
读:只是访问地址,进行读取。读完之后数据还在(区别于管道的字节流,它是将数据直接从管道拿走了)
3、共享内存的删除特性:
ipcs命令&&ipcrm命令(ipcrm -m [shmid]:删除一个共享内存)
一旦共享内存被删除掉之后,共享内存的物理内存当中的空间就被销毁了。
删除共享内存的时候,若共享内存附加的进程数量为0,则内核当中描述该共享内存的结构体也被释放了。
删除共享内存的时候,若共享内存附加的进程数量不为0,则会将该共享内存的key变成0X00000000。表示当前共享内存不能被其他进程所附加,共享内存的状态会被设置为destory。附加的进程一旦全部退出之后,该共享内存内存在内核的结构体会被操作系统释放。
等到进程全部退出后,OS会自动释放该共享内存的内核结构体。
代码验证:
通信成功,使用ipcs查看共享内存的使用情况:
消息队列
原理
msgqueue采用链表来实现消息队列,该链表由系统内核来维护。
系统中可能会有很多的msgqueue,每个MQ用消息队列描述符(消息队列-qid)来区分,qid是唯一的,用来区分不用的MQ
在进行进程间通信的时候,一个进程将消息队列追加到MQ的尾端,另一个进程从消息队列里取数据(不一定按照先进先出的原则取数据,也可以按照消息类型字段来取)
接口:
1、创建消息队列
key:消息队列标识符
msgflg:创建的标志
IPC_CREAT:如果不存在,则创建
按位或(|)加权限(8进制数字)
返回:
成功:返回队列id
失败:返回-1,并设置errno
2、发送消息
msqid:消息队列的id
msgp:指向msgbuf的指针,用来指定发送的消息(操作系统为函数发送的消息定义了发送格式,但是只定义了一部分,另外一部分需要程序员自己定义)
msgsz:要发送消息的长度(消息内容,就是struct msbuf结构体中char mtext[]数组的大小)
msgflg:创建标记
O:阻塞发送
IPC_NOWAIT:非阻塞发送
返回值:
成功:0
失败:-1并设置errno
3、接受消息
msqid:消息队列id
msgp:指向msguf的指针,用来接受消息
msgsz:要接受消息的长度。注意:参数 msgsz指定由msgp参数指向的结构的成员mtext的最大大小(以字节为单位)
msgtyp:接受消息的方式:
等于0:读取队列中第一条消息
大于0:读取队列中类型为msgtpy的第一条消息,如果制定了MSG_EXECPT,将 读取类型不等于msgtyp的队列中的第一条信息。
小于0:读取队列中最小类型小于或等于msgtpy绝对值的第一条信息
msgflg:创建标记:
0:阻塞发送
IPC_NOWAIT:非阻塞码,获取失败立刻返回
返回值:
成功:返回实际读取消息的字节数
失败:-1并设置errno
验证 :
创建一个消息队列(声明周期跟随操作系统),不同进程想要使用消息队列进行通信的时候只需要获取同样的消息队列标识符就好了。
发送一次,第二次接受的时候:
由于msgflg设置的是0位阻塞状态,所以,接受信息的进程被阻塞了。
下面发送5次接受一次。
可以看到现在的消息数为4。
信号量
system V信号量
原理:
信号量本质是资源计数器,能够保证多个进程访问临界资源, 执行临界区代码的时候,互斥访问,同时也可以用于同步
临界资源:多个进程都可以访问的资源(比如共享内存)
临界区:访问临界资源时的代码区称之为临界区
互斥访问:同一时刻,多个进程当中,只有一个进程可以访问临界区资源
如何保证互斥访问:通过信号量的值来保证,每个进程需要先获取信号量,只有获取到信号量才可以访问临界资源,如果获取失败,就会阻塞等待
为什么要互斥访问临界区?(以共享内存为例)
有两个进程,它们通过共享内存进行信息交互。两个进程在附加成功后,都能够访问共享内存,假设两个进程都进行写,会造成进程结果的二义性吗?
1、当并行的时候,两个进程你同时往共享内存中写数据,造成结果二义性
2、单核CPU也是可以的,比如A在向共享内存中写完数据,正要读的时候,由于某些原因,他被剥离CPU。此时进程B执行,正常读写完成后,脱离CPU。然后进程A再次执行上CPU执行(由于程序计数器和上下文信息的存在,进程A可以快速定位到下一条执行的指令),进程A会直接在共享内存中读取数据。但是读到的数据是B的。
所以多个进程访问临界资源,如果不加以约束(互斥访问临界资源),就一定会产生程序结果的二义性。
所以说:互斥访问存在的原因就是为了保证程序结果不会有二义性。
同步访问: 当临界资源空闲之后,通知正在等待的进程进行访问。