文章目录
- 📕 进程间通信介绍
- 📕 匿名管道
- 原理
- 使用
- 读写规则
- 特点
- 📕 命名管道
- 原理
- 使用
- 匿名管道和命名管道的区别
📕 进程间通信介绍
进程间通信,顾名思义,就是两个进程之间的 “交流” ,我们知道,进程是相互独立的,也就是说,正常情况下,两个进程之间无法传递消息,但是有时候又需要 进程间通信,如下,这是进程间通信的目的。
- 数据传输:一个进程需要将它的数据发送给另一个进程
- 资源共享:多个进程之间共享同样的资源。
- 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
- 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
要达到这些目的,就需要使用一些技术来完成进程间通信。以下是三类技术,管道 、System V、POSIX 。本篇文章主要讲解管道的方法。
- 管道
- 匿名管道pipe
- 命名管道
- System V IPC
- System V 消息队列
- System V 共享内存
- System V 信号量
- POSIX IPC
- 消息队列
- 共享内存
- 信号量
- 互斥量
- 条件变量
- 读写锁
那么什么是管道呢?
管道是Unix中最古老的进程间通信的形式。我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”。
听起来很抽象,可以了解一下管道的原理,就明白了!
📕 匿名管道
原理
如下图,一个进程可以打开多个文件,其 file_struct 里面的文件描述符 0、1、2 分别指向了内存里的 三个 struct file,在文件描述符那篇文章有提到,每一个 struct file 都有对应的缓冲区,是内存级别的。(要理解这里的内容,首先要了解文件描述符哦,可以看一下这篇文章:【Linux】文件描述符)
所以,我们可以用操作系统创建管道的系统调用,打开一个文件(下图中绿色部分),分别以 读、写 方式打开(具体原因下文会讲),然后这个打开的文件会对应有 struct file 和 缓冲区。这些都是操作系统的行为,所以即使磁盘上没有对应的文件,也可以这样打开。当不需要使用管道的时候,直接将内存里的对应 struct file 和缓冲区释放即可。
所以,管道就是一个妥妥的内存级的文件!!
此时,我们再 fork() ,产生一个子进程,子进程会继承父进程的绝大多数内容,包括 file_struct ,但是子进程并不会创建新的文件,因为这是属于文件系统范畴了, fork() 只是创建子进程。此时,子进程同样地也以读写的方式打开了父进程创建的管道文件。
然后,由于管道是半双工的,所以要在父子进程里面,一个关闭读端,一个关闭写端,就可以完成进程间通信的基本条件。
比如这里,我需要父进程写入信息,子进程读取父进程的信息,那么就把父进程的读端关闭,子进程的写端关闭,这样子就是父进程写、子进程读!
使用
创建匿名管道需要用到 pipe() 系统调用。
头文件:#include <unistd.h>
功能:创建一个匿名管道
原型
int pipe(int fd[2]);
参数
fd:文件描述符数组,其中fd[0]表示读端, fd[1]表示写端
返回值:成功返回0,失败返回错误代码
使用匿名管道需要注意几点,首先是要在创建子进程之前,创建管道。否则子进程无法继承父进程的文件描述符。
其次,不要忘记关闭读端或者写端。
#include <iostream>
#include <unistd.h>
#include <cerrno>
#include <string.h>
#include <stdio.h>
using std::cout;
using std::endl;
int main()
{
int pipefd[2] = {0};
// 1.创建管道
int n = pipe(pipefd); // pipefd[0] 写端 pipefd[1] 读端
if (n == -1)
{
cout << "create pipe error," << errno << strerror(errno) << endl;
exit(1);
}
// cout<<pipefd[0]<<endl<<pipefd[1];
// 2.创建子进程
pid_t pid = fork();
// 子进程
if (pid == 0)
{
// 3.子进程写,那么关闭读端
close(pipefd[0]);
// 4.通信
char buf[128];
int cnt = 1;
while (true)
{
snprintf(buf, sizeof buf, "我是子进程,cnt:%d ,pid: %d\n", cnt++, getpid());
write(pipefd[1], buf, strlen(buf));
}
close(pipefd[1]); // 管道声明周期随进程,所以不手动关闭,进程结束后也会自动关闭
exit(1);
}
// 父进程
// 3.父进程读,关闭写端
close(pipefd[1]);
// 4.通信
char buf[128];
while (true)
{
sleep(3);
int n = read(pipefd[0], buf, sizeof(buf) - 1); // 留出一个位置放\0
if (n > 0)
{
buf[n]='\0';
cout << "我是父进程,子进程给的信息:" << buf << endl;
}
}
close(pipefd[0]);
return 0;
}
上面的 写入、读取 代码可以看出,写入速度快,读写数据慢。 写入了很多次,才读取一次。但是下图运行结果,并不是写入一次,就读取一次,而是可以写入多次,然后一次性读取。
从这里可以看出,读写不是强相关的。(这里指的是读写次数的多少)
读写规则
- 如果读端读取了管道内的所有数据,而写端不写入,那么只能等待。
- 如果写端将管道写满了,读端没有读取数据,那么就无法写入。
- 如果写端关闭,读端依然打开,在读取完管道内剩余的数据之后,再次读取数据,则 read 返回0。
- 如果读端关闭,而写端还向管道写入数据,毫无疑问这是没有意义的,操作系统不会运行这样的事情发送。所以,write 操作会产生信号SIGPIPE,进而可能导致write进程退出。
特点
- 单向通信(半双工)
- 管道的本质是文件,因为 fd 的生命周期随进程,所以管道的生命周期也是随进程的
- 管道通信,通常用来 对具有“血缘”关系的进程,进行进程间通信。常用于父子通信 – pipe 打开管道,并不清楚管道的名字(匿名管道)。
- 在管道通信中,写入的次数 和 读取的次数是不严格匹配的,读写次数的多少没有强相关,读取是按照字节流来读取的。
- 根据上面的读写规则,可以知道,管道具有一定的协同能力,能让 reader 和 writer 按照一定的步骤进行通信——自带同步机制。
📕 命名管道
原理
如下,一个进程打开了磁盘上的一个文件。当另一个进程(和之前的进程没有血缘关系),也打开同一个文件的时候,操作系统不会重新创建一个 struct file 对象,而是直接使用之前的。
那么,此时,两个进程就看到了同一个文件,也就可以进行进程间通信。
但是,如果这个文件是普通文件,那么数据会定期刷新的磁盘里。可是如果我们要进行进程间通信,数据就不应该被刷新到磁盘里面,而是在进程之间进行传输。所以,这就要求创建的文件是一个内存级别的文件,由此诞生了命名管道文件!不需要维护其 datablock,只需要告诉操作系统,这个文件存在!以后 两个进程可以打开这个文件,打开文件就会在内核匹配对应的缓冲区,所以两个进程就看到同一份资源!!
这个原理和上面匿名管道的原理其实是一样的!!
使用
命名管道可以从命令行上创建,命令行方法是使用下面这个命令:
- $ mkfifo filename
命名管道也可以从程序里创建,相关函数有:
- int mkfifo(const char *filename,mode_t mode);
如下是在程序里面实现命名管道,以及进程间通信。 server 进程和 client 进程进行交互。
comm.hpp 头文件
#pragma once
#include<iostream>
#include<string>
#define NUM 1024
const std::string filename="./fifo";
mode_t mode=0666;
server.cc 文件
#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include<fcntl.h>
#include <cerrno>
#include <cstring>
#include<unistd.h>
#include"comm.hpp"
using std::cin;
using std::cout;
using std::endl;
int main()
{
// 1.创建命名管道
umask(0);
int n = mkfifo(filename.c_str(),mode);
if(n < 0)
{
cout<<"create fifo error:"<<errno<<":"<<strerror(errno)<<endl;
return 1;
}
cout<<"create fifo success"<<endl;
// 2.开启管道文件
int rfd=open(filename.c_str(),O_RDONLY);
if(rfd < 0)
{
cout<<"open fifo file error:"<<errno<<":"<<strerror(errno)<<endl;
return 1;
}
cout<<"open fifo file success"<<endl;
// 3. 开始通信
char buf[NUM];
while(true)
{
// 先将缓冲区置0
buf[0]=0;
size_t n=read(rfd,buf,sizeof(buf)-1); // 读取是按字节流,所以去掉 \0
if(n > 0)
{
buf[n]='\0';
cout<<"client#"<<buf<<endl;
fflush(stdout);
}
else if(n==0)
{
cout<<"client quit,i will quit either"<<endl;
break;
}
else{
cout<<errno<<":"<<strerror(errno)<<endl;
}
}
close(rfd);
unlink(filename.c_str());
return 0;
}
client.cc 文件
#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <cerrno>
#include <cstring>
#include <unistd.h>
#include<cassert>
#include "comm.hpp"
using std::cin;
using std::cout;
using std::endl;
int main()
{
// 打开fifo
int wfd=open(filename.c_str(),O_WRONLY);
if(wfd < 0)
{
cout<<"open fifo error"<<errno<<":"<<strerror(errno)<<endl;
return 1;
}
cout<<"open fifo success"<<endl;
// 写入
char buf[NUM];
while(true)
{
cout<<"请输入你的信息#";
char* msg=fgets(buf,sizeof(buf),stdin); // C库的函数,会默认加上 \0
assert(msg);
(void*)msg;
size_t n=write(wfd,buf,sizeof(buf)-1);
if(strcasecmp(buf,"quit") == 0) break;
}
close(wfd);
return 0;
}
如下,server 是读端, client 是写端,只有 client 写入消息, server 才会读取,实现进程间通信。
如果单单在 server 写入消息, client 是不会读取的,因为 server是读端, client 是写端。
匿名管道和命名管道的区别
- 匿名管道由pipe函数创建并打开。
- 命名管道由mkfifo函数创建,打开用open
- FIFO(命名管道)与pipe(匿名管道)之间唯一的区别在它们创建与打开的方式不同,一但这些工作完成之后,它们具有相同的语义。