Introduction to Control Groups

개요

이번 파트는 linux insides book의 새로운 장의 첫 부분이며 파트 제목으로 추측 할 수 있듯이 이번 파트는 Linux 커널의 control groups 다른 말로는 cgroups 메커니즘을 다룹니다.

Cgroups는 리눅스 커널이 제공하는 특별한 메커니즘으로, 프로세서 시간, 그룹당 프로세스 수, 제어 그룹당 메모리 양 또는 프로세스 또는 프로세스 집합에 대한 리소스 조합과 같은 일종의 '리소스'(resources)를 할당할 수 있게 해줍니다. Cgroups는 일반적인 프로세스가 계층적인 것과 유사하게 계층적으로 구성되어 있으며 자식 cgroups는 부모로부터 특정 매개 변수 집합을 상속합니다. 그러나 실제로 그들이 완전히 동일하지는 않습니다. cgroup과 (여러 프로세스 그룹의 계층이 동시에 존재할 수 있는) 일반 프로세스와의 주요 차이점은 일반 프로세스 트리는 항상 단일이라는 점입니다. 이(cgroup)는 쉬운 단계가 아니었는데, 각 제어 그룹 계층 구조가 '서브시스템'(subsystems) 제어 그룹 집합에 붙어 있기 때문입니다.

하나의 '제어 그룹 서브시스템'(control group subsystem)은 프로세서 시간 또는 pids, 다른 말로는 control group에 대한 프로세스 수 등과 같이 한 종류의 리소스를 나타냅니다. 리눅스 커널은 다음과 같은 12 개의 control group subsystem을 지원합니다.

  • cpuset - 개별 프로세서 및 메모리 노드를 그룹의 작업에 할당합니다;

  • cpu -스케줄러를 사용하여 cgroup 작업에 프로세서 리소스에 대한 액세스 권한을 제공합니다;

  • cpuacct - 그룹 별 프로세서 사용량에 대한 보고를 생성합니다;

  • io -block devices에서 읽기/쓰기 제한을 설정합니다;

  • memory - 그룹의 작업에 의한 메모리 사용 제한을 설정합니다;

  • devices - 그룹의 작업이 장치에 액세스 할 수 있도록 합니다;

  • freezer - 그룹에서 작업을 일시중지/재개 할 수 있도록 합니다;

  • net_cls - 그룹의 작업에서 네트워크 패킷을 표시(mark) 할 수 있게 합니다;

  • net_prio - 그룹의 네트워크 인터페이스마다 네트워크 트래픽 우선 순위를 동적으로 설정하는 방법을 제공합니다;

  • perf_event - 그룹에 perf events에 대한 액세스를 제공합니다;

  • hugetlb - 그룹의 huge pages에 대한 지원을 활성화합니다;

  • pid - 그룹의 프로세스 수에 제한을 설정합니다.

이러한 각 제어 그룹 하위 시스템은 관련한 구성 옵션에 따라 다릅니다. 예를 들어 cpuset 서브시스템은CONFIG_CPUSETS 커널 구성 옵션을 통해, io 서브 시스템은 CONFIG_BLK_CGROUP 커널 구성 옵션 등을 통해 활성화해야합니다. 이러한 모든 커널 구성 옵션은 General setup → Control Group에서 찾을 수 있습니다:

proc 파일 시스템을 통해서:

$ cat /proc/cgroups 
#subsys_name    hierarchy    num_cgroups    enabled
cpuset    8    1    1
cpu    7    66    1
cpuacct    7    66    1
blkio    11    66    1
memory    9    94    1
devices    6    66    1
freezer    2    1    1
net_cls    4    1    1
perf_event    3    1    1
net_prio    4    1    1
hugetlb    10    1    1
pids    5    69    1

혹은 sysfs을 통해서 컴퓨터에서 활성화 된 제어 그룹을 볼 수 있습니다.:

$ ls -l /sys/fs/cgroup/
total 0
dr-xr-xr-x 5 root root  0 Dec  2 22:37 blkio
lrwxrwxrwx 1 root root 11 Dec  2 22:37 cpu -> cpu,cpuacct
lrwxrwxrwx 1 root root 11 Dec  2 22:37 cpuacct -> cpu,cpuacct
dr-xr-xr-x 5 root root  0 Dec  2 22:37 cpu,cpuacct
dr-xr-xr-x 2 root root  0 Dec  2 22:37 cpuset
dr-xr-xr-x 5 root root  0 Dec  2 22:37 devices
dr-xr-xr-x 2 root root  0 Dec  2 22:37 freezer
dr-xr-xr-x 2 root root  0 Dec  2 22:37 hugetlb
dr-xr-xr-x 5 root root  0 Dec  2 22:37 memory
lrwxrwxrwx 1 root root 16 Dec  2 22:37 net_cls -> net_cls,net_prio
dr-xr-xr-x 2 root root  0 Dec  2 22:37 net_cls,net_prio
lrwxrwxrwx 1 root root 16 Dec  2 22:37 net_prio -> net_cls,net_prio
dr-xr-xr-x 2 root root  0 Dec  2 22:37 perf_event
dr-xr-xr-x 5 root root  0 Dec  2 22:37 pids
dr-xr-xr-x 5 root root  0 Dec  2 22:37 systemd

이미 예상 하셨겠지만 control groups 메커니즘은 직접적인 리눅스 커널에 대한 요구사항으로가 아니라 주로 사용자 공간 요구에 대한 요구사항으로 개발 된 메커니즘입니다. control group을 사용하려면 먼저 컨트롤 그룹을 만들어야합니다. 우리는 두 가지 방법으로 cgroup을 만들 수 있습니다.

첫 번째 방법은 /sys/fs/cgroup에서 서브시스템에 서브 디렉토리(subdirectory)를 생성하고 서브 디렉토리를 생성 한 직후 자동으로 생성되는tasks 파일에 작업의 pid를 추가하는 것입니다.

두 번째 방법은 libcgroup 라이브러리 (Fedora는 libcgroup-tools)에서 utils로 cgroups을 생성/파기/관리하는 것입니다.

간단한 예를 생각해 봅시다. bash 스크립트에 따르면 현재 프로세스의 제어 터미널을 나타내는 /dev/tty 디바이스에 한 줄이 출력될 것입니다:

#!/bin/bash

while :
do
    echo "print line" > /dev/tty
    sleep 5
done

따라서 이 스크립트를 실행하면 다음과 같은 결과가 나타납니다.

$ sudo chmod +x cgroup_test_script.sh
~$ ./cgroup_test_script.sh 
print line
print line
print line
...
...
...

이제 컴퓨터에 cgroupfs가 마운트되어있는 곳으로 갑시다. 방금 봤듯이, 이 디렉토리는 /sys/fs/cgroup 디렉토리이지만 원한다면 원하는 곳 어디든 마운트 할 수 있습니다.

$ cd /sys/fs/cgroup

이제 cgroup의 작업의 장치에 대한 액세스를 허용하거나 거부하는 종류의 리소스를 나타내는 devices 서브 디렉토리로 이동하겠습니다.

# cd devices

그리고 거기에 cgroup_test_group 디렉토리를 만듭니다:

# mkdir cgroup_test_group

cgroup_test_group 디렉토리가 생성되면 다음 파일이 생성됩니다:

/sys/fs/cgroup/devices/cgroup_test_group$ ls -l
total 0
-rw-r--r-- 1 root root 0 Dec  3 22:55 cgroup.clone_children
-rw-r--r-- 1 root root 0 Dec  3 22:55 cgroup.procs
--w------- 1 root root 0 Dec  3 22:55 devices.allow
--w------- 1 root root 0 Dec  3 22:55 devices.deny
-r--r--r-- 1 root root 0 Dec  3 22:55 devices.list
-rw-r--r-- 1 root root 0 Dec  3 22:55 notify_on_release
-rw-r--r-- 1 root root 0 Dec  3 22:55 tasks

여기서 우리의 관심사는 tasksdevices.deny 파일입니다. 첫 번째 tasks 파일은cgroup_test_group에 첨부 될 프로세스의 pid를 포함할 것입니다. 두 번째로 devices.deny 파일은 거부된 장치 목록을 포함합니다. 기본적으로 새로 만든 그룹에는 장치 액세스에 대한 제한이 없습니다. 장치를 금지하려면 (이 경우/dev/tty) devices.deny에 다음 명령줄을 작성합니다:

# echo "c 5:0 w" > devices.deny

이 명령줄을 단계별로 살펴 보겠습니다. 첫 번째 c문자는 장치의 유형을 나타냅니다. 우리의 경우 /dev/ttychar device입니다. ls 명령어의 출력에서 이를 확인할 수 있습니다.

~$ ls -l /dev/tty
crw-rw-rw- 1 root tty 5, 0 Dec  3 22:48 /dev/tty

권한 목록에서 첫 번째 c 문자를 살펴보세요. 두 번째 부분 5:0은 장치의 부(minor) 번호와 주요(major) 번호입니다. 이 숫자는 ls의 결과에서도 볼 수 있습니다. 그리고 마지막 w 문자는 작업이 지정된 장치에 쓰는 것을 금지합니다. 그럼 cgroup_test_script.sh 스크립트를 시작해 봅시다 :

~$ ./cgroup_test_script.sh 
print line
print line
print line
...
...

그리고 이 프로세스의 pid를 우리의 그룹의 devices/tasks 파일에 추가합니다:

# echo $(pidof -x cgroup_test_script.sh) > /sys/fs/cgroup/devices/cgroup_test_group/tasks

그 결과는 예상한 대로:

~$ ./cgroup_test_script.sh 
print line
print line
print line
print line
print line
print line
./cgroup_test_script.sh: line 5: /dev/tty: Operation not permitted

docker 컨테이너를 실행할 때도 비슷한 상황이 발생합니다. 예를 들어:

~$ docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                    NAMES
fa2d2085cd1c        mariadb:10          "docker-entrypoint..."   12 days ago         Up 4 minutes        0.0.0.0:3306->3306/tcp   mysql-work

~$ cat /sys/fs/cgroup/devices/docker/fa2d2085cd1c8d797002c77387d2061f56fefb470892f140d0dc511bd4d9bb61/tasks | head -3
5501
5584
5585
...
...
...

따라서, docker 컨테이너를 시작하는 동안 docker는 이 컨테이너 안의 프로세스에 대한 cgroup을 만듭니다.

$ docker exec -it mysql-work /bin/bash
$ top
  PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND                                                                                   1 mysql     20   0  963996 101268  15744 S   0.0  0.6   0:00.46 mysqld
   71 root      20   0   20248   3028   2732 S   0.0  0.0   0:00.01 bash
   77 root      20   0   21948   2424   2056 R   0.0  0.0   0:00.00 top

그리고 호스트 컴퓨터에서 이 cgroup을 볼 수 있습니다 :

$ systemd-cgls

Control group /:
-.slice
├─docker
│ └─fa2d2085cd1c8d797002c77387d2061f56fefb470892f140d0dc511bd4d9bb61
│   ├─5501 mysqld
│   └─6404 /bin/bash

이제 우리는 control group 메커니즘과 수동으로 이를 사용하는 방법 및 이 메커니즘의 목적에 대해 약간 알게 되었습니다. 리눅스 커널 소스 코드를 살펴보고이 메커니즘의 구현을 시작해봅시다.

제어 그룹의 초기 초기화

이제 제어 그룹 리눅스 커널 메커니즘에 대한 약간의 이론을 본 후, 이 메커니즘에 더 친숙해지기 위해 리눅스 커널의 소스 코드를 살펴보겠습니다. 항상 그랬듯이 우리는 control groups의 초기화부터 시작할 것입니다. cgroups의 초기화는 리눅스 커널에서 두 부분으로 나뉩니다 : 초기(early)와 후기(late). 이 부분에서는 초기(early)부분 만 고려하고 후기(late)부분은 다음 파트에서 다루겠습니다.

cgroups의 초기 초기화는 리눅스 커널의 초기 초기화 시기동안 init/main.c의:

cgroup_init_early();

함수 호출에서 시작합니다. 이 함수는 kernel/cgroup.c 소스 코드 파일에 정의되어 있으며 다음 두 가지 지역 변수를 정의하면서 시작합니다:

int __init cgroup_init_early(void)
{
    static struct cgroup_sb_opts __initdata opts;
    struct cgroup_subsys *ss;
    ...
    ...
    ...
}

cgroup_sb_opts 구조체는 동일한 소스 코드 파일에 정의되어 있으며 cgroupfs의 마운트 옵션을 나타내고 다음과 같이 생겼습니다:

struct cgroup_sb_opts {
    u16 subsys_mask;
    unsigned int flags;
    char *release_agent;
    bool cpuset_clone_children;
    char *name;
    bool none;
};

예를 들어 name=옵션이 있고 서브시스템은 없이 (my_cgrp로) 이름이 지정된 cgroup 계층(hierarchy)을 작성할 수 있습니다.

$ mount -t cgroup -oname=my_cgrp,none /mnt/cgroups

두 번째 변수- ssinclude/linux/cgroup-defs.h 헤더 파일에 정의된 cgroup_subsys 구조체 타입을 가지며, 형식 이름에서 알 수 있듯이 이는 cgroup 서브시스템을 나타냅니다. 이 구조체는 다음과 같은 다양한 필드와 콜백 함수를 가지고 있습니다:

struct cgroup_subsys {
    int (*css_online)(struct cgroup_subsys_state *css);
    void (*css_offline)(struct cgroup_subsys_state *css);
    ...
    ...
    ...
    bool early_init:1;
    int id;
    const char *name;
    struct cgroup_root *root;
    ...
    ...
    ...
}

예를 들어 여기서 ccs_onlinecss_offline 콜백은 cgroup이 모든 할당을 성공적으로 완료한 후 호출되고 cgroup은 각각 해제되기 전에 호출됩니다. early_init 플래그는 초기에 초기화 될 수 있는 서브시스템을 표시합니다. idname 필드는 각각 등록 된 서브 시스템 배열의 고유 식별자와 서브 시스템의 name을 나타냅니다. 마지막 root 필드는 cgroup 계층의 루트에 대한 포인터를 나타냅니다.

물론 cgroup_subsys 구조체는 더 크고 다른 필드들도 가지고 있지만 현재로서는 이걸로 충분합니다. 이제 cgroups 메커니즘과 관련된 중요한 구조체에 대해 알았으니 이제 cgroup_init_early 함수로 돌아갑시다. 이 함수의 주요 목적은 몇몇 서브 시스템을 초기 초기화하는 것입니다. 이미 짐작하셨겠지만, 이 '초기'(early) 서브 시스템은 cgroup_subsys-> early_init = 1을 가지고 있어야합니다. 어떤 서브 시스템이 초기에 초기화 될 수 있는지 살펴봅시다.

두 개의 로컬 변수를 정의한 후 다음 코드 줄을 볼 수 있습니다.

init_cgroup_root(&cgrp_dfl_root, &opts);
cgrp_dfl_root.cgrp.self.flags |= CSS_NO_REF;

여기서는 기본 통합 계층(default unified hierarchy)의 초기화를 실행하는 init_cgroup_root 함수의 호출을 볼 수 있으며, 그 후 이 css의 참조 카운트를 비활성화하기 위해 이 기본 cgroup 상태에서 CSS_NO_REF 플래그를 설정합니다. cgrp_dfl_root는 동일한 소스 코드 파일에 정의되어 있습니다 :

struct cgroup_root cgrp_dfl_root;

이미 추측하셨듯 cgrp 필드는 cgroup 구조체로 나타내어지고 cgroup을 나타내며 [include/linux/cgroup-defs.h] (https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/include/linux/cgroup-defs.h) 헤더 파일에 정의되어 있습니다. 우리는 이미 리눅스 커널에서 task_struct로 표현되는 프로세스를 알고 있습니다. task_struct에는 이 작업이 연결된 cgroup에 대한 직접적인 링크가 없습니다. 그러나 task_structcss_set 필드를 통해 이에 도달 할 수 있습니다. 이 css_set 구조체는 서브 시스템 상태의 배열에 대한 포인터를 가지고 있습니다.

struct css_set {
    ...
    ...
    ....
    struct cgroup_subsys_state *subsys[CGROUP_SUBSYS_COUNT];
    ...
    ...
    ...
}

또한 cgroup_subsys_state를 통해 프로세스는 그 프로세스가 연결된 cgroup을 얻을 수 있습니다:

struct cgroup_subsys_state {
    ...
    ...
    ...
    struct cgroup *cgroup;
    ...
    ...
    ...
}

따라서 cgroups과 관련된 데이터 구조의 전체적인 그림은 다음과 같습니다.

+-------------+         +---------------------+    +------------->+---------------------+          +----------------+
| task_struct |         |       css_set       |    |              | cgroup_subsys_state |          |     cgroup     |
+-------------+         |                     |    |              +---------------------+          +----------------+
|             |         |                     |    |              |                     |          |     flags      |
|             |         |                     |    |              +---------------------+          |  cgroup.procs  |
|             |         |                     |    |              |        cgroup       |--------->|       id       |
|             |         |                     |    |              +---------------------+          |      ....      | 
|-------------+         |---------------------+----+                                               +----------------+
|   cgroups   | ------> | cgroup_subsys_state | array of cgroup_subsys_state
|-------------+         +---------------------+------------------>+---------------------+          +----------------+
|             |         |                     |                   | cgroup_subsys_state |          |      cgroup    |
+-------------+         +---------------------+                   +---------------------+          +----------------+
                                                                  |                     |          |      flags     |
                                                                  +---------------------+          |   cgroup.procs |
                                                                  |        cgroup       |--------->|        id      |
                                                                  +---------------------+          |       ....     |
                                                                  |    cgroup_subsys    |          +----------------+
                                                                  +---------------------+
                                                                             |
                                                                             |

                                                                  +---------------------+
                                                                  |    cgroup_subsys    |
                                                                  +---------------------+
                                                                  |         id          |
                                                                  |        name         |
                                                                  |      css_online     |
                                                                  |      css_ofline     |
                                                                  |        attach       |
                                                                  |         ....        |
                                                                  +---------------------+

따라서 init_cgroup_rootcgrp_dfl_root를 기본값으로 채 웁니다. 그 다음은 초기 css_set을 시스템의 첫 번째 프로세스를 나타내는 init_task에 할당하는 것입니다.

RCU_INIT_POINTER(init_task.cgroups, &init_css_set);

그리고 cgroup_init_early 함수에서 마지막으로 짚어 봐야 할 것은 early cgroups의 초기화입니다. 여기서는 등록된 모든 서브 시스템을 살펴보고 고유 ID, 서브 시스템 이름을 지정하고 초기로 표시(mark)된 서브 시스템에 대해 cgroup_init_subsys 함수를 호출합니다.

for_each_subsys(ss, i) {
        ss->id = i;
        ss->name = cgroup_subsys_name[i];

        if (ss->early_init)
            cgroup_init_subsys(ss, true);
}

여기서 for_each_subsyskernel/cgroup.c 소스 코드 파일에 정의 된 매크로이며 그저 cgroup_subsys 배열에 대한 for 루프로 확장됩니다. 그 배열의 정의는 동일한 소스 코드 파일에서 찾을 수 있으며 약간 특이한 방식같아 보입니다:

#define SUBSYS(_x) [_x ## _cgrp_id] = &_x ## _cgrp_subsys,
    static struct cgroup_subsys *cgroup_subsys[] = {
        #include <linux/cgroup_subsys.h>
};
#undef SUBSYS

그것은 하나의 인자(서브 시스템의 이름)를 취하고 cgroup 서브시스템의 cgroup_subsys 배열을 정의하는SUBSYS 매크로로 정의됩니다. 또한 배열이 linux/cgroup_subsys.h 헤더 파일의 내용으로 초기화 된 것을 볼 수 있습니다. 이 헤더 파일을 살펴보면 주어진 서브시스템 이름을 가진 SUBSYS 매크로 세트가 다시 나타납니다.

#if IS_ENABLED(CONFIG_CPUSETS)
SUBSYS(cpuset)
#endif

#if IS_ENABLED(CONFIG_CGROUP_SCHED)
SUBSYS(cpu)
#endif
...
...
...

이것은 SUBSYS 매크로를 처음 정의한 뒤에 있는 #undef 문 때문에 작동합니다. &_x##_cgrp_subsys 표현식을보면 ##연산자는 C 매크로에서 오른쪽과 왼쪽 표현식을 연결합니다. 그러므로 cpuset,cpu 등을SUBSYS 매크로에 전달할 때 cpuset_cgrp_subsys,cp_cgrp_subsys가 정의되었어야 할 것입니다. 그리고 그것은 사실입니다. kernel/cpuset.c 소스 코드 파일을 보면 다음 정의가 표시됩니다.

struct cgroup_subsys cpuset_cgrp_subsys = {
    ...
    ...
    ...
    .early_init    = true,
};

따라서 cgroup_init_early 함수의 마지막 단계는 cgroup_init_subsys 함수를 호출하여 초기 서브시스템을 초기화하는 것입니다. 다음과 같은 초기 서브시스템이 초기화될 것입니다.

  • cpuset;

  • cpu;

  • cpuacct.

cgroup_init_subsys 함수는 주어진 서브 시스템을 기본값으로 초기화합니다. 예를 들어 계층의 루트를 설정하고, css_alloc 콜백 함수를 호출하여 지정된 서브 시스템에 공간을 할당하고, 서브시스템이 있는 경우 서브시스템을 부모와 연결하고, 할당 된 서브 시스템을 초기 프로세스에 추가하는 등입니다.

이것으로 이 순간 초기 서브 시스템이 초기화되었습니다.

결론

이것으로 리눅스 커널에서 Control Groups 메커니즘을 소개하는 것은 첫 번째 파트는 끝입니다. 우리는 Control Group메커니즘과 관련된 것들을 초기화하는 몇 가지 이론과 그 첫번재 단계를 다루었습니다. 다음 부분에서는 제어 그룹(control groups)의 보다 실용적인 측면을 계속해서 살펴볼 것입니다.

질문이나 제안 사항이 있으면 twitter에 의견을 보내거나 핑 (Ping) 해주십시오.

영어는 제 모국어가 아닙니다, 그리고 여타 불편하셨던 점에 대해서 정말로 사과드립니다. 만약 실수들을 찾아내셨다면 부디 linux-insides 원본으로, 번역에 대해서는 linux-insides 한국 번역로 PR을 보내주세요.

참고 링크

Last updated