Linux进程退出详解(do_exit)--Linux进程的管理与调度(十四)
Linux进程的退出
linux下进程退出的方式
正常退出
- 从main函数返回return
- 调用exit
- 调用_exit
异常退出
- 调用abort
- 由信号终止
_exit, exit和_Exit的区别和联系
_exit是linux系统调用,关闭所有文件描述符,然后退出进程。
exit是c语言的库函数,他最终调用_exit。在此之前,先清洗标准输出的缓存,调用用atexit注册的函数等, 在c语言的main函数中调用return就等价于调用exit。
_Exit是c语言的库函数,自c99后加入,等价于_exit,即可以认为它直接调用_Exit。
基本来说,_Exit(或 _exit,建议使用大写版本)是为 fork 之后的子进程准备的特殊 API。功能见 POSIX 标准:_Exit,讨论见
c - how to exit a child process
由fork()函数创建的子进程分支里,正常情况下使用函数exit()是不正确的,这是因为使用它会导致标准输入输出的缓冲区被清空两次,而且临时文件可能被意外删除。”
因为在 fork 之后,exec 之前,很多资源还是共享的(如某些文件描述符),如果使用 exit 会关闭这些资源,导致某些非预期的副作用(如删除临时文件等)。
「刷新」是对应 flush,意思是把内容从内存缓存写出到文件里,而不仅仅是清空(所以常见的对 stdin 调用 flush 的方法是耍流氓而已)。如果在 fork 的时候父进程内存有缓冲内容,则这个缓冲会带到子进程,并且两个进程会分别 fflush (写出)一次,造成数据重复。参见c - How does fork() work with buffered streams like stdout?
进程退出的系统调用
_exit和exit_group系统调用
_exit系统调用
进程退出由exit系统调用来完成, 这使得内核有机会将该进程所使用的资源释放回系统中
进程终止时,一般是调用exit库函数(无论是程序员显式调用还是编译器自动地把exit库函数插入到main函数的最后一条语句之后)来释放进程所拥有的资源。
exit系统调用的入口点是sys_exit()函数, 需要一个错误码作为参数, 以便退出进程。
其定义是体系结构无关的, 见kernel/exit.c
而我们用户空间的多线程应用程序, 对应内核中就有多个进程, 这些进程共享虚拟地址空间和资源, 他们有各自的进程id(pid), 但是他们的组进程id(tpid)是相同的, 都等于组长(领头进程)的pid
在linux内核中对线程并没有做特殊的处理,还是由task_struct来管理。所以从内核的角度看, 用户态的线程本质上还是一个进程。对于同一个进程(用户态角度)中不同的线程其tgid是相同的,但是pid各不相同。 主线程即group_leader(主线程会创建其他所有的子线程)。如果是单线程进程(用户态角度),它的pid等于tgid。
这个信息我们已经讨论过很多次了
参见
Linux进程ID号–Linux进程的管理与调度(三)
Linux进程描述符task_struct结构体详解–Linux进程的管理与调度(一)
为什么还需要exit_group
我们如果了解linux的线程实现机制的话, 会知道所有的线程是属于一个线程组的, 同时即使不是线程, linux也允许多个进程组成进程组, 多个进程组组成一个会话, 因此我们本质上了解到不管是多线程, 还是进程组起本质都是多个进程组成的一个集合, 那么我们的应用程序在退出的时候, 自然希望一次性的退出组内所有的进程。
因此exit_group就诞生了
group_exit函数会杀死属于当前进程所在线程组的所有进程。它接受进程终止代号作为参数,进程终止代号可能是系统调用exit_group(正常结束)指定的一个值,也可能是内核提供的一个错误码(异常结束)。
因此C语言的库函数exit使用系统调用exit_group来终止整个线程组,库函数pthread_exit使用系统调用_exit来终止某一个线程
_exit和exit_group这两个系统调用在Linux内核中的入口点函数分别为sys_exit和sys_exit_group。
因此exit_group就诞生了
group_exit函数会杀死属于当前进程所在线程组的所有进程。它接受进程终止代号作为参数,进程终止代号可能是系统调用exit_group(正常结束)指定的一个值,也可能是内核提供的一个错误码(异常结束)。
因此C语言的库函数exit使用系统调用exit_group来终止整个线程组,库函数pthread_exit使用系统调用_exit来终止某一个线程
_exit和exit_group这两个系统调用在Linux内核中的入口点函数分别为sys_exit和sys_exit_group。
系统调用声明
声明见include/linux/syscalls.h, line 535
asmlinkage long sys_exit(int error_code);
asmlinkage long sys_exit_group(int error_code);asmlinkage long sys_wait4(pid_t pid, int __user *stat_addr,int options, struct rusage __user *ru);
asmlinkage long sys_waitid(int which, pid_t pid,struct siginfo __user *infop,int options, struct rusage __user *ru);
asmlinkage long sys_waitpid(pid_t pid, int __user *stat_addr, int options);
系统调用号
然后系统调用的实现在kernel/exit.c 中
SYSCALL_DEFINE1(exit, int, error_code)
{do_exit((error_code&0xff)<<8);
}/** this kills every thread in the thread group. Note that any externally* wait4()-ing process will get the correct exit code - even if this* thread is not the thread group leader.*/
SYSCALL_DEFINE1(exit_group, int, error_code)
{do_group_exit((error_code & 0xff) << 8);/* NOTREACHED */return 0;
}
do_exit_group流程
do_group_exit()函数杀死属于current线程组的所有进程。它接受进程终止代码作为参数,进程终止代号可能是系统调用exit_group()指定的一个值,也可能是内核提供的一个错误代号。
该函数执行下述操作
- 检查退出进程的SIGNAL_GROUP_EXIT标志是否不为0,如果不为0,说明内核已经开始为线性组执行退出的过程。在这种情况下,就把存放在current->signal->group_exit_code的值当作退出码,然后跳转到第4步。
- 否则,设置进程的SIGNAL_GROUP_EXIT标志并把终止代号放到current->signal->group_exit_code字段。
- 调用zap_other_threads()函数杀死current线程组中的其它进程。为了完成这个步骤,函数扫描与current->tgid对应的PIDTYPE_TGID类型的散列表中的每PID链表,向表中所有不同于current的进程发送SIGKILL信号,结果,所有这样的进程都将执行do_exit()函数,从而被杀死。
- 调用do_exit()函数,把进程的终止代码传递给它。正如我们将在下面看到的,do_exit()杀死进程而且不再返回。
/** Take down every thread in the group. This is called by fatal signals* as well as by sys_exit_group (below).*/
void
do_group_exit(int exit_code)
{struct signal_struct *sig = current->signal;BUG_ON(exit_code & 0x80); /* core dumps don't get here *//*检查current->sig->flags的SIGNAL_GROUP_EXIT标志是否置位或者current->sig->group_exit_task是否不为NULL*/if (signal_group_exit(sig))exit_code = sig->group_exit_code; /* group_exit_code存放的是线程组终止代码 */else if (!thread_group_empty(current)) { /* 检查线程组链表是否不为空 */struct sighand_struct *const sighand = current->sighand;spin_lock_irq(&sighand->siglock);if (signal_group_exit(sig))/* Another thread got here before we took the lock. */exit_code = sig->group_exit_code;else {sig->group_exit_code = exit_code;sig->flags = SIGNAL_GROUP_EXIT;zap_other_threads(current); /* 遍历整个线程组链表,并杀死其中的每个线程 */}spin_unlock_irq(&sighand->siglock);}do_exit(exit_code);/* NOTREACHED */
}
do_exit流程
进程终止所要完成的任务都是由do_exit函数来处理。
该函数定义在kernel/exit.c中
触发task_exit_nb通知链实例的处理函数
profile_task_exit(tsk);
该函数会定义在触发kernel/profile.c
void profile_task_exit(struct task_struct *task)
{blocking_notifier_call_chain(&task_exit_notifier, 0, task);
}
会触发task_exit_notifier通知, 从而触发对应的处理函数
其中task_exit_notifier被定义如下
// http://lxr.free-electrons.com/source/kernel/profile.c?v=4.6#L134
static BLOCKING_NOTIFIER_HEAD(task_exit_notifier);// http://lxr.free-electrons.com/source/include/linux/notifier.h?v=4.6#L111
#define BLOCKING_NOTIFIER_INIT(name) { \.rwsem = __RWSEM_INITIALIZER((name).rwsem), \.head = NULL }// http://lxr.free-electrons.com/source/include/linux/rwsem.h?v4.6#L74
#define __RWSEM_INITIALIZER(name) \{ .count = RWSEM_UNLOCKED_VALUE, \.wait_list = LIST_HEAD_INIT((name).wait_list), \.wait_lock = __RAW_SPIN_LOCK_UNLOCKED(name.wait_lock) \__RWSEM_OPT_INIT(name) \__RWSEM_DEP_MAP_INIT(name) }
检查进程的blk_plug是否为空
保证task_struct中的plug字段是空的,或者plug字段指向的队列是空的。plug字段的意义是stack plugging
// http://lxr.free-electrons.com/source/include/linux/blkdev.h?v=4.6#L1095
WARN_ON(blk_needs_flush_plug(tsk));
其中blk_needs_flush_plug函数定义在include/linux/blkdev.h, 如下
static inline bool blk_needs_flush_plug(struct task_struct *tsk)
{struct blk_plug *plug = tsk->plug;return plug &&(!list_empty(&plug->list) ||!list_empty(&plug->mq_list) ||!list_empty(&plug->cb_list));
}
OOPS消息
中断上下文不能执行do_exit函数, 也不能终止PID为0的进程。
if (unlikely(in_interrupt()))panic("Aiee, killing interrupt handler!");
if (unlikely(!tsk->pid))panic("Attempted to kill the idle task!");
设定进程可以使用的虚拟地址的上限(用户空间)
/** If do_exit 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.** 设定进程可以使用的虚拟地址的上限(用户空间)* http://lxr.free-electrons.com/ident?v=4.6;i=set_fs*/
set_fs(USER_DS);
这个是一个体系结构相关的代码, 其定义如下
其定义在arch/对应体系/include/asm/uaccess.h中
体系 | 定义 |
---|---|
arm | arch/arm/include/asm/uaccess.h, line 99 |
arm64 | arch/arm64/include/asm/uaccess.h, line 66 |
x86 | arch/x86/include/asm/uaccess.h, line 32 |
通用 | include/asm-generic/uaccess.h, line 28 |
arm64的定义如下
static inline void set_fs(mm_segment_t fs)
{current_thread_info()->addr_limit = fs;/** Enable/disable UAO so that copy_to_user() etc can access* kernel memory with the unprivileged instructions.*/if (IS_ENABLED(CONFIG_ARM64_UAO) && fs == KERNEL_DS)asm(ALTERNATIVE("nop", SET_PSTATE_UAO(1), ARM64_HAS_UAO));elseasm(ALTERNATIVE("nop", SET_PSTATE_UAO(0), ARM64_HAS_UAO,CONFIG_ARM64_UAO));
}
检查进病设置进程程PF_EXITING
首先是检查PF_EXITING标识, 此标识表示进程正在退出,
如果此标识已被设置, 则进一步设置PF_EXITPIDONE标识, 并将进程的状态设置为不可中断状态TASK_UNINTERRUPTIBLE, 并进程一次进程调度
/*current->flags的PF_EXITING标志表示进程正在被删除 */if (unlikely(tsk->flags & PF_EXITING)) { /* 检查PF_EXITING标志是否未被设置 */pr_alert("Fixing recursive fault but reboot is needed!\n");/** We can do this unlocked here. The futex code uses* this flag just to verify whether the pi state* cleanup has been done or not. In the worst case it* loops once more. We pretend that the cleanup was* done as there is no way to return. Either the* OWNER_DIED bit is set by now or we push the blocked* task into the wait for ever nirwana as well.*//* 设置进程标识为PF_EXITPIDONE*/tsk->flags |= PF_EXITPIDONE;/* 设置进程状态为不可中断的等待状态 */set_current_state(TASK_UNINTERRUPTIBLE);/* 调度其它进程 */schedule();}
如果此标识未被设置, 则通过exit_signals来设置
/*tsk->flags |= PF_EXITING;http://lxr.free-electrons.com/source/kernel/signal.c#L2383*/exit_signals(tsk); /* sets tsk->flags PF_EXITING 设置PF_EXITING标志
内存屏障
/** tsk->flags are checked in the futex code to protect against* an exiting task cleaning up the robust pi futexes.*//* 内存屏障,用于确保在它之后的操作开始执行之前,它之前的操作已经完成 */smp_mb();/* 一直等待,直到获得current->pi_lock自旋锁 */raw_spin_unlock_wait(&tsk->pi_lock);
同步进程的mm的rss_stat
/* sync mm's RSS info before statistics gathering */if (tsk->mm)sync_mm_rss(tsk->mm);
获取current->mm->rss_stat.count[member]计数
/*cct_update_integrals - update mm integral fields in task_struct更新进程的运行时间, 获取current->mm->rss_stat.count[member]计数 http://lxr.free-electrons.com/source/kernel/tsacct.c?v=4.6#L152*/acct_update_integrals(tsk);
函数的实现如下, 参见 http://lxr.free-electrons.com/source/kernel/tsacct.c?v=4.6#L156
void acct_update_integrals(struct task_struct *tsk)
{cputime_t utime, stime;unsigned long flags;local_irq_save(flags);task_cputime(tsk, &utime, &stime);__acct_update_integrals(tsk, utime, stime);local_irq_restore(flags);
}
其中task_cputime获取了进程的cpu时间
__acct_update_integr定义如下
参照http://lxr.free-electrons.com/source/kernel/tsacct.c#L125
static void __acct_update_integrals(struct task_struct *tsk,cputime_t utime, cputime_t stime)
{cputime_t time, dtime;u64 delta;if (!likely(tsk->mm))return;time = stime + utime;dtime = time - tsk->acct_timexpd;/* Avoid division: cputime_t is often in nanoseconds already. */delta = cputime_to_nsecs(dtime);if (delta < TICK_NSEC)return;tsk->acct_timexpd = time;/** Divide by 1024 to avoid overflow, and to avoid division.* The final unit reported to userspace is Mbyte-usecs,* the rest of the math is done in xacct_add_tsk.*/tsk->acct_rss_mem1 += delta * get_mm_rss(tsk->mm) >> 10;tsk->acct_vm_mem1 += delta * tsk->mm->total_vm >> 10;
}
清除定时器
group_dead = atomic_dec_and_test(&tsk->signal->live);if (group_dead) {hrtimer_cancel(&tsk->signal->real_timer);exit_itimers(tsk->signal);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); // 释放struct audit_context结构体
释放进程占用的资源
释放线性区描述符和页表
/* 释放存储空间放弃进程占用的mm,如果没有其他进程使用该mm,则释放它。*/exit_mm(tsk);
输出进程会计信息
if (group_dead)acct_process();trace_sched_process_exit(tsk);
释放用户空间的“信号量”
exit_sem(tsk); /* 释放用户空间的“信号量” */
遍历current->sysvsem.undo_list链表,并清除进程所涉及的每个IPC信号量的操作痕迹
释放锁
exit_shm(tsk); /* 释放锁 */
释放文件对象相关资源
exit_files(tsk); /* 释放已经打开的文件 */exit_fs(tsk); /* 释放用于表示工作目录等结构 */
脱离控制终端
if (group_dead)disassociate_ctty(1);
释放命名空间
exit_task_namespaces(tsk); /* 释放命名空间 */
exit_task_work(tsk);
释放task_struct中的thread_struct结构
exit_thread(); /* */
触发thread_notify_head链表中所有通知链实例的处理函数,用于处理struct thread_info结构体
Performance Event功能相关资源的释放
perf_event_exit_task(tsk);
Performance Event功能相关资源的释放
cgroup_exit(tsk);
注销断点
/** FIXME: do that only when needed, using sched_exit tracepoint*/flush_ptrace_hw_breakpoint(tsk);
更新所有子进程的父进程
exit_notify(tsk, group_dead);
进程事件连接器(通过它来报告进程fork、exec、exit以及进程用户ID与组ID的变化)
proc_exit_connector(tsk);
用于NUMA,当引用计数为0时,释放struct mempolicy结构体所占用的内存
#ifdef CONFIG_NUMAtask_lock(tsk);mpol_put(tsk->mempolicy);tsk->mempolicy = NULL;task_unlock(tsk);
#endif
释放struct futex_pi_state结构体所占用的内存
if (tsk->io_context)exit_io_context(tsk);
释放与进程描述符splice_pipe字段相关的资源
if (tsk->splice_pipe)free_pipe_info(tsk->splice_pipe);
if (tsk->task_frag.page)put_page(tsk->task_frag.page);
检查有多少未使用的进程内核栈
check_stack_usage();
调度其它进程
/* causes final put_task_struct in finish_task_switch(). */tsk->state = TASK_DEAD;tsk->flags |= PF_NOFREEZE; /* tell freezer to ignore us *//*重新调度,因为该进程已经被设置成了僵死状态,因此永远都不会再把它调度回来运行了,也就实现了do_exit不会有返回的目标 */schedule();
在设置了进程状态为TASK_DEAD后, 进程进入僵死状态, 进程已经无法被再次调度, 因为对应用程序或者用户空间来说此进程已经死了, 但是尽管进程已经不能再被调度,但系统还是保留了它的进程描述符,这样做是为了让系统有办法在进程终止后仍能获得它的信息。
在父进程获得已终止子进程的信息后,子进程的task_struct结构体才被释放(包括此进程的内核栈)。
转载于:https://www.cnblogs.com/linhaostudy/p/9662144.html
Linux进程退出详解(do_exit)--Linux进程的管理与调度(十四)相关推荐
- Linux /dev目录详解和Linux系统各个目录的作用
Linux /dev目录详解和Linux系统各个目录的作用 标签: linuxtcpfunctionclassfirefoxtimer 2012-01-11 23:08 45517人阅读 评论(2) ...
- 19. linux中权限详解,Linux权限位,读写执行权限真正含义,chmod详解
linux中权限详解,Linux权限位,读写执行权限真正含义,chmod详解 文章目录 Linux权限位 读写执行 三种权限真正含义和作用 权限对文件的作用 权限对目录的作用 示例 chmod 使用数 ...
- Linux ps命令详解,Linux查看进程
「作者主页」:士别三日wyx 「作者简介」:CSDN top100.阿里云博客专家.华为云享专家.网络安全领域优质创作者 ps命令详解 一.常用操作 1.查看所有进程(连带命令行) 2.显示所有包含其 ...
- Linux - top命令详解(监视进程和Linux整体性能)
目录 top启动参数 基础字段说明 第一行,系统任务统计信息: 第二行,进程统计信息: 第三行,CPU统计信息: 第四行,内存统计信息: 第五行,swap交换分区统计信息: 第六行,进程信息 控制界面 ...
- Linux chmod命令详解,Linux修改文件权限
「作者主页」:士别三日wyx 「作者简介」:CSDN top100.阿里云博客专家.华为云享专家.网络安全领域优质创作者 chmod 命令 一.常用操作 1. 字母形式 2. 数字形式 3. 递归设置 ...
- linux系统kill进程,Linux kill命令详解:终止进程
kill 从字面来看,就是用来杀死进程的命令.但是,根据不同的信号,kill 命令可以完成不同的操作. kill 命令格式如下: [root@localhost ~]# kill [信号] PIDki ...
- linux cpu load 详解,理解linux cpu load - 什么时候应该担心了
load average: 0.09, 0.05, 0.01 1 大多数人对这些数都有一个模糊的概念:三个数分别代表了一个随着更长时间上的一个平均值(1分钟, 5分钟, 15分钟). 并且值越小越好. ...
- scsi 教程 linux,SCSI存储详解以及Linux下ISCSI的实现
一,概述 SCSI最初是一种专门为小型计算机系统设计的I/O技术,但由于其架构和协议自身的优点,后被广泛应用于实现DAS以及SAN的底层技术. SCSI的定义: SCSI: Small Compute ...
- Linux rm命令详解,Linux删除文件目录
「作者主页」:士别三日wyx 「作者简介」:CSDN top100.阿里云博客专家.华为云享专家.网络安全领域优质创作者 rm 命令 一.常用操作 1. 删除文件 2. 删除目录 二.其他操作 作用: ...
最新文章
- aix安装bff_AIX的yum安装
- hammer用法 jquery.hammer.js
- 总结面试时没有回答上的内存对齐问题
- 遇到一个gcc编译器版本导致的运行结果有差异的问题
- Git修改提交的用户名和Email
- php+5.3.15下载,Rapid PHP2018
- 2017 年最受欢迎的 10个编程挑战网站
- 操作系统--windows系列之windows8
- python图片鉴黄_深夜,使用NSFW尝试一下图片鉴黄
- 屏蔽点击BackSpace键页面后退
- 网站被降权了怎么办?被降权后的正确处理方法
- Linux三剑客学习笔记
- 【黑灰产犯罪研究】涉物联网犯罪
- [转] 晚上一般什么时候睡觉?
- C语言库函数access的使用
- 丁小平微积分研究成果刍议
- 技术人员如何创业之合伙人的模式
- 浙江工业大学计算机考研科目,浙江工业大学考研专业目录
- 江苏省泰州中学2021年高考成绩查询,泰州中学排名前十名,2021年泰州中学排名一览表...
- 量子密钥分发和BB84协议