📄 14.htm
字号:
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=gb2312">
<meta name="GENERATOR" content="Microsoft FrontPage 3.0">
<title>C:\WINDOWS\Desktop\UnixProg\7.htm</title>
</head>
<body>
<font SIZE="2">
<h1 align="center">第十四章; 进程间通信 </h1>
<p>14.1 介绍 </p>
<p>在第八章中,我们说明了进程控制原语并且观察了如何调用多个进程。但是这些进
</p>
<p>程之间交换信息的唯一方法是经由fork或exec传送打开文件,或通过文件系统。现
</p>
<p>在,我们说明进程之间相互通信的其它技术-IPC(interprocess
communication) </p>
<p>。 </p>
<p>Unix IPC已经是而且继续是各种进程通信方式的统称,其中极少能在所有Unix的实
</p>
<p>现中进行移植。图14.1摘要列出了不同实现所支持的不同形式的IPC。
</p>
<p>图14.1 UNIX IPC 摘要 </p>
<p>正如上图所示,不管哪一种Unix实现,我们都可依靠的唯一一种IPC是半双工的管
</p>
<p>道(pipes)。图中前7种IPC通常限于同一台主机的各个进程间的IPC。最后二种;
</p>
<p>套接口和流,是支持不同主机上各个进程间IPC。(关于网络IPC的详细情况,请参
</p>
<p>见Stevens[1990]。)虽然中间三种形式的IPC(消息队列、信号量以及共享存储器
</p>
<p>)在图中说明为只受到系统V的支持,但是在大多数制造商所支持的,从贝克莱Un
</p>
<p>ix导出的Unix系统中(例如,SunOS以及Ultrix),已经添加了这三种形式的IPC。
</p>
<p>几个Posix小组正在对IPC进行工作,但是最后结果还不很清楚,在1994年
</p>
<p>甚至 </p>
<p>更迟一点与IPC有关的Posix可能还不会制定出来。 </p>
<p>我们已将与IPC有关的讨论分成两章。在本章中,我们讨论经典的IPC;管道、FIF
</p>
<p>O、消息队列、信号量以及共享存储器。在下一章中,我们将观察SVR4和4.3+BSD共
</p>
<p>同支持的IPC的某些高级特征,包括;流管道和命名的流管道,以及用这些更高级
</p>
<p>形式IPC,我们可以做的一些事情。 </p>
<p>14.2 管道 </p>
<p>管道是Unix IPC的最老形式,并且所有Unix系统都提供此种通信机制,管道有两种
</p>
<p>限制; </p>
<p>1. 它们是半双工的。数据只在一个方向流动。 </p>
<p>2.
它们只能在具有公共祖先的进程之间使用。通常,一个管道由一个进程创建,
</p>
<p>然后该进程调用fork,此后父、子进程之间就可应用该管道。 </p>
<p>我们将会看到流管道(15.2节)没有第一种限制,FIFO(14.5节)和命名流管道(
</p>
<p>15.5节)则没有第二种限制。尽管有这两种限制,半双工管道仍是最常用的IPC形
</p>
<p>式。 </p>
<p>管道是由调用pipe函数而创建的。 </p>
<p>#include <unistd.h> </p>
<p>int pipe(int filedes[2]) ; </p>
<p>返回:若成功为0,出错为-1 </p>
<p>经由参数filedes返回两个文件描述符;filedes[0]是为读而打开,filedes[1]是
</p>
<p>为写而打开的。filedes[1]的输出是filedes[0]的输入。 </p>
<p>有两种方法来描画一个管道,如图14.2中所示。左半图显示了管道的两端在一个进
</p>
<p>甚至 </p>
<p>更迟一点与IPC有关的Posix可能还不会制定出来。 </p>
<p>我们已将与IPC有关的讨论分成两章。在本章中,我们讨论经典的IPC;管道、FIF
</p>
<p>O、消息队列、信号量以及共享存储器。在下一章中,我们将观察SVR4和4.3+BSD共
</p>
<p>同支持的IPC的某些高级特征,包括;流管道和命名的流管道,以及用这些更高级
</p>
<p>形式IPC,我们可以做的一些事情。 </p>
<p>14.2 管道 </p>
<p>管道是Unix IPC的最老形式,并且所有Unix系统都提供此种通信机制,管道有两种
</p>
<p>限制; </p>
<p>1. 它们是半双工的。数据只在一个方向流动。 </p>
<p>2.
它们只能在具有公共祖先的进程之间使用。通常,一个管道由一个进程创建,
</p>
<p>然后该进程调用fork,此后父、子进程之间就可应用该管道。 </p>
<p>我们将会看到流管道(15.2节)没有第一种限制,FIFO(14.5节)和命名流管道(
</p>
<p>15.5节)则没有第二种限制。尽管有这两种限制,半双工管道仍是最常用的IPC形
</p>
<p>式。 </p>
<p>管道是由调用pipe函数而创建的。 </p>
<p>#include <unistd.h> </p>
<p>int pipe(int filedes[2]) ; </p>
<p>返回:若成功为0,出错为-1 </p>
<p>经由参数filedes返回两个文件描述符;filedes[0]是为读而打开,filedes[1]是
</p>
<p>为写而打开的。filedes[1]的输出是filedes[0]的输入。 </p>
<p>有两种方法来描画一个管道,如图14.2中所示。左半图显示了管道的两端在一个进
</p>
<p>图14.4 从父进程到子进程的管道 </p>
<p>对于从子进程到父进程的管道,父进程关闭 fd[1],子进程关闭fd[0]。
</p>
<p>当管道的一端被关闭后,下列规定起作用; </p>
<p>1. 当读一个写端已被关闭的管道时,在所有数据都被读取后,read返回0,以指示
</p>
<p>达到了文件结束处。(从技术方面考虑,在管道的写端还有进程时,就不会产生文
</p>
<p>件的结束。可以复制一个管道的描述符,使得有多个进程具有写打开文件描述符。
</p>
<p>但是,通常一个管道只有一个读进程,一个写进程。在下一节介绍FIFO时,我们会
</p>
<p>看到对于一个单一的FIFO常常有多个写进程)。 </p>
<p>2. 如果写一个读端已被关闭的管道,则产生信号SIGPIPE。如果忽略该信号或者捕
</p>
<p>捉该信号并从其处理程序返回,则Write出错返回,errno设置为EPIPE。
</p>
<p>在写管道时,常数PIPE_BUF规定了核中管道缓存器的大小。如果对管道进行write
</p>
<p>调用,而且要求写的字节数小于等于PIPE_BUF,则此操作不会与其它进程对同一管
</p>
<p>道(或FIFO)的write操作穿插进行。但是,若有多个进程同时写一个管道(或FI
</p>
<p>FO),而且某个或某些进程要求写的字节数超过PIPE_BUF字节数,则数据可能会与
</p>
<p>其它写操作的数据相穿插。 </p>
<p>实例 </p>
<p>程序14.1中创建了一个从父进程到子进程的管道,并且父进程经由该管道向子
</p>
<p>进程传送数据。 </p>
<p>#include "ourhdr.h" </p>
<p>int </p>
<p>main(void) </p>
<p>{ </p>
<p>int n, fd[2]; </p>
<p>pid_t pid; </p>
<p>char line[MAXLINE]; </p>
<p>if (pipe(fd) < 0) </p>
<p>err_sys("pipe error"); </p>
<p>if ( (pid = fork()) < 0) </p>
<p>err_sys("fork error"); </p>
<p>else if (pid > 0) { /* parent */ </p>
<p>close(fd[0]); </p>
<p>write(fd[1], "hello world\n", 12); </p>
<p>} else { /* child */ </p>
<p>close(fd[1]); </p>
<p>n = read(fd[0], line, MAXLINE); </p>
<p>write(STDOUT_FILENO, line, n); </p>
<p>} </p>
<p>exit(0); </p>
<p>} </p>
<p>程序14.1 经由管道父进程向子进程传送数据 </p>
<p>在上面的例子中,我们直接对管道描述符调用read和write。更为有益的是将管道
</p>
<p>描述符复制为标准输入和标准输出。在此之后通常子进程调用exec,执行另一道程
</p>
<p>序,开从标准输入(已创建的管道)或将数据写至其标准输出(管道)。
</p>
<p>实例 </p>
<p>让我们编写一道程序,其功能是每次一页显示已产生的输出。已经有很多Unix公用
</p>
<p>程序具有分页功能,因此我们无需再构造一个新的分页程序,而是调用用户最喜爱
</p>
<p>的分页程序。为了避免先将所有数据写到一个临时文件中,然后再调用系统中有关
</p>
<p>程序显示该文件,我们希望将输出通过管道直接送到分页程序。为此,先创建一个
</p>
<p>管道,一个子进程,使子进程的标准输入成为管道的读端,然后exec用户喜爱的分
</p>
<p>页程序。程序14.2显示了如何实现这些操作。(本例要求在命令行中有一个参
</p>
<p>数说明要显示的文件的名称。通常,这种类型的程序要求在终端上显示的数据已经
</p>
<p>在存储器中。) </p>
<p>#include <sys/wait.h> </p>
<p>#include "ourhdr.h" </p>
<p>#define DEF_PAGER "/usr/bin/more" /* default pager program */ </p>
<p>int </p>
<p>main(int argc, char *argv[]) </p>
<p>{ </p>
<p>int n, fd[2]; </p>
<p>pid_t pid; </p>
<p>char line[MAXLINE], *pager, *argv0; </p>
<p>FILE *fp; </p>
<p>if (argc != 2) </p>
<p>err_quit("usage: a.out <pathname>"); </p>
<p>if ( (fp = fopen(argv[1], "r")) == NULL) </p>
<p>err_sys("can't open %s", argv[1]); </p>
<p>if (pipe(fd) < 0) </p>
<p>err_sys("pipe error"); </p>
<p>if ( (pid = fork()) < 0) </p>
<p>err_sys("fork error"); </p>
<p>else if (pid > 0) { </p>
<p>/* parent */ </p>
<p>close(fd[0]); /* close read end */ </p>
<p>/* parent copies argv[1] to pipe */ </p>
<p>while (fgets(line, MAXLINE, fp) != NULL) { </p>
<p>n = strlen(line); </p>
<p>if (write(fd[1], line, n) != n) </p>
<p>err_sys("write error to pipe"); </p>
<p>} </p>
<p>if (ferror(fp)) </p>
<p>err_sys("fgets error"); </p>
<p>close(fd[1]); /* close write end of pipe for reader */ </p>
<p>if (waitpid(pid, NULL, 0) < 0) </p>
<p>err_sys("waitpid error"); </p>
<p>exit(0); </p>
<p>} else { </p>
<p>/* child */ </p>
<p>close(fd[1]); /* close write end */ </p>
<p>if (fd[0] != STDIN_FILENO) { </p>
<p>if (dup2(fd[0], STDIN_FILENO) != STDIN_FILENO) </p>
<p>err_sys("dup2 error to stdin"); </p>
<p>close(fd[0]); /* don't need this after dup2 */ </p>
<p>} </p>
<p>/* get arguments for execl() */ </p>
<p>if ( (pager = getenv("PAGER")) == NULL) </p>
<p>pager = DEF_PAGER; </p>
<p>if ( (argv0 = strrchr(pager, '/')) != NULL) </p>
<p>argv0++; /* step past rightmost slash */ </p>
<p>else </p>
<p>argv0 = pager; /* no slash in pager */ </p>
<p>if (execl(pager, argv0, (char *) 0) < 0) </p>
<p>err_sys("execl error for %s", pager); </p>
<p>} </p>
<p>} </p>
<p>程序14.2 将文件复制到分页程序 </p>
<p>在调用fork之前先创建一个pipe。fork之后父进程关闭其读端,子进程关闭其写端
</p>
<p>。子进程然后调用dup2,使其标准输入成为管道的读端。当执行分页程序时,其标
</p>
<p>准输入将是管道的读端。 </p>
<p>当我们将一个描述符复制到另一个时(在子进程中,fd[0]复制到标准输入),应
</p>
<p>当注意该描述符的值并不已经是所希望的值。如果该描述符已经具有所希望的值,
</p>
<p>并且我们先调用dup2,然后close则将关闭在此进程中只有该单个描述符所代表的
</p>
<p>打开文件。(请回忆3.12节中所述,当dup2中的两个参数值相等时的操作。)在本
</p>
<p>程序中,如果shell没有打开标准输入,那么程序开始处的fopen应已使用描述符0
</p>
<p>,也就是最小末使用的描述符,所以fd[0]决不会等于标准输入。尽管如此,只要
</p>
<p>先调用dup2,然后close以复制一个描述符到另一个,作为一种保护性的编程措施
</p>
<p>,我们总是先将两个描述符进行比较。 </p>
<p>请注意,我们是如何使用环境变量PAGER而获得用户分页程序名称的。如果这种操
</p>
<p>作没有成功,则使用系统默认值。这是环境变量的常见用法。 </p>
<p>实例 </p>
<p>请回忆8.8节中的五个函数TELL_WAIT、TELL_PARENT、TELL_CHILD、WAIT_PARENT以 </p>
<p>及WAIT_CHILD。在程序10.17中,我们提供了一个使用信号的实现。程序14.3则是
</p>
<p>一个使用管道的实现。 </p>
<p>如图14.5所示,我们在fork之前创建了两个管道。 </p>
<p>图14.5 用两个管道实现父-子进程的同步 </p>
<p>父进程在调用TELL_CHILD时经由上一个管道写一个字符"P",子进程在调用TELL_P
</p>
<p>ARENT时,经由下一个管道写一个字符"C"。相应的WAIT_XXX函数调用read读一个字
</p>
<p>符,没有读到字符时阻塞(睡眠等待)。 </p>
<p>请注意,每一个管道都有一个额外的读取进程,这没有关系。也就是说除了子进程
</p>
<p>从pfd1[0]读取,父进程也有上一个管道的读端。因为父进程并没有执行对该管道
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -