ミニカーネルダンプ- 確実にダンプを取る White Paper : Mini Kernel Dump

Linuxのエンタープライズ・システムに対する適用が進むにつれ、障害解析の重要性もますます高まってきています。実際、運用を行っているシステムに関しては、障害が起きたとしてもそのシステム上でデバッグをすることは困難です。それに加えて、障害に再現性がないケースも少なくありません。
こうしたケースでは、クラッシュダンプが調査を行うための最後のよりどころとなります。したがって、障害解析のためにはシステムクラッシュ時に確実にダンプが取れることが必須の前提条件なのです。

既存のクラッシュダンプの問題点

標準のLinuxカーネルでは、クッラシュダンプ機能は入っていません。
そこで、Linuxにクラッシュダンプ機能を追加するためにLKCD[1]、netdump[2]、diskdump[3]等のツールの開発が行われています。
しかし、これらの既存のクラッシュダンプ機能には、ダンプ採取の確実性の観点から、大きく二つの問題点が存在します。
一つ目は、資源の問題です。特に資源のロックに問題があります。
いずれのクラッシュダンプ・システムもデバイスへの出力は、既存のドライバを使用します。ドライバは、通常のカーネルサービスを使用しますが、もし仮にクラッシュを引き起こした処理が、ダンプ出力のために必要な資源をロックしていた場合、ダンプ処理はデッドロックに陥ることになります。また、マルチプロセッサ・システムにおいては、クラッシュダンプの開始時に他のCPUで実行中の処理については強制的に中断させられるため、ダンプ出力にために必要な資源をロックしていれば、ダンプ処理はデッドロックに陥ります。
ロックはしていなくても、資源不足のためにダンプ処理が失敗する可能性もあります。
二つ目は、制御表の信頼性の問題です。カーネルがクラッシュするということは、カーネル内に何らかの矛盾が生じているということであり、制御表が破壊されてる可能性が高いわけです。既存のクラッシュダンプでは、ダンプの出力にカーネルの通常の機能を使用しているために、破壊されている制御表を参照する可能性が高くなります。
元々、クラッシュダンプ機能は、カーネルがもはや信用できないという前提で、必要最小限かつ動くルートを完全に把握して設計されていなければなりません。
Linuxの既存のクラッシュダンプ機能の問題点は、カーネルが正常に動くことを前提として設計されていることであると言えます(特にLKCD)。
Linux カーネルは、複雑化の一途をたどっており、SCSIサブシステムを取り上げても、その動きを完全に把握することは困難です。既存の Linuxカーネルの中に制御可能なダンプルートを設けることは難しいと考えられます。したがって、既存のカーネルと無依存にダンプを採取できる機能を考える必要が出てくるのです。

ミニカーネルダンプ

VA Linuxは、ミニカーネルダンプと呼ぶ手法により、ダンプ採取の確実性を向上させました。ミニカーネルダンプの基本的なアイデアは、クラッシュ時に別のカーネルを立ち上げてダンプを採取することです。ミニカーネルダンプの処理の流れは以下のようになります。

 

  1. ダンプ処理を行うのみの小さなカーネル(ミニカーネル)を用意しておきます。
  2. ミニカーネルが動作する領域をあらかじめ確保し、ミニカーネルをロードしておきます。
  3. クラッシュが発生したら、ミニカーネルを起動します。ミニカーネルの起動は、BIOSを使用せずに行います。メモリのクリアはしません。
  4. ミニカーネルは、全てのメモリをダンプデバイスに出力し、リブートします。

1.の処理は、システムを起動した後になるべく早い段階で行っておきます。
ミニカーネルが動作するのに必要な領域は、4MBもあれば十分です。
数GBも搭載するシステムにおいて、ダンプ採取のために4MBの領域を消費することは、実用上問題にならないと考えられます。ミニカーネルは、クラッシュしたカーネルの資源とは全く無関係に動作するため、【前述、既存のクラッシュダンプの問題点】で述べた問題はありません。

mkexec

ミニカーネルの起動は、kexec[4]と同じ仕組みを使っています。コードもkexecのコードを改造して使っています。ミニカーネル用kexec(mkexec)とkexecの大きな違いは、以下の通りです。

 

  • kexec では、カーネルを1ページ単位でバラバラの領域にロードします、mkexecでは連続した領域にロードします。
  • mkexecでは、ミニカーネルが使用する領域まで確保します。
  • kexecでは、起動するカーネルは、本来の位置にコピーされてから起動されますが、mkexecではコピーはしません。

ミニカーネル用に確保する領域は、複数の領域とすることも可能です。
しかし、ダンプ処理だけを行うのであれば、4MB あれば十分であり、一つの領域として確保した方が単純です。
mkexecは、カーネルモジュールにもできるようになってします。ただし、わずかでありますが、カーネルにパッチが必要です。もし、kexecが標準カーネルに取り込まれれば、mkexecで必要なカーネルパッチは、クラッシュ時のダンプ処理呼び出しフックだけとなります。mkexecは、kexecと共存可能です。
mkexecでは、ミニカーネルをロードした後に確保した領域をリードオンリーに設定します。カーネルクラッシュの影響でミニカーネルが破壊される可能性を少しでも低くすることができます。

ミニカーネル

ミニカーネルは、ダンプ取得専用のカーネルであり、通常のカーネルとは異なります。しかし、通常のカーネルのコードを使用して作成します。
ミニカーネルを作るためには、まず、通常のカーネルの以下の二つのパッチを適用します。

 

  • アドレス変換パッチ:ミニカーネルは通常のカーネルとロードされるアドレスが異なります。そのための修正パッチです。
  • ダンプ採取パッチ:ダンプ採取処理本体および、ダンプ採取処理以外の処理を行わないための修正パッチです。

初期化終了後、ダンプ採取処理を実行し、リブートするだけです。
ルートファイルシステムのマウントも行いません。次にコンフィグレーションを実施し、カーネルのmakeを行います。このとき、必要最小限のドライバのみを静的に組み込むようにします。
ダンプを確実に採取する観点から、余分な処理は極力実行しないようにすべきです。特にファイルシステムに関しては、システムクラッシュ時に破壊されている可能性があるため、マウントすべきではありません。
ミニカーネルで行える作業の柔軟性を高めるために、initrd と同じ仕組みで、ルートファイルシステムをあらかじめ用意し、ミニカーネルと同時にメモリに格納しておき、RAMディスクとしてマウントする機能のサポートを予定しています。ただし、ダンプを確実に採取することが重要であり、何でもしてよいというわけではないことに注意してください。
ミニカーネルには、デバイスドライバの修正が全く入っていません。
そのため、標準のカーネルでサポートしているデバイスはすべて使用できます。
これは、HBAドライバに修正が必要であるdiskdumpと比較して、大きな長所です。
ダンプデバイスとしては、ディスク装置のみに対応しています。
ネットワークに対応することももちろん可能ですが、ダンプ採取の確実性の観点からは、ネットワークダンプは望ましくないと考えます。

ダンプフォーマット

システムクラッシュ時には、ミニカーネルの起動前にコンテキストの保存を行います。ミニカーネルでは、基本的にメモリをそのままダンプデバイスに出力します。システムクラッシュ時には、カーネルの制御表は信頼できないため、メモリはそのまま出力すべきです。
ダンプの解析には、lcrash または、crashコマンドを使用します。
出力したダンプを LKCD形式または、netdump形式に変換するツールを用意してあります。変換は容易です。なお、解析ツールの改善にも取り組んでいます。

総括

クラッシュダンプを確実に採取するための手法として、ミニカーネルダンプを紹介しました。ミニカーネルダンプは、クラッシュしたカーネルの資源からまったく切り離されて処理を行うことにより、ダンプ採取の確実性を高めています。既存のカーネルに対する修正が非常に少ないため、組み込みのためのハードルは低くなります。ミニカーネルでは、デバイスドライバの修正がいらないのも長所の一つです。

参考

LKCD
 http://lkcd.sourceforge.net/


netdump

 http://www.redhat.com/support/wpapers/redhat/netdump/


diskdump

 http://sourceforge.net/projects/lkdump/

附録:ミニカーネル・テクニカルノート

ミニカーネルが実行するために使用する物理アドレスは、標準カーネルのものとは異なっている。ここでは、我々の実装の内部構造について解説します。

基本アイデア

基本的にストレート・マッピング領域のアドレスマッピングを変えることにより実装されています。

 

実装

実装方法として、__pa()/__va() マクロの変更を考えられた人もいるかもしれませんが、それは賢い方法ではありません。カーネルはあくまでもストレート・マッピング領域は、物理アドレス 0 からをマッピングしていると信じている(だまされている)方がコードの修正量は少なくなります。
(言い換えると、mem_map[0]は、pfn 0 に対応するという関係を維持すること。)
コードの修正は、以下の3種類に分類できます。
コードの修正量の少なさにきっと驚かれることでしょう。

 

  • ブート時の初期ページテーブルの設定
  • ページテーブルエントリの設定と参照
  • DMA

ブート時の初期ページテーブルの設定

カーネルは、ブート時の早い段階(正式なページテーブルが設定されるまで)では、arch/[i386|x86_64]/kernel/head.S の中で静的に設定された一時的なページテーブルを使用して動作します。
この一時的なページテーブルは、本当の物理アドレスを指し示すようにする必要があります。現実装では、ミニカーネルのテキストおよびデータ域は最初の4MBに収まることを前提としており、一時的ページテーブルでは、4MBのみマッピングしています。一時的ページテーブルの設定を以下に示します。

i386の場合

(*1)偽のidentマッピング

(*2)本当のident マッピング

(*3)カーネルアドレススペース(PAGE_OFFSET~)のマッピング

 

x86_64の場合

(*1)本当の物理アドレスを指し示すよう修正

(*2)本当のident マップ
(現実装では、ミニカーネルのテキスト領域が1GB以下の物理アドレスに置かれていることを前提)

(*3)コンソール出力のために追加

ページテーブルエントリの設定と参照

ページテーブルはハードウェアに参照されるものなので、エントリには本当の物理アドレスを設定する必要があります。
そのため、以下に示すページテーブルエントリの設定と参照に関する関数(マクロ)を修正しました。

– set_pte, set_pmd, set_pgd, set_pml4(x86_64のみ)

– pte_val, pmd_val, pgd_val, pml4_val(x86_64のみ)

また、以下の2つの補助関数を追加しました。

– true_phys: 偽の物理アドレスを本当の物理アドレスに変換する

– pseudo_phys: 本当の物理アドレスを偽の物理アドレスに変換する

(注: 偽の物理アドレスとは、カーネルが物理アドレスだと信じている(だまされている)もの)
例えば、set_pte の実装は以下のようになります。(x86_64の場合)

 

void set_pte(pte_t *dst, pte_t val)
{
        unsigned long flags = val.pte & _PGTABLE_FLAGS;
        unsigned long paddr = val.pte & ~_PGTABLE_FLAGS;

        dst->pte = val.pte & _PAGE_PRESENT ? true_phys(paddr) | flags : val.pte;
} /* _PAGE_NX exists in bit-63 :-( */ 

pte_val の実装は以下のとおりです。(x86_64の場合)

unsigned long pte_val(pte_t pte)
{
        unsigned long flags = pte.pte & _PGTABLE_FLAGS;
        unsigned long paddr = pte.pte & ~_PGTABLE_FLAGS;

        return (pte.pte & _PAGE_PRESENT) ? pseudo_phys(paddr) | flags : pte.pte;
}

いくつかのアーキテクチャ(x86_64もそうですが)pte_val (*_val) を左辺値(l-value)として使っています。左辺値として使っているところは修正が必要です。
カーネルアドレス空間に対する正式なページテーブルを構築する部分に関しては、本質的には変更が必要ありません。上記の関数(マクロ)の修正により、自然にうまくいきます。
(init_memory_mapping(): x86_64,kernel_physical_mapping_init(): i386)
ただし、実際には、少々修正が入っています。PSEを使うと0xa0000 ~ 0xfffff の部分だけマッピングを変えるということができないからです。そのため、PSEを使用しないように修正してあります。
(注: VRAM がどの部分か正確にはわかりません。しかし、 0xa0000 to 0xfffff がRAMでないのは確かです。)
cr3 レジスタへの設定も変更する必要があります。i386では、locd_cr3を以下のように修正しました。

#define load_cr3(pgdir) 
asm volatile("movl %0,%%cr3": :"r" (true_phys(__pa(pgdir))))

x86_64では、pda_init() を修正しました。

DMA

ドライバがデバイスに対してDMA転送を指示する場合は、本当の物理アドレスを渡す必要があります。Linuxでは、DMAに関してきちんとインタフェースが既定されている(*)ため、修正は簡単です。
(*) Documentation/DMA-API.txt
以下の4つの関数を修正します。

– pci_alloc_consistent (dma_alloc_coherent for i386)

– pci_map_sg (dma_map_sg for i386)

– pci_map_single (dma_map_single for i386)

– pci_map_page (dma_map_page for i386)

修正は下記の通りです。

-               *dma_handle = virt_to_phys(ret);
+               *dma_handle = true_phys(virt_to_phys(ret));

(注: x86_64: 手元の環境では、pci-gart.c の中の関数が使われていたため、現実装では、pci-gart.c のみ修正してあります。)
ドライバには修正は不要です(上記の関数はinlineのため、リコンパイルは必要です)。

制限事項

  • ミニカーネル用の領域は、4GB未満の物理アドレス領域になければなりません(i386 もx86_64も)。i386 では、alloc_pages に ZONE_NORMAL を指定すれば、4GB未満であることが保証されます。しかし、x86_64 では、 alloc_pages で 4GB未満のページを取得するインタフェースがありません。そのため、x86_64の現実装では、ミニカーネル用の領域をプリアロケーションしています。
  • ユーザプロセスの実行はサポートしていません。ユーザプロセスを実行させるためには、まだ修正が必要です。