news 2026/6/19 11:31:10

一行一行吃透 musl libc 的 fclose 实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
一行一行吃透 musl libc 的 fclose 实现

很多人用了一辈子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; }

一、整体思路:三步走

步骤做什么为什么
① 刷新 + 底层关闭fflush+f->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;

这是很多人忽略的分支。

stdinstdoutstderr这三个流,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_JUMPS

musl 的实现更短、更直白,glibc 的更复杂但功能更多(比如 locale 处理)。


五、你应该记住的三句话

  1. fclose不只是 close(fd),它还负责刷新缓冲区、摘除链表、释放内存。
  2. r |=是工业级错误处理的标准写法,任何一步失败都算失败。
  3. 关闭后的FILE对象是"僵尸"——还挂在链表上,但任何访问都是未定义行为。

参考:musl libc 1.2.5 src/stdio/fclose.c

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/19 11:19:20

从零到一:运算放大器(OPA)核心参数解析与典型电路实战

1. 运算放大器基础:从原理到选型 第一次接触运算放大器时,我完全被那些密密麻麻的参数搞懵了。直到在实验室烧坏三个芯片后,才真正理解这个"电子积木"的奥妙。运算放大器(Operational Amplifier,简称OPA&…

作者头像 李华
网站建设 2026/6/19 11:06:58

路由器急救指南:5步用nmrpflash拯救你的Netgear设备

路由器急救指南:5步用nmrpflash拯救你的Netgear设备 【免费下载链接】nmrpflash Netgear Unbrick Utility 项目地址: https://gitcode.com/gh_mirrors/nmr/nmrpflash 当你的Netgear路由器因为固件升级失败、意外断电或操作失误而"变砖"时&#xff…

作者头像 李华
网站建设 2026/6/19 10:53:48

LEO卫星网络计算与路由联合优化技术解析

1. LEO卫星网络中的计算与路由联合优化:从理论到实践在低地球轨道(LEO)卫星网络中,地球观测数据的高效回传一直是个棘手问题。传统方法主要关注星座内的原始数据路由优化,但随着高光谱成像等技术的进步,单颗卫星每天可产生高达1TB…

作者头像 李华
网站建设 2026/6/19 10:44:13

PowerToys中文汉化版:颠覆Windows效率体验的本地化革命

PowerToys中文汉化版:颠覆Windows效率体验的本地化革命 【免费下载链接】PowerToys-CN PowerToys Simplified Chinese Translation 微软增强工具箱 自制汉化 项目地址: https://gitcode.com/gh_mirrors/po/PowerToys-CN 你是否曾因PowerToys的英文界面而犹豫…

作者头像 李华