每一个进程在系统中都有一个唯一的标识,这个标识叫做进程标识符,或者叫 PID(process identity)。我们可以通过调用 getpid 函数来获取一个进程的PID,也可以调用getppid函数来获取当前进程的父进程PID。在 linux 系统中,有三个特殊的进程,它们的进程 PID 分别为0,1,2。0号进程是系统进程,它存在与内核当中,而不是磁盘文件上。该进程常被成为交换进程,或者叫 swapper。1号进程就是 init 进程,这个进程用来读取一些初始化文件,并且引导系统到一个状态,如多用户状态,init 进程是一个用户进程。2号进程是页精灵进程,用来做一些和虚拟内存相关的工作,可是这个进程我没有在系统中找到。如果有知道的朋友,请告诉我,这个页精灵进程在系统中是哪一个进程,另外页精灵进程也是系统进程。需要说明的是,这3个进程都是由特殊的方式产生的,除了这3个进程以外,linux 系统中的其它进程都是由调用 fork 函数产生的。
fork 函数用来创建一个子进程,这是 linux 系统中创建子进程的唯一方式。当调用 fork 函数之后,我们称调用 fork 函数产生的进程为子进程,调用fork函数的进程为父进程。fork 函数调用一次,但是返回两次。这两次返回分别是在父进程和子进程中,在父进程中返回子进程的 PID,在子进程中返回0。当调用 fork 函数成功后,父子进程就分别从调用 fork 的地方开始执行。子进程是父进程的一个拷贝,但是现在很多的实现,并不是调用 fork 成功之后就立即拷贝,而是采用了一种称为 COW(copy on write)的技术,只有当子进程需要修改一些值时,才进行拷贝。拷贝的是父进程的数据空间以及堆和栈。我们可以用一个小例子来解释一下 fork 函数的作用及特性:
#include <stdio.h> #include <unistd.h> int glob = 6; char buf[] = "a write to stdout\n"; int main(void) { pid_t pid; int var = 88; if( write(STDOUT_FILENO,buf,sizeof(buf)-1)!=sizeof(buf)-1 ) { printf("write error\n"); return -1; } printf("before fork\n"); if( (pid=fork())<0 ) { printf("fork error\n"); return -1; } else if(pid==0) { glob++; var++; } else { sleep(2); } printf("pid=%d\tglob=%d\tvar=%d\n",getpid(),glob,var); return 0; }
./a.out
a write to stdout before fork pid=3131 glob=7 var=89 pid=3130 glob=6 var=88
./a.out > /tmp/a
vim /tmp/a
a write to stdout before fork pid=3077 glob=7 var=89 before fork pid=3076 glob=6 var=88
fork 函数创建的子进程,会把在父进程中打开的文件描述符,复制到子进程中,也就是说它们共享同一个文件表项。
vfork 函数和 fork 函数的基本功能是一样的,它也是调用一次返回两次。但是 vfork 函数和 fork 函数的不同在于,vfork 的目的是创建一个新进程,该进程用来通过调用 exec 函数来执行一个新进程,所以子进程并不复制父进程的数据空间和堆栈,而是先在父进程的工作空间中运行。vfork 函数要求父进程等待子进程先执行,如果在调用了vfork函数之后,子进程的执行还需要依赖于父进程的进一步动作,这将导致死锁。只有当子进程执行了 exec 函数或者 exit 函数之后,父进程才可以被调度执行。我们将上面的程序中的fork函数替换为vfork函数,得到的程序如下:
#include <stdio.h> #include <sys/types.h> #include <unistd.h> #include <stdlib.h> int glob = 6; int main(void) { int var = 88; pid_t pid; printf("before vfork\n"); if( (pid=vfork())<0 ) { printf("vfork error\n"); return -1; } else if(pid==0) { var++; glob++; _exit(0); } printf("pid=%d var=%d glob=%d\n",getpid(),var,glob); return 0; }
before vfork pid=3487 var=89 glob=7
经过前面的介绍,我们已经了解了 init 进程。它不是一个系统进程,它就是存在于 /sbin/init 文件。它的作用是在系统启动时用来读取一些初始化信息,将系统引导到一个状态。另外,它还有一个重要的角色是收养系统中的孤儿进程。那么什么进程叫做孤儿进程呢?顾名思义,孤儿进程就是其父进程在其终止之前终止的进程。
init 进程收养孤儿进程的手续是怎么样的呢?当一个进程要终止时,内核会逐个检查系统中的各个进程,查看它们的父进程是否是正要终止的进程,如果是这样的话,就将它们的父进程修改为 init 进程。
另外,关于init进程清除系统中的僵死进程和僵死进程的一些概念,请参考博客点击打开链接。
wait 和 waitpid 函数,用来在父进程中等待子进程的结束。wait 函数用来等待父进程的任何一个子进程,只要有子进程终止,它就立即返回。调用 wait 函数,会发生3种情况:
1.当该进程没有任何子进程时,会发生错误,返回-1 2.当该进程的子进程都在运行,尚没有子进程结束时,该进程会发生阻塞 3.当该进程已经有子进程终止时,可期待 wait 函数立即返回,返回值为终止子进程 PID
exec 函数族包括很多的函数,这些函数的作用都是执行一个新的进程。内核执行一个进程的唯一方法是调用 exec 函数,但是 exec 函数并不修改进程的PID。一般的使用方法是先调用fork函数创建一个子进程,然后在子进程中执行 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(const char *path, char *const argv[]); int execvp(const char *file, char *const argv[]); int execve(const char *filename, char *const argv[], char *const envp[]);
pathname , arg1 , arg2 , (char*)0
上面的6个函数中只有 execve 是系统调用,其他的函数都是库函数,最终都需要调用 execve 函数。