テストステ論

高テス協会会長が, テストステロンに関する情報をお届けします.

(writeboost report) ジョーのツリーへのマージとバッファのアラインメントに関するバグの修正

4月に入り, writeboostはジョーのツリーにマージされた. ちょうど一週間前のことだ. 当時私は, dmtsへのテストのポーティングとそのテストが有用であることのJustificationに必死だった. ジョーとの議論の末, 私のテストはマージされた. このテストをマージするまでに, 今後のポーティング計画, writeboostのtype 0, 1をどうやってテストするかということについて主張をし, 結果, 認められた(ただし, コミット自体はジョー好みに同値変形された上でマージされた).

その議論の中でジョーが「writeboostもうそろそろいけそう?おれがテストしてやるよ, 最新のコードはどれ?」と聞いてきたのでGithubのコードだと言うと, 数日後にはジョーのツリーにマージされた. https://github.com/jthornber/linux-2.6/network

しかし次の瞬間にはジョーからアラートが上がった. まずはコンパイルのエラー. これは軽微なものであったためすぐに原因が分かったが, テストを実行しようとするとカーネルがクラッシュするという出会ったことのないバグの原因が分かるまで, 4日ほどかかった. writeboostを実行しようとするとほどなく以下のようなメッセージが出る. もともと私がデバグ用に使ってたコンフィグはデフォルトのものであり, kernel hackingのオプションは有効にしていなかった. しかしジョーのコンフィグでは有効になっており, それがこのバグを明らかにした.

=============================================================================
[   61.786776] BUG kmalloc-4096 (Not tainted): Redzone overwritten
[   61.787094] -----------------------------------------------------------------------------
[   61.787094]
[   61.787520] Disabling lock debugging due to kernel taint
[   61.787523] INFO: 0xffff8801f85743d8-0xffff8801f85743df. First byte 0x0 instead of 0xcc

このエラーはメモリ関連のエラーであり, 当初, スタックオーバーフローかなんかかと思っていたが, スタックオーバーフローであれば別のエラーで落ちる. 調べるとこのエラーは, slabから取得した領域外にあるRedzoneというはみ出し検出用領域をoverwriteしたことを知らせるものだと分かった. 具体的には, slabのフラグにSLAB_RED_ZONEがあるので参考にして欲しい.

色々調査した結果, I/O用に使っているバッファをkmallocからvmallocにすると症状が収まることが分かった. 私がジョーにこのことを報告すると「いやそれはvmallocはRedzone検出しないからじゃない?」という当たり前のことを言われて, 結局, 根本原因を突き止めることが必要となった.

ここで, dm_ioの奥底で何が起こっているか見てみよう(dm_ioについては http://akiradeveloper.hatenadiary.com/entry/2013/05/18/132406 も参照). dm_ioから下っていくとdo_regionという関数があり, この中で以下にようなことをしている.

                } else while (remaining) {
                        /*
                         * Try and add as many pages as possible.
                         */
                        dp->get_page(dp, &page, &len, &offset);
                        len = min(len, to_bytes(remaining));
                        if (!bio_add_page(bio, page, len, offset))
                                break;

                        offset = 0;
                        remaining -= to_sector(len);
                        dp->next_page(dp);
                }

                atomic_inc(&io->count);
                submit_bio(rw, bio);

dpというのは, dm_ioに渡すdm_io_requestにある「どうやって確保したバッファを使うか」フラグによって生じる分岐を隠蔽する抽象であり, やっていることは基本的に以下のようなことである.

  1. バッファの先頭アドレスが所属するpageを特定し, offsetを計算する.
  2. I/O長(remaining)と, len=4KB-offsetの短い方を採用して, bio_add_pageをする.
  3. bio_add_pageでは, Vectored I/O(http://en.wikipedia.org/wiki/Vectored_I/O)用のbio_vecが作られる. (#)
  4. 残りの領域がある場合これを繰り返す.
struct bio_vec {
        struct page     *bv_page;
        unsigned int    bv_len;
        unsigned int    bv_offset;
};

私はどうにかしてkmallocで4KB確保した場合なぜか512Bアラインすらされていないことに気づいた. 一方でvmallocから確保したバッファは512Bアラインされていた. ところで, そのアドレスがアラインされているかどうかを調べるには, IS_ALIGNEDマクロが使える. また, あるアドレスから次のアラインメントを計算する場合は, ALIGNマクロが使える. IS_ALIGNEDマクロを使って適当に調べたら, 512Bアラインすらされていないことに気づいた.

これはきな臭いと思ったが, 少なくともbio_add_pageくらいのコードを読む分には, コード上は512Bアラインされている必要はどこにもないように思った. あるとしたらもっと下層の要請だ. 私は上記(#)が作るbio_vecのoffsetについて3つの仮説を立てた.

  1. どういうoffsetであってもOK
  2. 512Bアラインされている必要がある
  3. 4096Bアラインされている必要がある

当然, 3は前提が多いため, 下層の最適化が効きやすくなる意味があるが, 反対に1は性能が悪くなる可能性があるが柔軟性は高い. 私の結論は2だ. この理由については後述する.

バッファが何かしらアラインされる必要があるということは簡単に気づくことが出来た. 以下のコードはdm-bufioからのものだが, バッファ用として使うために, c->block_sizeアラインされたslabを独自定義している. kmem_cache_createの引数は前から, 名前, 要素大きさ, アラインメントだ.

           if (!DM_BUFIO_CACHE(c)) {
                        DM_BUFIO_CACHE(c) = kmem_cache_create(DM_BUFIO_CACHE_NAME(c),
                                                              c->block_size,
                                                              c->block_size, 0, NULL);
                        if (!DM_BUFIO_CACHE(c)) {
                                r = -ENOMEM;
                                mutex_unlock(&dm_bufio_clients_lock);
                                goto bad_cache;
                        }
                }

私は, バッファは少なくとも512Bアラインされている必要がありそうなことは分かったので, とりあえず, __get_free_pagesを使ってバッファを確保してDM_IO_KMEMで発行することにした(原理的には可能である. kmallocは, 確保領域が大きい時, __get_free_pagesを使っている)が, なぜかこれはうまく行かなかった. そこで, dm-bufioと同様にslabを作ることとした. pageには色々なフラグがある. たぶん, バッファとして使うにはあまりにrawすぎたのだろうと推察する.

かくして私は, 512Bアラインされた, 512B, 4096Bバッファ確保用slabを定義して, バグが発現している箇所を修正し, ジョーにプルリクを送った. そしてさきほど, そのコミットはマージされた.

私がそのプルリクに書いた, なぜ512Bアラインで良いのかという理由はこうである. 「思考実験をしてみよう. もし, 今からI/Oをするデバイスが, その下にあるデバイスから1sectorシフトしたlinearデバイスであったとしてもI/Oが出来るのだから, アラインは512B(1sector)で良いということになる」我ながらエレガントな主張であると思う. ちなみに, この主張が正しいかどうかは何も言及されていない(たぶん正しいと思うが). しかし, 私にとってはそんなことどうだっていい. 少なくとも, おれがバグってるならdm-linearも誤りだという論理に帰着出来たからだ. もしdm-linearが8sectorずつしかシフト出来ないということになれば, 狂ってしまうシステムは世界で1つや2つではないだろう. つまり, writeboostと道連れだ.


今後もジョーのツリーにプルリクしてwriteboostを改善していく. そして一直線にメインラインへのマージを目指す. バッファに関するバグは失せたと思うので, 今頭にある修正を着実に積み重ねていけばいい. 仕事でも家でも全力で開発しており, 頭がおかしくなりそうというかもうなってると思うが, ジョーの期待に応えたい. こういう実績がきっとおれの世界への道を開くと思う.