摘要本文逐行解析 musl libc 中exit()函数的源码揭示其如何用极简的原子操作解决并发退出和递归调用两大难题以及.fini_array倒序调用的设计意图。一、为什么要看 exit() 的实现exit()是每个 C 程序的终点但很少有人关心它内部做了什么。标准只规定了行为没规定实现。而 musl libc 的实现只有不到 60 行代码却处理了多线程同时调用 exit 的竞态递归调用 exit 的未定义行为.fini_array中所有析构函数的倒序调用与 atexit / stdio 退出钩子的协作看完这段代码你会对简单接口背后的复杂工程有新的理解。二、weak_alias默认什么都不做static void dummy() { } weak_alias(dummy, __funcs_on_exit); weak_alias(dummy, __stdio_exit); weak_alias(dummy, _fini);weak_alias是 musl 的弱符号别名机制。意思是如果别的模块定义了同名强符号就用那个否则就用这个 dummy。所以默认情况下__funcs_on_exit()→ 什么都不做__stdio_exit()→ 什么都不做_fini()→ 什么都不做而atexit.c和__stdio_exit.c会分别覆盖这些弱符号注入真正的退出钩子。这种设计让核心库保持零依赖按需链接。三、__libc_exit_fini倒序遍历 .fini_arrayextern weak hidden void (*const __fini_array_start)(void), (*const __fini_array_end)(void); static void libc_exit_fini(void) { uintptr_t a (uintptr_t)__fini_array_end; for (; a (uintptr_t)__fini_array_start; a - sizeof(void(*)())) (*(void (**)())(a - sizeof(void(*)())))(); _fini(); }这是整段代码中最反直觉的部分。.fini_array是什么编译时每个有全局析构函数的目标文件会把函数指针放进.fini_array段。链接器把所有段拼在一起形成从__fini_array_start到__fini_array_end的连续数组。为什么要倒序遍历因为 C 保证构造函数正序调用析构函数倒序调用栈的 LIFO 语义。musl 严格遵守这个约定地址高 ← __fini_array_end [最后注册的析构函数] [ ... ] 地址低 ← __fini_array_start [最先注册的析构函数]循环从end往start走等价于从后往前调用完全符合语义。最后单独调用_fini()这是另一个弱符号通常由 crt 文件实现处理 C 层面的清理。四、exit() 核心用一个 int 解决并发和递归_Noreturn void exit(int code) { static volatile int exit_lock[1]; int tid __pthread_self()-tid; int prev a_cas(exit_lock, 0, tid); if (prev tid) a_crash(); // 递归调用 → 直接崩溃 else if (prev) for (;;) __sys_pause(); // 有别人在退 → 无限等待 __funcs_on_exit(); __libc_exit_fini(); __stdio_exit(); _Exit(code); }这 10 行是整段代码的精髓。4.1 一个全局锁不用 mutexstatic volatile int exit_lock[1];不是 pthread_mutex_t就是一个普通 int。为什么因为 exit 场景下不需要可重入不需要等待超时只需要谁先抢到谁走其他人等着用原子 CAS 足够了而且零依赖不用拖入整个锁实现。4.2 CAS 抢锁int prev a_cas(exit_lock, 0, tid);prev 0锁空闲当前线程抢到了exit_lock现在存的是tidprev tid当前线程之前已经抢过了 →递归调用→ 标准说这是未定义行为musl 选择直接a_crash()prev ! 0 prev ! tid别的线程抢到了 → 当前线程进入等待4.3 等待策略不是 sleep是 pauseelse if (prev) for (;;) __sys_pause();__sys_pause()是sched_yield/pause系统调用让出 CPU避免忙等。这个设计的潜台词是只有第一个调用 exit 的线程会真正执行清理逻辑其他线程只是等着被杀。这完全合理——进程都要退出了多个线程同时跑清理逻辑只会造成竞争和未定义行为。五、退出顺序一张图看清exit(code) │ ├─ 抢锁CAS──→ 失败pause 等待 │ ├─ __funcs_on_exit() ← atexit 注册的函数由 atexit.c 覆盖 weak_alias │ ├─ __libc_exit_fini() ← 倒序调用 .fini_array _fini() │ ├─ __stdio_exit() ← 刷新/关闭 stdio由 __stdio_exit.c 覆盖 │ └─ _Exit(code) ← 系统调用真正结束进程注意最后调用的是_Exit而不是exit。_Exit不会再触发任何钩子这是终点。六、几个值得思考的点问题musl 的选择对比 glibc退出锁手动 CAS无依赖用内部锁机制递归 exit直接 crash未定义表现不一多线程 exit只有一个线程执行清理其余 pause类似但实现更重.fini_array 遍历指针运算倒序类似但 glibc 代码更多musl 的哲学很清晰能用 10 行解决的绝不写 100 行。七、总结这段exit()实现给我最大的启发是好的底层代码不是什么都处理而是精确地只处理该处理的。并发退出 → 一个 CAS 够了递归退出 → crash 比假装没事更诚实析构顺序 → 倒序遍历指针数组零抽象成本模块协作 → weak_alias 让默认实现为空按需覆盖如果你在写需要高可靠退出的程序比如服务端 daemon这段代码值得反复读。参考musl libc 1.2.5 src/exit.c原创不易转载请注明出处。