Linux Namespace - 超简单容器构建

20 篇文章 0 订阅
14 篇文章 0 订阅

Namespace隔离了进程、网路、用户等系统资源,本文将讲述如何通过Namespace创建一个超简单容器,并在容器内部运行简单的busybox程序。本文内容参考了LinuxNamespace系列(09),虽然目的相同,但本文采用C语言系统调用的方式实现,因此更加实用。

P.S:本文全部代码都在Ubuntu18.04Server下编译运行通过

步骤概览

  1. 准备busybox二进制文件
    作者使用的是busybox_1.31.0,构建容器后会在容器内安装busybox,并运行busybox中的shell。
  2. 编写程序
    1. 创建所需目录,包括/bin, /proc, /old_root等
    2. 复制可执行文件
    3. 调用unshare创建Namespace
    4. 调用pivot_root设置root文件系统的路径
    5. 挂载/proc等文件系统
    6. 执行busybox --install ./bin,运行./bin/sh
  3. 执行程序,busybox中运行ls等命令

本文首先展示最终的运行结果,之后分模块讲述程序的构建过程。

运行结果展示

eric@ubuntu:~/coding/linux_learn/simple_container$ ./simp_container.out
Running......
preparing root...
preparing dirs...
copying file...
doing unshare...
setting hostname...
chdir to new root...
pivot root...
doing mount and umount...
set env and exec busybox sh...
/ $ ls
bin       data      old_root  proc
/ $ ps -ef
PID   USER     TIME  COMMAND
    1 65534     0:00 sh
    5 65534     0:00 ps -ef
/ $ 

上述执行结果中,以...结尾的为日志输出。容器创建后,执行了busybox中的shell程序,ps -ef命令的输出表明当前容器隔离了PID,即容器内进程无法看到外部进程。由于没有映射用户和组id,因此USER为默认的65534。程序运行后的目录结构如下所示,bin目录存放了busybox安装后的可执行文件:

simp_container_root/
└── new_root
    ├── bin
    ├── data
    ├── old_root
    └── proc

构建过程

目录准备

目录准备主要使用mkdir函数实现,该函数创建指定目录,并授予指定权限,函数原型为int mkdir(const char *pathname, mode_t mode)。如下为代码:

#define md(A) mkdir(A, S_IRWXU | S_IRWXG)

void prepare_dirs() {
    logger("preparing dirs");
    md("./simp_container_root/");
    chdir("./simp_container_root/");
    md("./new_root");
    md("./new_root/bin");
    md("./new_root/data");
    md("./new_root/proc");
    md("./new_root/old_root");
}

复制可执行文件

由于没有找到复制文件的库函数,因此这里直接使用标准库的FILE对象实现,复制后调用chmod添加执行权限:

void cpy_file(const char old_path[], const char new_path[]) {
    FILE *oldfp, *newfp;
    oldfp = fopen(old_path, "rb");
    newfp = fopen(new_path, "wb");
    while (!feof(oldfp)) {
        const int buf_size = 4096;
        char buf[buf_size] = {};
        int n = fread(buf, 1, buf_size, oldfp);
        fwrite(buf, 1, n, newfp);
    }
    fclose(oldfp);
    fflush(newfp);
    fclose(newfp);
    chmod(new_path, S_IRWXU | S_IRWXG);
}

创建Namespace,配置文件系统

通过调用unshare创建新的Namespace,这里指定PID, UTS, USER, NETWORK, MOUNT, IPC, CGROUP所有七个Namespace。之后调用fork在子进程中执行接下来的操作。关于pivot_root系统调用,作者未作深入研究,感兴趣的可以参考LinuxNamespace系列(09)

void do_unshare() {
    int ret = 0;
    unshare(CLONE_NEWCGROUP | CLONE_NEWIPC | CLONE_NEWNET | CLONE_NEWNS | CLONE_NEWPID | CLONE_NEWUSER | CLONE_NEWUTS);
    ret = fork();
    if (ret == 0) { // child
        const char hostname[] = "container01";
        sethostname(hostname, strlen(hostname));
        mount("./new_root", "./new_root", NULL, MS_BIND, NULL); // 来自参考文章,pivot_root命令要求原根目录和新根目录不能在同一个挂载点下
                                                                // 所以这里使用bind mount,在原地创建一个新的挂载点
        chdir("./new_root");
        syscall(SYS_pivot_root, "./", "./old_root");            // 调用pivot_root,只支持syscall方式,参考man手册
        mount("none", "/proc", "proc", 0, NULL);                // 挂载 proc 文件系统,ps命令依赖于该文件系统,若不挂载,其输出依旧是宿主机中的进程信息
        umount("/old_root");
        exec_cmd();                                             // 执行busybox安装程序
    } else if (ret > 0) { // parent
        waitpid(ret, NULL, 0);
        exit(0);
    }
}

安装busybox并执行shell程序

安装程序通过调用execl实现即可,安装之后以同样的方式启动/bin下的sh程序即可:

void exec_cmd() {
    int ret = 0;
    if ((ret = fork()) > 0) { // parent
        waitpid(ret, NULL, 0);                                          // 等待安装完毕
        execl("/bin/sh", "sh", NULL);                                   // 启动shell
        errExit("execl");
    }
    if (ret == 0) { // child
        execl("/bin/busybox", "busybox", "--install", "/bin/", NULL);   // 安装
        errExit("execl");
    }
    errExit("fork");
}

完整代码

#include <fcntl.h>
#include <sched.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mount.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <syscall.h>
#include <unistd.h>
#include <wordexp.h>

#define logger(A)                      \
    do {                               \
        fprintf(stdout, "%s...\n", A); \
        fflush(stdout);                \
    } while (0)

#define md(A) mkdir(A, S_IRWXU | S_IRWXG)

const void errExit(const char *fmt, ...) {
    va_list ap;
    va_start(ap, fmt);
    fprintf(stderr, fmt, ap);
    fprintf(stderr, ".\nerrno: %d, strerror: %s\n", errno, strerror(errno));
    va_end(ap);
    exit(EXIT_FAILURE);
}

void cpy_file(const char old_path[], const char new_path[]) {
    FILE *oldfp, *newfp;
    oldfp = fopen(old_path, "rb");
    newfp = fopen(new_path, "wb");
    while (!feof(oldfp)) {
        const int buf_size = 4096;
        char buf[buf_size] = {};
        int n = fread(buf, 1, buf_size, oldfp);
        fwrite(buf, 1, n, newfp);
    }
    fclose(oldfp);
    fflush(newfp);
    fclose(newfp);
    chmod(new_path, S_IRWXU | S_IRWXG);
}

void prepare_dirs() {
    logger("preparing dirs");
    md("./simp_container_root/");
    chdir("./simp_container_root/");
    md("./new_root");
    md("./new_root/bin");
    md("./new_root/data");
    md("./new_root/proc");
    md("./new_root/old_root");
}

void exec_cmd() {
    int ret = 0;
    if ((ret = fork()) > 0) { // parent
        waitpid(ret, NULL, 0);
        execl("/bin/sh", "sh", NULL);
        errExit("execl");
    }
    if (ret == 0) { // child
        execl("/bin/busybox", "busybox", "--install", "/bin/", NULL);
        errExit("execl");
    }
    errExit("fork");
}

void do_unshare() {
    logger("doing unshare");
    int ret = 0;
    int us_flags = CLONE_NEWCGROUP | CLONE_NEWIPC | CLONE_NEWNET | CLONE_NEWNS | CLONE_NEWPID | CLONE_NEWUSER | CLONE_NEWUTS;
    ret = unshare(us_flags);
    if (ret < 0)
        errExit("unshare");
    if ((ret = fork()) < 0)
        errExit("fork");
    if (ret == 0) { // child
        const char hostname[] = "container01";
        logger("setting hostname");
        sethostname(hostname, strlen(hostname));
        logger("chdir to new root");
        mount("./new_root", "./new_root", NULL, MS_BIND, NULL);
        chdir("./new_root");
        logger("pivot root");
        syscall(SYS_pivot_root, "./", "./old_root");
        logger("doing mount and umount");
        mount("none", "/proc", "proc", 0, NULL);
        umount("/old_root");
        logger("set env and exec busybox sh");
        setenv("hostname", "container01", 0);
        exec_cmd();
    }
    if (ret > 0) { // parent
        waitpid(ret, NULL, 0);
        exit(0);
    }
}

void create_container() {
    logger("preparing root");
    prepare_dirs();
    // copy file
    logger("copying file");
    cpy_file("/home/eric/coding/busybox", "./new_root/bin/busybox");
    do_unshare();
    errExit("do unshare");

}

int main(int argc, char const *argv[]) {
    logger("Running...");
    create_container();
}

总结

本文通过Linux中的系统调用实现了一个超简单容器,由于缺少必须的配置信息如用户ID、网络、hostname等,busybox的一些命令无法使用。另外,程序中缺少必要的错误处理,因此代码仅供参考,欢迎评论指正。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值