381 lines
15 KiB
Markdown
381 lines
15 KiB
Markdown
|
# 60 | 搭建操作系统实验环境(上):授人以鱼不如授人以渔
|
|||
|
|
|||
|
操作系统的理论部分我们就讲完了,但是计算机这门学科是实验性的。为了更加深入地了解操作系统的本质,我们必须能够做一些上手实验。操作系统的实验,相比其他计算机课程的实验要更加复杂一些。
|
|||
|
|
|||
|
我们做任何实验,都需要一个实验环境。这个实验环境要搭建在操作系统之上,但是,我们这个课程本身就是操作系统实验,难不成要自己debug自己?到底该咋整呢?
|
|||
|
|
|||
|
我们有一个利器,那就是qemu啊,不知道你还记得吗?它可以在操作系统之上模拟一个操作系统,就像一个普通的进程。那我们是否可以像debug普通进程那样,通过qemu来debug虚拟机里面的操作系统呢?
|
|||
|
|
|||
|
这一节和下一节,我们就按照这个思路,来试试看,搭建一个操作系统的实验环境。
|
|||
|
|
|||
|
运行一个qemu虚拟机,首先我们要有一个虚拟机的镜像。咱们在[虚拟机](https://time.geekbang.org/column/article/108964)那一节,已经制作了一个虚拟机的镜像。假设我们要基于 [ubuntu-18.04.2-live-server-amd64.iso](http://ubuntu-18.04.2-live-server-amd64.iso),它对应的内核版本是linux-source-4.15.0。
|
|||
|
|
|||
|
当时我们启动虚拟机的过程很复杂,设置参数的时候也很复杂,以至于解析这些参数就花了我们一章的时间。所以,这里我介绍一个简单的创建和管理虚拟机的方法。
|
|||
|
|
|||
|
在[CPU虚拟化](https://time.geekbang.org/column/article/109335)那一节,我留过一个思考题,OpenStack是如何创建和管理虚拟机的?当时我给了你一个提示,就是用libvirt。没错,这一节,我们就用libvirt来创建和管理虚拟机。
|
|||
|
|
|||
|
## 创建虚拟机
|
|||
|
|
|||
|
首先,在宿主机上,我们需要一个网桥。我们用下面的命令创建一个网桥,并且设置一个IP地址。
|
|||
|
|
|||
|
```
|
|||
|
brctl addbr br0
|
|||
|
ip link set br0 up
|
|||
|
ifconfig br0 192.168.57.1/24
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
为了访问外网,这里还需要设置/etc/sysctl.conf文件中net.ipv4.ip\_forward=1参数,并且执行以下的命令,设置NAT。
|
|||
|
|
|||
|
```
|
|||
|
iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
接下来,就要创建虚拟机了。这次我们就不再一个个指定虚拟机启动的参数,而是用libvirt。首先,使用下面的命令,安装libvirt。
|
|||
|
|
|||
|
```
|
|||
|
apt-get install libvirt-bin
|
|||
|
apt-get install virtinst
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
libvirt管理qemu虚拟机,是基于XML文件,这样容易维护。
|
|||
|
|
|||
|
```
|
|||
|
<domain type='qemu'>
|
|||
|
<name>ubuntutest</name>
|
|||
|
<uuid>0f0806ab-531d-6134-5def-c5b4955292aa</uuid>
|
|||
|
<memory unit='GiB'>4</memory>
|
|||
|
<currentMemory unit='GiB'>4</currentMemory>
|
|||
|
<vcpu placement='static'>2</vcpu>
|
|||
|
<os>
|
|||
|
<type arch='x86_64' machine='pc-i440fx-trusty'>hvm</type>
|
|||
|
<boot dev='hd'/>
|
|||
|
</os>
|
|||
|
<features>
|
|||
|
<acpi/>
|
|||
|
<apic/>
|
|||
|
<pae/>
|
|||
|
</features>
|
|||
|
<clock offset='utc'/>
|
|||
|
<on_poweroff>destroy</on_poweroff>
|
|||
|
<on_reboot>restart</on_reboot>
|
|||
|
<on_crash>restart</on_crash>
|
|||
|
<devices>
|
|||
|
<emulator>/usr/bin/qemu-system-x86_64</emulator>
|
|||
|
<disk type='file' device='disk'>
|
|||
|
<driver name='qemu' type='qcow2'/>
|
|||
|
<source file='/mnt/vdc/ubuntutest.img'/>
|
|||
|
<target dev='vda' bus='virtio'/>
|
|||
|
</disk>
|
|||
|
<controller type='pci' index='0' model='pci-root'/>
|
|||
|
<interface type='bridge'>
|
|||
|
<mac address='fa:16:3e:6e:89:ce'/>
|
|||
|
<source bridge='br0'/>
|
|||
|
<target dev='tap1'/>
|
|||
|
<model type='virtio'/>
|
|||
|
</interface>
|
|||
|
<serial type='pty'>
|
|||
|
<target port='0'/>
|
|||
|
</serial>
|
|||
|
<console type='pty'>
|
|||
|
<target type='serial' port='0'/>
|
|||
|
</console>
|
|||
|
<graphics type='vnc' port='-1' autoport='yes' listen='0.0.0.0'>
|
|||
|
<listen type='address' address='0.0.0.0'/>
|
|||
|
</graphics>
|
|||
|
<video>
|
|||
|
<model type='cirrus'/>
|
|||
|
</video>
|
|||
|
</devices>
|
|||
|
</domain>
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
在这个XML文件中,/mnt/vdc/ubuntutest.img就是虚拟机的镜像,br0就是我们创建的网桥,连接到网桥上的网卡libvirt会自动帮我们创建。
|
|||
|
|
|||
|
接下来,需要将这个XML保存为domain.xml,然后调用下面的命令,交给libvirt进行管理。
|
|||
|
|
|||
|
```
|
|||
|
virsh define domain.xml
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
接下来,运行virsh list --all,我们就可以看到这个定义好的虚拟机了,然后我们调用virsh start ubuntutest,启动这个虚拟机。
|
|||
|
|
|||
|
```
|
|||
|
# virsh list
|
|||
|
Id Name State
|
|||
|
----------------------------------------------------
|
|||
|
1 ubuntutest running
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
我们可以通过ps查看libvirt启动的qemu进程。这个命令行是不是很眼熟?我们之前花了一章来讲解。如果不记得了,你可以回去看看前面的内容。
|
|||
|
|
|||
|
```
|
|||
|
# ps aux | grep qemu
|
|||
|
libvirt+ 9343 85.1 34.7 10367352 5699400 ? Sl Jul27 1239:18 /usr/bin/qemu-system-x86_64 -name ubuntutest -S -machine pc-i440fx-trusty,accel=tcg,usb=off -m 4096 -realtime mlock=off -smp 2,sockets=2,cores=1,threads=1 -uuid 0f0806ab-531d-6134-5def-c5b4955292aa -no-user-config -nodefaults -chardev socket,id=charmonitor,path=/var/lib/libvirt/qemu/domain-ubuntutest/monitor.sock,server,nowait -mon chardev=charmonitor,id=monitor,mode=control -rtc base=utc -no-shutdown -boot strict=on -device piix3-usb-uhci,id=usb,bus=pci.0,addr=0x1.0x2 -drive file=/mnt/vdc/ubuntutest.img,format=qcow2,if=none,id=drive-virtio-disk0 -device virtio-blk-pci,scsi=off,bus=pci.0,addr=0x4,drive=drive-virtio-disk0,id=virtio-disk0,bootindex=1 -netdev tap,fd=26,id=hostnet0 -device virtio-net-pci,netdev=hostnet0,id=net0,mac=fa:16:3e:6e:89:ce,bus=pci.0,addr=0x3 -chardev pty,id=charserial0 -device isa-serial,chardev=charserial0,id=serial0 -vnc 0.0.0.0:0 -device cirrus-vga,id=video0,bus=pci.0,addr=0x2 -device virtio-balloon-pci,id=balloon0,bus=pci.0,addr=0x5 -msg timestamp=on
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
从这里,我们可以看到,VNC的设置为0.0.0.0:0。我们可以用VNCViewer工具登录到这个虚拟机的界面,但是这样实在是太麻烦了,其实virsh有一个特别好的工具,但是需要在虚拟机里面配置一些东西。
|
|||
|
|
|||
|
在虚拟机里面,我们修改/boot/grub/里面的两个文件,一个是grub.cfg,另一个是menu.lst,这里面就是咱们在[系统初始化](https://time.geekbang.org/column/article/89739)的时候,讲过的那个启动列表。
|
|||
|
|
|||
|
在grub.cfg中,在submenu ‘Advanced options for Ubuntu’ 这一项,在这一行的linux /boot/vmlinuz-4.15.0-55-generic root=UUID=470f3a42-7a97-4b9d-aaa0-26deb3d234f9 ro console=ttyS0 maybe-ubiquity中,加上了console=ttyS0。
|
|||
|
|
|||
|
```
|
|||
|
submenu 'Advanced options for Ubuntu' $menuentry_id_option 'gnulinux-advanced-470f3a42-7a97-4b9d-aaa0-26deb3d234f9' {
|
|||
|
menuentry 'Ubuntu, with Linux 4.15.0-55-generic' --class ubuntu --class gnu-linux --class gnu --class os $menuentry_id_option 'gnulinux-4.15.0-55-generic-advanced-470f3a42-7a97-4b9d-aaa0-26deb3d234f9' {
|
|||
|
recordfail
|
|||
|
load_video
|
|||
|
gfxmode $linux_gfx_mode
|
|||
|
insmod gzio
|
|||
|
if [ x$grub_platform = xxen ]; then insmod xzio; insmod lzopio; fi
|
|||
|
insmod part_gpt
|
|||
|
insmod ext2
|
|||
|
set root='hd0,gpt2'
|
|||
|
if [ x$feature_platform_search_hint = xy ]; then
|
|||
|
search --no-floppy --fs-uuid --set=root --hint-bios=hd0,gpt2 --hint-efi=hd0,gpt2 --hint-baremetal=ahci0,gpt2 470f3a42-7a97-4b9d-aaa0-26deb3d234f9
|
|||
|
else
|
|||
|
search --no-floppy --fs-uuid --set=root 470f3a42-7a97-4b9d-aaa0-26deb3d234f9
|
|||
|
fi
|
|||
|
echo 'Loading Linux 4.15.0-55-generic ...'
|
|||
|
linux /boot/vmlinuz-4.15.0-55-generic root=UUID=470f3a42-7a97-4b9d-aaa0-26deb3d234f9 ro console=ttyS0 maybe-ubiquity
|
|||
|
echo 'Loading initial ramdisk ...'
|
|||
|
initrd /boot/initrd.img-4.15.0-55-generic
|
|||
|
}
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
在menu.lst文件中,在Ubuntu 18.04.2 LTS, kernel 4.15.0-55-generic这一项,在kernel /boot/vmlinuz-4.15.0-55-generic root=/dev/hda1 ro console=hvc0 console=ttyS0这一行加入console=ttyS0。
|
|||
|
|
|||
|
```
|
|||
|
title Ubuntu 18.04.2 LTS, kernel 4.15.0-55-generic
|
|||
|
root (hd0)
|
|||
|
kernel /boot/vmlinuz-4.15.0-55-generic root=/dev/hda1 ro console=hvc0 console=ttyS0
|
|||
|
initrd /boot/initrd.img-4.15.0-55-generic
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
接下来,我们重启虚拟机,重启后上面的配置就起作用了。这时候,我们可以通过下面的命令,进入机器的控制台,可以不依赖于SSH和IP地址进行登录。
|
|||
|
|
|||
|
```
|
|||
|
# virsh console ubuntutest
|
|||
|
Connected to domain ubuntutest
|
|||
|
Escape character is ^]
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
下面,我们可以配置这台机器的IP地址了。对于ubuntu-18.04来讲,IP地址的配置方式为修改/etc/netplan/50-cloud-init.yaml文件。
|
|||
|
|
|||
|
```
|
|||
|
network:
|
|||
|
ethernets:
|
|||
|
ens3:
|
|||
|
addresses: [192.168.57.100/24]
|
|||
|
gateway4: 192.168.57.1
|
|||
|
dhcp4: no
|
|||
|
nameservers:
|
|||
|
addresses: [8.8.8.8,114.114.114.114]
|
|||
|
optional: true
|
|||
|
version: 2
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
然后,我们可以通过netplan apply,让配置生效,这样,虚拟机里面的IP地址就配置好了。现在,我们应该能ping得通公网的一个网站了。
|
|||
|
|
|||
|
虚拟机就此创建好了,接下来我们需要下载源代码重新编译。
|
|||
|
|
|||
|
## 下载源代码
|
|||
|
|
|||
|
首先,我们先下载源代码。
|
|||
|
|
|||
|
```
|
|||
|
apt-get install linux-source-4.15.0
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
这行命令会将代码下载到/usr/src/目录下,我们可以通过下面的命令解压缩。
|
|||
|
|
|||
|
```
|
|||
|
tar vjxkf linux-source-4.15.0.tar.bz2
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
至此,路径/usr/src/linux-source-4.15.0下,就是解压好的内核代码。
|
|||
|
|
|||
|
准备工作都做好了。这一节,我们先来做第一个实验,也就是,在原有内核代码的基础上加一个我们自己的系统调用。
|
|||
|
|
|||
|
在哪里加代码呢?如果你忘了,请出门左转,回顾一下[系统调用](https://time.geekbang.org/column/article/90394)那一节。
|
|||
|
|
|||
|
第一个要加的地方是arch/x86/entry/syscalls/syscall\_64.tbl。这里面登记了所有的系统调用号以及相应的处理函数。
|
|||
|
|
|||
|
```
|
|||
|
332 common statx sys_statx
|
|||
|
333 64 sayhelloworld sys_sayhelloworld
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
在这里,我们找到332号系统调用sys\_statx,然后照猫画虎,添加一个sys\_sayhelloworld,这里我们只添加64位操作系统的。
|
|||
|
|
|||
|
第二个要加的地方是include/linux/syscalls.h,也就是系统调用的头文件,然后添加一个系统调用的声明。
|
|||
|
|
|||
|
```
|
|||
|
asmlinkage long sys_statx(int dfd, const char __user *path, unsigned flags,
|
|||
|
unsigned mask, struct statx __user *buffer);
|
|||
|
|
|||
|
asmlinkage int sys_sayhelloworld(char * words, int count);
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
同样,我们找到sys\_statx的声明,照猫画虎,声明一个sys\_sayhelloworld。其中,words参数是用户态传递给内核态的文本的指针,count是数目。
|
|||
|
|
|||
|
第三个就是对于这个系统调用的实现,方便起见,我们不再用SYSCALL\_DEFINEx系列的宏来定义了,直接在kernel/sys.c中实现。
|
|||
|
|
|||
|
```
|
|||
|
asmlinkage int sys_sayhelloworld(char * words, int count){
|
|||
|
int ret;
|
|||
|
char buffer[512];
|
|||
|
if(count >= 512){
|
|||
|
return -1;
|
|||
|
}
|
|||
|
copy_from_user(buffer, words, count);
|
|||
|
ret=printk("User Mode says %s to the Kernel Mode!", buffer);
|
|||
|
return ret;
|
|||
|
}
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
接下来就要开始编译内核了。
|
|||
|
|
|||
|
## 编译内核
|
|||
|
|
|||
|
编译之前,我们需要安装一些编译要依赖的包。
|
|||
|
|
|||
|
```
|
|||
|
apt-get install libncurses5-dev libssl-dev bison flex libelf-dev gcc make openssl libc6-dev
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
首先,我们要定义编译选项。
|
|||
|
|
|||
|
```
|
|||
|
make menuconfig
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
然后,我们能通过选中下面的选项,激活CONFIG\_DEBUG\_INFO和CONFIG\_FRAME\_POINTER选项。
|
|||
|
|
|||
|
```
|
|||
|
Kernel hacking --->
|
|||
|
Compile-time checks and compiler options --->
|
|||
|
[*] Compile the kernel with debug info
|
|||
|
[*] Compile the kernel with frame pointers
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
选择完毕之后,配置会保存在.config文件中。如果我们打开看,能看到这样的配置:
|
|||
|
|
|||
|
```
|
|||
|
CONFIG_FRAME_POINTER=y
|
|||
|
CONFIG_DEBUG_INFO=y
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
接下来,我们编译内核。
|
|||
|
|
|||
|
```
|
|||
|
nohup make -j8 > make1.log 2>&1 &
|
|||
|
nohup make modules_install > make2.log 2>&1 &
|
|||
|
nohup make install > make3.log 2>&1 &
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
这是一个非常长的过程,请耐心等待,可能需要数个小时,因而这里用了nohup,你可以去干别的事情。
|
|||
|
|
|||
|
当编译完毕之后,grub和menu.lst都会发生改变。例如,grub.conf里面会多一个新内核的项。
|
|||
|
|
|||
|
```
|
|||
|
submenu 'Advanced options for Ubuntu' $menuentry_id_option 'gnulinux-advanced-470f3a42-7a97-4b9d-aaa0-26deb3d234f9' {
|
|||
|
menuentry 'Ubuntu, with Linux 4.15.18' --class ubuntu --class gnu-linux --class gnu --class os $menuentry_id_option 'gnulinux-4.15.18-advanced-470f3a42-7a97-4b9d-aaa0-26deb3d234f9' {
|
|||
|
recordfail
|
|||
|
load_video
|
|||
|
gfxmode $linux_gfx_mode
|
|||
|
insmod gzio
|
|||
|
if [ x$grub_platform = xxen ]; then insmod xzio; insmod lzopio; fi
|
|||
|
insmod part_gpt
|
|||
|
insmod ext2
|
|||
|
if [ x$feature_platform_search_hint = xy ]; then
|
|||
|
search --no-floppy --fs-uuid --set=root 470f3a42-7a97-4b9d-aaa0-26deb3d234f9
|
|||
|
else
|
|||
|
search --no-floppy --fs-uuid --set=root 470f3a42-7a97-4b9d-aaa0-26deb3d234f9
|
|||
|
fi
|
|||
|
echo 'Loading Linux 4.15.18 ...'
|
|||
|
linux /boot/vmlinuz-4.15.18 root=UUID=470f3a42-7a97-4b9d-aaa0-26deb3d234f9 ro console=ttyS0 maybe-ubiquity
|
|||
|
echo 'Loading initial ramdisk ...'
|
|||
|
initrd /boot/initrd.img-4.15.18
|
|||
|
}
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
例如,menu.lst也多了新的内核的项。
|
|||
|
|
|||
|
```
|
|||
|
title Ubuntu 18.04.2 LTS, kernel 4.15.18
|
|||
|
root (hd0)
|
|||
|
kernel /boot/vmlinuz-4.15.18 root=/dev/hda1 ro console=hvc0 console=ttyS0
|
|||
|
initrd /boot/initrd.img-4.15.18
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
别忘了,这里面都要加上console=ttyS0。
|
|||
|
|
|||
|
下面,我们要做的就是重启虚拟机。进入的时候,会出现GRUB界面。我们选择Ubuntu高级选项,然后选择第一项进去,通过uname命令,我们就进入了新的内核。
|
|||
|
|
|||
|
```
|
|||
|
# uname -a
|
|||
|
Linux popsuper 4.15.18 #1 SMP Sat Jul 27 13:43:42 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
进入新的系统后,我们写一个测试程序。
|
|||
|
|
|||
|
```
|
|||
|
#include <stdio.h>
|
|||
|
#include <stdlib.h>
|
|||
|
#include <unistd.h>
|
|||
|
#include <linux/kernel.h>
|
|||
|
#include <sys/syscall.h>
|
|||
|
#include <string.h>
|
|||
|
|
|||
|
int main ()
|
|||
|
{
|
|||
|
char * words = "I am liuchao from user mode.";
|
|||
|
int ret;
|
|||
|
ret = syscall(333, words, strlen(words)+1);
|
|||
|
printf("return %d from kernel mode.\n", ret);
|
|||
|
return 0;
|
|||
|
}
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
然后,我们能利用gcc编译器编译后运行。如果我们查看日志/var/log/syslog,就能够看到里面打印出来下面的日志,这说明我们的系统调用已经添加成功了。
|
|||
|
|
|||
|
```
|
|||
|
Aug 1 06:33:12 popsuper kernel: [ 2048.873393] User Mode says I am liuchao from user mode. to the Kernel Mode!
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
## 总结时刻
|
|||
|
|
|||
|
这一节是一节实战课,我们创建了一台虚拟机,在里面下载源代码,尝试修改了Linux内核,添加了一个自己的系统调用,并且进行了编译并安装了新内核。如果你按照这个过程做下来,你会惊喜地发现,原来令我们敬畏的内核,也是能够加以干预,为我而用的呢。没错,这就是你开始逐渐掌握内核的重要一步。
|
|||
|
|
|||
|
## 课堂练习
|
|||
|
|
|||
|
这一节的课堂练习,希望你能够按照整个过程,一步一步操作下来。毕竟看懂不算懂,做出来才算入门啊。
|
|||
|
|
|||
|
欢迎留言和我分享你的疑惑和见解,也欢迎你收藏本节内容,反复研读。你也可以把今天的内容分享给你的朋友,和他一起学习、进步。
|
|||
|
|