LXCで学ぶコンテナ入門 -軽量仮想化環境を実現する技術

第30回Linuxカーネルのコンテナ機能[8]― cgroupのpidsサブシステム

今年も年末を迎えて、Advent Calendarの季節になり、いろいろなカレンダーの案内をみかけるようになりました。

昨年のこの時期に書いた第16回は、Linux Advent Calendar 2014の16日目の記事として書きました。連載記事を直接Advent Calendarに参加させるということが新鮮だったのか、うれしい反応をいただきました。

今年同じことをやっても新鮮味にはかけると思いますが、今年も今回の記事をLinux Advent Calendar 2015の15日目の記事としても書きました。

Linux Advent Calendarということで、前回の最後に「引き続きLXC 1.1の新機能や変更点を紹介」と書きましたが、今回は予定を変更してLinuxカーネルに新たに導入されたコンテナ関連の機能について書きたいと思います。

cgroup

この連載では、cgroupについて第3回から第5回にかけて紹介しています。今回はcgroupの基本的な機能については説明しませんので、必要に応じてそちらをご参照ください。

その際に説明したように、cgroupでは、制限するリソースごとに「サブシステム」または「コントローラ」と呼ばれるモジュールにわかれています。

カーネルのバージョン2.6の後半からバージョン3の前半にかけて、新しいサブシステムが続々と追加されました。その頃は、筆者は新しいカーネルがリリースされるたびに、cgroupに新しい機能が追加されていないかをチェックして、追加されるたびに試してブログに書いたりしていました。

その後、一通り機能が充実したあたりからは、すぐに試してわかりやすい結果が得られるような新機能の追加は少なくなりました。そういうこともあり、新しいcgroupの機能を試してブログに書くことは少なくなりました。それだけコンテナ関連の機能が成熟してきたということかもしれませんね。

新しい機能のリリースはなくても、コンテナが注目されるにつれ、コンテナ関連の機能をカーネルに取り入れたいという提案はいろいろなされていました。最近そのような中から、久々にカーネルにマージされる機能が出てきました。

それが4.3カーネルで導入された、pidsサブシステム(Process Number Controller)です。

cgroupによるプロセス数の制限

コンテナごとに起動できるプロセスの数を制限したいという要求は、特にマルチテナント環境では昔からあり、OpenVZでも実現されていた機能でした。

cgroupでも、以前"fork"という名前で似たような機能を持つサブシステムが提案されていたことがあります。しかし、この時はrejectされていました。

筆者が主催しているコンテナ型仮想化の情報交換会第2回では、mizzyさんに、提案されていた"fork"サブシステムに改造を加えてfork bomb対策を行ったという発表をしていただきました。筆者は発表を聞きながら、提案されているパッチにさらに改造を加えてカーネルに機能を追加し、サービスを作り上げる技術力の高さに感心したことを覚えています。いずれにせよ、実際にサービスを提供する際には、このような機能が必要な場合があるということを示していると思います。

私は、pidsサブシステムがマージされるまでの経緯を追っていないので、どういう議論がなされてサブシステムがマージされるに至ったのかはわかりませんが、待望されていた機能には違いないと思います。

pidsサブシステムを有効にしたカーネルの作成

それでは早速pidsサブシステムを使ってみましょう。今回の記事の実行例はPlamo 6.0上で作成した4.3.0カーネル上で実行しています。

pidsサブシステムを使うには、カーネルの設定でCONFIG_CGROUP_PIDSを有効にする必要があります。make menuconfigなどで設定する場所は他のcgroupサブシステムと同じ場所です。

General setup  --->
  Control Group support  --->
    [*] PIDs cgroup subsystem

新しく作ったカーネルで起動すると、/proc/cgroupspidsという行が現れています。

$ cat /proc/cgroups | grep pids
pids    12  5   1

マウントとグループの作成

pidsサブシステムを使う際にマウントを行う方法は、他のサブシステムと同様です。-oオプションでpidsを指定します。

たとえば以下のように行います。

$ sudo mkdir -p /sys/fs/cgroup/pids
$ sudo mount -t cgroup -o pids none /sys/fs/cgroup/pids

このマウントした/(ルート)を見てみると以下のようなファイルができています。

$ ls /sys/fs/cgroup/pids/
cgroup.clone_children  cgroup.sane_behavior  notify_on_release  release_agent
cgroup.procs           pids.current          tasks

ここにtest01というグループを作成してみましょう。

$ sudo mkdir /sys/fs/cgroup/pids/test01
$ ls /sys/fs/cgroup/pids/test01
cgroup.clone_children  notify_on_release  pids.max
cgroup.procs           pids.current       tasks

pids.maxpids.currentが、pidsサブシステムが独自に持つファイルのようですね。

pidsサブシステム用ファイル

pidsサブシステム独自で使用するファイルは以上の 2 つです。ファイル名ですぐに役割が分かったのではないでしょうか。

ファイル名 ファイルの役割
pids.current グループ内の現在のプロセス数
pids.max グループ内で許可するプロセス数

それぞれのファイルが、グループを作成した直後にはどのような内容であるかを見てみましょう。さきほど作成したtest01グループで見てみます。

$ cat /sys/fs/cgroup/pids/test01/pids.current
0
$ cat /sys/fs/cgroup/pids/test01/pids.max
max

グループを作成した直後ですから、グループにプロセスは登録されていませんのでpids.currentは当然0になりますね。制限なしとするにはpids.maxmaxと書きます。初期値は制限なしでmaxです。

制限を追加する

制限値を設定する方法は通常のcgroupの使い方と同じです。制限値として2を設定してみましょう。

$ echo 2 | sudo tee /sys/fs/cgroup/pids/test01/pids.max
2
$ cat /sys/fs/cgroup/pids/test01/pids.max
2

ファイルの内容は、設定した通り2になっていますね。

現在のシェルをグループに追加してみましょう。

$ echo $$ | sudo tee /sys/fs/cgroup/pids/test01/tasks
5600
$ cat /sys/fs/cgroup/pids/test01/pids.current
2

pids.currentの内容は、追加したシェルと確認用に実行したcatで2になっています。

ここでパイプでつないでコマンドを実行してみましょう。

$ ( /bin/echo "Gihyo" | cat )
bash: fork: retry: No child processes
bash: fork: retry: No child processes
bash: fork: retry: No child processes
bash: fork: retry: No child processes
bash: fork: Resource temporarily unavailable
Terminated

シェルを登録してひとつ消費した状態で2つ実行しようとしたので、実行できずにエラーになっています。制限が効いていることがわかりますね。

階層構造

第3回でcgroupの特徴として階層構造が取れることを紹介しました。

それでは多層の場合に、pidsサブシステムでどのように制限がかかるのかを確認してみましょう。

先ほどのtest01グループの下層にtest02グループを作成します。test01は2を設定しておき、test02は作成したままで特に制限をかけないままにしておきます。

$ sudo mkdir /sys/fs/cgroup/pids/test01/test02
$ cat /sys/fs/cgroup/pids/test01/pids.max 
2
$ cat /sys/fs/cgroup/pids/test01/test02/pids.max 
max

test02にプロセスを追加して、test01test02pids.currentを確認してみます。

$ echo $$ | sudo tee /sys/fs/cgroup/pids/test01/test02/tasks 
26500
$ cat /sys/fs/cgroup/pids/test01/tasks
$ cat /sys/fs/cgroup/pids/test01/test02/tasks
26500
26582
$ cat /sys/fs/cgroup/pids/test01/pids.current 
2
$ cat /sys/fs/cgroup/pids/test01/test02/pids.current 
2

test01tasksは空ですので、test01グループにはプロセスは登録されていない状態です。test02tasksには追加したシェルのPIDとcatのPIDが表示されています。つまりcatを実行した瞬間は、test02には2つプロセスが存在する状態ですね。

pids.currentを見てみると、test01test02ともに2となっています。これは、階層構造の子孫のプロセスが上位でカウントされているということです。

ここで先ほどと同様にpids.maxを超えるプロセスを生成してみます。

$ ( /bin/echo "Gihyo" | cat )
bash: fork: retry: No child processes
bash: fork: retry: No child processes
bash: fork: retry: No child processes
bash: fork: retry: No child processes
bash: fork: Resource temporarily unavailable
Terminated

プロセスは生成できませんね。つまり階層構造を持つグループの場合、先祖であるグループのどれかひとつの制限にかかると、プロセスが生成できないということです。

pidsサブシステムのカーネルの実装

今回は、Linux Advent Calendar 2015への参加を兼ねているので、普段の連載とは少し趣向を変えて、pidsサブシステムがカーネルでどのように実装されているかを軽く見てみましょう。

昨年のLinux Advent Calendar 201425日目「システムコールの探しかた」を参考に、fork()を探してみるとkernel/fork.c中に以下が見つかります(見やすくするために必要な行だけ抜き出しています⁠⁠。

  1792 SYSCALL_DEFINE0(fork)
  1793 {
           :(略)
  1795         return _do_fork(SIGCHLD, 0, 0, NULL, NULL, 0);
           :(略)
  1800 }

以上のfork()の定義行の少し下にはvfork()clone()の定義もあります。いずれも_do_forkという関数を呼んでいますので、新しくプロセスを作る場合の処理は_do_fork関数を見れば良いことがわかります。

_do_fork関数も同じくkernel/fork.c中にあります。

  1697 long _do_fork(unsigned long clone_flags,
  1698         unsigned long stack_start,
  1699         unsigned long stack_size,
  1700         int __user *parent_tidptr,
  1701         int __user *child_tidptr,
  1702         unsigned long tls)
  1703 {
           :(略)
  1726         p = copy_process(clone_flags, stack_start, stack_size,
  1727                          child_tidptr, NULL, trace, tls);
           :(略)

_do_fork関数は指定されたcloneフラグにしたがって各種資源のコピーを作成します。このコピーを行うのが、上記のcopy_process関数です。この関数の中では、指定されたcloneフラグの組み合わせをチェックしたり、copy_で始まる名前の関数が多数呼ばれて、いろいろコピーされてるっぽいことがなんとなくわかります。

このcopy_process関数をきちっと追っていけば、pidsサブシステムで制限をかけている部分がわかるはずです。しかし、私の技術力では途中でつまづく可能性が高いので、手っ取り早くpidsサブシステムがカーネルにマージされた際のパッチを見てみました。

pidsサブシステムは比較的シンプルに実装されていて、以下の2つのマージされたパッチで実現されています。

1つ目のパッチを見てみると、ずばりcopy_process()関数内の処理にパッチが当たっていることがわかります。パッチが当たっている部分を見てみると、以下のようなコメントとともにcgroup_can_fork()関数が呼ばれています。

  1526         /*
  1527          * Ensure that the cgroup subsystem policies allow the new process to be
  1528          * forked. It should be noted the the new process's css_set can be changed
  1529          * between here and cgroup_post_fork() if an organisation operation is in
  1530          * progress.
  1531          */
  1532         retval = cgroup_can_fork(p, cgrp_ss_priv);
  1533         if (retval)
  1534                 goto bad_fork_free_pid;

ここでエラーになると、エラーの後処理部分に処理が飛び、新しいプロセスは生成できなさそうなことがわかります。

2つ目のパッチはpidsサブシステム自体の処理を実装してあり、実際のプロセスの生成を制限するような処理はありません。

もう少し詳しく見ると正確に処理がわかるとは思います。しかしパッチをさらっと見るだけでも、fork()vfork()clone()システムコールの実体である関数内にチェックが追加され、新しいプロセスの生成が制御されてるらしいということはわかります。他に同様のチェックが追加されている部分はなさそうです。これを理解した上で、もう少しpidsサブシステムの動きを見てみましょう。

制限以上のプロセスをグループに追加する

pids.maxを設定した上で、グループにプロセスを追加していってみましょう。

$ sudo mkdir /sys/fs/cgroup/pids/test01
$ echo 2 | sudo tee /sys/fs/cgroup/pids/test01/pids.max
2

制限を2に設定したので、test01グループにどんどんプロセスを追加してみましょう。

$ cat /sys/fs/cgroup/pids/test01/tasks  (登録されているプロセスはない)
$ echo 7189 | sudo tee /sys/fs/cgroup/pids/test01/tasks
7189  (1つ目のプロセス追加)
$ echo 7855 | sudo tee /sys/fs/cgroup/pids/test01/tasks
7855  (2つ目のプロセス追加)
$ echo 8885 | sudo tee /sys/fs/cgroup/pids/test01/tasks
8885  (3つ目のプロセス追加。エラーにならない)
$ echo 8992 | sudo tee /sys/fs/cgroup/pids/test01/tasks
8992  (4つ目のプロセス追加。エラーにならない)
$ cat /sys/fs/cgroup/pids/test01/pids.current
4     (pids.max以上のプロセスが登録されている)

pids.maxを2に設定したのに、pids.currentがそれ以上になってもエラーにはなりません。

この動きは、先に紹介したカーネルの実装を考えると納得できます。実装では、新たにプロセスを生成しようとする処理で制限のチェックがされていました。すでに生成されているプロセスを新たにグループに追加しても、そのチェックには引っかからないので、このように制限値以上にtasksにプロセスを追加していっても、既存のプロセスが影響を受けることはありません。

もちろん、上記の状態で新たにプロセスを生成させようとすると、pidsサブシステムにより新たにプロセスは生成できません。

$ echo $$ | sudo tee /sys/fs/cgroup/pids/test01/tasks
10648  (さらにプロセスをtest01に追加)
$ cat /sys/fs/cgroup/pids/test01/tasks
bash: fork: retry: No child processes  (新たにプロセスは起動しない)
    :(略)

プロセスが登録された状態でグループの制限値を変更する

さらに、pids.max < pids.currentとなるようなケースを試してみましょう。

$ cat /sys/fs/cgroup/pids/test01/pids.max
4
$ cat /sys/fs/cgroup/pids/test01/pids.current
4

以上のようにすでに制限値に達しているグループがあります。この状態でpids.maxを減らしてみましょう。

$ echo 2 | sudo tee /sys/fs/cgroup/pids/test01/pids.max
2  (制限値を2に減らす)
$ cat /sys/fs/cgroup/pids/test01/pids.current
4  (グループ内のプロセス数は4のまま)

プロセスが登録された状態で、登録されているプロセス数以下の制限値を設定しても、プロセスはそのままです。

この動きも、先ほどのプロセスをグループに追加していった場合と同様に、カーネルの実装から考えると納得できる動きですね。

まとめ

今回は少し予定を変更して、カーネル4.3で導入されたpidsサブシステムを紹介しました。

  • pidsサブシステムを使うと、グループ内で起動するプロセス数が制限できる
  • 制限はこれから起動しようとするプロセスに対する制限で、既存のプロセスには影響を与えない

サブシステムの動きだけを見ると、なぜこのような動きになるのだろうという疑問がわくかもしれません。実装を少し見ることにより、そのような動きになる理由が理解できたのではないでしょうか。

カーネルのコードはもう少し詳細に追っかけないと、サブシステムの動きが納得できないかもしれません。しかしこれ以上やるとそれだけで連載何回分にもなりそうなので、最小限の紹介にとどめました。興味のある方はこの記事をきっかけに詳細に動きを追ってみてください。

次回は、またLXC 1.1の新機能の紹介に戻る予定です。

おすすめ記事

記事・ニュース一覧