编程接口指南

第 6 章 进程间通信

本章适用于开发多进程应用程序的程序员。

SunOS 5.10 及兼容的操作系统具有多种用于并发进程交换数据以及同步执行的机制。 本章介绍这些机制中除映射内存之外的所有机制。

进程之间的管道

两个进程之间的管道是指在父进程中创建的一对文件。 此管道会在父进程派生时连接生成的进程。 由于管道并不存在于任何文件名称空间中,因此说管道是匿名的。 虽然可通过单个管道将任意数量的子进程相互连接以及连接到其父进程,但是一个管道通常只连接两个进程。

管道是通过调用 pipe(2) 在成为父进程的进程中创建的。 此调用在传递给它的数组中返回两个文件描述符。 派生之后,两个进程将从 p[0] 中读取,向 p[1] 中写入。 实际上,进程将从为它们管理的循环缓冲区中读取以及向其中写入。

由于调用 fork(2) 会复制每个进程的打开文件表,因此每个进程都有两个读取器以及两个写入器。 关闭额外的读取器和写入器即可使管道正常运行。 例如,如果读取器的另一端保持打开状态以便同一进程进行写入,则永远不会返回文件结束指示。 以下代码说明了管道创建、派生以及清除重复的管道端。

#include <stdio.h>

#include <unistd.h>

 ...

 	int p[2];

 ...

 	if (pipe(p) == -1) exit(1);

 	switch( fork() )

 	{

 		case 0:						/* in child */

 			close( p[0] );

 			dup2( p[1], 1);

 			close P[1] );

 			exec( ... );

 			exit(1);

 		default:						/* in parent */

 			close( p[1] );

 			dup2( P[0], 0 );

 			close( p[0] );

 			break;

 	}

 	...

下表显示了在特定情况下,从管道中读取以及向其中写入的结果。

表 6–1 管道中的读/写结果

尝试 

情况 

结果 

读取 

管道为空,已连接写入器 

阻塞读取 

写入 

管道已满,已连接读取器 

阻塞写入 

读取 

管道为空,未连接写入器 

返回 EOF 

写入 

没有读取器 

SIGPIPE

可以通过为描述符调用 fcntl(2) 来设置 FNDELAY,从而避免发生阻塞。 这样会导致从 I/O 调用返回错误 (-1),并将 errno 设置为 EWOULDBLOCK

命名管道

命名管道的运行方式与管道非常相似,但它们是在文件系统中作为命名实体创建的。 这使得所有进程均可打开命名管道,而不要求进程与管道通过派生关联。 命名管道是通过调用 mknod(2) 创建的。 然后,任何具有相应权限的进程均可对命名管道进行读取或写入。

open(2) 调用中,将会阻塞打开管道的进程,直到其他进程也打开此管道为止。

要在不发生阻塞的情况下打开命名管道,open(2) 调用应通过针对其本身使用布尔 or 运算将 O_NDELAY 掩码(位于 sys/fcntl.h 中)与选定的文件模式掩码连接。 如果在调用 open(2) 时没有其他进程连接到管道,则会返回 -1,并将 errno 设置为 EWOULDBLOCK

套接字概述

套接字在两个进程之间提供点对点的双向通信。 套接字是进程间通信以及系统间通信的一个基本组件。 套接字是一个可以绑定名称的通信端点。 它具有一个类型以及一个或多个关联的进程。

套接字存在于通信域中。 套接字域是指提供一种寻址结构以及一组协议的抽象对象。 套接字仅与同一域中的套接字连接。 已确定了二十三个套接字域(请参见 sys/socket.h),其中通常只有 UNIX 域和 Internet 域用于 Solaris 10 及兼容的操作系统。

可以使用套接字在单个系统上的进程之间进行通信,如同其他形式的 IPC。 UNIX 域 (AF_UNIX) 在单个系统上提供一个套接字地址空间。 UNIX 域套接字以 UNIX 路径命名。 UNIX 域套接字将在附录 A,UNIX 域套接字 中进一步介绍。 套接字还可用于在不同系统上的进程之间进行通信。 已连接系统之间的套接字地址空间称为 Internet 域 (AF_INET)。 Internet 域通信使用 TCP/IP Internet 协议套件。 Internet 域套接字将在第 7 章,套接字接口中介绍。

POSIX 进程间通信

POSIX 进程间通信 (interprocess communication, IPC) 是 System V 进程间通信的变体。 它是在 Solaris 7 发行版中引入的。 与 System V 对象类似,POSIX IPC 对象的属主、属主的组以及其他用户具有读取和写入权限,但是没有执行权限。 POSIX IPC 对象的属主无法将对象分配给其他属主。 POSIX IPC 包括以下功能:

与 System V IPC 接口不同,POSIX IPC 接口均为多线程安全接口。

POSIX 消息

下表中列出了 POSIX 消息队列接口。

表 6–2 POSIX 消息队列接口

接口名称 

目的 

mq_open(3RT)

连接到以及创建(可选)命名消息队列 

mq_close(3RT)

结束到开放式消息队列的连接 

mq_unlink(3RT)

结束到开放式消息队列的连接,并在最后一个进程关闭此队列时将其删除 

mq_send(3RT)

将消息放入队列 

mq_receive(3RT)

在队列中接收(删除)最早且优先级最高的消息 

mq_notify(3RT)

通知进程或线程消息已存在于队列中 

mq_setattr(3RT), mq_getattr(3RT)

设置或获取消息队列属性 

POSIX 信号量

POSIX 信号量比 System V 信号量轻得多。 POSIX 信号量结构定义单个信号量,而不是定义最多包含 25 个信号量的数组。

POSIX 信号量接口如下所示。

sem_open(3RT)

连接到以及创建(可选)命名信号量

sem_init(3RT)

初始化信号量结构(在调用程序内部,因此不是命名信号量)

sem_close(3RT)

结束到打开信号量的连接

sem_unlink(3RT)

结束到打开信号量的连接,并在最后一个进程关闭此信号量时将其删除

sem_destroy(3RT)

销毁信号量结构(在调用程序内部,因此不是命名信号量)

sem_getvalue(3RT)

将信号量的值复制到指定整数中

sem_wait(3RT)sem_trywait(3RT)

当其他进程拥有信号量时进行阻塞,或者当其他进程拥有信号量时返回错误

sem_post(3RT)

递增信号量计数

POSIX 共享内存

POSIX 共享内存实际上是映射内存的变体(请参见创建和使用映射)。 二者的主要差异在于:

shm_open(3RT) 中的选项数实际上少于 open(2) 中提供的选项数。

System V IPC

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 功能或提供 IPC 功能访问权限的接口应使用 key_t key 参数。 key 为任意值,或者为运行时能从通用种子派生的值。 派生此类密钥的一种方法是使用 ftok(3C),它可将文件名转换为在系统内具有唯一性的密钥值。

初始化消息、信号量或共享内存或者获取消息、信号量或共享内存访问权限的接口将返回类型为 int 的 ID 号。 执行读取、写入以及控制操作的 IPC 接口使用此 ID。

如果将密钥参数指定为 IPC_PRIVATE,则此调用会初始化创建进程专有的新 IPC 功能实例。

在适用于此调用的标志参数中提供 IPC_CREAT 标志时,如果此功能不存在,则接口会尝试创建此功能。

当使用 IPC_CREATIPC_EXCL 标志进行调用时,如果此功能已存在,则接口便会失败。 当多个进程可能尝试初始化此功能时,此行为会非常有用。 这样一个案例可能会涉及多个具有相同功能访问权限的服务器进程。 如果它们都尝试在 IPC_EXCL 生效的情况下创建此功能,则只有第一个尝试会成功。

如果没有提供上述任一标志并且此功能已存在,则接口会返回此功能的 ID 以获取访问权限。 如果省略了 IPC_CREAT 并且尚未初始化此功能,则调用会失败。

将逻辑(按位)ORIPC_CREATIPC_EXCL 与八进制权限模式组合使用,以形成标志参数。 例如,以下语句将在队列不存在的情况下初始化一个新消息队列:

msqid = msgget(ftok("/tmp", 'A'), (IPC_CREAT | IPC_EXCL | 0400)); 

第一个参数基于字符串 ("/tmp") 取值为密钥 ('A')。 第二个参数取值为组合的权限和控制标志。

System V 消息

必须先通过 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 参数为以下各项之一:

IPC_STAT

将有关队列状态的信息放入 buf 指向的数据结构中。 进程必须具有读取权限,此调用才会成功。

IPC_SET

设置消息队列的属主用户和组 ID、权限以及大小(以字节数为单位)。 进程必须具有属主、创建者或超级用户的有效用户 ID,此调用才会成功。

IPC_RMID

删除 msqid 参数所指定的消息队列。

以下代码说明了 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;

 	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 信号量

使用信号量,进程可以查询或更改状态信息。 通常使用信号量来监视和控制系统资源(如共享内存段)的可用性。 信号量既可以作为单个单元处理,也可以作为集中的元素处理。

由于 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 参数为以下控制标志之一。

GETVAL

返回单个信号量的值。

SETVAL

设置单个信号量的值。 在这种情况下,arg 被视为 arg.val,其类型为 int

GETPID

返回针对信号量或数组执行上次操作的进程的 PID

GETNCNT

返回等待信号量值增加的进程数。

GETZCNT

返回等待特定信号量的值达到零的进程数。

GETALL

返回信号量集中所有信号量的值。 在这种情况下,arg 被视为 arg.array,即指向数组的指针(值的类型为 unsigned short)。

SETALL

设置信号量集中所有信号量的值。 在这种情况下,arg 被视为 arg.array,即指向数组的指针(值为 unsigned short)。

IPC_STAT

从控制结构中返回信号量集的状态信息,并将其放入 arg.buf(指向类型为 semid_ds 的缓冲区的指针)指向的数据结构中。

IPC_SET

设置有效的用户和组标识以及权限。 在这种情况下,arg 被视为 arg.buf

IPC_RMID

删除指定的信号量集。

进程的属主、创建者或超级用户必须具有有效的用户标识,才能执行 IPC_SETIPC_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。

要执行的操作按如下方式确定:

可与 semop(2) 一起使用的两个控制标志是 IPC_NOWAITSEM_UNDO

IPC_NOWAIT

可以针对数组中的任何操作设置。 如果接口无法执行任何设置了 IPC_NOWAIT 的操作,则使接口返回且不更改任何信号量值。 如果接口尝试以大于信号量当前值的数量递减信号量,或者测试非零信号量等于零,则接口会失败。

SEM_UNDO

当进程存在时,允许撤消数组中的单个操作。

以下代码说明了 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);

 ...

System V 共享内存

在 SunOS 5.10 操作系统中,实现共享内存应用程序的最有效方法是依赖 mmap(2) 以及系统的本机虚拟内存功能。有关更多信息,请参见第 1 章,内存和 CPU 管理

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 参数为以下控制命令之一。

SHM_LOCK

锁定内存中的指定共享内存段。 进程必须具有超级用户的有效 ID,才能执行此命令。

SHM_UNLOCK

解除锁定共享内存段。 进程必须具有超级用户的有效 ID,才能执行此命令。

IPC_STAT

返回控制结构中包含的状态信息,并将其放入 buf 指向的缓冲区中。 进程必须具有段的读取权限,才能执行此命令。

IPC_SET

设置有效的用户和组标识以及访问权限。 进程必须具有属主、创建者或超级用户的有效 ID,才能执行此命令。

IPC_RMID

删除共享内存段。 进程必须具有属主、创建者或超级用户的有效 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];

 		}

 	}

 	...