第十五章 InterProcess Communication , IPC
15.2 管道
管道是UNIX系统IPC的最古老形式,它有以下两种局限性
- 历史上,它们是半双工的
- 只能在具有公共祖先的两个进程之间使用
每当在一个管道中键入一个命令序列,让 shell 执行,shell 都会为 每一条命令单独创建 一个进程,然后用管道将前一个进程的标准输出与后一个进程的标准输入相连
管道通过调用 pipe 函数创建
1 |
|
参数 fd 返回两个文件描述符,fd[1] 的输出是 fd[0] 的输入,它们之间的关系就像下图里描述的这样
管道是用来进行进程间通信的。通常的用法是先调用 pipe ,接着调用 fork ,从而创建从父进程到子进程 的IPC通道。下图显示了这种情况
这一步之后,可以自由选择两个方向的数据流。一是从父进程到子进程的管道,也就是数据从父进程流向子进程
父进程关闭 读端(fd[0]),子进程关闭 写端(fd[1]),注意,这里的 读/写 端是针对管道来说的,向管道写数据的就是写端,从管道读数据的就是读端。
二是从子进程流向父进程的数据流。读写段开闭情况与第一种相反。
15.3 popen 和 pclose 函数
这两个函数实现的操作是:创建一个管道,fork 一个子进程,关闭未使用的管道端,执行一个 shell 命令,然后等待命令终止
1 |
|
popen 先执行 fork ,然后调用 exec 执行 cmdstring ,并且返回一个标准 I/O 文件指针。
如果 type 是 "r" ,文件指针指向 cmdstring 的标准输出
如果 type 是 "w" ,文件指针指向 cmdstring 的标准输入
15.4 协同进程 coprocesses
popen 只提供连接到另一个进程的标准输入或标准输出的一个单向管道,而协同进程则有连接到另一个进程的两个单向管道,一个接到标准输出,一个接到标准输入
15.5 FIFO
FIFO 也被叫做 命名管道 。未命名的管道只能在两个进程之间使用,而且这两个进程还要有一个共同创建的它们的祖先进程。
通过FIFO,不相关 的进程也能交换数据。
FIFO 是一种 文件类型 。 通过 stat 结构的 st_mode 成员可以知道文件是否是 FIFO 类型。可以用 S_ISFIFO 宏对此进行测试。
创建 FIFO 也就像创建文件:
1 |
|
mode 参数可选值与 3.3 节中 open 函数 的 mode 参数可选值一样
新 FIFO 的用户和组的所有权规则与 4.6 节讲的一样
当用 open 打开一个 FIFO 时,非阻塞标志(O_NONBLOCK)会产生以下影响:
- 不指定
O_NONBLOCK,只读open要阻塞到某个其他进程为写而打开这个FIFO为止;只写open要阻塞到某个其他进程为读而打开它为止 - 指定了
O_NONBLOCK,则只读open立即返回。
类似于管道,若 write 一个尚无进程为读而打开的FIFO,则产生信号 SIGPIPE。若某个FIFO的最后一个写进程关闭了该FIFO,则将为该FIFO的读进程产生一个文件结束标志
常量 PIPE_BUF 说明了可以被原子地写到 FIFO 的最大数据量
FIFO 有以下两种用途:
shell 命令使用FIFO 将数据从一条管道传送到另一条时,无需创建中间临时文件
客户-服务器进程应用程序中,FIFO用作汇聚点,在客户进程和服务器进程二者之间传递数据
15.6 XSI IPC
消息队列、信号量和共享存储器
15.6.1 标识符和键
标识符:非负整数,用于在内核中引用 IPC
键:IPC 的外部名,用于使多个进程能够使用同一 IPC 对象
从 键 到 标识符 的映射是由内核完成的
15.6.2 权限结构
XSI IPC 为每一个IPC结构关联了一个 ipc_perm 结构
1 | struct ipc_perm { |
15.7 消息队列
消息队列就是消息的链表,由消息队列标识符(队列ID)标识。
每个队列都有一个 msqid_ds 结构
1 | struct msqid_ds { |
msgget 用于创建新队列或打开一个现有队列
1 |
|
创建新队列时,需要初始化 msqid-ds 结构的下列成员
ipc-perm结构按 15.6.2 节中所述进行初始化msg_qnum,msg_lspid,msg_lrpid,msg_stime,msg_rtime全部设置为 0msg_ctime设置为当前时间msg_qbytes设置为系统限制值
msgctl 函数能对队列进行多种操作
1 |
|
cmd参数指定要对队列执行的命令
IPC_STAT取队列的msqid-ds结构并放到buf里IPC_SET将队列的msg_perm.uid,msg_perm.gid,msg_perm.mode,msg_qbytes字段设置为buf指定的值IPC_RMID从系统中删除该消息队列以及仍在该队列中的所有数据
msgsnd 将队列添加到队列尾端
1 |
|
ptr 是一个指针,它可以指向一个结构,结构内有一个长整型字段标识消息类型,还有真正要发送的消息(长度为 nbytes)。该结构体可以是下面这样的
1 | struct mymesg { |
flag 参数可以是 IPC_NOWAIT ,非阻塞,若消息队列已满则立即返回
msgrcv 从队列中取出消息
1 |
|
ptr 指向一个长整型数,其后跟随的是存储实际数据的缓冲区。nbytes 指定缓冲区长度。
type 值指定想要那种消息
type==0返回队列中的第一个消息type > 0返回队列中类型为type的第一个消息type < 0返回队列中消息类型小于等于type绝对值的消息,如果这个消息有多个,则返回类型值最小的消息
15.8 信号量 semaphore
信号量和 管道、FIFO以及消息队列不同。它是一个计数器,用于为多个进程提供对共享数据对象 的访问
为了获得共享资源,进程需要执行下列操作
- 测试控制该资源的信号量
- 若信号量为正,进程可以使用该资源。这种情况下信号量减1
- 若信号量为 0 ,进程被阻塞直到信号量大于 0,然后从第一步开始执行
内核为每个信号量集合维护着一个 semid_ds 结构
1 | struct semid_ds { |
每个信号量由一个无名结构表示,它至少包含下列成员
1 | struct { |
想使用 XSI 信号量时,首先需要通过调用函数 semget 来获得一个信号量ID
1 |
|
创建一个新集合时需要对 semid_ds 结构的下列成员赋值
- 按15.6.2 节初始化
ipc_perm结构 sem_otime设置为 0sem_ctime设置为当前时间sem_nsems设置为nsems
semctl 执行对信号量的操作
1 |
|
semop 对信号量集合执行一系列操作
1 |
|
15.9 共享存储 shared memory
允许两个或多个进程共享一个给定的存储区(匿名的存储段),最快的一种IPC
内核为每个共享存储段维护着一个结构
1 | struct shmid_ds { |
使用共享存储调用的第一个函数通常是 shmget ,它获得一个共享存储标识符
1 |
|
当创建一个新段时,初始化 shmid_ds 结构的下列成员
ipc_perm结构按 15.6.2 节初始化shm_lpid,shm_nattch,shm_atime,shm_dtime全部初始化为 0shm_ctime设置为当前时间shm_segsz设置为请求的size
参数 size 是请求的存储段长度,以字节为单位。
shmctl 函数对共享存储段进行多种操作
1 |
|
cmd 是下列五个值之一
IPC_STAT取此段的shmid_ds结构并存入bufIPC_SET将此段的shmid_ds结构设置为buf指定的内容IPC_RMID从系统中删除此共享存储段,因为有 计数,所以直到最后一个使用该共享存储段的进程终止或与该段分离,此存储段才会真正被删除
以下操作不是 SUS 要求的,都只能由超级用户执行
SHM_LOCK对共享存储段加锁SHM_UNLOCK对共享存储段解锁
创建了存储段后,进程可以使用 shmat 将其连接到它的地址空间中
1 |
|
addr 指定要连接到的地址,一般把它设置为 0 ,以便由系统自动选择
使用 shmdt 分离共享存储段
1 |
|
15.10 POSIX 信号量
解决了 XSI 信号量接口的几个缺陷:
- 更高性能
- 接口使用简单
- 删除操作更加完美
使用 sem_open 创建一个新的命名信号量或者使用一个现有的信号量
1 |
|
oflag 指定为 O_CREAT 时,mode 需要指定权限(和创建普通文件的权限设置方法相同),value 需要指定信号量的初始值(0~SEM_VALLUE_MAX)
命名信号量时最好遵循的一些规则
- 名字的第一个字符应该为 /
- 名字不应该包含其它的 /
- 名字不应该长于
_POSIX_NAME_MAX
sem_close 释放与信号量相关的资源
1 |
|
sem_unlink 销毁一个命名信号量
1 |
|
sem_wait 和 sem_trywait 函数对信号量 减 1
1 |
|
如果信号量已经是 0 ,则 sem_wait 会阻塞直到成功减 1,sem_trywait 不会阻塞,它会立即返回 -1 ,并将 errno 设置为 EAGAIN
sem_timedwait 阻塞指定的时间
1 |
|
sem_post 使信号量值 加 1
1 |
|
如果仅仅在单个进程中使用POSIX 信号量,那么使用未命名信号量更为容易。这仅仅改变创建和销毁信号量的方式
sem_init 函数创建一个未命名的信号量
1 |
|
pshared 表明是否在多个进程中使用信号量,非零代表是,0 代表不是
value 指定信号量的初始值
sem_destroy 函数销毁未命名信号量
1 |
|
sem_getvalue 获得信号量的值
1 |
|
课后习题
1. 在 此程序 中,在父进程代码的末尾删除 waitpid 前的 close ,结果将如何?
分页程序能够正常读取指定文件,但读取完成后无法退出。因为父进程的写管道没有关闭,导致分页程序阻塞等待标准输入。
2. 在 此程序 中,在父进程代码的末尾删除 waitpid ,结果将如何?
由于父进程不再等待子进程终止,分页程序提前退出了
3. 如果popen 函数的参数是一个不存在的命令,会造成什么结果?
可见,popen 返回一个 非空指针,紧接着 输出 sh: 1: meaninglesscmd: not found
popen 函数本身并不出错,它使用 shell 执行一个并不存在的命令,出错的是 shell