*
管道技术是Linux的一种基本的进程间通信技术。在本文中,我们将为读者介绍管道技术的模型,匿名管道和命名管道技术的定义和区别,以及这两种管道的创建方法。同时,阐述如何在应用程序和命令行中通过管道进行通信的详细方法。
一、管道技术模型
管道技术是Linux操作系统中历来已久的一种进程间通信机制。所有的管道技术,无论是半双工的匿名管道,还是命名管道,它们都是利用FIFO排队模型来指挥进程间的通信。对于管道,我们可以形象地把它们当作是连接两个实体的一个单向连接器。例如,请看下面的命令:
该命令首先创建两个进程,一个对应于ls –1,另一个对应于wc –l。然后,把第一个进程的标准输出设为第二个进程的标准输入(如图1所示)。它的作用是计算当前目录下的文件数量。
点击查看大图
图1:管道示意图
如上图所示,前面的例子实际上就是在两个命令之间建立了一根管道(有时我们也将之称为命令的流水线操作)。第一个命令ls执行后产生的输出作为了第二个命 令wc的输入。这是一个半双工通信,因为通信是单向的。两个命令之间的连接的具体工作,是由内核来完成的。下面我们将会看到,除了命令之外,应用程序也可 以使用管道进行连接。
二、信号和消息的区别
我们知道,进程间的信号通信机制在传递信息时是以信号为载体的,但管道通信机制的信息载体是消息。那么信号和消息之间的区别在哪里呢?
首先,在数据内容方面,信号只是一些预定义的代码,用于表示系统发生的某一状况;消息则为一组连续语句或符号,不过量也不会太大。在作用方面,信号担任进程间少量信息的传送,一般为内核程序用来通知用户进程一些异常情况的发生;消息则用于进程间交换彼此的数据。
在 发送时机方面,信号可以在任何时候发送;信息则不可以在任何时刻发送。在发送者方面,信号不能确定发送者是谁;信息则知道发送者是谁。在发送对象方面,信 号是发给某个进程;消息则是发给消息队列。在处理方式上,信号可以不予理会;消息则是必须处理的。在数据传输效率方面,信号不适合进大量的信息传输,因为 它的效率不高;消息虽然不适合大量的数据传送,但它的效率比信号强,因此适于中等数量的数据传送。
三、管道和命名管道的区别
我们知道,命名管道和管道都可以在进程间传送消息,但它们也是有区别的。
管道技术只能用于连接具有共同祖先的进程,例如父子进程间的通信,它无法实现不同用户的进程间的信息共享。再者,管道不能常设,当访问管道的进程终止时,管道也就撤销。这些限制给它的使用带来不少限制,但是命名管道却克服了这些限制。
命名管道也称为FIFO,是一种永久性的机构。FIFO文件也具有文件名、文件长度、访问许可权等属性,它也能像其它Linux文件那样被打开、关闭和删除,所以任何进程都能找到它。换句话说,即使是不同祖先的进程,也可以利用命名管道进行通信。
如果想要全双工通信,那最好使用Sockets API。下面我们分别介绍这两种管道,然后详细说明用来进行管道编程的编程接口和系统级命令。
四、管道编程技术
在程序中利用管道进行通信时,根据通信主体大体可以分为两种情况:一种是具有共同祖先的进程间的通信,比较简单;另一种是任意进程间通信,相对较为复杂。下面我们先从较为简单的进程内通信开始介绍。
1. 具有共同祖先的进程间通信管道编程
为了了解管道编程技术,我们先举一个例子。在这个例中,我们将在进程中新建一个管道,然后向它写入一个消息,管道读取消息后将其发出。代码如下所示:
示例代码1:管道程序示例
1: #include <unistd.h>
2: #include <stdio.h>
3: #include <string.h>
4:
5: #define MAX_LINE 80
6: #define PIPE_STDIN 0
7: #define PIPE_STDOUT 1
8:
9: int main()
10: ...{
11: const char *string=...{"A sample message."};
12: int ret, myPipe[2];
13: char buffer[MAX_LINE+1];
14:
15: /**//* 建立管道 */
16: ret = pipe( myPipe );
17:
18: if (ret == 0) ...{
19:
20: /**//* 将消息写入管道 */
21: write( myPipe[PIPE_STDOUT], string, strlen(string) );
22:
23: /**//* 从管道读取消息 */
24: ret = read( myPipe[PIPE_STDIN], buffer, MAX_LINE );
25:
26: /**//* 利用Null结束字符串 */
27: buffer[ ret ] = 0;
28:
29: printf("%s/n", buffer);
30:
31: }
32:
33: return 0;
34: }
上面的示例代码中,我们利用pipe调用新建了一个管道,参见第16行代码。 我们还建立了一个由两个元素组成的数组,用来描述我们的管道。我们的管道被定义为两个单独的文件描述符,一个用来输入,一个用来输出。我们能从管道的一端 输入,然后从另一端读出。如果调用成功,pipe函数返回值为0。返回后,数组myPipe中存放的是两个新的文件描述符,其中元素myPipe[1]包 含的文件描述符用于管道的输入,元素myPipe[0] 包含的文件描述符用于管道的输出。
在第21行代码,我们利用write函数把消息写入管道。站在应用程序的角度,它是在向stdout输出。现在,该管道存有我们的消息,我们可以利用第 24行的read函数来读它。对于应用程序来说,我们是利用stdin描述符从管道读取消息的。read函数把从管道读取的数据存放到buffer变量 中。然后在buffer变量的末尾添加一个NULL,这样就能利用printf函数正确的输出它了。在本例中的管道可以利用下图解释:
点击查看大图
图2:示例代码1中半双工管道的示意图
这个例子中,通信是在具有共同祖先的进程间发生的,即父进程和子进程通信。这样做局限性太大,但我们只是用它来给读者一个感性的认识。接下来,我们将介绍更为高级的进程间的管道通信。
2.进程间通信管道编程
在利用管道技术进行编程时,处理要用到上面介绍的pipe函数外,还用到另外三个函数,如下所示。
pipe函数:该函数用于创建一个新的匿名管道。
dup函数:该函数用于拷贝文件描述符。
mkfifo函数:该函数用于创建一个命名管道(fifo)。
当然,在管道通信过程中还用到其它函数,到时我们会加以介绍。需要注意的是,说到底,管道无非就是一对文件描述符,因此任何能够操作文件操作符的函数都可以使用管道。这包括但不限于这些函数:select、read、write、 fcntl、freopen,等等。
2.1函数pipe
函数pipe用来建立一个新的管道,该管道用两个文件描述符进行描述。函数pipe的原型如下所示:
#include <unistd.h>
int pipe( int fds[2] );
当调用成功时,函数pipe返回值为0,否则返回值为-1。成功返回时,数组fds被填入两个有效的文件描述符。数组的第一个元素中的文件描述符供应用程序读取之用,数组的第二个元素中的文件描述符可以用来供应用程序写入。
下 面我们考察在一个包含多个进程的应用程序中的管道示例。在该程序中(见示例代码2),第14行用于创建一个管道,然后进程在第16行分叉,变成一个父进程 和一个子进程。在子进程中,我们尝试从(在第18行建立的)管道的输入描述符读取,这时该进程将被挂起,直到管道中有可以读取的内容为止。
读完后,我们用NULL作为读取的内容的结束符,这样的话,读的这些内容就能使用printf函数正确打印输出了。父进程先是利用存放在thePipe[1]中的“写文件标识符”向管道写入测试字符串,然后就使用wait函数来等待子进程退出。
在 我们的这个程序中需要加以注意的是,我们的子进程是如何继承父进程利用pipe函数建立的文件描述符的,以及如何利用该文件描述符进行通信的。函数 fork一旦执行,子进程会继承父进程的功能和管道的文件描述符,但对于内核来说,父进程和子进程是平等的,它们是独立运行的。也就是说,两个进程分别具 有单独的内存空间,它们正是通过pipe函数来互通有无的。
示例代码2:演示两个进程间的管道模型的代码
1: #include <stdio.h>
2: #include <unistd.h>
3: #include <string.h>
4: #include <wait.h>
5:
6: #define MAX_LINE 80
7:
8: int main()
9: ...{
10: int thePipe[2], ret;
11: char buf[MAX_LINE+1];
12: const char *testbuf=...{"a test string."};
13:
14: if ( pipe( thePipe ) == 0 ) ...{
15:
16: if (fork() == 0) ...{
17:
18: ret = read( thePipe[0], buf, MAX_LINE );
19: buf[ret] = 0;
20: printf( "Child read %s/n", buf );
21:
22: } else ...{
23:
24: ret = write( thePipe[1], testbuf, strlen(testbuf) );
25: ret = wait( NULL );
26:
27: }
28:
29: }
30:
31: return 0;
32: }
需要注意的是,在这个示例程序中我们没有说明如何关闭管道,因为一旦进程结束,与管道有关的资源将被自动释放。尽管如此,为了养成一种良好的编程习惯,最好利用close调用来关闭管道的描述符,如下所示:
ret = pipe( myPipe );
...
close( myPipe[0] );
close( myPipe[1] );
如果管道的写入端关闭,但是还有进程尝试从管道读取的话,将被返回0,用来指出管道已不可用,并且应当关闭它。如果管道的读出端关闭,但是还有进程尝试向管道写入的话,试图写入的进程将收到一个SIGPIPE信号,至于信号的具体处理则要视其信号处理程序而定了。
2.2 dup函数和dup2函数
dup和dup2也是两个非常有用的调用,它们的作用都是用来复制一个文件的描述符。它们经常用来重定向进程的stdin、stdout和stderr。这两个函数的原型如下所示:
#include <unistd.h>
int dup( int oldfd );
int dup2( int oldfd, int targetfd )
利用函数dup,我们可以复制一个描述符。传给该函数一个既有的描述符,它就会返回一个新的描述符,这个新的描述符是传给它的描述符的拷贝。这意味着,这两个描述符共享同一个数据结构。例如,如果我们对一个文件描述符执行lseek操作,得到的第一个文件的位置和第二个是一样的。下面是用来说明dup函数使用方法的代码片段:
int fd1, fd2;
...
fd2 = dup( fd1 );
需要注意的是,我们可以在调用fork之前建立一个描述符,这与调用dup建立描述符的效果是一样的,子进程也同样会收到一个复制出来的描述符。
dup2函数跟dup函数相似,但dup2函数允许调用者规定一个有效描述符和目标描述符的id。dup2函数成功返回时,目标描述符(dup2函数的第 二个参数)将变成源描述符(dup2函数的第一个参数)的复制品,换句话说,两个文件描述符现在都指向同一个文件,并且是函数第一个参数指向的文件。下面 我们用一段代码加以说明:
int oldfd;
oldfd = open("app_log", (O_RDWR | O_CREATE), 0644 );
dup2( oldfd, 1 );
close( oldfd );
本例中,我们打开了一个新文件,称为“app_log”,并收到一个文件描述符,该描述符叫做fd1。我们调用dup2函数,参数为oldfd和1,这会 导致用我们新打开的文件描述符替换掉由1代表的文件描述符(即stdout,因为标准输出文件的id为1)。任何写到stdout的东西,现在都将改为写 入名为“app_log”的文件中。需要注意的是,dup2函数在复制了oldfd之后,会立即将其关闭,但不会关掉新近打开的文件描述符,因为文件描述 符1现在也指向它。
下面我们介绍一个更加深入的示例代码。回忆一下本文前面讲的命令行管道,在那里,我们将ls –1命令的标准输出作为标准输入连接到wc –l命令。接下来,我们就用一个C程序来加以说明这个过程的实现。代码如下面的示例代码3所示。
在示例代码3中,首先在第9行代码中建立一个管道,然后将应用程序分成两个进程:一个子进程(第13–16行)和一个父进程(第20–23行)。接下来, 在子进程中首先关闭stdout描述符(第13行),然后提供了ls –1命令功能,不过它不是写到stdout(第13行),而是写到我们建立的管道的输入端,这是通过dup函数来完成重定向的。在第14行,使用dup2 函数把stdout重定向到管道(pfds[1])。之后,马上关掉管道的输入端。然后,使用execlp函数把子进程的映像替换为命令ls –1的进程映像,一旦该命令执行,它的任何输出都将发给管道的输入端。
现在来研究一下管道的接收端。从代码中可以看出,管道的接收端是由父进程来担当的。首先关闭stdin描述符(第20行),因为我们不会从机器的键盘等标 准设备文件来接收数据的输入,而是从其它程序的输出中接收数据。然后,再一次用到dup2函数(第21行),让stdin变成管道的输出端,这是通过让文 件描述符0(即常规的stdin)等于pfds[0]来实现的。关闭管道的stdout端(pfds[1]),因为在这里用不到它。最后,使用 execlp函数把父进程的映像替换为命令wc -1的进程映像,命令wc -1把管道的内容作为它的输入(第23行)。
示例代码3:利用C实现命令的流水线操作的代码
1: #include <stdio.h>
2: #include <stdlib.h>
3: #include <unistd.h>
4:
5: int main()
6: ...{
7: int pfds[2];
8:
9: if ( pipe(pfds) == 0 ) ...{
10:
11: if ( fork() == 0 ) ...{
12:
13: close(1);
14: dup2( pfds[1], 1 );
15: close( pfds[0] );
16: execlp( "ls", "ls", "-1", NULL );
17:
18: } else ...{
19:
20: close(0);
21: dup2( pfds[0], 0 );
22: close( pfds[1] );
23: execlp( "wc", "wc", "-l", NULL );
24:
25: }
26:
27: }
28:
29: return 0;
30: }
在该程序中,需要格外关注的是,我们的子进程把它的输出重定向的管道的输入,然后,父进程将它的输入重定向到管道的输出。这在实际的应用程序开发中是非常有用的一种技术。
2.3 mkfifo函数
mkfifo函数的作用是在文件系统中创建一个文件,该文件用于提供FIFO功能,即命名管道。前边讲的那些管道都没有名字,因此它们被称为匿名管道,或 简称管道。对文件系统来说,匿名管道是不可见的,它的作用仅限于在父进程和子进程两个进程间进行通信。而命名管道是一个可见的文件,因此,它可以用于任何 两个进程之间的通信,不管这两个进程是不是父子进程,也不管这两个进程之间有没有关系。Mkfifo函数的原型如下所示:
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo( const char *pathname, mode_t mode );
mkfifo函数需要两个参数,第一个参数(pathname)是将要在文件系统中创建的一个专用文件。第二个参数(mode)用来规定FIFO的读写 权限。Mkfifo函数如果调用成功的话,返回值为0;如果调用失败返回值为-1。下面我们以一个实例来说明如何使用mkfifo函数建一个fifo,具 体代码如下所示:
int ret;
...
ret = mkfifo( "/tmp/cmd_pipe", S_IFIFO | 0666 );
if (ret == 0) ...{
// 成功建立命名管道
} else ...{
// 创建命名管道失败
}
在这个例子中,利用/tmp目录中的cmd_pipe文件建立了一个命名管道(即fifo)。之后,就可以打开这个文件进行读写操作,并以此进行通信了。 命名管道一旦打开,就可以利用典型的输入输出函数从中读取内容。举例来说,下面的代码段向我们展示了如何通过fgets函数来从管道中读取内容:
pfp = fopen( "/tmp/cmd_pipe", "r" );
...
ret = fgets( buffer, MAX_LINE, pfp );
我们还能向管道中写入内容,下面的代码段向我们展示了利用fprintf函数向管道写入的具体方法:
pfp = fopen( "/tmp/cmd_pipe", "w+ );
...
ret = fprintf( pfp, "Here’s a test string!/n" );
对命名管道来说,除非写入方主动打开管道的读取端,否则读取方是无法打开命名管道的。Open调用执行后,读取方将被锁住,直到写入方出现为止。尽管命名管道有这样的局限性,但它仍不失为一种有效的进程间通信工具。
分享到:
相关推荐
整理的Linux下的管道编程技术文档。包含管道技术模型,信号和消息的区别 ,管道和命名管道的区别 ,管道编程技术,进程间通信管道编程,与管道相关的系统命令等内容;
用Linux语言编程,关于管道进出的,有详细的注释
《linux/unix系统编程手册(上、下册)》总共分为64章,主要讲解了高效读写文件,对信号、时钟和定时器的运用,创建进程、执行程序,编写安全的应用程序,运用posix线程技术编写多线程程序,创建和使用共享库,运用...
linux管道专题编程笔记
《Linux/UNIX系统编程手册(上、下册)》总共分为64章,主要讲解了高效读写文件,对信号、时钟和定时器的运用,创建进程、执行程序,编写安全的应用程序,运用POSIX线程技术编写多线程程序,创建和使用共享库,运用...
《linux/unix系统编程手册(上、下册)》总共分为64章,主要讲解了高效读写文件,对信号、时钟和定时器的运用,创建进程、执行程序,编写安全的应用程序,运用posix线程技术编写多线程程序,创建和使用共享库,运用...
基于Linux管道技术的编程方法研究.pdf
《linux/unix系统编程手册(上、下册)》总共分为64章,主要讲解了高效读写文件,对信号、时钟和定时器的运用,创建进程、执行程序,编写安全的应用程序,运用posix线程技术编写多线程程序,创建和使用共享库,运用...
第2章 Linux下的C语言编程环境 2.1 Linux编程简介 2.2 Linux下的C语言开发环境 2.3 编辑器的使用 2.4 编译器gcc的使用 2.5 LinuxC程序的开发过程 2.6 make工具及其使用 2.7 使用autoconf 2.8 使用automake ...
第18章 管道相关函数 第19章 socket相关函数 第20章 进程通信(IPC)函数 第21章 记录函数 第22章 环境变量函数 第23章 正则表达式 第24章 动态函数 第25章 其它函数 附录:编译程序,宏,不定参数,linux信号列表,常见...
第2章 Linux下的C语言编程环境 2.1 Linux编程简介 2.2 Linux下的C语言开发环境 2.3 编辑器的使用 2.4 编译器gcc的使用 2.5 LinuxC程序的开发过程 2.6 make工具及其使用 2.7 使用autoconf 2.8 使用automake ...
本书全面介绍了Linux编程相关的知识,内容涵盖Linux基本知识、如何建立Linux开发环境、Linux开发工具、Linux文件系统、文件I/O操作、设备文件、进程与进程环境、守护进程、基本进程间通信方法、管道与命名管道等。...
这是一个演示linux下线程之间如何进行通讯的例子,使用了管道技术。主要使用了popen函数
Linux网络编程(总共41集) 讲解Linux网络编程知识,分以下四个篇章。 Linux网络编程之TCP/IP基础篇 Linux网络编程之socket编程篇 Linux网络编程之进程间通信篇 Linux网络编程之线程篇 Linux网络编程之TCP/IP...
第2章 Linux下的C语言编程环境 2.1 Linux编程简介 2.2 Linux下的C语言开发环境 2.3 编辑器的使用 2.4 编译器gcc的使用 2.5 LinuxC程序的开发过程 2.6 make工具及其使用 2.7 使用autoconf 2.8 使用automake ...
linux 进程和线程编程 pipe --原始管道 命名管道 消息队列 信号量 内存共享 线程编程
Linux实验报告(详细)
I/O操作、设备文件、进程与进程环境、守护进程、基本进程间通信方法、管道与命名管道、POSIX IPC、Linux下的多线程、Linux网络编程、网络嗅探器、Linux图形界面开发基础、GTK+图形界面编程、界面布局与按钮构件、GTK...
Linux系统进程通信中信号概念及信号处理,进程间的管道通信编程,进程间的内存共享编程