운영체제/LINUX

[Linux] PID 네임스페이스

benjykim 2019. 12. 14. 12:01
반응형
  • PID 네임스페이스는 프로세스 ID공간을 격리한다.
    • 즉, 다른 PID 네임스페이스의 프로세스들은 같은 PID를 가질 수도 있음을 의미한다.
  • PID 네임스페이스들은 컨테이너로 하여금 프로세스 집합의 종료/재시작 같은 기능을 제공한다. 또한 컨테이너를 새로운 호스트로 마이그레이션 하는 등의 기능을 제공한다.(마이그레이션 할 때 컨테이너 안의 프로세스들은 자신의 PID를 유지한다.)
  • 새로운 PID 네임스페이스는 1부터 시작한다.
    • standalone 시스템과 동일하게 각 네임스페이스의 시작 프로세스는 PID 1번을 가지게 된다.

The namespace init process

  • 새로운 네임스페이스에서 생성된 첫번째 프로세스는 PID가 1이고, 새 네임스페이스의 `init`프로세스이다(`CLONE_NEWPI`플래그와 `clone(2)`를 사용하여 생성된 프로세스 또는 `CLONE_NEWPID`플래그와 `unshare(2)`를 호출한 후 프로세스에 의해 생성된 첫 번째 자식). 이 프로세스는 새로운 네임스페이스에 있는 프로세스가 종료되며 발생하는 고아 프로세스의 부모가 된다.
  • 만일 PID 네임스페이스의 `init`프로세스가 종료되면, 커널은 `SIGKILL`시그널을 통해 네임스페이스에 있는 모든 프로세스를 종료시킨다.
  • `init`프로세스가 시그널 핸들러를 등록한(establish) 시그널만 `init`프로세스에 보내질 수 있다(PID 네임스페이스의 다른 멤버가 `init`프로세스로 시그널을 보낼 수 있다). 이 제한은 `privileged` 프로세스에게도 적용되고, PID 네임스페이스의 다른 멤버가 실수로 `init`프로세스를 죽이는 것을 방지한다.
  • 마찬가지로, 조상(ancestor) 네임스페이스의 프로세스는 `kill(2)`에 설명된 일반적인 권한 확인에 따라 `init`프로세스가 해당 시그널 핸들러를 등록한 경우에만 자식 PID 네임스페이스의 `init`프로세스에 시그널을 보낼 수 있다. `SIGKILL`이나 `SIGSTOP`은 예외적으로 처리된다. 이 시그널들은 조상 PID 네임스페이스에서 보내졌을 때 강제적으로 전달된다. 이런 시그널 중 어느 것도 `init`프로세스에 의해 포착(be caught)될 수 없으므로, 이러한 시그널과 관련된 일반적인 동작(`SIGKILL`-프로세스 중지 / `SIGSTOP`- 프로세스 중지)이 발생한다.

Nesting PID namespaces

  • PID 네임스페이스는 중첩될 수 있다. 각각의 PID 네임스페이스는 부모를 가지고 있다(초기(`root`) PID 네임스페이스를 제외하고). PID 네임스페이스의 부모는 `clone(2)` 또는 `unshare(2)`를 사용하여 네임스페이스를 생성한 프로세스의 PID 네임스페이스다. 즉, `clone(2)` 또는 `unshare(2)` 시스템 콜을 호출한 프로세스의 PID 네임스페이스가 부모 네임스페이스다(시스템 콜을 호출하여 생성된 네임스페이스가 자식 네임스페이스다. 헷갈린다면 그림을 그려보자).
  • PID 네임스페이스는 트리형태로 되어 있다. 모든 네임스페이스는 자신의 상위 네임스페이스(`root`네임스페이스까지) 들을`root` 따라 올라갈 수 있다.
  • A라는 프로세스는 A가 속한 PID 네임스페이스 안의 다른 프로세스에서 보인다(`visible`). 즉, 같은 PID 네임스페이스에 속한 프로세스들은 서로를 볼 수 있다. 그리고 A가 속한 네임스페이스의 직접적인 상위(조상) PID 네임스페이스 안의 프로세스들도 A 프로세스를 볼 수 있다.
    • 여기서 `보인다(visible)`는 의미는 한 프로세스가 시스템 콜을 사용하는 다른 프로세스에 의한 조작, 작동(operation)의 대상이 될 수 있다는 의미다.
  • 반대로, 자식 PID 네임스페이스에 있는 프로세스들은 부모 PID 네임스페이스안의 프로세스들을 볼 수 없고 추가적으로 상위 네임스페이스에서의 제거된 프로세스들도 볼 수 없다. 즉, "프로세스는 자신의 PID 네임스페이스와 그 네임스페이스의 후손에 포함된 프로세스만 볼 수 있다."
  • PID에서 작동하는 시스템 콜은 항상 호출자의 PID 네임스페이스에 표시되는 PID를 사용하여 작동한다.`getpid(2)`에 대한 호출은 프로세스가 만들어진 네임스페이스와 연관된 PID를 리턴한다.

  • 만일 `(8, 1)`프로세스가 `getpid(2)`를 호출한다면 어떤 PID를 리턴할 것인가. 1을 리턴하게 된다. 위에서 말했던 내용을 보다 직관적으로 이해할 수 있다. PID에서 작동하는(PID를 이용하는) 시스템 콜은 항상 호출자의 PID 네임스페이스에 표시되는 PID를 사용한다. 따라서 `8`이 아니라 `1`이 리턴된다.
  • PID 네임스페이스에 있는 몇몇 프로세스들은 네임스페이스 밖에 부모 프로세스가 존재할 수도 있다. 위의 그림을 보면, `(8, 1)` 프로세스의 부모는 `6`이고 밖에 존재한다.
  • 프로세스는 자식 PID 네임스페이스에 자유롭게 내려갈 수 있지만(PID 네임스페이스 파일 디스크립터와 함께 `setns(2)`사용) 다른 방향으로 이동하지 않을 수 있다. 즉, 프로세스는 상위 네임스페이스에 들어가지 않을 수 있다. PID 네임스페이스 변경은 단방향 작업이다. 
    • man page 설명
While processes may freely descend into child PID namespaces (e.g., using setns(2) with a PID namespace file descriptor), they may not move in the other direction.  
That is to say, processes may not enter any ancestor namespaces (parent, grandparent, etc.).  Changing PID namespaces is a one-way operation.

setns(2) and unshare(2) semantics

  • `CLONE_NEWPID`플래그를 사용한 `unshare(2)`를 호출하면 이후에 `unshare(2)`를 호출한 프로세스가 생성한 자식 프로세스가 호출한 프로세스와 다른 PID 네임스페이스에 배치된다.
  • PID 네임스페이스 파일 디스크립터를 지정하는 `setns(2)` 시스템 콜과 `CLONE_NEWPID`플래그를 사용한 `unshare(2)`시스템 콜 호출은 호출 이후에 호출자가 생성한 자식을 호출자와 다른 PID 네임스페이스에 배치한다. 그러나 이러한 호출은 호출하는 프로세스의 PID 네임스페이스를 변경하지 않는다. 왜냐하면 그렇게 하면 자신의 PID에 대한 호출자의 idea가 바뀌게 되고, 이는 많은 응용 프로그램과 라이브러리를 깨뜨릴(break) 것이기 때문이다.
  • 다른 방식으로 말하자면, 프로세스의 PID 네임스페이스 멤버십은 프로세스가 생성될 때 결정되며 그 이후에는 변경할 수 없다. 무엇보다도 이것은 프로세스 간의 부모 관계가 PID 네임스페이스 간의 부모 관계를 반영한다는 것을 의미한다. 즉, 프로세스의 부모는 동일한 네임스페이스에 있거나 바로 위의 부모 PID 네임스페이스에 있다.
  • 프로세스는 `CLONE_NEWPID`플래그가 있는 `unshare(2)`를 단 한 번만 호출할 수 있다. 이 작업을 수행한 후에, 네임스페이스안에서 첫 자식이 생성될 때까지 `/proc/[pid]/ns/pid_children`심볼릭 링크가 비어 있을 것이다.

Adoption of orphaned children

  • 자식 프로세스가 고아가 되면 부모의 PID 네임스페이스에 있는 `init`프로세스로 다시 연결된다. 위에서 설명한 `setns(2)`, `unshare(2)`시맨틱으로 인해, 이는 자식의 `init`프로세스가 아니라 자식의 PID 네임스페이스의 상위인 PID 네임스페이스의 `init` 프로세스 일 수 있다.

Compatibility of CLONE_NEWPID with other CLONE_*flags

  • 현재 리눅스 버전에서, `CLONE_NEWPID` `CLONE_THREAD`와 결합될 수 없다. 쓰레드는 프로세스의 스레드가 서로 시그널을 보낼 수 있도록 동일한 PID 네임스페이스에 있어야 한다. 마찬가지로, `proc(5)` 파일 시스템에서 프로세스의 모든 쓰레드를 볼 수 있어야 한다. 또한 두 개의 스레드가 서로 다른 PID 네임스페이스에 있는 경우. 시그널을 보낼 때 시그널을 보내는 프로세스의 PID를 의미 있게 인코딩할 수 없었다. 이는 시그널이 대기열에 있을 때 계산되기 때문에, 여러 PID 네임스페이스에 있는 프로세스들이 공유하는 시그널 대기열(signal queue)이 이를 무효화한다(defeat).

/proc and PID namespaces

  • `/proc`파일 시스템은 마운트를 수행한 프로세스의 PID 네임스페이스 안에 있는 프로세스들만 보여준다(다른 네임스페이스의 프로세스에서 /proc 파일 시스템을 보는 경우에도). 새 PID 네임스페이스를 만든 후에는 `ps(1)`와 같은 툴이 정상적으로 작동하도록 `/proc` 파일 시스템에 새로운 procfs 인스턴스를 마운트하고 루트 디렉터리를 변경하는 것이 좋다. `clone(2)` 또는 `unshare(2)`의 플래그에 `CLONE_NEWNS`를 포함하여 새로운 마운트 네임 스페이스를 동시에 생성하면 루트 디렉토리를 변경할 필요가 없다. 새로운 procfs 인스턴스를 `/proc`에 직접 마운트 할 수 있다.
    • Shell에서 /proc을 마운트하기 위한 명령은 아래와 같다. 
$ mount -t proc proc /proc

소스코드 예제

#define _GNU_SOURCE
#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>
#include <sched.h>
#include <signal.h>
#include <unistd.h>
#define STACK_SIZE (1024 * 1024)

static char child_stack[STACK_SIZE];

char* const child_args[] = {
  "/bin/bash",
  NULL
};

int child_main(void* arg)
{
  printf(" - [%4d] Current namspace, Parent PID : %d\n", getpid(), getppid() );
  sethostname("namespace", 12);
  execv(child_args[0], child_args);
  return 1;
}

int main()
{
  printf(" - [%4d] New namespace, Parent PID : %d\n", getpid(), getppid() );
  int child_pid = clone(child_main, child_stack+STACK_SIZE, CLONE_NEWUTS | CLONE_NEWIPC | CLONE_NEWPID | SIGCHLD, NULL);
  waitpid(child_pid, NULL, 0);
  return 0;
}

출처: https://bluese05.tistory.com/18 [ㅍㅍㅋㄷ]
root@master:~/nsexp# gcc -o nsexp nsexp.c
root@master:~/nsexp# ./nsexp
 - [16964] New namespace, Parent PID : 16739
 - [   1] Current namspace, Parent PID : 0
  • `clone()` 실행 전에는 PID가 16964이고, 새로 만든 네임스페이스의 PID는 1이다. 그리고 새로 만들어진 네임스페이스의 부모 PID는 0으로 init process와 동일한 성격을 지닌다.
  • 새로운 네임스페이스에서 본래 부모였던 PID인 16964를 인식할 수 있는가?
root@namespace:~/nsexp# kill -9 16964
bash: kill: (16964) - No such process
  • 위와 같이 새로운 네임스페이스는 PID 관리 영역이 분리되었으므로 PID 16964를 못 찾는다.
반응형