本文主要介绍在 MacOS 上使用 qemu 搭建 Linux Kernel 的开发环境。(在开始以前须要注意的是,本文中的 Linux 开发环境是一个远程服务器,而 qemu 被安装在本地的 MacOS 上。一般并不须要这样折腾,直接将 qemu 安装在 Linux 中更加方便,并且 qemu 是能够 -nographic
无图形界面运行的。)html
qemu 是一个硬件虚拟化程序( hypervisor that performs hardware virtualization),与传统的 VMware / VirtualBox 之类的虚拟机不一样,它能够经过 binary translation 模拟各类硬件平台(好比在 x86 机器上模拟 ARM 处理器)。而 VirtualBox 等更可能是经过虚拟化来进行资源隔离,以便在其上运行多个 guest os。linux
基于 qemu 的硬件模拟能力,咱们能够轻松搭建指定硬件平台的运行实验环境。git
qemu 与 VirtualBox 另外一个不一样点在于,在 VirtualBox 上必须安装一个完整的操做系统套件,而经过 qemu 咱们能够经过参数直接启动到一个裸的 Linux Kernel,连 bootloader 都不须要关心。在此以外,按需配置相关工具套件与启动好的 Kernel 一块儿工做便可。shell
qemu 提供的这种高度可定制化的『白盒』能力,使得咱们能够按需构建快速、轻量级的开发环境,提供流畅的开发体验。bash
首先,为了进行内核开发,须要一个现成的 Linux 操做系统环境。能够是一个经过 ssh 工做的远程 Linux Server,或者也能够在 MacOS 上经过 VirtualBox (或者使用 qemu 也能够)安装一个虚拟机用于开发。VirtualBox 的安装和 Linux Guest OS 的安装配置此处略过不提。服务器
接下来,安装 qemu。在 MacOS 上可使用 Homebrew 包管理工具进行安装(本文使用的 qemu 版本为 2.9.0_2):app
brew install qemu
安装完成后,能够看到系统中有不少个 qemu-system-
开头的命令,用于模拟各类硬件平台,好比 qemu-system-x86_64
。运行其中一个命令来验证安装是否成功:ssh
qemu-system-x86_64
上述命令会启动一个相似 VirtualBox 虚拟机启动时的窗口。固然,因为咱们没有指定任何设备,最终会提示找不到可启动设备。ide
按需编译内核,此处只进行简单说明(基于内核 v4.13)。工具
能够先执行 make help
能够查看 make 支持哪些 target。
一般先进行内核编译配置:
make menuconfig
会启动一个基于文本的配置界面进行各类选项、模块、驱动等配置。或者也能够直接使用目标平台默认的配置,如针对 x86_64 平台(后续平台相关的地方均以 x86_64 为例进行说明)可使用:
make x86_64_defconfig
配置完成后相应的配置项会保存在 .config
文件中。下一次执行 make menuconfig
时能够 load 这份配置文件,在此基础上进行修改。
咱们构建一个压缩过的内核镜像:
make bzImage
编译成功后,bzImage 文件将出如今 arch/x86_64/boot/bzImage
。记住文件路径或者拷贝到一个方便的路径,便于后续启动时使用。
接下来,编译在配置阶段选择的内核模块:
make modules
编译好的内核模块 *.ko
文件存在于模块对应的源码目录中。
编译好内核之后,咱们就可使用 qemu 启动内核了。只须要使用 -kernel
参数告诉 qemu 内核文件的位置便可:
qemu-system-x86_64 \ -m 512M \ # 指定内存大小 -smp 4\ # 指定虚拟的 CPU 数量 -kernel ./bzImage # 指定内核文件路径
上述命令假设编译好的 bzImage 内核文件就存放在当前目录下。由于以前编译好的内核文件是在 VirtualBox 的虚拟机中(或者在远程服务器上),而 qemu 在本地 MacOS 上,能够经过 VirtualBox 的 share folder 来共享目录,或者使用 NFS 共享,甚至简单使用 rsync 来在二者之间同步文件。后续关于文件同步与共享再也不赘述。
不出意外的话,就能够在启动窗口中看到内核的启动日志了。在内核启动的最后,会出现一条 panic 日志:
Kernel panic - not syncing: VFS: Unable to mount root fs on unknown-block(0, 0)
从日志内容能够看出,内核启动到必定阶段后尝试加载根文件系统,但咱们没有指定任何磁盘设备,因此没法挂载根文件系统。并且上一节中编译出来的内核模块如今也没有用上,内核模块也须要存放到文件系统中供内核须要的时候进行加载。
因此,接下来须要制做一个磁盘镜像文件供内核做为根文件系统加载。
如上一节所述,须要制做一个磁盘镜像文件做为根文件系统供内核加载,同时也用于存放编译好的内核模块,以及后续所需的各类配套工具程序。
使用 qemu-img
建立一个 512M 的磁盘镜像文件:
qemu-img create -f raw disk.raw 512M
如今 disk.raw 文件就至关于一块磁盘,为了在里面存储文件,须要先进行格式化,建立文件系统。好比在 Linux 系统中使用 ext4 文件系统进行格式化:
mkfs -t ext4 ./disk.raw
格式化完成以后,能够在 Linux 系统中以 loop 方式将磁盘镜像文件挂载到一个目录上,这样就能够操做磁盘镜像文件中的内容了。
下面的命令将磁盘镜像文件挂载到 img 目录上:
sudo mount -o loop ./disk.raw ./img
如今能够将以前编译好的内核模块安装到磁盘镜像中了。命令以下:
sudo make modules_install \ # 安装内核模块 INSTALL_MOD_PATH=./img # 指定安装路径
执行完成后便可在 ./img/lib/modules/
下看到安装好的内核模块。
准备好磁盘镜像文件后,使用下面的命令再次启动 qemu:
qemu-system-x86_64 \ -m 512M \ -smp 4\ -kernel ./bzImage \ -drive format=raw,file=./disk.raw \ # 指定文件做为磁盘 -append "root=/dev/sda" # 内核启动参数,指定根文件系统所在设备
这一次,内核再也不报根文件系统找不到了。可是报了另外一个错误:
Kernel panic - not syncing: No working init found. Try passing init= option to Kernel. See Linux Documentation/admin-guide/init.rst for guidance.
这说明内核启动已经接近完成了,准备启动 1 号进程,也就是 init 进程。但咱们的启动参数里面没有指定 init 选项,并且磁盘镜像中也没有相应的 init 程序。所以,接下来须要准备一个 init 程序供内核启动。
经常使用的 init 程序有下面几种:
ls
、cat
等。busybox 很是轻量级,能够编译出彻底独立无依赖的 busybox 套件。这里选用 busybox 做为 init 程序及其它命令工具的提供者。
下载 busybox 的源码到 Linux 系统中,准备进行编译,这里使用的 busybox 版本为 1.27.2。
busybox 的编译流程与内核很像,这里咱们基于默认配置进行编译。首先,执行以下命令让默认配置生效:
make defconfig
接下来,在默认配置的基础上进行定制:
make menuconfig
这里有一个重要的配置,由于 busybox 将被用做 init 程序,并且咱们的磁盘镜像中没有任何其它库,因此 busybox 须要被静态编译成一个独立、无依赖的可执行文件,以避免运行时发生连接错误。配置路径以下:
Busybox Settings ---> --- Build Options [*] Build BusyBox as a static binary (no shared libs)
最后,配置完成后执行编译:
make
编译完成后在当前目录下能够看到 busybox
可执行文件,查看大小才 2.5M 左右。整个 busybox 套件只有这一个可执行文件,里面包含了若干工具。好比:
./busybox ls -l ./busybox ps
编译好 busybox 以后须要将其安装到磁盘镜像中以供使用。执行以下命令进行安装:
make CONFIG_PREFIX=<path_to_disk_img_mount_point> install
CONFIG_PREFIX
用于指定安装路径,须要指定到以前磁盘镜像文件的挂载目录,好比 ./img
。进入磁盘镜像挂载目录查看,常见的文件系统结构已经创建起来了。查看 bin 和 sbin 目录下的命令,能够看到都是连接到 bin/busybox
的,busybox 会根据执行时的文件名来执行不一样的功能。
busybox 安装完成以后,使用内核启动参数 init=
来指定 busybox 做为 init 程序,再次尝试启动。
qemu-system-x86_64 \ -m 512M \ -smp 4\ -kernel ./bzImage \ -drive format=raw,file=./disk.raw \ -append "init=/linuxrc root=/dev/sda"
上述命令经过 init=/linuxrc
指定了 init 程序为根目录下的 linuxrc,其实是一个指向 busybox 的软连接。
这一次内核成功找到了 init 程序而且建立出 init 进程,可是 init 执行过程当中出现以下报错:
can't run '/etc/init.d/rcS': No such file or directory can't open /dev/tty3: No such file or directory can't open /dev/tty4: No such file or directory
看样子,init 程序须要一些配置才能正常运行起来。
参考 busybox 代码中的 文档 可知,init 启动后会扫描 /etc/inittab
配置文件,这个配置文件决定了 init 程序的行为。而 busybox init 在没有 /etc/inittab
文件的状况下也能工做,由于它有默认行为。它的默认行为至关于以下配置:
::sysinit:/etc/init.d/rcS ::askfirst:/bin/sh ::ctrlaltdel:/sbin/reboot ::shutdown:/sbin/swapoff -a ::shutdown:/bin/umount -a -r ::restart:/sbin/init tty2::askfirst:/bin/sh tty3::askfirst:/bin/sh tty4::askfirst:/bin/sh
参考文档,咱们提供一份 /etc/inittab
配置文件以下:
::sysinit:/etc/init.d/rcS ::askfirst:/bin/ash ::ctrlaltdel:/sbin/reboot ::shutdown:/sbin/swapoff -a ::shutdown:/bin/umount -a -r ::restart:/sbin/init
而且根据配置,咱们建立可执行文件 /etc/init.d/rcS
,内容以下(暂时什么事都不作):
#!/bin/sh
配置完成之后再次尝试启动,此次将成功启动,而且出现以下提示:
Please press Enter to activate this console.
按提示按下 Enter 键以后将会启动 shell,进行到咱们熟悉的环境,能够执行各类经常使用命令了。
查看当前系统环境,会发现当前文件系统结构是不完整的。好比没有 /dev, /proc 以及 /sys 挂载点。这样咱们没法经过 /dev 查看系统中的设备,若是执行 df
命令也会由于没有 /proc 挂载点而报错:
df: /proc/mounts: No such file or directory
所以,咱们须要手工建立 /dev, /proc, /sys 这三个目录。/dev 目录建立完成后重启系统便可工做,但 /proc 和 /sys 须要执行挂载才可工做,能够将 /proc 和 /sys 的挂载动做放到 /etc/init.d/rcS
中,每次系统启动时自动挂载。修改 /etc/init.d/rcS
内容以下:
#!/bin/sh mount -t proc proc /proc mount -t sysfs sysfs /sys
从新启动系统查看,能够看到 /dev, /proc, /sys 挂载点都相应有了内容。
本文介绍了经过 qemu 做为模拟器,本身动手编译内核,并从头配置 init 进程,构建出一个最小的可运行系统,可用于验证对内核的改动。
经过此次开发环境搭建,对系统的启动过程有了一个粗略的了解。但这只是迈出了第一步,后续还有长路漫漫。