zoukankan      html  css  js  c++  java
  • 析构函数、多线程及进程退出

    一、主要的问题

    这里主要讨论的是C++中全局/静态局部对象析构函数的执行时机问题。我们知道:全局变量的初始化时在main函数执行之前完成,静态局部变量的初始化是在首次执行到所在函数时执行。但是这些对象的析构函数在什么时候执行,它们在多线程中的表象又是如何?
    下面首先看下例子:
    tsecer@harry: cat local.static.destructor.cpp
    #include <stdlib.h>
    #include <sys/types.h>
    #include <sys/syscall.h>
    #include <unistd.h>
    #include <pthread.h>
    #include <stdio.h>
    int gettid()
    {
    return syscall(__NR_gettid);
    }

    struct S
    {
    S(const char *pWhere)
    :m_pWhere(pWhere)
    {

    }

    ~S()
    {
    printf("%s %d ", m_pWhere, (int)getpid());
    }
    private:
    const char *m_pWhere;
    };


    void * threadfn(void *)
    {
    printf("thread pid %d ", (int)gettid());
    static S s(__func__);
    S ss(__func__);

    while(1) sleep(1);
    return NULL;
    }

    S gs("global");

    int main()
    {
    printf("main pid %d ", (int)gettid());
    pthread_t stThread;
    pthread_attr_t attr;
    pthread_attr_init(&attr);
    pthread_create(&stThread, &attr, threadfn, NULL);

    static S s(__func__);
    sleep(2);
    exit(1);
    return 0;

    }
    tsecer@harry: g++ -g local.static.destructor.cpp -lpthread
    tsecer@harry: ./a.out
    main pid 15209
    thread pid 15210
    threadfn 15209
    main 15209
    global 15209
    tsecer@harry:
    这个例子中有两个值得注意的地方:
    a、析构函数的执行顺序和构造函数的执行顺序相反。
    b、析构函数均在主线程中执行。

    2、析构函数何时执行

    通过反汇编代码,可以明显的看到;当执行一个全局/静态变量的构造函数时,会顺便执行对于析构函数的注册,注册使用的方法就是atexit函数。下面可以看到在执行S变量构造函数的时候都通过__cxa_atexit向C库注册了在用户执行exit这个"C库函数"(不是exit系统调用)后执行对象的析构函数。
    (gdb) disas __static_initialization_and_destruction_0
    Dump of assembler code for function __static_initialization_and_destruction_0(int, int):
    0x0000000000400b3f <+0>: push %rbp
    0x0000000000400b40 <+1>: mov %rsp,%rbp
    0x0000000000400b43 <+4>: sub $0x10,%rsp
    0x0000000000400b47 <+8>: mov %edi,-0x4(%rbp)
    0x0000000000400b4a <+11>: mov %esi,-0x8(%rbp)
    0x0000000000400b4d <+14>: cmpl $0x1,-0x4(%rbp)
    0x0000000000400b51 <+18>: jne 0x400b7f <__static_initialization_and_destruction_0(int, int)+64>
    0x0000000000400b53 <+20>: cmpl $0xffff,-0x8(%rbp)
    0x0000000000400b5a <+27>: jne 0x400b7f <__static_initialization_and_destruction_0(int, int)+64>
    0x0000000000400b5c <+29>: mov $0x400c93,%esi
    0x0000000000400b61 <+34>: mov $0x602098,%edi
    0x0000000000400b66 <+39>: callq 0x400b96 <S::S(char const*)>
    0x0000000000400b6b <+44>: mov $0x400c68,%edx
    0x0000000000400b70 <+49>: mov $0x602098,%esi
    0x0000000000400b75 <+54>: mov $0x400bb0,%edi
    0x0000000000400b7a <+59>: callq 0x400860 <__cxa_atexit@plt>
    0x0000000000400b7f <+64>: leaveq
    0x0000000000400b80 <+65>: retq
    End of assembler dump.
    (gdb) info symbol 0x400bb0
    S::~S() in section .text
    (gdb) info symbol 0x602098
    gs in section .bss
    (gdb) info symbol 0x400c68
    __dso_handle in section .rodata
    (gdb) disas threadfn
    Dump of assembler code for function threadfn(void*):
    0x00000000004009f5 <+0>: push %rbp
    0x00000000004009f6 <+1>: mov %rsp,%rbp
    0x00000000004009f9 <+4>: push %rbx
    0x00000000004009fa <+5>: sub $0x28,%rsp
    0x00000000004009fe <+9>: mov %rdi,-0x28(%rbp)
    0x0000000000400a02 <+13>: callq 0x4009e0 <gettid()>
    0x0000000000400a07 <+18>: mov %eax,%esi
    0x0000000000400a09 <+20>: mov $0x400c77,%edi
    0x0000000000400a0e <+25>: mov $0x0,%eax
    0x0000000000400a13 <+30>: callq 0x400810 <printf@plt>
    0x0000000000400a18 <+35>: mov $0x6020a0,%eax
    0x0000000000400a1d <+40>: movzbl (%rax),%eax
    0x0000000000400a20 <+43>: test %al,%al
    0x0000000000400a22 <+45>: jne 0x400a64 <threadfn(void*)+111>
    0x0000000000400a24 <+47>: mov $0x6020a0,%edi
    0x0000000000400a29 <+52>: callq 0x400820 <__cxa_guard_acquire@plt>
    0x0000000000400a2e <+57>: test %eax,%eax
    0x0000000000400a30 <+59>: setne %al
    0x0000000000400a33 <+62>: test %al,%al
    0x0000000000400a35 <+64>: je 0x400a64 <threadfn(void*)+111>
    0x0000000000400a37 <+66>: mov $0x400c9a,%esi
    0x0000000000400a3c <+71>: mov $0x6020b0,%edi
    0x0000000000400a41 <+76>: callq 0x400b96 <S::S(char const*)>
    0x0000000000400a46 <+81>: mov $0x6020a0,%edi
    0x0000000000400a4b <+86>: callq 0x400890 <__cxa_guard_release@plt>
    0x0000000000400a50 <+91>: mov $0x400c68,%edx
    0x0000000000400a55 <+96>: mov $0x6020b0,%esi
    0x0000000000400a5a <+101>: mov $0x400bb0,%edi
    0x0000000000400a5f <+106>: callq 0x400860 <__cxa_atexit@plt>
    0x0000000000400a64 <+111>: lea -0x20(%rbp),%rax
    0x0000000000400a68 <+115>: mov $0x400c9a,%esi
    0x0000000000400a6d <+120>: mov %rax,%rdi
    0x0000000000400a70 <+123>: callq 0x400b96 <S::S(char const*)>
    0x0000000000400a75 <+128>: mov $0x1,%edi
    0x0000000000400a7a <+133>: callq 0x4008b0 <sleep@plt>
    0x0000000000400a7f <+138>: jmp 0x400a75 <threadfn(void*)+128>
    0x0000000000400a81 <+140>: mov %rax,%rbx
    0x0000000000400a84 <+143>: lea -0x20(%rbp),%rax
    0x0000000000400a88 <+147>: mov %rax,%rdi
    0x0000000000400a8b <+150>: callq 0x400bb0 <S::~S()>
    0x0000000000400a90 <+155>: mov %rbx,%rax
    0x0000000000400a93 <+158>: mov %rax,%rdi
    0x0000000000400a96 <+161>: callq 0x4008e0 <_Unwind_Resume@plt>
    End of assembler dump.
    (gdb) disas main
    Dump of assembler code for function main():
    0x0000000000400a9b <+0>: push %rbp
    0x0000000000400a9c <+1>: mov %rsp,%rbp
    0x0000000000400a9f <+4>: sub $0x40,%rsp
    0x0000000000400aa3 <+8>: callq 0x4009e0 <gettid()>
    0x0000000000400aa8 <+13>: mov %eax,%esi
    0x0000000000400aaa <+15>: mov $0x400c86,%edi
    0x0000000000400aaf <+20>: mov $0x0,%eax
    0x0000000000400ab4 <+25>: callq 0x400810 <printf@plt>
    0x0000000000400ab9 <+30>: lea -0x40(%rbp),%rax
    0x0000000000400abd <+34>: mov %rax,%rdi
    0x0000000000400ac0 <+37>: callq 0x4008c0 <pthread_attr_init@plt>
    0x0000000000400ac5 <+42>: lea -0x40(%rbp),%rsi
    0x0000000000400ac9 <+46>: lea -0x8(%rbp),%rax
    0x0000000000400acd <+50>: mov $0x0,%ecx
    0x0000000000400ad2 <+55>: mov $0x4009f5,%edx
    0x0000000000400ad7 <+60>: mov %rax,%rdi
    0x0000000000400ada <+63>: callq 0x400880 <pthread_create@plt>
    0x0000000000400adf <+68>: mov $0x6020a8,%eax
    0x0000000000400ae4 <+73>: movzbl (%rax),%eax
    0x0000000000400ae7 <+76>: test %al,%al
    0x0000000000400ae9 <+78>: jne 0x400b2b <main()+144>
    0x0000000000400aeb <+80>: mov $0x6020a8,%edi
    0x0000000000400af0 <+85>: callq 0x400820 <__cxa_guard_acquire@plt>
    0x0000000000400af5 <+90>: test %eax,%eax
    0x0000000000400af7 <+92>: setne %al
    0x0000000000400afa <+95>: test %al,%al
    0x0000000000400afc <+97>: je 0x400b2b <main()+144>
    0x0000000000400afe <+99>: mov $0x400ca3,%esi
    0x0000000000400b03 <+104>: mov $0x6020b8,%edi
    0x0000000000400b08 <+109>: callq 0x400b96 <S::S(char const*)>
    0x0000000000400b0d <+114>: mov $0x6020a8,%edi
    0x0000000000400b12 <+119>: callq 0x400890 <__cxa_guard_release@plt>
    0x0000000000400b17 <+124>: mov $0x400c68,%edx
    0x0000000000400b1c <+129>: mov $0x6020b8,%esi
    0x0000000000400b21 <+134>: mov $0x400bb0,%edi
    0x0000000000400b26 <+139>: callq 0x400860 <__cxa_atexit@plt>
    0x0000000000400b2b <+144>: mov $0x2,%edi
    0x0000000000400b30 <+149>: callq 0x4008b0 <sleep@plt>
    0x0000000000400b35 <+154>: mov $0x1,%edi
    0x0000000000400b3a <+159>: callq 0x400840 <exit@plt>
    End of assembler dump.
    (gdb)

    3、C库中pthread_exit和exit函数的区别

    其实这里想讨论的问题是:在多线程环境下,调用exit退出进程,其它线程如何退出?
    可以看到,在用户态的C库执行的exit最终执行的是exit_group系统调用,
    glibc-2.11sysdepsunixsysvlinux\_exit.c
    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
    }
    }
    而执行pthread_exit函数执行的系统调用是__NR_exit系统调用
    glibc-2.11 ptlsysdepsi386pthreaddef.h
    /* While there is no such syscall. */
    #define __exit_thread_inline(val)
    while (1) {
    if (__builtin_constant_p (val) && (val) == 0)
    asm volatile ("xorl %%ebx, %%ebx; int $0x80" :: "a" (__NR_exit));
    else
    asm volatile ("movl %1, %%ebx; int $0x80"
    :: "a" (__NR_exit), "r" (val));
    }

    4、操作系统对两者实现的区别

    这里可以看到,如果通过exit_group系统调用,此时会给进程中的每个线程发送一个SIGKILL信号导致进程退出。这个也就是C库中exit调用巨大杀伤力的来源,因为它会导致线程组中所有线程的退出。
    linux-2.6.21kernelsignal.c
    /*
    * Nuke all other threads in the group.
    */
    void zap_other_threads(struct task_struct *p)
    {
    struct task_struct *t;

    p->signal->flags = SIGNAL_GROUP_EXIT;
    p->signal->group_stop_count = 0;

    if (thread_group_empty(p))
    return;

    for (t = next_thread(p); t != p; t = next_thread(t)) {
    /*
    * Don't bother with already dead threads
    */
    if (t->exit_state)
    continue;

    /*
    * We don't want to notify the parent, since we are
    * killed as part of a thread group due to another
    * thread doing an execve() or similar. So set the
    * exit signal to -1 to allow immediate reaping of
    * the process. But don't detach the thread group
    * leader.
    */
    if (t != p->group_leader)
    t->exit_signal = -1;

    /* SIGKILL will be handled before any pending SIGSTOP */
    sigaddset(&t->pending.signal, SIGKILL);
    signal_wake_up(t, 1);
    }
    }

    5、屏蔽掉SIGKILL会怎样

    linux-2.6.21kernelsignal.c
    这个问题本身是错误的:因为SIGKILL不能被屏蔽。
    #define SIG_KERNEL_ONLY_MASK (
    M(SIGKILL) | M(SIGSTOP) )
    #define sig_kernel_only(sig)
    (((sig) < SIGRTMIN) && T(sig, SIG_KERNEL_ONLY_MASK))
    int do_sigaction(int sig, struct k_sigaction *act, struct k_sigaction *oact)
    {
    struct k_sigaction *k;
    sigset_t mask;

    if (!valid_signal(sig) || sig < 1 || (act && sig_kernel_only(sig)))
    return -EINVAL;
    ……
    }

    6、这意味着什么

    其实这意味着对于静态局部变量,它的析构可能是在线程退出之前完成的。也就是说,在多线程下,可能某个线程在使用单件对象时,这个单件对象可能是已经被析构了。这种场景发生在:
    主线程                                                                              非主线程
    调用C库的exit函数
    执行atexit注册的对象析构函数
                                                                                            访问单件对象(此时可能会访问到已经析构的单件对象)
    进程exit_group系统调用、SIGKILL
                                                                                            收到SIGKILL退出

    当然,这种场景是偶现的,但是有隐患,特别是在析构函数可能比较耗时的情况下。

  • 相关阅读:
    为什么大多数IOC容器使用ApplicationContext,而不用BeanFactory
    重温Java泛型,带你更深入地理解它,更好的使用它!
    看完了这篇,面试的时候人人都能单手撸冒泡排序!
    JAVA基础4---序列化和反序列化深入整理(Hessian序列化)
    VS Code 变身小霸王游戏机!
    equals()方法和hashCode()方法详解
    openFeign远程调用时使用Mybatis-plus的IPage接口进行返回分页数据失败的记录
    通过express快速搭建一个node服务
    UML 类图
    jdk命令行工具系列——检视阅读
  • 原文地址:https://www.cnblogs.com/tsecer/p/12193976.html
Copyright © 2011-2022 走看看