BBS水木清华站∶精华区
发信人: forest (轻寒小楼~~小吉篇), 信区: Unix
标 题: [转载]unix环境高级编程-14 (转载)
发信站: BBS 水木清华站 (Fri Mar 17 17:52:18 2000)
【 以下文字转载自 Linux 讨论区 】
【 原文由 SuperSB 所发表 】
发信人: taosm (128+64-->cool), 信区: unix
标 题: unix环境高级编程--第14章 进程间通信
发信站: 西十八BBS (Sat Mar 11 13:57:20 2000), 转信
第十四章; 进程间通信
14.1 介绍
在第八章中,我们说明了进程控制原语并且观察了如何调用多个进程。但是这些进
程之间交换信息的唯一方法是经由fork或exec传送打开文件,或通过文件系统。现
在,我们说明进程之间相互通信的其它技术-IPC(interprocess communication)
。
Unix IPC已经是而且继续是各种进程通信方式的统称,其中极少能在所有Unix的实
现中进行移植。图14.1摘要列出了不同实现所支持的不同形式的IPC。
图14.1 UNIX IPC 摘要
正如上图所示,不管哪一种Unix实现,我们都可依靠的唯一一种IPC是半双工的管
道(pipes)。图中前7种IPC通常限于同一台主机的各个进程间的IPC。最后二种;
套接口和流,是支持不同主机上各个进程间IPC。(关于网络IPC的详细情况,请参
见Stevens[1990]。)虽然中间三种形式的IPC(消息队列、信号量以及共享存储器
)在图中说明为只受到系统V的支持,但是在大多数制造商所支持的,从贝克莱Un
ix导出的Unix系统中(例如,SunOS以及Ultrix),已经添加了这三种形式的IPC。
几个Posix小组正在对IPC进行工作,但是最后结果还不很清楚,在1994年
甚至
更迟一点与IPC有关的Posix可能还不会制定出来。
我们已将与IPC有关的讨论分成两章。在本章中,我们讨论经典的IPC;管道、FIF
O、消息队列、信号量以及共享存储器。在下一章中,我们将观察SVR4和4.3+BSD共
同支持的IPC的某些高级特征,包括;流管道和命名的流管道,以及用这些更高级
形式IPC,我们可以做的一些事情。
14.2 管道
管道是Unix IPC的最老形式,并且所有Unix系统都提供此种通信机制,管道有两种
限制;
1. 它们是半双工的。数据只在一个方向流动。
2. 它们只能在具有公共祖先的进程之间使用。通常,一个管道由一个进程创建,
然后该进程调用fork,此后父、子进程之间就可应用该管道。
我们将会看到流管道(15.2节)没有第一种限制,FIFO(14.5节)和命名流管道(
15.5节)则没有第二种限制。尽管有这两种限制,半双工管道仍是最常用的IPC形
式。
管道是由调用pipe函数而创建的。
#include <unistd.h>
int pipe(int filedes[2]) ;
返回:若成功为0,出错为-1
经由参数filedes返回两个文件描述符;filedes[0]是为读而打开,filedes[1]是
为写而打开的。filedes[1]的输出是filedes[0]的输入。
有两种方法来描画一个管道,如图14.2中所示。左半图显示了管道的两端在一个进
甚至
更迟一点与IPC有关的Posix可能还不会制定出来。
我们已将与IPC有关的讨论分成两章。在本章中,我们讨论经典的IPC;管道、FIF
O、消息队列、信号量以及共享存储器。在下一章中,我们将观察SVR4和4.3+BSD共
同支持的IPC的某些高级特征,包括;流管道和命名的流管道,以及用这些更高级
形式IPC,我们可以做的一些事情。
14.2 管道
管道是Unix IPC的最老形式,并且所有Unix系统都提供此种通信机制,管道有两种
限制;
1. 它们是半双工的。数据只在一个方向流动。
2. 它们只能在具有公共祖先的进程之间使用。通常,一个管道由一个进程创建,
然后该进程调用fork,此后父、子进程之间就可应用该管道。
我们将会看到流管道(15.2节)没有第一种限制,FIFO(14.5节)和命名流管道(
15.5节)则没有第二种限制。尽管有这两种限制,半双工管道仍是最常用的IPC形
式。
管道是由调用pipe函数而创建的。
#include <unistd.h>
int pipe(int filedes[2]) ;
返回:若成功为0,出错为-1
经由参数filedes返回两个文件描述符;filedes[0]是为读而打开,filedes[1]是
为写而打开的。filedes[1]的输出是filedes[0]的输入。
有两种方法来描画一个管道,如图14.2中所示。左半图显示了管道的两端在一个进
图14.4 从父进程到子进程的管道
对于从子进程到父进程的管道,父进程关闭 fd[1],子进程关闭fd[0]。
当管道的一端被关闭后,下列规定起作用;
1. 当读一个写端已被关闭的管道时,在所有数据都被读取后,read返回0,以指示
达到了文件结束处。(从技术方面考虑,在管道的写端还有进程时,就不会产生文
件的结束。可以复制一个管道的描述符,使得有多个进程具有写打开文件描述符。
但是,通常一个管道只有一个读进程,一个写进程。在下一节介绍FIFO时,我们会
看到对于一个单一的FIFO常常有多个写进程)。
2. 如果写一个读端已被关闭的管道,则产生信号SIGPIPE。如果忽略该信号或者捕
捉该信号并从其处理程序返回,则Write出错返回,errno设置为EPIPE。
在写管道时,常数PIPE_BUF规定了核中管道缓存器的大小。如果对管道进行write
调用,而且要求写的字节数小于等于PIPE_BUF,则此操作不会与其它进程对同一管
道(或FIFO)的write操作穿插进行。但是,若有多个进程同时写一个管道(或FI
FO),而且某个或某些进程要求写的字节数超过PIPE_BUF字节数,则数据可能会与
其它写操作的数据相穿插。
实例
程序14.1中创建了一个从父进程到子进程的管道,并且父进程经由该管道向子
进程传送数据。
#include "ourhdr.h"
int
main(void)
{
int n, fd[2];
pid_t pid;
char line[MAXLINE];
if (pipe(fd) < 0)
err_sys("pipe error");
if ( (pid = fork()) < 0)
err_sys("fork error");
else if (pid > 0) { /* parent */
close(fd[0]);
write(fd[1], "hello world\n", 12);
} else { /* child */
close(fd[1]);
n = read(fd[0], line, MAXLINE);
write(STDOUT_FILENO, line, n);
}
exit(0);
}
程序14.1 经由管道父进程向子进程传送数据
在上面的例子中,我们直接对管道描述符调用read和write。更为有益的是将管道
描述符复制为标准输入和标准输出。在此之后通常子进程调用exec,执行另一道程
序,开从标准输入(已创建的管道)或将数据写至其标准输出(管道)。
实例
让我们编写一道程序,其功能是每次一页显示已产生的输出。已经有很多Unix公用
程序具有分页功能,因此我们无需再构造一个新的分页程序,而是调用用户最喜爱
的分页程序。为了避免先将所有数据写到一个临时文件中,然后再调用系统中有关
程序显示该文件,我们希望将输出通过管道直接送到分页程序。为此,先创建一个
管道,一个子进程,使子进程的标准输入成为管道的读端,然后exec用户喜爱的分
页程序。程序14.2显示了如何实现这些操作。(本例要求在命令行中有一个参
数说明要显示的文件的名称。通常,这种类型的程序要求在终端上显示的数据已经
在存储器中。)
#include <sys/wait.h>
#include "ourhdr.h"
#define DEF_PAGER "/usr/bin/more" /* default pager program */
int
main(int argc, char *argv[])
{
int n, fd[2];
pid_t pid;
char line[MAXLINE], *pager, *argv0;
FILE *fp;
if (argc != 2)
err_quit("usage: a.out <pathname>");
if ( (fp = fopen(argv[1], "r")) == NULL)
err_sys("can't open %s", argv[1]);
if (pipe(fd) < 0)
err_sys("pipe error");
if ( (pid = fork()) < 0)
err_sys("fork error");
else if (pid > 0) {
/* parent */
close(fd[0]); /* close read end */
/* parent copies argv[1] to pipe */
while (fgets(line, MAXLINE, fp) != NULL) {
n = strlen(line);
if (write(fd[1], line, n) != n)
err_sys("write error to pipe");
}
if (ferror(fp))
err_sys("fgets error");
close(fd[1]); /* close write end of pipe for reader */
if (waitpid(pid, NULL, 0) < 0)
err_sys("waitpid error");
exit(0);
} else {
/* child */
close(fd[1]); /* close write end */
if (fd[0] != STDIN_FILENO) {
if (dup2(fd[0], STDIN_FILENO) != STDIN_FILENO)
err_sys("dup2 error to stdin");
close(fd[0]); /* don't need this after dup2 */
}
/* get arguments for execl() */
if ( (pager = getenv("PAGER")) == NULL)
pager = DEF_PAGER;
if ( (argv0 = strrchr(pager, '/')) != NULL)
argv0++; /* step past rightmost slash */
else
argv0 = pager; /* no slash in pager */
if (execl(pager, argv0, (char *) 0) < 0)
err_sys("execl error for %s", pager);
}
}
程序14.2 将文件复制到分页程序
在调用fork之前先创建一个pipe。fork之后父进程关闭其读端,子进程关闭其写端
。子进程然后调用dup2,使其标准输入成为管道的读端。当执行分页程序时,其标
准输入将是管道的读端。
当我们将一个描述符复制到另一个时(在子进程中,fd[0]复制到标准输入),应
当注意该描述符的值并不已经是所希望的值。如果该描述符已经具有所希望的值,
并且我们先调用dup2,然后close则将关闭在此进程中只有该单个描述符所代表的
打开文件。(请回忆3.12节中所述,当dup2中的两个参数值相等时的操作。)在本
程序中,如果shell没有打开标准输入,那么程序开始处的fopen应已使用描述符0
,也就是最小末使用的描述符,所以fd[0]决不会等于标准输入。尽管如此,只要
先调用dup2,然后close以复制一个描述符到另一个,作为一种保护性的编程措施
,我们总是先将两个描述符进行比较。
请注意,我们是如何使用环境变量PAGER而获得用户分页程序名称的。如果这种操
作没有成功,则使用系统默认值。这是环境变量的常见用法。
实例
请回忆8.8节中的五个函数TELL_WAIT、TELL_PARENT、TELL_CHILD、WAIT_PARENT以
及WAIT_CHILD。在程序10.17中,我们提供了一个使用信号的实现。程序14.3则是
一个使用管道的实现。
如图14.5所示,我们在fork之前创建了两个管道。
图14.5 用两个管道实现父-子进程的同步
父进程在调用TELL_CHILD时经由上一个管道写一个字符"P",子进程在调用TELL_P
ARENT时,经由下一个管道写一个字符"C"。相应的WAIT_XXX函数调用read读一个字
符,没有读到字符时阻塞(睡眠等待)。
请注意,每一个管道都有一个额外的读取进程,这没有关系。也就是说除了子进程
从pfd1[0]读取,父进程也有上一个管道的读端。因为父进程并没有执行对该管道
的读操作,所以这不会产生任何影响。
#include "ourhdr.h"
static int pfd1[2], pfd2[2];
void
TELL_WAIT()
{
if (pipe(pfd1) < 0 || pipe(pfd2) < 0)
err_sys("pipe error");
}
void
TELL_PARENT(pid_t pid)
{
if (write(pfd2[1], "c", 1) != 1)
err_sys("write error");
}
void
WAIT_PARENT(void)
{
char c;
if (read(pfd1[0], &c, 1) != 1)
err_sys("read error");
if (c != 'p')
err_quit("WAIT_PARENT: incorrect data");
}
void
TELL_CHILD(pid_t pid)
{
if (write(pfd1[1], "p", 1) != 1)
err_sys("write error");
}
void
WAIT_CHILD(void)
{
char c;
if (read(pfd2[0], &c, 1) != 1)
err_sys("read error");
if (c != 'c')
err_quit("WAIT_CHILD: incorrect data");
}
程序14.3 使父、子进程同步的例程
14.3 popen和pclose函数
因为常见的操作是创建一个连到另一个进程的管道,然后读其输出或向其发送输入
,所以标准I/O库为实现这些操作提供了两个函数popen和pclose。这两个函数实现
的操作是;创建一个管道,fork一个子进程,关闭管道的不使用端,exec一个she
ll以执行命令,等待命令终止。
#include <stdio.h>
FILE *popen(const char *cmdstring, const char *type);
返回:若成功为文件指针,出错为NULL
int pclose(FILE *fp);
返回:cmdstring�
终止状态,出错为-1
函数popen 先做fork,然后exec以执行cmdstring,并且返回一个标准I/O文件指针
。如果type是"r",则文件指针连到cmdstring的标准输出(图14.6)
图14.6 fp=popen(command , "r")的结果
如果type 是 "w",则文件指针连接到cmdstring 的标准输入(图14.7)。
图14.7 fp=popen(command,"w")的结果
有一种方法可以帮助我们记住popen最后一个参数及其作用,这种方法就是与fope
n进行类比。如果type是"r",则返回的文件指针是可读的,如果type是"w",则是可
写的。
pclose函数关闭标准I/O流,等待命令执行结束,然后返回shell的终止状态。(我
们曾在8.6节对终止状态进行过说明,system函数(8.12节)也返回终止状态。)
如果shell不能被执行,则pclose返回的终止状态与shell执行exit(127)一样。
cmdstring 由Bourne shell以下列方式执行;
sh -c cmdstring
这表示shell将扩展cmdstring中的任何特殊字符。例如,我们可以使用;
fp=popen("ls *.c" , "r");
或者
fp=popen("cmd 2>&1" , "r");
返回:若成功为文件指针,出错为NULL
int pclose(FILE *fp);
返回:cmdstring�
终止状态,出错为-1
函数popen 先做fork,然后exec以执行cmdstring,并且返回一个标准I/O文件指针
。如果type是"r",则文件指针连到cmdstring的标准输出(图14.6)
图14.6 fp=popen(command , "r")的结果
如果type 是 "w",则文件指针连接到cmdstring 的标准输入(图14.7)。
图14.7 fp=popen(command,"w")的结果
有一种方法可以帮助我们记住popen最后一个参数及其作用,这种方法就是与fope
n进行类比。如果type是"r",则返回的文件指针是可读的,如果type是"w",则是可
写的。
pclose函数关闭标准I/O流,等待命令执行结束,然后返回shell的终止状态。(我
们曾在8.6节对终止状态进行过说明,system函数(8.12节)也返回终止状态。)
如果shell不能被执行,则pclose返回的终止状态与shell执行exit(127)一样。
cmdstring 由Bourne shell以下列方式执行;
sh -c cmdstring
这表示shell将扩展cmdstring中的任何特殊字符。例如,我们可以使用;
fp=popen("ls *.c" , "r");
或者
fp=popen("cmd 2>&1" , "r");
POSIX.1没有说明popen、pclose,因为它们与shell有交互作用,而shell 是由POS
IX.2说明的。我们对这两个函数的说明与POSIX.2的 Draft11.2(11.2草案)相一致
。该POSIX.2草案对这两个函数的说明与以前的实现有些区别。
实例#include <sys/wait.h>
#include "ourhdr.h"
#define PAGER "${PAGER:-more}" /* environment variable, or default */
int
main(int argc, char *argv[])
{
char line[MAXLINE];
FILE *fpin, *fpout;
if (argc != 2)
err_quit("usage: a.out <pathname>");
if ( (fpin = fopen(argv[1], "r")) == NULL)
err_sys("can't open %s", argv[1]);
if ( (fpout = popen(PAGER, "w")) == NULL)
err_sys("popen error");
/* copy argv[1] to pager */
while (fgets(line, MAXLINE, fpin) != NULL) {
if (fputs(line, fpout) == EOF)
err_sys("fputs error to pipe");
}
if (ferror(fpin))
err_sys("fgets error");
if (pclose(fpout) == -1)
err_sys("pclose error");
exit(0);
}
程序14.4 用popen向分页程序传送文件
让我们用popen重写程序14.2,其结果是程序14.4。使用popen减少了需要编写的代
码量。
shell命令${PAGER:-more}的意思是;如果shell变量PAGER已经定义,且其值非空
(nonnull),则使用其值,否则使用字符串more。
实例-popen函数
程序14.5是我们编写的popen和pclose版本。虽然popen的核心部分与本章中以前用
过的代码类似,但是增加了很多需要考虑的细节。首先每次调用popen时,应当记
住所创建的子进程的进程ID,以及其文件描述符或FILE指针。我们选择在数组chi
ldpid中保存子进程ID,并用文件描述符作为其下标。于是,当以FILE指针作为参
数调用pclose时,我们调用标准I/O函数fileno以得到文件描述符,然后取得子进
程ID,并用于调用waitpid。因为一个进程可能调用popen多次,所以我们在动态分
配childpid数组时(第一次调用popen时),其长度可以容纳与文件描述符数相同
的进程数。
调用pipe、fork以及为每个进程复制相应的文件描述符,这些操作与本章前面所述
的类似。
POSIX.2要求子进程关闭在以前调用popen时形成,当前仍旧打开的所有I/O流。为
此,我们在子进程中从头逐个检查childpid数组的各元素,关闭仍旧打开的任一描
述符。
若pclose的调用者已经为信号SIGCHLD设置了一个信号处理程序,则waitpid将返回
一个出错号EINTR。因为允许调用者捕捉此信号(或者任何其它可能中断waitpid调
用的信号),所以当waitpid被一个捕捉到的信号中断时,我们只是再次调用wait
pid。
如果一个信号中断了wait, pclose的早期版本返回EINTR出错号。
pclose的早期版本在wait期间,阻塞或忽略信号SIGINT、SIGQUIT以及SI
GHUP。
POSIX.2则不允许这一点。
#include <sys/wait.h>
#include <errno.h>
#include <fcntl.h>
#include "ourhdr.h"
static pid_t *childpid = NULL;
/* ptr to array allocated at run
time */
static int maxfd; /* from our open_max(), {Prog openmax} */
#define SHELL "/bin/sh"
FILE *
popen(const char *cmdstring, const char *type)
{
int i, pfd[2];
pid_t pid;
FILE *fp;
/* only allow "r" or "w" */
if ((type[0] != 'r' && type[0] != 'w') || type[1] != 0) {
errno = EINVAL; /* required by POSIX.2 */
return(NULL);
}
if (childpid == NULL) { /* first time through */
/* allocate zeroed out array for child pids */
maxfd = open_max();
if ( (childpid = calloc(maxfd, sizeof(pid_t))) == NULL)
return(NULL);
}
if (pipe(pfd) < 0)
return(NULL); /* errno set by pipe() */
if ( (pid = fork()) < 0)
return(NULL); /* errno set by fork() */
else if (pid == 0) {
* child */
if (*type == 'r') {
if ( (fp = fdopen(pfd[0], type)) == NULL)
return(NULL);
} else {
close(pfd[0]);
if ( (fp = fdopen(pfd[1], type)) == NULL)
return(NULL);
}
childpid[fileno(fp)] = pid; /* remember child pid for this fd */
return(fp);
}
int
pclose(FILE *fp)
{
int fd, stat;
pid_t pid;
if (childpid == NULL)
return(-1); /* popen() has never been called */
fd = fileno(fp);
if ( (pid = childpid[fd]) == 0)
return(-1); /* fp wasn't opened by popen() */
childpid[fd] = 0;
if (fclose(fp) == EOF)
return(-1);
while (waitpid(pid, &stat, 0) < 0)
if (errno != EINTR)
return(-1); /* error other than EINTR from waitpid()
*/
return(stat); /* return child's termination status */
}
程序14.5 popen和pclose函数
实例
考虑一个应用程序,它向标准输出写一个提示,然后从标准输入读1行。使用pope
n,我们可以在应用程序和输入之间插入一道程序以对输入进行变换处理。图14.8显
示了进程的安排。
图14.8 用popen变换输入
#include <ctype.h>
#include "ourhdr.h"
int
main(void)
{
int c;
while ( (c = getchar()) != EOF) {
if (isupper(c))
c = tolower(c);
if (putchar(c) == EOF)
err_sys("output error");
if (c == '\n')
fflush(stdout);
}
exit(0);
}
程序14.6 过滤程序,将大写字符变换成小写字符
#include <sys/wait.h>
#include "ourhdr.h"
int
main(void)
{
char line[MAXLINE];
FILE *fpin;
if ( (fpin = popen("myuclc", "r")) == NULL)
err_sys("popen error");
for ( ; ; ) {
fputs("prompt> ", stdout);
fflush(stdout);
if (fgets(line, MAXLINE, fpin) == NULL) /* read from pipe */
break;
if (fputs(line, stdout) == EOF)
err_sys("fputs error to pipe");
}
if (pclose(fpin) == -1)
err_sys("pclose error");
putchar('\n');
exit(0);
}
程序14.7 调用大写/小写过滤程序以读取命令
对输入进行的变换可能是路径名的扩充,或者是提供一种历史机制(记住以前输入
的命令)。
程序14.6是一个简单的过滤程序,它只是将输入复制到输出,在复制时将任一大
写字符变换为小写字符。在写了一行之后,我们对标准输出进行了刷清(用fflus
h),其理由将在下一节介绍协同进程时讨论。
我们对该过滤程序进行编译,其可执行目标代码存放在文件myuclc中,然后在程序
14.7中用popen调用它们。
因为标准输出通常是按行进行缓冲的,而提示并不包含新行符,所以在写了提示之
后,需要调用fflush。
14.4 协同进程
Unix过滤程序从标准输入读取数据,对其进行适当处理后写到标准输出。几个过滤
进程通常在shell管道命令中线性地连接。当同一道程序产生某个过滤程序的输入
,同时又读取该过滤程序的输出时,则该过滤程序就成为协同进程。
KornShell提供了协同进程。Bourne shell和C shell并没有提供将进程连接起来按
协同进程方式工作的方法。协同进程通常在shell的后台运行,其标准输入和标准
输出通过管道连接到另一道程序。虽然要求初始化一个协同进程,并将其输入和输
出连接到另一个进程的shell语法是十分奇特的(详细情况见Bolsky和Korn[1989]
中的pp.66-66),但是协同进程的工作方式在C程序中也是非常有用的。
popen提供连到另一个进程的标准输入或标准输出的一个单行管道,而对于协同进
程,则它有连到另一个进程的两个单行管道--一个接到其标准输入,另一个则来从
标准输出开始。我们先要将数据写到其标准输入,经其处理后,再从其标准输出读
取数据。
实例
让我们通过一个实例来观察协同进程。进程先创建两个管道:一个是协同进程的标
准输入,另一个是协同进程的标准输出。图14.9显示了这种安排。
图14.9 驱动一个协同进程--写其标准输入,读其标准输出
程序14.8是一个简单的协同进程,它从其标准输入读两个数,计算它们的和,然后
将结果写至标准输出。
#include "ourhdr.h"
int
main(void)
{
int n, int1, int2;
char line[MAXLINE];
while ( (n = read(STDIN_FILENO, line, MAXLINE)) > 0) {
line[n] = 0; /* null terminate */
if (sscanf(line, "%d%d", &int1, &int2) == 2) {
sprintf(line, "%d\n", int1 + int2);
n = strlen(line);
if (write(STDOUT_FILENO, line, n) != n)
err_sys("write error");
} else {
if (write(STDOUT_FILENO, "invalid args\n", 13) != 13)
err_sys("write error");
}
}
exit(0);
}
程序14.8 加两个数的简单滤波器
对此程序进行编译,将其可执行目标代码存入名为add2的文件。
程序14.9在从其标准输入读入两个数之后调用add2协同进程。从协同进程送来的值
则写到其标准输出。
#include <signal.h>
#include "ourhdr.h"
static void sig_pipe(int); /* our signal handler */
int
main(void)
{
int n, fd1[2], fd2[2];
pid_t pid;
char line[MAXLINE];
if (signal(SIGPIPE, sig_pipe) == SIG_ERR)
err_sys("signal error");
if (pipe(fd1) < 0 || pipe(fd2) < 0)
err_sys("pipe error");
if ( (pid = fork()) < 0)
err_sys("fork error");
else if (pid > 0) { /* parent */
close(fd1[0]);
close(fd2[1]);
while (fgets(line, MAXLINE, stdin) != NULL) {
n = strlen(line);
if (write(fd1[1], line, n) != n)
err_sys("write error to pipe");
if ( (n = read(fd2[0], line, MAXLINE)) < 0)
err_sys("read error from pipe");
if (n == 0) {
err_msg("child closed pipe");
break;
}
line[n] = 0; /* null terminate */
if (fputs(line, stdout) == EOF)
err_sys("fputs error");
}
if (ferror(stdin))
err_sys("fgets error on stdin");
exit(0);
} else { /* child */
close(fd1[1]);
close(fd2[0]);
if (fd1[0] != STDIN_FILENO) {
if (dup2(fd1[0], STDIN_FILENO) != STDIN_FILENO)
err_sys("dup2 error to stdin");
close(fd1[0]);
}
if (fd2[1] != STDOUT_FILENO) {
if (dup2(fd2[1], STDOUT_FILENO) != STDOUT_FILENO)
err_sys("dup2 error to stdout");
close(fd2[1]);
}
if (execl("./add2", "add2", (char *) 0) < 0)
err_sys("execl error");
}
}
static void
sig_pipe(int signo)
{
printf("SIGPIPE caught\n");
exit(1);
}
程序14.9 驱动add2过滤程序的程序
在程序中创建了两个管道,父、子进程各自关闭它们不需使用的端口。创建两个管
道的理由是;一个用作为协同进程的标准输入,另一个则用作为它的标准输出。然
后在调用execl之前,子进程调用dup2使管道描述符移至其标准输入和输出。
若编译和运行程序14.9,它如所希望的那样进行工作。进一步考虑,在程序14.9正
等待我们的输入时,若kill add2协同进程;然后输入两个数;当程序14.9对管道
进行写操作时,由于该管道无读进程,于是调用信号处理程序(见练习14.4)。
在程序15.1中,我们将提供这一实例的另一个版本,它使用一个全双工管道而不是
两个半双工管道。
实例
在协同进程add2(程序14.8)中,我们使用了Unix I/O:read和write。如果我们使
用标准I/O改写该协同进程,其后果是什么呢?程序14.10就是这个新版本。
#include "ourhdr.h"
int
main(void)
{
int int1, int2;
char line[MAXLINE];
while (fgets(line, MAXLINE, stdin) != NULL) {
if (sscanf(line, "%d%d", &int1, &int2) == 2) {
if (printf("%d\n", int1 + int2) == EOF)
err_sys("printf error");
} else {
if (printf("invalid args\n") == EOF)
err_sys("printf error");
}
}
exit(0);
}
程序14.10 加两个数的滤波器,使用标准I/O
若程序14.9调用此新的协同进程,则它就不再工作。问题出在系统默认的标准I/O
缓存机制上。当程序14.10被调用时,对标准输入的第一个fgets引起标准I/O库分
配一个缓存,并选择缓存的类型。因为标准输入是个管道,所以isatty为假,于是
标准I/O库由系统默认是全缓冲的。对标准输出也有同样的处理。当add2从其标准
输入读取而发生堵塞时,程序14.9在从管道读时也发生堵塞,于是产生了死锁。
我们对将要执行(exec)的这样一个协同进程可以加以控制。在程序14.10中的wh
ile循环之前加上下面4行。
if ( setvbuf(stdin ,NULL, _IOLBF, O) !=0 )
err_sys("setvbuf error");
if ( setvbuf(stdout,NULL,_IOLBF,O)!=0 )
err_sys("setvbuf error");
这使得当有一行可用时,fgets即返回,使得当输出一新行符时,printf即执行ff
lush操作。对setvbuf进行了这些显式调用,使得程序14.10能正常工作。
如果我们不能修改这样的协同进程,则需使用其它技术。例如,如果在我们的程序
中使用awk(1)代替add2作为协同进程,则下列命令行不能工作;
#!/bin/awk/ -f
{print $1+$2}
不能工作的原因还是标准I/O的缓冲机制问题。但是,在这种情况下我们不能改变
awk的工作方式(除非我们有awk的源代码)。
对这种问题的一般解决方法是使被调用(在本例中是awk)的协同进程认为它的标
准输入和输出是连到一个终端。这使得在协同进程中的标准I/O例程对这两个I/O流
进行行缓存,这类似于我们在前面所做的显式setvbuf调用。在第十九章中,我们
将用伪终端实现这一点。
14.5 FIFO
FIFO有时被称为命名管道。管道只能由相关进程使用,它们共同的祖先进程创建了
管道。但是,通过FIFO,不相关的进程也能交换数据。
在十四章中,已经提及FIFO是一种文件类型。Stat结构(4.2节)成员st_mode的编
码指明文件是否是FIFO类型。我们可以用S_ISFIFO宏对此进行测试。
创建一个FIFO类似于创建一个文件。确实,一个FIFO的路径名存在于文件系统中。
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
返回:若成功为0,出错为-1
mkfifo函数中mode参数的规格说明与open函数中的mode相同(见3.3节)。新FIFO
的用户和组的所有权与我们在4.6节所述的相同。
一旦已经用mkfifo创建了一个FIFO,我们就可用open打开它。 确实,一般的文件
I/O函数(close,read,write,unlink等)都可用于FIFO。
mkfifo是POSIX.1首先提出的。SVR3用mknod(2)系统调用创建FIFO。而在
SVR4中,mkfifo调用mknod创建FIFO。
POSIX.2已经建议了一个mkfifo(1)命令。SVR4和4.3+BSD现在支持此命令
。于
是,用一条shell命令就可以创建一个FIFO,然后用一般的shell I/O重新
定向对
其进行存取。
当打开一个FIFO时,非阻塞标志(O_NONBLOCK)产生下列影响:
1. 在一般情况中(没有说明O_NONBLOCK),只读打开要阻塞到某个其它进程为写
打开此FIFO。类似,为写而打开一个FIFO要阻塞到某个其它进程为读而打开它。
2. 如果指定了O_NONBLOCK,则只读打开立即返回。但是,如果没有进程已经为读
而打开一个FIFO,那么只写打开将出错返回,其errno是ENXIO。
类似于管道,若写一个尚无进程为读而打开的FIFO,则产生信号SIGPIPE。若某个
FIFO的最后一个写进程关闭了该FIFO,则将为该FIFO的读进程产生一个文件结束标
志。
一个给定的FIFO有多个写进程是常见的。这就意味这如果我们不希望多个进程所写
的数据互相穿插,则 需考虑原子写操作。正如对于管道一样,常数PIPE_BUF说明
了可被原子写到FIFO的最大数据量。
FIFO有两种用途。
1. FIFO由shell命令使用以便将数据从一条管道线传送到另一条,为此无需创建中
间临时文件。
2. FIFO用于客户-服务者应用程序中,以在客户和服务者之间传递数据。
我们各用一个例子来说明这两种用途。
实例-用FIFO复制输出流
FIFO可被用于复制串行管道命令之间的输出流,于是也就不需要写数据到中间磁盘
文件中(类似于使用管道以避免中间磁盘文件)。管道只能用于进程间的线性连接
,然而,因为FIFO具有名字,所以它可用于非线性连接。
考虑这样一个操作过程,它需要处理一个经过过滤的输入流两次。图14.10表示了
这种安排。
使用FIFO以及Unix程序tee(1),我们就可以实现这样的过程而无需使用临时文件。
(tee程序将其标准输入同时复制到其标准输出以及其命令行中包含的命名文件中
。)
mkfifo fifo1
prog3<fifo1&
prog1<infile 1 tee fifo1 | prog2
图14.10 处理一个经过过滤的输入流两次
我们创建FIFO,然后在后台启动prog3,它从FIFO读数据。然后,我们启动progl,用
tee将其输出发送到FIFO和prog2。图14.11 显示了有关安排。
图14.11 使用FIFO和tee将一个流发送到两个不同的进程
实例-客户-服务者使用FIFO进行通信
FIFO的另一个应用是在客户和服务者之间传送数据。如果有一个服务者,它与很多
客户有关,每个客户都可将其请求写到一个该服务者创建的众所周知的FIFO中。(
"众所周知"的意思是;所有需与服务者联系的客户都知道该FIFO的路径名。)图1
4.12显示了这种安排。因为对于该FIFO有多个写进程,客户发送给服务者的请求其
长度要小于PIPE_BUF字节。这样就能避免客户各次写之间的穿插。
图14.12 客户用-FIFO向服务者发送请求
在这种类型的客户-服务者通信中使用FIFO的问题是:服务者如何将回答送回各个
客户。不能使用单个FIFO的问题是:服务者如何将回答送回各个客户。因为服务者
会发出对各个客户请求的响应,而请求者却不可能知道什么时候去读才能恰恰得到
对它的响应。一种解决方法是每个客户都在其请求中发送其进程ID。然后服务者为
每个客户创建一个FIFO,所使用的路径名是以客户的进程ID为基础的。例如,服务
者可以用名字/tmp/serv1.xxxxx创建FIFO,其中xxxxx被代换成客户的进程ID。图
14.13显示了这种安排。
图14.13 客户-服务者用FIFO进行通信
这种安排可以工作,但也有一些不足之处。其中之一是服务者不能判断一个客户是
否崩溃终止,这就使得客户专用的FIFO会遗留在文件系统中。另一个是服务者必须
捕捉SIGPIPE信号,因为客户在发送一个请求后没有读取响应就可能终止,于是留
下一个有写进程(服务者)而无读进程的客户专用FIFO。
按照图14.13中的安排,如果服务者以只读方式打开众所周知的FIFO(因为它只需
读该FIFO),则每次客户数从1变成0,服务者就将在FIFO中读到一个文件结束标记
。为使服务者免于处理这种情况,一种常见的技巧是使服务者以读-写方式打开该
FIFO(见练习14.10)。
14.6 系统V IPC
三种系统V IPC-消息队列、信号量以及共享存储器之间有很多相似之处。在以下各
节说明这些IPC的各自特殊功能之前,我们先在本节介绍它们的类似特征。
这三种IPC源自于1970年的一种称为"Columbus Unix"的Unix内部版本。后
来
它们被加到SV上。
14.6.1 标识符和关键字
每个在核中的IPC结构(消息队列、信号量或共享存储段)都用一个非负整数的标
识符加以引用。例如,为了对一个消息队列发送或取消息,我们只需知道其队列标
识符。与文件描述符不同,IPC标识符不是小的整数。当一个IPC结构被创建,以后
又被删除时,与这种结构相关的标识符连续加1,直至达到一个整型数的最大正值
,然后又回转到0。(即使在IPC结构被删除后也记住该值,每次使用此结构时则增
1,该值被称为"槽使用顺序号"。它在ipc_perm结构中,我们将在下一节说明此结
构。)
无论何时在创建IPC结构时(调用msgget、semget或shmget),都应指定一个key(
关键字),关键字的数据类型由系统规定为key_t,通常在头文件<sys/types.h>中
被规定为长整型。关键字由核变换成标识符。
有多种方法使客户和服务者在同一IPC结构上会合。
1. 服务者可以指定关键字IPC_PRIVATE创建一个新IPC结构,将返回的标识符存放
在某处(例如一个文件)以便客户取用。关键字IPC_PRIVATE保证服务者创建一个
新IPC结构。这种技术的缺点是;服务者要将整型标识符写到文件中,然后客户在
此后又要读文件取得此标识符。
IPC_PRIVATE关键字也可用于父、子关系进程。父进程指定IPC_PRIVATE创建一个新
IPC结构,所返回的标识符在fork后可由子进程使用。子进程可将此标识符作为ex
ec函数的一个参数传给一个新程序。
2. 在一个公用头文件中定义一个客户和服务者都认可的关键字。然后服务者指定
此关键字创建一个新的IPC结构。这种方法的问题是该关键字可能已与一个IPC结构
相结合,在此情况下,get函数(msgget,semget或shmget)出错返回。服务者必须
处理这一错误,删除已存在的IPC结构,然后试着再创建它。
3. 客户和服务者认同一个路径名和课题ID(课题ID是在0~255之间的字符值),然
后调用函数ftok将这两个值变换为一个关键字。(函数ftok在手册页stdipc(3)
中说明。)然后在方法2中使用此关键字。ftok提供的唯一服务就是由一个路径名
和课题ID产生一个关键字。因为客户和服务者典型地至少共享一个头文件,所以一
个比较简写的方法是避免使用ftok,而只是在该头文件中存放一个大家都知道的关
键字。这样做还避免了使用另一个函数。
三个get函数(msgget,semget和shmget)都有两个类似的参数key和一个整型的fl
ag。如若满足下列条件,则创建一个新的IPC结构(通常由服务者创建);
1. key是IPC_PRIVATE,或
2. key当前末与特定类型的IPC结构相结合,flag中指定了IPC_CREAT位。为访问现
存的队列(通常由客户进行),key必须等于创建该队列时所指定的关键字,并且
不应指定IPC_CREAT。
注意,为了访问一个现存队列,决不能指定IPC_PRIVATE作为关键字因为这是
一个特殊的键值,它总是用于创建一个新队列。为了访问一个用IPC_PRIVATE关键
字创建的现存队列,一定要知道与该队列相结合的标识符,然后在其它IPC调用中
(例如msgsnd、msgrcv)使用该标识符。
如果我们希望创建一个新IPC结构,保证不是引用具有同一标识符的一个现行IPC结
构,那么我们必须在flag中同时指定IPC_CREAT和IPC_EXCL位。这样做了以后,如
果IPC结构已经存在就会造成出错,返回EEXIST(这与指定了O_CREAT和O_EXCL标志
的open相类似)。
14.6.2 许可权结构
系统V IPC为每一个IPC结构设置了一个ipc_perm结构。该结构规定了许可权和属主
。
struct ipc_perm {
uid_t uid; 所有者的有效用户id
gid_t gid; 所有者的有效组id
uid_t cuid; 创建者的有效用户id
gid_t cgid; 创建者的有效组id
mode_t mode; 访问方式
ulong seq; 槽使用序列号
key_t key; 关键字
}
在创建IPC结构时,除seq以外的所有字段都赋初值。以后,可以调用msgctl、sem
ctl或shmctl修改uid、gid和mode字段。为了改变这些值,调用进程必须或者是IP
C结构的创建者,或者是超级用户。更改这些字段类似于对文件调用chown和chmod
。
mode字段的值类似于在图4.4中所示的值,但是对于任何IPC结构都不存在执行许可
权。另外,消息队列和共享存储使用术语"读"和"写",而信号量则用术语"读"和"
更改"。图14.14中对每种IPC说明了六种许可权。
图14.14 系统V IPC许可权
14.6.3 结构限制
三种形式的系统V IPC都有我们可能会遇到的内在限制。这些限制的大多数是可以
通过重新配置而加以更改的。当叙说每种IPC时,我们都会指出它的限制
在SVR4中,这些值,以及它们的最小、最大值都在文件/etc/conf/cf.d/mtun
e中。
14.6.4 优、缺点
系统V IPC的主要问题是;IPC结构是在系统范围内起作用的,没有访问计数。例如
,如若我们创建了一个消息队列,在该队列中放入了几则消息,然后终止,但是该
消息队列及其内容并不被删除。它们余留在系统中直至;由某个进程调用msgrcv或
msgctl读消息或删除消息队列;某个进程执行ipcrm(1)命令删除消息队列;或由正
在再启动的系统删除消息队列。将此与管道pipe相比,那么当最后一个访问管道的
进程终止时,管道就被完全地删除了。对于FIFO而言虽然当最后一个引用FIFO的进
程终止时其名字仍保留在系统中,直至显式地删除它,但是留在FIFO中的数据却在
此时全部删除。
系统V IPC的另一个问题是;这些IPC结构并不按名字为文件系统所知。我们不能用
第3、4章中所述的函数来存取它们或修改它们的特性。为了支持它们不得不增加了
十个以上全新的系统调用(msgget、semop、shmat等)。我们不能用ls命令见到它
们,不能用rm命令删除它们,不能用chmod命令更改它们的存取权。于是,也不得
不增加了全新的命令ipcs和 ipcrm。
因为这些IPC不使用文件描述符,所以我们不能对它们使用多路转接I/O函数:sel
ect和poll。这就使得难于在一次使用多个IPC结构,难于用文件或设备I/O来使用
任何一个IPC结构。例如,没有某种形式的忙一等待循环,我们就不能使一个服务
者等待一个消息放在两个消息队列中的任一个上。
Andrade、Carges以及Kovach[1989]对使用系统V IPC的一个实际事务处理系统进行
了综述。他们认为系统V IPC使用的名字空间(标识符)是一个优点而不是我们在
前面所说的问题,理由是使用标识符使一个进程只要使用单个函数调用(msgsnd)
就能将一个消息发送到一个队列,而其它形式的IPC则通常要求open,write和clos
e。这种论据是不真实的。为了避免使用一个关键字和调用msgget,客户总要以某
种方式获得服务者队列的标识符。分派给特定队列的标识符取决于在创建该队列时
,有多少消息队列已经存在,取决于自核自举以来,在核中将分配给新队列的表项
已经使用了多少次。这是一个动态值,不能被猜测或事先存放在一个头文件中。正
如我们在14.6.1中所述,至少服务者应将分配给队列的标识符写到一个文件中以便
客户读取。
这些作者列举的消息队列的其它优点是:(a)它们是可靠的(b)流是受到控制的
,(c)面向记录,(d)可以用非先进先出方式处理。正如我们在12.4节中所见,
流也具有所有这些优点,虽然在向一个流发送数据之前,需要一个open,在结束时
需要一个close。图14.15对这些不同形式的IPC的某些特征进行了比较。
图14.15 不同形式IPC之间特征的比较
(我们将在第十五章对Unix流和数据报套接口进行简要说明。)表中的"无连接",
我们指的是无需先调用某种形式的open,就能发送消息的能力。正如前述,因为需
要有某种技术以获得队列标识符,所以我们并不认为消息队列具有无连接特性。因
为所有这些形式的IPC都限制用在单主机上,所以它们都是可靠的。当消息通过网
络传送时,丢失消息的可能性就要加以考虑。流控制的意思是:如果系统资源短缺
(缓存)或者如果接收进程不能再接收更多消息,则发送进程就要睡眠。当流控制
条件消失时,发送进程应自动地被唤醒。
在图14.15中我们没有表示的一个特征是:IPC设施能否自动地为每个客户自动地创
建一个到服务者的唯一连接。我们将在第十五章中说明,流以及Unix流套接口提供
这种能力。
下面三节顺次对三种形式的系统V IPC进行详细说明。
14.7消息队列(Message Queues)
消息队列是消息的链接表,存放在核内并由消息队列标识符标识。我们将称消
息队列为"队列",其标识符为"队列ID"。msgget创建一个新队列或打开一个现存的
队列。msgsnd将新消息添加到队列尾端。每个消息包含有一个正长整型类型字段,
一个非负长度以及实际数据字节(对应于长度),所有这些都在将消息添加到队列
时,传送给msgsnd。用msgrcv从队列中取消息。我们并不一定要以先进先出次序取
消息。我们可以按消息的类型字段取消息。
每个队列都有一个msqid_ds结构与其相关。此结构规定了队列的当前状态。
struct msqid_ds {
struct ipc_perm msg_perm ; 参见14.6.2 节
struct msg *msg_first; 指向队列中第一条消息的指针
struct msg *msg_last; 指向队列中最后一条消息的指针
ulong msg_cbytest; 队列中的当前字节号
ulong msg_qnum; 队列中的消息数
ulong msg_qbytes; 队列中的最大字节数
pid_t msg_lspid; 最后msgsnd()的pid
pid_t msg_lrpid; 最后msgrcv()的pid
time_t msg_stime 最后msgsnd()时间
time_t msg_rtime 最后msgrcv()时间
time_t msg_ctime; 最后更改时间
};
两个指针msgfirst和msglast分别指向相应消息在核内的存放位置,所以它们对用
户进程而言是无价值的。结构的其他成员是自定义的。
图14.16列出了影响消息队列的系统限制(14。6。3节)。
图14.6影响消息队列的系统限制
调用的第一个函数通常是msgget,其功能是打开一个现存队列或创建一个新队列
。
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgget(key_t key, int flag);
返回:若成功为消息队列ID,出错为-1
在14.6节中,我们说明了将key变换成一个标识符的规则,并且讨论是否创建一个
新队列或访一个现存队列。当创建一个新队列时,初始化msqid-ds结构的下列成员
∶
·ipc-perm结构按14.6.2节中所述进行初始化。该结构中mode按flag;中的相
应许可权位设置。这些许可权用图14.14中的常数指定
· msg_qnum,msg_lspid、msg_lrpid、msg_stime和msg_rtime都设置为0。
· msg_ctime设置为当前时间。
· msg_gbytes设置为系统限制值。
若执行成功,则返回非负队列ID。此后,此值就可被用于其它三个消息队列函数。
msgctl函数对队列执行多种操作。它以及另外两个与信号量和共享存储有关的函数
(semctl和shmctl)是系统V IPC的类似于ioctl的函数(亦即垃圾桶函数)。
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
返回:若成功为0,出错为-1
cmd参数指定对于由msqid规定的队列要执行的命令:
IPC_STAT 取此队列的msqid_ds结构,并将其存放在buf指向的结构中。
IPC_SET 按由buf指向的结构中的值,设置与此队列相关的结构中的下列四
个字段msg_perm.uid、msg_perm.gid、msg_perm;mode和msg_qbytes。此命令只能
由下列两种进程执行:一种进程是其有效用户ID等于msg_perm. cuid或msg_per
m.uid;另一种进程是具有超级用户特权的进程。只有超级用户才能增加msg_qbyte
s的值
IPC-RMID 从系统中删除该消息队列以及仍在该队列上的所有数据。这种删除
是立即生效的。仍在使用这一消息队列的其它进程在它们下一次试图对此队列进行
操作时,将出错返回,错误号为EIDRM。此命令只能由下列两种进程执行:一种进
程是其有效用户ID等于msg_perm.cuid或msg_perm.uid;另一种进程是具有超级用户
特权的进程。
我们将会看到这三条命令(IPC_STAT、IPC_SET和IPC_RMID)也用于信号量和共享
存储。
调用msgsnd将数据放到消息队列上。
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgsnd(int msqid, const void *ptr, size_t nbytes, int flag);
返回:若成功为0,出错为-1
正如我们在前面提及的,每个消息都由三部分组成,它们是:正长整型类型字段、
非负长度(nbytes)以及实际数据字节(对应于长度)。消息总是放在队列尾端。
Ptr指向一个长整型数,它包含了正整型消息类型,在其后立即跟随了消息数据。
(若nbytes是0,则无消息数据。)若我们发送的最长消息是512字节,则可定义下
列结构:
struct mymesg {
long mtypes; 正的消息类型
char mtext[512]; 消息数据,长度为nbytes
};
于是,ptr就是一个指向mymesg结构的指针。接收者可以使用消息类型以非先进先
出的次序取消息。
flag的值可以指定为IPC_NOWAIT。这类似于文件I/O的非阻塞I/O标志(见12.
2)。若消息队列已满(或者是队列中的消息总数等于系统限制值,或队列中的字
节总数等于系统限制值),则指定IPC_NOWAIT
使得msgsnd立即出错返回,出错号是EAGAIN。如若没有指定IPC_NOWAIT,则进程阻
塞直到(a)有空间可以容纳要发送的消息,(b)从系统中删除了此队列,或(c
)捕捉到一个信号,并从信号处理程序返回。在第二种情况,返回出错号EIDRM(
"标志符被删除"),最后一种情况则返回出错号EINTR。
注意,对消息队列删除的处理是不很得体的。因为对每个消息队列并没有设置一个
引用计数器(对打开文件则有这种计数器),所以删除一个队列使得仍在使用这一
队列的进程在下次对队列进行操作时产生出错返回。信号量机构也以同样方式处理
其删除。删除一个文件则要等到使用该文件的最后一个进程关闭了它,才删除文件
的内容。
Msgrcv从队列中取用消息
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgrcv(int msqid, void *ptr, size_t nbytes, long type, int flag);
返回:若成功为消息数据部分的长度,出错为-1
如同msgsnd中一样,ptr参数指向一个长整型数(返回的消息类型存放在其中
),跟随其后的是存放实际消息数据的缓存。nbytes说明数据缓存的长度。若返回
的消息大于nbytes,而且在flag中设置MSG_
NOERROR,则该消息就被截短。(在这种情况下,不通知我们消息截短了。)如果
没有设置这一标志,而消息又太长,则出错返回,出错号是EIBIG(消息仍留在队
列中)。
参数type使我们可以指定想要哪一种消息:
type== 返回队列中的第一个消息。
type>0 返回队列中消息类型为type的第一个消息。
type<0 返回队列中消息类型值小于或等于type绝对值,而且在这种消息中,其
类型值又最小的消息。
非0type用于以非先进先出次序读消息。例如,若应用程序对消息赋优先权,那么
type就可以是优先权值。如果一个消息队列由多个客户和一个服务者使用,那么t
ype字段可以用来包含客户进程ID。
我们可以指定flag值为IPC_NOWAIT,使操作不阻塞。这造成如果没有所指定类
型的消息,则ms
-grcv出错返回,出错号为ENOMSG。如果没有指定IPC_NOWAIT,则进程阻塞直至(
a)有了指定类型的消息,(b)从系统中删除了此队列(返回出错号EIDRM),或
(c)捕捉到一个信号并从信号处理程序返回(返回出错号EINTR)。
实例一消息队列对流管道的时间比较
如若需要在客户和服务者之间的双向数据流,我们可以使用消息队列或流管道
。(我们在15.2介绍流管道,它与管道类似,但是是全双工的。)
图14.17显示了在两个不同系统上这两种技术在时间方面的比较。测试程序先
创建IPC通道,调用fork,然后从父进程向子进程发送20mbytes数据。数据发送的
方式是:对于消息队列,调用10,000次msgsnd,每个消息长度为20,000bytes;
对于流管道,调用10,000次write,每次写20,000bytes。时间都以秒为单位。
图14.17 消息队列和流管道的时间比较
在SPARC上,流管道是用Unix域套接口实现的。在SVR4之下,pipe函数提供流管道
(使用我们在12.4中所述的流机制)。
从这些数字中可见,消息队列原来的实施目的是提供比一般IPC更高速度的进
程通信方法,但现在与其它形式的IPC相比,在速度方面已经没有什么差别了。(
在原来实施消息队列时,唯一的其它形式的IPC是半双工管道。)当我们考虑到使
用消息队列具有的问题时(14.6.4),我们得出的结论是,在新的应用程序中不应
当再使用它们。
14.8 信号量(Semaphores)
信号量与我们已经介绍过的IPC机构(管道、FIFO以及消息列队)不同。它是
一个计数器,用于多进程对共享数据对象的存取。为了获得共享资源,进程需要执
行下列操作:
1. 测试控制该资源的信号量。
2. 若此信号量的值为正,则进程可以使用该资源。进程将信号量值减1,表示它使
用了一个资源单位。
3. 若此信号量的值为0,则进程进入睡眠状态,直至信号量值大于0。若进程被唤
醒后,它返回至步骤1。
当进程不再使用由一个信息量控制的共享资源时,该信号量值增1。如果有进程正
在睡眠等待此信号量,则唤醒它们。
为了正确地实现信息量,信号量值的测试及减1操作应当是原子操作。为此,
信号量通常是在核内实现的。
常用的信号量形式被称之为双态信号量。它控制单个资源,其初试值为1。但
是,一般而言,信号量的初值可以是任一正值,该值说明有多少个共享资源单位可
供共享应用。
不幸的是,系统V的信号量与此相比要复杂得多。三种特性造成了这种并非必要的
复杂性。
1. 一个信号量并非是一个非负值。代之以我们必需将一个信号量定义为含有一个
或多个信号量值的集合。当创建一个信号量时,我们要指定该集合中的各个值
2. 创建信息量(semget)与对其赋初值(semctl)分开。这是一个致命的弱点,
因为我们不能原子地创建一个信号量集合,并且对该集合中的所有值赋初值。
3. 即使没有进程正在使用各种形式的系统V IPC它们仍然是存在的,所以不得不为
这种程序担心,它在终止时并没有释放已经分配给它的信号量。将在下面说明的"
undo"功能就是假定要处理这种情况的。
核为每个信号量设置了一个semid_ds结构。
struct semid_ds {
struct ipc_perm sem_perm; 见14.6.2节
struct sem *sem_base; 指向集中的第一个信号量
ushort sem_nsems; 集中的信号量数
time_t sem_otime; 最后semop()时间
time_t sem_ctime; 最后更改时间
};
对用户而言,sem_base指针是没有价值的,它指向核内的sem结构数组,该数
组中包含了sem_nsems个元素每个元素,各对应于集合中的一个信号量值。
struct sem {
ushort semval; 信号量值,总是>=0
pid_t sempid; 最后操作的pid
ushort semncnt; 等待semval>currval的进程数
ushort semzcnt; 等待semval=0的进程数
};
图14.18列出了影响信号量集合的系统限制(见14.6.3)
图14.18 影响信号量的系统限制
要调用的第一个函数是semget以获得一个信号量ID。
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semget(key_t key, int nsems, int flag);
返回:若成功返回信号量ID,出错为-1
在14.6.1中,我们说明了将key变换为标识符的规则,讨论了是否创建一个新
集合,或是引用一个现存的集合。但创建一个新集合时,对semid_ds结构的下列成
员赋初值:
l 按14.6.2节中所述,对ipc_perm结构赋初值。该结构中的mode被设置为flag中的
相应许可权位。这些许可权是用图14.14中的常数设置的。
l sem_otime设置为0。
l sem_ctime设置为当前时间。
l sem_nsem 设置为nsems。
nsems是该集合中的信号量数。如果是创建新集合(典型地在服务者中),我们必
须指定nsems。如果引用一个现存的集合(一个客户),则将nsems指定为0。
semctl函数包含了多种信号量操作。
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semctl(int semid, int semnum, int cmd, union semun arg);
注意,最后一个参数是个联合(union),而非指向一个union的指针。
union semun {
int l; 用于SETVAL
struct semid_ds *bf; 用于IPC_STAT和IPC_SET
ushort *array; 用于GETALL和SETALL
};
Cmd参数指定下列十种命令中的一种,使其在semid指定的信号量集合上执行此命令
。其中有五条命令是针对一个特定的信号量值的,它们用semnum指定该集合中的一
个成员。Semnum值在0和nsems-1之间(包括0和nsem-1)。
IPC_STAT 对此集合取semid_ds结构,并存放在由arg.buf指向的结构中。
IPC_SET 按由arg.buf指向的结构中的值设置与此集合和相关结构中的下列三
个
字段值:sem_perm,sem_perm.gid和sem_perm.mode。此命令只能由下
列
两种进程执行:一种进程是其有效用户ID等于sem_perm.cuid或sem_
perm.uid;另一种进程是具有超级用户特权的进程。
IPC_RMIK 从系统中删除该信号量集合。这种删除是立即的。仍在使用此信号量
的
其它进程在它们下次意图对此信号量进行操作时,将出错返回,错误
号
是EIDRM。此命令只能由下列两种进程执行:一种进程是具有效用户
ID
等于sem_perm.cuid或sem_perm.uid;另一种进程是具有超级用户特
权
的进程。
GETVAL 返回成员semnum的semval值。
SETVAL 设置成员semnum的semval值。该值由arg.val指定。
SETVAL 返回成员semnum的sempid值。
GETNCNT 返回成员semunm的semncnt值。
GETZCNT 返回成员semnum的semzcnt值。
GETALL 取该集合中所有信号量的值,并将它们存放在由arg.array指向的数组
中。
SETALL 按arg.array指向的数组中的值设置该集合中所有信号量的值。
对于除GETALL以外的所有GET命令,semctl函数都返回相应值。其它命令的返回值
为0。函数semop自动执行在信号量集合上的操作数组。
---------------------------------------
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semop(int semid, struct sembuf semoparray[], size_t nops);
返回:若成功为0,出错为-1
---------------------------------------
semoparray是一个指针,它指向一个信号量操作数组。
struct sembuf {
ushort sem_num; 在set(0,1,…,nsems-1)中的成员数
short sem_op; 操作(负,0或正)
short sem_flg; IPC_NOWAIT, SEM_UNDO
};
nops规定在该数组中操作的数量(元素数)。
对集合中每个成员的操作由相应的sem_op规定。此值可以是负值、0或正值。(在
下面的讨论中,我们将提到信号量的"undo"标志。此标志对应于相应sem_flg成员
的SEM_UNDO位。)
1. 最易于处理的情况是sem_op为正。这对应于返回进程占用的资源。sem_op值加
到信号量的值上。如果指定了undo标志,则也从该进程的此信号量调整值中减去s
em_op。
2. 若sem_op为负,则表示要获取由该信号量控制的资源。
如若该信号量的值大于或等于sem_op的绝对值(具有所需的资源),则从信号量值
中减去sem_op的绝对值。这保证信号量的结果值大于或等于0。如果指定了undo标
志,则sem_op的绝对值也加到该进程的此信号量调整值上。
如果信号量值小于sem_op的绝对值(资源不能满足要求):
a) 若指定了IPC_NOWAIT,则出错返回,出错号的EAGAIN;
b) 若末指定IPC_NOWAIT,则该信号量的semncnt值加1(因为将进入睡眠状态),
然后调用进程被挂起直至下列事件之一发生:
i. 此信号量变成大于或等于sem_op的绝对值(也就是,某个进程已释放了某些资
源)。此信号量的semncnt值减1(因为已结束等待),并且从信号量值中减去sem_
op的绝对值。如果指定了undo标志,则sem_op的绝对值也加到该进程的此信号量调
整值上。
ii. 从系统中删除了此信号量。在此情况下,函数出错返回,出错号为ERMID。
iii. 进程捕捉到一个信号,并从信号处理程序返回,在此情况下,此信号量的se
mncnt值减1(因为不再等待),并且函数出错返回,出错号为EINTR.
3. 若sem_op为0,这表示希望等待到该信号量值变成0。
如果信号量值当前是0,则此函数立即返回。
如果信号量值非0:
a) 若指定了IPC_NOWAIT,则出错返回,出错号是EAGAIN;
b) 若末指定IPC_NOWAIT,则该信号量的semncnt值加1(因为将进入睡眠状态),
然后调用进程被挂起,直至下列事件之一发生:
i. 此信号量值变成0。此信号量的semzcnt值减1(因为已结束等待)。
ii. 从系统中删除了此信号量。在此情况下,函数出错返回,出错号为ERMID。
iii. 进程捕捉到一个信号,并从信号处理程序返回。在此情况下,此信号量的se
mzent值减1(因为不再等待),并且函数出错返回,出错号为EINTR.
semop具有原子性,因为它或者执行数组中的所有操作,或者一个也不做。
在exit时的信号量调整
正如我们在前面提到的,如果在进程终止时,它占用了经由信号量分配的资源,那
么就会成为一个问题。无论何时只要我们为信号量操作指定了SEM_UNDO标志,然后
分配(sem_op值小于0),那么核就会记住对于该特定信号量,分配给我们多少资
源(sem_op的绝对值)。当该进程终止时或者自愿或者不自愿,核都将检验该进程
是否还有尚未处理的信号量调整值,如果有,则按调整值对相应量值进行调整。
如若我们用带SETVAL或SETALL命令的semctl设置一信号量的值,则在所有进程中,
对于该信号量的调整值都设置为0。
实例-信号量和记录锁的时间比较
如果多个进程共享一个资源,则我们可使用信号量或记录锁。对这两种技术在时间
上的差别进行比较是有益的。
若使用信号量,则先创建一个包含一个成员的信号量集合,然后对该信号量值赋初
值1。为了分配资源,以sem_op为-1调用semop,为了释放资源,则以sem_op为+1调
用semop。对每个操作都指定SEM_UNDO,以处理在末释放资源情况下进程终止的情
况。
若使用记录锁,则先创建一个空文件,并且用该文件的第一个字节(无需存在)作
为锁字节。为了分配资源,我们先对该字节获得一个写锁,释放该资源时,则对该
字节开锁。记录锁的性质保证了,当保有一个锁的进程终止时,核会自动释放该锁
。
图14.19显示了在两个不同系统上,使用这两种不同技术进行锁操作所需的时间。
在各种情况中,资源都被分配,然后又释放10,000次。这同时由3个不同的进程执
行。图14.19中所示的时间是3个进程的总计,单位是秒。
图14.19 信号量锁和记录锁的时间比较
在SPARC上,记录锁与信号量锁相比,在系统时间方面要多耗用10%。在80386上,
多耗用约50%。
虽然,记录锁稍慢于信号量锁,如果只需锁一个资源(例如共享存储段)并且不需
要使用系统V信号量的所有花俏的功能,则宁可使用记录锁。理由是:(a)使用简易
(b)进程终止时,会处理任一遗留下的锁。
14.9 共享存储(Shared Memory)
共享存储允许两个或多个进程共享一给定的存储区。因为数据不需要在客户和服务
者之间复制,所以这是最快的一种IPC。使用共享存储的唯一窍门是多个进程之间
对一给定存储区的同步存取。若服务者将数据放入共享存储区,则在服务者做完这
一操作之前,客户不应当去取这些数据。通常,信号量被用来实现对共享存储存取
的同步。(不过正如前节最后部分所述,记录锁也可用于这种场合。)
核为每个共享存储段设置了一个shmid_ds结构。
struct shmid_ds {
struct ipc_perm shm_perm; 见14.6.2节
struct anon_map *shm_amp; 内核中的指针
int shm_segsz; 段长(字
冢�
ushort shm_lkcnt; 对段加锁的次数
pid_t shm_lpid; 最后shmop()的pid
pid_t shm_cpid; 创建者pid
ulong shm_nattch; 当前连接数
ulong shm_cnattch; 只用于shminfo
time_t shm_atime; 最后连接时间
time_t shm_dtime; 最后脱接时间
time_t shm_ctime; 最后更改时间
};
图14.20 列出了影响共享存储的系统限制(14.6.3节)。
图14.20 影响共享存储的系统限制
调用的第一个函数通常是shmget,它获得一个共享存储标识符。
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, int size, int flag);
返回:若成功为共
内存ID,出错为-1
在14.6.1节中,我们说明了将key变换成一个标识符的规则,以及是否是创建一个
新共享存储段或存访一个现存的共享存储段。当创建一个新段时,初始化shmid_d
s结构的下列成员:
l ipe_perm结构按14.6.2节中所述进行初始化。该结构中的mode按flag中的相应许
可权位设置。这些许可权用图14.14中的常数指定。
l shm_lpid、shm_nattach、shm_atime、以及shm_dtime都设置为0。
l shm_ctime设置为当前时间。
Size是该共享存储段的最小值。如果正在创建一个新段(典型地在服务者中),我
们必须指定其size。如果我们正在存访一个现存的段(一个客户),我们将size指
定为0。
Shmctt函数对共享存储段执行多种操作。
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
返回:若成功为0�
出错为-1
Cmd参数指定下列5种命令中一种,使其在shmid指定的段上执行。
IPC_STAT 对此段取shmid_ds结构,并存放在由buf指向的结构中。
IPC_SET 按buf指向的结构中的值设置与此段相关结构中的下列三个字段: s
hm_perm.uid、shm_perm.gid以及shm_perm.mode。此命令只能由下列两种进程执行
:一种进程是其有效用户ID等于shm_perm.cuid或shm_perm.uid;另一种进程是具
有超级用户特权的进程。
IPC_RMID 从系统中删除该共享存储段。因为每个共享存储段有一个连接计数(
shm_nattch 在shmid_ds结构中),所以除非使用该段的最后一个进程终止或与该段
脱接,否则不会实际上删除该存储段。不管此段是否仍在使用,该段标识符立即被
删除,所以不能再用shmat与该段连接。此命令只能由下列两种进程执行:一种进程
是其有效用户ID等于shm_perm.cuid或shm_perm.uid:另一种进程是具有超级用户特
权的进程。
SHM_LOCK 在内存中锁住共享存储段。此命令只能由超级用户执行。
SHM_UNLOCK 解锁共享存储段。此命令只能由超级用户执行。
一旦创建了一个共享存储段,进程就可调用shmat将其连接到它的地址空间中。
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
void *shmatt(int shmid, void *addr, int flag);
返回:若成功为指
共享内存的指针,出错为-1
共享存储段连接到调用进程的哪个地址上与addr参数以及在flag中是否指定SHM_R
ND位有关。
1. 如果addr是0,则此段连接接到由系统核选择的第一个可用地址上。
2. 如果addr非0,并且没有指定SHM_RND,则此段连接到addr所指定的地址上。
3. 如果addr非0,并且指定了SHM_RND,则此段连接到(addr_(addr摸除SHMLBA))
所表示的地址上。SHM_RND命令的意思是:取整。SHMLBA的意思是:低边界地址倍
数量(low boundary address multiple),它总是2的乘方。该算式做的是将地址
向下取最近1个SHMLBA的倍数。
除非我们计划只在一种硬件上运行应用程序(这在当今是不大可能的),否则就不
会指定共享段所连接到的地址。所以一般应指定addr为0,以便由系统选择地址。
如果在flag中指定了SHM_RDONLY位,则以只读方式连接此段。否则以读写方式连接
此段。
Shmat的返回值是该段所连接的实际地址,如果出错则返回-1。
当对共享存储段的操作已经结束时,则调用shmdt脱接该段。注意,这并不从系统
中删除其标识符以及其数据结构。该标识符仍然存在,直至某个进程(一般是服务
者)调用shmctl(带命令IPC_RMID)特地删除它。
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
int shmdt(void *addr);
返回:若成功为0�
出错为-1
addr参数是以前调用shmat时的返回值。
实例
系统核将以地址0连接的共享存储段放在什么位置上是与系统密切相关的。程序14
.11打印一些信息,它们与一个指定系统将不同类型的数据放在什么位置有关。在
一个特定的系统上运行此程序给出下列输出:
$ a.out
array[] from 18f48 to 22b88
stack around f7fffb2c
malloced from 24c28 to 3d2c8
shared memory attached from f77d0000 to f77e86a0
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include "ourhdr.h"
#define ARRAY_SIZE 40000
#define MALLOC_SIZE 100000
#define SHM_SIZE 100000
#define SHM_MODE (SHM_R | SHM_W) /* user read/write */
char array[ARRAY_SIZE]; /* uninitialized data = bss */
int
main(void)
{
int shmid;
char *ptr, *shmptr;
printf("array[] from %x to %x\n", &array[0], &array[ARRAY_SIZE]);
printf("stack around %x\n", &shmid);
if ( (ptr = malloc(MALLOC_SIZE)) == NULL)
err_sys("malloc error");
printf("malloced from %x to %x\n", ptr, ptr+MALLOC_SIZE);
if ( (shmid = shmget(IPC_PRIVATE, SHM_SIZE, SHM_MODE)) < 0)
err_sys("shmget error");
if ( (shmptr = shmat(shmid, 0, 0)) == (void *) -1)
err_sys("shmat error");
printf("shared memory attached from %x to %x\n",
shmptr, shmptr+SHM_SIZE);
if (shmctl(shmid, IPC_RMID, 0) < 0)
err_sys("shmctl error");
exit(0);
}
程序14.11 打印不同的类型的数据所存放的位置
图14.21显示了这种情况,这与图7.3中所示的典型存储区布局类似。注意,共享存
储段紧靠在栈之下。实际上,在共享存储段和栈之间有大约8mbytes的末用地址空
间。
图14.21 在一个特定系统上的存储区布局
实例一 /dev/zero的存储映射
共享存储可由不相关的进程使用。但是,如果进程是有关系的,则SVR4提供了一种
不同的技术。
设备/dev/zero在读时,是0字节的无限资源。此设备也接收写向它的任何数据,但
忽略此数据。我们对此设备作为IPC的兴趣在于,当对其进行存储映射时,它具有
一些特殊性质。
l 创建一个未名存储区,其长度是mmap的第二个参数,将其取整为系统上的最近页
长。
l 存储区都初始化为0。
l 如果多个进程的共同祖先进程对mmap指定了MAP_SHARED标志,则这些进程可共享
此存储区。
程序14.12是使用此特殊设备的一个例子。它打开此/dev/zero设备,然后指定一个
长整型的size调用mmap。注意,一旦该存储区被映射了,我们就能关闭此设备。然
后,进程创建一个子进程。因为在调用mmap时指定了MAP_SHARED,所以一个进程写
到存储映照区的数据可由另一进程见到。(如果我们已指定MAP_PRIVATE,则此程
序不会工作。)
然后,父、子进程交替运行,使用8.8节中的同步函数各自对共享存储映射区中的
一个长整型数加1。存储映射区由mmap初始化为0。父进程先对它进行增1操作,使
其成为1,然后子进程对其进行增1操作,是其成为2,然后父进程使其成为3……。
注意,当我们在update函数中,对长整型值增1时,必须使用括号;因为我们增加
其值,而不是指针。
#include <sys/types.h>
#include <sys/mman.h>
#include <fcntl.h>
#include "ourhdr.h"
设备/dev/zero在读时,是0字节的无限资源。此设备也接收写向它的任何数据,但
忽略此数据。我们对此设备作为IPC的兴趣在于,当对其进行存储映射时,它具有
一些特殊性质。
l 创建一个未名存储区,其长度是mmap的第二个参数,将其取整为系统上的最近页
长。
l 存储区都初始化为0。
l 如果多个进程的共同祖先进程对mmap指定了MAP_SHARED标志,则这些进程可共享
此存储区。
程序14.12是使用此特殊设备的一个例子。它打开此/dev/zero设备,然后指定一个
长整型的size调用mmap。注意,一旦该存储区被映射了,我们就能关闭此设备。然
后,进程创建一个子进程。因为在调用mmap时指定了MAP_SHARED,所以一个进程写
到存储映照区的数据可由另一进程见到。(如果我们已指定MAP_PRIVATE,则此程
序不会工作。)
然后,父、子进程交替运行,使用8.8节中的同步函数各自对共享存储映射区中的
一个长整型数加1。存储映射区由mmap初始化为0。父进程先对它进行增1操作,使
其成为1,然后子进程对其进行增1操作,是其成为2,然后父进程使其成为3……。
注意,当我们在update函数中,对长整型值增1时,必须使用括号;因为我们增加
其值,而不是指针。
#include <sys/types.h>
#include <sys/mman.h>
#include <fcntl.h>
#include "ourhdr.h"
#define NLOOPS 1000
#define SIZE sizeof(long) /* size of shared memory area */
static int update(long *);
int
main()
{
int fd, i, counter;
pid_t pid;
caddr_t area;
if ( (fd = open("/dev/zero", O_RDWR)) < 0)
err_sys("open error");
if ( (area = mmap(0, SIZE, PROT_READ | PROT_WRITE,
MAP_SHAR
D, fd, 0)) == (caddr_t) -1)
err_sys("mmap error");
close(fd); /* can close /dev/zero now that it's mapped */
TELL_WAIT();
if ( (pid = fork()) < 0) {
err_sys("fork error");
} else if (pid > 0) { /* parent */
for (i = 0; i < NLOOPS; i += 2) {
if ( (counter = update((long *) area)) != i)
以我们所示方式使用/dev/zero的优点是;在调用mmap以创建映射区之前,无需存
在一个实际文件。映射/dev/zero自动创建一个指定长度的映射区。这种技术的缺
点是;它只能由相关进程使用。如果在无关进程之间需要使用共享存储区,则必须
使用shmXXX函数。
实例-匿名存储映射
4.3+BSD提供了一种类似于/dev/zero的施设,它被称为匿名存储映射。为了使
用这种功能,在调用mmap时指定MAP_ANON标志,并将描述符指定为-1。结果得到的
区域是匿名的(因为它并不通过一个文件描述符与一个路径名相结合),并且创建
一个存储区,它可与后代进程共享。
为了修改程序14.12使其应用4.3+BSD的这种特征,我们作了两个修改;(a)删除
open /dev/zero和close(fd)条语句,(b)将mmap调用修改成下列形式;
if((area = mmap(o,SIZE,PROT_READ|PROT_ WRITE,
MAP_ANON|MAP_SHARED,-1,0))= =(caddr_t)-1)
在此调用中,我们指定了MAP_ANON标志,并将文件描述符设置为-1。程序14.12的
其余部分则不加改变。
14.10 客户-服务者属性
下面详细说明客户和服务者的属性,这些属性受到它们之间所使用的IPC的不同类
型的影响。
最简单的关系类型是使客户fork并执行所希望的服务者。在fork之前先创建两个单
向管道以便使数据可在两个方向传输。图14.9是这种形式的一个例子。被执行的服
务者可能是设置_用户_ID的程序,这使它具有了特权。查看客户的实际用户ID就可
以决定客户的身份。(请回忆8.9节,从中可了解到在exec前后实际用户ID和实际
组ID并没有改变。)
在这种安排下,我们可以构筑一个"开放式的服务者"。(在15.4节中我们提供了这
种客户和服务者的一种实现。)它为客户开放文件而不是客户调用open函数。这样
就可以增加在正常的Unix用户/组/其他许可权之上或之外的附加的许可权检查。我
们假定服务者是设置_用户_ID程序,这给予了它附加的许可权(很可能是root许可
权)。服务者用客户的实际用户ID以决定是否给予它对所要求的文件的存取。使用
这种方式,我们可以构筑一个服务者,它允许某种用户通常没有的存取权。
在此例子中,因为服务者是父进程的子进程,所以它能做的一切是将文件内容传送
给父进程。这种方式对一般文件工作得很好,同时,也可被用于专用设备文件。我
们希望能做的是使服务者打开所要的文件,并将文件描述符送回。父进程可向子进
程传送打开文件描述符,而子进程则不能向父进程传回一个描述符。(除非使用我
们将在下一章介绍的专门编程技术。)
下一种服务者类型我们已显示于图14.13中,服务者是一个精灵进程,客户则用某
种形式的IPC与其联系。我们可以将管道用于这种形式的客户一服务者关系。要求
有一种命名的IPC,例如FIFO或消息队列。对于FIFO如果服务者须将数据送回客户
,则对每个客户都要有单独使用的FIFO。如果客户一服务者应用程序只有客户向服
务者送数据,则只需要一个众所周知的FIFO。(系统V行式打印机假脱机程序使用
这种形式的客户一服务者。客户是lp(1)命令,服务者是lpsched进程。因为只有从
客户到服务者的数据流,没有任何数据需送回客户,所有只需使用一个FIFO。
使用消息队列则存在多种可能性:
1. 在服务者和要客户之间可以只使用一个队列,使用每个消息的类型字段指明谁
是消息的接受者。例如,客户可以用类型字段为1发送它们的消息。在要求之中应
包括客户的进程ID。此后,服务者在发送响应消息时,将类型字段设置为客户的进
程ID。服务者只接受类型字段为1的消息(msgrcv的第四个参数),客户则只接受
类型字段等于它们的进程ID的消息。
2. 另一种方法是每个客户使用一个单独的消息队列。在向服务者发送第一个请求
之前,每个客户先创建它自己的消息队列,创建时用关键字IPC_PRIVATE服务者也
有它自己的队列,其关键字或标识符是所有客户是能知道的。客户将其第一个请求
送到服务者的众所周知的队列上,该请求中应包含其客户消息队列的队列ID。服务
者将其第一个响应送至客户队列,此后的所有请求和响应都在此队列上交换。
使用这种技术的一个问题是:每个客户专用队列通常只有一个消息在其中--或者是
对服务者的一个请求,或者是对客户的响应。这似乎是对有限的系统资源(消息队
列)的浪费,可以用一个FIFO来代替。另一个问题是服务者需从多个队列读消息。
对于消息队列,select和poll都不起作用
使用消息队列的这两种技术都可以用共享存储段和同步方法(信号量或记录锁)实
现。使用共享存储段的问题是,一次只能有一个消息在共享存储段中--类似于队列
限制为只能有一个消息。为此,在使用共享存储IPC时,通常每个客户使用一个共
享存储段。
这种类型的客户一服务者关系(客户和服务者是无关系进程)的问题是;服务者如
何准确地标识客户。除非服务者正在执行一种非特权操作,否则服务者知道谁是客
户是很重要的。例如,若服务者是一个设置_用户_ ID程序,就有这种要求。虽然
,所有这几种形式的IPC都是经由系统核的,但是它们并未提供任何措施使系统核
能够标识发送者。
对于消息队列,如果在客户和服务者之间使用一个专用队列(于是一次只有一个消
息在该队列上),那么队列的msg_lspid包含了对方进程的进程ID。但是当客户将
请求发送给服务者时,我们想要客户的有效用户ID,而不是它的进程ID。现在还没
有一种可移植的方法,在已知进程ID情况下用其可以得到有效用户ID。(确实,系
统核在进程表项中保持有这两种值,但是除非彻底检查系统核存储空间,否则已知
一个,无法得到另一个。)
我们将在15.5.2中使用下列技术,使服务者可以标识客户。同样的技术也可用于FI
FO、消息队列、信号量或共享存储。下面的说明是具体针对按图14.13中的方式使
用FIFO情况的。客户必须创建它自己的FIFO,并且设置FIFO的文件存取许可权,使
得只允许用户_读,用户_写。我们假定服务者具有超级用户特权(或者它很可能并
不关心客户的真实标识),所以服务者仍可读、写此FIFO。当服务者在服务者的众
所周知的FIFO上接受到客户的第一个请求时(它应当包含客户专用FIFO的标识),
服务者调用针对客户专用FIFO的stat或fstat。服务者所采用的假设是;客户的有
效用户ID是FIFO的属主(stat结构的st_uid字段)。服务者验证该FIFO只有用户_
读、用户_写许可权。服务者还应检查的是该FIFO的三个时间量(stat结构中的st
_atime,st_mtime和st_ctime字段),要检查它们与当前时间是否很接近(例如不
早于当前时间15秒或30秒)。如果一个有预谋的客户可以创建一个FIFO,使另一个
用户成为其属主,并且设置该文件的许可权为用户读和用户写,那么在系统中就存
在了其它基础性的安全问题。
为了在系统V IPC中应用这种技术,请回想一下与每个消息队列、信号量、以及共
享存储段相关的ipc_perm结构,其中cuid和cgid字段标识IPC结构的创建者。以FI
FO为例,服务者应当要求客户创建该IPC结构,并使客户将存取权设置为只允许用
户读和用户写。服务者也应检验与该IPC相关的时间量与当前时间是否很接近(因
为这些IPC结构在显式地删除之前,一直存在)。
在15.5.1中,我们将会看到进行这种身份验证工作的一种更好方法是系统核提供客
户的有效用户ID和有效组ID。SVR4在进程之间传送文件描述符时可以做到这一点。
14.11摘要
本章详细说明了进程间通信的多种形式;管道、命名管道(FIFO)以及另外三种I
PC形式,通常称之为系统V IPC-消息队列,信号量和共享存储。信号量实际上是同
步原语而不是IPC,常用于共享资源的同步存取,例如共享存储段。对于管道,我
们说明了popen的实现,说明了协同进程,以及使用标准I/O库缓存机制时可能遇到
的问题。
在时间方面,对消息队列-流管道、信号量-记录锁作了比较后,我们提出了下列建
议;学会使用管道和FIFO,因为在大量应用程序中仍可有效地使用这两种基本技术
。在新的应用程序中,要尽可能避免使用消息队列以及信号量。代之以应当考虑流
管道和记录锁,因为它们与Unix系统核的其它部分集成得要好得多。共享存储段有
其应用场合,而mmap函数(12.0节)则可能在以后的版本中起更大作用。
下一章将介绍一些更先进的IPC形式,它们是由更加新的系统,例如SVR4和4.3+BS
D提供的。
习题:
14.1 在程序14.2的父进程代码的末尾,删除waitpid前的close的结果如何?
14.2 在程序14.2的父进程代码的末尾,删除waitpid的结果如何?
14.3 如果popen的参数是一个不存在的命令会有什么结果?编写一段程序测试一下
。
14.4 删除程序14.9中的信号量处理程序,执行程序并终止子进程。输入一行后,
怎样能说明父进程是由SIGPIPE终止的?
14.5 将程序14.9中进行管道读、写的read和write用标准I/O库代替。
14.6 POSIX.1加入waitpid函数的理由之一是POSIX.1前的大多数系统不能处理下面
的代码。
if ( (fp = popen ( "bin/true", "r" ) ) = NULL )
…
if ( ( rc = system ( "sleep 100" ) ) == -1 )
…
if ( pclose ( fp ) == -1)
…
若在这段代码中不使用waitpid函数会如何?用wait代替呢?
14.7 当一个管道被写者关闭后,解释select和poll如何处理该管道的输入描述符
?当一个管道的读端百关闭时,请重做此习题以查看该管道的输出描述符。编两个
测试程序,一个用select一个用poll确定答案是否正确。
14.8 如果popen以类型"r"执行cmdstring并将结果写到标准出错输出,结果如何?
14.9 popen会使得shell执行它的cmdstring参数,当cmdstring终止时会产生什么
结果?(提示:画出包含的所有进程)
14.10 大多数Unix系统允许读写FIFO,但是POSIX.1特别声明没有定义为读写而打
开FIFO。请用非阻塞方法实现为读写而打开FIFO。
14.11 除非文件包含敏感或机密数据,否则允许其它用户读文件不会造成损害。但
是,如果一个恶意进程读取了被一个服务器和几个客户进程使用的消息队列中的一
条消息后会有什么结果?恶意进程需要知道哪些信息就可以读消息队列?
14.12 编写一段程序完成下面的工作:
循环五次创建一个消息队列,并打印队列的标识符,然后删除队列。接着再循环
五次利用关键字IPC_PRIVATE创建消息队列并将一条消息放在队列中,程序终止后
用ipcs(1)查看消息队列。解释队列标识符的变化。
14.13 描述如何在共享内存段中建立一个数据对象的连接列表。列表指针如何保存
?
14.14 画出程序14.12的变量i在父进程和子进程中随时间变化的值。由update函数
返回该值,其类型为长整型,保存在共享内存区。假设fork后子进程先运行。
14.15 使用14.9节的shmXXX函数代替共享内存映射区重写程序14.12。
14.16 使用14.8节系统V提供的信号量函数重写程序14.12实现父进程与子进程间的
交替。
14.17 使用建议性记录锁定方法重写程序14.12实现父进程与子进程间的交替。
14.18 解释mmap函数的文件描述符参数如何在4.3+BSD系统匿名内存映射方式下允
许不相关进程间的内存共享。
--
--
※ 来源:·BBS 水木清华站 smth.org·[FROM: 202.38.248.38]
BBS水木清华站∶精华区