获取进程ID
每个进程都有一个唯一的正数(非零)进程ID(PID)。getpid函数返回调用进程的PID。getpid函数返回它的父进程的PID(创建调用进程的进程)。
#incldue <sys/types.h>
#include <unistd.h>
pid_t getpid(void);
pid_t getppid(void);
复制代码
getpid
和getppid
函数返回一个类型为pid_t
的正数值,在Linux系统上它在types.h
中被定义为int。
创建和终止进程
从程序员角度,我们可以认为进程总是处于以下三种状态之一:
- 运行。进程要么在CPU上执行,要么在等待被执行且最终会被内核调度
- 停止。进程的执行被挂起,且不会调度。当收到SIGSTOP,SIGTSTP,SIGTTIN或者SIGTTOU信号时,进程就会停止,并且保持停止直到它收到一个SIGCONT信号,这个时刻,进程再次开始运行。(信号是一种软件中断的形式)
- 终止。进程永远的停止了工作,进程会因为三种原因中止:1)收到一个信号,该信号的默认行为时终止进程,2)从主程序返回,3)调用exit函数
#include <stdlib.h>
void exit(int status);
复制代码
exit函数以status退出状态来终止进程(另一种设置退出状态的方法是从主程序中返回一个整数值)。
父进程通过调用fork函数创建一个新的运行的子程序。
#include <sys/types.h>
#incldue <unistd.h>
pid_t fork(void);
复制代码
新创建的子进程几乎但不完全与父进程相同。子进程得到父进程用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段,堆,共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,这就意味着父进程调用fork时,子进程可以读写父进程中打开的任何文件。父进程和新创建的子进程之间最大的区别在于它们有不同的PID。
fork函数是非常有趣的,因为它只被调用一次,却会返回两次;一次是在调用进程(父进程)中,一次是在新创建的子进程中。在父进程中,fork返回子进程的PID。在子进程中,fork返回0。因为子进程的PID总是为非0,返回值就提供一个明确的方法来分辨程序是在父进程还是在子进程中执行。
例如:
#include "csapp.h"
volatile int x = 1;
pid_t Fork(void) {
pid_t pid;
if ((pid = fork()) < 0)
unix_error("Fork error");
return pid;
}
int main(void) {
pid_t pid;
pid = Fork();
printf("pid 是:%d\n", pid);
if (pid == 0) {
printf("child: x = %d \n", ++x);
exit(0);
}
printf("parent: x = %d \n", ++x);
exit(0);
}
复制代码
当我们运行这个程序的时候,会得到下面这个结果:
pid 是:96698
parent: x = 2
pid 是:0
child: x = 2
Program ended with exit code: 0
复制代码
介绍一下这个例子的微妙方面:
- 调用一次,返回两次。fork函数被父进程调用一次,但是却返回两次:一次是返回到父进程,一次返回到新创建的子进程。创建一个子进程的程序来说,这是相当简单的。但是具有多个fork实例的程序可能就会令人困惑。
- 并发执行。父进程和子进程是并发运行的独立进程。内核能够以任意方式交替执行它们的逻辑控制流中的指令。
- 相同但是独立的地址空间。如果能够在fork函数在父进程和子进程中返回后立即暂停这两个进程,我们会看到两个进程的地址空间都是相同的。每个进程有相同的用户栈,相同的本地变量值,相同的堆,相同的全局变量值,以及相同的代码。
- 共享文件。当运行这个实例程序时,我们注意到父进程和子进程都把它们的输出显示在屏幕上。原因是子进程继承了父进程所打开的文件。当父进程调用fork时,stdout文件是打开的,并指向屏幕。子进程继承这个文件,因此它的输出也是只想屏幕的。
回收子进程
当一个进程由于某种原因终止时,内核并不是立即把它从系统中清除。相反,进程被保持在一种已终止状态中,直到被它的父进程回收。当父进程回收已终止的子进程时,内核将子进程的退出状态传递给父进程,然后抛弃掉已经终止的进程,从此时开始,该进程不存在。一个终止了但未被回收的进程成为僵死进程
僵死进程已经终止了,而内核却任保留着它的某些状态直到父进程回收它为止。
如果一个父进程终止了,内核会安排init进程成为它的孤儿进程的养父。init进程的PID为1,是在系统启动时由内核创建的,它不会终止,是所有进程的祖先。如果父进程没有回收它的僵死进程就终止了,那么内核会安排init进程去回收它们。
#include <sys/types.h>
#include <sys/wait.h>
// 如果成功,则为子进程的PID,如果WNOHANG,则为0,如果其他错误,则为-1
pid_t waitpid(pid_t pid, int *statusp, int options);
复制代码
waitpid函数有点复杂。默认情况下(当options = 0时),waitpid挂起调用进程的执行,直到它的等待集合中的一个子进程终止。如果等待集合中的一个进程在刚调用的时刻就已经终止了,那么waitpid就立刻返回。这两种情况中,waitpid返回导致waitpid返回的已终止子进程的PID。此时,已终止的子进程已经被回收,内核会从系统中删除掉它的所有痕迹。
判定等待集合成员
等待集合成员是由pid来确定的:
- 如果pid > 0,那么等待集合就是一个单独的子进程,它的进程ID等于pid
- 如果pid = -1,那么等待集合就是由父进程所有的子进程组成的。
修改默认行为
可以通过将options设置为常量WNOHANG, WUNTRACED 和 WCONTINUED的各种组合来修改默认行为:
- WNOHANG: 如果等待集合中的任何子进程都还没有终止,那么就立即返回(返回值为0)。默认的行为是挂起调用进程,直到有子进程终止。在等待子进程终止的同时,如果还想做些有用的工作,这个选项会有用。
- WUNTRACED: 挂起调用进程执行,直到等待集合中的一个进程变成已终止或者被停止。返回的PID为导致返回的已终止或被停止子进程的PID。默认的行为是只返回已经终止的子进程。当你想要检查已终止和被停止的子进程时,这个选项会有用
- WCONTINUED: 挂起调用进程的执行,直到等待集合中一个正在运行的进程终止或等待集合中一个被停止的进程收到SIGCONT信号重新开始执行。
可以用或运算把这些选项组合起来。例如:
- WNOHANG | WUNTRACED: 立即返回,如果等待集合中的子进程都没有被停止或终止,则返回值为0;如果有一个停止或终止,则返回值为该进程的PID。
检查已回收子进程的退出状态
如果statusp参数是空,那么waitpid就会在status中放上关于导致返回的子进程的状态信息,status是statusp指向的值。wait.h头文件中定义了解释status参数的几个宏:
- WIFEXITED:如果子进程通过调用exit或者一个返回正常终止(return 0)就返回真
- WIFSIGNALED:返回一个正常终止的子进程的退出状态。只有在WIFEXITED返回真的时候才定义这个状态。
- WIFSIGNALED:如果子进程是因为一个未被捕获的信号终止的,那么就返回真。
- WTERMSIG:返回导致子进程终止的信号的编号。只有在WIFSIGNALED()返回为真的时候,才定义这个状态。
- ………..
错误条件
如果调用进程没有子进程,那么waitpid返回-1,并且errno为ECHILD。如果waitpid函数被一个信号中断,那么它返回-1,并设置errno为EINTR。
wait
wait函数是waitpid函数的简单版本:
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *statusp);
复制代码
调用wait(&status)等价于调用waitpid(-1,&status,0)。
waitpid示例
#include "csapp.h"
int main(int argc, const char * argv[]) {
int status, i;
pid_t pid;
for (i = 0; i < 2; i++) {
if ((pid = Fork()) == 0) {
printf("child %d exit ok!\n", i);
exit(100 + i);
}
}
while ((pid = waitpid(-1, &status, 0)) > 0) {
if (WIFEXITED(status))
printf("child pid: %d, exit status: %d\n", pid, WEXITSTATUS(status));
else
printf("child pid: %d exit abnormally \n", pid);
}
if (errno != ECHILD)
unix_error("waitpid error");
exit(0);
}
复制代码
输出结果:
child 0 exit ok!
child 1 exit ok!
child pid: 15135, exit status: 101
child pid: 15134, exit status: 100
Program ended with exit code: 0
复制代码
这里我们先创建两个子进程,此时子进程没有被回收,属于僵死进程,然后在调用waitpid将僵死进程进行回收。