SunOS 5.10 及兼容的操作系统还提供了 System V 进程间通信 (Interprocess Communication, IPC) 软件包。POSIX IPC 已有效地替代了 System V IPC,但仍然保留了 System V IPC 以支持较早版本的应用程序。
有关 System V IPC 的更多信息,请参见 ipcrm(1)、ipcs(1)、Intro(2)、msgctl(2)、msgget(2)、msgrcv(2)、msgsnd(2)、semget(2)、 semctl(2)、semop(2)、shmget(2)、shmctl(2)、shmop(2) 和 ftok(3C) 手册页。
消息、信号量以及共享内存的属主、组以及其他用户具有读取和写入权限,但是没有执行权限,这一点与普通文件类似。与文件类似的是,创建进程会标识缺省属主。与文件不同的是,创建进程可以将此功能的拥有权指定给其他用户或撤消拥有权指定。
请求访问 IPC 功能的进程必须能够标识此功能。要标识进程请求访问的功能,初始化 IPC 功能或提供 IPC 功能访问权限的接口应使用 key_t key 参数。key 为任意值,或者为运行时能从通用种子派生的值。派生此类密钥的一种方法是使用 ftok(3C),它可将文件名转换为在系统内具有唯一性的密钥值。
初始化消息、信号量或共享内存或者获取消息、信号量或共享内存访问权限的接口将返回类型为 int 的 ID 号。执行读取、写入以及控制操作的 IPC 接口使用此 ID。
如果将密钥参数指定为 IPC_PRIVATE,则此调用会初始化创建进程专有的新 IPC 功能实例。
在适用于此调用的标志参数中提供 IPC_CREAT 标志时,如果此功能不存在,则接口会尝试创建此功能。
当使用 IPC_CREAT 和 IPC_EXCL 标志进行调用时,如果此功能已存在,则接口会失败。当多个进程可能尝试初始化此功能时,此行为会非常有用。这样一个案例可能会涉及多个具有相同功能访问权限的服务器进程。如果它们都尝试在 IPC_EXCL 生效的情况下创建此功能,则只有第一个尝试会成功。
如果没有提供上述任一标志并且此功能已存在,则接口会返回此功能的 ID 以获取访问权限。如果省略了 IPC_CREAT 并且尚未初始化此功能,则调用会失败。
将逻辑(按位)OR、IPC_CREAT 和 IPC_EXCL 与八进制权限模式组合使用,以形成标志参数。例如,以下语句将在队列不存在的情况下初始化一个新消息队列:
msqid = msgget(ftok("/tmp", 'A'), (IPC_CREAT | IPC_EXCL | 0400));
第一个参数基于字符串 ("/tmp") 取值为密钥 ('A)。第二个参数取值为组合的权限和控制标志。
必须先通过 msgget(2) 初始化队列,然后进程才能发送或接收消息。队列的属主或创建者可以使用 msgctl(2) 更改队列的拥有权或权限。任何具有相应权限的进程均可使用 msgctl(2) 执行控制操作。
通过 IPC 消息传送,进程可以发送和接收消息以及对消息进行排队,以便按任意顺序处理。与管道的文件字节流数据流不同,每条 IPC 消息都具有显式长度。
可以为消息指定特定类型。因此通过使用客户机进程 PID 作为消息类型,服务器进程可以将客户机间的消息流量定向到其队列中。对于单消息事务,多个服务器进程可以并行处理发送到共享消息队列的事务。
发送和接收消息的操作分别通过 msgsnd(2) 和 msgrcv(2) 执行。发送消息时,会将消息的文本复制到消息队列。msgsnd(2) 和 msgrcv(2) 可以作为阻塞操作或非阻塞操作执行。被阻塞的消息操作将保持暂停状态,直到出现以下三种情况之一:
调用成功。
进程接收到信号。
队列被删除。
msgget(2) 可初始化新的消息队列,并且还可返回对应于密钥参数的队列的消息队列 ID (msqid)。作为 msgflg 参数传递的值必须为八进制整数,并具有该队列的权限和控制标志的设置。
MSGMNI 内核配置选项确定内核支持的单一消息队列数的最大个数。如果超过此限制,msgget(2) 会失败。
以下代码说明了 msgget(2)。
#include <sys/ipc.h> #include <sys/msg.h> ... key_t key; /* key to be passed to msgget() */ int msgflg, /* msgflg to be passed to msgget() */ msqid; /* return value from msgget() */ ... key = ... msgflg = ... if ((msqid = msgget(key, msgflg)) == -1) { perror("msgget: msgget failed"); exit(1); } else (void) fprintf(stderr, "msgget succeeded"); ...
msgctl(2) 可更改消息队列的权限和其他特性。msqid 参数必须为现有消息队列的 ID。cmd 参数是以下各项之一:
以下代码说明了 msgctl(2) 及其各种标志。
#include <sys/types.h> #include <sys/ipc.h> #include <sys/msg.h> ... if (msgctl(msqid, IPC_STAT, &buf) == -1) { perror("msgctl: msgctl failed"); exit(1); } ... if (msgctl(msqid, IPC_SET, &buf) == –1) { perror("msgctl: msgctl failed"); exit(1); } ...
msgsnd(2) 和 msgrcv(2) 分别发送和接收消息。msqid 参数必须为现有消息队列的 ID。msgp 参数是指向包含消息类型及其文本的结构的指针。msgsz 参数以字节为单位指定消息的长度。msgflg 参数传递各种控制标志。
以下代码说明了 msgsnd(2) 和 msgrcv(2)。
#include <sys/types.h> #include <sys/ipc.h> #include <sys/msg.h> ... int msgflg; /* message flags for the operation */ struct msgbuf *msgp; /* pointer to the message buffer */ size_t msgsz; /* message size */ size_t maxmsgsize; /* maximum message size */ long msgtyp; /* desired message type */ int msqid /* message queue ID to be used */ ... msgp = malloc(sizeof(struct msgbuf) – sizeof (msgp–>mtext) + maxmsgsz); if (msgp == NULL) { (void) fprintf(stderr, "msgop: %s %ld byte messages.\n", "could not allocate message buffer for", maxmsgsz); exit(1); ... msgsz = ... msgflg = ... if (msgsnd(msqid, msgp, msgsz, msgflg) == –1) perror("msgop: msgsnd failed"); ... msgsz = ... msgtyp = first_on_queue; msgflg = ... if (rtrn = msgrcv(msqid, msgp, msgsz, msgtyp, msgflg) == –1) perror("msgop: msgrcv failed"); ...
使用信号量,进程可以查询或更改状态信息。通常使用信号量来监视和控制系统资源(如共享内存段)的可用性。信号量既可以作为单个单元处理,也可以作为集中的元素处理。
由于 System V IPC 信号量可以存在于大型数组中,因此它们的开销非常大,线程库提供了更轻量级的信号量。此外,POSIX 信号量是 System V 信号量的最新实现(请参见POSIX 信号量)。线程库信号量必须与映射内存一起使用(请参见内存管理接口)。
信号量集由一个控制结构以及一个包含各个信号量的数组组成。一个信号量集最多可以包含 25 个元素。必须使用 semget(2) 初始化信号量集。信号量创建者可以使用 semctl(2) 更改信号量的拥有权或权限。任何具有相应权限的进程均可使用 semctl(2) 执行控制操作。
信号量操作通过 semop(2) 执行。此接口采用指向信号量操作结构数组的指针。此数组中的每个结构都包含有关针对信号量所执行的操作的数据。任何具有读取权限的进程均可测试信号量的值是否为零。递增或递减信号量的操作需要写入权限。
当操作失败时,不会更改任何信号量。除非设置了 IPC_NOWAIT 标志,否则进程将阻塞,直到出现以下情况之一:
信号量操作可全部完成,因此调用成功。
进程接收到信号。
信号量集被删除。
一次只有一个进程能更新一个信号量。不同进程同时发出的请求可以按任意顺序执行。当通过 semop(2) 调用提供操作数组时,在针对数组的所有操作均成功完成之前,不会进行更新。
如果以独占方式使用信号量的进程异常终止,并且无法撤消操作或释放信号量,则信号量将在内存中保持锁定在进程使其处于的状态。为了防止出现这种情况,SEM_UNDO 控制标志使 semop(2) 为每个信号量操作分配撤消结构,其中包含将信号量返回到其先前状态的操作。如果进程中止,则系统将应用撤消结构中的操作。这样可防止异常中止的进程使信号量集处于不一致的状态。
如果进程共享受信号量控制的资源的访问权限,则不应在 SEM_UNDO 生效时对信号量执行操作。如果当前控制资源的进程异常终止,则假定资源不一致。其他进程必须能够识别这种情况,以将资源恢复为一致状态。
如果在 SEM_UNDO 生效时执行信号量操作,则在执行反向操作的调用时也必须使 SEM_UNDO 生效。当进程正常运行时,反向操作会使用补充值更新撤消结构。这样可确保将应用于撤消结构的值抵消为零,除非进程异常中止。当撤消结构达到零时,便会被删除。
以不一致的方式使用 SEM_UNDO 可能会导致内存泄漏,因为在重新引导系统之前可能不会释放已分配的撤消结构。
semget(2) 可初始化信号量或获取其访问权限。此调用成功时,会返回信号量 ID (semid)。密钥参数是与信号量 ID 关联的一个值。nsems 参数指定信号量数组中的元素数。如果 nsems 大于现有数组中的元素数,则此调用会失败。如果不知道正确的计数,则为此参数提供 0 可确保此调用会成功。semflg 参数指定初始访问权限和创建控制标志。
SEMMNI 系统配置选项可确定所允许的信号量数组的最大数目。SEMMNS 选项可确定所有信号量集中各个信号量的最大可能数目。由于信号量集之间存在分段,因此可能无法分配所有可用信号量。
以下代码说明了 semget(2)。
#include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h> ... key_t key; /* key to pass to semget() */ int semflg; /* semflg to pass to semget() */ int nsems; /* nsems to pass to semget() */ int semid; /* return value from semget() */ ... key = ... nsems = ... semflg = ... ... if ((semid = semget(key, nsems, semflg)) == –1) { perror("semget: semget failed"); exit(1); } else exit(0); ...
semctl(2) 可更改信号量集的权限和其他特性。进行调用时必须使用有效的信号量 ID。semnum 值按信号量索引选择数组中的信号量。cmd 参数为以下控制标志之一。
返回单个信号量的值。
设置单个信号量的值。在这种情况下,arg 被视为 arg.val,其类型为 int。
返回针对信号量或数组执行上次操作的进程的 PID。
返回等待信号量值增加的进程数。
返回等待特定信号量的值达到零的进程数。
返回信号量集中所有信号量的值。在这种情况下,arg 被视为 arg.array,即指向数组的指针(值为 unsigned short)。
设置信号量集中所有信号量的值。在这种情况下,arg 被视为 arg.array,即指向数组的指针(值为 unsigned short)。
从控制结构中返回信号量集的状态信息,并将其放入 arg.buf(指向类型为 semid_ds 的缓冲区的指针)指向的数据结构中。
设置有效的用户和组标识以及权限。在这种情况下,arg 被视为 arg.buf。
删除指定的信号量集。
进程必须具有属主、创建者或超级用户的有效用户标识,才能执行 IPC_SET 或 IPC_RMID 命令。与其他控制命令相同,执行上述两个命令也需要读写权限。
以下代码说明了 semctl(2)。
#include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h> ... register int i; ... i = semctl(semid, semnum, cmd, arg); if (i == –1) { perror("semctl: semctl failed"); exit(1); ...
semop(2) 可针对信号量集执行操作。semid 参数是先前 semget(2) 调用所返回的信号量 ID。sops 参数是指向结构数组的指针,其中每种结构都包含以下有关信号量操作的信息:
信号量数
要执行的操作
控制标志(如果存在)
sembuf 结构指定信号量操作,如 sys/sem.h 中所定义。nsops 参数指定数组的长度,其最大大小由 SEMOPM 配置选项确定。此选项可确定单个 semop(2) 调用所允许的最大操作数目,并缺省设置为 10。
要执行的操作按如下方式确定:
正整数使信号量值递增相应的值。
负整数使信号量值递减相应的值。尝试将信号量设置为小于零的值会失败或阻塞,具体取决于 IPC_NOWAIT 是否生效。
值为零表示等待信号量值达到零。
可与 semop(2) 一起使用的两个控制标志是 IPC_NOWAIT 和 SEM_UNDO。
可以针对数组中的任何操作设置。如果接口无法执行任何设置了 IPC_NOWAIT 的操作,则使接口返回且不更改任何信号量值。如果接口尝试以大于信号量当前值的数量递减信号量,或者测试非零信号量等于零,则接口会失败。
当进程存在时,允许撤消数组中的单个操作。
以下代码说明了 semop(2)。
#include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h> ... int i; /* work area */ int nsops; /* number of operations to do */ int semid; /* semid of semaphore set */ struct sembuf *sops; /* ptr to operations to perform */ ... if ((i = semop(semid, sops, nsops)) == –1) { perror("semop: semop failed"); } else (void) fprintf(stderr, "semop: returned %d\n", i); ...
在 SunOS 5.10 操作系统中,实现共享内存应用程序的最有效方法是依赖 mmap(2) 以及系统的本机虚拟内存功能。有关更多信息,请参见第 1 章。
SunOS 5.10 平台还支持 System V 共享内存,这是一种效率较低的将物理内存段附加到多个进程的虚拟地址空间的方法。允许针对多个进程使用写入访问权限时,可以使用外部协议或机制(如信号量)防止出现不一致和冲突。
进程使用 shmget(2) 创建共享内存段。还可以使用此调用获取现有共享段的 ID。创建进程将设置段的权限和大小(字节)。
共享内存段的初始属主可以使用 shmctl(2) 将拥有权指定给其他用户,也可以撤消此指定。其他具有适当权限的进程可以使用 shmctl(2) 在共享内存段上执行各种控制功能。
创建共享段之后,便可以使用 shmat(2) 将此段附加到进程地址空间。可以使用 shmdt(2) 拆离此共享段。附加进程必须具有相应的 shmat(2) 权限。附加此段之后,进程便可以对其执行读写操作,如附加操作中请求的权限所允许的那样。共享段可以由同一进程进行多次附加。
共享内存段由具有指向物理内存区域的唯一 ID 的控制结构说明。此段的标识符称为 shmid。可在 sys/shm.h 中找到共享内存段控制结构的结构定义。
shmget(2) 用于获取对共享内存段的访问。当此调用成功时,便会返回共享内存段 ID (shmid)。以下代码说明了 shmget(2)。
#include <sys/types.h> #include <sys/ipc.h> #include <sys/shm.h> ... key_t key; /* key to be passed to shmget() */ int shmflg; /* shmflg to be passed to shmget() */ int shmid; /* return value from shmget() */ size_t size; /* size to be passed to shmget() */ ... key = ... size = ... shmflg) = ... if ((shmid = shmget (key, size, shmflg)) == –1) { perror("shmget: shmget failed"); exit(1); } else { (void) fprintf(stderr, "shmget: shmget returned %d\n", shmid); exit(0); } ...
shmctl(2) 用于更改共享内存段的权限和其他特性。cmd 参数为以下控制命令之一。
锁定内存中的指定共享内存段。进程必须具有超级用户的有效 ID,才能执行此命令。
解除锁定共享内存段。进程必须具有超级用户的有效 ID,才能执行此命令。
返回控制结构中包含的状态信息,并将其放入 buf 指向的缓冲区中。进程必须具有段的读取权限,才能执行此命令。
设置有效的用户和组标识以及访问权限。进程必须具有属主、创建者或超级用户的有效 ID,才能执行此命令。
删除共享内存段。进程必须具有属主、创建者或超级用户的有效 ID,才能执行此命令。
以下代码说明了 shmctl(2)。
#include <sys/types.h> #include <sys/ipc.h> #include <sys/shm.h> ... int cmd; /* command code for shmctl() */ int shmid; /* segment ID */ struct shmid_ds shmid_ds; /* shared memory data structure to hold results */ ... shmid = ... cmd = ... if ((rtrn = shmctl(shmid, cmd, shmid_ds)) == –1) { perror("shmctl: shmctl failed"); exit(1); ...
shmat() 和 shmdt() 用于附加和拆离共享内存段(请参见 shmop(2) 手册页)。shmat(2) 返回指向共享段头的指针。shmdt(2) 可拆离位于 shmaddr 所指示的地址中的共享内存段。以下代码说明了对 shmat(2) 和 shmdt(2) 的调用。
#include <sys/types.h> #include <sys/ipc.h> #include <sys/shm.h> static struct state { /* Internal record of attached segments. */ int shmid; /* shmid of attached segment */ char *shmaddr; /* attach point */ int shmflg; /* flags used on attach */ } ap[MAXnap]; /* State of current attached segments. */ int nap; /* Number of currently attached segments. */ ... char *addr; /* address work variable */ register int i; /* work area */ register struct state *p; /* ptr to current state entry */ ... p = &ap[nap++]; p–>shmid = ... p–>shmaddr = ... p–>shmflg = ... p–>shmaddr = shmat(p->shmid, p->shmaddr, p->shmflg); if(p–>shmaddr == (char *)-1) { perror("shmat failed"); nap–-; } else (void) fprintf(stderr, "shmop: shmat returned %p\n", p–>shmaddr); ... i = shmdt(addr); if(i == –1) { perror("shmdt failed"); } else { (void) fprintf(stderr, "shmop: shmdt returned %d\n", i); for (p = ap, i = nap; i–-; p++) { if (p–>shmaddr == addr) *p = ap[–-nap]; } } ...