从文件系统到 SD 卡:数据写入的四层路径
本文讨论的是嵌入式 SoC 平台上的写入路径:片上带 DMA 控制器与存储控制器,跑 RTOS,通过 SDIO 接口外接 SD 卡。工业相机、行车记录仪、数据采集设备这类需要本地存盘的场合,走的都是这条路径。
它与通用计算机的存储栈有两点差别,值得先说明。一是 RTOS 侧没有页缓存,应用的一次写入不会先累积进内核缓冲,而是直接落到文件系统,因此对齐与请求拆分的行为在应用层就能感受到。二是这些环节由板级驱动直接实现,没有内核块层负责请求合并与重排。
1. 分层结构与各层职责
存储卡不认识「文件」,也不认识「偏移」。它只认一件事:把第若干个固定大小的块读出来或写进去。这个固定大小叫扇区,SD 卡通常是 512 字节。
「文件」是文件系统在块之上搭起来的一层抽象,并不是卡上的物理概念。一个文件由若干簇串成,簇号记在文件分配表(FAT)里,文件名、起始簇号、大小记在目录项里。要把「文件偏移 1000 处的 200 字节」写到卡上,得先查目录项找到起始簇,沿 FAT 链一段段走到目标簇,再换算成扇区。
这些工作既不该由调用写接口的应用承担,也不适合塞进驱动,于是分成了四层:
| 层 | 它理解的语言 | 它传递给下层的内容 | 是否复制数据 |
|---|---|---|---|
| 文件系统 | 文件名、字节偏移 | 缓冲区指针 + 扇区范围 | 仅非对齐部分 |
| 块设备层 | 扇区号、扇区数 | 缓冲区指针 + 设备块号 | 否 |
| 协议层 | 块号、读写方向 | 缓冲区指针 + 命令 | 否 |
| 控制器与 DMA | 内存地址 | 无(直接执行传输) | 是(硬件传输) |
四层的分工有一个共同点:它们交换的信息里,数据始终只是一个指针,真正被反复加工的是地址和计数。
#mermaid-svg-iZUDtpFLskxKHYlO{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-iZUDtpFLskxKHYlO .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-iZUDtpFLskxKHYlO .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-iZUDtpFLskxKHYlO .error-icon{fill:#552222;}#mermaid-svg-iZUDtpFLskxKHYlO .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-iZUDtpFLskxKHYlO .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-iZUDtpFLskxKHYlO .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-iZUDtpFLskxKHYlO .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-iZUDtpFLskxKHYlO .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-iZUDtpFLskxKHYlO .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-iZUDtpFLskxKHYlO .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-iZUDtpFLskxKHYlO .marker{fill:#333333;stroke:#333333;}#mermaid-svg-iZUDtpFLskxKHYlO .marker.cross{stroke:#333333;}#mermaid-svg-iZUDtpFLskxKHYlO svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-iZUDtpFLskxKHYlO p{margin:0;}#mermaid-svg-iZUDtpFLskxKHYlO .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-iZUDtpFLskxKHYlO .cluster-label text{fill:#333;}#mermaid-svg-iZUDtpFLskxKHYlO .cluster-label span{color:#333;}#mermaid-svg-iZUDtpFLskxKHYlO .cluster-label span p{background-color:transparent;}#mermaid-svg-iZUDtpFLskxKHYlO .label text,#mermaid-svg-iZUDtpFLskxKHYlO span{fill:#333;color:#333;}#mermaid-svg-iZUDtpFLskxKHYlO .node rect,#mermaid-svg-iZUDtpFLskxKHYlO .node circle,#mermaid-svg-iZUDtpFLskxKHYlO .node ellipse,#mermaid-svg-iZUDtpFLskxKHYlO .node polygon,#mermaid-svg-iZUDtpFLskxKHYlO .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-iZUDtpFLskxKHYlO .rough-node .label text,#mermaid-svg-iZUDtpFLskxKHYlO .node .label text,#mermaid-svg-iZUDtpFLskxKHYlO .image-shape .label,#mermaid-svg-iZUDtpFLskxKHYlO .icon-shape .label{text-anchor:middle;}#mermaid-svg-iZUDtpFLskxKHYlO .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-iZUDtpFLskxKHYlO .rough-node .label,#mermaid-svg-iZUDtpFLskxKHYlO .node .label,#mermaid-svg-iZUDtpFLskxKHYlO .image-shape .label,#mermaid-svg-iZUDtpFLskxKHYlO .icon-shape .label{text-align:center;}#mermaid-svg-iZUDtpFLskxKHYlO .node.clickable{cursor:pointer;}#mermaid-svg-iZUDtpFLskxKHYlO .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-iZUDtpFLskxKHYlO .arrowheadPath{fill:#333333;}#mermaid-svg-iZUDtpFLskxKHYlO .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-iZUDtpFLskxKHYlO .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-iZUDtpFLskxKHYlO .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-iZUDtpFLskxKHYlO .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-iZUDtpFLskxKHYlO .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-iZUDtpFLskxKHYlO .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-iZUDtpFLskxKHYlO .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-iZUDtpFLskxKHYlO .cluster text{fill:#333;}#mermaid-svg-iZUDtpFLskxKHYlO .cluster span{color:#333;}#mermaid-svg-iZUDtpFLskxKHYlO 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-iZUDtpFLskxKHYlO .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-iZUDtpFLskxKHYlO rect.text{fill:none;stroke-width:0;}#mermaid-svg-iZUDtpFLskxKHYlO .icon-shape,#mermaid-svg-iZUDtpFLskxKHYlO .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-iZUDtpFLskxKHYlO .icon-shape p,#mermaid-svg-iZUDtpFLskxKHYlO .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-iZUDtpFLskxKHYlO .icon-shape .label rect,#mermaid-svg-iZUDtpFLskxKHYlO .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-iZUDtpFLskxKHYlO .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-iZUDtpFLskxKHYlO .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-iZUDtpFLskxKHYlO :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}#mermaid-svg-iZUDtpFLskxKHYlO .app>*{fill:#cce5ff!important;stroke:#0d6efd!important;color:#084298!important;}#mermaid-svg-iZUDtpFLskxKHYlO .app span{fill:#cce5ff!important;stroke:#0d6efd!important;color:#084298!important;}#mermaid-svg-iZUDtpFLskxKHYlO .app tspan{fill:#084298!important;}#mermaid-svg-iZUDtpFLskxKHYlO .fs>*{fill:#d4edda!important;stroke:#28a745!important;color:#155724!important;}#mermaid-svg-iZUDtpFLskxKHYlO .fs span{fill:#d4edda!important;stroke:#28a745!important;color:#155724!important;}#mermaid-svg-iZUDtpFLskxKHYlO .fs tspan{fill:#155724!important;}#mermaid-svg-iZUDtpFLskxKHYlO .blk>*{fill:#fff5e6!important;stroke:#e65100!important;color:#4a2400!important;}#mermaid-svg-iZUDtpFLskxKHYlO .blk span{fill:#fff5e6!important;stroke:#e65100!important;color:#4a2400!important;}#mermaid-svg-iZUDtpFLskxKHYlO .blk tspan{fill:#4a2400!important;}#mermaid-svg-iZUDtpFLskxKHYlO .mmc>*{fill:#e2d9f3!important;stroke:#6f42c1!important;color:#3b2a6b!important;}#mermaid-svg-iZUDtpFLskxKHYlO .mmc span{fill:#e2d9f3!important;stroke:#6f42c1!important;color:#3b2a6b!important;}#mermaid-svg-iZUDtpFLskxKHYlO .mmc tspan{fill:#3b2a6b!important;}#mermaid-svg-iZUDtpFLskxKHYlO .hw>*{fill:#f8d7da!important;stroke:#dc3545!important;color:#721c24!important;}#mermaid-svg-iZUDtpFLskxKHYlO .hw span{fill:#f8d7da!important;stroke:#dc3545!important;color:#721c24!important;}#mermaid-svg-iZUDtpFLskxKHYlO .hw tspan{fill:#721c24!important;} 硬件
块与协议层
主机软件
应用:写文件
文件系统
字节 → 扇区
块设备层
扇区 → 设备块号
协议层
块号 → 命令
控制器与 DMA
命令 → 数据传输
存储卡
图 1:四层的翻译链。粗线是缓冲区指针的交接点,指针从文件系统一路下传到控制器,中间不复制内容。
四层里有两层完全属于软件,也正是数据唯一可能被复制的地方。先看这两层。
2. 软件侧两层:两次翻译,一次复制
2.1 文件系统层:唯一的软件复制点
文件系统拿到的是「文件偏移 + 长度」,交出去的是「扇区号 + 扇区数」。中间要解决定位与对齐两件事。
定位方面,文件偏移和簇号之间没有直接对应关系。目录项只记着起始簇号,后面的簇要沿 FAT 表一段段找出来。写入位置超出文件当前长度时,还得先分配新簇并追加到簇链末尾。
对齐方面,偏移可以是任意字节,扇区却只能整块操作。一段写入的两头多半不落在扇区边界上,只有中间那部分是完整扇区。
对齐的处理方式分成两条路:完整扇区直接把应用传进来的缓冲区指针往下传,不复制;两头的非对齐部分则必须先把该扇区整块读回来,把新数据覆盖到正确位置,再整块写回去。文件系统为此给每个打开的文件留了一块扇区大小的缓存,非对齐部分先并进这块缓存再写出。这是整条路径上唯一由软件执行的数据复制。
#mermaid-svg-FSPE8NajQfy49TAl{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-FSPE8NajQfy49TAl .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-FSPE8NajQfy49TAl .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-FSPE8NajQfy49TAl .error-icon{fill:#552222;}#mermaid-svg-FSPE8NajQfy49TAl .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-FSPE8NajQfy49TAl .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-FSPE8NajQfy49TAl .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-FSPE8NajQfy49TAl .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-FSPE8NajQfy49TAl .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-FSPE8NajQfy49TAl .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-FSPE8NajQfy49TAl .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-FSPE8NajQfy49TAl .marker{fill:#333333;stroke:#333333;}#mermaid-svg-FSPE8NajQfy49TAl .marker.cross{stroke:#333333;}#mermaid-svg-FSPE8NajQfy49TAl svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-FSPE8NajQfy49TAl p{margin:0;}#mermaid-svg-FSPE8NajQfy49TAl .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-FSPE8NajQfy49TAl .cluster-label text{fill:#333;}#mermaid-svg-FSPE8NajQfy49TAl .cluster-label span{color:#333;}#mermaid-svg-FSPE8NajQfy49TAl .cluster-label span p{background-color:transparent;}#mermaid-svg-FSPE8NajQfy49TAl .label text,#mermaid-svg-FSPE8NajQfy49TAl span{fill:#333;color:#333;}#mermaid-svg-FSPE8NajQfy49TAl .node rect,#mermaid-svg-FSPE8NajQfy49TAl .node circle,#mermaid-svg-FSPE8NajQfy49TAl .node ellipse,#mermaid-svg-FSPE8NajQfy49TAl .node polygon,#mermaid-svg-FSPE8NajQfy49TAl .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-FSPE8NajQfy49TAl .rough-node .label text,#mermaid-svg-FSPE8NajQfy49TAl .node .label text,#mermaid-svg-FSPE8NajQfy49TAl .image-shape .label,#mermaid-svg-FSPE8NajQfy49TAl .icon-shape .label{text-anchor:middle;}#mermaid-svg-FSPE8NajQfy49TAl .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-FSPE8NajQfy49TAl .rough-node .label,#mermaid-svg-FSPE8NajQfy49TAl .node .label,#mermaid-svg-FSPE8NajQfy49TAl .image-shape .label,#mermaid-svg-FSPE8NajQfy49TAl .icon-shape .label{text-align:center;}#mermaid-svg-FSPE8NajQfy49TAl .node.clickable{cursor:pointer;}#mermaid-svg-FSPE8NajQfy49TAl .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-FSPE8NajQfy49TAl .arrowheadPath{fill:#333333;}#mermaid-svg-FSPE8NajQfy49TAl .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-FSPE8NajQfy49TAl .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-FSPE8NajQfy49TAl .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-FSPE8NajQfy49TAl .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-FSPE8NajQfy49TAl .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-FSPE8NajQfy49TAl .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-FSPE8NajQfy49TAl .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-FSPE8NajQfy49TAl .cluster text{fill:#333;}#mermaid-svg-FSPE8NajQfy49TAl .cluster span{color:#333;}#mermaid-svg-FSPE8NajQfy49TAl 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-FSPE8NajQfy49TAl .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-FSPE8NajQfy49TAl rect.text{fill:none;stroke-width:0;}#mermaid-svg-FSPE8NajQfy49TAl .icon-shape,#mermaid-svg-FSPE8NajQfy49TAl .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-FSPE8NajQfy49TAl .icon-shape p,#mermaid-svg-FSPE8NajQfy49TAl .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-FSPE8NajQfy49TAl .icon-shape .label rect,#mermaid-svg-FSPE8NajQfy49TAl .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-FSPE8NajQfy49TAl .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-FSPE8NajQfy49TAl .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-FSPE8NajQfy49TAl :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}#mermaid-svg-FSPE8NajQfy49TAl .req>*{fill:#cce5ff!important;stroke:#0d6efd!important;color:#084298!important;}#mermaid-svg-FSPE8NajQfy49TAl .req span{fill:#cce5ff!important;stroke:#0d6efd!important;color:#084298!important;}#mermaid-svg-FSPE8NajQfy49TAl .req tspan{fill:#084298!important;}#mermaid-svg-FSPE8NajQfy49TAl .free>*{fill:#c8e6c9!important;stroke:#2e7d32!important;color:#1b5e20!important;}#mermaid-svg-FSPE8NajQfy49TAl .free span{fill:#c8e6c9!important;stroke:#2e7d32!important;color:#1b5e20!important;}#mermaid-svg-FSPE8NajQfy49TAl .free tspan{fill:#1b5e20!important;}#mermaid-svg-FSPE8NajQfy49TAl .cost>*{fill:#ffcdd2!important;stroke:#c62828!important;color:#b71c1c!important;}#mermaid-svg-FSPE8NajQfy49TAl .cost span{fill:#ffcdd2!important;stroke:#c62828!important;color:#b71c1c!important;}#mermaid-svg-FSPE8NajQfy49TAl .cost tspan{fill:#b71c1c!important;}#mermaid-svg-FSPE8NajQfy49TAl .next>*{fill:#fff5e6!important;stroke:#e65100!important;color:#4a2400!important;}#mermaid-svg-FSPE8NajQfy49TAl .next span{fill:#fff5e6!important;stroke:#e65100!important;color:#4a2400!important;}#mermaid-svg-FSPE8NajQfy49TAl .next tspan{fill:#4a2400!important;} 中间完整扇区
两端非对齐部分
一次写入:偏移 1000,长度 200
按扇区边界切分
扇区 = 512 字节
缓冲区指针直接下传
复制次数 0
读回该扇区
新数据并入文件缓存
复制次数 1
整块写回
传递给块设备层
图 2:文件系统层的两条分支。本图颜色表示是否产生复制,与层次无关:绿色路径不复制数据,红色路径是全文唯一的软件复制点。
举个具体例子。向文件偏移 1000 处写 200 字节,扇区 512 字节。这 200 字节跨在扇区 1 和扇区 2 上,两头各占一部分,于是实际发生的是:读回扇区 1、读回扇区 2、分别修改、再写回两个扇区。应用看到一次写入,卡上产生两次读加两次写。
同一位置若改成写 4096 字节且对齐到扇区边界,8 个扇区可以连续写下去,一次读回都不需要。
决定复制与否的是对齐,不是数据量。
2.2 块设备层:只翻译,不复制
扇区范围一旦确定,请求就离开文件系统,进入块设备层。块设备层收到的是「第几号扇区、连续几个扇区、数据在哪」。向协议层下发之前,它要做三件事,全都与数据内容无关。
一是叠加分区偏移。卡上通常不止一个分区,文件系统看到的第 0 号扇区未必是设备的物理第 0 号扇区。
二是限制单次请求的大小。一次请求能带多少扇区有上限,超出的部分拆成多次下发。上限取两个条件的较小值:主机侧 DMA 一次能描述的字节数,以及卡依据规范声明的单次最大块数。前者由板级硬件配置决定,后者来自卡的寄存器。拆分对上层是透明的,但总线上的命令次数会成倍增加。
三是保证访问互斥。同一分区的请求必须按序下发,交叉下发会让命令和数据错位。
#mermaid-svg-yW5YCmV4YXcJdA2e{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-yW5YCmV4YXcJdA2e .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-yW5YCmV4YXcJdA2e .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-yW5YCmV4YXcJdA2e .error-icon{fill:#552222;}#mermaid-svg-yW5YCmV4YXcJdA2e .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-yW5YCmV4YXcJdA2e .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-yW5YCmV4YXcJdA2e .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-yW5YCmV4YXcJdA2e .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-yW5YCmV4YXcJdA2e .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-yW5YCmV4YXcJdA2e .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-yW5YCmV4YXcJdA2e .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-yW5YCmV4YXcJdA2e .marker{fill:#333333;stroke:#333333;}#mermaid-svg-yW5YCmV4YXcJdA2e .marker.cross{stroke:#333333;}#mermaid-svg-yW5YCmV4YXcJdA2e svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-yW5YCmV4YXcJdA2e p{margin:0;}#mermaid-svg-yW5YCmV4YXcJdA2e .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-yW5YCmV4YXcJdA2e .cluster-label text{fill:#333;}#mermaid-svg-yW5YCmV4YXcJdA2e .cluster-label span{color:#333;}#mermaid-svg-yW5YCmV4YXcJdA2e .cluster-label span p{background-color:transparent;}#mermaid-svg-yW5YCmV4YXcJdA2e .label text,#mermaid-svg-yW5YCmV4YXcJdA2e span{fill:#333;color:#333;}#mermaid-svg-yW5YCmV4YXcJdA2e .node rect,#mermaid-svg-yW5YCmV4YXcJdA2e .node circle,#mermaid-svg-yW5YCmV4YXcJdA2e .node ellipse,#mermaid-svg-yW5YCmV4YXcJdA2e .node polygon,#mermaid-svg-yW5YCmV4YXcJdA2e .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-yW5YCmV4YXcJdA2e .rough-node .label text,#mermaid-svg-yW5YCmV4YXcJdA2e .node .label text,#mermaid-svg-yW5YCmV4YXcJdA2e .image-shape .label,#mermaid-svg-yW5YCmV4YXcJdA2e .icon-shape .label{text-anchor:middle;}#mermaid-svg-yW5YCmV4YXcJdA2e .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-yW5YCmV4YXcJdA2e .rough-node .label,#mermaid-svg-yW5YCmV4YXcJdA2e .node .label,#mermaid-svg-yW5YCmV4YXcJdA2e .image-shape .label,#mermaid-svg-yW5YCmV4YXcJdA2e .icon-shape .label{text-align:center;}#mermaid-svg-yW5YCmV4YXcJdA2e .node.clickable{cursor:pointer;}#mermaid-svg-yW5YCmV4YXcJdA2e .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-yW5YCmV4YXcJdA2e .arrowheadPath{fill:#333333;}#mermaid-svg-yW5YCmV4YXcJdA2e .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-yW5YCmV4YXcJdA2e .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-yW5YCmV4YXcJdA2e .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-yW5YCmV4YXcJdA2e .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-yW5YCmV4YXcJdA2e .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-yW5YCmV4YXcJdA2e .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-yW5YCmV4YXcJdA2e .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-yW5YCmV4YXcJdA2e .cluster text{fill:#333;}#mermaid-svg-yW5YCmV4YXcJdA2e .cluster span{color:#333;}#mermaid-svg-yW5YCmV4YXcJdA2e 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-yW5YCmV4YXcJdA2e .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-yW5YCmV4YXcJdA2e rect.text{fill:none;stroke-width:0;}#mermaid-svg-yW5YCmV4YXcJdA2e .icon-shape,#mermaid-svg-yW5YCmV4YXcJdA2e .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-yW5YCmV4YXcJdA2e .icon-shape p,#mermaid-svg-yW5YCmV4YXcJdA2e .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-yW5YCmV4YXcJdA2e .icon-shape .label rect,#mermaid-svg-yW5YCmV4YXcJdA2e .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-yW5YCmV4YXcJdA2e .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-yW5YCmV4YXcJdA2e .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-yW5YCmV4YXcJdA2e :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}#mermaid-svg-yW5YCmV4YXcJdA2e .blk>*{fill:#fff5e6!important;stroke:#e65100!important;color:#4a2400!important;}#mermaid-svg-yW5YCmV4YXcJdA2e .blk span{fill:#fff5e6!important;stroke:#e65100!important;color:#4a2400!important;}#mermaid-svg-yW5YCmV4YXcJdA2e .blk tspan{fill:#4a2400!important;}#mermaid-svg-yW5YCmV4YXcJdA2e .free>*{fill:#c8e6c9!important;stroke:#2e7d32!important;color:#1b5e20!important;}#mermaid-svg-yW5YCmV4YXcJdA2e .free span{fill:#c8e6c9!important;stroke:#2e7d32!important;color:#1b5e20!important;}#mermaid-svg-yW5YCmV4YXcJdA2e .free tspan{fill:#1b5e20!important;}#mermaid-svg-yW5YCmV4YXcJdA2e .cost>*{fill:#ffcdd2!important;stroke:#c62828!important;color:#b71c1c!important;}#mermaid-svg-yW5YCmV4YXcJdA2e .cost span{fill:#ffcdd2!important;stroke:#c62828!important;color:#b71c1c!important;}#mermaid-svg-yW5YCmV4YXcJdA2e .cost tspan{fill:#b71c1c!important;} 叠加分区起始偏移
未超过
超过
文件系统给出的
逻辑扇区号
设备物理块号
是否超过单次上限
单次请求下发
拆分为多次请求
命令次数增加
附带缓冲区指针
传递给协议层
图 3:块设备层的两项处理,全部作用于地址与计数。绿色路径只改写地址,不接触数据内容。
到这里为止,数据仍然只是一个被传递的指针。
3. 协议层与硬件:DMA 是唯一的搬运者
到了这一层才开始和卡直接打交道。存储卡和内存有一个根本区别:内存可以直接按地址写入,存储卡必须下命令、等响应。每次读写都得走一遍「发命令、收响应、传数据」。
按一次操作涉及的块数,用的命令不同:
| 一次操作 | 写命令 | 读命令 | 补充说明 |
|---|---|---|---|
| 单块 | CMD24 | CMD17 | 无 |
| 多块 | CMD25 | CMD18 | 结束后再发 CMD12 通知设备停止 |
命令的参数就是块地址。有一处兼容点容易忽略:并非所有卡都用块号寻址。高容量卡用块号,早期的标准容量卡用字节地址,得先把块号乘 512 再下发,否则地址会偏。
协议层仍然只组装命令,不碰数据。数据随后由控制器通过 DMA 传输,这是整条路径上唯一真正搬运数据的环节。与它配套的两项维护由软件负责。
写入会让卡进入忙状态,此时它不接受新命令。下一次请求前必须先确认卡已退出忙状态,否则命令会被直接忽略。
缓存一致性的处理方向相反。CPU 访问内存经过缓存,DMA 绕过缓存直接读写内存,两者不一致就出错:写数据前要先把缓冲区刷出缓存,否则 DMA 可能读到缓存里的旧内容;读数据前要先让缓存行失效,否则 CPU 随后读到的是缓存里的旧值。这两步的顺序不能调换。
存储卡 DMA 控制器 板级驱动 块设备层 文件系统 存储卡 DMA 控制器 板级驱动 块设备层 文件系统 #mermaid-svg-QSV3DYwp4YV7yZAF{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-QSV3DYwp4YV7yZAF .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-QSV3DYwp4YV7yZAF .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-QSV3DYwp4YV7yZAF .error-icon{fill:#552222;}#mermaid-svg-QSV3DYwp4YV7yZAF .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-QSV3DYwp4YV7yZAF .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-QSV3DYwp4YV7yZAF .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-QSV3DYwp4YV7yZAF .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-QSV3DYwp4YV7yZAF .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-QSV3DYwp4YV7yZAF .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-QSV3DYwp4YV7yZAF .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-QSV3DYwp4YV7yZAF .marker{fill:#333333;stroke:#333333;}#mermaid-svg-QSV3DYwp4YV7yZAF .marker.cross{stroke:#333333;}#mermaid-svg-QSV3DYwp4YV7yZAF svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-QSV3DYwp4YV7yZAF p{margin:0;}#mermaid-svg-QSV3DYwp4YV7yZAF .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-QSV3DYwp4YV7yZAF text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-QSV3DYwp4YV7yZAF .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-QSV3DYwp4YV7yZAF .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-QSV3DYwp4YV7yZAF .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-QSV3DYwp4YV7yZAF .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-QSV3DYwp4YV7yZAF #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-QSV3DYwp4YV7yZAF .sequenceNumber{fill:white;}#mermaid-svg-QSV3DYwp4YV7yZAF #sequencenumber{fill:#333;}#mermaid-svg-QSV3DYwp4YV7yZAF #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-QSV3DYwp4YV7yZAF .messageText{fill:#333;stroke:none;}#mermaid-svg-QSV3DYwp4YV7yZAF .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-QSV3DYwp4YV7yZAF .labelText,#mermaid-svg-QSV3DYwp4YV7yZAF .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-QSV3DYwp4YV7yZAF .loopText,#mermaid-svg-QSV3DYwp4YV7yZAF .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-QSV3DYwp4YV7yZAF .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-QSV3DYwp4YV7yZAF .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-QSV3DYwp4YV7yZAF .noteText,#mermaid-svg-QSV3DYwp4YV7yZAF .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-QSV3DYwp4YV7yZAF .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-QSV3DYwp4YV7yZAF .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-QSV3DYwp4YV7yZAF .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-QSV3DYwp4YV7yZAF .actorPopupMenu{position:absolute;}#mermaid-svg-QSV3DYwp4YV7yZAF .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-QSV3DYwp4YV7yZAF .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-QSV3DYwp4YV7yZAF .actor-man circle,#mermaid-svg-QSV3DYwp4YV7yZAF line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-QSV3DYwp4YV7yZAF :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 完整扇区直接传递指针,非对齐部分先并入缓存1请求:第 N 号扇区起,共 M 个扇区2叠加分区偏移,按上限拆分,获取分区锁3下发请求,附带缓冲区指针4将缓冲区刷出 CPU 缓存5建立描述符链并启动6按命令与块号将数据写入设备7传输完成中断8返回9返回10
图 4:一次写入的完整时序。同一个缓冲区指针从第一行传递到最后,虚线为返回路径,编号对应执行顺序。
路径走到这里就结束了。回到开头那个问题:这条路上软件真的没碰过数据吗。
4. 总结
写入慢的时候,先看对齐与命令次数,不必去找「哪儿多拷了一次」。前面那个例子已经说明,一次 200 字节的写入会变成两次读加两次写,这个代价远超任何一次内存复制。
缓冲区按扇区对齐、长度取扇区整数倍,文件系统的非对齐分支就永远不会进入,全链路复制次数为零。数据采集类应用比较容易做到这一点。
连续大块写入是效率最高的形态:每层只翻译一次,命令最少,也不产生读回。
缓存一致性和忙状态检测属于硬件约束,不是实现选择。它们只在特定时序下暴露,不能因为测试通过就删掉,顺序也不能调。
还有一点常被忽略:上位机看到的一次调用,在总线上未必是一次命令。超限会被拆分,不对齐会引入读回,真实的命令次数要用总线上的观测来确认。
一句话:这四层都在做地址翻译,唯一传输数据的是控制器。软件只在两种情况下直接处理数据:拼接不足一个扇区的部分,以及为 DMA 维护缓存一致性。