一个进程如果创建出子进程,如果此时子进程退出,而父进程没有进行善后工作( w a i t wait wait与 w a i t p i d waitpid waitpid获取子进程状态信息),那么此时子进程的进程描述符仍然保存在系统中。孤儿进程
如果一个父进程退出,它的子进程(有一个或者多个)还在,那么子进程将成为孤儿进程,这个时候这些孤儿进程会被 1 1 1号进程,也就是 i n i t init init进程所收养,并且由 i n i t init init进程完成善后工作(状态收集)。 过程分析
上面都是那些老八股文里面经常出现的内容,但是我们不能仅仅停留在现象而不知道原理,现在我们就来分析一下,僵尸进程和孤儿进程产生的具体过程
我们先来讨论一下一个正常的进程是怎么退出的
我们都知道,一个进程总归是要终结的,内核必须要释放进程所占有的字眼并且把进程结束的消息发送给父进程,也就是发送
S
I
G
C
H
L
D
SIGCHLD
SIGCHLD信号
一般来说,进程的析构是自身引起的,他发生在进程调用exit()时,这个调用既有可能是显式的调用,也有可能是隐式的从某个程序的主函数返回
所以我们来看一下exit()的源码
SYSCALL_DEFINE1(exit, int, error_code)
{
do_exit((error_code&0xff)<<8);
}
这里的error_code是main函数退出时候的状态,可以看出exit()最后都会调用do_exit(),所以我们再来看一下do_exit()的源码
void __noreturn do_exit(long code)
{
struct task_struct *tsk = current;
int group_dead;
WARN_ON(blk_needs_flush_plug(tsk));
/*
* If do_dead is called because this processes oopsed, it's possible
* that get_fs() was left as KERNEL_DS, so reset it to USER_DS before
* continuing. Amongst other possible reasons, this is to prevent
* mm_release()->clear_child_tid() from writing to a user-controlled
* kernel address.
*
* On uptodate architectures force_uaccess_begin is a noop. On
* architectures that still have set_fs/get_fs in addition to handling
* oopses handles kernel threads that run as set_fs(KERNEL_DS) by
* default.
*/
force_uaccess_begin();
kcov_task_exit(tsk);
coredump_task_exit(tsk);
ptrace_event(PTRACE_EVENT_EXIT, code);
validate_creds_for_do_exit(tsk);
io_uring_files_cancel();
exit_signals(tsk); /* sets PF_EXITING */
/* sync mm's RSS info before statistics gathering */
if (tsk->mm)
sync_mm_rss(tsk->mm);
acct_update_integrals(tsk);
group_dead = atomic_dec_and_test(&tsk->signal->live);
if (group_dead) {
/*
* If the last thread of global init has exited, panic
* immediately to get a useable coredump.
*/
if (unlikely(is_global_init(tsk)))
panic("Attempted to kill init! exitcode=0x%08x\n",
tsk->signal->group_exit_code ?: (int)code);
#ifdef CONFIG_POSIX_TIMERS
hrtimer_cancel(&tsk->signal->real_timer);
exit_itimers(tsk->signal);
#endif
if (tsk->mm)
setmax_mm_hiwater_rss(&tsk->signal->maxrss, tsk->mm);
}
acct_collect(code, group_dead);
if (group_dead)
tty_audit_exit();
audit_free(tsk);
tsk->exit_code = code;
taskstats_exit(tsk, group_dead);
exit_mm();
if (group_dead)
acct_process();
trace_sched_process_exit(tsk);
exit_sem(tsk);
exit_shm(tsk);
exit_files(tsk);
exit_fs(tsk);
if (group_dead)
disassociate_ctty(1);
exit_task_namespaces(tsk);
exit_task_work(tsk);
exit_thread(tsk);
/*
* Flush inherited counters to the parent - before the parent
* gets woken up by child-exit notifications.
*
* because of cgroup mode, must be called before cgroup_exit()
*/
perf_event_exit_task(tsk);
sched_autogroup_exit_task(tsk);
cgroup_exit(tsk);
/*
* FIXME: do that only when needed, using sched_exit tracepoint
*/
flush_ptrace_hw_breakpoint(tsk);
exit_tasks_rcu_start();
exit_notify(tsk, group_dead);
proc_exit_connector(tsk);
mpol_put_task_policy(tsk);
#ifdef CONFIG_FUTEX
if (unlikely(current->pi_state_cache))
kfree(current->pi_state_cache);
#endif
/*
* Make sure we are holding no locks:
*/
debug_check_no_locks_held();
if (tsk->io_context)
exit_io_context(tsk);
if (tsk->splice_pipe)
free_pipe_info(tsk->splice_pipe);
if (tsk->task_frag.page)
put_page(tsk->task_frag.page);
validate_creds_for_do_exit(tsk);
check_stack_usage();
preempt_disable();
if (tsk->nr_dirtied)
__this_cpu_add(dirty_throttle_leaks, tsk->nr_dirtied);
exit_rcu();
exit_tasks_rcu_finish();
lockdep_free_task(tsk);
do_task_dead();
}
需要注意的是这里的__noreturn关键字的意思是表明调用完成后函数不返回主调函数,也就是说这进程所执行的最后一段代码,do_exit()永不返回
由于这部分代码比较复杂,所以我们只分析重点的部分
首先是exit_mm(),他会释放进程所占用的mm_struct,如果没有别的进程咋用,也就是说这个地址空间没有被共享,就彻底释放他们
接下来是exit_files()和exit_fs(),这两个函数会分别递减文件描述符,文件系统数据的引用计数,如果饮用计数值降为0,那么表示没有进程再说你用相应的资源,此时可以释放
接下来会调用exit_notify(),会向父进程发信号,给子进程重新寻找养父,养父为进程组中的其他进程或者为init进程,并且把进程状态设置为EXIT_ZOMBLE,调用schedule()切换到新的进程,因为进程被设置为EXIT_ZOMBLE状态,所以不会再被调度
到现在为止,与进程相关联的所有资源都被释放掉了(假设该进程是这些资源的唯一使用者),并且进程不可运行,但此时进程还占有者内存——内核栈、thread_info结构和task_struct结构,进程并没有消亡,此时进程存在的唯一目的就是向他的父进程提供信息,父进程检索到信息之后,通知内核,由进程所持有的剩余内存被释放,归还给系统。
如果父进程没有对子进程有后续的 *** 作,那么子进程就成了僵尸进程,会占用大量的资源,也就是说,进程终结时,占有资源的清理工作和进程描述符的删除时分开进行的,进程描符的删除依赖于父进程调用wait一族函数来释放僵尸进程中所使用的全部剩余资源,释放进程内核栈和thread_info结构所占有的页,并释放task_struct所占有的高速缓存。至此,进程描述符和所有进程独享的资源就被全部释放掉了
以上是僵尸进程产生的具体过程,孤儿进程的分析与之类似,因为父进程提前结束,所以子进程在退出时就会永远处于僵死状态,消耗内存,但Linux系统已经为我们进行了善后处理,在调用exit_notify()函数的时候会给自进程在当前线程组找一个父亲,如果不行,就让init作为父进程,进行后面的善后过程。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)