第十五章 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
结构并存入buf
IPC_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