进程是操作系统运行程序的一个实例,也是操作系统分配资源的单位。在Linux环境中,每个进程都有独立的进程空间,以便对不同的进程进行隔离,使之不会互相影响。
当进程正常退出时,会调用C库的exit;而当进程崩溃或被kill掉时,C库的exit则不会被调用,只会执行内核退出进程的操作。
linux中有两个终止用户态应用的系统调用:
- exit_group()系统调用,它终止整个线程组,即整个多线程的应用。do_group_exit()是实现这个系统调用的主要内核函数。这是C库函数exit()应该调用的系统调用。
- exit()系统调用,它终止某一个进程,而不管改线程所属线程组中的所有其他进程。do_exit()是实现这个系统调用的主要内核函数。这是被诸如pthread_exit()的Linux线程库的函数所调用的系统调用。
C库的退出函数exit:
void
exit (int status)
{
__run_exit_handlers (status, &__exit_funcs, true);
}
库的exit主要用来执行所有注册的退出函数,比如使用atexit或on_exit注册的函数。执行完注册的退出函数后,__run_exit_handlers
会调用_exit
:
void
_exit (status)
int status;
{
while (1)
{
#ifdef __NR_exit_group
INLINE_SYSCALL (exit_group, 1, status);
#endif
INLINE_SYSCALL (exit, 1, status);
#ifdef ABORT_INSTRUCTION
ABORT_INSTRUCTION;
#endif
}
}
当平台有exit_group时,就调用exit_group,否则就调用exit。从Linux内核2.5.35版本以后,为了支持线程,就有了exit_group。这个系统调用不仅仅是用于退出当前线程,还会让所有线程组的线程全部退出。
系统调用exit_group的实现:
SYSCALL_DEFINE1(exit_group, int, error_code)
{
/* do_group_exit做真正的工作 */
do_group_exit((error_code & 0xff) << 8);
/* NOTREACHED */
return 0;
}
NORET_TYPE void
do_group_exit(int exit_code)
{
struct signal_struct *sig = current->signal;
BUG_ON(exit_code & 0x80); /* core dumps don't get here */
/* 检查该线程组是否正在退出,如果条件为真,则不需要设置线程组退出的条件,直接执行本线程task退出流程do_exit即可 */
if (signal_group_exit(sig))
exit_code = sig->group_exit_code;
else if (!thread_group_empty(current)) { /* 线程组不为空 */
struct sighand_struct *const sighand = current->sighand;
spin_lock_irq(&sighand->siglock);
/* 标准的双重条件检查机制。因为第一次检查signal_group_exit时为假,但是另外一个线程已经拿到锁,并设置了状态。当拿到锁的时候,需要再次检查 */
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;
/* 使用SIGKILL“干掉”线程组的其他线程 */
zap_other_threads(current);
}
spin_unlock_irq(&sighand->siglock);
}
/* 真正的退出动作,退出当前线程task */
do_exit(exit_code);
/* NOTREACHED */
}
下面来看看do_exit的实现:
NORET_TYPE void do_exit(long code)
{
struct task_struct *tsk = current;
int group_dead;
profile_task_exit(tsk);
WARN_ON(blk_needs_flush_plug(tsk));
/* 中断上下文不能使用退出,因为没有进程上下文 */
if (unlikely(in_interrupt()))
panic("Aiee, killing interrupt handler!");
/* pid为0,即内核的idle进程。这个task也是不应该退出的 */
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.
*/
set_fs(USER_DS);
/* 如果task正在被跟踪如gdb,则发送ptrace事件 */
ptrace_event(PTRACE_EVENT_EXIT, code);
validate_creds_for_do_exit(tsk);
/*
* We're taking recursive faults here in do_exit. Safest is to just
* leave this task alone and wait for reboot.
*/
/* 当task退出的时候,会被设置上PF_EXITING标志。如果发现此时flags已经设置了该标志,则说明发生了错误。此时就要按照注释所说的,最安全的方法是什么都不做,通知并等待重启 */
if (unlikely(tsk->flags & PF_EXITING)) {
printk(KERN_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.
*/
tsk->flags |= PF_EXITPIDONE;
/* 将当前task设置为不可中断的状态,然后放弃CPU。 */
set_current_state(TASK_UNINTERRUPTIBLE);
schedule();
}
/*如果当前task是中断线程,即每个CPU中断由一个线程来处理,则设置对应的中断停止来唤醒本线程。这是一个编译选项,默认情况下是关闭的。*/
exit_irq_thread();
/* 给task设置退出标志PF_EXITING */
exit_signals(tsk); /* sets PF_EXITING */
/*
* tsk->flags are checked in the futex code to protect against
* an exiting task cleaning up the robust pi futexes.
*/
smp_mb();
raw_spin_unlock_wait(&tsk->pi_lock);
if (unlikely(in_atomic()))
printk(KERN_INFO "note: %s[%d] exited with preempt_count %d\n",
current->comm, task_pid_nr(current),
preempt_count());
acct_update_integrals(tsk);
/* sync mm's RSS info before statistics gathering */
/* 该task有自己的内存空间 */
if (tsk->mm)
sync_mm_rss(tsk, tsk->mm); //更新内存统计计数
/* 判断整个线程组是否都已经退出。*/
group_dead = atomic_dec_and_test(&tsk->signal->live);
if (group_dead) {
/* 取消高精度定时器 */
hrtimer_cancel(&tsk->signal->real_timer);
/* 删除task的内部定时器,对应系统调用getitimer和setitimer */
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();
if (unlikely(tsk->audit_context))
audit_free(tsk);
/* 设置task的退出值 */
tsk->exit_code = code;
/* 释放任务统计资源 */
taskstats_exit(tsk, group_dead);
/*
释放task的内存空间。task使用的所有内存页都由内核来维护。对于用户程序,如果忘记释放申请的内存,则只会造成用户程序无法再使用该内存,因为内核认为该内存仍然在被用户程序使用。当task退出时,内核会负责释放所有的内存地址。因此当进程退出时,所有申请的内存都会被释放,不会有任何的内存泄漏。
*/
exit_mm(tsk);
if (group_dead)
acct_process();
trace_sched_process_exit(tsk);
/*
检查是否释放了semphore资源,如没有释放则执行semphore的undo操作。这点用于保证在进程意外退出时,能恢复semphore的正确状态,也可以用于预防错误的程序逻辑所导致的semphore释放操作遗漏。
*/
exit_sem(tsk);
/* 释放共享内存 */
exit_shm(tsk);
/*
如果文件资源没有被共享,则释放所有的文件资源。即使用户程序有文件泄漏也不必担心,一旦task退出,文件资源都会得到正确的释放—因为内核维护了所有的、打开的文件。
*/
exit_files(tsk);
/* 释放task的文件系统资源,如当前目录、根目录等*/
exit_fs(tsk);
check_stack_usage();
/* 释放task资源,如TSS段等 */
exit_thread();
/*
* 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);
/* 从控制组退出,并释放相关资源 */
cgroup_exit(tsk, 1);
/* 如果线程组都已经退出,则断开控制终端即tty */
if (group_dead)
disassociate_ctty(1);
/* 后面仍然是一些task退出的清理工作,因与本节关系不大,所以在此不再一一列出了 */
……
}
从exit的源码可以得知,即使应用程序在应用层有内存泄漏或文件句柄泄漏也不必担心,当进程退出时,内核的exit_group调用将会默默地在后面做着清理工作,释放所有内存,关闭所有文件,以及其他资源——当然,前提条件是这些资源是该进程独享的。
atexit用于注册进程正常退出时的回调函数。若注册了多个回调函数,最后的调用顺序与注册顺序相反,与我们熟悉的栈操作类似,先入后出。
#include <stdlib.h>
int atexit(void (*function)(void));
使用atexit注册的退出函数是在进程正常退出时,才会被调用。这里的正常退出是指,使用exit退出或使用main中最后的return语句退出。若是因为收到信号而导致程序退出,atexit注册的退出函数则不会被调用
程序正常退出时,系统就会调用exit。因此,问题的关键就在于exit函数了:
void
exit (int status)
{
__run_exit_handlers (status, &__exit_funcs, true);
}
atexit的实现是依赖于C库的代码的。当进程收到信号时,如果没有注册对应的信号处理函数,那么内核就会执行信号的默认动作,一般是直接终止进程。这时,进程的退出完全由内核来完成,自然不会调用到C库的exit函数,也就无法调用注册的退出函数了。
静态库在链接阶段,会被直接链接进最终的二进制文件中,因此最终生成的二进制文件体积会比较大,但是可以不再依赖于库文件。而动态库并不是被链接到文件中的,只是保存了依赖关系,因此最终生成的二进制文件体积较小,但是在运行阶段需要加载动态库。
其中动态库的一个重要优点就是,可执行程序并不包含动态库中的任何指令,而是在运行时加载动态库并完成调用。这就给我们提供了升级动态库的机会。
只要保证接口不变,使用新版本的动态库替换原来的动态库,就完成了动态库的升级。更新完库文件以后启动的可执行程序都会使用新的动态库。
这样的更新方法只能够影响更新以后启动的程序,对于正在运行的程序则无法产生效果,因为程序在运行时,旧的动态库文件已经加载到内存中了
valgrind作为一个免费且优秀的工具包,提供了很多有用的功能,其中最有名的就是对内存问题的检测和定位。
执行valgrind来检测内存错误:
valgrind --track-fds=yes --leak-check=full --undef-value-errors=yes ./mem_test