Linux プロセス間通信 (1): 匿名パイプの原理と使用法

I.はじめに

(この記事を読む前に、Linux の基本的な IO の基礎知識が必要です)
 特定のケースでは、マルチプロセスが協力してタスクを処理する必要があり、この時点でプロセスの相互作用が必要になります。ただし、プロセスは独立しているためデータ操作のコストは比較的高くなります。この矛盾に突き動かされて誕生したプロセス間通信は、主に次の 4 つの目的を持っています。

  • データ転送: あるプロセスがそのデータを別のプロセスに送信する必要がある
  • リソース共有:同じリソースが複数のプロセス間で共有されます。
  • 通知イベント: プロセスは、別のプロセスまたはプロセスのグループにメッセージを送信して、何らかのイベントが発生したこと (プロセスの終了時に親プロセスに通知するなど) を通知する必要があります。
  • プロセス制御: 一部のプロセスは、別のプロセス (デバッグ プロセスなど) の実行を完全に制御したい場合があります。このとき、制御プロセスは、別のプロセスのすべてのトラップと例外をインターセプトし、その状態の変化を知ることができることを望んでいます。間に合う

2 つのプロセス間に交差はありませんので、プロセス間通信の本質はどのように作るかです2 つのプロセスが同じリソースを参照する、コミュニケーションの方法ではなく、リソースの違いがコミュニケーション方法の違いを決定します。これを理解することは非常に重要です!!

コマンドcat()echo()同じディスク ファイルの内容を表示できる場合、これは基本的に非常に原始的な通信です。cat プロセスと echo プロセスは同じディスク リソースを認識します。ただし、ディスクの読み書き速度は非常に遅く、メモリレベルのプロセス間通信を実現することを期待しているため、メモリをデータバッファとして使用する必要があります。

2. 匿名チャンネルとは何ですか?

 誰もが匿名パイプラインにアクセスしているはずですが、実際にはコマンドラインウィンドウにあります。||出力結果grep ncursesコマンドとして入力データ. なぜそれをパイプと呼ぶのですか?実際、理解するのは難しいことではありません.パイプラインについて話すとき、人々は石油を思い浮かべるかもしれません.データはコンピューターの世界では石油です.データを伝送する媒体は当然パイプラインと呼ばれます.

 コマンドは実際にはプロセスに対応します. したがって, 匿名パイプを介して, yum プロセスと grep プロセスの間のデータ相互作用を実現します. 上記の例から、最初にパイプの特性を要約することもできます (名前付きパイプも同様です
ここに画像の説明を挿入
) ):

  • パイプはデータの転送に使用されます
  • パイプライン転送データ一方向

3.匿名パイプの原理

fork()関数 を使用して子プロセスを作成すると、子プロセスのカーネル データ構造は基本的に親プロセスからコピーされ、当然子プロセスも親プロセスのファイル記述子 Basic IO (2): Linux ファイル記述子 ) の深い理解。これは、子プロセスによって開かれたファイルが、親プロセスによって開かれたファイルとまったく同じであることを意味します。親プロセスと子プロセスが同じファイルを読み書きすることで、プロセス間通信を実現します。

 ただし、ディスクのアクセス速度が遅すぎることも前述しましたが、メモリレベルのプロセス間通信を実装する必要があります。したがって、親プロセスによって開かれたパイプ ファイルは特殊なファイルです。ディスクから切り離す、書き込み時はバッファに直接書き込み、読み取り時はバッファから直接読み取ることで、メモリレベルの通信を実現します。

[質問 1]: 通常のファイルとパイプライン ファイルの見分け方を教えてください。

// 在inode结构体下可以看到如下的联合体(adree_space包含有struct inode)
struct inode
{
     
     
  union {
     
     
		struct pipe_inode_info	*i_pipe;  // 管道设备
		struct block_device	*i_bdev;      // 磁盘设备
		struct cdev		*i_cdev;          // 字符设备
	};
}

最下層は共用体によってファイル属性を区別し、パイプラインが作成されると、対応するパイプライン フィールドが有効になります。

ファイル記述子の観点から - パイプラインの深い理解:
画像-20221113195145304

[質問 2]: 親プロセスが読み書きのために同じパイプ ファイルを連続して開くのはなぜですか?
 回答: 子プロセスが親プロセスをコピーした後、読み書きのためにパイプ ファイルを開く必要はありません。


[質問 3
 回答: パイプラインの一方通行の通信を保証します.]: 親プロセスと子プロセスがそれぞれ読み取り側と書き込み側を閉じるのはなぜですか?  はユーザーのニーズによって異なります。親プロセスから子プロセスにデータを流したい場合は、親プロセスの読み取り側と子プロセスの書き込み側を閉じ、子プロセスから親プロセスにデータを流したい場合は、書き込み側を閉じます。親プロセスの終了と子プロセスの読み取り終了


第三に、匿名パイプの作成

画像-20221113222841865
[関数] : 匿名パイプを作成します
[パラメータの説明] : pipefd[2] は戻り型パラメータです

  • pipefd[0] は、読み取り用にパイプ ファイルを開くことによって返されたファイル記述子を格納します。
  • pipefd[1] は、パイプ ファイルを書き込み用に開いて返されたファイル ディスクリプタを格納します
    (メモリ方法: 0 → 口 → 読み取り 1 → ペン → 書き込み)。

[戻り値] : パイプライン作成成功で0を返し、失敗で-1を返す
[関数説明]:

  • pipe()この関数は、読み取りと書き込みのために同じパイプ ファイルを自動的に開き、ファイル記述子を pipefd[2] に返します。
  • パイプ関数はシステム コールであるため、オペレーティング システムはファイル タイプをカーネル内のパイプに直接設定できます。

[パイプラインの読み取りと書き込みのルール]:

  • 読み取るデータがない場合
    1. O_NONBLOCKdisable: 読み取り呼び出しはブロックされ、データが到着するまで待機します
    2. O_NONBLOCKenable: read 呼び出しは -1 を返し、errno 値は EAGAIN です
      ( fcntl関数を使用してノンブロッキング オプションを設定します)。
  • パイプがいっぱいになったとき
    1. O_NONBLOCKdisable: プロセスがデータを読み取るまで、書き込み呼び出しはブロックされます
    2. O_NONBLOCKenable: 呼び出しは -1 を返し、errno 値は EAGAIN です
  • パイプの書き込み側に対応するすべてのファイル記述子が閉じている場合、 read は 0 を返し、ファイルの最後が読み取られたことを示します。
  • パイプの読み取り側に対応するすべてのファイル記述子が閉じている場合、書き込み操作によってシグナルが生成されSIGPIPE、書き込みプロセスが終了する可能性があります。
  • 書き込まれるデータ量が PIPE_BUF ( Linux では4096バイト) を超えない場合、Linux は書き込みの原子性を保証します。それ以外の場合は保証されません

4. データ送信用の匿名チャネル

// 使用案例:数据从父进程传递给子进程
int main()
{
    
    
    int pipefd[2] = {
    
    0};
    if(pipe(pipefd) != 0)         // 创建管道失败
    {
    
    
        cerr << "pipe" << endl;
        return 1;
    }

    pid_t pid = fork();

    if(pid == 0) // 子进程关闭写端
    {
    
    
        close(pipefd[1]);
        char buff[20];
        while(true)
        {
    
    
            ssize_t ss = read(pipefd[0], buff, sizeof(buff));
            if(ss == 0)
            {
    
    
                cout << "父进程不写入了,我也不读取了"  << endl;
                break;
            }
            buff[ss] = '\0';
            cout << "子进程收到消息:" << buff << " 时间:" << time(nullptr) << endl;
        }
    }
    else        // 父进程关闭读端
    {
    
    
        close(pipefd[0]);   
        char msg[] = "hello world!";
        for(int i = 0; i < 5; i++)
        {
    
    
        	// 不要写入字符串末尾的'\0'
        	// ‘\0’结尾是C语言的标准,文件可不吃这一套
            write(pipefd[1], msg, sizeof(msg) - 1);   
            sleep(1);
        }
        cout << "父进程写入完毕" << endl;   
        close(pipefd[1]);
        waitpid(pid, nullptr, 0);   
    }
    
    return 0;
}

ここに画像の説明を挿入

[質問 5]: なぜ子プロセスはスリープしないのに、親プロセスと一緒にスリープするのですか? (タイムスタンプに注意してください)
 回答: パイプ機能にはアクセス制御メカニズム、父と息子は読み書きするときに特定の順序を持​​っています。

  • プロセスが空のパイプから読み取ろうとすると、パイプにデータが存在するまで読み取りインターフェイスがブロックされます
  • プロセスが完全なパイプに書き込もうとすると、パイプから十分なデータが読み取られるまで、書き込みインターフェイスがブロックされます。


[質問6]: 親プロセスがパイプを閉じたことを、子プロセスはどのように認識しますか?
 回答: プロセスがファイルを開くたびに、ファイルの参照カウントは 1 ずつ増加し、プロセスがファイルを閉じるたびに、ファイルの参照カウントは 1 減少します。ファイルの参照カウントが 0 になると、そのファイルを開いているプロセスがないことを意味し、ファイルは実際に閉じられます。

 パイプライン ファイルの参照カウントが 1 の場合、親プロセスがパイプライン ファイルを閉じたことを示し、子プロセスは現在のメッセージを読み取った後、ファイルの最後として終了できます。したがって、子プロセスは、親プロセスが書き込み終了を閉じたかどうかを感知できます。

5. 匿名パイプでプロセス制御を実現

  1. 複数のパイプライン ファイルを作成します (実際には、1 つを使用することもできます)。
  2. 複数の子プロセスを作成する
  3. 親プロセスがタスクをランダムに割り当てる (どのプロセスを指定するか、どのタスクを指定するか)
  4. サブプロセス処理タスク

ここに画像の説明を挿入

#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <ctime>
#include <cassert>
#include <vector>

using namespace std;

typedef void(*function)();

vector<function> task;   

void func1()
{
    
    
    cout << "正在执行网络任务……" << "  时间" << time(nullptr) << endl;
}

void func2()
{
    
    
    cout << "正在执行磁盘任务……" << "  时间" << time(nullptr) << endl;
}

void func3()
{
    
    
    cout << "正在执行驱动任务……" << "  时间" << time(nullptr) << endl;
}

void LoadFunc()           // 加载任务到vector中
{
    
    
    task.push_back(func1);
    task.push_back(func2);
    task.push_back(func3);
}

int main()
{
    
    
    LoadFunc();
    srand((unsigned int)time(nullptr) ^ getpid());
    int pipefd[2] = {
    
    0};
    if(pipe(pipefd) != 0)
    {
    
    
        cerr << "pipe" << endl;
        return 1;
    }

    pid_t pid = fork();
    
    if(pid == 0)
    {
    
    
        close(pipefd[1]);
        while(true)
        {
    
    
            uint32_t n = 0;
            ssize_t ss = read(pipefd[0], &n, sizeof(uint32_t));
            if(ss == 0)
            {
    
    
                cout << "我是打工人,老板走了,我也下班了" << endl;
                break;
            }

            assert(ss == sizeof(uint32_t));

            task[n]();
        }
    }
    else 
    {
    
    
        close(pipefd[0]);
        for(int i = 0; i < 10; i++)
        {
    
    
            uint32_t ss = rand() % 3;
            write(pipefd[1], &ss, sizeof(uint32_t));
            sleep(1);
        }
        cout << "任务全部处理完毕" << endl;
        close(pipefd[1]);
        waitpid(pid, nullptr, 0);
    }

    return 0;
}

ここに画像の説明を挿入

6.匿名パイプライン機能のまとめ

  1. 匿名パイプは、血縁関係プロセス間。通常、親プロセスと子プロセス間の通信に使用されます(兄弟間でも使用できます。親プロセスはパイプを開いた後に2つの子プロセスを作成し、親プロセスのパイプの読み取りと書き込みの端を閉じて通信を実現します2 つの兄弟プロセス間)
  2. パイプは一方向でなければならず、パイプが一方向の場合、Linux カーネルは一方向になるように設計されています。
  3. パイプにはアクセス制御メカニズムが付属しています
  4. パイプはバイト ストリーム指向です。最初に書かれた文字は最初に読み込まれなければならず, を読むときフォーマットの境界はありません. ユーザーはコンテンツの境界を制限する必要があります.
  5. パイプもファイルであり、プロセスの参照カウントがパイプを終了すると、パイプのライフサイクルは終了します。
  6. 一般に、カーネルは同期し、ミューテックスパイプライン操作を行います。

おすすめ

転載: blog.csdn.net/whc18858/article/details/128380792