Linux Kernel Namespaces and Cgroups

Linux Kernel Namespaces & Cgroups 完全ガイド

最終更新日: 2026-04-10 対象カーネルバージョン: Linux 5.x / 6.x 対象読者: SRE、インフラエンジニア、コンテナ技術に携わるエンジニア


目次

  1. はじめに
  2. Namespaces 概要
  3. Namespace の種類
  4. Namespace の作成と管理
  5. PID Namespace 詳細
  6. Network Namespace 詳細
  7. Mount Namespace 詳細
  8. User Namespace 詳細
  9. UTS Namespace
  10. IPC Namespace
  11. Cgroup Namespace
  12. Time Namespace
  13. Cgroups v1 アーキテクチャ
  14. Cgroups v2 アーキテクチャ
  15. CPU コントローラ
  16. Memory コントローラ
  17. I/O コントローラ
  18. PID コントローラ
  19. RDMA コントローラ
  20. HugeTLB コントローラ
  21. Cgroups のデリゲーションとルートレスコンテナ
  22. コンテナランタイムにおける Namespaces と Cgroups
  23. systemd と Cgroups の統合
  24. ツールリファレンス
  25. トラブルシューティング
  26. セキュリティ考慮事項
  27. パフォーマンスチューニング
  28. まとめ
  29. 参考文献

はじめに

Linux カーネルの NamespacesCgroups (Control Groups) は、現代のコンテナ技術の基盤となる2つの核心的なカーネル機能である。Docker、Podman、LXC、Kubernetes など、今日広く使われているコンテナ技術は、これらのカーネル機能の上に構築されている。

Namespaces とは

Namespaces は、カーネルリソースの分離 (isolation) を提供する。各 namespace は、グローバルなシステムリソースを抽象化し、namespace 内のプロセスには独自のリソースインスタンスが見えるようにする。これにより、プロセスグループ間でリソースの可視性を分離できる。

Cgroups とは

Cgroups は、プロセスグループに対するリソースの制限 (limitation)優先度付け (prioritization)計測 (accounting)制御 (control) を提供する。CPU、メモリ、I/O などのリソース使用量を制限し、監視することができる。

両者の関係

+------------------------------------------+
|           コンテナ技術の基盤              |
+------------------------------------------+
|                                          |
|  Namespaces          Cgroups             |
|  ┌──────────┐       ┌──────────┐        |
|  │ 分離     │       │ 制限     │        |
|  │ (何が    │       │ (どれだけ │        |
|  │  見えるか)│       │  使えるか)│        |
|  └──────────┘       └──────────┘        |
|                                          |
|  - PID               - CPU              |
|  - Network           - Memory           |
|  - Mount             - I/O              |
|  - UTS               - PID数            |
|  - IPC               - HugeTLB          |
|  - User              - RDMA             |
|  - Cgroup            - cpuset           |
|  - Time                                 |
+------------------------------------------+

Namespaces が「何が見えるか」を制御し、Cgroups が「どれだけ使えるか」を制御する。この2つを組み合わせることで、コンテナのような軽量な仮想化環境が実現される。


Namespaces 概要

歴史

Linux Namespaces の歴史は長く、段階的に追加されてきた。

Namespaceカーネルバージョンclone フラグ
Mount2.4.192002CLONE_NEWNS
UTS2.6.192006CLONE_NEWUTS
IPC2.6.192006CLONE_NEWIPC
PID2.6.242008CLONE_NEWPID
Network2.6.292009CLONE_NEWNET
User3.82013CLONE_NEWUSER
Cgroup4.62016CLONE_NEWCGROUP
Time5.62020CLONE_NEWTIME

カーネル内部の仕組み

各プロセスは task_struct 構造体を持ち、その中に nsproxy へのポインタがある。nsproxy は各種 namespace への参照を保持する。

// include/linux/nsproxy.h
struct nsproxy {
    atomic_t count;
    struct uts_namespace *uts_ns;
    struct ipc_namespace *ipc_ns;
    struct mnt_namespace *mnt_ns;
    struct pid_namespace *pid_ns_for_children;
    struct net           *net_ns;
    struct time_namespace *time_ns;
    struct time_namespace *time_ns_for_children;
    struct cgroup_namespace *cgroup_ns;
};

Namespace の確認方法

現在のプロセスが属する namespace は /proc/[pid]/ns/ ディレクトリで確認できる。

# 現在のプロセスの namespace を確認
$ ls -la /proc/self/ns/
total 0
dr-x--x--x 2 root root 0 Apr 10 10:00 .
dr-xr-xr-x 9 root root 0 Apr 10 10:00 ..
lrwxrwxrwx 1 root root 0 Apr 10 10:00 cgroup -> 'cgroup:[4026531835]'
lrwxrwxrwx 1 root root 0 Apr 10 10:00 ipc -> 'ipc:[4026531839]'
lrwxrwxrwx 1 root root 0 Apr 10 10:00 mnt -> 'mnt:[4026531841]'
lrwxrwxrwx 1 root root 0 Apr 10 10:00 net -> 'net:[4026531840]'
lrwxrwxrwx 1 root root 0 Apr 10 10:00 pid -> 'pid:[4026531836]'
lrwxrwxrwx 1 root root 0 Apr 10 10:00 pid_for_children -> 'pid:[4026531836]'
lrwxrwxrwx 1 root root 0 Apr 10 10:00 time -> 'time:[4026531834]'
lrwxrwxrwx 1 root root 0 Apr 10 10:00 time_for_children -> 'time:[4026531834]'
lrwxrwxrwx 1 root root 0 Apr 10 10:00 user -> 'user:[4026531837]'
lrwxrwxrwx 1 root root 0 Apr 10 10:00 uts -> 'uts:[4026531838]'

各シンボリックリンクの [数字] は inode 番号であり、同じ inode 番号を持つプロセスは同じ namespace に属している。

# 2つのプロセスが同じ namespace に属しているか確認
$ readlink /proc/1/ns/net
net:[4026531840]
$ readlink /proc/$$/ns/net
net:[4026531840]
# 同じ inode 番号なら同じ namespace

Namespace の種類

8つの Namespace 概要

┌─────────────────────────────────────────────────────────┐
│                    Linux Namespaces                      │
├──────────┬──────────────────────────────────────────────┤
│ PID      │ プロセス ID の分離                            │
│ Network  │ ネットワークスタック(IF, ルーティング等)の分離 │
│ Mount    │ マウントポイントの分離                        │
│ UTS      │ ホスト名・ドメイン名の分離                    │
│ IPC      │ System V IPC / POSIX メッセージキューの分離   │
│ User     │ UID/GID の分離とマッピング                    │
│ Cgroup   │ cgroup ルートディレクトリの分離               │
│ Time     │ CLOCK_MONOTONIC / CLOCK_BOOTTIME の分離       │
└──────────┴──────────────────────────────────────────────┘

Namespace の作成と管理

Namespace を操作するための主要なシステムコールは以下の4つである。

clone() システムコール

clone() は新しいプロセスを生成すると同時に、新しい namespace を作成する。

#define _GNU_SOURCE
#include <sched.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>

#define STACK_SIZE (1024 * 1024)

static char child_stack[STACK_SIZE];

static int child_fn(void *arg) {
    printf("Child PID (内部から見た): %d\n", getpid());
    printf("Child hostname: ");
    
    // UTS namespace 内でホスト名を変更
    sethostname("container", 9);
    
    char hostname[256];
    gethostname(hostname, sizeof(hostname));
    printf("%s\n", hostname);
    
    // 新しい PID namespace 内のプロセスを確認
    system("ps aux");
    
    return 0;
}

int main() {
    printf("Parent PID: %d\n", getpid());
    
    // 新しい PID, UTS, Mount namespace でプロセスを生成
    int flags = CLONE_NEWPID | CLONE_NEWUTS | CLONE_NEWNS | SIGCHLD;
    
    pid_t child_pid = clone(child_fn,
                            child_stack + STACK_SIZE,
                            flags,
                            NULL);
    
    if (child_pid == -1) {
        perror("clone");
        exit(EXIT_FAILURE);
    }
    
    printf("Parent: 子プロセスの PID = %d\n", child_pid);
    waitpid(child_pid, NULL, 0);
    
    return 0;
}

コンパイルと実行:

$ gcc -o ns_demo ns_demo.c
$ sudo ./ns_demo
Parent PID: 12345
Parent: 子プロセスの PID = 12346
Child PID (内部から見た): 1
Child hostname: container

unshare() システムコール

unshare() は現在のプロセスの namespace を新しいものに置き換える。新しいプロセスを生成せずに namespace を分離できる。

#define _GNU_SOURCE
#include <sched.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main() {
    // 新しい UTS namespace に分離
    if (unshare(CLONE_NEWUTS) == -1) {
        perror("unshare");
        exit(EXIT_FAILURE);
    }
    
    // 新しい namespace 内でホスト名を変更
    sethostname("isolated", 8);
    
    char hostname[256];
    gethostname(hostname, sizeof(hostname));
    printf("新しい UTS namespace 内のホスト名: %s\n", hostname);
    
    // シェルを起動して確認
    execlp("/bin/bash", "/bin/bash", NULL);
    
    return 0;
}

setns() システムコール

setns() は既存の namespace にプロセスを参加させる。

#define _GNU_SOURCE
#include <fcntl.h>
#include <sched.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(int argc, char *argv[]) {
    if (argc < 2) {
        fprintf(stderr, "Usage: %s /proc/<pid>/ns/<ns>\n", argv[0]);
        exit(EXIT_FAILURE);
    }
    
    // 対象 namespace のファイルディスクリプタを取得
    int fd = open(argv[1], O_RDONLY);
    if (fd == -1) {
        perror("open");
        exit(EXIT_FAILURE);
    }
    
    // namespace に参加
    if (setns(fd, 0) == -1) {
        perror("setns");
        exit(EXIT_FAILURE);
    }
    
    close(fd);
    
    // 新しい namespace 内でシェルを起動
    execlp("/bin/bash", "/bin/bash", NULL);
    
    return 0;
}

unshare コマンド

unshare コマンドはシステムコールのラッパーであり、コマンドラインから簡単に namespace を作成できる。

# 新しい PID namespace でシェルを起動
$ sudo unshare --pid --fork --mount-proc /bin/bash

# 新しい Network namespace でシェルを起動
$ sudo unshare --net /bin/bash

# 新しい UTS namespace でシェルを起動
$ sudo unshare --uts /bin/bash

# 複数の namespace を同時に作成
$ sudo unshare --pid --net --mount --uts --ipc --fork --mount-proc /bin/bash

# User namespace を使用(root 不要)
$ unshare --user --map-root-user /bin/bash

# すべての namespace を分離
$ sudo unshare --pid --net --mount --uts --ipc --user --cgroup \
    --fork --mount-proc /bin/bash

nsenter コマンド

nsenter は既存の namespace に入るためのコマンドである。

# コンテナの PID を取得
$ CONTAINER_PID=$(docker inspect --format '{{.State.Pid}}' my_container)

# そのコンテナのすべての namespace に入る
$ sudo nsenter --target $CONTAINER_PID --all

# 特定の namespace のみ
$ sudo nsenter --target $CONTAINER_PID --net    # Network namespace のみ
$ sudo nsenter --target $CONTAINER_PID --pid --mount  # PID と Mount namespace

# namespace ファイルを直接指定
$ sudo nsenter --net=/proc/$CONTAINER_PID/ns/net /bin/bash

# コンテナの network namespace 内でコマンド実行
$ sudo nsenter --target $CONTAINER_PID --net ip addr show
$ sudo nsenter --target $CONTAINER_PID --net ss -tlnp
$ sudo nsenter --target $CONTAINER_PID --net iptables -L -n

lsns コマンド

lsns はシステム上の namespace を一覧表示する。

# すべての namespace を表示
$ lsns
        NS TYPE   NPROCS   PID USER    COMMAND
4026531834 time      245     1 root    /sbin/init
4026531835 cgroup    245     1 root    /sbin/init
4026531836 pid       240     1 root    /sbin/init
4026531837 user      245     1 root    /sbin/init
4026531838 uts       240     1 root    /sbin/init
4026531839 ipc       240     1 root    /sbin/init
4026531840 net       240     1 root    /sbin/init
4026531841 mnt       230     1 root    /sbin/init
4026532200 mnt         1   500 root    /usr/lib/systemd/systemd-udevd
4026532285 net         2  1200 nobody  /usr/sbin/nginx
4026532350 pid         5  2000 root    /usr/bin/containerd-shim-runc-v2

# 特定の種類のみ
$ lsns -t net
$ lsns -t pid

# JSON 出力
$ lsns -J

# 特定のプロセスの namespace
$ lsns -p $$

PID Namespace 詳細

PID Namespace は、プロセス ID の空間を分離する。各 PID namespace は独立した PID 番号体系を持つ。

基本概念

ホスト PID namespace
┌────────────────────────────────────┐
│ PID 1 (systemd/init)               │
│ PID 100 (sshd)                     │
│ PID 200 (bash)                     │
│                                    │
│  子 PID namespace                  │
│  ┌──────────────────────────┐      │
│  │ PID 1 (コンテナの init)   │      │
│  │ PID 2 (nginx master)     │      │
│  │ PID 3 (nginx worker)     │      │
│  │                          │      │
│  │ ※ ホストから見ると:       │      │
│  │   PID 300, 301, 302     │      │
│  └──────────────────────────┘      │
└────────────────────────────────────┘

PID Namespace の作成と /proc の再マウント

# PID namespace を作成し、/proc を再マウント
$ sudo unshare --pid --fork --mount-proc /bin/bash

# namespace 内でプロセスを確認
root@host:~# ps aux
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  0.0  0.0   8284  5200 pts/0    S    10:00   0:00 /bin/bash
root         8  0.0  0.0  10068  3400 pts/0    R+   10:00   0:00 ps aux

# PID 1 が bash になっている!
root@host:~# echo $$
1

init プロセスとしての PID 1

PID namespace 内の PID 1 は特別な役割を持つ:

  1. シグナルの特別な扱い: PID 1 は SIGKILL と SIGSTOP 以外のシグナルをデフォルトで無視する
  2. 孤児プロセスの親: namespace 内で親が死んだプロセスは PID 1 に引き取られる
  3. namespace の終了: PID 1 が終了すると、namespace 内のすべてのプロセスが SIGKILL される
#define _GNU_SOURCE
#include <sched.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/mount.h>
#include <sys/wait.h>
#include <unistd.h>

#define STACK_SIZE (1024 * 1024)
static char child_stack[STACK_SIZE];

// PID namespace 内の init プロセス
static int container_init(void *arg) {
    printf("[init] PID namespace 内の init プロセス (PID=%d)\n", getpid());
    
    // /proc を再マウント
    if (mount("proc", "/proc", "proc", 0, NULL) == -1) {
        perror("mount /proc");
    }
    
    // シグナルハンドラを設定
    signal(SIGCHLD, SIG_IGN);  // ゾンビ回避
    
    // 子プロセスを生成
    pid_t child = fork();
    if (child == 0) {
        printf("[worker] ワーカープロセス (PID=%d)\n", getpid());
        sleep(2);
        printf("[worker] ワーカー終了\n");
        exit(0);
    }
    
    printf("[init] ワーカーの PID = %d\n", child);
    
    // init プロセスはすべての子が終了するまで待機
    for (;;) {
        int status;
        pid_t pid = waitpid(-1, &status, 0);
        if (pid == -1) {
            break;  // もう子プロセスがない
        }
        printf("[init] 子プロセス %d が終了 (status=%d)\n", pid, status);
    }
    
    printf("[init] すべての子プロセスが終了。init も終了します。\n");
    return 0;
}

int main() {
    printf("[parent] ホストの PID = %d\n", getpid());
    
    pid_t child = clone(container_init,
                        child_stack + STACK_SIZE,
                        CLONE_NEWPID | CLONE_NEWNS | SIGCHLD,
                        NULL);
    
    if (child == -1) {
        perror("clone");
        exit(1);
    }
    
    printf("[parent] コンテナの init (ホストの PID=%d)\n", child);
    waitpid(child, NULL, 0);
    printf("[parent] コンテナ終了\n");
    
    return 0;
}

ネストされた PID Namespace

PID namespace はネスト可能であり、最大32レベルまでネストできる。

# レベル1
$ sudo unshare --pid --fork --mount-proc /bin/bash
root@host:~# echo "Level 1: PID = $$"
Level 1: PID = 1

# レベル2 (レベル1内から)
root@host:~# unshare --pid --fork --mount-proc /bin/bash
root@host:~# echo "Level 2: PID = $$"
Level 2: PID = 1

# 各レベルから見える PID は独立

/proc ファイルシステムと PID Namespace

# PID namespace 内の /proc を適切にマウント
$ sudo unshare --pid --fork /bin/bash

# この時点では /proc はホストのもの
root@host:~# ls /proc/1/comm
systemd

# /proc を再マウント
root@host:~# mount -t proc proc /proc

# 今度は namespace 内の PID 1 が見える
root@host:~# cat /proc/1/comm
bash

# /proc/[pid]/status で namespace 情報を確認
root@host:~# cat /proc/1/status | grep -i ns
NSpid:  1
NStgid: 1
NSsid:  0

Network Namespace 詳細

Network Namespace は、ネットワークスタック全体を分離する。各 Network Namespace は独自のネットワークインターフェース、ルーティングテーブル、iptables ルール、ソケットを持つ。

基本操作 (ip netns)

# Network namespace の作成
$ sudo ip netns add ns1
$ sudo ip netns add ns2

# namespace の一覧
$ ip netns list
ns2
ns1

# namespace 内でコマンド実行
$ sudo ip netns exec ns1 ip addr
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00

# namespace 内でループバックを有効化
$ sudo ip netns exec ns1 ip link set lo up
$ sudo ip netns exec ns1 ip addr show lo
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever

veth ペアによる namespace 間通信

# veth ペアの作成
$ sudo ip link add veth0 type veth peer name veth1

# 各 veth を異なる namespace に配置
$ sudo ip link set veth0 netns ns1
$ sudo ip link set veth1 netns ns2

# IP アドレスの設定
$ sudo ip netns exec ns1 ip addr add 10.0.0.1/24 dev veth0
$ sudo ip netns exec ns2 ip addr add 10.0.0.2/24 dev veth1

# インターフェースの有効化
$ sudo ip netns exec ns1 ip link set veth0 up
$ sudo ip netns exec ns2 ip link set veth1 up

# 疎通確認
$ sudo ip netns exec ns1 ping -c 3 10.0.0.2
PING 10.0.0.2 (10.0.0.2) 56(84) bytes of data.
64 bytes from 10.0.0.2: icmp_seq=1 ttl=64 time=0.042 ms
64 bytes from 10.0.0.2: icmp_seq=2 ttl=64 time=0.035 ms
64 bytes from 10.0.0.2: icmp_seq=3 ttl=64 time=0.034 ms

# ルーティングテーブルの確認
$ sudo ip netns exec ns1 ip route
10.0.0.0/24 dev veth0 proto kernel scope link src 10.0.0.1

ブリッジネットワーク構成

複数の namespace をブリッジで接続し、外部ネットワークへのアクセスを提供する構成:

┌─────────────────────────────────────────────────┐
│                ホスト                             │
│                                                  │
│  ┌──────┐    ┌──────────┐    ┌──────┐          │
│  │ ns1  │    │  br0     │    │ ns2  │          │
│  │      │    │ 10.0.0.1 │    │      │          │
│  │veth1a├────┤veth1b    │    │veth2a├───┐      │
│  │.10   │    │   veth2b─┼────┤.20   │   │      │
│  └──────┘    │          │    └──────┘   │      │
│              └────┬─────┘              │      │
│                   │                     │      │
│              eth0 (NAT)                        │
│              ↕ インターネット                    │
└─────────────────────────────────────────────────┘
#!/bin/bash
# ブリッジネットワーク構成スクリプト

set -e

# クリーンアップ
cleanup() {
    ip netns del ns1 2>/dev/null || true
    ip netns del ns2 2>/dev/null || true
    ip link del br0 2>/dev/null || true
}

# namespace 作成
ip netns add ns1
ip netns add ns2

# ブリッジの作成
ip link add br0 type bridge
ip addr add 10.0.0.1/24 dev br0
ip link set br0 up

# ns1 用の veth ペア
ip link add veth1a type veth peer name veth1b
ip link set veth1a netns ns1
ip link set veth1b master br0
ip link set veth1b up
ip netns exec ns1 ip addr add 10.0.0.10/24 dev veth1a
ip netns exec ns1 ip link set veth1a up
ip netns exec ns1 ip link set lo up
ip netns exec ns1 ip route add default via 10.0.0.1

# ns2 用の veth ペア
ip link add veth2a type veth peer name veth2b
ip link set veth2a netns ns2
ip link set veth2b master br0
ip link set veth2b up
ip netns exec ns2 ip addr add 10.0.0.20/24 dev veth2a
ip netns exec ns2 ip link set veth2a up
ip netns exec ns2 ip link set lo up
ip netns exec ns2 ip route add default via 10.0.0.1

# IP フォワーディングの有効化
echo 1 > /proc/sys/net/ipv4/ip_forward

# NAT の設定 (eth0 は外部インターフェース)
iptables -t nat -A POSTROUTING -s 10.0.0.0/24 -o eth0 -j MASQUERADE
iptables -A FORWARD -i br0 -o eth0 -j ACCEPT
iptables -A FORWARD -i eth0 -o br0 -m state --state RELATED,ESTABLISHED -j ACCEPT

echo "ネットワーク構成完了"
echo "ns1: 10.0.0.10"
echo "ns2: 10.0.0.20"
echo "ブリッジ: 10.0.0.1"

# テスト
echo "--- ns1 から ns2 への疎通確認 ---"
ip netns exec ns1 ping -c 2 10.0.0.20

echo "--- ns1 から外部への疎通確認 ---"
ip netns exec ns1 ping -c 2 8.8.8.8

ポートフォワーディング

namespace 内のサービスにホストからアクセスするためのポートフォワーディング:

# ns1 内で Web サーバーを起動
$ sudo ip netns exec ns1 python3 -m http.server 80 &

# ホストのポート 8080 を ns1 のポート 80 にフォワード
$ sudo iptables -t nat -A PREROUTING -p tcp --dport 8080 \
    -j DNAT --to-destination 10.0.0.10:80
$ sudo iptables -A FORWARD -p tcp -d 10.0.0.10 --dport 80 -j ACCEPT

Network Namespace のモニタリング

# namespace 内のネットワーク統計
$ sudo ip netns exec ns1 ss -tlnp
State  Recv-Q Send-Q Local Address:Port  Peer Address:Port
LISTEN 0      128    0.0.0.0:80          0.0.0.0:*     users:(("python3",pid=1,fd=3))

# namespace 内の iptables
$ sudo ip netns exec ns1 iptables -L -n -v

# namespace 内のトラフィックキャプチャ
$ sudo ip netns exec ns1 tcpdump -i veth1a -nn

# namespace 内の接続確認
$ sudo ip netns exec ns1 curl http://10.0.0.20

Mount Namespace 詳細

Mount Namespace は、ファイルシステムのマウントポイントを分離する。各 Mount Namespace は独自のマウントテーブルを持つ。

基本操作

# 新しい Mount namespace でシェルを起動
$ sudo unshare --mount /bin/bash

# namespace 内で tmpfs をマウント
root@host:~# mkdir -p /mnt/test
root@host:~# mount -t tmpfs tmpfs /mnt/test
root@host:~# echo "namespace data" > /mnt/test/hello.txt

# 別のターミナルからはこのマウントは見えない
$ ls /mnt/test/
# (空、またはディレクトリが存在しない)

マウントプロパゲーション

マウントプロパゲーションは、マウントイベントが namespace 間でどのように伝播するかを制御する。

┌─────────────────────────────────────────┐
│            マウントプロパゲーション       │
├──────────┬──────────────────────────────┤
│ shared   │ マウント/アンマウントが双方向  │
│          │ に伝播する                    │
├──────────┼──────────────────────────────┤
│ private  │ マウントイベントは伝播しない   │
├──────────┼──────────────────────────────┤
│ slave    │ マスターからスレーブへの       │
│          │ 一方向のみ伝播               │
├──────────┼──────────────────────────────┤
│ unbindable│ private + バインドマウント    │
│          │ 不可                         │
└──────────┴──────────────────────────────┘

shared マウント

# shared マウントの設定
$ sudo mount --make-shared /mnt/shared

# 確認
$ cat /proc/self/mountinfo | grep shared
# ... shared:1 のようなフラグが見える

# shared マウントの namespace での動作
$ sudo unshare --mount /bin/bash

# 親 namespace でのマウントが子にも反映される
# 子 namespace でのマウントが親にも反映される

private マウント

# private マウントの設定
$ sudo mount --make-private /mnt/private

# private namespace 内でのマウントは外部に影響しない
$ sudo unshare --mount /bin/bash
root@host:~# mount -t tmpfs tmpfs /mnt/private/data
# この /mnt/private/data は親 namespace からは見えない

slave マウント

# slave マウントの設定
$ sudo mount --make-slave /mnt/slave

# マスター → スレーブの方向のみ伝播
# マスター(親)でマウントすると、スレーブ(子)にも反映
# スレーブ(子)でマウントしても、マスター(親)には影響なし

unbindable マウント

# unbindable マウントの設定
$ sudo mount --make-unbindable /mnt/nobind

# バインドマウントを試みるとエラー
$ sudo mount --bind /mnt/nobind /mnt/other
mount: /mnt/other: wrong fs type, bad option, bad superblock...

再帰的プロパゲーション設定

# サブツリー全体を再帰的に shared に設定
$ sudo mount --make-rshared /

# サブツリー全体を再帰的に private に設定
$ sudo mount --make-rprivate /

# サブツリー全体を再帰的に slave に設定
$ sudo mount --make-rslave /

pivot_root によるルートファイルシステムの変更

コンテナは通常 pivot_root を使用してルートファイルシステムを切り替える。

#!/bin/bash
# pivot_root を使ったコンテナ風のルートファイルシステム変更

ROOTFS="/var/lib/container/rootfs"

# 新しい Mount namespace
unshare --mount --fork /bin/bash -c "
    # ルートを private に
    mount --make-rprivate /
    
    # rootfs をバインドマウント
    mount --bind $ROOTFS $ROOTFS
    
    # old_root ディレクトリの作成
    mkdir -p $ROOTFS/.old_root
    
    # pivot_root
    cd $ROOTFS
    pivot_root . .old_root
    
    # 基本的なファイルシステムのマウント
    mount -t proc proc /proc
    mount -t sysfs sysfs /sys
    mount -t tmpfs tmpfs /tmp
    mount -t tmpfs tmpfs /run
    
    # 古いルートのアンマウント
    umount -l /.old_root
    rmdir /.old_root
    
    # コンテナ内でシェル起動
    exec /bin/bash
"

Bind Mount

# ファイルのバインドマウント
$ sudo mount --bind /etc/resolv.conf /var/lib/container/rootfs/etc/resolv.conf

# ディレクトリのバインドマウント
$ sudo mount --bind /usr/share/data /var/lib/container/rootfs/data

# 読み取り専用バインドマウント
$ sudo mount --bind /etc/passwd /var/lib/container/rootfs/etc/passwd
$ sudo mount -o remount,ro,bind /var/lib/container/rootfs/etc/passwd

# 再帰的バインドマウント
$ sudo mount --rbind /sys /var/lib/container/rootfs/sys

User Namespace 詳細

User Namespace は、UID/GID の分離とマッピングを提供する。最も重要な特徴は、非特権ユーザーが User Namespace 内で root 権限を持てることである。

基本概念

ホスト                     User Namespace
┌─────────────────┐       ┌─────────────────┐
│ UID 1000 (user) │  →    │ UID 0 (root)    │
│ GID 1000 (user) │  →    │ GID 0 (root)    │
│                 │       │                 │
│ ※ ホストでは    │       │ ※ namespace 内  │
│   一般ユーザー   │       │   では root     │
└─────────────────┘       └─────────────────┘

User Namespace の作成

# 非特権ユーザーとして User Namespace を作成
$ unshare --user --map-root-user /bin/bash

# namespace 内では root
bash-5.1# id
uid=0(root) gid=0(root) groups=0(root),65534(nogroup)

bash-5.1# whoami
root

# しかし実際にはホストの一般ユーザー権限

UID/GID マッピング

# マッピングの確認
$ cat /proc/self/uid_map
         0       1000          1
# namespace 内の UID 0 がホストの UID 1000 にマッピング

$ cat /proc/self/gid_map
         0       1000          1

マッピングファイルの形式:

<namespace内のID>  <ホストのID>  <範囲>

プログラムによる UID/GID マッピング

#define _GNU_SOURCE
#include <fcntl.h>
#include <sched.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>

#define STACK_SIZE (1024 * 1024)
static char child_stack[STACK_SIZE];

static int child_fn(void *arg) {
    // 親がマッピングを設定するのを待つ
    sleep(1);
    
    printf("Namespace 内:\n");
    printf("  UID: %d\n", getuid());
    printf("  GID: %d\n", getgid());
    printf("  EUID: %d\n", geteuid());
    printf("  EGID: %d\n", getegid());
    
    // namespace 内での操作をテスト
    system("id");
    
    return 0;
}

void write_file(const char *path, const char *content) {
    int fd = open(path, O_WRONLY);
    if (fd == -1) {
        perror(path);
        return;
    }
    write(fd, content, strlen(content));
    close(fd);
}

int main() {
    pid_t child = clone(child_fn,
                        child_stack + STACK_SIZE,
                        CLONE_NEWUSER | SIGCHLD,
                        NULL);
    
    if (child == -1) {
        perror("clone");
        exit(1);
    }
    
    char path[256];
    char mapping[256];
    
    // setgroups を deny に設定 (gid_map 書き込み前に必要)
    snprintf(path, sizeof(path), "/proc/%d/setgroups", child);
    write_file(path, "deny");
    
    // UID マッピング: namespace 内の 0 → ホストの 1000, 範囲 65536
    snprintf(path, sizeof(path), "/proc/%d/uid_map", child);
    snprintf(mapping, sizeof(mapping), "0 1000 65536\n");
    write_file(path, mapping);
    
    // GID マッピング
    snprintf(path, sizeof(path), "/proc/%d/gid_map", child);
    snprintf(mapping, sizeof(mapping), "0 1000 65536\n");
    write_file(path, mapping);
    
    waitpid(child, NULL, 0);
    return 0;
}

subordinate UID/GID (/etc/subuid, /etc/subgid)

ルートレスコンテナでは、/etc/subuid/etc/subgid で広い範囲の UID/GID マッピングを設定する。

# /etc/subuid の内容
$ cat /etc/subuid
user1:100000:65536
user2:165536:65536

# /etc/subgid の内容
$ cat /etc/subgid
user1:100000:65536
user2:165536:65536

# 意味: user1 は namespace 内で 100000-165535 の UID を使用可能

newuidmap / newgidmap

# User Namespace 内の UID マッピングを設定
$ unshare --user /bin/bash &
$ CHILD_PID=$!

# newuidmap でマッピング設定
$ newuidmap $CHILD_PID 0 1000 1 1 100000 65536
# namespace UID 0 → ホスト 1000 (1個)
# namespace UID 1-65536 → ホスト 100000-165535 (65536個)

$ newgidmap $CHILD_PID 0 1000 1 1 100000 65536

User Namespace とケイパビリティ

User Namespace 内の root は、その namespace 内でのみケイパビリティを持つ。

# User Namespace を作成
$ unshare --user --map-root-user /bin/bash

# namespace 内のケイパビリティを確認
bash-5.1# cat /proc/self/status | grep Cap
CapInh: 0000000000000000
CapPrm: 000001ffffffffff
CapEff: 000001ffffffffff
CapBnd: 000001ffffffffff
CapAmb: 0000000000000000

# すべてのケイパビリティを持っているように見えるが、
# これは User Namespace 内でのみ有効

# ホストのファイルへの特権操作はできない
bash-5.1# chown root:root /etc/shadow
chown: changing ownership of '/etc/shadow': Operation not permitted

UTS Namespace

UTS (Unix Time-Sharing) Namespace は、ホスト名とドメイン名を分離する。

# 現在のホスト名を確認
$ hostname
production-server-01

# 新しい UTS namespace を作成
$ sudo unshare --uts /bin/bash

# namespace 内でホスト名を変更
root@production-server-01:~# hostname container-01
root@production-server-01:~# hostname
container-01

# ドメイン名も変更可能
root@container-01:~# domainname container.local

# 別のターミナルでホストのホスト名は変わっていないことを確認
$ hostname
production-server-01

UTS Namespace のプログラム例

#define _GNU_SOURCE
#include <sched.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>

int main() {
    char hostname[256];
    
    // 現在のホスト名
    gethostname(hostname, sizeof(hostname));
    printf("Before unshare: %s\n", hostname);
    
    // UTS namespace を分離
    if (unshare(CLONE_NEWUTS) == -1) {
        perror("unshare");
        return 1;
    }
    
    // 新しい namespace でホスト名を変更
    sethostname("isolated-host", 13);
    gethostname(hostname, sizeof(hostname));
    printf("After unshare: %s\n", hostname);
    
    return 0;
}

IPC Namespace

IPC (Inter-Process Communication) Namespace は、System V IPC オブジェクト (共有メモリ、セマフォ、メッセージキュー) と POSIX メッセージキューを分離する。

# IPC リソースの確認
$ ipcs
------ Message Queues --------
key        msqid      owner      perms      used-bytes   messages

------ Shared Memory Segments --------
key        shmid      owner      perms      bytes      nattch     status
0x00000000 0          root       644        80         2

------ Semaphore Arrays --------
key        semid      owner      perms      nsems

# 新しい IPC namespace を作成
$ sudo unshare --ipc /bin/bash

# namespace 内では IPC リソースは空
root@host:~# ipcs
------ Message Queues --------
key        msqid      owner      perms      used-bytes   messages

------ Shared Memory Segments --------
key        shmid      owner      perms      bytes      nattch     status

------ Semaphore Arrays --------
key        semid      owner      perms      nsems

# namespace 内で共有メモリを作成
root@host:~# ipcmk -M 1024
Shared memory id: 0

root@host:~# ipcs -m
------ Shared Memory Segments --------
key        shmid      owner      perms      bytes      nattch     status
0x684c4149 0          root       644        1024       0

Cgroup Namespace

Cgroup Namespace は、プロセスから見える cgroup 階層のルートを仮想化する。

# cgroup namespace の作成
$ sudo unshare --cgroup /bin/bash

# namespace 内では、自分の cgroup がルートに見える
root@host:~# cat /proc/self/cgroup
0::/

# ホストから見ると実際のパスが見える
$ cat /proc/$$/cgroup
0::/user.slice/user-1000.slice/session-1.scope

Cgroup Namespace とコンテナ

# コンテナプロセスの cgroup (ホストから見た場合)
$ cat /proc/$(docker inspect --format '{{.State.Pid}}' my_container)/cgroup
0::/system.slice/docker-abc123.scope

# コンテナ内から見た場合
$ docker exec my_container cat /proc/self/cgroup
0::/

Time Namespace

Time Namespace (Linux 5.6 以降) は、CLOCK_MONOTONICCLOCK_BOOTTIME を分離する。

# Time namespace の確認
$ ls -la /proc/self/ns/time
lrwxrwxrwx 1 root root 0 Apr 10 10:00 /proc/self/ns/time -> 'time:[4026531834]'

# Time namespace のオフセット確認
$ cat /proc/self/timens_offsets
monotonic           0         0
boottime            0         0

Time Namespace の使用例

# 新しい Time namespace を作成しオフセットを設定
$ sudo unshare --time /bin/bash

# オフセットを設定 (monotonic 時計を1日進める)
root@host:~# echo "monotonic 86400 0" > /proc/self/timens_offsets

# この namespace 内で fork したプロセスは、
# CLOCK_MONOTONIC が1日進んだ状態になる

Time Namespace はコンテナのマイグレーションやチェックポイント/リストア (CRIU) に有用である。


Cgroups v1 アーキテクチャ

概要

Cgroups v1 は Linux 2.6.24 (2008年) で導入された。複数の独立した階層構造を持ち、各階層に1つ以上のサブシステム (コントローラ) をアタッチする設計である。

基本アーキテクチャ

Cgroups v1 アーキテクチャ:

階層1 (cpu,cpuacct)        階層2 (memory)         階層3 (blkio)
┌─────────────┐           ┌─────────────┐       ┌─────────────┐
│ / (ルート)   │           │ / (ルート)   │       │ / (ルート)   │
├─────┬───────┤           ├─────┬───────┤       ├─────┬───────┤
│ grpA│ grpB  │           │ grpX│ grpY  │       │ grp1│ grp2  │
│     │       │           │     │       │       │     │       │
└─────┴───────┘           └─────┴───────┘       └─────┴───────┘

※ 各階層は独立したツリー構造
※ プロセスは各階層の1つの cgroup に属する

サブシステム (コントローラ) 一覧

コントローラ説明
cpuCPU 時間の配分制御
cpuacctCPU 使用時間の記録
cpusetCPU とメモリノードの割り当て
memoryメモリ使用量の制限と記録
blkioブロック I/O の制限
devicesデバイスアクセスの制御
freezerプロセスの一時停止/再開
net_clsネットワークパケットのクラス分け
net_prioネットワーク優先度
pidsプロセス数の制限
perf_eventパフォーマンスイベント
hugetlbHugeTLB の制限
rdmaRDMA リソースの制限

Cgroups v1 の操作

# マウントされている cgroup を確認
$ mount | grep cgroup
tmpfs on /sys/fs/cgroup type tmpfs (ro,nosuid,nodev,noexec,mode=755)
cgroup2 on /sys/fs/cgroup/unified type cgroup2 (rw,nosuid,nodev,noexec,relatime)
cgroup on /sys/fs/cgroup/systemd type cgroup (rw,nosuid,nodev,noexec,relatime,xattr,name=systemd)
cgroup on /sys/fs/cgroup/cpu,cpuacct type cgroup (rw,nosuid,nodev,noexec,relatime,cpu,cpuacct)
cgroup on /sys/fs/cgroup/memory type cgroup (rw,nosuid,nodev,noexec,relatime,memory)
cgroup on /sys/fs/cgroup/blkio type cgroup (rw,nosuid,nodev,noexec,relatime,blkio)
cgroup on /sys/fs/cgroup/pids type cgroup (rw,nosuid,nodev,noexec,relatime,pids)
cgroup on /sys/fs/cgroup/devices type cgroup (rw,nosuid,nodev,noexec,relatime,devices)

# cgroup の作成
$ sudo mkdir /sys/fs/cgroup/memory/mygroup
$ sudo mkdir /sys/fs/cgroup/cpu/mygroup

# cgroup の設定
$ echo 100M | sudo tee /sys/fs/cgroup/memory/mygroup/memory.limit_in_bytes
$ echo 50000 | sudo tee /sys/fs/cgroup/cpu/mygroup/cpu.cfs_quota_us
$ echo 100000 | sudo tee /sys/fs/cgroup/cpu/mygroup/cpu.cfs_period_us

# プロセスを cgroup に追加
$ echo $$ | sudo tee /sys/fs/cgroup/memory/mygroup/cgroup.procs
$ echo $$ | sudo tee /sys/fs/cgroup/cpu/mygroup/cgroup.procs

# cgroup 内のプロセスを確認
$ cat /sys/fs/cgroup/memory/mygroup/cgroup.procs

# cgroup の統計を確認
$ cat /sys/fs/cgroup/memory/mygroup/memory.usage_in_bytes
$ cat /sys/fs/cgroup/memory/mygroup/memory.stat
$ cat /sys/fs/cgroup/cpu/mygroup/cpuacct.usage

Cgroups v1 の CPU 制御

# CPU 帯域幅制御 (CFS Bandwidth Control)
# 50% の CPU 使用率に制限
$ echo 50000 | sudo tee /sys/fs/cgroup/cpu/mygroup/cpu.cfs_quota_us
$ echo 100000 | sudo tee /sys/fs/cgroup/cpu/mygroup/cpu.cfs_period_us

# CPU シェア (相対的な重み)
$ echo 512 | sudo tee /sys/fs/cgroup/cpu/mygroup/cpu.shares
# デフォルトは 1024

# リアルタイム帯域幅
$ echo 950000 | sudo tee /sys/fs/cgroup/cpu/mygroup/cpu.rt_runtime_us
$ echo 1000000 | sudo tee /sys/fs/cgroup/cpu/mygroup/cpu.rt_period_us

Cgroups v1 の Memory 制御

# メモリ制限
$ echo 256M | sudo tee /sys/fs/cgroup/memory/mygroup/memory.limit_in_bytes

# メモリ + スワップ制限
$ echo 512M | sudo tee /sys/fs/cgroup/memory/mygroup/memory.memsw.limit_in_bytes

# ソフトリミット
$ echo 128M | sudo tee /sys/fs/cgroup/memory/mygroup/memory.soft_limit_in_bytes

# OOM 制御
$ echo 1 | sudo tee /sys/fs/cgroup/memory/mygroup/memory.oom_control
# 1 = OOM killer を無効化

# スワップ使用率
$ cat /sys/fs/cgroup/memory/mygroup/memory.swappiness

# メモリ統計
$ cat /sys/fs/cgroup/memory/mygroup/memory.stat
cache 12345678
rss 87654321
rss_huge 0
shmem 0
mapped_file 1234567
dirty 4096
writeback 0
pgpgin 12345
pgpgout 6789
pgfault 23456
pgmajfault 12
...

Cgroups v1 の問題点

  1. 複数階層の複雑さ: 各コントローラが異なる階層にある場合、管理が複雑になる
  2. 一貫性のない API: コントローラごとに API が異なる
  3. スレッドの粒度: プロセス単位でしか制御できない(一部の例外を除く)
  4. 内部ノードの制約がない: 内部ノードにもプロセスを配置でき、リソース管理が混乱する

Cgroups v2 アーキテクチャ

概要

Cgroups v2 は Linux 4.5 (2016年) で導入され、v1 の問題点を解決するために設計された。最も大きな違いは統一階層 (unified hierarchy) を採用した点である。

v1 と v2 の比較

Cgroups v1:                    Cgroups v2:
  複数の独立した階層              単一の統一階層
                               
  階層1(cpu)  階層2(memory)      / (ルート)
  ┌───┐      ┌───┐             ├── group-a/
  │ A │      │ X │             │   ├── cpu.max
  │ B │      │ Y │             │   ├── memory.max
  └───┘      └───┘             │   └── io.max
                               └── group-b/
  管理が複雑                       ├── cpu.max
                                   ├── memory.max
                                   └── io.max
                               
                               すべてのコントローラが
                               1つの階層に統合

Cgroups v2 のマウント

# cgroup v2 の確認
$ mount | grep cgroup2
cgroup2 on /sys/fs/cgroup type cgroup2 (rw,nosuid,nodev,noexec,relatime)

# v2 のみを使用するように設定 (カーネルパラメータ)
# GRUB の設定例:
# GRUB_CMDLINE_LINUX="systemd.unified_cgroup_hierarchy=1"

# 手動マウント
$ mount -t cgroup2 none /sys/fs/cgroup

Cgroups v2 の基本操作

# 利用可能なコントローラの確認
$ cat /sys/fs/cgroup/cgroup.controllers
cpu io memory pids rdma misc

# 子 cgroup でコントローラを有効化
$ echo "+cpu +memory +io +pids" | sudo tee /sys/fs/cgroup/cgroup.subtree_control

# cgroup の作成
$ sudo mkdir /sys/fs/cgroup/mygroup

# mygroup 内で有効なコントローラを確認
$ cat /sys/fs/cgroup/mygroup/cgroup.controllers
cpu io memory pids

# プロセスの追加
$ echo $$ | sudo tee /sys/fs/cgroup/mygroup/cgroup.procs

# cgroup の種類を確認
$ cat /sys/fs/cgroup/mygroup/cgroup.type
domain

# cgroup 内のプロセス数
$ cat /sys/fs/cgroup/mygroup/cgroup.procs | wc -l

# cgroup の統計
$ cat /sys/fs/cgroup/mygroup/cgroup.stat
nr_descendants 0
nr_dying_descendants 0

# cgroup イベント
$ cat /sys/fs/cgroup/mygroup/cgroup.events
populated 1
frozen 0

No Internal Process 制約

Cgroups v2 では、コントローラが有効な内部ノードにプロセスを配置できない (no internal process constraint)。

有効な構成:              無効な構成:
┌─────────┐             ┌─────────┐
│ parent  │             │ parent  │ ← プロセスP
│ (empty) │             │ +cpu    │
├────┬────┤             ├────┬────┤
│ A  │ B  │             │ A  │ B  │
│ P1 │ P2 │             │ P1 │ P2 │
└────┴────┘             └────┴────┘
                        
プロセスは葉の          コントローラが有効な
cgroup にのみ配置       内部ノードにプロセスは
                       配置できない
# 子 cgroup のコントローラを有効化
$ echo "+cpu +memory" | sudo tee /sys/fs/cgroup/parent/cgroup.subtree_control

# 子 cgroup を作成
$ sudo mkdir /sys/fs/cgroup/parent/child

# parent にプロセスを追加しようとするとエラー
$ echo $$ | sudo tee /sys/fs/cgroup/parent/cgroup.procs
# エラー: device or resource busy (コントローラが有効な場合)

threaded cgroup

Cgroups v2 ではスレッドレベルの制御が可能である。

# threaded cgroup に変更
$ echo threaded | sudo tee /sys/fs/cgroup/mygroup/child/cgroup.type

# スレッドをスレッド cgroup に追加
$ echo $TID | sudo tee /sys/fs/cgroup/mygroup/child/cgroup.threads

CPU コントローラ

Cgroups v2 CPU コントローラ

cpu.max (帯域幅制限)

# cpu.max の形式: $MAX $PERIOD (マイクロ秒)
# デフォルト: "max 100000" (制限なし)

# 50% の CPU に制限 (100ms 周期のうち 50ms)
$ echo "50000 100000" | sudo tee /sys/fs/cgroup/mygroup/cpu.max

# 200% (2コア分) の CPU
$ echo "200000 100000" | sudo tee /sys/fs/cgroup/mygroup/cpu.max

# 制限なし
$ echo "max 100000" | sudo tee /sys/fs/cgroup/mygroup/cpu.max

# 現在の設定を確認
$ cat /sys/fs/cgroup/mygroup/cpu.max
50000 100000

cpu.weight (相対的な重み)

# cpu.weight: 1-10000 (デフォルト 100)
# v1 の cpu.shares に相当

# 通常の重み
$ echo 100 | sudo tee /sys/fs/cgroup/mygroup/cpu.weight

# 高優先度
$ echo 500 | sudo tee /sys/fs/cgroup/high-priority/cpu.weight

# 低優先度
$ echo 10 | sudo tee /sys/fs/cgroup/low-priority/cpu.weight

# nice 値との対応
$ cat /sys/fs/cgroup/mygroup/cpu.weight.nice
0

cpu.stat (統計情報)

$ cat /sys/fs/cgroup/mygroup/cpu.stat
usage_usec 1234567890
user_usec 987654321
system_usec 246913569
nr_periods 12345
nr_throttled 678
throttled_usec 901234
nr_bursts 0
burst_usec 0

各フィールドの意味:

フィールド説明
usage_usec合計 CPU 使用時間 (マイクロ秒)
user_usecユーザー空間の CPU 時間
system_usecカーネル空間の CPU 時間
nr_periodsCPU 帯域幅の周期数
nr_throttledスロットルされた回数
throttled_usecスロットルされた合計時間
nr_burstsバースト回数
burst_usecバースト使用時間

cpu.pressure (PSI: Pressure Stall Information)

$ cat /sys/fs/cgroup/mygroup/cpu.pressure
some avg10=0.50 avg60=0.30 avg300=0.10 total=12345678
full avg10=0.10 avg60=0.05 avg300=0.02 total=2345678

# some: 少なくとも1つのタスクが CPU を待っている時間の割合
# full: すべてのタスクが CPU を待っている時間の割合

cpuset コントローラ

# 使用する CPU コアを指定
$ echo "0-3" | sudo tee /sys/fs/cgroup/mygroup/cpuset.cpus

# 使用するメモリノードを指定
$ echo "0" | sudo tee /sys/fs/cgroup/mygroup/cpuset.mems

# CPU 排他制御
$ echo "root" | sudo tee /sys/fs/cgroup/mygroup/cpuset.cpus.partition

# cpuset の有効設定を確認
$ cat /sys/fs/cgroup/mygroup/cpuset.cpus.effective
0-3
$ cat /sys/fs/cgroup/mygroup/cpuset.mems.effective
0

実践例: CPU 制限のテスト

# テスト用の cgroup を作成
$ sudo mkdir /sys/fs/cgroup/cpu-test
$ echo "+cpu" | sudo tee /sys/fs/cgroup/cgroup.subtree_control
$ sudo mkdir /sys/fs/cgroup/cpu-test/worker

# CPU を 25% に制限
$ echo "25000 100000" | sudo tee /sys/fs/cgroup/cpu-test/worker/cpu.max

# CPU 負荷テスト (stress コマンド)
$ sudo sh -c 'echo $$ > /sys/fs/cgroup/cpu-test/worker/cgroup.procs && stress --cpu 4 --timeout 30'

# 別のターミナルで監視
$ watch -n 1 cat /sys/fs/cgroup/cpu-test/worker/cpu.stat

# top で確認 (CPU 使用率が約 25% に制限される)
$ top -p $(cat /sys/fs/cgroup/cpu-test/worker/cgroup.procs | head -1)

Memory コントローラ

Cgroups v2 Memory コントローラ

memory.max (ハードリミット)

# メモリのハードリミットを設定
$ echo "256M" | sudo tee /sys/fs/cgroup/mygroup/memory.max

# 無制限
$ echo "max" | sudo tee /sys/fs/cgroup/mygroup/memory.max

# バイト単位でも指定可能
$ echo 268435456 | sudo tee /sys/fs/cgroup/mygroup/memory.max

# 現在の使用量
$ cat /sys/fs/cgroup/mygroup/memory.current
12345678

memory.max を超えると、OOM killer が発動する。

memory.high (スロットリングポイント)

# ソフトリミット (このしきい値を超えるとプロセスがスロットルされる)
$ echo "200M" | sudo tee /sys/fs/cgroup/mygroup/memory.high

# memory.high を超えたプロセスはメモリ回収が積極的に行われ、
# メモリ確保が遅くなる (スロットリング)

memory.high は OOM を発生させずにメモリ使用を抑制するのに有効である。

memory.low / memory.min (メモリ保護)

# memory.min: 最低保証 (この量のメモリは絶対に回収されない)
$ echo "64M" | sudo tee /sys/fs/cgroup/mygroup/memory.min

# memory.low: ベストエフォート保護 (優先的に保護される)
$ echo "128M" | sudo tee /sys/fs/cgroup/mygroup/memory.low

memory.swap (スワップ制限)

# スワップの最大値
$ echo "100M" | sudo tee /sys/fs/cgroup/mygroup/memory.swap.max

# スワップを無効化
$ echo 0 | sudo tee /sys/fs/cgroup/mygroup/memory.swap.max

# 現在のスワップ使用量
$ cat /sys/fs/cgroup/mygroup/memory.swap.current
0

memory.stat (メモリ統計)

$ cat /sys/fs/cgroup/mygroup/memory.stat
anon 12345678
file 87654321
kernel 2345678
kernel_stack 524288
pagetables 1234567
sec_pagetables 0
percpu 123456
sock 0
vmalloc 0
shmem 4567890
zswap 0
zswapped 0
file_mapped 3456789
file_dirty 4096
file_writeback 0
swapcached 0
anon_thp 0
file_thp 0
shmem_thp 0
inactive_anon 10000000
active_anon 2345678
inactive_file 70000000
active_file 17654321
unevictable 0
slab_reclaimable 1234567
slab_unreclaimable 567890
pgfault 234567
pgmajfault 12
workingset_refault_anon 0
workingset_refault_file 0
workingset_activate_anon 0
workingset_activate_file 0
workingset_restore_anon 0
workingset_restore_file 0
workingset_nodereclaim 0
pgscan 0
pgsteal 0
pgscan_kswapd 0
pgscan_direct 0
pgsteal_kswapd 0
pgsteal_direct 0
pgactivate 12345
pgdeactivate 0
pglazyfree 0
pglazyfreed 0
thp_fault_alloc 0
thp_collapse_alloc 0

memory.pressure (PSI)

$ cat /sys/fs/cgroup/mygroup/memory.pressure
some avg10=0.00 avg60=0.00 avg300=0.00 total=0
full avg10=0.00 avg60=0.00 avg300=0.00 total=0

memory.events (OOM イベント等)

$ cat /sys/fs/cgroup/mygroup/memory.events
low 0
high 0
max 5
oom 2
oom_kill 2
oom_group_kill 0

実践例: メモリ制限のテスト

# テスト用 cgroup の作成
$ sudo mkdir -p /sys/fs/cgroup/mem-test/worker
$ echo "+memory" | sudo tee /sys/fs/cgroup/mem-test/cgroup.subtree_control

# 100MB に制限
$ echo "100M" | sudo tee /sys/fs/cgroup/mem-test/worker/memory.max
$ echo "80M" | sudo tee /sys/fs/cgroup/mem-test/worker/memory.high

# メモリ消費テスト
$ sudo sh -c 'echo $$ > /sys/fs/cgroup/mem-test/worker/cgroup.procs && \
    stress --vm 1 --vm-bytes 150M --timeout 10'
# OOM killer が発動するはず

# OOM イベントの確認
$ cat /sys/fs/cgroup/mem-test/worker/memory.events

# dmesg で OOM の詳細を確認
$ dmesg | tail -20

I/O コントローラ

Cgroups v2 I/O コントローラ

io.max (帯域幅/IOPS 制限)

# デバイスの major:minor 番号を確認
$ lsblk -o NAME,MAJ:MIN
NAME    MAJ:MIN
sda       8:0
├─sda1    8:1
└─sda2    8:2

# io.max の形式: MAJ:MIN rbps=N wbps=N riops=N wiops=N

# 読み取り帯域幅を 10MB/s に制限
$ echo "8:0 rbps=10485760" | sudo tee /sys/fs/cgroup/mygroup/io.max

# 書き込み帯域幅を 5MB/s に制限
$ echo "8:0 wbps=5242880" | sudo tee /sys/fs/cgroup/mygroup/io.max

# IOPS 制限
$ echo "8:0 riops=1000 wiops=500" | sudo tee /sys/fs/cgroup/mygroup/io.max

# 複数の制限を同時に設定
$ echo "8:0 rbps=10485760 wbps=5242880 riops=1000 wiops=500" | \
    sudo tee /sys/fs/cgroup/mygroup/io.max

# 制限解除
$ echo "8:0 rbps=max wbps=max riops=max wiops=max" | \
    sudo tee /sys/fs/cgroup/mygroup/io.max

io.weight (相対的な重み)

# グローバルウェイト (1-10000, デフォルト 100)
$ echo "default 100" | sudo tee /sys/fs/cgroup/mygroup/io.weight

# デバイスごとのウェイト
$ echo "8:0 200" | sudo tee /sys/fs/cgroup/mygroup/io.weight

io.stat (I/O 統計)

$ cat /sys/fs/cgroup/mygroup/io.stat
8:0 rbytes=1234567890 wbytes=987654321 rios=12345 wios=6789 dbytes=0 dios=0
フィールド説明
rbytes読み取りバイト数
wbytes書き込みバイト数
rios読み取り I/O 回数
wios書き込み I/O 回数
dbytes破棄バイト数
dios破棄 I/O 回数

io.pressure (PSI)

$ cat /sys/fs/cgroup/mygroup/io.pressure
some avg10=2.50 avg60=1.80 avg300=0.90 total=45678901
full avg10=1.20 avg60=0.80 avg300=0.40 total=23456789

実践例: I/O 制限のテスト

# テスト用 cgroup
$ sudo mkdir -p /sys/fs/cgroup/io-test/worker
$ echo "+io" | sudo tee /sys/fs/cgroup/io-test/cgroup.subtree_control

# 書き込みを 1MB/s に制限
$ echo "8:0 wbps=1048576" | sudo tee /sys/fs/cgroup/io-test/worker/io.max

# I/O テスト (direct I/O で)
$ sudo sh -c 'echo $$ > /sys/fs/cgroup/io-test/worker/cgroup.procs && \
    dd if=/dev/zero of=/tmp/testfile bs=1M count=100 oflag=direct 2>&1'

# 別のターミナルで監視
$ watch -n 1 'cat /sys/fs/cgroup/io-test/worker/io.stat'

# iostat で確認
$ iostat -x 1

PID コントローラ

PID コントローラは cgroup 内で作成可能なプロセス (タスク) 数を制限する。フォーク爆弾の防止に有効。

# 最大プロセス数を設定
$ echo 100 | sudo tee /sys/fs/cgroup/mygroup/pids.max

# 現在のプロセス数
$ cat /sys/fs/cgroup/mygroup/pids.current
5

# 制限到達イベント
$ cat /sys/fs/cgroup/mygroup/pids.events
max 0

# 無制限
$ echo "max" | sudo tee /sys/fs/cgroup/mygroup/pids.max

フォーク爆弾の防止テスト

# 20プロセスに制限
$ sudo mkdir -p /sys/fs/cgroup/fork-test/limited
$ echo "+pids" | sudo tee /sys/fs/cgroup/fork-test/cgroup.subtree_control
$ echo 20 | sudo tee /sys/fs/cgroup/fork-test/limited/pids.max

# 制限された cgroup 内でフォーク爆弾を試す
$ sudo sh -c 'echo $$ > /sys/fs/cgroup/fork-test/limited/cgroup.procs && \
    :(){ :|:& };:'
# 20プロセスで制限される

# イベントの確認
$ cat /sys/fs/cgroup/fork-test/limited/pids.events
max 150  # 150回制限に達した

RDMA コントローラ

RDMA (Remote Direct Memory Access) コントローラは、RDMA/IB デバイスリソースの使用を制限する。

# 利用可能な RDMA デバイスの確認
$ cat /sys/fs/cgroup/mygroup/rdma.max
mlx5_0 hca_handle=max hca_object=max

# RDMA リソースの制限
$ echo "mlx5_0 hca_handle=10 hca_object=100" | \
    sudo tee /sys/fs/cgroup/mygroup/rdma.max

# 現在の使用量
$ cat /sys/fs/cgroup/mygroup/rdma.current
mlx5_0 hca_handle=2 hca_object=15

HugeTLB コントローラ

HugeTLB コントローラは、HugePages の使用を cgroup ごとに制限する。

# HugePages のサイズ確認
$ ls /sys/fs/cgroup/mygroup/hugetlb.*
hugetlb.2MB.current
hugetlb.2MB.events
hugetlb.2MB.events.local
hugetlb.2MB.max
hugetlb.2MB.rsvd.current
hugetlb.2MB.rsvd.max
hugetlb.1GB.current
hugetlb.1GB.events
hugetlb.1GB.max

# 2MB HugePages を 512MB に制限
$ echo "536870912" | sudo tee /sys/fs/cgroup/mygroup/hugetlb.2MB.max

# 1GB HugePages を 2GB に制限
$ echo "2147483648" | sudo tee /sys/fs/cgroup/mygroup/hugetlb.1GB.max

# 現在の使用量
$ cat /sys/fs/cgroup/mygroup/hugetlb.2MB.current
0

# イベント
$ cat /sys/fs/cgroup/mygroup/hugetlb.2MB.events
max 0

Cgroups のデリゲーションとルートレスコンテナ

Cgroup デリゲーション

Cgroup デリゲーションにより、非 root ユーザーが自分の cgroup サブツリーを管理できる。

# systemd を使ったデリゲーション
$ systemd-run --user --scope --property=Delegate=yes /bin/bash

# デリゲートされた cgroup を確認
$ cat /proc/self/cgroup
0::/user.slice/user-1000.slice/user@1000.service/app.slice/run-u123.scope

# この scope 内で子 cgroup を作成可能
$ mkdir /sys/fs/cgroup/user.slice/user-1000.slice/user@1000.service/app.slice/run-u123.scope/child

# コントローラを有効化
$ echo "+cpu +memory +pids" > \
    /sys/fs/cgroup/.../run-u123.scope/cgroup.subtree_control

デリゲーションの要件

cgroup をデリゲートするには以下が必要:

  1. ディレクトリの所有権が非 root ユーザーに設定されている
  2. cgroup.procscgroup.subtree_control への書き込み権限がある
  3. cgroup.threads への書き込み権限がある (threaded cgroup の場合)
# 手動でデリゲーション設定
$ sudo mkdir /sys/fs/cgroup/delegated
$ sudo chown -R user1:user1 /sys/fs/cgroup/delegated
$ sudo chown user1:user1 /sys/fs/cgroup/delegated/cgroup.procs
$ sudo chown user1:user1 /sys/fs/cgroup/delegated/cgroup.subtree_control

ルートレスコンテナ

ルートレスコンテナは、User Namespace と cgroup デリゲーションを組み合わせて、非 root ユーザーがコンテナを実行する仕組み。

# Podman でルートレスコンテナを実行
$ podman run --rm -it alpine sh

# systemd のユーザーセッションで cgroup が管理される
$ podman info | grep cgroupManager
cgroupManager: systemd

# cgroup v2 でのデリゲーション確認
$ cat /proc/self/cgroup
0::/user.slice/user-1000.slice/user@1000.service/user.slice/libpod-abc123.scope

# ルートレスコンテナのネットワーク (slirp4netns)
$ podman run --rm alpine ip addr
# slirp4netns を使用した TAP デバイスが見える

/etc/subuid と /etc/subgid の設定

# ルートレスコンテナに必要な設定
$ grep $USER /etc/subuid
myuser:100000:65536

$ grep $USER /etc/subgid
myuser:100000:65536

# 設定がない場合は追加
$ sudo usermod --add-subuids 100000-165535 --add-subgids 100000-165535 myuser

# Podman のルートレス初期化
$ podman system migrate

コンテナランタイムにおける Namespaces と Cgroups

Docker のアーキテクチャ

Docker が使用する Namespaces と Cgroups:

docker run コマンド
    │
    ▼
dockerd (Docker デーモン)
    │
    ▼
containerd
    │
    ▼
containerd-shim-runc-v2
    │
    ▼
runc (OCI ランタイム)
    │
    ├── clone(CLONE_NEWPID | CLONE_NEWNET | CLONE_NEWNS |
    │         CLONE_NEWUTS | CLONE_NEWIPC | CLONE_NEWUSER)
    │
    ├── cgroup の作成と設定
    │   ├── cpu.max
    │   ├── memory.max
    │   ├── pids.max
    │   └── io.max
    │
    ├── pivot_root でルートファイルシステム変更
    │
    ├── ケイパビリティのドロップ
    │
    ├── seccomp フィルタの適用
    │
    └── コンテナプロセスの exec

Docker のリソース制限

# CPU 制限
$ docker run --cpus=2 --cpu-shares=512 nginx
# --cpus=2: 2コア分の CPU (cpu.max = 200000 100000)
# --cpu-shares=512: cpu.weight に変換

# メモリ制限
$ docker run --memory=256m --memory-swap=512m nginx
# --memory=256m: memory.max = 268435456
# --memory-swap=512m: memory.swap.max = 268435456

# I/O 制限
$ docker run --device-read-bps=/dev/sda:10mb \
    --device-write-bps=/dev/sda:5mb nginx

# PID 制限
$ docker run --pids-limit=100 nginx

# すべてを組み合わせ
$ docker run \
    --cpus=2 \
    --memory=256m \
    --memory-swap=512m \
    --pids-limit=100 \
    --device-read-bps=/dev/sda:10mb \
    --device-write-bps=/dev/sda:5mb \
    nginx

Docker コンテナの namespace 確認

# コンテナの PID を取得
$ CONTAINER_PID=$(docker inspect --format '{{.State.Pid}}' my_container)

# namespace の確認
$ sudo ls -la /proc/$CONTAINER_PID/ns/
total 0
lrwxrwxrwx 1 root root 0 Apr 10 10:00 cgroup -> 'cgroup:[4026532400]'
lrwxrwxrwx 1 root root 0 Apr 10 10:00 ipc -> 'ipc:[4026532398]'
lrwxrwxrwx 1 root root 0 Apr 10 10:00 mnt -> 'mnt:[4026532396]'
lrwxrwxrwx 1 root root 0 Apr 10 10:00 net -> 'net:[4026532401]'
lrwxrwxrwx 1 root root 0 Apr 10 10:00 pid -> 'pid:[4026532399]'
lrwxrwxrwx 1 root root 0 Apr 10 10:00 user -> 'user:[4026531837]'
lrwxrwxrwx 1 root root 0 Apr 10 10:00 uts -> 'uts:[4026532397]'

# cgroup の確認
$ cat /proc/$CONTAINER_PID/cgroup
0::/system.slice/docker-abc123def456.scope

# cgroup のリソース設定を確認
$ cat /sys/fs/cgroup/system.slice/docker-abc123def456.scope/cpu.max
200000 100000
$ cat /sys/fs/cgroup/system.slice/docker-abc123def456.scope/memory.max
268435456

OCI Runtime Spec

コンテナの設定は OCI Runtime Specification (config.json) で定義される:

{
    "ociVersion": "1.0.2",
    "process": {
        "terminal": true,
        "user": {
            "uid": 0,
            "gid": 0
        },
        "args": ["/bin/sh"],
        "env": [
            "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
            "TERM=xterm"
        ],
        "capabilities": {
            "bounding": ["CAP_AUDIT_WRITE", "CAP_KILL", "CAP_NET_BIND_SERVICE"],
            "effective": ["CAP_AUDIT_WRITE", "CAP_KILL", "CAP_NET_BIND_SERVICE"],
            "inheritable": ["CAP_AUDIT_WRITE", "CAP_KILL", "CAP_NET_BIND_SERVICE"],
            "permitted": ["CAP_AUDIT_WRITE", "CAP_KILL", "CAP_NET_BIND_SERVICE"],
            "ambient": ["CAP_AUDIT_WRITE", "CAP_KILL", "CAP_NET_BIND_SERVICE"]
        }
    },
    "root": {
        "path": "rootfs",
        "readonly": true
    },
    "linux": {
        "namespaces": [
            {"type": "pid"},
            {"type": "network"},
            {"type": "ipc"},
            {"type": "uts"},
            {"type": "mount"},
            {"type": "cgroup"}
        ],
        "resources": {
            "memory": {
                "limit": 268435456,
                "swap": 536870912
            },
            "cpu": {
                "shares": 1024,
                "quota": 200000,
                "period": 100000
            },
            "pids": {
                "limit": 100
            },
            "blockIO": {
                "weight": 500,
                "throttleReadBpsDevice": [
                    {"major": 8, "minor": 0, "rate": 10485760}
                ]
            }
        },
        "uidMappings": [
            {"containerID": 0, "hostID": 1000, "size": 65536}
        ],
        "gidMappings": [
            {"containerID": 0, "hostID": 1000, "size": 65536}
        ]
    }
}

Podman のルートレスコンテナ

# Podman ルートレスの仕組み
# 1. User Namespace で UID/GID マッピング
# 2. slirp4netns または pasta でネットワーク
# 3. fuse-overlayfs でストレージ
# 4. crun/runc で OCI ランタイム

# ルートレスで nginx を実行
$ podman run -d -p 8080:80 nginx

# namespace の確認
$ podman top -l hpid,huser,user,pid
HPID    HUSER   USER    PID
12345   user1   root    1
12346   user1   nginx   2
12347   user1   nginx   3

# cgroup の確認
$ podman inspect --format '{{.HostConfig.CgroupParent}}' my_container
user.slice

# リソース使用量
$ podman stats --no-stream
ID            NAME       CPU %   MEM USAGE / LIMIT  MEM %   NET I/O     BLOCK I/O
abc123def456  my_nginx   0.50%   10.5MB / 256MB     4.10%   1.2kB/0B   0B/0B

systemd と Cgroups の統合

systemd の cgroup 管理

systemd は cgroups v2 を使用してサービスのリソースを管理する。

systemd の cgroup 階層:

/sys/fs/cgroup/
├── init.scope                    # PID 1 (systemd)
├── system.slice/                 # システムサービス
│   ├── nginx.service/
│   ├── sshd.service/
│   ├── docker.service/
│   └── postgresql.service/
├── user.slice/                   # ユーザーセッション
│   └── user-1000.slice/
│       └── user@1000.service/
│           ├── app.slice/
│           └── session.slice/
└── machine.slice/                # 仮想マシン/コンテナ
    └── docker-abc123.scope/

systemd のリソース制御

# /etc/systemd/system/myservice.service
[Unit]
Description=My Service
After=network.target

[Service]
Type=simple
ExecStart=/usr/bin/myapp
Restart=always

# CPU 制御
CPUWeight=100
CPUQuota=200%
CPUAffinity=0-3
AllowedCPUs=0-3

# メモリ制御
MemoryMax=512M
MemoryHigh=400M
MemoryLow=128M
MemoryMin=64M
MemorySwapMax=256M

# I/O 制御
IOWeight=100
IODeviceWeight=/dev/sda 200
IOReadBandwidthMax=/dev/sda 10M
IOWriteBandwidthMax=/dev/sda 5M
IOReadIOPSMax=/dev/sda 1000
IOWriteIOPSMax=/dev/sda 500

# PID 制御
TasksMax=100

# その他
Delegate=yes
IPAddressAllow=10.0.0.0/8
IPAddressDeny=any

[Install]
WantedBy=multi-user.target

systemd-run でのリソース制限

# 一時的なサービスをリソース制限付きで実行
$ sudo systemd-run --property=CPUQuota=50% \
    --property=MemoryMax=256M \
    --property=TasksMax=50 \
    /usr/bin/stress --cpu 4 --timeout 60

# スコープとして実行
$ sudo systemd-run --scope --property=MemoryMax=128M \
    dd if=/dev/zero of=/dev/null bs=1M

# ユーザーとして実行
$ systemd-run --user --property=CPUQuota=25% \
    --property=MemoryMax=100M \
    /usr/bin/my_task

systemctl によるリソース確認と変更

# サービスの cgroup パスを確認
$ systemctl show nginx.service --property=ControlGroup
ControlGroup=/system.slice/nginx.service

# リソース設定の確認
$ systemctl show nginx.service --property=MemoryMax,CPUQuota,TasksMax
MemoryMax=infinity
CPUQuota=
TasksMax=4096

# リソース設定のランタイム変更
$ sudo systemctl set-property nginx.service CPUQuota=150%
$ sudo systemctl set-property nginx.service MemoryMax=512M
$ sudo systemctl set-property nginx.service TasksMax=200

# 設定の永続化 (自動的に drop-in ファイルが作成される)
$ ls /etc/systemd/system/nginx.service.d/
50-CPUQuota.conf  50-MemoryMax.conf  50-TasksMax.conf

$ cat /etc/systemd/system/nginx.service.d/50-MemoryMax.conf
[Service]
MemoryMax=536870912

ツールリファレンス

unshare コマンド

# 主要なオプション
$ unshare [オプション] [プログラム [引数...]]

# オプション:
#   -m, --mount          Mount namespace
#   -u, --uts            UTS namespace
#   -i, --ipc            IPC namespace
#   -n, --net            Network namespace
#   -p, --pid            PID namespace
#   -U, --user           User namespace
#   -C, --cgroup         Cgroup namespace
#   -T, --time           Time namespace
#   -f, --fork           unshare 後に fork
#   --mount-proc         PID namespace 用に /proc を再マウント
#   --map-root-user      User namespace で root にマッピング
#   --map-current-user   User namespace で現在のユーザーにマッピング
#   -r, --map-root-user  --map-root-user のショートカット
#   --keep-caps          ケイパビリティを保持

# 実用例:
# 完全に分離された環境
$ sudo unshare --pid --net --mount --uts --ipc --cgroup \
    --fork --mount-proc /bin/bash

# ネットワーク分離のみ
$ sudo unshare --net /bin/bash

# User namespace を使った非特権分離
$ unshare --user --map-root-user --pid --fork --mount-proc /bin/bash

nsenter コマンド

# 主要なオプション
$ nsenter [オプション] [プログラム [引数...]]

# オプション:
#   -t, --target <pid>   ターゲットプロセス
#   -a, --all            すべての namespace に入る
#   -m, --mount          Mount namespace
#   -u, --uts            UTS namespace
#   -i, --ipc            IPC namespace
#   -n, --net            Network namespace
#   -p, --pid            PID namespace
#   -U, --user           User namespace
#   -C, --cgroup         Cgroup namespace
#   -T, --time           Time namespace

# 実用例:
# Docker コンテナの namespace に入る
$ sudo nsenter --target $(docker inspect --format '{{.State.Pid}}' my_container) --all

# ネットワーク namespace のみ
$ sudo nsenter -t $PID -n ip addr show

# 特定の namespace ファイルを指定
$ sudo nsenter --net=/var/run/netns/ns1 ip addr show

lsns コマンド

# すべての namespace を表示
$ lsns

# 特定の種類のみ
$ lsns -t pid
$ lsns -t net
$ lsns -t mnt

# JSON 形式
$ lsns -J

# 特定プロセスの namespace
$ lsns -p 1

# 出力フィールドの指定
$ lsns -o NS,TYPE,NPROCS,PID,USER,COMMAND

cgroupfs の操作

# cgroup v2 のルート
$ ls /sys/fs/cgroup/
cgroup.controllers
cgroup.max.depth
cgroup.max.descendants
cgroup.procs
cgroup.stat
cgroup.subtree_control
cgroup.threads
cgroup.type
cpu.pressure
cpu.stat
cpuset.cpus.effective
cpuset.mems.effective
io.pressure
io.stat
memory.numa_stat
memory.pressure
memory.stat
system.slice/
user.slice/
init.scope/

# cgroup の作成
$ sudo mkdir /sys/fs/cgroup/mytest

# コントローラの有効化
$ echo "+cpu +memory +io +pids" | sudo tee /sys/fs/cgroup/cgroup.subtree_control

# プロセスの追加
$ echo $PID | sudo tee /sys/fs/cgroup/mytest/cgroup.procs

# cgroup の削除 (プロセスがいない場合)
$ sudo rmdir /sys/fs/cgroup/mytest

# cgroup の階層を確認
$ find /sys/fs/cgroup -name "cgroup.procs" -exec sh -c \
    'echo "=== {} ===" && cat {}' \;

systemd-cgls

# cgroup の階層をツリー表示
$ systemd-cgls
Control group /:
-.slice
├─init.scope
│ └─1 /sbin/init
├─system.slice
│ ├─nginx.service
│ │ ├─1000 nginx: master process
│ │ ├─1001 nginx: worker process
│ │ └─1002 nginx: worker process
│ ├─sshd.service
│ │ └─500 sshd: /usr/sbin/sshd
│ └─docker.service
│   └─800 /usr/bin/dockerd
└─user.slice
  └─user-1000.slice
    └─session-1.scope
      ├─2000 bash
      └─2001 systemd-cgls

# 特定のスライスのみ
$ systemd-cgls /system.slice

# 特定のサービス
$ systemd-cgls /system.slice/nginx.service

systemd-cgtop

# リアルタイムのリソース使用量
$ systemd-cgtop
Control Group          Tasks   %CPU   Memory  Input/s Output/s
/                        245   12.3   1.2G       -       -
/system.slice             80    8.5   800M       -       -
/system.slice/nginx        5    2.1   100M       -       -
/system.slice/docker      15    3.2   300M       -       -
/user.slice               50    1.5   200M       -       -

# 更新間隔の指定 (秒)
$ systemd-cgtop -d 2

# CPU 順にソート
$ systemd-cgtop --order=cpu

# メモリ順にソート
$ systemd-cgtop --order=memory

# タスク数順
$ systemd-cgtop --order=tasks

# バッチモード (1回だけ表示)
$ systemd-cgtop -b -n 1

その他のツール

# cgroup 内のプロセスを確認
$ cat /sys/fs/cgroup/system.slice/nginx.service/cgroup.procs

# プロセスの cgroup を確認
$ cat /proc/$PID/cgroup

# cgroup のマウント情報
$ cat /proc/mounts | grep cgroup

# cgroup のメモリ情報
$ cat /proc/meminfo | grep -i cgroup

# cgroup v1/v2 のハイブリッドモード確認
$ cat /proc/cmdline | grep cgroup

# namespace の inode を確認
$ stat -L /proc/$PID/ns/net

# findmnt でマウント情報を確認
$ findmnt -t cgroup2

トラブルシューティング

OOM Killer のデバッグ

# OOM イベントの確認
$ cat /sys/fs/cgroup/system.slice/myservice.service/memory.events
low 0
high 100
max 50
oom 3
oom_kill 3
oom_group_kill 0

# dmesg で OOM の詳細
$ dmesg | grep -A 20 "Out of memory"

# OOM スコアの確認
$ cat /proc/$PID/oom_score
$ cat /proc/$PID/oom_score_adj

# OOM を防ぐ (注意して使用)
$ echo -1000 | sudo tee /proc/$PID/oom_score_adj

cgroup の制限が効かない場合

# コントローラが有効か確認
$ cat /sys/fs/cgroup/mygroup/cgroup.controllers

# 親 cgroup で subtree_control が設定されているか
$ cat /sys/fs/cgroup/cgroup.subtree_control

# プロセスが正しい cgroup に入っているか
$ cat /proc/$PID/cgroup

# cgroup v1 と v2 の混在チェック
$ mount | grep cgroup

Network Namespace のトラブルシューティング

# namespace 内のネットワーク設定を確認
$ sudo ip netns exec ns1 ip addr show
$ sudo ip netns exec ns1 ip route show
$ sudo ip netns exec ns1 iptables -L -n -v

# ARP テーブル
$ sudo ip netns exec ns1 ip neigh show

# DNS 解決の確認
$ sudo ip netns exec ns1 cat /etc/resolv.conf
$ sudo ip netns exec ns1 nslookup google.com

# ソケットの確認
$ sudo ip netns exec ns1 ss -tlnp

# namespace 間の接続テスト
$ sudo ip netns exec ns1 traceroute 10.0.0.2

Mount Namespace の問題

# マウントプロパゲーションの確認
$ cat /proc/self/mountinfo | grep -E "shared:|master:|unbindable:"

# マウントポイントの詳細
$ findmnt -o TARGET,SOURCE,FSTYPE,OPTIONS,PROPAGATION

# 孤立したマウントの確認
$ cat /proc/self/mountinfo | wc -l

セキュリティ考慮事項

Namespace のセキュリティ

# User Namespace の制限 (sysctl)
$ sysctl user.max_user_namespaces
user.max_user_namespaces = 15000

# 非特権 namespace の無効化 (セキュリティ強化)
$ sudo sysctl -w user.max_user_namespaces=0

# PID namespace の可視性制限
$ sudo mount -o remount,hidepid=2 /proc
# hidepid=2: 他ユーザーのプロセスが見えなくなる

seccomp との組み合わせ

# Docker のデフォルト seccomp プロファイルで禁止されているシステムコール
# - clone (CLONE_NEWUSER 以外)
# - mount
# - umount
# - pivot_root
# - ...

# カスタム seccomp プロファイル
$ docker run --security-opt seccomp=/path/to/profile.json nginx

ケイパビリティの最小化

# Docker でケイパビリティを削除
$ docker run --cap-drop=ALL --cap-add=NET_BIND_SERVICE nginx

# コンテナのケイパビリティを確認
$ docker exec my_container cat /proc/1/status | grep Cap

# ケイパビリティのデコード
$ capsh --decode=00000000a80425fb

AppArmor / SELinux

# Docker + AppArmor
$ docker run --security-opt apparmor=docker-default nginx

# Docker + SELinux
$ docker run --security-opt label=type:container_t nginx

# 現在の SELinux コンテキスト
$ docker exec my_container cat /proc/1/attr/current

パフォーマンスチューニング

CPU のチューニング

# CPU ピニング (cpuset)
$ echo "0-3" > /sys/fs/cgroup/mygroup/cpuset.cpus

# NUMA ノードの指定
$ echo "0" > /sys/fs/cgroup/mygroup/cpuset.mems

# CPU 帯域幅のバースト設定 (Linux 5.14+)
$ echo "50000 100000" > /sys/fs/cgroup/mygroup/cpu.max
$ echo "50000" > /sys/fs/cgroup/mygroup/cpu.max.burst
# 未使用の帯域幅をバーストで利用可能

メモリのチューニング

# memory.high を使ったグレースフルな制限
$ echo "80%" > /sys/fs/cgroup/mygroup/memory.high  # ソフトリミット
$ echo "100%" > /sys/fs/cgroup/mygroup/memory.max   # ハードリミット

# NUMA バランシング
$ echo 1 > /proc/sys/kernel/numa_balancing

# THP (Transparent Huge Pages) の制御
$ echo always > /sys/kernel/mm/transparent_hugepage/enabled

I/O のチューニング

# I/O スケジューラの確認と変更
$ cat /sys/block/sda/queue/scheduler
[mq-deadline] kyber bfq none

$ echo bfq > /sys/block/sda/queue/scheduler

# BFQ の重み設定 (io.bfq.weight)
$ echo "default 200" > /sys/fs/cgroup/mygroup/io.bfq.weight

# readahead の調整
$ blockdev --setra 2048 /dev/sda

まとめ

Namespaces まとめ

Namespace分離対象主な用途
PIDプロセス IDコンテナ内で PID 1 を持つ
Networkネットワークスタック独立したネットワーク環境
Mountマウントポイント独立したファイルシステムビュー
UTSホスト名コンテナごとのホスト名
IPCIPC オブジェクトIPC の分離
UserUID/GIDルートレスコンテナ
Cgroupcgroup ルートcgroup の可視性制限
Time単調時計マイグレーション支援

Cgroups まとめ

コントローラ制御対象主要なファイル
cpuCPU 使用率cpu.max, cpu.weight
memoryメモリ使用量memory.max, memory.high
ioブロック I/Oio.max, io.weight
pidsプロセス数pids.max
cpusetCPU/メモリノードcpuset.cpus, cpuset.mems
rdmaRDMA リソースrdma.max
hugetlbHugePageshugetlb.*.max

ベストプラクティス

  1. Cgroups v2 を使用する: 新しいシステムでは v2 を推奨
  2. systemd 経由で管理する: cgroupfs を直接操作するよりも systemd のプロパティを使用
  3. memory.high を活用する: OOM を避けつつメモリを制御
  4. PID 制限を設定する: フォーク爆弾からの保護
  5. ルートレスコンテナを検討する: セキュリティの向上
  6. PSI (Pressure Stall Information) を監視する: リソース逼迫の早期検知
  7. Delegate=yes を適切に使用する: コンテナランタイムに必要

参考文献

公式ドキュメント

書籍

  • Michael Kerrisk, "The Linux Programming Interface" (特に Chapter 28-34)
  • Liz Rice, "Container Security" (O'Reilly)

関連する man ページ

  • man 2 clone - clone システムコール
  • man 2 unshare - unshare システムコール
  • man 2 setns - setns システムコール
  • man 1 unshare - unshare コマンド
  • man 1 nsenter - nsenter コマンド
  • man 8 lsns - lsns コマンド
  • man 1 systemd-cgls - systemd-cgls コマンド
  • man 1 systemd-cgtop - systemd-cgtop コマンド
  • man 5 systemd.resource-control - systemd リソース制御

本ドキュメントは AI によって生成されました。 実際のシステムで適用する前に、公式ドキュメントを参照し、テスト環境で検証してください。