Linux
kernel
Docker
cgroups

cgroupsを全く知らないところから、正味一時間ちょいでソースから雰囲気つかむ

More than 3 years have passed since last update.


cgroupについてのドキュメントについて(+雑談)

Wikipediaに載っており、Documentation/cgroups の下にもいっぱいドキュメントがあるのでそれを見れば良いのだろうが、やはりソース読んでみてなるほど感を得てみたいなと思い、先日秋葉原でやったLinuxもくもく会に出席しました。

自分は飯食っていなかったので21:30くらいで断念しました(食ってくればよかった)が、みんなでもくもくとソース読むのはとても集中できますし、雑談も自然と低レイヤなネタになりがちでそれが楽しく、愉悦です。


それはおいておいて

さすがに、cgroupsを実現するための全てのコード読むのは途方もない(多岐にわたる)し、現実的でありません。

ふと見ると、cgroupsにはプロセスIDの割り当てにも絡んでいるだろうことがわかります。

そこで、まずはプロセスID割り当て箇所に絞り、雰囲気を掴んでみることにしたいと思います。


で、読みましょう

プロセスIDの割り当てとくれば、fork。よって、kernel/の下を「fork」でgrep。

fork.cというそのまんまのソースがありますね。

fork.cをうろつくと、そのまんまのコードがあります。


kernel/fork.c

    pid = alloc_pid(p->nsproxy->pid_ns_for_children);

/* 略 */
/* ok, now we should be set up.. */
p->pid = pid_nr(pid);

alloc_pid()を見るためにgrepすると、以下のコードが見つかります。


kernel/pid.c

#include <linux/pid_namespace.h>

/* 略 */
struct pid *alloc_pid(struct pid_namespace *ns)
{
struct pid *pid;
enum pid_type type;
int i, nr;
struct pid_namespace *tmp;
struct upid *upid;

pid = kmem_cache_alloc(ns->pid_cachep, GFP_KERNEL);
if (!pid)
goto out;


ここで、データ構造の確認。

動的に割り当てているstruct pidはinclude/linux/pid.hで定義されています。


include/linux/pid.h

 57 struct pid

58 {
59 atomic_t count;
60 unsigned int level;
61 /* lists of tasks that use this pid */
62 struct hlist_head tasks[PIDTYPE_MAX];
63 struct rcu_head rcu;
64 struct upid numbers[1];
65 };

引数で渡されているstruct pid_namespaceは以下のとおりです。


include/linux/pid_namespace.h

struct pid_namespace {

struct kref kref;
struct pidmap pidmap[PIDMAP_ENTRIES];
struct rcu_head rcu;
int last_pid;
unsigned int nr_hashed;
struct task_struct *child_reaper;
struct kmem_cache *pid_cachep;
unsigned int level;
struct pid_namespace *parent;
#ifdef CONFIG_PROC_FS
struct vfsmount *proc_mnt;
struct dentry *proc_self;
#endif
unsigned int proc_inum;
};

この構造体の定義から、namespaceという何らかの階層化された範疇があり、その中で割り当てられるpidの範囲が決まっているように思われます。

以下Wikipediaからの引用。



PID 名前空間 - プロセス番号 (PID) の割り当て、プロセスの一覧とその詳細を隔離する。新しい名前空間は兄弟名前空間からは隔離されているが、親名前空間は子名前空間のプロセスを見ることができる[9]。



おそらく、親名前空間と言っているのは、メンバparentと推定できます。

また、pidmapというのが、おそらく割り当て状況を記録するためのビットマップだと思われます。カーネルではこの手のビットマップをたまに見かけるので、そう推定できます。

次に進みます。


kernel/pid.c

  tmp = ns;

pid->level = ns->level;
for (i = ns->level; i >= 0; i--) {
nr = alloc_pidmap(tmp);
if (nr < 0)
goto out_free;

pid->numbers[i].nr = nr;
pid->numbers[i].ns = tmp;
tmp = tmp->parent;
}


alloc_pidmap()の戻り結果(おそらくpidなのだろう)をnrに、nsにPID名前空間構造体をそれぞれ格納するように見えます。

そして、ループ内で親名前空間のにどんどんさかのぼって、その名前空間で割り当て可能なpidを名前空間ごとに割り当てているようにも見えます。

ここで登場するメンバnumbersの型は以下のとおりstruct upidになります。

/*

* struct upid is used to get the id of the struct pid, as it is
* seen in particular namespace. Later the struct pid is found with
* find_pid_ns() using the int nr and struct pid_namespace *ns.
*/

struct upid {
/* Try to keep pid_chain in the same cacheline as nr for find_vpid */
int nr;
struct pid_namespace *ns;
struct hlist_node pid_chain;
};

コメントやメンバから推定できるように、名前空間とその名前空間に割り当てられたidの組を表現したものです。

さて、alloc_pid()の戻り値を確認するため、alloc_pidmap()の実装を読みます。

static int alloc_pidmap(struct pid_namespace *pid_ns)

{
int i, offset, max_scan, pid, last = pid_ns->last_pid;
struct pidmap *map;

pid = last + 1;
offset = pid & BITS_PER_PAGE_MASK;
map = &pid_ns->pidmap[pid/BITS_PER_PAGE];
for (i = 0; i <= max_scan; ++i) {
if (likely(atomic_read(&map->nr_free))) {
for ( ; ; ) {
if (!test_and_set_bit(offset, map->page)) {
atomic_dec(&map->nr_free);
set_last_pid(pid_ns, last, pid);
return pid;
}
offset = find_next_offset(map, offset);
if (offset >= BITS_PER_PAGE)
break;
pid = mk_pid(pid_ns, map, offset);
if (pid >= pid_max)
break;
}
}

上のコード、要するに、namespace内で保持しているpidmapを調べて空いているpidの値を返す処理です。なので、先の推定は当たっていそうです。

ところで、一番最初のfork.cの実装に戻ると、pid_nr()の戻り値をtask_structのpidに格納しています。

そこで、task_structのpidの意味を調べるべく、pid_nr()の実装を確認します。


include/linux/pid.h

/*

* the helpers to get the pid's id seen from different namespaces
*
* pid_nr() : global id, i.e. the id seen from the init namespace;
* pid_vnr() : virtual id, i.e. the id seen from the pid namespace of
* current.
* pid_nr_ns() : id seen from the ns specified.
*
* see also task_xid_nr() etc in include/linux/sched.h
*/

static inline pid_t pid_nr(struct pid *pid)
{
pid_t nr = 0;
if (pid)
nr = pid->numbers[0].nr;
return nr;
}


一番最上位の親の名前空間に割り当てられたpidを返していることがわかります。そして、その意味もコメントに書いてあります。

ここまで読むと、以下Wikipediaの記事が実際のイメージを持って把握することができるようになります。

技術的には cgroups の一部ではないが、関連する機能として、名前空間の隔離があり、グループ内のプロセスから他のグループのリソースを見えないようにすることができる。例えば、PID 名前空間を使うとそれぞれの名前空間ごとに重複したプロセス番号を割り振ることが可能になる。

やはり、ソフトエンジニアがある機能のイメージをつかむには、カーネル(じゃなくても)ソースを読むのが優れた方法の一つですね。


最後に

cgroupsはここを足がかりにすればイメージが掴めそうです。

カーネルは大きな機能が多いけど、知らない機能でも小さめのところから少しずつ読んでいく・・・ということを繰り返すと、イメージが少しずつ湧いてきます。

次はcgroupsとメモリ管理というネタで行ってみたいですね。

(仕事ではNetBSD弄ることがほとんどなので、他のカーネル実装読むのはなかなか面白いものがあります。)