仓库源文

.. Kenneth Lee 版权所有 2024

:Authors: Kenneth Lee :Version: 0.1 :Date: 2024-04-10 :Status: Draft

操作系统实验


介绍

本文给一位初学操作系统的学生设计几个简单的操作系统实验,以便她可以对操作系统是 怎么设计的有一个感性的认识。

实验1:理解调度器

这个实验的背景我写在别的地方了,参考这里: https://cpp-aux-tutorial.readthedocs.io/zh-cn/latest/18.html

实验的目的主要就是看一次文中提到的调度程序,知道调度器的程序是怎么写的,然后简 单修改一下几个调度任务,看看调度的效果是什么样的。

进一步的实验是修改调度程序,分别实现:

  1. 以优先级为先后进行调度。
  2. 以调度时间公平进行调度。

实验2:安装Debian Linux

这个实验的目的是理解一个完整操作系统的文件系统中到底具体包含一些什么东西,我们 用比较原始的方法来完成这个安装。

实验步骤:

  1. 在Windows上安装virtualbox。VirtualBox是一个开源的,可以在Windows上模拟一台 物理计算机的软件。主页在www.virtualbox.org,上去下载安装即可。

  2. 下载Ubuntu Linux安装映像。这是一个可以制作启动光盘或者U盘的文件,可以通过一 些工具制作启动盘(比如Windows下可以用: uubyte <https://www.uubyte.com/download/uubyte-bootable-usb-creator.exe>_ ,Linux下更简单,直接用cp debian.img /dev/sdX把映像拷贝到设备上就可以了)。 但我们这里不需要,因为我们的机器就是软件模拟出来的,我们可以用virtualbox直 接模拟这个光盘。

    请注意,这个实验是用Ubuntu Linux的Live CD来安装Debian Linux,如果我们直接用 Debian的安装盘来安装Debian Linux,这个很简单,就像我们平时安装Windows一样, 顺着菜单一步步按确定就行了,我们这次是手工安装Debian,知道菜单背后是在干什 么。所以我们的行为是不一样的。这里用Ubuntu是因为Ubuntu有Live CD,实际上 Debian也有Live CD,所以你也可以选在使用Debian的Live CD,只要能用光盘直接启 动一个Linux都可以。

  3. 在VirtualBox中创建一台虚拟机器,用Ubuntu Linux Live CD启动。

  4. 启动以后退出安装程序,从菜单中选择“控制台”程序,从命令行开始,手工安装 Debian Linux。

手工安装Debian的过程

我们现在解释一下计算机的启动过程,它一般是这样的步骤:

  1. 机器加电,计算机启动,计算机内置了一个很小的软件,传统上叫BIOS,Basic Input/Output System,现在用得比较多的叫UEFI,Unified Extensible Firmware Interface。它是个比较通用的标准,标准叫EFI,但这个标准组织做了一个实现,让 大家(做计算机的),都可以直接拿着代码来用,所以叫UEFI。你也可以自己做一个 叫XEFI,YEFI,ZEFI。所以这些符合这个标准的实现,都叫EFI。

  2. BIOS或者UEFI会按你设置好的参数,去找光盘,硬件或者可以保存程序的程序来启动。 这个在光盘或者硬盘上的程序,一般会称为Boot Loader。严格来说,你可以直接启动 你的操作系统内核,但操作系统内部通常很大,所以会先启动个小的,比如Windows会 提供一个叫WBM,Windows Boot Manager。Linux是个开源软件,有很多Boot Loader可 以用,但现在用得最多的,是Grub。

  3. Grub启动以后,就可以开始去找操作系统内核了,然后它会启动操作系统内核。

  4. 操作系统内核会找它的根文件系统,然后根据根文件系统上的文件,启动指定的Shell。 这样你就有了一个可以控制操作系统的界面了,之后你再使用,就是在使用你的操作 系统了。

所以我们这个安装主要在做这些事情:

  1. 我们已经用Ubuntu的光盘启动了一个以光盘为根的操作系统了,我们现在需要在我们 的磁盘上安装操作系统的文件系统。

  2. 然后我们需要配置一下这个文件系统,让它符合我们的要求。

  3. 然后我们要在文件系统中放上我们的操作系统内核。

  4. 最后我们要装上我们的Bootloader,Grub。

这样,我们就装好了,把光盘退出来,然后我们重新启动机器,这样我们就不再用原来的 光盘(或者U盘)来启动我们的机器了,我们开始用我们刚才安装的Grub启动,我们磁盘 上的内核,然后用这个内核启动我们安装的Debian。

下面我们看具体怎么操作:

首先我们用光盘启动机器,在桌面上点右键,打开“Open In Terminal”,得到一个命令行 界面。Ubuntu默认的用户是ubuntu,这个是普通用户,很多权限没有的,我们切换到root: ::

sudo su

然后我们尝试找到我们的磁盘是哪一个:::

lsblk

这个命令显示所有的磁盘,找到类型是disk,大小和你在虚拟机中创建的磁盘的大小一样 的,那个就是你的磁盘,比如可能是/dev/sda。

.. note:: /dev/sda是串行硬件,很多虚拟机会模拟这种硬盘,但也有一些可以模拟虚拟 硬盘,这种情况下通常叫/dev/vda, /dev/vdb, /dev/vdc这样的。

磁盘就好像一个数组,可以顺序放数据。按惯例,我们需要分成多个“区”(Partition) 来用。分区的信息也放在磁盘上。分区有很多种格式,传统的格式叫DOS,以前的BIOS都 用这个格式,现在UEFI常用的格式是EFI格式。我本来想建议用EFI分区的,但我自己验证 用这种分区部分虚拟机支持有问题,所以,我们这次用DOS格式来分区:::

fdisk /dev/sda

进去以后可以运行很多单字符的命令,m是帮助,可以先看看帮助。用o表示用DOS格式分 区,用g表示用EFI方式分区。我们可以用o。如果你已经用g,再向用o它会拒绝,怕你写 错命令把分好的分区内存覆盖了。这种情况你要用这个命令进fdisk:::

fdisk --wipe=always /dev/sda

这样你再用o命令,它还是会报错(不是Error,而是Warning),但这时是可以成功的。 然后你就可以创建分区了。我们简单些,这次就创建一个分区:用n(new)创建,选在是 第一个分区,用它建议的开始和结束位置,这样就会用掉整个磁盘作为分区。

如果你用EFI,EFI的bootload是要求你必须有一个EFI分区的,这样你至少要两个分区。 下面是一个DOS分区和EFI分区的磁盘上的数据布局的对比:

.. figure:: _static/fdisk后磁盘格式.svg

可以用下面命令直接看看你的磁盘的分区分布:::

fdisk -l /dev/sda

磁盘前面有个分区表,读磁盘的程序就能知道磁盘的格式,从而找到每个分区。但分区只 是把整个分区都给了某个程序,并不说明里面的文件怎么保存,我们还需要对分区进行格 式化。Linux的格式化程序叫mkfs(Make FileSystem),格式EFI(这个格式模式是vfat, 微软的一种简单格式)的格式化程序叫mkfs.vfat。Linux操作系统可以支持很多种文件系 统,比如ext2, ext3, ext4, xfs, zfs, btrfs……但用得最多的还是ext4,我们就用这个 格式,它的格式化程序叫mkfs.ext4。我们这样格式化每个分区:::

mkfs.ext4 /dev/sda1

这里我提醒一下:整个磁盘叫/dev/sda,它的第一个分区叫/dev/sda1,第二个分区叫 /dev/sda2。

.. note:: 如果创建了swap分区,可以用mkfs.swap来格式化。

好了,现在我们要访问分区里面的内容了,我们需要把这个分区加入到我们的文件树。 这称为mount。我们现在不是用光盘启动的吗?我们的目录树是这样的:::

/(光盘)--bin +-----sbin +-----home | +---ubuntu +-----dev | +---sda | +---sda1 | +---sda2 | +---... +-----mnt +-----...

按着这棵树,我们怎么都访问不了我们的磁盘,这里看到的sda,sda1,都只是单个的, 表示那个磁盘和分区的“文件”,不是磁盘或者分区里面的内容。要把它们变成内容,就需 要mount到这个目录树中:::

mount /dev/sda2 /mnt

mount可以把一个设备挂到你想要的地方,我这里选择了/mnt(这个习惯上用作临时 mount)。现在我们的目录树是这样的:::

/(光盘)--bin +-----sbin +-----home | +---ubuntu +-----dev +-----mnt(sda1) +-----...

好了现在我们要修改的磁盘已经在我们的目录树中了,我们可以拷贝文件进去了:::

apt install debootstrap debootstrap stable /mnt https://mirrors.163.com/debian

Ubuntu的Live CD里面默认没有debootstrap这个软件,我们用apt安装一下。这个软件的 作用就是从网络上把debian的基本文件拷贝到你指定的目录中,我们上面的命令就是把 Debian的stable版本(Debian默认有三个版本:unstable,testing,stable,一个比一 个稳定,前面不稳定的软件比较新,我们只是做实验,所以装一个稳定版本。)拷贝到 /mnt目录下,最后一个参数是从那个网站下载软件,可以不写,不写会用默认的服务器, 也是可以的,但如果用国内的镜像,会更快而已。

.. note::

debootstrap就是个sh脚本,如果你关心它怎么拷贝的,打开看看就知道了。

这个步骤要花点时间,取决于你的网络有多快。

这样,我们就装完最基本的系统了,我们要配置我们的系统。做这种配置,常常我们需要 运行命令,但这些命令都是改当前的root为基础的目录的,所以我们最好改成用我们的 /mnt作为root,但我们的root不是当前的内核认可的root,所以有些动态生成的文件不在 里面,我们在我们的root中也加上这些动态目录:::

  mount -t proc none /mnt/proc             # OS运行信息文件
  mount --rbind /dev /mnt/dev              # 设备文件
  mount --rbind /sys /mnt/sys              # 另一部分OS运行信息文件
  cp /etc/resolv.conf /mnt/etc/resolv.conf # DNS
  chroot /mnt /bin/bash                    # 用目标系统作root

前面三个mount都是在我们的/mnt目录中加上内核自动生成的文件系统,最后一个是借用 一下我们的Live CD中自动找到的DNS,拷贝到我们的操作系统中。

然后最后一个chroot命令用/mnt作为root运行一个shell:/bin/bash。

现在在这儿shell中,我们的root改成了原来的/mnt了。现在我们再用apt安装软件,就不 是装到光盘上了——光盘其实没法写内容,这里其实是用了一些内存临时放那些改动。但无 论如何,我们原来是用光盘启动的,所以如果你运行apt,它是安装到光盘那个系统中的。 现在我们chroot到了/mnt,以后再运行apt,就是我们/mnt中的apt命令,也安装到我们 /mnt这个root中了。

为了使用apt,我们需要配置一个apt使用的下载网站,修改这个文件: /etc/apt/source.list,加上这一句:::

deb https://mirrors.163.com/debian stable main

如果你前面运行debootstrap的时候已经指定这个镜像了,这就已经设置好了,什么都不 用改了。

然后就是标准的apt命令了:::

apt update # 更新软件列表 apt install linux-image-amd64 # 安装操作系统内核 apt install grub # 安装bootloader apt install vim sudo # 安装最基本的软件 apt install network-manager # 这个是网卡的配置工具,最好装上避免用自己的系统启动后没有网卡

核心就是这个内核和grub,我们前面解释过了,一个是内核,一个是bootloader。

最后安装vim和sudo,是因为我习惯用这两个工具,如果你需要其他工具,也可以安装更 多的。

grub不是拷贝了就能用的,它需要修改分区表,而且我们需要告诉它内核在哪里,所以我 们还需要做这个动作:::

grub-install /dev/sda # 把bootloader安装到/dev/sda的分区表中 update-grub # 更新grub的配置让它启动我们刚安装的内核

这样这个系统就可以启动了,但仅仅是启动内核,我们还需要做更多的配置让内核知道谁 是我们系统的root:::

cp /proc/mounts /etc/fstab

/proc/mounts是操作系统内部中说明的所有mount,我们拷贝完可以编辑一下/etc/fstab, 留下sda1和sda2两个mount就够了,其他都是内核自动挂载的。下面是一个里面内容的示 例:::

分区 mount到哪里 格式 mount参数 出错的时候是否备份文件系统 启动是否检查文件系统

/dev/sda1 / ext4 defaults 0 1 /dev/sda2 none swap sw 0 0

如果你只有一个分区,那第一项就可以了,这里的第二项是交换分区,用于用一部分磁盘 用来交换部分内存来用。

然后我们设置操作系统使用的时区:::

cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime # 设置时区

/usr/share/zoneinfo目录下分地区有很多而时区配置文件(二进制的,没法直接看), 拷贝你的地区到/etc/localtime上,就可以了。

然后我们创建一个用户:::

useradd kenny # 创建用户 passwd kenny # 可以修改密码,如果前面你没有指定密码的话。 passwd root # 设置root的密码。这个尽量设置一下,应急,宁愿以后再删除

下面这一步如果你只是做实验,可以不做:::

apt install locales # 多语言支持 dpkg-reconfigure locales # 设置默认语言 vi /etc/locale.gen # 设置支持的语言(默认之外的) locale-gen # 生成所支持的语言数据

apt install gnome # 安装图形界面 exit # 退出chroot的shell

这样就装好了,重启虚拟机器就可以用了。

重启前主要你现在的机器配置是不是用磁盘启动,启动以后看看普通用户和密码能否登录。 用su加root的密码看看能否切换到根用户。如果用的是图形登录界面,直接用root登录, 然后看看你安装的系统的各个目录都有些什么内容。

这个实验重点要关注的要点

  1. 在磁盘上安装一个操作系统和简单拷贝一些文件进去有什么区别(注意bootloader的 安装)

  2. 操作系统运行除了需要文件还需要什么(proc, sys, dev等文件系统)

  3. 操作系统的核心配置包括些什么东西(locale,timezone,ip/DNS, root)

实验3:编译和运行Linux Kernel

这个实验我们的主要目的是理解Kernel和文件系统(的内容)的关系。大部分操作系统都 是分成Kernel和Shell两个部分,这好像一个核桃:

.. figure:: _static/kernel-shell.svg

里面是核(Kernel),外面是一层外壳(Shell),我们只能使用Shell的功能,接触不到 Kernel,Kernel掌握着所有的硬件资源,比如内存,硬盘,网卡,鼠标等等,这些东西 Shell都是不能直接用的,必须通过向内核请求,才能使用,这种请求,叫做系统调用。

这是简单的理解。但其实Shell通常只是指我们能接触到的部分(用户界面),还有一些 我们接触不到的,比如我们运行一个排序算法程序,这个程序没有输入输出,把数据读出 来,排完就送回去了,这种程序和Shell一样,都不能直接使用内核的资源(需要经过系 统调用。Shell仅仅是管理这些程序而已。我们把所有这些在Kernel之外的程序,都叫“用 户程序”。这样,前面这个核桃应该画成这样:

.. figure:: _static/kernel-user.svg

Shell只是一个(或者多个)和人交互的用户程序,我们还有更多的用户程序,如果我们 运行一个叫/home/user/test的排序算法,Shell只是告诉内核要从“文件系统”找到这个叫 /home/user/test的文件,然后把它送到内存中执行,之后这个test程序就直接和kernel 通话了,shell只是一个代理,帮助用户启动更多的程序,这些程序运气起来后,就直接 和内核打交道,访问硬件也好,申请内存也罢,都是它和Kernel的事情了。

Kernel也是一个程序,只是它的权限比较高。这里我们谈到“权限”,和我们平时说的root 权限,kenny用户权限,ubuntu用户权限是不同的。在操作系统的角度,就两个权限: kernel和user权限。但kernel为了管理不同的使用者,它给使用者分配了另一个权限(我 们这里称为登录权限:你的机器启动的时候,先启动bootload,然后启动内核,这时都是 kernel权限,然后kernel会启动一个“登录程序”,让你用某个“登录用户”登录,登录到一 个Shell上,这个Shell的权限就是一种“登录权限”,比如你用root登录,这时你的登录权 限就是root,如果你用kenny登录,你的登录权限就是kenny。之后你在这个shell里面发 起一个系统调用,找kernel做点什么,kernel根据你的登录权限决定让不让你干。所以登 录权限只是kernel为了管理方便而做的权限,而不是我们这里说的操作系统的Kernel和 user的权限。

我们原来kernel这个程序是放在文件系统里面的,这样机器启动的时候其实没法知道它在 哪里,所以我们在磁盘的分区表中先装了一个bootload(grub),机器启动的时候固定从 磁盘启动,找到分区表,找到grub,grub找到内核的位置,然后先启动内核,grub还会配 置内核的参数,这个参数告诉内核文件系统在哪里,需要启动那个shell,这样操作系统 就能执行自己的代码后,启动第一个用户进程,然后一步步启动到shell了。

我们的实验做这些工作:

  1. 下载Linux Kernel的源代码,编译出内核这个“程序”。
  2. 用虚拟机启动这个内核
  3. 用这个内核运行启动我们前面安装的Debian的文件系统。

实验步骤

编译Linux Kernel最好在Linux系统上做,因为大部分时候它是在这种平台上调试的,我 们这里使用WSL(Ubuntu或者Debian)来完成这个编译。我们首先需要安装编译工具(比 如gcc):::

apt-get install build-essential bc flex bison git libncurses-dev apt-get install qemu-system

这里你看不到gcc,但其实gcc依赖biuld-essential所以装这个就会连gcc一起装了,还会 包含make这些基本的工具。bc是个计算器,Linux编译用到这个命令了。flex和bison是一 个编译器辅助工具,Linux Kerenl编译的时候用这个工具自动处理一些代码了,所以也需 要装一下,libncurses-dev是一个Linux Kernel配置工具要用的库。

qemu-system是我们一会儿运行内核要用的虚拟机。

然后我们下载编译Linux内核的源代码:::

git clone git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git cd linux make oldconfig make menuconfig make -j

现在我们有了一个Linux的内核了,它的位置在这里:arch/x86_64/boot/bzImage。

上面的make menuconfig会提供一个菜单(这就是前面安装libncurse-dev的作用了,它可 以用来画这个菜单),允许你修改用内核的什么功能,不要什么功能,每个配置可以是:

由于我们一开始没有文件系统,所以所有我们需要的功能,都要用Y的方式编译到内核中, 我这里建议开这些配置:

我们直接就可以用qemu-system(以下简称qemu,它表示Quick Emulator)运行它了:::

qemu-system-x86_64 -kernel arch/x86_64/boot/bzImage

内核启动到最后会告诉你没有root文件系统,然后它就停了。你也没有shell可以输入任 何东西了。

现在我们把我们在实验2中用virtualbox安装的文件系统拿过来当作root用。virtualbox 用来模拟磁盘的文件格式是vdi,如果你装了一个debian.vdi,我们现在把它转化为裸磁 盘的格式:::

vboxmanage clonemedium debian.vdi debian.img --format raw

qemu的虚拟磁盘一般用qcow2格式,但它也可以直接认识裸磁盘(就是和真的磁盘一样放, 没有额外的文件头说明自己的格式)的格式。有了这个磁盘,我们其实可以直接用qemu去 启动它的:::

qemu-system-x86_64 -hda debian.img

我们之前其实完全可以直接用qemu来安装我们的debian的,只是qemu只能在WSL上运行, WSL自己已经是个模拟器了,再用它运行另一个模拟器,效率比较低,问题也多。所以我 们先简单点,用virtualbox。

但virtualbox不能直接运行内核,要做我们现在这个实验,就只能用qemu了。

现在我们加足够多的参数来控制qemu来运行我们的内核和root文件系统:::

qemu-system-x86_64 -drive file=debian.img,format=raw,index=0,if=virtio \ -snapshot \ -machine ubuntu-q35 \ -smp 2 \ -m 1024m \ -kernel arch/x86_64/boot/bzImage \ -append "rw root=/dev/sda1" \ -nic user,hostfwd=tcp::2422-:22

解释一下参数的函数的含义:

这样就可以看到用这个内核运行,并且mount你的磁盘,用它作为root来工作了。

Kernel也是一个普通的程序,有了虚拟机,我们其实可以直接调试它,为此,我们把上述 参数简单调整一下:::

qemu-system-x86_64 -drive file=debian.img,format=raw,index=0,if=virtio \ -S -s \ -snapshot \ -machine ubuntu-q35 \ -smp 2 \ -m 1024m \ -kernel arch/x86_64/boot/bzImage \ -append "nokaslr rw root=/dev/sda1" \ -nic user,hostfwd=tcp::2422-:22

qemu增加了两个参数:-S -s,前者表示启动以后不要运行,等待调试器连入;后者表示 启动内置的调试器。

内核增加了一个nokaslr参数,这是内核不要做反跟踪,如果没有这个参数,内核会故意 改变自己在内存中的位置,让调试器无法找到正确的信息。

然后我们到kernel编译的目录中找到vmlinux这个文件,然后用gdb连接它:::

bash> gdb vmlinux # 调试vmlinux程序(这是内核运行程序的符号文件) gdb> target remote:1234 # 连接远端的gdb gdb> break start_kernel # 在start_kernel函数上设置一个断点 gdb> cont # 让内核继续运行 gdb> next # 现在开始可以单步跟踪内核的运行了

这样我们可以单步看看内核启动会经过多少个步骤。

这个实验重点要关注的要点

这个实验在前面知道一个操作系统的磁盘的形态后,理解内核和这个磁盘的关系是什么。 我们重点学会:

  1. Linux内核如何编译和安装。
  2. 理解内核的文件系统是如何结合在一起的。
  3. 从qemu的参数理解虚拟机是怎么模拟一个硬件的
  4. 看一次Kernel启动过程的打印,理解操作系统启动主要会做些什么事情。
  5. 理解内核是怎么调试的。