15.1 引言
第8章说明的进程控制原语,并且观察了如何调用多个进程。但是这些进程间交互信息的唯一途径就是传送打开的文件,可以经由fork或exec来传送,也可以通过文件系统来传送。
本章讨论经典IPC:管道、FIFO、消息队列、信号量已经共享存储。
下一章讨论使用套接字机制的网络IPC。
15.2 管道
- 历史上,管道是半双工的。现在某些系统提供了全双工的管道,但为了移植,我们不该假设系统支持全双工管道。
- 管道只能在具有公共祖先的两个进程间使用。通常,一个管道由一个进程创建,在进程调用fork之后,这个管道就能在父进程和子进程之间使用了。
(FIFO没有第二种局限性,UNIX域套接字没有这两种局限性。)
尽管有这两种局限,半双工管道仍然是常用的IPC形式。每当在管道中键入一个命令序列,让shell执行时,shell都会为每一个条命令单独创建一个进程,然后用管道将前一条命令进程的标准输出与后一条命令的标准输入相链接。
管道是通过pipe函数创建的。
#include <unistd.h>
int pipe(int fd[2]);
//经由参数fd返回两个文件描述符:fd[0]为读而打开,fd[1]为写而打开。
单个进程中的管道计划没有任何用处。通常,经常进程会先调用pipe,接着调用fork,从而创建从父进程到子进程的IPC通道。
在写管道(或FIFO)时,常量PIPE_BUF规定了内核的管道缓冲区大小。可以通过pathconf或fpathconf函数来确定其值。
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#define MAXLINE 20
int main(void)
{
int n;
int fd[2];
pid_t pid;
char line[MAXLINE];
if(pipe(fd)<0)
printf("pipe error. \n");
if((pid=fork())<0){
printf("fork error. \n");
} else if(pid>0){
close(fd[0]);
printf("current pid is %d. \n",getpid());
write(fd[1],"hello world.\n",12);
} else {
close(fd[1]);
n=read(fd[0],line,MAXLINE);
printf("current pid is %d. \n",getpid());
write(STDOUT_FILENO,line,n);
}
exit(0);
}
15.3 函数popen和pclose
常见的操作是创建一个连接到另一个进程的管道,然后读其输出或向其输入端发送数据,为此,标准IO库提供了两个函数popen和pclose。
这两个函数实现的操作是:创建一个管道,fork一个子进程,关闭未使用的管道端,执行一个shell运行命令,然后等待命令终止。
#include <stdio.h>
FILE *popen(const char *cmdstring,const char *type);
int pclose(FILE *fp);
函数popen**先执行fork**,然后调用exec执行cmdstring,并且返回一个标准IO文件指针。
type参数如果是“r”则返回的文件指针是可读的,若是“w”则返回的文件指针是可写的。(就是说,“r”返回的是子进程的stdout,“w”返回的是子进程的stdin。)
15.4 协同进程
UNIX系统过滤程序从标准输入读取数据,向标准输出写数据。几个过滤程序通常在shell管道中线性连接。
当一个过滤程序即产生某个过滤程序的输入,又读取该过滤程序的输出时,它就变成了协同进程(coprocess)。
协同进程通常在shell的后台运行,其标准输入和标准输出通过管道连接到另一个程序。
popen只提供连接到另一个进程的标准输入或标准输出的一个单向管道,而协同进程则有连接到另一个进程的两个单向管道:一个连接到其标准输入,另一个则来自标准输出。我们想将数据写到其标准输入,经其处理后,再从其标准输出读取数据。
15.5 FIFO
FIFO有时被称为命名管道。未命名管道只能在两个相关的进程之间使用,而且这两个相关的进程还要有一个共同创建了它们的祖先进程。但是,通过FIFO,不相关的进程也能交互数据。
14章中已经提到,FIFO是一种文件类型。通过stat结构的st_mode成员的编码可以知道文件是否是FIFO类型。可以使用S_ISFIFO宏对此进行测试。
创建FIFO类似于创建文件。确实,FIFO的路径名存在于文件系统中。
#include <sys/stat.h>
int mkfifo(const char *path,mode_t mode);
int mkfifoat(int fd,const char *path,mode_t mode);
FIFO有以下两种用途:
- shell命令使用FIFO将数据从一条管道传送到另一条,无需创建中间临时文件。
- 客户端进程-服务器进程应用程序中,FIFO用作汇聚点,在客户进程和服务器进程二者之间传递数据。
15.6 XSI IPC
有3种称为XSI IPC 的IPC :消息队列、信号量以及共享存储器。
15.6.1 标识符和键
- 每个内核中的IPC结构(消息队列、信号量以及共享存储器)都用一个非负整数标识符(identifier)加以引用。例如:要向一个消息队列发送消息或者从一个消息队列取消息,只需要知道其队列标识符。与文件描述符不同,IPC标识符不是小整数。当一个IPC结构被创建然后又被删除,这种结构相关的标识符连续加1,直到达到一个整形数的最大值,然后又回到0。
- 标识符是对象的内部名。为了使多个合作进程能够在同一IPC对象上汇聚,需要提供一个外部命名方案。为此,每个IPC对象都与一个键(key)相关联,将这个键作为该对象的外部名。
- 无论何时创建IPC结构(通过调用msgget、semget或shmget创建),都应该指定一个键。这个键的数据类型是基本数据类型key_t,在头文件sys/types.h中被定义为长整型。这个键由内核变换成标识符。
有多种方法使客户进程和服务器进程在同一IPC上汇聚。
- 服务器进程可以指定键IPC_PRIVATE创建一个新IPC结构,将返回的标识符存放在某处(如一个文件中)以便客户进程取用。键IPC_PRIVATE保证服务器进程创建一个新IPC结构。这种技术的缺点是:两个进程需要通过文件系统来传递IPC标识符。IPC_PRIVATE键也可以用于父子进程,子进程将此标识符通过exec函数做为参数传递给一个新程序。
- 可以在一个公用头文件中定义一个客户进程和服务器进程都认可的键。然后服务器进程指定此键创建一个新的IPC结构。
- 客户进程和服务进程认同一个路径名和项目ID(项目ID为0-255之间的值),然后调用函数ftok将这两个值变换为一个键。然后在方法(2)中使用此键。ftok提供的唯一服务就是由一个路径名和项目ID产生一个键。
#include <sys/ipc.h>
key_t ftok(const char *path,int id);
15.6.2 权限结构
XSI IPC为每一个IPC结构管理了一个ipc_perm结构。该结构规定了权限和所有者,它至少包括下列成员:
struct ipc_perm{
uid_t uid; //owner's effective user id
gid_t gid; //owner's effective group id
uid_t cuid; //creator's effective user id
gid_t cgid; //creator's effective group id
mode_t mode; //access modes
};
在创建IPC结构时,对所有字段都赋初始值。以后可以调用msgctl、semctl后shmctl修改uid、gid和mode字段。
15.6.3 结构限制
所有3种形式的XSI IPC都有内置限制。大多数限制都可以通过重新配置内核来改变。
15.6.4 优点和缺点
APUE pg 451.
15.7 消息队列
消息队列是消息的连接表,存储在内核中,由消息队列标识符标识。
msgget用于创建一个新队列或打开一个现有队列。
msgsnd用于将消息添加到队列尾端。
msgrcv用于从队列中取消息。
每个队列都有一个msgid_ds结构与其关联:
struct msgid_ds{
struct ipc_perm msg_perm; //see section 15.6.2
msgqnum_t msg_qnum; //队列的消息编号
msglen_t msg_qbytes; //队列的最大字节数
pid_t msg_lspid; //最后发送者的pid
pid_t msg_lrpid; //最后接收者的pid
time_t msg_stime; //最后发送的时间
time_t msg_rtime; //最后接收时间
time_t msg_ctime; //最后改变时间
.
.
.
};
此结构定义了队列的当前状态。
msgget函数得到队列ID,此值就可以被其他队列函数所用.
#include <sys/msg.h>
int msgget(key_t key,int falg);
msgctl函数对队列执行多种操作。它和另外两个信号量及共享存储有关函数(semctl和shmctl)都是XSI IPC的类似于ioctl的函数(即垃圾桶函数)。
#include <sys/msg.h>
int msgctl(int msgid,int cmd,struct msgid_ds *buf);
cmd参数对msgid指定的队列执行命令。
msgsnd将数据放到消息队列中。
#include <sys/msg.h>
int msgsnd(int msgid,const void *ptr,size_t nbytes,int flag);
msgrcv从队列中取用消息。
#include <sys/msg.h>
int msgrcv(int msgid,void *ptr,size_t nbytes,long type,int flag);
15.8 信号量
信号量与已经介绍过的IPC结构(管道、FIFO已经消息队列)不同。它是一个计数器,用于为多个进程提供对共享数据对象的访问。
为了获得共享资源,进程需要执行下列操作:
- 测试控制该资源的信号量。
- 若此信号量为正,则进程可以使用该资源。在这种情况下,进程会将信号值减1,表示它使用了一个资源单位。
- 否则,若此信号量的值为0,则进程进入休眠状态,直至信号量值大于0.进程被唤醒后它返回步骤(1)。
内核为每个信号量集合维护着一个semid_ds结构:
struct semid_ds{
struct ipc_perm sem_perm;
unsigned short sem_nsems;
time_t sem_otime;
time_t sem_ctime;
};
当我们想使用XSI信号量时,首先需要通过函数semget来获得一个信号量ID。
#include <sys/sem.h>
int semget(key_t key,int nsems,int flag);
semctl函数包含了多种信号量操作。
#include <sys/sem.h>
int semctl(int semid,int semnum,int cmd,.../*union semum arg*/);
cmd参数拥有10种命令,不多说了;
第四个参数是可以选的,是否使用取决于所请求的命令,如果使用该参数,则其类型是semun,它是多个命令特定参数的联合:
union semun{
int val;
struct semid_ds *buf;
unsigned short *array;
};
semctl同样是类似于ioctl的垃圾桶函数。
semop自动执行信号量集合上的操作数组。
#include <sys/sem.h>
int semop(int semid,struct sembuf semoparray[],size_t nops);
15.9 共享存储
共享存储允许两个或多个进程共享一个给定的存储区。因为数据不需要在客户进程和服务器进程之间复制,所以这是最快的一种IPC。使用共享存储时要掌握的唯一窍门是,在多个进程之间同步访问一个给定的存储区。若服务器进程正在讲数据放入共享存储区,则在它做完这一操作之前,客户进程不应当去取这些数据。通常信号量用于同步共享存储访问。
之前我们已经看到多个进程共享存储的一种形式,就是在多个进程将同一个文件映射到它们的地址空间的时候。XSI共享存储和内存映射的文件不同之处在于,前者没有相关的文件。XSI共享存储段是内存的匿名段。
内核为每个共享存储段维护着一个结构,该结构至少包含以下成员:
struct shmid_ds{
struct ipc_perm shm_perm; //see section 15.6.2
size_t shm_segsz; //存储区大小
pid_t shm_lpid; //最后shmop()进程的pid
pid_t shm_cpid; //最后接收者的pid
shmatt_t shm_nattch;//本次附加号
time_t shm_atime; //最后附加时间
time_t shm_dtime; //最后分离时间
time_t shm_ctime; //最后改变时间
.
.
.
};
调用的第一个函数通常是shmget,它获得一个共享存储标识符。
#include <sys.shm.h>
int shmget(key_t key,size_t size,int flag);
shmctl函数对共享存储段执行多种操作(垃圾桶函数)。
#include <sys.shm.h>
int shmctl(int shmid,int cmd,struct shmid_ds *buf);
一旦创建了一个共享存储段,进程就可以调用shmat将其连接到它的地址空间中。
#include <sys.shm.h>
void *shmat(int shmid,const void *addr,int flag);
参数addr设为0时,由系统选择地址。
shmat返回值是该段所连接的实际地址,如果出错返回-1.
当对共享存储段的操作已经结束时,则调用shmdt与该段分离。注意,这并不从系统中删除其标识符以及其相关的数据结构。该标识符仍然存在,直至某个进程(一般是服务器进程)带IPC_RMID命令的调用shmctl特地删除它为止。
#include <sys.shm.h>
int shmdt(const void *addr);
addr参数是以前调用shmat时的返回值。如果成功,shmdt将使相关shmid_ds结构中的shm_nattch计数器值减1.
共享存储区的布局:
15.10 POSIX 信号量
POSIX信号量有两种形式:命名的和未命名的。它们的差异在于创建和销毁的形式上,但其他工作一样。
- 未命名信号量 只存在于内存中,并要求能使用信号量的进程必须可以访问内存。这意味着它们只能应用在同一进程中的线程,或者不同进程中已经映射相同内存内容到它们的地址空间中的线程。
- 命名信号量 可以通过名字访问,因此可以被任何已知它们名字的进程中的线程使用。
我们可以调用sem_open函数来创建一个新的命名信号量或者使用一个现有信号量。
#include <semaphore.h>
sem_t *sem_open(const char *name,int oflag,.../*mode_t mode,unsigned int value */);
当完成信号量操作时,可以调用sem_close函数来释放任何信号量相关的资源。
#include <semaphore.h>
int sem_close(sem_t *sem);
可以使用sem_unlink函数来销毁一个命名信号量。
#include <semaphore.h>
int sem_unlink(const char *name);
sem_unlink函数删除信号量的名字。如果没有打开信号量的引用,则信号量会被销毁。否则,销毁延迟到最后一个打开的引用关闭。
可以使用sem_wait或sem_trywait函数来实现信号量的减1操作。
#include <semaphore.h>
int sem_trywait(sem_t *sem);
int sem_wait(sem_t *sem);
int sem_timedwait(sem_t *sem,const struct timespec *restrict tsptr);
使用sem_wait函数时,如果信号量计数是0就会发生阻塞。而sem_trywait则不会。sem_timewait则可以指定一段阻塞的时间。
可以使用sem_post函数使信号量值增1。这和解锁一个二进制信号量或者释放一个计数信号量相关资源的过程是类似的。
#include <semaphore.h>
int sem_post(sem_t *sem);
当我们想在单个进程中使用POSIX信号量时,使用未命名信号量更容易。这仅仅需要改变创建和销毁信号量的方式。使用sem_init函数来创建一个未命名信号量。
#include <semaphore.h>
int sem_init(sem_t *sem,int pshared,unsigent int value);
pshared 非0时,表示可以在多个进程中使用。
value参数指定了信号量的初始值。
如果要在两个进程之间使用信号量,需要确保sem参数指向两个进程之间共享的内存范围。
对命名信号量的使用以及完成时,可以用sem_destroy函数丢弃它。
#include <semaphore.h>
int sem_destroy(sem_t *sem);
调用sem_destroy之后不能再使用任何带有sem的信号量函数,除非通过调用sem_init重新初始化它。
sem_getvalue函数可以用来检索信号量值。
#include <semaphore.h>
int sem_getvalue(sem_t *restrict sem,int *restrict valp);
成功之后,valp指向的整数值将包含信号量值。但我们试图使用刚取出来的值的时候,信号量的值可能已经变了。除非使用额外的同步机制来避免这种竞争,否则sem_getvalue函数只能用于调试。
15.11 客户进程-服务器进程属性
15.12 小结
本章详细说明了进程间通信的多种新式:管道、命名管道(FIFO)、称为XSI IPC的3种形式的IPC(消息队列、信号量和共享存储),以及POSIX提供的代替信号量机制。
信号量实际上是同步原语而不是IPC,常用于共享资源(如共享存储段)的同步访问。