管道(无名管道)通信机制原理和实现详解
管道(pipe)允许两个进程进行通信,是早期 UNIX 系统最早使用的一种 IPC 机制。管道为进程之间的相互通信提供了一种较为简单的方法,尽管也有一定的局限性。
在实现管道时,应该考虑以下四个问题:
有两种常见类型的用于 UNIX 和 Windows 系统的管道:无名管道(普通管道)和有名管道,本节先讲解无名管道。
因此,普通管道是单向的,只允许单向通信。如果需要双向通信,那么就要采用两个管道,而每个管道向不同方向发送数据。
下面我们讨论在 UNIX 和 Windows 系统上创建普通管道。在这两个程序实例中,一个进程向管道中写入消息 Greetings,而另一个进程从管道中读取此消息。
在 UNIX 系统上,普通管道的创建采用函数
普通管道只能由创建进程所访问。通常情况下,父进程创建一个管道,并使用它来与其子进程进行通信(该子进程由 fork() 来创建)。正如《进程的创建》一节所讲的那样,子进程继承了父进程的打开文件。由于管道是一种特殊类型的文件,因此子进程也继承了父进程的管道。
图 1 普通管道的文件描述符
图 1 说明了文件描述符 fd 与父子进程之间的关系。
对于这个实例,父进程向管道写,而子进程从管道读。重要的是要注意,父进程和子进程开始就关闭了管道的未使用端。有一个重要的步骤是确保当管道的写入者关闭了管道写入端时,从管道读取的进程能检测到 end-of-file(调用 read() 返回 0),不过上边所示的程序中没有这个操作。
对于 Windows 系统,普通管道被称为匿名管道(anonymous pipe),它们的行为类似于 UNIX 的管道:它们是单向的,通信进程之间具有父子关系。
另外,读取和写入管道可以采用普通函数 ReadFile() 和 WriteFile()。用于创建管道的 Windows API 是 CreatePipe() 函数,它有四个参数,包括:
下面的代码说明了一个父进程创建一个匿名管道,以便与子进程通信:
创建子进程的程序中,第五个参数设置为 TRUE,表示子进程会从父进程那里继承指定的句柄。父进程向管道写入时,应先关闭未使用的管道读出端。从管道读的子进程如下所示:
请注意,对于 UNIX 和 Windows 系统,采用普通管道的进程通信需要有父子关系。这 意味着,这些管道只可用于同一机器的进程间通信。
在实现管道时,应该考虑以下四个问题:
- 管道允许单向通信还是双向通信?
- 如果允许双向通信,它是半双工的(数据在同一时间内只能按一个方向传输)还是全双工的(数据在同一时间内可在两个方向上传输)?
- 通信进程之间是否应有一定的关系(如父子关系)?
- 管道通信能否通过网络,还是只能在同一台机器上进行?
有两种常见类型的用于 UNIX 和 Windows 系统的管道:无名管道(普通管道)和有名管道,本节先讲解无名管道。
无名管道(普通管道)
普通管道允许两个进程按标准的生产者-消费者方式进行通信:生产者向管道的一端(写入端)写,消费者从管道的另一端(读出端)读。因此,普通管道是单向的,只允许单向通信。如果需要双向通信,那么就要采用两个管道,而每个管道向不同方向发送数据。
下面我们讨论在 UNIX 和 Windows 系统上创建普通管道。在这两个程序实例中,一个进程向管道中写入消息 Greetings,而另一个进程从管道中读取此消息。
在 UNIX 系统上,普通管道的创建采用函数
pipe (int fd[])
这个函数创建一个管道,以便通过文件描述符 int fd[] 来访问:fd[0] 为管道的读出端,而 fd[1] 为管道的写入端。UNIX 将管道作为一种特殊类型的文件。因此,访问管道可以采用普通的系统调用 read() 和 write()。普通管道只能由创建进程所访问。通常情况下,父进程创建一个管道,并使用它来与其子进程进行通信(该子进程由 fork() 来创建)。正如《进程的创建》一节所讲的那样,子进程继承了父进程的打开文件。由于管道是一种特殊类型的文件,因此子进程也继承了父进程的管道。
图 1 普通管道的文件描述符
图 1 说明了文件描述符 fd 与父子进程之间的关系。
#include <sys/types.h> #include <stdio.h> #include <string.h> #include <unistd.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]; int fd[2]; pid_t pid; /* create the pipe */ if (pipe(fd) == -1) { fprintf(stderr,"Pipe failed"); return 1; } /* fork a child process */ pid = fork(); if (pid < 0) { /* error occurred */ fprintf(stderr, "Fork Failed"); return 1; } if (pid > 0) { /* parent process */ /* close tlie unused end of the pipe */ close (fd [READJEND]); /* write to the pipe */ write (fd [WRITE_END],write_msg, strlen (write_msg)+1); /* close the read end of the pipe */ close (fd[WRITE_END]); } else { /* child process */ /* close the unused end of the pipe */ close(fd[WRITEJEND]); /* read from the pipe */ read(fd[READ_END] , read_msg, BUFFER_SIZE); printf ("read %s",read_msg); /* close the write end of the pipe */ close (fd[READ_END]); } return 0; }上边的 UNIX 程序中,父进程创建了一个管道,然后调用 fork() 来创建子进程。调用 fork() 之后的行为取决于数据流如何流过管道。
对于这个实例,父进程向管道写,而子进程从管道读。重要的是要注意,父进程和子进程开始就关闭了管道的未使用端。有一个重要的步骤是确保当管道的写入者关闭了管道写入端时,从管道读取的进程能检测到 end-of-file(调用 read() 返回 0),不过上边所示的程序中没有这个操作。
对于 Windows 系统,普通管道被称为匿名管道(anonymous pipe),它们的行为类似于 UNIX 的管道:它们是单向的,通信进程之间具有父子关系。
另外,读取和写入管道可以采用普通函数 ReadFile() 和 WriteFile()。用于创建管道的 Windows API 是 CreatePipe() 函数,它有四个参数,包括:
- 读取管道的句柄;
- 写入管道的句柄;
- STARTUPINFO结构的一个实例,用于指定子进程继承管道的句柄;
- 可以指定管道的大小(以字节为单位);
下面的代码说明了一个父进程创建一个匿名管道,以便与子进程通信:
#include <stdio.h> #include <stdlib.h> #include <windows.h> #define BUFFER_SIZE 25 int main(VOID) { HANDLE ReadHandle, WriteHandle; STARTUPINFO si; PR0CESS_INF0RMATI0N pi; char message [BUFFER_SIZE] = "Greetings"; DWORD written; /* set up security attributes allowing pipes to be inherited */ SECURITY_ATTRIBUTES sa = {sizeof (SECURITY_ATTRIBUTES) ,NULL,TRUE}; /* allocate memory */ ZeroMemory(&pi,sizeof(pi)); /* create the pipe */ if (!CreatePipe(&ReadHandle, &WriteHandle, &sa, 0)) { fprintf(stderr, "Create Pipe Failed"); return 1; } /* establish the STARTJNFO structure for the child process */ GetStartupInfo(&si); si.hStdOutput = GetStdHandle(STD_OUTPUT_HANDLE); /* redirect standard input to the read end of the pipe */ si.hStdlnput = ReadHandle;\ si.dwFlags = STARTF.USESTDHANDLES; /* don5t allow the child to inherit the write end of pipe */ SetHandlelnformat ion (WriteHandle, HANDLEJFLAGJNHERIT, 0); /* create the child process */ CreateProcess (NULL, "child.exe" , NULL, NULL,TRUE, /* inherit handles */ 0,NULL, NULL, &si, &pi); /* close the unused end of the pipe */ CloseHandle(ReadHandle); /* the parent writes to the pipe */ if (! WriteFile (WriteHandle, message,BUFFER.SIZE,&written,NULL)) fprintf(stderr, "Error writing to pipe."); /* close the write end of the pipe */ CloseHandle(WriteHandle); /* wait for the child to exit */ WaitForSingleObject(pi.hProcess, INFINITE); CloseHandle(pi.hProcess); CloseHandle(pi.hThread); return 0; }对于 UNIX 系统,子进程自动继承由父进程创建的管道;对于 Windows 系统,程序员需要指定子进程继承的属性。首先,初始化结构 SECURITY_ATTRIBUTES,以便允许句柄继承;然后,重定向子进程的句柄,以便标准输入或输出为管道的读出或写入。由于子进程从管道上读,父进程应将子进程的标准输入重定向为管道的读出句柄。另外,由于管道为半双工,需要禁止子进程继承管道的写入端。
创建子进程的程序中,第五个参数设置为 TRUE,表示子进程会从父进程那里继承指定的句柄。父进程向管道写入时,应先关闭未使用的管道读出端。从管道读的子进程如下所示:
#include <stdio.h> #include <windows.h> #define BUFFER_SIZE 25 int main (VOID) { HANDLE Readhandle; CHAR buffer [BUFFER_SIZE]; DWORD read; /* get the read handle of the pipe */ ReadHandle = GetStdHandle (STDJNPUTJHANDLE); /* the child reads from the pipe */ if (ReadFile(ReadHandle, buffer, BUFFER_SIZE, &read, NULL)) printf ("child read %s",buffer); else fprintf(stderr, "Error reading from pipe"); return 0; }从管道读之前,这个程序应通过调用 GetStdHandle(),以得到管道的读句柄。
请注意,对于 UNIX 和 Windows 系统,采用普通管道的进程通信需要有父子关系。这 意味着,这些管道只可用于同一机器的进程间通信。
所有教程
- C语言入门
- C语言编译器
- C语言项目案例
- 数据结构
- C++
- STL
- C++11
- socket
- GCC
- GDB
- Makefile
- OpenCV
- Qt教程
- Unity 3D
- UE4
- 游戏引擎
- Python
- Python并发编程
- TensorFlow
- Django
- NumPy
- Linux
- Shell
- Java教程
- 设计模式
- Java Swing
- Servlet
- JSP教程
- Struts2
- Maven
- Spring
- Spring MVC
- Spring Boot
- Spring Cloud
- Hibernate
- Mybatis
- MySQL教程
- MySQL函数
- NoSQL
- Redis
- MongoDB
- HBase
- Go语言
- C#
- MATLAB
- JavaScript
- Bootstrap
- HTML
- CSS教程
- PHP
- 汇编语言
- TCP/IP
- vi命令
- Android教程
- 区块链
- Docker
- 大数据
- 云计算