前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Linux内核内幕:深入解析进程的结束过程

Linux内核内幕:深入解析进程的结束过程

作者头像
AshinZ
发布2023-11-01 17:03:52
3190
发布2023-11-01 17:03:52
举报

大家好,我是程栩,一个专注于性能的大厂程序员,分享包括但不限于计算机体系结构、性能优化、云原生的知识。

天下没有不散的宴席,有进程的创建就会有进程的消亡。那么内核是如何处理进程自身的消亡的,又是如何处理它的子进程、父进程的呢?让我们来结合《Linux内核设计与实现》以及Linux v6.3版本进行学习与了解。

进程终结的原因

一般来说,进程的结束是尤其自身引起的。当进程调用exit的时候,就出触发进程的结束操作;而对于一些不会显式exit的程序,其可能隐式的进行退出。例如C语言编译器可能会在mian函数末尾加上exit函数来中介进程。

当然,进程也可能因为收到某些信号被强制结束,例如我们可以通过kill -9来关闭进程。

进程终结全过程

进程在调用exit后,最后会通过内核中的do_exit函数来进行终结。

接下来我们基于代码进行讲解:

代码语言:javascript
复制
// kernel/exit.c L924
void __noreturn do_exit(long code)
{
 struct task_struct *tsk = current;
 int group_dead;

这里就是函数的入口,请注意这里使用tsk指针指向了当前的进程。

代码语言:javascript
复制
 WARN_ON(irqs_disabled());

 synchronize_group_exit(tsk, code);

 WARN_ON(tsk->plug);

WARN_ON是负责向内核输出警告信息的函数,在这里先关闭了中断,之后调用synchronize_group_exit来确保SIGNAL_GROUP_EXIT信号一定在进程调用exit的时候被设置了。该函数用于同步当前进程所在进程组的退出状态。因为一个进程组中的所有进程都共享同一个信号处理器(sighand_struct)和信号结构体(signal_struct),因此在进程退出时需要同步整个进程组的退出状态。具体来说,该函数会对信号处理器进行加锁,然后将当前进程所在进程组的 quick_threads 计数器减 1。如果 quick_threads 计数器变为 0,且当前进程组的 SIGNAL_GROUP_EXIT 标志位未被设置,则将该标志位设置,并将进程组的退出码、停止计数器等信息保存到信号结构体中。最后解锁信号处理器。synchronize_group_exit的实现如下:

代码语言:javascript
复制
// kernel/exit.c L789
static void synchronize_group_exit(struct task_struct *tsk, long code)
{
 struct sighand_struct *sighand = tsk->sighand;
 struct signal_struct *signal = tsk->signal;

 spin_lock_irq(&sighand->siglock);
 signal->quick_threads--;
 if ((signal->quick_threads == 0) &&
     !(signal->flags & SIGNAL_GROUP_EXIT)) {
  signal->flags = SIGNAL_GROUP_EXIT;
  signal->group_exit_code = code;
  signal->group_stop_count = 0;
 }
 spin_unlock_irq(&sighand->siglock);
}

接着检查plug变量是否为空,这里的plug和内核的plug/unplug机制有关,可以暂时不用去深究。

?关于WARN_ON(tsk->plug),patch的解释是:blk_needs_flush_plug fails to account for the cb_list, which needs flushing as well. Remove it and just check if there is a plug instead of poking into the internals of the plug structure. ?

代码语言:javascript
复制
 kcov_task_exit(tsk);
 kmsan_task_exit(tsk);

 coredump_task_exit(tsk);
 ptrace_event(PTRACE_EVENT_EXIT, code);
 
 validate_creds_for_do_exit(tsk);
 
 io_uring_files_cancel();

接着内核分别调用了kcov_task_exitkmsan_task_exitcoredump_task_exit来通知kcovkmsan进程的退出。并且通过ptrace_event关闭掉一些tracehooks。接着通过validate_creds_for_do_exit来检验进程的cred结构体是否有效,该结构体是与进程的安全相关,并通过io_uring_files_cancel取消已经提交的io_uring请求。

代码语言:javascript
复制
 exit_signals(tsk);  /* sets PF_EXITING */

发送SIGCHLD信号给父进程,并设置内核的标志成员为PF_EXITING

代码语言:javascript
复制
 /* sync mm's RSS info before statistics gathering */
 if (tsk->mm)
  sync_mm_rss(tsk->mm);
 acct_update_integrals(tsk);

调用acct_update_integrals来输出内核的记账信息。

代码语言:javascript
复制
 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);
#endif
  if (tsk->mm)
   setmax_mm_hiwater_rss(&tsk->signal->maxrss, tsk->mm);
 }

这里首先通过atomic_dec_and_test来让tsk->signal->live信号原子减一,如果减完以后值为0则返回真。而当真的时候,会调用exit_itimers释放掉进程的计时器相关内容,因为此时已经没有进程了。

代码语言:javascript
复制
 acct_collect(code, group_dead);
 if (group_dead)
  tty_audit_exit();
 audit_free(tsk);

 tsk->exit_code = code;
 taskstats_exit(tsk, group_dead);

这里首先调用acct_collect来收集进程的系统资源使用情况,接着如果进程组已经退出的话,就调用tty_audit_exit来更新当前进程的审计状态,接着调用audit_free来释放审计相关资源,并设置任务的状态码为传入的code,并通过taskstats_exit来更新当前进程的任务统计信息,并告知用户进程空间。 接下来就到了释放资源的时候了。

代码语言:javascript
复制
 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);
 

这里分别通过调用exit_xx函数释放了诸如内存、文件、文件系统、线程、工作队列、perf_event事件、自动调度组、cgroup等资源。并且如果进程退出(group_dead),则调用disassociate_ctty解除与终端的关联。 至此,进程的相关资源都已经被释放的差不多了,接下来就要做一些收尾的操作。

代码语言:javascript
复制
 /*
  * FIXME: do that only when needed, using sched_exit tracepoint
  */
 // 刷新当前进程的硬件断点信息
 flush_ptrace_hw_breakpoint(tsk);
 // 开启一个RCU临界区
 exit_tasks_rcu_start();
 // 通知父进程已经退出,给子进程寻找新的养父,并把进程的状态设置为僵尸状态
 exit_notify(tsk, group_dead);
 // 向/proc文件系统发出进程退出的事件通知
 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();
 // 释放io上下文
 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);
 // 验证当前进程的安全凭证cred是否合法
 validate_creds_for_do_exit(tsk);
 // 更新当前进程的栈资源统计信息
 exit_task_stack_account(tsk);
 // 检查栈调用是否合法
 check_stack_usage();
 // 禁止抢占
 preempt_disable();
 // 如果进程有脏页的话,就把脏页加到CPU变量中,以后处理
 if (tsk->nr_dirtied)
  __this_cpu_add(dirty_throttle_leaks, tsk->nr_dirtied);
 // 退出RCU临界区
 exit_rcu();
 // 释放RCU临界区
 exit_tasks_rcu_finish();
 // 释放当前进程的锁依赖资源
 lockdep_free_task(tsk);
 // 通知内核当前进程已结束
 do_task_dead();
}

至此,一个进程就已经终结了,但是注意这个进程只是作为一个僵尸进程存在,并没有真正的消亡。它的实体task_struct也即进程描述符仍然存在,需要等待其父进程调用wait来收集它,这个进程才算是真正的消亡了。

小结

总结一下,除去安全等防御性编程外,进程的exit大概做了以下的三件事:

  • 释放资源
  • 通知其他组件该进程已经结束,将子进程等资源托付给其他进程

这其中自然释放资源是占很大的比重的,可以看到我们释放了数十种进程的资源,这个函数才结束。这也不枉费进程的task_struct有着那么多的成员变量,可谓是拖家带口。

小结

本文参与?腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2023-05-10,如有侵权请联系?cloudcommunity@tencent.com 删除

本文分享自 程栩的性能优化笔记 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与?腾讯云自媒体分享计划? ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 进程终结的原因
  • 进程终结全过程
  • 小结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
http://www.vxiaotou.com