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:~$

try_charge()めも2

前回の続き

前回はここの2238行目のpage_counter_try_charge()を読んだのでその下からです。

2236         if (!do_swap_account ||
2237             !page_counter_try_charge(&memcg->memsw, batch, &counter)) {
2238                 if (!page_counter_try_charge(&memcg->memory, batch, &counter))
2239                         goto done_restock;
2240                 if (do_swap_account)
2241                         page_counter_uncharge(&memcg->memsw, batch);
2242                 mem_over_limit = mem_cgroup_from_counter(counter, memory);
2243         } else {
2244                 mem_over_limit = mem_cgroup_from_counter(counter, memsw);
2245                 may_swap = false;
2246         }
2247 

2回目のpage_counter_try_charge()で0が返ればcharge可能ということなのでdone_restockラベルにジャンプしてchargeの処理に入ってました。 では、2回目の呼び出しで0以外が帰った場合は2240行目のところになります。ここのif文はdo_swap_accountが真かどうかなので2236行目のチェックとは逆になってます。通常はdo_swap_accountは0でないでしょうから、この場合はmemswの課金を取り消してます。 そして、mem_cgroup_from_counterマクロでmem_cgroup構造体を取得します。 mem_cgroup_from_counterマクロはみんな大好きcontainer_ofマクロのラッパーです。どの変数を使うかmemory/memswはif文の結果次第です。if文が偽だったときはmay_swapをfalseにしてますが、これはcharge対象がswapじゃないよーということでしょう。この変数はtrueで初期化されています。

やっとif文を抜けて次は下のif文になります。chargeしようとしていたページ数(batch)がnr_pagesより大きい場合はbatchの数をnr_pagesに設定して、retryラベルにジャンプしてcharge処理を再チャレンジします。retryラベルはconsume_stock()を実行する手前にあります。

2248         if (batch > nr_pages) {
2249                 batch = nr_pages;
2250                 goto retry;
2251         }

retryラベルにジャンプしてもchargeが出来なかった場合や、if文が偽だった場合はここより下の処理が実行されます。これより先の処理はchargeできかった場合の処理になります。charge可能なら2239行目のgoto文でdone_restockラベルにジャンプしています。この後も色々な試みを行ってchargeしようとします。

コメントにあるようにここまで着たからといってメモリが無いわけでは無いということです。たしかに、ページの取得に失敗しているわけではないですからね。で、チェックしていることとしては、プロセスが終了に向かってるならbypassラベルにジャンプします。

2253         /*
2254          * Unlike in global OOM situations, memcg is not in a physical
2255          * memory shortage.  Allow dying and OOM-killed tasks to
2256          * bypass the last charges so that they can exit quickly and
2257          * free their memory.
2258          */
2259         if (unlikely(test_thread_flag(TIF_MEMDIE) ||
2260                      fatal_signal_pending(current) ||
2261                      current->flags & PF_EXITING))
2262                 goto bypass;

bypassラベルは-EINTRを返して終了です。

2319 bypass:
2320         return -EINTR;

プロセスがOOM中の場合はnomemラベルにジャンプします。

2264         if (unlikely(task_in_memcg_oom(current)))
2265                 goto nomem;

gfp_maskに__GFP_NOFAILが設定されている場合は -ENOMEMは返らずに、その直後にあるbypassラベルのところで-EINTRを返します。

2316 nomem:
2317         if (!(gfp_mask & __GFP_NOFAIL))
2318                 return -ENOMEM;

OOM中でなければこちらの処理が行われます。これはmemcg->stat->events[MEMCG_MAX]++的な処理です。

2270         mem_cgroup_events(mem_over_limit, MEMCG_MAX, 1);

次にもう一回ページ回収を試みます。

2272         nr_reclaimed = try_to_free_mem_cgroup_pages(mem_over_limit, nr_pages,
2273                                                     gfp_mask, may_swap);

そして、nr_pages分のchargeができそうならretryラベルに戻ってchargeの再チャレンジです。

2275         if (mem_cgroup_margin(mem_over_limit) >= nr_pages)
2276                 goto retry;

drainedはローカル変数で、falseで初期化されているため1回目は必ずこの条件は真になります。ここまではプロセスが動いているcpuからchargeできるか試みていましたが、drain_all_stock()でオンラインなcpu全てチェックしてchargeできるか調べます。そしてdrainedをtrueに返るので2度目はありません。

2278         if (!drained) {
2279                 drain_all_stock(mem_over_limit);
2280                 drained = true;
2281                 goto retry;
2282         }
2283 

次に以下のチェックが有り、リトライしなくて良いと言われていたらnomemラベルに飛びます。

2284         if (gfp_mask & __GFP_NORETRY)
2285                 goto nomem;

先程のページ回収処理で回収できたページ数がありかつ、charge対象のページ数が8ページ以下なら再チャレンジします。PAGE_ALLOC_COSTLY_ORDERの値は3です。

2286         /*
2287          * Even though the limit is exceeded at this point, reclaim
2288          * may have been able to free some pages.  Retry the charge
2289          * before killing the task.
2290          *
2291          * Only for regular pages, though: huge pages are rather
2292          * unlikely to succeed so close to the limit, and we fall back
2293          * to regular pages anyway in case of failure.
2294          */
2295         if (nr_reclaimed && nr_pages <= (1 << PAGE_ALLOC_COSTLY_ORDER))
2296                 goto retry;

もし、移動中のcgroupがあればそれの移動完了を待ってchargeのリトライがあります。これはかなり最後の手段っぽいですね。

2297         /*
2298          * At task move, charge accounts can be doubly counted. So, it's
2299          * better to wait until the end of task_move if something is going on.
2300          */
2301         if (mem_cgroup_wait_acct_move(mem_over_limit))
2302                 goto retry;

まだ頑張ってみます。nr_retriesはMEM_CGROUP_RECLAIM_RETRIESで初期化されています。

2304         if (nr_retries--)
2305                 goto retry;
2306

これはmm/memcontrol.cで5と定義されているので、gfp_maskに__GFP_NORETRYが設定されていなければ5回リトライを試みるようになっています。

  78 #define MEM_CGROUP_RECLAIM_RETRIES      5

リトライ回数を使い切った場合、gfp_maskに__GFP_NOFAILが設定されていなければ、bypassラベルにジャンプします。

2307         if (gfp_mask & __GFP_NOFAIL)
2308                 goto bypass;
2309 

シグナル待ちならbypassラベルにジャンプ。

2310         if (fatal_signal_pending(current))
2311                 goto bypass;
2312

最後にindexがMEMCG_OOMのstatusを1増やして、mem_cgroup_oom()を実行。

2313         mem_cgroup_events(mem_over_limit, MEMCG_OOM, 1);
2314 
2315         mem_cgroup_oom(mem_over_limit, gfp_mask, get_order(nr_pages));

mem_cgroup_oom()はOOMを発生させるものではなくて、OOM用に設定をしているだけです。

1862 static void mem_cgroup_oom(struct mem_cgroup *memcg, gfp_t mask, int order)
1863 {
1864         if (!current->memcg_oom.may_oom)
1865                 return;
1866         /*
1867          * We are in the middle of the charge context here, so we
1868          * don't want to block when potentially sitting on a callstack
1869          * that holds all kinds of filesystem and mm locks.
1870          *
1871          * Also, the caller may handle a failed allocation gracefully
1872          * (like optional page cache readahead) and so an OOM killer
1873          * invocation might not even be necessary.
1874          *
1875          * That's why we don't do anything here except remember the
1876          * OOM context and then deal with it at the end of the page
1877          * fault when the stack is unwound, the locks are released,
1878          * and when we know whether the fault was overall successful.
1879          */
1880         css_get(&memcg->css);
1881         current->memcg_oom.memcg = memcg;
1882         current->memcg_oom.gfp_mask = mask;
1883         current->memcg_oom.order = order;
1884 }

これでtry_charge()の処理を一通り読み終わり( ´ー`)フゥー...

Professional Linux Kernel Architecture (Wrox Programmer to Programmer)

Professional Linux Kernel Architecture (Wrox Programmer to Programmer)

try_charge()の処理めも(1)

chargeの処理としてはこの辺が重要だろうと思われる(要出典)try_charge()の処理を読みましょう。

早速処理を見ていきますが、まず対象のmemcgがルートのmemcgだった場合は何もしません。このチェックはmem_cgroup_is_root()で行えます。次にconsume_stock()を呼びます。引数としてはtry_charge()に渡されたmemcgを渡します。この関数は何をするかというと、このcpuに課金済みの使用可能なpageがあるか調べて、そういうpageがあればtrueを返します。try_charge()ではconsume_stock()がtrueを返したら処理を終了します。

次にmem_over_limit変数に値をセットするところがあります。swapを対象にしないとか、page_counter_try_charge()が0を返した場合は2238~2242行目の処理になります。do_swap_accountは明示的に無効にしない限りは大抵1になってると思うので、見るべきはpage_counter_try_charge()となります。

2236         if (!do_swap_account ||
2237             !page_counter_try_charge(&memcg->memsw, batch, &counter)) {
2238                 if (!page_counter_try_charge(&memcg->memory, batch, &counter))
2239                         goto done_restock;
2240                 if (do_swap_account)
2241                         page_counter_uncharge(&memcg->memsw, batch);
2242                 mem_over_limit = mem_cgroup_from_counter(counter, memory);
2243         } else {
2244                 mem_over_limit = mem_cgroup_from_counter(counter, memsw);
2245                 may_swap = false;
2246         }

batchという変数は宣言時に以下のように初期化されています。

 unsigned int batch = max(CHARGE_BATCH, nr_pages);

CHARGE_BATCHはmm/memcontrol.cで定義されていて値は32です。

page_counter_try_charge()は自分がいるmemcgの階層からルート階層までの各階層でcharge可能かチェックします。全階層でcharge可能なら0を返し、 一箇所でもchargeできなければ-ENOMEMを返します。ということで、先のif文ではcharge可能な場合にifのブロックが実行されます。 次にもう一回page_counter_try_charge()を呼びますが、このときの1番目の引数は先ほどと違います。最初はmemcg->memswで今度はmemcg->memoryです。どちらもstruct page_counter型の変数です。この型はlinux/page_counter.hで定義されています。これらの使い分けはこれを書いてる現段階ではよく分かってないけど、名前からしてmemswはswap、memoryはrssに使うんじゃないか?という気がします。chargeするものの種別に毎に分かれてそうな気がします。TODO:これら3変数の使われ方から変数の意味を調べよう。

 266         /* Accounted resources */
 267         struct page_counter memory;
 268         struct page_counter memsw;
 269         struct page_counter kmem;
 270 

ここでcharge可能だった場合はdone_restockラベルの場所にジャンプします。このラベルにジャンプする処理はここだけです。なのでどんな処理なのか見てしまいます。

2323         css_get_many(&memcg->css, batch);
2324         if (batch > nr_pages)
2325                 refill_stock(memcg, batch - nr_pages);
2326         if (!(gfp_mask & __GFP_WAIT))
2327                 goto done;
2328         /*
2329          * If the hierarchy is above the normal consumption range,
2330          * make the charging task trim their excess contribution.
2331          */
2332         do {
2333                 if (page_counter_read(&memcg->memory) <= memcg->high)
2334                         continue;
2335                 mem_cgroup_events(memcg, MEMCG_HIGH, 1);
2336                 try_to_free_mem_cgroup_pages(memcg, nr_pages, gfp_mask, true);
2337         } while ((memcg = parent_mem_cgroup(memcg)));

最初にcss_get_many()を呼んでますが、これは簡単に説明するとcssの参照カウンタを増やすだけです。つぎにbatchの値がnr_pagesより大きい場合はrefill_stock()を呼んでます。refill_stock()はさっき使用したconsume_stock()と関連します。refill_stock()でキャッシュして、consume_stock()で消費するような感じです。gfp_maskに__GFP_WAITが設定されていなければここで処理終了となります。そうでない場合は、do-whileの処理になります。

page_counter_read()はpage_counter構造体のcount変数をatomicに読み出してその値を返します。この値がmemcg構造体のhigh変数に設定されている値以下の場合はcontinueします。TODO: この辺の処理の意図は別途調べます。 次のmem_cgroup_events()は以下のような関数で、端的に書くとやってることとしては memcg->stat->events[MEMCG_HIGH] += nr;ですね。this_cpu_add()なのでこのcpuにバインドされてる変数に対してというのはありますが。

5400 void mem_cgroup_events(struct mem_cgroup *memcg,
5401                        enum mem_cgroup_events_index idx,
5402                        unsigned int nr)
5403 {
5404         this_cpu_add(memcg->stat->events[idx], nr);
5405 }

次は try_to_free_mem_cgroup_pages(memcg, nr_pages, gfp_mask, true); です。この関数はmm/vmscan.cにあります。この関数の子毎回処理内容は別途調べるとして、処理内容はページ回収です。nr_pages分のページ回収を行います。先のif文でmemcg->highとのチェックがあったので、上限超えてるからページ回収しないといけなということでしょうね。

最後はwhileの条件のところで、これはmemcg = parent_mem_cgroup(memcg)となっています。

この関数は以下のような処理で、mem_cgroup構造体のpage_counter型の変数memoryから上位階層のmem_cgroup構造体を取得して返します。

4418 struct mem_cgroup *parent_mem_cgroup(struct mem_cgroup *memcg)
4419 {
4420         if (!memcg->memory.parent)
4421                 return NULL;
4422         return mem_cgroup_from_counter(memcg->memory.parent, memory);
4423 }

これで自身の階層から上位に向かって処理していってるんですね。

try_charge()は一回で終わらすの大変そうなので、まずは切りの良いところでここまで ( ´ー`)フゥー...

まんがでわかるLinux シス管系女子 2(日経BP Next ICT選書)

まんがでわかるLinux シス管系女子 2(日経BP Next ICT選書)