弄りながらなんとなく学ぶLinuxのスラブアロケーター

この記事はLinux Advent Calendar 2017の1日目の記事です。Linuxのスラブアロケーター仕組みを多少弄りながら学んでみます。 スラブアロケーターとはなんぞやというところはSlab allocation - Wikipediaを参照してください。スラブアロケーターはSolaris5.4で最初に実装されたようです。 Linuxカーネルプログラミングで動的にメモリを確保するのに使用するkmalloc()もスラブアロケーターを使用しています。

目次

Linuxのスラブアロケーター

Linuxには3種類の実装があり、カーネルのコンフィグでどれを使用するかを選択します。

f:id:masami256:20171201002553p:plain

SLAB

Solaris型のスラブアローケーターのようです。SLUBが出てくるまではこちらがLinuxのスラブアロケーターとして選択されてました。詳解Linuxカーネルで説明されているスラブアロケーターもこれです。openSUSEはこれを使ってたと思います。

SLUB

FedoraやArch Linuxなんかではこれが使われています。スラブに使うpageはcpu毎に割り当てたり、fast pathの場合はlocklessにスラブオブジェクトを確保できるような作りになってます。SLUBに関してはslub の検索結果 - φ(・・*)ゞ ウーン カーネルとか弄ったりのメモ過去にいくつか記事を書きました。

SLOB

K&R型のアロケーターです。SLABやSLUBと違い、スラブオブジェクトは固定長になっていなくて、キューが3つあり、256bytes以下ならsmallリストから必要なサイズを切り出して返す形になってます。cgroupsのmemcgは非対応です。

各スラブアロケーターのまとめ

さくっと説明しましたが詳しい違いはSlab allocators in the Linux Kernel: SLAB, SLOB, SLUBにわかりやすくまとまっています。 そういえば、UNIX V7のmallocの実装が UNIXカーネルソースツアー!で見ることができます。

実装

3種類の実装がありますが、全てが独立しているわけではなくて共通部分と個別の実装部に分かれています。 共通で使われるコードは以下の3ファイルにあります。

例えばスラブを新規に作るkmem_cache_create()はslab_common.cに実装があり、実装に依存しない共通的な処理はこちらで行い、スラブアロケーター固有の処理は各スラブアロケーターで実装している__kmem_cache_create()で呼ぶ流れになっています。

kmem_cache構造体がスラブアロケーターの主要なデータ構造です。どの実装でも使用する変数はありますが、実装としてはスラブアロケーター毎に分かれています。例えば、kmem_cache構造体のlistという変数はkmem_cache_create()で作成したスラブを管理するためのリストでこれはどのスラブアロケーターでも必要な変数となってます。

struct list_head list;

kmem_cache構造体はこの他にもどの実装でも共通で必要になる変数があります。

また、page構造体にもスラブアロケーター用のデータ構造が入っています。連結リストに使うlru変数や、次の空きスラブオブジェクトを指すfreelist変数などがあります。

SLUBとSLOBのfreelist変数はこのような感じになります。

f:id:masami256:20171124171302p:plain 図_slubとslobのfreelist

SLUBの場合はスラブオブジェクトは固定長なので次の空きスラブオブジェクトを指します。ページ内に空きスラブオブジェクトが無い場合はNULLを指します。SLOBの場合はリストから必要なサイズを切り出すので、freelist変数は次の利用可能な位置を指すことになります。フラグメンテーションが無ければここを起点としてサイズを切り出せますし、フラグメンテーションがある場合は確保したいサイズがある領域をページ内で探す必要があります。

スラブ用のページ

SetPageSlab()を使ってスラブ用のページとしてセットします。その他、必要に応じてフラグを設定するのですが、SetPageSlab()は必須です。SLOBの場合は、空き領域があるページにはSetPageSlobFree()でフラグをセットし、空き領域がなくなったページにはClearPageSlobFree()でフラグを外します。 SLOBでは使用していませんが、page構造体からkmem_cache構造体にアクセスできるようにslab_cache変数があります。SLUBでkfree()が呼ばれた場合はこのslab_cache変数から、該当のkmem_cache構造体にアクセスできます。

弄ってみる

ここではSLOBをベースに新しいスラブアロケーターを足す形で弄ってみたいと思います。名前はslmbとします。今のところ大雑把にリストを各cpuに持たせるようにしたのと、リストの数を増やした程度の実装となってます。

最低限必要な準備

新しく追加する場合、自前のスラブアロケーターをカーネルのコンフィグレーションで設定できる必要があるのでinit/Kconfigに設定を追加します。あ、ベースのカーネルは4.14.0です。

diff --git a/init/Kconfig b/init/Kconfig
index 3c1faaa2af4a..2d1ac7f952fb 100644
--- a/init/Kconfig
+++ b/init/Kconfig
@@ -1551,6 +1551,12 @@ config SLOB
           allocator. SLOB is generally more space efficient but
           does not perform as well on large systems.
 
+config SLMB
+       depends on EXPERT
+       bool "SLMB (SLOB with multi processer support Allocator)"
+       help
+          SLMB is based on SLOB.
+
 endchoice
 
 config SLAB_MERGE_DEFAULT

アロケータの実装はmm/slmb.cに書くのでMakefileも変更が必要です。

diff --git a/mm/Makefile b/mm/Makefile
index 4659b93cba43..4d39ad32e641 100644
--- a/mm/Makefile
+++ b/mm/Makefile
@@ -12,6 +12,7 @@ KASAN_SANITIZE_slub.o := n
 # free pages, or a task is migrated between nodes.
 KCOV_INSTRUMENT_slab_common.o := n
 KCOV_INSTRUMENT_slob.o := n
+KCOV_INSTRUMENT_slmb.o := n
 KCOV_INSTRUMENT_slab.o := n
 KCOV_INSTRUMENT_slub.o := n
 KCOV_INSTRUMENT_page_alloc.o := n
@@ -65,6 +66,7 @@ obj-$(CONFIG_NUMA)    += mempolicy.o
 obj-$(CONFIG_SPARSEMEM)        += sparse.o
 obj-$(CONFIG_SPARSEMEM_VMEMMAP) += sparse-vmemmap.o
 obj-$(CONFIG_SLOB) += slob.o
+obj-$(CONFIG_SLMB) += slmb.o
 obj-$(CONFIG_MMU_NOTIFIER) += mmu_notifier.o
 obj-$(CONFIG_KSM) += ksm.o
 obj-$(CONFIG_PAGE_POISONING) += page_poison.o

kmem_cache構造体も自前で用意するのでinclude/linux/slmb_deh.hというファイルを作成し、そこで実装を書いていきます。

あとはmm/slab.hでslmb_def.hを読み込ませれば完了です。

+#ifdef CONFIG_SLMB
+#include <linux/slmb_def.h>
+#endif
+

最低限必要なのはこれくらいです。後はガンガン実装していけます( ´∀`)bグッ!t もし、memcgの対応はとりあえず止めとくという場合は以下のような条件の部分に自分のアロケータの定義も入れていきます。

#ifndef CONFIG_SLOB

↑と↓です。

#if defined(CONFIG_MEMCG) && !defined(CONFIG_SLOB)

今回最終的に変更したファイルはこのようになってます。ほぼmemcgに対応しないための変更です。

masami@miko:~/linux-kernel (slmb %=)$ git diff origin/master --stat
 include/linux/list_lru.h   |   4 +-
 include/linux/memcontrol.h |   6 +-
 include/linux/sched.h      |   2 +-
 include/linux/slab.h       |  14 +--
 include/linux/slmb_def.h   |  28 ++++++
 init/Kconfig               |   6 ++
 mm/Makefile                |   2 +
 mm/list_lru.c              |  12 +--
 mm/memcontrol.c            |  16 ++--
 mm/slab.h                  |  18 ++--
 mm/slab_common.c           |  14 +--
 mm/slmb.c                  | 729 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 12 files changed, 810 insertions(+), 41 deletions(-)

実装開始

SLMBはSLOBをベースとしているので解説も基本的にはSLOBベースとなります。

データ構造

雑にリストを増やしてます。 SLOBの場合は256バイト以下用のfree_slob_small、1024バイト以下用のfree_slob_medium、1024バイト以上用のfree_slob_largeがあります。SLMBでは以下のようにサイズを分けてみました。

+enum SLMB_LIST_SIZE_TYPES {
+   SLMB_SLAB_BREAK_8 = 0,
+   SLMB_SLAB_BREAK_16,
+   SLMB_SLAB_BREAK_24,
+   SLMB_SLAB_BREAK_32,
+   SLMB_SLAB_BREAK_40,
+   SLMB_SLAB_BREAK_48,
+   SLMB_SLAB_BREAK_56,
+   SLMB_SLAB_BREAK_64,
+   SLMB_SLAB_BREAK_96,
+   SLMB_SLAB_BREAK_128,
+   SLMB_SLAB_BREAK_256,
+   SLMB_SLAB_BREAK_1024,
+};

リストはpage構造体を要素とするリストです。page構造体のlru変数がリストに使われます。これはSLUBなどでも同じです。

SLMBではリストをこのようにして、

+struct slmb_slab_lists {
+   struct slmb_slab_list lists[SLMB_NUM_LIST_TYPES];
+};

以下のようにしてcpu毎にリストを持たせてます。

+DEFINE_PER_CPU(struct slmb_slab_lists, slab_lists);

SLOBでは空き領域がなくなったスラブ(ページ)はリストから外します。SLUBも空きオブジェクトがなくなったスラブを管理するリストはありません。

ロック

ロックについてはSLOBと同様にspin_lock_irqsave()でローカルCPUからの割り込みを止める形です。SLUBはfastpathの場合はlocklessで動作します。SLUBで空きオブジェクトが存在する場合、先に示した図_slubとslobのfreelistのようにSLUBの場合はfreelistが指す先を更新すれば良いだけです。SLUBではこの更新にはcmpxchg命令を使った更新方法を採用していてアトミックにポインタを書き換えることができます。 SLOBの場合はページ内から適切な空き要素を探さないといけないのでその手段が使えないですね。

初期化

スラブアロケーターは初期化してから使えるようになります。初期化の関数は2つあります。

  • kmem_cache_init()
  • kmem_cache_init_late()

初期化のメイン処理はkmem_cache_init()です。SLUB、SLOBはkmem_cache_init_late()での処理は特にありません。SLABは処理があります。 kmem_cache_init()ではスラブアロケーターを使用可能にするため、kmem_cache構造体用のスラブを作成します。

また、スラブアロケーターは状態を持っていて、slab_stateという変数で状態管理されています。mm/slab.hに状態が定義されています。

enum slab_state {
    DOWN,           /* No slab functionality yet */
    PARTIAL,        /* SLUB: kmem_cache_node available */
    PARTIAL_NODE,       /* SLAB: kmalloc size for node struct available */
    UP,         /* Slab caches usable but not all extras yet */
    FULL            /* Everything is working */
};

SLOBの場合はkmem_cache_init()の段階ではUP、kmem_cache_init_late()でFULLに変わります。kmem_cache_init_late()は状態をFULLに変えているだけなので、実質kmem_cache_init()でFULL状態になっても問題ない気がします。SLUBの場合はslab_sysfs_init内でFULLに変更しています。

初期化時はリストの初期化を行うのでfor_each_possible_cpu()でcpu毎にループして初期化をしています。

+    for_each_possible_cpu(cpu) {
+       struct slmb_slab_lists *lists = &per_cpu(slab_lists, cpu);
+       pr_info("%s: SLMB: intialize slob list cpu %d\n", __func__, cpu);
+       init_slmb_slab_lists(lists);
+   }

最初はfor_each_online_cpu()で良いだろうと思っていたのですが、kmem_cache_init()が呼ばれた時点ではonlineなcpuが1つしかなかったのでcpu0のリストしか初期化できず、あとになってcpu1とかのリストにアクセスしようとしたところでOOPSが発生しました。そんなわけで、onlineではなくて、カーネルが認識したcpu数でループとしてます。

+    for (i = 0; i < SLMB_NUM_LIST_TYPES; i++) {
+       struct slmb_slab_list *p = &lists->lists[i];
+       INIT_LIST_HEAD(&p->list);
+   }

SLMB_NUM_LIST_TYPESはスラブオブジェクトのサイズの数です。SLOBは256バイト以下、1024バイト以下、1024バイト以上の3つのリストで実装していたのでそれを増やしています。

kmem_cache_init()ではkmem_cache構造体用のスラブを作ります。SLOBでは以下のように作っています

struct kmem_cache kmem_cache_boot = {
    .name = "kmem_cache",
    .size = sizeof(struct kmem_cache),
    .flags = SLAB_PANIC,
    .align = ARCH_KMALLOC_MINALIGN,
};

SLMBはSLOBそのままの作りになってます。このkmem_cache_boot変数をkmem_cacheという変数(mm/slab_common.cにあり)に代入します。 kmem_cache_bootの内容としては、スラブの名前はkmem_cache、サイズはkmem_cache構造体のサイズ、メモリ確保できない場合はパニックさせるって感じですね。

SLUBの場合はkmem_cache_int()mm/slub.cで設定して、kmem_cache変数に代入しています。

kmem_cacheはkmem_cache_create()でスラブを作る時のスラブとして使用します。

初期化処理はSLOBは大した処理はありませんが、SLUBやSLABだとkmem_cache構造体のためのスラブキャッシュを作るなど、SLOBよりも行う処理は多いです。

kmem_cache_create()

スラブの作成はkmem_cache_create()で行います。ここで指定されたサイズのスラブオブジェクトを作るわけです。SLUBやSLABの場合は実際に専用のkmme_cache構造体を作ります。スラブのマージ機能が有効な場合はちょっと違いますが。スラブのマージ機能は、作ろうとしたスラブと同じサイズのスラブがあれば新たにスラブを作るのではなくてそのスラブを使うようにします。

/sys/kernel/slab/でスラブの情報が見れるのですが、例えば、user_namespace用のスラブは488バイトのサイズで、t-0000488というスラブを参照してます。これはuser_namespaceという名前がt-0000488へのエイリアスとなってる感じですね。

$ ls -la /sys/kernel/slab/
lrwxrwxrwx.   1 root root 0 Nov 18 14:55 kmalloc-1024 -> :t-0001024/
lrwxrwxrwx.   1 root root 0 Nov 18 14:55 kmalloc-128 -> :t-0000128/
lrwxrwxrwx.   1 root root 0 Nov 18 14:55 kmalloc-16 -> :t-0000016/
lrwxrwxrwx.   1 root root 0 Nov 18 14:55 kmalloc-192 -> :t-0000192/
lrwxrwxrwx.   1 root root 0 Nov 18 14:55 kmalloc-2048 -> :t-0002048/
~
lrwxrwxrwx.   1 root root 0 Nov 18 14:55 user_namespace -> :t-0000488/
lrwxrwxrwx.   1 root root 0 Nov 18 14:55 vm_area_struct -> :tA-0000192/
lrwxrwxrwx.   1 root root 0 Nov 18 14:55 xfrm_dst_cache -> :t-0000448/
lrwxrwxrwx.   1 root root 0 Nov 18 14:55 zswap_entry -> :t-0000056/

SLOBの場合はスラブの管理方法が3個のリストで行われていて、サイズごとの管理は実質していません。そのため、新しいkmem_cache構造体にkmem_cache_alloc()時に使用するgfpフラグの設定だけ行っています。サイズごとに管理してないので/sys/kernel/slab/もありません。また、/proc/slabinfoもありません。

スラブオブジェクトのアロケート

スラブオブジェクトのアロケートはkmem_cache_alloc()ですね。SLOBの場合はslob_alloc_node()がメインの処理です。ちなみに、kmem_cache_alloc()はNUMAのノード指定なし、kmem_cache_alloc_node()は指定したノードからアロケートするとなります。

SLOBではアロケートするサイズがPAGE_SIZE以上の場合はBuddyシステムを使ってページを確保し、先頭アドレスをそのまま返します。なので、スラブオブジェクトとして管理しない形です。

アロケートするサイズが小さければ事前に用意したリストを使って必要なサイズを切り出して返します。

kmem_cache_alloc()はSLOBがSLAB・SLUBと大きく違うところですね。SLAB・SLUBはスラブオブジェクトのサイズごとに専用のスラブがありますが、SLOB・SLMBはリストから必要なサイズを切り出して返します。SLMBではSLOBよりは細かくリストを分けてますが、確保するサイズによっては切り出しが必要です。 SLOBでのスラブオブジェクト取得は以下のような流れで行います。

kmem_cache_alloc()
  -> slob_alloc_node()
    -> slob_alloc()
      -> slob_page_alloc() *1
      -> slob_new_pages()
      -> slob_page_alloc()

*1のslob_page_alloc()でスラブオブジェクトが取得できなかった場合は、その後のslob_new_pages()でスラブ用に新しいページを確保し、再度slob_page_alloc()でスラブオブジェクトの取得を行います。スラブオブジェクトが取得できなかったらページを確保するという動きはSLUBなんかも同じです。SLUBの場合はページを確保する前に別のNUMAノードを見に行ったりとかありますが。

SLOBの場合はリストにあるページから必要なサイズの空き領域を探す形です。ページ内に空き領域が見つかった場合はそのページが次回の呼び出し時に最初に探索するページになるようにしたりしてます。また、リストの操作中はspin_lock_irqsave()でロックを取っています。

SLUBの場合は空きオブジェクトがあればそのオブジェクトを返すのと、freelistが次の空きオブジェクトを指すようにします。

スラブオブジェクトのfree

kfree()やkmem_cache_free()の処理です。SLOBの場合、PAGE_SIZE以上の大きさの場合はBuddyシステムを使ってページを確保していたので、freeの場合もBuddyシステムにページを返します。そうでない場合は、SLOB側で処理します。kfree()の場合、解放するオブジェクトのアドレスしかわかりません。そのため、ここからスラブオブジェクトが所属するリストやkmem_cache構造体にアクセスする必要があります。と言っても、SLOBの場合はkmem_cache構造体は必要なくて、リストさえ分かればOKです。

アドレスからそのアドレスのpage構造体にアクセスするのにはvirt_to_page()を使います。SLUBの場合はvirt_to_head_page()を使っていますが、基本的には同じようなものです。

リストにはどうやってアクセスするかというとkfree()でサイズを計算していて、そこで出した結果から該当のリストを見つけることができます。kmem_cache_free()、kfree()どちらも解放処理の実装はslob_free()で行います。SLOBの場合は使用中のオブジェクトの前にある空きスラブオブジェクトにサイズや次の空きスラブオブジェクトまでのoffsetが書かれています。

実験

hackbenchをこんな感じで動かしてみます。実行環境はkvm上のfedora 26でcpuは4個です。ここではmenuconfigでスラブアロケーターをSLUBやSLOBに変えたというだけで、カーネルの設定、バージョンなどはSLMBと同じです。

#!/bin/bash 
uname -a
echo "./hackbench 10 process 20000"
./hackbench 10 process 20000
echo "./hackbench 10 process 20000"
./hackbench 10 process 20000
echo "./hackbench 10 process 20000"
./hackbench 10 process 20000
echo "./hackbench 1 process 20000"
./hackbench 1 process 20000
echo "./hackbench 1 process 20000"
./hackbench 1 process 20000
echo "./hackbench 1 process 20000"
./hackbench 1 process 20000

これで、SLOB、SLMB、SLUBのベンチマークを取ってみます。

./hackbench 10 process 20000 の結果

test slob slmb slub
1 121.498 97.809 39.611
2 122.532 96.766 58.401
3 118.115 95.485 50.799

./hackbench 1 process 20000 の結果

test slob slmb slub
1 9.591 8.539 8.097
2 9.656 8.433 7.580
3 9.594 8.782 7.830

SLUBはやっぱ速いですね(゚д゚)!でも、SLMBも性能は良くなってます。

SLOBとSLMBでcpuを1個にして比較するとこうなります。

test slob slmb
10 process 20000 211.408 199.935
10 process 20000 232.415 218.371
10 process 20000 220.579 218.583
1 process 20000 10.107 19.218
1 process 20000 10.070 19.597
1 process 20000 10.521 19.472

1cpuで1プロセスだと逆に遅くなると。

ボトルネック

SLMBで./hackbench 10 process 20000を4cpuの環境で実行した時の遅い人たち。

slmb svg

f:id:masami256:20171125142207p:plain

結局のところ、リストを扱う時のロックに行き着きますね。

f:id:masami256:20171125142310p:plain

f:id:masami256:20171125142340p:plain

f:id:masami256:20171125142354p:plain

SLUBの場合はこうなります。

slub svg

f:id:masami256:20171125143213p:plain

SLUBもロックに時間を取られる傾向はありますが、よく見てみると、wake_up_sync_key()で時間がかかっていて、SLUBに関連する処理に時間がかかっているのではないというのが見えます。

f:id:masami256:20171125143527p:plain

この後

(´-`).。oO(やっぱりスクラッチから作る?もしくは、より改造していくか

プログラミング言語C 第2版 ANSI規格準拠

プログラミング言語C 第2版 ANSI規格準拠

cgroup: プロセスが所属しているmemoryサブシステムが使用しているメモリの使用量を見るツール

pythonでなんとなく。

show memory usage in memcg.

memoryサブシステムにtest1を作ってメモリの上限を100Mで設定。

root@saga:/sys/fs/cgroup/memory/test1# cat memory.limit_in_bytes         
104857600
root@saga:/sys/fs/cgroup/memory/test1# cat memory.memsw.limit_in_bytes
104857600
root@saga:/sys/fs/cgroup/memory/test1#      
                                             

pid 4473はtest1に所属するbashのプロセス。このシェルから1MiBのmalloc()を繰り返すプロセスを実行する。 ツールはpid 4473を指定して実行

masami@saga:~/codes/memcgstat$ ./memcgstat.py -p 4473 -c 1000

1秒おきにメモリの使用量が表示されて、最後のほうで使用量が一気に減ったのはメモリが足りなくなってプロセスが殺されたため。

f:id:masami256:20170924202125p:plain

( ´ー`)フゥー...

CentOS 7実践ガイド (impress top gear)

CentOS 7実践ガイド (impress top gear)

memory cgroupとpageのLRUめも

カーネルは4.1系です。

include/linux/mm_inline.hにLRUへの登録・削除処理の実装があります。

static __always_inline void add_page_to_lru_list(struct page *page,
                struct lruvec *lruvec, enum lru_list lru)
{
    int nr_pages = hpage_nr_pages(page);
    mem_cgroup_update_lru_size(lruvec, lru, nr_pages);
    list_add(&page->lru, &lruvec->lists[lru]);
    __mod_zone_page_state(lruvec_zone(lruvec), NR_LRU_BASE + lru, nr_pages);
}

static __always_inline void del_page_from_lru_list(struct page *page,
                struct lruvec *lruvec, enum lru_list lru)
{
    int nr_pages = hpage_nr_pages(page);
    mem_cgroup_update_lru_size(lruvec, lru, -nr_pages);
    list_del(&page->lru);
    __mod_zone_page_state(lruvec_zone(lruvec), NR_LRU_BASE + lru, -nr_pages);
}

LRUに関連する構造体はこれらです。

これらの構造体の関連はこのようになります。

f:id:masami256:20170817002559p:plain

LRUはadd_page_to_lru_list()見ての通りlruvec構造体のlists配列のどこかにつながる感じです。

page構造体からlruvec構造体はmem_cgroup_page_lruvec()を使うことで取得できます。

lruvec = mem_cgroup_page_lruvec(page, page_zone(page));

mem_cgroup_page_lruvec()の処理のうち、最初にmemcgを使わない場合の処理がありますが、ここではmemcgは有効の場合を見てるので飛ばして、本筋的なところはこの辺です。

 memcg = page->mem_cgroup;
    /*
    * Swapcache readahead pages are added to the LRU - and
    * possibly migrated - before they are charged.
    */
    if (!memcg)
        memcg = root_mem_cgroup;

    mz = mem_cgroup_page_zoneinfo(memcg, page);
    lruvec = &mz->lruvec;

page構造体の構造体のメンバ変数mem_cgroupがpageが所属するmemcgのmem_cgroup構造体にアクセスできます。そしてmem_cgroup_page_zoneinfo()を使ってmem_cgroup_per_zone構造体を取得します。mem_cgroup_page_zoneinfo()はこんな関数です。

static struct mem_cgroup_per_zone *
mem_cgroup_page_zoneinfo(struct mem_cgroup *memcg, struct page *page)
{
    int nid = page_to_nid(page);
    int zid = page_zonenum(page);

    return &memcg->nodeinfo[nid]->zoneinfo[zid];
}

nidがNUMAのノード番号でzidがZONE_NORMALとかのインデックスですね(きっと)。page構造体からそのmemcg使っているNUMAノードとZONEを取得してるはずです。これでlruvec構造体は取得できました。

では、実際にadd_page_to_lru_list()を呼んでるところを見てみると、例えばこのunlock_page_lru()があります。

static void unlock_page_lru(struct page *page, int isolated)
{
    struct zone *zone = page_zone(page);

    if (isolated) {
        struct lruvec *lruvec;

        lruvec = mem_cgroup_page_lruvec(page, zone);
        VM_BUG_ON_PAGE(PageLRU(page), page);
        SetPageLRU(page);
        add_page_to_lru_list(page, lruvec, page_lru(page));
    }
    spin_unlock_irq(&zone->lru_lock);
}

この関数だと対象のzoneはpage構造体から取得しています。そして、mem_cgroup_page_lruvec()でlruvec構造体を取得してからのadd_page_to_lru_list()でlruvecのlistsのリストにpageを繋いでいます。listsのインデックスはpage_lru()で決めていますね。

static inline enum lru_list page_lru_base_type(struct page *page)
{
    if (page_is_file_cache(page))
        return LRU_INACTIVE_FILE;
    return LRU_INACTIVE_ANON;
}

ファイルキャッシュとして使うかどうかでindexが変わるようです。

というわけで、LRUはmemcg単位に存在していて、NUMAノードやZONEによって違うlruvecが使われます。そして、pageの用途によっても違うリストに繋がるというのがわかりました。 ( ´ー`)フゥー...

Linuxのsystem call fuzzer「syzkaller」めも

Linuxシステムコールのファジングツールとしてsyzkallerというのがあって、これはコードカバレッジを見つつ入力を変えていってくれるというファジングするツールです。 試してみたのでどんな感じなのかを簡単にめも。

まず、ツール自体はgolangで書かれているのでgoの実行環境が必要です。あとテスト対象のカーネル側でコードカバレッジを利用可能になっている必要があります。この辺はsyzkallerのドキュメントに書かれています。 ツールはビルドして行います。make一発で良いのですが、デフォルトだとstatic linkなバイナリを作ります。うちだと-lpthreadと-lcのところでそんなライブラリはないとか怒られてしまったので、動的リンクにしました。ホストとゲストのディストリビューションが同じならライブラリのABIの違いとかないですし、これで良いでしょう。

ファジングはVMを使って行います。自分はqemuでやりました。qemuを使う場合、Linuxがインストールされた環境が必要です。自分はホストとゲストはともにfedora 25でやりました。ゲストのイメージを作るのは面倒だったので既存のVMを/var/lib/libvirt/imagesからコピーしてきて使った感じです。

syzkallerはwebのコンソールもあって現在の状況とかクラッシュしたときの情報、コードカバレッジを見ることができます。簡単な使い方はsyz-managerというツールに設定ファイルを引数として渡して実行するパターンです。今回はこれでやりました。この場合、ファジングを行うバイナリはscpで転送して、ゲスト側でそのバイナリで実行するようです。

では、実際に試してみます。と言っても実際にクラッシュしたところを見たいので特定の条件でクラッシュするようにパッチを当てます。 unshare(2)でCLONE_NEWUTSとCLONE_NEWPIDがセットされている場合にpanic()を呼ぶようにしたカーネルで実験します。

masami@saga:~/codes/linux-stable (4.11.5 *)$ git diff
diff --git a/kernel/fork.c b/kernel/fork.c
index 4f7151d..4e3fe8a 100644
--- a/kernel/fork.c
+++ b/kernel/fork.c
@@ -2273,6 +2273,8 @@ SYSCALL_DEFINE1(unshare, unsigned long, unshare_flags)
        int do_sysvsem = 0;
        int err;

+       if (unshare_flags & CLONE_NEWUTS && unshare_flags & CLONE_NEWPID)
+               panic("syscall fuzzer found it!");
        /*
         * If unsharing a user namespace must also unshare the thread group
         * and unshare the filesystem root and working directories.
masami@saga:~/codes/linux-stable (4.11.5 *)$

次に設定ファイルです。これはこのようにしました。だいたい見たとおりの設定内容ですね。vmのところはqemuで使う設定です。自分が使っているイメージはパーティションが2つあるのでcmdlineでrootファイルシステムを/dev/sda2に設定しています。unshare(2)以外をテストする必要はないのでenable_syscallsでunshare(2)だけを指定しています。

masami@saga:~/go/src/github.com/google/syzkaller (master %=)$ cat ktest-config.cfg 
{
        "http": "localhost:56741",
        "workdir": "/home/masami/tmp/syzkaller_tmp",
        "vmlinux": "/home/masami/tmp/vmlinux",
        "image": "/home/masami/ktest/linux-ktest.img",
        "syzkaller": "/home/masami/go/src/github.com/google/syzkaller",
        "enable_syscalls": [ "unshare" ],
        "procs": 4,
        "type": "qemu",
        "leak": false,
        "vm": {
                "count": 4,
                "cpu": 2,
                "mem": 2048,
                "kernel": "/home/masami/tmp/bzImage",
                "initrd": "/home/masami/tmp/initramfs-ktest.img",
                "sshkey": "/home/masami/.ssh/id_rsa_nopass",
                "cmdline": "root=/dev/sda2"
        }
}

これで以下のようにコマンドを実行するとファジング開始します。

masami@saga:~/go/src/github.com/google/syzkaller (master %=)$ ./bin/syz-manager -config ./ktest-config.cfg

実行開始してwebコンソールを開くとこんなふうな画面が見れます。

f:id:masami256:20170618190630p:plain 

Crashesのところにcrashした情報が表示されます。下のほうにはコードカバレッジのリンクがあります。

コードカバレッジはこんな画面で赤いところがクラッシュした行、実行した行には /* covered */ というコメントが付きます。

f:id:masami256:20170618191013p:plain

OOPSが出たときの情報はこのように見えます。これはunshare(2)に渡した引数とその結果っぽいですね。

f:id:masami256:20170618191241p:plain

Crashesのリンクをクリックするとhas C reproというリンクがあります。

f:id:masami256:20170618191448p:plain

これをクリックすると以下のようにコードが見れます。これはバグの再現プログラムです。

f:id:masami256:20170618191339p:plain

このプログラムをホストがゲストでコンパイルして実行するとoopsします。qemuを起動させていおいて、

masami@saga:~/tmp$ qemu-system-x86_64 -enable-kvm -kernel ./bzImage -initrd ./initramfs-ktest.img -hda ~/ktest/linux-ktest.img -append root=/dev/sda2 -m 2048 -net nic -net user,host=10.0.2.10,hostfwd=tcp::45243-:22

別のターミナルでコードをコンパイルしてscpで転送して、sshでログインして実行。a.outの実行後に文字が出てないのはoopsが出てるからです。

masami@saga:~$ vi test.c
masami@saga:~$ gcc test.c 
masami@saga:~$ scp -i ~/.ssh/id_rsa_nopass -P 45243 a.out root@localhost:~/.
a.out                                                                                                                                                                            100% 8536     9.2MB/s   00:00    
masami@saga:~$ ssh -i ~/.ssh/id_rsa_nopass -p 45243 root@localhost
Last login: Sun Jun 18 19:19:06 2017 from 10.0.2.10
[root@linux-ktest ~]# ./a.out

oopsでてます。

f:id:masami256:20170618192320p:plain

syzkallerはコードカバレッジを見て入力を変えていくのと、再現プログラムも残してくれるので良いですね。

サイバーセキュリティテスト完全ガイド ~Kali Linuxによるペネトレーションテスト~

サイバーセキュリティテスト完全ガイド ~Kali Linuxによるペネトレーションテスト~

cgroupでタスクの移動処理

cgroupでタスクの移動というのは例えば、/sys/fs/cgroup/cpusetにfooとbarというディレクトリがあって、fooに所属しているタスクをbarに移動するとかです。

タスクの移動といってもcgroupのコアと呼ばれてるcgroupの基本機能側での処理とサブシステム(cpusetとかmemoryといったもの)側での処理があり、移動そのものはコア側で行います。移動に伴ってサブシステム側での処理もありますが、今日はコアの部分だけみます。カーネルは4.1系です。タスク・プロセス・スレッドの単語の使い分けが雑なので適宜コードに合わせていい感じに読んでくださいm( )m

移動処理を行うのはcgroup_attach_task()です。移動するタスク(プロセス)がスレッドグループリーダーならそのプロセスから作成されたスレッドもすべて移動になります。

static int cgroup_attach_task(struct cgroup *dst_cgrp,
                  struct task_struct *leader, bool threadgroup)
{
    LIST_HEAD(preloaded_csets);
    struct task_struct *task;
    int ret;

    /* look up all src csets */
    down_read(&css_set_rwsem);
    rcu_read_lock();
    task = leader;
    do {
        cgroup_migrate_add_src(task_css_set(task), dst_cgrp,
                       &preloaded_csets);
        if (!threadgroup)
            break;
    } while_each_thread(leader, task);
    rcu_read_unlock();
    up_read(&css_set_rwsem);

    /* prepare dst csets and commit */
    ret = cgroup_migrate_prepare_dst(dst_cgrp, &preloaded_csets);
    if (!ret)
        ret = cgroup_migrate(dst_cgrp, leader, threadgroup);

    cgroup_migrate_finish(&preloaded_csets);
    return ret;
}

cgroup_attach_task()での移動は3段階あって、準備・移動・後処理です。cgroup_migrate_add_src()cgroup_migrate_prepare_dst()は準備の処理で、移動元のcgroupの処理と移動先のcgroupの処理です。 移動させるのはcgroup_migrate()で行い、最後にcgroup_migrate_finish()で終了処理となっています。

タスクが所属するcgroupは基本的に連結リストで管理されているので移動もリストの移動を行うというのが基本的なところです。

cgroup_migrate_add_src()の主な処理はこれくらいです。

        src_cset->mg_src_cgrp = src_cgrp;
        get_css_set(src_cset);
        list_add(&src_cset->mg_preload_node, preloaded_csets);

src_csetは第一引数で、移動するタスクのtask_struct構造体から取得したcss_set構造体で、src_cgrpは移動元のcgroupです。css_set構造体のmg_src_cgrpに移動元のcgroupを設定するのが1つで、もう一つはpreloaded_csetsリストにsrc_csetを繋いでます。preloaded_csetsはこの関数の第3引数です。cgroup_attach_task()で定義・初期化したローカル変数です。

次のcgroup_migrate_prepare_dst()ですが、こちらは主要な処理は以下のところです。

 /* look up the dst cset for each src cset and link it to src */
    list_for_each_entry_safe(src_cset, tmp_cset, preloaded_csets, mg_preload_node) {
        struct css_set *dst_cset;

        dst_cset = find_css_set(src_cset,
                    dst_cgrp ?: src_cset->dfl_cgrp);
        if (!dst_cset)
            goto err;

        WARN_ON_ONCE(src_cset->mg_dst_cset || dst_cset->mg_dst_cset);

        /*
        * If src cset equals dst, it's noop.  Drop the src.
        * cgroup_migrate() will skip the cset too.  Note that we
        * can't handle src == dst as some nodes are used by both.
        */
        if (src_cset == dst_cset) {
            src_cset->mg_src_cgrp = NULL;
            list_del_init(&src_cset->mg_preload_node);
            put_css_set(src_cset);
            put_css_set(dst_cset);
            continue;
        }

        src_cset->mg_dst_cset = dst_cset;

        if (list_empty(&dst_cset->mg_preload_node))
            list_add(&dst_cset->mg_preload_node, &csets);
        else
            put_css_set(dst_cset);
    }

    list_splice_tail(&csets, preloaded_csets);

この関数はpreloaded_csetsにcsetsを結合するというのが最終的な処理で、csetsにつなぐデータをlist_for_each_entry_safeのループで作っている感じです。find_css_set()も多少処理はあるのですが、基本的には移動先のcss_set構造体を取得して、それに含まれているリストのmg_preload_nodeが空なら、dst_csetをcstesにつなぎ、最後にlist_splice_tail()でcsetsをpreloaded_csetsにつなぎます。移動先のcss_set構造体って簡単にまとめてしまってるけど、この取得処理での肝となるfund_css_set()は別途読まないといけないな。

この移動準備系の処理でpreloaded_csetsに移動元と移動先のcss_set構造体が登録されます。そして、cgroup_migrate()で実際の移動を行います。ここではpreloaded_csetsは使いません。

     ret = cgroup_migrate(dst_cgrp, leader, threadgroup);

cgroup_migrate()は長いので分割しつつ見ていきます。

static int cgroup_migrate(struct cgroup *cgrp, struct task_struct *leader,
              bool threadgroup)
{
    struct cgroup_taskset tset = {
        .src_csets  = LIST_HEAD_INIT(tset.src_csets),
        .dst_csets  = LIST_HEAD_INIT(tset.dst_csets),
        .csets      = &tset.src_csets,
    };
    struct cgroup_subsys_state *css, *failed_css = NULL;
    struct css_set *cset, *tmp_cset;
    struct task_struct *task, *tmp_task;
    int i, ret;

最初は変数の定義ですが、この関数で重要な変数としてcgroup_tasksetがあります。これは移動元・移動先のcss_set構造体を登録したりします。

 /*
    * Prevent freeing of tasks while we take a snapshot. Tasks that are
    * already PF_EXITING could be freed from underneath us unless we
    * take an rcu_read_lock.
    */
    down_write(&css_set_rwsem);
    rcu_read_lock();
    task = leader;
    do {
        /* @task either already exited or can't exit until the end */
        if (task->flags & PF_EXITING)
            goto next;

        /* leave @task alone if post_fork() hasn't linked it yet */
        if (list_empty(&task->cg_list))
            goto next;

        cset = task_css_set(task);
        if (!cset->mg_src_cgrp)
            goto next;

        /*
        * cgroup_taskset_first() must always return the leader.
        * Take care to avoid disturbing the ordering.
        */
        list_move_tail(&task->cg_list, &cset->mg_tasks);
        if (list_empty(&cset->mg_node))
            list_add_tail(&cset->mg_node, &tset.src_csets);
        if (list_empty(&cset->mg_dst_cset->mg_node))
            list_move_tail(&cset->mg_dst_cset->mg_node,
                       &tset.dst_csets);
    next:
        if (!threadgroup)
            break;
    } while_each_thread(leader, task);
    rcu_read_unlock();
    up_write(&css_set_rwsem);

ここではプロセス内の全スレッドを対象に処理してます。処理内容はtask(スレッド)のcss_set構造体を取得して、list_move_tail()でtask_structをcsetのmg_tasksリストに最後に登録します。 cset->mg_dst_csetはcgroup_migrate_prepare_dst()のループ内で以下のように設定されています。

 src_cset->mg_dst_cset = dst_cset;

ここまででtsetのsrc_csetsとdst_testsリストの設定が完了です。

 /* methods shouldn't be called if no task is actually migrating */
    if (list_empty(&tset.src_csets))
        return 0;

この段階で移動元のcss_setが存在しなければ移動対象がないってことなのでここで終了ですね。

 /* check that we can legitimately attach to the cgroup */
    for_each_e_css(css, i, cgrp) {
        if (css->ss->can_attach) {
            ret = css->ss->can_attach(css, &tset);
            if (ret) {
                failed_css = css;
                goto out_cancel_attach;
            }
        }
    }

つぎはサブシステム側での移動処理を行います。can_attach()なので移動実行ではなくて、移動できるか?というチェックですね。ここは今回のコードリーディング対象外です。

 /*
    * Now that we're guaranteed success, proceed to move all tasks to
    * the new cgroup.  There are no failure cases after here, so this
    * is the commit point.
    */
    down_write(&css_set_rwsem);
    list_for_each_entry(cset, &tset.src_csets, mg_node) {
        list_for_each_entry_safe(task, tmp_task, &cset->mg_tasks, cg_list)
            cgroup_task_migrate(cset->mg_src_cgrp, task,
                        cset->mg_dst_cset);
    }
    up_write(&css_set_rwsem);

cgroup_task_migrate()で移動させていきます。引数は3個ですが、1番目の引数は使ってないので2番目のtaskとcset->mg_dst_csetが実際に使われます。cgroup_task_migrate()の処理はロック取ったりとか参照カウンタの処理を除くとこれくらいです。

 rcu_assign_pointer(tsk->cgroups, new_cset);

    /*
    * Use move_tail so that cgroup_taskset_first() still returns the
    * leader after migration.  This works because cgroup_migrate()
    * ensures that the dst_cset of the leader is the first on the
    * tset's dst_csets list.
    */
    list_move_tail(&tsk->cg_list, &new_cset->mg_tasks);

プロセスのtask_structにあるcgroups変数にnew_csetを設定します。new_csetはcset->mg_dst_csetなので、単純に書くと↓ですね。

task->cgroups = cset->mg_dst_cset;

そして、list_move_tail()でtask_struct構造体が今つながっているリストからnew_csetのmb_tasksリストに移動します。ここでの移動先のcss_setはcset->mg_dst_csetで最初の方のループでlist_move_tail()を使ってデータを移動したリスト(tset.dst_csets)です。

 /*
    * Migration is committed, all target tasks are now on dst_csets.
    * Nothing is sensitive to fork() after this point.  Notify
    * controllers that migration is complete.
    */
    tset.csets = &tset.dst_csets;

testのcsets変数は最初移動元のcsetを設定しましたが、ここで移動先のcsetを設定します。これでコア側での移動が完了です。

 for_each_e_css(css, i, cgrp)
        if (css->ss->attach)
            css->ss->attach(css, &tset);

サブシステム側で移動を行います。

 ret = 0;
    goto out_release_tset;

ここまでの処理で特にエラーがなければout_release_tsetラベルに飛びます。

out_cancel_attach:
    for_each_e_css(css, i, cgrp) {
        if (css == failed_css)
            break;
        if (css->ss->cancel_attach)
            css->ss->cancel_attach(css, &tset);
    }

エラーがあった場合はこのラベルに飛んできて移動のキャンセルを行います。キャンセル処理は主にサブシステム側になります。

out_release_tset:
    down_write(&css_set_rwsem);
    list_splice_init(&tset.dst_csets, &tset.src_csets);
    list_for_each_entry_safe(cset, tmp_cset, &tset.src_csets, mg_node) {
        list_splice_tail_init(&cset->mg_tasks, &cset->tasks);
        list_del_init(&cset->mg_node);
    }
    up_write(&css_set_rwsem);
    return ret;
}

最後はロックを解除したり、使用したリストを初期化します。

そして、cgroup_migrate()が終わり、次にcgroup_migrate()を実行します。

 list_for_each_entry_safe(cset, tmp_cset, preloaded_csets, mg_preload_node) {
        cset->mg_src_cgrp = NULL;
        cset->mg_dst_cset = NULL;
        list_del_init(&cset->mg_preload_node);
        put_css_set_locked(cset);
    }

こちらも処理としては使用したリストのクリアで、preloaded_csetsを綺麗にして終了します。

φ(..)メモメモ gccの3項演算子の拡張メモ

Linuxカーネルのコードを読んでいて?:なんて演算子が使われていて???と思ったのでめも。

gcc拡張機能で3項演算子の拡張としてUsing the GNU Compiler Collection (GCC): Conditionalsなんてのがありました。

以下のようなコードを

z = x ? x : y

このように置き換えることが可能

z = x ?: y

実際使ってみるとこんな感じに。

masami@saga:~$ cat a.c
#include <stdio.h>

int main(int argc, char **argv)
{
        char *p = argc > 1 ? argv[1] : NULL;

        printf("%s\n", p ?: "hoge");

        return 0;
}
masami@saga:~$ ./a.out 
hoge
masami@saga:~$ ./a.out foo
foo

( ´ー`)フゥー...

/proc/<pid>にファイルをおいてデータを読みたい

/proc/<pid>/ にファイルを作ってデータを読めると便利なときがあったりするのでめも。

今回はcgroup.cで処理を実装して、その関数をcgroup.hにて宣言。fs/proc/base.cでcgroupのほうに追加した関数を登録する形です。/proc/<pid> にあるファイルに対する関数はここで登録されてます。 差分はこのような感じです。

git masami@kerntest:~/linux-kernel (ktest *=)$ git diff --stat
 fs/proc/base.c         | 2 ++
 include/linux/cgroup.h | 2 ++
 kernel/cgroup/cgroup.c | 7 +++++++
 3 files changed, 11 insertions(+)

最小限のコードはこのようになります。

masami@kerntest:~/linux-kernel (ktest *=)$ git diff
diff --git a/fs/proc/base.c b/fs/proc/base.c
index 45f6bf6..db65c94 100644
--- a/fs/proc/base.c
+++ b/fs/proc/base.c
@@ -2909,6 +2909,7 @@ static const struct pid_entry tgid_base_stuff[] = {
 #endif
 #ifdef CONFIG_CGROUPS
        ONE("cgroup",  S_IRUGO, proc_cgroup_show),
+       ONE("cg_list",  S_IRUGO, proc_cg_list_show),
 #endif
        ONE("oom_score",  S_IRUGO, proc_oom_score),
        REG("oom_adj",    S_IRUGO|S_IWUSR, proc_oom_adj_operations),
@@ -3301,6 +3302,7 @@ static const struct pid_entry tid_base_stuff[] = {
 #endif
 #ifdef CONFIG_CGROUPS
        ONE("cgroup",  S_IRUGO, proc_cgroup_show),
+       ONE("cg_list",  S_IRUGO, proc_cg_list_show),
 #endif
        ONE("oom_score", S_IRUGO, proc_oom_score),
        REG("oom_adj",   S_IRUGO|S_IWUSR, proc_oom_adj_operations),
diff --git a/include/linux/cgroup.h b/include/linux/cgroup.h
index ed2573e..41ae434 100644
--- a/include/linux/cgroup.h
+++ b/include/linux/cgroup.h
@@ -101,6 +101,8 @@ int task_cgroup_path(struct task_struct *task, char *buf, size_t buflen);
 int cgroupstats_build(struct cgroupstats *stats, struct dentry *dentry);
 int proc_cgroup_show(struct seq_file *m, struct pid_namespace *ns,
                     struct pid *pid, struct task_struct *tsk);
+int proc_cg_list_show(struct seq_file *m, struct pid_namespace *ns,
+                     struct pid *pid, struct task_struct *task);

 void cgroup_fork(struct task_struct *p);
 extern int cgroup_can_fork(struct task_struct *p);
diff --git a/kernel/cgroup/cgroup.c b/kernel/cgroup/cgroup.c
index c3c9a0e..ab5094f 100644
--- a/kernel/cgroup/cgroup.c
+++ b/kernel/cgroup/cgroup.c
@@ -4600,6 +4600,13 @@ static int __init cgroup_wq_init(void)
 }
 core_initcall(cgroup_wq_init);

+int proc_cg_list_show(struct seq_file *m, struct pid_namespace *ns,
+                     struct pid *pid, struct task_struct *task)
+{
+       seq_printf(m, "%s\n", __func__);
+       return 0;
+}
+
 /*
  * proc_cgroup_show()
  *  - Print task's cgroup paths into seq_file, one line for each hierarchy

実行するとこうなります。

masami@kerntest:~$ ls /proc/self/
./     autogroup  cgroup      comm             cwd@     fd/      io       loginuid      maps       mounts      ns/        oom_score      patch_state  root@      sessionid  stack  status   timers         wchan
../    auxv       clear_refs  coredump_filter  environ  fdinfo/  latency  make-it-fail  mem        mountstats  numa_maps  oom_score_adj  personality  sched      setgroups  stat   syscall  timerslack_ns
attr/  cg_list    cmdline     cpuset           exe@     gid_map  limits   map_files/    mountinfo  net/        oom_adj    pagemap        projid_map   schedstat  smaps      statm  task/    uid_map
masami@kerntest:~$ cat /proc/self/cg_list
proc_cg_list_show
masami@kerntest:~$