知识点回顾
进程
进程的经典定义就是一个执行中程序的实例。系统中的每个程序都运行在某个进程的上下文(context)中。上下文是由程序正确运行所需的状态组成的。这个状态包括存放在内存中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。
每次用户通过向shell输人一个可执行目标文件的名字,运行程序时,shell 就会创建一个新的进程, 然后在这个新进程的上下文中运行这个可执行目标文件。应用程序也能够创建新进程,并且在这个新进程的上下文中运行它们自己的代码或其他应用程序。
获取进程ID
每个进程都有一个唯一的正数(非零)进程ID(PID)。getpid 函数返回调用进程的PID。getppid 函数返回它的父进程的PID(创建调用进程的进程)。
pid_t getpid(void);
pid_t getppid(void);
getpid 和 getppid 函数返回一个类型为 pid_t 的整数值,在 Linux 系统上他在 types.h 中被定义为 int。
创建进程
父进程通过调用 fork 函数创建一个新的运行的子进程。
新创建的子进程几乎但不完全与父进程相同。子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,这就意味着当父进程调用fork时,子进程可以读写父进程中打开的任何文件。父进程和新创建的子进程之间最大的区别在于它们有不同的PID。
fork函数是有趣的(也常常令人迷惑),因为它只被调用一次,却会返回两次:一次是在调用进程(父进程)中,一次是在新创建的子进程中。在父进程中,fork返回子进程的PID。在子进程中,fork返回0。 因为子进程的PID总是为非零,返回值就提供一个明确的方法来分辨程序是在父进程还是在子进程中执行。
我们先用书上的一个例子,进行简单的分析:
int main(int argc, char *argv[])
{
pid_t pid;
int x = 1;
pid = fork(); //line:ecf:forkreturn
if (pid == 0) { /* Child */
printf("child : x=%d\n", ++x); //line:ecf:childprint
fflush(stdout);
return 0;
}
/* Parent */
printf("parent: x=%d\n", --x); //line:ecf:parentprint
fflush(stdout);
return 0;
}
运行后,很容易得到结果:
可以通过简单的进程图进行分析:
分析:在遇到 fork() 之前,x的值为1,然后,父进程调用 fork() 函数,创建了子进程,实际上,两个进程中的代码、数据等信息全部一样,父进程中, fork() 返回一个非零值,执行代码x-1,同时输出副本,子进程中pid=0,x+1并输出副本,两者不会发生影响。
同时我们还应注意:
- 父进程和子进程是并发进行的,内核能够以任意方式交替执行它们的逻辑控制流中的指令,即两个进程完成的顺序不是一定的
- 习惯使用进程图来分析 fork() 函数,有逻辑的思路能帮助我们更快的理解
实操代码
例子1
void fork1()
{
int x = 1;
pid_t pid = fork();
if (pid == 0) {
printf("Child has x = %d\n", ++x);
}
else {
printf("Parent has x = %d\n", --x);
}
printf("Bye from process %d with x = %d\n", getpid(), x);
}
运行结果:
进程图:
分析:
两个进程执行同样的代码段,pid的值是连在一起的,所以相应的+1
例子2
void fork2()
{
printf("L0\n");
fork();
printf("L1\n");
fork();
printf("Bye\n");
}
运行结果:
进程图:
分析:
嵌套的 fork() 函数,两个进程分别又调用 fork() ,通过进程图就很好理解
例子3
void cleanup(void) {
printf("Cleaning up\n");
}
void fork6()
{
atexit(cleanup);
fork();
exit(0);
}
运行结果:
进程图:
分析:
上网查了资料,一个进程中可以登记32个函数,当 exit() 等函数返回时,会调用这些函数,执行其中的代码,我们称之为终止处理程序,将函数名传递给 atexit() ,就可注册登记这些函数,等待调用
本函数中,cleanup() 就是终止处理程序,exit(0) 返回时调用它,执行 printf
例子4
void fork7()
{
if (fork() == 0) {
/* Child */
printf("Terminating Child, PID = %d\n", getpid());
exit(0);
} else {
printf("Running Parent, PID = %d\n", getpid());
while (1)
; /* Infinite loop */
}
}
运行结果:
进程图:
分析:
产生了僵死子进程,消耗内存资源,还不能运行代码,可以通过 Ctrl+C 或Ctrl+Z,结束程序
僵死进程:当一个进程由于某种原因终止时,内核并不是立即把它从系统中清除。相反,进程被保持在一种已终 止的状态中,直到被它的父进程回收(reaped)。当父进程回收已终止的子进程时,内核将子进程的退出状态传递给父进程,然后抛弃已终止的进程,从此时开始,该进程就不存在了。一个终止了但还未被回收的进程称为僵死进程(zombie)。
本例子中,父进程陷入 while() 死循环,子进程正常终止后,不能被父进程回收,成为僵死进程
例子5
void fork8()
{
if (fork() == 0) {
/* Child */
printf("Running Child, PID = %d\n",
getpid());
while (1)
; /* Infinite loop */
} else {
printf("Terminating Parent, PID = %d\n",
getpid());
exit(0);
}
}
运行结果:
进程图:
分析:
产生孤儿进程,主进程运行然后fork一个子进程,父进程正常执行完程序,但是子进程打印后在后台挂起,执行一个死循环,此时父进程已经退出,子进程成为孤儿进程。
孤儿进程:如果一个父进程终止了,内核会安排init进程成为它的孤儿进程的养父。init 进程的PID为1,是在系统启动时由内核创建的,它不会终止,是所有进程的祖先。如果父进程没有回收它的僵死子进程就终止了,那么内核会安排init进程去回收它们。
例子6
void fork9()
{
int child_status;
if (fork() == 0) {
printf("HC: hello from child\n");
exit(0);
} else {
printf("HP: hello from parent\n");
wait(&child_status);
printf("CT: child has terminated\n");
}
printf("Bye\n");
}
运行结果:
进程图:
分析:
一个进程可以通过调用waitpid函数来等待它的子进程终止或者停止。
pid_ t waitpid(pid_t pid, int *statusp, int options) ;
waitpid函数有点复杂。默认情况下(当options=0时),waitpid挂起调用进程的执行,直到它的等待集合(wait set)中的一个子进程终止。如果等待集合中的一个进程在刚调用的时刻就已经终止了,那么waitpid就立即返回。在这两种情况中,waitpid返回导致waitpid返回的已终止子进程的PID。此时,已终止的子进程已经被回收,内核会从系统中删除掉它的所有痕迹。
例子7
void fork10()
{
pid_t pid[N];
int i, child_status;
for (i = 0; i < N; i++)
if ((pid[i] = fork()) == 0) {
exit(100+i); /* Child */
}
for (i = 0; i < N; i++) { /* Parent */
pid_t wpid = wait(&child_status);
if (WIFEXITED(child_status))
printf("Child %d terminated with exit status %d\n",
wpid, WEXITSTATUS(child_status));
else
printf("Child %d terminate abnormally\n", wpid);
}
}
运行结果:
例子8
void fork11()
{
pid_t pid[N];
int i;
int child_status;
for (i = 0; i < N; i++)
if ((pid[i] = fork()) == 0)
exit(100+i); /* Child */
for (i = N-1; i >= 0; i--) {
pid_t wpid = waitpid(pid[i], &child_status, 0);
if (WIFEXITED(child_status))
printf("Child %d terminated with exit status %d\n",
wpid, WEXITSTATUS(child_status));
else
printf("Child %d terminate abnormally\n", wpid);
}
}
运行结果:
分析:
pid_t wait(int *statusp);
调用 wait(&statusp) 等价于调用 waitpid(-1,&status,0)
我们可以设置 status 的值:
WIFEXITED(status): 如果子进程通过调用exit或者一个返回(return)正常终止,就返回真。
WEXITSTATUS(status):返回一个正常终止的子进程的退出状态。只有在WIFEXITED()返回为真时,才会定义这个状态。
WIFSIGNALED(status):如果子进程是因为一个未被捕获的信号终止的,那么就返回真。
WTERMSIG(status):返回导致子进程终止的信号的编号。只有在WIFSIG-NALED()返回为真时,才定义这个状态。
WIFSTOPPED(status):如果引起返回的子进程当前是停止的,那么就返回真。
WSTOPSIG(status):返回引起子进程停止的信号的编号。只有在WIFSTOPPED()返回为真时,才定义这个状态。
WIFCONTINUED( status):如果子进程收到SIGCONT信号重新启动,则返回真。
例子9
void fork12()
{
int i;
for(i=0; i<2; i++)
{
fork();
printf("*");
}
wait(NULL);
return 0;
}
运行结果:
进程图:
例子10
void fork13()
{
int i;
for(i=0; i<2; i++)
{
fork();
printf("*\n");
}
wait(NULL);
return 0;
}
运行结果:
进程图:
分析:
父进程调用 fork() 函数创建子进程时,所有信息会复制到子进程,包括缓冲区,第二轮 fork() 之前,printf 把 * 放进缓冲区,最后在两个进程中随机输出两个。
第二个例子的 \n ,将缓冲区中的 * 清空了,就不会再重复输出。