記事ディレクトリ
1. プロセス間通信の概要
以前に学んだのは単一のプロセスですが、複数のプロセスはどのように機能するのでしょうか?
1。目的
データ転送: あるプロセスはそのデータを別のプロセスに送信する必要があります
。 リソース共有: 同じリソースが複数のプロセス間で共有されます。通知イベント: プロセスは、別のプロセスまたはプロセスのグループにメッセージを送信して、何らかのイベントが発生したことを通知する必要があります (プロセスの終了時に親プロセスに通知する
など)。プロセス制御: 一部のプロセスは、別のプロセス (デバッグ プロセスなど) の実行を完全に制御したいと考えています。このとき、制御プロセスは、別のプロセスのすべてのトラップと例外をインターセプトし、その状態を知ることができることを望んでいます。時間の変化。
プロセスは独立しているため、通信コストが増加します。2 つの異なるプロセスが通信するための前提条件は、2 つのプロセスが同じリソースを参照できるようにすることです。これは、オペレーティング システムによって直接的または間接的に提供されます。
どの通信手段でも、異なるプロセスはまず同じリソースを参照し、次に一方が書き込み、もう一方が読み取りを行って通信を完了する必要があります。
2.開発
パイプ
System V プロセス間通信
POSIX プロセス間通信
2. パイプライン
1. 原則
パイプは、Unix におけるプロセス間通信の最も古い形式です。
あるプロセスから別のプロセスへのデータの流れを「パイプ」と呼びます。
複数のプロセスを同時に作成し、パイプラインで接続でき、親プロセスも同じ bash になります。
パイプはファイルでもあり、ファイルは書き込み用にパイプを開き、標準出力をパイプにリダイレクトし、もう一方の端のファイルは読み取り用にパイプを開き、標準入力をパイプにリダイレクトします。
ファイル プロセスが開始されると、構造内に各ファイルを指すファイル記述子のテーブルが存在します。さらに、匿名パイプが開きます。これはオペレーティング システムによって提供される純粋なメモリ ファイルであり、ファイルをリフレッシュする必要はありません。コンテンツをディスクにコピーします。プロセス呼び出しを通じて、この匿名パイプは読み取りと書き込みを通じて同じファイルを開きます。現在のプロセスはフォークして、独自のファイル記述子テーブルを子プロセスにコピーします。それに応じて特定のコンテンツが変更され、開かれたファイルはコピーされましたか?コピーしないで。しかし、それは問題ではありません。ファイル記述テーブルにより、子プロセスは依然として親プロセスによって作成されたファイル (匿名パイプを含む) を指します。これは実際には浅いコピーに相当します。親プロセスと子プロセスが指すファイルは同じです。したがって、父と息子は両方とも匿名パイプラインを指します。一方が変更すると、もう一方は新しいデータを取得できます。ただし、現時点では、パイプラインは一方向の通信のみをサポートしています。次に行うことは、データ フローを決定することです。指示して不要な fds を閉じてください。オペレーティング システムは、子プロセスの書き込みメソッドと親プロセスの読み取りメソッドの両方をオフにするため、親が書き込み、子プロセスが読み取りを行い、パイプラインを形成します。
パイプラインは一方向でのみ通信できます。双方向通信が必要な場合は、2 つのパイプラインを定義します。
2. 簡単なシミュレーションの実装
単純なコードでは、子プロセスが親プロセスにメッセージを送信します。これは一種のパイプラインです。全体の手順は、パイプラインを作成し、サブプロセスを作成し、不要な fds を閉じて、通信を開始することです。
unistd.h ヘッダー ファイルを使用して、パイプ関数 int Pipe(int fd[2]) でパイプラインを作成します。Pipefd は出力パラメータです。システムはパイプを作成し、読み取り端と書き込み端のファイル記述子を Pipefd 配列に渡します。配列には 2 つの整数があります。パイプ関数は失敗すると -1 を返し、エラー コードが設定され、成功すると 0 を返します。
#include <iostream>
#include <unistd.h>
#include <string>
int main()
{
//一定要保证不同进程看到同一份资源
int pipefd[2] = {
0};
//1、创建管道
int n = pipe(pipefd);
if(n < 0)
{
std::cout << "pipe error, " << errno << ":" << strerror(errno) << std::ednl;
return 1;
}
std::cout << "pipefd[0]: " << pipefd[0] << std::endl;//读端
std::cout << "pipefd[0]: " << pipefd[1] << std::endl;//写端
//2、创建子进程
pid_t id = fork();
assert(id != -1)//断言或者判断都行
//子进程
if(id == 0)
{
//3、关闭不需要的fd,让父进程进行读取,让子进程进行写入
close(pipdfd[0]);
//4、开始通信
const std::string namestr = "hello, 我是子进程";//要传变化的数据,来证明是通信过来的数据
int cnt = 1;
char buffer[1024];
while(true)
{
snprintf(buffer, sizeof(buffer), "%s, 计数器: %d, 我的PID: %d\n", namestr.c_str(), cnt++, getpid());
write(pipefd[1], buffer, strlen(buffer));
sleep(1);
}
close(pipefd[1]);
exit(0);
}
//父进程
//3、关闭不需要的fd,让父进程进行读取,让子进程进行写入
close(pipefd[1]);
//4、开始通信
char buffer[1024];
while(true)
{
int n = read(pipefd[0], buffer, sizeof(buffer) - 1);
if(n > 0)
{
buffer[n] = '\0';
std::cout << "我是父进程, child give me message: " << buffer << std::endl;
}
}
close(pipefd[0]);
return 0;
}
3. まとめ
システムはパイプライン ファイルと通常のファイルを区別できます。
特徴
1. 一方向通信。半二重の一種で、双方が交互に動作することを意味します。全二重は、双方が同時に動作できることを意味します。 2. パイプラインの本質はファイルです
。ファイル記述子のプロセスに従い、ファイルはプロセスに従います。そのため、パイプラインのライフサイクルもプロセスに従います。親子プロセスが終了すると、以前に閉じられたファイル記述子は以前の位置に戻ります
3. 親子プロセスは通信でき、継承が行われます。パイプライン通信は通常、「血の」関係を持つプロセスに使用され、親子プロセスによく使用されます。兄弟プロセス間の通信も可能です。
パイプがパイプをオープンするとき、パイプは匿名パイプをオープンするため、ファイル記述子を2 つ取得する限り、どのファイルがオープンされるかを気にする必要はありません。読み取り時間は厳密には一致していません。7 件書き込まれましたが、すべてを読み取るには 1 件のみを読み取りました。読み書きには強い相関関係はありません。読み取りはバイト ストリーム指向であり、読み取りは読み取る必要があるバイト数にのみ依存します。
5. パイプラインには特定の調整機能があり、読み取りと書き込みが特定の手順に従って通信できるようになります。機構
シーン
1. すべてのパイプライン データを読み取り、相手が書き込みを行わない場合、リーダーは待つことしかできません
2. 書き込み端がいっぱいになると (通常は 65535/65536、約 64 kb)、書き込みは続行されません。読者が読みます。子プロセスに sleep を書き込むことはできませんが、親プロセス sleep(10)、つまり、実際の現象に応じて、子プロセスは異常に書き込み、親プロセスは読み取りが遅くなります。 3. 書き込みエンドが閉じられている場合、その後パイプライン データを読み取ると、再度読み取ります
。ファイルの終わりが読み取られたことを示す 0 が返されます。
4. 書き込み側は書き込みを続け、読み取り側は閉じられるため、書き込みは無意味になります。オペレーティング システムは維持されません。意味がなく、非効率で、リソースを浪費するプロセスなので、プロセスは直接強制終了されます。このプロセスを削除してください。システムはシグナルを通じてプロセスを終了し、SIGPIPE -13 がプロセスを終了します。
パイプラインの 1 回の読み取りおよび書き込みで読み書きされるデータの量は、マクロ PIPE_BUF であり、man 7 で表示できます。パイプラインが一度に書き込むデータ量はこの PIPE_BUF より小さい必要があり、そのサイズは 4096 バイトです。これより小さい場合、書き込み操作はアトミックになります。アトミックとは何かについては後ほど説明します。
3. 制御プロセス - 匿名パイプ
親プロセスは複数の子プロセスを持つことができ、各子プロセスはパイプラインを通じて親プロセスと通信します。親プロセスがパイプラインにデータを書き込まない場合、子プロセスはブロックされ、データが書き込まれると、子プロセスはブロックされます。再び実行状態になります。親プロセスは、子プロセスに特定のメッセージを書き込むことで子プロセスを起動したり、指示された方法で子プロセスに特定のタスクを実行させたりすることもできます。親プロセスは最初に記述し、それ自体で作成されたパイプラインとプロセスを整理します。
子プロセスは作成時に親プロセスのファイル記述子を継承するため、最初の親プロセスには多数の子プロセスが接続されることになり、混乱が生じます。実際に実現する構造は 1 対 1 であり、一対の親子プロセスが他のプロセスの影響を受けることなく独立したパイプラインを持ちます。
この構造を段階的に設計する
始まりを書く
#include <iostream>
#include <string>
#include <unistd.h>
#include <cassert>
using namespace std;
const int gnum = 5;//子进程数量
int main()
{
//1、先进行构建控制结构,1父多子,每个子都有自己的管道
//父写子读
for(int i = 0; i < gnum; i++)
{
//1.1 创建管道
int pipefd[2] = {
0};
int n = pipe(pipefd);
assert(n == 0);
(void)n;
//1.2 创建进程
pid_t id = fork();
assert(id != -1);
if(id == 0)//子进程,读
{
//1.3 关闭不要的fd
close(pipefd[1]);
//...
close(pipefd[0]);
exit(0);
}
//父进程,写
//1.3 关闭不要的fd
close(pipefd[0]);
}
return 0;
}
サイクルごとに、pipefd を再作成する必要があり、すべて 0 に戻されるため、親プロセスにとっては、完全に繰り返されたアクションを実行しているようなもので、子プロセスと対応するパイプラインを区別できません。したがって、パイプがどこにあるかに関係なく、どのパイプがどの子プロセスに属しているかを親プロセスに知らせる必要があります。ここではクラスを作成し、各ループの後にこのクラスによって構築されたベクトルにクラスを挿入することで、各通信の結果が保存されます。
#include <iostream>
#include <string>
#include <vector>
#include <unistd.h>
#include <cassert>
using namespace std;
const int gnum = 5;//子进程数量
class EndPoint
{
public:
pid_t child_id;//哪一个子进程
int _write_fd;//哪一个管道
public:
EndPoint(int id, int fd):_child_id(id), _write_fd(fd)
{
}
~EndPoint()
{
}
};
int main()
{
vector<EndPoint> end_pints;
//1、先进行构建控制结构,1父多子,每个子都有自己的管道
//父写子读
for(int i = 0; i < gnum; i++)
{
//1.1 创建管道
int pipefd[2] = {
0};
int n = pipe(pipefd);
assert(n == 0);
(void)n;
//1.2 创建进程
pid_t id = fork();
assert(id != -1);
if(id == 0)//子进程,读
{
//1.3 关闭不要的fd
close(pipefd[1]);
//让所有的子进程都从标准输入中读
//1.3.1 重定向
dup2(pipefd[0], 0);
//1.3.2 子进程开始等待获取命令
WaitCommand();
close(pipefd[0]);
exit(0);
}
//父进程,写
//1.3 关闭不要的fd
close(pipefd[0]);
//1.4 将新的子进程和他的管道写端,构建对象
end_points.push_back(EndPoint(id, pipefd[1]));
}
//2. 循环结束,我们已经拿到了所有子进程和他的管道
return 0;
}
次に、この部分を関数 createProcess に入れます。関数のパラメーターはベクトルです。そして、入力を待つ子プロセスを制御する関数 WaitCommand を作成します。
現在のコード
#include <iostream>
#include <string>
#include <vector>
#include <unistd.h>
#include <cassert>
using namespace std;
const int gnum = 5;//子进程数量
class EndPoint
{
public:
pid_t child_id;//哪一个子进程
int _write_fd;//哪一个管道
public:
EndPoint(int id, int fd):_child_id(id), _write_fd(fd)
{
}
~EndPoint()
{
}
};
void WaitCommand()
{
;
}
void createProcess(vector<EndPoint>* end_points)
{
//1、先进行构建控制结构,1父多子,每个子都有自己的管道
//父写子读
for(int i = 0; i < gnum; i++)
{
//1.1 创建管道
int pipefd[2] = {
0};
int n = pipe(pipefd);
assert(n == 0);
(void)n;
//1.2 创建进程
pid_t id = fork();
assert(id != -1);
if(id == 0)//子进程,读
{
//1.3 关闭不要的fd
close(pipefd[1]);
//让所有的子进程都从标准输入中读
//1.3.1 重定向
dup2(pipefd[0], 0);
//1.3.2 子进程开始等待获取命令
WaitCommand();
close(pipefd[0]);
exit(0);
}
//父进程,写
//1.3 关闭不要的fd
close(pipefd[0]);
//1.4 将新的子进程和他的管道写端,构建对象
end_points->push_back(EndPoint(id, pipefd[1]));
}
}
int main()
{
vector<EndPoint> end_pints;
createProcess(&end_points);
//2. 循环结束,我们已经拿到了所有子进程和他的管道
return 0;
}
拡張子 .hpp が付いた新しいファイルを開き、そのファイルに子プロセスによって実行されるタスクを書き込みます。
タスク.hpp
#pragma once
#include <iostream>
#include <vector>
typedef void(*fun_t)();//函数指针
void PrintLog()
{
std::cout << "打印日志任务,正在被执行..." << std::endl;
}
void InsertMySQL()
{
std::cout << "执行数据库任务,正在被执行..." << std::endl;
}
void NetRequest()
{
std::cout << "执行网络请求任务,正在被执行..." << std::endl;
}
//规定每个command都必须是4字节
#define COMMAND_LOG 0
#define COMMAND_MYSQL 1
#define COMMAND_REQUEST 2
class Task
{
public:
Task()
{
funcs.push_back(PrintLog);
funcs.push_back(InsertMySQL);
funcs.push_back(NetRequest);
}
void Execute(int command)
{
if(command >= 0 && command < funcs.size()) funcs[command]();
}
~Task()
{
}
public:
std::vector<fun_t> funcs;
};
CtrlProcess.cc ファイルに戻ります。
void WaitCommand()
{
while(true)
{
int command;
int n = read(0, &command, sizeof(char));
if(n == sizeof(int))
{
t.Execute(command);
}
else if(n == 0)//子进程要退出
{
break;
}
else//有异常,就需要
{
break;
}
}
}
グローバルな Task オブジェクト t を定義し、各入力コマンドが 4 バイト単位であることを規定します。これらをすべて記述した後、親子プロセスはパイプラインを確立し、すべてのパイプラインを管理します。その後、CreateProcess 関数が完了した後、親プロセスは実行を継続し、この時点で子プロセスにメッセージを送信できます。Taskの各タスクが出力する文中に、対応する子プロセスのpidを出力できます。ヘッダファイルはunistd.hです。
int main()
{
vector<EndPoint> end_pints;
createProcess(&end_points);
//2. 循环结束,我们已经拿到了所有子进程和他的管道
int num = 0;
while(true)
{
//1. 选择任务
int command = COMMAND_LOG;
//2. 选择进程
int index = rand() % end_points.size();
//3. 下发任务
write(end_points[index]._write_fd, &command, sizeof(command));
sleep(1);
}
return 0;
}
コードの改善を続けます。サブプロセスにタスクを順番に選択させ、メインで変更させます。各サブプロセスには名前があります。入力コマンド番号を制御し、間違っている場合は再入力し、全体として終了するには 3 を入力します。最後に、すべて終了します。親プロセスがデータの一部を書き込んで終了すると、子プロセスも読み取り後に終了します waitcommand 関数では、0 を読み取ったときに子プロセスが中断され、次の処理に進みますcreateProcess 関数 子プロセスは独自の読み取り側を終了するため、親プロセスの書き込み側を閉じてから、子プロセスをリサイクルします。
コード全体
#include <iostream>
#include <string>
#include <vector>
#include <unistd.h>
#include <cassert>
#include <sys/wait.h>
#include <sys/types.h>
#include "Task.hpp"
using namespace std;
const int gnum = 3;//子进程数量
Task t;
class EndPoint
{
private:
static int number;
public:
pid_t child_id;//哪一个子进程
int _write_fd;//哪一个管道
string processname;
public:
EndPoint(int id, int fd):_child_id(id), _write_fd(fd)
{
char namebuffer[64];
snprintf(namebuffer, sizeof(namebuffer), "process-%d[%d:%d]", number++, _child_id, _write_fd);//格式化形式输入到buffer中
processname = buffer;
}
string name() const
{
return processname;
}
~EndPoint()
{
}
};
int EndPoint::number = 0;
void WaitCommand()
{
while(true)
{
int command;
int n = read(0, &command, sizeof(char));
if(n == sizeof(int))
{
t.Execute(command);
}
else if(n == 0)
{
cout << "父进程让我退出,我就退出: " << getpid() << endl;
break;
}
else
{
break;
}
}
}
void createProcess(vector<EndPoint>* end_points)
{
//1、先进行构建控制结构,1父多子,每个子都有自己的管道
//父写子读
for(int i = 0; i < gnum; i++)
{
//1.1 创建管道
int pipefd[2] = {
0};
int n = pipe(pipefd);
assert(n == 0);
(void)n;
//1.2 创建进程
pid_t id = fork();
assert(id != -1);
if(id == 0)//子进程,读
{
//1.3 关闭不要的fd
close(pipefd[1]);
//让所有的子进程都从标准输入中读
//1.3.1 重定向
dup2(pipefd[0], 0);
//1.3.2 子进程开始等待获取命令
WaitCommand();
close(pipefd[0]);
exit(0);
}
//父进程,写
//1.3 关闭不要的fd
close(pipefd[0]);
//1.4 将新的子进程和他的管道写端,构建对象
end_points->push_back(EndPoint(id, pipefd[1]));
}
}
int ShowBoard()
{
cout << "*******************************************" << endl;
cout << "| 0. 执行日志任务 1. 执行数据库任务 |" << endl;
cout << "| 2. 执行请求任务 3. 退出 |" << endl;
cout << "*******************************************" << endl;
cout << "请选择: ";
int command = 0;
cin >> command;
return command;
}
void ctrlProcess(const vector<EndPoint> &end_pints)
{
//2.1 我们可以写成自动化的,也可以写成交互式的
int num = 0;
int cnt = 0;
while(true)
{
//1. 选择任务
int command = ShowBoard();
if(command == 3) break;//输入3就退出,再回到main处
if(command < 0 || command > 2)
{
cout << "没有对应任务" << endl;
continue;
}
//2. 选择进程
int index = cnt++;//按照顺序来选择任务
cnt %= end_points.size();
string name = end_points[index].name();
cout << "选择了进程: " << name << " | 处理任务" << command << endl;
//3. 下发任务
write(end_points[index]._write_fd, &command, sizeof(command));
}
}
void waitProcess(const vector<EndPoint> &end_points)
{
//1. 让子进程全部退出 --- 只需要让父进程关闭所有的写端
for(const auto &ep : end_points) close(ep._write_fd);
cout << "父进程让所有的子进程全部退出" << endl;
sleep(10);
//2. 父进程要回收子进程的僵尸状态
for(const auto &ep : end_points) waitpid(ep._child_id, nullptr, 0);
cout << "父进程回收了所有的子进程" << endl;
sleep(10);
}
int main()
{
vector<EndPoint> end_pints;
createProcess(&end_points);
//2. 循环结束,我们已经拿到了所有子进程和他的管道
ctrlProcess(&end_points);
//3. 处理所有的退出问题
waitProcess(end_points);
return 0;
}
現在、waitProcess 関数に問題があります。この関数は 2 つのループを 1 つのループに変換し、子プロセスは終了後に直接リサイクルされるため、プログラムは終了できないことがわかります。この問題は createProcess 関数で発生します。
void createProcess(vector<EndPoint>* end_points)
{
//1、先进行构建控制结构,1父多子,每个子都有自己的管道
//父写子读
for(int i = 0; i < gnum; i++)
{
//1.1 创建管道
int pipefd[2] = {
0};
int n = pipe(pipefd);
assert(n == 0);
(void)n;
//1.2 创建进程
pid_t id = fork();
assert(id != -1);
if(id == 0)//子进程,读
{
//1.3 关闭不要的fd
close(pipefd[1]);
//让所有的子进程都从标准输入中读
//1.3.1 重定向
dup2(pipefd[0], 0);
//1.3.2 子进程开始等待获取命令
WaitCommand();
close(pipefd[0]);
exit(0);
}
//父进程,写
//1.3 关闭不要的fd
close(pipefd[0]);
//1.4 将新的子进程和他的管道写端,构建对象
end_points->push_back(EndPoint(id, pipefd[1]));
}
}
循環しているように見えますが、子プロセスが作成され、対応するパイプラインが確立されるたびに、フォークされた子プロセスは親プロセスによってオープンされたファイル、つまり最後のパイプラインを継承し、子プロセスは同様に、構造全体はまったく独立していません。この問題を解決するには、最初の方法は、リサイクルを逆方向に終了することです。子プロセスが終了すると、子プロセス自体の読み取りおよび書き込みターミナルが閉じられ、上記のパイプラインを参照しなくなります。
void waitProcess(const vector<EndPoint> &end_points)
{
//1. 让子进程全部退出 --- 只需要让父进程关闭所有的写端
for(int end = end_points.size() - 1; end >= 0; end--)
{
cout << "父进程让所有的子进程全部退出: " << end_points[end]._child_id << endl;
close(end_points[end]._write_fd);
//2. 父进程要回收子进程的僵尸状态
waitpid(end_points[end]._child_id, nullptr, 0);
cout << "父进程回收了所有的子进程: " << end_points[end]._child_id << endl;
}
sleep(10);
}
しかし、それでも理想的な構造を構築したい場合は、創造することから始めなければなりません。親プロセスの書き込みエンドは毎回閉じられるわけではありません。このサイクルの最後に、書き込みエンドを保存します。次の子プロセスでは、上記の親プロセスの書き込みエンドを継承し、さらに子プロセスで作業を行う前に、以前に保存された書き込みポートをすべて閉じてください。
void createProcess(vector<EndPoint>* end_points)
{
vector<int> fds;
//1、先进行构建控制结构,1父多子,每个子都有自己的管道
//父写子读
for(int i = 0; i < gnum; i++)
{
//1.1 创建管道
int pipefd[2] = {
0};
int n = pipe(pipefd);
assert(n == 0);
(void)n;
//1.2 创建进程
pid_t id = fork();
assert(id != -1);
if(id == 0)//子进程,读
{
//这里可以只写一个close,其他的打印语句是为了更好地观察
cout << getpid() << " 子进程关闭父进程对应的写端: ";
for(auto &fd : fds)
{
cout << fd << " ";
close(fd);
}
cout << endl;
//1.3 关闭不要的fd
close(pipefd[1]);
//让所有的子进程都从标准输入中读
//1.3.1 重定向
dup2(pipefd[0], 0);
//1.3.2 子进程开始等待获取命令
WaitCommand();
close(pipefd[0]);
exit(0);
}
//父进程,写
//1.3 关闭不要的fd
close(pipefd[0]);
//1.4 将新的子进程和他的管道写端,构建对象
end_points->push_back(EndPoint(id, pipefd[1]));
fds.push_back(pipefd[1]);
}
}
void waitProcess(const vector<EndPoint> &end_points)
{
//1. 让子进程全部退出 --- 只需要让父进程关闭所有的写端
for(int end = end_points.size() - 1; end >= 0; end--)
{
cout << "父进程让所有的子进程全部退出: " << end_points[end]._child_id << endl;
close(end_points[end]._write_fd);
//2. 父进程要回收子进程的僵尸状态
waitpid(end_points[end]._child_id, nullptr, 0);
cout << "父进程回收了所有的子进程: " << end_points[end]._child_id << endl;
}
sleep(10);
}
すべてのコード
#include <iostream>
#include <string>
#include <vector>
#include <unistd.h>
#include <cassert>
#include <sys/wait.h>
#include <sys/types.h>
#include "Task.hpp"
using namespace std;
const int gnum = 3;//子进程数量
Task t;
class EndPoint
{
private:
static int number;
public:
pid_t child_id;//哪一个子进程
int _write_fd;//哪一个管道
string processname;
public:
EndPoint(int id, int fd):_child_id(id), _write_fd(fd)
{
char namebuffer[64];
snprintf(namebuffer, sizeof(namebuffer), "process-%d[%d:%d]", number++, _child_id, _write_fd);//格式化形式输入到buffer中
processname = buffer;
}
string name() const
{
return processname;
}
~EndPoint()
{
}
};
int EndPoint::number = 0;
void WaitCommand()
{
while(true)
{
int command;
int n = read(0, &command, sizeof(char));
if(n == sizeof(int))
{
t.Execute(command);
}
else if(n == 0)
{
cout << "父进程让我退出,我就退出: " << getpid() << endl;
break;
}
else
{
break;
}
}
}
void createProcess(vector<EndPoint>* end_points)
{
vector<int> fds;
//1、先进行构建控制结构,1父多子,每个子都有自己的管道
//父写子读
for(int i = 0; i < gnum; i++)
{
//1.1 创建管道
int pipefd[2] = {
0};
int n = pipe(pipefd);
assert(n == 0);
(void)n;
//1.2 创建进程
pid_t id = fork();
assert(id != -1);
if(id == 0)//子进程,读
{
cout << getpid() << " 子进程关闭父进程对应的写端: ";
for(auto &fd : fds)
{
cout << fd << " ";
close(fd);
}
cout << endl;
//1.3 关闭不要的fd
close(pipefd[1]);
//让所有的子进程都从标准输入中读
//1.3.1 重定向
dup2(pipefd[0], 0);
//1.3.2 子进程开始等待获取命令
WaitCommand();
close(pipefd[0]);
exit(0);
}
//父进程,写
//1.3 关闭不要的fd
close(pipefd[0]);
//1.4 将新的子进程和他的管道写端,构建对象
end_points->push_back(EndPoint(id, pipefd[1]));
fds.push_back(pipefd[1]);
}
}
int ShowBoard()
{
cout << "*******************************************" << endl;
cout << "| 0. 执行日志任务 1. 执行数据库任务 |" << endl;
cout << "| 2. 执行请求任务 3. 退出 |" << endl;
cout << "*******************************************" << endl;
cout << "请选择: ";
int command = 0;
cin >> command;
return command;
}
void ctrlProcess(const vector<EndPoint> &end_pints)
{
//2.1 我们可以写成自动化的,也可以写成交互式的
int num = 0;
int cnt = 0;
while(true)
{
//1. 选择任务
int command = ShowBoard();
if(command == 3) break;//输入3就退出,再回到main处
if(command < 0 || command > 2)
{
cout << "没有对应任务" << endl;
continue;
}
//2. 选择进程
int index = cnt++;//按照顺序来选择任务
cnt %= end_points.size();
string name = end_points[index].name();
cout << "选择了进程: " << name << " | 处理任务" << command << endl;
//3. 下发任务
write(end_points[index]._write_fd, &command, sizeof(command));
}
}
void waitProcess(const vector<EndPoint> &end_points)
{
//1. 让子进程全部退出 --- 只需要让父进程关闭所有的写端
for(int end = end_points.size() - 1; end >= 0; end--)
{
cout << "父进程让所有的子进程全部退出: " << end_points[end]._child_id << endl;
close(end_points[end]._write_fd);
//2. 父进程要回收子进程的僵尸状态
waitpid(end_points[end]._child_id, nullptr, 0);
cout << "父进程回收了所有的子进程: " << end_points[end]._child_id << endl;
}
sleep(10);
}
int main()
{
vector<EndPoint> end_pints;
createProcess(&end_points);
//2. 循环结束,我们已经拿到了所有子进程和他的管道
ctrlProcess(&end_points);
//3. 处理所有的退出问题
waitProcess(end_points);
return 0;
}
4. 名前付きパイプ
匿名パイプには制限があり、血液関連のプロセス間の通信にのみ使用できます。馴染みのない 2 つのプロセス間の通信を可能にするには、名前付きパイプが必要です。
名前付きパイプを作成するには、mkfifo コマンドを使用する必要があります。括弧内のパラメータは名前付きパイプのパラメータで、fifo は先入れ先出しを意味します。
ファイルの種類は p で始まり、パイプライン ファイルであることを示します。
ここで、コマンド echo "string" > fifo を記述して内容を書き込みますが、カーソルは次の行の先頭で停止し、点滅し続けます。これは、fifo ファイルは単なるシンボルであり、そこに書き込まれた内容はそのままであるためです。実際にはディスク上に存在しません。パイプ ファイルに書き込むだけです。読み取りには cat を使用できます。デフォルトではディスプレイから読み取ります。cat < fifo はパイプ ファイルから読み取り、ディスプレイに出力します。コンテンツを出力し続けるコードを書いた場合でも、別のウィンドウを開いて cat を使用してコンテンツを取得できます。
1. 原則
前回の基本 IO ブログのハードリンク部分で参照カウントについて書きましたが、名前付きパイプでも参照カウントが使用されます。ディスク内のファイルが開かれていない場合、そのファイルはディスク内に残ります。開いた後は、参照カウントを含むさまざまなパラメータを含む独自のファイル構造になります。オペレーターがプロセスを作成した後は、ファイルの説明が存在します。シンボルテーブルなど、プロセスはメモリ上のファイルを開くことができ、このときの参照カウントは1になりますが、別のプロセスが開いて同じファイルを開いた場合、システムはこのファイルの構造を開きませんが、この 2 つのプロセスが同じファイル構造を指している場合、カウントは 2 になり、プロセスは 1 つずつ消滅し、カウントは 2 から 0 に変わります。しかし、2 つのプロセスが同じファイルを開くことはどのようにして保証されるのでしょうか? ファイルを一意にするために、ファイル パスとファイル名はすべて同じになります。
2. シミュレーションの実装
パイプライン ファイルを作成し、読み取りおよび書き込みの終了プロセスがそれぞれのニーズに応じてファイルを開いて、通信を開始します。
2 つの実行可能プログラムを形成するには、これらのファイルを記述する必要があります。
通信.hpp
#pragma once
#include <iostream>
#include <string>
#define NUM 1024
const std::string fifoname = "./fifo";
uint32_t mode = 0666;
メイクファイル
.PHONY:all
all:Server Client
Server:Server.cc
g++ -0 Server Server.cc
Client:Client.cc
g++ -0 Client Client.cc
.PHONY:clean
clean:
rm -f Server Client
サーバー.cc
#include <iostream>
#include <cerrno>
#include <cstring>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#include "comm.hpp"
using namespace std;
int main()
{
//1、创建管道文件,只需要一次创建
umask(0);//不影响系统默认设置,只会影响当前进程
int n = mkfifo(fifoname.c_str(), mode);
if(n != 0)
{
cout << errno << " : " << strerror(errno) << endl;
return 1;
}
//2、让服务端开启管道文件
cout << "create fifo success, begin ipc" << endl;
int rfd = open(fifoname.c_str(), O_RDONLY);
if(rfd < 0)
{
cout << errno << " : " << strerror(errno) << endl;
return 2;
}
cout << "open fifo success, begin ipc" << endl;
//3、通信
char buffer[NUM];
while(true)
{
buffer[0] = 0;//C风格的字符串只要第一个位置是0,那就是空字符串
ssize_t n = read(rfd, buffer, sizeof(buffer) - 1);//不一定会发字符串,也有可能发4字节数据流,只是这里就看作字符串
if(n > 0)
{
buffer[n] = 0;
cout << "client# " << buffer << endl;
}
else if (n == 0)//写端关闭,读端就会读到0
{
cout << "client quit, me too" << endl;
break;
}
else
{
cout << errno << " : " << strerror(errno) << endl;
break;
}
}
//关闭不要的fd
close(rfd);
unlink(fifoname.c_str());//它会自己去除所有引用计数,退出管道文件
return 0;
}
Client.cc
#include <iostream>
#include <cstdio>
#include <cerrno>
#include <cassert>
#include <cstring>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#include "comm.hpp"
using namespace std;
int main()
{
//1、需不需要创建管道文件?不需要,只需要打开对应的文件即可
int wfd = open(fifoname.c_str(), O_WRONLY);
if(wfd < 0)
{
cerr << errno << " : " << strerror(errno) << endl;
return 1;
}
//通信
char buffer[NUM];
while(true)
{
cout << "请输入你的消息: ";
char* msg = fgets(buffer, sizeof(buffer), stdin);//sizeof(buffer)不用减1,因为C语言接口会自动处理成字符串,如果是系统接口就需要减1
//但有时候会混淆,所以也可以无脑减1,代码风格统一。
assert(msg);
(void)msg;
ssize_t n = write(wfd, buffer, sizeof(buffer));//虽然这是系统接口,不加1就得不到最后的\0,我们只需要获取字符串内容即可
assert(n > 0);
(void)n;
}
close(wfd);
return 0;
}
2 つのウィンドウを開いてクライアントとサーバーを観察します。実行を開始した後、サーバーはパイプラインを開くときにスタックします。これは、パイプラインが読み取り端のみを開くためであり、クライアントは実行を続けるために書き込み端を開く必要があるためです。つまり、 ./ の後に最初にファイルを作成するだけです。クライアント、Client.cc が実行を開始すると、サーバーは開いているファイルを出力します...; プログラムは正常に通信できますが、ステートメントが入力されるたびにスペースがある場合、サーバーもスペースを受け取ることになり、各行の下に空白行があるため、クライアントでそれを変更します。
cout << "请输入你的消息: ";
char* msg = fgets(buffer, sizeof(buffer), stdin);//sizeof(buffer)不用减1,因为C语言接口会自动处理成字符串,如果是系统接口就需要减1
//但有时候会混淆,所以也可以无脑减1,代码风格统一。
assert(msg);
(void)msg;
buffer[strlen(buffer) - 1] = 0;
ssize_t n = write(wfd, buffer, sizeof(buffer));//虽然这是系统接口,不加1就得不到最后的\0,我们只需要获取字符串内容即可
assert(n > 0);
(void)n;
空の文字列を入力するとサーバーは終了します。これは、上記のコードの最後のアサートがアサーションに失敗し、サーバーはそれを受信して n が 0 になった後に終了するため、assert(n >= と記述されます) 0)。
以下のリンクは修正され、スペースを入力しなくても自動的に印刷されるように変更されました。
仕上げる。