デバイス記述子内のアドレスは、CPUポインタを無理やりキャストして得られるものではありません。unmapもまた、ハードウェアに処理を停止させるコマンドではありません。DMAでは、アドレス、方向、可視性、所有権の引継ぎを同時に管理する必要があります。
| 参照目標 | 読書位置 |
| アドレスとcoherent/streaming | 第1~2節 |
| 方向、完了、SGカウント | 第3~4節 |
| IOMMU、バウンス、分離 | 第5~6節 |
1. 3種類のアドレスは互いにキャストできない
| アドレス | 使用主体 | 典型型 |
| CPU仮想アドレス | カーネルコードによるバッファアクセス | void * |
| CPU物理アドレス | メモリとリソース管理 | phys_addr_t |
| DMAアドレス | デバイス記述子への書き込み、デバイスによるアドレス指定 | dma_addr_t |
IOMMUは、デバイスが発行するIOVAを物理ページアドレスに変換できる。IOMMUがない場合、DMAアドレスはブリッジオフセットなどの影響を受ける可能性がある。dma_addr_tは直接間参照することも、CPUポインタを適当にキャストして得ることもできない。あるマシンでは数値が偶然一致することがあっても、それは移植性の保証にはならない。
ドライバはハードウェアの能力に応じてDMAマスクを設定する。記述子とデータペイロードのアドレスビット幅は異なる場合があり、必要に応じてstreaming maskとcoherent maskをそれぞれ設定する。狭いマスクはプラットフォームでサポートされない可能性があるため、APIの戻り値を処理する必要がある。CPUが64ビットだからといって、デバイスが64ビットDMAをサポートすると主張してはならない。
2. Coherentとstreamingは異なるアクセスパターンを解決する
| モデル | 一般的な用途 | ドライバが依然として責任を持つこと |
| coherent allocation | 長期間存在し、双方が頻繁にアクセスする記述子とステータス領域 | アクセス順序、排他制御、ハードウェアの完了と解放のタイミング |
| streaming mapping | ネットワークパケット、ストレージデータなどのバッチ転送 | 方向、所有権の引継ぎ、同期とマッピングの寿命 |
dma_alloc_coherent()はCPUアドレスを返し、デバイスが使用するためのDMAアドレスを出力する。解放する際は、一致するデバイス、サイズ、CPUアドレス、DMAハンドルを使用する。coherentは、このメモリが通常のアクセスに必要な明示的なキャッシュ管理を免除するが、すべての書き込みが即座にソースコードの順序でデバイスに見えるわけではない。また、複合操作を自動的にアトミックにするものでもない。
例えば、記述子を公開する際、まずアドレスと長さを書き込み、適切なdma_wmb()を実行し、最後にデバイスプロトコルに従って有効ビットを設定する。デバイスが完了した後、CPUは完了フラグと共に公開されたデータを安全に読み取るためにdma_rmb()を必要とする場合もある。MMIOドアベルへのアクセス順序は、I/Oアクセサとハードウェアの要件も満たす必要がある。バリアはDMAの完了を待つものではない。
streamingの「ストリーミング」とは、必ずしも1回だけマッピングできるという意味ではなく、coherentよりも必ずしも高速であることも保証されない。パフォーマンスはプラットフォーム、キャッシュ、マッピングオーバーヘッド、アクセスパターンに依存する。概念とアドレスのルールについてはDMA HOWTOを、具体的なインターフェースについてはDMA APIを参照のこと。
3. 方向はデバイスとメモリ間のデータフローに基づく
DMA_TO_DEVICE:デバイスがメモリを読み取る(例:ネットワークパケットの送信)。
DMA_FROM_DEVICE:デバイスがメモリに書き込む(例:ネットワークパケットの受信)。
DMA_BIDIRECTIONAL:双方が書き込みを行う場合。所有権と同期プロトコルに従う必要がある。
「読み取り」や「書き込み」とだけマークしないでください。そうすると、CPUとデバイスの2つの視点で逆になってしまいます。方向が確定している場合は正確な方向を使用してください。map、sync、unmapで使用するデバイス、範囲、方向は、APIのペアリング要件に一致している必要があります。
受信の1回限りの処理は以下のように理解できます。CPUが適切なバッファを割り当て、FROM_DEVICEとしてマッピングし、返されたDMAアドレスをデバイスに渡します。デバイスが本当に完了したことを確認してからマッピングを解除し、その後CPUが読み取ります。マッピング解除はハードウェアを停止させるコマンドではありません。デバイスがアクセスしなくなったことを確認してから解除する必要があります。
マッピングを解除しない場合でも、CPUとデバイスはアクセス権を引継ぐ必要がある
マッピングを長期間保持する場合、デバイスが完了したことを確認した後でdma_sync_single_for_cpu()を呼び出し、CPUがアクセスフェーズに入ります。デバイスを返す前にdma_sync_single_for_device()を呼び出します。これらの呼び出しはキャッシュ管理やバウンスバッファのコピーを含む場合がありますが、他方の同時アクセスをドライバが阻止するわけではありません。
非一貫性プラットフォームの簡略化モデルでは、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、4バイトの独占キャッシュライン、バウンスなし。初期マッピングはsync_for_cpuを1回実行済み。図は観察者視点で双方のコピーを表示し、デバイスが所有している間にCPUが読み取ることを許可しない。引継ぎには必要なキャッシュ管理を折りたたみ、すべての coherentプラットフォームで毎回データがコピーされるわけではないと理解してはならない。","scopeLabel":"モデルの範囲と規約","unmap":"DMAマッピングの解除"}}
どのメモリを直接マッピングできるか
dma_map_single()は、制約を満たす連続メモリ、例えば適切に割り当てられたkmallocバッファに適用できる。スタックメモリ、カーネルイメージアドレス、およびvmallocが返す仮想連続アドレスは、汎用の単一セグメントDMAバッファとして直接使用できない。vmallocの基盤となるページは、適切なページ単位の処理またはSGパスを介して処理する必要がある。
また、CPUフィールドとデバイスが書き込む領域がキャッシュラインを共有しないようにする必要がある。非一貫性プラットフォームでは、この共有がデータの上書きを引き起こす可能性がある。dma_mapping_error(dev, addr)でマッピングの失敗を判断する。DMAアドレスが0だからといって、自動的に失敗を意味するわけではない。
4. Scatter-gatherの2つの数量
SGにより、複数のメモリ断片を使って1回のデータ転送を表現できる。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はバウンスバッファを提供する。これはデバイスが操作できる中間バッファであり、DMA層が対応する同期ポイントでデータを転送する。デバイスのアドレス指定が制限されている場合のほか、機密計算における共有メモリの制約などにもバウンスが使用される。したがって、「IOMMUがあればコピーなし、IOMMUがない場合のみSWIOTLB」という単純な話ではない。
コピーの方向はDMAの方向に関連する。TO_DEVICEの場合、CPUが準備したデータをデバイスが読み取り可能な領域に送る必要がある。FROM_DEVICEの場合、CPUがデータを受信する際にデバイスからの結果を取り戻す必要がある。コピーが必要かどうか、どの同期ポイントで発生するかは、マッピングと同期パスによって決定され、map/unmapのたびに双方向コピーが行われると仮定してはならない。
SWIOTLBのプールサイズパラメータには独自の単位があり、スロット数を直接KiBとして扱ってはいけない。例えば、一般的な2 KiBスロットの場合、65536スロットは64 MiBではなく128 MiBである。具体的なパラメータ、動的プール、制限については、対象カーネルのSWIOTLBドキュメントを参照のこと。
6. Domain、group、およびVFIO
| オブジェクト | 回答する質問 |
| IOMMU domain | どのアドレス空間とマッピングポリシーを使用するか?複数のデバイスが共有可能 |
| IOMMU group | システムが確実に分離できる最小限のデバイスは何か? |
groupはトポロジ、ACS、およびデバイスの動作に影響され、固定された「PCI function 1つにつき1グループ」とは限らない。
group内の特定のfunctionを信頼できないユーザー空間に渡す場合、グループ内の他の関連デバイスも制御する必要があり、分離の境界を回避できるドライバアクセスを残してはならない。これは、各functionが実際に同じVMに提示されることを要求するものではない。具体的なバインディングと所有権の制約はVFIO/IOMMUインターフェースによって管理される。
VFIOは、デバイスリソース、DMAマッピング、割り込みの制御された公開を許可する。デバイスがアクセスするゲストメモリの基盤は依然としてホストの物理ページであり、保護の目標は「ホストメモリに一切アクセスできない」ことではなく、認可されたマッピングのみをアクセスできるようにすることである。IOMMUがない特殊なモードも、同じ分離保証を持つとは見なせない。VFIOドキュメントを参照。
参照順序
アドレスの異常については、まずDMAマスク、戻り値、および記述子への書き込まれたアドレスを確認する。データの不整合については、方向、完了のタイミング、キャッシュラインの共有、およびバリアを確認する。解放後の破損については、ハードウェアとコールバックの終了前にunmap/freeが行われていないか確認する。IOMMUフォールトは、あるデバイスのアクセスがマッピング条件に違反したことを示すが、フォールトがあるからといって、ページテーブルの実装に誤りがあると断定してはならない。
関連読書:PCIeとUSBドライバモデル、DRMとグラフィックス。