前回, device-mapperはブロックデバイスを完全にemulateする仮想デバイスを作成することを述べた. そして, 仮想デバイスが受け取ったbioに対して, 「そのbioをどのように処理するか」というstrategy*1を選択し, strategyは, bioを処理したあと, bioに設定されたコールバック関数endioを呼び出す, という話をした.
この話に若干の省略がある. 例えば,
range(from, to) | target |
---|---|
(0, 10) | linear /dev/sdb1 0 |
(11, 20) | linear /dev/sdc1 0 |
というテーブルがあったとする. このテーブルを持った仮想デバイスに対して, sector番号8から12までのI/Oが来たらどうすればいいだろうか. 当然, 8から10まではsdb1に送り, 11から12まではsdc1に送るのが望まれる動作である.
仮想デバイスは受け取ったbioに対していきなりstrategyを選択するわけではなく, まず, splitする. その設定は, 例えばdm-lcの中では以下のように書かれている*2.
|
|
3.5まではsplit_ioに直接設定する形だったが, 3.6からは(やや潔癖すぎると私は思うのだが), dm_set_target_max_ioという関数を呼び出すように再設計された*3. dm-lcでは, 受け取ったbioは(1<<3)セクタ(4KB)ごとに分割されて, キャッシュ処理が行われる.
ただし, 分割されたbioは必ず4KBというわけではなく, もともと4KB未満のbioであればそのままであるし, 4KB境界に沿わなければ, 中途半端な分割がなされることもある. ここらへんは, device-mapperフレームワークが適当に計算して分割してくれる.
targetがなすことは, この小さなbioを処理して, 小さなbioに対するendioを呼ぶことである. 元bioのendioは, すべての小さなbioについてendioが呼ばれた後で呼ばれる.
まとめると, 仮想デバイスが受け取ったbioは, 以下のように処理される.
- 仮想デバイスが元bioを受け取る.
- 元bioは小さなbio(0, 1, 2, 3, ..)に分割される(device-mapperではこの小さなbioのことをclone bioと呼んでいる). このsplit境界は, split_ioで設定される. また, 分割方法は, 仮想デバイスの持つテーブルのrange keyを意識して行われ, 小さなbioが異なる2つのtargetにまたがることはない.
- 小さなbioは, target#mapに入る (#).
- 小さなbioのendio(clone_endioという)が呼ばれる. この中でtarget#endioが呼ばれる.
- すべての小さなbioについてendioが呼ばれると, 元bioについてendioが呼ばれる.
まるで並列処理におけるmap-reduceのようである. device-mapperのtargetを実装する上でもっとも本質的な部分は, (#)の部分であり, dm-lcにおいては, 半分以上のコードはmap関数(lc_map)のために書かれたものである.
以上である. 今回は, 仮想デバイスがbioをターゲットに渡すためにsplitするという話をした. 次回以降, mapとendioについて, 具体的にコードベースで説明していく.