パイプのもう一方の端を密封してください。

パイプのもう一方の端を密封してください。

私は以下を使用してIPC用のコードを書きましたpipe()

#include <unistd.h>
#include <stdio.h>
#include <sys/wait.h>
    
    
int main(void) {
    char message_buffer[15] = "Hello World \n";  
    char read_buffer[15];
    int fd[2]; 
   
    int return_value = pipe(fd);
   
    if (return_value < 0) {
        printf("Error creating the pipe");
    }
    
    int rc = fork();
    if (rc  < 0) {
        printf("Error forking a child");
    }
    
    if (rc > 0) {
        close(fd[0]);
        write(fd[1], message_buffer, 15);
        close(fd[1]);
        wait(NULL);
    } else {
        close(fd[1]);
        read(fd[0], read_buffer, 15);
        close(fd[0]);
        printf("The Message: %s", read_buffer);
    }

    return 0;
}

配管に初めて接するのに、次のような質問があります。

  1. なぜ親が書く前に読んでいる側を閉じ、書いた後に書く側を閉じなければならないのかわかりませんか?
  2. 子供も同じですが、読む前に書き込み側を閉じる必要があるのはなぜですか。
  3. 親プロセスと子プロセスが同時に実行されているので、親プロセスがメッセージを書き込んでいる間に子プロセスがメッセージを読み取るとどうなりますか?
  4. 親と子が同時に実行されているので、子が読んで親がまだパイプに何も書いていない場合はどうなりますか?

私の質問は愚かなようですが、コース試験のために一般的な配管を勉強しているので、答えを助けてください。

答え1

正解は質問1と2内部にpipe マニュアルページ(「はい」セクション):

分岐後、各プロセスはパイプに不要なファイル記述子を閉じます(pipe(7)を参照)。

パイプは一方向なので、端が指定されています。読む終わり書く終わり。親がこのパイプを使用して子にデータを書き込む場合、親は読み取り側を開いたままにする必要はありません。対照的に、子プロセスがパイプからデータを読み取ろうとすると、書き込み側を開く必要はありません。

しかし、一つもっと重要な理由パイプの不要な端部を閉じるために使用されます。説明する@ilkkachu ユーザーが作成しました。リンクされた回答を参照してください。

編集する:

なぜ両親は文章を書いた後に書く側を閉じなければならないのか、子供はなぜ読んだ後に読んだ方を閉じるべきか尋ねました。

彼らはこれを行う必要はありません。両方のプログラムがパイプを使用して引き続き実行され、データを交換するにはパイプを開いたままにする必要があります。パイプの使用を示し、メッセージの送信後に終了する単純なサンプルプログラムでは、親プロセスと子プロセスは、プログラムが終了する前にリソースを適切にクリーンアップするためにパイプファイル記述子を閉じることができます。

質問#3と#4への答えは次のとおりです。pipe(7) マニュアルページ

あなたの質問#3:

親プロセスと子プロセスを同時に実行できるため、親プロセスがメッセージを書き込んでいる間に子プロセスがメッセージを読み取るとどうなりますか?

子は、親によって作成されたパイプで利用可能なすべてのデータを読み取ることができます。マニュアルページによると:

POSIX.1 は、PIPE_BUF バイトより小さい書き込みはアトミックでなければならないことを指定します。出力データは連続シーケンスでパイプに書き込まれます。 PIPE_BUF バイトを超える書き込みは非原子的である可能性があります。カーネルは、データを他のプロセスによって書き込まれたデータとインターリーブできます。 POSIX.1では、PIPE_BUFは512バイト以上でなければなりません。 (Linuxでは、PIPE_BUFは4096バイトです。)

あなたの質問#4:

親と子を同時に実行できるので、子が読み取られ、親がまだパイプに何も書いていない場合はどうなりますか?

マニュアルページには次のように記載されています。

プロセスが空のパイプからデータを読み取ろうとすると、read(2)はデータが利用可能になるまでブロックされます。プロセスがパイプ全体(以下を参照)に書き込みを試みると、書き込みが完了するのに十分なデータがパイプから読み取られるまでwrite(2)がブロックされます。

コメントの質問に答えてください。

質問 1 と 2 の場合、不要な端を閉じないとプログラムに影響はありません。

パイプラインの動作を妨げてはいけませんが、プログラムが使用するリソースには少しスペースがかかります。パイプの不要な端を閉じても、これらのリソースは保持されません。

質問3の場合、これは親が書いた内容を子供が読むことを意味します。両親が書くべきことを完了したことを子供はどうやって知ることができますか?

マニュアルページには次のように記載されています。

パイプが提供する通信チャネルはバイトストリームです。メッセージ境界の概念はありません。

これは、パイプが送信するデータに興味がないことを意味します。 「メッセージ」が何を意味するのか、親が書き込みを終えたのか、さらに多くのデータを書きたいかどうかもわかりません。

「完全なメッセージ」が何であるかを判断するには、独自の技術を実装する必要があります。たとえば、親は\0特殊文字(パイプが使用される特定のコンテキストで意味のある他の文字)を送信して、完全なメッセージが作成されたことを子に指示できます。

答え2

よりpipe(7)マニュアルページ

「パイプとFIFOのI / O」の下に次のように表示されます。

パイプの書き込みの終わりを参照するすべてのファイル記述子が閉じられている場合、パイプがread(2)を試みるとファイルの終わりが表示されます(read(2)は0を返します)。

パイプの読み取りの終わりを参照するすべてのファイル記述子が閉じられた場合、write(2)は呼び出しプロセスに対してSIGPIPE信号が生成されるようにします。

子プロセスが書き込み側のコピーを閉じるようにして(使用しない)、子プロセスがいつ検出できるかを確認します。この方法。子プロセスが書き込み側を開いたままにすると、本質的にそれ自体が待機するため、パイプ内のEOFを見ることはできません。 (2)

同様に、親プロセスが読み取り側のコピーを閉じるようにすることで、子プロセスが消えるタイミングを親プロセスで検出できます。 (1)

可変量のデータを読み書きする試みの合計read()または戻り値を確認するコードとは異なるため、基本的にSIGPIPEシグナルを受け取る親以外には意味がありません。write()

書き込み後に親の書き込み側を閉じ、子から読み取った後に読み取り側を閉じるのは一般的な管理作業です。プロセスがすぐに終了すると、明示的な終了は効果がありません。

あなたの質問3と4が同じかどうかはわかりませんが、読書内容がない場合に読者が読んだ場合、システムコールはブロックされます。

プロセスが空のパイプからデータを読み取ろうとすると、read(2)はデータが利用可能になるまでブロックされます。

作成者が他のタスクの実行中に作成すると、データはオペレーティングシステムのバッファにコピーされます(少なくとも十分なスペースがある場合)。そうでない場合、作成者は以下をブロックします。

プロセスがパイプ全体(以下を参照)に書き込みを試みると、書き込みが完了するのに十分なデータがパイプから読み取られるまでwrite(2)がブロックされます。

「下」は「パイプライン容量」セクションです。

同時にこれを実行すると、オペレーティングシステムはデータのみをコピーします。

答え3

他の人は素晴らしい答えを与えた。これは実験的に役立つための上記の答えに対するilkkachuのコメントの拡張です。元の投稿プログラムを少し変更してから、プログラムを検討してください。

#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <time.h>
#include <unistd.h>

struct message {
    char content[15];
};

int main(void) {
    int fds[2];

    if (pipe(fds) < 0) {
        printf("Error creating the pipe\n");
        return 1;
    }

    srand(time(NULL));

    const pid_t pid = fork();
    if (pid < 0) {
        printf("Error forking a child");
        return 1;
    }

    if (pid > 0) {
        const int message_count = (rand() % 9) + 1;
        const struct message message_buffer = {
            .content = "Hello, World\n",
        };

        /* 1 */ close(fds[0]);

        for (int i = 0; i < message_count; ++i) {
            write(fds[1], &message_buffer, sizeof(message_buffer));
        }

        /* 2 */ close(fds[1]);

        wait(NULL);
    } else {
        struct message read_buffer;

        /* 3 */ close(fds[1]);

        while (read(fds[0], &read_buffer, sizeof(read_buffer)) > 0) {
            printf("The Message: %s", read_buffer.content);
        }

        /* 4 */ close(fds[0]);
    }

    return 0;
}

このバージョンは送信されません。メッセージを送信すると、1から10までの任意の数のメッセージが送信されます。これにより、子プロセスは読み取る必要があるメッセージの数を事前に知ることができません。パイプからすべての内容を読み取ると停止します。そしてパイプには他に何も書くことができません(つまり、すべての書き込みが閉じられ、負の値がread返される場合)以下は、いくつかの実行例です。

$ ./a.out
The Message: Hello, World
$ ./a.out
The Message: Hello, World
The Message: Hello, World
The Message: Hello, World
The Message: Hello, World
The Message: Hello, World
The Message: Hello, World
The Message: Hello, World
The Message: Hello, World
$ ./a.out
The Message: Hello, World
The Message: Hello, World
The Message: Hello, World
The Message: Hello, World
$

close()パイプラインに関連する各呼び出しの前に説明を追加しました。 (1)行をコメントアウトすると、プログラムの動作は大きく変わりません。

$ ./a.out
The Message: Hello, World
The Message: Hello, World
The Message: Hello, World
$ ./a.out
The Message: Hello, World
The Message: Hello, World
$

(2)行のみコメントアウトすると、プログラム実行時にデッドロックが発生します。たとえば、次のようになります。

$ ./a.out
The Message: Hello, World
The Message: Hello, World
(program hangs here)

なぜ?親プロセスがパイプの書き込み端を閉じない場合、子プロセスは呼び出しで永久にブロックされ、read追加データを待ちます。 (readもしあればどのパイプの書き込みの終わりに関連付けられたファイル記述子を開きます。 )その後、親はへの呼び出しで永久にブロックされますwait。両親と子はお互いを永遠に待ちます。

(3)行のみコメントアウトすると、プログラム実行時にデッドロックが発生します。たとえば、次のようになります。

$ ./a.out
The Message: Hello, World
The Message: Hello, World
The Message: Hello, World
The Message: Hello, World
(program hangs here)

なぜ?同様に、readペアへの呼び出しは存在する限りブロックされます。どのパイプの書き込みの終わりに関連付けられたファイル記述子を開くと、子プロセスには1つあります。その結果、子供は再び通話がブロックされread、親は通話がブロックされ、wait進行しなくなります。

最後に(4)行をコメントアウトすると、プログラムの動作は大きく変わりません。

$ ./a.out
The Message: Hello, World
The Message: Hello, World
The Message: Hello, World
$ ./a.out
The Message: Hello, World
The Message: Hello, World
The Message: Hello, World
The Message: Hello, World
The Message: Hello, World
$

すべてのプロセスが不要なファイル記述子を閉じるようにすることで、決して来ないパイプからデータを読み取るのを待つプロセスがブロックされないようにすることができます。

答え4

場合によっては、使用されていないパイプ記述子を適切に閉じないと、重要な結果が生じる可能性があります。

  1. 作成者が読み取り記述子をオフにする理由

リーダーがないパイプに書き込もうとすると、このSIGPIPE信号が生成されます。これはリーダーが離れたときに発生する必要があります(プロセスが終了するなど)。

しかし、ライターがパイプの読取りディスクリプタを開いたままにしている間、SIGPIPE技術的にパイプが損傷を受けず、ライター自体が可能なリーダーであるため、何も起こりません。

  1. 読者が書き込み記述子をオフにする理由

パイプを閉じると、EOF条件が生成されます。リーダーが書き込みディスクリプタのコピーを閉じないと、リーダーはパイプにさらにデータを書き込むことができるため、ライターはEOFを送信できません。

関連情報