进程是Linux操作系统环境的基础,它控制着系统上几乎所有的活动。以下我们将探讨Linux多进程编程,包括如下内容:
- 复制进程映象的fork系统调用和替换进程映象的exec系列系统调用。
- 僵尸进程以及如何避免僵尸进程。
- 进程间通信
- 在进程间传递文件描述符的通用方法
本文的内容是阅读总结游双的《Linux高性能服务器编程》第13章“多进程编程“而来
fork系统调用
#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);
该函数的每次调用都返回两次,在父进程中返回的是子进程的PID
,在子进程返回0。调用失败时返回-1,并设置errno
。
fork
函数复制当前进程,在内核进程表中创建一个新的进程表项。新的进程表项有很多属性和原进程相同,比如堆指针、栈指针和标志寄存器的值。但也有许多属性被赋予了新的值,比如该进程的PPID
被设置成原进程的PID
,信号位图被清除。
子进程的代码与父进程完全相同,同时它还会复制父进程的数据,复制采用的是写时拷贝,即只有在任一进程(父进程或子进程)对数据执行了写操作时,复制才会发生(先是缺页中断然后操作系统给子进程分配内存并复制父进程的数据)。
此外,创建子进程后,父进程中打开的文件描述符默认在子进程中也是打开的,且文件描述符的引用计数加1。不仅如此,父进程的用户根目录、当前工作目录等变量的引用计数均会加1。
exec系列系统调用
#include <unistd.h>
extern char** environ;
int execl(const char* path,const char* arg, ...);
int execlp(const char* file,const char* arg, ...);
int execlpe(const char* path, const char* arg, ..., char* const envp[]);
...
path
参数指定可执行文件的完整路径,file
参数可以接受文件名,该文件的具体位置则在环境表里PATH
中搜寻。arg
接受可变参数。envp
用于设置新程序的环境变量。
一般情况下,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
参数指向的内存中。waitpid
只等待有pid
参数指定的子进程。如果pid
取值为-1,那么它就和wait
一样等待任意一个子进程结束,stat_loc
和wait
函数一样,options
的取值一般为WNOHANG
,即非阻塞:如果pid
指定的目标子进程还没有结束或者意外终止,则waitpid
立即返回0;如果目标子进程正常退出了,则waitpid
返回该子进程的PID
。waitpid
调用失败是返回-1并设置errno
。
管道
管道是父进程和子进程间通信的常用手段。管道能在父、子进程间传递数据,利用的是fork
调用之后两个管道文件描述符(fd[0]
和fd[1]
)都保持打开。一对这样的文件描述符只能保证父、子进程间一个方向的数据传输。如果要实现父、子进程之间的双向数据传输,就必须使用两个管道。
信号量
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
:指定一组标志。
semop系统调用
semop
系统调用改变信号量的值,即执行P,V操作。
#include <sys/sem.h>
int semop(int sem_id, struct sembuf* sem_ops, size_t num_sem_ops);
sem_id
是由semget
调用返回的信号量标识符,用以指定被操作的目标信号量集。sem_ops
指向一个sembuf
结构体类型的数组,num_sem_ops
指定要执行的操作个数,即sem_ops
数组中元素的个数。semop
对数组sem_ops
中的每个成员按照数组顺序依次执行操作,并且该过程是原子操作。semop
成功时返回0,失败则返回-1并设置errno
。失败的时候,sem_ops
数组中指定的所有操作都不被执行。
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_NOWAWIT
:无论信号量操作是否成功,semop
调用都将立即返回;SEM_UNDO
:当进程退出时取消正在进行的semop
操作。
semctl系统调用
semctl
系统调用允许调用者对信号量进行直接控制。其定义如下:
#include <sys/sem.h>
int semctl(int sem_id, int sem_num, int command, ...);
sem_id
参数是由semget
调用返回的信号量集标识符,用以指定被操作的信号量集。sem_num
参数指定被操作的信号量在信号量集中的标号。command
参数指定要执行的命令。有的命令需要调用者传递第4个参数。
数是由semget
调用返回的信号量集标识符,用以指定被操作的信号量集。sem_num
参数指定被操作的信号量在信号量集中的标号。command
参数指定要执行的命令。有的命令需要调用者传递第4个参数。