目录
进程间通信(IPC)操作系统为用户提供的几种用于进程间通信的方式(管道,共享内存,消息队列,信号量)
目的:进程具有独立性(每个进程都有自己的虚拟地址空间,访问的都是自己的虚拟地址,其他进程在自己的虚拟地址空间映射中实际上是没有这个数据的),因此进程间无法直接通信,但是在大型项目中多个协同工作很常见,这时候进程间通信就尤为重要.
进程间通信根据不同的实际应用场景来采用不同的方式.
一.管道-PIPE
半双工通信:管道有两端,但是数据从哪端到哪端这个由用户来定
1.管道的分类
1.1匿名管道
内核中开辟的一块缓冲区,没有标识符,无法被其他进程找到(只能用于具有亲缘关系的进程间通信)
例如:一个进程通过系统调用在内核创建了一个匿名管道,为了能够让用户操作这个管道,因此调用了文件描述符作为这个管道的操作句柄,其它进程因为这个管道没有标识符,找不到因此无法通信.但是,如果这个创建管道的进程创建了一个子进程,这时候子进程复制了父进程(文件描述符表),所以子进程相当于也有文件描述符可以操作这个管道.
只有通过子进程复制父进程的方式才能获取到同一个管道的操作句柄.
int main(){
//匿名管道只能用于具有亲缘关系的进程间通信,子进程复制父进程的方式获取操作句柄
//注意:创建管道一定要放在创建子进程之前
int pipefd[2];
int ret=pipe(pipefd);
if(ret<0){
perror("pipe error");
return -1;
}
pid_t pid =fork();
if(pid<0)
{
perror("fork error");
return -1;
}
if(pid==0){
//子进程向管道写数据
char*str="一天课,难受~! \n";
write(pipefd[1],str,strlen(str));
}
else
{
//父进程从管道读数据
char buf[1024]={0};
read(pipefd[0],buf,1023);
printf("read:%s",buf);
}
close(pipefd[0]);
close(pipefd[1]);
return 0;
}
1.2命名管道
内核中开辟的一块缓冲区,具有标识符,可以被其他进程找到
命名管道:一个进程创建一个命名管道,这个命名管道会在文件系统中创建出一个管道文件(可以看得到的,实际上就是管道的名字),多个进程通过打开同一个管道文件,访问内核中的同一个缓冲区实现通信,可以用于同一主机上的任意进程间通信.
操作:mkfifo 文件创建命名管道文件
接口:int mkfifo(char *filename,mode_t mode);
以p开头的是管道文件,创建命名管道文件,并不会立即创建内核中的
2.管道的本质和特性
2.1本质
内核中的一块缓冲区,多个进程访问同一个缓冲区来实现通信,并且不能无限制增长(会造成资源耗尽,系统崩溃).
2.2特性
匿名管道特性:
如果管道中没有数据,则read会阻塞;如果管道中数据满了,则write会阻塞
阻塞:为了完成一个功能,发起调用,功能无法完成则一直等待
非阻塞:为了完成一个功能,发起调用,功能无法完成则调用报错返回
其他特性:管道的操作是会阻塞的,但是如果管道所有写端被关闭(管道已经不可能写入数据)这时候read读完管道中的数据后就不再阻塞了,而返回0(这里的0表示没人写入,而不是没数据了)
管道的所有读端被关闭(管道没人读取数据)则继续write就会触发异常,进程退出
命名管道特性:
独特的打开特性:如果命名管道文件以只读打开,则会阻塞,直到管道文件被其它进程以写的方式打开
如果命名管道文件以只写打开,则会阻塞,直到管道文件被其它进程以读的方式打开
原理是:管道的缓冲区在没有进程确定要写数据且有进程读取数据的情况下,缓冲区就没必要开辟,有读有写才会开辟缓冲区,否则空占资源.
总特性:1.半双工通信
2.生命周期随进程
3.管道提供流逝传输(数据先进先出,所有读端关闭write异常,所有写端关闭则read返回0不在阻塞)
4.管道自带同步与互斥
同步:操作按照一定秩序进行,没有数据read阻塞,数据满了write阻塞,
互斥:操作都是安全可靠的,同一时间只有一个进程能够访问,,避免进行交叉写入.
3.管道符
管道符:连接两个命令,将前边命令的输出结果,作为后边命令的输入传递给后边命令进行处理
管道符的实现:
让前面的命令进程,不再把数据写入到标准输出,而是写入一个管道中
让后面的命令进程,不再从标准输入读取数据,而是从一个管道中读取
//通过匿名管道实现管道符的作用
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
#include<wait.h>
int main(){
int pipefd[2]={-1};
if(pipe(pipefd)<0){
perror("pipe error");
return -1;
}
pid_t ps_pid=fork();
if(ps_pid==0){
//ps子进程
close(pipefd[0]);
dup2(pipefd[1],1);//将标准输出重定向到管道写入端,向1中写入数据就相当于向管道写入数据
execlp("ps","ps","-ef",NULL);
exit(0);
}
pid_t grep_pid=fork();
if(grep_pid==0){
close(pipefd[1]);
//grep子进程
//将标准输出重定向到管道写入端,向1中写入数据就相当于向管道写入数据);
dup2(pipefd[0],0);
execlp("grep","grep","ssh",NULL);
exit(0);
}
close(pipefd[0]);
close(pipefd[1]);
waitpid(ps_pid,NULL,0);
waitpid(grep_pid ,NULL,0);
return 0;
}
二.共享内存
1.原理与特性
原理:开辟一块新的物理内存,需要进行通信的进程将这块物理内存映射到自己的虚拟地址空间中,直接使用自己的用户空间地址进行访问
特性:所有进程间通信方式中最快的一种,因为通过虚拟地址直接访问内存,相较于其他方式少了两次用户空间与内核空间之间的数据拷贝,它的生命周期随内核.
注意:共享内存是一种覆盖式内存操作,且没有互斥同步关系,操作需要考虑安全问题
2.操作流程及接口
(1)创建或打开共享内存
int shmget(key_t key,size_t size,int shmflag)
key:共享内存标识符
size:需要创建的共享内存大小
shmflag:IPC_CREAT | IPCEXCL | 0664
IPC_EXCL与IPC_CREAT搭配使用,表示存在则报错,不存在则创建打开
mode:0664 设置共享内存访问权限
返回值:成功返回一个非负整数-操作句柄,失败返回-1;
(2)将共享内存映射到虚拟地址空间
void*shmat(int shm_id ,void *shmaddr,int shmflag)
shm_id:shmget返回的操作句柄
shmaddr:通常置NULL,让系统自动分配建立映射
shmflag:SHM_RDONLY-只读;0-默认是可读可写;
返回值:成功返回映射的首地址;失败返回-1;
(3)内存操作
(4)解除映射关系
int shmdt(void *shm_start)
shm_start:映射首地址-shmat的返回值
返回值:成功返回0;失败返回-1;
(5)删除共享内存(实际上我们进行了删除操作,共享内存也不会立即删除,等到共享内存的映射连接数为0时才会被删除)shm_nattch(映射连接数)一旦这个共享内存被标记为将要被销毁,那么这块共享内存不会再被建立映射了
int shmctl(int shm_id,int cmd,struct shmid_ds *buf)
shmid:操作句柄
cmd:IPC_RMID-标记删除(不再接受新的映射)
buf:用于获取共享内存信息,用不上就置NULL)
返回值:成功返回0;针对IPC_RMID 失败返回-1;
写端
#include<unistd.h>
#include<string.h>
#include<sys/shm.h>
#include<stdio.h>
#define IPC_KEY 0x12345678
int main(){
//1.创建共享内存
//int shmget(标识符,大小,标志位|权限)
int shm_id=shmget(IPC_KEY,32,IPC_CREAT|0664);
if(shm_id<0){
perror("shmget error");
return -1;
}
//2.建立映射关系
//shmat(操作句柄,映射首地址,操作权限);
void *shm_start=shmat(shm_id,NULL,0);
if(shm_start==(void*)-1){
perror("shmat error");
return -1;
}
//3.进行内存操作
int i=0;
while(1){
snprintf(shm_start,"%s+%d","又下雨了~",i++);
sleep(1);
}
//4.解除映射关系
shmdt(shm_start);
//5.删除共享内存
shmctl(shm_id,IPC_RMID,NULL);
return 0;
}
读端
#include<unistd.h>
#include<string.h>
#include<sys/shm.h>
#include<stdio.h>
#define IPC_KEY 0x12345678
int main(){
//1.创建共享内存
//int shmget(标识符,大小,标志位|权限)
int shm_id=shmget(IPC_KEY,32,IPC_CREAT|0664);
if(shm_id<0){
perror("shmget error");
return -1;
}
//2.建立映射关系
//shmat(操作句柄,映射首地址,操作权限);
void *shm_start=shmat(shm_id,NULL,0);
if(shm_start==(void*)-1){
perror("shmat error");
return -1;
}
//3.进行内存操作
while(1){
sleep(1);
printf("%s\n",shm_start);
}
//4.解除映射关系
shmdt(shm_start);
//5.删除共享内存
shmctl(shm_id,IPC_RMID,NULL);
return 0;
}
三.消息队列
1.本质
在内核中创建的一个优先级队列;具有标识符能够被其他进程找到,多个进程通过访问同一个队列,通过添加或者获取节点实现通信
2.通信原理
消息队列传输的都是数据节点
节点包含两个信息:1.类型:用于区别身份 2.数据
3.特性
(1)双工通信 (2)自带同步与互斥 (3)生命周期随内核 (4)内核控制其大小,有大小的限制
四.信号量
1.本质
信号量本质上就是内核中的一个计数器
作用:实现进程间的同步与互斥(保护进程间对临界资源的访问操作)
2.信号量实现同步与互斥的原理
实现同步:根据资源数量初始化计数器
通过计数器对资源进行计数,若计数大于0则表示可以访问资源,若资源小于等于0则表示不能访问,则阻塞进程
P操作:在进程访问资源之前进行,判断计数是否大于0;
大于0,则正确返回,计数-1;若小于0,阻塞进程,计数-1;
V操作:当产生一个新的资源,计数+1,唤醒一个阻塞的进程
实现互斥:
初始化临界资源计数器为1;
在访问临界资源之前进行P操作,在访问临界资源之后进行V操作.实现唯一访问