..

Linux Namespace에 대하여

Linux namespace

네임스페이스라는 단어를 어디선가 한 번쯤 들어보신 적이 있으실 겁니다. 기본적으로 네임스페이스는 서로 독자적으로 존재하면서, 무언가 - 변수나 함수의 이름 등을 고유하게 만들어주는 존재입니다. 파이썬으로 한번 생각해봅시다. a.pyb.py 모두에 똑같은 함수가 존재할 수 있습니다.

# a.py
def hello():
    print("hello from a!")
# b.py
def hello():
    print("hello from b!")

만약 이 둘을 동시에 같은 이름으로 import를 시도하면…

from a import hello
from b import hello

hello()

나중에 import한 b에서의 hello가 실행된다는 사실을 확인할 수 있습니다. 조금 더 복잡한 경우로 나가봅시다. 만약 여러분이 비디오와 이미지를 동시에 다루어야 하고, 각각을 서로 다른 패키지에 구현했다고 합시다. 이렇게 되면 두 패키지 모두에 IO 관련한 함수가 존재하게 될 겁니다 - load, save 등등… 이런 방식으로 import를 하고 사용한다면 save와 load가 어디서 왔는지 알 수 없다는 문제가 생깁니다. 하지만

import a
import b

a.hello()
b.hello()

이런 식으로 import를 하고, 함수를 호출하게 되면 이제 명시적으로 어떤 패키지의 함수를 실행할 것인지를 표현해줄 수 있습니다. a와 b는 같은 함수 이름을 가지지만, 서로 독립적으로 존재하게 됩니다.

리눅스에도 이런 네임스페이스라는 기술이 존재하며, 이는 도커 등 컨테이너 기술의 기반이 됩니다. 이에 대해서 간단하게 정리해봅시다.

Init

여의도에 IFC라는 거대한 쇼핑몰이 하나 있습니다. 여러 옷 가게나 식당이 모여있는 구조인데요, 이 지하상가를 공유하면서 위로 뻗은 4개의 건물이 존재합니다. 네임스페이스는 바로 이런 것입니다. 이 네 개의 건물은 공유할 건 공유하면서 서로 독립적으로 존재합니다. 지하상가는 네 개의 건물이 같이 쓰지만, 우편 주소나 전기 배선, 수도 등은 서로 따로 사용하겠죠. 이것이 바로 네임스페이스입니다. 공유할 수 있는 부분은 공유하고, 독립적으로 사용해야 하는 것은 독립적으로 가지고 있는 것입니다.

Process와 PID

PID는 리눅스에서 프로세스에 붙여주는 아이디입니다. 시스템이 부팅되면 Init을 통해 프로세스를 실행하게 됩니다. 이렇게 생성된 프로세스는 PID 1을 가지게 되며, 시스템이 종료되기 전까지 계속해서 실행됩니다. 유닉스에서는 기본적으로 fork를 통해서만 새로운 프로세스를 생성할 수 있습니다. 프로세스 A가 fork를 실행했다면, 프로세스 A와 완전히 같은 프로세스 B가 생성됩니다.

하지만 생각해보면 이건 좀 이상합니다. 계속 똑같은 프로세스만을 만들 수 있다면 어떻게 해야 서로 다른 프로그램을 한 컴퓨터에서 구동할 수 있는 걸까요? 답은 바로 exec에 있습니다. fork와 같은 system call인데요, exec을 통해서 프로세스의 변수나 기타 등등을 새롭게 불러오고, 새로운 main을 실행할 수 있게 됩니다.

여러분이 쉘을 켜서 ls를 실행했다고 해봅시다. 그렇다면 1번 프로세스로부터 fork가 실행되고, exec을 통해서 쉘이 실행됩니다. 다시 이 쉘에서 fork를 호출하고, exec을 통해 ls를 실행하게 되는 것입니다.

이 과정에서 프로세스가 생성될 때마다 서로 다른 PID를 부여받게 됩니다. 이렇게 할당받은 PID를 이용해서 프로세스를 구분할 수 있습니다. 중복된 PID는 불가능합니다. 대한민국이라는 컴퓨터에서 나라는 프로세스가 존재한다면 고유한 주민등록번호를 가지게 되어 이를 바탕으로 프로세스를 구분할 수 있을 것입니다.

또한 여기서 주목해야 하는 점은 fork를 이용해서만 프로세스를 생성할 수 있다는 부분입니다. 어떤 프로세스가 생성되기 위해서는 필연적으로 fork를 호출해준 프로세스가 존재해야만 합니다. 곧 프로세스끼리 부모 자식 관계가 존재하게 되는 것입니다(이는 pstree라는 명령어로 확인할 수 있습니다). 만약 부모가 예기치 못한 오류로 종료되었다면 자식 프로세스는 부모를 잃게 됩니다. 말 그대로 고아(orphan process)가 되는 것인데요, 이런 경우에는 커널이 이 고아 프로세스를 입양해서 부모를 PID 1로 바꿔줍니다. 또한 어떠한 이유로 1번 프로세스가 종료된다면 시스템이 종료됩니다.

PID namespace, IPC namespace

PID 네임스페이스는 이런 PID가 서로 독자적으로 존재하는 영역을 생각해보면 됩니다.

PID namespace

출처: https://www.toptal.com/linux/separation-anxiety-isolating-your-system-with-linux-namespaces

8번 프로세스에서 PID 네임스페이스가 생성되었다면 이 네임스페이스에서는 8번이 곧 1번 프로세스로서 기능하게 됩니다. 만약 10번 프로세스가 자식을 만들고 이후에 예기치 못한 종료로 고아 프로세스가 만들어졌다면 8번 프로세스를 새 부모로 가지게 됩니다. 또한 앞에서 보았듯 네임스페이스는 서로 독자적으로 존재하기 때문에 자식은 부모 네임스페이스의 존재를 알 수 없습니다. 하지만 자식 네임스페이스는 부모의 일부로 존재하므로 부모는 이를 인식할 수 있습니다. 도커에서 새로운 프로세스를 실행하면 1번 프로세스로 실행되는 것을 확인할 수 있습니다. 이는 다른 네임스페이스에 존재하기 때문입니다.

서로 다른 프로세스가 통신하기 위해서는 Interprocess Communication, IPC라는 방법이 필요합니다. IPC에는 여러 가지 방법이 존재합니다. 파이프를 통해서 통신할 수도 있고, 애초에 공유하는 메모리 영역을 프로세스끼리 가질 수도 있습니다. 하지만 어떤 프로세스가 다른 모든 프로세스와 이러한 통신을 주고받게 되면 보안 등의 문제가 발생할 수 있습니다. 따라서 자연적으로 이 또한 격리하고자 할 수 있습니다. 이를 위해서 사용되는 것이 IPC 네임스페이스입니다. 이를 통해서 관계가 없거나 권한이 없는 프로세스로부터의 통신을 제한할 수 있습니다.

Network Namespace

네트워크에도 마찬가지로 네임스페이스가 존재합니다. 곧 서로 다른 프로세스가 아예 다른 네트워크 인터페이스 - IP를 가지고 동작할 수 있다는 것입니다. 네트워크 서버를 예로 들어봅시다. 일반적인 HTTP 요청은 80반, HTTPS 요청은 443번 포트로 처리하게 됩니다. 이때 프로세스 하나당 하나의 포트만을 사용할 수 있습니다. 그러니까 어떤 서버에서 80번 포트를 다룰 수 있는 것은 단 하나의 프로세스뿐인 것이죠. 하지만 네트워크 네임스페이스를 통해 이를 격리하게 되면 서로 다른 프로세스가 서로 다른 IP와 전체 포트를 가지게 됩니다. 따라서 서로 다른 프로세스들이 동시에 80번 포트를 사용할 수 있게 되는 것입니다.

Network namespace

출처: https://www.toptal.com/linux/separation-anxiety-isolating-your-system-with-linux-namespaces

UTS Namespace

일반적으로 서버에 접근할 때는 IP주소와 포트가 필요합니다. 하지만 여기에 도메인 이름을 붙여서 관리하는 경우도 존재합니다. 이러한 호스트 이름이나 도메인 이름 또한 마찬가지로 격리해 별개의 네임스페이스로 운영할 수 있습니다.

User namespace

외부 서버에 연결하다보면 어떤 파일이나 폴더에 접근할 수 없거나, 실행 혹은 쓸 수 없는 경우를 만나게 됩니다. 이는 RWX에 대한 권한이 없기 때문입니다. 이런 식으로 파일마다 이를 소유하고 있는 유저와 그룹을 메타데이터의 형태로 가지고 있습니다. ls -l 등을 이용하면 이 파일에 대한 권한을 확인할 수 있습니다. 유저 네임스페이스는 이러한 UID와 GID를 격리하여 원래의 호스트와는 다른 유저, 그룹을 가질 수 있게 합니다. 또한 유저 네임스페이스는 PID 네임스페이스에서 보았던 것처럼 계층적인 구조를 가질 수 있습니다. 이러한 유저 네임스페이스를 이용하면 어떤 프로세스가 네임스페이스 안에서 루트 권한을 가지고 동작할 수도 있습니다.

Mount namespace

마운트 네임스페이스는 프로세스가 다른 네임스페이스에 있는 파일을 보지 못하게 격리하는 역할을 합니다. 그러니까 프로세스는 자신만의 파일 시스템을 유지하면서 다른 프로세스의 접근을 차단할 수 있게 되는 것입니다.