引入
进程间通信,顾名思义就是一个进程和另一个进程之间进行对话,以此完成数据传输、资源共享、通知事件或进程控制等。
众所周知,进程具有独立性,即使是父子进程也会彼此独立,互相看不到对方的任何信息。
而独立性是阻碍通信的,所以进程间通信要打破这种阻碍,打破进程独立性,也就是要让两个不想干的进程看到同一份资源。
管道就是其中一种实现手段。
匿名管道
匿名管道,顾名思义就是没有名字的管道,一般适用于父子进程或兄弟进程之间进行通信。其本质就是一个文件。
通信原理也很简单,进程A建立管道,我们理解为建立并打开了一个管道文件。
然后创建一个子进程B,此时B继承A的PCB(process control block),自然而然地继承A的文件描述符表,“先天”打开了管道文件。
此时A和B同时看到了一份文件,A向文件中写内容,B就可以从文件中读取内容,此时就完成了父子进程之间的通信。
其原理图示如下:
当然,为了防止乱写导致读写混乱,还要分谁向谁写的,父子进程需要各自关掉不需要的进程描述符,比如父进程关闭读端,子进程关闭写端,这样一个子进程可以向父进程发送信息的管道就建立完成了。
所以管道如其名,是单向通信的。
linux也提供了创建匿名管道的系统调用接口:
#include <unistd.h>
int pipe(int pipefd[2]);
其参数为一个数组,是一个输出型参数,pipefd[0]对应管道读端的文件描述符,pipefd[1]对应管道写端的文件描述符。创建管道成功则返回0,否则返回-1。
有了接口就可以尝试写代码建立一个管道尝试通信:
#include <iostream>
#include <cstring> // strlen()
#include <cassert> // assert()
#include <unistd.h> // pipe(), fork(), read(), write()
#include <sys/types.h> // waitpid()
#include <sys/wait.h> // waitpid()
using namespace std;
int main()
{
int fds[2];
int r = pipe(fds);
assert(r == 0); // 断言判断是否正确创建管道
(void)r; // 将之后用不到的r置为无效避免警告
pid_t id = fork(); // 创建子进程
if (id == 0)
{
// 子进程
close(fds[0]); // 子进程关闭读端
// 子进程向父进程发送信息
char msg[] = "hello, world.";
write(fds[1], msg, strlen(msg));
exit(0); // 子进程退出
}
// 父进程
close(fds[1]); // 父进程关闭写端
// 父进程接收子进程发送的信息
char msg[1024] = { 0 };
read(fds[0], msg, sizeof(msg) - 1);
// 等待子进程结束并回收退出信息
waitpid(id, nullptr, 0);
return 0;
}
管道本质是一个文件,否则也无法用文件描述符进行标识。
而文件一般是存放在磁盘中的,操作文件内容一般要从磁盘中存取,那么从管道读写信息也需要这个过程吗?显然太多余了。
管道文件是内存级的文件,是操作系统在内存中创建的临时文件。
向管道中写入的文件是存放在管道文件的缓冲区中,是缓冲区就要考虑大小。那管道的缓冲区是无限大的吗?换句话说就是,我们可以一直向管道中写信息而不发生阻塞吗?
改进一下上面的代码看一下,子进程设为死循环,向管道中不断写信息,每发一条就打印一条信息:
if (id == 0)
{
// 子进程
close(fds[0]); // 子进程关闭读端
// 子进程向父进程发送信息
char msg[] = "hello, world.";
int count = 0;
while(1)
{
write(fds[1], msg, strlen(msg));
cout << count++ << ": hello, world" << endl;
}
exit(0); // 子进程退出
}
父进程不读,一直停着:
// 父进程接收子进程发送的信息
char msg[1024] = {0};
此时运行结果如下:
子进程在发送了五千多条信息后就阻塞了,这说明管道的缓冲区大小是有限的。
下面再介绍几个管道读写数据的特点:
上面讲的是管道满了之后,写端会发生阻塞,直到读端读走数据。
而当管道是空的,读端还在读数据时,此时读端会发生阻塞,直到写端写入数据。
而如果管道的写端对应的文件描述符关闭,或者负责写端的进程退出后,读端还在读数据,那么read将返回0,此时就要意识到写端已经关闭,读端也没有读下去的必要了。
如果管道的读端对应的文件描述符关闭,或者负责读端的进程退出后,写端还在写数据,那么操作系统将会给写数据的进程发送SIGPIPE信号,进而可能导致写端进程退出。
有了管道,可以用父进程创建一堆子进程,然后与每个子进程之间都建立一个管道,父进程就可以通过管道发送信息控制子进程,向子进程分配任务,做一个基于管道的进程池。
代码一大片就不贴了,有兴趣可以查看,代码仓库链接:https://gitee.com/LeePlace_OUC/linux_code/tree/master/code_2023/test1107_procPool
命名管道
命名管道和匿名管道大致无差,只不过打破了进程之间的血缘关系,适用于完全不相干的两个进程通信。
linux提供创建命名管道的命令mkfifo
。
我们可以直接在命令行中创建命名管道:
我们可以创建出来的文件类型为p。我们可以通过这个命令从任意路径下创建命名管道文件。
管道有了名字,就有了唯一路径进行标识,那么任意两个进程可以分别以读和写的方式打开这个唯一路径标识的文件,那这两个进程就通过管道连接了起来,因此说命名管道可以用于任意两个进程之间进行通信。
linux同样提供了建立命名管道的系统调用接口mkfifo
:
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
mkfifo()
创建一个名为pathname
的FIFO特殊文件,其中pathname
应为文件的完整路径。mode
指定FIFO的权限,这个权限会权限掩码umask的影响,实际为mode & ~umask
。
FIFO特殊文件类似于管道,只是其创建方式不同。FIFO不是匿名通信通道,而是通过调用mkfifo()
将FIFO特殊文件输入到文件系统中。因此我们称之为命名管道。
当我们以这种方式创建了一个FIFO特殊文件,任何进程都可以打开它进行读写,就像普通文件一样。但是,在我们可以对其进行任何输入或输出操作之前,它必须同时在两端打开。所以当只有一端打开文件时,在另一端打开之前都会一直阻塞,直到管道两端畅通。
这个函数的返回值和pipe一样,创建成功返回0,失败返回-1。
匿名管道在读写端都退出后会自动销毁,命名文件则提供了主动关闭的接口:
#include <unistd.h>
int unlink(const char *path);
直接unlink文件名就ok了,文件名也是一个完整路径,就不过多赘述了。
两种管道除了创建的方式不同,适用范围不同外,其余特点都是一样的。所以就不再过多介绍了。
下面举一个两个进程通过命名管道进行通信的小例子:
// commen.hpp - 提供头文件、创建和关闭管道的接口
#include <iostream>
#include <string>
#include <cstring>
#include <cassert>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#define PATH "/tmp/named_pipe"
bool createFifo(const std::string &path)
{
umask(0);
int n = mkfifo(path.c_str(), 0600);
if (n == -1)
std::cout << "errno:" << errno << " errmsg: " << strerror(errno) << std::endl;
else
return true;
}
bool removeFifo(const std::string &path)
{
int n = unlink(path.c_str());
if (n == -1)
std::cout << "errno:" << errno << " errmsg: " << strerror(errno) << std::endl;
else
return true;
}
// client.cpp - 写端,向管道中发送信息
#include "common.hpp"
int main()
{
int wfd = open(PATH, O_WRONLY);
if (wfd == -1)
exit(1);
char buffer[1024] = { 0 };
while (true)
{
std::cout << "client# ";
fgets(buffer, sizeof(buffer) - 1, stdin);
buffer[strlen(buffer) - 1] = 0;
write(wfd, buffer, strlen(buffer));
}
close(wfd);
return 0;
}
// server.cpp - 读端,从管道中接收信息
#include "common.hpp"
int main()
{
createFifo(PATH);
int rfd = open(PATH, O_RDONLY);
if (rfd == -1)
exit(1);
char buffer[1024] = { 0 };
while (true)
{
ssize_t n = read(rfd, buffer, sizeof(buffer) - 1);
if (n > 0)
std::cout << "client->server# " << buffer << std::endl;
else if (n == 0)
{
std::cout << "client quit, me too." << std::endl;
break;
}
else
{
std::cout << strerror(errno) << std::endl;
break;
}
}
close(rfd);
removeFifo(PATH);
return 0;
}