device-mapperの仕組み (2) I/Oの概要

前回, 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.

1
2
3
4
5
#if LINUX_VERSION_CODE >= KERNEL_VERSION(3,6,0)
        r = dm_set_target_max_io_len(ti, (1 << 3));
#else
        ti->split_io = (1 << 3);
#endif

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は, 以下のように処理される.

  1. 仮想デバイスが元bioを受け取る.
  2. 元bioは小さなbio(0, 1, 2, 3, ..)に分割される(device-mapperではこの小さなbioのことをclone bioと呼んでいる). このsplit境界は, split_ioで設定される. また, 分割方法は, 仮想デバイスの持つテーブルのrange keyを意識して行われ, 小さなbioが異なる2つのtargetにまたがることはない.
  3. 小さなbioは, target#mapに入る (#).
  4. 小さなbioのendio(clone_endioという)が呼ばれる. この中でtarget#endioが呼ばれる.
  5. すべての小さなbioについてendioが呼ばれると, 元bioについてendioが呼ばれる.

まるで並列処理におけるmap-reduceのようである. device-mapperのtargetを実装する上でもっとも本質的な部分は, (#)の部分であり, dm-lcにおいては, 半分以上のコードはmap関数(lc_map)のために書かれたものである.

以上である. 今回は, 仮想デバイスがbioをターゲットに渡すためにsplitするという話をした. 次回以降, mapとendioについて, 具体的にコードベースで説明していく.

*1:これは旧式のstrategy patternである *2:dm-lcは, 現在, 3.4以降の全カーネルに対応している. Debian wheezyのカーネルが3.2なので, 3.2もサポートしようかと考えている *3:抽象度を上げて将来の柔軟性をとったわけであるが, 後方互換を捨てるほどの価値があったか
comments powered by Disqus
Built with Hugo
テーマ StackJimmy によって設計されています。