析构函数、多线程及进程退出

一、主要的问题

这里主要讨论的是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\n", m_pWhere, (int)getpid());
}
private:
const char *m_pWhere;
};


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

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

S gs("global");

int main()
{
printf("main pid %d\n", (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.11\sysdeps\unix\sysv\linux\_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\nptl\sysdeps\i386\pthreaddef.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.21\kernel\signal.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.21\kernel\signal.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退出

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

posted on 2020-01-14 20:36  tsecer  阅读(2850)  评论(0编辑  收藏  举报

导航