APUE - 进程间通信

管道

管道是 UNIX 系统 IPC 的最古老形式,并且所有 UNIX 系统都提供此种通信机制。管道有下面两种局限性:

  1. 历史上,它们是半双工的。现在,某些系统提供全双工管道,但是为了最佳的可移植性,我们决不应预先假定系统使用此特性。
  2. 它们只能在具有公共祖先的进程之间使用。通常,一个管道由一个进程创建,然后该进程调用 fork,此后父、子进程之间就可应用该管道。

管道是由调用 pipe 函数而创建的:

#include <unistd.h>
int pipe(int filedes[2]);

经由参数 filedes 返回两个文件描述符:filedes[0] 为读而打开,filedes[1] 为写而打开。filedes[1] 的输出是 filedes[0] 的输入。

通常,调用 pipe 的进程接着调用 fork,这样就创建了从父进程到子进程(或反向)的 IPC 通道。

调用 fork 之后做什么取决于我们想要有的数据流的方向。对于从父进程到子进程的管道,父进程关闭管道的读端,子进程则关闭写端。为了构造从子进程到父进程的管道,父进程关闭写端,子进程关闭读端。

当管道的一端被关闭后,下列两条规则起作用:

  1. 当读一个写端已被关闭的管道时,在所有数据都被读取后,read 返回 0,以指示达到了文件结束处。
  2. 如果写一个读端已被关闭的管道,则产生信号 SIGPIPE。如果忽略该信号或者捕捉该信号并从其处理程序返回,则 write 返回 -1,errno 设置为 EPIPE。

popen 和 pclose 函数

常见的操作是创建一个管道连接到另一个进程,然后读其输出或向其输入端发送数据。为此,标准 I/O 库提供了两个函数 popen 和 pclose。

#include <stdio.h>
FILE *popen(const char *cmdstring, const char *type);
int pclose(FILE *fp);

FIFO

FIFO 有时被称为命名管道。管道只能由相关进程使用,通过 FIFO,不相关的进程也能交换数据。

#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);

当打开一个 FIFO 时,非阻塞标志(O_NONBLOCK)产生下列影响:

  • 在一般情况中(没有指定O_NONBLOCK),只读 open 要阻塞到某个其他进程为写而打开此 FIFO。类似地,只写 open 要阻塞到某个其他进程为读而打开它。
  • 如果指定了 O_NONBLOCK,则只读 open 立即返回。但是,如果没有进程已经为读而打开一个 FIFO,那么只写 open 将出错返回 -1,其 errno 是 ENXIO。

XSI IPC

有三种 IPC 我们称作 XSI IPC,即消息队列、信号量以及共享存储器。

标识符和键

每个内核中的 IPC 结构(消息队列、信号量或共享存储段)都用一个非负整数的标识符加以引用。与文件描述符不同,IPC 标识符不是小的整数。当一个 IPC 结构被创建,以后又被删除时,与这种结构相关的标识符连续加 1,直至达到一个整型数的最大正值,然后又回转到 0。

标识符是 IPC 对象的内部名。每个 IPC 对象都与一个键相关联,键就用作为该对象的外部名。

有多种方法使客户进程和服务器进程在同一 IPC 结构上会合:

  1. 服务器进程可以指定键 IPC_PRIVATE 创建一个新 IPC 结构,将返回的标示符存放在某处以便客户进程取用。键 IPC_PRIVATE 保证服务器进程创建一个新 IPC 结构。
  2. 在一个公用头文件中定义一个客户进程和服务器进程都认可的键。然后服务器进程指定此键创建一个新的 IPC 结构。
  3. 客户进程和服务器进程认同一个路径名和项目 ID,接着调用 ftok 函数将这两个值变换为一个键。

ftok 提供的唯一服务就是由一个路径名和项目 ID 产生一个键。

#include <sys/ipc.h>
key_t ftok(const char *path, int id);

权限结构

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 */
    /* ... */
};

消息队列

消息队列是消息的链接表,存放在内核中并由消息队列标识符标识。

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

struct msqid_ds {
    struct ipc_perm msg_perm;
    msgqnum_t       msg_qnum;
    msglen_t        msg_qbytes;
    pid_t           msg_lspid;
    pid_t           msg_lrpid;
    time_t          msg_stime;
    time_t          msg_rtime;
    time_t          msg_ctime;
    /* ... */
};

msgget 函数打开一个现存队列或创建一个新队列。

#include <sys/msg.h>
int msgget(key_t key, int flag);

msgctl 函数对队列执行多种操作。

#include <sys/msg.h>
int msgctl(int msqid, int cmd, struct msgid_ds *buf);

msgsnd 函数将数据放到消息队列中。

#include <sys/msg.h>
int msgsnd(int msqid, const void *ptr, size_t nbytes, int flag);

msgrcv 函数从队列中取用消息。

#include <sys/msg.h>
ssize_t msgrcv(int msqid, void *ptr, size_t nbytes, long type, int flag);

信号量

信号量是一个计数器,用于多进程对共享数据对象的访问。

为了获得共享资源,进程需要执行下列操作:

  1. 测试控制该资源的信号量。
  2. 若此信号量的值为正,则进程可以使用该资源。进程将信号量值减 1,表示它使用了一个资源单位。
  3. 若此信号量的值为 0,则进程进入休眠状态,直至信号量值大于 0。进程被唤醒后,它返回至第 1 步。

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

内核为每个信号量集合设置了一个 semid_ds 结构:

struct semid_ds {
    struct ipc_perm sem_perm;
    unsigned short  sem_nsems;
    time_t          sem_otime;
    time_t          sem_ctime;
    /* ... */
};

每个信号量由一个无名结构表示:

struct {
    unsigned short  semval;
    pid_t           sempid;
    unsigned short  semncnt;
    unsigned short  semzcnt;
    /* ... */
};

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 semun arg*/ );

union semun {
    int              val;   /* for SETVAL */
    struct semid_ds *buf;   /* for IPC_STAT 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);

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 */
};

共享存储

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

内核为每个共享存储段设置了一个 shmid_ds 结构:

struct shmid_ds {
    struct ipc_perm shm_perm;
    size_t          shm_segsz;
    pid_t           shm_lpid;
    pid_t           shm_cpid;
    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);

当对共享存储段的操作已经结束时,则调用 shmdt 函数脱接该段。

#include <sys/shm.h>
int shmdt(void *addr);