ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

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

TCP 缓冲区:用户空间 stdio 与内核 Socket 缓冲 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 或 writeSocket 发送缓冲区TCP 分段和重传网卡与网络send()通常把应用数据交给内核 Socket 发送缓冲区。调用成功只表示内核接受了返回值所表示的字节数并不表示数据已经离开网卡对端内核已经收到对端应用已经执行recv()对端业务逻辑已经处理完成。发送缓冲区空间不足时阻塞 Socket 的send()可能等待非阻塞 Socket 可能返回-1并把errno设置为EAGAIN或EWOULDBLOCK。4.3 应用能否控制内核 Socket 缓冲录音中“用户无法干预”是入门化说法。应用不能直接读写内核缓冲区内部结构但可以通过setsockopt()请求调整intsize256*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 *的缓冲模式#includestdio.hintsetvbuf(FILE*stream,char*buf,intmode,size_tsize);参数含义stream已打开的FILE *流例如stdout或fopen()返回值buf用户提供的缓冲内存传NULL时由 libc 分配mode_IOFBF、_IOLBF或_IONBFsize缓冲区大小无缓冲模式通常写 0返回 0 表示成功非 0 表示设置失败。6.1 必须在什么时候调用setvbuf()应在流打开之后、对该流执行其他读写操作之前调用FILE*fpfopen(testfile.txt,w);if(fpNULL){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()主动刷新用户空间输出缓冲#includestdio.hintfflush(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. 实验一全缓冲数据没有及时写出下面是教材思路的精简版#includestdio.h#includeunistd.hintmain(void){FILE*fpfopen(testfile.txt,w);if(fpNULL){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. 录音与教材表述校正转写中的receive结合代码应为recv()多处刷机应为“刷新”或“刷写”。fopen()返回FILE *流对象open()才直接返回整数文件描述符。setvbuf()的第一个参数是FILE *stream不是文件描述符。C 标准库缓冲、应用自建数组和内核 Socket 缓冲属于不同层次。_IONBF只关闭 stdio 用户空间缓冲不会绕过内核页缓存或 Socket 缓冲。fflush()只刷新 C 库用户空间缓冲不保证物理落盘。execve()成功后替换进程映像不会自动刷新旧程序的 stdio 缓冲。未设置FD_CLOEXEC的文件描述符默认可以跨execve()保留但旧FILE *及其缓冲状态不能继续使用。普通文件通常全缓冲终端上的stdout通常行缓冲stderr默认无缓冲不能把一种默认模式套到所有流。Socket 内核缓冲由内核管理但应用可以用SO_SNDBUF、SO_RCVBUF请求调整大小。send()返回成功只表示本机内核接受了相应数据不表示对端应用已收到。stdio 没有可移植的“等待固定时间就自动刷新”保证不应依赖录音最后描述的定时刷新。17. 本节最低掌握标准学完后应能回答应用数组、stdio 缓冲和内核 Socket 缓冲分别在哪里全缓冲、行缓冲、无缓冲各在什么条件下写出普通文件、终端stdout和stderr常见默认模式分别是什么setvbuf()为什么必须在首次读写前调用fflush()能否保证数据已经物理落盘为什么execve()前没有刷新的FILE *数据可能丢失execve()后文件描述符和FILE *有什么区别为什么_IONBF仍然可能经过内核缓存send()返回成功为什么不等于对端应用收到recv()为什么可能一次只返回部分数据最低实践要求能用setvbuf()分别设置三种模式能用fflush()检查并刷新输出流能复现“无换行不刷、带换行刷出”的行缓冲实验能画出 TCP 数据从用户数组到内核缓冲再到网络的路径能说明 stdio 缓冲与 Socket 内核缓冲的区别。18. PDF 页码与录音时间索引主题PDF 页码录音时间缓冲区概念与输入、输出缓冲290—29100:01—00:41全缓冲、行缓冲、无缓冲29100:42—01:28setvbuf()与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-pagessetbuf/setvbuf(3)Linux man-pagesfflush(3)Linux man-pagesexecve(2)Linux man-pagessend(2)Linux man-pagesrecv(2)Linux man-pagessocket(7)19. 下一步学习教材第 297—299 页会继续完成全缓冲和手动fflush()实验随后从第 299 页开始使用netstat观察 TCP 连接建立与断开状态。建议按这个顺序继续完成全缓冲和 fflush 对照实验 ↓ 运行单连接 TCP 服务端和客户端 ↓ 用 ss 或 netstat 观察 LISTEN 与 ESTABLISHED ↓ 理解 send 只是写入本机内核缓冲 ↓ 循环处理部分发送和部分接收这部分为后续 TCP Server/Client 练习提供运行时理解程序出现“已经发送但对端没显示”“一次没有接收完整”“发送线程卡住”等现象时要先判断数据停留在应用缓冲、stdio 缓冲还是内核 Socket 缓冲。
返回列表