很多人用了一辈子fclose但从没想过它内部到底在干什么。这篇文章把 musl libc 的fclose实现拆开揉碎逐行讲透。先看完整代码int fclose(FILE *f) { int r; FLOCK(f); r fflush(f); r | f-close(f); FUNLOCK(f); /* 注释见下文 */ if (f-flags F_PERM) return r; __unlist_locked_file(f); FILE **head __ofl_lock(); if (f-prev) f-prev-next f-next; if (f-next) f-next-prev f-prev; if (*head f) *head f-next; __ofl_unlock(); free(f-getln_buf); free(f); return r; }一、整体思路三步走步骤做什么为什么① 刷新 底层关闭fflushf-close把数据落盘释放文件描述符② 从链表中摘除双向链表操作把FILE从全局 open file list 中移除③ 释放内存free回收FILE对象本身看起来简单但每个细节都有讲究。二、逐行拆解第1行FLOCK(f)FLOCK(f);上锁。FILE对象是多线程共享的fclose必须是原子操作。在锁定状态下完成刷新和关闭防止其他线程同时读写。这就是为什么你不能在一个线程fclose的同时另一个线程对同一个FILE*调用fread—— 未定义行为。第2-3行刷新 关闭错误用|合并r fflush(f); r | f-close(f);这是整段代码最精妙的地方之一。fflush(f)把用户态缓冲区的数据刷到内核。失败返回 EOF。f-close(f)调用底层的close(fd)释放文件描述符。失败返回 -1。关键在于r | ...// 假设 fflush 成功(0)close 失败(-1) r 0; r | -1; // r -1 (非零 错误) // 假设 fflush 失败(-1)close 成功(0) r -1; r | 0; // r -1 (非零 错误) // 两者都成功 r 0; r | 0; // r 0 (成功)任何一步出错最终返回值都是非零错误。这符合 POSIX 规范fclose成功返回 0失败返回 EOF。对比 glibc 的实现也是类似的错误合并策略。这是工业级代码的共识。第4行FUNLOCK(f)FUNLOCK(f);解锁。注意此时FILE已经关闭但对象还没释放。为什么这么早解锁看那段核心注释Past this point, f is closed and any further explicit access to it is undefined. However, it still exists as an entry in the open file list...翻译从这里开始f已死但尸体还挂在链表上。后续操作摘除链表、free不需要持有锁因为其他线程不应该再碰这个FILE*了。提前解锁的好处缩短锁持有时间减少竞争。第7行永久文件直接返回if (f-flags F_PERM) return r;这是很多人忽略的分支。stdin、stdout、stderr这三个流flags里有F_PERM标记。它们的FILE对象是静态分配的程序结束才回收不能 free。所以遇到永久文件只做刷新关闭不摘除链表、不 free直接返回。这就是为什么你fclose(stdout)不会 crash但fclose一个malloc出来的FILE*就会 free 掉。第9行__unlist_locked_file(f)__unlist_locked_file(f);这是一个weak alias弱别名static void dummy(FILE *f) { } weak_alias(dummy, __unlist_locked_file);默认是空操作。但如果其他模块比如pthread线程取消处理需要在fclose时做额外清理可以覆盖这个符号。弱别名 钩子机制。默认为空可选覆盖。设计非常优雅。第11-14行双向链表摘除FILE **head __ofl_lock(); if (f-prev) f-prev-next f-next; if (f-next) f-next-prev f-prev; if (*head f) *head f-next; __ofl_unlock();musl 维护了一个全局双向链表__ofl_head所有malloc出来的FILE对象都挂在上面。摘除操作标准三步操作含义f-prev-next f-next前驱的 next 指向后继f-next-prev f-prev后继的 prev 指向前驱*head f-next如果摘的是头节点更新头指针为什么要上锁因为其他线程可能正在遍历这个链表比如fwalk、__fmod。第16-17行释放内存free(f-getln_buf); free(f);getln_buffgets/getline用的行缓冲区单独 malloc 的先释放。fFILE对象本身最后释放。顺序不能反。先放子资源再放父对象。三、核心设计思想总结设计点体现错误合并r最小锁粒度刷新完就解锁链表操作不持锁永久文件保护F_PERM分支防止 free 掉stdin/stdout/stderr弱别名扩展__unlist_locked_file可被线程模块覆盖dead object 容忍注释明确说明链表操作必须能处理已关闭的 FILE四、对比 glibc差在哪对比项muslglibc错误合并r |类似但 glibc 用 永久文件F_PERM标志用_IO_IS_FILEBUF等宏判断链表摘除手动双向链表用_IO_list_all全局锁 链表弱别名钩子weak_alias函数指针表_IO_JUMPSmusl 的实现更短、更直白glibc 的更复杂但功能更多比如 locale 处理。五、你应该记住的三句话fclose不只是 close(fd)它还负责刷新缓冲区、摘除链表、释放内存。r |是工业级错误处理的标准写法任何一步失败都算失败。关闭后的FILE对象是僵尸——还挂在链表上但任何访问都是未定义行为。参考musl libc 1.2.5 src/stdio/fclose.c