<audio id="audio" title="60 | 搭建操作系统实验环境（上）：授人以鱼不如授人以渔" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/33/51/333d3d5a45fa87ff0761fd8ad0d28151.mp3"></audio>

操作系统的理论部分我们就讲完了，但是计算机这门学科是实验性的。为了更加深入地了解操作系统的本质，我们必须能够做一些上手实验。操作系统的实验，相比其他计算机课程的实验要更加复杂一些。

我们做任何实验，都需要一个实验环境。这个实验环境要搭建在操作系统之上，但是，我们这个课程本身就是操作系统实验，难不成要自己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文件，这样容易维护。

```
&lt;domain type='qemu'&gt;
  &lt;name&gt;ubuntutest&lt;/name&gt;
  &lt;uuid&gt;0f0806ab-531d-6134-5def-c5b4955292aa&lt;/uuid&gt;
  &lt;memory unit='GiB'&gt;4&lt;/memory&gt;
  &lt;currentMemory unit='GiB'&gt;4&lt;/currentMemory&gt;
  &lt;vcpu placement='static'&gt;2&lt;/vcpu&gt;
  &lt;os&gt;
    &lt;type arch='x86_64' machine='pc-i440fx-trusty'&gt;hvm&lt;/type&gt;
    &lt;boot dev='hd'/&gt;
  &lt;/os&gt;
  &lt;features&gt;
    &lt;acpi/&gt;
    &lt;apic/&gt;
    &lt;pae/&gt;
  &lt;/features&gt;
  &lt;clock offset='utc'/&gt;
  &lt;on_poweroff&gt;destroy&lt;/on_poweroff&gt;
  &lt;on_reboot&gt;restart&lt;/on_reboot&gt;
  &lt;on_crash&gt;restart&lt;/on_crash&gt;
 &lt;devices&gt;
    &lt;emulator&gt;/usr/bin/qemu-system-x86_64&lt;/emulator&gt;
    &lt;disk type='file' device='disk'&gt;
      &lt;driver name='qemu' type='qcow2'/&gt;
      &lt;source file='/mnt/vdc/ubuntutest.img'/&gt;
      &lt;target dev='vda' bus='virtio'/&gt;
    &lt;/disk&gt;
    &lt;controller type='pci' index='0' model='pci-root'/&gt;
    &lt;interface type='bridge'&gt;
      &lt;mac address='fa:16:3e:6e:89:ce'/&gt;
      &lt;source bridge='br0'/&gt;
      &lt;target dev='tap1'/&gt;
      &lt;model type='virtio'/&gt;
    &lt;/interface&gt;
    &lt;serial type='pty'&gt;
      &lt;target port='0'/&gt;
    &lt;/serial&gt;
    &lt;console type='pty'&gt;
      &lt;target type='serial' port='0'/&gt;
    &lt;/console&gt;
    &lt;graphics type='vnc' port='-1' autoport='yes' listen='0.0.0.0'&gt;
      &lt;listen type='address' address='0.0.0.0'/&gt;
    &lt;/graphics&gt;
    &lt;video&gt;
      &lt;model type='cirrus'/&gt;
    &lt;/video&gt;
  &lt;/devices&gt;
&lt;/domain&gt;

```

在这个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 &gt;= 512){
		return -1;
	}
	copy_from_user(buffer, words, count);
	ret=printk(&quot;User Mode says %s to the Kernel Mode!&quot;, 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  ---&gt;
Compile-time checks and compiler options  ---&gt;
[*] Compile the kernel with debug info 
[*] Compile the kernel with frame pointers

```

选择完毕之后，配置会保存在.config文件中。如果我们打开看，能看到这样的配置：

```
CONFIG_FRAME_POINTER=y
CONFIG_DEBUG_INFO=y

```

接下来，我们编译内核。

```
nohup make -j8 &gt; make1.log 2&gt;&amp;1 &amp;
nohup make modules_install &gt; make2.log 2&gt;&amp;1 &amp;
nohup make install &gt; make3.log 2&gt;&amp;1 &amp;

```

这是一个非常长的过程，请耐心等待，可能需要数个小时，因而这里用了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 &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
#include &lt;unistd.h&gt;
#include &lt;linux/kernel.h&gt;
#include &lt;sys/syscall.h&gt;
#include &lt;string.h&gt;

int main ()
{
  char * words = &quot;I am liuchao from user mode.&quot;;
  int ret;
  ret = syscall(333, words, strlen(words)+1);
  printf(&quot;return %d from kernel mode.\n&quot;, 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内核，添加了一个自己的系统调用，并且进行了编译并安装了新内核。如果你按照这个过程做下来，你会惊喜地发现，原来令我们敬畏的内核，也是能够加以干预，为我而用的呢。没错，这就是你开始逐渐掌握内核的重要一步。

## 课堂练习

这一节的课堂练习，希望你能够按照整个过程，一步一步操作下来。毕竟看懂不算懂，做出来才算入门啊。

欢迎留言和我分享你的疑惑和见解，也欢迎你收藏本节内容，反复研读。你也可以把今天的内容分享给你的朋友，和他一起学习、进步。


