Chapter 15 进程间通信
1.管道
管道是UNIX系统IPC的最古老形式,在shell下的表现形式为管道线。每当在管道线中输入一个由shell执行的命令序列时,shell为每一条命令单独创建一进程,然后将前一条命令进程的标准输出用管道与后一条命令的标准输入相连接。管道有两个主要局限:
1).管道是半双工的,即数据只能在一个方向上流动。
2).管道只能在具有公共祖先的进程之间使用。
管道是由调用pipe函数而创建的.
#include <unistd.h> int pipe(int filedes[2]); //成功返回0,错误返回-1。
经由参数filedes返回两个文件描述符:filedes[0]为读而打开,filedes[1]为写而打开。filedes[1]的输出是filedes[0]的输入。
单个进程中的管道几乎没有任何用处。通常,调用pipe的进程接着调用fork,这样就创建了从父进程到子进程或反之的IPC通道。下面显示了这种情况
当管道的一端被关闭后,下列规则起作用:
(1) 当读一个写端已被关闭的管道时,在所有数据都被读取后,read返回0,以指示达到了文件结束处
(2) 如果写一个读端已被关闭的管道,则产生信号SIGPIPE
2.popen和pclose函数
这两个函数实现的操作是:创建一个管道,fork一个子进程,关闭管道的不使用端, exec一个shell以执行命令,等待命令终止
#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 - 5)。如果type是"w",则文件指针连接到cmdstring的标准输入(见图14 - 6)
有一种方法可以帮助我们记住popen最后一个参数及其作用,这种方法就是与fopen进行类比。如果type是"r",则返回的文件指针是可读的,如果type是"w",则是可写的。
pclose函数关闭标准I/O流,等待命令执行结束,然后返回shell的终止状态。
3.FIFO
FIFO有时被称为命名管道。管道只能由相关进程使用,它们共同的祖先进程创建了管道。
创建FIFO类似于创建文件。确实,FIFO的路径名存在于文件系统中。
#include <sys/stat.h> int mkfifo(const char *pathname, mode_t mode); //成功返回0,错误返回-1。
当打开一个FIFO时,非阻塞标志(O_NONBLOCK)产生下列影响:
(1) 在一般情况中(没有说明O_NONBLOCK),只读打开要阻塞到某个其他进程为写打开此FIFO。类似,为写而打开一个FIFO要阻塞到某个其他进程为读而打开它。
(2) 如果指定了O_NONBLOCK,则只读打开立即返回。但是,如果没有进程已经为读而打开一个FIFO,那么只写打开将出错返回,其errno是ENXIO。
类似于管道,若写一个尚无进程为读而打开的FIFO,则产生信号SIGPIPE。若某个FIFO的最后一个写进程关闭了该FIFO,则将为该FIFO的读进程产生一个文件结束标志
4.协同进程
当一个程序产生某个过滤程序的输入,同时又读取该过滤程序的输出时,则该过滤程序就成为协同进程。协同进程通常在shell的后台运行,它有连接到另一个进程的两个单向管道,一个接到其标准输入,另一个则来自其标准输出。实例:
4.XSI IPC
有三种IPC称作XSI IPC,即消息队列、信号量以及共享存储器
1).标示符和键
每个内核中的IPC结构都用一个非负整数的标识符加以引用。标识符是IPC对象的内部名。为使多个合作进程能够在同一IPC对象上会合,需要提供一个外部命名方案。为此使用键与每个IPC对象关联。键的数据类型为key_t,由内核变换成标识符。
ftok提供的唯一服务是从一个路径名和工程ID产生一个关键字的一个方法。
#include <sys/ipc.h> key_t ftok(const char *path, int id); //成功返回关键字,失败返回(key_t)-1。
path参数必须指向一个已有的文件。在产生关键字时只有id的低8位被使用。
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 */ ... };
可以调用msgctl、semctl或shmctl函数修改uid、gid和mode字段。为了改变这些值,调用进程必须是IPC结构的创建者或超级用户。对于任何IPC结构不存在执行权限。
3)优点和缺点
IPC类型 | 无连接的? | 可靠的? | 流控制? | 记录? | 消息类型或优先级? |
---|---|---|---|---|---|
消息队列 | 否 | 是 | 是 | 是 | 是 |
STREAMS | 否 | 是 | 是 | 是 | 是 |
UNIX域流套接字 | 否 | 是 | 是 | 否 | 否 |
UNIX域数据报套接字 | 是 | 是 | 否 | 是 | 否 |
FIFO(非STREAMS) | 否 | 是 | 是 | 否 | 否 |
5.消息队列
消息队列是消息的链接表,存放在内核中并由消息队列标识符标识.。我们将称消息队列为“队列”,其标识符为“队列ID”
1).每个队列都有一个msqidds结构与其相关。此结构规定了队列的当前状态。
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 */ ... };
这个结构体定义了队列的当前状态
2).通常第一个被调用的函数是msgget,其功能是打开一个现存队列或创建一个新队列
#include <sys/msg.h> int msgget (key_t key, int flag); //成功返回消息队列ID。错误返回-1。
3).msgctl函数对队列执行多种操作。它以及另外两个与信号量和共享存储有关的函数(semctl和shmctl)是系统V IPC的类似于ioctl的函数
#include <sys/msg.h> int msgctl (int msqid, int cmd, struct msqid_ds *buf); //成功返回0,错误返回-1。
cmd参数指定对于由msqid规定的队列要执行的命令(详见APUE)
4).调用msgsnd将数据放到消息队列上
#include <sys/msg.h> int msgsnd(int msqid, const void *ptr, size_t nbytes, int flag); //成功返回0,错误返回-1。
5).msgrcv从队列中取用消息
#include <sys/msg.h> ssize_t msgrcv(int msqid, void *ptr, size_t nbytes, long type, int flag); //成功返回消息的数据部分的尺寸,错误返回-1。
6.信号量
信号量与已经介绍过的IPC机构(管道、FIFO以及消息列队)不同。它是一个计数器,用于多进程对共享数据对象的存取。为了获得共享资源,进程需要执行下列操作:
(1) 测试控制该资源的信号量。
(2) 若此信号量的值为正,则进程可以使用该资源。进程将信号量值减1,表示它使用了一个资源单位。
(3) 若此信号量的值为0,则进程进入睡眠状态,直至信号量值大于0。若进程被唤醒后,它返回至(第(1)步)。
不幸的是,XSI的信号量与此相比要复杂得多。三种特性造成了这种并非必要的复杂性(详见APUE)
内核为每个信号量设置了一个semidds结构:
struct semid_ds { struct ipc_perm sem_perm; /* see Section 15.6.2 */ unsigned short sem_nsems; /* # of semaphores in set */ time_t sem_otime; /* last-semop() time */ time_t sem_ctime; /* last-change time */ ... };
每个信号量都表示了一个匿名结构体,包含至少以下成员:
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 */ };
要调用的第一个函数是semget以获得一个信号量ID
#include <sys/sem.h> int semget(key_t key, int nsems, int flag); //成功返回信号量ID。错误返回-1。
semctl函数包含了多种信号量操作。
#include <sys/sem.h> int semctl(int semid, int semnum, int cmd, ... /* union semun arg */); //对于除了GETALL之外的所有GET命令,函数返回对应的值。对于其它的命令,返回值是0(详见APUE)
注意,最后一个参数是个联合(union),而非指向一个联合的指针
union semun { int val; /* for SETVAL */ struct semid_ds *buf; /* for IPC_START and IPC_SET */ unsigned short *array; /* for GETALL and SETALL */ };
函数semop自动执行信号量集合上的操作数组。
#include <sys/sem.h> int semop(int semid, struct sembuf semoparray[], size_t nops); //成功返回0,错误返回-1。
semoparray是一个指针,它指向一个信号量操作数组。
struct sembuf { unsigned short sem_num; /* member # in set (0, 1, ... nsems - 1) */ short sem_op; /* operation (negative, 0, or positive) */ short sem_flg; /* IPC_NOWAIT, SEM_UNDO */ };
7.共享存储
共享存储允许两个或多个进程共享一给定的存储区。因为数据不需要在客户机和服务器之间复制,所以这是最快的一种IPC。使用共享存储的唯一窍门是多个进程之间对一给定存储区的同步存取。
1).内核为每个共享存储段设置了一个shmidds结构。
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 */ ... };
2).调用的第一个函数通常是shmget,它获得一个共享存储标识符。
#include <sys/shm.h> int shmget(key_t key, size_t size, int flag); //成功返回共享内存ID,错误返回-1。
3).shmctl函数对共享存储段执行多种操作
#include <sys/shm.h> int shmctl(int shmid, int cmd, struct shmid_ds *buf); //成功返回0,错误返回-1。
4).一旦创建了一个共享存储段,进程就可调用shmat将其连接到它的地址空间中。
#include <sys/shm.h> void *shmat(int shmid, const void *addr, int flag); //成功返回共享内存段的指针,错误返回-1。
5).当对共享存储段的操作已经结束时,则调用shmdt脱接该段。注意,这并不从系统中删除其标识符以及其数据结构。该标识符仍然存在,直至某个进程(一般是服务器)调用shmctl(带命令IPCRMID)特地删除它。
#include <sys/shm.h> int shmdt (void *addr); //成功返回0,错误返回-1