APUE - 进程间通信
管道
管道是 UNIX 系统 IPC 的最古老形式,并且所有 UNIX 系统都提供此种通信机制。管道有下面两种局限性:
- 历史上,它们是半双工的。现在,某些系统提供全双工管道,但是为了最佳的可移植性,我们决不应预先假定系统使用此特性。
- 它们只能在具有公共祖先的进程之间使用。通常,一个管道由一个进程创建,然后该进程调用 fork,此后父、子进程之间就可应用该管道。
管道是由调用 pipe 函数而创建的:
#include <unistd.h>
int pipe(int filedes[2]);
经由参数 filedes 返回两个文件描述符:filedes[0] 为读而打开,filedes[1] 为写而打开。filedes[1] 的输出是 filedes[0] 的输入。
通常,调用 pipe 的进程接着调用 fork,这样就创建了从父进程到子进程(或反向)的 IPC 通道。
调用 fork 之后做什么取决于我们想要有的数据流的方向。对于从父进程到子进程的管道,父进程关闭管道的读端,子进程则关闭写端。为了构造从子进程到父进程的管道,父进程关闭写端,子进程关闭读端。
当管道的一端被关闭后,下列两条规则起作用:
- 当读一个写端已被关闭的管道时,在所有数据都被读取后,read 返回 0,以指示达到了文件结束处。
- 如果写一个读端已被关闭的管道,则产生信号 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 结构上会合:
- 服务器进程可以指定键 IPC_PRIVATE 创建一个新 IPC 结构,将返回的标示符存放在某处以便客户进程取用。键 IPC_PRIVATE 保证服务器进程创建一个新 IPC 结构。
- 在一个公用头文件中定义一个客户进程和服务器进程都认可的键。然后服务器进程指定此键创建一个新的 IPC 结构。
- 客户进程和服务器进程认同一个路径名和项目 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,表示它使用了一个资源单位。
- 若此信号量的值为 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);