一、本章重点
1、什么是管道?
2、匿名管道
3、用一个进程遥控另一个进程
4、基于匿名管道的进程池
5、命名管道
01 什么是管道?
1、管道本质就是一段由操作系统维护的内核级别的缓冲区,简单点就是一段内存,只不过这段内存是为进程通信而存在的。
2、进程间通信的成本是比较高的,因为进程的独立性,当父子之间要发生写数据时,常常发生写实拷贝,因而需要操作系统专门提供一段特殊的缓冲区,让进程之间通信。
3、为什么需要进程通信?因为常常需要进程之间交互数据,协同完成任务。
4、管道分为两种:匿名管道和命名管道。
02 匿名管道
1、故名思议,就是没有名字的管道,该管道只能用于具有血缘关系的两个进程通信,常用于父子。
2、管道具体在哪?管道那么多,父子如何看待相同的管道?
这里需要解释的是:子进程会继承父进程的文件描述符表信息,也就是说,父进程打开的文件,子进程默认也打开了。
以下是证明:
1 #include<stdio.h> 2 #include<unistd.h> 3 #include<sys/types.h> 4 #include<sys/stat.h> 5 #include<fcntl.h> 6 #include<stdlib.h> 7 #include<string.h> 8 9 int main() 10 { 11 umask(0); 12 int fd = open("log.txt",O_WRONLY | O_CREAT | O_TRUNC , 0666); 13 if(fd<0) 14 { 15 perror("open"); 16 } 17 18 if(fork()==0) 19 { 20 const char* msg = "haha\n"; 21 write(fd,msg,strlen(msg)); 22 exit(1); 23 } 24 return 0; 25 }
3、匿名管道具有以下特性
①只有血缘关系才能够使用,常见于父子通信。
②已经实现了同步互斥功能
③面向字节流
④生命周期随进程
⑤半双工
4、命令行中常用的 '|' 是匿名管道
证明:
sleep100 和sleep 200是兄弟关系
5、匿名管道简单实现进程之间交互数据
#include<iostream> #include<cstring> #include<errno.h> #include<cstdlib> #include<unistd.h> #include<stdio.h> using namespace std; int main() { int pipefd[2] = {0}; if(pipe(pipefd)!=0) { cerr<<"pipe: "<<strerror(errno)<<endl; } int ret = fork(); if(ret == 0) { //我们让子进程读,父进程写,因此子进程需要关闭写端。 close(pipefd[1]);//1代表写端,0代表读端 char buf[1024] = { 0 }; while(true) { ssize_t s = read(pipefd[0],buf,sizeof(char)*1024); if(s < 0) { cerr<<"read error"<<endl; exit(1); } if(s == 0) { cerr<<"写端关闭"<<endl; exit(2); } if(s > 0) { buf[s] = '\0'; cout<<"子进程收到的数据: "<<buf<<endl; } } close(pipefd[0]); } else if(ret > 0) { //让父进程写数据,父进程关闭读端 close(pipefd[0]); char msg[1024] = { 0 }; while(true) { if(cin>>msg) { ssize_t s = write(pipefd[1],msg,strlen(msg)); } else { break; } } close(pipefd[1]); } else { cerr<<"fork error"<<endl; } return 0; }
5、匿名管道简单实现一个进程控制另一个进程
#include<iostream> #include<cstring> #include<errno.h> #include<cstdlib> #include<unistd.h> #include<stdio.h> #include<vector> using namespace std; typedef void(*task)(); vector<task> tasks; void func1() { cout<<"进程"<<getpid()<<"正在执行处理日志任务"<<endl; } void func2() { cout<<"进程"<<getpid()<<"正在执行网络传输任务"<<endl; } void func3() { cout<<"进程"<<getpid()<<"正在执行数据库交互任务"<<endl; } void func4() { cout<<"进程"<<getpid()<<"正在执行安全检测任务"<<endl; } void LoadTask() { tasks.push_back(func1); tasks.push_back(func2); tasks.push_back(func3); tasks.push_back(func4); } int main() { LoadTask(); int pipefd[2] = {0}; if(pipe(pipefd)!=0) { cerr<<"pipe: "<<strerror(errno)<<endl; } int ret = fork(); if(ret == 0) { //我们让子进程读,父进程写,因此子进程需要关闭写端。 close(pipefd[1]);//1代表写端,0代表读端 int options = 0; while(true) { ssize_t s = read(pipefd[0],&options,sizeof(int)); if(s == 0) { cerr<<"写端关闭"<<endl; exit(2); } if(s < 0) { cerr<<"read error"<<endl; exit(1); } if(s == 4) { tasks[options-1](); } else { cerr<<"bug? s = "<<s<<endl; } } close(pipefd[0]); } else if(ret > 0) { //让父进程写数据,父进程关闭读端 close(pipefd[0]); int options = 0; while(true) { if(cin>>options) { ssize_t s = write(pipefd[1],&options,sizeof(int)); } else { break; } } close(pipefd[1]); } else { cerr<<"fork error"<<endl; } return 0; }
5、基于匿名管道的进程池
下面是一个简单的模型
#include<iostream> #include<cstring> #include<errno.h> #include<cstdlib> #include<unistd.h> #include<stdio.h> #include<vector> #include<ctime> #include<sys/wait.h> using namespace std; typedef void(*task)(); vector<task> tasks; typedef pair<int,int> ele; vector<ele> controls; void func1() { cout<<"进程"<<getpid()<<"正在执行处理日志任务"<<endl; } void func2() { cout<<"进程"<<getpid()<<"正在执行网络传输任务"<<endl; } void func3() { cout<<"进程"<<getpid()<<"正在执行数据库交互任务"<<endl; } void func4() { cout<<"进程"<<getpid()<<"正在执行安全检测任务"<<endl; } void LoadTask() { tasks.push_back(func1); tasks.push_back(func2); tasks.push_back(func3); tasks.push_back(func4); } int main() { LoadTask(); int num = 10; //创建num个进程 for(int i = 0 ; i < num ; i++) { int pipefd[2] = { 0 }; if(pipe(pipefd)!=0) { cerr<<"pipe error"<<endl; } int id = fork(); if(id == 0) { //child close(pipefd[1]); int options = 0; while(true) { ssize_t s = read(pipefd[0],&options,sizeof(int)); if(s == 0) { cerr<<"写端关闭"<<endl; exit(2); } if(s < 0) { cerr<<"read error"<<endl; exit(1); } if(s == 4) { tasks[options](); } else { cerr<<"bug? s = "<<s<<endl; } } close(pipefd[0]); exit(1); } else { close(pipefd[0]); ele e(pipefd[1] , id); controls.push_back(e); } } //父进程 while(true) { sleep(1); srand(time(0)); int select = rand() % num; int opt = rand() % tasks.size(); int fd = controls[select].first; int id = controls[select].second; write(fd,&opt,sizeof(int)); cout<<"父进程发了一个任务给"<<id<<endl; } for(int i = 0;i < num ;i++) { waitpid(-1,0,0); } return 0; }
可以看出父进程在随机的给进程派发任务,不至于只给一个进程派发,导致其他进程饿死的现象,实现了负载均衡。
03 命名管道
一、介绍命名管道
1、顾名思义,就是有名字的管道。
2、为啥有名字,因为它要创建一个磁盘文件,名字指的是磁盘文件的名字。
3、创建磁盘文件不代表是要进行IO的读写,命名管道还是在内存中进行数据交互。
4、匿名管道和命名管道相同点和不同点
相同点:以管道通信的两个进程指向的都是同一个内存文件,并在此基础上建立管道。
不同点:文件描述符获取的方式不同,匿名管道的文件描述符来源于父子继承,而命名管道的文件描述符来源于打开同一个磁盘文件。
图解:
二、完成命名管道的client和server编程
我们使用mkfifo函数来创建命名管道,第一个参数是要创建的磁盘管道文件的路径,第二个参数是设置初始权限,需要注意的是初始权限受umask的影响。
返回值:调用成功返回0,否则返回-1。
server.cc
#include "common.h" int main() { umask(0); if(mkfifo(PATH,0666)!=0) { cerr<<"mkfifo error"<<endl; return 1; } int fd = open(PATH,O_RDONLY); if(fd < 0) { cerr<<"open error"<<endl; return 2; } #define SIZE 1024 char buf[1024] = {0}; while(true) { ssize_t s = read(fd,buf,sizeof(buf)-1); if(s==0) { cout<<"写端关闭"<<endl; unlink(".fifo"); close(fd); exit(0); } else if(s>0) { buf[s] = '\0'; cout<<"client->server: "<<buf<<endl; } else { cerr<<"read error"<<endl; close(fd); exit(3); } } return 0; }
这里我在写端关闭的时候调用了unlink(),它负责删除管道文件,防止下次运行server的时候创建管道失败,参数是文件名。
client.cc
#include "common.h" int main() { int fd = open(PATH,O_WRONLY | O_APPEND); if(fd<0) { cerr<<"open error"<<endl; } while(true) { char msg[1024] = {0}; cout<<"请输入信息# "; fflush(stdout); if(fgets(msg,sizeof(char)*1024,stdin)!=nullptr) { //msg为 xxxx\n\0 //下面的write我只传了xxxx write(fd,msg,strlen(msg)-1); } else { exit(0); } } return 0; }
common.h
#include<iostream> #include<stdio.h> #include<cstdlib> #include<sys/types.h> #include<sys/stat.h> #include<cstring> #include<fcntl.h> #include<unistd.h> #define PATH "./.fifo" using namespace std;