วันนี้เราจะมาลองสร้าง Simple Container Runtime กันใน ภาษา Odin
ในการสร้าง Container run time เเบบง่ายๆๆจะต้องมี
-
Root File system ( Linux Alpine) https://dl-cdn.alpinelinux.org/alpine/latest-stable/releases/x86_64/alpine-minirootfs-3.22.0-x86_64.tar.gz
-
Linux Name Space
- Domain Namespace เเยก Hostname
- Process Namespace เเยก ProcessID
- Net Namespace เเยก Network stack
- Mnt Namespace เเยก Filesystem
-
Overlay Filesystem การทำ union file system ทำให้เราสามารถทำ isolation ตัว rootfs เพื่อป้องกันไม่ให้ตัว rootfs ถูกเขียนทับ
งั้นมาเริ่มกันเลยย
package main
import "core:fmt" # module formatting เหมือนกับ GO fmt.println("Hello")
import os "core:os" # module ของ OS ใช้จัดการเรื่อง env arguments file operations
import "core:strings" # module จัดการ string
import posix "core:sys/posix" # module system call
import c "core:c" # module ที่ใช้เรีัยก C library
foreign import libc "system:c"
foreign libc {
unshare :: proc "c" (flags: c.int) -> c.int ---
setns :: proc "c" (fd: c.int, nstype: c.int) -> c.int ---
mount :: proc "c" (source, target, fstype: cstring, flags: c.ulong, data: cstring) -> c.int ---
umount2 :: proc "c" (target: cstring, flags: c.int) -> c.int ---
execvp :: proc "c" (file: cstring, argv: [^]cstring) -> c.int ---
chroot :: proc "c" (path: cstring) -> c.int ---
chdir :: proc "c" (path: cstring) -> c.int ---
sethostname :: proc "c" (name: cstring, len: c.size_t) -> c.int ---
}
CLONE_NEWNS :: int(0x00020000)
CLONE_NEWPID :: int(0x20000000)
CLONE_NEWNET :: int(0x40000000)
CLONE_NEWUTS :: int(0x04000000)
MS_BIND :: int(0x00001000)
MS_MOVE :: int(0x00002000)
MS_REC :: int(0x00004000)
MS_PRIVATE :: int(0x00040000)
MNT_DETACH :: int(0x2)
Config :: struct {
rootfs: string,
hostname: string,
net_mode: string,
cmd: []string,
}
parse_args :: proc() -> Config {
cfg: Config
cfg.rootfs = "./rootfs"
cfg.net_mode = "host"
cfg.hostname = ""
start := 1
for i := 1; i < len(os.args); i += 1 {
arg := os.args[i]
if arg == "--" {
start = i + 1
break
}
if arg == "--rootfs" && i+1 < len(os.args) {
cfg.rootfs = os.args[i+1]
i += 1
continue
}
if arg == "--net" && i+1 < len(os.args) {
mode := os.args[i+1]
if mode == "host" || mode == "none" {
cfg.net_mode = mode
}
i += 1
continue
}
if arg == "--hostname" && i+1 < len(os.args) {
cfg.hostname = os.args[i+1]
i += 1
continue
}
}
if start < len(os.args) {
cfg.cmd = os.args[start:]
} else {
cfg.cmd = []string{"/bin/sh"}
}
return cfg
}
main :: proc() {
cfg := parse_args()
flags := CLONE_NEWNS | CLONE_NEWPID | CLONE_NEWUTS
if cfg.net_mode == "none" {
flags |= CLONE_NEWNET
}
เราจะเริ่มจาก เรียกใช้ function parse_args() เพื่อดึงค่า จาก struct Config เเล้วก็ ประกาศ flag ของ Namespace ที่เราจะสร้าง คือ Hostname, Process, Mount Namespace โดยที่ ถ้า config net_mode เป็น none ก็จะเพิ่ม Net Namespace มาด้วย
จากนั้น
if unshare(c.int(flags)) != 0 {
err := posix.get_errno()
fmt.printf("unshare failed: %v\n", err)
return
}
เรียก system call เเล้วใส่ flag เข้าไป เพื่อทำการเเยก process ออกจาก namespace
root := strings.clone_to_cstring("/", context.temp_allocator)
if mount(nil, root, nil, c.ulong(MS_PRIVATE|MS_REC), nil) != 0 {
err := posix.get_errno()
fmt.printf("mount(MS_PRIVATE|MS_REC) failed: %v\n", err)
return
}
int mount(const char *source, const char *target,
const char *filesystemtype, unsigned long mountflags,
const void *data);
จากนั้น เเปลง string เป็น C string เพื่อไปใช้กับ system call เเละทำให้ mount point ของ process เป็น private
pid := posix.fork()
if pid < 0 {
err := posix.get_errno()
fmt.printf("fork failed: %v\n", err)
return
}
จากนั้น Clone Process เดิม มาใช่ใน namespace
if pid == 0 {
lower := cfg.rootfs
ovl_base := "./ovl"
upper := fmt.tprintf("%s/upper", ovl_base)
work := fmt.tprintf("%s/work", ovl_base)
merged := fmt.tprintf("%s/merged", ovl_base)
_ = os.make_directory(ovl_base, 0)
_ = os.make_directory(upper, 0)
_ = os.make_directory(work, 0)
_ = os.make_directory(merged, 0)
จากนั้น ทำการ overlay file system
ovl_src := strings.clone_to_cstring("overlay", context.temp_allocator)
ovl_dir := strings.clone_to_cstring(merged, context.temp_allocator)
ovl_fs := strings.clone_to_cstring("overlay", context.temp_allocator)
opts_str := fmt.tprintf("lowerdir=%s,upperdir=%s,workdir=%s", lower, upper, work)
ovl_opts := strings.clone_to_cstring(opts_str, context.temp_allocator)
if mount(ovl_src, ovl_dir, ovl_fs, c.ulong(0), ovl_opts) != 0 {
err := posix.get_errno()
fmt.printf("overlay mount failed: %v\n", err)
os.exit(1)
}
เอาตัว overlay มา mount
rt := strings.clone_to_cstring(merged, context.temp_allocator)
if chroot(rt) != 0 {
err := posix.get_errno()
fmt.printf("chroot failed: %v\n", err)
os.exit(1)
}
if chdir(strings.clone_to_cstring("/", context.temp_allocator)) != 0 {
err := posix.get_errno()
fmt.printf("chdir failed: %v\n", err)
os.exit(1)
}
if cfg.hostname != "" {
cname := strings.clone_to_cstring(cfg.hostname, context.temp_allocator)
if sethostname(cname, c.size_t(len(cfg.hostname))) != 0 {
err := posix.get_errno()
fmt.printf("sethostname failed: %v\n", err)
}
จากนั้น เราจะมาเตรียม rootfs เเล้วก็เปียน root เดิม ให้การ root ของ rootfs เเล้วตั้งค่า host name
_ = os.make_directory("/proc", 0)
_ = os.make_directory("/sys", 0)
proc_src := strings.clone_to_cstring("proc", context.temp_allocator)
proc_dir := strings.clone_to_cstring("/proc", context.temp_allocator)
proc_fs := strings.clone_to_cstring("proc", context.temp_allocator)
if mount(proc_src, proc_dir, proc_fs, c.ulong(0), nil) != 0 {
err := posix.get_errno()
fmt.printf("mount /proc failed: %v\n", err)
os.exit(1)
}
sys_src := strings.clone_to_cstring("sysfs", context.temp_allocator)
sys_dir := strings.clone_to_cstring("/sys", context.temp_allocator)
sys_fs := strings.clone_to_cstring("sysfs", context.temp_allocator)
_ = mount(sys_src, sys_dir, sys_fs, c.ulong(0), nil)
argv: [dynamic]cstring
for arg in cfg.cmd {
_, _ = append(&argv, strings.clone_to_cstring(arg, context.temp_allocator))
}
_, _ = append(&argv, cast(cstring)nil)
if execvp(argv[0], raw_data(argv)) != 0 {
err := posix.get_errno()
fmt.printf("execvp failed: %v\n", err)
os.exit(127)
}
- เตรียม
/procและ/sys→ mountprocfsและsysfs - สร้าง
argvสำหรับ command ที่จะรัน execvp()→ เปลี่ยนตัวเองเป็น process จริงใน container
} else {
status: i32
_ = posix.waitpid(pid, &status, posix.Wait_Flags{})
if posix.WIFEXITED(status) {
ex := posix.WEXITSTATUS(status)
fmt.printf("child exited with status %d\n", ex)
} else if posix.WIFSIGNALED(status) {
sig := posix.WTERMSIG(status)
fmt.printf("child killed by signal %d\n", sig)
} else {
fmt.println("child terminated")
}
}
}
จากนั้น ถ้ามีอะไร ผิดพลาดก็
- ออกจากเอง → print exit code
- ถูก kill → print signal
- อื่น ๆ → print terminated