Linux:进程通信、管道通信
目录
一、认识进程通信
二、管道通信
1.匿名管道
2.命名管道
一、认识进程通信
在了解进程之间的通信之前,我们知道进程具有独立性,不会干扰彼此的运行,但是进程是可以互相发送消息的,这种通信不会影响进程的独立。
- 进程通信的目的
- 进程通信的本质
进程之间通信的本质,就是让两个进程同时操作同一份资源。这份资源只能由操作系统提供,不能由进程之间的任何一方提供,否则,就会破坏进程的独立性。
- 进程通信的分类
二、管道通信
- 管道分为匿名管道和命名管道。
1.匿名管道
匿名管道通常用于具有亲缘关系的进程之间,比如父子进程之间。
管道通信是半双工模式通信,即只能单方向通信。
匿名管道文件是内存级文件,即用即销毁,在磁盘中没有其对应的存储。
- 从文件描述符角度理解匿名管道
一个进程的PCB关联着一张文件描述符表,0、1、2默认是被三个标准流占用的,如果一个进程以'r'的方式打开一个文件,又以'w'的方式打开同一个文件,在文件描述符表中是会占用两个文件描述符的。
在父进程fork创建子进程,发生浅拷贝,各自的fd指向的内容是相同的。
由于匿名管道是单向通信,因此通信时,只能有一个进程在读管道,也只能有一个进程在向管道写数据,所以,父进程和子进程管道通信之前,父子进程要各自关掉一个描述符,(由于此时的文件被多个进程打开,当其中一个进程关文件,则只是将文件的引用计数减一,并不是真的关掉文件)。
- 父子进程管道通信的流程
- 匿名管道通信时的4种情况
1.父进程阻塞:管道内没有数据,并且此时子进程不关闭写端文件fd,则读端(父进程)就要阻塞等待,直到管道中有数据。
2.子进程阻塞:管道大小被写满,此时父进程不关闭读端文件fd,则写端(子进程)就要阻塞等待
3.如果写端不再向管道中写数据,并且此时关闭了写端文件fd,则父进程读完管道中的数据后,最后会读到0,用来表示读到了文件尾。
4.如果读端不再从管道中读数据,并且此时关闭了读端文件fd,而写端文件还在向管道中写数据,则操作系统会直接终止写入的进程(子进程),通过发送信号13 SIGPIPE来杀掉子进程。
- 管道通信的5种特性
1.由于管道通信时,要么是读端在读,要么是写端在写,不会出现一边读一边写的情况,所以管道自带同步机制。
2.管道通信常见于具有亲缘关系的进程,比如父子进程。
3.管道通信是面向字节流的。
4.父进程或者子进程退出,则管道自动释放,管道文件的生命周期是跟随进程的。
5.管道通信是半双工模式,并且是一种特殊的半双工模式,特殊指在一次通信中,写端只能写,读端只能读。
- 匿名管道的实例
int main()
{//1.父进程创建管道int pipefd[2];int n = pipe(pipefd);if(n < 0)return 1;//管道规定,0为读,1为写printf("pipefd[0]:%d,pipefd[1]:%d\n",pipefd[0],pipefd[1]);//2.创建子进程pid_t id = fork();if(id == 0){//子进程close(pipefd[0]);Pro_Wrte(pipefd[1]);exit(0);} //父进程close(pipefd[1]);Pro_Read(pipefd[0]);wait(NULL);return 0;
}
- 实现一个进程池
主进程有多个子进程。要求实现负载均衡,给每个进程均衡分配任务。
2.命名管道
进程通信本质就是让不同进程在内存级别看到同一份资源,而两个毫不相干的进程,要看到同一份资源,那么这份资源就得是磁盘上面有路径、有文件名的一个文件。
命名管道和匿名管道唯一的区分就是,命名管道对应在磁盘上是有名称的特殊文件,但是作为内存中的管道来通信同样不需要和磁盘做IO。
总结就是,命名管道就是磁盘上的一个文件,只不过是一种特殊文件,这个文件内所发生的变动不需要刷新到磁盘上同步。
- 创建一个命名管道
man mkfifo
mkfifo是命令行指令,Fifo是命名管道的名字,同样可以发现,文件类型这一位,目录文件是d,普通文件是-,而管道文件就是p开头。
- 代码中创建命名管道
man 3 mkfifo
- 模拟服务器、客户机不同进程通信
接下来,写一段代码,模拟不同进程之间的通信,模拟服务器、客户机的通信,这里不考虑网络部分,只假设同一台机器上,两个可执行程序变成两个毫不相干的进程。
模拟服务器端的那个进程要创建命名管道,服务器往往是读端。
模拟客机的那个进程要给这个管道里面写数据。
Makefile
.PHONY:all
all:PipeClient PipeServePipeServe:PipeServe.ccg++ -o $@ $^ -std=c++11
PipeClient:PipeClient.ccg++ -o $@ $^ -std=c++11
.PHONY:clean
clean:rm -f PipeClient PipeServe
公共文件
#pragma once#ifndef __COMM_HPP__
#define __COMM_HPP__#include <iostream>
#include <string>#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>#include <cerrno>
#include <cstring>#include <unistd.h>
using namespace std;#define Mode 0666
//目的是为了让客机看见这个管道文件
#define Path "./fifo"//命名管道
class Fifo
{public:Fifo(const string& path):_path(path){umask(0);int n = mkfifo(_path.c_str(),Mode);if(n == 0){cout << "mkfifo success!!!" << endl;}else{cerr << "mkfifo fail:->errno->" << errno<< "->"<<strerror(errno) << endl;}}~Fifo(){//可以用在代码中的删除文件的系统调用int ret = unlink(_path.c_str());if(ret == 0){cout << "mkfifo file:->"<< _path << "remove success!!!" << endl;}else{cout << "mkfifo fifl remove fail:->"<< errno << "->" << strerror(errno)<< endl;}}private:string _path;//文件路径+文件名
};#endif
服务作读端:
#include "Comm.hpp"#include <unistd.h>
int main()
{Fifo ff(Path);sleep(1);int read_fd = open(Path,O_RDONLY);if(read_fd < 0){cerr << "open fail:->errno->" << errno<< "->"<<strerror(errno) << endl;return 1;}cout << "open success" << endl;char buffer[1024];while(1){ssize_t n = read(read_fd,buffer,sizeof(buffer)-1);if(n < 0){cerr << "read fail:->errno->" << errno<< "->"<<strerror(errno) << endl;break;}else if(n == 0){//读到了文件尾cout << "client quit,server close" << endl;break;}else {buffer[n] = 0;cout << "client say " << buffer << endl;}}close(read_fd);return 0;
}
客机作写端
#include "Comm.hpp"int main()
{int write_fd = open(Path,O_WRONLY);if(write_fd < 0){cerr << "open fail:->errno->" << errno<< "->"<<strerror(errno) << endl;return 1;}string in;while(1){cout << "Please cin Your Message" << endl;getline(cin,in);if(in == "quit")break;ssize_t n = write(write_fd,in.c_str(),in.size());if(n < 0){cerr << "write fail:->errno->" << errno<< "->"<<strerror(errno) << endl;break;}}close(write_fd);return 0;
}
最终效果
需要我们记住的是,匿名管道常常用于具有亲缘关系的进程之间通信,而命名管道可以帮助两个毫不相干的进程通信。
管道通信的本质就是利用了多个文件描述符指向的多个文件结构体的缓冲区是同一份,由于毫不相干的进程要利用管道通信,就必须要让这两个进程都能“看到”这同一个管道文件,因此命名管道常常用于不相干的进程之间通信,而具有亲缘关系的进程只用匿名管道即可通信。