目录
包括如下内容:
- 复制进程映像的fork系统调用和替换进程映像的exec系列系统调用。
- 僵尸进程以及如何避免僵尸进程。
- 进程间通信(Inter-Process Communication,IPC)最简单的方式:管道。
- 3种System V进程间通信方式:信号量、消息队列和共享内存。它们都是由AT&T System V2版本的UNIX引入的,所以统称为System V IPC。
- 在进程间传递文件描述符的通用方法:通过UNIX本地域socket传递特殊的辅助数据
fork系统调用
Linux下创建新进程的系统调用是fork,定义如下:
#include<sys/types.h>
#include<unistd.h>
pid_t fork(void);
该函数的每次调用都返回两次,在父进程中返回的是子进程的PID,在子进程中则返回0.该返回值是后续代码判断当前进程是父进程还是子进程的依据。fork调用失败时返回-1,并设置errno。
fork函数复制当前进程,在内核进程表中创建一个新的进程表项。新的进程表项有很多属性和原进程相同,比如堆指针、栈指针和标志寄存器的值。但也有许多属性被赋予了新的值,比如该进程的PPID被设置成原进程的PID,信号位图被清除(原进程设置的信号处理函数不再对新进程起作用)。
子进程的代码和父进程完全相同,同时它还会复制父进程的数据(堆数据、栈数据和静态数据)数据的复制采用的是所谓的写时复制(copy on write),即只有在任一进程(父进程或子进程)对数据执行了写操作时,复制才会发生(先是缺页中断,然后操作系统给子进程分配内存并复制父进程的数据)。即便如此,如果我们在程序中分配了大量内存,那么使用fork时也应十分谨慎,尽量避免没必要的内存分配和数据复制。
此外,创建子进程后,父进程中打开的文件描述符默认在子进程中也是打开的,且文件描述符的引用计数加1.不仅如此,父进程的用户根目录、当前工作目录等变量的引用计数均会加1.
exec系列系统调用
有时我们需要在子进程中执行其他程序,即替换当前进程映像,这就需要使用如下exec系列函数之一:
#include<unistd.h>
extern char **environ;
int execl(const char *path , const char *arg , ...);
int execlp(const char *file , const char *arg , ...);
int execle(const char *path , const char *arg , ... , char *const envp[]);
int execv(cosnt 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的属性。
处理僵尸进程
对于多进程程序而言,父进程一般需要跟踪子进程的退出状态。因此,当子进程结束运行时,内核不会立即释放该进程的进程表表项,以满足父进程后续对该子进程退出信息的查询(如果父进程还在运行)。在子进程结束运行之后,父进程读取其退出状态之前,我们称该子进程处于僵尸态。另外一种使子进程进入僵尸态的情况是:父进程结束或者异常终止,而子进程继续运行。此时子进程的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,它就返回一个非0值 WIFSTOPPED(stat_val) 如果子进程意外终止,它就返回一个非0值 WSTOPSIG 如果WIFSTOPPED非0,它就返回一个信号值 wait函数的阻塞特性显然不是服务器期望的,而waitpid函数解决了这个问题。waitpid只等待由pid参数指定的子程序。如果pid取值为-1,那么他就和wait函数相同,即等待任意一个子进程结束。stat_loc参数的含义和wait函数的stat_loc参数相同。options参数可以控制waitpid函数的行为。该参数最常用的取值是WNOHANG。当options的取值是WNOHANG时,waitpid调用将是非阻塞的;如果pid指定的目标子进程还没有结束或意外终止,则waitpid立即返回0;如果目标子进程确实正确退出了,则waitpid返回该子进程的PID。waitpid调用失败时返回-1并设置errno。
要在事件已经发生的情况下执行非阻塞调用才能提高程序的效率。对waitpid函数而言,我们最好在某个子进程退出之后再调用它,那么父进程从何得知某个子进程已经退出了呢?这正是SIGCHLD信号的用途。当一个进程结束时,它将给其父进程发送一个SIGCHLD信号。因此我们可以在父进程中捕获SIGCHLD信号,并在信号处理函数中调用waitpid函数以彻底结束一个子进程。如代码清单所示:
static void handle_child(int sig) { pid_t pid; int stat; while((pid=waitpid(-1,&stat,WNOHANG))>0) { /*对结束的子进程进行善后处理*/ } }
进程组
所谓进程组,就是一个或者多个进程的集合。作为一个进程组,里面每一个进程都有统一的进程标识。
#include<sys/types.h>
#include<unistd.h>
pid_t getpgrp(void);
每一个进程都有其生命周期,从创建进程到进程终止,这是一个进程的生命期,而进程组的生命期是从该进程组的创建到最后一个进程终止。
int setpgid(pid_t pid , pid_t pgid);
时间片分配
该策略原理是首先将CPU分配给当前优先级别最高的进程,然后每当出现一个新进程时,都会与正在使用CPU的进程进行比较,若是新的进程优先级别高,则会触发进程调度,这个优先级别高的进程将会抢占CPU
#include<sched.h>
int sched_setscheduler(pid_t pid , int policy , const struct sched_param *param);
int sched_getscheduler(pid_t pid);
获取进程标识
#include<sys/types.h>
#include<unistd.h>
pid_t getpid(void);(获取自己进程ID)
pid_t getppid(void);(获取父进程ID)
获取用户ID和有效用户ID
在Linux系统中,每一个用户都有其唯一的用户标识,即用户ID(UID),在/etc/passwd文件下可以获取到每一个用户ID
用户ID(UID)用于表示进程创建者信息,有效用户ID(EUID)用于表示创建进程的用户,在任意时刻对资源和文件都具有访问权限。通常情况下,UID和EUID的值是相同的
#include<sys/types.h>
#include<unistd.h>
uid_t getuid(void);
uid_t geteuid(void);
这两个函数总是调用成功
设置进程标识成功0,失败-1;
#include<sys/types.h>
#include<unistd.h>
int setuid(uid_t uid);
int setgid(gid_t gid);
setuid函数用于修改当前进程用户标识,成功0,失败-1;
setgid函数用于设置当前进程的有效用户组标识。成功0,失败-1;
管道
我们介绍过创建管道的系统调用pipe,我们也多次在代码中利用它来实现进程内部的通信。实际上,管道也是父进程和子进程间通信的常用手段。
管道能在父、子进程间传递数据,利用的是fork调用之后两个管道文件描述符(fd[0]和fd[1])都保持打开。一对这样的文件描述符只能保证父、子进程间一个方向的数据传输,父进程和子进程必须有一个关闭fd[0],另一个关闭fd[1]。比如,我们要使用管道实现父进程向子进程写数据,父进程要close(fd[0]),子进程要close(fd[1])。
显然,如果要实现父、子进程之间的双向数据传输,就必须要使用两个管道。socket编程接口提供了一个创建全双工管道的系统调用:socketpair。squid服务程序就是利用socketpair创建管道,以实现在父进程和日志服务子进程之间传递日志信息
不过管道只能用于有关联的两个进程(比如父、子进程)间的通信。下面要讨论的3种System V IPC能用于无关联的多个进程之间的通信,因为它们都使用一个全局唯一的键值来标识一条信道。不过,有一种特殊的管道称为FIFO(First In First Out,先进先出),也叫命名管道。它也能用于无关联进程之间的通信。因为FIFO管道在网络编程中使用不多,所以不讨论它。
信号量
信号量原语
当多个进程同时访问系统上的某个资源的时候,比如同时写一个数据库的某条记录,或者同时修改某个文件,就需要考虑进程的同步问题,以确保任一时刻只有一个进程可以拥有对资源的独占式访问。通常,程序对共享资源的访问代码只是很短的一段,但就是这一段代码引发了进程之间的竞态条件。我们称这段代码为关键代码段,或者临界区。对进程同步,也就是确保任一时刻只有一个进程能进入关键代码段。
Linux信号量的API都定义在sys/sem.h头文件中,主要包含3个系统调用:semget、semop和semctl。它们都被设计为操作一组信号量,即信号量集,而不是单个信号量,因此这些接口看上去多少比我们期望的要复杂一点。
semget系统调用
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参数相同。此外,它还可以和IPC_CREAT标志做按位或运算以创建新的信号量集。此时即使信号量已经存在,semget也不会产生错误。我们还可以联合使用IPC_CREAT和 IPC_EXCL标志来确保创建一组新的、唯一的信号量集。在这种情况下,如果信号量集已经存在,则semget返回错误并设置errno为EEXIST。这种创建信号量的行为与用O_CREAT和O_EXCL标志调用open来排他式地打开一个文件相似。
semget成功时返回一个正整数值,它是信号量集的标识符;semget失败时返回-1,并设置errno。
如果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设置为当前的系统时间。
semop系统调用
semop系统调用改变信号量的值,即执行P、V操作。在讨论semop之前,我们需要先介绍每个与信号量关联的一些重要的内核变量:
unsigned short semval; /*信号量的值*/
unsigned short semzcnt; /*等待信号量值变为0的进程数量*/
unsigned short semncnt; /*等待信号量值增加的进程数量*/
pid_t sempid; /*最后一次执行semop操作的进程ID*/
semop对信号量的操作实际上就是对这些内核变量的操作。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结构体类型的数组,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。IPC_NOWAIT的含义是,无论信号量操作是否成功,semop调用都将立即返回,这类似于非阻塞I/O操作。SEM_UNDO的含义是,当进程退出时取消正在进行的semop操作。具体来说,sem_op和sem_flg将按照如下方式来影响semop的行为:
- 如果sem_op大于0,则semop将被操作的信号量的值semval增加sem_op。该操作要求调用进程对被操作信号量集拥有写权限。此时若设置了SEM_UNDO标志,则系统将更新进程的semadj变量(用以跟踪进程对信号量的修改情况)。
- 如果sem_op等于0,则表示这是一个“等待0”(wait-for-zero)操作。该操作要求调用进程对被操作信号量集拥有读权限。如果此时信号量的值是0,则调用立即成功返回。如果信号量的值不是0,则semop失败返回或者阻塞进程以等待信号量变为0.在这种情况下,当IPC_NOWAIT标志被指定时,semop立即返回一个错误,并设置errno为EAGAIN。如果未指定IPC_NOWAIT标志,则信号量的semzcnt值加1,进程被投入睡眠直到下列3个条件之一发生:信号量的值semval变为0,此时系统将该信号量的semzcnt值减1;被操作信号量所在的信号量集被进程移除,此时semop调用失败返回,errno被设置为EIDRM;调用被信号中断,此时semop调用失败返回,errno被设置为EINTR,同时系统将该信号量的semzcnt值减1.
- 如果sem_op小于0,则表示对信号量值进行减操作,即期望获得信号量。该操作要求调用进程对被操作信号量集有写权限。如果信号量的值semval大于或等于sem_op的绝对值,则semop操作成功,调用进程立即获得信号量,并且系统将该信号量的semval减去sem_op的绝对值。此时如果设置了SEM_UNDO标志,则系统将更新进程的semadj变量。如果信号量的值semval小于sem_op的绝对值,则semop失败返回或者阻塞进程以等待信号量可用,在这种情况下,当IPC_NOWAIT标志被指定时,semop立即返回一个错误,并设置errno为EAGAIN。如果未指定IPC_NOWAIT标志,则信号量的semncnt值加1,进程被投入睡眠直到下列3个条件之一发生:信号量的值semval变得大于或等于sem_op的绝对值,此时系统将该信号量的semncnt值减1,并将semval减去sem_op的绝对值,同时,如果SEM_UNDO标志被设置,则系统更新semadj变量;被操作信号量所在的信号集被进程移除,此时semop调用失败返回,errno被设置为EIDRM;调用被信号中断,此时semop调用失败返回,errno被设置为EINTR,同时系统将信号量的semncnt值减1.
semop系统调用的第3个参数num_sem_ops指定要执行的操作个数,即sem_ops数组中元素的个数。semop对数组sem_ops中的每个成员按照数组顺序依次执行操作,并且该过程是原子操作,以避免别的进程在同一时刻按照不同的顺序对该信号集中的信号量执行semop操作导致的竞态操作。
semop成功时返回0,失败则返回-1并设置errno。失败的时候,sem_ops数组中指定的所有操作都将不被执行。
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; }
semctl支持的所有命令如表所示:
命令 含义 semctl成功时的返回值 IPC_STAT 将信号量集关联的内核数据结构复制到semun.buf中 0 IPC_SET 将semun.buf中的部分成员复制到信号集关联的内核数据结构中,同时内核数据中semid_ds.sem_ctime被更新 0 IPC_RMID 立即移除信号量集,唤醒所有等待该信号量集的进程(semop返回错误,并设置errno为EIDRM) 0 IPC_INFO 获取系统信号量资源配置信息,将结果存储在semun.__buf中。这些信息的含义见结构体seminfo的注释部分 内核信号量集数组中已经被使用的项的最大索引值 SEM_INGO 与IPC_INFO类似,不过semun.__buf.semusz被设置为系统目前拥有的信号量集数目,而semnu.__buf.semacm被设置为系统目前拥有的信号量数目 同IPC_INFO SEM_STAT 与IPC_STAT类似,不过此时sem_id参数不是用来表示信号量集标识符,而是内核中信号量集数组的索引(系统的所有信号量集都是该数组中的一项) 内核信号量集数组中已经被使用的最大项的最大索引值 GETALL 将由sem_id标识的信号量集中的所有信号量的semval值导出到semun.array中 0 GETNCNT 获取信号量的semncnt值 信号量的semncnt值 GETPID 获取信号量的sempid值 信号量的sempid值 GETVAL 获取信号量的semval值 信号量的semval值 GETZCNT 获取信号量的semzcnt值 信号量的semzcnt值 SETALL 用semun.array中的数据填充由sem_id标识的信号量集中的所有信号量的semval值,同时内核数据中的semid_ds.sem_ctime被更新 0 SETVAL 将信号量的semval值设置为semun.val,同时内核数据中的semid_ds.sem_ctime被更新 0 注意:这些操作中,GETNCNT、GETPID、GETVAL、GETZCNT和SETVAL操作的是单个信号量,它是由标识符sem_id指定的信号量集中的第sem_num个信号量;而其他操作的是针对整个信号量集,此时semctl的参数sem_num被忽略。
semctl成功的返回值取决于command参数,如表所示。semctl失败时返回-1,并设置errno。
特殊键值IPC_PRICATE
semget的调用者可以给其key参数传递一个特殊的键值IPC_PRIVATE(其值为0),这样无论该信号量是否已经存在,semget都将创建一个新的信号量。使用该键值创建的信号量并非像它的名字声称的那样是进程私有的。其他进程,尤其是子进程,也有方法来访问这个信号量。所以semget的man手册的BUGS部分上说,使用名字IPC_PRIVATE有些误导(历史原因),应该称为IPC_NEW。
Linux上输:
ipcs -s
可以看到创建的信号量
共享内存
共享内存是最高效的IPC机制,因为它不涉及进程之间的任何数据传输。这种高效率带来的问题是,我们必须用其他辅助手段来同步进程对共享内存的访问,否则会产生竞态条件。因此,共享内存通常和其他进程间通信方式一起使用。
Linux共享内存的API都定义在sys/shm.h头文件中,包括4个系统调用:shmget、shmat、shmdt和shmctl。我们将依次讨论之。
ftok函数
系统建立IPC通讯(如消息队列、共享内存等) 必须指定一个ID值。通常情况下,该id值通过ftok函数得到。
ftok原型如下:
#include
#include
key_t ftok(const char *pathname, int proj_id);
pathname参数: 必须是一个已经存在且程序可范围的文件。
proj_id参数: 虽然定义为一个整数,其实实际只有8个bit位有效,即如果该参数大于255,则只有后8bit有效。
函数作用: convert a pathname and a project identifier to a System V IPC key, Key可用于msgget, semget, or shmget的key参数
shmget系统调用
shmget系统调用创建一段新的共享内存,或者获取一段已经存在的共享内存。其定义如下:
#include<sys/shm.h>
int shmget(key_t key , size_t size , int shmflg);
和shmget系统调用一样,key参数是一个键值对,用来标识一段全局唯一的共享内存。size参数指定共享内存的大小,单位是字节。如果是创建新的共享内存,则size值必须被指定。如果是获取已经存在的共享内存,则可以把size设置为0.
shmflg参数的使用和含义与semget系统调用的sem_flags参数相同。不过shmget支持两个额外的标志——SHM_HUGETLB和SHM_NORESERVE。它们的含义如下:
- SHM_HUGETLB,类似于mmap的MAP_NORESERVE标志,系统将使用“大页面”来为共享内存分配空间。
- SHM_NORESERVE,类似于mmap的MAP_NORESERVE标志,不为共享内存保留交换分区(swap空间)。这样,当物理内存不足的时候,对该共享内存执行写操作将触发SIGSEGV信号。
shmget成功时返回一个正整数值,它是共享内存的标识符。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操作的进程的PID*/ shmatt_t shm_nattach; /*目前关联到此共享内存的进程数量*/ /*省略一些填充字段*/ };
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设置为当前的时间。
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的影响:
- 如果shm_addr为NULL,则被关联的地址由操作系统选择。这是推介的做法,以确保代码的可移植性
- 如果shm_addr非空,并且SHM_RND标志未被设置,则共享内存被关联到addr指定的地址处。
- 如果shm_addr非空,并且设置了SHM_RND标志,则被关联的地址是[shm_addr-(shm_addr%SHMLBA)] ,SHMLBA的含义是“段低端边界地址倍数”(Segment Low Boundary Address Multiple),它必须是内存页面大小(PAGE_SIZE)的整数倍。现在的Linux内核中,它等于一个内存页的大小。SHM_RND的含义是圆整(round),即将共享内存被关联的地址向下圆整到离shm_addr最近的SHMLBA的整数倍地址处。
除了SHM_RND标志外,shmflg参数还支持如下标志:
- SHM_RDONLY。进程仅能读取共享内存中的内容。若没有指定该标志,则进程可同时对共享内存进行读写操作(当然,这需要在创建共享内存的时候指定其读写权限)。
- SHM_REMAP。如果地址shmaddr已经被关联到一段共享内存上,则重新关联。
- SHM_EXEC。它指定对共享内存段的执行权限。对共享内存而言,执行权限实际上和读权限是一样的。
shmat成功时返回共享内存段被关联到的地址,失败则返回(void*)-1并设置errno。shmat成功时,将修改内核数据结构shmid_ds的部分字段,如下:
- 将shm_nattach加1
- 将shm_lpid设置为调用进程的PID
- 将shm_atime设置为当前的时间。
shmdt函数将关联到shm_addr处的共享内存从进程中分离。它成功时返回0,失败则返回-1并设置errno。shmdt在成功调用时将修改内核数据结构shmid_ds的部分字段,如下:
- 将shm_nattach减1
- 将shm_lpid设置为调用进程的PID。
- 将shm_dtime设置为当前的时间
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(第3个参数,下同)中 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_LOCK 禁止共享内存被移动至交换分区 0 SHM_UNLOCK 允许共享内存被移动至交换分区 0 shmctl成功时的返回值取决于command参数,如表所示。shmctl失败时返回-1,并设置errno。
共享内存的POSIX方法
我们介绍过mmap函数。利用它的MAP_ANONYMOUS标志我们可以实现父、子进程之间的匿名内存共享。通过打开同一个文件,mmap也可以实现无关进程之间的内存共享。Linux提供了另外一种利用mmap在无关进程之间共享内存的方式。这种方式无须任何文件的支持,但它需要先使用如下函数来创建或打开一个POSIX共享内存对象:
#include<sys/mmap.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失败时返回-1,并设置errno。
和打开的文件最后需要关闭一样,由shm_open创建的共享内存对象使用完之后也需要被删除。这个过程是通过如下函数实现的:
#include<sys/mman.h>
#include<sys/stat.h>
#include<fcntl.h>
int shm_unlink(const char* name);
该函数将name参数指定的共享内存对象标记为等待删除。当所有使用该共享内存对象的进程都使用ummap将它从进程中分离之后,系统将销毁这个共享内存对象所占据的资源。
如果代码中使用了上述POSIX共享内存函数,则编译的时候需要指定链接选项-Irt。
消息队列
消息队列是在两个进程之间传递二进制块数据的一种简单有效的方式。每个数据块都有一个特定的类型,接收方可以根据类型来有选择地接收数据,而不一定像管道和命名管道那样必须以先进先出的方式接收数据。
Linux消息队列的API都定义在sys/msg.h头文件中,包括4个系统调用:msgget、msgsnd、msgrcv和msgctl。
msgget系统调用
msgget系统调用创建一个消息队列,或者获取一个已有的消息队列。其定义如下:
#include<sys/msg.h>
int msgget(ket_t key , int msgflg);
和semget系统调用一样,key参数是一个键值,用来标识一个全局唯一的消息队列。
msgflg参数的使用和含义与semget系统调用的sem_flags参数相同。
msgget成功时返回一个正整数值,它是消息队列的标识符。msgget失败时返回-1,并设置errno。
如果msgget用于创建消息队列,则与之关联的内核数据结构msqid_ds将被创建并初始化。msqid_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_lspid; /*最后执行msgsnd的进程的PID*/ pid_t msg_lrpid; /*最后执行msgrcv的进程的PID*/ };
msgsnd系统调用
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成功时返回0,失败则返回-1并设置errno。msgsnd成功时将修改内核数据结构msqid_ds的部分字段,如下所示:
- 将msg_qnum加1
- 将msg_lspid设置为调用进程的PID
- 将msg_stime设置为当前的时间
msgrcv系统调用
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_NOERROR。如果消息数据部分的长度超过了msg_sz,就将它截断。
处于阻塞状态的msgrcv调用还可能被如下两种异常情况所中断:
- 消息队列被移除。此时msgrcv调用将立即返回并设置errno为EIDRM。
- 程序接收到信号。此时msgrcv调用将立即返回并设置errno为EINTR。
msgrcv成功时返回0,失败则返回-1并设置errno。msgrcv成功时将修改内核数据结构msqid_ds的部分字段,如下:
- 将msg_qnum减1
- 将msg_lrpid设置为调用进程的PID
- 将msg_rtime设置为当前的时间
msgctl系统调用
msgctl系统调用控制消息队列的某些属性。其定义如下:
#include<sys/msg.h>
int msgctl(int msqid , int command , struct msqid_ds* buf);
命令 | 含义 | msgctl成功时的返回值 |
IPC_STAT | 将消息队列关联的内核数据结构复制到buf | 0 |
IPC_SET | 将buf中的部分成员复制到消息队列关联的内核数据结构中,同时内核数据中的msqid_ds.msg_ctime被更新 | 0 |
IPC_RMID | 立即移除消息队列,唤醒所有等待读消息和写消息的进程(这些调用立即返回并设置errno为EIDRM) | 0 |
IPC_INFO | 获取系统消息队列资源配置信息,将结果存储在buf中。应用程序需要将buf转换成msginfo结构体类型来读取这些系统信息。msginfo结构体与seminfo类似 | 内核消息队列信息数组中已经被使用的项的最大索引值 |
MSG_INFO | 与IPC_INFO类似,不过返回的是已经分配的消息队列占用的资源信息 | 同IPC_INFO |
MSG_STAT | 与IPC_STAT类似,不过此时msqid参数不是用来表示消息队列标识符,而是内核消息队列信息数组的索引(每个消息队列的信息都是该数组中的一项) | 内核消息队列信息数组中索引值为msqid的消息队列的标识符 |
msgctl成功时的返回值取决于command参数,如表。msgctl函数失败时返回-1并设置errno。
IPC命令
用ipcs命令来观察当前系统上拥有哪些共享资源实例