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的一些命令無法使用。另外,程序中缺少必要的錯誤處理,因此代碼僅供參考,歡迎評論指正。