进程间通信(IPC):操作系统为用户提供的几种进程间通信方式。
管道–用于进程间的数据传输
本质:内核中的一块缓冲区–通过半双工(可以选择方向的单向通信)通信实现数据传输。
原理:通过让多个进程都能访问到同一块缓冲区,来实现进程间通信。
管道分类:匿名管道\命名管道
匿名管道
概念:这块内核中的缓冲区没有标识。
特性:只能用于具有亲缘关系的进程间通信。子进程通过复制父进程的方式,获取到管道的操作句柄进而实现访问一个管道通信。
创建管道时,操作系统会提供两个操作句柄(文件描述符),其中一个用于从管道读取数据,一个向管道写入数据。每次通信时只能有一个操作。
文件描述符:内核(kernel)利用文件描述符(file descriptor)来访问文件。文件描述符是非负整数。打开现存文件或新建文件时,内核会返回一个文件描述符。读写文件也需要使用文件描述符来指定待读写的文件。相当于就是内核给这个文件的一个标记。
int pipe(int pipefd[2]);创建一个匿名管道,向用户通过参数pipefd返回管道的操作句柄。
pipefd[0]:用于从管道读取数据
pipefd[1]:用于从管道写入数据
返回值:0-成功,-1-失败
特性:若管道中没有数,则read会阻塞;若管道写满了,则write会阻塞;管道自带同步与互斥。
同步:对临界资源访问的合理性。一个进程使用完资源之后,若是还要再次使用,若有其他进程等待使用资源,该进程就必须排队等待。
互斥:通过保证同一时间只有一个进程能够访问临界资源,保证临界资源访问的安全性。对管道进行数据操作的大小不超过PIPE_BUF=4096的时候,则保证操作的原子性。
若管道所有的写端被关闭(表示当前没有进程继续写入数据了),read读完管道中的数据之后,就不会再阻塞而是返回0。
若管道所有读端被关闭(表示没有进程读取数据了),继续write会触发异常,程序退出。
管道的使用:
1 #include<stdio.h>
2 #include<stdlib.h>
3 #include<unistd.h>
4 #include<string.h>
5 int main(){
6
7 int pipefd[2]={0};
8 int ret=pipe(pipefd);
9 if(ret<0){
10 perror("pipe error");
11 return -1;
12 }
13 pid_t pid=fork();
14 if(pid<0){
15 perror("fork error");
16 return -1;
17 }else if(pid==0){
18 char buf[1024]={0};
19 int ret=read(pipefd[0],buf,1023);
20 printf("buf:[%s]-[%d]\n",buf,ret);
21
22 }else{
23 char *ptr="超级开心";
24 write(pipefd[1],ptr,strlen(ptr));
25 }
26 while(1){
27 printf("---------------------%d\n",getpid());
28
29 }
30
31 return 0;
32 }
命名管道
内核中的缓冲区具有标识符(标识符是一个可见于文件系统的管道文件),其他的进程可以通过这个标识符,找到这块缓冲区(通过打开同一个管道文件,进而访问到同一块缓冲区),进而实现通信。
命令操作:mkfifo filename
int mkfifo(const char* pathname,mode_t mode);—创建命名管道文件
pathname:管道文件名称
mode:文件权限
创建管道文件:
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<stdlib.h>
4 #include<sys/stat.h>
5 #include<errno.h>
6
7 int main(){
8
9 umask(0);
10 char* file="./test.fifo";
11 int ret=mkfifo(file,0664);
12 if(ret<0&&errno!=EEXIST){
13 perror("mkfifo error");
14 return -1;
15 }
16 return 0;
17 }
运行结果:
打开特性
若管道文件以只读的方式打开,则会阻塞,直到这个管道文件以被以写的方式打开。
若管道以只写的方式发开,则会阻塞,直到这个管道文件被以读的方式打开。
若管道以读写的方式打开,则不会阻塞。
打开特性:
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<stdlib.h>
4 #include<sys/stat.h>
5 #include<errno.h>
6 #include<fcntl.h>
7 int main(){
8
9 umask(0);
10 char* file="./test.fifo";
11 int ret=mkfifo(file,0664);
12 if(ret<0&&errno!=EEXIST){
13 perror("mkfifo error");
14 return -1;
15 }
16 printf("create fifo success\n");
17 int fd=open(file,O_RDONLY);
18 if(fd<0){
19 perror("open error");
20 return -1;
21 }
22 printf("open fifo success\n");
23 return 0;
24 }
运行这个程序,程序阻塞在读取管道这里
当我们向管道中写入数据后,程序运行结束。
管道特性
1.管道生命周期随进程。
2.半双工特性。
3.自带同步和互斥。
4.提供字节流服务–有序,链接,可靠的字节流传输。
共享内存–用于进程间的数据共享
最快的进程间通信方式。为什么呢?
因为共享内存直接通过虚拟地址映射访问物理内存,而其他方式因为都是在内核中的缓冲区,因此通信时会涉及到用户态与内核态之间的两次数据拷贝。但是共享内存通信方式不会,所以通信速度最快。
共享内存的实现:
1.创建共享内存–在物理内存上开辟一块内存空间(具有标识符)。
2.将共享内存映射到各个进程的虚拟地址空间。
3.进程就可以通过虚拟地址直接访问到共享内存–多个进程要是映射同一块物理内存,就可以通过这块内存实现数据共享。
4.解除映射关系。
5.删除共享内存。
查看&删除进程间通信资源命令:
ipcs:查看进程间通信资源 ipcrm:删除进程间通信资源
-m:查看共享内存
-q:查看消息队列
-s:查看信号量
int shmget(key_t key,int size,int flag);–创建共享内存
key:共享内存的标识符,多个进程通过相同的标识符可以打开同一块共享内存
size:共享内存大小
flag:IPC_CREAT(存在打开,不存在创建)|IPC_EXCL(报错,不报错) |权限
返回值:成功返回一个操作句柄,失败返回-1
void shmat(int shmid,void addr,int flag);–映射共享内存
shmid:共享内存操作句柄
addr:映射到虚拟地址空间的首地址,通常置NULL
flag:通常0-可读可写 SHM_RDONLY-只读
返回值:
成功,返回映射的虚拟空间首地址,通过这个地址对共享内存进行操作
失败,返回-1
int shmdt(void shmstart);–解除映射*
shmstart:映射到虚拟地址空间的首地址
成功返回0,失败返回-1
int shmctl(int shmid,int cmd,struct shmid_ds buf);–删除共享内存*
shmid:共享内存操作句柄
cmd:具体对共享内存要进行的操作—IPC_RMID(删除共享内存)
成功返回0,失败返回-1
修改共享内存中的值:
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/shm.h>
#define IPC_KEY 0x12345678
int main(){
//创建共享内存
int shmid=shmget(IPC_KEY,32,IPC_CREAT|0664);
if(shmid<0){
perror("create error");
return -1;
}
//映射共享内存
void* shmstart=shmat(shmid,NULL,0);
if(shmstart==(void*)-1){
perror("shmat errror");
return -1;
}
//修改共享内存中的值
int i=0;
while(1){
//格式化数据写入标准输出
//覆盖式写入
sprintf(shmstart,"%s-%d\n","share memory",i++);
sleep(1);
}
//解除映射
shmdt(shmstart);
//删除映射
shmctl(shmid,IPC_RMID,NULL);
return 0;
}
读取共享内存中的值:
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/shm.h>
#define IPC_KEY 0x12345678
int main(){
//创建共享内存
int shmid=shmget(IPC_KEY,32,IPC_CREAT|0664);
if(shmid<0){
perror("create error");
return -1;
}
//映射共享内存
void* shmstart=shmat(shmid,NULL,0);
if(shmstart==(void*)-1){
perror("shmat errror");
return -1;
}
//读取共享内存中的值
int i=0;
while(1){
printf("%s\n",shmstart);
sleep(1);
}
//解除映射
shmdt(shmstart);
//删除映射
shmctl(shmid,IPC_RMID,NULL);
return 0;
}
运行结果:
当删除共享内存的时候,共享内存不会立即被删除(因为有可能会造成正在访问的进程崩溃),而是将key修改为0,表示这块共享内存将不再继续接收映射链接,当这块共享内存的映射链接数为0的时候则自动被释放。
共享内存特性:
1.最快的进程间通信方式。
2.生命周期随内核。
注意:共享内存的操作时不安全的(并不会自动具备同步与互斥关系,需要操作用户进行控制)
消息队列–用于进程间的数据传输
本质:内核中的一个队列,多个进程通过向同一个队列中添加节点和获取节点实现通信。传输一个有类型(优先级)的数据块。
特性:
1.自带同步与互斥。
2.生命周期随内核。
3.数据传输自带优先级。
信号量–用于实现进程间的控制
作用:用于实现进程间的同步和互斥。
本质:内核中的一个计数器+pcb等待队列(对资源进行计数)。
互斥的实现:
通过只有0/1的计数器,实现对临界资源访问状态的标记;在访问临界资源之前先获取信号量,计数-1;若计数<0则使进程等待(将进程pcb加入队列);否则可以对临界资源进行访问(并且在访问期间,已经将临界资源的状态置为不可访问状态,因此可以保证其他进程不会再访问临界资源),当前进程访问完毕之后,则对计数器进行+1,并且唤醒一个进程(将一个pcb出队,置为运行状态)。
同步的实现:
信号量是一个对资源的计数,可以通过计数判断是否能够获取一个资源进行处理;若计数<0,则表示不能获取(并且计数进行-1),则需要等待(加入pcb队列)。这时候若其他进程产生一个资源,则会对计数进行+1,若计数<=0,则唤醒一个进程。假如说开始有100个停车位,每进来一辆车就会-1,当停车位数为0的时候,说明停车场已经停满了车,这时候再进来车时计数器还是会-1,但是车不能进入,只能在外面排队等着,当停车场有车开走了,计数器+1(此时计数器的数不一定>0),并且唤醒等待队列中的一个车,告诉他有停车为了可以去停车了。