为什么要进行进程间通信?
进程之间可能会存在特定的协同工作场景!也就意味着一个进程要把自己的数据交付给另外一个进程让其进行处理,这就叫做进程间通信
进程间通信发展
1.管道
2.System V进程间通信
3.POSIX进程间通信
这里我们主要讨论的是System V标准。标准的最大意义是它的接口,各种参数都是明确的
进程间通信分类
管道
匿名管道pipe
命名管道
System V IPC
System V 消息队列
System V 共享内存
System V 信号量
POSIX IPC
消息队列
共享内存
信号量
互斥量
条件变量
读写锁
进程间通信目的
数据传输:一个进程需要将它的数据发送给另一个进程
资源共享:多个进程之间共享同样的资源。
通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变
管道通信
我们之前使用的“|”就是管道样例
我们从cat这个命令中获得的数据,写在管道里,然后wc -l再从管道中读取,就是一个管道通信的流程
这是我们的管道函数
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
int main(){
int fds[2];
char buffer[100];
int len;
//pipe(fds[2])是管道函数的原型,fds[0]表示管道读端,fds[1]是管道写端
if(pipe(fds)==-1){
perror("make a pipe"),exit(1);
}
while(fgets(buffer,100,stdin)){//从键盘端获取我们输入的东西
len=strlen(buffer);
if(write(fds[1],buffer,len)!=len){
perror("write to pipe");
break;
}
memset(buffer,0,sizeof(buffer)==-1);
if((len=read(fds[0],buffer,100))==-1){
perror("read from pipe");
break;
}
if(write(1,buffer,len)!=len){
perror("write to stdout");
break;
}
}
return 0;
}
上面的代码也是一个管道的应用,如果你输入1234567890,输出如下:
现在详细学一下
匿名管道
我们把一个进程链接另一个进程中间的数据流称为管道
管道通信是如何进行的?
先来理解一下这个:
files_struct和struct file的区别?
每个进程在内核都有自己的PCB块,PCB块表现在Linux结构体叫task_struct,他包含了一个进程的各种信息,其中的一个信息就是我们的files_struct *fd_array[]。
我们的files_struct包含了进程中打开的文件的描述信息,是进程管理文件描述符(inode)的核心结构,他就像一个辅导员管理专业下的人,比较抽象的用一些标志概括了每个人,比如学号,就相当于inode(文件描述符),还有一些操作方法的集合,内核级文件的缓冲区
而struct file是描述进程下每个打开的文件的结构体,储存一个文件的相关状态信息,就像辅导员没时间了解每个学生的个人兴趣爱好,但是我们每个人都是不一样的,我们每个人不一样的信息就存储在struct file下,他们之间的联系就是靠struct_files struct来映射到具体的struct file
我们的匿名管道是基于父子进程通信的
上图的第一步:
可以看出我们的子进程先是复制了一份父进程的struct_files struct,因为我们的struct _files struct都一样嘛,所以他们映射的文件也是同一个文件嘛,所以上图中的两个进程指向了同一个文件。如果把struct file也复制一份的话,也能达到这个效果,但是这样就多此一举了,这样也会打破进程之间的独立性,父子进程就能看见对方了
第二步:当我们调用系统的读或写方法时,不是直接写入磁盘中,而是打开文件的内核缓冲区
当我们调用write方法时,除了调用底层实现他的部分,还需要把内容写到文件的内核缓冲区内,这个缓冲区也得被我们的struct file(也就是我们的文件)找到,然后操作系统定期把缓冲区的数据更新到磁盘中
本来我们的进程是独立的,互相看不见对方的内容,但是现在他们有一个公共资源:struct file
如果父进程将自己的数据写入了对应缓冲区中不刷新磁盘,那么子进程就可以通过fd找到同一个struct file读取到这个缓冲区的数据,然后把父进程这个不干事的把数据交给子进程,这就叫让不同进程看到同一份资源
基于这种的通信方式就叫管道通信:两个进程可以看见同一个文件,然后文件里面有缓冲区,一个向缓冲区写数据,一个向缓冲区读数据,就完成了通信
如图这就是:匿名管道通信的原理,通过管道函数,在进程中开辟两个文件来实现读写功能,再通过进程的拷贝,实现对同一个文件的读写,最终各自释放一个读/写端,实现单向通信
如何实现我们对一个缓冲区的又读又写?
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(){
int fd1=open("log.txt",O_WRONLY|O_CREAT,0644);
if(fd1<0){
perror("open");
return 1;
}
int fd2=open("log.txt",O_RDWR|O_CREAT,0644);
if(fd2<0){
perror("open");
return 1;
}
printf("fd1==%d\nfd2==%d\n",fd1,fd2);
return 0;
}
为什么这里的父进程有两个文件描述符?因为如果只打开一个,子进程继承缓冲区的时候也只能读了,不能达成写方法
我们不能选择向上面的代码一样把同一个文件打开两次,因为如果打开两次,读和写是实现了,但是我们的管道是单向通讯的(要想双向通讯应该建立两个管道)所以接下来就需要父子进程关闭自己多余的读或写端(取决于你希望谁读、谁写)
谁能做到这个?当然是我们的pipe函数啦
所以来拿我们上文提到的管道函数写一个父子进程间通信的实例吧:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(){
int pipefd[2]={0};
if(pipe(pipefd)!=0){
perror("pipe error");
return 1;
}
setbuf(stdout, NULL);
printf("fd1==%d\nfd2==%d\n",pipefd[0],pipefd[1]);
if(fork()==0){//子进程部分
close(pipefd[0]);//关掉读
const char* msg="hello linux";
while(1){
write(pipefd[1],msg,strlen(msg));
sleep(1);
}
exit(0);
}
close(pipefd[1]);//父进程部分,关掉写方法
while(1){
char buffer[64]={0};
ssize_t s=read(pipefd[0],buffer,sizeof(buffer));
if(s<=0){
break;
}else if(s>0){
buffer[s]=0;
printf("child say to father:%s\n",buffer);
//fflush(stdout);
}
}
return 0;
}
流程:子进程写入一行后睡觉,同时父进程刷新打印,这样就可以一行一行打印
ps:气死我了vscode,神经。在你给代码做更改的时候摆烂,不给你更新代码。非要你进行非运行调试,才给你更新啊闹谭
那如果让父进程睡呢?
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(){
int pipefd[2]={0};
if(pipe(pipefd)!=0){
perror("pipe error");
return 1;
}
setbuf(stdout, NULL);
printf("fd1==%d\nfd2==%d\n",pipefd[0],pipefd[1]);
if(fork()==0){//子进程部分
close(pipefd[0]);//关掉读
const char* msg="hello linux";
while(1){
write(pipefd[1],msg,strlen(msg));
//sleep(1);
}
exit(0);
}
close(pipefd[1]);//父进程部分,关掉写方法
while(1){
sleep(1);
char buffer[64]={0};
ssize_t s=read(pipefd[0],buffer,sizeof(buffer));
if(s<=0){
break;
}else if(s>0){
buffer[s]=0;
printf("child say to father:%s\n",buffer);
//fflush(stdout);
}
}
return 0;
}
就会一直刷
为什么一直刷?
父进程睡觉的时候,子进程一直在管道里写东西,所以父进程醒来一刷就可以刷很多东西
如果我们让父进程一直睡觉,让子进程在输入一个字符的时候再计数:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(){
int pipefd[2]={0};
if(pipe(pipefd)!=0){
perror("pipe error");
return 1;
}
setbuf(stdout, NULL);
int count=0;
printf("fd1==%d\nfd2==%d\n",pipefd[0],pipefd[1]);
if(fork()==0){//子进程部分
close(pipefd[0]);//关掉读
const char* msg="hello linux";
while(1){
// write(pipefd[1],msg,strlen(msg));
//sleep(1);
write(pipefd[1],"a",1);
printf("count==%d\n",count);
count++;
//write(pipefd[1],msg,strlen(msg));
}
exit(0);
}
close(pipefd[1]);//父进程部分,关掉写方法
while(1){
sleep(1);
char buffer[64]={0};
ssize_t s=read(pipefd[0],buffer,sizeof(buffer));
if(s<=0){
break;
}else if(s>0){
buffer[s]=0;
//printf("child say to father:%s\n",buffer);
//fflush(stdout);
}
}
return 0;
}
就会发现:
我们的count最多到65535,为什么?65536字节就是64kb。当写满64kb的时候,write就不再写入了,这是因为管道有大小!
其实就是我们的管道写满了,子进程进入阻塞等待,等有空位的时候就可以继续往里写
那如果我们让父进程睡上十秒,然后读取里面的字符“a",每读取一个里面就会多一个字符的大小
此时我们的子进程岂不是可以继续写了?我们的count是不是库继续增加呢?
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(){
int pipefd[2]={0};
if(pipe(pipefd)!=0){
perror("pipe error");
return 1;
}
setbuf(stdout, NULL);
int count=0;
printf("fd1==%d\nfd2==%d\n",pipefd[0],pipefd[1]);
if(fork()==0){//子进程部分
close(pipefd[0]);//关掉读
const char* msg="hello linux";
while(1){
// write(pipefd[1],msg,strlen(msg));
//sleep(1);
write(pipefd[1],"a",1);
printf("count==%d\n",count);
count++;
//write(pipefd[1],msg,strlen(msg));
}
exit(0);
}
close(pipefd[1]);//父进程部分,关掉写方法
while(1){
sleep(10);
char c;
char buffer[64]={0};
ssize_t s=read(pipefd[0],buffer,sizeof(buffer));
read(pipefd[0],&c,1);
printf("father take:%c\n",c);
if(s<=0){
break;
}else if(s>0){
buffer[s]=0;
//printf("child say to father:%s\n",buffer);
//fflush(stdout);
}
}
return 0;
}
事实上父进程确实在读取,但是子进程没反应啊?!
我们让父进程每次多读几个字节:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(){
int pipefd[2]={0};
if(pipe(pipefd)!=0){
perror("pipe error");
return 1;
}
setbuf(stdout, NULL);
int count=0;
printf("fd1==%d\nfd2==%d\n",pipefd[0],pipefd[1]);
if(fork()==0){//子进程部分
close(pipefd[0]);//关掉读
const char* msg="hello linux";
while(1){
// write(pipefd[1],msg,strlen(msg));
//sleep(1);
write(pipefd[1],"a",1);
printf("count==%d\n",count);
count++;
//write(pipefd[1],msg,strlen(msg));
}
exit(0);
}
close(pipefd[1]);//父进程部分,关掉写方法
while(1){
sleep(10);
char c;
char buffer[1024*4]={0};
ssize_t s=read(pipefd[0],buffer,sizeof(buffer));
//read(pipefd[0],&c,1);
//printf("father take:%c\n",c);
if(s<=0){
break;
}else if(s>0){
buffer[s]=0;
printf("child say to father:%c\n",buffer[0]);
//fflush(stdout);
}
}
return 0;
}
你会发现count现在++了,在到64kb的时候等十秒,就会继续++
为什么?
我们要保证写入或者读取的原子性
- 当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。
- 当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。
如果父进程读的比子进程写的还快,那是不是就变成父进程等子进程了(读端等写端)
如果子进程写一半自己先退了,还把写端文件描述符关了,父进程会通过read的返回符判断
子进程退出之后,父进程读到子进程的消息后发现read返回值为0,就也退出了。所以如果写端关闭,读端就直接读到文件结尾;
如果写端一直在写,读端读了几条就退了,也关闭了读端描述符怎么办?
操作系统会直接把你们这些抽象的进程回收了(写端进程被OS直接用13号信号关掉,相当于进程出现了异常)
这就叫匿名管道
匿名管道只能用作父子间通信,同样我们也可以实现爷孙通信,俩fork就行了呗(下次再写)
除了匿名管道还有命名管道,命名管道可以解决匿名管道只能父子间通信的弊端