ホームページ > 記事 > システムチュートリアル > Linux パイプと FIFO アプリケーション ノート
######概要######
wc
をそれぞれ実行する 2 つのプロセスを作成します (fork() と
exec を介して)。 ( ) 完了)、次のように:
上の図からわかるように、パイプラインは一連の水道管とみなすことができ、データをあるプロセスから別のプロセスに流すことができます。これがパイプラインの名前の由来でもあります。
上の図からわかるように、2 つのプロセスがパイプに接続されているため、書き込みプロセス
wc
は、標準入力 (ファイル記述子 0) をパイプの読み取り側に接続します。実際、これら 2 つのプロセスはパイプの存在を認識しておらず、標準のファイル記述子からデータを読み書きするだけです。シェルがその作業を実行する必要があります。
パイプはバイト ストリームです
lseek()
を使用してランダム化することはできませんデータへのアクセス
パイプからデータを読み取るパイプの書き込み側が閉じている場合、パイプからデータを読み取るプロセスは、パイプ内の残りのデータをすべて読み取った後、ファイルの終わりを確認します(つまり、
パイプは一方通行です
パイプライン内のデータの送信方向は一方向です。パイプの一端は書き込みに使用され、もう一端は読み取りに使用されます。
他の一部の UNIX 実装、特に System V リリース 4 から進化したものでは、パイプは双方向です (いわゆるストリーム パイプ)。双方向パイプはどの UNIX 標準でも規定されていないため、双方向パイプを提供する実装であっても、このセマンティクスに依存しないことが最善です。代わりに、UNIX ドメイン ストリーム ソケット ペア (socketpair()
システム コールで作成) を使用できます。これは標準の双方向通信メカニズムを提供し、そのセマンティクスはストリーム パイプと同等です。
複数のプロセスが同じパイプに書き込む場合、一度に書き込むデータ量が PIPE_BUF バイトを超えなければ、書き込まれたデータが互いに混在しないことが保証されます。
SUSv3 では、PIPE_BUF が少なくとも _POSIX_PIPE_BUF(512)
である必要があります。実装では、PIPE_BUF (<limits.h></limits.h>
内) を定義するか、fpathconf(fd,_PC_PIPE_BUF)
への呼び出しを許可して、アトミックな書き込み操作の実際の上限を返す必要があります (SHOULD)。 PIPE_BUF は UNIX 実装によって異なります。たとえば、FreeBSD 6.0 ではその値は 512 バイト、Tru64 5.1 ではその値は 4096 バイト、Solaris 8 ではその値は 5120 バイトです。 Linux では、PIPE_BUF の値は 4096 です。
write()##) #すべてのデータがパイプに書き込まれるまで呼び出しはブロックされます)
write() は必要に応じてブロックされます。書き込まれたデータが PIPE_BUF バイトより大きい場合、
write() はパイプ全体を満たすためにできるだけ多くのデータを転送し、読み取りプロセスによってパイプからデータが削除されるまでブロックします。このようなブロッキング
write() がシグナル ハンドラーによって中断された場合、呼び出しはブロックが解除され、パイプに正常に転送されたバイト数が返されますが、これは書き込み要求されたバイト数よりも少なくなります。入力されたバイト数 (いわゆる部分書き込み)。
SUSv3 はパイプラインのストレージ容量を指定しません。 2.6.11 より前の Linux カーネルでは、パイプのストレージ容量はシステム ページ サイズと一致します (たとえば、x86-32 では 4096 バイト)。Linux 2.6.11 以降では、パイプのストレージ容量は 65,536 です。バイト。他の UNIX 実装上のパイプのストレージ機能は異なる場合があります。
一般来讲,一个应用程序无需知道管道的实际存储能力。如果需要防止写者进程阻塞,那么从管道中读取数据的进程应该被设计成以尽可能快的速度从管道中读取数据。
#include int pipe(int fd[2]);
pipe()
创建一个新管道fd
中返回两个打开的文件描述符,一个表示管道的读取端 fd[0]
,一个表示管道的写入端 fd[1]
调用 pipe()
函数时,首先在内核中开辟一块缓冲区用于通信,它有一个读端和一个写端,然后通过 fd
参数传出给用户进程两个文件描述符,fd[0]
指向管道的读端,fd[1]
指向管道的写段。
不要用 fd[0]
写数据,也不要用 fd[1]
读数据,其行为未定义的,但在有些系统上可能会返回 -1 表示调用失败。数据只能从 fd[0]
中读取,数据也只能写入到fd[1]
,不能倒过来。
与所有文件描述符一样,可以使用 read()
和 write()
系统调用来在管道上执行 IO,一旦向管道的写入端写入数据之后立即就能从管道的读取端读取数据。管道上的 read()
调用会读取的数据量为所请求的字节数与管道中当前存在的字节数两者之间的较小值。当管道为空时,读取操作阻塞。
パイプ上で stdio 関数 (printf()
、scanf()
など) を使用することもできます。最初に fdopen()# を使用するだけで済みます。 ## を取得するには、
filedes の記述子に対応するファイル ストリームだけで十分です。ただし、これを行う場合は、
stdio バッファリングの問題に対処する必要があります。
パイプは、血縁関係でのプロセス内通信に使用できます (子プロセスは親プロセスのファイル記述子のコピーを継承します):
単一のパイプを全二重として使用したり、対応する読み取り/書き込みエンドを閉じずに半二重として使用したりすることはお勧めできません。デッドロックが発生する可能性があります。2 つのプロセスが試行すると、デッドロックが発生する可能性があります。パイプからデータを読み取る場合、どのプロセスが最初に正常に読み取るかを判断することは不可能であり、その結果、2 つのプロセスがデータを求めて競合します。この競合状態の発生を防ぐには、何らかの同期メカニズムを使用する必要があります。この時点で、デッドロックの問題を考慮する必要があります。両方のプロセスが空のパイプからデータを読み取ろうとしたり、満杯のパイプにデータを書き込もうとしたりするとデッドロックが発生する可能性があるためです。
双方向のデータ フローが必要な場合は、各方向に 1 つずつ、2 つのパイプを作成できます。
未使用のパイプ ファイル記述子を閉じます
read()
がブロックされます。
当一个进程视图向一个管道中写入数据但没有任何进程拥有该管道的打开着的读取描述符时,内核会向写入进程发送一个 SIGPIPE
信号,默认情况下,这个信号将会杀死进程,但进程可以选择忽略或者设置信号处理器,这样 write()
将因为 EPIPE
错误而失败。收到 SIGPIPE
信号和得到 EPIPE
错误对于标识管道的状态是有意义的,这就是为什么需要关闭管道的未使用读取描述符的原因。如果写入进程没有关闭管道的读取端,那么即使在其他进程已经关闭了管道的读取端之后,写入进程仍然能够向管道写入数据,最后写入进程会将数据充满整个管道,后续的写入请求会将永远阻塞。
当管道被创建之后,为管道的两端分配的文件描述符是可用描述符中数值最小的两个,由于通常情况下,进程已经使用了描述符 0,1,2,因此会为管道分配一些数值更大的描述符。如果需要使用管道连接两个过滤器(即从 stdin
读取和写入到 stdout
),使得一个程序的标准输出被重定向到管道中,就需要采用复制文件描述符技术。
int pfd[2]; pipe(pfd); close(STDOUT_FILENO); dup2(pfd[1],STDOUT_FILENO);
上面这些调用的最终结果是进程的标准输出被绑定到管道的写入端,而对应的一组调用可以用来将进程的标准的输入绑定到管道的读取端上。
popen()
#include FILE *popen (const char *command, const char *mode);
pipe()
和 close()
是最底层的系统调用,它的进一步封装是 popen()
和 pclose()
popen()
函数创建了一个管道,然后创建了一个子进程来执行 shell,而 shell 又创建了一个子进程来执行command
字符串
mode
参数是一个字符串:
mode
是 r
)还是将数据写入到管道中(mode
是 w
)command
中进行双向通信mode
的取值确定了所执行的命令的标准输出是连接到管道的写入端还是将其标准输入连接到管道的读取端popen()
在成功时会返回可供 stdio
库函数使用的文件流指针。当发生错误时,popen()
会返回 NULL
并设置 errno
以标示出发生错误的原因popen()
调用之后,调用进程使用管道来读取 command
的输出或使用管道向其发送输入。与使用 pipe()
创建的管道一样,当从管道中读取数据时,调用进程在 command
关闭管道的写入端之后会看到文件结束;当向管道写入数据时,如果 command
已经关闭了管道的读取端,那么调用进程就会收到 SIGPIPE
信号并得到 EPIPE
错误#include int pclose ( FILE * stream);
pclose()
函数关闭管道并等待子进程中的 shell 终止(不应该使用 fclose()
函数,因为它不会等待子进程。)pclose()
成功すると、子プロセス内のシェルの終了ステータス (つまり、シェルがシグナルによって強制終了されない限り、シェルによって実行された最後のコマンドの終了ステータス) が返されます。 system()
と同様に、シェルが実行できない場合、pclose()
は _exit(127)# を呼び出すことによって、子プロセスのシェルと同様の値を返します。 ## 同じを終了するには
#
別のエラーが発生した場合、
が system()
と同じである必要があります。つまり、内部的にはwaitpid ()
シグナル ハンドラーによって通話が中断された後、通話を自動的に再開します。
system()
popen() は特権プロセスでは決して使用しないでください。
popen
command
コマンドを開始する前に、プログラムはまず command
文字列を分析するためにシェルを開始します。さまざまなシェル拡張 (ワイルドカードなど) を使用できるため、## を使用できるようになります。 #popen() 非常に複雑なシェル コマンドの呼び出し
呼び出しごとに、要求されたプログラムだけでなくシェルも起動する必要があります。つまり、各
popen() は 2 つのプロセスを開始します。从效率和资源的角度看,<code style="font-size: 14px;padding: 2px 4px;border-radius: 4px;margin: 0 2px;font-family: Operator Mono, Consolas, Monaco, Menlo, monospace;color: #009688">popen()
函数的调用比正常方式要慢一些
pipe()` VS `popen()
pipe()
是一个底层调用,popen()
是一个高级的函数pipe()
单纯的创建管道,而 popen()
创建管道的同时 fork()
子进程popen()
在两个进程中传递数据时需要调用 shell 来解释请求命令;pipe()
在两个进程中传递数据不需要启动 shell 来解释请求命令,同时提供了对读写数据的更多控制(popen()
必须时 shell 命令,pipe()
则无硬性要求)popen()
函数是基于文件流(FILE)工作的,而 pipe()
是基于文件描述符工作的,所以在使用 pipe()
后,数据必须要用底层的read()
和 write()
调用来读取和发送由于 popen()
调用返回的文件流指针没有引用一个终端,因此 stdio 库会对这种流应用块缓冲。这意味着当 mode 的值为 w 来调用 popen()
时,默认情况下只有当 stdio 缓冲区被充满或者使用 pclose()
关闭了管道之后才会被发送到管道的另一端的子进程。在很多情况下,这种处理方式是不存在问题的。ただし、子プロセスがパイプからデータをすぐに受信できるようにする必要がある場合は、fflush()
を定期的に呼び出すか、setbuf(fp, NULL)
呼び出しを使用して、標準入出力バッファリングを無効にします。この手法は、pipe()
システム コールを使用してパイプを作成し、その後 fdopen()
を使用してパイプの書き込み側に対応する stdio ストリームを取得するときにも使用できます
popen()
を呼び出すプロセスがパイプから読み取っている場合 (つまり、mode
が r
である場合)、物事はそれほど単純ではありません。この場合、子プロセスが stdio ライブラリを使用している場合、明示的に fflush()
または setbuf()
を呼び出さない限り、その出力は子プロセスによってのみ埋められます。 fclose()
が呼び出されるまで、呼び出しプロセスは stdio バッファを使用できません。 (pipe()
を使用して作成されたパイプから読み取り、もう一方の端に書き込むプロセスが stdio ライブラリを使用している場合にも、同じルールが適用されます。) これが問題である場合、考えられる対策は次のとおりです。子プロセスで実行されているプログラムのソース コードを変更して setbuf()
または fflush()
への呼び出しを含めることができない限り、取得できるものは比較的限られています。
ソース コードを変更できない場合は、疑似ターミナルを使用してパイプを置き換えることができます。擬似端末は、プロセスに対して端末として見える IPC チャネルです。その結果、stdio ライブラリはバッファ内のデータを 1 行ずつ出力します。
上記のパイプラインはプロセス間通信を実現しますが、いくつかの制限があります:
任意の 2 つのプロセス間の通信を可能にするために、名前付きパイプ (名前付きパイプまたは FIFO) が提案されています。
read()
,write()
,close()
。与管道一样,FIFO 也有一个写入端和读取端,并且总是遵循先进先出的原则,即第一个进来的数据会第一个被读走mkfifo
命令可以在 shell 中创建一个 FIFO:mkfifo [-m mode] pathname
pathname
是创建的 FIFO 的名称,-m
选项指定权限 mode
,其工作方式与 chmod
命令一样fstat()
和 stat()
函数会在 stat
结构的 st_mode
字段返回 S_IFIFO
,使用 ls -l
列出文件时,FIFO 文件在第一列的类型为 p
,ls -F
会在 FIFO 路径名后面附加管道符 |
#include #include int mkfifo(const char *pathname,mode_t mode);
mode
参数指定了新 FIFO 的权限,这些权限会按照进程的 umask
值来取掩码open() O_RDONLY
标记)将会阻塞直到另一个进程打开 FIFO 以写入数(open() O_WRONLY
标记)为止。相应地,打开一个 FIFO 以写入数据将会阻塞直到另一个进程打开 FIFO 以读取数据为止。换句话说,打开一个 FIFO 会同步读取进程和写入进程。如果一个 FIFO 的另一端已经打开(可能是因为一对进程已经打开了 FIFO 的两端),那么open()
调用会立即成功。在大多数 Unix 实现上(包含 Linux),当打开一个 FIFO 时可以通过指定 O_RDWR
标记来绕过打开 FIFO 时的阻塞行为。这样,open()
会立即返回,但无法使用返回的文件描述符在 FIFO 上读取和写入数据。这种做法破坏了 FIFO 的 IO 模型,SUSv3 明确指出以 O_RDWR
标记打开一个 FIFO 的结果是未知的,因此出于可移植性的原因,开发人员不应该使用这项技术。对于那些需要避免在打开 FIFO 时发生阻塞的需求,open()
的 O_NONBLOCK
标记提供了一种标准化的方法来完成这个任务:
open(const char *path, O_RDONLY | O_NONBLOCK); open(const char *path, O_WRONLY | O_NONBLOCK);
在打开一个 FIFO 时避免使用 O_RDWR
标记还有另外一个原因,当采用那种方式调用 open()
之后,调用进程在从返回的文件描述符中读取数据时永远都不会看到文件结束,因为永远都至少存在一个文件描述符被打开着以等待数据被写入 FIFO,即进程从中读取数据的那个描述符。
tee
创建双重管道线shell 管道线的其中一个特征是它们是线性的,管道线中的每个进程都能读取前一个进程产生的数据并将数据发送到其后一个进程中,使用 FIFO 就能够在管道线中创建子进程,这样除了将一个进程的输出发送给管道线中的后面一个进程之外,还可以复制进程的输出并将数据发送到另一个进程中,要完成这个任务就需要使用 tee
命令,它将其从标准输入中读取到的数据复制两份并输出:一份写入标准输出,另一份写入到通过命令行参数指定的文件中。
mkfifo myfifo wc -l
当一个进程打开一个 FIFO 的一端时,如果 FIFO 的另一端还没有被打开,那么该进程会被阻塞。但有些时候阻塞并不是期望的行为,而这可以通过在调用 open()
时指定 O_NONBLOCK
标记来实现。
如果 FIFO 的另一端已经被打开,那么 O_NONBLOCK
对 open()
调用不会产生任何影响,它会像往常一样立即成功地打开 FIFO。只有当 FIFO 的另一端还没有被打开的时候 O_NONBLOCK
标记才会起作用,而具体产生的影响则依赖于打开 FIFO 是用于读取还是用于写入的:
open()
调用会立即成功(就像 FIFO 的另一端已经被打开一样)open()
调用会失败,并将 errno
设置为 ENXIO
为读取而打开 FIFO 和为写入而打开 FIFO 时 O_NONBLOCK
标记所起的作用不同是有原因的。当 FIFO 的另一个端没有写者时打开一个 FIFO 以便读取数据是没有问题的,因为任何试图从 FIFO 读取数据的操作都不会返回任何数据。但当试图向没有读者的 FIFO 中写入数据时将会导致 SIGPIPE
信号的产生以及 write()
返回 EPIPE
错误。
在 FIFO 上调用 open()
的语义总结如下:
在打开一个 FIFO 时,使用 O_NOBLOCK
标记存在两个目的:
O_NOBLOCK
标记以便读取数据,接着打开 FIFO 以便写入数据例如,下面的情况将会发生死锁:
read()
和 write()
O_NONBLOCK
标记不仅会影响 open()
的语义,而且还会影响——因为在打开的文件描述中这个标记仍然被设置着——后续的 read()
和 write()
调用的语义。
有些时候需要修改一个已经打开的 FIFO(或另一种类型的文件)的 O_NONBLOCK
标记的状态,具体存在这个需求的场景包括以下几种:
O_NONBLOCK
打开了一个 FIFO 但需要后续的 read()
和 write()
在阻塞模式下运行pipe()
返回的一个文件描述符的非阻塞模式。更一般地,可能需要更改从除 open()
调用之外的其他调用中,如每个由 shell 运行的新程序中自动被打开的三个标准描述符的其中一个或 socket()
返回的文件描述符,取得的任意文件描述符的非阻塞状态O_NONBLOCK
设置的开启和关闭状态当碰到上面的需求时可以使用 fcntl()
启用或禁用打开着的文件的 O_NONBLOCK
状态标记。通过下面的代码(忽略的错误检查)可以启用这个标记:
int flags; flags = fcntl(fd, F_GETFL); flags != O_NONBLOCK; fcntl(fd, F_SETFL, flags);
通过下面的代码可以禁用这个标记:
flags = fcntl(fd, F_GETFL); flags &= ~O_NONBLOCK; fcntl(fd, F_SETFL, flags);
read()
和 write()
的语义FIFO 上的 read()
操作:
只有当没有数据并且写入端没有被打开时阻塞和非阻塞读取之间才存在差别。在这种情况下,普通的 read()
会被阻塞,而非阻塞 read()
会失败并返回 EAGAIN
错误。
当 O_NONBLOCK
标记与 PIPE_BUF
限制共同起作用时 O_NONBLOCK
标记对象管道或 FIFO 写入数据的影响会变得复杂。
FIFO 上的 write()
操作:
O_NONBLOCK
フラグにより、データをすぐに転送できない場合、パイプまたは FIFO の write()
が失敗します (エラー EAGAIN
が発生します)。これは、PIPE_BUF
バイトを書き込んだ後、パイプまたは FIFO に十分なスペースがない場合、カーネルが操作をすぐに完了できず、部分的な書き込みができないため、write()
が失敗することを意味します。 PIPE_BUF
バイト PIPE_BUF
バイトを超える場合、書き込み操作をアトミックにする必要はありません。したがって、write()
は、パイプまたは FIFO を満たすためにできるだけ多くのバイトを転送 (部分的に書き込み) します。この場合、write()
から返される値は実際に転送されたバイト数であり、呼び出し元は残りのバイトの書き込みを再試行する必要があります。しかし、パイプまたは FIFO がいっぱいで 1 バイトも転送できない場合、write()
は失敗し、EAGAIN
エラー 以上がLinux パイプと FIFO アプリケーション ノートの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。