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を綺麗にして終了します。