プロセスは互いに独立しており、プロセス間でデータを送信するにはプロセス間通信が必要であることは誰もが知っています。プロセス間通信は 3 つのタイプに分類されます。1 つ目はファイル システムに基づくパイプライン通信、2 つ目は system v 規格に基づくローカル通信、3 つ目は POSIX 標準に基づくもので、クロスホスト通信を実現します。今日説明するのは、パイプライン通信の最初の主要なカテゴリです。パイプライン通信を理解する前に、まずパイプラインとは何かを知る必要があります。
パイプラインの概念
水道管を例に挙げると、水道管の中の水は端から端まで流れます。コンピューターの世界では、パイプラインの両端に 2 つのプロセスがあり、水の流れは2 つのプロセス間で送信されるデータに相当します。では、このパイプラインとは一体何なのでしょうか? それをグラフで表します。
パイプはカーネルに属しているため、OS はパイプを作成するためのシステム コールを提供します。これについては後で説明します。ここで、パイプラインを使用する理由について話しましょう。
パイプラインがファイルシステムに基づいたメモリレベルのファイルであることに疑問を抱く人もいますが、この文章をどのように理解しますか?
この質問に答える前に、考えてみましょう。ファイル システムに基づいて 2 つのプロセス間の通信を実現したい場合、それは非常に簡単です。ディスク上にファイルを作成し、それをプロセスに書き込むだけです。ファイルは最初にロードされます。データはメモリに書き込まれ、その後ファイルに書き込まれ、最後に定期的にディスクにフラッシュされます。1 つのプロセスは、ファイルを読み取り、メモリにロードし、メモリからデータを読み取ります。ファイル IO はプロセス間の通信効率を大幅に低下させます。したがって、パイプラインはファイル システムに基づいており、その目的は他のファイルと同様に読み書きすることです。パイプラインもメモリ上に作成されるためメモリレベルのファイルであり、各プロセスはデータ送信のためにメモリを扱うだけでよく、ディスクとは関係がないため、データ通信の効率が向上します。
匿名パイプ
パイプは、匿名パイプと名前付きパイプに分けられます。パイプはファイル システムに基づくメモリ レベルのファイルであるため、プロセスがシステム コールを通じてパイプを作成する場合。したがって、このファイルが開かれると、プロセスのファイル記述子テーブルにファイルの記述子が格納されます。ファイルには 2 つの記述子があり、1 つは読み取り側用、もう 1 つは書き込み側用です。
しかし、プロセスとプロセスは互いに独立しているため、別のプロセスにこのパイプラインを表示させるにはどうすればよいでしょうか? フォークを通じて子プロセスを作成できるため、子プロセスは親プロセスのファイル記述子テーブルを継承し、ファイル記述子テーブルを通じて同じパイプラインを見つけることができます。
2 つのプロセスがそれぞれ独自の読み取りおよび書き込みタスクを実行できるようにするために、タスク以外のファイル記述子を閉じることができます。
したがって、血縁関係間のプロセス通信に使用されるパイプラインを匿名パイプラインと呼びます。そのシステムコールインターフェイスはパイプです。
Pipefd 配列には、パイプラインの読み取りおよび書き込みファイル記述子が格納されます。pipefd[0] は読み取り終了記述子、pipefd[1] は書き込み終了記述子です。次に、親プロセスと子プロセス間の通信を実装してみます。
#include <iostream>
#include <cstdio>
#include <cstring>
#include <string>
#include <cassert>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#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]);
// 子进程的通信代码
// string msg = "hello , i am child";
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));
cout << "count: " << cnt << endl;
// sleep(50); //细节,我每隔1s写一次
// break;
}
// 子进程
close(fds[1]); // 子进程关闭写端fd
cout << "子进程关闭自己的写端" << endl;
// sleep(10000);
exit(0);
}
// 父进程进行读取
close(fds[1]);
// 父进程的通信代码
while (true)
{
sleep(2);
char buffer[1024];
// cout << "AAAAAAAAAAAAAAAAAAAAAA" << endl;
// 如果管道中没有了数据,读端在读,默认会直接阻塞当前正在读取的进程!
ssize_t s = read(fds[0], buffer, sizeof(buffer) - 1);
// cout << "BBBBBBBBBBBBBBBBBBBBBB" << endl;
if (s > 0)
{
buffer[s] = 0;
cout << "Get Message# " << buffer << " | my pid: " << getpid() << endl;
}
else if(s == 0)
{
//读到文件结尾
cout << "read: " << s << endl;
break;
}
break;
// 细节:父进程可没有进行sleep
// sleep(5);
}
close(fds[0]);
cout << "父进程关闭读端" << endl;
int status = 0;
n = waitpid(id, &status, 0);
assert(n == id);
cout <<"pid->"<< n << " : "<< (status & 0x7F) << endl;
return 0;
}
パイプラインの読み取りおよび書き込みルール:
匿名パイプを使用してプロセス プールを実装します。
#include <iostream>
#include <string>
#include <vector>
#include <cstdlib>
#include <cassert>
#include <ctime>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#define MakeSeed() srand((unsigned long)time(nullptr) ^ getpid() ^ 0x171237 ^ rand() % 1234)
#define PROCSS_NUM 10
///子进程要完成的某种任务 -- 模拟一下/
// 函数指针 类型
typedef void (*func_t)();
void downLoadTask()
{
std::cout << getpid() << ": 下载任务\n"
<< std::endl;
sleep(1);
}
void ioTask()
{
std::cout << getpid() << ": IO任务\n"
<< std::endl;
sleep(1);
}
void flushTask()
{
std::cout << getpid() << ": 刷新任务\n"
<< std::endl;
sleep(1);
}
void loadTaskFunc(std::vector<func_t> *out)
{
assert(out);
out->push_back(downLoadTask);
out->push_back(ioTask);
out->push_back(flushTask);
}
/下面的代码是一个多进程程序//
class subEp // Endpoint
{
public:
subEp(pid_t subId, int writeFd)
: subId_(subId), writeFd_(writeFd)
{
char nameBuffer[1024];
snprintf(nameBuffer, sizeof nameBuffer, "process-%d[pid(%d)-fd(%d)]", num++, subId_, writeFd_);
name_ = nameBuffer;
}
public:
static int num;
std::string name_;
pid_t subId_;
int writeFd_;
};
int subEp::num = 0;
int recvTask(int readFd)
{
int code = 0;
ssize_t s = read(readFd, &code, sizeof code);
if(s == 4) return code;
else if(s <= 0) return -1;
else return 0;
}
void sendTask(const subEp &process, int taskNum)
{
std::cout << "send task num: " << taskNum << " send to -> " << process.name_ << std::endl;
int n = write(process.writeFd_, &taskNum, sizeof(taskNum));
assert(n == sizeof(int));
(void)n;
}
void createSubProcess(std::vector<subEp> *subs, std::vector<func_t> &funcMap)
{
std::vector<int> deleteFd;
for (int i = 0; i < PROCSS_NUM; i++)
{
int fds[2];
int n = pipe(fds);
assert(n == 0);
(void)n;
// 父进程打开的文件,是会被子进程共享的
// 你试着多想几轮
pid_t id = fork();
if (id == 0)
{
for(int i = 0; i < deleteFd.size(); i++) close(deleteFd[i]);
// 子进程, 进行处理任务
close(fds[1]);
while (true)
{
// 1. 获取命令码,如果没有发送,我们子进程应该阻塞
int commandCode = recvTask(fds[0]);
// 2. 完成任务
if (commandCode >= 0 && commandCode < funcMap.size())
funcMap[commandCode]();
else if(commandCode == -1) break;
}
exit(0);
}
close(fds[0]);
subEp sub(id, fds[1]);
subs->push_back(sub);
deleteFd.push_back(fds[1]);
}
}
void loadBlanceContrl(const std::vector<subEp> &subs, const std::vector<func_t> &funcMap, int count)
{
int processnum = subs.size();
int tasknum = funcMap.size();
bool forever = (count == 0 ? true : false);
while (true)
{
// 1. 选择一个子进程 --> std::vector<subEp> -> index - 随机数
int subIdx = rand() % processnum;
// 2. 选择一个任务 --> std::vector<func_t> -> index
int taskIdx = rand() % tasknum;
// 3. 任务发送给选择的进程
sendTask(subs[subIdx], taskIdx);
sleep(1);
if(!forever)
{
count--;
if(count == 0) break;
}
}
// write quit -> read 0
for(int i = 0; i < processnum; i++) close(subs[i].writeFd_); // waitpid();
}
void waitProcess(std::vector<subEp> processes)
{
int processnum = processes.size();
for(int i = 0; i < processnum; i++)
{
waitpid(processes[i].subId_, nullptr, 0);
std::cout << "wait sub process success ...: " << processes[i].subId_ << std::endl;
}
}
int main()
{
MakeSeed();
// 1. 建立子进程并建立和子进程通信的信道, 有bug的,但是不影响我们后面编写
// 1.1 加载方发表
std::vector<func_t> funcMap;
loadTaskFunc(&funcMap);
// 1.2 创建子进程,并且维护好父子通信信道
std::vector<subEp> subs;
createSubProcess(&subs, funcMap);
// 2. 走到这里就是父进程, 控制子进程,负载均衡的向子进程发送命令码
int taskCnt = 3; // 0: 永远进行
loadBlanceContrl(subs, funcMap, taskCnt);
// 3. 回收子进程信息
waitProcess(subs);
return 0;
}
プロセス プールのコードは少し難しく、自分で実装して理解する必要があるため、ここでは多くを説明しません。今回は、血縁関係に基づく通信を必要としないパイプ、名前付きパイプについて説明します。
名前付きパイプ:
これを 2 つの無関係なプロセスに適用して通信し (血縁関係はありません)、fifo インターフェイスを呼び出してパイプライン ファイルを作成できます。このパイプを名前付きパイプと呼びます。名前付きパイプは特別な種類のファイルです。
名前付きパイプのインターフェイスを作成します。
int mkfifo(const char *ファイル名,mode_t モード);
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <cassert>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#define NAMED_PIPE "/tmp/mypipe.106"
bool createFifo(const std::string &path)
{
umask(0);
int n = mkfifo(path.c_str(), 0600);
if (n == 0)
return true;
else
{
std::cout << "errno: " << errno << " err string: " << strerror(errno) << std::endl;
return false;
}
}
void removeFifo(const std::string &path)
{
int n = unlink(path.c_str());
assert(n == 0); // debug , release 里面就没有了
(void)n;
}
client.cc:
#include "comm.hpp"
// 你可不可以把刚刚写的改成命名管道呢!
int main()
{
std::cout << "client begin" << std::endl;
int wfd = open(NAMED_PIPE, O_WRONLY);
std::cout << "client end" << std::endl;
if(wfd < 0) exit(1);
//write
char buffer[1024];
while(true)
{
std::cout << "Please Say# ";
fgets(buffer, sizeof(buffer), stdin); // abcd\n
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;
}
サーバー.cc:
#include "comm.hpp"
int main()
{
bool r = createFifo(NAMED_PIPE);
assert(r);
(void)r;
std::cout << "server begin" << std::endl;
int rfd = open(NAMED_PIPE, O_RDONLY);
std::cout << "server end" << std::endl;
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, me too!" << std::endl;
break;
}
else
{
std::cout << "err string: " << strerror(errno) << std::endl;
break;
}
}
close(rfd);
// sleep(10);
removeFifo(NAMED_PIPE);
return 0;
}
匿名パイプと名前付きパイプの違いは次のとおりです。
1. 匿名パイプが作成され、 pipe 関数によって開かれます。名前付きパイプは mkfififo 関数によって作成され、 openで開かれます。2. FIFO (名前付きパイプ) と パイプ(匿名パイプ) の唯一の違いは、作成方法とオープン方法にありますが、これらのタスクが完了すると、セマンティクスは同じになります。
名前付きパイプを開くルール
現在のオープン操作が読み取りのために FIFO をオープンしている場合O_NONBLOCK 無効 : 対応するプロセスが書き込み用に FIFOを開くまでブロックします。O_NONBLOCK 有効 : 成功をすぐに返します現在のオープン操作が書き込み用に FIFO をオープンしている場合O_NONBLOCK 無効 : 対応するプロセスが 読み取り用の FIFO を開くまでブロックします。 O_NONBLOCK 有効: すぐに戻り、失敗します。エラー コードは ENXIOです。
ここまでで、プロセス間通信のパイプライン通信は終了となりますので、皆様もぜひサポートして一緒に進めていただければと思います!