Linux4.14.12(x86_64)のPage Global Directoryの設定を見てみる

Linux 4.14でプロセスをforkした時のPage Global Directoryの設定を見てみます。読むカーネルはv4.14.12です。 前にLinux x86_64のPaging:Page Global Directory辺りの扱いを見てみる - φ(・・*)ゞ ウーン カーネルとか弄ったりのメモ書いてたけど、最新のカーネルで調べてみたので。

プロセス生成時のPage Global Directoryの設定の流れ

Page Global Directory(pgd)の設定はpgd_alloc()で行います。fork()からの流れはこのような形。

_do_fork()
  -> copy_process()
    -> copy_mm()
      ->  dup_mm()
        -> mm_init()
          -> mm_alloc_pgd()
            -> pgd_alloc()

pgd_alloc()

pgd_alloc()はこのような関数です。

pgd_t *pgd_alloc(struct mm_struct *mm)
{
    pgd_t *pgd;
    pmd_t *pmds[PREALLOCATED_PMDS];

    pgd = _pgd_alloc();

    if (pgd == NULL)
        goto out;

    mm->pgd = pgd;

    if (preallocate_pmds(mm, pmds) != 0)
        goto out_free_pgd;

    if (paravirt_pgd_alloc(mm) != 0)
        goto out_free_pmds;

    /*
    * Make sure that pre-populating the pmds is atomic with
    * respect to anything walking the pgd_list, so that they
    * never see a partially populated pgd.
    */
    spin_lock(&pgd_lock);

    pgd_ctor(mm, pgd);
    pgd_prepopulate_pmd(mm, pgd, pmds);

    spin_unlock(&pgd_lock);

    return pgd;

out_free_pmds:
    free_pmds(mm, pmds);
out_free_pgd:
    _pgd_free(pgd);
out:
    return NULL;
}

pgd_tとpmd_tの変数が宣言されてますね。pgdのほうは良いとして、pmdsですが配列となっています。サイズはPREALLOCATED_PMDSですが、これはPAEの設定で変わります。x86_64環境だとPAEは無いのでPREALLOCATED_PMDSは0と定義されています。

/* No need to prepopulate any pagetable entries in non-PAE modes. */
#define PREALLOCATED_PMDS  0

では、処理を見ていきます。

 pgd = _pgd_alloc();

最初に_pgd_alloc()を呼んでメモリを確保してます。_pgd_alloc()もPAEの有無で処理が変わるのですが、x86_64の場合は__get_free_pages()でページを確保します。 ここで最近話題のKPTIに関する分岐がありました。KPTIが有効なら2ページ確保してます。

#ifdef CONFIG_PAGE_TABLE_ISOLATION
/*
 * Instead of one PGD, we acquire two PGDs.  Being order-1, it is
 * both 8k in size and 8k-aligned.  That lets us just flip bit 12
 * in a pointer to swap between the two 4k halves.
 */
#define PGD_ALLOCATION_ORDER 1
#else
#define PGD_ALLOCATION_ORDER 0
#endif

ページが確保できたら mm->pgd にpgdをセットします。

次にpreallocate_pmds()でpmd用のメモリを確保します。

 if (preallocate_pmds(mm, pmds) != 0)
        goto out_free_pgd;

preallocate_pmds()x86_64環境だと特にやることはありません。ここもPAEが有効な場合に主要な処理があるだけです。x86_64だと引数で渡されたmm構造体がinit_mmだったらアカウンティングをしないという設定をする程度です。

 if (mm == &init_mm)
        gfp &= ~__GFP_ACCOUNT;

ここは特になにもありません。

 if (paravirt_pgd_alloc(mm) != 0)
        goto out_free_pmds;

関数はarch/x86/include/asm/paravirt.hで定義されていて、

static inline int paravirt_pgd_alloc(struct mm_struct *mm)
{
    return PVOP_CALL1(int, pv_mmu_ops.pgd_alloc, mm);
}

pgd_allocは__paravirt_pgd_alloc()がセットされてます。

 .pgd_alloc = __paravirt_pgd_alloc,
    .pgd_free = paravirt_nop,

__paravirt_pgd_alloc()は単に0を返してます。

static inline int  __paravirt_pgd_alloc(struct mm_struct *mm) { return 0; }

こちらはコンストラクタ的な処理です。pgd_ctor()は後ほど。

 pgd_ctor(mm, pgd);

最後はpmdの設定です。

 pgd_prepopulate_pmd(mm, pgd, pmds);

x86_64の場合、pmd用のメモリ確保もしていないのでpgd_prepopulate_pmd()も特に処理はありません。 こんな感じになってます。

 if (PREALLOCATED_PMDS == 0) /* Work around gcc-3.4.x bug */
        return;

ここまででエラーが無ければ終了です。

pgd_ctor()

pgd_ctor()はコンストラクタ的な処理ですね。

static void pgd_ctor(struct mm_struct *mm, pgd_t *pgd)
{
    /* If the pgd points to a shared pagetable level (either the
      ptes in non-PAE, or shared PMD in PAE), then just copy the
      references from swapper_pg_dir. */
    if (CONFIG_PGTABLE_LEVELS == 2 ||
        (CONFIG_PGTABLE_LEVELS == 3 && SHARED_KERNEL_PMD) ||
        CONFIG_PGTABLE_LEVELS >= 4) {
        clone_pgd_range(pgd + KERNEL_PGD_BOUNDARY,
                swapper_pg_dir + KERNEL_PGD_BOUNDARY,
                KERNEL_PGD_PTRS);
    }

    /* list required to sync kernel mapping updates */
    if (!SHARED_KERNEL_PMD) {
        pgd_set_mm(pgd, mm);
        pgd_list_add(pgd);
    }
}

最初のif文は、通常のx86_64環境ならCONFIG_PGTABLE_LEVELSは4だと思います。5段階ページングとか有効にしてたら5でしょうけども。それはともかく、clone_pgd_range()の実行があります。 clone_pgd_range()はこのような関数です。

static inline void clone_pgd_range(pgd_t *dst, pgd_t *src, int count)
{
    memcpy(dst, src, count * sizeof(pgd_t));
#ifdef CONFIG_PAGE_TABLE_ISOLATION
    if (!static_cpu_has(X86_FEATURE_PTI))
        return;
    /* Clone the user space pgd as well */
    memcpy(kernel_to_user_pgdp(dst), kernel_to_user_pgdp(src),
           count * sizeof(pgd_t));
#endif
}

最初にアドレスswapper_pg_dir + KERNEL_PGD_BOUNDARYからKERNEL_PGD_PTRS*sizeof(pgd_t)バイトをアドレスpgd + KERNEL_PGD_BOUNDARYにコピーします。KERNEL_PGD_BOUNDARYについてはKASLRの有効無効で値が変わります。KERNEL_PGD_PTRSは以下の用になっていて、PTRS_PER_PGDは512です。

#define KERNEL_PGD_PTRS     (PTRS_PER_PGD - KERNEL_PGD_BOUNDARY)

static_cpu_has()のところはCPUがPTIをサポートしてないならここで終了です。このフラグについては/arch/x86/include/asm/cpufeatures.hに定義があります。 そうでなければユーザー空間のマッピングもコピーしてます。

2018/01/10追記 このフラグはpti_check_boottime_disable()の処理でPTIが無効にセットされていなければ、setup_force_cpu_cap()を使ってセットしてます。

 setup_force_cpu_cap(X86_FEATURE_PTI);

pgd_ctor()に戻って、SHARED_KERNEL_PMDの値をチェックして0ならpgd_set_mm()pgd_list_add()の実行があります。4段階のページングを使ってるならSHARED_KERNEL_PMDの値は0です。

pgd_set_mm()はpgd変数のアドレスに該当するpage構造体のindexメンバ変数にmm構造体をセットしてます。

static void pgd_set_mm(pgd_t *pgd, struct mm_struct *mm)
{
    BUILD_BUG_ON(sizeof(virt_to_page(pgd)->index) < sizeof(mm));
    virt_to_page(pgd)->index = (pgoff_t)mm;
}

pgd_list_add()のほうはarch/x86/mm/fault.cにあるpgd_listにpgdのpage構造体を追加してます。

static inline void pgd_list_add(pgd_t *pgd)
{
    struct page *page = virt_to_page(pgd);

    list_add(&page->lru, &pgd_list);
}

ここまででpgdの設定が終わりです。

最後に

pgd_alloc()はx86_64の場合はpmdの設定はなくて、pgdの設定のみ行います。また、KPTIが入ったことでこの機能関連の処理が追加されていました。

この後、kernel/fork.cのmm_init()まで戻ってinit_new_context()を実行します。init_new_context()はまたの機会に。

( ´ー`)フゥー...

動くメカニズムを図解&実験! Linux超入門 (My Linuxシリーズ)

動くメカニズムを図解&実験! Linux超入門 (My Linuxシリーズ)

Linuxカーネルで一回だけ実行する関数を作る

この記事はLinux Advent Calendar 2017の9日目の記事です。 なんとなくlib/を見ていたらonce.cなんてファイルを見つけて、一度だけ実行したいという時に使う関数を見つけたのでその機能についてのきじになります。

使い方

まず使い方をザクっと見てみましょう。使用するのはDO_ONCEマクロです。 実装はこんな感じです。

サンプルコード

#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/debugfs.h>
#include <asm/uaccess.h>
#include <linux/err.h>
#include <linux/once.h>

MODULE_DESCRIPTION("once test");
MODULE_AUTHOR("masami256");
MODULE_LICENSE("GPL");

static struct dentry *once_test_file;

static void once_test_run_once(int *count)
{
    (*count)++;
    pr_info("%s called\n", __func__);
}

static ssize_t once_test_write(struct file *filp, const char __user *buf, size_t len, loff_t *ppos)
{
    static int called_cnt = 0;

    DO_ONCE(once_test_run_once, &called_cnt);
    pr_info("called_cnt: %d\n", called_cnt);
    return strnlen_user(buf, 8);
}

static struct file_operations once_test_fops = {
    .owner = THIS_MODULE,
    .write = once_test_write,
};

static int once_test_init(void)
{
    once_test_file = debugfs_create_file("once_test", 0200,
                   NULL, NULL,
                   &once_test_fops);

    if (IS_ERR(once_test_file)) {
        WARN_ON(1);
        return PTR_ERR(once_test_file);
    }

    pr_info("setup done.\n");
    return 0;
}

static void once_test_cleanup(void)
{
    pr_info("cleanup\n");
    debugfs_remove(once_test_file);
}

module_init(once_test_init);
module_exit(once_test_cleanup);

使い方的には/sys/kernel/debug/once_testというファイルに何か書き込むとpr_info()で変数の値を表示します。ここで、初回に限りcalled_cnt変数の値をインクリメントしています。

実行結果

実行するとこうなります。想像通りですね( ´∀`)bグッ!

[  998.207119] once_test: setup done.
[ 1004.934061] once_test: once_test_run_once called
[ 1004.934069] once_test: called_cnt: 1
[ 1015.924961] once_test: called_cnt: 1

DO_ONCEの実装

一度だけ実行したい関数と、その関数用の引数を任意の数だけ受け取ります。先のサンプルコードは1個しか引数を受け取ってませんが。

        #define DO_ONCE(func, ...)                           \
   ({                                     \
       bool ___ret = false;                       \
       static bool ___done = false;                  \
       static struct static_key ___once_key = STATIC_KEY_INIT_TRUE; \
       if (static_key_true(&___once_key)) {                 \
           unsigned long ___flags;                    \
           ___ret = __do_once_start(&___done, &___flags);       \
           if (unlikely(___ret)) {                  \
               func(__VA_ARGS__);               \
               __do_once_done(&___done, &___once_key,       \
                          &___flags);           \
           }                          \
       }                              \
       ___ret;                              \
   })

実行したかのチェックは単純なstatic変数のdoneです。で、doneがfalseならまだ未実行なので引数で渡された関数を実行します。funcの実行前後で_do_once_start()__do_once_done()の呼び出しがあります。 do_once_start()のほうはfuncの実行前にロックを取るだけです。__do_once_done()のほうは多少の処理があります。こちらは後ほど。

WARN*ONCEマクロとの違い

自分は一度だけ実行ってことで思い浮かぶのはWARN*ONCE系のマクロだったりします。DO_ONCEマクロはそれらとは違っています。WARN*ONCE系のマクロの実装、カウンタのクリアに関する実装はこちらに書いたので興味のある方は読んでみてくださいm( )m

qiita.com

一度だけというところをチェックする変数の置き場所もWARN*ONCE系マクロとは違っています。WARN*ONCE系マクロの場合、チェックに使用する変数は.dataセクションのstart_onceとend_onceの間に変数が置かれますが、DO_ONCEの場合は単なるstatic変数です。あと、チェックの変数が、doneとonce_keyの2種類があります。doneの方はdo_once_start()でロックを取る時にというか、ロックを取ったあとに値をチェックして、trueだったら即ロックを解放します。その場合、do_once_start()はfalseを返すのでfuncの実行はありません。funcを実行した場合は、do_once_done()でdoneをtrueに変えます。_once_keyのほうは__do_once_done()で使用します。

__do_once_done()の処理

この関数の処理は3つあります。1つはdone変数の値をtrueに変える。2つ目はdo_once_start()で取ったロックの解放です。そして3つ目が__once_keyの処理です。と言っても難しいことはなくて、static_key構造体のenabled変数の値を1から0にするだけです。

___once_keyはこのように初期化されていました。

static struct static_key ___once_key = STATIC_KEY_INIT_TRUE;

これはinclude/linux/jump_label.hを見るとこのようになっています。

#define STATIC_KEY_INIT_TRUE                    \
   { .enabled = { 1 },                    \
     { .entries = (void *)JUMP_TYPE_TRUE } }

そして、値を変えているのはlib/once.cのこの部分です。

static_key_slow_dec(work->key);

static_key_slow_dec()はinclude/linux/jump_label.hにある関数で、enabledメンバ変数の値を減らしているだけです。

static inline void static_key_slow_dec(struct static_key *key)
{
    STATIC_KEY_CHECK_USE();
    atomic_dec(&key->enabled);
}

と、やっていることは簡単です。ただ、do_once_done()で_once_keyの値を変えるのではなくて、値の変更をする関数をワークキューに突っ込んで、スケジューラによってワーカーが実行されたら値を変えるというようになってます。

string_get_size()でサイズのお手軽表示

この記事はLinux Advent Calendar 2017の22日目の記事です。 カーネルのコードを書いていてサイズを表示したい時にstring_get_size()を使うとお手軽に2進接頭辞(KiBとか)とSI接頭辞(KBとか)を使ったサイズの文字列を作ることができます。

関数のプロトタイプはこうです。

void string_get_size(u64 size, u64 blk_size, enum string_size_units units,
             char *buf, int len);

sizeとblk_sizeは使い分けが有ります。バイト数を扱いたい場合はsizeにバイト数、blk_sizeには1を指定します。ブロックデバイスやページなどを扱う場合などはそのサイズをblk_sizeに指定して、それがいくつあるかをsizeで指定します。

たとえば、1024という数字を2進接頭辞にするならsizeには1024、blk_sizeは1をセットします。1ページのサイズが4096バイトで1ページのバイト数を2進接頭辞にするならsizeは1、blk_sizeは4096という感じです。 unitsは2進接頭辞かSI接頭辞を指定します。bufに渡した変数に結果が入ります。lenはbufで利用可能なバイト数ですね。

こんな感じで使えます。

#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/string_helpers.h>

MODULE_DESCRIPTION("size test");
MODULE_AUTHOR("masami256");
MODULE_LICENSE("GPL");

struct size_test_data {
    u64 size;
    u64 blk_size;
    int unit;
};

static struct size_test_data data[] = {
    {
        .size = 1024,
        .blk_size = 1,
        .unit = STRING_UNITS_2,
    },
    {
        .size = 1024,
        .blk_size = 1,
        .unit = STRING_UNITS_10,
    },
    {
        .size = 1,
        .blk_size = 4096,
        .unit = STRING_UNITS_2,
    },
    {
        .size = 1,
        .blk_size = 4096,
        .unit = STRING_UNITS_10,
    }
};

static int size_test_init(void)
{
    int i;

    for (i = 0; i < sizeof(data) / sizeof(data[0]); i++) {
        char buf[16] = { 0 };
        struct size_test_data tmp = data[i];
        string_get_size(tmp.size, tmp.blk_size, tmp.unit, buf, sizeof(buf) - 1);
        pr_info("size:%lld, blk_size: %lld, unit:%d,  %s\n",
            tmp.size, tmp.blk_size,
            tmp.unit, buf);
    }

    return 0;
}

static void size_test_cleanup(void)
{
    pr_info("%s bye\n", __func__);
}

module_init(size_test_init);
module_exit(size_test_cleanup);

実行するとこう表示されます。

[ 1402.100472] size_test: size:1024, blk_size: 1, unit:1,  1.00 KiB                                      
[ 1402.100474] size_test: size:1024, blk_size: 1, unit:0,  1.02 kB                                       
[ 1402.100474] size_test: size:1, blk_size: 4096, unit:1,  4.00 KiB                                      
[ 1402.100475] size_test: size:1, blk_size: 4096, unit:0,  4.10 kB  

地味に便利ですね( ´ー`)フゥー...

argbashでbashスクリプトのオプション引数を受け取る

この記事はShell Script Advent Calendar 2017の15日目の記事です。

bashスクリプトでオプション引数を処理したい時にcaseで処理したりすると思いますが、引数の処理をサポートするargbashというツールがありました。というわけで、試してみます。

argbashでは3種類の引数の形式を使用できます。オプション引数は-が2つのロングオプション形式です。例えば、--fooとかですね。

引数のタイプ argbashでの使い方
bool型。引数を受け取らないタイプ --bool-opt 名前
引数を受け取るタイプ --opt 名前
--で始まるオプションを使わないタイプ。rmコマンドのファイル名とかディレクトリ名みたいなタイプ --pos 名前

argbashの使いかとしてはargbash-initというツールでテンプレートを作って、argbashでシェルスクリプト化するのが基本のようです。また、bool型の引数の場合、--bool-opt fooとすると--fooと--no-fooという2個のオプションが作られます。後者の方は--fooを無効にするってことですね。

試しに--name 文字列という形の引数を受け取るような設定でコマンドを使うとこのような感じになります。

masami@saga:~/codes/argtest$ argbash-init --opt name
#!/bin/bash

# m4_ignore(
echo "This is just a script template, not the script (yet) - pass it to 'argbash' to fix this." >&2
exit 11  #)Created by argbash-init v2.5.0
# ARG_OPTIONAL_SINGLE([name], , [<name's help message goes here>])
# ARG_HELP([<The general help message of my script>])
# ARGBASH_GO

# [ <-- needed because of Argbash

echo "Value of --name: $_arg_name"

# ] <-- needed because of Argbash

この例では標準出力に出力してますが、パイプでargbashにデータを渡しても構いません。では、実行してみます。

masami@saga:~/codes/argtest$ argbash-init --opt name | argbash -o name.sh -
masami@saga:~/codes/argtest$ wc -l name.sh
82 name.sh
masami@saga:~/codes/argtest$ 

82行ほどのファイルができあがります。ヘルプ用に-h/--helpがサポートされています。自分で作った--nameオプションを使うと値が表示されます。あとは自分で弄っていく感じですね。

masami@saga:~/codes/argtest$ ./name.sh -h
<The general help message of my script>
Usage: ./name.sh [--name <arg>] [-h|--help]
        --name: <name's help message goes here> (no default)
        -h,--help: Prints help
masami@saga:~/codes/argtest$ ./name.sh --name foobar
Value of --name: foobar

このスクリプトのオプション引数はこのように解析されます。

parse_commandline ()
{
        while test $# -gt 0
        do
                _key="$1"
                case "$_key" in
                        --name)
                                test $# -lt 2 && die "Missing value for the optional argument '$_key'." 1
                                _arg_name="$2"
                                shift
                                ;;
                        --name=*)
                                _arg_name="${_key##--name=}"
                                ;;
                        -h|--help)
                                print_help
                                exit 0
                                ;;
                        -h*)
                                print_help
                                exit 0
                                ;;
                        *)
                                _PRINT_HELP=yes die "FATAL ERROR: Got an unexpected argument '$1'" 1
                                ;;
                esac
                shift
        done
}

引数3パターン使ってみて、greeting引数には適当なメッセージ、to-upppercaseでname引数の大文字化をするようなことをしてみます。

masami@saga:~/codes/argtest$ argbash-init --pos name --opt greeting --opt-bool to-upppercase | argbash -o test.sh -                                                                                                

そして、test.shをちょろっと弄ります。

name=${_arg_name}
if [ ${_arg_to_upppercase} = "on" ]; then
    name=${_arg_name^^}
fi

echo "Value of --greeting: $_arg_greeting"
echo "to-upppercase is $_arg_to_upppercase"
echo "Value of name: ${name}"

これを実行するとこんなふうになります。

masami@saga:~/codes/argtest$ ./test.sh --greeting hello --to-upppercase foobar
Value of --greeting: hello
to-upppercase is on
Value of name: FOOBAR
masami@saga:~/codes/argtest$ ./test.sh --greeting hello --no-to-upppercase foobar
Value of --greeting: hello
to-upppercase is off
Value of name: foobar
masami@saga:~/codes/argtest$ ./test.sh --greeting hello  foobar
Value of --greeting: hello
to-upppercase is off
Value of name: foobar

argbashはオプションの解析部分・ヘルプメッセージ表示をサポートしてくれるので結構良いですね( ´∀`)bグッ!

シェルプログラミング実用テクニック

シェルプログラミング実用テクニック

Linuxカーネルもくもく会の運営

この記事はIT勉強会/コミュニティ運営 Advent Calendar 2017の3日目の記事です。2日目はikkouさんのIT勉強会/コミュニティ運営 Advent Calendar 2017 をやるよ、あるいは今からでも書いてくれる人を募集しているよ、という話でした。

f:id:masami256:20171202234618p:plain

Linuxカーネルもくもく会

linmoku.connpass.com

いうのを不定期に開催してます。去年までは大体月一ペースでてきてたんだけど、最近は不定期気味に(´・ω・`) 今のところ28回やりました。Linuxカーネルもくもく会とは別の番外編的なものも実はやっていて、自作エミュレータで学ぶx86アーキテクチャもくもく会なんかもやったりしました。 linmoku.connpass.com

開催のきっかけとしては話を聞くタイプの勉強会もいいけど、実際に手を動かすもくもく会も楽しいよね〜ってところと、Linuxカーネルをメインとするような勉強会とかそうそう無いのでそんなの合ったら自分が行きたいというところから始まってます。

開催場所

勉強会開催では場所の確保というのが大事なところですよね。個人的に面倒事はさけたくて、気楽にやっていきたいので会場はルノアールを利用しています。場所は秋葉原ルノアールです。ルノアールは店舗によっては会議室を借りることもできますが、それはやってません。 ルノアールを選んでる理由は秋葉原駅徒歩3分以内くらいには昭和通り口出てすぐ、アキヨドの近く、電気街口をセガ側に出て信号渡ったところ、電気街口を右側に出てすぐ(前はアキバ献血ルームだった場所)の4店舗があるというところです。第一候補の店が満員で待ってもダメそうな場合はみんなに待っててもらって第2候補のお店を見に行っって席の確保をしたりなんてこともしてます。 この店舗の候補をたくさん持てるというのが秋葉原ルノアールを選んでる理由の一つです。あと、秋葉原は個人的に都合が良いとかありますけど。 connpassでイベントを作ることを除くと、主催者らしい仕事ってこれくらいじゃないでしょうか。

開催日は金曜夜もしくは土曜の昼間が多いです。これはなんとなくその辺が良いという好みの問題です。 席は基本的には禁煙席を選んでますが、喫煙席しかないような場合は喫煙席でやることもありますが、このケースは今までで1,2回だけですね。

もくもく会の方針

「ゆるくやる」です。 もくもくやるもよし、雑談、質問なんでもありです。Linuxネタ以外にもBSDの話とか、自作OSの話なんかも出てきます。

ネットワーク

これは各自でお願いしますという方式です

絶対のルール

コーヒー1杯で粘るとかはしないで、お店に対してお金を落としましょうというのが唯一のルールです。

f:id:masami256:20171202234503p:plain

Linuxカーネルもくもく会なんでLinuxカーネルに関することをやるという基本ルールもあるんですけど、絶対のルールは↑ですw まあ、実際のもくもく会では軽食類を頼む人もいれば、飲み物を何杯か頼んだりとそれなりにお金は使ってると思います。

募集人数

これは座席を取る都合上、自分含めて4人位にしてます。最悪固まって座れなくても良いという方針にするしても、6人くらいまでが限度かなと思ってます。

参加者の傾向

Linuxカーネルに興味があるけどっていう初学者の方から、ガチ勢まで幅広く参加されています。

まとめ

Linuxカーネルもくもく会はかなり小規模なんですが、気楽にもくもく会/読書会を開く方式としてはいい感じのやり方かな?なんて思います。主催者の負担はほぼ0です。金銭的なコストは自分の飲食費だけですし、仮に全員来れなくなったとしても一人もくもく会になるだけですからね。 ただ、自分は秋葉原という場所でやっていて、地理的にも良いところで開催しているというのはあると思います。そのためLinuxカーネルというかなり限定したネタでも続いていけるのはあるかも。

Linuxカーネルもくもく会方式はもくもく会のネタを絞り過ぎなければ、ゆるめのもくもく会の開催方法としてオススメできると思います。

今後

これからも気の向くままに開催していくと思いますので、よろしくお願いしますm( )m

弄りながらなんとなく学ぶ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)