进程间通信
1.1进程间通信介绍
什么是进程间通信?
答:进程具有独立性,每个进程都有自己的PCB,所以进程间需要通信,并且通信的成本一定不低(通信的本质:OS需要直接或者间接给通信双方的进程提供“内存空间”,并且要通信的进程,必须看到一份公共的资源)
而我们所说的不同通信种类本质就是:上面所说的资源,是OS中的哪一个模块提供的。如文件系统提供的叫管道通信;OS对应的System V模块提供的…
📝ps:成本不低是因为我们需要让不同的进程看到同一份资源
1.2进程间通信目的
进程间通信的目的在于:
数据传输:一个进程需要将它的数据发送给另一个进程
资源共享:多个进程之间共享同样的资源
通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)
进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程)
为什么要有进程间通信?
答:有时候我们需要多进程协同的,完成某种业务内容。比如管道。
1.3进程间通信分类
如何去通信?
答:1.采用标准的做法:System V进程间通信(聚焦在本地通信,如共享内存)、POSIX进程间通信(让通信过程可以跨主机)。
2.采用文件的做法:管道-基于文件系统(匿名管道、命名管道)
而本篇博客主要介绍管道,接着往下看把👇
管道
2.1管道介绍
管道是Unix中最古老的进程间通信的形式。我们把从一个进程连接到另一个进程的一个数据流称为一个"管道"
任何一个文件包括两套资源:1.file的操作方法 2.有属于自己的内核缓冲区,所以父进程和子进程有一份公共的资源:文件系统提供的内核缓冲区,父进程可以向对应的文件的文件缓冲区写入,子进程可以通过文件缓冲区读取,此时就完成了进程间通信,这种方式提供的文件称为管道文件。管道文件本质就是内存级文件,不需要IO。
两个进程如何看到同一个管道文件:fork创建子进程完成
管道创建时分别以读和写方式打开同一个文件(如果只读或者只写,子进程也只会继承只读或只写,父子双方打开文件的方式一样,无法完成单向通信);父进程创建子进程,父进程以读写打开,子进程也是以读写打开(一般而言,管道只用来进行单向数据通信);关闭父子进程不需要的文件描述符,完成通信:
管道分为匿名管道和命名管道
2.2匿名管道
我们通过文件名区分文件,但是如果当前进程的文件没有名字,这样的内存级文件称为匿名管道。让两个进程看到同一个文件,通过父进程创建子进程,子进程继承文件地址的方式,看到同一个内存级文件,此时内存级文件没有名称就是匿名管道了。匿名管道能用来父进程和子进程之间进行进程间通信。
pipe
pipe:创建一个管道只需要调用pipe,注意头文件,返回值,以及函数的参数
头文件为#include <unistd.h>,调用成功返回0,调用失败返回-1。参数是输出型参数
SYNOPSIS
#include <unistd.h>
int pipe(int pipefd[2]);
DESCRIPTION
pipe() creates a pipe,pipefd[0] refers to the read end of the pipe. pipefd[1] refers to the write end of the pipe.
RETURN VALUE
On success, zero is returned. On error, -1 is returned, and errno is set appropriately.
创建管道文件,打开读写端:
#include <iostream>
#include <unistd.h>
#include <cassert>
using namespace std;
int main()
{
int fds[2];
int n = pipe(fds);
assert(n == 0);
//0,1,2->3,4
//[0]:读取 [1]:写入
cout<<"fds[0]:"<<fds[0]<<endl;//3
cout<<"fds[1]:"<<fds[1]<<endl;//4
return 0;
}
所以[0]:3代表读取👄,[1]:4代表写入✍。
fork子进程:
#include <iostream>
#include <unistd.h>
#include <cassert>
#include <sys/types.h>
#include <sys/wait.h>
using namespace std;
int main()
{
int fds[2];
int n = pipe(fds);
assert(n == 0);
//fork
pid_t id = fork();
assert(id>=0);
if(id==0)
{
//子进程通信
exit(0);
}
//父进程通信
n = waitpid(id,nullptr,0);
assert(n==id);
return 0;
}
关闭父子进程不需要的文件描述符,完成通信:
子进程写入,父进程读取:
#include <iostream>
#include <unistd.h>
#include <cassert>
#include <sys/types.h>
#include <sys/wait.h>
#include <string>
#include <cstdio>
#include <cstring>
#include <unistd.h>
using namespace std;
int main()
{
int fds[2];
int n = pipe(fds);
assert(n == 0);
//fork
pid_t id = fork();
assert(id>=0);
if(id==0)
{
//子进程通信:子进程进行写入,关闭读
close(fds[0]);
//通信
const char*s = "这是子进程,正在进行通信";
int cnt = 0;
while(true)
{
cnt++;
char buffer[1024];
snprintf(buffer,sizeof buffer,"child->parent say:%s[%d][%d]",s,cnt,getpid());
//写端写满的时候,在写会阻塞,等对方进行读取
write(fds[1],buffer,strlen(buffer));//系统接口
sleep(1);//一秒写一次
}
//退出前关闭子进程
close(fds[1]);
exit(0);
}
//父进程通信:父进程进行读取,关闭写
close(fds[1]);
//通信
while(true)
{
char buffer[1024];
//管道中如果没有数据,读端在读,默认会直接阻塞当前正在读取的进程
ssize_t s = read(fds[0],buffer,sizeof(buffer)-1);
if(s>0) buffer[s] = 0;
cout<<"Get Message# "<<buffer<<"|mypid:"<<getpid()<<endl;
}
n = waitpid(id,nullptr,0);
assert(n==id);
//结束前关闭
close(fds[0]);
return 0;
}
读写特征
管道读写特征:
1.读快写慢
子进程休眠时,不在写入,父进程在读取(如果管道中没有数据,读端在读,此时默认会直接阻塞当前正在读取的进程)
2.读慢写快
拿着管道读端不读,写端一直在写:写端往管道里写,而管道是有大小的,不断往写端写,会被写满
管道是固定大小的缓冲区,当管道被写满,就不能再写了。此时写端会阻塞。
如果父进程只是sleep(2),稍微睡眠比较少:
在这里不断读取的时候:写端是把数据塞到管道里,管道读取的是按照指定大小读取!而不是一行一行。而我们刚开始按行读取的是因为发送的慢,一次塞一行数据。
3.写入关闭,读到0
子进程写入端关闭:
4.读取关闭,写入
管道是单向的:读端关闭,在写入就没有意义了:OS会终止写端,会给写进程发送信号,终止写端
管道特征
1.管道的生命周期随进程,进程退出,管道释放
2.管道可以用来进行具有血缘关系的进程间通信(常用于父子通信)
3.管道是面向字节流的
4.半双工—单向通信(特殊)
5.互斥与同步机制——对共享资源进行保护的方案
2.3命名管道
我们前面已经知道:匿名管道应用的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信。
那如果两个毫不相干的进程间通信交互呢?如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,它经常被称为命名管道。
命名管道是一种特殊类型的文件
mkfifo
NAME
mkfifo - make FIFOs (named pipes)
SYNOPSIS
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
RETURN VALUE
On success mkfifo() returns 0. In the case of an error, -1 is returned (in which case, errno is set appropriately).
在当前路径下直接创建命名管道:
mkfifo named_pipe
往管道文件写东西:
两个进程打开同一个文件:站在内核的角度,第二个文件不需要继续创建struct file对象,因为OS会识别到打开的文件被打开了。在内核中,此时就看到了同一份资源,有着操作方法和缓冲区,不需要把数据刷新到磁盘上去,不需要IO。所以无论是匿名还是命名,本质都是管道。
匿名管道通过继承的方式看到同一份资源。命名管道:通过让不同的进程打开指定名称(路径+文件名,具备唯一性)的同一个文件看到同一份资源。所以命名管道是通过文件的文件名来标定唯一性的。而匿名管道是通过继承的方式来标定的。
创建管道文件
准备工作
分为三个文件:comm.hpp:公共文件(同一份资源),:读取端,:写入端
在目录tmp下创建文件:
:
#include "comm.hpp"
int main()
{
bool ret = createFifo(NAMED_PIPE);
assert(ret);
(void)ret;
return 0;
}
comm.hpp:
#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <string>
#include <cerrno>
#include <cstring>
#include <cassert>
#define NAMED_PIPE "/tmp/"
bool createFifo(const std::string &path)
{
umask(0);
int n = mkfifo(path.c_str(),0666);
if(n==0) return true;
else
{
std::cout<<"errno:"<<errno<<"err string:"<<strerror(errno)<<std::endl;
return false;
}
}
运行:
删除管道文件
unlink
注意头文件,函数的参数以及返回值这三个主要部分:
NAME
unlink - remove a directory entry
SYNOPSIS
#include <unistd.h>
int unlink(const char *path);
RETURN VALUE
Upon successful completion, 0 shall be returned. Otherwise, -1 shall be returned and errno set to indicate the error. If -1 is returned, the named file shall not be changed.
在comm.hpp中封装好删除的函数:
void removeFifo(const std::string &path)
{
int n = unlink(path.c_str());
assert(n==0);
(void)n;//防止n没使用而警告
}
在中进行调用:
#include "comm.hpp"
int main()
{
bool ret = createFifo(NAMED_PIPE);
assert(ret);
(void)ret;
removeFifo(NAMED_PIPE);
return 0;
}
至此,创建和删除管道文件的操作我们实现完毕。下面进入通信环节
通信
其实在了解完了匿名管道之后,对于命名管道我们能够更好的理解:
(写端):
#include "comm.hpp"
int main()
{
int wfd = open(NAMED_PIPE,O_WRONLY);
if(wfd<0) exit(1);
//write
char buffer[1024];
while(true)
{
std::cout<<"Please Say:";
fgets(buffer,sizeof(buffer),stdin);
//if(strlen(buffer)>0) buffer[strlen(buffer)-1] = 0;
ssize_t n = write(wfd,buffer,strlen(buffer));
assert(n==strlen(buffer));
(void)n;
}
close(wfd);
return 0;
}
(读端):
#include "comm.hpp"
int main()
{
bool ret = createFifo(NAMED_PIPE);
assert(ret);
(void)ret;
int rfd = open(NAMED_PIPE,O_RDONLY);
if(rfd<0) exit(1);
//read
char buffer[1024];
while(true)
{
ssize_t s = read(rfd,buffer,sizeof(buffer)-1);
if(s>0)
{
buffer[s] = 0;
std::cout<<"client->server" <<buffer<<std::endl;
}
else if(s==0)
{
std::cout<<"client quit,俺也一样"<<std::endl;
break;
}
else
{
std::cout<<"err string:"<<strerror(errno)<<std::endl;
break;
}
}
close(rfd);
removeFifo(NAMED_PIPE);
return 0;
}
进行通信:
读端多出一行空行:写端输入之后多按了回车,修改为buffer[strlen(buffer)-1] = 0;
总结
进程间通信的内容是比较多的,在这里,本文只是对进程间通信——管道这一部分进行介绍,后续会继续更新其他部分。
我们从进程间通信开始介绍,而后进入了进程间通信——管道这部分,管道又分为匿名管道和命名管道,以及之间的区别,匿名管道需要具有血缘关系的进程,而命名管道则不需要,同时,匿名管道通过子进程继承文件地址的方式,看到同一个内存级文件,而命名管道通过不同进程打开同一个文件,看到同一份资源。至此,对于管道的理解我们就先到这里结束。