"进程间通信IPC"

  "进程间通信"

Posted by Xu on August 28, 2017

进程间通信

1. 管道

管道一般是半双工的,它们只能在拥有公共祖先的进程之间使用。

管道的创建:

int pipe(int fd[2]);
  • 其中fd[0]为读入段,fd[1]为写入端,一般管道用于父子进程间的通信,在创建管道后调用fork建立父子进程之间的管道通信。
  • 若要建立父进程向子进程发送消息,则父进程中关闭fd[1],子进程中关闭fd[0]。
    • 若要子进程向父进程中写入数据则相反。
  • 如果读一个写端被关闭的管道时,在所有数据都被读取后,read返回0,指示达到文件结束处
  • 如果写一个读端关闭的管道,则报SIGPIPE信号,如果忽略该信号或者捕捉该信号并从其处理程序返回,则write返回-1,errno设置为EPIPE.
  • 写管道时,常量PIPE_BUF表示管道缓冲区的大小,当一个写进程要写入的字节数小于PIPE_BUF,则不会与其它写进程穿插进行,若大于PIPE_BUF则可能和其它写进程穿插写入数据

1.1 popen,pclose函数

一般利用管道的过程就是:

  1. 创建一个管道连接到另一个进程
  2. 然后读取其输出或向输入端写入数据

所以提供这两个函数一步完成这些操作

popen

FILE *popen(const char *cmdstring,const char *type) 
  • 这个函数的执行流程:
    • 创建一个管道,调用fork产生一个子进程,关闭管道的不使用端
    • 调用 exec执行一个shell运行命令cmdstring,等待命令的终止,根据类型type返回一个标准I/O文件指针(r返回cmdstring标志输出,w返回cmdstring标准输入),只能获得标准输入(只读)输出(只写)的一端
    • 总的来说就是调用一个子进程执行命令直到结束。

pclose

int pclose(FILE *fp);

pclose函数关闭标准I/O流,等待命令结束返回shell的终止状态

可以利用popen pclose实现一个简单的过滤器,先编写好过滤程序,然后在父进程中调用popen执行过滤程序,父进程中获取该过滤结果。

popen

1.2 协同进程

当几个过滤程序通常在shell管道命令线性的连接。当一个程序产生某个过滤程序的输入,同时又读取该过滤程序的输出时,则该过滤程序就叫做协同进程,需要控制协同进程的读写端。

co-process

  • 死锁问题:
    • 这里要注意一下协同进程与父进程之间的标准I/O读写的死锁
    • 由于父进程需要对协同进程的标准I/O fgets进行读写,与此同时若协同进程采取标准I/O fgets进行读取,由于标准I/O是个管道,默认是 全缓冲的(全缓冲,行缓冲,无缓冲),所以会发生堵塞,协同进程 读取数据发生堵塞的时候,父进程从 管道读取数据也会发生堵塞,从而产生死锁。

deadlock

2.FIFO

  • FIFO有时被称作命名管道,可是管道只能在拥有相同祖先进程的相关进程之中使用,但是FIFO可以使不相关的进程也能交换数据。
  • FIFO是一种文件类型,其中stat结构成员中的st_mode指明文件是否为FIFO类型。

FIFO创建:

int mkfifo(const char *pathname,mode_t mode);

int mkfifoat(int fd,const char *path,mode_t mode);
  • mode与open中的mode参数含义相同。一般的文件操作函数都可以用在FIFO文件上

  • mode其中的非阻塞标志(O_NONBLOCK)没有使用时
    • 调用read_only open函数会阻塞直到有进程为写打开该FIFO文件
    • 调用write_only open函数会阻塞直到有进程为读打开该FIFO文件。
    • 当使用(O_NONBLOCK)标志时,read_only open函数会立即返回,write_only open函数会报错。
  • mkfifoat:和mkfifo函数相似,但是该函数可以被用来在fd文件描述符表示的目录相关的位置创建一个FIFO
    • path
      • 如果指定的是绝对路径名,则fd参数会被忽略掉,并且mkfifoat函数的行为和mkfifo类似
      • 如果指定的是相对路径名,则fd参数是一个打开目录的有效文件描述符,路径名和路径有关
      • 如果指定的是相对路径名,并且fd参数有一个特殊值AT_FDCWD,则路径名以当前目录开始,mkfifoatmkfifo类型 FIFO两种用途:
  • FIFO由shell命令使用以便将数据从一条管道线传输到另一条,无需创建中间临时文件。
  • FIFO用于客户进程-服务器进程应用程序中间传递数据。

3.XSI IPC

三种通信方式我们称作XSI IPC 即消息队列,信号量,及共享存储器

3.1 标示符和键

  • 每个内核IPC结构(消息队列,信号量,共享存储段),都用一个非负整数的标示符加以引用。当一个IPC结构被创建时,然后又被删除时,与这种结构相关的标示符连续加1,直到达到一个整形数的最大值,然后又回转到0
  • 标示符是IPC内部名,对外部进程的引用需要使用另一个对象“键”(IPC对象的外部名),每个IPC对象都有一个“键“与之相关联。
  • 键在内核转换成对应标示符。

满足如下两个条件,创建一个新的IPC结构:

  • key为IPC_PRIVATE;
  • key当前未与特定类型的IPC结构结合,且在flag位指定IPC_CREATE位

key的产生可以由函数ftok函数生成:

key_t ftok(const char *path,int id);//文件路径和项目id(0-255)

使用键来让外部进程在指定IPC会和的三种方式:

  • 1.服务器进程使用IPC_PRIVATE键创建IPC返回标示符供客户端进程使用,这种技术的缺点是:服务器进程要将整型标示符写到文件中,此后客户进程又要读文件取得该标示符
    • 该方法也可以用于父子进程关系,父进程指定IPC_PRIVATE 创建一个新的IPC结构所返回的标示符在调用fork后由子进程使用,子进程将该标示符作为exec参数传给一个新的程序。
  • 2.服务进程与客户端进程共用的头文件中定义一个均认可的键,服务器指定该键创建IPC结构
  • 3.服务进程与客户端进程均认同的文件路径和项目id调用ftok产生键值,再使用方法2

3.2 权限结构

struct ipc_perm{
uid_t uid;
gid_t gid;
uid_t cuid;
gid_t cgid;
mode_t mode;//访问权限

}

访问权限一共有6种:

ipc_mode

3.3 优点和缺点

缺点一:IPC结构没有访问计数,如果进程创建了一个消息队列,并放入几条消息,然后终止,但是该消息队列及其内容并不会自动被删除,相对于管道和FIFO,最后一个引用它们的进程终止后,其内部的数据也会被删除。

缺点二:IPC结构在文件系统中没有名称,为了访问操作这些对象,我们需要使用额外特定的系统调用如ipcs(ls),ipcrm(rm)。

缺点三:IPC不使用文件描述符,所以不能对它们使用多路转接I/O函数:select/poll

优点:可靠,流是受控的,面向纪录,可以用非先进先出的方式处理。

3.4 消息队列

  • msgget:该函数可以创建也可以打开一个已存的队列,每个队列都有一个队列ID
  • msgsnd:用于将新消息添加到队列尾端,每个消息包含一个正长整型字段,一个非负长度及实际数据字节。
  • msgrcv:从队列中取消息,我们不一定以先进先出次序取消息,也可以按消息的类型字段取消息。
int msgget(key_t key,int flag);//键值和标示位

int msgctl(int msqid ,int cmd,struct msqid_ds *buf);//根据cmd来对msgid队列进行相应操作

int msgsnd(int msqid,const void *ptr,size_t nbytes,int flag);//向msgid指定的队列发送ptr指向的数据结构mymesg

int ssize_t msgrcv(int msqid,void *ptr,size_t nbytes,long type,int flag);//根据type(0,>0,<0)进行从msqid队列取相应的数据存放到 ptr指向的缓冲区

每个队列都有一个msgid_ds结构与其关联:

struct msqid_ds{

    struct ipc_perm msg_perm;
    msgqnum_t msg_qnum;//队列中的消息数量
    msglen_t msg_qbytes;//队列中最大字节数量
    pid_t msg_lspid;//上一次msgsnd的线程id
    pid_t msg_lrpid;//上一次msgrcv的线程id
    time_t msg_stime;//上一次msgsnd的时间
    time_t msg_rtimr;//上一次msgrcv的时间
    time_t msg_ctime;//上一次改变的时间
    ...
}

该结构定义了队列的当前状态,下图中给出影响消息队列的系统限制:

queue_limit

1) msgget:创建或打开消息队列

int msgget(key_t key,int flag);//键值和标示位
  • msgget:创建一个新队列或打开一个现有队列
    • key_t:创建IPC结构时需要指定一个键,作为IPC对象的外部名。键由内核转变成标识符
    • 返回值:若成功,返回非负队列ID(标识符),该值可被用于其余几个消息队列函数

创建队列时,需要初始化msqid_ds结构的下列成员:

  • ipc_perm:按XSI IPC中的描述初始化
  • msg_qnummsg_lspidmsg_lrpidmsg_stimemsg_rtime都设为0
  • msg_ctime设置为当前时间
  • msg_qbytes设置为系统限制值

2) msgctl操作消息队列

int msgctl(int msqid ,int cmd,struct msqid_ds *buf);//根据cmd来对msgid队列进行相应操作
  • msqid:队列ID(标识符),msgget的返回值
  • cmd
    • IPC_STAT获取此队列的msgid_qs结构,并存放在buf指向的结构中
    • IPC_SET:将字段msg_perm.uid、msg_perm.gid、msg_perm.mode和msg_qbytes从Buf指向的结构赋值设置到这个队列的msqid_ds结构中,此命令只能由下列2种进程执行:
      • 1)其有效ID等于msg_perm.cuid或msg_perm.uid
      • 2)具有超级用户特权的进程;只有超级用户才能增加msg_qbytes的值)
    • IPC_RMID:从系统中删除消息队列以及仍在队列中的所有数据。这种删除立即生效。
      • 仍在使用这一消息队列的其它进程在他们下一次试图对此队列进行操作时,将得到EIDRM错误,此命令只能由下列2种进程执行:
      • 1)其有效ID等于msg_perm.cuid或msg_perm.uid;
      • 2)具有超级用户特权的进程)

上面3条命令IPC_STAT,IPC_SET,IPC_RMID也可用于信号量共享存储

3) msgsnd添加消息


int msgsnd(int msqid,const void *ptr,size_t nbytes,int flag);//向msgid指定的队列发送ptr指向的数据结构mymesg

每个消息由3部分组成:一个正的长整型类型的字段、一个非负的长度、实际数据字节(对应于长度)

  • ptr:指向一个长整型数,它包含了正的整型消息类型,其后紧接着消息数据(若nbytes为0则无消息数据)
      struct mymesg{
          long mtype;         /* 正的长整型类型字段 */
          char mtext[512];    /*  */
      };
    

    因此,ptr可以是一个指向mymesg结构的指针

  • nbytes:消息数据的长度
  • flag
    • ICP_NOWAIT:类似于文件I/O的非阻塞I/O标准
      • 若消息队列已满(数量或字节总数达到限制)
        • 若指定ICP_NOWAIT,函数立即出错返回EAGAIN
        • 若没指定ICP_NOWAIT,进程会阻塞到:
          • 1)有空间可用;
          • 2)从系统中删除了此队列(会返回EIDRM错误);
          • 3)捕捉到一个信号,并从信号处理程序返回(会返回EINTR错误)

msgsnd返回成功时,消息队列相关的msqid_ds结构会随之更新

4) msgrcv接受消息

int ssize_t msgrcv(int msqid,void *ptr,size_t nbytes,long type,int flag);//根据type(0,>0,<0)进行从msqid队列取相应的数据存放到 ptr指向的缓冲区
  • msgrcv:从队列中取消息(并不一定要以先进先出的顺序取消息,也可以按类型字段取消息)
    • ptr:与msgsnd中一样
    • nbytes:指定数据缓冲区的长度
      • 若返回的长度大于nbyte
        • flag中设置了MSG_NOERROR,则消息被截断,但是不会有通知
        • 如果没有设置MSG_NOERROR,则出错返回E2BIG(消息仍留在队列中)
    • type:欲获取的消息类型
      • 0:返回队列中的第一个消息
      • >0:返回队列中消息类型为type的第一个消息
      • <0:返回队列中消息类型小于等于type绝对值的消息,如果有若干个,则取类型值最小的消息
    • flag
      • IPC_NOWAIT:可使操作不阻塞
        • 当队列中无消息时
          • 若指定了该标志,函数会返回-1,error设置为ENOMSG
          • 若没有指定该标志,函数会一直阻塞直到:
            • 1)有了指定类型的消息可用;
            • 2)从系统中删除了此队列(会导致函数返回-1,error设置为EIDRM);
            • 3)捕捉到一个信号并从信号处理程序返回(会导致函数返回-1,error设置为EINTR
  • type值非零可以用于以非先进先出次序读消息
  • 若应用程序对消息赋予优先权,那么type就可以是优先权值,相当于优先队列
  • 如果一个消息队列由多个客户进程和一个服务器进程使用,那么type字段可以用来包含客户进程的进程ID

msgrcv成功执行时,内核会更新与该消息队列相关的msgid_ds结构

3.5 信号量

信号量是一个计数器,用于多进程对共享数据的访问。(P.V操作)

为了获取共享资源。进程需要执行下列操作: (1)测试控制该资源的信号量 (2)若此信号量为正,进程可以使用该资源,进程将信号量值减1,表示它使用了一个资源单位 (3)若此信号量的值为0,则进程进入休眠状态,直至信号量值大于0,被唤醒后,返回至第一步

当进程不再使用一个信号量控制的共享资源时,信号量的值加1,如果有进程正在休眠等待此信号量,则唤醒它们。

下面的特性使得XSI信号量更复杂:

  • 信号量并非是单个非负值,而必须定义为含有一个或多个信号量值的集合。当创建信号量时,要指定集合中信号量值的数量
  • 信号量的创建是独立于它的初始化的。这是一个致命缺点。因此不能原子地创建一个信号量集合,并且对该集合中的各个信号量赋初值
  • 即使没有进程正在使用各种形式的XSI IPC,他们仍然是存在的。有的程序在终止时并没有释放已经分配给它的信号量,我们不得不为这种程序担心

内核为每个信号量集合维护着一个semid_ds结构:

struct semid_ds{
    struct ipc_perm sem_perm;   
    unsigned short  sem_nsems;  /* 集合中的信号量数目 */
    time_t          sem_otime;  /* 最后一次调用semop()的时间 */
    time_t          sem_ctime;  /* 最后一次改变的时间 */
    ...
};

每个信号量由一个无名结构表示,它至少包含下列成员:

struct{
    unsigned short  semval;     /* 信号量的值,总是>=0 */
    pid_t           sempid;     /* 最后一个操作信号量的进程ID */
    unsigned short  semncnt;    /* 等待 semval>curval 的进程数 */
    unsigned short  semzcnt;    /* 等待 semval==0 的进程数 */
    ...
};

下图是影响信号量集合的系统限制:

signal_limits.png

3.6 获取信号量

int semget(key_t key,int nsems,int flag);//键值,集合中信号量数,flag同上
  • key:创建IPC结构时需要指定一个键,作为IPC对象的外部名。键由内核转变成标识符
  • nsems:该信号量集合中的信号量数
    • 如果是创建新集合(一般在服务器进程中),则必须指定nsems
    • 如果是引用现有集合(一个客户进程),则将nsems指定为0
  • flag

创建队列时,需要初始化semid_ds结构的下列成员:

  • ipc_perm结构按XSI IPC中的描述初始化。结构中的mode成员被设置为flag中的相应权限位
  • sem_otime设置为0
  • sem_ctime设置为当前时间
  • sem_nsems设置为nsems

3.7 操作信号量

int semctr(int semid,int semnum,int cmd...)//根据cmd对semid信号集合中semnum指定的信号进行对应操作
  • 参数
    • semid:信号量集合
    • semnum:信号量集合中的某一信号量
    • cmd:命令
      • IPC_STAT:获取信号量集合的semid_ds结构,存储在arg.buf指向的结构中
      • IPC_SET:按arg.buf指向的结构中的值设置集合semid_ds结构中的sem_perm.uid、sem_perm.gid和sem_perm.mode字段(此命令只能由下列2种进程执行:1)其有效ID等于sem_perm.cuid或sem_perm.uid;2)具有超级用户特权的进程;)
      • IPC_RMID:从系统中删除该信号量集合。这种删除是立即发生的。删除时仍在使用这一信号量集合的其它进程在他们下一次试图对此信号量集合进行操作时,将得到EIDRM错误(此命令只能由下列2种进程执行:1)其有效ID等于sem_perm.cuid或sem_perm.uid;2)具有超级用户特权的进程;)
      • GETVAL:返回semnum指定信号量的值
      • SETVAL:设置semnum指定信号量的值
      • GETPID:返回semnum指定信号量的sempid(最后一个操作信号量的进程ID)
      • GETNCNT:返回semnum指定信号量的semncnt
      • GETZCNT:返回semnum指定信号量的semzcnt
      • GETALL:取该集合中所有的信号量值。这些值存储在arg.array指向的数组中
      • SETALL:将该集合中所有的信号量值设置成arg.array指向的数组中的值
    • semun:可选参数,是否使用取决于命令cmd,如果使用则类型是联合结构semun
        union semun{
            int             val;    /* for SETVAL */
            struct semid_ds *buf;   /* for ICP_STAT and IPC_SET */
            unsigned short  *array; /* for GETALL and SETALL */
        };
      
  • 返回值:对于除GETALL以外的所有GET命令,函数都返回相应值。对于其他命令,若成功则返回值为0,若出错,则设置errno并返回-1
int semop(int semid,struct sembuf semoparray[],size_t nops)//对semid信号队列进行sembuf操作数组中的操作,nops指定数组中操作的数量
  • 指定信号量集合
  • semoparray:一个指针,指向一个由sembuf结构表示的信号量操作数组
      struct sembuf{
          unsigned short  sem_num;    /* 信号量集合中的某个信号量 */
          short           sem_op;     /* 操作,其实就是想要获取的资源个数 */
          short           sem_flg;    /* IPC_NOWAIT,SEM_UNDO */
      };
    
    • sem_op为正值:这对应于进程释放的占用的资源数。sem_op值会加到该信号量的值上
    • sem_op为负值:则表示要获取由该信号量控制的资源
      • 如果信号量的值大于等于sem_op的绝对值,则从信号值中减去sem_op的绝对值
      • 如果信号量的值小于sem_op的绝对值
        • 若指定了IPC_NOWAIT,则出错返回EAGAIN
        • 若未指定IPC_NOWAIT,则该信号量的semncnt增加1,然后调用进程被挂起直到下列事件之一发生
          • 该信号量的值变成大于等于sem_op的绝对值。此信号量的semncnt值减1,并且从信号量值中减去sem_op的绝对值
          • 从系统中删除了此信号量。在这种情况下,函数出错返回EIDRM
          • 进程捕捉到一个信号,并从信号处理程序返回,在这种情况下,此信号量的semncnt值减1,并且函数出错返回EINTR
    • sem_op为0:则表示调用进程希望等待到信号量的值变为0
      • 如果信号量的值是0,则表示函数立即返回
      • 如果信号量的值非0,则:
        • 若指定了IPC_NOWAIT,则semop出错返回EAGAIN
        • 若未指定IPC_NOWAIT,则该信号量的semncnt增加1,然后调用进程被挂起直到下列事件之一发生
          • 该信号量值变为0,此信号量的semncnt值减1
          • 从系统中删除了此信号量。在这种情况下,函数出错返回EIDRM
          • 进程捕捉到一个信号,并从信号处理程序返回,在这种情况下,此信号量的semncnt值减1,并且函数出错返回EINTR
  • nops:数组的数量,即操作的数量

struct sembuf{//信号操作结构体 unsigned short sem_num;//信号量集合中指定信号 short sem_op;//对信号的操作 short sem_flag;//操作标示位 }

信号量比记录锁耗时少,但是记录锁只需要锁一个资源,操作更简单,信号则可以提供更多其它的功能。

3.8 共享存储

共享存储允许2个或多个进程共享一个给定的存储区

因为数据不需要再客户进程和服务器进程之间复制,所以这是最快的一种IPC

使用共享存储要注意的是:进程在往共享存储写完成之前,读进程不应该去取数据。通常,信号量用于同步共享存储访问

mmap就是共享存储的一种形式,但是XSI共享存储与其区别在于,XSI共享存储没有相关文件。XSI共享存储段是内存的匿名段

1) 共享存储的内核结构

内核为每个共享存储段维护着一个结构,至少包含以下成员:

struct shmid_ds{
    struct ipc_perm     shm_perm;   
    size_t              shm_segsz;  /* 共享存储段的字节大小 */
    pid_t               shm_lpid;   /* 最后调用shmop()的进程ID */
    pid_t               shm_cpid;   /* 创建该共享存储段的进程ID */
    shmatt_t            shm_nattch; /* 当前访问计数 */
    time_t              shm_atime;  /* 最后一次attach的时间 */
    time_t              shm_dtime;  /* 最后一次detach的时间 */
    time_t              shm_ctime;  /* 最后一次change的时间 */
    ...
};

下图为影响共享存储的系统限制:

shmem_limits.png

2) 创建或获得共享存储

shmem_create.png

  • key:创建IPC结构时需要指定一个键,作为IPC对象的外部名。键由内核转变成标识符
  • size:共享存储段的长度,单位是字节。实现通常将其向上取为系统页长的整倍数。但是,如果指定的值不是系统页长的整倍数,那么最后一页的余下部分是不可使用的
    • 如果正在创建一个新段,则必须指定size(段内的内容初始化为0)
    • 如果正在引用一个现存的段,则将size指定为0

创建一个新共享存储段时,初始化shmid_ds结构的下列成员:

  • ipc_perm结构按XSI IPC中的描述初始化。结构中的mode成员被设置为flag中的相应权限位
  • shm_lpidshm_nattachshm_atimeshm_dtime都设置为0
  • shm_ctime设置为当前时间
  • sem_segsz设置为size

3) 操作共享存储

shmem_op.png

  • shmid:共享存储标识符,由函数shmget得到
  • cmd
    • IPC_STAT:获取段对应的shmid_ds结构,并将其存储在由buf指向的结构中
    • IPC_SET:按buf指向的结构中的值设置此共享存储段相关的shmid_ds结构中的下列3个字段:shm_perm.uid、shm_perm.gid和shm_perm.mode字段(此命令只能由下列2种进程执行:1)其有效ID等于shm_perm.cuid或shm_perm.uid;2)具有超级用户特权的进程;)
    • IPC_RMID:从系统中删除该共享存储段。因为每个共享存储段维护着一个连接计数(shmid_ds中的shm_nattach字段),所以除非使用该段的最后一个进程终止或与该段分离,否则不会实际上删除该存储段。不管此段是否仍在使用,该段标识符都会被立即删除,所以不能再用shmat与该段连接(此命令只能由下列2种进程执行:1)其有效ID等于shm_perm.cuid或shm_perm.uid;2)具有超级用户特权的进程;) Linux和Solaris提供了另外两种命令,但它们并非Single UNIX Specification的组成部分

    • SHM_LOCK:在内存中对共享存储段加锁(该命令只能由超级用户执行)
    • SHM_UNLOCK:解锁共享存储段(该命令只能由超级用户执行)

4) 与共享存储段连接

可以调用shmat将共享存储段连接到进程的地址空间中

shmem_at.png

  • shmid:共享存储段的标识符
  • addr:共享存储段连接到进程的该地址
    • 0:由内核选择(推荐的方式)
    • 非0
      • flag指定了SHM_RND,则连接到addr所指的地址上
      • flag没指定SHM_RND,则此段连接到 addr-(addr mod SHMLAB) 所表示的地址上(SHM_RND意思是”取整“,SHMLBA的意思是”低边界地址倍数“)
  • flag
    • SHM_RDONLY:以只读方式连接此段
    • 否则:以读写方式连接此段

如果函数成功,内核会将与该共享存储段相关的shmid_ds结构中的shm_nattch计数器值加1

5) 与共享存储段分离

下列函数可以与共享存储段分离。该调用并不从系统中删除其标识符以及其相关的数据结构。该标识符仍然存在,直到某个进程调用shmctl并使用IPC_RMID命令特地删除它为止

shmem_dt.png

  • addr:进程与共享存储段连接的地址

如果函数成功,共享存储段相关的shmid_ds结构中的shm_nattch计数器值减1

6) 进程连接共享存储段的位置

内核将以地址0连接共享存储段放在什么位置上与系统密切相关,下列程序可以进行测试:

#include "apue.h"
#include <sys/shm.h>

#define ARRAY_SIZE  40000
#define MALLOC_SIZE 100000
#define SHM_SIZE    100000
#define SHM_MODE    0600    /* user read/write */

char    array[ARRAY_SIZE];  /* uninitialized data = bss */

int
main(void)
{
    int     shmid;
    char    *ptr, *shmptr;

    printf("array[] from %p to %p\n", (void *)&array[0],
      (void *)&array[ARRAY_SIZE]);
    printf("stack around %p\n", (void *)&shmid);

    if ((ptr = malloc(MALLOC_SIZE)) == NULL)
        err_sys("malloc error");
    printf("malloced from %p to %p\n", (void *)ptr,
      (void *)ptr+MALLOC_SIZE);

    if ((shmid = shmget(IPC_PRIVATE, SHM_SIZE, SHM_MODE)) < 0)
        err_sys("shmget error");
    if ((shmptr = shmat(shmid, 0, 0)) == (void *)-1)
        err_sys("shmat error");
    printf("shared memory attached from %p to %p\n", (void *)shmptr,
      (void *)shmptr+SHM_SIZE);

    if (shmctl(shmid, IPC_RMID, 0) < 0)
        err_sys("shmctl error");

    exit(0);
}