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. 录音与教材表述校正
- 转写中的
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---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 页起 | 本次录音未讲 |
进一步核对资料:
- 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 连接建立与断开状态。
建议按这个顺序继续:
text
完成全缓冲和 fflush 对照实验
↓
运行单连接 TCP 服务端和客户端
↓
用 ss 或 netstat 观察 LISTEN 与 ESTABLISHED
↓
理解 send 只是写入本机内核缓冲
↓
循环处理部分发送和部分接收
这部分为后续 TCP Server/Client 练习提供运行时理解:程序出现"已经发送但对端没显示""一次没有接收完整""发送线程卡住"等现象时,要先判断数据停留在应用缓冲、stdio 缓冲还是内核 Socket 缓冲。