在Linux系统中,每个进程都有自己的生命周期。
Linux进程状态
在Linux中,大多数进程都是被fork()
出来的,进程被fork
创建后,则会进入就绪态,进入就绪态的进程等待CPU资源,一旦进程获取到了CPU,该进程就进入到运行态,若是进程需要等待资源或IO,则进入就绪态。一下这张图大家应该不陌生。
但是,在Linux中的进程,我们其实可以用下面这张图详细描述:
关于僵尸进程
僵死态(僵尸进程):处于僵死态的进程的task_struct
还没消失,但是该进程所依赖的资源都内核被回收了。
僵尸进程留下来的目的是为了让其父进程调用Linux中的一个API:wait4
来等待子进程完全结束。在Linux操作系统中,一个进程的task_struct
是在其父进程等待其结束时才会消失。因为这样的话在子进程死亡时父进程可以做一些善后工作:比如通过死亡子进程的task_struct
了解子进程的死因。
另一方面,进程的task_struct
结构中有很多统计信息,比如CPU使用时间等,让父进程来料理后事可以将这些信息并入父进程的统计信息而不至于丢失;另一方面,也是更重要的一方面,无论如何系统必须得有一个当前进程,在中断以及异常的服务程序中要用到当前进程的系统空间堆栈。如果在下一个进程投入运行之前,就把当前进程的系统空间回收,这样就存在一个空档,如果恰巧此时有中断发生就会造成问题。
僵尸进程是一个非常短的临界的情况:一个进程结束了,但父进程还没来得及调用wait4
函数,该进程就是一个僵尸进程。一旦父进程调用wait4
,该进程的task_struct
才会消失。
有三个问题值得注意:
- 如果父进程在子进程退出之前退出呢?子进程退出时该把报丧信号发给谁?这种情况下将由
init
进程“领养”父进程的所有子进程。 - 如果子进程已经终止了,但父进程没有调用wait函数获取它的终止状态又如何?内核为每个终止进程保存了一定量的信息,包括子进程的ID、进程终止状态以及进程使用的CPU时间总量,可以理解为子进程虽已去世,但还留着“尸体”等着父进程“收尸”。尸体要保留到父进程调用wait函数来收尸为止,在此之前,该子进程便成为一个僵尸进程(zombile)。
- 如果被
init
进程“领养”的进程终止了,系统中岂不会有大量的僵尸进程?不用担心,init
进程被设计成“无论何时,只要有一个子进程终止,init
就会调用wait
函数来为之收尸”,从而防止了在系统中有很多僵尸进程。
下图为wait4
的原语:
关于进程退出的详细介绍,可参考此篇博文:https://my.oschina.net/u/3857782/blog/1857551
以下为wait_task_zombie
函数中的代码段:
static int wait_task_zombie(struct wait_opts *wo, struct task_struct *p)
{
int status;
....
if (unlikely(wo->wo_flag & WNOWAIT)) {
int exit_code = p->exit_code;
int why;
....;
if ((exit_code & 0x7f) == 0) {
why = CLD_EXITED;
status = exit_code >> 8;
} else {
why = (exit_code & 0x80) ? CLD_DUMPED : CLD_KILLED;
status = exit_code & 0x7f;
}
....
}
....
}
其中的exit_code
即为子进程退出原因的相关信息。
举个例子
我们使用的用例程序如下,我们可以通过控制#if 0
那段while
循环的代码来决定是否让父进程给子进程“处理后事”。
#include <stdio.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
pid_t pid,wait_pid;
int status;
pid = fork();
if (pid==-1) {
perror("Cannot create new process");
exit(1);
} else if (pid==0) {
printf("child process id: %ld\n", (long) getpid());
pause();
_exit(0);
} else {
#if 0 /* define 1 to make child process always a zomie */
printf("ppid:%d\n", getpid());
while(1);
#endif
do {
wait_pid=waitpid(pid, &status, WUNTRACED | WCONTINUED);
if (wait_pid == -1) {
perror("cannot using waitpid function");
exit(1);
}
if (WIFEXITED(status))
printf("child process exites, status=%d\n", WEXITSTATUS(status));
if(WIFSIGNALED(status))
printf("child process is killed by signal %d\n", WTERMSIG(status));
if (WIFSTOPPED(status))
printf("child process is stopped by signal %d\n", WSTOPSIG(status));
if (WIFCONTINUED(status))
printf("child process resume running....\n");
} while (!WIFEXITED(status) && !WIFSIGNALED(status));
exit(0);
}
}
该测试程序中的waitpid
函数的作用就是等待子进程死亡并处理task_struct
,函数参数中的pid
即子进程的pid
,而status
参数则是保存子进程的退出原因。
当程序运行时,父进程输出子进程的pid
,并等待子进程结束,当我们使用kill
命令杀掉子进程后,父进程将输出子进程的“死亡原因”:
当我们用kill
发送信号干掉子进程时,父进程将收到通知,并能获取到子进程的死因:
若子进程已经死亡,且父进程一直不做清理(执行wait
),那么子进程将一直处于僵尸进程状态。例如如果我们把上述代码中的while
循环打开之后,父进程进入死循环且不等待子进程结束:
此时父进程打印出了子进程的pid
,我们使用kill -2 1307
结束子进程后,子进程的状态将会变成僵尸进程,我们可通过ps aux
查看,此处子进程的STAT
变成了Z+
,表示已经变成了僵尸进程:
悲哀的是,僵尸进程已经死亡,我们再怎么kill
这具尸体还是会在,除非我们杀掉子进程的父进程,或是父进程执行wait
,僵尸进程的task_struct
才会被清理。
也许有人会问:若父进程创建了多个子进程都变成了僵尸进程,那么这种情况不就白白耗费了很多资源吗?注意:**进程从死亡时到变成僵尸进程,所耗费的资源将全部由内核释放。**此处仅仅保留了该进程的task_struct
来描述该僵尸进程的一些信息,其占用的资源都已被内核回收。所以这里需要纠正一个误区:内存泄露并不是指一个进程申请的内存在进程结束后未得到释放导致内存被消耗,而是进程在一直运行的过程中已经使用完了的内存未正确释放且丢失指针,从而导致该进程所耗费的内存不断增多。
在进程结束之后,进程的所有内存都将被释放,包括堆上的内存泄露的内存。原因是,当进程结束时,GDT、LDT和页目录都被操作系统更改,逻辑内存全部消失,可能物理内存的内容还在但是逻辑内存已经从LDT和GDT删除,页目录表全部销毁,所以内存会被全部收回。(若没有该机制,面对各种牛鬼蛇神的应用Linux怎么可能健壮运行那么多年)
防止僵尸进程
当我们只fork()一次后,存在父进程和子进程。这时有两种方法来避免产生僵尸进程:
- 父进程调用waitpid()等函数来接收子进程退出状态。
- 父进程先结束,子进程则自动托管到Init进程(pid = 1)。
目前先考虑子进程先于父进程结束的情况:
- 若父进程未处理子进程退出状态,在父进程退出前,子进程一直处于僵尸进程状态。
- 若父进程调用waitpid()(这里使用阻塞调用确保子进程先于父进程结束)来等待子进程结束,将会使父进程在调用waitpid()后进入睡眠状态,只有子进程结束父进程的waitpid()才会返回。 如果存在子进程结束,但父进程还未执行到waitpid()的情况,那么这段时期子进程也将处于僵尸进程状态。
由此,可以看出父进程与子进程有父子关系,除非保证父进程先于子进程结束或者保证父进程在子进程结束前执行waitpid(),子进程均有机会成为僵尸进程。那么如何使父进程更方便地创建不会成为僵尸进程的子进程呢?这就要用两次fork()了。
父进程一次fork()后产生一个子进程随后立即执行waitpid(子进程pid, NULL, 0)来等待子进程结束,然后子进程fork()后产生孙子进程随后立即exit(0)。这样子进程顺利终止(父进程仅仅给子进程收尸,并不需要子进程的返回值),然后父进程继续执行。这时的孙子进程由于失去了它的父进程(即是父进程的子进程),将被转交给Init进程托管。于是父进程与孙子进程无继承关系了,它们的父进程均为Init,Init进程在其子进程结束时会自动收尸,这样也就不会产生僵尸进程了。
进程的暂停态
进程在运行时不睡眠,人为让进程停止而不死亡时,进程将进入暂停态,比如发送STOP信号。
发送STOP信号一般在两种情况下发生:
- 键盘输入
ctrl+z
时,为了job control(JC) gdb attatch
debug时,
停止态不占用CPU资源,需要再次唤醒该进程时,需要发送CONTINUE
信号,使得进程再次进入就绪态。
举个例子
若有一个程序如下所示:
#include <stdio.h>
int main()
{
int i = 0;
while(1){
volatile int j, k;
for (i = 0; i < 1000000; i++);
printf("hello %d\n", j++);
printf("world %d\n", j++);
}
return 0;
}
我们可以使用ctrl + z
和fg
命令不断暂停和恢复该进程到前台,如下图所示:
注:在Linux系统中存在cpulimit
这么一个控制工具,它能限制一个进程的cpu使用率,大致的方法就是不断让进程进入暂停态然后再唤醒。
例如cpulimit -l 10 -p 12296
表示将pid
为12296的进程的CPU使用率限制在10%。
深度睡眠及浅度睡眠描述
-
深睡眠:必须等到资源到来才会醒
-
浅睡眠:可以被资源或者是信号唤醒
注:当一个进程处于深睡眠时,是不会响应任何信号的(包括kill -9
)。深度睡眠为内核调用的结果。
reference
https://www.cnblogs.com/codingmylife/archive/2010/11/10/1874235.html
https://my.oschina.net/u/3857782/blog/1857551