2.4 缓冲与 stdio
printf 什么时候真正调用 write?为什么重定向到文件后输出「晚了」?setvbuf 该在什么时候调用?为什么 fork 之后同一行会打印两次?
画出 FILE 的用户态缓冲区与 fd 的关系;按场景选择全缓冲/行缓冲/无缓冲;识别 fread/fwrite 的「项数」语义;复现并修复 fork 导致的重复输出。
2.3 里我们两次提醒「先 fflush 再 dup2/fork」,但没解释为什么。这一章补上:printf 不是系统调用,它只是把字节搬进一块用户态缓冲区;真正调用 write 的时机由缓冲模式决定。理解这块缓冲区,你才能解释「程序崩了但日志最后一行没了」「重定向后输出顺序变了」「fork 后同一行出现两次」。
stdio 是 C 标准库最成功的抽象之一,也是最容易在系统编程里反咬一口的一层。它把「格式化」「缓冲」「编码」都塞进 FILE,让你写 fprintf(f, "%d", x) 而不用拼字符串再 write。代价是:你看到的写入顺序,不等于内核收到的顺序。
这一章只讲 Linux/glibc 上的行为;C 标准给的是下限,glibc 的实现细节(默认缓冲区大小、st_blksize 的使用)会明确标出。
2.4.1 FILE 是什么:一层用户态缓冲
FILE 是不透明结构(glibc 里叫 struct _IO_FILE),你只能通过 FILE * 操作它。它至少有四样东西:
| 成员(概念) | 作用 |
|---|---|
_fileno | 底层的 fd |
_flags | 读/写/追加、EOF、错误、用户缓冲等标志 |
| 读缓冲区 | _IO_read_base/ptr/end:预读进来的数据 |
| 写缓冲区 | _IO_write_base/ptr/end:待写出的数据 |
| 锁 | 多线程下保护这个 FILE(flockfile) |
FILE *f
┌────────────────────────────────┐
│ struct _IO_FILE │
│ _fileno = 3 │
│ _flags = 写 | 错误 | ... │
│ _IO_write_base ──┐ │
│ _IO_write_ptr ──┤ 用户态缓冲区(malloc,通常 4 KiB)
│ _IO_write_end ──┘ │
└────────────────────────────────┘
│ fflush / 缓冲区满 / fclose / exit
▼
write(3, base, ptr - base) ← 系统调用
│
▼
page cache ──► 磁盘
读图:printf 只移动 _IO_write_ptr,不碰内核。只有三种情况会把缓冲区交出去:显式 fflush、缓冲区满、流关闭/进程 exit。所以「printf 之后立刻 kill -9」会丢数据;「printf 之后 write 同一 fd」顺序会乱。
stdin/stdout/stderr 是三个预打开的 FILE *,分别包着 fd 0/1/2。printf(...) 等价于 fprintf(stdout, ...)。
每次 write 都是一次系统调用(约几百纳秒到几微秒)。逐字节 printf 一个 1 MB 的文本会触发上百万次调用。缓冲把「很多小写」合并成「少量大写」,这是 stdio 存在的主要理由。代价是时机不再由你控制,除非你显式 fflush。
2.4.2 三种缓冲模式与默认规则
| 模式 | 常量 | 何时写出 |
|---|---|---|
| 全缓冲 | _IOFBF | 缓冲区满、fflush、fclose、exit |
| 行缓冲 | _IOLBF | 遇到 \n、缓冲区满、fflush、fclose、exit |
| 无缓冲 | _IONBF | 每次 stdio 调用都直接 write |
glibc 的默认规则:
stderr永远无缓冲。错误信息必须立刻可见,哪怕程序随后崩溃。- 连接到终端的流是行缓冲。判断依据是
isatty(fileno(stream))。 - 其他流是全缓冲。缓冲区大小通常取
st_blksize(常见 4096),否则用BUFSIZ(glibc 是 8192)。
这解释了一个常见现象:同一份 printf("hello\n"),在终端上立刻出现;./app > out.txt 之后要等程序退出才写进文件。不是内核变慢了,是 stdio 从行缓冲切成了全缓冲。
# 终端:行缓冲,每行一次 write
strace -e trace=write ./hello
write(1, "line 1\n", 7) = 7
write(1, "line 2\n", 7) = 7
# 重定向到文件:全缓冲,退出时一次 write
strace -e trace=write ./hello > out.txt
write(1, "line 1\nline 2\n", 14) = 14
setvbuf:必须在第一次 I/O 之前
int setvbuf(FILE *stream, char *buf, int mode, size_t size);
void setbuf(FILE *stream, char *buf);
setvbuf 成功返回 0,失败返回非 0(POSIX 允许它总是成功)。mode 是 _IOFBF/_IOLBF/_IONBF;buf 为 NULL 时由 libc 分配;size 为 0 时用默认值。
FILE *f = fopen("app.log", "a");
if (!f) errExit("fopen");
/* 必须在任何 fwrite/fprintf 之前调用 */
if (setvbuf(f, NULL, _IOLBF, 0) != 0)
errExit("setvbuf");
- 「第一次
fprintf之后再setvbuf也行」——标准说必须在任何 I/O 之前;glibc 可能忽略你的设置或行为未定义。fopen之后立刻调用。 - 「
buf可以是局部数组」——缓冲区必须在流关闭前一直有效。传栈上的数组、函数返回后就悬空,后续写会踩坏内存。 - 「
fflush等于落盘」——fflush只把数据交给内核(write),要落盘还得fsync(fileno(f))。 - 「
setbuf(f, NULL)和setvbuf(f, NULL, _IONBF, 0)一样」——setbuf(f, NULL)是无缓冲;setvbuf(..., NULL, _IOFBF, ...)是让 libc 分配全缓冲。参数NULL的含义取决于函数。
用文件大小证明缓冲存在
/* bufsize.c — 观察 stdio 何时真正写文件 */
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <unistd.h>
static long file_size(const char *path)
{
struct stat st;
if (stat(path, &st) == -1)
return -1;
return (long)st.st_size;
}
int main(void)
{
const char *path = "buf_demo.txt";
FILE *f = fopen(path, "w");
if (!f) { perror("fopen"); return 1; }
printf("默认缓冲:写入前 size=%ld\n", file_size(path));
fprintf(f, "line one\nline two\n");
printf("fprintf 之后、fflush 之前 size=%ld\n", file_size(path));
if (fflush(f) == EOF) { perror("fflush"); return 1; }
printf("fflush 之后 size=%ld\n", file_size(path));
if (fclose(f) == EOF) { perror("fclose"); return 1; }
printf("fclose 之后 size=%ld\n", file_size(path));
unlink(path);
return 0;
}
cc -std=c17 -D_GNU_SOURCE -Wall -Wextra -Werror -O2 bufsize.c -o bufsize
./bufsize
# 默认缓冲:写入前 size=0
# fprintf 之后、fflush 之前 size=0
# fflush 之后 size=20
# fclose 之后 size=20
fprintf 之后文件仍是 0 字节,是「缓冲」最直接的证据。fflush 之后才变成 20 字节(两行共 20 字节)。
2.4.3 fflush、fclose 与混用 write
int fflush(FILE *stream); /* 成功 0;失败 EOF,errno 来自底层 write */
int fclose(FILE *stream); /* 成功 0;失败 EOF(可能 flush 失败) */
fflush(NULL) 刷新所有输出流。对输入流调用 fflush 在 C 标准里是未定义行为;POSIX 允许对可寻址输入流用它丢弃缓冲,但不要依赖。
fclose 会先 flush 再 close。必须检查返回值:磁盘满时,fprintf 可能早就返回成功(只是写进了缓冲区),错误直到 fclose 才出现。忽略 fclose 的返回值等于丢掉最后一次写错误。
延迟错误:fprintf 成功,fclose 才报错
/* late_error.c — 写 /dev/full,观察错误延迟到 fflush/fclose */
#include <errno.h>
#include <stdio.h>
#include <string.h>
int main(void)
{
FILE *f = fopen("/dev/full", "w");
if (!f) { perror("fopen"); return 1; }
int rc = fprintf(f, "hello\n");
printf("fprintf 返回 %d,errno=%d\n", rc, errno);
fflush(stdout); /* 让上面的输出先于错误信息 */
int e1 = 0, e2 = 0;
if (fflush(f) == EOF)
e1 = errno;
if (fclose(f) == EOF)
e2 = errno;
printf("fflush errno=%d (%s), fclose errno=%d (%s)\n",
e1, strerror(e1), e2, strerror(e2));
return 0;
}
/dev/full 是「写入永远返回 ENOSPC」的设备。Linux 上的预期输出:
$ ./late_error
fprintf 返回 6,errno=0
fflush errno=28 (No space left on device), fclose errno=0 (Success)
fprintf 返回 6(hello\n 六个字符),errno 仍是 0;错误直到 fflush 才出现。fclose 返回 0,因为缓冲区已经在失败的 fflush 里被丢弃——具体行为依赖实现,所以两个返回值都要看。
fseek 与缓冲区
fseek/fseeko 在写流上会先 flush,在读流上会丢弃缓冲区并调整内核偏移。C 标准要求:同一个流上交替读写时,写后读必须 fflush 或 fseek,读后写必须 fseek(除非读到了 EOF)。不遵守是未定义行为,不是「碰巧能跑」。
与 write 混用:顺序会反过来
printf("A"); /* 进 stdout 缓冲区 */
write(STDOUT_FILENO, "B", 1); /* 直接进内核 */
printf("C"); /* 继续进缓冲区 */
/* 程序退出时 flush:内核看到的顺序是 B A C */
正确写法只有两种:混用前 fflush,或者只用一层。读方向同理:read(0, ...) 绕过 stdio,stdio 缓冲区里预读的数据就丢了;fgets 与 read 混用会出现「少读了一行」。
printf("A");
fflush(stdout); /* 先把 A 交给内核 */
write(STDOUT_FILENO, "B", 1); /* 顺序正确:A B */
printf("C");
- 输出顺序错乱:缓冲区里的旧数据在
write之后才被 flush。 - 输入丢数据:
read把字节读走后,stdio 缓冲区里还留着上一次预读的副本,fgets从副本继续,逻辑上「重复/跳过」。 - 偏移不一致:
lseek(fd)改了内核偏移,但FILE的缓冲区仍按旧位置解释数据。
2.4.4 返回值:字符数、项数、赋值数
stdio 函数的返回值语义不统一,这是 bug 高发区:
| 函数 | 成功返回 | 失败/边界 | 易混点 |
|---|---|---|---|
fprintf/printf | 写出的字符数 | 负数,errno 置位 | 不是写入 fd 的字节数(可能还在缓冲区) |
fscanf/scanf | 成功赋值的项数 | EOF(首个转换前输入失败)、0(匹配失败) | 0 不是错误,是「格式不匹配」 |
getline | 读到的字节数(含 \n) | -1:EOF 或错误 | 必须用 feof/ferror 区分 |
fread/fwrite | 读/写的项数 | 短计数:EOF 或错误 | 不是字节数;size=1 才等于字节数 |
fgetc/getchar | unsigned char 转成的 int | EOF | 必须用 int 接,不能用 char |
fclose | 0 | EOF | flush 失败在这里才暴露 |
fread 的「项数」语义最容易错:
char buf[4096];
size_t n = fread(buf, 1, sizeof buf, f); /* size=1:返回值就是字节数 */
if (n < sizeof buf) {
if (ferror(f)) { perror("fread"); }
else if (feof(f)) { /* 正常读到末尾 */ }
}
struct rec recs[100];
size_t k = fread(recs, sizeof recs[0], 100, f); /* 返回「完整记录」数 */
/* 可能读到一个不完整的尾部记录,它被留在流里或丢弃,取决于实现 */
用 fread(ptr, size, nmemb, f) 时,短计数可能意味着「读到了 k 个完整项 + 半个项」。如果数据是变长的,改用 size=1 自己解析。
fscanf:0 与 EOF 不是一回事
/* scan_demo.c — sscanf 的三种返回值:3、0、EOF */
#include <stdio.h>
static void try(const char *input)
{
int a = -1, b = -1;
char unit[8] = "?";
int rc = sscanf(input, "%d %d %7s", &a, &b, unit);
if (rc == 3)
printf("input=%-10s rc=3 a=%d b=%d unit=%s\n", input, a, b, unit);
else if (rc == 0)
printf("input=%-10s rc=0 匹配失败,a/b 未被赋值\n", input);
else if (rc == EOF)
printf("input=%-10s rc=EOF 首个转换前就失败\n", input);
else
printf("input=%-10s rc=%d 只匹配了部分\n", input, rc);
}
int main(void)
{
try("42 7 ms");
try("oops 7 ms");
try("");
return 0;
}
cc -std=c17 -D_GNU_SOURCE -Wall -Wextra -Werror -O2 scan_demo.c -o scan_demo
./scan_demo
# input=42 7 ms rc=3 a=42 b=7 unit=ms
# input=oops 7 ms rc=0 匹配失败,a/b 未被赋值
# input= rc=EOF 首个转换前就失败
三种结果的含义完全不同:
3:三个转换都成功;0:输入不匹配格式(oops不是整数),流的位置没有前进,直接重试会死循环;EOF:还没开始转换就遇到输入结束或读错误。
%7s 的宽度不能省:没有宽度限制的 %s 会写爆缓冲区。fscanf 循环的正确写法是 while (fscanf(f, "%d", &x) == 1),并在 0 时消费掉当前字符再继续。
完整示例:getline 逐行读
/* linecount.c — getline 读文件,统计行数与最长行 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(int argc, char *argv[])
{
if (argc != 2) { fprintf(stderr, "usage: %s FILE\n", argv[0]); return 1; }
FILE *f = fopen(argv[1], "r");
if (!f) { perror("fopen"); return 1; }
char *line = NULL;
size_t cap = 0;
ssize_t n;
long lines = 0, longest = 0;
while ((n = getline(&line, &cap, f)) != -1) {
lines++;
if (n > longest) longest = n;
}
if (ferror(f)) { /* 必须先区分错误与 EOF */
perror("getline");
free(line);
fclose(f);
return 1;
}
printf("lines=%ld longest=%ld bytes\n", lines, longest);
free(line);
if (fclose(f) == EOF) { perror("fclose"); return 1; }
return 0;
}
cc -std=c17 -D_GNU_SOURCE -Wall -Wextra -Werror -O2 linecount.c -o linecount
printf 'a\nbbb\ncc\n' > /tmp/l.txt && ./linecount /tmp/l.txt
# lines=3 longest=4 bytes
getline 会按需 realloc 缓冲区,调用者只需 free(line)。n 含换行符,所以最长行 4 字节是 bbb\n。循环结束后必须用 ferror 判断是「正常 EOF」还是「读错误」——只看 -1 分不出来。
2.4.5 fork 之后为什么会重复输出
fork 复制的是整个用户地址空间,包括 stdio 缓冲区。如果缓冲区里还有没写出的数据,父子进程各有一份;两者退出时都 flush,同一段字节就被写了两次。
/* fork_dup.c — 复现 fork 复制 stdio 缓冲区;fix 参数给出修复 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
int fix = (argc > 1 && strcmp(argv[1], "fix") == 0);
printf("record:"); /* 没有换行,留在缓冲区 */
if (fix)
fflush(stdout); /* 修复 1:fork 前刷缓冲 */
pid_t pid = fork();
if (pid == -1) { perror("fork"); return 1; }
if (pid == 0) {
if (fix)
_exit(0); /* 修复 2:子进程不 flush 复制来的缓冲 */
exit(0); /* 子进程 flush,写出重复的一份 */
}
waitpid(pid, NULL, 0);
exit(0); /* 父进程也 flush */
}
cc -std=c17 -D_GNU_SOURCE -Wall -Wextra -Werror -O2 fork_dup.c -o fork_dup
./fork_dup > out.txt; cat out.txt
# record:record:
./fork_dup fix > out2.txt; cat out2.txt
# record:
两处修复任选其一即可:fflush 让缓冲区在 fork 前清空;_exit 让子进程跳过 flush。不要只改一处又保留另一处——如果子进程 exit,即使父进程 flush 过,子进程缓冲区里也可能有 fork 之后新写的数据。
- 「
printf带\n就不会重复」——输出到终端时行缓冲会 flush;重定向到文件时是全缓冲,\n不触发 flush,照样重复。测试必须重定向。 - 「
fork会复制 fd,所以不会重复」——fd 复制的是文件描述,缓冲区是用户内存,两回事。 - 「子进程用
exit更好,能刷新数据」——在 fork 场景里恰恰相反:子进程应_exit,否则会把继承来的缓冲再写一遍。 - 「
exec会 flush 缓冲区」——不会。exec直接替换地址空间,未 flush 的数据丢失。fork后exec前要fflush。
正确的心智模型
把 stdio 想成「每个进程一份的写队列」:
fork复制队列,两个进程各写各的;exec丢弃队列;exit刷队列,_exit不刷;fflush是唯一能让你精确控制队列何时变成write的操作。
所以任何「fork/exec/dup2 之前」都应该问一句:这个流的队列里还有东西吗?
2.4.6 fileno、fdopen、freopen
| 函数 | 语义 | 返回/失败 |
|---|---|---|
int fileno(FILE *f) | 取底层 fd | 失败 -1,EBADF |
FILE *fdopen(int fd, const char *mode) | 把已有 fd 包成流 | 失败 NULL,EBADF/EINVAL |
FILE *freopen(const char *path, const char *mode, FILE *f) | 关闭 f 再按路径重开 | 失败 NULL(f 已关闭) |
fdopen 不复制 fd:流关闭时底层 fd 也关闭。要保留原 fd,先 dup:
FILE *f = fdopen(dup(fd), "r"); /* 关闭 f 不影响原 fd */
if (!f) errExit("fdopen");
freopen 的典型用途是重定向标准流:
if (freopen("/var/log/app.log", "a", stdout) == NULL)
errExit("freopen"); /* 之后 printf 都进日志 */
失败时 stdout 已经被关闭,不要再用它。fdopen 的 mode 必须与 fd 的打开模式兼容(只读 fd 不能 "w"),否则行为未定义;"a" 不会改变内核的 O_APPEND,它只是 stdio 的写位置策略。
线程安全:flockfile 与 _unlocked
POSIX 要求 stdio 函数对同一个 FILE 是线程安全的:glibc 在每次 fprintf/fgets 内部加锁。但「两条 fprintf 不被其他线程插进来」需要你自己包住:
flockfile(f);
fprintf(f, "%s %d\n", tag, value); /* 这两行必须原子地出现在文件里 */
fprintf(f, "%s done\n", tag);
funlockfile(f);
已经持锁时,用 putc_unlocked/getc_unlocked 可以省掉重复加锁;单独使用它们(不先 flockfile)不安全。feof/ferror 只在上一次读操作之后才有意义:读到 EOF 后 feof 才为真,clearerr 清除 EOF 与错误标志。
- 输出没出现:先看是否全缓冲;加
fflush或setvbuf(_IONBF)验证。 - 输出顺序乱:查是否混用
write/printf;统一层或每次fflush。 - 最后一行丢了:查是否
_exit/崩溃/exec跳过了 flush。 - 重复输出:查
fork前是否 flush;子进程是否用了exit。 - 写失败没报:查
fclose/fflush返回值;错误可能延迟到最后。
2.4.7 缓冲区大小从哪来:st_blksize、BUFSIZ 与默认规则
这一节回答:fopen 之后那块缓冲区到底多大?为什么同一个程序在终端和文件上的行为不同?
glibc 的 fopen 在第一次 I/O 时决定缓冲区,规则可以概括为三条(Kerrisk 13.2,p223–225):
stderr永远无缓冲,保证错误立刻可见;isatty(fileno(stream))为真时行缓冲,否则全缓冲;- 全缓冲的大小取
fstat的st_blksize(常见 4096),取不到时退回BUFSIZ(glibc 定义为 8192)。
st_blksize 是「这个文件系统上最优 I/O 块大小」,不是页大小。它在不同文件系统上可能不同(ext4 通常 4096,某些网络文件系统更大),所以「缓冲区一定是 4096」是错的。BUFSIZ 只是兜底常量。
这也解释了 2.4.2 的现象:printf("hello\n") 在终端上立刻出现,是因为行缓冲遇到 \n 就 write;重定向到文件后变成全缓冲,\n 不再触发,数据要等缓冲区满、fflush 或 exit。
setvbuf 的 size 什么时候有用
int setvbuf(FILE *stream, char *buf, int mode, size_t size);
buf != NULL:用你提供的size字节内存作为缓冲区;buf == NULL:由 libc 分配,glibc 会忽略size(Kerrisk p195 明确说明)。也就是说setvbuf(f, NULL, _IOFBF, 1<<20)在 glibc 上不会得到 1 MiB 缓冲,只是请求全缓冲;要指定大小必须自己提供buf。
buf 的生命周期必须覆盖流的整个使用期:用 malloc 或静态数组,不能用局部栈数组。setvbuf 必须在任何 I/O 之前调用;成功后返回 0,失败返回非 0(不一定是 -1)。
三种模式的实际差异
| 模式 | 触发 write 的时机 | 典型用途 |
|---|---|---|
_IONBF | 每次 stdio 调用 | stderr、崩溃日志、交互提示 |
_IOLBF | 遇到 \n、缓冲满、fflush | 终端、按行消费的日志 |
_IOFBF | 缓冲满、fflush、fclose、exit | 磁盘文件、批量输出 |
/* bufmodes.c — 三种模式下,未 flush 时文件大小的差异 */
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <unistd.h>
static long size_of(const char *path)
{
struct stat st;
return stat(path, &st) == 0 ? (long)st.st_size : -1;
}
static void try_mode(const char *path, int mode, const char *name)
{
FILE *f = fopen(path, "w");
if (!f) { perror("fopen"); return; }
char *buf = NULL;
if (mode != _IONBF) {
buf = malloc(BUFSIZ);
if (!buf) { fclose(f); return; }
}
if (setvbuf(f, buf, mode, BUFSIZ) != 0) {
fprintf(stderr, "setvbuf(%s) failed\n", name);
free(buf);
fclose(f);
return;
}
fprintf(f, "abc"); /* 无换行,三种模式表现不同 */
printf("%-8s before fflush: size=%ld\n", name, size_of(path));
if (fflush(f) == EOF) perror("fflush");
printf("%-8s after fflush: size=%ld\n", name, size_of(path));
fclose(f);
free(buf);
}
int main(void)
{
printf("BUFSIZ=%d PAGE=%ld\n", BUFSIZ, sysconf(_SC_PAGESIZE));
try_mode("/tmp/buf_none.txt", _IONBF, "IONBF");
try_mode("/tmp/buf_line.txt", _IOLBF, "IOLBF");
try_mode("/tmp/buf_full.txt", _IOFBF, "IOFBF");
unlink("/tmp/buf_none.txt");
unlink("/tmp/buf_line.txt");
unlink("/tmp/buf_full.txt");
return 0;
}
cc -std=c17 -D_GNU_SOURCE -Wall -Wextra -Werror -O2 bufmodes.c -o bufmodes
./bufmodes
# BUFSIZ=8192 PAGE=4096
# IONBF before fflush: size=3
# IONBF after fflush: size=3
# IOLBF before fflush: size=0
# IOLBF after fflush: size=3
# IOFBF before fflush: size=0
# IOFBF after fflush: size=3
IONBF 下 fprintf 立刻变成 write,所以 fflush 前后都是 3 字节;IOLBF 因为没有换行,abc 仍在缓冲区;IOFBF 同理。把 fprintf(f, "abc") 改成 "abc\n",IOLBF 的前一行会变成 4。
- 「
setvbuf(f, NULL, _IOFBF, n)能指定 n 字节缓冲」——glibc 忽略size;要指定大小就自己传buf。 - 「缓冲区默认一定是 4096」——取
st_blksize,不同文件系统可能不同。 - 「
stderr也可以改成全缓冲」——可以,但错误信息会延迟到崩溃前才出现,通常不该改。 - 「
setvbuf可以在fprintf之后调用」——标准要求在第一次 I/O 之前;之后调用行为未定义。 - 「
fflush只对输出流有效」——POSIX 允许对输入流fflush丢弃缓冲;C 标准说未定义,跨平台不要依赖。
2.4.8 setvbuf 家族:setbuf、setbuffer、setlinebuf
这一节回答:setbuf/setbuffer/setlinebuf 与 setvbuf 是什么关系?什么时候该用哪个?
| 函数 | 原型 | 等价于 | 备注 |
|---|---|---|---|
setvbuf | int setvbuf(FILE *, char *, int, size_t) | — | POSIX,返回 0/非 0 |
setbuf | void setbuf(FILE *, char *) | buf ? setvbuf(f, buf, _IOFBF, BUFSIZ) : setvbuf(f, NULL, _IONBF, 0) | 无返回值,无法报错 |
setbuffer | void setbuffer(FILE *, char *, size_t) | setvbuf(f, buf, buf ? _IOFBF : _IONBF, size) | GNU/BSD 扩展 |
setlinebuf | void setlinebuf(FILE *) | setvbuf(f, NULL, _IOLBF, 0) | GNU/BSD 扩展,无返回值 |
setbuf(f, NULL) 是无缓冲,不是「让 libc 分配缓冲」;这一点最容易记反。setbuffer 允许指定大小,但仍然要求缓冲区生命周期覆盖流。
setlinebuf(f) 是「把这个流改成行缓冲」的最短写法,常用于「日志文件希望按行落盘,但不想每次 fflush」的场景。注意行缓冲只保证遇到 \n 时 write,不保证写到磁盘;要落盘仍需 fsync(fileno(f))。
fpurge 与输入缓冲
void __fpurge(FILE *stream); /* glibc 扩展,无返回值 */
int fpurge(FILE *stream); /* BSD 扩展;glibc 不提供,用 __fpurge */
fflush 对输入流的行为在 C 标准里未定义;glibc 的 __fpurge 明确丢弃输入缓冲区,下一次读重新从内核取。交互式程序在 fork/切换输入源时可能需要它,但更稳的做法是不要混用 read 与 stdio 读同一个 fd(2.4.3)。
stdbuf -o0/-oL/-oB cmd 通过 LD_PRELOAD 加载 libstdbuf.so,在程序启动时修改 stdin/stdout/stderr 的缓冲模式。它只能影响这三个标准流,且对已经 setvbuf 过的流无效;对静态链接程序也无效。调试时它很方便,但不要把「生产环境靠 stdbuf 修缓冲」当成方案。
2.4.9 内存流:fmemopen 与 open_memstream
这一节回答:怎样让 fprintf/getline 直接读写内存,而不是文件?
fmemopen:把已有内存块包成流
FILE *fmemopen(void *buf, size_t size, const char *mode);
mode为"r"/"w"/"a"/"r+"等,语义与fopen类似,但作用在内存上;"w"会从buf[0]开始写,不会自动在末尾补\0,除非缓冲区还有空间且你显式写入;"a"的追加位置是buf中第一个\0(不是size末尾);size == 0返回NULL、errno=EINVAL;- 关闭流不会
free(buf),缓冲区归调用者。
它适合「解析一段已经在内存里的文本」:把 buf 包成 FILE *,然后 getline/fscanf,不用手写解析器。
open_memstream:让流自己长
FILE *open_memstream(char **ptr, size_t *sizeloc);
- 初始
*ptr = NULL、*sizeloc = 0; - 写入时 libc 自动
realloc; fflush或fclose之后,*ptr指向缓冲区,*sizeloc是长度(不含结尾\0);fclose会补\0,但*sizeloc不计它;- 缓冲区由 libc 分配,调用者负责
free(*ptr)。
open_wmemstream 是宽字符版本。它们适合「先格式化到内存、再一次性写文件/发送」的场景,避免预估缓冲区大小。
/* memstream.c — open_memstream 生成文本,fmemopen 再按行解析 */
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void)
{
char *buf = NULL;
size_t len = 0;
FILE *out = open_memstream(&buf, &len);
if (!out) { perror("open_memstream"); return 1; }
for (int i = 0; i < 3; i++)
fprintf(out, "line-%d: value=%d\n", i, i * i);
/* 不 fflush 时 buf/len 可能还没更新 */
printf("before fflush: buf=%p len=%zu\n", (void *)buf, len);
if (fflush(out) == EOF) { perror("fflush"); return 1; }
printf("after fflush: len=%zu\n", len);
if (fclose(out) == EOF) { perror("fclose"); return 1; }
printf("after fclose: len=%zu content:\n%s", len, buf);
/* 用 fmemopen 把同一块内存包成输入流,逐行读 */
FILE *in = fmemopen(buf, len, "r");
if (!in) { perror("fmemopen"); free(buf); return 1; }
char *line = NULL;
size_t cap = 0;
ssize_t n;
int lines = 0;
while ((n = getline(&line, &cap, in)) != -1) {
lines++;
printf("parsed[%d] n=%zd %s", lines - 1, n, line);
}
if (ferror(in)) perror("getline");
free(line);
fclose(in);
free(buf);
return 0;
}
cc -std=c17 -D_GNU_SOURCE -Wall -Wextra -Werror -O2 memstream.c -o memstream
./memstream
# before fflush: buf=(nil) len=0
# after fflush: len=48
# after fclose: len=48 content:
# line-0: value=0
# line-1: value=1
# line-2: value=4
# parsed[0] n=16 line-0: value=0
# ...
第一行 buf=(nil) len=0 是关键:open_memstream 的 ptr/sizeloc 只在 flush 后才有意义。不要在 fflush 前读它们,也不要在 fclose 后继续用 FILE *。
- 「
fmemopen会在写完时自动补\0」——只有缓冲区还有空间且你写了\0才有;size用满时没有终止符。 - 「
open_memstream的size包含\0」——不包含;fclose补的\0不计入*sizeloc。 - 「
fmemopen关闭时会释放buf」——不会,缓冲区归调用者。 - 「
open_memstream可以在fflush前读buf」——buf可能是 NULL 或旧值。
2.4.10 getline/getdelim 与定长读的边界
这一节回答:为什么 fgets 读不全一行?getline 的返回值到底含不含 \n?怎样读二进制记录?
ssize_t getline(char **lineptr, size_t *n, FILE *stream);
ssize_t getdelim(char **lineptr, size_t *n, int delim, FILE *stream);
- 返回读到的字节数,包含分隔符(
getline是\n),不含结尾\0; - 返回
-1:EOF 或错误;必须用feof/ferror区分; *lineptr为 NULL 且*n为 0 时由 libc 分配;之后传入的缓冲区会被复用/扩容;- 嵌入的
\0不会截断读取(getline按分隔符而不是字符串结尾工作),返回值仍然是字节数; - 失败:
EINVAL(lineptr或n为 NULL)、ENOMEM。
fgets(buf, size, f) 最多读 size-1 字节,遇到 \n 停止,总是补 \0;如果一行超过 size-1,剩下的部分留在流里,下一次 fgets 继续读。这就是「用 fgets 读长行会得到多条记录」的原因。getline 没有这个限制,代价是可能反复 realloc。
getdelim 适合非换行分隔的记录,例如 \0 分隔的字段、; 分隔的配置。读二进制记录不要用 getline:它遇到 \0 不停止,但返回值语义与「字符串」不同,容易把长度算错;用 fread 按固定长度读,并处理短读。
/* 片段:用 getdelim 读以 ';' 分隔的记录;完整错误处理见 linecount.c */
char *rec = NULL;
size_t cap = 0;
ssize_t n;
while ((n = getdelim(&rec, &cap, ';', f)) != -1) {
/* n 含结尾 ';';rec 以 '\0' 结尾,但记录内可能有 '\0' */
printf("record bytes=%zd\n", n);
}
if (ferror(f)) perror("getdelim");
free(rec);
这段是完整程序的一部分;单独编译需要补 #include <stdio.h>、<stdlib.h> 和 f 的定义。getline 的完整程序见 2.4.4 的 linecount.c。
- 「返回 -1 就是 EOF」——也可能是错误;必须
feof/ferror。 - 「返回值不含
\n」——含;"abc\n"返回 4。 - 「
lineptr可以指向栈数组」——可以,但getline可能realloc它;栈数组不能被realloc,必须用malloc的指针或 NULL。 - 「
getline遇到\0会停」——不会,它按分隔符读;字符串函数会在\0处停,长度要用返回值。 - 「
getline是标准 C」——POSIX.1-2008,不是 C17;需要_POSIX_C_SOURCE>=200809或_GNU_SOURCE。
2.4.11 格式化 I/O 的返回值与安全边界
这一节回答:snprintf 截断时返回什么?asprintf 什么时候该用?%n 为什么危险?
snprintf:返回值是「本该写多少」
int snprintf(char *buf, size_t size, const char *fmt, ...);
int vsnprintf(char *buf, size_t size, const char *fmt, va_list ap);
成功返回若缓冲区足够大本应写入的字符数(不含 \0);截断不是错误。所以判断截断要看 ret >= size(size > 0):
char path[64];
int n = snprintf(path, sizeof path, "%s/%s", dir, name);
if (n < 0) { /* 编码错误 */ }
else if ((size_t)n >= sizeof path) { /* 被截断,需要更大缓冲 */ }
snprintf(NULL, 0, ...) 是合法的,用来计算所需长度(C99)。不要写 strcpy/strcat 拼路径;snprintf 的返回值检查是必须的。
asprintf:让 libc 分配
int asprintf(char **strp, const char *fmt, ...); /* GNU/POSIX.1-2008 扩展 */
成功返回写入字符数,*strp 由 libc malloc,调用者 free;失败返回 -1,*strp 未定义。它省掉「先算长度再分配」的两步,适合长度不可预测的格式化。不要把它用在信号处理器里(会 malloc)。
%n、%s 与 %m
%n把「已写字符数」写进一个int *参数。格式化字符串来自用户输入时,%n可以写任意地址,是经典漏洞;Glibc 的_FORTIFY_SOURCE会在部分场景拒绝%n。永远不要把用户输入当格式串,用printf("%s", user)。%s没有宽度限制时会一直读到\0;用%.*s或%10s限制。%m是 glibc 扩展,打印strerror(errno),不消耗参数;不可移植。scanf的%m是另一个含义:让 libc 分配字符串(GNU 扩展),如%ms;与printf的%m无关。
/* snprintf_demo.c — 截断检测与 asprintf */
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void)
{
char small[8];
int n = snprintf(small, sizeof small, "%s", "0123456789");
printf("snprintf ret=%d size=%zu truncated=%d buf=%s\n",
n, sizeof small, n >= (int)sizeof small, small);
char *dyn = NULL;
int m = asprintf(&dyn, "%s-%d-%s", "build", 42, "linux");
if (m < 0) { perror("asprintf"); return 1; }
printf("asprintf ret=%d str=%s\n", m, dyn);
free(dyn);
/* 正确:用户输入当数据,不当格式串 */
const char *user = "%s%s%n";
printf("safe: %s\n", user);
/* 计算所需长度,不写缓冲区 */
int need = snprintf(NULL, 0, "%s/%s", "/var/log", "app.log");
printf("need=%d\n", need);
return 0;
}
cc -std=c17 -D_GNU_SOURCE -Wall -Wextra -Werror -O2 snprintf_demo.c -o snprintf_demo
./snprintf_demo
# snprintf ret=10 size=8 truncated=1 buf=0123456
# asprintf ret=14 str=build-42-linux
# safe: %s%s%n
# need=16
snprintf 返回 10 但只写了 7 个字符加 \0;truncated=1 是唯一可靠的截断信号。asprintf 返回 14(build5 + -1 + 422 + -1 + linux5 = 14)。
- 「
snprintf截断返回 -1」——返回本应写入的长度;只有编码错误才返回负值。 - 「
snprintf一定补\0」——size > 0时补;size == 0不写任何字节。 - 「
asprintf失败时*strp是 NULL」——不保证;失败后不要读它。 - 「
printf(user_input)只是少写个%s」——这是格式化字符串漏洞,可读栈、可写内存(%n)。 - 「
%m是标准 C」——glibc 扩展;可移植代码用strerror(errno)。
2.4.12 locale 对 stdio 的影响
这一节回答:为什么同一个 printf("%.2f") 在不同语言环境下输出的小数点不同?scanf 会不会因此读错?
C 程序启动时 locale 是 "C",小数点固定为 .。调用 setlocale(LC_ALL, "") 后,printf/scanf 的数值格式、strerror 的错误文本、strftime 的月份名都会随环境变量变化:
#include <locale.h>
setlocale(LC_ALL, ""); /* 按 LANG/LC_* 环境变量 */
printf("%'d\n", 1234567); /* 千分位:1,234,567(取决于 locale) */
printf("%.2f\n", 3.14); /* 德语环境可能输出 3,14 */
locale 不是「一种语言」,而是一组分类,每类独立生效:
| 分类 | 影响的函数 | 典型差异 |
|---|---|---|
LC_CTYPE | isalpha、mbstowcs、printf 的宽字符 | 哪些字节算字母 |
LC_NUMERIC | printf/scanf 的浮点、localeconv | 小数点 . vs , |
LC_TIME | strftime、strptime | 月份/星期缩写 |
LC_COLLATE | strcoll、strxfrm | 排序规则 |
LC_MONETARY | localeconv | 货币符号、千分位 |
LC_MESSAGES | strerror、gettext | 错误文本语言 |
这会带来两个真实问题:
- 序列化不能用 locale 相关的格式。把
3,14写进 JSON/配置文件,其他程序按.解析会出错。写机器可读数据时用"C"locale,或自己格式化整数/用%.17g并确保LC_NUMERIC=C。 scanf的%f受LC_NUMERIC影响。在德语环境读3.14可能只匹配到3,剩下.14留在流里,下一次解析错位。
setlocale 是进程级状态,多线程同时改会竞争;POSIX.1-2008 的 uselocale()/newlocale() 可以给线程设置独立 locale。服务端通常在校验配置时显式 setlocale(LC_ALL, "C"),只在面向用户的输出上使用用户 locale。
strerror 返回的错误文本也受 LC_MESSAGES 影响;日志里想要稳定文本,用 strerror_l(err, locale) 指定 "C" locale。
LC_ALL 覆盖所有分类;否则各 LC_* 分别生效;都没有时用 LANG;最后退回 "C"。locale 命令输出当前分类;locale -a 列出可用 locale。容器里常只有 C/C.UTF-8,程序不要假设 en_US.UTF-8 存在。
2.4.13 非阻塞 fd 上的 stdio:EAGAIN 与 clearerr
这一节回答:把 O_NONBLOCK 的 fd 包成 FILE * 后,fread 会怎样报告「现在没数据」?
stdio 不知道「非阻塞」这回事。fread 内部调用 read,如果返回 -1/EAGAIN,glibc 会:
- 返回 0(或已读到的项数);
- 设置流的错误标志(
ferror为真); - 保留
errno = EAGAIN。
它不是 EOF(feof 为假)。要继续读,必须先 clearerr(f),否则流处于错误状态,后续调用可能直接返回失败。正确循环:
size_t n = fread(buf, 1, sizeof buf, f);
if (n == 0 && ferror(f) && (errno == EAGAIN || errno == EWOULDBLOCK)) {
clearerr(f);
/* 等 epoll 可读,稍后再试 */
}
写方向同理:fwrite 可能只把数据放进 stdio 缓冲并返回成功;真正 write 发生在 fflush,EAGAIN 在 fflush 返回 EOF 时才出现。混用 epoll 与 stdio 很容易漏掉「缓冲区里还有数据」这一层,所以事件驱动程序通常直接用 read/write,只在格式化输出时用 snprintf 拼字符串。
/* stdio_err.c — 非阻塞管道上的 fread:EAGAIN、clearerr、feof 的区别 */
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main(void)
{
int p[2];
if (pipe2(p, O_CLOEXEC | O_NONBLOCK) == -1) { perror("pipe2"); return 1; }
FILE *in = fdopen(p[0], "r");
if (!in) { perror("fdopen"); return 1; }
char buf[64];
errno = 0;
size_t n = fread(buf, 1, sizeof buf, in);
printf("fread empty: n=%zu ferror=%d feof=%d errno=%d (%s)\n",
n, ferror(in), feof(in), errno, strerror(errno));
/* 写入数据后必须 clearerr,否则流仍处于错误状态 */
if (write(p[1], "hello\n", 6) != 6) { perror("write"); return 1; }
clearerr(in);
errno = 0;
n = fread(buf, 1, sizeof buf, in);
printf("after clearerr: n=%zu ferror=%d feof=%d\n", n, ferror(in), feof(in));
if (n > 0) printf("data=%.*s", (int)n, buf);
/* 关闭写端后,下一次读才是 EOF */
close(p[1]);
clearerr(in);
n = fread(buf, 1, sizeof buf, in);
printf("after close writer: n=%zu feof=%d ferror=%d\n",
n, feof(in), ferror(in));
fclose(in);
return 0;
}
cc -std=c17 -D_GNU_SOURCE -Wall -Wextra -Werror -O2 stdio_err.c -o stdio_err
./stdio_err
# fread empty: n=0 ferror=1 feof=0 errno=11 (Resource temporarily unavailable)
# after clearerr: n=6 ferror=0 feof=0
# data=hello
# after close writer: n=0 feof=1 ferror=0
第一行同时出现 ferror=1 与 feof=0,说明「没数据」不是「结束」;第三行才是真正的 EOF。把 clearerr 去掉,第二次 fread 仍会返回 0 并保持错误状态。
- 「
fread返回 0 就是 EOF」——非阻塞 fd 上可能是EAGAIN;看ferror/feof。 - 「
EAGAIN后直接重试fread」——流仍是错误状态,必须先clearerr。 - 「
fwrite成功就写进内核了」——可能只在 stdio 缓冲区;错误在fflush才出现。 - 「
epoll可读就等于fread能读到」——stdio 缓冲区可能还有旧数据,事件触发与缓冲状态不同步。
2.4.14 内核路径:一次 printf 走到哪
这一节回答:从 printf 到磁盘/终端,中间经过哪些函数?为什么 strace 只看到 write?
glibc 内部
| 层次 | 位置 | 动作 |
|---|---|---|
| 格式化 | stdio-common/vfprintf-internal.c | 解析格式串,生成字符到 FILE 缓冲 |
| 写缓冲 | libio/fileops.c | _IO_file_xsputn 把字节拷进 _IO_write_base..ptr |
| 溢出 | libio/fileops.c | _IO_file_overflow → _IO_do_write |
| 系统调用 | libio/fileops.c | _IO_file_write → write/writev |
| 内核 | fs/read_write.c | ksys_write → vfs_write(见 2.2.17) |
| 回写 | mm/page-writeback.c | 脏页由回写线程刷盘;fsync 才等待 |
printf 本身不做系统调用;只有缓冲区满、遇到换行(行缓冲)、fflush、fclose、exit 时才触发 write。strace -e write 看到的是 _IO_file_write 发出的那一次,不是 printf 的每一次。
fclose 的路径
fclose → _IO_new_fclose → _IO_file_close_it → _IO_do_flush(写回)→ _IO_file_close → close(fd) → filp_close → f_op->flush(如 NFS 提交)→ fput。所以 fclose 可能因为 write/close 失败而返回 EOF;错误可能来自磁盘满、NFS 提交失败或配额。
exit 调用 __run_exit_handlers:按注册逆序执行 atexit/on_exit,然后 _IO_cleanup 刷新所有流,最后 _exit。_exit 直接系统调用,不刷 stdio、不跑 atexit。fork 后子进程用 exit 会把继承来的缓冲区再写一遍(2.4.5);用 _exit 才能避免。
2.4.15 两个实验:strace 看 flush 与 stdbuf 改缓冲
这一节回答:怎样用外部工具证明「缓冲存在」,以及怎样在不改代码的情况下改变它?
实验一:终端 vs 文件
# 写一个每行 sleep 的小程序
cat > tick.c <<'EOF'
#include <stdio.h>
#include <unistd.h>
int main(void) {
for (int i = 0; i < 3; i++) { printf("tick %d\n", i); sleep(1); }
return 0;
}
EOF
cc -std=c17 -D_GNU_SOURCE -Wall -Wextra -Werror -O2 tick.c -o tick
# 终端:行缓冲,每行一次 write
strace -e trace=write ./tick
# 重定向:全缓冲,退出时一次 write
strace -e trace=write ./tick > /tmp/tick.out
典型输出:
$ strace -e trace=write ./tick write(1, "tick 0\n", 7) = 7 write(1, "tick 1\n", 7) = 7 write(1, "tick 2\n", 7) = 7 $ strace -e trace=write ./tick > /tmp/tick.out write(1, "tick 0\ntick 1\ntick 2\n", 21) = 21
同一个程序,write 的次数和内容完全不同。strace 的输出是「内核收到了什么」,不是「程序调用了什么」。
实验二:stdbuf 与伪终端
# 强制无缓冲:每行一次 write
stdbuf -o0 ./tick > /tmp/t0.out
strace -e trace=write stdbuf -o0 ./tick > /dev/null
# 强制行缓冲:即使重定向到文件,也每行一次
stdbuf -oL ./tick > /tmp/tl.out
# 用 script 造一个伪终端,让 isatty 为真
script -qc './tick' /dev/null | cat
stdbuf 只对标准流生效,实现方式是 LD_PRELOAD;静态链接或自己 setvbuf 的程序不受影响。script 让程序以为自己连着终端,从而触发行缓冲——这也是「在 CI 里输出顺序和本地不同」的常见原因。
- 输出没出现:
strace -e write看有没有write;没有就是缓冲区没刷。 - 顺序乱:查是否混用
write/printf;fflush(stdout)后再write。 - 最后一行丢了:查是否
_exit/崩溃/exec前没fflush。 - 重复输出:查
fork前是否 flush、子进程是否用exit。 - 写失败没报:查
fflush/fclose返回值,错误可能延迟到最后。
2.4.16 本章陷阱清单
- 现象:
printf后立刻kill -9,日志最后一行没了。原因:数据在用户态缓冲区。修复:关键日志后fflush,或用_IONBF/write。 - 现象:重定向到文件后输出顺序变了。原因:全缓冲,
write先于printf的 flush。修复:混用前fflush,或统一用一层。 - 现象:
fread返回 0,循环退出但数据没读完。原因:短读/EAGAIN被当成 EOF。修复:看ferror/feof,循环处理短读。 - 现象:
fread(buf, sizeof(rec), 100, f)返回 37,却读了 37 条半。原因:返回值是完整项数。修复:变长数据用size=1。 - 现象:
fclose返回EOF,之前fprintf都成功。原因:错误延迟到 flush。修复:检查fflush/fclose。 - 现象:
fork后同一行打印两次。原因:缓冲区被复制,父子各 flush 一次。修复:fork前fflush,子进程_exit。 - 现象:
setvbuf用了局部数组,之后崩溃。原因:缓冲区悬空。修复:用malloc/静态数组,或传 NULL。 - 现象:
snprintf截断但程序没发现。原因:返回值是本应长度。修复:检查ret >= size。 - 现象:
printf("%s", user)被写成printf(user)后崩溃/泄露。原因:格式化字符串漏洞。修复:永远把用户输入当参数。 - 现象:德语环境读
3.14只得到3。原因:LC_NUMERIC用逗号作小数点。修复:机器可读数据固定"C"locale。 - 现象:非阻塞 fd 上
fread一直返回 0。原因:流的错误标志没清。修复:clearerr后再试。 - 现象:
getline返回 -1 就认为读完。原因:可能是读错误。修复:feof/ferror区分。
2.4.17 本章出口检查
| 检查项 | 达标线 | 没达标去哪 |
|---|---|---|
画出 FILE 缓冲区到 write 的路径 | 能说出三种 flush 时机 | 2.4.1 |
| 判断一个流默认是哪种缓冲 | 说出 stderr/终端/文件三条规则 | 2.4.2 |
解释 fread 返回值的单位 | 说出「项数」而非字节 | 2.4.4 |
说出 setvbuf 三种模式与默认规则 | 能解释 st_blksize/BUFSIZ | 2.4.7、2.4.8 |
用 open_memstream 生成文本 | 知道 fflush 后才能读 buf | 2.4.9 |
复现 fork 重复输出 | 能给出 fflush 与 _exit 两种修复 | 2.4.5 |
说出 fflush 与 fsync 的区别 | 前者到内核,后者到磁盘 | 2.4.3 |
Q1 printf("hello\n") 之后立刻 _exit(0),终端上能看到,重定向到文件却看不到。为什么?
Q2 fread(buf, sizeof(struct rec), 100, f) 返回 37。这一定表示读了 37 条完整记录吗?
Q3 fork 前没有 fflush,子进程用 _exit,还会重复输出吗?父进程用 _exit 呢?
Q4 fclose(f) 返回 EOF,但之前的 fprintf 都返回了正数。错误是什么时候发生的?
Q5 setvbuf(f, buf, _IOFBF, sizeof buf) 里的 buf 是局部数组,f 在函数返回后继续使用。会怎样?
答案
A1 终端是行缓冲,\n 触发 flush,数据已经 write 出去;重定向到文件是全缓冲,\n 不触发,_exit 又不 flush,数据还在用户态缓冲区,随进程消失。
A2 不一定。返回值是「完整项」数,可能读了 37 个完整项加半个第 38 项;半个项可能留在流里。变长数据应使用 size=1 或自己解析长度。
A3 子进程 _exit 不 flush,所以不会写出重复的一份;父进程若也 _exit,父子都不 flush,缓冲区里的数据全部丢失(不重复,但也没了)。正确做法是父进程正常 exit 或显式 fflush。
A4 错误发生在 fclose 内部的 flush:fprintf 只写进了用户态缓冲区并返回字符数;真正 write 到磁盘满/配额超限时,fclose 才拿到 ENOSPC。所以必须检查 fclose 的返回值。
A5 缓冲区内存已经失效(栈帧被回收),后续 stdio 写入会踩坏别的栈数据,行为未定义。缓冲区必须用静态数组、堆内存,或传 NULL 让 libc 分配。
母本对应
| 母本 | 章节 | 本书落点 | 程度 |
|---|---|---|---|
| K | 第 13 章(文件 I/O 缓冲:stdio 缓冲、setvbuf、fflush)、第 25 章 25.4(fork/stdio/_exit) | 2.4.1–2.4.8、2.4.14–2.4.15 | 完整(缓冲模式与 fork 交互) |
| POSIX/glibc | getline/getdelim、fmemopen/open_memstream、asprintf | 2.4.9–2.4.13 | 母本未单列,按 POSIX.1-2008 与 glibc 手册 |
| C | 第 10 章 10.10(带缓冲的 I/O) | 2.4.1–2.4.4 | 对照(RIO 与 stdio) |
| O | 第 39 章(文件与目录) | 2.4.1 | 背景(fd 与文件对象) |
| G | — | — | 未用:本章不涉及权限/账号/进程管理 |
| N | — | — | 未用:本章不涉及网络栈 |
下一站:2.5 文件属性、扩展属性与 ACL 从「文件里有什么」转向「文件本身是什么」:
stat的字段、稀疏文件、xattr 与 ACL 的落盘方式。