管道是一个允许单向信息传递的通信设备。从管道“写入端”写入的数据可以从“读取
端”读回。管道是一个串行设备;从管道中读取的数据总保持它们被写入时的顺序。一般来
说,管道通常用于一个进程中两个线程之间的通信,或用于父子进程之间的通信。
在shell 中,| 符号用于创建一个管道。例如,下面的程序会使 shell 创建两个子进程,
一个运行ls而一个运行less:
% ls | less
Shell同时还会创建一个管道,将运行 ls的子进程的标准输出连接到运行less 的子进
程的标准输入。由ls输出的文件名将被按照与发送到终端时完全相同的顺序发送到less
的标准输入。
管道的数据容量是有限的。如果写入的进程写入数据的速度比读取进程消耗数据的速
更快,且管道无法容纳更多数据的时候,写入端的进程将被阻塞,直到管道中出现更多的空
间为止。换言之,管道可以自动同步两个进程。
5.4.1 创建管道
要创建一个管道,请调用 pipe 命令。提供一个包含两个 int 值的数组作为参数。Pipe
命令会将读取端文件描述符保存在数组的第0 个元素而将写入端文件描述符保存在第 1 个
元素中。举个例子,考虑如下代码:
int pipe_fds[2];
int read_fd;
int write_fd;
pipe (pipe_fds);
read_fd = pipe_fds[0];
write_fd = pipe_fds[1];
对文件描述符write_fd 写入的数据可以从read_fd中读回。
5.4.2 父子进程之间的通信
通过调用pipe 得到的文件描述符只在调用进程及子进程中有效。一个进程中的文件描
述符不能传递给另一个无关进程;不过,当这个进程调用 fork 的时候,文件描述符将复制
给新创建的子进程。因此,管道只能用于连接相关的进程。
列表5.7中的程序中,fork 产生了一个子进程。子进程继承了指向管道的文件描述符。
父进程向管道写入一个字符串,然后子进程将字符串读出。实例程序将文件描述符利用
fdopen 函数转换为FILE *流。因为我们使用文件流而不是文件描述符,我们可以使用包括
printf 和scanf 在内的标准C 库提供的高级 I/O 函数。
代码列表 5.7 (pipe.c)通过管道与子进程通信
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
/* 将 COUNT 份 MESSAGE 的副本写入 STREAM,每次写入之后暂停 1 秒钟 */
void writer (const char* message, int count, FILE* stream)
{
for (; count > 0; --count) {
/* 将消息写入流,然后立刻发送 */
fprintf (stream, "%s\n", message);
fflush (stream);
/* 休息,休息一会儿 */
sleep (1);
}
}
/* 从流中读取尽可能多的随机字符串 */
void reader (FILE* stream)
{
char buffer[1024];
/* 从流中读取直到流结束。 fgets 会不断读取直到遇见换行或文件结束符。 */
while (!feof (stream)
&& !ferror (stream)
&& fgets (buffer, sizeof (buffer), stream) != NULL)
fputs (buffer, stdout);
}
int main ()
{
int fds[2];
pid_t pid;
/* 创建一个管道。代表管道两端的文件描述符将被放置在 fds 中。*/
pipe (fds);
/* 创建子进程。*/
pid = fork ();
if (pid == (pid_t) 0) {
FILE* stream;
/* 这里是子进程。关闭我们得到的写入端文件描述符副本。*/
close (fds[1]);
/* 将读取端文件描述符转换为一个 FILE 对象然后从中读取消息 */
stream = fdopen (fds[0], "r");
reader (stream);
close (fds[0]);
}
clse {
/* 这是父进程。*/
FILE* stream;
/* 关闭我们的读取端文件描述符副本。*/
close (fds[0]);
/* 将写入端文件描述符转换为一个 FILE 对象然后写入数据。*/
stream = fdopen (fds[1], "w");
writer ("Hello, world.", 5, stream);
close (fds[1]);
}
return 0;
}
在 main 函数开始的时候,fds 被声明为一个包含两个整型变量的数组。对 pipe 的调
用创建了一个管道,并将读写两个文件描述符存放在这个数组中。程序随后创建一个子进程。
在关闭了管道的读取端之后,父进程开始向管道写入字符串。而在关闭了管道的写入端之后,
子进程开始从管道读取字符串。
注意,在writer 函数中,父进程在每次写入操作之后通过调用fflush 刷新管道内容。
否则,字符串可能不会立刻被通过管道发送出去。
当你调用ls | less 这个命令的时候会出现两次fork 过程:一次为ls创建子进程,
一次为less 创建子进程。两个进程都继承了这些指向管道的文件描述符,因此它们可以通
过管道进行通信。如果希望不相关的进程互相通信,应该用FIFO 代替管道。FIFO 将在5.4.5
节“FIFO”中进行介绍。
5.4.3 重定向标准输入、输出和错误流
你可能经常希望创建一个子进程,并将一个管道的一端设置为它的标准输入或输出。利
用dup2 系统调用你可以使一个文件描述符等效于另外一个。例如,下面的命令可以将一个
进程的标准输入重定向到文件描述符fd:
dup2 (fd, STDIN_FILENO);
符号常量STDIN_FILENO 代表指向标准输入的文件描述符。它的值为 0。这个函数会
关闭标准输入,然后将它作为fd的副本重新打开,从而使两个标识符可以互换使用。
相互等效的文件描述符之间共享文件访问位置和相同的一组文件状态标志。因此,从
fd中读取的字符不会再次从标志输入中被读取。
列表5.8 中的程序利用dup2 系统调用将一个管道的输出发送到sort命令当创建了
一个管道之后,程序生成了子进程。父进程向管道中写入一些字符串,而子进程利用dup2
将管道的读取端描述符复制到自己的标准输入,然后执行sort程序。
代码列表 5.8 (dup2.c )用 dup2 重定向管道输出
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
int main ()
{
int fds[2];
pid_t pid;
/* 创建管道。标识管道两端的文件描述符会被放置在 fds 中。*/
pipe (fds);
/* 产生子进程。*/
pid = fork ();
if (pid == (pid_t) 0) {
/* 这里是子进程。关闭我们的写入端描述符。*/
close (fds[1]);
/* 将读取端连接到标准输入*/
dup2 (fds[0], STDIN_FILENO);
/* 用 sort 替换子进程。*/
execlp ("sort", "sort", 0); ---????
}
else {
/* 这是父进程。*/
FILE* stream;
/* 关闭我们的读取端描述符。*/
close (fds[0]);
/* 将写入端描述符转换为一个 FILE 对象,然后将信息写入。*/
stream = fdopen (fds[1], "w");
fprintf (stream, "This is a test.\n");
fprintf (stream, "Hello, world.\n");
fprintf (stream, "My dog has fleas.\n");
fprintf (stream, "This program is great.\n");
fprintf (stream, "One fish, two fish.\n");
fflush (stream);
close (fds[1]);
/* 等待子进程结束。*/
waitpid (pid, NULL, 0);
}
return 0;
}
5.4.4 popen 和 pclose
管道的一个常见用途是与一个在子进程中运行的程序发送和接受数据。而 popen 和
pclose 函数简化了这个过程。它取代了对pipe、fork、dup2、exec 和fdopen 的一系
列调用。
下面将使用了popen 和pclose 的列表 5.9 与之前一个例子(列表 5.8 )进行比较。
代码列表 5.9 (popen.c)使用 popen 的示例
#include <stdio.h>
#include <unistd.h>
int main ()
{
FILE* stream = popen ("sort", "w");
fprintf (stream, "This is a test.\n");
fprintf (stream, "Hello, world.\n");
fprintf (stream, "My dog has fleas.\n");
fprintf (stream, "This program is great.\n");
fprintf (stream, "One fish, two fish.\n");
return pclose (stream);
}
通过调用popen 取代 pipe、fork、dup2 和execlp 等,一个子进程被创建以执行了
sort 命令,。第二个参数,” w”,指示出这个进程希望对子进程输出信息。Popen 的返回值
是管道的一端;另外一端连接到了子进程的标准输入。在数据输出结束后,pclose 关闭了
子进程的流,等待子进程结束,然后将子进程的返回值作为函数的返回值返回给调用进程。
传递给popen 的第一个参数会作为一条shell 命令在一个运行/bin/sh的子进程中执
行。Shell会照常搜索 PAT H 环境变量以寻找应执行的程序。如果第二个参数是"r",函数会
返回子进程的标准输出流以便父进程读取子进程的输出。如果第二个参数是"w" ,函数返回
子进程的标准输入流一边父进程发送数据。如果出现错误,popen 返回空指针。
调用pclose 会关闭一个由popen 返回的流。在关闭指定的流之后,pclose 将等待子
进程退出。
5.4.5 FIFO
先入先出(first-in, first-out, FIFO)文件是一个在文件系统中有一个名字的管道。任何
进程均可以打开或关闭FIFO;通过 FIFO 连接的进程不需要是彼此关联的。FIFO 也被称为
命名管道。
可以用mkfifo 命令创建FIFO;通过命令行参数指定FIFO 的路径。例如,运行这个
命令将在/tmp/fifo 创建一个FIFO:
% mkfifo /tmp/fifo
% ls -l /tmp/fifo
prw-rw-rw- 1 samuel users 0 Jan 16 14:04 /tmp/fifo
ls输出的第一个字符是p,表示这个文件实际是一个FIFO(命名管道)。在一个窗口
中这样从FIFO 中读取内容:
% cat < /tmp/fifo
在第二个窗口中这样向 FIFO 中写入内容:
% cat > /tmp/fifo
然后输入几行文字。每次你按下回车后,当前一行文字都会经由FIFO 发送到第一个窗
口。通过在第二个窗口中按Ctrl+D关闭这个FIFO。运行下面的命令删除这个 FIFO:
% rm /tmp/fifo
创建 FIFO
通过编程方法创建一个FIFO 需要调用mkfifo 函数。第一个参数是要创建FIFO 的路
径,第二个参数是被创建的 FIFO 的属主、属组和其它用户权限。关于权限,第十章“安全”
的10.3 节“文件系统权限”中进行了介绍。因为管道必然有程序读取信息、有程序写入信
息,因此权限中必须包含读写两种权限。如果无法成功创建管道(如当同名文件已经存在的
时候),mkfifo 返回-1 。当你调用 mkfifo 的时候需要包含<sys/types.h> 和
<sys/stat.h>。
访问 FIFO
访问FIFO与访问普通文件完全相同。要通过FIFO通信,必须有一个程序打开这个FIFO
写入信息,而另一个程序打开这个FIFO 读取信息。底层I/O 函数(open、write 、read、
close 等,列举在附录B “底层 I/O ”中)或C 库I/O 函数(fopen 、fprintf、fscanf、
fclose 等)均适用于访问FIFO。
例如,要利用底层I/O 将一块缓存区的数据写入FIFO 可以这样完成:
int fd = open (fifo_path, O_WRONLY);
write (fd, data, data_length);
close (fd);
利用C 库I/O 从FIFO 读取一个字符串可以这样做:
FILE* fifo = fopen (fifo_path, "r");
fscanf (fifo, "%s", buffer");
fclose (fifo);
FIFO 可以有多个读取进程和多个写入进程。来自每个写入进程的数据当到达
PIPE_BUF (Linux 系统中为 4KB )的时候会自动写入 FIFO。并发写入可能导致数据块的互
相穿插。同步读取也会出现相似的问题。
与Windows 命名管道的区别
Win32 操作系统的管道与Linux系统中提供的相当类似。(相关技术细节可以从Win32 库
文档中获得印证。)主要的区别在于,Win32 系统上的命名管道的功能更接近套接字。Win32
命名管道可以用于连接处于同一个网络中不同主机上的不同进程之间相互通信。Linux系统
中,套接字被用于这种情况。同时,Win32 保证同一个命名管道上的多个读——写连接不出
现数据交叉情况,而且管道可以用于双向交流。