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. 本节知识路线

#mermaid-svg-Yq3O5Xi3NhNQicw8{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Yq3O5Xi3NhNQicw8 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Yq3O5Xi3NhNQicw8 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Yq3O5Xi3NhNQicw8 .error-icon{fill:#552222;}#mermaid-svg-Yq3O5Xi3NhNQicw8 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Yq3O5Xi3NhNQicw8 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Yq3O5Xi3NhNQicw8 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Yq3O5Xi3NhNQicw8 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Yq3O5Xi3NhNQicw8 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Yq3O5Xi3NhNQicw8 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Yq3O5Xi3NhNQicw8 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Yq3O5Xi3NhNQicw8 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Yq3O5Xi3NhNQicw8 .marker.cross{stroke:#333333;}#mermaid-svg-Yq3O5Xi3NhNQicw8 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Yq3O5Xi3NhNQicw8 p{margin:0;}#mermaid-svg-Yq3O5Xi3NhNQicw8 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Yq3O5Xi3NhNQicw8 .cluster-label text{fill:#333;}#mermaid-svg-Yq3O5Xi3NhNQicw8 .cluster-label span{color:#333;}#mermaid-svg-Yq3O5Xi3NhNQicw8 .cluster-label span p{background-color:transparent;}#mermaid-svg-Yq3O5Xi3NhNQicw8 .label text,#mermaid-svg-Yq3O5Xi3NhNQicw8 span{fill:#333;color:#333;}#mermaid-svg-Yq3O5Xi3NhNQicw8 .node rect,#mermaid-svg-Yq3O5Xi3NhNQicw8 .node circle,#mermaid-svg-Yq3O5Xi3NhNQicw8 .node ellipse,#mermaid-svg-Yq3O5Xi3NhNQicw8 .node polygon,#mermaid-svg-Yq3O5Xi3NhNQicw8 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Yq3O5Xi3NhNQicw8 .rough-node .label text,#mermaid-svg-Yq3O5Xi3NhNQicw8 .node .label text,#mermaid-svg-Yq3O5Xi3NhNQicw8 .image-shape .label,#mermaid-svg-Yq3O5Xi3NhNQicw8 .icon-shape .label{text-anchor:middle;}#mermaid-svg-Yq3O5Xi3NhNQicw8 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Yq3O5Xi3NhNQicw8 .rough-node .label,#mermaid-svg-Yq3O5Xi3NhNQicw8 .node .label,#mermaid-svg-Yq3O5Xi3NhNQicw8 .image-shape .label,#mermaid-svg-Yq3O5Xi3NhNQicw8 .icon-shape .label{text-align:center;}#mermaid-svg-Yq3O5Xi3NhNQicw8 .node.clickable{cursor:pointer;}#mermaid-svg-Yq3O5Xi3NhNQicw8 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Yq3O5Xi3NhNQicw8 .arrowheadPath{fill:#333333;}#mermaid-svg-Yq3O5Xi3NhNQicw8 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Yq3O5Xi3NhNQicw8 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Yq3O5Xi3NhNQicw8 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Yq3O5Xi3NhNQicw8 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Yq3O5Xi3NhNQicw8 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Yq3O5Xi3NhNQicw8 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Yq3O5Xi3NhNQicw8 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Yq3O5Xi3NhNQicw8 .cluster text{fill:#333;}#mermaid-svg-Yq3O5Xi3NhNQicw8 .cluster span{color:#333;}#mermaid-svg-Yq3O5Xi3NhNQicw8 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Yq3O5Xi3NhNQicw8 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Yq3O5Xi3NhNQicw8 rect.text{fill:none;stroke-width:0;}#mermaid-svg-Yq3O5Xi3NhNQicw8 .icon-shape,#mermaid-svg-Yq3O5Xi3NhNQicw8 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Yq3O5Xi3NhNQicw8 .icon-shape p,#mermaid-svg-Yq3O5Xi3NhNQicw8 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Yq3O5Xi3NhNQicw8 .icon-shape .label rect,#mermaid-svg-Yq3O5Xi3NhNQicw8 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Yq3O5Xi3NhNQicw8 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Yq3O5Xi3NhNQicw8 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Yq3O5Xi3NhNQicw8 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 为什么需要缓冲
区分三层缓冲
应用自己定义的数组
C 标准库 FILE 缓冲
内核 Socket 收发缓冲
全缓冲 行缓冲 无缓冲
setvbuf 设置模式
fflush 主动刷新
execve 替换进程映像
send 写入发送缓冲
recv 读取接收缓冲

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

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

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

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

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

3. 为什么要有缓冲区

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

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

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

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

4.1 接收方向

#mermaid-svg-VP4ourIaIpIcwRZY{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-VP4ourIaIpIcwRZY .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-VP4ourIaIpIcwRZY .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-VP4ourIaIpIcwRZY .error-icon{fill:#552222;}#mermaid-svg-VP4ourIaIpIcwRZY .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-VP4ourIaIpIcwRZY .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-VP4ourIaIpIcwRZY .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-VP4ourIaIpIcwRZY .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-VP4ourIaIpIcwRZY .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-VP4ourIaIpIcwRZY .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-VP4ourIaIpIcwRZY .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-VP4ourIaIpIcwRZY .marker{fill:#333333;stroke:#333333;}#mermaid-svg-VP4ourIaIpIcwRZY .marker.cross{stroke:#333333;}#mermaid-svg-VP4ourIaIpIcwRZY svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-VP4ourIaIpIcwRZY p{margin:0;}#mermaid-svg-VP4ourIaIpIcwRZY .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-VP4ourIaIpIcwRZY .cluster-label text{fill:#333;}#mermaid-svg-VP4ourIaIpIcwRZY .cluster-label span{color:#333;}#mermaid-svg-VP4ourIaIpIcwRZY .cluster-label span p{background-color:transparent;}#mermaid-svg-VP4ourIaIpIcwRZY .label text,#mermaid-svg-VP4ourIaIpIcwRZY span{fill:#333;color:#333;}#mermaid-svg-VP4ourIaIpIcwRZY .node rect,#mermaid-svg-VP4ourIaIpIcwRZY .node circle,#mermaid-svg-VP4ourIaIpIcwRZY .node ellipse,#mermaid-svg-VP4ourIaIpIcwRZY .node polygon,#mermaid-svg-VP4ourIaIpIcwRZY .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-VP4ourIaIpIcwRZY .rough-node .label text,#mermaid-svg-VP4ourIaIpIcwRZY .node .label text,#mermaid-svg-VP4ourIaIpIcwRZY .image-shape .label,#mermaid-svg-VP4ourIaIpIcwRZY .icon-shape .label{text-anchor:middle;}#mermaid-svg-VP4ourIaIpIcwRZY .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-VP4ourIaIpIcwRZY .rough-node .label,#mermaid-svg-VP4ourIaIpIcwRZY .node .label,#mermaid-svg-VP4ourIaIpIcwRZY .image-shape .label,#mermaid-svg-VP4ourIaIpIcwRZY .icon-shape .label{text-align:center;}#mermaid-svg-VP4ourIaIpIcwRZY .node.clickable{cursor:pointer;}#mermaid-svg-VP4ourIaIpIcwRZY .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-VP4ourIaIpIcwRZY .arrowheadPath{fill:#333333;}#mermaid-svg-VP4ourIaIpIcwRZY .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-VP4ourIaIpIcwRZY .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-VP4ourIaIpIcwRZY .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-VP4ourIaIpIcwRZY .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-VP4ourIaIpIcwRZY .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-VP4ourIaIpIcwRZY .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-VP4ourIaIpIcwRZY .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-VP4ourIaIpIcwRZY .cluster text{fill:#333;}#mermaid-svg-VP4ourIaIpIcwRZY .cluster span{color:#333;}#mermaid-svg-VP4ourIaIpIcwRZY div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-VP4ourIaIpIcwRZY .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-VP4ourIaIpIcwRZY rect.text{fill:none;stroke-width:0;}#mermaid-svg-VP4ourIaIpIcwRZY .icon-shape,#mermaid-svg-VP4ourIaIpIcwRZY .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-VP4ourIaIpIcwRZY .icon-shape p,#mermaid-svg-VP4ourIaIpIcwRZY .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-VP4ourIaIpIcwRZY .icon-shape .label rect,#mermaid-svg-VP4ourIaIpIcwRZY .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-VP4ourIaIpIcwRZY .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-VP4ourIaIpIcwRZY .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-VP4ourIaIpIcwRZY :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 网卡收到帧
驱动与协议栈处理
Socket 接收缓冲区
recv 或 read
用户空间数组

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

默认阻塞模式下:

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

4.2 发送方向

#mermaid-svg-bGcVr5ZpGKSmN64i{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-bGcVr5ZpGKSmN64i .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-bGcVr5ZpGKSmN64i .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-bGcVr5ZpGKSmN64i .error-icon{fill:#552222;}#mermaid-svg-bGcVr5ZpGKSmN64i .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-bGcVr5ZpGKSmN64i .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-bGcVr5ZpGKSmN64i .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-bGcVr5ZpGKSmN64i .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-bGcVr5ZpGKSmN64i .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-bGcVr5ZpGKSmN64i .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-bGcVr5ZpGKSmN64i .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-bGcVr5ZpGKSmN64i .marker{fill:#333333;stroke:#333333;}#mermaid-svg-bGcVr5ZpGKSmN64i .marker.cross{stroke:#333333;}#mermaid-svg-bGcVr5ZpGKSmN64i svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-bGcVr5ZpGKSmN64i p{margin:0;}#mermaid-svg-bGcVr5ZpGKSmN64i .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-bGcVr5ZpGKSmN64i .cluster-label text{fill:#333;}#mermaid-svg-bGcVr5ZpGKSmN64i .cluster-label span{color:#333;}#mermaid-svg-bGcVr5ZpGKSmN64i .cluster-label span p{background-color:transparent;}#mermaid-svg-bGcVr5ZpGKSmN64i .label text,#mermaid-svg-bGcVr5ZpGKSmN64i span{fill:#333;color:#333;}#mermaid-svg-bGcVr5ZpGKSmN64i .node rect,#mermaid-svg-bGcVr5ZpGKSmN64i .node circle,#mermaid-svg-bGcVr5ZpGKSmN64i .node ellipse,#mermaid-svg-bGcVr5ZpGKSmN64i .node polygon,#mermaid-svg-bGcVr5ZpGKSmN64i .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-bGcVr5ZpGKSmN64i .rough-node .label text,#mermaid-svg-bGcVr5ZpGKSmN64i .node .label text,#mermaid-svg-bGcVr5ZpGKSmN64i .image-shape .label,#mermaid-svg-bGcVr5ZpGKSmN64i .icon-shape .label{text-anchor:middle;}#mermaid-svg-bGcVr5ZpGKSmN64i .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-bGcVr5ZpGKSmN64i .rough-node .label,#mermaid-svg-bGcVr5ZpGKSmN64i .node .label,#mermaid-svg-bGcVr5ZpGKSmN64i .image-shape .label,#mermaid-svg-bGcVr5ZpGKSmN64i .icon-shape .label{text-align:center;}#mermaid-svg-bGcVr5ZpGKSmN64i .node.clickable{cursor:pointer;}#mermaid-svg-bGcVr5ZpGKSmN64i .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-bGcVr5ZpGKSmN64i .arrowheadPath{fill:#333333;}#mermaid-svg-bGcVr5ZpGKSmN64i .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-bGcVr5ZpGKSmN64i .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-bGcVr5ZpGKSmN64i .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-bGcVr5ZpGKSmN64i .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-bGcVr5ZpGKSmN64i .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-bGcVr5ZpGKSmN64i .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-bGcVr5ZpGKSmN64i .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-bGcVr5ZpGKSmN64i .cluster text{fill:#333;}#mermaid-svg-bGcVr5ZpGKSmN64i .cluster span{color:#333;}#mermaid-svg-bGcVr5ZpGKSmN64i div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-bGcVr5ZpGKSmN64i .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-bGcVr5ZpGKSmN64i rect.text{fill:none;stroke-width:0;}#mermaid-svg-bGcVr5ZpGKSmN64i .icon-shape,#mermaid-svg-bGcVr5ZpGKSmN64i .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-bGcVr5ZpGKSmN64i .icon-shape p,#mermaid-svg-bGcVr5ZpGKSmN64i .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-bGcVr5ZpGKSmN64i .icon-shape .label rect,#mermaid-svg-bGcVr5ZpGKSmN64i .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-bGcVr5ZpGKSmN64i .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-bGcVr5ZpGKSmN64i .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-bGcVr5ZpGKSmN64i :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 用户空间数组
send 或 write
Socket 发送缓冲区
TCP 分段和重传
网卡与网络

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

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

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

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

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

c 复制代码
int size = 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 * 的缓冲模式

c 复制代码
#include <stdio.h>

int setvbuf(FILE *stream,
            char *buf,
            int mode,
            size_t size);
参数 含义
stream 已打开的 FILE * 流,例如 stdout 或 fopen() 返回值
buf 用户提供的缓冲内存;传 NULL 时由 libc 分配
mode _IOFBF、_IOLBF 或 _IONBF
size 缓冲区大小;无缓冲模式通常写 0

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

6.1 必须在什么时候调用

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

c 复制代码
FILE *fp = fopen("testfile.txt", "w");
if (fp == NULL) {
    perror("fopen");
    return 1;
}

if (setvbuf(fp, NULL, _IOLBF, BUFSIZ) != 0) {
    fprintf(stderr, "setvbuf failed\n");
    fclose(fp);
    return 1;
}

fputs("hello\n", fp);

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

6.2 三种常见写法

c 复制代码
setvbuf(fp, NULL, _IOFBF, BUFSIZ); /* 全缓冲 */
setvbuf(fp, NULL, _IOLBF, BUFSIZ); /* 行缓冲 */
setvbuf(fp, NULL, _IONBF, 0);      /* 无缓冲 */

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

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

c 复制代码
#include <stdio.h>

int fflush(FILE *stream);

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

c 复制代码
if (fflush(fp) == EOF) {
    perror("fflush");
}

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

7.1 fflush() 不等于物理落盘

数据路径可能是:

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

因此:

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

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

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

c 复制代码
int execve(const char *path,
           char *const argv[],
           char *const envp[]);

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

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

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

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

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

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

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

c 复制代码
#include <stdio.h>
#include <unistd.h>

int main(void)
{
    FILE *fp = fopen("testfile.txt", "w");
    if (fp == NULL) {
        perror("fopen");
        return 1;
    }

    if (setvbuf(fp, NULL, _IOFBF, BUFSIZ) != 0) {
        fprintf(stderr, "setvbuf failed\n");
        fclose(fp);
        return 1;
    }

    fputs("hello", fp);       /* 可能仍停留在 libc 缓冲 */

    char *argv[] = {"true", NULL};
    char *envp[] = {NULL};
    execve("/usr/bin/true", argv, envp);

    perror("execve");        /* 只有 execve 失败才执行 */
    fclose(fp);
    return 1;
}

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

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

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

在 execve() 前增加:

c 复制代码
if (fflush(fp) == EOF) {
    perror("fflush");
    fclose(fp);
    return 1;
}

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

11. 实验三:设置无缓冲

在首次写入前设置:

c 复制代码
if (setvbuf(fp, NULL, _IONBF, 0) != 0) {
    fprintf(stderr, "setvbuf failed\n");
    fclose(fp);
    return 1;
}

fputs("hello", fp);

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

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

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

12.1 没有换行

c 复制代码
setvbuf(fp, NULL, _IOLBF, BUFSIZ);
fputs("hello", fp);

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

12.2 带换行

c 复制代码
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.1 send() 成功不代表对端收到

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

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

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---291 00:01---00:41
全缓冲、行缓冲、无缓冲 291 00:42---01:28
setvbuf() 与 fflush() 292 01:29---02:12
建立 fopen、fprintf、execve 实验 292---293 02:13---04:48
全缓冲未刷写现象与 fflush() 293---294 04:49---06:19
_IONBF 无缓冲实验 293---294 06:20---07:12
_IOLBF 无换行与带换行实验 294---296 07:13---07:43
录音末尾的定时刷新说法 296 附近 07:44---07:58
全缓冲独立实验 297 页起 本次录音未讲

进一步核对资料:

19. 下一步学习

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

建议按这个顺序继续:

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

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

相关推荐
web打印社区2 小时前
JS 静默打印怎么做:纯前端为什么不行,以及最小可跑通写法
开发语言·前端·javascript·websocket·网络协议·http·pdf
一条破秋裤2 小时前
TCP 并发服务器:连接分析、多线程与多进程
服务器·网络·tcp/ip
自己的九又四分之三站台2 小时前
TCP 与 UDP:从可靠字节流到无连接数据报,怎么选、怎么测
网络协议·tcp/ip·udp
liangshanbo12153 小时前
面试题:游戏、直播为什么常用 UDP?KCP 是什么?为什么比 TCP 更适合低延迟场景?
tcp/ip·游戏·udp
wangbing11254 小时前
开发指南147-WebSocket-前端
前端·websocket·网络协议
阿钱真强道15 小时前
17 嵌入式操作系统 | uloop 事件循环:TCP 客户端
c语言·tcp/ip·tcp·事件循环·uloop
要吃这碗饭16 小时前
深入理解 TCP 协议:从三次握手到可靠传输
网络·网络协议·tcp/ip
Horn Still Sounds18 小时前
IMX6ULL裸机|I2C通信协议 + ADC模数转换学习笔记
arm开发·笔记·单片机·嵌入式硬件
大侠归来18 小时前
TCP/IP 网络编程:多播与广播
网络·tcp/ip·智能路由器