Namespace隔离了进程、网路、用户等系统资源,本文将讲述如何通过Namespace创建一个超简单容器,并在容器内部运行简单的busybox程序。本文内容参考了LinuxNamespace系列(09),虽然目的相同,但本文采用C语言系统调用的方式实现,因此更加实用。
P.S:本文全部代码都在Ubuntu18.04Server下编译运行通过
步骤概览
- 准备busybox二进制文件
作者使用的是busybox_1.31.0,构建容器后会在容器内安装busybox,并运行busybox中的shell。 - 编写程序
- 创建所需目录,包括/bin, /proc, /old_root等
- 复制可执行文件
- 调用
unshare
创建Namespace - 调用
pivot_root
设置root文件系统的路径 - 挂载/proc等文件系统
- 执行
busybox --install ./bin
,运行./bin/sh
- 执行程序,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的一些命令无法使用。另外,程序中缺少必要的错误处理,因此代码仅供参考,欢迎评论指正。