☰
TCP 缓冲区:用户空间 stdio 与内核 Socket 缓冲
2026/10/3 13:02:24 网站建设 项目流程

TCP 缓冲区:用户空间 stdio 与内核 Socket 缓冲

课程:尚硅谷《嵌入式 Linux 应用层开发》第 6 章 Socket 编程
依据:2026-09-30 10:18 录音转写;课程 PDF 第 290—296 页。
本节边界:从缓冲区的作用讲到 C 标准 I/O 的三种缓冲模式、setvbuf()、fflush()以及execve()实验;第 297 页开始的全缓冲独立实验未进入本次录音。
页码说明:录音从第 290 页开始,实际继续讲到第 296 页,因此按 290—296 页整理。

1. 本节知识路线

为什么需要缓冲

区分三层缓冲

应用自己定义的数组

C 标准库 FILE 缓冲

内核 Socket 收发缓冲

全缓冲 行缓冲 无缓冲

setvbuf 设置模式

fflush 主动刷新

execve 替换进程映像

send 写入发送缓冲

recv 读取接收缓冲

老师通过文件写入实验讲解缓冲模式,再把这个概念联系到网络编程。理解本节时,首先要把不同层次的“缓冲区”分开。

2. 三种容易混淆的缓冲区

假设程序要通过 TCP 发送一段数据,可能同时出现三种缓冲:

层次典型对象位于哪里谁管理
应用缓冲char buf[1024]、malloc()内存用户空间程序员
C 标准库缓冲FILE *背后的 stdio 缓冲用户空间libc,可用setvbuf()调整
Socket 缓冲发送缓冲区、接收缓冲区内核空间内核网络协议栈
应用数组 │ send/write ▼ 内核 Socket 发送缓冲 │ TCP/IP、网卡驱动 ▼ 网络 │ 网卡驱动、TCP/IP ▼ 内核 Socket 接收缓冲 │ recv/read ▼ 应用数组

而教材中的fopen()、fprintf()、setvbuf()、fflush()操作的是C 标准库的FILE *流缓冲,不能直接拿来控制 Socket 的内核收发缓冲。

3. 为什么要有缓冲区

一次函数调用、一次系统调用和一次设备访问都有成本。缓冲区先聚合一批小数据,再成批交给下一层,可以:

  • 减少频繁系统调用;
  • 减少小块设备访问或小包处理;
  • 缓解生产数据和消费数据速度不一致;
  • 让应用与设备、网络协议栈各自按合适节奏工作。

代价是数据可能暂时停留在某一层,因此“函数已经返回”不一定等于数据已经到达最终目标。

4. 网络编程中的内核缓冲区

4.1 接收方向

网卡收到帧

驱动与协议栈处理

Socket 接收缓冲区

recv 或 read

用户空间数组

网络数据先由网卡驱动和协议栈处理,符合该连接的数据进入 Socket 接收缓冲区。应用调用recv()或read(),才把数据复制或交付到用户空间。

默认阻塞模式下:

  • 接收缓冲区暂时没有数据时,recv()通常等待;
  • 有数据时,recv()返回当前可提供的字节;
  • 对端有序关闭发送方向且缓冲数据已经读完时,recv()返回 0。

4.2 发送方向

用户空间数组

send 或 write

Socket 发送缓冲区

TCP 分段和重传

网卡与网络

send()通常把应用数据交给内核 Socket 发送缓冲区。调用成功只表示内核接受了返回值所表示的字节数,并不表示:

  • 数据已经离开网卡;
  • 对端内核已经收到;
  • 对端应用已经执行recv();
  • 对端业务逻辑已经处理完成。

发送缓冲区空间不足时,阻塞 Socket 的send()可能等待;非阻塞 Socket 可能返回-1,并把errno设置为EAGAIN或EWOULDBLOCK。

4.3 应用能否控制内核 Socket 缓冲

录音中“用户无法干预”是入门化说法。应用不能直接读写内核缓冲区内部结构,但可以通过setsockopt()请求调整:

intsize=256*1024;setsockopt(fd,SOL_SOCKET,SO_SNDBUF,&size,sizeof(size));setsockopt(fd,SOL_SOCKET,SO_RCVBUF,&size,sizeof(size));

系统会受默认值、上下限和内核策略约束,实际值应再用getsockopt()查询。当前基础阶段先理解数据路径,不需要急着调大缓冲区。

5. C 标准 I/O 的三种缓冲模式

FILE *流支持三种模式:

模式宏何时把用户空间缓冲交给底层写操作
全缓冲_IOFBF通常在缓冲区满、主动刷新或关闭流时
行缓冲_IOLBF通常在遇到换行、缓冲区满或主动刷新时
无缓冲_IONBF每次 stdio 输出尽快调用底层写操作

常见默认行为:

  • 普通文件流通常采用全缓冲;
  • stdout连接终端时通常采用行缓冲;
  • stderr默认不缓冲;
  • stdout被重定向到普通文件时,通常会变成全缓冲。

“无缓冲”只表示跳过 stdio 的用户空间缓冲。底层仍可能经过内核页缓存、设备缓存或 Socket 缓冲,所以不能理解为直接写到物理介质或直接到达网络对端。

6.setvbuf():设置FILE *的缓冲模式

#include<stdio.h>intsetvbuf(FILE*stream,char*buf,intmode,size_tsize);
参数含义
stream已打开的FILE *流,例如stdout或fopen()返回值
buf用户提供的缓冲内存;传NULL时由 libc 分配
mode_IOFBF、_IOLBF或_IONBF
size缓冲区大小;无缓冲模式通常写 0

返回 0 表示成功,非 0 表示设置失败。

6.1 必须在什么时候调用

setvbuf()应在流打开之后、对该流执行其他读写操作之前调用:

FILE*fp=fopen("testfile.txt","w");if(fp==NULL){perror("fopen");return1;}if(setvbuf(fp,NULL,_IOLBF,BUFSIZ)!=0){fprintf(stderr,"setvbuf failed\n");fclose(fp);return1;}fputs("hello\n",fp);

若自己提供buf,这块内存在流关闭前必须一直有效,不能提前释放,也不能是已经离开作用域的局部数组。

6.2 三种常见写法

setvbuf(fp,NULL,_IOFBF,BUFSIZ);/* 全缓冲 */setvbuf(fp,NULL,_IOLBF,BUFSIZ);/* 行缓冲 */setvbuf(fp,NULL,_IONBF,0);/* 无缓冲 */

教材为buf == NULL的示例多处把size写成 0。对_IONBF这样写很常见;为了让全缓冲和行缓冲意图清楚,练习时使用BUFSIZ更容易理解。

7.fflush():主动刷新用户空间输出缓冲

#include<stdio.h>intfflush(FILE*stream);

对于输出流,fflush(fp)会把该FILE *中尚未提交的用户空间数据交给底层写函数:

if(fflush(fp)==EOF){perror("fflush");}

fflush(NULL)会刷新所有打开的输出流。

7.1fflush()不等于物理落盘

数据路径可能是:

stdio 用户空间缓冲 │ fflush ▼ 内核文件页缓存 │ fsync 等机制 ▼ 存储设备缓存与物理介质

因此:

  • fflush()解决 libc 缓冲尚未交给内核的问题;
  • 需要保证普通文件数据提交给存储设备时,还要检查fflush()、取得文件描述符并按需求调用fsync();
  • 即使调用fsync(),硬件自身缓存和断电保证仍取决于设备与系统设计。

本节实验只验证“文件中是否已经能看到数据”,不讨论断电持久性。

8. 为什么用execve()做实验

intexecve(constchar*path,char*constargv[],char*constenvp[]);

execve()成功后,用新程序替换当前进程的代码、数据、堆和栈,并且不会返回。原程序保存在用户空间的 stdio 缓冲也随原进程映像消失。

默认情况下,未设置FD_CLOEXEC的文件描述符可以跨execve()保留;但FILE *及其 libc 缓冲属于原程序的用户空间状态,新程序不会替原程序自动调用fflush()。

这正是教材实验的观察点:

fopen 创建 FILE 流 ↓ fprintf 写入 libc 缓冲 ↓ 没有 fflush 或 fclose ↓ execve 成功替换程序 ↓ 原 libc 缓冲未写出,文件可能为空

不能简单说execve()“把所有东西全部销毁”:进程 ID 保持不变,很多进程属性和未设置FD_CLOEXEC的文件描述符会保留;被替换的是进程映像及不被规范保留的状态。

9. 实验一:全缓冲数据没有及时写出

下面是教材思路的精简版:

#include<stdio.h>#include<unistd.h>intmain(void){FILE*fp=fopen("testfile.txt","w");if(fp==NULL){perror("fopen");return1;}if(setvbuf(fp,NULL,_IOFBF,BUFSIZ)!=0){fprintf(stderr,"setvbuf failed\n");fclose(fp);return1;}fputs("hello",fp);/* 可能仍停留在 libc 缓冲 */char*argv[]={"true",NULL};char*envp[]={NULL};execve("/usr/bin/true",argv,envp);perror("execve");/* 只有 execve 失败才执行 */fclose(fp);return1;}

若execve()成功,程序没有机会执行fclose(fp);hello又没有触发全缓冲刷新,因此文件可能仍为空。

如果execve()失败,它会返回原程序,后续fclose()会刷新数据,所以观察结果会不同。实验时必须确认execve()确实成功。

10. 实验二:使用fflush()主动刷新

在execve()前增加:

if(fflush(fp)==EOF){perror("fflush");fclose(fp);return1;}

此时hello已经从 stdio 用户空间缓冲提交给底层写操作。即使随后成功执行execve(),文件中也能看到该内容。

11. 实验三:设置无缓冲

在首次写入前设置:

if(setvbuf(fp,NULL,_IONBF,0)!=0){fprintf(stderr,"setvbuf failed\n");fclose(fp);return1;}fputs("hello",fp);

fputs()不再把数据长期留在 stdio 缓冲中,所以在execve()前已经发起底层写操作,文件中能够看到hello。

这不表示每次调用已经物理写入磁盘,只能说明 stdio 层没有继续积压该数据。

12. 实验四:行缓冲是否遇到换行

12.1 没有换行

setvbuf(fp,NULL,_IOLBF,BUFSIZ);fputs("hello",fp);

缓冲区未满、没有换行、没有fflush()或fclose(),随后成功execve(),数据可能没有写出。

12.2 带换行

setvbuf(fp,NULL,_IOLBF,BUFSIZ);fputs("hello\n",fp);

输出换行符会触发行缓冲刷新,因此execve()前数据已经交给底层写操作。

12.3 实验结论表

模式与操作写入内容execve()前是否触发 stdio 刷新文件观察结果
全缓冲hello通常否可能为空
全缓冲 +fflush()hello是可看到hello
无缓冲hello每次尽快写出可看到hello
行缓冲hello通常否可能为空
行缓冲hello\n换行触发可看到一行内容

13. 这和 TCP 程序有什么关系

本节使用普通文件讲FILE *缓冲,但对网络程序有三条直接启发。

13.1send()成功不代表对端收到

数据进入本机内核发送缓冲后,TCP 还要完成分段、发送、确认和可能的重传。若业务需要确认“对方程序已处理”,必须在应用协议中设计响应消息。

13.2recv()一次不保证读到完整消息

Socket 接收缓冲是字节流。数据可能分多次到达,也可能把多次send()的数据一起提供给一次recv()。程序需要循环接收并根据应用协议判断消息边界。

13.3 不要用fflush()控制普通 Socket 描述符

fflush()的参数是FILE *,作用于 stdio 缓冲。普通的 Socket 文件描述符使用send()、write()、recv()、read(),并由内核管理收发缓冲。

虽然可以通过fdopen()把描述符包装成FILE *,但这会额外引入 stdio 缓冲层,基础 Socket 练习阶段不建议这样做。

14. 什么时候数据会自动刷新

对输出FILE *流,常见触发条件包括:

  • 全缓冲区填满;
  • 行缓冲流输出换行符;
  • 显式调用fflush();
  • 调用fclose();
  • exit()或从main()正常返回时,C 运行库关闭并刷新输出流。

以下情况不能依赖自动刷新:

  • 成功调用execve();
  • 调用_exit();
  • 进程收到导致立即终止的信号;
  • 进程崩溃或机器掉电。

录音最后提到“缓冲区会用定时器定期自动刷写”。这不是 C 标准 stdio 三种模式的通用规则,写可移植程序时不能依赖一个未明确说明的定时刷新机制。

15. 常见错误排查

现象优先检查
printf()后终端没有立即显示是否缺少换行;stdout是否被重定向;是否需要fflush(stdout)
文件创建了但内容为空是否仍在 stdio 缓冲;是否未执行fflush()/fclose()就execve()或异常退出
setvbuf()没有效果是否已经对该流执行过其他 I/O;返回值是否非 0
换行没有触发写出流是否确实设置为_IOLBF;是否检查了setvbuf()返回值
fflush()后仍担心掉电丢失fflush()只到内核,按持久性要求考虑fsync()
send()返回正数但对端没打印正返回只表示本机内核接受相应字节;检查对端读取和应用协议
recv()数据不完整TCP 无消息边界;按返回长度累计和解析
Socket 发送阻塞内核发送缓冲可能暂时没有空间;检查对端接收速度与阻塞模式

16. 录音与教材表述校正

  1. 转写中的receive结合代码应为recv();多处刷机应为“刷新”或“刷写”。
  2. fopen()返回FILE *流对象,open()才直接返回整数文件描述符。
  3. setvbuf()的第一个参数是FILE *stream,不是文件描述符。
  4. C 标准库缓冲、应用自建数组和内核 Socket 缓冲属于不同层次。
  5. _IONBF只关闭 stdio 用户空间缓冲,不会绕过内核页缓存或 Socket 缓冲。
  6. fflush()只刷新 C 库用户空间缓冲,不保证物理落盘。
  7. execve()成功后替换进程映像,不会自动刷新旧程序的 stdio 缓冲。
  8. 未设置FD_CLOEXEC的文件描述符默认可以跨execve()保留,但旧FILE *及其缓冲状态不能继续使用。
  9. 普通文件通常全缓冲,终端上的stdout通常行缓冲,stderr默认无缓冲;不能把一种默认模式套到所有流。
  10. Socket 内核缓冲由内核管理,但应用可以用SO_SNDBUF、SO_RCVBUF请求调整大小。
  11. send()返回成功只表示本机内核接受了相应数据,不表示对端应用已收到。
  12. stdio 没有可移植的“等待固定时间就自动刷新”保证,不应依赖录音最后描述的定时刷新。

17. 本节最低掌握标准

学完后应能回答:

  1. 应用数组、stdio 缓冲和内核 Socket 缓冲分别在哪里?
  2. 全缓冲、行缓冲、无缓冲各在什么条件下写出?
  3. 普通文件、终端stdout和stderr常见默认模式分别是什么?
  4. setvbuf()为什么必须在首次读写前调用?
  5. fflush()能否保证数据已经物理落盘?
  6. 为什么execve()前没有刷新的FILE *数据可能丢失?
  7. execve()后文件描述符和FILE *有什么区别?
  8. 为什么_IONBF仍然可能经过内核缓存?
  9. send()返回成功为什么不等于对端应用收到?
  10. recv()为什么可能一次只返回部分数据?

最低实践要求:

  • 能用setvbuf()分别设置三种模式;
  • 能用fflush()检查并刷新输出流;
  • 能复现“无换行不刷、带换行刷出”的行缓冲实验;
  • 能画出 TCP 数据从用户数组到内核缓冲再到网络的路径;
  • 能说明 stdio 缓冲与 Socket 内核缓冲的区别。

18. PDF 页码与录音时间索引

主题PDF 页码录音时间
缓冲区概念与输入、输出缓冲290—29100:01—00:41
全缓冲、行缓冲、无缓冲29100:42—01:28
setvbuf()与fflush()29201:29—02:12
建立fopen、fprintf、execve实验292—29302:13—04:48
全缓冲未刷写现象与fflush()293—29404:49—06:19
_IONBF无缓冲实验293—29406:20—07:12
_IOLBF无换行与带换行实验294—29607:13—07:43
录音末尾的定时刷新说法296 附近07:44—07:58
全缓冲独立实验297 页起本次录音未讲

进一步核对资料:

  • Linux man-pages:setbuf/setvbuf(3)
  • Linux man-pages:fflush(3)
  • Linux man-pages:execve(2)
  • Linux man-pages:send(2)
  • Linux man-pages:recv(2)
  • Linux man-pages:socket(7)

19. 下一步学习

教材第 297—299 页会继续完成全缓冲和手动fflush()实验;随后从第 299 页开始使用netstat观察 TCP 连接建立与断开状态。

建议按这个顺序继续:

完成全缓冲和 fflush 对照实验 ↓ 运行单连接 TCP 服务端和客户端 ↓ 用 ss 或 netstat 观察 LISTEN 与 ESTABLISHED ↓ 理解 send 只是写入本机内核缓冲 ↓ 循环处理部分发送和部分接收

这部分为后续 TCP Server/Client 练习提供运行时理解:程序出现“已经发送但对端没显示”“一次没有接收完整”“发送线程卡住”等现象时,要先判断数据停留在应用缓冲、stdio 缓冲还是内核 Socket 缓冲。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询