컨테이너 서브 프로세스 생성
~/project 디렉토리에 들어가서 docker.hpp라는 파일을 만듭니다. 이 파일에서 먼저 외부 코드에서 호출할 수 있는 docker 네임스페이스를 생성합니다.
//
// docker.hpp
// cpp_docker
//
// 시스템 호출을 위한 헤더 파일
#include <sys/wait.h> // waitpid
#include <sys/mount.h> // mount
#include <fcntl.h> // open
#include <unistd.h> // execv, sethostname, chroot, fchdir
#include <sched.h> // clone
// C 표준 라이브러리
#include <cstring>
// C++ 표준 라이브러리
#include <string> // std::string
#define STACK_SIZE (512 * 512) // 자식 프로세스 공간의 크기를 정의합니다.
namespace docker {
// .. docker 마법이 시작되는 곳
}
가독성을 높이기 위해 몇 가지 변수를 정의해 보겠습니다:
// `docker` 네임스페이스 내에서 정의됨
typedef int proc_status;
proc_status proc_err = -1;
proc_status proc_exit = 0;
proc_status proc_wait = 1;
컨테이너 클래스를 정의하기 전에 컨테이너를 생성하는 데 필요한 매개변수를 분석해 보겠습니다. 지금은 네트워크 관련 구성을 고려하지 않겠습니다. 이미지에서 Docker 컨테이너를 생성하려면 호스트 이름과 이미지 위치만 지정하면 됩니다. 따라서:
// Docker 컨테이너 시작 구성
typedef struct container_config {
std::string host_name; // 호스트 이름
std::string root_dir; // 컨테이너의 루트 디렉토리
} container_config;
이제 container 클래스를 정의하고 생성자에서 컨테이너에 필요한 구성을 수행하도록 하겠습니다:
class container {
private:
// 가독성 향상
typedef int process_pid;
// 자식 프로세스 스택
char child_stack[STACK_SIZE];
// 컨테이너 구성
container_config config;
public:
container(container_config &config) {
this->config = config;
}
};
container 클래스의 특정 메서드에 대해 생각하기 전에 먼저 이 container 클래스를 어떻게 사용할지 생각해 보겠습니다. 이를 위해 ~/project 폴더에 main.cpp 파일을 생성해 보겠습니다:
//
// main.cpp
// cpp_docker
//
#include "docker.hpp"
#include <iostream>
int main(int argc, char** argv) {
std::cout << "...start container" << std::endl;
docker::container_config config;
// 컨테이너 구성
// ...
docker::container container(config);// config 를 기반으로 컨테이너 구성
container.start(); // 컨테이너 시작
std::cout << "stop container..." << std::endl;
return 0;
}
main.cpp에서 컨테이너 시작을 간결하고 이해하기 쉽게 만들기 위해 start() 메서드를 사용하여 컨테이너를 시작한다고 가정해 보겠습니다. 이는 나중에 docker.hpp 파일을 작성하기 위한 기반을 제공합니다.
이제 docker.hpp로 돌아가서 start() 메서드를 구현해 보겠습니다:
void start() {
auto setup = [](void *args) -> int {
auto _this = reinterpret_cast<container *>(args);
// 컨테이너에 대한 관련 구성 수행
// ...
return proc_wait;
};
process_pid child_pid = clone(setup, child_stack+STACK_SIZE, // 스택의 맨 아래로 이동
SIGCHLD, // 자식 프로세스가 종료되면 부모 프로세스에 신호 전송
this);
waitpid(child_pid, nullptr, 0); // 자식 프로세스가 종료될 때까지 대기
}
docker::container::start() 메서드는 Linux 에서 clone() 시스템 호출을 사용합니다. docker::container 인스턴스 객체를 콜백 함수 setup에 전달하기 위해 clone()의 네 번째 인수를 사용하여 전달할 수 있습니다. 여기서는 this 포인터를 전달합니다.
setup 함수의 경우 람다 표현식을 생성합니다. C++ 에서 빈 캡처 목록이 있는 람다 표현식은 함수 포인터로 전달될 수 있습니다. 따라서 setup은 clone()에 전달된 콜백 함수가 됩니다.
람다 표현식 대신 클래스에 정의된 정적 멤버 함수를 사용할 수도 있지만, 그러면 코드가 덜 우아해집니다.
이 container 클래스의 생성자에서 clone() 시스템 호출에 의해 호출될 자식 프로세스 처리 함수를 정의합니다. typedef를 사용하여 이 함수의 반환 유형을 proc_status로 변경합니다. 이 함수가 proc_wait를 반환하면 clone()에 의해 복제된 자식 프로세스는 종료될 때까지 대기합니다.
그러나 프로세스 내에서 어떤 구성도 수행하지 않았으므로 이것으로는 충분하지 않습니다. 결과적으로 프로세스가 시작되면 다른 할 일이 없으므로 프로그램이 즉시 종료됩니다. 우리가 알고 있듯이 Docker 에서 컨테이너를 실행 상태로 유지하려면 다음을 사용할 수 있습니다:
docker run -it ubuntu:14.04 /bin/bash
이는 STDIN 을 컨테이너의 /bin/bash에 바인딩합니다. 따라서 start_bash() 메서드를 docker::container 클래스에 추가해 보겠습니다:
private:
void start_bash() {
// C++ std::string 을 C 스타일 문자열 char *로 안전하게 변환
// C++14 부터 이 직접 할당은 금지됩니다: `char *str = "test";`
std::string bash = "/bin/bash";
char *c_bash = new char[bash.length()+1]; // '\0'을 위해 +1
strcpy(c_bash, bash.c_str());
char* const child_args[] = { c_bash, NULL };
execv(child_args[0], child_args); // 자식 프로세스에서 /bin/bash 실행
delete []c_bash;
}
그리고 setup 내에서 호출합니다:
auto setup = [](void *args) -> int {
auto _this = reinterpret_cast<container *>(args);
_this->start_bash();
return proc_wait;
}
이제 다음 작업을 볼 수 있습니다:
labex:project/ $ hostname
iZj6cboigynrxh4mn2oo16Z
labex:project/ $ g++ main.cpp -std=c++11
labex:project/ $ ./a.out
...start container
labex@iZj6cboigynrxh4mn2oo16Z:~/project$ mkdir test
labex@iZj6cboigynrxh4mn2oo16Z:~/project$ ls
a.out docker.hpp main.cpp test
labex@iZj6cboigynrxh4mn2oo16Z:~/project$ exit
exit
stop container...
위 단계에서 먼저 현재 hostname을 확인하고, 지금까지 작성한 코드를 컴파일하고, 실행하고, 컨테이너에 들어갑니다. 컨테이너에 들어간 후 bash 프롬프트가 변경되는 것을 볼 수 있는데, 이는 우리가 예상했던 것입니다.
그러나 이것이 우리가 원하는 결과가 아니라는 것을 쉽게 알 수 있습니다. 이는 호스트 시스템과 정확히 동일합니다. 이 "컨테이너" 내에서 수행된 모든 작업은 호스트 시스템에 직접적인 영향을 미칩니다.
이것이 clone API 에 필요한 네임스페이스를 도입하는 곳입니다.