进程间通信
- 1. 管道
管道是UNIX IPC的最老形式,并且所有U N I X系统都提供此种通信机制,管道有两种限制;
(1) 它们是半双工的。数据只能在一个方向上流动。
(2) 它们只能在具有公共祖先的进程之间使用。通常,一个管道由一个进程创建,然后该进程调用f o r k,此后父、子进程之间就可应用该管道。
管道是由调用p i p e函数而创建的。
#include <unistd.h> int pipe(int filedes[2]); |
Returns: 0 if OK, 1 on error |
经由参数f i l e d e s返回两个文件描述符: f i l e d e s [ 0 ]为读而打开,f i l e d e s [ 1 ]为写而打开。f i l e d e s [ 1 ]的输出是f i l e d e s [ 0 ]的输入。
当管道的一端被关闭后,下列规则起作用:
(1) 当读一个写端已被关闭的管道时,在所有数据都被读取后, r e a d返回0,以指示达到了
文件结束处(从技术方面考虑,管道的写端还有进程时,就不会产生文件的结束。可以复制一
个管道的描述符,使得有多个进程具有写打开文件描述符。但是,通常一个管道只有一个读进
程,一个写进程。下一节介绍F I F O时,我们会看到对于一个单一的F I F O常常有多个写进程)。
(2) 如果写一个读端已被关闭的管道,则产生信号S I G P I P E。如果忽略该信号或者捕捉该信
号并从其处理程序返回,则w r i t e出错返回,e r r n o设置为E P I P E。
在写管道时,常数P I P E _ B U F规定了内核中管道缓存器的大小。如果对管道进行w r i t e调用,而且要求写的字节数小于等于P I P E _ B U F,则此操作不会与其他进程对同一管道(或F I F O)的w r i t e操作穿插进行。但是,若有多个进程同时写一个管道(或F I F O),而且某个或某些进程要求写的字节数超过P I P E _ B U F字节数,则数据可能会与其他写操作的数据相穿插。
popen和p c l o s e函数
因为常见的操作是创建一个连接到另一个进程的管道,然后读其输出或向其发送输入,所以标准I / O库为实现这些操作提供了两个函数p o p e n和p c l o s e。这两个函数实现的操作是:创建
一个管道,f o r k一个子进程,关闭管道的不使用端, e x e c一个s h e l l以执行命令,等待命令终止。
#include <stdio.h> FILE *popen(const char *cmdstring, const char *type); |
Returns: file pointer if OK, NULL on error |
int pclose(FILE *fp); |
Returns: termination status of cmdstring, or 1 on error |
函数popen 先执行f o r k,然后调用e x e c以执行c m d s t r i n g,并且返回一个标准I / O文件指针。
如果t y p e是"r",则文件指针连接到c m d s t r i n g的标准输出(见图1 4 - 5)。
如果t y p e 是"w",则文件指针连接到c m d s t r i n g 的标准输入(见图1 4 - 6)。
实例
考虑一个应用程序,它向标准输出写一个提示,然后从标准输入读1行。使用p o p e n ,可以在应用程序和输入之间插入一个程序以对输入进行变换处
理。图1 4 - 7显示了进程的安排。
对输入进行的变换可能是路径名的扩充,或者是提供一种历史机制(记住以前输入的命令)。(本实例取自P O S I X . 2草案。)
程序1 4 - 6是一个简单的过滤程序,它只是将输入复制到输出,在复制时将任一大写字符变换为小写字符。在写了一行之后,对标准输出进行了刷清(用ff l u s h),其理由将在下一节介绍协同进程时讨:
Figure 15.14. Filter to convert uppercase characters to lowercase
#include "apue.h"
#include <ctype.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);
}
Figure 15.15. Invoke uppercase/lowercase filter to read commands
#include "apue.h"
#include <sys/wait.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);
}
U N I X过滤程序从标准输入读取数据,对其进行适当处理后写到标准输出。几个过滤进程通常在s h e l l管道命令中线性地连接。当同一个程序产生某个过滤程序的输入,同时又读取该过滤程序的输出时,则该过滤程序就成为协同进程( c o p r o c e s s )。
程序1 4 - 8是一个简单的协同进程,它从其标准输入读两个数,计算它们的和,然后将结果写至标准输出。
Figure 15.17. Simple filter to add two numbers
#include "apue.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);
}
Figure 15.18. Program to drive the add2 filter
#include "apue.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");
}
exit(0);
}
static void
sig_pipe(int signo)
{
printf("SIGPIPE caught\n");
exit(1);
}
在协同进程a d d 2(见程序1 4 - 8)中,使用了UNIX I/O:read和w r i t e。如果使用标准I / O改写该协同进程,它将不能正常工作。问题出在系统默认的标准I / O缓存机制上。当程序1 4 - 1 0被调用时,对标准输入的第一个f g e t s引起标准I / O库分配一个缓存,并选择缓存的类型。因为标准输入是个管道,所以i s a t t y为假,于是标准I / O库由系统默认是全缓存的。对标准输出也有同样的处理。当a d d 2从其标准输入读取而发生堵塞时,程序1 4 - 9从管道读时也发生堵塞,于是产生了死锁。
对将要执行的这样一个协同进程可以加以控制。在程序1 4 - 1 0中的w h i l e循环之前加上下面4行:
if (setvbuf(stdin, NULL, _IOLBF, 0) != 0)
err_sys("setvbuf error;")
if (setvbuf(stdout, NULL, _IOLBF, 0)!= 0)
err_sys("setvbuf error;")
这使得当有一行可用时,f g e t s即返回,并使得当输出一新行符时, p r i n t f即执行ff l u s h操作。对s e t v b u f进行了这些显式调用,使得程序1 4 - 1 0能正常工作。
- 2. 命名管道FIFO
F I F O有时被称为命名管道。管道只能由相关进程使用,它们共同的祖先进程创建了管道。但是,通过F I F O,不相关的进程也能交换数据。
#include <sys/stat.h> int mkfifo(const char *pathname, mode_t mode); |
Returns: 0 if OK, 1 on error |
一旦已经用m k f i f o创建了一个F I F O,就可用o p e n打开它。确实,一般的文件I / O函数(c l o s e、r e a d、w r i t e、u n l i n k等)都可用于F I F O。
当打开一个F I F O时,非阻塞标志( O _ N O N B L O C K)产生下列影响:
(1) 在一般情况中(没有说明O _ N O N B L O C K),只读打开要阻塞到某个其他进程为写打开此F I F O。类似,为写而打开一个F I F O要阻塞到某个其他进程为读而打开它。
(2) 如果指定了O _ N O N B L O C K,则只读打开立即返回。但是,如果没有进程已经为读而打开一个F I F O,那么只写打开将出错返回,其e r r n o是E N X I O。
类似于管道,若写一个尚无进程为读而打开的F I F O,则产生信号S I G P I P E。若某个F I F O的最后一个写进程关闭了该F I F O,则将为该F I F O的读进程产生一个文件结束标志。一个给定的F I F O有多个写进程是常见的。这就意味着如果不希望多个进程所写的数据互相穿插,则需考虑原子写操作。正如对于管道一样,常数P I P E _ B U F说明了可被原子写到F I F O的最大数据量。
F I F O有两种用途:
(1) FIFO由s h e l l命令使用以便将数据从一条管道线传送到另一条,为此无需创建中间临时文件。
(2) FIFO用于客户机-服务器应用程序中,以在客户机和服务器之间传递数据。
- 3. SYS-V IPC
三种系统V IPC:消息队列、信号量以及共享存储器之间有很多相似之处。以下各节将说明这些I P C的各自特殊功能,本节先介绍它们类似的特征。
三个g e t函数(m s g g e t、s e m g e t和s h m g e t)都有两个类似的参数k e y和一个整型的f l a g。如若满足下列条件,则创建一个新的I P C结构(通常由服务器创建):
(1) k e y是I P C _ P R I VAT E,或
(2) k e y当前未与特定类型的I P C结构相结合,f l a g中指定了I P C _ C R E AT位。为访问现存的队列(通常由客户机进行),k e y必须等于创建该队列时所指定的关键字,并且不应指定I P C _ C R E AT。
经常使用ftok创建一个通用的key:
#include <sys/ipc.h> key_t ftok(const char *path, int id); |
Returns: key if OK, (key_t)-1 on error |
The path argument must refer to an existing file. Only the lower 8 bits of id are used when generating the key.
一. 消息队列
每个消息队列都对应一个控制结构。
struct msqid_ds {
struct ipc_perm msg_perm; /* see Section 15.6.2 */
msgqnum_t msg_qnum; /* # of messages on queue */
msglen_t msg_qbytes; /* max # of bytes on queue */
pid_t msg_lspid; /* pid of last msgsnd() */
pid_t msg_lrpid; /* pid of last msgrcv() */
time_t msg_stime; /* last-msgsnd() time */
time_t msg_rtime; /* last-msgrcv() time */
time_t msg_ctime; /* last-change time */
.
.
.
};
#include <sys/msg.h> int msgget(key_t key, int flag); |
Returns: message queue ID if OK, 1 on error |
1 4 . 6节说明了将k e y变换成一个标识符的规则,并且讨论是否创建一个新队列或访问一个现存队列。当创建一个新队列时,初始化m s q i d - d s结构的下列成员:
• ipc-perm结构按1 4 . 6 . 2节中所述进行初始化。该结构中m o d e按f l a g中的相应许可权位设置。这些许可权用表1 4 - 2中的常数指定。
• msg_qnum,msg_lspid、m s g _ l r p i d、m s g _ s t i m e和m s g _ r t i m e都设置为0。
• msg_ctime设置为当前时间。
• msg_qbytes设置为系统限制值。
若执行成功,则返回非负队列I D。此后,此值就可被用于其他三个消息队列函数。
名字说明典型值
M S G M A X 可发送的最长消息的字节长度2 0 4 8
M S G M N B 特定队列的最大字节长度(亦即队列中所有消息之和) 4 0 9 6
M S G M N I 系统中最大消息队列数5 0
M S G T O L 系统中最大消息数5 0
m s g c t l函数对队列执行多种操作。它以及另外两个与信号量和共享存储有关的函数( s e m c t l
和s h m c t l )是系统V IPC的类似于i o c t l的函数(亦即垃圾桶函数)。
#include <sys/msg.h> int msgctl(int msqid, int cmd, struct msqid_ds *buf ); |
Returns: 0 if OK, 1 on error |
c m d参数指定对于由m s q i d规定的队列要执行的命令:
• IPC_STAT 取此队列的m s q i d _ d s结构,并将其存放在b u f指向的结构中。
• IPC_SET 按由b u f指向的结构中的值,设置与此队列相关的结构中的下列四个字段:
m s g _ p e r m . u i d、m s g _ p e r m . g i d、m s g _ p e r m ; m o d e和m s g _ q b y t e s。此命令只能由下列两种进程执行:一种是其有效用户I D等于m s g _ p e r m . c u i d或m s g _ p e r m . u i d ;另一种是具有超级用户特权的进程。只有超级用户才能增加m s g _ q b y t e s的值
• IPC_RMID 从系统中删除该消息队列以及仍在该队列上的所有数据。这种删除立即生效。仍在使用这一消息队列的其他进程在它们下一次试图对此队列进行操作时,将出错返回E I D R M。此命令只能由下列两种进程执行:一种是其有效用户I D等于m s g _ p e r m . c u i d或m s g _ p e r m . u i d ;另一种是具有超级用户特权的进程。
这三条命令(I P C _ S TAT、I P C _ S E T和I P C _ R M I D)也可用于信号量和共享存储。
调用m s g s n d将数据放到消息队列上。
#include <sys/msg.h> int msgsnd(int msqid, const void *ptr, size_t nbytes, int flag); |
Returns: 0 if OK, 1 on error |
m s g r c v从队列中取用消息
#include <sys/msg.h> ssize_t msgrcv(int msqid, void *ptr, size_t nbytes , long type, int flag); |
Returns: size of data portion of message if OK, 1 on error |
如同m s g s n d中一样,p t r参数指向一个长整型数(返回的消息类型存放在其中),跟随其后的是存放实际消息数据的缓存。n b y t e s说明数据缓存的长度。若返回的消息大于n b y t e s,而且在f l a g中设置了M S G _ N O E R R O R,则该消息被截短(在这种情况下,不通知我们消息截短了)。
如果没有设置这一标志,而消息又太长,则出错返回E 2 B I G(消息仍留在队列中)。
参数t y p e使我们可以指定想要哪一种消息:
• type == 0 返回队列中的第一个消息。
• type > 0 返回队列中消息类型为t y p e的第一个消息。
• type < 0 返回队列中消息类型值小于或等于t y p e绝对值,而且在这种消息中,其类型值又最小的消息。
非0t y p e用于以非先进先出次序读消息。例如,若应用程序对消息赋优先权,那么t y p e就可以是优先权值。如果一个消息队列由多个客户机和一个服务器使用,那么t y p e字段可以用来包含客户机进程I D。
可以指定f l a g值为I P C _ N O WA I T,使操作不阻塞。这使得如果没有所指定类型的消息,则m s g r c v出错返回E N O M S G。如果没有指定I P C _ N O WA I T,则进程阻塞直至(a)有了指定类型的消息,或(b)从系统中删除了此队列(出错返回E I D R M),或(c)捕捉到一个信号并从信号处理程序返回(出错返回E I N T R)。
二. 信号量
内核为每个信号量设置了一个s e m i d _ d s结构。
struct {
unsigned short semval; /* semaphore value, always >= 0 */
pid_t sempid; /* pid for last operation */
unsigned short semncnt; /* # processes awaiting semval>curval */
unsigned short semzcnt; /* # processes awaiting semval==0 */
.
.
.
};
The first function to call is semget to obtain a semaphore ID.
#include <sys/sem.h> int semget(key_t key, int nsems, int flag); |
Returns: semaphore ID if OK, 1 on error |
n s e m s是该集合中的信号量数。如果是创建新集合(一般在服务器中),则必须指定n s e m s。
如果引用一个现存的集合(一个客户机),则将n s e m s指定为0。
s e m c t l函数包含了多种信号量操作。
#include <sys/sem.h> int semctl(int semid, int semnum, int cmd, ... /* union semun arg */); |
Returns: (see following) |
The fourth argument is optional, depending on the command requested, and if present, is of type semun, a union of various command-specific arguments:
union semun {
int val; /* for SETVAL */
struct semid_ds *buf; /* for IPC_STAT and IPC_SET */
unsigned short *array; /* for GETALL and SETALL */
};
Note that the optional argument is the actual union, not a pointer to the union.
c m d参数指定下列十种命令中的一种,使其在s e m i d指定的信号量集合上执行此命令。其中有五条命令是针对一个特定的信号量值的,它们用s e m n u m指定该集合中的一个成员。s e m n u m值在0和n s e m s-1之间(包括0和n s e m s-1)。
• IPC_STAT 对此集合取s e m i d _ d s结构,并存放在由a rg . b u f指向的结构中。
• IPC_SET 按由a rg . b u f指向的结构中的值设置与此集合相关结构中的下列三个字段值:
s e m _ p e r m . u i d , s e m _ p e r m . g i d和s e m _ p e r m . m o d e。此命令只能由下列两种进程执行:一种是其有效用户I D等于s e m _ p e r m . c u i d或s e m _ p e r m . u i d的进程;另一种是具有超级用户特权的进程。
• IPC_RMID 从系统中删除该信号量集合。这种删除是立即的。仍在使用此信号量的其他进程在它们下次意图对此信号量进行操作时,将出错返回E I D R M。此命令只能由下列两种进程执行:一种是具有效用户I D等于s e m _ p e r m . c u i d或s e m _ p e r m . u i d的进程;另一种是具有超级用户特权的进程。
• GETVAL 返回成员s e m n u m的s e m v a l值。
• SETVAL 设置成员s e m n u m的s e m v a l值。该值由a rg . v a l指定。
• GETPID 返回成员s e m n u m的s e m p i d值。
• GETNCNT 返回成员s e m n u m的s e m n c n t值。
• GETZCNT 返回成员s e m n u m的s e m z c n t值。
• GETALL 取该集合中所有信号量的值,并将它们存放在由a rg . a rr a y指向的数组中。
• SETALL 按a rg . a rr a y指向的数组中的值设置该集合中所有信号量的值。
对于除G E TA L L以外的所有G E T命令,s e m c t l函数都返回相应值。其他命令的返回值为0。
函数s e m o p自动执行信号量集合上的操作数组。
Tips:注意,APUE不提倡使用sysv的设施,相对与消息队列,更推荐基于流的管道,相对信号量,更推荐记录锁。
三. 共享存储
Hello
内核为每个共享存储段设置了一个s h m i d _ d s结构。
struct shmid_ds {
struct ipc_perm shm_perm; /* see Section 15.6.2 */
size_t shm_segsz; /* size of segment in bytes */
pid_t shm_lpid; /* pid of last shmop() */
pid_t shm_cpid; /* pid of creator */
shmatt_t shm_nattch; /* number of current attaches */
time_t shm_atime; /* last-attach time */
time_t shm_dtime; /* last-detach time */
time_t shm_ctime; /* last-change time */
.
.
.
};
调用的第一个函数通常是s h m g e t,它获得一个共享存储标识符。
#include <sys/shm.h> int shmget(key_t key, size_t size, int flag); |
Returns: shared memory ID if OK, 1 on error |
1 4 . 6 . 1节说明了将k e y变换成一个标识符的规则,以及是创建一个新共享存储段或是存访一个现存的共享存储段。当创建一个新段时,初始化s h m i d _ d s结构的下列成员:
• ipc_perm结构按1 4 . 6 . 2节中所述进行初始化。该结构中的m o d e按f l a g中的相应许可权位设置。这些许可权用表1 4 - 2中的常数指定。
• shm_lpid、s h m _ n a t t a c h、s h m _ a t i m e、以及s h m _ d t i m e都设置为0。
• shm_ctime设置为当前时间。
s i z e是该共享存储段的最小值。如果正在创建一个新段(一般在服务器中),则必须指定其s i z e。如果正在存访一个现存的段(一个客户机),则将s i z e指定为0。
s h m c t l函数对共享存储段执行多种操作。
#include <sys/shm.h> int shmctl(int shmid, int cmd, struct shmid_ds *buf); |
Returns: 0 if OK, 1 on error |
c m d参数指定下列5种命令中一种,使其在s h m i d指定的段上执行。
• IPC_STAT 对此段取s h m i d _ d s结构,并存放在由b u f指向的结构中。
• IPC_SET 按b u f指向的结构中的值设置与此段相关结构中的下列三个字段:
s h m _ p e r m . u i d、s h m _ p e r m . g i d以及s h m _ p e r m . m o d e。此命令只能由下列两种进程执行:一种是其有效用户I D等于s h m _ p e r m . c u i d或s h m _ p e r m . u i d的进程;另一种是具有超级用户特权的进程。
• IPC_RMID 从系统中删除该共享存储段。因为每个共享存储段有一个连接计数
(s h m _ n a t t c h在s h m i d _ d s结构中) ,所以除非使用该段的最后一个进程终止或与该段脱接,否则不会实际上删除该存储段。不管此段是否仍在使用,该段标识符立即被删除,所以不能再用s h m a t与该段连接。此命令只能由下列两种进程执行:一种是其有效用户I D等于s h m _ p e r m . c u i d或s h m _ p e r m . u i d的进程;另一种是具有超级用户特权的进程。
• SHM_LOCK 锁住共享存储段。此命令只能由超级用户执行。
• SHM_UNLOCK 解锁共享存储段。此命令只能由超级用户执行。
一旦创建了一个共享存储段,进程就可调用s h m a t将其连接到它的地址空间中。
#include <sys/shm.h> void *shmat(int shmid, const void *addr, int flag); |
Returns: pointer to shared memory segment if OK, 1 on error |
共享存储段连接到调用进程的哪个地址上与a d d r参数以及在f l a g中是否指定S H M _ R N D位有关。
(1) 如果a d d r为0,则此段连接到由内核选择的第一个可用地址上。
(2) 如果a d d r非0,并且没有指定S H M _ R N D,则此段连接到a d d r所指定的地址上。
(3) 如果a d d r非0,并且指定了S H M _ R N D,则此段连接到( a d d r-(a d d r mod SHMLBA))
所表示的地址上。S H M _ R N D命令的意思是:取整。S H M L B A的意思是:低边界地址倍数,它总是2的乘方。该算式是将地址向下取最近1个S H M L B A的倍数。
除非只计划在一种硬件上运行应用程序(这在当今是不大可能的),否则不用指定共享段所连接到的地址。所以一般应指定a d d r为0,以便由内核选择地址。
如果在f l a g中指定了S H M _ R D O N LY位,则以只读方式连接此段。否则以读写方式连接此段。
s h m a t的返回值是该段所连接的实际地址,如果出错则返回- 1。
当对共享存储段的操作已经结束时,则调用s h m d t脱接该段。注意,这并不从系统中删除其标识符以及其数据结构。该标识符仍然存在,直至某个进程(一般是服务器)调用s h m c t l(带命令I P C _ R M I D)特地删除它。
#include <sys/shm.h> int shmdt(void *addr); |
Returns: 0 if OK, 1 on error |
小结
本章详细说明了进程间通信的多种形式;管道、命名管道( F I F O)以及另外三种I P C形式,通常称之为系统V IPC——消息队列、信号量和共享存储。信号量实际上是同步原语而不是I P C,常用于共享资源的同步存取,例如共享存储段。对于管道,说明了p o p e n的实现,说明了协同进程,以及使用标准I / O库缓存机制时可能遇到的问题。
在时间方面,对消息队列与流管道、信号量与记录锁做了比较后,提出了下列建议:学会使用管道和F I F O,因为在大量应用程序中仍可有效地使用这两种基本技术。在新的应用程序中,要尽可能避免使用消息队列以及信号量,而应当考虑流管道和记录锁,因为它们与U N I X内核的其他部分集成得要好得多。共享存储段有其应用场合,而m m a p函数(见1 2 - 9节)则可能在以后的版本中起更大作用。