普通的管道允许两个进程以标准的生产者-消费者的方式进行通信:生产者写到管道的一端(写端,write-end),而使用者从另一端(读端,read-end)来读取数据。因此,普通管道是单向的,只允许单向通信。如果需要双向通信,必须使用两个管道,每个管道将数据发送到不同的方向。接下来我们将演示在UNIX和Windows系统上构建普通管道。在两个程序示例中,一个进程向管道写入消息,而另一个进程读取这个消息。
UNIX系统上,普通管道是由下面的方法来构建的:
pipe(int fd[])
这个方法创建了通过int fd[]文件描述符来访问的一个管道:fd[0]是管道的读端,而fd[1]则是管道的写端。
UNIX将管道视为特殊类型的文件。因此,可以使用普通read()和write()系统调用来访问管道。
一个普通的管道不能从创建它的进程外部访问。通常,父进程创建一个管道并使用它来与通过fork()创建的子进程通信。回顾3.3.1节,子进程可以从其父进程继承打开的文件。由于管道是一种特殊类型的文件,因此该子程序可以从其父进程继承管道。图3.24说明了文件描述符fd与父进程和子进程之间的关系。
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <string.h>
#define BUFFER_SIZE 25
#define READ_END 0
#define WRITE_END 1
int main(void)
{
char write_msg[BUFFER_SIZE] = "Greetings";
char read_msg[BUFFER_SIZE];
pid_t pid;
int fd[2];
/* create the pipe */
if (pipe(fd) == -1) {
fprintf(stderr,"Pipe failed");
return 1;
}
/* now fork a child process */
pid = fork();
if (pid < 0) {
fprintf(stderr, "Fork failed");
return 1;
}
if (pid > 0) { /* parent process */
/* close the unused end of the pipe */
close(fd[READ_END]);
/* write to the pipe */
write(fd[WRITE_END], write_msg, strlen(write_msg)+1);
/* close the write end of the pipe */
close(fd[WRITE_END]);
}
else { /* child process */
/* close the unused end of the pipe */
close(fd[WRITE_END]);
/* read from the pipe */
read(fd[READ_END], read_msg, BUFFER_SIZE);
printf("child read %s\n",read_msg);
/* close the write end of the pipe */
close(fd[READ_END]);
}
return 0;
}
在上面的的UNIX程序中,父进程创建一个管道,然后发送一个fork()调用来创建子进程。fork()调用之后会发生什么,这取决于数据如何流经管道。在这个实例中,父进程写入到管道中,而子进程则读取它。重要的是要注意到父进程和子进程最初都关闭了自己不会使用的管道的某一端。虽然代码所示的程序不需要此操作,但这样做,可以使得当写端关闭管道的时候,读端可以检测到文件结尾(read()的返回值为0).
Windows上的普通的管道称为匿名管道(anonymous pipes),它们的行为与UNIX的管道相似:它们是单向的,并且在通信过程中使用父子关系。此外,可以通过普通的ReadFile()和WriteFile()函数来完成对管道的读写。Windows API 创建管道是使用CreatePipe()函数,它传递四个参数。参数分别为:
- (1)读取的管道的单独句柄
- (2)写入的管道的单独句柄
- (3)STARTUPINFO结构的实例,用于指定子进程继承管道的句柄。
- (4)可以指定管道的大小(以字节为单位)。
Window 父节点代码:
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
#define BUFFER_SIZE 25
int main(VOID)
{
HANDLE ReadHandle, WriteHandle;
STARTUPINFO si;
PROCESS_INFORMATION pi;
char message[BUFFER_SIZE] = "Greetings";
DWORD written;
/* 设置安全属性,使管道句柄是继承的。 */
SECURITY_ATTRIBUTES sa = {sizeof(SECURITY_ATTRIBUTES), NULL,TRUE};
/* allocate memory */
ZeroMemory(&pi, sizeof(pi));
/* 创建管道 */
if ( !CreatePipe(&ReadHandle, &WriteHandle, &sa, 0)) {
fprintf(stderr,"Create Pipe Failed\n");
return 1;
}
/* establish the START_INFO structure for the child process */
GetStartupInfo(&si);
si.hStdError = GetStdHandle(STD_ERROR_HANDLE);
si.hStdOutput = GetStdHandle(STD_OUTPUT_HANDLE);
/* 将标准输入重定向到管道的读端 */
si.hStdInput = ReadHandle;
si.dwFlags = STARTF_USESTDHANDLES;
/* 设置不让子进程成管道的写端 */
SetHandleInformation( WriteHandle, HANDLE_FLAG_INHERIT, 0);
/* 创建子进程 */
if (!CreateProcess(NULL,
".\\child.exe",
NULL,
NULL,
TRUE, /* inherit handles */
0,
NULL,
NULL,
&si,
&pi))
{
fprintf(stderr, "Process Creation Failed\n");
return -1;
}
/* 关闭管道未使用的一端 */
CloseHandle(ReadHandle);
/* 父进程向管道写数据 */
if (!WriteFile (WriteHandle, message, BUFFER_SIZE, &written, NULL))
fprintf(stderr, "Error writing to pipe\n");
/* 关闭管道的写端。 */
CloseHandle(WriteHandle);
/* 等待子进程退出 */
WaitForSingleObject(pi.hProcess, INFINITE);
/* 关闭所有的句柄 */
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
}
Windows子节点代码:
#include <stdio.h>
#include <windows.h>
#define BUFFER_SIZE 25
int main(VOID)
{
HANDLE ReadHandle, WriteHandle;
CHAR buffer[BUFFER_SIZE];
DWORD read;
ReadHandle = GetStdHandle(STD_INPUT_HANDLE);
WriteHandle= GetStdHandle(STD_OUTPUT_HANDLE);
/* have the child read from the pipe */
if (ReadFile(ReadHandle, buffer, BUFFER_SIZE, &read, NULL))
printf("child: >%s<",buffer);
else
fprintf(stderr, "Child: Error reading from pipe\n");
return 0;
}
服务器端代码展示了父进程创建一个匿名管道用于与其子通信。与UNIX系统不同,UNIX系统中,子进程自动继承其父创建的管道,Windows要求程序员指定子进程将继承的属性。首先初始化 SECURITY_ATTRIBUTES结构,设置安全属性,使管道句柄是可继承的,然后将标准输入或标准输出重定向到管道的读写端句柄。由于子进程将从管道中读取信息,父进程必须将子进程的标准输入重定向到管道的读端句柄。此外,由于管道是半双工的,因此有必要禁止子进程继承管道的写端。创建子进程的CreateProcess()的第五个参数设置为TRUE,这表明子进程将从其父进程继承指定的句柄。在写入管道之前,父节点首先关闭管道未使用的读端。
从管道读取信息的子进程代码见上面。在从管道读取之前,该程序通过调用GetStdHandle()获得对管道的读句柄。
注意,普通管道需要在UNIX和Windows系统上的通信进程之间建立父子关系。这意味着这些管道只能用于同一台机器上的进程之间的通信。