会玩,使用 init 进程运行 Container

news/2025/2/23 5:49:39

0c071edb12395754fc9ea82e8f133fb8.gif

背景

关注过 Bare Metal 相关项目的同学应该都了解过系统的启动流程、如何快速的置备一台物理服务器等之类的实现方式,通常都需要运行一个 LiveOS 来实现某些动作。在 Tinkerbell 项目中,使用 Linuxkit 来作为 LiveOS,Plunder 项目中使用 BOOTy 来作为  LiveOS。前几天 @thebsdbox 将 BOOTy 中的一部分抽离了出来,作为 ginit  展示主要的实现方式,可以更好的让我们理解安装环节中的具体细节。今天来看一下这个项目。

如果安装一个 CentOS,那么通常是会通过 kernel + initramfs.img 启动,initramfs.img 中会包含  systemd 、anaconda、dracut 等一些列组件,然后通过 systemd 指定不同的 Target 所属/依赖/顺序来完成最终  Anaconda 调用。Anaconda 通过解析 /proc/cmdline 中的 KickStart 参数来决定自己的安装方式。

ginit 项目展示了以下内容:

  • 制作 initramfs.img

  • 通过 Container image 制作一个 RAW image

  • 通过 QEMU 使用 RAW image 和 Linux Kernel 来运行一个虚拟机

  • ginit 自动运行 Container 中 entrypoint 指令

流程演示

通过 Container image 制作一个 RAW image

RAW image 中最终不会包含 Kernel 部分,以 Nginx Container 为例。提取 nginx:latest image 中的 Entrypoint ,通过 dd 置备一个 RAW image,并格式化为 ext4 ,raw image 作为 loop 设备挂载到本地,通过 docker export 将 Nginx Image 拷贝到挂载点下,卸载挂载点,最终 RAW image 包含了 Nginx Container 的所有内容。这里的 RAW image 因为不包含 kernel,所以无法直接启动,只是作为后续动作的依赖。

Nginx Container 默认的 Entrypointdocker-entrypoint.sh ,通过这个脚本来做一些参数检查动作。

#!/bin/bash

echo "Lets build you a disk image!"
docker pull $1
ENTRYPOINT=$(docker inspect -f '{{.Config.Entrypoint}}' $1 | sed 's/[][]//g')
echo "Creating a 200MB Disk"
dd if=/dev/zero of=disk.img bs=1024k count=200
mkfs.ext4 -F disk.img
mkdir -p /tmp/disk
mount -t ext4 -o loop disk.img /tmp/disk/
echo "Converting $1 to disk image"
docker create --name exporter $1 null
docker export exporter | tar xv -C /tmp/disk
docker rm exporter
umount /tmp/disk
echo The command $ENTRYPOINT will start this container

使用 ginit 制作 initramfs.img

静态编译 ginit;下载并编译 busybox ,将 ginit 编译结果 init 放置到 / 路径下,通过 cpio 将 busybox 归档,使用 gzip 进行压缩。所有流程完成后,将最终得到的 initramfs.cpio.gz 拷贝到项目路径下。initramfs 最终包含的是 busybox + ginit

# syntax=docker/dockerfile:experimental

# Build ginit as an init
FROM golang:1.17-alpine as dev
RUN apk add --no-cache git ca-certificates gcc linux-headers musl-dev
COPY . /go/src/github.com/thebsdbox/ginit/
WORKDIR /go/src/github.com/thebsdbox/ginit
ENV GO111MODULE=on
RUN --mount=type=cache,sharing=locked,id=gomod,target=/go/pkg/mod/cache \
    --mount=type=cache,sharing=locked,id=goroot,target=/root/.cache/go-build \
    CGO_ENABLED=1 GOOS=linux go build -a -ldflags "-linkmode external -extldflags '-static' -s -w" -o init
    

# Build Busybox
FROM gcc:10.1.0 as Busybox
RUN apt-get update; apt-get install -y cpio
RUN curl -O https://busybox.net/downloads/busybox-1.31.1.tar.bz2
RUN tar -xf busybox*bz2
WORKDIR busybox-1.31.1
RUN make defconfig; make LDFLAGS=-static CONFIG_PREFIX=./initramfs install

WORKDIR initramfs 
COPY --from=dev /go/src/github.com/thebsdbox/ginit/init .

# Package initramfs
RUN find . -print0 | cpio --null -ov --format=newc > ../initramfs.cpio 
RUN gzip ../initramfs.cpio
RUN mv ../initramfs.cpio.gz /

FROM scratch
COPY --from=Busybox /initramfs.cpio.gz .

通过 QEMU 运行 Container 中的 EntryPoint 指令

到目前状态,我们得到了 initramfs.img ,得到了 raw image,但是还缺少 Linux Kernel 。可以直接从 Ubuntu 提供的 netboot[1] 下载 boot executable bzImage 文件。

现在所有的准备工作都进行完成了,我们可以直接通过 QEMU 来运行虚拟机,其中 Nginx 所需运行环境在 RAW Image 中,ginit 所需运行环境在 initramfs 中。

前面有提到,Nginx Container 默认的 Entrypointdocker-entrypoint.sh,用来做一些参数包装,所以这里我将参数改为了 /usr/sbin/nginx

qemu-system-x86_64 -nographic \
  -kernel ./linux \
  -append "entrypoint=/usr/sbin/nginx root=/dev/sda console=ttyS0" \
  -initrd ./initramfs.cpio.gz \
  -hda ./disk.img \
  -m 1G

虚拟机 console 是 ttyS0 ,通过终端运行可以直接查看启动日志:

...
[    1.469920] rtc_cmos 00:00: setting system clock to 2022-03-05T06:36:19 UTC (1646462179)
[    1.525397] ata1.00: ATA-7: QEMU HARDDISK, 2.5+, max UDMA/100
[    1.525579] ata1.00: 409600 sectors, multi 16: LBA48 
[    1.532980] ata2.00: ATAPI: QEMU DVD-ROM, 2.5+, max UDMA/100
[    1.540741] scsi 0:0:0:0: Direct-Access     ATA      QEMU HARDDISK    2.5+ PQ: 0 ANSI: 5
[    1.545673] sd 0:0:0:0: [sda] 409600 512-byte logical blocks: (210 MB/200 MiB)
[    1.547063] sd 0:0:0:0: [sda] Write Protect is off
[    1.547515] sd 0:0:0:0: Attached scsi generic sg0 type 0
[    1.548188] sd 0:0:0:0: [sda] Write cache: enabled, read cache: enabled, doesn't support DPO or FUA
[    1.550227] scsi 1:0:0:0: CD-ROM            QEMU     QEMU DVD-ROM     2.5+ PQ: 0 ANSI: 5
[    1.568178] sd 0:0:0:0: [sda] Attached SCSI disk
[    1.578345] sr 1:0:0:0: [sr0] scsi3-mmc drive: 4x/4x cd/rw xa/form2 tray
[    1.578736] cdrom: Uniform CD-ROM driver Revision: 3.20
[    1.582611] sr 1:0:0:0: Attached scsi generic sg1 type 5
[    1.595655] Freeing unused decrypted memory: 2040K
[    1.666044] Freeing unused kernel image memory: 2712K
[    1.666482] Write protecting the kernel read-only data: 22528k
[    1.669246] Freeing unused kernel image memory: 2008K
[    1.670507] Freeing unused kernel image memory: 1192K
[    1.742691] x86/mm: Checked W+X mappings: passed, no W+X pages found.
[    1.743002] Run /init as init process
INFO[0000] Folder created [dev] -> [/dev]          
INFO[0000] Folder created [proc] -> [/proc]        
INFO[0000] Folder created [sys] -> [/sys]          
INFO[0000] Folder created [tmp] -> [/tmp]          
INFO[0000] Mounted [dev] -> [/dev]                 
INFO[0000] Mounted [proc] -> [/proc]               
INFO[0000] Mounted [sys] -> [/sys]                 
INFO[0000] Mounted [tmp] -> [/tmp]                 
INFO[0000] Starting DHCP client                    
INFO[0000] Starting ginit                          
ERRO[0000] Error finding adapter [Link not found]  
[    2.209227] tsc: Refined TSC clocksource calibration: 2893.182 MHz
[    2.209573] clocksource: tsc: mask: 0xffffffffffffffff max_cycles: 0x29b41aa25d4, max_idle_ns: 440795325238 ns
[    2.209984] clocksource: Switched to clocksource tsc
INFO[0002] Beginning provisioning process          
ERRO[0002] route ip+net: no such network interface 
INFO[0002] Folder created [root] -> [/mnt]         
[    3.902861] random: fast init done
[    3.912319] EXT4-fs (sda): recovery complete
[    3.913757] EXT4-fs (sda): mounted filesystem with ordered data mode. Opts: (null)
[    3.914463] ext4 filesystem being mounted at /mnt supports timestamps until 2038 (0x7fffffff)
INFO[0002] Mounted [root] -> [/mnt]                
INFO[0002] Mounted [dev] -> [/mnt/dev]             
INFO[0002] Mounted [proc] -> [/mnt/proc]           
INFO[0002] Starting Shell                          
INFO[0002] Waiting for command to finish...        
/ #

其中 [ 1.743002] Run /init as init process 中的 /init 已经是我们上面编译的 ginitginit 运行的日志输出为:

INFO[0000] Folder created [dev] -> [/dev]          
INFO[0000] Folder created [proc] -> [/proc]        
INFO[0000] Folder created [sys] -> [/sys]          
INFO[0000] Folder created [tmp] -> [/tmp]          
INFO[0000] Mounted [dev] -> [/dev]                 
INFO[0000] Mounted [proc] -> [/proc]               
INFO[0000] Mounted [sys] -> [/sys]                 
INFO[0000] Mounted [tmp] -> [/tmp]                 
INFO[0000] Starting DHCP client                    
INFO[0000] Starting ginit                          
ERRO[0000] Error finding adapter [Link not found]  
[    2.209227] tsc: Refined TSC clocksource calibration: 2893.182 MHz
[    2.209573] clocksource: tsc: mask: 0xffffffffffffffff max_cycles: 0x29b41aa25d4, max_idle_ns: 440795325238 ns
[    2.209984] clocksource: Switched to clocksource tsc
INFO[0002] Beginning provisioning process          
ERRO[0002] route ip+net: no such network interface 
INFO[0002] Folder created [root] -> [/mnt]         
[    3.902861] random: fast init done
[    3.912319] EXT4-fs (sda): recovery complete
[    3.913757] EXT4-fs (sda): mounted filesystem with ordered data mode. Opts: (null)
[    3.914463] ext4 filesystem being mounted at /mnt supports timestamps until 2038 (0x7fffffff)
INFO[0002] Mounted [root] -> [/mnt]                
INFO[0002] Mounted [dev] -> [/mnt/dev]             
INFO[0002] Mounted [proc] -> [/mnt/proc]           
INFO[0002] Starting Shell                          
INFO[0002] Waiting for command to finish...

主要做了几件事情:创建必要的路径,创建对应的设备,启动一个 DHCP Client 来获取 IP 地址,挂载 RAW Image 到 /mnt 下,通过 chroot 运行 entrypoint 参数中指定的程序,在这里是 /usr/sbin/nginx ,最终提供一个 Shell 环境给用户。我们可以通过 ps 命令查看当前所运行的进程:

/ # ps -ef |grep -v '\['
PID   USER     TIME  COMMAND
    1 0         0:01 /init
  178 0         0:00 nginx: master process /usr/sbin/nginx
  179 0         0:00 /bin/sh
  180 101       0:00 nginx: worker process
  193 0         0:00 ps -ef
/ # df 
Filesystem           1K-blocks      Used Available Use% Mounted on
devtmpfs                497020         4    497016   0% /dev
tmpfs                   502392         0    502392   0% /tmp
/dev/sda                181984    150940     16708  90% /mnt
devtmpfs                497020         4    497016   0% /mnt/dev
/ # ls /mnt/docker-entrypoint.sh 
/mnt/docker-entrypoint.sh
/ # ls /mnt/usr/sbin/nginx
/mnt/usr/sbin/nginx
/ # ls -hl /init
-rwxr-xr-x    1 0        0           3.4M Mar  5 04:20 /init

现在我们已经将一个 Container Image 中要运行的指令,通过 Linux kernel 配合 initramfs  来运行了起来,在 Bare Metal 场景下,我们可以将 Nginx 内置到 initramfs 中,将 Nginx 替换为 Docker  或者 Container 然后暴露出去,物理服务器作为 Docker Server,置备服务器作为 Docker Client  连接物理服务器进行指定容器的运行,最终完成物理服务器 OS 的安装,这也是目前 TinkerBell 的实现方式。

ginit 具体实现

创建系统设备并挂载

DefaultMountsDefaultDevices 中定义了一些必须的设备如 /dev/null, /dev/random, /dev/urandom ,和挂载点,如 /dev,/proc, /tmp, /sys

urandom := Device{
  CreateDevice: false,

  Name:  "urandom",
  Path:  "/dev/urandom",
  Mode:  syscall.S_IFCHR,
  Major: 1,
  Minor: 9,
 }

dev := Mount{
  CreateMount: false,
  EnableMount: false,
  Name:        "dev",
  Source:      "devtmpfs",
  Path:        "/dev",
  FSType:      "devtmpfs",
  Flags:       syscall.MS_MGC_VAL,
  Mode:        0777,
 }
 m.Mount = append(m.Mount, dev)

//cmd.Execute()
m := realm.DefaultMounts()
d := realm.DefaultDevices()
dev := m.GetMount("dev")
dev.CreateMount = true
dev.EnableMount = true

proc := m.GetMount("proc")
proc.CreateMount = true
proc.EnableMount = true

tmp := m.GetMount("tmp")
tmp.CreateMount = true
tmp.EnableMount = true

sys := m.GetMount("sys")
sys.CreateMount = true
sys.EnableMount = true

// Create all folders
m.CreateFolder()
// Ensure that /dev is mounted (first)
m.MountNamed("dev", true)

// Create all devices
d.CreateDevice()

// Mount any additional mounts
m.MountAll()

在基本环境准备完成后,启动 DHCP Client,获取 IP 地址:

log.Println("Starting DHCP client")
 go realm.DHCPClient()

 // HERE IS WHERE THE MAIN CODE GOES
 log.Infoln("Starting ginit")
 time.Sleep(time.Second * 2)

 log.Infoln("Beginning provisioning process")

 mac, err := realm.GetMAC()
 if err != nil {
  log.Errorln(err)
  //realm.Shell()
 }
 fmt.Print(mac)

现在系统环境准备好了,网络也准备好了,那么可以运行具体的指令了,获取指令的方式是通过解析 /proc/cmdline/proc/cmdline 是通过我们在创建 VM 的时候通过 --append 传递的:

在解析到 rootentrypoint 参数值后,通过 Mountroot 挂载到对应的挂载点,通过 chroot 运行 entrypoint

stuffs, err := ParseCmdLine(CmdlinePath)
if err != nil {
 log.Errorln(err)
}
_, err = realm.MountRootVolume(stuffs["root"])
if err != nil {
 log.Errorf("Disk Error: [%v]", err)
}

cmd := exec.Command("/usr/sbin/chroot", []string{"/mnt", stuffs["entrypoint"]}...)
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr

err = cmd.Start()
if err != nil {
 log.Errorf("command error [%v]", err)
}
err = cmd.Wait()
if err != nil {
 log.Errorf("error [%v]", err)
}

realm.Shell()

所有程序运行完成后,提供一个Shell 环境给用户:

// Shell will Start a userland shell
func Shell() {
 // Shell stuff
 log.Println("Starting Shell")

 // TTY hack to support ctrl+c
 cmd := exec.Command("/usr/bin/setsid", "cttyhack", "/bin/sh")
 cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr

 err := cmd.Start()
 if err != nil {
  log.Errorf("Shell error [%v]", err)
 }
 log.Printf("Waiting for command to finish...")
 err = cmd.Wait()
 if err != nil {
  log.Errorf("Shell error [%v]", err)
 }
}

总结

ginit 作为一个最小实现方便我们快速了解 init 具体做了什么,将 ginit 替换为 systemd 同理,但是直接看 systemd bootup 容易迷失在成堆的 Target 依赖中。在查找资料的过程中还看到了 https://github.com/QuentinPerez/busygox 做了类似的事情,可以作为参考。

参考链接

  • https://github.com/thebsdbox/ginit

  • https://unix.stackexchange.com/questions/146284/minimal-linux-with-kernel-and-busybox-etc-inittab-is-ignored-only-init-is-ex/147688#147688

  • https://github.com/QuentinPerez/busygox

引用链接

[1]

netboot: http://archive.ubuntu.com/ubuntu/dists/focal-updates/main/installer-amd64/current/legacy-images/netboot/ubuntu-installer/amd64/

原文链接:https://zdyxry.github.io/2022/03/05/%E4%BD%BF%E7%94%A8-init-%E8%BF%9B%E7%A8%8B%E8%BF%90%E8%A1%8C-Container/

a1c443d9482b79dd1baabbe47b3d605f.gif

afb1982248997cddd137ef268b88cb73.png

你可能还喜欢

点击下方图片即可阅读

48f42c90614b35b930f3e7a711e14399.png

Koyeb 容器云——Heroku 的继承者?

c2901615ba4228bc35be22b17658864d.gif

云原生是一种信仰 🤘

关注公众号

后台回复◉k8s◉获取史上最方便快捷的 Kubernetes 高可用部署工具,只需一条命令,连 ssh 都不需要!

d7cbb36cc46274f8b1745d645c50ed8b.gif

1c912db9ec0fc00bf0a49085897d361a.gif

点击 "阅读原文" 获取更好的阅读体验!

发现朋友圈变“安静”了吗?

7d41e9569ef638a43685eb6673bb86f5.gif


http://www.niftyadmin.cn/n/1749443.html

相关文章

nginx使用时间戳+rewrite实现请求动态调度

2019独角兽企业重金招聘Python工程师标准>>> 要实现一个随时间周期动态调度请求的功能,简单查了下都说内置变量没有时间戳,$time_local变量因为格式问题用起来不方便,都推荐用tengine了。又仔细看了下,发现是有一个$ms…

oracle数据库怎么设置f8查询,PL/SQL设置

1、PL/SQL Developer记住登陆密码在使用PL/SQL Developer时,为了工作方便希望PL/SQL Developer记住登录Oracle的用户名和密码;设置方法:PL/SQL Developer 7.1.2 ->tools->Preferences->Oracle->Logon History , “St…

TCPDUMP 高级规则使用

概述 在了解 tcpdump 的高级规则之前, 需要对 IP, TCP 和 UDP 的报文首部有大致的了解, 实际上很多网络工具的使用都是基于报文首部的结构做相应的操作. 在了解报文结构后, 也可以按需实现私有的功能, 比如抓取匹配的请求, 再做相应的处理, Snapper 就是根据 TCP 首部信息实现的…

又一宕机事故!都怪当初没做好故障演练系统……

刘志志去哪儿网 系统运维工程师致力于提升工程师的效率和保证交付过程的质量,在私有云搭建及测试领域都有深入研究,目前主要从事Devops平台以及混沌工程相关系统的开发和维护。大家好,我是来自去哪儿网的刘志志,19年加入去哪儿网&…

php论文答辩一般会问什么问题,一般论文答辩可以看稿子吗

各个学校要求不同。但是从给各位答辩老师的印象来看,脱稿或者半脱稿会比较好。毕竟你要写这篇论文,应该对相关资料和写作思路比较有把握了,而且答辩老师们不会故意为难你什么。论文答辩的一般会问什么答辩老师出题是有严格的界定范围的&#…

字节数据库技术大揭秘!

数据库技术一直是 IT 信息技术中极其重要的一环,在步入云原生时代以后,分布式数据库系统获得了令人振奋的发展,云基础设施和数据库进一步整合,弥补了传统数据库的痛点,带来了高可扩展性、全面自动化、快速部署、节约成…

页面检测用户浏览器是否为IE,判断客户端是否安装flash插件

为什么80%的码农都做不了架构师?>>> 一、检测客户端浏览器是否有flash插件(静态方法) $.extend({checkBrowserFalsh:function(){var userAgent navigator.userAgent; //获取浏览器userAgent信息if(userAgent ! undefined){//检测是否是IE浏览器if(…

php 阿贾克斯页面回调输出,AJAX(阿贾克斯)的简单应用

1.ajax 对象的属性说明(常用的)(1)open(method,url,boolean);创建请求,method :填请求类型(get post),url :请求的地址,boolean:true代表同步(即ajax操作过程中仍然可以操作html)(2)send("name"name); 发送请求,通过属性…