Skip to content

Latest commit

 

History

History
267 lines (231 loc) · 11.6 KB

File metadata and controls

267 lines (231 loc) · 11.6 KB

进程环境

进程是操作系统运行程序的一个实例,也是操作系统分配资源的单位。在Linux环境中,每个进程都有独立的进程空间,以便对不同的进程进行隔离,使之不会互相影响。

“活雷锋”exit

当进程正常退出时,会调用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介绍

使用atexit

atexit用于注册进程正常退出时的回调函数。若注册了多个回调函数,最后的调用顺序与注册顺序相反,与我们熟悉的栈操作类似,先入后出。

#include <stdlib.h>
int atexit(void (*function)(void));

atexit的局限性

使用atexit注册的退出函数是在进程正常退出时,才会被调用。这里的正常退出是指,使用exit退出或使用main中最后的return语句退出。若是因为收到信号而导致程序退出,atexit注册的退出函数则不会被调用

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