Linux 管道
管道是允许单向通信的通信设备。数据从管道的一端写入并从管道的另一端读出。管道是串行设备;数据总是以写入时的顺序被读取出来。通常,管道用于同一进程的两个不同线程或在父子进程间通行。
在shell中,使用|符号创建管道。例如,下面的shell命令将导致shell创建两个子进程,分别用于ls和less命令:
% ls | less
shell同时也创建了一个管道用于连接ls子进程的标准输出和less进程的标准输入。通过ls列出的文件名传输到less中,并且其顺序与它直接在终端显示一致。
管道的数据容量是有限的。如果写进程速度快于读进程读取数据,并且管道不能存储过多的数据,那么写进程将会阻塞直到有更多的容量可以使用。因此,管道将自动同步两个进程。
创建管道
要创建一个管道,调用pipe命令。指定一个大小为2的整数数组。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文件描述符读取回来。
父子进程间通信
通过pipe创建的文件描述符只在当前进程和其子进程中有效。进程文件描述符不能传递给不相关进程;然而,当进程调用fork时,文件描述符被复制到子进程中。因此,管道只能连接相关进程。
如下列出的程序中,fork创建一个子进程。子进程继承父进程的管道文件描述符。父进程写入字符串在管道中,子进程把它读取出来。这个程序使用fdopen将文件描述符转换为FILE*的文件流。因为使用流而不是文件描述符,可以让我们使用高层的标准C的I/O库函数,例如printf和fgets。
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
/* Write COUNT copies of MESSAGE to STREAM, pausing for a second
* between each. */
void writer(const char *message, int count, FILE *stream)
{
for (; count > 0; --count) {
/* Write the message to the stream, and send it off immediately. */
fprintf(stream, "%s\n", message);
fflush(stream);
/* Snooze a while. */
sleep(1);
}
}
/* Read random strings from the stream as long as possible. */
void reader(FILE *stream)
{
char buffer[1024];
/* Read until we hit the end of the stream. fgets reads until
* either a newline or the end-of-file. */
while (!feof(stream) && !ferror(stream) &&
fgets(buffer, sizeof(buffer), stream) != NULL)
fputs(buffer, stdout);
}<pre name="code" class="cpp">int main()
{
int fds[2];
pid_t pid;
/* Create a pipe. File descriptors for the two ends of the
* pipe are placed in fds. */
pipe(fds);
/* Fork a child process. */
if ((pid = fork()) == 0) {
FILE *stream;
/* This is the child process. Close our copy the write end of
* the file descriptor. */
close(fds[1]);
stream = fdopen(fds[0], "r");
reader(stream);
close(fds[0]);
} else if (pid == -1) {
} else {
/* This is the parent process. */
FILE *stream;
/* Close our copy of the read end of the file descriptor. */
close(fds[0]);
stream = fdopen(fds[0], "r");
reader(stream);
close(fds[1]);
}
return 0;
}
在main函数的开始,声明大小为2的整数数组。通过pipe创建管道,并将读写文件描述符放在数组中。程序接着创建子进程。在关闭了读管道后,父进程开始向管道中写入字符串。子进程在关闭写管道后,从读管道中读出字符串。
注意,在writer函数每次写入数据后,父进程通过调用fflush刷新管道。否则,字符串可能不会字符通过管道发送出去。
当调用ls | less命令时,出现两个进程:一个ls子进程和一个less子进程。这两个进程都继承管道文件描述符,因此,它们可以通过管道通信。为了使不相关的进程通信,可以使用FIFO代替。
重定向标准输入、输出和错误输出
通常,你需要创建一个子进程,并设置它管道的一端为标准输入或输出。使用dup2函数,你可以将一个文件描述符等同于另一个文件描述符。例如,为了将fd文件描述符的输入重定向到标准输入,可以使用以下方式:
dup2 (fd,STDI_FILENO);
常量STDIN_FILENO代表标准输入的文件描述符,其值为0。该函数关闭标准输入,随后作为fd的副本重新打开标准输入,这样,这两个文件描述符就可交换使用。Equated file descriptors share the same file position and the sameset of file status flags.Thus, characters read from fd are not reread fromstandard input.
popen和pclose
管道通常用于向程序的子进程发送或接收数据。popen和pclose函数简化了调用pipe,fork,dup2,exec和fdopen。
#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函数创建子进程执行sort命令。第二个参数“w”表明该进程想要向子进程中写入数据。popen函数的返回值是管道的一端;另一端用于连接子进程的标准输入。在写入数据完成后,调用pclose函数关闭子进程的流,等待进程终止,并返回状态值。
函数popen的第一个参数的执行类似于执行shell命令。shell按照通常的方式搜寻PATH环境变量找到程序执行。如果第二个参数是“r”,函数返回子进程的标准输出流,这样,父进程便可读取输出。如果第二个参数是“w”,函数返回子进程的标准输入流,父进程便可写入数据。如果出现错误,函数popen返回NULL指针。
调用pclose关闭由popen返回的流。在关闭指定的流后,pclose等待子进程终止。
FIFOs
先进先出(FIFO)文件是在文件系统中有名称的管道。任何进程都可打开或关闭FIFO;管道两端的进程不需要彼此相关。FIFOs通常也被称为命名管道。
要创建命名管道,可以使用mkfifo命令。在命令行上指定命名管道的路径。例如,创建一个/tmp/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
接着,输入一些文本。每次按下Enter键时,一行的文本内容通过FIFO发送并在第一个窗口中显示。通过在第二个窗口中按下Ctrl+D关闭FIFO。通过以下命令删除FIFO:
% rm /tmp/fifo
1 创建FIFO
通过mkfifo以编程的方式创建FIFO。第一个参数是FIFO创建的路径;第二个参数指定管道的拥有者,组和权限。因为,关闭必定有读和写,因此权限必须包含读和写的权限。如果管道不能创建(例如,如果该文件已经存在),mkfifo返回-1。如果调用mkfifo,需要包含<sys/types.h>和<sys/stat.h>。
2 访问FIFO
对FIFO的访问就像访问普通文件一样。通过FIFO通信,一个程序必须以写的方式打开,另一个程序则以读的方式打开。低级I/O函数或者标准C库I/O函数都可使用。
例如,使用低级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可以有多个读进程或写进程。Bytesfrom each writer are written atomically up to a maximum size of PIPE_BUF (4KBon Linux). Chunks from simultaneous writers can be interleaved. Similar rulesapply to simultaneous reads.
3 与Windows命名管道的不同
Win32操作系统中的管道类似与Linux中的管道。主要的区别集中在命名管道上,对于Win32来说,命名管道的功能更像是套接字。Win32命名管道可以通过网络连接分离的计算机的进程。同样,Win32允许多个读写连接到同一命名管道上,并不会存在交叉数据,并且支持双向通信。