GNU/Linux应用程序编程:用管道进行编程

来源:互联网 发布:java字符串转base64 编辑:程序博客网 时间:2024/05/18 10:04

简介

本文中,我们将学习GNU/Linux管道。管道模型虽然很老但是就算是现在它仍然是一个十分有用的进程间通信机制。我们将会学习什么是半双向管道以及有名管道。它们都提供了一个FIFO(先进先出)排队模型来允许进程间通信。

管道模型

一个形象化管道的描述为——一个在两个实体之间的单向连接器。例如,让我们来看一看下面的这个GNU/Linux命令:

ls -1 | wc –l

这个命令创建了两个进程,一个和ls -l关联而另一个则和wc -l关联。接着它通过设置第二个进程的标准输入到第一个进程的标准输出连接了这两个进程(如图11.1)。产生的结果是——计算了当前子目录的文件数目。

我们的命令设置了一个在两个GNU/Linux命令之间的管道。命令ls被执行之后它产生的输出被用作第二个命令wc(wordcount)的输入。这是一个单向管道——通信发生在一个方向上。两个命令之间的连接由GNU/Linux来完成。我们也可以在应用程序当中做到这一点(等一下我们将会证明这一点)。

匿名管道和有名管道

一个匿名管道或者说单向管道,为一个进程提供了和它的一个子进程(匿名的种类)进行通信的方法。这是因为没有可以在操作系统当中找到一个匿名进程的方法。它的最通常的用法是在父进程建立一个匿名管道,然后将这个管道传递给它的子进程,然后它们就可以进行通信了。注意,如果需要双向通信的话,我们考虑使用的API就应该是套接字(sockets)API了。

管道的另一种类型是有名管道。一个有名管道其功能和匿名管道差不多,差别就在于它是可以在文件系统中存在的并且所有进程都可以找到它。这意味着没有血缘关系的进程之间可以使用它来进行通信。

在接下来的部分当中我们将会同时学习有名管道和匿名管道。我们将对管道进行一个快速的游览,然后我们就详细地学习管道API以及支持管道编程的GNU/Linux系统级的命令。

旋风式的游览

让我们以一个简单的管道编程的模型的例子来开始我们的旋风式游览。在这个简单的例子当中,我们在一个进程当中创建了一个管道,然后对它写入一个消息,接下来的就是从这个管道当中读取前面写入的消息,然后将它显示出来。

清单11.1: 一个简单的管道例子

1:#include 
2:#include
3:#include
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: /* Create the pipe */
16: ret = pipe( myPipe );
17:
18: if (ret == 0) {
19:
20: /* Write the message into the pipe */
21: write( myPipe[PIPE_STDOUT], string, strlen(string) );
22:
23: /* Read the message from the pipe */
24: ret = read( myPipe[PIPE_STDIN], buffer, MAX_LINE );
25:
26: /* Null terminate the string */
27: buffer[ ret ] = 0;
28:
29: printf("%s/n", buffer);
30:
31: }
32:
33: return 0;
34: }

在清单11.1当中,我们在16行当中使用了函数pipe创建了我们的管道。我们向函数pipe传入了一个拥有两个元素的代表我们的管道的整型数组。管道被定义为一对分开的文件描述符——一个输入和一个输出。我们可以从管道的一端写入然后从管道的另一端读取。如果管道成功建立的话,API函数pipe就会返回0。基于返回,myPipe将会包括两个新的代表管道的输入(myPipe[1])和对管道的输出(myPipe[0])的文件描述符。(PS:in文件描述符代表其所关联的文件或者设备相当于一个输入设备;out文件描述符则代表其所关联的文件或者设备相当于一个输出设备。)

在21行,我们使用函数write将我们的消息写入管道。我们指定了stdout(标准输出)描述符(这是从应用程序的角度来看的,而非管道)。管道现在包含了我们的消息,然后它能够在24行被函数read读取。在这里我们再次说明一下,从应用程序的角度来看,我们使用了stdin(标准输入)描述符来从管道进行读取。函数read将从管道当中读取到的消息保存到了变量buffer。为了让buffer真正地成为一个字符串,我们在其后添加了一个0 (NULL),然后我们就可以在29行使用函数printf显示它了。

虽然这个例子很有趣,但是我们自己之间的通信的执行可以使用任意数量的机制。接下来我们将要学习提供了进程(不论是有关系的还是无关系的)间通信的更为复杂的例子。

详细的回顾

虽然管道模型的绝大部分为函数pipe,但是还有两个应该对它们的基于管道编程的可用性进行讨论的其它函数。表格11.1列举了我们在本章当中要详细地讨论的函数:

API函数 用处

pipe 创建一个匿名管道

dup 创建一个文件描述符的拷贝

mkfifo 创建一个有名管道(先进先出)

同时我们还将会学习一些可用于管道通信的其它函数,特别是那些能够使用管道进行通信的函数。

注意:我们要记住一个管道不过是一对文件描述符而已,因此任何能够在文件描述符上进行操作的函数都可以使用管道。这就包括了select、read、write、fcntl以及freopen等等,但并不是仅仅限于这些函数。

pipe

API函数pipe创建了一个新的由一个包含两个文件描述符的数组所代表的管道。函数pipe的原型如下:

#include

int pipe( int fds[2] );

如果函数pipe的操作成功的话,它将会返回0;反之则会返回-1,同时适当地设置了errno。如若成功返回,数组fds(它是按引用传递值)将会拥有两个激活的文件描述符。数组的第一个元素是一个能够被应用程序读取的文件描述符;而第二个则是一个能够被应用程序写入的文件描述符。

现在让我们来看一下一个应用管道于多进程应用程序的稍微更为复杂一点的例子。在这个应用程序,当中,我们将在14行当中创建一个管道,然后在16行使用函数fork在程序的父进程当中创建一个新的进程(子进程)。在子进程当中,我们尝试从我们的管道的输入文件描述符当中进行读取(18行),但是如果没有什么可以读取的话,我们就将这个子进程挂起。当进行读取操作之后,我们就通过一个NULL将字符串终止(就是让一个数组字符成为一个字符串),然后打印我们所读取的字符串。父进程只是简单地通过使用输出(写)文件描述符(管道结构的数组的第二个元素)将一个测试字符串写入管道,然后使用函数wait等待子进程退出。

注意:除了我们的子进程继承了父进程使用函数pipe创建的的文件描述符然后使用它们来和另一个子进程或者和父进程来通信,关于这个程序不再没有什么可以引人注目的了。回想一下:一旦函数fork的操作完成,我们的进程是独立的(除了子进程继承了父进程的特征,比如管道描述符)。由于内存是分开的, pipe方法为我们提供了一个有趣的进程间通信的模型。

清单11.2: 举例说明两个进程间的管道模型

1:       #include 
2: #include
3: #include
4: #include
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。要对管道进行写入操作的应用程序通常都包含一个捕获这么一个状况的信号句柄。

dup和dup2

函数dup和dup2能够复制一个文件描述符的十分有用的函数。它们经常被用来重定向一个进程的stdin、stdout或者stderr。函数dup和dup2的原型如下:

#include

int dup( int oldfd );

int dup2( int oldfd, int targetfd );

函数dup允许我们复制一个描述符。我们向函数传递一个已经存在了的描述符,然后它返回了一个和前面那个一模一样的新的描述符。这意味着两个描述符共享着相同的结构。例如,我们用一个文件描述符执行了一个lseek(在文件当中查找),文件的当前位置(文件的内部指针)在第二个当中也是一样的。函数 dup的使用如下面的代码片段所示:

int fd1, fd2;

...

fd2 = dup( fd1 );

注意:在fork的调用之前创建一个描述符产生的效果和调用函数dup是一样的。子进程接收了一个复制的描述符,就好像它模仿了dup的调用一样。

函数dup2和dup类似,但是它允许调用者指定一个激活的描述符已经目标描述符的id。在函数dup2成功返回之后,新的目标描述符复制了第一个描述符(targetfd =oldfd)。让我们来看一下下面的举例说明dup2的用法的代码片段:

int oldfd;

oldfd = open("app_log", (O_RDWR | O_CREATE), 0644 );

dup2( oldfd, 1 );

close( oldfd );

在这个例子当中,我们打开了一个叫做“app_log”的新的文件并且接收到了一个叫做fd1的文件描述符。我们通过使用oldfd和1调用了dup2,就这样我们用oldfd(我们新打开的文件)替换了文件描述符1(stdout标准输出)。任何写入标准输出stdout的数据现在将会被写入一个叫做“app_log”的文件当中。注意我们在复制了oldfd之后就关闭了它。然而这并没有关闭了我们新打开的文件,这是因为文件描述符1现在就是它——我们可以这么认为。

现在让我们来学习一个更为复杂的例子。回想本章的前面部分我们研究的让ls –l的输出成为wc –l的输入。我们现在通过一个C应用程序来探讨允许这个例子(清单11.3)。

在清单11.3的开始部分,我们在9行创建了我们的管道然后在13-16行创建了程序的子进程接下来在20-23行创建了父进程。我们在13行关闭了 stdout(标准输出)描述符。子进程在这里提供了ls–l的功能并且不会写入stdout当中去相反它被写入了我们的管道(用了dup来作重定向)。在14行,我们使用了dup2来重定向stdout到我们的管道(pfds[1])当中去。一旦这样的一个操作完成之后,我们关闭了我们的管道的输入端(因为它永远不会被使用)。最后,我们使用了函数execlp来将我们的子进程的镜像替换为命令ls –l的镜像。一旦这个命令被执行了之后,任何产生的输出将会被传送到输入当中去。

现在让我们来看一看,管道的接收端。父进程充当了这么一个角色并遵循了一个相似的模式。我们首先在20行关闭了stdin(标准输入),这是因为我们不会从它那里接收任何数据。接下来,我们再次使用函数dup2(21行)来使得stdin成为管道的输出端。这是通过使文件描述符0(一般来说为stdin)在功能上变得和pfds[0]一样来完成的。我们关闭了管道的stdout标准输出端(pfds[1]),这是因为在这里我们不会使用到它(22行)。最后,我们使用函数execlp来执行将管道当中的内容当成它的输入的命令wc -1(23行)。

清单11.3: 流水线命令C程序

1:       #include 
2: #include
3: #include
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: }

在这个程序当中有一点十分重要并值得我们记下来——这就是我们的子进程把它自己的输出重定向到管道的输入上,而父进程则将它自己的输入重定向到管道的输出——这是一个值得我们去记的十分有用的技术。

mkfifo

函数mkfifo被用来创建一个在文件系统当中的提供FIFO(先进先出)的功能的文件(也可以称之为有名管道)。到目前为止我们讨论的都是匿名管道。它们是专用于一个父进程和它的子进程之间的通信。有名管道在文件系统当中是可见的,因此可以被任何进程使用。函数mkfifo原型如下:

#include

#include

int mkfifo( const char *pathname, mode_t mode );

命令mkfifo需要两个参数。第一个(pathname)是要被创建在文件系统当中的一个特殊文件。第二个(mode)指定了FIFO的读写权限。成功的话命令mkfifo将会返回0,失败的话将会返回-1(同时errno将会被适当地写入)。让我们来看一下一个使用函数mkfifo创建了一个 fifo的例子:

int ret;
...
ret = mkfifo( "/tmp/cmd_pipe", S_IFIFO | 0666 );
if (ret == 0) {
// Named pipe successfully created
} else {
// Failed to create named pipe
}

在这个例子当中,我们使用文件cmd_pipe在子目录/tmp当中创建了一个fifo(有名管道)。接下来我们就打开了这个文件来进行读写,通过它我们就可以进行通信了。一旦我们打开了一个有名管道,我们可以使用典型的I/O命令进行读写了。例如,下面是一个使用fgets从管道当中进行读取的代码段:

pfp = fopen( "/tmp/cmd_pipe", "r" );

...

ret = fgets( buffer, MAX_LINE, pfp );

我们可以使用下面的代码段来为上面的代码对管道进行写入操作:

pfp = fopen( "/tmp/cmd_pipe", "w+ );

...

ret = fprintf( pfp, "Here’s a test string!/n" );

关于有名管道的使人感兴趣的地方是它们在被看做是集合点的地方工作,我们将会在对系统命令mkfifo的讨论当中对这一点进行探讨。一个读取者是不能够打开一个有名管道的,除非一个写入者主动地打开管道的另一端。读取者将会在调用open函数时被阻塞直到一个写入者出现。尽管有这样的一个限制,有名管道仍然是进程间通信的一个有用的机制。

系统命令

让我们来看一下一个和用于IPC的管道模型相关的系统命令。命令mkfifo,就和API函数mkfifo一样,允许我们在命令行上创建一个有名管道。

mkfifo

命令mkfifo是在命令行上创建有名管道(fifo特殊文件)的两个方法当中的一个。命令mkfifo的一般用法如下:

mkfifo [options] name

在这里,选项(options)-m代表mode(权限)而name则是要创建的有名管道的名字(如果需要的话还要包含路径)。如果没有指定权限,默认的是0644。下面是一个例子,它在目录/tmp当中创建了一个叫做cmd_pipe的有名管道:

$ mkfifo /tmp/cmd_pipe

我们可以简单地通过使用选项-m调整选项。下面是一个将权限设置为0644的例子(但是我们必须首先删除掉原先的那一个):

$ rm cmd_pipe

$ mkfifo -m 0644 /tmp/cmd_pipe

一旦权限被创建完毕,我们可以通过这个管道来在命令行上进行通信。在另一个终端上,我们使用命令echo向有名管道cmd_pipe写入:

$ echo Hi > cmd_pipe

当这个命令结束的时候,我们的读取者将会被唤醒并结束(下面清楚地完成了读取者的命令序列):

$ cat cmd_pipe

Hi

$

以上举例说明了有名管道不仅可以用于C程序当中还可以用在脚本当中。

有名管道的创建还可以使用命令mknod(紧随其后的特殊文件可以是很多其它类型的文件)。下面我们使用命令mknod创建了一个有名管道;

$ mknod cmd_pipe p

在这里,有名管道cmd_pipe被创建在当前子目录当中(p是有名管道的类型)。

总结

在这一章当中,我们对有名管道和匿名管道作了一次旋风式的游览。我们回顾了创建管道的程序方法和命令行方法,同时还回顾了使用它们来进行通信的典型的I/O机制。我们还回顾了如何使用函数dup和dup2来进行I/O的重定向。虽然管道十分有用,在其它某一特定情节当中这些命令或者函数依然十分地有用(无论在上面地方一个文件描述符被使用,比如一个套接字或者文件)。

管道编程API

#include

int pipe( int filedes[2] );

int dup( int oldfd );

int dup2( int oldfd, int targetfd );

int mkfifo( const char *pathname, mode_t mode );