✨ 无人扶我青云志,我自踏雪至山巅 🌏
🔥个人专栏:Linux—登神长阶
为了实现两个或者多个进程实现数据层面的交互,因为进程独立性的存在,导致进程通信的成本比较高
很多场景下需要多个进程协同工作来完成要求。如下:
|
) 将输出传递给 grep 命令:
这个命令搜索包含 "Hello" 的行。管道(通过文件系统通信)
System V IPC (聚焦在本地通信)
POSIX IPC (让通信可以跨主机)
注意:
知识补充:
(1)进程间通信的本质:必须让不同的进程看到同一份“资源”(资源:特定形式的内存空间)
(2)这个资源谁提供?一般是操作系统
(3)我们进程访问这个空间,进行通信,本质就是访问操作系统!
进程可以通过 读/写 的方式打开同一个文件,操作系统会创建两个不同的文件对象 file,但是文件对象 file 中的内核级缓冲区、操作方法集合等并不会额外创建,而是一个文件的文件对象的内核级缓冲区、操作方法集合等通过指针直接指向另一个文件的内核级缓冲区、操作方法集合等。
所以根据上述原理,父子进程可以看到同一份共享资源:被打开文件的内核级缓冲区
注意:
此外,管道通信只支持单向通信,即只允许父进程传输数据给子进程,或者子进程传输数据给父进程。
管道特点总结:
匿名管道:没有名字的文件(struct file)
匿名管道用于父子间通信,或者由一个父创建的兄弟进程(必须有“血缘“)之间进行通信
#include <unistd.h>
原型:int pipe(int fd[2]);
功能:创建匿名管道
参数 fd:文件描述符数组,其中fd[0]表示读端, fd[1]表示写端
返回值:成功返回0,失败返回错误代码
使用如下:
int main()
{
// 1. 创建管道
int fds[2] = {0};
int n = pipe(fds); // fds: 输出型参数
if(n != 0){
std::cerr << "pipe error" << std::endl;
return 1;
}
std::cout << "fds[0]: " << fds[0] << std::endl;
std::cout << "fds[1]: " << fds[1] << std::endl;
return 0;
}
// 运行如下:
island@VM-8-10-ubuntu:~/code$ ./code
fds[0]: 3
fds[1]: 4
注意:匿名管道需要在创建子进程之前创建,因为只有这样才能复制到管道的操作句柄,与具有亲缘关系的进程实现访问同一个管道通信
具体代码演示如下:(子进程写入,父进程读取)
#include <iostream>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <cstdlib>
// 父进程 -- 读取
// 子进程 -- 写入
void write(std::string &info, int cnt){
info += std::to_string(getpid());
info += ", cnt: ";
info += std::to_string(cnt);
info += ')';
}
int main()
{
// 1. 创建管道
int fds[2] = {0};
int n = pipe(fds); // fds: 输出型参数
if (n != 0){
std::cerr << "pipe error" << std::endl;
return 1;
}
// 2. 创建子进程
pid_t id = fork();
if (id < 0){
std::cerr << "fork error" << std::endl;
return 2;
}
else if (id == 0){
// 子进程
// 3. 关闭不需要的 fd, 关闭 read
int cnt = 0;
while (true){
close(fds[0]);
std::string message = "(IsLand1314, pid: ";
write(message, cnt);
::write(fds[1], message.c_str(), message.size());
cnt++;
sleep(2);
}
exit(0);
}
else{
// 父进程
// 3. 关闭不需要的 fd, 关闭 write
close(fds[1]);
char buffer[1024];
while(true){
ssize_t n = ::read(fds[0], buffer, 1024);
if(n > 0){
buffer[n] = 0;
std::cout << "child->father, message: " << buffer << std::endl;
}
}
// 记录退出信息
pid_t rid = waitpid(id, nullptr, 0);
std::cout << "father wait chile success" << rid << std::endl;
}
return 0;
}
子进程每隔 2 s 向父进程写入数据,并且打印,如下:
从上面可以知道:
如下对代码做点修改(红框内的代码)
管道有上限,Ubuntu -> 64 KB
如果我们让父进程正常读取,那么结果又是怎样的呢?
运行如下:
当我们到 65536 个字节时,管道已满,父进程读取了管道数据,子进程会继续进行写入,然后进行继续读取,就有点数据溢出的感觉
代码修改如下:
else if (id == 0)
{
int cnt = 0, total = 0;
while (true)
{
close(fds[0]);
std::string message = "h";
// fds[1]
total += ::write(fds[1], message.c_str(), message.size());
cnt++;
std::cout << "total: " << total << std::endl; // 最后写到 65536 个字节
sleep(2);
break; // 写端关闭
}
exit(0);
}
else
{
// 父进程
// 3. 关闭不需要的 fd, 关闭 write
close(fds[1]);
char buffer[1024];
while (true) {
sleep(1);
ssize_t n = ::read(fds[0], buffer, 1024);
if (n > 0) {
buffer[n] = 0;
std::cout << "child->father, message: " << buffer << std::endl;
}
else if (n == 0) {
std::cout << "n: " << n << std::endl;
std::cout << "child quit??? me too " << std::endl;
break;
}
std::cout << std::endl;
}
pid_t rid = waitpid(id, nullptr, 0);
std::cout << "father wait chile success" << rid << std::endl;
}
运行如下:
结论:如果写端关闭,读端读完管道内部数据,再读取就会读取到返回值 0,表示对端关闭,也表示读到文件结尾
如何杀死呢?
a. OS 会给 目标进程发送信号:13) SIGPIPE
b. 证明如下;
else if (id == 0){
int cnt = 0, total = 0;
while (true){
close(fds[0]);
std::string message = "h";
// fds[1]
total += ::write(fds[1], message.c_str(), message.size());
cnt++;
std::cout << "total: " << total << std::endl; // 最后写到 65536 个字节
sleep(2);
}
exit(0);
}
else{
close(fds[1]);
char buffer[1024];
while (true){
sleep(1);
ssize_t n = ::read(fds[0], buffer, 1024);
if (n > 0){
buffer[n] = 0;
std::cout << "child->father, message: " << buffer << std::endl;
}
else if (n == 0){
std::cout << "n: " << n << std::endl;
std::cout << "child quit??? me too " << std::endl;
break;
}
close(fds[0]); // 读端关闭
break;
std::cout << std::endl;
}
// 记录退出信息
int status = 0;
pid_t rid = waitpid(id, &status, 0);
std::cout << "father wait chile success: " << rid << " exit code: " <<
((status << 8) & 0xFF) << ", exit sig: " << (status & 0x7F) << std::endl;
}
运行如下:
🦋 管道读写规则
🔥 命名管道(Named Pipe),也称为 FIFO(First In, First Out),是一种用于进程间通信(IPC)的机制。它允许两个或多个进程通过文件系统中的特殊文件进行通信。命名管道与匿名管道(Anonymous Pipe)的主要区别在于,命名管道有一个文件系统中的路径名,因此可以被不相关的进程访问。
命名管道的特点
$ mkfifo filename
如上图,当我们在终端1创建了一个命名管道后,往里面写东西,管道不会关闭,在终端2上发现,它的内存大小还是0。
int mkfifo(const char *pathname, mode_t mode);
参数
返回值
功能
🎢 案例:
std::string fifoPath = "/tmp/my_named_pipe"; // 命名管道的路径名
mkfifo(fifoPath.c_str(), 0666); // 创建权限为0666的命名管道
注意事项
先写如下的几个文件:
Comm.hpp
#pragma once
#include <iostream>
#include <string>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
const std::string gpipeFile = "./fifo";
const mode_t gmode = 0600;
const int gdefultfd = -1;
const int gsize = 1024;
Client.hpp
#pragma once
#include <iostream>
#include "Comm.hpp" // 让不同代码看到同一份资源
class Client
{
public:
Client():_fd(gdefultfd)
{}
bool OpenPipe()
{
_fd = ::open(gpipeFile.c_str(), O_WRONLY);
if(_fd < 0)
{
std::cerr << "open error" << std::endl;
return false;
}
return true;
}
// std::string *: 输出型参数
// const std::string &: 输入型参数
// std::string &: 输入输出型参数
int SendPipe(const std::string &in)
{
return ::write(_fd, in.c_str(), in.size());
}
void ClosePipe()
{
if(_fd>=0)
::close(_fd);
}
~Client()
{}
private:
int _fd;
};
Server.hpp
#pragma once
#include <iostream>
#include "Comm.hpp"
class Init
{
public:
Init()
{
umask(0);
int n = ::mkfifo(gpipeFile.c_str(), gmode);
if (n < 0)
{
std::cerr << "mkfifo error" << std::endl;
return;
}
std::cout << "mkfifo success" << std::endl;
// sleep(10);
}
~Init()
{
int n = ::unlink(gpipeFile.c_str());
if (n < 0)
{
std::cerr << "unlink error" << std::endl;
return;
}
std::cout << "unlink success" << std::endl;
}
};
Init init;
class Server
{
public:
Server(): _fd(gdefultfd)
{}
bool OpenPipe()
{
_fd = ::open(gpipeFile.c_str(), O_RDONLY);
if(_fd < 0)
{
std::cerr << "open cerr" << std::endl;
return false;
}
return true;
}
// std::string *: 输出型参数
// const std::string & : 输入型参数
// std::string &: 输入输出型参数
int RecvPipe(std::string *out)
{
char buffer[gsize];
ssize_t n = ::read(_fd, buffer, sizeof(buffer) - 1);
if(n > 0)
{
buffer[n] = 0;
*out = buffer;
}
return n;
}
void ClosePipe()
{
if(_fd>=0)
::close(_fd);
}
~Server()
{}
private:
int _fd;
};
Client.cc(客户端,写入)
#include "Client.hpp"
#include <iostream>
int main()
{
Client client;
client.OpenPipe();
std::string message;
while(true)
{
std::cout << "Please Entere# ";
std::getline(std::cin, message);
client.SendPipe(message);
}
client.ClosePipe();
return 0;
}
Server.cc(服务端,读取显示)
#include "Server.hpp"
#include <iostream>
int main()
{
Server server;
server.OpenPipe();
std::string message;
while(true)
{
server.RecvPipe(&message);
std::cout << "client Say# " << message << std::endl;
}
server.ClosePipe();
return 0;
}
Makefile(通过 make 指令来生成可执行文件)
SERVER=server
CLIENT=client
CC=g++
SERVER_SRC=Server.cc
Client_SRC=Client.cc
.PHONY:all
all:$(SERVER) $(CLIENT)
$(SERVER):$(SERVER_SRC)
$(CC) -o $@ $^ -std=c++11
$(CLIENT):$(Client_SRC)
$(CC) -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f $(SERVER) $(CLIENT)
运行结果如下:
命名管道演示1
注意:如果客户端先退出,那么接收端就会进入死循环
命名管道演示 - 死循环
原因:
运行结果如下:
命名管道-死循环解决演示
为了让我们代码更加优美,并且解决一些代码重复问题,我们再进行一步完善
// Comm.hpp
#pragma once
#include <iostream>
#include <string>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
const std::string gpipeFile = "./fifo";
const mode_t gmode = 0600;
const int gdefultfd = -1;
const int gsize = 1024;
const int gForRead = O_RDONLY;
const int gForWrite = O_WRONLY;
int OpenPipe(int flag)
{
int fd = ::open(gpipeFile.c_str(), gForRead);
if(fd < 0)
{
std::cerr << "open cerr" << std::endl;
return false;
}
return fd;
}
void ClosePipeHelper(int fd)
{
if(fd>=0) ::close(fd);
}
// Client.hpp
#pragma once
#include <iostream>
#include "Comm.hpp" // 让不同代码看到同一份资源
class Client
{
public:
Client():_fd(gdefultfd)
{}
bool OpenPipeForWrite()
{
_fd = OpenPipe(gForWrite);
if(_fd < 0) return false;
return true;
}
int SendPipe(const std::string &in)
{
return ::write(_fd, in.c_str(), in.size());
}
void ClosePipe()
{
ClosePipeHelper(_fd);
}
~Client()
{}
private:
int _fd;
};
// Server.hpp
#pragma once
#include <iostream>
#include "Comm.hpp"
class Init
{
public:
Init()
{
umask(0);
int n = ::mkfifo(gpipeFile.c_str(), gmode);
if (n < 0)
{
std::cerr << "mkfifo error" << std::endl;
return;
}
std::cout << "mkfifo success" << std::endl;
// sleep(10);
}
~Init()
{
int n = ::unlink(gpipeFile.c_str());
if (n < 0)
{
std::cerr << "unlink error" << std::endl;
return;
}
std::cout << "unlink success" << std::endl;
}
};
Init init;
class Server
{
public:
Server(): _fd(gdefultfd)
{}
bool OpenPipeForRead()
{
_fd = OpenPipe(gForRead);
if(_fd < 0) return false;
return true;
}
int RecvPipe(std::string *out)
{
char buffer[gsize];
ssize_t n = ::read(_fd, buffer, sizeof(buffer) - 1);
if(n > 0)
{
buffer[n] = 0;
*out = buffer;
}
return n;
}
void ClosePipe()
{
ClosePipeHelper(_fd);
}
~Server()
{}
private:
int _fd;
};
深入研究管道,继续对 Server.cc 文件进行修改,看看其第一次打开的时候在哪里阻塞
int main()
{
Server server;
std::cout << "pos 1" << std::endl;
server.OpenPipeForRead();
std::cout << "pos 2" << std::endl;
std::string message;
while (true)
{
if (server.RecvPipe(&message) > 0)
{
std::cout << "client Say# " << message << std::endl;
}
else
{
break;
}
std::cout << "pos 3" << std::endl;
}
std::cout << "client quit, me too!" << std::endl;
server.ClosePipe();
return 0;
}
运行如下:
命名管道-阻塞演示
结论:
🎀 匿名管道与命名管道的区别
🎈 如果当前打开操作是为读而打开FIFO时
🎈 如果当前打开操作是为写而打开FIFO时
🐋 在 Linux 系统中,unlink 是一个系统调用,用于删除文件系统中的文件或符号链接。对于命名管道(FIFO),unlink 可以用于删除命名管道文件。删除命名管道后,文件系统中的路径名将不再存在,但已经打开命名管道的进程仍然可以继续使用它,直到所有进程都关闭它。
unlink
的作用🔥 管道是一种用于进程间通信(IPC)的机制,允许一个进程将数据传递给另一个进程。在类Unix操作系统中,管道通常由内核提供,使用简单的读写接口。
管道分为两种类型:无名管道 和 命名管道
管道的特点
🔥 管道的优点在于其简单性和高效性,适用于需要实时数据传输的场景。然而,由于其单向特性和有限的缓冲区,复杂的通信需求可能需要其他IPC机制,如消息队列或共享内存。总的来说,管道是一种基础而有效的进程间通信工具