在调试内核一节中我们使用 initramfs + bzImage 构建了一个基础的 linux kernel 调试环境, 但是目前的所有操作都只会保存在 initramfs 中, 目前可用的软件很少, 没有编译器, 没有联网, 没有 apt 包管理工具, 不能持久化存储
本文介绍如何使用 qemu 模拟更加复杂的环境, 以及如何利用现有的 Linux 发行版根文件系统(比如说 Ubuntu 的基础软件包)来搭建一个可用的 linux 开发环境, 并且使用自己编译的内核启动
手动构建软件包环境是十分繁琐的, 读者感兴趣的话可以了解一下 linux from scratch 项目,lfs 介绍的非常完善,您可以从软件代码开始一点一点编译得到一个真正的可以使用的 linux 发行版。这非常有趣!
本文介绍一种比较简单直接的方式,即利用现有的 Linux 发行版提供的系统映像安装, 这里以 Ubuntu 为例。Ubuntu 提供了已经构建好的云镜像 ubuntu cloud-images。
这个网址有很多目录,按照不同的 ubuntu 版本名区分,这里读者可以自行选择希望使用的版本,笔者这里选用一个较为稳定且年轻的版本 ubuntu22.04(jammy)
点进去之后可以看到很多日期,ubuntu 云镜像的构建是比较频繁的,基本上隔几天就会自动构建一次。这里随便选一个新的即可。里面可以看到不同架构(amd64/arm64/riscv64)等,对应后面有介绍说明,我们下载 .img 镜像。它是一个 QCow2 UEFI/GPT Bootable disk image,也就是说我们可以直接通过 qemu 启动这个镜像。
同理我们可以在 minimal 目录下的 release/jammy 下载一个最小化的镜像(290M),本文后续会使用这个镜像作为演示完成基础配置,基础配置大同小异,并无很大差别,读者可以放心阅读。
wget https://cloud-images.ubuntu.com/minimal/releases/jammy/release-20251001/ubuntu-22.04-minimal-cloudimg-amd64.imgTIP
直接使用 wget 下载速度可能有点慢,国内下载可以使用清华源 tuna mirrors,mirror 中搜索 ubuntu 选择 ubuntu-cloud-images 即可,它的目录结构和 ubuntu 官方源是一样的
前文调试内核中介绍了一个使用 initramfs 启动调试内核的 Makefile,但是现在有了一个完整的 linux 发行版镜像我们就不再需要自己构建的 initramfs 了,我们将 Makefile 更新如下。其中 IMG 修改为你的镜像名即可
.PHONY: init qemu clean debug
KERNEL = bzImage
QEMU = qemu-system-x86_64
IMG = jammy-minimal-cloudimg-amd64.img
disk:
$(QEMU) \
-m 4G \
-enable-kvm \
-kernel $(KERNEL) \
-drive format=qcow2,file=$(IMG) \
-append "root=/dev/sda1 console=ttyS0" \
-nographic -no-reboot -d guest_errors
debug:
$(QEMU) \
-m 4G \
-enable-kvm \
-kernel $(KERNEL) \
-drive format=qcow2,file=$(IMG) \
-append "root=/dev/sda1 console=ttyS0" \
-nographic -no-reboot -d guest_errors \
-S -s接着可以启动 qemu 了,这里我们使用了 -enable-kvm 进行加速(如果kvm需要sudo那就加上)
TIP
启动镜像会发现内核日志正常启动,然后 systemd 启动各种服务 ok,然后在一个地方卡了许久 "A-start-job-is-running-for-wait-for-network-to-be-configured-ubuntu-server" 这是因为当前的宿主机没有联网
WARNING
如果启动失败了,那么先看一下内核的报错日志能否解决,大概率是因为缺少了相关的内核模块,因为部分内核模块是在核外编译的,需要把它们安装到内核 /lib/modules/ 下面之后才可以完成装载。可以先跳过此步完成后面的磁盘扩容/挂载/安装内核模块之后再次尝试启动。或者删除掉 --kernel 和 --append 参数采用镜像默认的内核启动
之后进入了登录页面,要求我们输入用户名和密码
这时你会发现怎么输入都不对,因为这个镜像并没有配置默认的密码,我们需要使用 virt-customize 来为镜像初始化密码,首先安装 virt-customize 所属的软件包
sudo apt install guestfs-tools使用 virt-customize 为当前镜像重新设置 root 密码,然后就可以通过 root + 密码登陆了
virt-customize -a jammy-minimal-cloudimg-amd64.img --root-password password:<your-passwd>NOTE
virt-customize 是一个处理磁盘镜像很好用的工具,除了修改密码它还有很多有趣的功能(包括拷贝删除文件,安装软件包等等)。它本质是使用了 supermin + libguestfs,在一个微型虚拟机(appliance VM)中挂载磁盘并修改
WARNING
virt-customize 在 WSL 中不生效,因为 WSL 不能支持 libguestfs,你可以把镜像拷贝到一个真实的 ubuntu 服务器上操作完成再回到 wsl
如果读者使用 df 查看当前存储系统可以发现这个系统竟然有 2GB 的磁盘空间?但是明明我下载的 img 没有这么大啊?
root@ubuntu:~# df -h
Filesystem Size Used Avail Use% Mounted on
/dev/root 2.0G 860M 1.2G 44% /
tmpfs 2.0G 0 2.0G 0% /dev/shm
tmpfs 787M 420K 786M 1% /run
tmpfs 5.0M 0 5.0M 0% /run/lock
/dev/sda15 105M 6.1M 99M 6% /boot/efi
tmpfs 394M 4.0K 394M 1% /run/user/0这是因为 .img 是 QCOW2,这种格式是稀疏,按需分配的。宿主机上看到的 319M 是已经分配的数据量,虚拟机内部看到的 2.2G 是“虚拟磁盘的逻辑大小 (virtual size)”
$ qemu-img info jammy-minimal-cloudimg-amd64.img
image: jammy-minimal-cloudimg-amd64.img
file format: qcow2
virtual size: 2.2 GiB (2361393152 bytes)
disk size: 318 MiB
cluster_size: 65536
Format specific information:
compat: 0.10
compression type: zlib
refcount bits: 16
Child node '/file':
filename: jammy-minimal-cloudimg-amd64.img
protocol type: file
file length: 318 MiB (333774848 bytes)
disk size: 318 MiB初始下载的镜像是一个 minimal 的镜像,还并不能像一个成熟的发行版一样使用,下面我们依次解决磁盘和网络的配置问题
初始磁盘容量一般不大,联网之后稍微下载几个文件就满了。为磁盘扩容非常简单,使用 qemu-img 为当前镜像 +8GB 磁盘容量
qemu-img resize <img> +8G
# 直接 resize 到 10G
qemu-img resize <img> 10G但是进入虚拟机之后 df -h 虽然可以看到 /dev/sda 变大了,但是显示的磁盘容量还是 2.2GB?
上文 qemu-img info 的信息我们可以看到,一个虚拟磁盘(qcow2/raw)实际上是一个容器,里面包含:
而 qemu-img resize 只改了虚拟磁盘大小,没有改分区表、文件系统。所以当前的容器状态可以理解为 2.2GB 之后加了一段 free 的空闲磁盘,但是并没有划分分区也没有创建文件系统
[ qcow2 image ]
|-- sda1 (2.2G) ----|---- free 8G ----|此时需要在虚拟机内部执行
growpart /dev/sda 1
resize2fs /dev/sda1其中第一步 growpart 用于修改分区表,让 /dev/sda1 从 2.2GB 到占满整个磁盘
[ qcow2 image ]
|-- sda1 (2.2G) ----|---- free 8G ----|
$ growpart /dev/sda 1
|-- sda1 (10.2G) ----------------------|growpart 扩大了 sda 分区,但是此时 free 磁盘部分还没有被文件系统使用,所以我们需要 resize2fs /dev/sda1 来扩展文件系统的元数据和数据区,让文件系统(一般来说是ext4)真正使用新的空间
文件系统需要初始化磁盘划分 inode/data block 之后才能正常存储数据,并不是直接拿来空磁盘就可以直接使用的,详见文件系统磁盘布局
NOTE
简单总结一下,磁盘扩容需要三步
qemu-img resize <img> +8G growpart /dev/sda 1 resize2fs /dev/sda1
- qemu-img resize 扩大的是“虚拟磁盘容器”
- growpart 扩大的是“分区”
- resize2fs 扩大的是“文件系统”
多块磁盘或者多个分区只需要调整 /dev/xxx 即可
有时候我们希望添加一些在磁盘镜像中添加一些内容, 比如希望把本机上面的文件拷贝到虚拟机中,一种可用的方式当然是通过网络(ftp/scp),另一种更简单性能更好的方式就是挂载磁盘镜像然后直接复制过去。
要把 QCOW2 磁盘镜像挂载到本地(像挂载普通目录一样访问)很简单,最常用、最稳定的方法是 使用 qemu-nbd。这是官方推荐方式,不需要转换格式,也不需要复制磁盘。
sudo modprobe nbd max_part=8sudo qemu-nbd --connect=/dev/nbd0 your.qcow2此时我们可以查看分区信息
$ lsblk /dev/nbd0
nbd0 8G
├─nbd0p1 1G
└─nbd0p2 7Gsudo mount /dev/nbd0p1 mnt/sudo umount mnt
sudo qemu-nbd --disconnect /dev/nbd0
sudo rmmod nbd另一种方式是使用 guestmount
# 安装
sudo apt install libguestfs-tools
# 挂载
guestmount -a your.qcow2 -i my_mnt
# 卸载
guestunmount /mntTIP
虽然看上去好像 guestmount 步骤少了很多,但是你用一下就会发现拷贝文件等操作非常慢,因为 guestmount 本质上是 libguestfs + FUSE 文件系统,libguestfs 内部会运行一个轻量化 QEMU,FUSE 有多次用户态/内核态切换,以及 guestmount 默认的安全模式(甚至root用户都不能cp,只能调用guestmount的普通用户才有权限)都会拉低性能
所以建议使用 qemu-nbd 的方式
如果磁盘并不是 qcow2 格式,例如读者使用了一个 raw 格式的磁盘安装 iso 得到的镜像,那么需要先查看一下该磁盘的分区,因为安装 ubuntu 的话默认第一个分区是 /boot 的分区不能直接挂载
sudo fdisk -l ubuntu.raw此时发现该磁盘有两个分区, 第二个分区为根分区, 所以应该跳过第一个分区, 找到对应的偏移量开始挂载
偏移量计算为 (4095 + 1) * 512 = 2097152,每个扇区 512 字节,需要跳过前面 4095 个扇区,第 4096 个扇区是对应的根分区起始地址
sudo mount -o loop,offset=2097152 ubuntu.raw tmp关于挂载的更多内容详见mount
内核中有一些组件并不是直接编译进内核的, 绝大部分内核模块都是核外编译保存的,它们可以在合适的时机装载进内核,在内核启动之后也会加载一些内核模块。
我们在之前的 Makefile 中使用了 --kernel 参数传递了自己编译的内核,如果使用 uname -r 查看应该是 6.6.0+ 版本
root@ubuntu:~# uname -r
6.6.0+默认所有内核模块都会被安装到 /lib/modules/$(uname -r) 下,这个目录下目前只有一个 5.15.0-1088-kvm,它是这个镜像默认使用的内核
首先挂载镜像,然后在内核的源码库下执行
make modules_install INSTALL_MOD_PATH=/path/to/mnt将 modules 目录下的格式如下, kernel 目录下是所有的内核模块,build 目录是一个指向源码目录的软链接,如果要在虚拟机内编译对应的内核模块可以考虑将内核源码树拷贝进去。当然也可以选择在外侧编译然后把编译好的模块拷贝进去。本系列文章选择使用在外侧编译,内核模块编译方法详见内核模块编译
└── lib
└── modules
└── 6.6.0+
├── build -> /home/kamilu/klinux
└── kernel
├── drivers
│ ├── net
│ │ └── ethernet
│ │ └── intel
│ │ └── e1000
│ └── thermal
│ └── intel
├── fs
│ └── efivarfs
└── net
├── ipv4
│ └── netfilter
└── netfilter将该目录 copy 到磁盘镜像的 /lib/modules 下即可
进去之后发现终端没有彩色输出, 修改 .bashrc, 找一个 ubuntu 默认的 .bashrc 拷贝过来 github gist
export TERM=xterm-256color这一步我们选择一个比较老的 Ubuntu 18.04 mini.iso, 只有 70MB 大小, 其余软件是从网络上下载的
这个版本的镜像没有安装 openssh, 需要先安装
sudo apt install net-tools
sudo apt-get install openssh-client
sudo apt-get install openssh-server
sudo /etc/init.d/ssh restart最后检查一下防火墙是不是关闭了
sudo ufw status
# inactive 表示关闭接下来就可以使用 ssh 从外部连接了
qemu 的网络配置比较复杂, 可以做很多模式的连接, 这里简单介绍一下最基础的联网配置, 至少可以用 apt 下载软件包了
大部分情况我们需要使用自己编译的内核, 在编译内核的时候选择 E1000 网卡驱动
-> Device Drivers
-> Network device support (NETDEVICES [=y])
-> Ethernet driver support (ETHERNET [=y])
-> Intel devices (NET_VENDOR_INTEL [=y])
-> Intel(R) PRO/1000 Gigabit Ethernet support (E1000 [=y])也可以作为模块编译, 不过建议编译进内核比较省事
启动 qemu 时添加如下参数
qemu-system-x86_64 \
-m 4G \
-kernel /home/kamilu/klinux/arch/x86/boot/bzImage \
-drive format=raw,file=disk/ubuntu.raw \
-append "root=/dev/sda2 console=ttyS0" \
-nographic -no-reboot -d guest_errors -serial mon:stdio \
-netdev user,id=net0,hostfwd=tcp:127.0.0.1:2222-:22 -device e1000,netdev=net0查看网络配置, 不出意外的话网卡名应该是 enp0s3, 如果不是的话下面对应的名字也替换一下
kamilu@ubuntu2404:~$ ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
...
2: enp0s3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
...
3: sit0@NONE: <NOARP> mtu 1480 qdisc noop state DOWN group default qlen 1000
link/sit 0.0.0.0 brd 0.0.0.0其中 enp0s3 网卡还并未配置, 创建一个网卡配置文件
sudo vim /etc/netplan/01-netcfg.yaml写入如下内容, 需要注意的是 YAML 对缩进非常敏感, 直接复制进去可能会有缩进问题, 建议手打
network:
version: 2
ethernets:
enp0s3:
addresses:
- 10.0.2.15/24
routes:
- to: default
via: 10.0.2.2
nameservers:
addresses:
- 8.8.8.8
- 8.8.4.4修改文件权限
sudo chmod 600 /etc/netplan/01-netcfg.yaml应用 Netplan 配置
sudo netplan apply此时网络就已经完成了配置, 再次查看网络配置
kamilu@ubuntu2404:~$ ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host noprefixroute
valid_lft forever preferred_lft forever
2: enp0s3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
link/ether 52:54:00:12:34:56 brd ff:ff:ff:ff:ff:ff
inet 10.0.2.15/24 brd 10.0.2.255 scope global enp0s3
valid_lft forever preferred_lft forever
inet6 fec0::5054:ff:fe12:3456/64 scope site dynamic mngtmpaddr noprefixroute
valid_lft 86274sec preferred_lft 14274sec
inet6 fe80::5054:ff:fe12:3456/64 scope link
valid_lft forever preferred_lft forever
3: sit0@NONE: <NOARP> mtu 1480 qdisc noop state DOWN group default qlen 1000
link/sit 0.0.0.0 brd 0.0.0.0然后就可以正常联网使用了, 再次启动也会自动联网
WARNING
存疑, 还有问题! 不要用!
一般的网络配置建议直接使用 user
-netdev user 模式适用于简单的网络需求,但如果你需要更复杂的网络配置或完整的桥接网络连接,可以考虑使用桥接模式。
首先在你的主机上创建 tap0 接口,并将其添加到一个网桥中
sudo ip tuntap add tap0 mode tap user $(whoami)
sudo ip link set tap0 up
sudo brctl addbr br0
sudo brctl addif br0 tap0
sudo dhclient br0其中最后一步可能会卡住, 通常是因为网桥 br0 没有正确配置或没有有效的网络接口连接, 可以尝试为网桥 br0 手动分配静态 IP 地址,避免依赖 DHCP:
sudo ip addr add 192.168.1.100/24 dev br0 # 替换为你网络的 IP 地址段
sudo ip link set br0 up要配置桥接网络,修改启动参数
qemu-system-x86_64 \
-m 4G \
-kernel /home/kamilu/klinux/arch/x86/boot/bzImage \
-drive format=raw,file=disk/ubuntu.raw \
-append "root=/dev/sda2 console=ttyS0" \
-nographic -no-reboot -d guest_errors -serial mon:stdio \
-netdev tap,id=net0,ifname=tap0,script=no,downscript=no -device e1000,netdev=net0如果初始创建的磁盘镜像随着使用发现空间不足, 可以比较方便的扩容
第一步将磁盘镜像文件的大小从 20GB 扩展到 50GB
qemu-img resize ubuntu.raw 50G扩展磁盘后,还需要在虚拟机内部调整分区和文件系统以使用新增的空间, 使用 cfdisk 调整分区 /dev/sda
如果是用的 if=virtio 那么是 /dev/vda
sudo cfdisk /dev/sda选择 /dev/sda2 分区将其扩容至 50GB
使用 df -h 查看磁盘剩余容量, 发现还是 20G
kamilu@ubuntu2404-VM:~$ df -h
Filesystem Size Used Avail Use% Mounted on
/dev/root 20G 8.2G 11G 45% /
tmpfs 95G 0 95G 0% /dev/shm
tmpfs 38G 892K 38G 1% /run
tmpfs 5.0M 0 5.0M 0% /run/lock
tmpfs 19G 12K 19G 1% /run/user/1000但是 lsblk 可以看到分区信息已经更新
kamilu@ubuntu2404-VM:~$ lsblk
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS
sda 8:0 0 50G 0 disk
├─sda1 8:1 0 1M 0 part
└─sda2 8:2 0 50G 0 part /
sr0 11:0 1 1024M 0 rom这是因为文件系统的信息还没有更新, 查看根目录的文件系统类型(默认是ext4)
kamilu@ubuntu2404-VM:~$ df -Th
Filesystem Type Size Used Avail Use% Mounted on
/dev/root ext4 20G 8.2G 11G 45% /
tmpfs tmpfs 95G 0 95G 0% /dev/shm
tmpfs tmpfs 38G 892K 38G 1% /run
tmpfs tmpfs 5.0M 0 5.0M 0% /run/lock
tmpfs tmpfs 19G 12K 19G 1% /run/user/1000重新更新文件系统信息
sudo resize2fs /dev/sda2ext4 更新块信息后再次查看发现磁盘大小正常
kamilu@ubuntu2404-VM:~$ df -h
Filesystem Size Used Avail Use% Mounted on
/dev/root 50G 8.2G 39G 18% /
tmpfs 95G 0 95G 0% /dev/shm
tmpfs 38G 892K 38G 1% /run
tmpfs 5.0M 0 5.0M 0% /run/lock
tmpfs 19G 12K 19G 1% /run/user/1000也有一些读者可能就像手动安装一下,当然也可以,在 ubuntu server 查找到 Ubuntu 的 server 镜像 iso 文件,也有一些其他存档镜像:
下载的 iso 是光盘 CD/DVD 映像, 需要先创建一个存储介质用于安装系统
qemu-img create ubuntu.raw -f raw 20G安装系统, 其中 -drive -cdrom 的路径自行调整
qemu-system-x86_64 \
-m 4G \
-drive format=raw,file=disk/ubuntu.raw,if=virtio \
-cdrom iso/ubuntu-24.04-live-server-amd64.iso具体的安装过程略过, 注意不要开启 LVM, 启用 OpenSSH
安装完成之后就不再需要 -cdrom 了, 可以直接通过磁盘启动
qemu-system-x86_64 \
-m 4G \
-drive format=raw,file=disk/ubuntu.raw,if=virtio启动会有点慢, 耐心一些
启动之后可以使用 lsblk 看一下当前的所有磁盘, 启动 sr0 代表第一个光盘驱动器(CD-ROM Drive), vda 代表第一个 Virtio 磁盘设备
kamilu@kamilu:~$ lsblk
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS
sr0 11:0 1 1024M 0 rom
vda 253:0 0 20G 0 disk
├─vda1 253:1 0 1M 0 part
└─vda2 253:2 0 20G 0 part /由于命令行中设置参数中 if(interface) 指定为
if=virtio所以这里显示的是 vda, 如果设置为if=sd则这里应该是 sda, 这里设置为 virtio 可以让设备通过允许客体系统直接与主机通信而绕过仿真层, 以实现高效的磁盘I
可以看到 /dev/vda1 是用于启动的 /boot 分区, /dev/vda2 是挂载的根分区, 也是后面启动时需要手动指定的分区