设备描述符中的地址不能直接由 CPU 指针强转而来;unmap 也不是停止硬件的命令。DMA 需要同时管理地址、方向、可见性与使用权交接。
| 查阅目标 | 阅读位置 |
| 地址与 coherent/streaming | 第 1~2 节 |
| 方向、完成与 SG 计数 | 第 3~4 节 |
| IOMMU、bounce 与隔离 | 第 5~6 节 |
1. 三种地址不能互相强转
| 地址 | 谁使用 | 典型类型 |
| CPU 虚拟地址 | 内核代码访问缓冲区 | void * |
| CPU 物理地址 | 内存与资源管理 | phys_addr_t |
| DMA 地址 | 写入设备描述符,供设备寻址 | dma_addr_t |
IOMMU 可将设备发出的 IOVA 转换成物理页地址;没有 IOMMU 时,DMA 地址也可能受主桥偏移等影响。dma_addr_t 既不能直接解引用,也不能通过随意强转 CPU 指针得到。即使某台机器数值恰好相同,也不是可移植保证。
驱动按照硬件能力设置 DMA mask。描述符与数据载荷的地址位宽可以不同,必要时分别设置 streaming mask 与 coherent mask。较窄的 mask 可能无法在平台上得到支持,应处理 API 返回值;不要根据 CPU 是 64 位就声称设备支持 64 位 DMA。
2. Coherent 与 streaming 解决不同访问模式
| 模型 | 常见用途 | 驱动仍要负责 |
| coherent allocation | 长期存在、双方频繁访问的描述符和状态区 | 访问顺序、互斥、硬件完成与释放时机 |
| streaming mapping | 网络包、存储数据等批量传输 | 方向、所有权交接、同步与映射寿命 |
dma_alloc_coherent() 返回 CPU 地址,并输出供设备使用的 DMA 地址。释放时使用匹配的设备、大小、CPU 地址与 DMA handle。coherent 免除这块内存常规访问所需的显式缓存维护,不代表所有写入立即按源代码顺序对设备可见,也不使复合操作自动原子化。
例如发布描述符时,先写地址和长度,再执行适当的 dma_wmb(),最后按设备协议设置有效位;设备完成后,CPU 也可能需要 dma_rmb() 才能安全读取随完成标志发布的数据。MMIO doorbell 的访问顺序还需满足 I/O 访问器和硬件要求。屏障不等于等待 DMA 完成。
streaming 的“流式”不意味着只能映射一次,也不承诺一定比 coherent 快;性能依赖平台、缓存、映射开销和访问模式。概念与地址规则见 DMA HOWTO,具体接口见 DMA API。
3. 方向以设备和内存的数据流为准
DMA_TO_DEVICE:设备读取内存,例如发送网络包。
DMA_FROM_DEVICE:设备写入内存,例如接收网络包。
DMA_BIDIRECTIONAL:双方都要写,并仍需遵守所有权和同步协议。
不要只标“读”或“写”,否则 CPU 与设备两个视角正好相反。能确定方向时使用准确方向;map、sync、unmap 使用的设备、范围和方向必须符合 API 的配对要求。
一次接收可这样理解:CPU 分配合适缓冲区并映射为 FROM_DEVICE,将返回的 DMA 地址提交给设备;确认设备真正完成后解除映射,再由 CPU 读取。解除映射不是停止硬件的命令,必须先确认设备不再访问。
映射不撤销时,CPU 与设备仍要交接访问权
长期保留映射时,在确认设备完成后调用 dma_sync_single_for_cpu(),CPU 才进入访问阶段;交还设备前调用 dma_sync_single_for_device()。这些调用可能涉及缓存维护或 bounce buffer 拷贝,但不会替驱动阻止另一方同时访问。
在非一致性平台的简化模型里,把 CPU 缓存与 RAM 内容分开显示。通过同步动作转移所有权,观察设备完成与 CPU 已可读取之间还缺哪一步。
正在呈现知识画面
DMA 已写 RAM,CPU 的旧缓存为什么还不能直接用?设备完成、缓存可见性和映射寿命是不同条件。sync 不等待仍在进行的 DMA,unmap 不停止硬件;CPU VA、物理地址和 DMA 地址也不能互换。
{"id":"dma-streaming-ownership","title":"DMA 已写 RAM,CPU 的旧缓存为什么还不能直接用?","summary":"设备完成、缓存可见性和映射寿命是不同条件。sync 不等待仍在进行的 DMA,unmap 不停止硬件;CPU VA、物理地址和 DMA 地址也不能互换。","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"actions\"><button id=\"cpuWrite\" data-i18n=\"cpuWrite\"></button><button id=\"forDevice\" data-i18n=\"forDevice\"></button></div><div class=\"actions\"><button id=\"deviceWrite\" data-i18n=\"deviceWrite\"></button><button id=\"complete\" data-i18n=\"complete\"></button></div><div class=\"actions\"><button id=\"forCPU\" data-i18n=\"forCPU\"></button><button id=\"unmap\" data-i18n=\"unmap\"></button></div><div class=\"actions\"><button id=\"map\" data-i18n=\"map\"></button></div><div class=\"address\">CPU VA 0xffff800000001000<br>PA 0x4000 · DMA 0x8000</div><div class=\"track-row\"><span data-i18n=\"mapping\"></span><div id=\"mapping\" class=\"track\"></div></div><div class=\"track-row\"><span data-i18n=\"cpu\"></span><div id=\"cpu\" class=\"track\"></div></div><div class=\"track-row\"><span data-i18n=\"ram\"></span><div id=\"ram\" class=\"track\"></div></div><div class=\"card\"><header data-i18n=\"owner\"></header><div id=\"owner\" class=\"readout\"></div></div><div class=\"copy\"><p id=\"explanation\" role=\"status\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"note\"></p></div><div class=\"actions\"><button id=\"reset\" data-i18n=\"reset\"></button></div><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:500;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:500 20px var(--viz-font-ui)}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:var(--viz-font-mono);font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px var(--viz-font-ui);background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n.address{font:11px/1.8 var(--viz-font-mono);overflow-wrap:anywhere;margin:15px 0}.readout{min-height:5.3em}","js":"const $=s=>document.querySelector(s);const set=(id,v)=>$('#'+id).textContent=v;const t=k=>viz.t(k);function cells(id,values){$('#'+id).replaceChildren(...values.map(v=>{const e=document.createElement('div');e.className='cell '+(v.cls||'');e.textContent=v.text;e.title=v.title||v.text;return e;}));}function pressed(mode){document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}let cpu=[0,0,0,0],ram=[0,0,0,0],owner='CPU',mapped=true,active=false,serial=0;function draw(){cells('cpu',cpu.map(n=>({text:String(n),cls:owner==='CPU'?'on':'empty'})));cells('ram',ram.map(n=>({text:String(n),cls:owner==='DEVICE'?'gold':'empty'})));cells('mapping',[{text:mapped?'0x8000 → 0x4000':'0x8000 × 0x4000',cls:mapped?'on':'empty'}]);set('owner',owner+'\\n'+t(active?'active':'idle'));$('#cpuWrite').disabled=owner!=='CPU';$('#forDevice').disabled=!mapped||owner!=='CPU';$('#deviceWrite').disabled=!mapped||owner!=='DEVICE'||active;$('#complete').disabled=!active;$('#forCPU').disabled=!mapped||owner!=='DEVICE'||active;$('#unmap').disabled=!mapped||active;$('#map').disabled=mapped;set('explanation',t('note'));}$('#cpuWrite').onclick=()=>{cpu=[++serial,serial,serial,serial];draw();};$('#forDevice').onclick=()=>{ram=[...cpu];owner='DEVICE';draw();};$('#deviceWrite').onclick=()=>{ram=[++serial,serial,serial,serial];active=true;draw();};$('#complete').onclick=()=>{active=false;draw();};$('#forCPU').onclick=()=>{cpu=[...ram];owner='CPU';draw();};$('#unmap').onclick=()=>{if(owner==='DEVICE')cpu=[...ram];mapped=false;owner='CPU';draw();};$('#map').onclick=()=>{ram=[...cpu];mapped=true;owner='DEVICE';draw();};$('#reset').onclick=()=>{cpu=[0,0,0,0];ram=[0,0,0,0];owner='CPU';mapped=true;active=false;serial=0;draw();};draw();","audio":false,"strings":{"active":"设备请求进行中","complete":"确认 DMA 已完成","cpu":"CPU 缓存视图","cpuWrite":"CPU 修改内容","deviceWrite":"设备开始写入","forCPU":"sync_for_cpu","forDevice":"sync_for_device","heading":"DMA 已写 RAM,CPU 的旧缓存为什么还不能直接用?","idle":"设备不再访问","intro":"在 CPU 与设备之间交接 streaming 缓冲区。先改 CPU 内容,再交给设备写入;分别完成 DMA、同步回 CPU 或解除映射。","map":"重新映射给设备","mapping":"DMA 地址 → 物理地址","note":"设备写入后,CPU 那一行可以仍是旧值。先确认完成,再 sync_for_cpu,才能按协议进入 CPU 访问阶段;图中禁用的动作正是这些未满足的条件。","owner":"当前允许访问的一方","ram":"RAM / 设备访问视图","reset":"重置","scope":"非一致性平台的 DMA_BIDIRECTIONAL,四字节独占缓存行,无 bounce。初始映射已完成一次 sync_for_cpu。图以观察者视角展示双方副本,不许可 CPU 在设备拥有期间读取。交接折叠必要缓存维护,不能理解为所有 coherent 平台每次都复制数据。","scopeLabel":"模型范围与约定","unmap":"解除 DMA 映射"}}
什么内存可以直接映射
dma_map_single() 适用于满足约束的连续内存,例如适当分配的 kmalloc 缓冲区。栈内存、内核映像地址,以及 vmalloc 返回的虚拟连续地址不能直接当通用单段 DMA 缓冲区使用。vmalloc 底层页面需要通过合适的逐页/SG 路径处理。
还应避免 CPU 字段与设备写入区域共享缓存行,在非一致性平台上这种共享可能造成数据覆盖。dma_mapping_error(dev, addr) 判断映射是否失败;DMA 地址为零并不自动代表失败。
4. Scatter-gather 的两种数量
SG 允许用多个内存片段表达一次数据传输。DMA 层可以合并相邻片段,设备看到的段数可能少于输入条目数。
以下是局部 API 用法,假设 sgl 已正确建立,且 orig_nents > 0:
int mapped_nents = dma_map_sg(dev, sgl, orig_nents, direction);
if (mapped_nents == 0)
return -EIO;
struct scatterlist *sg;
int i;
for_each_sg(sgl, sg, mapped_nents, i) {
dma_addr_t addr = sg_dma_address(sg);
unsigned int len = sg_dma_len(sg);
dev_dbg(dev, "DMA segment %d: %pad / %u\n", i, &addr, len);
}
dma_unmap_sg(dev, sgl, orig_nents, direction);
| 使用位置 | 应传哪种数量 |
| 给硬件设置 DMA 段 | mapped_nents |
| dma_unmap_sg | 原始 orig_nents |
| dma_sync_sg_for_* | 原始 orig_nents |
映射层会生成设备视角的段,也要保留还原原始映射所需的信息。不能把合并后的计数用于全部接口。见 SG API 说明。
5. IOMMU 与 SWIOTLB 可以同时存在
IOMMU 通过页表提供地址转换、权限与隔离,还可能把离散物理页映射到连续 IOVA。它不保证任意 SG 都变成单段,仍受段长、边界和映射资源约束。中断重映射是平台提供的相关但独立能力,不能仅因存在 DMA 翻译就断言中断安全条件全部满足。
SWIOTLB 提供 bounce buffer:设备操作可访问的中间缓冲区,由 DMA 层在相应同步点搬运数据。除了设备寻址受限,机密计算中的共享内存约束等也可能使用 bounce;因此不是简单的“有 IOMMU 就无拷贝,无 IOMMU 才 SWIOTLB”。
拷贝方向与 DMA 方向有关:TO_DEVICE 需要把 CPU 准备的数据送到设备可读区域,FROM_DEVICE 则要在 CPU 接收数据时取回设备结果。是否需要拷贝、发生在哪个同步点由映射与同步路径决定,不能假设每次 map/unmap 都双向复制。
SWIOTLB 池大小参数有自己的单位,不能把 slot 数直接当 KiB。例如常见 2 KiB slot 下,65536 个 slot 是 128 MiB,而不是 64 MiB。具体参数、动态池及限制以目标内核的 SWIOTLB 文档为准。
6. Domain、group 与 VFIO
| 对象 | 回答的问题 |
| IOMMU domain | 使用哪套地址空间与映射策略?多个设备可共享 |
| IOMMU group | 系统最小能可靠隔离哪些设备? |
group 受拓扑、ACS 与设备行为影响,不是固定一项 PCI function 一组。
将一个 group 中某功能交给不受信任的用户态时,还必须控制组内其他相关设备,不能留下能够绕过隔离边界的驱动访问。这不等于要求每个 function 都实际呈现给同一 VM;具体绑定与所有权约束由 VFIO/IOMMU 接口管理。
VFIO 允许受控暴露设备资源、DMA 映射和中断。设备访问的 guest 内存底层依然是宿主物理页,保护目标是让它只能访问授权映射,不能写成“完全不能访问 host 内存”。无 IOMMU 的特殊模式也不能视为具有相同隔离保证。见 VFIO 文档。
查阅顺序
地址异常先查 DMA mask、返回值和写入描述符的地址;数据损坏再查方向、完成时机、缓存行共享及屏障;释放后损坏则查是否在硬件和回调结束前 unmap/free。IOMMU fault 能说明某次设备访问违反了映射条件,但不能仅凭 fault 就断定页表实现有错。
关联阅读:PCIe 与 USB 驱动模型、DRM 与图形。