Linuxカーネル4.1のvmalloc()(ドラフト)

はじめに

前回のLinuxカーネル4.1のSLUBアローケータ(ドラフト) - φ(・・*)ゞ ウーン カーネルとか弄ったりのメモと同じくドラフト版公開です。こちらはLinuxカーネル4.1のメモリレイアウト(ドラフト) - φ(・・*)ゞ ウーン カーネルとか弄ったりのメモで説明しなかったvmalloc()の説明です。

カーネルのバージョンは4.1系です。

文書自体も完成版ではないし、markdownから手作業ではてなblogにコピペして修正してるので章立てとか変になってるところとかあるかもしれませんが気にしないでください。 一部は文書修正してます。

スラブアロケータ以外の動的メモリ確保

vmalloc関数

Linuxカーネルの動的メモリ確保手段にはページ単位で確保するバディシステムや、任意のサイズでメモリを確保するスラブアローケータがありますが、これらの他にもvmalloc関数があります。 vmalloc関数のインターフェースはmalloc(3)と同様で確保するサイズを指定します。vmalloc関数で確保したメモリ領域の解放にはvfree関数を使用します。

void *vmalloc(unsigned long size);
void vfree(const void *addr);

非連続領域で使用する仮想アドレスの範囲はアーキテクチャによって違い、x86_64ではarch/x86/include/asm/pgtable_64_types.hにて以下のように32TiBの範囲が割当られています。

#define VMALLOC_START    _AC(0xffffc90000000000, UL)
#define VMALLOC_END      _AC(0xffffe8ffffffffff, UL)

vmalloc関数はkmalloc関数との違い、物理的に連続していないページフレームよりメモリを確保します。kmalloc関数で2ページ分のメモリを確保する場合、2つのページフレームは物理的に連続しますが、vmalloc関数の場合は物理的に連続していません。どちらの場合もリニアアドレスとしては連続しています。このようにvmalloc関数でメモリを確保する場合、ページフレームは物理的に連続していないため、DMAなどで物理的に連続したアドレスを要求するような場合には使用できません。 また、kmalloc関数は要求したサイズに合うサイズのスラブキャッシュからメモリを確保していましたが、vmalloc関数は要求されたサイズを確保できる分のページフレームを確保します。そのため、16バイトのメモリを要求した場合でも1ページをメモリ確保用に使用します。

vmalloc関数で使用するデータ構造

vmalloc関数やioremap関数など、非連続メモリ領域からメモリを確保する場合に使用するデータ構造は主に2つあります。 vm_struct構造体はinclude/linux/vmalloc.hにて定義されており、割当を行ったページフレームや確保したメモリのサイズなど非連続メモリのデータにおける、実際のメモリ管理に関連するデータを扱います。ioremap関数の場合はphys_addrに物理アドレスを設定しますが、それ以外はリニアアドレスを使用します。

変数名 内容
addr 確保した領域の先頭のリニアアドレス
size 確保したメモリのサイズ
flags メモリを確保する時のフラグ。表_非連続メモリ領域の確保時に使用するフラグ参照
pages リニアアドレスに割り当てたページフレームの配列
nr_pages pagesで使用しているpage構造体数
phys_addr 物理メモリのアドレス(ioremap関数の場合に使用)
caller 非連続メモリ領域を確保した関数のアドレス

表_vm_struct構造体

非連続メモリ領域の確保時に使用するフラグは表_非連続メモリ領域の確保時に使用するフラグに示したフラグがあります。これらのフラグの定義はinclude/linux/vmalloc.hにあります。

変数名 内容
VM_IOREMAP ioremap関数でメモリを確保する場合に設定
VM_ALLOC vmalloc関数でメモリを確保する場合に設定
VM_MAP vmap関数でメモリを確保する場合に設定
VM_VPAGES page構造体の配列作成時にvmalloc関数でメモリを確保した場合に設定
VM_UNINITIALIZED メモリの確保途中を表すために設定
VM_NO_GUARD ガード領域を使用しない場合に設定

表_非連続メモリ領域の確保時に使用するフラグ

vmap_area構造体は非連続メモリ領域に関するデータを扱います。この構造体のvm変数から、ページフレームなどを管理しているvm_struct構造体にアクセスできます。vmap_area構造体もvm_struct構造体と同じくinclude/linux/vmalloc.hにあります。

変数名 内容
va_start 開始アドレス
va_and 終了アドレス
flags この構造体の状態を表すフラグ。表_vmap_areaの状態フラグ参照
rb_node vmap_area構造体をアドレスでソートした赤黒木
list vmap_area構造体を管理する連結リスト
purge_list vmap_area構造体のリスト。vmap areaを解放する時に使用する
vm このvmap areaと関連しているvm_strcuct構造体
rcu_head vmap_area構造体のメモリを解放するときに使用するRCU構造体

表_vmap_area構造体

非連続メモリ領域の確保や解放などではvmap_area構造体のflags変数に現在の処理状況を設定します。この内容は mm/vmalloc.cにて定義されています。

変数名 内容
VM_LAZY_FREE 使用していたvmap_area構造体を解放する時に設定
VM_LAZY_FREEING vmap_areaa構造体解放処理が進んだ時に、VM_LAZY_FREEからこの状態に変更
VM_VM_AREA vmalloc用にvmap_areaを設定した場合に設定

表_vmap_areaの状態フラグ

vmap_area構造体とvm_struct構造体は、vmap_area構造体のrb_node、list変数にて赤黒木と連結リストで結びついています。vmap_area構造体とvm_struct構造体は図_vmap_area構造体とvmstruct構造体の関連のようになります。

f:id:masami256:20190510205850p:plain

図_vmap_area構造体とvm_struct構造体の関連

vmallocの初期化

vmalloc関数を使用する前に、カーネルのブート処理の一環としてvmalloc_init関数で初期化を行います。vmalloc_init関数での初期化の前にvm_area_register_early関数で非連続領域の初期設定が終わっている必要があります。vm_area_register_early関数はブート中に非連続メモリ領域をリストに登録しておき、vmalloc_init関数でvmalloc関数で使用できるようにします。 vmalloc_init関数は最初に全cpuを対象にデータを初期化します。各cpuは連結リストのvmap_block_queueと、vfree関数実行時に使用するワークキューのvfree_deferredを持ちます。vmap_block_queueはリストの初期化、vfree_deferredはリストの初期化と、遅延実行時に呼ばれる関数(free_work関数)の登録を行います。

次に、vm_area_register_early関数で非連続メモリ領域がリストに登録されていた場合は、これらの領域用にvmap_area構造体を作成し、__insert_vmap_area関数で赤黒木のvmap_area_rootと、連結リストのvmap_area_listに登録します。 vmap_area_pcpu_holeにはvmallocが使用する領域の最後のアドレスを設定します。最後にvmap_initializedをtrueに設定して、vmap領域の初期化とマークします。

vmallocでのメモリ確保

vmalloc関数がメモリ確保のインターフェースですが、実際に処理を制御するのは__vmalloc_node_range関数です。vmalloc関数の処理の流れを図_vmallocのコールフローに示します。

vmalloc()                                  (1)
  -> __vmalloc_node_flags()                (2)
    -> __vmalloc_node()                    (3)
      -> __vmalloc_node_range()            (4)
        -> __get_vm_area_node()            (5)
      -> alloc_vmap_area()             (6)
          -> setup_vmalloc_vm()            (7)
    -> vmalloc_area_node()             (8)       
      -> remove_vm_area()              (9)
      -> map_vm_area()                 (10)
    -> clear_vm_uninitialized_flag()   (11)

図_vmallocのコールフロー

vmalloc関数によるメモリの確保では、最初に要求されたsizeをPAGE_SIZEの倍数に切り上げます。そして、確保するページ数を割り出します。そして、vmalloc関数で確保した領域を管理するためのデータとしてvm_struct構造体を使用するため、この構造体用にメモリを確保します。ここではスラブアロケータよりメモリを確保します。 次に、vmap_area構造体の設定を行います。この処理はalloc_vmap_area関数にて行います。まず、vmap_area構造体用にスラブアロケータよりメモリを確保します。 vmalloc関数によるメモリ確保ではガード領域として1ページ余分にページを確保します。この領域はプログラムのミスにより、本来のサイズを超えた位置への書き込みが行われた場合への対処です。この結果、PAGE_SIZE以下のメモリを確保する場合、「図_vmallocで確保するメモリ」のように2ページ分のページフレームを確保することになります。

f:id:masami256:20190510205654p:plain

図_vmallocで確保するメモリ

そして、vmalloc関数で確保するアドレス範囲を決定する処理を行います。この時、vmapキャッシュを使用の有無を選択します。 vmapキャッシュを使用するのは以下の条件の場合です。

  • 以前にalloc_vmap_area関数が実行され、vmap探索用のキャッシュ変数のfree_vmap_cacheにデータが設定されている

以下のいずれかの条件を満たす場合にvmapキャッシュを使用しません。

  • vmapキャッシュが設定されていない
  • sizeが前回設定したcached_hole_sizeより小さい
  • vstartが前回設定したcached_vstartより小さい
  • alignが前回設定したalignより小さい

vmapキャッシュを設定しない場合は、cached_hole_sizeを0に、vmapキャッシュをNULLにします。 cached_vstartとcached_alignには、引数で受け取ったvstartとalignをそれぞれ設定します。この設定はvmapキャッシュの使用有無に関わらず行います。

そして、探索の第一弾目の処理に入ります。まず、vmapキャッシュを使用する場合、vmapキャッシュからvmap_area構造体を取得します。そして、このvmap_area構造体に設定されている終了アドレスを、引数のalignの境界に整列させます。 この計算結果のアドレスがvstartより小さい場合は、vmapキャッシュ使用なしとしてアドレスの探索をやり直します。アドレス+sizeの結果がオーバーフローする場合はエラーになります。取得した要素はfirst変数に代入します。

vmapキャッシュを使用しない場合は、vstartをalignの境界に整列させてアドレスを計算します。この場合も、アドレス+sizeの結果がオーバーフローする場合はエラーになります。次に、vmap_area構造体を管理している赤黒木のvmap_area_rootより、全vmpa_area構造体を調べていきます。ツリー構造の左右どちらに進むかは、取得したvmap_area構造体の終了アドレスにより変わります。終了アドレスがアドレスよりも小さい場合は右の要素を取得します。もし、終了アドレスがアドレスよりも大きい場合、まず、この要素をfirst変数に代入しておきます。そして、このvmap_area構造体の開始アドレスよりも大きければ探索はここで終了します。違った場合は左の要素を取得し、再度チェックを行います。これで、要素を全て探索するか、途中で探索を終了できるまで行います。ここでの赤黒木の探索は、startで指定されたアドレスよりも大きなアドレスがva_end変数に設定されているvmap_area構造体を探すことです。 この処理で、first変数が設定された場合は以下に続く処理を行います。見つからなければ、先のfoundラベルからの処理を行います。

キャッシュ未使用時に赤黒木の探索後にfirst変数がNULLで無い場合、もしくはキャッシュから探索した場合は以下の処理でアドレスの再設定を行います。

  1. アドレス+sizeがfirstの開始アドレスより大きく、引数で渡されたvend未満なら2へ
  2. アドレス+cached_hole_sizeがfirstの開始アドレスよりも小さい場合は、cached_hole_sizeを「firstの開始アドレス - アドレス」に設定
  3. firstの終了アドレスをalignの境界に合わせた結果をアドレスに代入
  4. アドレス+sizeがオーバーフローする場合はエラーにする
  5. firstがvmap_area構造体を管理するリスト(vmap_area_list)の最後の要素なら探索終了してループを抜ける
  6. firstのlist変数より、firstとリストでつながっている次の要素を取得し、1に戻る

上記のループを抜けた段階で"アドレス+size"がvendを超えている場合はエラーにします。 ここにたどり着くのは、上記のループを行った場合と、キャッシュ未使用時に、赤黒木の探索結果でfirst変数がNULLだった場合です。この場所にはfoundというgoto文のラベルが設定されていいます。 ここまで着た場合、アドレスを利用することができるので、呼び出し元に返却するvmap_area構造体のデータを設定します。開始アドレスはここまでで計算したアドレスを設定します。終了アドレスはアドレス+sizeになります。そして、このvmap_area構造体を__insert_vmap_area関数を使用して赤黒木のvmap_area_rootと、通常のリストのvmap_area_listへ登録します。最後に、設定を行ったvmap_area構造体のrb_nodeをvmapキャッシュのfree_vmap_cacheに設定します。ここまででvmap_area構造体の設定が完了になります。

リニアアドレスの確保はvmapキャッシュの有無などでコードは複雑になっていますが、シンプルなケースではファーストヒットにより割り当てるリニアアドレスを決定しています。

ここで、リニアアドレスを初めて割り当てるのにvmalloc関数を使用する場合を例に見てみます。

最初のリニアアドレス割当時にはvmapキャッシュは存在しませんので、赤黒木のノードを調べることになりますが、初回の場合は赤黒木にデータはないのでfoundラベルに移動することになります。 この時はaddrはリニアアドレス探索開始アドレスのVMALLOC_STARTが設定されています。結果として、vmap_area構造体のva_startとva_endは以下のようになります。

va->va_start = VMALLOC_START
va->va_end = va->va_start + size

そして、vmapキャッシュにはこのvmap_area構造体が設定されます。

2回目のvmalloc関数呼び出し時にはvmapキャッシュが設定されています。よって、addrは前回割り当てたリニアアドレスの末端(va->va_end)が設定されます。そして、先に説明した、キャッシュから探索した場合の6ステップの処理を実行します。この場合、5ステップ目のfirst変数がリストの最後の要素(実際に1つしか要素が登録されていませんので、最初の要素でもあり最後の要素でもあります)となってループを抜けます。 この場合もfoundラベルの処理になりますので、以下のようにvmap_area構造体の要素を設定します。addrは前回割り当てたリニアアドレスの終端のアドレスです。

va->va_start = addr
va->va_end = addr + size

この時のリニアアドレスの割当状態が図_リニアアドレスの割当です。

f:id:masami256:20190510205719p:plain

図_リニアアドレスの割当

alloc_vmap_area関数の処理が終わり、__get_vm_area_node関数に戻ったら最後にsetup_vmalloc_vm関数を実行します。 ここでは、vmap_area構造体とvm_struct構造体の設定を行います。vm_struct構造体にはアドレス、サイズなどを設定し、vmap_area構造体のvmメンバ変数にvm_struct構造体を設定します。この時にvm_struct構造体のflags変数にVM_UNINITIALIZEDを設定し、この領域はまだ初期化中とマークします。また、この時点でflags変数に対してVM_VM_AREAフラグも設定します。

次にページの割当と設定を行っていきます。まず、メモリを確保するsizeから必要なページフレーム数を計算します。この時点ではsizeは、呼び出し元が設定したサイズではなく、PAGE_SIZEの倍数になっています。 vmalloc関数ではページフレームは物理的に連続していないため、page構造体の配列にて管理する必要があります。 そして、page構造体を管理するためのメモリを確保しますが、この時に、確保するpage構造体の配列のサイズが1ページに収まらない場合、 __vmalloc_node関数を用いてメモリを確保します。1ページに収まる場合はスラブアローケータを利用します。__vmalloc_node関数は__vmalloc_node_range関数を呼び出します。 page構造体管理用の配列のメモリを確保できたら、バディシステムを利用してページフレームを1ページフレームずつ確保していきます。 ページフレームの確保を行ったら、map_vm_area関数にて、ページフレームをページテーブルに設定します。最初にアドレスからページグローバルディレクトリ(PGD)のインデックスを取得し、そこからページアッパーディレクトリ(PUD)、ページミドルディレクトリ(PMD)、ページテーブルエントリ(PTE)と設定していきます。 最後にvm_struct構造体のflagsに設定したVM_UNINITIALIZEDフラグを落としてvmalloc関数の処理は完了です。

vfree関数でのメモリ解放

vfree関数でメモリの解放を行う場合、割り込みコンテキスト中に呼ばれたかを確認します。もし、割り込みコンテキストだった場合は、解放処理を遅延させます。この場合、cpuに設定されているvfree_deferred構造体を取得し、この構造体に解放対象のアドレスを設定し、schedule_work関数でキューに登録して終了します。その後free_work()が呼ばれた時に__vunmap()を呼び出します。 割り込みコンテキストでない場合は__vunmap関数を使用してメモリの解放を行います。メモリの解放は__vunmapがメインの処理です。vfree関数のコールフローを図_vfreeのコールフローに示します。

vfree()                           (1)
  -> free_work()                  (2)
    -> __vunmap()                 (3)
  -> __vunmap()                   (4)
    -> remove_vm_area()           (5)
      -> find_vmap_area()         (6)
      -> free_unmap_vmap_area()   (7)

図_vfreeのコールフロー

メモリを解放する場合、まずは解放するアドレスで使用しているvmap_area構造体を取得します。そして、vmap_area構造体からvm_struct構造体を取得し、vmap_area構造体からvm_struct構造体への参照を外します。また、vmap_area構造体のflagsからVM_AREAフラグを外して、未使用の状態として設定します。 そして、解放対象のアドレスで使用しているページテーブルを全てクリアします。クリアする順番はPGD、PUD、PMD、PTEの順番です。 次に、使用していた非連続メモリ領域の解放処理を行います。この処理は__purge_vmap_area_lazy関数にて行います。解放処理に入る前にvma_area構造体のflags変数にVM_LAZY_FREEフラグをセットして、これから解放するとマークします。また、vmap_area構造体に設定されているva_endからva_startの範囲から、ページ数を割り出し、vmap_lazy_nr変数に設定します。この変数は後ほど使用します。 __purge_vmap_area_lazy関数ではページを割り当てたvmap_area構造体を管理しているvmap_area_listを1要素ずつ辿りながら解放対象のvmap_area構造体を設定していきます。解放するのはvmap_area構造体のflags変数にVM_LAZY_FREEが設定されているものです。 vmap_area構造体に対する操作は以下の3処理です。

  1. 解放対象のvmap_area構造体を登録するリストへ構造体の登録
  2. flags変数にVM_LAZY_FREEINGフラグを設定
  3. flags変数からVM_LAZY_FREEフラグを外す

この時に解放するアドレスの範囲から解放するページフレーム数の計算と、解放するアドレスの範囲の再設定を行います。__purge_vmap_area_lazy関数はtry_purge_vmap_area_lazy関数から呼ばれますが、この時、解放するアドレスの範囲として、開始アドレスをULONG_MAX、終了アドレスを0で設定して関数を呼び出します。そして、ループ内で実際の開始・終了アドレスの設定を行います。 vmap_area構造体をvaとして、以下のように設定します。

  • va->va_start < startなら開始アドレスをva->startのアドレスに変更
  • va->va_end > endなら終了アドレスをva->endのアドレスに変更

ページ数はva->va_end - va->va_startの結果から計算します。

vmap_area_list内の要素に対して設定変更完了後にページ数が設定されている場合、以下の処理を行います。

  • vmap_lazy_nrからループ内で数えたページ数を減らす
  • start - endの範囲のアドレスを無効にするため、TLBをフラッシュ
  • vmap_area構造体の解放

vmap_area構造体の解放処理は__free_vmap_area関数にて行います。この処理ではメモリの確保でvmap_area構造体を設定する時にvmapキャッシュを設定していた場合に、そのキャッシュを無効にします。キャッシュの無効化は2パータンあり、解放するvmap_area構造体の終了アドレスが、cached_vstartに設定されているアドレスより小さい場合はキャッシュをNULLに設定します。 そうでない場合、vmapキャッシュの赤黒木よりvmap_area構造体を取得します。そして、解放対象のvmap_area構造体とキャッシュのvmap_area構造体の開始アドレスを比較し、解放対象の開始アドレスがキャッシュの開始アドレスより小さければ、vmapキャッシュにはキャッシュが登録されている赤黒木のprev要素をvmapキャッシュに設定します。そうでない場合は最初に赤黒木より取得したvmap_area構造体をvmapキャッシュに設定します。 そして、解放対象のvmap_area構造体を赤黒木のvmap_area_rootや、RCUのリストから外します。 解放対象のvmap_area構造体の開始・終了アドレスがvmalloc関数が使用するアドレス範囲(VMALLOC_START - VMALLOC_END)にある時に、既存のvmap_area_per_cpu_holeに設定されているアドレスが、解放対象のvmap_area構造体の終了アドレスより小さい場合はvmap_arep_per_cpu_holeの値を、このvmap_area構造体の終了アドレスに更新します。 最後にvmap_area構造体をメモリを解放します。

その他の非連続メモリ領域からメモリを確保する関数

非連続領域からメモリを確保する関数は、vmalloc関数の他にvmap関数とioremap関数があります。

vmap関数

vmap関数はvmallocと同じくinclude/linux/vmalloc.hにて宣言されています。

extern void *vmap(struct page **pages, unsigned int count,
                        unsigned long flags, pgprot_t prot);
            

vmap関数は引数で渡されたpage構造体の配列pagesをリニアアドレスにマップします。この関数に渡すpageは非連続メモリ領域からアローケートしたページです。よって、リニアアドレスが連続していない複数個のpage構造体を連続したリニアアドレスにマップすることになります。 vmap関数の場合、vmalloc関数と同様に__get_vm_area_node関数でvmap_areaの設定を行います。この処理はvmalloc関数の場合と変わりません。この後、vmap関数の場合はページフレームはすでに確保されているので、ページフレームの確保は行わず、map_vm_area関数を使用してリニアアドレスとページフレームのマッピングを行います。

ioremap関数

ioremap関数は主にデバイスドライバが使用するための関数です。x86x86_64環境では物理アドレス0xa000-0xffff(640KiB~1MiB)の範囲はISAデバイスが使用するために予約されていますが、それ以外のアドレス範囲については規定されていません。このような場合にデバイスの物理メモリ領域をカーネルのリニアアドレスにマッピングするためにioremap関数を使用します。

ioremap関数はアーキテクチャ毎に実装が違います。x86_64ではarch/x86/include/asm/io.hにて宣言されています。

static inline void __iomem *ioremap(resource_size_t offset, unsigned long size);

ioremap関数の処理のうち、メモリのマッピングを行う主要な関数は__ioremap_caller()です。この関数よりget_vma_area_caller関数が呼ばれ、さらに__get_vm_area_node関数を実行することで要求したサイズのリニアアドレス範囲を確保することができます。

/proc/vmallocinfoによるメモリ確保状況の確認

vmalloc領域の使用状況は/proc/vmallocinfoファイルで確認することができます。vmalloc関数やvmap関数などではメモリの確保時に__vmap_area関数にて連結リストのvmap_area_listにvmap_area構造体を登録していますので、cat時に表示する内容はこのリストを辿って、vmap_area構造体のデータを整形して表示します。このファイルをcatすると図_vmallocinfoのように出力されます。

表示内容は以下の通りです。

  1. 使用しているアドレスの範囲
  2. サイズ
  3. 呼び出し元の関数(機械語での呼び出し位置/機械語での関数のサイズ)
  4. 使用しているページフレーム数
  5. 物理アドレス
  6. メモリを確保した関数(vmalloc/vmap/ioremap/user(vmalloc_user関数)/vpages(__vmalloc_area_node関数)
  7. 使用しているNUMAノード(Nの後ろの数値がノードの番号、=の後ろの数字が使用しているページ数)
# cat /proc/vmallocinfo
0xffffc90000000000-0xffffc90000004000   16384 acpi_os_map_iomem+0xef/0x14e phys=bffdf000 ioremap
0xffffc90000004000-0xffffc90000805000 8392704 alloc_large_system_hash+0x160/0x236 pages=2048 vmalloc vpages N0=2048
0xffffc90000805000-0xffffc9000080a000   20480 alloc_large_system_hash+0x160/0x236 pages=4 vmalloc N0=4
0xffffc9000080a000-0xffffc90000c0b000 4198400 alloc_large_system_hash+0x160/0x236 pages=1024 vmalloc vpages N0=1024
0xffffc90000c0b000-0xffffc90000c0e000   12288 alloc_large_system_hash+0x160/0x236 pages=2 vmalloc N0=2
0xffffc90000c0e000-0xffffc90000c2f000  135168 alloc_large_system_hash+0x160/0x236 pages=32 vmalloc N0=32
0xffffc90000c2f000-0xffffc90000c50000  135168 alloc_large_system_hash+0x160/0x236 pages=32 vmalloc N0=32
0xffffc90000c50000-0xffffc90000c52000    8192 bpf_prog_alloc+0x39/0xb0 pages=1 vmalloc N0=1
0xffffc90000c52000-0xffffc90000c54000    8192 acpi_os_map_iomem+0xef/0x14e phys=fed00000 ioremap
0xffffc90000c54000-0xffffc90000cd5000  528384 alloc_large_system_hash+0x160/0x236 pages=128 vmalloc N0=128
0xffffc90000cd5000-0xffffc90000dd6000 1052672 alloc_large_system_hash+0x160/0x236 pages=256 vmalloc N0=256
0xffffc90000dd6000-0xffffc90000df7000  135168 alloc_large_system_hash+0x160/0x236 pages=32 vmalloc N0=32
0xffffc90000df7000-0xffffc90000e18000  135168 alloc_large_system_hash+0x160/0x236 pages=32 vmalloc N0=32
0xffffc90000e18000-0xffffc90000e29000   69632 alloc_large_system_hash+0x160/0x236 pages=16 vmalloc N0=16

図_vmalloinfo

ゼロからよくわかる!  ラズベリー・パイで電子工作入門ガイド

ゼロからよくわかる! ラズベリー・パイで電子工作入門ガイド

Linuxカーネル4.1のメモリレイアウト(ドラフト)

はじめに

前回のLinuxカーネル4.1のSLUBアローケータ(ドラフト) - φ(・・*)ゞ ウーン カーネルとか弄ったりのメモと同じくドラフト版公開です。

カーネルのバージョンは4.1系です。

文書自体も完成版ではないし、markdownから手作業ではてなblogにコピペして修正してるので章立てとか変になってるところとかあるかもしれませんが気にしないでください。 一部は文書修正してます。

ユーザプロセス空間とカーネル空間

Linux x86_64では48bit(256TiB)のアドレス空間を使用できます。この256TiBの範囲のうち、128TiBをユーザープロセスが使用し、残りの128TiBをカーネルが使用します。32bitのカーネルではユーザプロセスに3GiB、カーネルに1GiBの割当でしたので、64bit環境では使用可能なメモリを使用量が大幅に増えました。 Linuxではこの256TiBのアドレス空間をプロセスとカーネルが使用しますが、0〜0xffff800000000000までのユーザプロセス空間はプロセスごとに割り当てられ、後半のカーネル空間に関しては常に1つとなり、「図_ユーザプロセス空間とカーネル空間」のようになります。

f:id:masami256:20190510203636p:plain

図_ユーザプロセス空間とカーネル空間

この256TiBのメモリはアドレスの範囲によって使用目的が決まっています。その内訳を「表_メモリマップ」に示します。

x86_64のメモリレイアウト
開始アドレス 終了アドレス サイズ 内容
0x0000000000000000 0x00007fffffffffff 128TiB ユーザプロセス空間
0xffff800000000000 0xffff87ffffffffff 8TiB ハイパーバイザー向けに予約
0xffff880000000000 0xffffc7ffffffffff 64TiB ダイレクトマップ領域
0xffffc80000000000 0xffffc8ffffffffff 1TiB 未使用
0xffffc90000000000 0xffffe8ffffffffff 32TiB vmalloc/ioremapが使用
0xffffe90000000000 0xffffe9ffffffffff 1TiB 未使用
0xffffea0000000000 0xffffeaffffffffff 1TiB 仮想メモリマップ
0xffffeb0000000000 0xffffebffffffffff 1023GiB 未使用
0xffffec0000000000 0xfffffc0000000000 16TiB kasan shadow memory
0xfffffc0000000000 0xfffffeffffffffff 3072GiB 未使用
0xffffff0000000000 0xffffff7fffffffff 512GiB スタック領域
0xffffff8000000000 0xffffffff7fffffff 509GiB 未使用
0xffffffff80000000 0xffffffffa0000000 512MiB カーネルテキストマッピング領域。物理メモリ0~512MiB
0xffffffffa0000000 0xffffffffff5fffff 1525MiB カーネルモジュールマッピング領域
0xffffffffff600000 0xffffffffffdfffff 8MiB vsyscallsで使用
0xffffffffffe00000 0xffffffffffffffff 2MiB 未使用

表_メモリマップ

この内訳はDocumentation/x86/x86_64/mm.txtにて確認することができます。 ユーザープロセス空間は128TiBをフルに使用しますが、カーネル空間の場合、ある特定の範囲を使用目的毎に分けています。128TiB全てが使用されているわけではなく、現在は未使用の領域として空いているところもあります。

また、EFIを使用するシステムでは図_メモリマップのように0xffffffef00000000から0xffffffff00000000の範囲をEFIのランタイムサービスにマッピングします。

f:id:masami256:20190510203723p:plain

図_メモリマップ

ダイレクトマップ(ストレートマップ)領域

カーネルは全ての物理メモリをこの領域にマッピングします。x86_32アーキテクチャではアーキテクチャの制限として、物理アドレスを直接マッピングできるのは896MiBまでで、それ以上のアドレスを使用するためにHIGHMEM領域を使用していましたが、x86_64アーキテクチャではHIGHMEM領域が不要になり、ダイレクトマップ領域だけを使用します。

ダイレクトマップ領域の開始アドレスはPAGE_OFFSETとして定義されていています。

arch/x86/include/asm/page_types.h
#define PAGE_OFFSET             ((unsigned long)__PAGE_OFFSET)

arch/x86/include/asm/page_64_types.h
#define __PAGE_OFFSET           _AC(0xffff880000000000, UL)

図_PAGE_OFFSETの定義

物理アドレスからリニアアドレスへの変換は__vaマクロを使用します。この場合は物理アドレスに対してPAGE_OFFSETを足すことでリニアアドレスを得ることができます。

#define __va(x)                 ((void *)((unsigned long)(x)+PAGE_OFFSET))

リニアアドレスから物理アドレスへの変換は__pa関数マクロです。このマクロの実体は__phys_addr_nodebug関数です。x86_32ではアドレスからPAGE_OFFSETを引くだけでアドレスを計算できたのですが、x86_64ではカーネルテキストマッピング領域の開始アドレス 0xffffffff80000000(__START_KERNEL_map)を基準としてアドレスの計算を行います。この関数で使用するphys_baseは通常0となっています。アドレスの変換は、「図_リニアアドレスから物理アドレスへの変換」のように行います

f:id:masami256:20190510203830p:plain

図_リニアアドレスから物理アドレスへの変換

vmalloc/ioremap領域

vmalloc関数やioremap関数などでメモリを確保する場合、アドレスをこの領域から割り当てます。vmalloc関数については別記事

Linuxカーネル4.1のvmalloc()(ドラフト) - φ(・・*)ゞ ウーン カーネルとか弄ったりのメモ

にて説明します。この領域で使用するページフレームは物理的に連続していませんが、論理的に連続します。

仮想メモリマップ

この領域は主にページフレーム番号とpage構造体の相互変換に使用します。ページフレーム番号からページ構造体を取得するには__pfn_to_pageマクロ、page構造体からページフレーム番号の取得には__page_to_pfnマクロを使用します。

/* memmap is virtually contiguous.  */
#define __pfn_to_page(pfn)      (vmemmap + (pfn))
#define __page_to_pfn(page)     (unsigned long)((page) - vmemmap)

図_ページフレーム番号とpage構造体の変換インターフェース

これらの変換操作にはグローバル変数のvmemmapに設定されている仮想メモリマップ領域の開始アドレスを基準として、このアドレスに対してページフレーム番号を足すことで、該当のpage構造体を取得、また、page構造体のアドレスからvmemmapを減算することで、ページフレーム番号の取得を行います。

%esp fixup stack

このスタックはPAGE_SIZEが4096の場合、各CPUに対して64個のスタックを読み込み専用として作成します。このスタックはministacksと呼ばれます。 ministacksはStack-Segment Fault(SS)からiret命令により、カーネルからユーザランドへ制御が戻る時に、iretで使用していたスタックフレームをLDTにコピーしてからユーザランドへ戻ります。 この後、General Faultなどが発生した場合はInterrupt Stack Tableから専用のスタックを使用します。 このような読み込み用のスタックを使用するのはセキュリティ上の理由です(注1)。iret命令は16bitのセグメントを元に戻しますが、%espの16〜31bitはカーネルのスタックを指しているため、カーネルの情報が漏れてしまいます。この状況に対応するため、専用のスタックを用意してiret命令から戻るようになっています。

カーネルテキスト領域

物理メモリのアドレス0〜512MiBをこの範囲にマッピングします。この領域の開始アドレスは__START_KERNEL_mapとして定義されています。この領域にはカーネルマッピングするため、このアドレス範囲に対して操作を行うことはありませんが、先に見たように__paマクロによる物理アドレスの取得など、アドレスを扱う場合に__START_KERNEL_mapを利用することがあります。

モジュールマッピング領域

カーネルモジュールをロードする領域です。この領域はカーネルテキストマッピング領域の直後に位置します。

#define MODULES_VADDR    (__START_KERNEL_map + KERNEL_IMAGE_SIZE)
#define MODULES_END      _AC(0xffffffffff000000, UL)
#define MODULES_LEN   (MODULES_END - MODULES_VADDR)

図_モジュールマッピング領域の定義

カーネルモジュールをロードする時にmodule_alloc関数が__vmalloc_node_range関数を用いて、モジュールのために確保するメモリ領域をMODULES_VADDRからのMODULES_ENDまでの範囲を指定します。

vsyscall

vsyscallのために割り当てられた領域で、開始アドレスはVSYSCALL_ADDRとして以下のように定義されています。値は0xffffffffff600000となります。

10 #define VSYSCALL_ADDR (-10UL << 20)

vsyscall領域は/proc/pid/mapsファイルにて、1ページ分がマッピングされていることを確認できます。

$ cat /proc/self/maps
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0                  [vsyscall]
固定マップ領域

機能によって、特定のアドレスにメモリを割り当てて使用したいという要求があり、これを実現するのが固定マップ領域です。固定マップ領域は0xffffffffff7ff000からの領域でアドレスの低位に向かって使用します。 固定マップ領域で使用する機能目的ごとのアドレスの定義はarch/x86/include/asm/fixmap.hで、enumのfixed_addressesで各領域ごとにインデックスが決められています。

固定マップ領域のインデックス(fixed_addresses)からリニアアドレスへの変換は__fix_to_virtマクロ、リニアアドレスからインデックス番号への変換は__virt_to_fixマクロで行います。

20 #define __fix_to_virt(x)        (FIXADDR_TOP - ((x) << PAGE_SHIFT))
21 #define __virt_to_fix(x)        ((FIXADDR_TOP - ((x)&PAGE_MASK)) >> PAGE_SHIFT)

FIXDADDR_TOPマクロは以下のように定義されています。VSYSCALL_ADDRはvsyscall領域の開始アドレスで、x86_64では0xffffffffff600000となります。

#define FIXADDR_TOP     (round_up(VSYSCALL_ADDR + PAGE_SIZE, 1<<PMD_SHIFT) - \
                         PAGE_SIZE)

固定マップ領域の確保はset_fixmap関数にて行います。これはマクロで__set_fixmap関数を呼び出します。この関数の実装はCPUアーキテクチャに依存します。x86_64アーキテクチャでは__native_set_fixmap関数が使われます。固定マップ領域の設定は先に説明した__fix_to_virtマクロによるリニアアドレスへの変換を行い、変換結果のアドレスをPTEテーブルへの登録を行います。

例えば、固定マップ領域のインデックス「VSYSCALL_PAGE」は0x1ffで、このインデックスに対するアドレスは0xffffffffff600000となります。この値は先に見たようにvsyscall領域のアドレスです。よって、vsyscall領域は固定マップ領域の仕組みを利用して、アドレスを固定して確保していることがわかります。 固定マップ領域のインデックス範囲は0x1ff-0x600です。アドレスに変換すると0xffffffffff600000〜0xffffffffff1ff000となります。

デバッグの理論と実践 ―なぜプログラムはうまく動かないのか

デバッグの理論と実践 ―なぜプログラムはうまく動かないのか

Linuxカーネル4.1のSLUBアローケータ(ドラフト)

はじめに

本記事は過去に書いたけど諸事情により陽の目に出ていなかった文書です。完全版ではありません。satさんがスケジューラの記事を公開したのに影響され、自分も書いたものを死蔵させておくのはもったいないので公開することにしました。

qiita.com

カーネルのバージョンは4.1系です。

文書自体も完成版ではないし、markdownから手作業ではてなblogにコピペして修正してるので章立てとか変になってるところとかあるかもしれませんが気にしないでください。 一部は文書修正してます。

スラブアローケータ

Linuxにおけるメモリ確保の方法としてページ単位でメモリ確保を行うバディシステムがあります。バディアロケータはページ単位に管理を行うため、構造体など小規模なメモリを確保するために使用すると無駄が多くなってしまいます。そこで、スラブアローケータというメモリをより小さな単位で管理する仕組みを使用します。

スラブアローケータはアローケータの実装方式の一つで、Jeff Bonwick氏によりSolaris 2.4で最初に実装されました。今日ではスラブアローケータはSolarisだけでなく、LinuxFreeBSDなどのオペレーティングシステムや、memcachedのようなメモリ管理を効率的に行う必要があるミドルウェアでも採用されています。

スラブアローケータの説明に入る前に、基本的な用語を説明します。

  • スラブキャッシュ

スラブオブジェクトを管理します。スラブキャッシュはスラブオブジェクトの種類毎に作成します。メモリの確保や解放処理の要求の受付、スラブアローケータ内でのメモリ管理など、スラブアローケータの機能を提供します。スラブアローケータではメモリの確保時に実行するコンストラクタ関数を設定することもできます。Linuxではkmem_cache構造体がスラブキャッシュです。

  • スラブオブジェクト

スラブキャッシュが管理するオブジェクトです。スラブアローケータはこのスラブオブジェクトをメモリ確保要求時に返却します。

  • スラブ

スラブオブジェクトの集合です。スラブオブジェクトはバディアロケータよりページを確保し、そこにスラブオブジェクトを作成します。

  • freelist

slubアローケータにおいて、次のメモリ確保要求時に返却するするスラブオブジェクトを指すポインタです。

  • 使用中リスト

slubアローケータでは、スラブの管理には使用中リストを使用します。

スラブアローケータは管理するオブジェクトの単位でスラブキャッシュを作成します。たとえば、inode構造体用のスラブキャッシュ、task_struct構造体用のスラブキャッシュなど、通常は構造体毎にスラブキャッシュを作成します。こうして、特定の構造体用にメモリを確保する場合、その構造体に特化したアローケータよりメモリを確保を行います。このように特定のサイズに応じたアローケータを使用することで、メモリのフラグメンテーションを防ぎやすくなります。 任意のサイズのメモリを確保できるアローケータの場合、オブジェクトの確保と解放を繰り返していくうちに、メモリのフラグメンテーションが進み、空き領域の合計は十分にあるのに、メモリが断片化しているためまとまった量のメモリを確保できずに失敗する状況が発生します。

f:id:masami256:20190509234229p:plain

図_メモリのフラグメンテーション

「図_メモリのフラグメンテーション」では、最初のうちはメモリの確保のみが行われていたのでフラグメンテーションは発生していませんが、時刻が進みオブジェクトの解放が行われたため、メモリのフラグメンテーションが発生している状態です。未使用領域が2つあり、メモリ確保の要求がどちらかの未使用領域で満たせる場合は大丈夫ですが、要求サイズが各未使用領域のサイズよりも多いきい場合、仮に2つの未使用領域の合計サイズが、要求サイズよりも大きくてもメモリ確保に失敗します。 スラブアローケータは特定のサイズに特化しているため、このようなメモリのフラグメンテーションを防いでいます。

Linuxのスラブアローケータ

Linuxでは3種類のスラブアローケータの実装があり、カーネルコンフィギュレーションで使用するアロケータを変更できます。Linux 2.6.23よりslubがデフォルトのスラブアローケータとなっています。ディストリビューションによってはslabを選択しているものもあります。

slabアローケータ

slub登場以前はデフォルトのスラブアローケータでした。Solarisのスラブアローケータと同様の設計方針で実装されています。 スラブの管理にはスラブの状態に応じて複数のリストを使用します。使用するリストは、使用中オブジェクトが存在しないempty list、使用中オブジェクトと空きオブジェクトが存在するpartial list、全てオブジェクトが使用中のfull listの3つのリストがあります。 また、cache coloringと呼ぶ領域があり、ページフレーム内で空きオブジェクトの開始位置を他のスラブキャッシュとずらすために使用します。この機能は、スラブが複数存在する場合に、キャッシュラインの競合が起きないようにするための仕組みです。

slobアローケータ

シンプルな実装で、K&Rアローケータとも呼ばれています(注1)。slobアローケータはLinuxで使用可能なスラブアローケータの一つですが、slabやslubアローケータと違い、スラブをオブジェクトのサイズによって分けていません。一つのスラブで様々なサイスを取り扱います。これはプログラミング言語Cにおけるmalloc関数の実装と同様で、これがK&Rアローケータとも言われる所以です。

注1 プログラミング言語C で紹介されているUnixでのmalloc関数の実装に由来します。

slubアローケータ

slubアローケータはLinux固有のアローケータの実装です。本書ではslubアローケータについて解説します。

スラブオブジェクトの管理

slubアローケータでは空きスラブオブジェクトの管理はメタデータを別途持たせずに、スラブオブジェクトそのものに次の空きオブジェクトのアドレスを書き込みます。これにより、空きオブジェクトを管理するために別途メタデータを使用しなくてすむように設計されています。アドレスはスラブオブジェクトの先頭に書き込みます。最後の空きスラブオブジェクトには次のオブジェクトが無いため、NULLを設定します。

f:id:masami256:20190509234329p:plain

図_空きスラブオブジェクトの管理

使用中のスラブオブジェクトは特に管理を行いません。これはオブジェクトが不要になってオブジェクトを解放するときは、そのスラブオブジェクトのアドレスは自明なため、解放対象のオブジェクトが一意に決まるためです。

slubアロケータのスラブキャッシュはfreelist変数により、次回のスラブオブジェクト要求時に返却する空きスラブオブジェクトを参照しています。これにより、スラブ内に空きスラブオブジェクトが存在する間は高速にスラブオブジェクトを返却することができます。slubアローケータはスラブオブジェクトを返却する前に、次回のスラブオブジェクトのalloc時に返却するオブジェクトを参照するようにします。もし、最後の空きオブジェクトを返却した場合は、NULLを参照するようになります。この場合、次回の空きオブジェクト確保要求時に空きスラブオブジェクトの探索、もしくはスラブの作成を行うことになります。

f:id:masami256:20190509234421p:plain

図_空きスラブオブジェクトの確保

スラブオブジェクトを解放する場合、解放するスラブオブジェクトを次回のalloc要求時に返却するようにします。この時に次回のalloc要求時に返却予定だったスラブオブジェクトのアドレスを解放するスラブオブジェクトの先頭に書き込みます。

f:id:masami256:20190509234516p:plain

図_スラブオブジェクトの解放

slubアローケータではオブジェクトの確保に使用するスラブを常にCPUごとのメモリ域に置き、kmalloc関数などでスラブオブジェクトを要求された場合、このスラブから返却を行います。

f:id:masami256:20190509234657p:plain

図_percpuエリアにある構造体からスラブキャッシュの参照

使用中リスト

slubアローケータではスラブを管理する2つのリストがあります。一つはCPUと紐付いているスラブを管理するリストで、もう一つは、CPUとは紐付いておらず、スラブが所属するNUMAノード毎に管理するリストです。 CPUと紐付いている使用中リストに登録するスラブは1つだけ空きオブジェクトが存在するスラブです。また、このリストに登録できるスラブの数はcpu_partial変数にて設定されています。この値はスラブキャッシュ作成時に決定します。このリストはpage構造体のnext変数にてつながっています。この使用中リストは図_CPUに紐づく使用中リストのようになります。

f:id:masami256:20190509235150p:plain

図_CPUに紐づく使用中リスト

NUMAノードごとのは使用中リストは図_NUMAノードごとの使用中リストのように連結リストにて管理します。このリストには登録するスラブの上限はありません。

f:id:masami256:20190509235318p:plain

図_NUMAノードごとの使用中リスト

CPUと紐付いているスラブも、使用中リストにあるスラブもいずれも使用中ですが、CPUと紐付いているスラブは使用中リストでは管理しません。何かしらの理由でCPUとの紐付けを外す時に、CPUごとに保持しているスラブを使用中リストへ移動します。例えば、スラブ中に空きオブジェクトがなくなり、使用中リストから空きオブジェクトのあるスラブを探し、見つかったスラブとCPUごとのメモリ域ににあるスラブと交換したり、または、新規にスラブを作成した時に、作成したスラブをCPUごとのメモリ域に置き、CPUから参照されていたスラブを使用中リストに繋げるなどです。

CPUがスラブオブジェクトを取得するのに使用しているスラブはいずれの使用中リストの管理下にありません。スラブから空きオブジェクトが無くなった等の理由により、オブジェクトの取得対象のスラブで無くなった時に使用中リストに移動します。

スラブのマージ機能

slubとslabアロケータは同サイズのスラブキャッシュを一つのスラブキャッシュで管理する機能があり、デフォルトでこの機能は有効になっています。この機能を使用しない場合、128バイトのスラブオブジェクトを使用する機能が2つ存在した場合、スラブアローケータは2つの128バイトのスラブキャッシュを管理する必要があります。これは管理コストが増えたり、使用するページが違うため、CPUキャッシュヒット率が悪くなります。このような問題を解決するため、slab・slubアローケータでは同サイズのスラブキャッシュは一箇所で管理する仕様となっています。

マージの機能はslabとslubアローケータで詳細は異なっていて、slubアローケータでは、新規作成するスラブオブジェクトが以下の条件に当てはまる場合、新規にスラブキャッシュを作成せずに、既存のスラブキャッシュへのエイリアスを設定します。

  1. スラブオブジェクトのサイズが同一
  2. コンストラクタを使用しない
  3. slubのデバッグ機能を使用しない
  4. スラブ作成時のフラグにマージ機能を使用しないフラグが設定されていない

4のフラグについては「スラブキャッシュ生成時に設定するフラグ」にて説明します。

Chache Coloring

Cacje Coloringはスラブオブジェクトの開始アドレスをスラブ毎にずらすことによって、CPUのキャッシュライン競合を軽減させる仕組みです。slabアローケータはこの機能を採用していますが、 slubアローケータでは採用していません。CPUのキャッシュのライン調整に関してはCPUに任せる方針となっています(注1)。

注1: http://lkml.iu.edu/hypermail/linux/kernel/0808.0/1632.html

スラブの不活性化

slubアローケータはスラブオブジェクトを現在のCPUから参照されているスラブから返却します。参照されていないスラブはNUMAごとの使用中リストに繋がれますが、CPUごとのメモリ域にあるスラブをこの領域から外し、NUMAノードごとの使用中リストに移動することをスラブの不活性化と呼びます。

frozen状態

slubアローケータではスラブに対してfrozenという状態を設定します。fronzen状態にあるスラブはページの属性にPG_activeが設定されます。kmem_cache_alloc関数などによるオブジェクトの取得はこのスラブから行います。他のCPUはこのスラブからオブジェクトを取得することは出来ず、空きオブジェクトを追加する操作のみ行なえます。 CPUと紐付いているスラブはfrozen状態となり、スラブの不活性化によりCPUとのひも付けがなくなり、使用中リストにつなぐ時にスラブはfrozen状態が解除されます。

スラブキャッシュの情報

カーネルが管理しているスラブオブジェクトは/sys/kernel/debugディレクトリにて確認できます。Acpi-Namespaceなどシンボリックリンクとなっているものはslubアローケータのスラブマージ機能により、同サイズのオブジェクトを管理するスラブキャッシュとマージを行ったためです。

$ ls -l /sys/kernel/slab/  
total 0  
drwxr-xr-x 94 root root 0 May 20 20:19 ./  
drwxr-xr-x 11 root root 0 May 20 20:14 ../  
lrwxrwxrwx  1 root root 0 May 20 20:19 Acpi-Namespace -> :t-0000040/  
lrwxrwxrwx  1 root root 0 May 20 20:19 Acpi-Operand -> :t-0000072/  
lrwxrwxrwx  1 root root 0 May 20 20:19 Acpi-Parse -> :t-0000048/  
lrwxrwxrwx  1 root root 0 May 20 20:19 Acpi-ParseExt -> :t-0000072/  
lrwxrwxrwx  1 root root 0 May 20 20:19 Acpi-State -> :t-0000080/  
lrwxrwxrwx  1 root root 0 May 20 20:19 aio_kiocb -> :t-0000128/  
drwxr-xr-x  2 root root 0 May 20 20:19 anon_vma/  
lrwxrwxrwx  1 root root 0 May 20 20:19 anon_vma_chain -> :t-0000064/  
drwxr-xr-x  2 root root 0 May 20 20:14 :at-0000016/  
drwxr-xr-x  2 root root 0 May 20 20:14 :at-0000032/  
drwxr-xr-x  2 root root 0 May 20 20:14 :at-0000040/  

図_/sys/kernel/slabディレクト

ディレクトリ名には:tから始まるものや:atから始まるものがあります。これらのprefixは、スラブキャッシュ作成時に指定するflags引数に渡した(kmem_cache構造体のflags変数に設定されます)フラグを表します。表_slab_のprefixにフラグとprefixの対応を示します。:atや:dtなどはSLAB_CACHE_DMAとSLAB_NOTRACKの複合となります。これらのprefixはフラグが設定されていれば、そのprefixが使われるのですが、:tに関してはkmem_cache構造体のflagsとSLAB_NOTRACKのAND演算の結果が0の場合に設定されます。これはSLAB_NOTRACKがデバッグ機能の設定のため、値が0に設定されているためです。各フラグの内容については「スラブキャッシュ生成時に設定するフラグ」にて説明します。

prefix フラグ
:d SLAB_CACHE_DMA
:a SLAB_RECLAIM_ACCOUNT
:F SLAB_DEBUG_FREE
:t SLAB_NOTRACK

表_slabのprefix

個々のスラブキャッシュのディレクトリには以下のようなファイルが存在します。これらがスラブキャッシュの詳細になります。これらのファイルでスラブキャッシュが有効にしている機能などを確認できます。図_at-0000032ディレクトリは:at-0000032ディレクトリの様子です。

$ ls
./       align        cpu_partial  destroy_by_rcu  min_partial  objects_partial  partial          red_zone                  sanity_checks  slabs_cpu_partial  total_objects
../      alloc_calls  cpu_slabs    free_calls      objects      objs_per_slab    poison           remote_node_defrag_ratio  shrink         slab_size          trace
aliases  cache_dma    ctor         hwcache_align   object_size  order            reclaim_account  reserved                  slabs          store_user         validate

図_at-0000032ディレクト

slubアローケータのデータ構造

slubアローケータの主要な構造体には以下の4つの構造体です。

  • kmem_cache構造体
  • kmem_cache_cpu構造体
  • kmem_cache_node構造体
  • page構造体

これら4つの構造体のうち、page構造体以外はスラブアローケータのための構造体です。 スラブアローケータの核となる構造体はkmem_cache構造体です。kmem_cache構造体はslabやslubなど、スラブアローケータの実装によってデータ構造は違います。

名前 内容
cpu_slab cpuに紐付いているスラブキャッシュ
flags スラブキャッシュ生成時に指定したフラグ
min_partial 使用中のスラブキャッシュを保持できる最小の数。デフォルトは5、最大で10
size object_sizeをalignの境界に整列したサイズ
object_size メタデータを含まないスラブオブジェクトのサイズ
offset オブジェクトのサイズ+offsetで次の空きスラブオブジェクトを指す
cpu_partial cpu毎に保持できる使用中スラブキャッシュ数
oo スラブキャッシュ作成時に確保するページ数のオーダー
max スラブキャッシュ作成時に確保するページ数のオーダー数最大値
min スラブキャッシュ作成時に確保するページ数のオーダー数最小値
allocflags slabオブジェクトを確保する際に使用するgfpフラグ
refcount スラブマージ機能によってマージされたスラブキャッシュの数
ctor スラブキャッシュ作成時に呼び出すコンストラクタ関数
inuse スラブオブジェクトの使用数
align オブジェクトをsizeof(void *)の境界に整列した値。CPUのキャッシュラインに整列する場合は、先にCPUのキャッシュラインに合わせる
name スラブキャッシュの名前
list 作成したスラブキャッシュのリスト
memcg_params memory cgroup用の設定
remote_node_defrag_ratio 他のNUMAノードからスラブオブジェクトを取得する割合
node NUMAノード毎のスラブキャッシュ情報

表_kmem_cache構造体

slubオブジェクトのsize、object_sizeは図_スラブオブジェクトのようになります。

f:id:masami256:20190509235434p:plain

図_スラブオブジェクト

kmem_cache_cpu構造体はcpuコア毎にデータを保持します。page変数はpage構造体で、この変数はオブジェクトの返却を行うスラブのpage構造体を指します。 partial変数は使用中のスラブをつなぐリストとして使用しますが、list_head構造体は使用せず、page構造体のnext変数を利用してリスト管理を行います。このリストサイズはkmem_cache構造体のcpu_partial変数で設定します。

名前 内容
freelist 次の空きスラブオブジェクトへのポインタ
tid システムでユニークなトランザクションID
page スラブオブジェクトを管理しているページ
partial 使用中スラブの小規模なリスト

表_kmem_cache_cpu構造体

kmem_cache_node構造体はNUMAノード毎のデータを保持する構造体です。この構造体のデータ構造はスラブアローケータにより違い、ifdefによって、スラブアローケータ固有のメンバ変数が定義されます。slobアローケータでは使用しません。

名前 内容
list_lock sl[au]bアローケータ共通
nr_partial 使用中リストで管理しているデータ数
partial 使用中リストにあるスラブが使用しているpage構造体を管理するリスト

表_kmem_cache_node構造体

page構造体はページフレームを管理する構造体ですが、一部のフィールドはスラブアローケータが使用します。

名前 内容
freelist 空きオブジェクトを参照
counters オブジェクト数などを管理するカウンター
inuse 使用中のスラブオブジェクト数
object スラブオブジェクト数
frozen ページのfronzen状態
next 次の使用中リストへのポインタ
pages スラブキャッシュに使用しているページフレーム数
slab_page kmem_cache_cpu構造体が指すpageを指す
slab_cache ページが管理しているスラブへのポインタ

表_page構造体

page構造体はスラブが使用するページフレームに該当します。page構造体はメンバ変数に共用体を多用しているため、データ構造が複雑になっています。そのため、ソースコードを読み進める際には注意が必要です。 特によく使用するfrozen状態を設定するfrozen変数は構造体と共用体が入り組んだデータ構造のメンバ変数となっています。counters変数により1回の処理でfrozen変数を含めてデータを一括で設定した後に、frozenだけ変更する場合もあります。

データの関連

slubアローケータで使用するデータの関連

cpu、kmem_cache_cpu構造体、kmem_cache構造体、kmem_cache_node構造体、page構造体の関連は図_スラブで使用する構造体の関連のようになります。

f:id:masami256:20190509235714p:plain

図_スラブで使用する構造体の関連

スラブのライフサイクル

スラブがkmem_cache_cpu構造体から参照されているかか、kmem_cache_node構造体から参照されているかでfrozen状態が変わります。この時のfrozen状態を図_スラブの状態に示します。

f:id:masami256:20190509235746p:plain

図_スラブの状態

(1)はスラブを作成した時の状態です。スラブを作成した場合は必ずkmem_cache_cpu構造体のpage変数からスラブを参照して、このスラブからスラブオブジェクトを返却します。この状態はkmem_cache_cpu構造体からスラブが参照されているのでfrozenは1となります。

(2)はCPUごとのメモリ域からスラブが外れた状態です。この状態はスラブ内のスラブオブジェクトが全て使用中になる、または現在のCPUが使用するNUMAノードがスラブのNUMAノードと違う場合に、kmem_cache構造体のpage変数が参照するスラブの差し替えた場合です。この状態はスラブはkmem_cache_cpu構造体から参照されていませんのでfrozenは0となります。この状態に変更するために後述するdeactivate_slab関数によるスラブの不活性化処理を行います。

(3)の状態はスラブから空きスラブオブジェクトがなくなり、不活性化されたスラブがスラブオブジェクトの解放によりスラブ内に1つだけ空きオブジェクトができた場合、もしくはkmem_cache_cpu構造体が参照しているスラブのNUMAノードがCPUのNUMAノードと違った場合に、kmem_cache_node構造体の使用中リストからkmem_cache_cpu構造体の使用中リストで参照するように変更する時に起こります。この場合は、kmem_cache_cpu構造体からスラブが参照されるようになるので、frozenは1となります。

スラブアローケータの機能

スラブキャッシュ、スラブ、スラブオブジェクトに対する主な絹おは表_スラブに対する主な操作に示したものがあります。これらのうち、kmem_cache_で始まっている関数はシンボルが公開されていて、他のサブシステムから使用することができます。スラブの作成とスラブの不活性化はslub内部の処理です。

内容 対応する関数
スラブキャッシュの作成 kmem_cache_create
スラブキャッシュの破棄 kmem_cache_destroy
スラブオブエジェクトの確保 kmem_cache_alloc
スラブオブジェクトの解放 kmem_cache_free
スラブの作成 new_slab_objects
スラブの不活性化 deactivate_slab

表_スラブに対する主な機能

スラブキャッシュの作成

スラブキャッシュの作成はkmem_cache_create関数を使用します。スラブキャッシュの作成は各スラブアローケータ共通の処理と、各スラブアローケータ固有部分に分かれています。 スラブキャッシュの作成では最初にスラブがマージ可能かチェックします。作成しようとしたスラブキャッシュがマージ可能な場合は、マージ先のスラブキャッシュの参照数を増やします。既存のobject_size変数と作成しようとしていたスラブキャッシュのオブジェクトの大きさを比較し、大きいほうをobject_sizeに再設定や、オブジェクトの使用数などの設定も行います。最後に/sys/kernel/slabにシンボリックリンクファイルを作成します。シンボリックリンクの指す先はマージ先のスラブキャッシュです。マージに成功した場合はkmem_cache_create関数の処理はこれで完了となります。 スラブキャッシュのマージを行わなかった場合は、スラブキャッシュの新規作成を行います。 スラブキャッシュを新規作成する場合はdo_kmem_cache_create関数にて行います。まず、作成するスラブキャッシュのためにkmem_cache構造体のメモリを確保します。この時もスラブアローケータによりメモリを確保します。ここで、スラブキャッシュの名前、オブジェクトサイズなどのデータを設定します。また、memcgサブシステムで使用するmemcg_params変数の設定も行います。この時にmemcg_chache_params構造体のis_root_cache変数をtrueに設定しています。is_root_chache変数はスラブキャッシュの削除などで使用します。次に、各スラブアローケータ固有の処理を_kmem_cache_create関数にて行います。ここでの処理は主に、スラブキャッシュのオブジェクトサイズ、ページフレームを確保する時のオーダー、kmem_cache_alloc関数時に使用するGFPフラグなどの設定を行います。kmem_cache構造体のcpu_partial変数は各cpu毎にいくつの使用中リストを持てるかの設定です。この設定は表cpu_partial変数の設定にある条件できまります。条件は上から順にチェックします。

条件
kmem_cache_has_cpu_pertial関数がfalseを返す 0
kmem_cache構造体のsize変数がPAGE_SIZEより大きい 2
kmem_cache構造体のsize変数が1024より大きい 6
kmem_cache構造体のsize変数が256より大きい 13
全ての条件に当てはまらない場合 30

表_cpu_partial変数の設定

kmem_cache_has_cpu_partial関数はカーネルの設定でスラブアローケータの設定を有効にしない限り、常にtrueを返すのでcpu_partialが0に設定されることはありません。

次に、各NUMAノードで使用するkmem_cache_node構造体の初期化を行います。この構造体の初期化はリスト構造の初期化やスラブ数、スラブオブジェクト数を0にするなど目立った処理は特にありません。続いて、kmem_cache_cpu構造体の初期化を行いますが、こちらも特筆すべき内容はありません。__kmem_cache_create関数では最後に、作成したスラブキャッシュをsysfsより見えるように、sysfsのエントリーを作成して終了します。 __kmem_cache_create関数に成功し、do_kmem_cache_create関数に戻ったら作成したスラブキャッシュをスラブキャッシュを管理するリストに登録します。これでスラブキャッシュの作成が完了し、スラブオブジェクトの確保ができるようになります。ただし、この時点ではスラブオブジェクトはありませんので、初回のkmem_cache_alloc関数使用時にスラブを作成します。

スラブキャッシュ生成時に設定するフラグ

スラブキャッシュ生成時に設定するフラグのうちデバッグ向けのフラグを表_スラブキャッシュ生成時のフラグに示します。これらフラグの中で、SLAB_DESTROY_BY_RCU、SLAB_DESTROY_BY_RCU、SLAB_NOLEAKTRACEが設定されている場合、マージ機能は使用しません。

フラグ 内容
SLAB_HWCACHE_ALIGN スラブオブジェクトをCPUのキャッシュの境界に合わせる
SLAB_CACHE_DMA GFPフラグにGFP_DMAを設定し、DMA向けのスラブキャッシュを作成する
SLAB_PANIC kmem_cache_crate関数が失敗した場合にカーネルパニックする
SLAB_DESTROY_BY_RCU スラブキャッシュの解放時にRCUを使用して、非同期に行う
SLAB_MEM_SPREAD cpuset機能を使用しているプロセスの場合、cpusetで設定されたcpuのノードよりメモリを確保する(slabアローケータ専用)
SLAB_NOLEAKTRACE kmemleakによるトレースを行わない
SLAB_NOTRACK kmemcheckによるチェックを行わない
SLAB_RECLAIM_ACCOUNT スラブオブジェクトの回収を許する
SLAB_TEMPORARY スラブオブジェクトが短命と設定する。SLAB_RECLAIM_ACCOUNTと同様に回収可能となる

表_スラブキャッシュ作成時のフラグ

空きオブジェクトの管理

スラブ内の空きオブジェクトは、オブジェクト内のポインタによって次の空きオブジェクトを参照します。オブジェクトは使用中はデータが書き込まれていますが、オブジェクトが未使用の場合は重要なデータは存在せず、アクセスもされないため、オブジェクトの先頭に次の空きオブジェクトのアドレスを書き込んでいます。オブジェクトが不要になって解放された場合も、解放するオブジェクトの先頭に次の空きオブジェクトのアドレスを書き込みます。最後の空きオブジェクトは次の空きが無いため、NULLを設定します。ソースコード上は空きオブジェクトをfreelistと呼んでいますが、一般的な単方向連結リストなどと違い、次のオブジェクトを指す専用のポインタを持っておらず、未使用領域を次のオブジェクトを指すポインタとして使用していまます。

空きスラブオブジェクトの設定

スラブオブジェクトを作成するときは、ループしながらスラブ内の作成できるオブジェクトの数だけを設定していきます。 kmem_cache構造体の変数をs、s->offsetを0、s->sizeを16、オブジェクトのポインタをpとした時に、以下の手順で空きオブジェクトのリストを設定していきます。

  1. pの初期値をpageの先頭アドレスにする
  2. 次の空きオブジェクトのアドレスをpのアドレス + s->sizeとする
  3. pのアドレス + s->offsetの位置に、次のオブジェクトのアドレスを書き込む
  4. pの指すアドレスを現在のアドレス + s->sizeの位置に変更する
  5. 2〜4を繰り返す
  6. 作成できるオブジェクト数分、設定を行ったら、次の空きオブジェクトのアドレスをNULLに設定する。

上記の手順を図にすると「図_スラブオブジェクトのセットアップ」のようになります。

f:id:masami256:20190509235826p:plain

図_スラブオブジェクトのセットアップ

スラブオブジェクトを解放する場合は、以下の手順によって空きオブジェクトのリストを更新し、次のkmem_cache_alloc関数時には解放対象のスラブオブジェクトを返却するようにします。

  1. 解放対象のスラブオブジェクトが次の空きオブジェクトとして、現在設定されている次の空きオブジェクトを指すようにする
  2. 現在のCPUに設定されているkmem_cache_cpu構造体のfreelist変数を解放対象のスラブオブジェクトに変更

スラブオブジェクトの確保

スラブオブジェクトの確保はkmem_cache_alloc関数を使用します。kmem_cache_alloc関数はslab、slub、slobの各スラブアローケータ共通のインターフェースですが、実装はアローケータによって異なります。

スラブオブジェクトの確保におけるフローとして、下記の4パターンがあります。1はfastpathと呼ばれるパターンで、現在のCPUで使われているスラブからオブジェクトを確保できる場合で、最も高速に処理が完了します。2〜4のパターンはslowpathとなります。2はAとBのパターンがありますが、これはオブジェクトの確保時にノード番号を指定していない場合(A)・した場合(B)です。ここでオブジェクトが確保できなければ、全NUMAノードより使用中のスラブを探し、そこからオブジェクトの確保を試みます。全ノードを探索したが空きオブジェクトが見つからなかった場合は新規にスラブを作成します。

  1. 現在のCPUにあるスラブからオブジェクトを確保
  2. (A)ローカルメモリにある使用中のスラブからオブジェクトを確保。または、 (B)指定されたNUMAノードにある使用中のスラブからオブジェクトを確保
  3. 全NUMAノードから使用中のスラブを探し、そこからオブジェクトを確保
  4. スラブを新規に作成し、そこからオブジェクトを確保

スラブオブジェクトを確保する流れは図_スラブオブジェクトの確保フローのようになります。

kmem_cache_alloc()                      (1)
  -> slab_alloc()                       (2)
    -> slab_alloc_node()                (3)
      -> __slab_alloc()                 (4)
        -> deactivate_slab()            (5)
        -> get_freelist()               (6)
        -> new_slab_objects()           (7)
          -> get_partial()              (8)
            -> get_partial_node()       (9)
            -> get_any_partial()        (10)
          -> new_slab()                 (11)
            -> allocate_slab()          (12)
        -> set_freepointer()        (13)

図_スラブオブジェクトの確保フロー

スラブオブジェクトの取得はkmem_cache_alloc関数を使用しますが、実際の処理はslab_alloc_node関数から始まります。slab_alloc_node関数は、最初に現在のCPUに設定されているkmem_cache_cpu構造体を取得します。次にこのkmem_cache_cpu構造体より次の空きオブジェクトへのポインタ(freelist変数)を取得します。また、page構造体も取得します。ここで、node_match関数を呼び出し、次の2条件のいずれかを満たす場合はfastpathとなり、freelist変数をスラブオブジェクトとして呼び出し元に返却します。

  1. スラブが作成済み(page構造体がNULLでない)
  2. スラブオブジェクトを確保するNUMAノードが指定されていて、pageが所属するノードが指定されたノードと同じ

fastpathの場合は、次のkmem_cache_alloc関数の呼び出しに備え、freelistを次の空きオブジェクトを指すようにします。この時にCPU固有の先読み命令(*注1)にてfreelistをCPUのキャッシュに読み込みます。この手法により、次回のkmem_cache_alloc関数時にデータをメモリからでなくCPUのキャッシュから読み出せるようにすることで、高速化を図っています。

slowpathの場合は、__slab_alloc関数を実行します。ここでは、ローカルメモリにあるスラブより空きオブジェクトを探索します。kmem_cache_alloc関数によるスラブオブジェクトの確保では、NUMAノードの指定は行わないため、必ずローカルメモリを優先して探索します。ローカルメモリからの探索では、再度、現在のCPUに設定されているスラブキャッシュを取得し、このスラブキャッシュより参照されているスラブを取得します。 もし、スラブが存在しない場合、新たなスラブの作成に進みます。スラブの新規作成処理は後述します。スラブが存在した場合はスラブが所属するNUMAノードのチェックをnode_match関数を用いて行います。(この処理は、後述するkmem_cache_cpu構造体の更新後にジャンプして来る場合があります。)この場合もNUMAノードの指定は行っていません。よって、page構造体がNULLでなければ(スラブ用にページフレームが確保されている)、このスラブが利用可能なスラブオブジェクトの候補となります。 スラブに対する次のチェックとして、PageActiveマクロを利用し、スラブとしてい使用しているページがアクティブか調べます。もし、アクティブで無い場合、gfpフラグにALLOC_NO_WATERMARKSが設定されているか確認します。ALLOC_NO_WATERMARKSはウォーターマークのチェックをしないという設定です。このフラグがセットされていない場合は、deactivate_slab関数を使用して、スラブを不活性化します。この処理は「スラブの不活性化」で説明します。

次にpageのfreelist変数を対象にチェックを行っていきます。まず、kmem_cache_cpu構造体のfreelistが空きオブジェクトを指しているか確認し、参照先がスラブオブジェクトで無い場合は、page構造体のfreelist変数が空きオブジェクトを指しているかget_freelist関数を実行して調べます。

kmem_cache_cpu構造体のfreelist、もしくはpage構造体のfreelist変数が空きオブジェクトを指している場合、kmem_cache_cpu構造体のfreelist変数に空きオブジェクトを指しているfreelist変数を設定します。これでCPUが参照するスラブオブジェクトがfreelistになります。この処理にはload_freelistラベルが設定されて、スラブの新規作成が成功した場合にも実行します。スラブオブジェクトの設定を行ったら、呼び出し元の関数にこのスラブオブジェクトを返して、__slab_alloc関数を終了します。

スラブを新規に作成する前に、kmeme_cache_cpu構造体のpartial変数がNULLでないかチェックします。これまでの処理ではkmem_cache_cpu構造体のpage変数に設定されているスラブを対象に空きオブジェクトを探索しましたが、partial変数のチェックで、kmem_cache_cpu構造体の使用中リストに空きオブジェクトが存在するかチェックします。使用中のスラブオブジェクトが存在している場合に、kmem_cache_cpu構造体を更新し、スラブオブジェクトを取得する対象のスラブをこのスラブにします。この処理でkmem_cache_cpu構造体のpage変数とpartital変数が同じスラブを指すようになります。そして、partial変数はスラブが指す次のpage構造体に設定します。

そして、kmem_cache_cpu構造体のfreelist変数はNULLにします。これで、kmem_cache_cpu構造体のpageとpartialの設定を更新しfreelistがNULLを指すことで、、空きスラブオブジェクトは存在してないという状態になります。ここまで行ったら、slowpathの最初の処理にジャンプします。この時、ページフレームはすでに存在しているので、node_match関数によるノードのチェックから開始します。こまでの処理でローカルメモリからはスラブオブジェクトが見つからなかったため、new_slab_objects関数を実行します。この関数は大きく2つの処理があり、1つ目の処理はリモートメモリより空きオブジェクトを検索します。この処理で空きオブジェクトが見つかれば、このオブジェクトを返却できます。リモートメモリより空きオブジェクトを探すのはget_partial関数にて行います。

get_partial関数で空きスラブオブジェクトを探索する場合は、まずスラブオブジェクトを確保するNUMAノードが指定されているか確認し、ノードが指定されていなければローカルメモリ、指定があればそのノードよりスラブオブジェクトの取得を試みます。指定したノードよりスラブオブジェクトを確保する処理はget_pertial_node関数にて行います。この関数は指定されたNUMAノードにあるスラブのリスト(kmem_cache_node構造体のpartial変数)を辿り、空きスラブオブジェクトが存在するスラブを探します。スラブ中の空きスラブオブジェクトの探索はaqcuire_slab関数にて行います。スラブ内に空きスラブオブジェクトが見つかった場合は、そのスラブをfrozen状態に変更します。そして、使用中リストより、スラブのページフレームを外します。 aquire_slab関数にてり空きスラブオブジェクトが見つかった場合、スラブは以下のいずれかの状態になります。

  1. スラブオブジェクト確保したことで空きスラブオブジェクトなくなる
  2. スラブオブジェクトを確保したが、まだ空きスラブオブジェクトが存在する

1の場合は、kmem_cache_cpu構造体のpage変数にスラブが使用してるpageを設定します。 2の場合は、kmem_cache_cpu構造体のpartial変数にスラブが設定されているか確認し、スラブが設定されていた場合は、そのスラブのfrozen状態を解除し、そして、そのスラブに使用中のスラブオブジェクトがなく、使用中スラブの数が下限以上あればそのスラブはslab_discard関数にてスラブの不活性化処理を行います。使用中のスラブオブジェクトがあれば使用中リストにそのスラブを追加します。これで元々partial変数に設定されていたスラブの処理が完了したので、新しいスラブのための設定を行います。設定するのはpage構造体のデータで、オブジェクト数の設定などを行います。 スラブ中の空きスラブオブジェクト数が十分にあれば、この見つかったスラブオブジェクトを返却します。もし、空きスラブオブジェクト数が十分になければ、別のスラブを探索します。

ローカルメモリもしくは指定したノードよりスラブオブジェクトを確保できなかった場合は、全スラブを対象に空きスラブオブジェクトを探索します。これはget_any_pertial関数にて行いますが、空きスラブオブジェクト探索するか否かは、kmem_cache構造体のremote_node_defrag_ratioの設定値により変わります。remote_node_defrag_ratioが0の場合は他のノードの探索は行いません。次にCPUのタイムスタンプカウンタ値を取得し、この値を1024で割った余りがremote_node_defrag_ratioより小さかった場合も他ノードの探索は行いません。他のノードを探索しなかった場合は、new_slab_objects関数に戻り、新規にスラブオブジェクトを作成します。remote_node_defrag_ratioの値はカーネル内部とsysfsでエクスポートするときで桁数が違います。sysfs経由でこの値を設定/確認する場合は0~100までの数値を使用しますが、カーネル内部ではその値を10倍して使用します。デフォルト値は1000となっています。 スラブオブジェクトを探索する場合は、スラブが使用するZONEより、そのNUMAノードに存在するkmem_cache_node構造体を取得します。そして、そのノードからの空きスラブオブジェクトの確保が許可されている場合は、get_partial_node関数を使用して、空きスラブオブジェクトを探します。これでオブジェクトが見つかれば、そのオブジェクトを返却します。

リモートメモリを検索しても利用可能なかった場合は、new_slab_objects関数の2段階目の処理としてスラブの新規作成処理を行います。スラブの作成処理はnew_slab関数にて行います。スラブの新規作成では、まずページフレームを確保行います。ページフレームの確保とpage構造体の基本的な設定はallocate_slab関数で行います。

最初にpageの確保処理をalloc_slab_page関数にて行います。pageの確保するNUMAオードが指定されていないければ、ローカルメモリから、指定されている場合は対象のNUMAノードよりpageの確保を試みます。pageを確保する時のオーダーはkmem_cache構造体のoo変数に設定されている値を使用します。もしページフレームの確保に失敗した場合、オーダーをkmem_cache構造体のmin変数に設定されている最小のオーダー数に変更して再度alloc_slab_page関数を実行します。これでもページフレームを確保できなければNULLを返却します。

allocate_slab関数にてpageが確保できたらpage構造体のobjects変数にオブジェクト数の設定を行います。また、ページフレームを確保したZONEの状態を変更します。kmem_cache構造体のflags変数にNR_SLAB_RECLAIMABLEが設定されている場合はページ回収可能なページ数を、設定されていない場合はページ回収不可能なページ数を既存の値に追加します。ここまでの処理でスラブのためのpageの確保処理が完了となります。ページフレームの確保ができたら、次にpage構造体への設定として、page構造体のslab_cache変数に現在のkmem_cache構造体を設定し、page構造体とスラブを関連付けます。ページフレームをスラブとしてに使用していることを示すために、pageにPG_slabビットを設定します。page構造体のpfmemallocが設定されている場合は、pageにPG_activeビットを設定します。

次に、pageに対してスラブオブジェクトを1つずつ設定していきます。この処理については「スラブオブジェクトの作成」を参照してください。 最後に、page構造体の設定で、freelist変数をpageの開始アドレスに設定します。pageの開始アドレスは1つ目のスラブオブジェクトのアドレスですので、page構造体のfreelistは最初の空きスラブオブジェクトを指すことになります。inuse変数にはpage構造体のobjects変数を設定します。frozen変数には1を設定し、pageの管理をslubアローケータが行っているとマークします。以上でスラブオブジェクトの作成処理が完了となり、呼び出し元にpage構造体を返却します。

new_slab関数の実行によりスラブの作成が成功した場合は、kmem_cache_cpu構造体の設定を行います。まず、現在のCPUに設定されているkmem_cache構造体よりkmem_cache_cpu構造体のcpu_slab変数を取得します。kmem_cache_cpu構造体のpage変数がスラブのページフレームを指している場合は、現在設定されているkmem_cache_cpu構造体を一旦無効化します。この処理は「kmem_cache_cpu構造体の初期化」にて説明します。そして、page構造体のfreelistを別の変数にコピーし、page構造体のfreelist変数はNULLにします。そして、kmem_cache_cpu構造体のpage変数にnew_slab関数で作成したスラブのページフレームを設定します。kmem_cache_cpu構造体の設定が完了したら、new_slab_object関数の引数で渡されたkmem_cache_cpu構造体の変数に、設定したkmem_cache_cpu構造体を設定します。よって、new_slab_object関数の呼び出しにより、呼び出し元の__slab_alloc関数で使用していたkmem_cache_cpu構造体の内容が変わることになります。new_slab_objects関数では返り値として、コピーしておいたpage構造体のfreelist変数を返却します。

new_slab_objects関数を呼び出した__slab_alloc関数では、スラブの作成に失敗した場合は、これ以上の処理を行えないため、NULLを返します。スラブの作成に成功した場合はload_freelistラベルにジャンプします。

slab_alloc_node関数では、fastpath、slowpathいずれの場合も、スラブオブジェクトを返す前にGFPフラグをチェックし、__GFP_ZEROフラグが設定されている場合は、スラブオブジェクトに0x0を書き込みます。また、スラブオブジェクト確保時にフック関数を登録することができ、これらが設定されている場合は、スラブオブジェクトの確保前、確保後に登録されているフック関数を実行します。これらのフック関数は通常デバッグ機能向けに使われていて、スラブを使用する側からは設定できません。

注1:x86x86_64の場合はprefetcht0命令。

スラブオブジェクトの解放

kmem_cache_alloc関数で確保したスラブオブジェクトはkmem_cache_free関数で解放します。スラブオブジェクトの解放処理は図_スラブオブジェクト解放のようになります。

kmem_cache_free()
  -> cache_from_obj()
  -> slab_free()
    -> set_freepointer()    // fastpath
    -> __slab_free()        // slowpath
      -> put_cpu_partial()
        -> unfreeze_partials()
      -> discard_slab()
      -> remove_partial()

図_スラブオブジェクトの解放

kmem_cache_free関数では実質的な処理はせず、slab_free関数にて解放の処理を行います。スラブオブジェクトの解放は、確保の場合と同じくfastpathとslowpathの2パターンがあります。解放対象のオブジェクトがCPUから参照されている場合がfastpathとなり、そうでない場合はslowpathとなります。fastpathの場合は、解放するオブジェクトを次の空きオブジェクトに設定します。この時の様子を図_スラブオブジェクト解放前と図_スラブオブジェクト解放後に示します。解放前はkmem_cache_cpu構造体のfreelistは解放するスラブオブジェクトの隣にあるスラブオブジェクトを指していますが、スラブの解放後は解放したスラブオブジェクトを指すようになります。fastpathの場合は、現在使用中のスラブに対してオブジェクトの解放処理を行うため、freelistの変更だけで済んでいます。

f:id:masami256:20190509235911p:plain

図_スラブオブジェクト解放前

f:id:masami256:20190509235927p:plain

図_スラブオブジェクト解放後

slowpathの場合は__slab_\free関数にて処理を行います。

まず、以下のループでスラブが使用するpage構造体のfreelistを設定します。この時にpage構造体のデータを一部保存する一時変数として、newを使用します。

  1. kmem_cache_node構造体がNULL出ない場合は、この構造体のロックを取得(ループ1回目はkmem_cache_node構造体はNULL)
  2. page構造体のfreelist変数をprior変数にコピーし、set_freepointer関数で、priorを次に返すオブジェクトに設定
  3. newにスラブが使用しているpage構造体のcountersをコピー
  4. newのスラブオブジェクト使用数をデクリメント
  5. スラブのfrozen状態をwas_frozen変数にコピー
  6. newのオブジェクト使用数が0またはpriorがNULLかつ、スラブがfrozen状態で無かった場合7の処理を行います
  7. page構造体のfreelist変数がNULLの場合、8の処理を行い、NULLでなかった場合は9の処理を行います。いずれの処理も終了したら10に移動します
  8. スラブのfrozen状態を1にセット
  9. スラブが所属するNUMAノードのkmem_cache_node構造体を取得
  10. cmpxchg_double_slab関数でスラブのpage構造体のfreelistとcounters変数をnewに設定したデータに更新
  11. cmpxchg_double_slab関数が変更を行わなかった場合はループを終了

7の処理はソースコード上ではfreelistのチェックだけでなく、kmem_cache_has_cpu_partial関数がtrueを返すことも条件の一つなのですが、スラブのデバック機能を使用しない場合はtrueが返ります。 8の処理でfrozen状態を1に設定していますが、これはスラブが使用中リストで管理されていなかった場合になります。

上述のループを終了した段階で、kmem_cache_node構造体が取得されていない場合で、スラブをfrozen状態に変更した場合は、スラブをkmem_cache_cpu構造体のpartial変数のリストにつなぎます。 スラブをkmem_cache_cpu構造体から外すにはput_cpu_partial関数を使用します。この処理中は割り込みを禁止します。

次に、以下の処理をループで行います。

  1. 現在のCPUが参照しているkmem_cache構造体よりkmem_cache_cpu構造体のpartial変数を取得してoldpage変数に代入
  2. oldpageがNULLで無い場合は3の処理を行い、NULLだった場合はXに移動します
  3. 使用中オブジェクト数(ループ1回目は0、4手順目で更新する)がkmem_cache構造体のcpu_partialより大きい場合、unfreeze_partials関数を実行してkmem_cache_cpu構造体のpartial変数に紐づく全てのスラブのforzne状態を解除
  4. 引数で渡されたpae構造体のpage変数のpage数、使用中オブジェクト数を更新命令
  5. page変数next変数に取得したoldpageを設定
  6. this_cpu_cmpxchg関数でkmem_cache構造体のcpu_slab変数が参照しているpartialを更新したpage変数に変更
  7. this_cpu_cmpxchg関数が更新を行わなかった場合はループを終了

unfreeze_partials関数はkmem_cache_cpuのpartialリストにあるスラブのfrozen状態に変更し、kmem_cache_nodeの使用中リストに登録します。この処理では2段階のループがあります。 page構造体のデータを更新するための作業用変数としてpage、oldとnewを使用します。

  1. kmem_cache_cpu構造体のpartialに設定されているpage構造体をpageに代入
  2. スラブが使用しているpage構造体が所属するNUMAノードのkmem_cache_node構造体を取得
  3. 取得したkmem_cache_node構造体が前回取得したノード違った場合、前回のkmem_cache_nodeがNULLでなければ、ロックを解除
  4. 取得したkmem_cache_node構造体のロックを取得
  5. pageに設定されているfreelistとcountersをoldにコピー
  6. oldに設定したcountersとfreelistをnewにコピー
  7. newのfrozen変数を0に設定
  8. __cmpxchg_double_slab関数でpage変数のfreelistとcountersをnewに設定した値に更新
  9. __cmpxchg_double_slab関数が値の更新を行わなかったら11に進み、値の更新を行った場合は5に戻り、freelistとcouters変数の更新を続けます
  10. newに設定されているスラブオブジェクトの使用数が0かつ、kmem_cache_nodeにある使用中スラブ数が十分にある場合、そのスラブは不活性化の対象とします。そうでない場合はスラブをkmem_cache_node構造体の使用中リストに登録します。

上記手順の5と6は同じデータを設定していますが、7手順目でfrozen状態を解除するため、8手順目でpage変数にはfrozen状態が解除されたデータとして登録されます。

ループを抜けた時にkmem_cache_node構造体で取得したロックを解除します。 そして、ループの10手順目で削除対象のスラブが設定した場合は、discard_slab関数を実行してスラブを不活性化します。この処理は「スラブの不活性化」にて説明します。

kmem_cache_cpu構造体のデータクリア

スラブを無効化する時にkmem_cache_cpu構造体に設定されているデータのクリアを行います。この処理はflush_slab関数で行います。この関数は最初にdeactivate_slab関数を実行し、スラブを無効化します。次に、以下の3変数の値を変更します。

スラブの不活性化

スラブの不活性化処理は、下記3処理を行います。

  1. frozen状態スラブのfreelistで管理されている未使用のスラブオブジェクトを、同じスラブのpage構造体のfreelistへ移動
  2. スラブのfrozen状態解除
  3. 移動後のスラブの状態により、使用中リストへ移動、もしくは使用中リストから削除

これらの処理により、kmem_cache_cpu構造体が使用しているスラブを差し替えることができるようになります。この処理が必要になるのは2パターンあります。

  1. スラブを新規作成し、既存のスラブと置き換える
  2. スラブが不要になった場合

1のパターンは、スラブオブジェクトの確保時に既存のスラブからはスラブオブジェクトが確保できなかったため、新規にスラブを作成する場合です。2のパターンはkmem_cache_free関数によるスラブオブジェクトの解放時に、スラブ内のオブジェクトが全て空きオブジェクトになり、かつ使用中リストの数が下限以上ある場合、もしくはkmem_cache_destroy関数にてスラブキャッシュ自体を削除する場合です。

スラブの不活性化処理を行うのはdeactivate_slab関数です。処理は大きく2段階に別れていて、1段階目は空きオブジェクトの移動。2段階目はスラブのfrozen解除と使用中リストへの追加・削除です。 空きオブジェクトの移動では、frozen状態のスラブにあるfreelist(kmem_cache_cpu構造体のfreelist変数)より、空きオブジェクトを全て、同スラブのpage構造体のfreelistへ移動します。

第2段階では、最初に1段階目の処理の後に、freelistがまだ空きオブジェクトを指しているか確認し、空きオブジェクトを指している場合は、freelistがpage構造体のfreelistに設定した空きオブジェクトを指すように変更します。 この時にテンポラリの変数として、old、newと呼ぶpage構造体を使います。空きオブジェクトを指していた場合は、newの使用中スラブ数を減らし、newのfreelistがfreelistを指すようにします。空きオブジェクトを指していない場合は、newのfreelistとoldのfreelistを同じに設定します。以降の処理で、page構造体の変数を参照する際はnewを使用します。

次に、スラブが使用しているページに対する操作を決定します。どのような操作を行うかはスラブの状態に応じて決定します。スラブの状態を表_スラブの状態に示します。

状態 内容
M_NONE 何もしない(初期値)
M_PARTIAL スラブには使用中のオブジェクトがある
M_FULL スラブに空きオブジェクトが無い
M_FREE スラブに使用中オブジェクトが無い

表_スラブの状態

スラブの状態は2つの変数(l、m)により管理します。両者とも初期値はM_NONEです。まずはmについて、表_スラブの状態設定のようにmの状態を設定します。これら状態のうちM_FULLはデバッグ用途のフラグですので、通常はM_FULL状態の場合の処理はありません。

条件 設定する状態
スラブに使用中のオブジェクトがなく、NUMAノード内の使用中リスト数が下限以上に存在する M_FREE
page構造体のfreelistが空きオブジェクトを指している M_PARTIAL
その他 M_FULL

表_スラブの状態設定

次にlとmが同値でなければ、スラブが使用しているページに対する操作を行います。最初にこのコードパスを通る場合、lにはM_NONEが設定されているため、この条件は必ず真になります。そして、以下の条件に合致する処理を行います。

条件 処理内容
l == M_PARTIAL 使用中リストからスラブを削除
l == M_FULL 空きオブジェクト無しリストからスラブを削除(このリストはデバッグ用のため、通常は存在しません)
m == M_PARTIAL スラブを使用中リストに追加
m == M_FULL スラブを空きオブジェクト無しリストに追加(このリストはデバッグ用のため、通常は存在しません)

この処理が終わった後にlの値をmの値に設定します。次に、引数で渡されたpageのfreelistとcountersがoldのfreelistとcountersが同じなら、これらをnewのfreelist、countersに変更します。変更は__cmpxchg_double_slab関数で行います。もし__cmpxchg_double_slab関数がfalseを返した場合は変更を行わず、2段階目の最初の処理にジャンプし、2段階目の処理を再度実行します。変更を行った場合は最後の処理として、mがM_FREEに設定されているか確認し、M_FREEが設定されている場合はスラブは不要と判断し、discard_slab関数を実行してスラブの削除を行います。

スラブの削除

kmem_cache_free関数や、スラブの不活性化などにより、スラブ内に使用中のオブジェクトがなくなった場合にスラブの削除処理を行います。スラブの削除を行うインターフェースとなる関数はdiscard_slab関数です。スラブの削除はkmem_cache構造体のflags変数を見て、SLAB_DESTROY_BY_RCUが設定されている場合は、RCUを使用して遅延実行し、設定されていない場合はそのまま削除処理を行います。どちらの場合も削除処理として__free_slab関数を使用します。

discard_slab関数は削除対象のスラブが所属するNUMAノードのkmem_cache_node構造体を取得し、nr_slabs変数からスラブ数の減算と、解放することになるスラブオブジェクト数をobjects変数から減算します。次に、free_slab関数にて、kmem_cache構造体のflags変数にSLAB_DESTROY_BY_RCUが設定されているか確認し、設定されている場合はスラブをスラブ削除用のRCUリストにつなぎ、処理を終了します。 SLAB_DESTROY_BY_RCUが設定されていない場合は__free_salb関数にてスラブの削除を行います。RCUを使用する場合もスラブの削除実行には__free_slab関数を使用します。 __free_slab関数が呼ばれる時点で、スラブオブジェクトに対する解放処理は行われているため、この関数ではスラブが使用しているpageの解放処理が主となります。ここでの処理は、ページが所属しているZONEより、スラブが使用していたページ数の減算。ページの属性としてPG_activeとPG_slabビットを下ろします。次にpage構造体の_mapcount変数を-1に設定します。_mapcountは共用体のメンバ変数で、他にinuse、objects、frozenの3変数を持つ構造体も共用体のメンバ変数で、_mapcountに-1を設定することで、他のinuseなどの変数も無効な値に上書きされます。task_struct構造体のreclaim_state変数がNULLでなければ、回収済みのページ数に今回解放するページ数を足します。そして、__free_pages関数を使用し、ページの解放を行います。削除したスラブがkmem_cache_create関数で作成したスラブの場合は、memcgサブシステムのアカウンティング情報よりより使用していたページ数を減らします。

スラブキャッシュの削除

スラブキャッシュが不要になったらkmem_cache_destroy関数でスラブキャッシュを削除します。スラブキャッシュの削除処理はslabアローケータと共通処理とslubアローケータ固有の処理があります。スラブキャッシュの削除はまず、各アローケータ共通であるkmem_cache_destroy関数から処理が始まります。削除処理中はロックを確保し、他のスレッドから操作できないようにします。そして、スラブキャッシュの削除を行います。これは各アローケータ固有の処理で、インターフェースは__kmem_cache_shutdown関数です。__kmem_cache_shutdown関数自体は特に処理は行わず、処理はkmem_cache_close関数にて行います。 slubアローケータでは、最初に全CPUに対して以下の処理を行います。

  1. CPUから参照されているkmem_cache_cpu構造体のpageもしくはpartial変数がNULLの場合(スラブが存在しない)、何もしない
  2. page変数にスラブが設定されている場合、flush_cpu_slab関数が__flush_cpu_slab関数を実行し、3〜5の処理を行う
  3. deactivate_slab関数を実行し、スラブを不活性化。kmem_cache_cpu構造体の変数を初期化
  4. unfreeze_partials関数にてkmem_cache_cpu構造体のpartial変数に設定されてるスラブのfrozen状態を解除
  5. スラブが使用していた不要になったページをdiscard_slab関数にて解放

上記1〜5の処理で、各CPUから参照されていたkmem_cache_cpu構造体のデータを処理したので、次にNUMAノードに設定されているスラブを片付けます。この処理はkmem_cache構造体のnode変数に設定されている、kmem_cache_node構造体のpartialリストよりノードに存在するスラブに対してdiscard_slab関数を実行し、スラブを削除します。

次にCPUごとのメモリ域にあるkmem_cache構造体のcpu_slab変数のメモリを解放します。最後に、kmem_cache構造体のnodeメンバ変数で使用していたkmem_cache_node構造体のメモリを解放します。

ここまででslubアローケータ固有の処理を終了し、スラブアローケータ共通処理に戻ります。共通処理に戻ったら確保していたロックを解除し、do_kmem_cache_release関数を実行し、sysfsでエクスポートしているスラブキャッシュのエントリを削除します。これでスラブキャッシュの削除が完了となります。

スラブキャッシュのセットアップ

スラブアロケータを使用できるようにするために、カーネルの起動時にセットアップを行います。ここでセットアップするのは、kmem_cache構造体とkmem_cache_node構造体のメモリを確保するためのスラブキャッシュと、kmalloc関数で使用する様々なサイズのスラブキャッシュです。slubアローケータのスラブキャッシュを管理するためのメモリ管理機構に、slubスラブアロケータを使用します。

スラブキャッシュのセットアップ状況はいくつかの段階があります。この定義を表_スラブキャッシュのセットアップ状況に示します。セットアップの状態は表のDOWNからFULLに向かって進みます。最初はDOWNから始まります。

状態 内容
DOWN スラブキャッシュをまだ使用できない
PARTIAL slubアローケータのkmem_cache_node構造体が使用可能になった(kmem_cache_init関数内で設定)
PARTIAL_NODE slabアローケータで使用
UP スラブキャッシュを使えるようになったが、まだスラブの全機能は使用可能にはなっていない(create_kmalloc_caches関数で設定)
FULL 全て使用可能(slab_sysfs_init関数で設定)

表_スラブキャッシュのセットアップ状況

スラブキャッシュの初期化はkmem_cache_init関数が行います。この関数では最初に__initdata領域に存在する初期化時用のkmem_cache構造体とkmem_cache_node構造体のデータを使用し、スラブキャッシュを作成します。このスラブキャッシュの作成では、kmem_cache構造体のname、size、alignなどの変数を設定し、__kmem_cache_create関数を呼び出します。__kmem_cache_create関数は通常のスラブキャッシュ作成時にも使用しますが、通常時との違いとして、スラブキャッシュの作成状況による処理の変更が2箇所あります。 1つ目の変更箇所はkmem_cache_open関数から呼ばれるinit_kmem_cache_nodes関数の処理です。init_kmem_cache_nodes関数ではスラブキャッシュの作成状態がDOWNの場合、early_kmem_cache_node_alloc関数を使用してスラブキャッシュを作成します。これは、状態がDOWNの場合、まだスラブキャッシュを使用することが出来ないため、スラブを仮に作成し、そこからメモリを確保してスラブキャッシュを作成します。この場合でもスラブやページフレーム、スラブの使用中リストは設定します。

もう一箇所は__kmem_cache_create関数で、kmem_cache_open関数を実行し、スラブキャッシュが作成できたあとに状態をチェックし、状態がUP以下の場合は、sysfsへのスラブキャッシュ登録処理を行いません。よって、最初期のセットアップではスラブキャッシュを作成しただけで処理を終了します。__kmem_cache_create関数でスラブキャッシュを設定した後は、kmem_cache構造体のrefcountを-1に設定し、スラブのマージ機能対象外になるようにします。 この手順をkmem_cache構造体、kmem_cache_node構造体のスラブキャッシュのために行います。ここまで完了したらスラブキャッシュのセットアップ状況をPARTIALに設定します。

ここまでで仮のセットアップを行ったので、次に本格的なセットアップに入ります。ここからのセットアップもまずは、kmem_cache構造体用のスラブキャッシュと、kmem_cache_node構造体用のスラブキャッシュのセットアップです。これらのセットアップはbootstrap関数にて行います。まず、仮にセットアップしたスラブキャッシュを使用し、kmem_cache構造体用のメモリを確保します。そして、新しい構造体に既存のデータをコピーします。これで__flush_cpu_slab関数を使用し、CPUが参照しているスラブを無効にします。 つぎに、全NUMAノードにあるkmem_cache_node構造体のpartialリストにつながっているページフレームが参照しているslab_cache変数に、先ほど新規作成したkmem_cache構造体を設定します。 そして、スラブキャッシュを管理するslab_cachesリストに作成したkmem_cache構造体を設定します。

kmem_cache構造体とkmem_cache_node構造体のスラブキャッシュを作成したら、次にkmalloc関数用のスラブキャッシュを作成します。この処理はcreate_kmalloc_caches関数にて行います。create_kmalloc_caches関数では作成するサイズの設定を行い、create_kmalloc_cache関数にてスラブキャッシュを作成します。スラブキャッシュの作成はcreate_boot_cache関数を使用します。create_boot_cache関数は__kmem_cache_create関数を使用して、スラブキャッシュを作成しますが、この時点ではスラブキャッシュの作成状況はPARTIALになっているため、最初にkmem_cache構造体・kmem_cache_node構造体を作成した時の用にinit_kmem_cache_nodes関数ではearly_kmem_cache_node_alloc関数ではなく、通常のkmem_cache_alloc_node関数を使用します。kmalloc関数用のスラブキャッシュを作成したらスラブキャッシュの作成状態をUPに変更します。

スラブキャッシュの作成が完了したら、slab_sysfs_init関数にてスラブキャッシュの情報をsysfs経由でエクスポートします。この時にスラブキャッシュの作成状態をFULLに変更し、スラブキャッシュの機能の設定が完了します。

slubアローケータで使用するヘルパー関数

slubアローケータ内でよく使われる関数を紹介します。

__cmpxchg_double_slab関数

__cmpxchg_double_slab関数は引数を7つ受け取りますが、そのうち重要なものを表___cmpxchg_double_slabの引数に示します。

名前
struct kmem_cache * s
struct page * page
void * freelist_old
unsigned long counters_old
void * freelist_new
unsigned long counters_new

表___cmpxchg_double_slabの引数

この関数はpage->freelistとpage->countersの値がfreelist_old、counters_oldと同一の場合は、page->freelistをfreelist_new、page->countersをcounters_newの値に設定します。CPUがアトミックな更新命令を持っている場合はCPUの命令を使用し、アトミックな更新命令を持っていない場合はロックを取得した上で値の更新を行います。

slubアローケータでは他にもthis_cpu_cmpxchg_double関数やcmpxchg_double_slab関数を値の交換のために使用していますが、処理の基本的な考え方は__cmpxchg_double_slab関数と同じです。

kmalloc関数とkfree関数による動的なメモリ確保と解放

kmalloc関数

任意のサイズのメモリを動的に確保したい場合、ユーザランドのプログラムではmalloc(3)を使用しますが、Linuxカーネル内ではkmalloc関数を使用します。 kmalloc関数のインターフェースは「図_kmallocのインターフェース」のようになっています。sizeは確保するメモリの量、flagsは__get_free_pages関数でも使用している、メモリ確保時の制御フラグです。また、メモリを0x0で埋めて返すkzalloc関数などの補助的な関数もあります。

static __always_inline void *kmalloc(size_t size, gfp_t flags);
static inline void *kzalloc(size_t size, gfp_t flags);

図_kmallocのインターフェース

kmalloc関数は確保するサイズに応じて、スラブアローケータよりメモリを確保します。kmalloc関数はカーネルコンパイル時にサイズが確定していて、サイズがkmalloc用のスラブキャッシュの最大サイズ(KMALLOC_MAX_SIZE)を超える場合はページアローケータを使用してメモリを確保します。それ以外の場合はサイズに応じたkmalloc用のスラブキャッシュよりスラブオブジェクトを取得して返します。 kmalloc用のスラブキャッシュはcreate_kmalloc_caches関数にてカーネルの起動時に作成します。

ZONE_NORMALに所属するページフレームからなるkmalloc用のスラブは「図_kmallocのスラブキャッシュ」のようになっています。

lrwxrwxrwx  1 root root 0 Jun  9 00:27 kmalloc-1024 -> :t-0001024
lrwxrwxrwx  1 root root 0 Jun  9 00:27 kmalloc-128 -> :t-0000128
lrwxrwxrwx  1 root root 0 Jun  9 00:27 kmalloc-16 -> :t-0000016
lrwxrwxrwx  1 root root 0 Jun  9 00:27 kmalloc-192 -> :t-0000192
lrwxrwxrwx  1 root root 0 Jun  9 00:27 kmalloc-2048 -> :t-0002048
lrwxrwxrwx  1 root root 0 Jun  9 00:27 kmalloc-256 -> :t-0000256
lrwxrwxrwx  1 root root 0 Jun  9 00:27 kmalloc-32 -> :t-0000032
lrwxrwxrwx  1 root root 0 Jun  9 00:27 kmalloc-4096 -> :t-0004096
lrwxrwxrwx  1 root root 0 Jun  9 00:27 kmalloc-512 -> :t-0000512
lrwxrwxrwx  1 root root 0 Jun  9 00:27 kmalloc-64 -> :t-0000064
lrwxrwxrwx  1 root root 0 Jun  9 00:27 kmalloc-8 -> :t-0000008
lrwxrwxrwx  1 root root 0 Jun  9 00:27 kmalloc-8192 -> :t-0008192
lrwxrwxrwx  1 root root 0 Jun  9 00:27 kmalloc-96 -> :t-0000096

図_kmallocのスラブキャッシュ

例えば、48バイトのsizeを指定された場合、次に大きいkmalloc-64のスラブキャッシュよりオブジェクトを確保することになります。このようにスラブアロケータを使用することで、要求するsizeによっては無駄が出てしまいますが、メモリを効率的に速く確保できるという利点もあります。

kfree関数

kmalloc関数で確保したメモリを解放するときはkfree関数を使用します。kfree関数のインターフェースは図_kfreeのインターフェースの通りです。引数に渡すのはkmalloc関数の戻り値です。

void kfree(const void *);

図_kfreeのインターフェース

kfree関数でのメモリ解放では、実質的な処理はslab_free関数にて行います。kfree関数固有な処理は特にありません。

fedora 30からgrubの仕様が変わったのでφ(..)メモメモ

Fedora 30でChanges/BootLoaderSpecByDefaultというプロポーザルがあって、Fedora 30βでは実際にこの仕様になってます。カーネルを弄る人にはちょっと影響があるかなって感じです。

仕様

The Boot Loader Specificationに書かれています

どんな風になったか

boot時のgrubの画面は変化はありません。

f:id:masami256:20190409201409p:plain
grub

変わったのはエントリーの書き方です。今まではgrub.cfgにエントリーを追加していたわけですが、カーネルのエントリー部分が独立した感じです。

root@kerntest:/home/masami# ls /boot/loader/entries/
9575947ef933448ea2005318c989c813-0-rescue.conf                     9575947ef933448ea2005318c989c813-5.0.6-200.fc29.x86_64+debug.conf
9575947ef933448ea2005318c989c813-5.0.5-200.fc29.x86_64.conf        9575947ef933448ea2005318c989c813-5.0.6-300.fc30.x86_64.conf
9575947ef933448ea2005318c989c813-5.0.5-200.fc29.x86_64+debug.conf  9575947ef933448ea2005318c989c813-5.0.6-300.fc30.x86_64+debug.conf
9575947ef933448ea2005318c989c813-5.0.6-200.fc29.x86_64.conf        9575947ef933448ea2005318c989c813-5.1.0-rc4-test+.conf

たとえば、9575947ef933448ea2005318c989c813-5.1.0-rc4-test+.confはこんなファイルです。

title Fedora (5.1.0-rc4-test+) 30 (Thirty)
version 5.1.0-rc4-test+
linux /boot/vmlinuz-5.1.0-rc4-test+
initrd /boot/initramfs-5.1.0-rc4-test+.img
options $kernelopts
id fedora-20190408151310-5.1.0-rc4-test+
grub_users $grub_users
grub_arg --unrestricted
grub_class kernel-

uefiとnon uefi

従来はuefi環境なら/boot/efi/EFI/fedora/grub.cfg、biosなら/boot/grub2/grub.cfgが使われていましたが、BLSでは/boot/loader/entries/にあるファイルを読むことになります。

uefiな環境でも/boot/loader/entriesにファイルが置かれ、

masami@saga:~$ file /sys/firmware/efi/
/sys/firmware/efi/: directory
masami@saga:~$ sudo ls /boot/loader/entries
6ae2390b6ece4d5c86324fadb81d220d-0-rescue.conf                     6ae2390b6ece4d5c86324fadb81d220d-5.0.5-200.fc29.x86_64+debug.conf
6ae2390b6ece4d5c86324fadb81d220d-5.0.4-200.fc29.x86_64.conf        6ae2390b6ece4d5c86324fadb81d220d-5.0.6-300.fc30.x86_64.conf
6ae2390b6ece4d5c86324fadb81d220d-5.0.4-200.fc29.x86_64+debug.conf  6ae2390b6ece4d5c86324fadb81d220d-5.0.6-300.fc30.x86_64+debug.conf
6ae2390b6ece4d5c86324fadb81d220d-5.0.5-200.fc29.x86_64.conf        6ae2390b6ece4d5c86324fadb81d220d-5.1.0-rc4-test+.conf

non uefiな環境でも/boot/loader/entriesにファイルが置かれます。

root@kerntest:/boot/loader/entries# file /sys/firmware/efi                                                                                                                                   
/sys/firmware/efi: cannot open `/sys/firmware/efi' (No such file or directory)
root@kerntest:/boot/loader/entries# ls /boot/loader/entries/
9575947ef933448ea2005318c989c813-0-rescue.conf                     9575947ef933448ea2005318c989c813-5.0.6-200.fc29.x86_64+debug.conf
9575947ef933448ea2005318c989c813-5.0.5-200.fc29.x86_64.conf        9575947ef933448ea2005318c989c813-5.0.6-300.fc30.x86_64.conf
9575947ef933448ea2005318c989c813-5.0.5-200.fc29.x86_64+debug.conf  9575947ef933448ea2005318c989c813-5.0.6-300.fc30.x86_64+debug.conf
9575947ef933448ea2005318c989c813-5.0.6-200.fc29.x86_64.conf        9575947ef933448ea2005318c989c813-5.1.0-rc4-test+.conf

カーネルのmake install

make modules_install -> make installしますよね。この場合、以下のような流れで処理が走ります。

arch/x86/boot/install.sh 
     -> /sbin/installkernel 
         -> /usr/libexec/installkernel/installkernel-bls 
             -> /bin/kernel-install

make_installの挙動

/sbin/installkernelやusr/libexec/installkernel/installkernel-bls 、それに/bin/kernelinstallはfedora 29でもありますが、f29ではBLSには対応していないのでgrub.cfgにエントリを書く方式が使われます。ちなみに、f29には/usr/libexec/installkernel/installkernel-blsと/usr/libexec/installkernel/installkernelの2つがあります(if文の判定的に-blsのほうは使われない)が、f30では-blsの方しかありません。

これらのコマンドが含まれているパッケージは以下の表のようになります。

コマンド コマンドを提供しているパッケージ
/sbin/installkernel grubby
/usr/libexec/installkernel/installkernel-bls grubby
/bin/kernel-install systemd-udev

パッケージ

カーネルの登録

/bin/kernel-installコマンドでできますが、これは/boot/loader/entriesにファイルを作るだけです。make installするのではなくて、コマンド打っていく場合はこんな感じになります。make moduels_installは済んでいるとして、まずはdracutでinitramfsをつくり、

root@kerntest:/home/masami/linux-kernel (ktest)# dracut /boot/initramfs-5.1.0-rc4-test+.img 5.1.0-rc4-test+                                                                                  

/sbin/installkernelでカーネルを/bootにインストールします。

root@kerntest:/home/masami/linux-kernel (ktest)# /sbin/installkernel 5.1.0-rc4-test+ ./arch/x86/boot/bzImage ./System.map 

そうすると、/bin/kernel-installが内部で呼ばれるのでファイルができてます。

root@kerntest:/home/masami/linux-kernel (ktest)# cat /boot/loader/entries/9575947ef933448ea2005318c989c813-5.1.0-rc4-test+.conf
title Fedora (5.1.0-rc4-test+) 30 (Thirty)
version 5.1.0-rc4-test+
linux /boot/vmlinuz-5.1.0-rc4-test+
initrd /boot/initramfs-5.1.0-rc4-test+.img
options $kernelopts
id fedora-20190409141515-5.1.0-rc4-test+
grub_users $grub_users
grub_arg --unrestricted
grub_class kernel-

もしkernel-installを自分で実行するならこうなります。

root@kerntest:/home/masami/linux-kernel (ktest)# kernel-install add 5.1.0-rc4-test+ /boot/vmlinuz-5.1.0-rc4-test+ 

カーネルの削除

/bootにインストールしたカーネルとinitramfs、/boot/loader/entiresのファイルを消したい場合もkernel-installコマンドで出来ます。removeオプションにカーネルの話バージョンを渡せばokです

root@kerntest:/home/masami/linux-kernel (ktest)# kernel-install remove 5.1.0-rc4-test+

カーネルコマンドラインオプション

特定のエントリーに対してオプションを足したり、全部に足したりしたいですよね。Setting kernel command line arguments with Fedora 30 - Fedora Magazineが参考になります。

grubのメニューへの登録・削除

ブート時に/boot/loader/entriesにあるファイルを見てくれるので、ファイルを置けばメニューに追加され、消せばメニューから消えるという楽な仕様です。

grubの設定を確認したい

grubbyの出番です。

全部見たい場合はgrubby --info=ALLでみれます。エントリのindex番号もこれでわかります。

root@kerntest:/home/masami/linux-kernel (ktest)# grubby --info=ALL                                                                                                                            
index=0                                                                                                                                                                                       
kernel="/boot/vmlinuz-5.1.0-rc4-test+"                                                                                                                                                        
args="ro rhgb quiet nokaslr console=ttyS0,115200"                                                                                                                                             
root="UUID=620d1324-eba7-4beb-aab5-dd9975f25690"                                                                                                                                              
initrd="/boot/initramfs-5.1.0-rc4-test+.img"                                                                                                                                                  
title="Fedora (5.1.0-rc4-test+) 30 (Thirty)"                                                                                                                                                  
id="9575947ef933448ea2005318c989c813-5.1.0-rc4-test+"                                                                                                                                         
index=1                                                                                                                                                                                       
kernel="/boot/vmlinuz-5.0.6-300.fc30.x86_64+debug"

その他はgrubby --helpすれば良いと思います。

dynamic_debugはどのようにソースコードの行数、関数名などを読み取っているのか

dynamic_debugのcontrolファイルを読むとこんな感じでファイル名、関数名、そしてpr_debugに渡している文字列などが見れます。これってどうやってんだろ?というのが今回調べたところです。

masami@kerntest:~/pr_debug_test$ sudo cat /sys/kernel/debug/dynamic_debug/control | head -n 10
# filename:lineno [module]function flags format
init/main.c:804 [main]initcall_blacklisted =p "initcall %s blacklisted\012"
init/main.c:771 [main]initcall_blacklist =p "blacklisting initcall %s\012"
init/initramfs.c:477 [initramfs]unpack_to_rootfs =_ "Detected %s compressed data\012"
arch/x86/events/amd/ibs.c:885 [ibs]force_ibs_eilvt_setup =_ "No EILVT entry available\012"
arch/x86/events/amd/ibs.c:856 [ibs]setup_ibs_ctl =_ "No CPU node configured for IBS\012"
arch/x86/events/amd/ibs.c:850 [ibs]setup_ibs_ctl =_ "Failed to setup IBS LVT offset, IBSCTL = 0x%08x\012"
arch/x86/events/intel/pt.c:736 [pt]pt_topa_dump =_ "# entry @%p (%lx sz %u %c%c%c) raw=%16llx\012"
arch/x86/events/intel/pt.c:727 [pt]pt_topa_dump =_ "# table @%p (%016Lx), off %llx size %zx\012"
arch/x86/kernel/tboot.c:98 [tboot]tboot_probe =_ "tboot_size: 0x%x\012"

自前のカーネルモジュールを作ってロードした後にはcontorolファイルに値が追加されます。

/home/masami/pr_debug_test/pr_debug_test.c:20 [pr_debug_test]pr_debug_test_read =_ "debug pr_debug test\012"                                                                                                      

pr_debugの仕組み

さて、include/linux/printk.hでpr_debugがどのように定義されているか見てみます。dynamic_debugが有効な場合はCONFIG_DYNAMIC_DEBUGが定義されているのでdynamic_pr_debugが使われます。

/* If you are writing a driver, please use dev_dbg instead */
#if defined(CONFIG_DYNAMIC_DEBUG)
#include <linux/dynamic_debug.h>

/* dynamic_pr_debug() uses pr_fmt() internally so we don't need it here */
#define pr_debug(fmt, ...) \
   dynamic_pr_debug(fmt, ##__VA_ARGS__)
#elif defined(DEBUG)
#define pr_debug(fmt, ...) \
   printk(KERN_DEBUG pr_fmt(fmt), ##__VA_ARGS__)
#else
#define pr_debug(fmt, ...) \
   no_printk(KERN_DEBUG pr_fmt(fmt), ##__VA_ARGS__)
#endif

dynamic_pr_debug()はこのようなマクロです。

#define dynamic_pr_debug(fmt, ...)              \
do {                             \
   DEFINE_DYNAMIC_DEBUG_METADATA(descriptor, fmt);     \
   if (DYNAMIC_DEBUG_BRANCH(descriptor))         \
       __dynamic_pr_debug(&descriptor, pr_fmt(fmt),    \
                  ##__VA_ARGS__);      \
} while (0)

ここの最初の処理がDEFINE_DYNAMIC_DEBUG_METADATAですのでこれを見てみるとこうなっています

#define DEFINE_DYNAMIC_DEBUG_METADATA(name, fmt) \
   DEFINE_DYNAMIC_DEBUG_METADATA_KEY(name, fmt, .key.dd_key_true, \
                     (STATIC_KEY_TRUE_INIT))

DEFINE_DYNAMIC_DEBUG_METADATA_KEYはこのように展開されます。

#define DEFINE_DYNAMIC_DEBUG_METADATA_KEY(name, fmt, key, init) \
   static struct _ddebug  __aligned(8)          \
   __attribute__((section("__verbose"))) name = {       \
       .modname = KBUILD_MODNAME,          \
       .function = __func__,              \
       .filename = __FILE__,              \
       .format = (fmt),                \
       .lineno = __LINE__,                \
       .flags = _DPRINTK_FLAGS_DEFAULT,        \
       dd_key_init(key, init)              \
   }

関数名、ファイル名、行数などがここで出てきてますね。この処理がdynamic_debugの肝っぽいです。nameという変数を定義して、その変数はverboseというセクションに置くようにしています(このマクロはdo {} while(0)の中で展開されるので変数はこのブロック中にあります)。この変数に値を設定してverboseセクションに変数を置くというのがdynamic_debugのポイントですね。ちなみにdd_key_initはjump_labelを使う場合の初期化用マクロです。

というわけで、dynamic_debugが有効なカーネルではpr_debugを使っているところはそのデータが__verboseセクションに置かれるということが分かりました。

__verboseセクションはinclude/asm-generic/vmlinux.lds.hで確認できます。カーネルのビルド後ならarch/x86/kernel/vmlinux.ldsがリンカのファイルです。

    /* implement dynamic printk debug */                \
    . = ALIGN(8);                          \
    __start___verbose = .;                      \
    KEEP(*(__verbose))                                              \
    __stop___verbose = .;                       \

__verboseセクションからデータを読んでいるところ

dynamic_debugの初期化時は以下のようにstartverboseからstopverboseまでのメモリ領域を読んでいってデータをddebug_add_module()でリストに登録しています。

 if (__start___verbose == __stop___verbose) {
        pr_warn("_ddebug table is empty in a CONFIG_DYNAMIC_DEBUG build\n");
        return 1;
    }
    iter = __start___verbose;
    modname = iter->modname;
    iter_start = iter;
    for (; iter < __stop___verbose; iter++) {
        entries++;
        verbose_bytes += strlen(iter->modname) + strlen(iter->function)
            + strlen(iter->filename) + strlen(iter->format);

        if (strcmp(modname, iter->modname)) {
            modct++;
            ret = ddebug_add_module(iter_start, n, modname);
            if (ret)
                goto out_err;
            n = 0;
            modname = iter->modname;
            iter_start = iter;
        }
        n++;
    }
    ret = ddebug_add_module(iter_start, n, modname);
    if (ret)

モジュールのロード時はload_module()からfind_module_sections()が呼ばれ、ここで__verboseセクションのデータを読み取っています。

 info->debug = section_objs(info, "__verbose",
                   sizeof(*info->debug), &info->num_debug);

そして、load_module()からdynamic_debug_setup()を呼び、ddebug_add_module()を読んでデータを登録する形となっています。

static void dynamic_debug_setup(struct module *mod, struct _ddebug *debug, unsigned int num)
{
    if (!debug)
        return;
#ifdef CONFIG_DYNAMIC_DEBUG
    if (ddebug_add_module(debug, num, mod->name))
        pr_err("dynamic debug error adding module: %s\n",
            debug->modname);
#endif
}

まとめ

dynamic_debugが有効なカーネルではソースコード中のpr_debug記述箇所でファイル行数、ファイル名などの情報を変数として登録し、その変数はverboseセクションに置かれる。dynamic_debugはverboseセクションからデータを読んでpr_debugの利用箇所などの情報を取得することができるという感じでした。

ハッキング・ラボのつくりかた 仮想環境におけるハッカー体験学習

ハッキング・ラボのつくりかた 仮想環境におけるハッカー体験学習

FedoraのRISC-V portを試す

FedoraRISC-VポートはPorting Fedora to RISC-Vで記事になってて気になってたのと、RISC-V原典オープンアーキテクチャのススメを買ったしarch/riscvを読んでみようかな〜ということでqemuで動かしてみました。

(´-`).。oO(FedoraRISC-V関連の情報はArchitectures/RISC-V - Fedora Project Wikiに集まるんだと思います。

qemuのセットアップ

FedoraのドキュメントとしてはArchitectures/RISC-V/Installing - Fedora Project Wikiだと思うんですが、ナイトリービルドのサーバが繋がらなくなってました。ということで、QEMUDocumentation/Platforms/RISCV - QEMUを参考にしました。Fedoraqemurisc-vが使えるのでqemu-system-riscvパッケージをインストールするだけでOKです。

あとはドキュメントの通りでできます。

boot

起動もドキュメントの通りにやればOKです。こんな感じでログインプロンプトがでます。

f:id:masami256:20181231134107p:plain
riscv

初期状態でsshdが動いていて、rootでのsshも可能になっています。

RISC-V原典  オープンアーキテクチャのススメ

RISC-V原典 オープンアーキテクチャのススメ

Linuxカーネルソースに付属のscripts/の探索

この記事はLinux Advent Calendar 2018の12日目の記事です。

カーネルソースコードに含まれるscripts/を探索してみます。カーネルのビルド中で使うようなものもあるし、それ以外の時に使うものもあります。今回は後者の方を探してみましょう。

diffconfig

ファイルはsrcipts/diffconfigです。カーネルの.configのdiffを見やすくするツールです。

普通にdiffを取るとこんな感じです。

masami@saga:~$ diff -u /boot/config-4.19.9-300.fc29.x86_64 /boot/config-4.19.12-301.fc29.x86_64
--- /boot/config-4.19.9-300.fc29.x86_64 2018-12-14 02:44:50.000000000 +0900
+++ /boot/config-4.19.12-301.fc29.x86_64        2018-12-24 11:20:56.000000000 +0900
@@ -1,10 +1,10 @@
 #
 # Automatically generated file; DO NOT EDIT.
-# Linux/x86_64 4.19.9-300.fc29.x86_64 Kernel Configuration
+# Linux/x86_64 4.19.12-301.fc29.x86_64 Kernel Configuration
 #
 
 #
-# Compiler: gcc (GCC) 8.2.1 20181105 (Red Hat 8.2.1-5)
+# Compiler: gcc (GCC) 8.2.1 20181215 (Red Hat 8.2.1-6)
 #
 CONFIG_CC_IS_GCC=y
 CONFIG_GCC_VERSION=80201
@@ -20,7 +20,7 @@
 # CONFIG_COMPILE_TEST is not set
 CONFIG_LOCALVERSION=""
 # CONFIG_LOCALVERSION_AUTO is not set
-CONFIG_BUILD_SALT="4.19.9-300.fc29.x86_64"
+CONFIG_BUILD_SALT="4.19.12-301.fc29.x86_64"
 CONFIG_HAVE_KERNEL_GZIP=y
 CONFIG_HAVE_KERNEL_BZIP2=y
 CONFIG_HAVE_KERNEL_LZMA=y
@@ -93,6 +93,7 @@
 CONFIG_VIRT_CPU_ACCOUNTING=y
 CONFIG_VIRT_CPU_ACCOUNTING_GEN=y
 CONFIG_IRQ_TIME_ACCOUNTING=y
+CONFIG_HAVE_SCHED_AVG_IRQ=y
 CONFIG_BSD_PROCESS_ACCT=y
 CONFIG_BSD_PROCESS_ACCT_V3=y
 CONFIG_TASKSTATS=y
@@ -4655,7 +4656,7 @@
 # CONFIG_DVB_MMAP is not set
 CONFIG_DVB_NET=y
 CONFIG_TTPCI_EEPROM=m
-CONFIG_DVB_MAX_ADAPTERS=8
+CONFIG_DVB_MAX_ADAPTERS=16
 CONFIG_DVB_DYNAMIC_MINORS=y
 # CONFIG_DVB_DEMUX_SECTION_LOSS_LOG is not set
 # CONFIG_DVB_ULE_DEBUG is not set

diifconfigを使うとこのようになります。

masami@saga:~$ ./linux-kernel/scripts/diffconfig /boot/config-4.19.9-300.fc29.x86_64 /boot/config-4.19.12-301.fc29.x86_64
 BUILD_SALT "4.19.9-300.fc29.x86_64" -> "4.19.12-301.fc29.x86_64"
 DVB_MAX_ADAPTERS 8 -> 16
+HAVE_SCHED_AVG_IRQ y

通常のdiffと違って何がどう変わったのかが見やすくなりますね。

objdiff

ファイルはscripts/objdiffです。これはobjectファイル間のdiffを取ります。カーネル以外でも使えます。

例えば、コミット間で以下のようなファイルの変更が有った場合、

masami@saga:~/codes/objdiff-test (test %)$ git diff master a.c
diff --git a/a.c b/a.c
index ab39f2e..a0068b8 100644
--- a/a.c
+++ b/a.c
@@ -7,7 +7,7 @@ struct test {
 int main(int argc, char **argv)
 {
        struct test t = {
-               .s = "hello",
+               .s = "hello\n",
        };
 
        printf("%s\n", t.s);

こんな感じになります。

masami@saga:~/codes/objdiff-test (test %)$ ~/linux-kernel/scripts/objdiff diff
which: no colordiff in (/usr/share/Modules/bin:/usr/lib64/ccache:/usr/local/bin:/usr/local/sbin:/usr/bin:/usr/sbin:/home/masami/bin:/home/masami/.gem/ruby/2.2.0/bin:/home/masami/go/bin:/home/masami/ltp:/home/masami/ltp/bin:/home/masami/.local/bin:/home/masami/bin:/home/masami/bin:/home/masami/.gem/ruby/2.2.0/bin:/home/masami/go/bin:/home/masami/ltp:/home/masami/ltp/bin:/home/masami/.local/bin:/home/masami/bin)
diff -Nurd /home/masami/codes/objdiff-test/.tmp_objdiff/b5b839d/a.dis /home/masami/codes/objdiff-test/.tmp_objdiff/374a3c2/a.dis
--- /home/masami/codes/objdiff-test/.tmp_objdiff/b5b839d/a.dis  2018-12-30 23:36:28.063937470 +0900
+++ /home/masami/codes/objdiff-test/.tmp_objdiff/374a3c2/a.dis  2018-12-30 23:37:13.345071254 +0900
@@ -10,8 +10,8 @@
 :      48 81 ec 10 01 00 00    sub    $0x110,%rsp
 :      89 bd fc fe ff ff       mov    %edi,-0x104(%rbp)
 :      48 89 b5 f0 fe ff ff    mov    %rsi,-0x110(%rbp)
-:      48 b8 68 65 6c 6c 6f    movabs $0x6f6c6c6568,%rax
-:      00 00 00 
+:      48 b8 68 65 6c 6c 6f    movabs $0xa6f6c6c6568,%rax
+:      0a 00 00 
 :      ba 00 00 00 00          mov    $0x0,%edx
 :      48 89 85 00 ff ff ff    mov    %rax,-0x100(%rbp)
 :      48 89 95 08 ff ff ff    mov    %rdx,-0xf8(%rbp)
Binary files /home/masami/codes/objdiff-test/.tmp_objdiff/b5b839d/a.stripped and /home/masami/codes/objdiff-test/.tmp_objdiff/374a3c2/a.stripped differ

bootgraph.pl

ファイルはscripts/bootgraph.plです。 カーネルの起動時にprintk.time=1とinitcall_debugをカーネルコマンドラインオプションに付けて起動する必要があります。dmesgの経過時間のところから何にどれだけ時間を使ったかを見る感じですね。

全体像はこんな感じです。

f:id:masami256:20181230224632p:plain
boot graph

拡大するとこうなってます。

f:id:masami256:20181230225324p:plain
bootgraph2

show_delta

ファイルはscripts/show_deltaです。 これも実行時間の計測ですね。前回のprintkの時刻から今回までの時間を表示してくれます。

('[0.280670 < 0.003444 >] pci 0000:00:06.0: reg 0x20: [io  0xc0a0-0xc0bf]\n',)
('[0.281820 < 0.001150 >] pci 0000:00:06.1: [8086:2935] type 00 class 0x0c0300\n',)
('[0.306576 < 0.000002 >] initcall xen_setup_shutdown_event+0x0/0x30 returned -19 after 0 usecs\n',)
('[0.307587 < 0.000000 >] calling  power_supply_class_init+0x0/0x40 @ 1\n',)
('[0.309641 < 0.000000 >] NetLabel:  domain hash size = 128\n',)
('[0.309642 < 0.000001 >] NetLabel:  protocols = UNLABELED CIPSOv4 CALIPSO\n',)
('[0.359181 < 0.000003 >] initcall proc_version_init+0x0/0x22 returned 0 after 1 usecs\n',)
('[0.359707 < 0.000090 >] pnp 00:02: Plug and Play ACPI device, IDs PNP0f13 (active)\n',)
('[0.365880 < 0.000002 >] calling  thermal_init+0x0/0xe1 @ 1\n',)
('[0.370580 < 0.000040 >] initcall intel_pmc_ipc_init+0x0/0x5f returned 0 after 37 usecs\n',)
('[0.370701 < 0.000006 >] initcall ipv4_offload_init+0x0/0x74 returned 0 after 0 usecs\n',)
('[0.890552 < 0.000001 >] calling  crypto_module_init+0x0/0x11 @ 1\n',)

faddr2line

ファイルはscripts/faddr2lineです。oopsにある関数名+オフセット/関数サイズからそれがどのファイルの何行目かを調べることができます。自作したカーネルモジュールでは使えなかったけど。。

こんな風に使えます。

masami@kerntest:~/a2l$ ~/linux-kernel/scripts/faddr2line ~/linux-kernel/vmlinux do_init_module+0x22/0x210
do_init_module+0x22/0x210:
do_init_module at kernel/module.c:3436

decode_stacktrace.sh

ファイルはscripts/decode_stacktrace.shです。カーネルoopsを読み込んでデコードしてくれます。使い方はlinux-3.16で入ったdecode_stacktrace.shを試してみる - φ(・・*)ゞ ウーン カーネルとか弄ったりのメモを参照してください。

decodecode

ファイルはscripts/decodecodeです。decode_stacktrace.shと似たような感じです。使い方はlinuxカーネルデバッグめも decodecodeでoopsの機械語列を逆アセンブル - φ(・・*)ゞ ウーン カーネルとか弄ったりのメモを参照してください。

ver_linux

ファイルはscripts/ver_linuxです。カーネル、ビルドとか諸々のツール類の情報表示します。

masami@saga:~/linux-kernel (test %)$ ./scripts/ver_linux 
If some fields are empty or look unusual you may have an old version.
Compare to the current minimal requirements in Documentation/Changes.

Linux saga 4.19.12-301.fc29.x86_64 #1 SMP Mon Dec 24 01:58:57 UTC 2018 x86_64 x86_64 x86_64 GNU/Linux

GNU Make                4.2.1
Binutils                2.31.1
Util-linux              2.32.1
Mount                   2.32.1
Module-init-tools       25
E2fsprogs               1.44.3
Xfsprogs                4.17.0
Quota-tools             4.04
PPP                     2.4.7
Nfs-utils               2.3.3
Linux C Library         2.28
Dynamic linker (ldd)    2.28
Linux C++ Library       6.0.25
Procps                  3.3.15
Net-tools               2.10
Kbd                     2.0.4
Console-tools           2.0.4
Sh-utils                8.30
Udev                    239
Wireless-tools          29
Modules Loaded          binfmt_misc bridge coretemp crc32c_intel crc32_pclmul crct10dif_pclmul devlink drm drm_kms_helper e1000e ebtable_filter ebtable_nat ebtables fat fuse ghash_clmulni_intel hid_logitech_dj hid_logitech_hidpp i2c_algo_bit i2c_i801 intel_cstate intel_powerclamp intel_rapl intel_rapl_perf intel_uncore intel_wmi_thunderbolt ip6table_filter ip6table_mangle ip6table_nat ip6table_raw ip6_tables ip6table_security ip6t_REJECT ip6t_rpfilter ip_set iptable_mangle iptable_nat iptable_raw iptable_security ipt_MASQUERADE irqbypass iTCO_vendor_support iTCO_wdt kvm kvm_intel libcrc32c llc lpc_ich mei mei_me mxm_wmi nf_conntrack nf_conntrack_broadcast nf_conntrack_netbios_ns nf_defrag_ipv4 nf_defrag_ipv6 nf_nat nf_nat_ipv4 nf_nat_ipv6 nfnetlink nf_reject_ipv6 nouveau pcc_cpufreq snd snd_hda_codec snd_hda_codec_ca0132 snd_hda_codec_hdmi snd_hda_core snd_hda_intel snd_hwdep snd_pcm snd_seq snd_seq_device snd_seq_dummy snd_timer soundcore stp sunrpc tap ttm tun uas usb_storage vfat vhost vhost_net video wmi x86_pkg_temp_thermal xfs xt_CHECKSUM xt_conntrack xt_CT

spelling.txt

ファイルはscripts/spelling.txtです。このファイルは単体で使うと言うか、checkpatch.plでスペルチェックに使っています。ファイルの書式はこんな感じで、左側がよく有る間違いで右側が正解です。

abov||above

まとめ

他にもスクリプトは色々有ります。たとえばscripts/gdbカーネルgdbデバッグする時のスクリプトで.gdbinitで add-auto-load-safe-path /home/masami/codes/slmb/scripts/gdb/vmlinux-gdb.py という感じに読み込ませます。

ということで、なんとなくscripts/を探索してみました( ´ー`)フゥー...

ハッキング・ラボのつくりかた 仮想環境におけるハッカー体験学習

ハッキング・ラボのつくりかた 仮想環境におけるハッカー体験学習