创建容器子进程
进入~/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);// 根据配置构造容器
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
函数,我们为它创建了一个lambda表达式。在C++中,一个捕获列表为空的lambda表达式可以作为函数指针传递。因此,setup
成为传递给clone()
的回调函数。
你也可以使用类中定义的静态成员函数代替lambda表达式,但那样代码会不那么优雅。
在这个container
类的构造函数中,我们定义了一个子进程处理函数,供clone()
系统调用调用。我们使用typedef
将这个函数的返回类型改为proc_status
。当这个函数返回proc_wait
时,由clone()
克隆的子进程将等待退出。
然而,这还不够,因为我们在进程内部尚未执行任何配置。结果,我们的程序一旦启动,由于没有其他事情可做,将立即退出。如我们所知,在Docker中,为了保持容器运行,我们可以使用:
docker run -it ubuntu:14.04 /bin/bash
这将STDIN绑定到容器的/bin/bash
。所以,让我们在docker::container
类中添加一个start_bash()
方法:
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]; // +1用于'\0'
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中引入所需命名空间的地方。