基于 debootstrap 和 busybox 构建 mini ubuntu

时间:2024-01-01 20:31:33

基于 debootstrap 和 busybox 构建 mini ubuntu

最近的工作涉及到服务器自动安装和网络部署操作系统,然后使用 ansible 和 saltsatck 进行配置并安装 openstack 。

难点在于服务器的自动安装,由于不单只是通过 PXE 安装服务器,还需要能够安装时进行分区、配置网卡等工作,因此需要在开始安装前,必须先收集服务器的硬件信息。

调研了一下目前的开源项目中,提供此类功能的有 tinycorelinux 、 puppet razor-el-mk 可做类似的工作。tinycorelinux 是个很好的工具,整个系统在 PXE 之后在内存中执行,可在里面加上简单的 agent 完成任务报告的工作;razor 是 puppet 绑定在一起用的,el-mk 基于 centos ,它在里面装了 razor 的 agent,使用 facter 进行硬件信息收集。

这些方案的基本思路都是相通的,首先通过 PXE 下载 microkernel ,然后直接在内存中执行,启动网卡,运行 agent 并向服务器汇报信息,并接收来自服务器的命令。基本的技术原理都是 PXE + linux initramfs ,根据不同的需要向 initramfs 中加硬件驱动。

仔细研究了一下之后,发现用 debootstrap + busybox 工具做这样的小系统会更加简单,有以下的优点:

  1. debootstrap 生成的小 ubuntu 能方便使用 apt 安装额外的工具
  2. 可直接把驱动模块拷贝到小镜像内使用
  3. 定制脚本非常简单容易

整个小系统在不安装额外的软件和内核模块的情况下,为 100 M 左右,并可加入 busybox 后裁减到 40-50 M(包含完整的基础库)。在安装了 python3 (完整的 python3 ),可裁减到 110 M 左右。

在开始前,可能需要先了解一下 initramfs 的原理,可看 http://www.iteedu.com/os/linux/mklinuxdiary/ch3initrd/index.php 。从本质上看,initramfs 就是一个经过裁减过的 linux 系统的完整文件系统(去掉 kernel 、删掉没有用的软件),然后在内存中展开,并执行程序(/init /sbin/init)。

一个最简单的 mini ubuntu

在 ubuntu 14.04 上,用 debootstrap 生成一个 minibase 的 ubuntu 系统,其中包含了整个基本的文件系统和 apt 工具(不包含 kernel) ::

sudo debootstrap --variant=minbase trusty mini \
http://mirrors.aliyun.com/ubuntu/

这个地方用了 aliyun 的 mirror ,也可换成 163 或其它的 mirror 。可以使用 chroot 切换进去,用 dpkg 查看已安装的软件 ::

sudo chroot mini dpkg -l
# 或
sudo chroot mini /bin/bash

为了让它能作为 initramfs 被 kernel 启动,需要在根目录下放一个 init 文件,在 init 文件中写上启动过程(文件系统挂载、执行 /sbin/init等),以下是从 initramfs-tools 工具里面抄了一部分出来(/usr/share/initramfs-tools/init),由于不挂载硬盘上的 root 分区,因此减少了很多代码。

cat << EOF | sudo tee mini/init
#!/bin/sh [ -d /dev ] || mkdir -m 0755 /dev
[ -d /root ] || mkdir -m 0700 /root
[ -d /sys ] || mkdir /sys
[ -d /proc ] || mkdir /proc
[ -d /tmp ] || mkdir /tmp
mkdir -p /var/lock
mount -t sysfs -o nodev,noexec,nosuid sysfs /sys
mount -t proc -o nodev,noexec,nosuid proc /proc
# Some things don't work properly without /etc/mtab.
ln -sf /proc/mounts /etc/mtab grep -q '\<quiet\>' /proc/cmdline || echo "Loading, please wait..." # Note that this only becomes /dev on the real filesystem if udev's scripts
# are used; which they will be, but it's worth pointing out
if ! mount -t devtmpfs -o mode=0755 udev /dev; then
echo "W: devtmpfs not available, falling back to tmpfs for /dev"
mount -t tmpfs -o mode=0755 udev /dev
[ -e /dev/console ] || mknod -m 0600 /dev/console c 5 1
[ -e /dev/null ] || mknod /dev/null c 1 3
fi
mkdir /dev/pts
mount -t devpts -o noexec,nosuid,gid=5,mode=0620 devpts /dev/pts || true
mount -t tmpfs -o "noexec,nosuid,size=10%,mode=0755" tmpfs /run
mkdir /run/initramfs
# compatibility symlink for the pre-oneiric locations
ln -s /run/initramfs /dev/.initramfs # Set modprobe env
export MODPROBE_OPTIONS="-qb" # mdadm needs hostname to be set. This has to be done before the udev rules are called!
if [ -f "/etc/hostname" ]; then
/bin/hostname -b -F /etc/hostname 2>&1 1>/dev/null
fi exec /sbin/init
EOF sudo chmod +x mini/init

当 GRUB 载入 kernel 和 initramfs 后,kernel 会把 initramfs 在内存中展开,然后执行其根目录下的 init ,也就是上面的脚本。以上的脚本会执行 mount 工作,准备好目录结构,然后执行 /sbin/init 转换入 ubuntu 的初始化过程(system-v init ,upstart , systemd,用 udev 自动创建设备文件等)。

修改 ubuntu 的启动配置,进行自动登录 ::

for i in $(find mini/etc/init -type f -name "tty*"); do
sed -e "s|/sbin/getty -8|/sbin/getty --autologin root -8|" -i $i
done

注意,minibase 中没有包含内核模块和硬件驱动,需要从 kernel 或本机中把模块 copy 进去,这里通过 apt-get 下载一个 linux-image 并解压得到内核模块 ::

sudo apt-get download linux-image-$(uname -r)
sudo rm -rf linux-image-$(uname -r)
sudo dpkg -x $(find . -type f -name "linux-image-$(uname -r)*.deb" | head -n 1) linux-image-$(uname -r)
sudo cp -af linux-image-$(uname -r)/lib mini/

然后生成模块依赖关系 ::

sudo chroot mini depmod

清理一下 ::

sudo chroot mini apt-get clean

现在整个 initramfs 已经准备好,可以打包,并 copy 到 boot 目录 ::

(cd mini; sudo find . | sudo cpio -o -H newc | sudo gzip -9 > ../initramfs-mini.gz)
sudo cp -f initramfs-mini.gz /boot

重启电脑,在 grub 目录处按 c 进入 cmdline ,用以下的命令引导(可用 tab 补全) ::

linux /vmlinux-<xxxxx>
initrd /initramfs-mini.gz
boot

boot 之后,会由 kernel 解压 initramfs-mini.gz ,然后很快进入到熟悉的 ubuntu 命令提示中。这个基本的 linux 能运行常见的操作,除了还缺少像 ping 之类的工具(可通过 apt-get 安装),直接在内存中执行,与平常使用无异(由于所有的文件都在内存中,加载命令的速度应该更加快一点)。

使用 busybox 替换基本命令并裁减

上面生成的小系统已经基本可用了,但如果还想继续减少体积,以供在内存较少的机器上运行,那么还可以继续进行裁减,最重要的步骤就是用 busybox 处理大部分的工作,甚至包括设备驱动的加载和热插拔。

在 minbase 的基础上安装 busybox-staic ::

sudo chroot mini apt-get -y --no-install-recommends install \
busybox-static

定义 apps 和 extra_apps 变量来保存 busybox 所支持的命令,定义函数用于用 bubybox 替换原来的命令 ::

applets=$(mini/bin/busybox --list)
apps=
for i in $applets; do
apps="$apps $(sudo chroot mini which $i)"
done
extra_apps=
for i in $applets; do
if ! sudo chroot mini which $i > /dev/null; then
extra_apps="$extra_apps /bin/$i "
fi
done function fix_missing() {
for i in $apps $extra_apps; do
if ! test -f mini/$i; then
sudo ln -sf /bin/busybox mini/$i
fi
done
}

然后,开始大扫除,清理可被直接删除的包 ::

sudo chroot mini apt-get -y --force-yes purge adduser busybox-initramfs \
cpio ifupdown initramfs-tools initscripts initramfs-tools-bin \
iproute2 locales mountall makedev plymouth procps upstart libprocps3 \
libcgmanager0 libusb-1.0-0 usbutils libdbus-1-3 libnih-dbus1 libdrm2 \
libjson-c2 libjson0 kmod libkmod2 module-init-tools file libmagic1 \
libnih1 libplymouth2 pciutils libudev1 udev
fix_missing

强制去掉不需要的包 ::

sudo chroot mini /bin/sh -c 'echo "Yes, do as I say!" |\
apt-get -y --force-yes purge diffutils findutils hostname'
fix_missing sudo chroot mini /bin/sh -c 'echo "Yes, do as I say!" |\
apt-get -y --force-yes purge -y --force-yes login libmount1 mount\
grep gzip sed mount'
fix_missing sudo chroot mini /bin/sh -c 'echo "Yes, do as I say!" |\
apt-get -y --force-yes purge -y --force-yes sysvinit-utils lsb-base\
e2fsprogs e2fslibs bsdutils libblkid1 libuuid1 passwd tzdata insserv'
fix_missing sudo chroot mini /bin/sh -c 'echo "Yes, do as I say!" |\
apt-get -y --force-yes purge ncurses-base ncurses-bin'
sudo chroot mini /bin/sh -c 'echo "Yes, do as I say!" |\
apt-get purge coreutils'
fix_missing

这里还可以继续清理,把一些不需要的 lib 都去掉。

然后清理 apt 的缓存 ::

sudo chroot mini apt-get clean
sudo rm -rf mini/usr/share/locale/*
sudo rm -rf mini/usr/share/man/*
sudo rm -rf mini/usr/share/doc/*
sudo rm -rf mini/var/log/*
sudo rm -rf mini/var/lib/apt/lists/*
sudo rm -rf mini/var/cache/*
sudo rm -rf mini/etc/rc*

由于在清理的过程中已经去掉了 udev 、 system-v init 、 upstart 等,需要一个支持 busybox 的新 init 脚本。在 ubuntu 里面原来的 udev 本身支持初始化加载驱动和设备插拔,但是 busybox 的 mdev 只在 hotplug 的时候调用,网上的很多资料也没有提到怎样在初始化时加载驱动模块,搜了一下 busybox 的邮件,找到了直接查找 /sys/devices 目录加载驱动模块的方式 http://lists.busybox.net/pipermail/busybox/2009-April/068894.html ::

cat > mini/init << EOF
#!/bin/sh
mount -t proc -o nodev,noexec,nosuid proc /proc
mount -t sysfs -o nodev,noexec,nosuid sysfs /sys
ln -sf /proc/mounts /etc/mtab
mount -t tmpfs -o size=64k,mode=0755 tmpfs /dev
[ -e /dev/console ] || mknod -m 0600 /dev/console c 5 1
[ -e /dev/null ] || mknod /dev/null c 1 3
mkdir -p /dev/pts
mount -t devpts -o noexec,nosuid,gid=5,mode=0620 devpts /dev/pts
mount -t tmpfs -o "noexec,nosuid,size=10%,mode=0755" tmpfs /run
echo "Loading modules..."
# hotplug with mdev
echo /bin/mdev > /proc/sys/kernel/hotplug
mdev -s
# coldplug: load devices supporting modules
find /sys/devices -name modalias | xargs -r cat | xargs -r modprobe -qa
# extra modules
[ -f /etc/modules ] && cat /etc/modules | grep -v "^[[:blank:]]#" |\
xargs -r modprobe -qa
exec /sbin/init
EOF
chmod +x mini/init

修改使用 busybox 的 init ::

ln -sf /bin/busybox mini/bin/sh
ln -sf /bin/busybox mini/sbin/init

busybox 的 init 需要一个 inittab 文件描述初始化过程 ::

cat > mini/etc/inittab << EOF
::sysinit:/etc/init.d/rcS
::ctrlaltdel:/sbin/reboot
::shutdown:/sbin/swapoff -a
::shutdown:/bin/umount -a -r
::restart:/sbin/init
tty1::respawn:/bin/sh
tty2::askfirst:/bin/sh
tty3::askfirst:/bin/sh
tty4::askfirst:/bin/sh
EOF touch mini/etc/init.d/rcS
chmod +x mini/etc/init.d/rcS

同样,加入 kernel 模块 ::

sudo apt-get download linux-image-$(uname -r)
sudo rm -rf linux-image-$(uname -r)
sudo dpkg -x $(find . -type f -name "linux-image-$(uname -r)*.deb" | head -n 1) linux-image-$(uname -r)
sudo cp -af linux-image-$(uname -r)/lib mini/
sudo chroot mini depmod

打包,并 copy 到 boot 目录 ::

(cd mini; sudo find . | sudo cpio -o -H newc | sudo gzip -9 > ../initramfs-mini.gz)
sudo cp -f initramfs-mini.gz /boot

重启,在 grub 目录处按 c 进入 cmdline ::

linux /vmlinux-<xxxxx>
initrd /initramfs-mini.gz
boot

在这一步,得到的整个目录的大小为 80 M 左右(包含 kernel 模块),但是还可以继续进行裁减。

深入裁减

du -hs * mini 查看整个目录,可看到目前占用空间最多的就是内核的模块目录了,看一下内核的模块目录,差不多占了一半的空间。

以下是内核模块的占用空间情况(mini/lib/modules/4.4.0-31-generic) ::

2.6M    arch
1.4M crypto
11M drivers
8.1M fs
1.8M lib
12M net
832K sound
508K ubuntu
20K virt

net 里面包括了 ceph 、netfilter 、openvswitch、atm(如果你只是一个普通程序员,可能你一辈子都不会碰到这货)、x2、appletalk 等等,都是可以删除的。

其它的看情况办,记得删完之后,重新检查依赖关系 ::

chroot mini depmod

mini/usr/lib 目录也很大,里面有些库是可以删的,像 mini/usr/lib/x86_64-linux-gnu/gconv 里面的一大堆编码库,看不过去的删掉一些。libdb-5.3.so 也很大,没有用 man 等是可以删掉的。

这个阶段删东西就不能用 apt-get 了,最多用一下 dpkg ,并且强制清理掉,最后可以把 apt 也清理了。到这个阶段就没有什么技术问题,只要有足够的细心和耐心,就能弄到一个足够小的系统。

其它

还有一些其它方法可进行 linux 的定制,如 LFS http://linuxfromscratch.org/ 、 buildroot https://buildroot.org/ 等。

如果想把这个小 linux 保存到硬盘中,也很好办。整个 mini copy 到一个单独的分区上,加载根目录所在的分区为根分区,在 init 脚本通过 switch_root 切换到 /sbin/init ,假如在 /sda3 ::

# 模块加载完成之后 ...
mount /dev/sda3 /mnt
mkdir -p /mnt/mnt
exec switch_root /mnt /sbin/init

需要注意的是,由于根分区切换, /proc /sys /dev /tmp 等目录需要进行额外的处理,要么把该目录用 mount -o move 到 mnt 下,或者在 mini/fstab 文件中定义挂载点。