进程间通信概念
今天为大家带来的是有关进程间通信的知识,我们需要了解什么是进程间通信?进程间通信的作用是什么?本次我们还会介绍几种重点的进程间通信方式,希望可以帮到大家。
首先,我们要知道什么是进程??我们给出定义:
进程:进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。
通俗易懂的讲,进程就是一段程序的执行过程。
由于每个进程都具有独立性,一个进程在访问一个数据时都是通过地址来进行访问的,而进程中的地址都是虚拟地址,只有经过页表映射后才能访问物理内存。一般情况下,如果进程想要访问进程的数据,那么就需要进程的地址;但是,这里的进程地址只是虚拟地址,因此5进程就不能通过页表映射访问到进程的数据。
这时,我们就需要一种东西,可以使进程之间建立关系,能够通过这个东西访问同一块物理内存。因此,操作系统就根据不通场景,提供了几种方式,来实现开辟公共内存。
进程间通信的方式有很多,这里我们只讲四种进程间通信:管道,共享内存,消息队列和信号量。其中管道和共享内存是我们讲解的重点,消息队列和信号量只说明概念。本次代码实现为C语言,代码运行在Linux中完成。
管道
我们首先要讲的是管道,简单来说,管道就是从一个进程连接到另一个进程的一个数据流,其本质就是内存中的一块缓冲区;并且管道具有半双工通信的特点,即可以选择方向的单向通信(同一时间不能既写入又读出);管道分为匿名管道和命名管道,下面我们来看两种管道的定义。
管道分类
命名管道:管道具有标识符,能够被其他进程找到,因此可以用于同一主机上的任意进程间通信。
匿名管道:管道没有标识符,不能被其他进程找到,因此只能用于具有亲缘关系的进程间通信。
上图就是匿名管道的含义,这里我为大家讲解一下。进程为进程的进程,开始时进程就具有管道的操作句柄,而子进程通过复制父进程的全部信息,包括与管道相连的操作句柄,所以才能访问管道。
这里的操作句柄,即文件描述符,通过文件描述符的下标,可以找到管道的描述信息,进而找到管道的执行操作。
匿名管道的实现
下面我们来实现匿名管道:
#include<stdio.h>
#include<unistd.h>//创建管道的头文件
#include<string.h>
#include<stdlib.h>
#include<sys/wait.h>//等待的头文件
int main()
{
int pipefd[2];//pipefd[0]用于读,pipefd[1]用于写,pipefd[2]可以读写
int ret=pipe(pipefd);//创建管道
if(ret<0)//创建失败,返回报错
{
perror("pipe error");
return -1;
}
//必须先创建管道,再创建子进程,这样子进程才能复制父进程的操作句柄
int pid1=fork();//创建第一个子进程,具有上述管道的句柄
if(pid1==0)
{
close(pipefd[0]);
char *data="我是你哥,我来教你学习\n";
printf("write start!\n");
//size_t write(int fd,char *data,int len)//写入数据,成功返回写入的个数
int ret=write(pipefd[1],data,strlen(data));//写入管道数据
if(ret<0)//写入失败,报错
{
perror("write error");
}
printf("write over!\n");
exit(0);//子进程1退出,不继续向下
}
int pid2=fork();//创建第二个,也有上述管道的句柄
if(pid2==0)
{
close(pipefd[0]);
exit(0);
while(1){
printf("现在老二已经醒了,准备开始接收数据!\n");
char buf[1024]={0};
//size_t read(int fd, void *buf, size_t count);
int ret=read(pipefd[0],buf,1023);//开始读取数据
printf("弟弟收到了信息:%s\n",buf);
}
exit(0);
}
//父进程等待两个子进程都退出
wait(NULL);
wait(NULL);
return 0;
}
上述就是匿名管道的创建及其实现。主要的操作符为int pipe(int pipedf[2])。
现在都知道管道是具有半双工通信的特点,那么如果管道中不写入数据一直读取会怎么样呢??一直写入数据而不读取会怎么样呢??下面我们直接揭晓答案。
1.管道中如果没有数据,而read一直从管道中读取数据会出现阻塞,直到有数据后,读取到数据后才返回。
2.如果管道中数据满了,而write继续相关到写入数据会出现阻塞,直到管道中有剩余空间才行。
3.管道的所有读端被关闭,则继续向管道写入数据会导致进程崩溃退出。本质:没有进程在读取数据,所以这时写入数据也就没有意义,系统就会进程干掉。
4.管道的所有写端被关闭,则read从管道读取完所有数据后,将不再阻塞,而是直接返回0。当read从管道中读取数据时,返回0,这就意味着这个管道已经不可能读取到数据,没必要继续等待了。因此,我们可以通过read的返回值,来决定什么时候可以停止从管道读取数据。
命名管道
命名管道与命名管道最大的不同就是,管道具有名称,可以被其他进程找到并访问,因此可以用于同意主机上的任意进程间通信操作。
命名管道的通信原理:一个进程创建了一个有名字的命名管道,多个进程可以通过管道的名称,打开同一个管道,访问同一块内核缓冲区。
命名管道的名称:相当于一个可见文件系统的管道文件。
注意:命名管道文件,虽然是个文件,但实际上只是一个名字,只是一个能够让多进程通过打开同一个命名管道文件,进而获取到同一块缓冲区的描述信息或操作句柄,进而访问同一块缓冲区的数据内容。实际上的通信依旧是通过内核中的缓冲区完成的,而不是这个文件。
对于我们上述讲到的命名管道名称,作为管道的标识符是一直存在的,但如果没有进程用该管道进行进程间通信,那么这个管道就不会创建专属的缓冲区。因此,在管道创建初,内存是不会为其开辟相应的缓冲区;只有需要进程间通信时,才会为其创建缓冲区。
下面我们来实现命名管道,首先我们来看看创建命名管道的接口:
首先创建命名管道的写入管道:
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
#include<fcntl.h>
#include<sys/stat.h>//管道文件mkfifo的头文件
#include<errno.h>
int main()
{
//创建管道
umask(0);//将当前进程文件权限掩码设置为0
int ret=mkfifo("./test.fifo",0664);//或者掩码可以默认,这里权限改为666
if(ret<0&&errno!=EEXIST)//errno是一个全局变量,每个系统调用接口返回前都会重置自己的错误编号
{
perror("mkfifo error");
return -1;
}
//只写方式打开管道
int fd=open("./test.fifo",O_WRONLY);//运行时不能只打开只写管道,会阻塞
if(fd<0)
{
perror("open error");
return -1;
}
//写入数据
while(1)
{
//使用键盘捕捉数据
fflush(stdout);//清空缓冲区
char buf[1024]={0};
scanf("%s",buf);
int ret=write(fd,buf,strlen(buf));//将buf中的内容写入管道中
if(ret<0)//读取失败
{
perror("write error");
close(fd);
return -1;
}
}
close(fd);
return 0;
}
接下来我们来实现读取管道数据:
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
#include<fcntl.h>
#include<sys/stat.h>
#include<errno.h>
int main()
{
//创建管道
umask(0);//将当前进程文件权限掩码设置为0
int ret=mkfifo("./test.fifo",0664);
if(ret<0&&errno!=EEXIST)//errno是一个全局变量,每个系统调用接口返回前都会重置自己的错误编号
{
perror("mkfifo error");
return -1;
}
//以只读的方式打开管道
int fd=open("./test.fifo",O_RDONLY);
if(fd<0)
{
perror("open error");
return -1;
}
//读取数据
while(1)
{
char buf[1024]={0};
int ret=read(fd,buf,1023);//读取数据``
if(ret<0)//读取失败
{
perror("read error");
close(fd);
return -1;
}
else if(ret==0)//写端被关闭,会阻塞
{
printf("所有写端被关闭!!\n");
close(fd);
return -1;
}
printf("%s\n",buf);//读写端都打开
}
close(fd);
return 0;
}
最后我们再来总结一下管道的特点:
1.半双工通信
2.管道生命周期随进程结束
3.提供字节流传输服务(数据先进先出,不会丢失数据)
4.自带同步与互斥(同一时间唯一访问,对资源的访问具有一定限制)
共享内存
作为我们进程间通信方式中最快的方式,共享内存也是进程间通信中,我们着重要讲的知识点。
共享内存:就是允许多个进程访问同一个内存空间,是在多个进程之间共享和传递数据最高效的方式。操作系统将不同进程之间共享内存安排为同一段物理内存,进程可以将共享内存连接到它们自己的地址空间中,如果某个进程修改了共享内存中的数据,其它的进程读到的数据也将会改变。
由上述可知,共享内存就是两进程直接访问同一块物理内存,省去了管道操作的两次拷贝,所以大大节省了时间。
共享内存的接口
下面我们来介绍共享内存的接口:
1.函数,创建或打开共享内存
2.函数,将共享内存映射到虚拟地址空间
3.函数,解除映射
4.函数,释放共享内存
这里我们需要注意,当为IPC_RMID时,表示标记一个共享内存需要被删除。但是在实际中,如果多个进程一起访问同一块共享内存时,共享内存并不会被直接删除。共享内存中有个当前映射连接技术,只有当连接计数为0时,共享内存才会被删除。
共享内存的实现
下面我们来实现创建共享内存并写入数据:
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<stdlib.h>
#include<sys/shm.h>
#define SHM_KEY 0x12345678//自定义共享内存地址
int main()
{
//创建并打开共享内存
int shmid=shmget(SHM_KEY,4096,IPC_CREAT|0664);
if(shmid<0)//创建失败,返回
{
perror("shmid error");
return -1;
}
//映射地址空间
void *start=shmat(shmid,NULL,0);
if(!start)//映射失败,则返回
{
perror("start error");
return -1;
}
while(1)
{
sprintf(start,"i am processA\n");//格式化数据写入start指向的内存中
sleep(1);
}
shmdt(start);//解除映射
shmctl(shmid,IPC_RMID,NULL);//删除
return 0;
}
接下来我们来实现读取共享内存:
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<stdlib.h>
#include<sys/shm.h>
#define SHM_KEY 0x12345678
int main()
{
//打开共享内存
int shmid=shmget(SHM_KEY,4096,IPC_CREAT|0664);
if(shmid<0)
{
perror("shmid error");
return -1;
}
void *start=shmat(shmid,NULL,0);
if(!start)
{
perror("start error");
return -1;
}
while(1)
{
printf("%s\n",start);//把内存中的数据打印出来
sleep(1);
}
shmdt(start);//解除映射
shmctl(shmid,IPC_RMID,NULL);//删除
return 0;
}
最后,我们在来总结一下共享内存的特点:
1.最快的进程间通信方式
2.生命周期随内核(并不会随着打开的进程退出而退出)
3.共享内存的访问操作存在安全问题
消息队列
消息队列:是一种应用间的通信方式,消息发送后可以立即返回,有消息系统来确保信息的可靠专递,消息发布者只管把消息发布到消息队列中而不管谁来取,消息使用者只管从消息队列中取消息而不管谁发布的,这样发布者和使用者都不用知道对方的存在。
消息队列的本质是内核中的一个优先级队列,多进程通过访问同一个消息队列,以添加数据节点和获取数据节点来实现进程间通信。
消息队列特点:1.生命周期随内核结束而结束,2.自带同步与互斥,3.传输数据是以数据块的方式传输。
信号量
信号量:信号量的本质是数据操作锁,它本身不具有数据交换的功能,而是通过控制其他的通信资源来实现进程间通信,它本身只是一种外部资源的标识。信号量在此过程中负责数据操作的互斥、同步等功能。
信号量的本质是一个计算器+pcb等待队列。计算器的主要实现方式为PV操作。
P操作:对计算器进行-1操作,判断计算是否大于等于0,正确则返回;失败则阻塞。
V操作:对计算器进行+1操作,若计时器小于等于0,则唤醒一个等待的进程。
通过上述操作对计数器进行加减操作,若计数器大于0,则表示有剩余资源可以分配;若计数器小于等于0,则表示没有剩余资源可以分配,需要进程等待有资源可以利用。
在平常情况下,信号量的初始化计数器为1,表示资源只有一个,这样就能实现信号量的互斥。
今天的内容就到这里,希望对大家的理解有帮助;如果有什么不对的地方,希望大佬可以批评指针。