进程间是如何进行通信的?
通过前面的学习之后,我们知道进程间是具有独立性的,在操作系统的层面来看,进程就是一块pcb,是对运行中的程序动态运行过程的描述,在Linux角度下,进程就是一个task_struct结构体,pcb里面的信息存放在虚拟地址空间并通过页表来映射到一块物理内存上,每一个进程都对应着其自己的pcb,所以说进程间是相互独立的,那么如何使这一些相互独立的pcb之间能够形成通信呢?
Linux中这这么一些通信方式、管道、消息队列、共享内存、信号量……他们建立进程间通信都是通过同样的一种关联关系:能够共同访问的同一块内存
一、管道
1、介绍
管道的灵感来源于生活,管道在生活中随处可见,排水输水管道、自来水管……
Linux中也引入了这一想法:并根据通信传输方向分为
单工通信:单向通信,比如有两端,只能从A->B
双工通信:双向通信,比如有两端,既可以从A->B,也可以从B->A
半双工通信:一种可以选择方向的单向通信,比如有A和B两端,要么从A->B,要么从B->A
(同一时间不能同时既发送又接收)
管道的本质:操作系统为进程间通信提供了一个空间交叉点,进程间都能访问这个交叉点,
在程序中,管道就是内核的一块缓冲区(缓冲区本质就是内核中的一块内存)
2、匿名管道
定义:没有文件描述符,不能被其他进程找到,所以只能用于具有亲缘关系进程间通信
实现原理概述:
一个进程创建一个匿名管道,(就是在内核空间中创建一块缓冲区,也就是一块内存或者说一个文件),然后创建之后,给该进程返回一个文件描述符,这时候我们创建一个或多个子进程,那么子进程就会复制父进程大部分的描述信息其中就有该管道文件的描述符,那么子进程就可以与该父进程对同一块文件进程进行读写实现通信。
如果这时候再创建一个子进程,那么这个子进程和父进程可以进行通信,当然也可以与之前创建的子进程进行通信,该子进程再创建一个子进程(父进程的孙进程)当然,它们之间也能进行通信
接口:
int pipe(int pipefd[2])
功能:创建一个管道,并通过参数返回管道的俩个操作句柄
参数:pipefd - 具有2个整形元素的数组,内部创建管道会将描述符存储在数组中
pipefd[0] -- 用于从管道中读数据
pipefd[1] -- 用于向管道中写数据
返回值:成功返回0,失败返回-1
注意:创建匿名管道前,一定要在创建子进程之前
进行一段代码通信:在第一个子进程中向管道写入数据,然后第二个子进程进行读取数据
#include<stdio.h> #include<unistd.h> #include<string.h> #include<sys/wait.h> #include<stdlib.h> int main() { int pipefd[2]; // [0] 为读 // [1] 为写 int ret = pipe(pipefd); if(ret < 0){ perror("pipe error"); return -1; } pid_t pid1 = fork(); if(pid1==0){ // 兄 子进程 const char *str = "我是哥哥,新年好啊\n"; write(pipefd[1], str, strlen(str)); // 向管道中写入数据 exit(0); } pid_t pid2 = fork(); if(pid2 == 0){ // 弟 子进程 char buf[1024] = {0}; read(pipefd[0], buf, 1023); // 从管道中读取数据 printf("%s",buf); exit(0); } wait(NULL); wait(NULL); return 0; }
管道特性
1、当管道中没有数据时,读端就会阻塞,直到有数据被写入管道才会进行读取
2、当管道中数据满了,那么写端就会阻塞,等待管道中数据被读取,有空间可以写入了才会进行写入数据。
3、当管道的所有读端被关闭,再次写入数据就会导致程序崩溃
因为没有进程来进行读取了,那么在进行写入都是没有意义的,所以系统就进行处理
4、当管道的所有写端被关闭,那么读端读取完管道中的所有数据后,将不在阻塞等待,而是返回0
所以没有意义的事情大佬就会替我们考虑好,读端关闭写入就会报错,写端关闭读取完剩余就会退出返回0
2、命名管道
具有标识符的管道,其他进程可以找到。
实现概述:
进程通过mkfifo创建一个管道文件,这个管道文件的标识符可以被其他进程访问到,当A进程通过这个管道文件描述符访问这个缓冲区,进行写入数据,那么B进程打开通过这个管道文件访问管道,可以进行数据读取
注意点:
当创建管道文件时,并不是直接就将数据传输管道(内存中的缓冲区)创建好了,而是只有在有进程打开这个管道进行数据访问时,才会真正的创建这个管道。(联想写时拷贝技术)。
命名管道的独有特性:(没读端,只写就会阻塞 || 没写端,只读就会阻塞)
只以只写的方式打开管道,写端就会被阻塞,直到管道有以可读的进程访问
当只以只读方式打开管道,读端就会阻塞,直到管道有以可写的进程访问
原理:因为当一个管道没有构成读写都满足的条件时,这个管道是没有意义的,也就没有必要创建这个缓冲区。(也正好对应上方只有真正满足数据传递的时候才会真正创建这个缓冲区)
接口:
int mkfifo(char* pathname, mode_t mode) 头文件#include<sys/stat.h>
pathname (创建的管道文件路径 如果当前路径下可以 ./pipe.fifo)
mode (创建文件的访问权限)
返回值:成功返回0,失败返回-1;
读端进程:
#include<stdio.h>
#include<fcntl.h>
#include<sys/stat.h>
#include<string.h>
#include<stdlib.h>
#include<unistd.h>
#include<errno.h>
int main()
{
umask(0);
int ret = mkfifo("./named_pipe.fifo", 0664);
if(ret < 0 && errno!=EEXIST){ // 因为创建管道的文件如果存在那么访问就会出错,加入errno判断可以去掉该报错
perror("mkfifo error");
return -1;
}
int fd = open("named_pipe.fifo", O_WRONLY);
if(fd < 0){
perror("open error");
return -1;
}
while(1)
{
printf("A进程说:");
fflush(stdout);
char buf[1024] = {0};
scanf("%s",buf);
int ret = write(fd, buf, strlen(buf));
if(ret < 0){
perror("write error");
close(fd);
return -1;
}
}
close(fd);
return 0;
}
写端进程:
#include<stdio.h>
#include<fcntl.h>
#include<sys/stat.h>
#include<string.h>
#include<stdlib.h>
#include<unistd.h>
#include<errno.h>
int main()
{
umask(0);
int ret = mkfifo("./named_pipe.fifo", 0664);
if(ret < 0 && errno!=EEXIST){ // 因为创建管道的文件如果存在那么访问就会出错,加入errno判断可以去掉该报错
perror("mkfifo error");
return -1;
}
int fd = open("named_pipe.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("所有的写端被关闭,读端读取完数据,返回0\n");
close(fd);
return -1;
}
printf("%s\n", buf);
}
close(fd);
return 0;
}
3、管道学习总结
本质:
内存中的一块缓冲区,多个进程通过访问同一块缓冲区来实现通信
分类:
匿名管道:只能用于具有亲缘关系的进程间通信(只能通过子进程复制父进程访问获取操作句柄)
命名管道:可用于同一主机的任意进程间通信(通过打开同一个管道文件来访问同一块缓冲区)
特性:
1)半双工通信
2)管道声明周期随进程(不需要人为关闭,当所有打开管道的进程退出后,管道释放)
3)提供字节流传输服务(数据先进进出、按序到达、不会丢失数据、面向连接)
所有读端被关闭,继续写就会异常;所有写端被关闭,读端读完剩余不在阻塞返回0
4)自带同步与互斥
互斥:通过同一时间对共享资源的唯一访问,保证原子性
原子性:具有不可分割特性 原子操作:一个操作不会被打断
同步:通过进程对资源的访问限制,让进程对资源访问更加合理
饥饿问题:不会出现A进程一直在写,另外B进程一直等着。
数据满了 write进程阻塞,没有数据read阻塞。
二、共享内存
1、简介
作用:实现多个进程之间的数据共享
特性:最快的数据传输方式
生命周期随内核(删除并非直接删除,而是拒绝后续映射,当映射连接数为0时,表示没有进程访问了,操作系统才会对其回收资源)
原理:开辟出一块物理内存,然后多个进程都将这块内存映射到自己的虚拟地址上,再通过虚拟地址来访问一块空间中的数据。
通过访问同一块物理内存,将物理地址映射到各自的虚拟地址中,进行数据访问。
管道通信通过在内核中开辟的缓冲区,共同访问这一块缓冲区,数据传输是从发送端拷贝到管道中,接收端再从管道中拷贝取出数据,而共享内存就不需要这俩次拷贝处理,所以它是最快的数据传输方式。
2、流程
1)、创建/打开指定共享内存
2)、将内存映射到自己的虚拟地址空间
3)、内存操作……
4)、解除映射关系
5)、删除共享内存
1)创建/打开指定共享内存
头文件 #include<sys/shm.h>
int shmget (key_t key, size_t size, int shmflg)
key:共享内存的标识符(名字)
size:要创建的共享内存大小,最好是PAGE_SIZE的整数倍
shmfg:IPC_CREAT | IPC_EXCL | 0664
IPC_CUEAT:如果共享内存不存在则创建打开,存在则直接打开
IPC_EXCL: 与IPC_CREAT搭配使用,共享内存不存在则创建打开,存在则报错返回
mode_flags: 共享内存的访问权限 0664
返回值:成功返回一个操作句柄(非负整数)、失败返回-1;
2)将共享内存映射到当前进程的虚拟地址空间
获取到首地址后,就可以通过首地址访问内存中的数据,以及可以修改内存中的数据
头文件 #include<sys/shm.h>
void *shmat(int shmid,const void *shmaddr, int shmflg);
shmid:之前shmget打开共享内存返回的操作句柄
addr:映射首地址,通常置为NULL,让操作系统进行分配
shmflag:默认为0表示可读可写,SHM_RDONLY 表示只读
(前提该共享内存创建的权限允许)
返回值:成功返回映射首地址,失败返回(void*)-1
3)解除映射关系
int shmdt(const void *shmaddr);
shmaddr:映射首地址,也就是shmat的返回值
4)数据交互
对内存内容进行修改或者读取
5)删除共享内存
int shmctl(int shmid, int cmd,struct shmid_ds *buf)
shmid:创建共享内存的返回值
cmd:对共享内存指向的操作命令
使用IPC_RMID:标记一个共享内存段需要被删除
buf:当cmd命令为接收获取共享内存信息时的容器(删除命令不需要用)
真的会实际删除这个共享内存嘛?
不会的,正如上面命令所说的,只会进行一次标记,标记的共享内存将不会接受新的映射而是等当前的映射连接计数为0时,再实际删除。
真正的删除是由系统执行的,进程所做的删除操作,只是进程标记一下。
3、代码模拟通信
给共享内存中写入数据
#include<stdio.h>
#include<sys/shm.h>
#include<unistd.h>
#define SHM_KEY 0x12345678
int main()
{
// 创建/打开共享内存
int shmid = shmget(SHM_KEY, 4096, IPC_CREAT | 0664);
if(shmid < 0){
perror("shget error");
return -1;
}
// 进行组织映射
void *start = shmat(shmid, NULL, 0);
if(start == (void*)-1){
perror("shmat error");
return -1;
}
// 进行数据交互
int id = 0;
while(1)
{
sprintf(start, "已经传输了%d内容到共享内存中\n", id++);
sleep(1);
}
// 解除映射
shmdt(start);
shmctl(shmid, IPC_RMID, NULL);
return 0;
}
使用共享内存读取数据
#include<stdio.h>
#include<sys/shm.h>
#include<unistd.h>
#define SHM_KEY 0x12345678
int main()
{
// 创建/打开共享内存
int shmid = shmget(SHM_KEY, 4096, IPC_CREAT | 0664);
if(shmid < 0){
perror("shget error");
return -1;
}
// 进行组织映射
void *start = shmat(shmid, NULL, 0);
if(start == (void*)-1){
perror("shmat error");
return -1;
}
// 进行数据交互
while(1)
{
printf("%s\n", start);// 之前进行映射后返回的就是共享内存的地址
sleep(1);
}
// 解除映射
shmdt(start);
shmctl(shmid, IPC_RMID, NULL);
return 0;
}
三、进程间通信所学命令小结
ipcs 查看进程间通信资源/ipcrm 删除进程间通信资源
-m 针对共享内存的操作
-q 针对消息队列的操作
-s 针对信号量的操作
-a 针对所有资源的操作
使用ipcs -m命令获取共享内存通信方式信息
ipcsrm -m shmid 删除shmid对应的共享内存。
注意删除的时候,如果没有其他进程访问会直接删除共享内存,如果有其他进程访问则会将共享内存键值设置为一个无法访问的量。
ipcs 获取当前进程间通信所有方式信息
底行模式下使用 :1,2s/shmread/shmwrite/g
将1-2行中的shmread修改为shmwrite。
四、消息队列
功能:实现进程间通信
本质:内核中的一个优先级队列
实现:多个进程通过访问同一个消息队列,以添加数据结点和获取数据结点方式实现通信
结点中一个结构为type类型(身份标识、优先级比较方式)
第二个结构为data存放数据
特性:
声明周期随内核
以数据块传输
自带同步与互斥
五、信号量
作用:用于实现进程间的同步与互斥
本质:计数器 + pcb队列
P、V操作:
P操作:对计数器进行-1操作,判断计数是否大于等于0,正确则返回;失败则阻塞
V操作:对计数器进行+1操作,判断计数是否小于等于0,释放一个资源,如果有进程等待中,则唤醒一个等待的进程
同步实现:
获取资源之前进行P操作,判断是否有资源可使用,如果条件满足则访问,否则阻塞
当产生一个资源时,进行V操作,唤醒阻塞的进程
互斥实现:
初始计数器为1,表示资源只有一个
访问资源之前进程P操作、访问资源完毕后进行V操作。