第13章 多进程编程
本章主要讨论如下内容:
- 复制进程映像的
fork
系统调用和替换进程映像的exec
系列系统调用 - 僵尸进程以及如何避免僵尸进程
- 进程间通信 ( Inter-Process Communication, IPC ) 最简单的方式:管道
- 3 中 System V 进程间通信方式:信号量、消息队列和共享内存。它们都是由 AT&T System V2 版本的 UNIX 引入的,所有同城为 System V IPC
- 在进程间传递文件描述符的通用方法:通过 UNIX 本地域 socket 传递特殊的辅助数据
13.1 fork 系统调用
Linux
下创建新进程的系统调用是 fork
。定义如下:
#include <sys/types.h>
#include <unistd.h>
pid_t fork( void );
该函数的每次调用都返回两次,在父进程中返回的是子进程的 PID,在子进程中则返回 0。该返回值是后续代码判断当前进程是父进程还是子进程的依据。
fork
函数赋值当前进程,在内核进程表中创建一个新的进程表项。新的进程标像有很多属性和原进程相同,比如堆指针、栈指针和标志寄存器的值。但也有许多属性被赋予了新的值,比如该进程的 PPID
被设置成原进程的 PID
, 信号位图被清除( 原进程设置的信号处理函数不再对新进程起作用 )
子进程的代码与父进程完全相同,同时它还会赋值父进程的数据 ( 堆数据、栈数据和静态数据 )。数据的赋值采用的是所谓的写时复制( copy on writte ),即只有在任一进程( 父进程或子进程 ) 对数据执行的写操作时,复制才会发生(先是缺页中断,然后操作哦系统给子进程分配内存并复制父进程的数据)。即使如此,如果我们在程序中分配了大量内存,那么使用 fork
时也应当十分谨慎,尽量避免没有毕要的内存分配和数据复制。
此外,创建子进程后,父进程中打开的文件描述符默认在子进程中也是打开的。且文件描述符的引用计数 +1 。不仅如此,父进程的用户根目录、当前工作目录等变量的引用计数均会 +1;
测试:
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
using namespace std;
int main(){
int a = 10;
printf("%d ", &a);
printf("%d ", a);
fork();
a += 1;
printf("%d ", &a);
printf("%d ", a);
return 0;
}
/*
-1425197020 10 -1425197020 11 -1425197020 10 -1425197020 11
*/
写时复制,但是这里打印出来的地址是一样的,经过查阅,这里的地址应该是操作系统给我们的虚拟地址,实际应该有两个真实的物理地址。只是在这两个进程切换过程中,两个物理地址都映射到了这个给出的虚拟地址。
13.2 exec 系列系统调用
有时需要在子进程中执行其他程序,即替换当前进程映像,这就需要使用如下 exec
系列函数之一:
#include <unistd.h>
extern char** environ;
int execl( const char* path, const char* arg, ... );
int execlp( const char* file, const cahr* arg, ... );
int execle( const char* path, const char* arg, ..., char* const envp[] );
int execv( const char* path, char* const argv[] );
int execvp( const char* file, char* const argv[] );
int execve( const char* path, char* const argv[], char* const envp[] );
- path 指定可执行文件的完整路径
- file 可以接收文件名,该文件的距离位置则在环境变量 PATH 中搜寻。
- arg 接收可变参数
- argv 接收参数数组,和上一个参数都会被传递给新程序( path 或 file 指定的程序) 的 main 函数
- envp 用于设置新程序的环境变量。如果未设置它,则新程序将使用由全局变量
environ
指定的环境变量
一般情况下,exec
函数是不返回的,除非出错,出错时返回 -1,并设置 errno
。如果没出错,则源程序中 exec 调用之后的代码都不会执行,因为此时源程序已经被 exec
的参数指定的程序完全替换
exec
函数不会关闭源程序打开的文件描述符,除非该文件描述符被设置了类似 SOCK_CLOEXEC
的属性 (int socket( int domain, int type, int protocol)
里的 type 参数就可以设置)
13.3 处理僵尸进程
对应多进程程序而言,父进程一般需要跟粽子进程的退出状态。因此,当子进程结束运行时,内核不会立即释放该进程的进程表表项,以满足父进程后续对该子进程退出信息的查询( 如果父进程还在运行 )。在子进程结束运行之后,父进程读取其退出状态之前,我们称该子进程处于僵尸态。另外一种使子进程进入僵尸态的情况是:父进程结束或者异常终止。而子进程继续运行。此时子进程的 PPID
将被操作系统设置为 1,即 init
进程。init
进程接管了该子进程,并等待它结束。在父进程退出之后,自己成退出之前,该子进程处于僵尸态
由此,如果父进程没有正确地处理子进程的返回信息,子进程都将停留在僵尸态,并占据着内核资源。而着绝对不被允许,因为内核资源有限。下面这对函数在父进程中调用,以等待子进程的结束,并获取子进程的返回信息,从而避免了僵尸进程的产生,或者使子进程的僵尸态立即结束
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait( int* stat_loc );
pid_t waitpid( pid_t pid, int* stat_loc, int options );
wait
函数将阻塞进程,知道该进程的某个子进程结束运行为止。它返回结束运行的子进程的 PID
,并将该子进程的退出状态信息存储于 stat_loc
参数指向的内存中。 sys/wait.h
头文件中定义了几个宏来帮助解释子进程的退出状态信息,如表
宏 | 含义 |
---|---|
WIFEXITED( stat_val ) | 如果子进程正常结束,它就返回一个非 0 值 |
WEXITSTATUS( stat_val ) | 如果 WIFEXITED 非 0 ,它返回子进程的退出码 |
WIFSIGNALED( stat_val ) | 如果子进程是因为一个未捕获的信号而重指,它就返回一个非 0 值 |
WTERMSIG( stat_val ) | 如果 WIFSIGNALED 非 0 ,它返回一个信号值 |
WIFSTOPPED( stat_val ) | 如果子进程意外终止,它就返回一个 非 0 值 |
WSTOPSIG( stat_val ) | 如果 WIFSTOPPED 非 0,它返回一个信号值 |
wait
函数的阻塞特性被 waitpid
函数解决了。waitpid
只等待由 pid
参数指定的子进程。如果 pid
取值为 -1,那么它就和 wait
含义相同,即等待任意一个子进程结束。stat_loc
参数含义与 wait
相同。option
参数可以控制 waitpid
函数行为。常用取值是 WNOHANG
。当 option
的取值是 WNOHANG
时,waitpid
调用将是非阻塞的;如果 pid
指定的目标子进程还没有结束或意外终止,则 waitpid
立即返回 0;如果目标子进程确实正常退出了,则 waitpid
返回该进程的 PID
。waitpid
调用失败时返回 -1 并设置 errno
。
对于 waitpid
函数而言,最好在某个子进程退出之后再调用它。而父进程从何得知子进程已经退出了呢? SIGCHLD
信号。我们再父进程中捕获 SIGCHLD
信号,并在信号处理函数中调用 waitpid
函数以“彻底结束”一个子进程。
static void handle_child( int sig ){
pid_t pid;
int stat;
while( ( pid == waitpid( -1, &stat, WNOHANG ) ) > 0 ){
/* 对结束进程进行善后处理 */
}
}
13.4 管道
管道能在父、子进程间传递数据,利用的是 fork
调用之后两个管道文件描述符 ( fd[0] 和 fd[1] ,参考 pipe ), 都保持打开。一堆这样的文件描述符只能保证父、子进程间一个方向的数据传输,父进程和子进程必须有一个关闭 fd[0]
,另一个关闭 fd[1]
。要实现从父进程像子进程写数据,如果所示。
显然,如果要实现双向数据传输,需使用两个管道。socket
编程接口提供了一个创建全双工管道的系统调用:socketpair
。squid
服务器程序就是利用 socketpair
创建管道,以实现再父进程和日志服务子进程之间传递日志信息。
管道只能用于有关联的两个进程( 比如父、子进程 )间的通信。而System V IPC
能用于无关联的多个进程之间的通信,因为它们都是用一个全局唯一的键值来标识一条信道。
13.5 信号量
13.5.1 信号量原语
当多个进程同时访问系统某个资源时,就需要考虑进程的同步问题,以确保任一时刻只有一个进程可以拥有对资源的独占式访问。通常,程序中对共享资源访问的那段代码被称为关键代码段,或者临界区。
信号量是一种特殊的变量,只能取自然数值并且只支持两种操作:等待(wait)和信号(signal)。这两种操作更常用的称呼是 P、V 操作。假设信号量 SV,则对它的 P、V操作含义如下:
- P(SV),如果 SV 的值大于 0,就将它减 1;如果 SV 的值为 0,则挂起进程的执行
- V(SV),如果有其他进程因为等待 SV 而挂起,则唤醒;如果没有,则将 SV 加 1;
Linux
信号量的 API
都定义在 sys/sem.h
头文件中,主要包含 3 个系统调用:semget、semop
和 semctl
。它们都被设计为操作一组信号量,即信号集,而不是单个信号量。
13.5.2 semget 系统调用
该调用创建一个新的信号量集,或者获取一个已经存在的信号集量。定义如下:
#include <sys/sem.h>
int semget( key_t key, int num_sems, int sem_flags );
- key 键值,标识一个全局唯一的信号量集,就行文件名全局唯一地标识一个文件一样。要通过信号量通信地进程需要使用相同的剑指来创建 / 获取信号量
- num_sems 指定要创建 / 获取的信号量集中信号量的数目。如果是创建信号量,则该值必须被指定;如果是获取已经存在的信号量,则可以把它设置为 0
- sem_flags 指定一组标志。它低端的 9 个比特是该信号量的权限,其格式和含义都与系统调用
open
的mode
参数相同
如果 semget
用于创建信号量集,则与之管理的内核数据结构体 semid_ds
将被创建并初始化。semid_ds
结构体定义如下:
#include <sys/sem.h>
/* 该结构体用于描述 IPC 对象 ( 信号量、共享内存和消息队列 ) 的权限 */
struct ipc_perm{
key_t key; /* 键值 */
uid_t uid; /* 所有者的有效用户 ID */
gid_t gid; /* 所有者的有效组 ID */
uid_t cuid; /* 创建者的有效用户 ID */
gid_t cgid; /* 创建者的有效组 ID */
mode_t mode; /* 访问权限 */
/* 其他填充字段 */
}
struct semid_ds{
struct ipc_perm sem_perm; /* 信号量的操作权限 */
unsigned long int sem_nsems; /* 该信号量集中的信号量数目 */
time_t sem_otime; /* 最后一次调用 semop 的时间 */
time_t sem_ctime; /* 最后一次调用 semctl 的时间 */
/* 其他填充字段 */
}
semget
对 semid_ds
结构体的初始化包括:
- 将
sem_perm.cuid
和sem_perm.uid
设置为调用进程的有效用户 ID - 将
sem_perm.cgid
和sem_perm.gid
设置为调用进程的有效组 ID - 将
sem_perm.mode
的最低 9 为设置为sem_flags
参数的最低 9 位 - 将
sem_nsems
设置为num_sems
- 将
sem_otime
设置为 0 - 将
sem_ctime
设置为当前的系统时间
13.5.3 semop 系统调用
semop
系统调用改变信号量的值,即执行 P、V 操作。以下是与每个信号量关联的一些重要的内核变量:
unsigned short semval; /* 信号量的值 */
unsigned short semzcnt; /* 等待信号量值变为 0 的进程数量 */
unsigned short semncnt; /* 等待信号量值增加的进程数量 */
pid_t sempid; /* 最后一次执行 semop 操作的进程 ID */
semop
对信号量的操作实际上就是对这些内核变量的操作。定义如下
#include <sys/sem.h>
int semop( int sem_id, struct sembuf* sem_ops, size_t num_sem_ops );
-
sem_id
由semget
调用返回的信号量集标识符,用以指定被操作的目标信号量集。 -
sem_ops
指向一个sembuf
结构体类型的数组,定义如下struct sembuf{ unsigned short int sem_num; short int sem_op; short int sem_flg; }
- sem_num 成员是信号量集中信号量的编号, 0 标识信号量集中的第一个信号量。
- sem_op 指定操作类型,其可选值为正整数、0、负整数。每种类型的行为又收到
sem_flg
成员的影响 - sem_flg 可选值的
IPC_NOWAIT
和SEM_UNDO
。前者的含义是,无论信号量操作是否成功,semop
的调用都立即返回,类似于非阻塞 I/O 操作。后者的含义是,当进程退出时取消正在进行的semop
操作。
semop
系统调用的第三个参数 num_sem_ops
指定要执行的操作个数,即 sem_ops
数组中元素的个数。semop
对数组 sem_ops
中的每个成员按照数组顺序依次执行操作,并且该过程是原子操作,以避免别的进程在同一时间按照不同的顺序对该信号集中的信号量执行 semop
操作导致的竞态条件
semop
成功时返回 0,失败则返回 -1 并设置 errno
。失败的时候,sem_ops
数组中指定的所有操作都不被执行
13.5.4 semctl 系统调用
semctl
系统调用允许调用者对信号量进行直接控制。定义如下
#include <sys/sem.h>
int semctl( int sem_id, int sem_num, int command, ... );
- sem_id 由
semget
调用返回的信号量集标识符,用以指定被操作的信号量集。 - sem_num 指定被操作的信号量在信号量集中的编号
- command 指定要执行的命令
有的命令需要调用者传递第 4 个参数。第 4 个参数由我们用户自己定义,sys/sem.h
头文件给出了它的推荐格式
union semun{
int val; /* 用于 SETVAL 命令 */
struct semid_ds* buf; /* 用于 IPC_STAT 和 IPC_SET 命令 */
unsigned short* array; /* 用于 GETALL 和 SETALL 命令 */
struct seminfo* __buf; /* 用于 IPC_INFO 命令 */
};
struct seminfo{
int semmap; /* Linux 内核没有使用 */
int semmni; /* 系统最多可以拥有的信号量集数目 */
int semmns; /* 系统最多可以拥有的信号量数目 */
int semmnu; /* Linux 内核没有使用 */
int semmsl; /* 一个信号量集最多允许包含的信号量数目 */
int semopm; /* semop 一次最多能执行的 sem_op 操作数目 */
int semume; /* Linux 内核没有使用 */
int semusz; /* sem_undo 结构体的大小 */
int semvmx; /* 最大允许的信号量值 */
/* 最多允许的 UNDO 次数 ( 带 SEM_UNDO 标志的 semop 操作的次数 ) */
int semaem;
};
13.5.5 特殊键值 IPC_PRIVATE
semget
的调用者可以给其 key
参数传递一个特殊的键值 IPC_PRIVATE
( 其值为 0 ),这样无论该信号量是否已经存在,semget
都将创建一个新的信号量。使用该键值创建的信号量并非像其名称声明的那样时进程私有的。其他进程,尤其是子进程,也有方法来访问这个信号量。所以应该称为 IPC_NEW
。
IPC_PRIVATE.c
该代码在父、子进程间使用一个 IPC_PRIVATE
信号来同步。
另:工作在 prefork
模式下的 httpd
网页服务器程序使用 1 个 PRIVATE
信号量来同步各子进程对 epoll_wait
的调用权。
还有两种 IPC
– 共享内存和消息队列。这两种 IPC 在创建资源得时候也支持 IPC_PRIVATE
键值,其含义和信号量的 IPC_PRIVATE
键值相同
13.6 共享内存
共享内存是最高效的 IPC
机制,因为它不涉及进程之间的任何数据传输。这种高效率所带来的问题是我们必须用其他的辅助手段来同步进程对共享内存的访问,否则会产生竞态条件。因此,共享内存通常和其他进程间通信方式一起使用
Linux
共享内存的 API
定义在 sys/shm.h
头文件中,包括四个系统调用:shmget、shmat、shmdt
和 shmctl
。
13.6.1 shmget 系统调用
创建一段新的共享内存,或者获取已经存在的共享内存。定义如下
#include <sys/shm.h>
int shmget( key_t key, size_t size, int shmflg );
和 semget
系统调用一样,key
参数是一个键值,用来标识一段全局唯一的共享内存。size
参数指定共享内存的大小,单位是字节。如果是创建新的共享内存,则 size
值必须被指定。如果是获取已经存在的共享内存,则可以把 size
设置为 0 .
- shmflg 和
sem_flags
参数相同,不过shmget
支持两个额外的标志 –SHM_HUGETLB
和SHM_NORESERVE
- SHM_HUGETLB 类似于
mmap
的MAP_HUGETLB
标志,系统将使用 “大页面” 来为共享内存分配空间 - SHM_NORESERVE 类似于
mmap
的MAP_NORESERVE
标志,不为共享内存保存交换分区 ( swap 空间 )。这样,当物理内存不足时,对该共享内存执行写操作将触发 SIGSEGV 信号
- SHM_HUGETLB 类似于
shmget
成功时返回一个正整数值,它是共享内存的标识符。失败时返回 -1,并设置 errno
如果 shmget
用于创建共享内存,则这段共享内存的所有字节都被初始化为 0,与之关联的内核数据结构 shmid_ds
将被创建并初始化。shmid_ds
结构体的定义如下
struct shmid_ds{
struct ipc_perm shm_perm; /* 共享内存的操作权限 */
size_t shm_segsz; /* 共享内存大小,单位是字节 */
__time_t shm_atime; /* 对这段内存最后一次调用 shmat 的时间 */
__time_t shm_dtime; /* 对这段内存最后一次调用 shmdt 的时间 */
__time_t shm_ctime; /* 对这段内存最后一次调用 shmctl 的时间 */
__pid_t shm_cpid; /* 创建者的 PID */
__pid_t shm_lpid; /* 最后一次执行 shmat 或 shmdt 操作的进程 */
shmatt_t shmnattach; /* 目前关联到此共享内存的进程数量 */
/* 其他填充字段 */
};
shmget
对 shmid_ds
结构体的初始化包括:
- 将
shm_perm.cuid
和shm_perm.uid
设置为调用进程的有效用户 ID - 将
shm_perm.cgid
和shm_perm.gid
设置为调用进程的有效组 ID - 将
shm_perm.mode
的最低 9 位设置为shmflg
参数的最低 9 位 - 将
shm_segsz
设置为size
- 将
shm_lpid、shm_nattach、shm_atime、shm_dtime
设置为 0 - 将
shm_ctime
设置为当前的时间
13.6.2 shmat 和 shmdt 系统调用
共享内存被创建 / 获取之后,我们不能立即访问它,而是需要先将它关联到进程的地址空间中。使用完共享内存之后,我们也需要将它从进程地址空间中分离。分别由如下两个系统调用实现
#include <sys/shm.h>
void* shmat( int shm_id, const void* shm_addr, int shmflg );
int shmdt( const void* shm_addr );
- shm_id 由
shmget
调用返回的共享内存标识符 - shm_addr 指定将共享内存关联到进程的哪块地址空间,最终的效果还收到
shmflg
参数的可选标志SHM_RND
的影响
shmat 成功时返回共享内存被关联到的地址,失败则返回 (void*)-1 并设置 errno
。shmat 成功时,将修改内核数据结构shmid_ds
的部分字段
- 将
shm_nattach
加 1 - 将
shm_lpid
设置为调用进程的PID
- 将
shm_atime
设置为当前的时间
shmdt 将关联到 shm_addr
处的共享内存从进程中分离。它成功时返回 0,失败则返回 -1 并设置 errno
。shmde
在成功调用时将修改内核数据结构 shmid_ds
的部分字段
- 将
shm_nattach
减 1 - 将
shm_lpid
设置为调用进程的PID
- 将
shm_dtime
设置为当前的时间
13.6.3 shmctl 系统调用
shmctl
系统调用控制共享内存的某些属性,定义如下
#include<sys/shm.h>
int shmctl( int shm_id, int command, struct shmid_ds* buf );
- shm_id 由
shmget
调用返回的共享内存标志 - command 指定要执行的命令
shmctl
支持的命令如下
命令 | 含义 | shmctl 成功时的返回值 |
---|---|---|
IPC_STAT | 将共享内存相关的内核数据结构复制到 buf 中 | 0 |
IPC_SET | 将 buf 中的部分成员复制到共享内存相关的内核数据结构中,同时内核数据中的 shmid_ds.shm_ctime 更新 | 0 |
IPC_RMID | 将共享内存打上删除的标记。这样当最后一个使用它的进程调用 shmdt 将它从进程中分离时,该共享内存就被删除了 | 0 |
IPC_INFO | 获取系统共享内存资源配置信息,将结果存储在 buf 中。应用程序需要将 buf 装换成 shminfo 结构体类型来读取这些系统信息。shminfo 结构体与 seminfo 类似。 | 内存共享内存信息数组中已经被使用的项的最大索引值 |
SHM_INFO | 与 IPC_INFO 类似,不过返回的是已经分配的共享内存占用的资源信息。应用程序需要将 buf 转换成 shm_info 结构体类型来读取这些信息。shm_info 和 shminfo 类似 | 同 IPC_INFO |
SHM_STAT | 与 IPC_STAT 类似,不过此时 shm_id 参数不是用来标识共享内存标识符,而是内核中共享内存信息数组的索引(每个共享内存的信息都是该数组中的一项) | 内核共享内存信息数组中索引值为 shm_id 的共享内存的标识符 |
SHM_LOCK | 禁止共享内存被移动至交换分区 | 0 |
SHM_UNLOCK | 允许共享内存被移动至交换分区 | 0 |
shmctl
成功时的返回值取决于 command
参数。失败时返回 -1, 并设置 errno
13.6.4 共享内存的 POSIX 方法
无须任何文件的支持,通过利用 mmap
在无关进程之间共享内存。需先使用如下函数来创建或打开一个 POSIX
共享内存对象
#include <sys/man.h>
#include <sys/stat.h>
#include <fcntl.h>
int shm_open( const char* name, int oflag, mode_t mode );
shm_open
的使用方法与 open
系统调用完全相同
-
name 指定要创建 / 打开的共享内存对象。从可移植性的角度考虑,该参数应该使用 “/somename” 的格式:以 “/” 开始,后接多个字符,且这些字符都不是 “/”; 以 “\0” 结尾,长度不超过
NAME_MAX
( 通常是 255 ) -
oflag 指定创建方式。它可以是下列标志中的一个或者多个的按位或
- O_RDONLY 以制度方式打开共享内存对象
- O_RDWR 以可读、可写方式打开共享内存对象
- O_CREAT 如果共享内存对象不存在,则创建之。此时
mode
参数的最低 9 位将指定该共享内存对象的访问权限。共享内存被创建时,其初始长度为 0 - O_EXCL 和 O_CREAT 一起使用,如果由
name
指定的共享内存对象已经存在,则shm_open
调用返回错误,否则就创建一个新的共享内存对象 - O_TRUNC 如果共享内存对象已经存在,则把它截断,使其长度为 0
shm_open
调用成功返回一个文件描述符。该文件描述符可用于后续的mmap
调用,从而将共享内存关联到进程和打开的文件最后需要关闭一样,由
shm_open
创建的共享内存使用完之后也需要被删除。该过程通过如下函数实现#include <sys/mman.h> #include <sys/stat.h> #include <fcntl.h> int shm_unlink( const char* name );
该函数将
name
指定的共享内存对象标记为等待删除。当所有使用该共享内存对象的进程都是用 munmap 将它从进程中分离之后,系统将教会这个共享内存对象所占据的资源如果代码中使用了上诉
POSIX
共享内存函数,则编译时需要指定连接选项-lrt
13.6.5 共享内存实例
将 9.6.2 中的聊天室服务器程序修改为一个多进程服务器:一个子进程处理一个客户连接。同时,将所有客户 socket
连接缓冲设计为一块共享内存
server.c
该源代码有两点注意:
- 虽然我们使用了共享内存,但每个子进程都只会往自己所处理的客户连接所对应的那一部分读缓存中写入数据,所以我们使用共享内存的目的只是为了“共享读”。因此,每个子进程再使用共享内存的时候都无须加锁。这样做符合 “聊天室服务器” 的应用场景,同时提高了程序性能
- 我们的服务器程序再启动的时候给数组 users 分配了足够多的空间,使得它可以存储所有可能的客户连接的相关数据。同样,我们一次性给数组
sub_process
分配的空间也足以存储所有可能的子进程的相关数据。这是牺牲空间换取事件的又一例子
13.7 消息队列
消息队列是再两个进程之间传递二进制块数据的一种简单有效的方式。每个数据块都有一个特定的类型,接收方可以根据类型来有选择地接收数据,而不一定像管道和命令管道那样必须以先进先出地方式接收数据
Linux
消息队列 API 都定义在 sys/msg.h
头文件中,包括 4 个系统调用:msgget、msgnd、msgrcv
和 msgctl
13.7.1 msgget 系统调用
msgget
系统调用创建一个消息队列,或者获取一个已有地消息队列。其定义如下:
#include <sys/msg.h>
int msgget( key_t key, int msgflg );
和 semget
系统调用一样, key
参数是一个键值,用来标识一个全局唯一地消息队列
- msgflg 与
semget
系统调用地sem_flags
参数相同
成功时返回一个正整数数值,它时消息队列的标识符。
如果用 msgget
创建消息队列,则与之关联的内核数据结构 msgid_ds
将被创建并初始化。msgid_ds
结构体定义如下:
struct msqid_ds{
struct ipc_perm msg_perm /* 消息队列的操作权限 */
time_t msg_stime; /* 最后一次调用 msgsnd 的时间 */
time_t msg_rtime; /* 最后一次调用 msgrcv 的时间 */
time_t msg_ctime; /* 最后一次被修改的时间 */
unsigned long __msg_cbytes; /* 消息队列中已有的字节数 */
msgqnum_t msg_qnum; /* 消息队列中已有的消息数 */
msglen_t msg_qbytes; /* 消息队列允许的最大字节数 */
pid_t msg_lsqid; /* 最后执行 msgsnd 进程的 PID */
pid_t msg_lrpid; /* 最后执行 msgrcv 的进程的 PID */
};
13.7.2 msgsnd 系统调用
该系统调用把一条消息添加到消息队列中。定义如下:
#include <sys/msg.h>
int msgsnd( int msqid, const void* msg_ptr, size_t msg_sz, int msgflg );
-
msqid 由
msgget
调用返回的消息队列标识符 -
msg_ptr 指向一个准备发送的消息,消息必须被定义为如下类型
struct msgbuf{ long mtype; /* 消息类型 */ char mtext[512]; /* 消息数据 */ };
- mtype 指定消息的类型,必须是一个正整数。
- mtext 消息数据。
-
msg_sz 消息的数据部分(mtext)长度。0 表示没有消息数据
-
msgflg 控制
msgsnd
行为。通常仅支持IPC_NOWAIT
标志,即以非阻塞的方式发送消息。默认情况下,发送消息时如果消息队列满了,则msgsnd
将阻塞。若IPC_NOWAIT
标志被指定,则msgsnd
将立即返回并这是errno
为EAGAIN
处于阻塞状态的msgsnd
调用可能被如下两种异常情况中断:
- 消息队列被移除。此时
msgsnd
调用将立即返回并设置 errno 为 EIDRM - 程序接收到信号。此时
msgsnd
调用将立即返回并这是 errno 为 EINTR
msgsnd
成功时将修改内核数据结构 msqid_ds
的部分字段
- 将
msg_qnum
加 1 - 将
msg_lspid
设置为调用进程的PID
- 将
msg_stime
设置为当前的时间
13.7.3 msgrcv 系统调用
该系统调用从消息队列中获取消息。定义如下:
#include <sys/msg.h>
int msgrcv( int msqid, void* msg_ptr, size_t msg_sz, long int msgtype, int msgflg );
- msqid 是由
msgget
调用返回的消息队列标识符 - msg_ptr 用于存储接收的信息
- msg_sz 消息数据部分长度
- msgtype 指定接收何种类型的消息
- msgtype == 0 读取消息队列中第一个消息
- msgtype > 0 读取消息队列中第一个类型为
msgtype
的消息(除非指定了 MSG_EXCEPT) - msgtype < 0 读取队列中第一个类型值比
msgtype
小的消息
- msgflg 控制 msgrcv 函数的行为。可以是如下一些标志的按位或
IPC_NOWAIT
如果没有消息,msgrcv 调用理解返回并设置 errno ENOMSG- MSG_EXCEPT 如果 msgtype > 0,则接收消息队列中第一个非
msgtype
类型的消息 - MSG_NOERRBO 如果消息数据部分长度超过
msg_sz
,截断 - 消息队列被移除。
msgrcv
调用立即返回并设置errno
为EIDRM
- 程序接收到信号。立即返回并设置 errno 为 EINTR。
msgrcv
成功时将修改内核数据结构 msqid_ds
的部分字段
- 将 msg_qnum 减 1
- 将 msg_lrpid 设置为调用进程的 ID
- 将 msg_rtime 设置为当前的时间
13.7.4 msgctl 系统调用
控制消息队列的某些属性。定义如下:
#include <sys/msg.h>
int msgctl( int msqid, int command, struct msqid_ds* buf );
- msqid 是由
msgget
调用返回的共享内存标识符。 - command 指定要执行的命令
13.8 IPC 命令
上诉三种信号量、消息队列和共享内存的 System V IPC 进程间通信方式都是使用一个全局唯一的键值(key)来描述一个共享资源。当程序调用 semget、shmget
或者 msgget
时,就创建了这些共享资源的一个实例。Linux
提供了 ipcs 命令,以观察当前系统上拥有哪些共享资源实例。
13.9 在进程间传递文件描述符
由于 fork
调用之后,父进程中打开的文件描述符在子进程中仍然保持打开,所以文件描述符可以很方便地从父进程传递到子进程。需要注意地时,传递一个文件描述符并不是传递一个文件描述符的值。而是要在接收进程中创建一个新的文件描述符,并且该文件描述符和发送进程中被传递的文件描述符指向内核中相同的文件表项。
在 Linux
下,我们利用 UNIX
域 socket
在进程间传递特殊的辅助数据,以实现文件描述符的传递,在两个不相干的进程间传递文件描述符。
transmit.cpp
描述符是通过结构体 msghdr 的 msg_control 成员送的,因此在继续向下进行之前,有必要了解一下msghdr 和 cmsghdr 结构体,先来看看 msghdr 。
struct msghdr {
void *msg_name;
socklen_t msg_namelen;
struct iovec *msg_iov;
size_t msg_iovlen;
void *msg_control;
size_t msg_controllen;
int msg_flags;
};
结构成员可以分为下面的四组,这样看起来就清晰多了:
-
套接口地址成员 msg_name 与 msg_namelen ;
只有当通道是数据报套接口时才需要; msg_name 指向要发送或是接收信息的套接口地址。
msg_namelen 指明了这个套接口地址的长度。msg_name 在调用 recvmsg 时指向接收地址,在调用 sendmsg 时指向目的地址。注意, msg_name 定义为一个(void *) 数据类型,因此并不需要将套接口地址显示转换为 (struct sockaddr *) 。
-
I/O 向量引用 msg_iov 与 msg_iovlen
它是实际的数据缓冲区,从下面的代码能看到,我们的 1 个字节就交给了它;这个 msg_iovlen 是 msg_iov 的个数,不是什么长度。msg_iov 成员指向一个 struct iovec 数组, iovc 结构体在 sys/uio.h 头文件定义,它没有什么特别的。
struct iovec { ptr_t iov_base; /* Starting address */ size_t iov_len; /* Length in bytes */ };
有了 iovec ,就可以使用 readv 和 writev 函数在一次函数调用中读取或是写入多个缓冲区,显然比多次 read ,write 更有效率。 readv 和 writev 的函数原型如下:
#include <sys/uio.h> int readv(int fd, const struct iovec *vector, int count); int writev(int fd, const struct iovec *vector, int count);
-
附属数据缓冲区成员 msg_control 与 msg_controllen ,描述符就是通过它发送的,后面将会看到, msg_control指向附属数据缓冲区,而 msg_controllen 指明了缓冲区大小。
-
接收信息标记位 msg_flags ;忽略
轮到 cmsghdr 结构了,附属信息可以包括若干个单独的附属数据对象。在每一个对象之前都有一个 struct cmsghdr 结构。头部之后是填充字节,然后是对象本身。最后,附属数据对象之后,下一个 cmsghdr 之前也许要有更多的填充字节。
struct cmsghdr {
socklen_t cmsg_len;
int cmsg_level;
int cmsg_type;
/* u_char cmsg_data[]; */
};
cmsg_len 附属数据的字节数,这包含结构头的尺寸,这个值是由 CMSG_LEN() 宏计算的;
cmsg_level 表明了原始的协议级别 ( 例如, SOL_SOCKET) ;
cmsg_type 表明了控制信息类型 ( 例如, SCM_RIGHTS ,附属数据对象是文件描述符; SCM_CREDENTIALS,附属数据对象是一个包含证书信息的结构 ) ;
被注释的 cmsg_data 用来指明实际的附属数据的位置,帮助理解。
对于 cmsg_level 和 cmsg_type ,当下我们只关心 SOL_SOCKET 和 SCM_RIGHTS 。