Linux下基于futex的线程等待原理及libc实现差异疑问
我尝试通过Linux的clone()创建线程并等待其结束,但Linux的wait()仅适用于进程,无法实现线程等待。为此我研究了不同libc中pthread库的实现,编写了调用pthread_join()的测试程序:
void* waited_foo(void* p) { sleep(1); //EDIT printf("1111\n"); return NULL; } int main(int argc, char* agrv[]) { pthread_t tid; pthread_attr_t attr; pthread_attr_init(&attr); if (pthread_create(&tid, &attr, &waited_foo, NULL)) { fprintf(stderr, "Error creating thread\n"); return 1; } pthread_join(tid,NULL); sleep(1); return 0; }
我通过strace追踪所有系统调用:
strace -f -e trace=\!brk,mmap,mprotect,munmap,rt_sigprocmask ./a.out
在musl libc的Alpine Linux上的结果:
clone(child_stack=0x7f9f63666af8, flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID|0x400000strace: Process 163 attached , parent_tid=[163], tls=0x7f9f63666b38, child_tidptr=0x7f9f636fdf90) = 163 [pid 163] nanosleep({tv_sec=1, tv_nsec=0}, <unfinished ...> [pid 162] futex(0x7f9f63666b70, FUTEX_WAIT_PRIVATE, 2, NULL <unfinished ...> [pid 163] <... nanosleep resumed>0x7f9f63666aa0) = 0 [pid 163] ioctl(1, TIOCGWINSZ, {ws_row=39, ws_col=231, ws_xpixel=0, ws_ypixel=0}) = 0 [pid 163] writev(1, [{iov_base="1111", iov_len=4}, {iov_base="\n", iov_len=1}], 21111) = 5 [pid 163] futex(0x7f9f63666b70, FUTEX_WAKE_PRIVATE, 1 <unfinished ...> [pid 162] <... futex resumed>) = 0 [pid 163] <... futex resumed>) = 1 [pid 162] futex(0x7f9f636fdf90, FUTEX_WAIT, 163, NULL <unfinished ...> [pid 163] exit(0) = ? [pid 162] <... futex resumed>) = 0 [pid 163] +++ exited with 0 +++ nanosleep({tv_sec=1, tv_nsec=0}, 0x7fff336faca0) = 0
在GNU libc的Debian Linux上的结果:
clone(child_stack=0x7f4859865e30, flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID, parent_tidptr=0x7f48598669d0, tls=0x7f4859866700, child_tidptr=0x7f48598669d0) = 11383 futex(0x7f48598669d0, FUTEX_WAIT, 11383, NULLstrace: Process 11383 attached <unfinished ...> [pid 11383] set_robust_list(0x7f48598669e0, 24) = 0 [pid 11383] nanosleep({tv_sec=1, tv_nsec=0}, 0x7f4859865d50) = 0 [pid 11383] write(1, "1111\n", 51111) = 5 [pid 11383] madvise(0x7f4859066000, 8368128, MADV_DONTNEED) = 0 [pid 11383] exit(0) = ? [pid 11383] +++ exited with 0 +++ <... futex resumed> ) = 0 nanosleep({tv_sec=1, tv_nsec=0}, 0x7ffd9d9d3460) = 0
疑问解答:
为何musl libc中需要两次futex wait?
第一次futex_wait是musl实现的用户态线程同步:pthread_join会等待线程的退出标志,线程执行完毕后主动调用futex_wake唤醒主线程。第二次futex_wait是等待内核完成CLONE_CHILD_CLEARTID的操作——当线程调用exit后,内核会将clone时指定的child_tidptr地址的值清零,musl会在这里等待该操作完成,确保线程资源完全释放,避免后续操作出现竞态。为何glibc中仅使用一次futex_wait且无对应的futex_wake?
glibc直接利用了Linux内核CLONE_CHILD_CLEARTID的特性:当线程终止时,内核不仅会将child_tidptr指向的内存清零,还会自动对该地址执行FUTEX_WAKE操作。因此pthread_join只需等待child_tidptr对应的futex,线程退出后内核会自动唤醒主线程,无需用户态调用futex_wake。而set_robust_list是glibc用于处理健壮互斥锁的机制,和本次线程等待无关。为何futex_wait要使用新线程的pid作为参数?
这里的pid实际是Linux轻量级线程的tid(线程ID)。FUTEX_WAIT的第三个参数是期望值,只有当futex地址中的值等于这个期望值时,调用才会进入阻塞等待。这样做是为了避免竞态:如果在调用futex_wait之前,线程已经退出,child_tidptr的值已经被内核清零,此时futex_wait会发现值不等于tid,直接返回,不会无意义地阻塞,保证同步逻辑的正确性。
内容的提问来源于stack exchange,提问作者xperious

