进程间通信
进程间具有独立性,那么如果想要进程之间进行信息交互,应该怎么办?
如果想要两个进程之间交互,就需要这两个进程能够访问同一份资源(文件,内存块……)
资源的不同决定了不同的通信方式,在这里采用管道文件
管道
匿名管道
管道的原理
父进程创建子进程,子进程相关的数据结构(
task_struck ,mm_struct,files_struct
)继承自父进程(就是把父进程的数据结构拷贝一份),那么在父进程的files_struct
中的指针数组和子进程的files_struct
中的指针数组指向同一批文件,那么这些文件就是同一份资源,也就是管道文件,但是管道文件的数据只存储在内核缓冲区中,不会向磁盘中刷新管道的特点
管道传输数据是单向的
制作一个匿名管道
1、父进程分别用读和写的方式打开文件 2、创建子进程 3、根据需要关闭父子进程的写端或读端
管道的代码
#include<iostream> #include<string.h> #include<unistd.h> #include<fcntl.h> #include<sys/types.h> #include<sys/wait.h> #include<time.h> #define NUM 1024 using namespace std; int main() { int pipefd[2]={0}; if(pipe(pipefd)!=0) //父进程以读和写的方式打开管道文件 { return 1; } pid_t id=fork(); //创建子进程 if(id<0) { cout<<"error"<<endl; } else if(id==0) { //子进程关闭读端 close(pipefd[0]); //向管道文件写入 char mag[NUM]; char tmp[]="hello world"; int cnt=5; while(cnt--) { sprintf(mag,"时间戳:%ld,%s\n",time(nullptr),tmp); write(pipefd[1],mag,strlen(mag)); sleep(1); } close(pipefd[1]); //写完,关闭写端 exit(1); //子进程退出 } else { //父进程关闭写端 close(pipefd[1]); //读出 char mag[NUM]; while(1) { memset(mag,0,sizeof(mag)); int judge=read(pipefd[0],mag,sizeof(mag)-1); mag[strlen(mag)]='\0'; if(judge>0) { printf("父进程读取子进程成功:%s\n",mag); } else if(judge==0) { printf("父进程读取结束\n"); break; } else { exit(2); } } close(pipefd[0]); //关闭读端 int status=0; waitpid(id,&status,0); //父进程等待子进程 cout<<"hello code"<<endl; } return 0; }
在上述代码中,读端并没有设置sleep(1),但是也是会休眠一秒,为什么?
因为当父进程读取管道文件时,管道文件中没有数据,无法读取数据,阻塞等待
等到子进程向管道中写入数据,父进程才可以读取文件,这时管道文件中有没有数据了,等一秒后,子进程再向管道中写入数据,父进程再读取,所以父进程才会和子进程一样休眠1秒。
这就是管道内部自带的访问控制机制(同步和互斥机制)
除此之外,如果父进程一直不读取文件,而子进程一直向管道文件中写入,如果管道文件被写满了,那么子进程写入管道文件,也会发生阻塞等待,只有父进程读取文件,管道中有空间了,才能继续写入。
在管道文件写满或为空,发生阻塞等待时,进程
pcb
在那个资源的等待队列上?在管道文件的等待队列上,等到资源就绪了,再回到运行队列。
read函数的返回值
管道的读端可以感知到写端是否关闭,因为在
struct file
中的文件属性有一个引用计数,引用计数记录了多少个指针指向这个文件,写端关闭,read就可以读到文件结尾,返回值为0
进程控制
我们已经实现了使用匿名管道实现父子进程的通信,接下来介绍如何使用父进程控制子进程
父进程控制单个子进程
原理:
代码:
typedef void(*functor)(); vector<functor>functors; //函数方法集 unordered_map<int,string> info; //记录编号对应的方法 void f1() { cout<<"方法1"<<endl; } void f2() { cout<<"方法2"<<endl; } void f3() { cout<<"方法3"<<endl; } //加载方法 void LoadFunctors() { functors.push_back(f1); info.insert({functors.size(),"方法1"}); functors.push_back(f2); info.insert({functors.size(),"方法2"}); functors.push_back(f3); info.insert({functors.size(),"方法3"}); cout<<"方法集加载成功"<<endl; } int main() { LoadFunctors(); int pipefd[2]={0}; //父进程以读和写的方式打开管道文件 if(pipe(pipefd)!=0) { cerr<<"pipe error"<<endl; return 1; } pid_t id=fork(); if(id==0) { //子进程关闭写端 close(pipefd[1]); int tag=0; while(1) { //从管道文件中读出父进程写入的任务编号 int dia=read(pipefd[0],&tag,4); if(dia==4) { //执行方法 if(tag<functors.size()) { cout<<"子进程执行方法: "; functors[tag](); } else { cout<<"error"<<endl; } } else if(dia==0) { //方法执行结束 cout<<"方法执行结束"<<endl; break; } else { //读取失败 } } //子进程关闭读端 close(pipefd[0]); //子进程退出 exit(0); } else { //父进程关闭读端 close(pipefd[0]); srandom(time(nullptr)); while(1) { int tag=random()%functors.size(); //向管道文件中写入任务编号 write(pipefd[1],&tag,4); cout<<"父进程指派任务:编号:"<<tag+1<<"任务为:"<<info[tag+1]<<endl; sleep(2); } } return 0; }
父进程控制多个子进程
原理:
代码:
typedef void(*functor)(); vector<functor>functors; unordered_map<int,string> info; vector<pair<int,int>> free_info; void work(int fd) { while(1) { int tag=0; int dia=read(fd,&tag,sizeof(tag)); if(dia==4) { cout<<"pid: "<<getpid()<<" 执行任务: "; functors[tag](); } else if(dia==0) { cout<<"pid: "<<getpid()<<" 任务执行结束"<<endl; break; } else { //DO NOTHING } } } void send(vector<pair<int,int>> _info) { srand(time(nullptr)); while(1) { int tag1=random()%_info.size(); int tag2=random()%functors.size(); write(_info[tag1].second,&tag2,sizeof(tag2)); sleep(1); cout<<"父进程分配给子进程 pid: "<<_info[tag1].first<<"任务编号: "<<tag2+1<<endl; } } int main() { //加载方法集 LoadFunctors(); //循环创建多个子进程 int cnt=3; for(int i=0;i<cnt;i++) { int pipefd[2]={0}; pipe(pipefd); pid_t id=fork(); if(id==0) { //关闭写端 close(pipefd[1]); //子进程,执行任务 work(pipefd[0]); close(pipefd[0]); exit(0); } //父进程关闭读端 close(pipefd[0]); //保存子进程pid,父进程的写端文件描述符 free_info.push_back(pair<int,int>(id,pipefd[1])); } //父进程分配任务 send(free_info); //关闭父进程写端,回收子进程 for(int i=0;i<cnt;i++) { int status=0; close(free_info[i].second); waitpid(free_info[i].first,&status,0); } return 0; }
管道特征总结
- 匿名管道只能用来进行具有血缘关系的进程之间,进行进程间通信,常用于父子进程
- 管道只能单向通信,半双工
- 管道自带同步机制(管道文件满,就不能再向管道文件中写入;管道文件空,就不能从管道文件中读出)
- 管道是面向字节流的(先写入的字符,先被读出,没有格式边界,需要用户来定义区分内容的边界)
- 管道的生命周期————管道是文件,进程退出了,曾经打开的文件会随着进程一起退出
命名管道
匿名管道只能使具有血缘关系的进程进行通信,那毫不相干的进程如何进行通信?
使用命名管道
命名管道原理
进程通信的本质就是不同的进程看到同一份资源,匿名管道是通过创建子进程,继承符号表来实现;而命名管道则是通过打开磁盘上的同一份文件(命名管道),向这个文件写入,读出,同样这个数据不会向磁盘中写入,磁盘中的命名管道文件只是符号。
int mkfifo(const char *pathname(路径), mode_t mode(权限)); //创建命名管道文件(创建命名管道文件时,命名管道文件不能已存在)
comm.h
#pragma once #include<iostream> #include<sys/types.h> #include<sys/stat.h> #include<fcntl.h> #include<cstdio> #include<cstring> #include<unistd.h> #include<cerrno> #define IPC_PATH "./.fifo_1" #define NUM 1024
Clientfifo.cpp
//客户端 #include "comm.h" using namespace std; int main() { //打开管道文件 int pipefd=open(IPC_PATH,O_WRONLY); //写入 char line[NUM]; while (1) { cout<<"pid: "<<getpid()<<"客户端输入: "; fgets(line,NUM,stdin); line[strlen(line)-1]='\0'; write(pipefd,line,strlen(line)); sleep(1); } close(pipefd); return 0; }
Serverfifo.cpp
//服务器端 #include "comm.h" using namespace std; int main() { //创建命名管道文件 if(mkfifo(IPC_PATH,0600)!=0) { cerr<<"fifo error"<<endl; exit(1); } //打开文件 int pipefd = open(IPC_PATH,O_RDONLY); //读取数据 char line[NUM]; while(1) { int dia = read(pipefd,line,NUM); if(dia>0) { line[dia]='\0'; cout<<"pid: "<<getpid()<<" 服务器端 读取:"<<line<<endl; } else if(dia==0) { cout<<"读取结束"<<endl; break; } else { //do nothing } } close(pipefd); unlink(IPC_PATH); return 0; }