ネットワークからデータが届くとき、CPUはケーブルから1バイトずつ読み続けているのでしょうか。一般的なOSでは、デバイスへの指示、メモリへの転送、完了の通知を分担します。CPUが計算している間にも機器は動くため、メモリを誰がいつ使ってよいかを決める必要があります。
デバイスへの指示と状態の読取り
ネットワークカードやストレージには、機器を制御するコントローラがあります。ドライバは、制御用レジスタへ値を書いて処理を始めさせたり、状態を読んだりします。メモリマップトI/Oでは、アドレス空間の一部をこうしたレジスタに対応させます。
CPUから同じような読書き命令に見えても、機器レジスタは通常のRAMとは性質が違います。読んだだけで通知が消える、書く順序で動作が変わる、といった副作用があります。実際のドライバでは、OSが用意するI/Oアクセス用APIと、機器の仕様に従います。普通のポインタ操作に置き換えてよいとは限りません。
完了を確かめる方法を選ぶ
状態を繰り返し読んで完了を調べるのがポーリングです。待つ間もCPUを使いますが、短い待ち時間を低い遅延で見つけたい場面などでは有用です。割込みでは、機器などからの通知をきっかけにCPUが処理を移します。常に監視しなくてよい一方、処理の切替えにも費用があります。
外部の機器やタイマによる割込みと、実行した命令に関係して起こる例外は区別します。例えば存在しないページへのアクセスは、命令に由来するページフォルトです。どちらもOSへ制御を渡す入口になりますが、発生原因も、元の命令へ戻るときの扱いも同じとは限りません。
割込み処理では、何が起きたかを確認し、機器へ応答し、必要な後続処理につなぎます。大量のパケットごとに割込みを起こすと費用が増えるため、まとめて通知したり、割込みをきっかけに一定数をポーリングで処理したりする方式もあります。割込みとポーリングは、組み合わせて使えます。
DMAではCPU以外がメモリへ転送する
DMAは、CPUが各データを逐一ロード・ストアしてコピーする代わりに、デバイス側などがメモリとの転送を進める仕組みです。CPUはバッファや転送情報を準備し、完了後に結果を受け取ります。転送の指示や完了処理まで不要になるわけではありません。
受信バッファを確保する
→ デバイスが使えるDMAアドレスを設定する
→ バッファをデバイスに渡す
→ デバイスがデータを書き込む
→ 完了を通知・確認する
→ CPUがバッファの内容を処理するCPUの仮想アドレスを、そのまま機器へ渡せるとは限りません。DMAアドレスには機器のアドレス幅やIOMMUなどが関わります。LinuxのDMA APIは、この対応付けや必要なキャッシュ同期を扱います。CPUが読んでいる途中で機器が書き換えないように、バッファの所有権を切り替える考え方も大切です。
複数コアのキャッシュをそろえる
各コアが自分のキャッシュを持つ場合、一方のコアが書いた値を、別のコアが古いコピーから読み続けない仕組みが必要です。キャッシュコヒーレンスは、同じメモリ位置のコピーを扱うための仕組みです。ただし、複数の場所への操作をどんな順番で他のコアが観測するかというメモリ順序の問題は、これだけでは片付きません。
二つのスレッドが同じカウンタを増やすとき、キャッシュの値が整合していても、読取り・加算・書込みが一つにまとまるとは限りません。双方が5を読み、双方が6を書けば、一回分の増加が失われます。原子的な読取り変更書込み命令やロックを使い、必要な操作全体を守ります。
違う変数でも同じキャッシュラインに置かれると、別々のコアからの書込みでラインが行き来し、性能が落ちることがあります。これは偽共有です。正しさの問題と性能の問題を分け、計測したうえでデータ配置や分担を見直します。
確かめてみよう
問1:DMAを使えばCPUは何もする必要がないでしょうか。CPUはバッファの準備、転送の設定、完了後の処理などを担当します。
問2:キャッシュコヒーレンスがあれば、通常の加算で共有カウンタを安全に増やせるでしょうか。読取りから書込みまでのまとまりは保証されないので、安全にはなりません。
操作が混ざる順番と守り方は、インターリーブ・競合状態・原子性で具体的に追います。