CUBE SUGAR CONTAINER

技術系のこと書きます。

Linux の PID Namespace について

Linux のコンテナ仮想化を構成する機能の一つに Namespace (名前空間) がある。 Namespace は、カーネルのリソースを隔離して扱うための仕組みで、リソース毎に色々とある。 今回は、その中でも PID (Process Identifier) を隔離する PID Namespace を扱ってみる。

使った環境は次のとおり。

$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 20.04.3 LTS
Release:    20.04
Codename:   focal
$ uname -rm
5.4.0-96-generic aarch64
$ unshare --version
unshare from util-linux 2.34
$ gcc --version
gcc (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0
Copyright (C) 2019 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
$ ps --version
ps from procps-ng 3.3.16

PID と PID Namespace

まず、PID の前提について確認しておく。 PID は 1 から始まって、新しいプロセスができるたびにインクリメントされた整数がプロセスの識別子として付与される。 PID の数が割り当てられる上限に達したときは、また 1 に戻って空いている数字が割り当てられる。 たとえば Ubuntu 20.04 LTS であれば、上限は以下に設定されているようだ。

$ cat /proc/sys/kernel/pid_max 
4194304

プロセスが一意に識別できなくなるため、同じ PID を持ったプロセスが複数できることはない。 しかし、コンテナ仮想化においては、コンテナの中では独立した PID を見せたい。 そこで、PID Namespace によって、PID のリソースをシステムから隔離して扱うことができる。 言いかえると、コンテナの中で PID が 1 から始まるのは PID Namespace によって実現されている。

下準備

下準備として、あらかじめ unshare(1) のために util-linux をインストールしておく。 また、unshare(2) を呼び出すコードをビルドするために build-essential をインストールする。

$ sudo apt-get update
$ sudo apt-get install -y util-linux build-essential

unshare(1) を使って PID Namespace を使ってみる

まずはコマンドラインツールの unshare(1) から PID Namespace を使ってみよう。

unshare(1) を使って PID Namespace を新たに作るには --pid オプションを使う。 また、同時に --fork オプションと --mount-proc オプションもつけた方が良い。 この理由は後ほど説明する。 起動するプログラムとしては bash を指定しておこう。

$ sudo unshare --pid --fork --mount-proc bash

起動した bash では、PID が 1 になっていることがわかる。 また、ps(1) でも PID が 1 から振り直されていることが確認できる。 ちゃんとシステムから独立した PID が利用できているようだ。

# echo $$
1
# ps
    PID TTY          TIME CMD
      1 pts/0    00:00:00 bash
      8 pts/0    00:00:00 ps

さて、それでは先ほど --pid とは別で追加で指定したオプションについて見ていこう。 まずは --fork オプションから。 このオプションをつけないと何が起こるだろうか。 一旦、先ほど起動した bash は終了した上で、改めて unshare(1) を使おう。 今度は --pid オプションだけつける。

$ sudo unshare --pid bash
bash: fork: Cannot allocate memory

すると fork: Cannot allocate memory というエラーになってしまう。 とはいえ、一応エラーにはなりつつも bash は起動しているようだ。 しかし、何をするにしても同じ fork: Cannot allocate memory というエラーになってしまう。

# ls
bash: fork: Cannot allocate memory

このエラーの原因は、次の stackoverflow の質問に詳しい解説がある。

stackoverflow.com

かいつまんで説明すると、こういうことらしい。 まず、unshare(1) から起動した bash は、新たに作成した PID Namespace には所属しない。 代わりに、bash が最初に (fork(2) によって) 生成するサブプロセスが、新たに作成した PID Namespace に所属することになる。 新たに作成した PID Namespace では、PID が 1 から始まるため、bash のサブプロセスが PID 1 になる。 しかし、bash のサブプロセスは直後に終了するため、PID 1 のプロセスがいなくなる。 Linux において PID 1 のプロセスは特別な意味を持つことから、それがいなくなることで上記のエラーが生じているらしい。

一応、サブプロセスを起動しないタイプのシェルを利用すればエラーは出ない。 たとえば Ubuntu 20.04 LTS の sh は dash を使っているらしいので、指定してみよう。 しかし、その場合はそもそも起動したシェルが新しい PID Namespace に所属していないので何も意味がない。

$ sudo unshare --pid sh
# echo $$
1136

ということで --fork オプションが必要な理由がわかった。 続いては --mount-proc オプションについて。 今度はこのオプションを付けないで実行してみよう。

$ sudo unshare --pid --fork bash

一見すると何も問題なさそうに見えるけど、ps(1) なんかを呼び出すと随分と大きな数字が見える。 そもそも、隔離して見えないはずの unshare(1) の PID が見えているのはどうしたことか。

# ps
    PID TTY          TIME CMD
    959 pts/0    00:00:00 sudo
    960 pts/0    00:00:00 unshare
    961 pts/0    00:00:00 bash
    968 pts/0    00:00:00 ps

これは、/proc ファイルシステムが、PID Namespace を隔離する前の状態のままであることが原因。 要するにシステムの状態が見えたままということ。 Ubuntu 20.04 LTS の ps (=procps-ng) は /proc ファイルシステムを見ているので、さもありなん。

# ls /proc | egrep ^[0-9] | sort -n | tail -n 5
974
989
990
991
992

この状態は /proc ファイルシステムをマウントし直せば解消できる。

# mount -t proc proc /proc
# ls /proc | egrep ^[0-9] | sort -n | tail -n 5
1
20
21
22
23

つまり、これこそが --mount-proc オプションがやっていたこと、というわけだ。

ちなみに、上記のように新しいプロセスでマウントし直すやり方を取ると、そのままではマウントのプロパゲーションが起こってしまう。 この振る舞いについては以下のエントリで説明している。

blog.amedama.jp

要するに、上記のようなことをしたければ --mount オプションをつける必要がある。 あるいは、次のコマンドを使って事前にプロパゲーションを無効にしても良い。

$ sudo mount --make-private /

unshare(2) を使って PID Namespace を使ってみる

さて、続いては unshare(2) から PID Namespace を扱ってみよう。 ソースコードは、以下のコマンドと等価なものにする。

$ sudo unshare --pid --fork --mount-proc bash

早速だけどサンプルコードを以下に示す。 まず、unshare(2) で Mount Namespace と PID Namespace を新たに作成している。 その上で fork(2) で子プロセスを作っている。 この子プロセスが新しく作った PID Namespace に所属することになる。 その上で、マウントのプロパゲーションを無効にした上で /proc ファイルシステムをマウントし直している。 そして、最後にシェルを起動している。

#define _GNU_SOURCE

#include <sched.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/mount.h>

int main(int argc, char *argv[]) {
    // Mount & PID Namespace を作成する
    if (unshare(CLONE_NEWPID | CLONE_NEWNS) != 0) {
        fprintf(stderr, "Failed to create a new PID namespace: %s\n", strerror(errno));
        exit(EXIT_FAILURE);
    }

    // fork(2) で子プロセスを作る
    pid_t pid = fork();
    if (pid < 0) {
        fprintf(stderr, "Failed to fork a new process: %s\n", strerror(errno));
        exit(EXIT_FAILURE);
    }

    if (pid != 0) {
        // 親プロセスは wait(2) で子プロセスの完了を待つ
        wait(NULL);
        exit(EXIT_SUCCESS);
    }

    // 以降は子プロセスの処理

    // ルート以下のマウントプロパゲーションを再帰的に無効にする
    if (mount("none", "/", NULL, MS_REC | MS_PRIVATE, NULL) != 0) {
        fprintf(stderr, "cannot change root filesystem propagation: %s\n", strerror(errno));
        exit(EXIT_FAILURE);
    }

    // mount(2) で /proc ファイルシステムをマウントする
    if (mount("proc", "/proc", "proc", MS_NOSUID | MS_NOEXEC | MS_NODEV, NULL) != 0) {
        fprintf(stderr, "Failed to mount /proc: %s\n", strerror(errno));
        exit(EXIT_FAILURE);
    }

    // execvp(3) でシェルを起動する
    char* const args[] = {"bash", NULL};
    if (execvp(args[0], args) != 0) {
        fprintf(stderr, "Failed to exec \"%s\": %s\n", args[0], strerror(errno));
        exit(EXIT_FAILURE);
    }

    return EXIT_SUCCESS;
}

上記に適当な名前をつけて保存したらビルドして実行する。

$ gcc -std=c11 -Wall example.c
$ sudo ./a.out

シェルの PID を確認すると、ちゃんと 1 になっている。

# echo $$
1

ps(1) を実行しても、PID がリセットされていることがわかる。

# ps
    PID TTY          TIME CMD
      1 pts/0    00:00:00 bash
      8 pts/0    00:00:00 ps

PID Namespace が異なっているのは /proc ファイルシステムの以下を見ても確認できる。

# ls -l /proc/self/ns/pid
lrwxrwxrwx 1 root root 0 Jan 23 01:29 /proc/self/ns/pid -> 'pid:[4026532130]'
# exit
$ ls -l /proc/self/ns/pid
lrwxrwxrwx 1 ubuntu ubuntu 0 Jan 23 01:29 /proc/self/ns/pid -> 'pid:[4026531836]'

いじょう。

参考

man7.org

man7.org

man7.org