PID namespace
이전에 리눅스 컨테이너 (2)편에서는 프로세스 격리 기능 중 하나인 chroot에 대해 살펴보았습니다. 이번 글에서는 리눅스 컨테이너 기술의 핵심 개념 중 하나인 namespace 중에서 PID namespace에 대해 알아보겠습니다.
컨테이너와 커널
PID namespace에 대해 보기 전에, 먼저 리눅스 컨테이너 (1)편의 서론에서 언급했던 내용을 다시 살펴보겠습니다.
컨테이너는 호스트 커널을 공유하면서도 사용자 공간은 독립적으로 분리된 실행 환경을 제공합니다.
이 개념을 실제로 확인하기 위해, 컨테이너 기술의 대표적인 도구인 docker를 사용해 실습해보겠습니다. 실습 환경은 (2)편과 동일하게 Multipass를 사용했습니다.
우선 docker가 설치되어 있는지 확인해보겠습니다.
$ docker --version
'docker'를 찾을 수 없다는 메세지가 나오며 설치 명령어를 안내해줍니다. 저 중에서 두 번째 설치 명령어를 통해 docker를 설치하고 다시 확인해보겠습니다.
$ sudo apt install docker.io
위와 같이 docker가 정상적으로 설치되었다면, 이제 Ubuntu 이미지를 실행하고 bash 셸을 활성화해보겠습니다.
이어서 커널 및 시스템 아키텍처 정보를 출력하는 명령어를 통해 확인해보겠습니다.
$ sudo docker run -it ubuntu:latest bash
$ uname -a
(위에서 사용한 docker run과 uname 명령어의 상세한 사용법은 man 명령어를 통해 확인할 수 있습니다.)
Linux 68dc98881e74 6.8.0-60-generic #63-Ubuntu SMP PREEMPT_DYNAMIC Tue Apr 15 18:51:58 UTC 2025 aarch64 aarch64 aarch64 GNU/Linux
다음에는 Centos 이미지를 실행하고 bash 셸을 활성화해보겠습니다.
$ sudo docker run -it centos:7 bash
$ uname -a
Linux d7aa4f84732b 6.8.0-60-generic #63-Ubuntu SMP PREEMPT_DYNAMIC Tue Apr 15 18:51:58 UTC 2025 aarch64 aarch64 aarch64 GNU/Linux
서로 다른 컨테이너 안에서 커널 정보를 확인해 보았는데 hostname(컨테이너 ID)를 제외하면 그 결과가 동일한 것을 볼 수 있습니다.
마지막으로 호스트의 커널을 확인해보겠습니다.
Linux capable-lamprey 6.8.0-60-generic #63-Ubuntu SMP PREEMPT_DYNAMIC Tue Apr 15 18:51:58 UTC 2025 aarch64 aarch64 aarch64 GNU/Linux
이 역시 hostname을 제외하면 두 컨테이너에서와 동일한 결과가 나옵니다.
이렇게 컨테이너는 서로 분리된 독립적인 실행 환경을 제공하지만 호스트 커널을 공유한다는 것을 직접 확인해보았습니다.
Process ID
PID(Process ID)는 운영체제가 각 프로세스를 고유하게 식별하기 위해 부여하는 숫자 ID입니다. PID는 각 프로세스마다 고유(unique) 하며, 프로세스가 시작될 때 운영체제에 의해 자동으로 할당되며 프로세스가 종료되면 해당 PID는 다시 재사용될 수 있습니다.
다음 명령어로 전체 프로세스를 확인할 수 있습니다.
$ ps -ef
결과를 확인해보면 (1) 편에서 언급했던대로 init 시스템이 1번 PID를 할당받은 것을 볼 수 있습니다.
이는 실행 중인 프로세스들을 트리(tree) 형태로 보여주는 명령어를 사용하면 더욱 직관적으로 파악할 수 있습니다.
$ pstree
루트 프로세스에 위치한 systemd가 바로 PID 1번의 init 시스템입니다.
또한 다음 명령어로 현재 셸 프로세스의 PID를 확인할 수 있습니다.
$ echo $$
현재 셸의 PID는 1707348이며, ps -ef로 확인한 /bin/bash 프로세스의 PID와 동일합니다.
컨테이너와 PID
이번에는 리눅스 컨테이너 (1)편에서 언급했던 또 다른 내용을 살펴보겠습니다.
기술적으로 보면, 컨테이너는 일종의 프로세스입니다.
앞서 Docker로 Ubuntu 이미지를 실행하고 bash를 띄웠던 실습을 다시 진행한 뒤, 해당 프로세스의 PID를 확인해보겠습니다.
이어서 pstree를 설치한 후, 프로세스 트리도 확인해보겠습니다.
# apt update
# apt install psmisc
호스트 환경에서 실행한 bash 셸은 1580907번과 같은 PID를 가지지만, Docker 컨테이너 내에서 실행한 bash 셸은 PID 1을 가집니다. 이는 해당 컨테이너에서 bash가 루트(init) 프로세스 역할을 수행하고 있기 때문입니다. 즉, 별도의 init 시스템 없이 bash가 컨테이너의 첫 번째 프로세스로서 전체 프로세스 트리를 시작하는 것입니다.
지금까지의 내용을 종합해보면, docker컨테이너는 호스트와는 분리된 고유한 PID 체계를 가지고 있다는 것을 알 수 있습니다.
이 기능이 바로 리눅스의 Namespace 기술 중 하나인 PID Namespace입니다. PID Namespace를 분리하면 컨테이너 내부에서는 자신만의 PID 공간을 가지게 되어, 마치 독립된 시스템처럼 동작하게 됩니다.
그렇다면, Docker로 실행한 bash 셸은 호스트 입장에서는 어떻게 보일까요?
bash 셸을 실행한 터미널 외에, 또 다른 터미널을 활성화한 후 다음의 명령어를 통해 확인해보겠습니다.
$ sudo docker ps
$ sudo docekr top <container ID>
앞서 ubuntu 이미지를 사용해 실행했던 도커 컨테이너의 프로세스의 PID는 1631261, PPID(부모 PID)는 1631240 으로 나타납니다.
이를 실제 프로세스 목록에서 확인해보겠습니다.
전체 프로세스 목록을 확인해보면, PID 1631261에 bash 셸이 등록되어 있는 것을 확인할 수 있습니다.
이 bash 셸의 부모 프로세스는 PID 1631240이며, 이는 컨테이너 실행을 담당하는 containerd-shim-runc-v2 프로세스입니다. 이 프로세스는 moby라는 namespace와 함께, ddc491ed2dfccc9be35a4502d568773a4036b6feba라는 ID를 가지고 실행되고 있습니다. 주목할 점은, 이 ID의 앞 12자리가 해당 도커 컨테이너의 ID와 일치한다는 점입니다.
이처럼, 컨테이너 내부에서 실행되는 모든 프로세스는 호스트 입장에서는 일반적인 개별 프로세스로 보인다는 것을 알 수 있습니다.
정확히 말하면, 컨테이너는 프로세스 그 자체가 아니라, 프로세스를 감싸는 격리된 실행 환경이며, 컨테이너 자체가 PID를 가지는 것은 아닙니다.
PID namespace 실습
이번에는 docker 없이 unshare 명령어로 PID namespace를 분리해보겠습니다.
unshare는 리눅스에서 새로운 네임스페이스(namespace)를 생성하여 격리된 실행 환경을 만드는 명령어입니다. 즉, 현재 쉘이나 프로세스와 독립적인 환경에서 커맨드를 실행할 수 있도록 해줍니다. 컨테이너 기술의 핵심 개념 중 하나이기도 하며, chroot보다 훨씬 강력한 격리를 제공합니다.
PID namespace는 다음과 같이 unshare 명령어에 --pid(-p) 옵션을 지정해주면 됩니다.
$ sudo unshare -p /bin/bash
위와 같이 입력해본 결과 두 가지 문제점이 확인됩니다.
첫 번째는 메모리를 할당할 수 없다는 에러가 발생하는 것이고, 두 번째는 bash 셸의 PID가 격리되지 않고 호스트의 PID가 출력된다는 점입니다.
1. Cannot allocate memory
이 에러가 발생해도 bash 셸은 겉보기엔 정상적으로 동작하는 것처럼 보입니다.
하지만 실제로는 컨테이너 내부에서 PID 1번으로 실행된 bash 셸이 init 프로세스 역할을 제대로 수행하지 못할 가능성을 경고하는 의미입니다.
리눅스에서 모든 프로세스는 루트 프로세스로부터 fork 되어 자식 프로세스가 되며, 이때 메모리 할당과 해제를 포함한 프로세스 생명주기 관리를 init(PID 1) 프로세스가 담당합니다.
즉, 여기서의 “메모리를 할당할 수 없다”는 에러는 단순한 부족이 아니라, PID 1이 된 bash가 자식 프로세스를 적절히 관리하지 못할 것 같다는 커널의 판단을 의미합니다.
다음과 같이 ps 명령어로 현재 프로세스 목록을 확인해보면, 역시 메모리 할당 불가 에러가 발생합니다.
이 문제는 다른 터미널을 열어 확인하면 좀 더 명확해집니다.
첫 번째 줄에 표시된 프로세스의 PPID(부모 프로세스 ID)는 앞서 확인한 호스트 bash 셸의 PID인 1707348입니다.
이 과정은 다음과 같이 이루어집니다:
- sudo가 먼저 bash 셸의 자식 프로세스를 생성합니다.
- sudo는 반드시 내부적으로 fork()를 수행합니다.
- 이 자식이 unshare를 실행하고,
- 이어서 /bin/bash를 실행하면서 unshare 프로세스를 덮어씌우게 됩니다.
결과적으로 /bin/bash는 PID 1이 되지만, init 프로세스 역할을 제대로 수행할 준비가 안 된 상태로 실행되었기 때문에 위와 같은 에러가 발생한 것입니다.
이 문제는 unshare 명령에 --fork(-f) 옵션을 추가함으로써 해결할 수 있습니다.
$ sudo unshare -p -f /bin/bash
--fork 옵션을 사용하니 메모리 할당 불가 에러 없이 bash 셸이 실행되었고, 실행된 bash 셸의 PID도 정상적으로 1번이 되었습니다.
동일하게 다른 터미널을 활성화해서 확인해보겠습니다.
앞서 fork 옵션을 주지 않았을 때와 달리 unshare 프로세스가 동일한 자식 프로세스를 생성한 후 /bin/bash를 exec()하여 자식 프로세스를 덮어씌웁니다.
2. fatal library error, looup self
그런데 이번에는 ps 명령어를 입력하면 다음과 같이 라이브러리를 찾을 수 없다는 에러가 발생합니다.
리눅스에서 ps 명령은 /proc 파일 시스템을 통해 현재 프로세스 정보를 가져오는데, 현재 PID 네임스페이스로 프로세스 ID는 격리했지만, /proc은 여전히 호스트의 프로세스 트리를 바라보고 있는 상황입니다.
즉, 컨테이너의 프로세스를 조회할 수 없는 상태이기 때문에 에러가 발생하는 것입니다.
이 문제는 다음과 같이 /proc 파일 시스템을 새로 마운트해주는 옵션을 추가하면 해결됩니다.
$ sudo unshare -p -f --mount-proc /bin/bash
이제 ps 명령어도 정상적으로 작동하며, 격리된 PID 네임스페이스 안의 프로세스 목록을 제대로 확인할 수 있습니다
마치며
이번 글에서는 unshare 명령어를 활용해 PID 네임스페이스 기반의 간단한 격리 환경을 구성해보고, 그 동작 원리와 주의할 점들을 살펴보았습니다. 다음 글에서는 다른 네임스페이스에 대해서도 실습해보겠습니다.
'Cloud' 카테고리의 다른 글
리눅스 컨테이너 (5) - namespace [NET] (2) | 2025.06.30 |
---|---|
리눅스 컨테이너 (4) - namespace [UTS] (1) | 2025.06.09 |
리눅스 컨테이너 (2) - chroot (1) | 2025.05.28 |
리눅스 컨테이너 (1) (2) | 2025.05.11 |