跳转至

准备你的容器环境

构建容器的根文件系统(rootfs)

容器的根文件系统和完整操作系统的根文件系统并无太大区别,都包含了一个完整的操作系统目录结构和必要的文件。下面介绍三种构建容器根文件系统的方式。

本次实验推荐使用 LXC 镜像。

自己编写 init 程序与使用 BusyBox 构建根文件系统

实验一中介绍的两种构建初始内存盘(initrd)的方式在这里可以直接使用。唯一的区别是,在将文件放置妥当后,你不再需要将它们打包为 initrd.cpio.gz 文件,而是可以在容器中直接使用。如果你准备采用这两种方式,可以直接阅读实验一文档的相关章节。

从 LXC 镜像库下载容器镜像

Linux Containers(LXC)是一个系统容器实现,其使用 Linux 的容器技术运行完整的 Linux 子系统。由于容器直接使用主机的内核,没有了虚拟化中间层,与传统虚拟机(KVM, VMware)相比更轻便。

LXC 提供了多种操作系统镜像,你可以从 https://images.linuxcontainers.org/images/ 直接选择下载(请下载 amd64 架构下 default 中的 rootfs.tar.gzrootfs.tar.xz 文件)。不同的 Linux 发行版镜像不影响本实验的后续内容,但如果你不了解它们的区别的话,我们推荐 Ubuntu 18.04Ubuntu 20.04Debian Buster 的镜像。

下载到的打包 rootfs 文件只需要解压到一个新的目录即可:

mkdir rootfs/
cd rootfs/
tar zxf ../rootfs.tar.gz  # 或 tar Jxf ../rootfs.tar.xz

Vlab 平台的虚拟机即是 LXC 系统容器,但本实验不对 LXC 的实现作过多探究。

使用 chroot 与 systemd-nspawn 体验隔离环境与容器技术

chroot 是一个用于限制进程可以看见的 root 目录(根目录)的机制。在 chroot 之后产生的所有子进程,在正常情况下是无法看见 chroot 目录以外的内容的。尽管实际的容器并不使用 chroot(后面会讲到),理解它仍然是理解容器原理的重要的一步。

在你准备好的 BusyBox 或 LXC 系统镜像目录中运行 chroot . /bin/shchroot . /bin/bash,即可 chroot 进入这个系统镜像并对文件系统进行一些操作。如果你使用 LXC 镜像,你甚至可以在这里进行 apt update 等包管理操作。

这只是一个非常简单基础的隔离。root 用户可以轻易脱离不加额外限制的 chroot 监狱,并且 chroot 不阻止其他访问系统资源或其他进程的方式(你可以试试在 chroot 环境下执行 reboot——注意保存未完成的工作)。

systemd-nspawn

作为一个对比,systemd-nspawn 是一个最小化但完整的容器实现。你可以使用软件包管理器安装 systemd-container 来获得 systemd-nspawn 命令。

systemd-nspawn 的用法与 chroot 类似,首先 cd 到存有容器系统镜像的目录,然后运行 systemd-nspawn,你就可以得到一个 shell。尽管这个 shell 看起来与 chroot 中的 shell 没有什么区别,但是尝试使用某些命令就会发行明显的不同。下面列出几个例子,你可以自己分别在 systemd-nspawn 中和 chroot 中运行并比较:

reboot
mount  # 查看挂载点
dd if=/dev/sda of=test bs=64k count=1  # 尝试访问设备文件
echo $$  # 检查 shell 本身的 PID

错误处理

如果你使用 systemd-nspawn 时遇到了下面这个报错:

Failed to read machine ID from container image: Invalid argument

请向容器镜像中的 /etc/machine-id 文件写入一个 machine ID 字符串(使用以下命令),然后重新尝试运行.

$ systemd-machine-id-setup --root=容器的rootfs路径

对于 Ubuntu 20.04(或其他任何运行 systemd 240 或以上版本的系统),也可以使用新的命令

$ systemd-id128 new > etc/machine-id

使用 systemd-nspawn 启动一个容器

与 chroot 不同,systemd-nspawn 作为一个完整的容器实现,是可以启动容器中的操作系统的。方便起见,这一步推荐使用 LXC 镜像

为了确保你能够登录进容器系统中的用户,首先需要为 root 用户设置密码(或者清除密码)。使用 chroot 或 systemd-nspawn 进入容器的根文件系统,运行 passwd root 设置密码或 passwd -d root 清除密码,然后退出这个 shell。

再次使用 systemd-nspawn 将这个文件系统作为容器运行,不过这一次加上 --boot 参数。该参数会让 systemd-nspawn 尝试运行文件系统中的 init 程序(而不是一个 shell),从而“启动”这个文件系统中的操作系统。看到提示后,你就可以输入用户名 root 和密码登录进系统了,你甚至可以 poweroffreboot 这个容器中的系统而不用担心会导致主机被关机或重启。

准备你的容器

在你准备好 rootfs 之后,可以将本页底部助教编写的一个示例程序保存为 main.c 并编译运行它:

gcc -o lab4 main.c
./lab4 rootfs/ /bin/bash

这时候你就能看到一个“容器”中的 shell 了。当然了,这里的“容器”加上了双引号是因为它仅仅是一个 chroot 程序,因此接下来的任务就是对这个程序进行修改升级(你也可以选择自己从头编写一个)。

参考资料

示例程序

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h> // For wait(2)
#include <sys/wait.h>  // For wait(2)

const char *usage =
"Usage: %s <directory> <command> [args...]\n"
"\n"
"  Run <directory> as a container and execute <command>.\n";

void error_exit(int code, const char *message) {
    perror(message);
    _exit(code);
}

int main(int argc, char **argv) {
    if (argc < 3) {
        fprintf(stderr, usage, argv[0]);
        return 1;
    }
    if (chdir(argv[1]) == -1)
        error_exit(1, argv[1]);

    pid_t pid = fork();
    if (pid == 0) {
        // Child goes for target program
        if (chroot(".") == -1)
            error_exit(1, "chroot");
        execvp(argv[2], argv + 2);
        error_exit(255, "exec");
    }

    // Parent waits for child
    int status, ecode = 0;
    wait(&status);
    if (WIFEXITED(status)) {
        printf("Exited with status %d\n", WEXITSTATUS(status));
        ecode = WEXITSTATUS(status);
    } else if (WIFSIGNALED(status)) {
        printf("Killed by signal %d\n", WTERMSIG(status));
        ecode = -WTERMSIG(status);
    }
    return ecode;
}