musl libc 的 fwrite 到底在干什么?逐行拆解
源码来自 musl libc,这是 Alpine Linux 等轻量发行版的默认 C 库。相比 glibc,它的代码更短、更直觉。这段
__fwritex不到 50 行,却藏着行缓冲、延迟分配、零拷贝等多个设计。
一、先看调用链
fwrite(src, size, nmemb, f)
└─→ FLOCK(f)
└─→ __fwritex(src, size*nmemb, f) ← 核心在这里
└─→ 可能调用 f->write() 直写
└─→ 可能 memcpy 进缓冲区
└─→ FUNLOCK(f)
└─→ 返回 nmemb 或 实际写入个数
fwrite_unlocked = fwrite (弱别名,不加锁)
二、__fwritex 逐行拆解
第 1 行:参数类型
size_t __fwritex(const unsigned char *restrict s, size_t l, FILE *restrict f)
s用unsigned char*而不是void*—— 因为后面要做s[i-1]字节级访问,unsigned char保证不会被符号扩展坑。restrict告诉编译器指针不别名,可以激进优化。
第 2-4 行:延迟分配 + 早期退出
if (!f->wend && __towrite(f)) return 0;
| 条件 | 含义 |
|---|---|
!f->wend |
写缓冲区还没分配 |
__towrite(f) |
当前需要写入(不是只读模式) |
两个都成立 → 直接返回 0,不分配。
这是 延迟分配(lazy allocation) 策略:不写就不分配缓冲区,省内存。
第 6-7 行:缓冲区塞不下?直接 flush
if (l > f->wend - f->wpos) return f->write(f, s, l);
f->wend f->wpos
|-----------------| ← 剩余空间
| 已写入数据 |
如果 l 大于剩余空间,不进缓冲区,直接调用底层 write 系统调用写入。
为什么不先把缓冲区填满再 flush?因为
memcpy+write两次拷贝不如一次write高效。
第 9-23 行:行缓冲的核心逻辑 ⭐
if (f->lbf >= 0) {
/* Match /^(.*\n|)/ */
for (i=l; i && s[i-1] != '\n'; i--);
f->lbf >= 0 表示 行缓冲模式(lbf = line buffered flag)。
这个 for 循环在干什么?
从后往前找最后一个 \n 的位置。
s = "hello\nworld\n"
^ ^
i=6 i=12
循环从 i=12 开始,s[11]='\n' → 找到!i=12
匹配的正则是 /^(.*\n|)/:要么找到一个换行,要么什么都没找到(i=0)。
如果找到了换行(i > 0):
size_t n = f->write(f, s, i); // 先把带换行的部分 flush 掉
if (n < i) return n; // 写入不完整?直接返回,不继续
s += i; // 指针后移
l -= i; // 长度减少
这就是行缓冲的本质:遇到 \n 就触发 flush。
比如 printf("hello\n"); 之所以立即输出,就是这段代码在工作。
第 25-27 行:零拷贝进缓冲区
memcpy(f->wpos, s, l);
f->wpos += l;
剩下的数据(不含换行的部分,或者非行缓冲模式的全部数据),直接 memcpy 进缓冲区。
零拷贝、无循环、无逐字节操作。
第 28 行:返回值
return l + i;
l:最后 memcpy 进缓冲区的字节数i:之前 flush 掉的字节数(含换行)
总共写入的字节数 = 缓冲区内 + 已 flush
三、fwrite 外层包装
size_t l = size * nmemb;
if (!size) nmemb = 0; // 边界:size=0 时避免除零
FLOCK(f);
k = __fwritex(src, l, f);
FUNLOCK(f);
return k == l ? nmemb : k / size;
返回值的处理很精巧:
| 情况 | 返回值 |
|---|---|
全部写入成功(k == l) |
nmemb(元素个数) |
部分写入(k < l) |
k / size(实际写入的元素个数) |
这就是为什么
fwrite返回的是"成功写入的元素个数",而不是字节数。
四、关键设计点总结
| 设计点 | 代码位置 | 收益 |
|---|---|---|
| 延迟分配 | !f->wend && __towrite(f) |
不写不分配,省内存 |
| 大数据直写 | l > f->wend - f->wpos |
避免 buf 满后二次拷贝 |
| 行缓冲 | f->lbf >= 0 + 找 \n |
printf 立即输出的根基 |
| 零拷贝 | memcpy(f->wpos, s, l) |
比逐字节写快一个数量级 |
| 弱别名解锁版 | weak_alias(fwrite, fwrite_unlocked) |
单线程场景省去锁开销 |
五、对比 glibc
glibc 的 fwrite 有 200+ 行,核心差异:
| musl | glibc | |
|---|---|---|
| 行数 | ~50 | ~200+ |
| 行缓冲实现 | 逆序找 \n,一次 write |
逐字符判断,可能多次写 |
| 缓冲区管理 | 简单指针算术 | 复杂的 buffer chain |
| 可读性 | 高 | 低(但功能更全) |
musl 的哲学:够用就好,每一行都有明确目的。
六、一句话总结
__fwritex的本质:能进缓冲区的memcpy,塞不下的或遇到换行的直接write,全程不做无用功。
源码分析基于 musl libc 1.2.x,不同版本细节可能有差异。
更多推荐


所有评论(0)