进程间通信主要讲的是操作系统为进程间能够进行通信所提供的几种方式。
实际上,两个进程之间是无法直接进行通信的,操作系统为每个进程分配了虚拟地址空间
,通过页表映射
来访问它的物理地址空间
,但是这种映射信息
,其它的进程无法获取,所以实际上两个进程间是无法直接进行通信的。
那么操作系统是如何解决这一问题的?
进程间通信原理:操作系统为进程间提供一个公共的传输媒介
支持共同访问,从而实现进程间的通信
操作系统根据通信需求的不同提供了不同的进程间通信方式:管道
,共享内存
,消息队列
,信号量
管道:用来实现数据传输,具有先进先出的特点
特性:半双工通信—可以选择方向的单向通信
本质:系统内核中的一块缓冲区(内核空间中开辟的内存)
原理:多个进程只要能够访问到同一块缓冲区(管道),就能实现通信
管道的分类:匿名管道
和命名管道
匿名管道:只能用于
具有亲缘关系
的进程间通信
原理:管道缓冲区没有标识符,无法被其他进程找到,因此只能通过子进程复制父进程的方式获取匿名管道的操作句柄,进行通信
代码操作:int pipe(int pipefd[2]);
成功,返回0;失败返回,-1;
pipefd[0] : 用于从管道读数据 pipefd[1] : 用于向管道写数据;
通过IO操作完后堆管道的操作(当做文件来操作)
注意:一定要在创建子进程之前创建管道,否则子进程无法获取管道信息
此处引入两个思考题:如果父进程还未写入数据,子进程就已经去读取数据,会有什么后果?如果管道中已经写满了数据,我们再去写入数据会有什么后果?
这就引出了管道的读写特性:
管道的读写特性
:
1、若管道中没有数据,则read会阻塞;若管道中数据写满了,则write会阻塞
2、如果管道的所有读端被关闭,则继续write就会触发异常,导致进程退出;
如果管道的所有写端被关闭,read读取完所有的数据后将不在阻塞,返回0
。
阅读下面两段代码理解以上内容:
运行结果:程序运行一秒后直接退出
命名管道:可以用于同一主机上任意进程间通信----管道缓冲区具有
标识符
原理:命名管道依然是内核中的一块缓冲区
,但是命名管道有名字,具有标识符,而这个标识符就是一个文件系统可见的管道类型文件
,多个进程可以通过访问同一个管道文件,访问同一个内核中的缓冲区,实现进程间通信
命令操作:mkfifo test.fifo
—创建一个名为test.fifo的管道文件
代码操作:int mkfifo(char*path,int mode)
path:管道文件的路径名称;mode:设置管道文件的操作权限
堆管道文件的读写操作依然是IO操作
打开特性:必须以有读有写
的方式打开命名管道文件,否则程序会阻塞;命名管道也具有匿名管道的读写特性
代码实例:
读端代码:
#include
#include
#include
#include
#include
#include
int main()
{
umask(0);
char *fifo_name="./test.fifo";
int ret=mkfifo(fifo_name,0664);
if(ret<0 && errno!=EEXIST)
{
perror("mkfifo error:");
return -1;
}
//open(文件名,打开方式,权限)
int fd=open(fifo_name,O_RDONLY);
if(fd<0){
perror("open error:");
return -1;
}
printf("open fifo success\n");
while(1){
char buf[1024]={0};
int ret=read(fd,buf,1023);
if(ret<0){
perror("read error:");
return -1;
}else if(ret==0){
printf("all write closed\n");
return -1;
}
printf("buf:%s\n",buf);
}
close(fd);
return 0;
}
写端代码:
#include
#include
#include
#include
#include
#include
#include
int main()
{
umask(0);
char *fifo_name="./test.fifo";
int ret=mkfifo(fifo_name,0664);
if(ret<0 && errno!=EEXIST){
perror("mkfifo error:");
return -1;
}
int fd=open(fifo_name,O_WRONLY);
if(fd<0){
perror("open error:");
return -1;
}
while(1){
char buf[1024];
scanf("%s",buf);
int ret=write(fd,buf,strlen(buf));
if(ret<0){
perror("write error:");
return -1;
}
}
close(fd);
return 0;
}
此处的通信就是通过
读写端共同访问
命名管道文件"test.fifo"实现的。
只有读写端都存在并且同时运行时,才能够正常的写入和读取数据,若一端被强制关闭,另一端也会退出。
共享内存:开辟一块公共内存,使每个进程都可以写入或者读取数据
原理:开辟一块公共的物理地址空间,各个进程将该物理内存空间映射到自己的虚拟地址空间上,通过虚拟地址进行访问,实现数据共享
特点:1、共享内存是最快的进程间通信方式
,直接通过虚拟地址访问物理内存,相较于其他方式少了数据拷贝的操作
2、生命周期随内核,并不会随着链接该共享内存的进程的退出而销毁
注意事项:对共享内存的操作并非安全操作,
1、创建或打开共享内存:
int shmget(key_t key, size_t size, int shmflg);
key:标识符,指定操作的是哪一块共享内存,同一标识符可以让多个进程打开同一块共享内存
size:要创建的共享内存大小
shmflg:打开方式+权限;IPC_CREAT|IPC_EXCL|0664
返回值:成功,返回操作句柄(非负整数);失败,返回-1
2、与进程建立映射关系
void *shmat(int shmid, const void *shmaddr, int shmflg);
shmid:shget()返回的操作句柄
shmaddr:映射首地址,通常置NULL,让用户自己选择映射地址
shmflg:SHM_RDONLY ——只读;0——可读可写
返回值:成功,返回映射后的首地址;失败,返回(void*)-1——当作指针进行操作;
3、对共享内存进行内存操作
凡是涉及内存操作的接口,都可以对共享内存进行操作:memcpy,strcpy,printf......
并且只能进行内存操作,IO操作不能在共享内存中使用
4、解除与进程间的映射关系
int shmdt(void* shmaddr);
shmaddr:shmat返回的映射首地址
5、删除共享内存
int shmctl(int shmid,int cmd,struct shmid_ds*buf);
共享内存只能通过以上代码手动释放,或者重启操作系统,系统自动释放;
shimid:shmget返回的操作句柄
cmd:要对共享内存进行的操作类型
IPC_RMID:标记要删除的共享内存;
并不是立即删除,当映射连接数为0时删除,并且禁止新的映射链接该共享内存
buf:用于获取或设置共享内存属性,删除时置NULL
返回值:成功,返回0;失败,返回-1
代码实例:
读端:
#include
#include
#include
#include
#define IPC_KEY 0x0123456
int main(){
//创建共享内存
//shmget(标识符,大小,打开方式和权限)
int shmid=shmget(IPC_KEY,32,IPC_CREAT|0664);
if(shmid<0){
perror("shmget error:");
return -1;
}
//与进程建立映射关系
//shmat(操作句柄,映射首地址,访问方式)
void * shm_start=shmat(shmid,NULL,0);
if(shm_start==(void*)-1){
perror("shmat error:");
return -1;
}
while(1){
printf("%s\n",(char *)shm_start);
sleep(1);
}
//解除与进程的映射关系
shmdt(shm_start);
//删除共享内存
//shmctl(操作句柄,操作类型,信息结构)
shmctl(shmid,IPC_RMID,NULL);
return 0;
}
写端:
#include
#include
#include
#include
#define IPC_KEY 0x0123456
int main(){
//创建共享内存
//shmget(标识符,大小,打开方式和权限)
int shmid=shmget(IPC_KEY,32,IPC_CREAT|0664);
if(shmid<0){
perror("shmget error:");
return -1;
}
void * shm_start=shmat(shmid,NULL,0);
if(shm_start==(void*)-1){
perror("shmat error:");
return -1;
}
int i=0;
while(1){
//sprintf--将格式化后的字符串放到指定的内存空间中
sprintf(shm_start,"好好学习,天天向上!-%d",i++);
sleep(1);
}
shmdt(shm_start);
shmctl(shmid,IPC_RMID,NULL);
return 0;
}
运行结果:
消息队列:实际上就是内核中的一个优先级队列(先进先出)
通信原理:在内核中创建一个优先级队列,多个进程通过对同一队列进行添加或者获取结点实现数据通信
特性:1、自带同步与互斥;2、生命周期随内核
信号量:就是一个内核中的
计数器
+pcb等待队列,用来实现进程间的同步与互斥
原理:对资源进行计数,在进程获取资源之前先通过计数判断获取是否合理;不合理则阻塞等待,直到条件满足后唤醒阻塞进程
同步的实现:获取资源前进行P操作
,合理则获取,不合理则阻塞;产生一次资源进行一次V操作
互斥的实现:计数最大为1,表示只有一个资源,访问前执行P操作
,在访问期间其他进程不可访问,访问完成后执行V操作
;
P操作
:计数-1,判断访问是否合理,不合理则阻塞;合理则正确返回
V操作
:计数+1,唤醒阻塞进程
ipcs
ipcs -m
:查看进程间共享内存信息
ipcs -q
:查看消息队列信息
ipcs -s
:查看信号量信息