前言
操作系统是一种复杂的系统软件。本书通过介绍操作系统的基本概念和原理,并结合操作
系统原理来分析一个小型但全面的操作系统 xv6,并进一步进行各种基于 xv6 操作系统的实验,
来让读者了解和掌握操作系统的设计与实现。xv6 是一个运行在基于 x86 架构的计算机系统上
的类似 UNIX 的教学用操作系统。xv6 起源于 MIT。在 2002 年秋季,Frans Kaashoek, Josh
Cates, and Emil Sit 在 MIT 开设了一门新的实验型课程“操作系统工程”,英文名称是“Operating
Systems Engineering”,课程代号是“6.097”,后改为“6.828”,在此课程上,一开始采用了“莱昂
氏 UNIX 源代码分析”(英文书名是“Lion'Cornmentary on UNIX 6th Edition With Source Code”)
作为参考资料。此参考资料描述的 UNIX v6(简称 V6)是运行在古老的 PDP-11 计算机系统上。
为了让学生更好地理解 V6 的实现,Frans Kaashoek 等从 2006 年夏季开始,参考 V6 的架构,
在 x86 计算机系统上重新实现了一个支持多处理器计算机系统的类似 UNIX 的教学用操作系统,
称为为 xv6。在目前的 MIT 本科生课程“6.828: Operating Systems Engineering”中,xv6 主要
用于讲课,而另一个基于 exokernel 架构的 JOS 主要用于做试验。 目前 xv6 在 MIT 的网址在
http://pdos.csail.mit.edu/6.828/xv6/
第零章 安装使用
如果是 Linux 初学者,请看附录 F,了解如何安装、使用 Ubuntu Linux,如何在 Ubuntu Linux
下编程。
编译[need update]
安装 Ubuntu Linux 8.10,具体安装方法可以参考附录 C。并通过 apt 工具进一步安装相关软件
包
$ sudo apt-get install gcc binutils libc 6-dev
gdb
然后解压 xv6 软件包,到某一目录,然后到此目录下执行
$make
就可以生成相关执行文件和镜像,包括 xv6.img(包含 bootloader 和 xv6 kernel)和 fs.img(包含
应用程序)
运行[need update]
安装 Ubuntu Linux 8.10,并通过 apt 工具进一步安装相关软件包
$sudo apt-get install qemu bochsbios vgabios libsdl1.2debian kvm
如果通过 qemu 执行,可执行如下命令
qemu -smp 4 -parallel stdio -hdb fs.img -hda xv6.img
如果通过 kvm 执行,可执行如下命令
kvm -smp 4 -parallel stdio -hdb fs.img xv6.img
qemu 和 kvm 的相关运行参数的含义可参考附录 B。
调试[need update]
对 qemu 而言,可以同时实现 qemu 内嵌的 debugger 调试(需要打陈渝老师扩展的 patch 并
重新编译生成新的 qemu,特点是简单,可控制硬件的手段多,缺点是不是 C 源码级调试)和
通过 gdb 远程调试(特点是是可进行 C 源码级调试,缺点是可能会有奇怪的问题,对硬件控制
不够)。
1 用 gdb 远程调试的方法如下:
a qemu 调试方式启动
qemu -S -s
-smp 2 -monitor stdio -hdb fs.img -hda
xv6.img
b gdb 启动并调试
gdb kernel
(gdb) target remote :1234
(gdb) break FUNCTION-NAME
(gdb) continue
...
(gdb) quit
2 用 qemu internal debugger 调试
a qemu 启动命令
qemu -smp 2 -monitor stdio -hdb fs.img -hda xv6.img
然后在 qemu 的 monitor 中可执行如下命令进行调试分析
x /fmt addr -- virtual memory dump starting at 'addr'
info cpus -- show infos for each CPU
info registers -- show the cpu registers
singlestep singlestap_enabled -- toggle singlestep mode
breakpoint_insert addr -- insert breakpoint
breakpoint_remove addr -- remove breakpoint
breakpoint_show -- show breakpoint
watchpoint_insert addr type -- insert watchpoint type 0=read 1=write
watchpoint_remove addr -- remove watchpoint
watchpoint_show -- show watchpoint
where -- show calls stack
第一章 总体结构和系统组成
本章将给出 xv6 启动实现的概貌。读者将学习以下一些内容:
操作系统是什么?
xv6 是如何产生的?
xv6 的总体结构是什么?
xv6 包含哪些重要的组成部分?
操作系统是一种软件,操作系统没有一个精确和统一的定义。操作系统是一种比较复杂的软
件,我们可以从多种角度来了解操作系统。从操作系统的任务来看,操作系统的任务主要是控
制和管理计算机系统中的硬件资源并对应用软件和用户提供各种方便使用计算机的功能。通过
操作系统,能有效地组织和管理计算机系统中的硬件资源和其他软件资源,向用户和应用软件
提供各种服务功能,使得用户和应用软件能 够灵活、方便、有效地使用计算机,并使整个计算
机系统能高效地运行。从操作系统在计算机系统中的实现层次上看,操作系统位于计算机硬件
之上,应用软件之下。由于操作系统是一个复杂的软件系统,为了能够更好地设计和实现操作
系统,我们可以从功能上对操作系统进行分解,可把操作系统分解为系统调用、进程调度、内
存管理、中断处理、文件系统和设备管理等功能模块,在具体实现上可采用模块化、层次化和
面向对象等设计方法来设计实现操作系统。一个 OS 组成结构图[need to update]。要了解 xv6,
首先我们需要了解操作系统的一些基本概念(请参考附录 A)。 xv6(基于 xv6-rev2 版本)是一
个支持对称多处理器(SMP)的类 Unix 系统。它包含操作系统一些最基本的要素,包括系统调用、
进程调度、内存管理、中断处理和文件系统等。
xv6 总体设计思路
xv6 基于典型的 UNIX 操作系统设计思路。简单地说,xv6 是一种能区分内核态和用户态,
基于扁平内存管理的层次型单体内核,应用程序和操作系统是处于不同的特权状态和地址空间。
代表应用程序的用户态进程运行在 CPU 的用户态(又称非特权模式,用户模式),无法直接访问
系统硬件和操作系统中的系统数据,而操作系统运行在 CPU 的核心态(又称特权模式,内核模
式),可以访问系统硬件和核心数据。下面分别从系统调用接口、进程/线程管理、内存管理、
文件系统、I/O 管理等几个方面进行总体分析。
系统调用是应用程序访问操作系统的接口。在系统调用接口上,通用操作系统与基于此操作
系统的应用程序处于两个不同的 CPU 特权态,操作系统处于核心态,而应用程序处于用户态。
在核心态可以执行 CPU 特权指令,而用户态无法执行特权指令,且只能通过特定的指令或中断
来访问操作系统提供的各种功能。这在一定程度上保证了系统整体的安全,避免应用程序对操
作系统可能的破坏。
在内存管理方面,通用操作系统采用了虚拟内存管理方式,这样可以让内存需求超过实际
物理内存的进程/线程能够执行,其主要思想是把重要和常用的数据和执行代码放在物理内存中,
把不常用的数据和执行代码放到二级存储(这里主要指的是硬盘等可在掉电后保存数据的存储
介质),随时根据系统执行情况替换放在内存中的数据和代码。而且通过虚存管理可以实现对不
同内存区域的保护,不同进程之间,或者应用程序和操作系统之间的地址空间相对隔离。这样
一般情况下不同进程的地址空间不能直接访问,且应用程序不能直接访问内核地址空间。所以
一个与错误的应用程序不会导致系统的崩溃,从而增加了系统的可靠性。xv6 操作系统没有采
用虚拟内存管理,而是采用了简单的基于 X86 段模式的单一地址空间管理方式。在内存分配和
释放的管理上,xv6 相对实现得比较简单,采用基于可变分区分配的首次适配算法,容易产生
内存碎片。
在进程/线程管理方面,当前通用操作系统结合虚存管理,采用进程和线程结合的管理方式。
进程代表了一个程序执行的过程以及其所占用的计算机资源(包括 CPU、内存、文件等),进程
的执行流可用线程来表示。操作系统的调度单位可以是进程或线程。一个进程可以包含多个线
程,属于同一进程的多个线程共享进程管理的资源,比如属于同一进程的多个线程共享进程所
管理的内存,这样这些线程可以直接访问属于进程的全局地址空间。 xv6 操作系统实现了一个
基于进程(没有实现线程)的简单进程管理机制。
在文件系统管理方面,当前通用操作系统结合虚存管理,实现了多种复杂、高效且可靠的文
件系统,且建立了一个统一的虚拟文件系统层,屏蔽不同文件系统的差异,对上层提供统一的
接口。且与用户管理和进程管理结合,可实现安全管理,保证对文件的安全访问。xv6 操作系
统实现了一个相对简单的基于 inode 索引方式的文件系统。
在 I/O 管理方面,xv6 操作系统与通用操作系统(特别是类 UNIX 操作系统)差别不是特别
大,都把设备“看成”是一种特殊的设备文件,有设备号,用文件的访问接口来进行打开、关闭、
读、写和控制等操作。在灵活性方面,xv6 驱动程序不能象通用操作系统那样根据硬件情况动
态加载,而是在编译时候就静态确定的。
xv6 总体架构
从操作系统模型上来看,xv6 是一个单地址空间的层次式单体内核,不是微内核
(microkernel)模型的操作系统(如 Mach,QNX),与通用操作系统(如 Linux)的架构在地址
空间和特权模式上也有一定的差别。下面主要分进程调度、内存管理、同步互斥、文件系统几
方面对 xv6 进行介绍。
同步互斥
由于在 SMP 架构中,内存磁盘等硬件资源在所有 CPU 中都是共享的,所以在需要某
种机制对资源进行互斥访问控制。在 xv6 中,通 过实现了 spinlock,从而可以对共享
资源加锁来限制同时访问此资源的 CPU 数量。
内存管理
在内存管理方面, xv6 采用了段式虚拟内存的管理方式。每个用户进程所占用的内存都
是在一个连续的段中。用户进程内存的分布为: 代码段、静态变量段、固定大小的栈
和可变大小的堆空间。由于进程内存是按照段管理的,因此在每次分配进程内存时,
xv6 将找一片 正好能放下整段的连续内存块进行放置。
进程管理
因为是基于 SMP 架构,操作系统中的多个进程会占用计算机系统中的多个 CPU 执行
其具体功能,由于进程数量大于 CPU 数量,这就涉及到进程如何分时共享 CPU 的操
作系统管理问题,具体包括如果创建进程、如何删除进程、选 择哪个进程占用哪个
CPU,何时进行进程切换,进程能够持续占用 CPU 的时间片段的大小设定等。在 xv6
中,首先其进程是基于时间片来调度的。每次进程的 调度是由时钟中断产生的,或者
是因当前进程主动放弃。 其次,每个 CPU 之间都共享一个进程池(具体实现为一个全
局数组),其中有所有待运行的进程。在每个时间片中,CPU 将当前运行的进程放回进
程池,然后从 进程池中选取另一个待运行的进程进行执行。
文件系统
xv6 中提供了一个简单的文件系统,这个文件系统提供了大多数 POSIX 标准的接口。
由于这个文件系统比较简单,其中一个文件最多由(12+128)个组成, 所以文件的大小
也被限制在(12+128)*512Bytes。在这个文件系统中提供了一个 Buf 层,用来缓存磁盘
上的数据。但是此文件系统是写直达的,因此每次更新都会直接写到磁盘上。
中断管理和系统调用管理[NTU]
外设管理[NTU]
第二章 启动流程(boot)
1.概述
本章将给出 xv6 启动实现的概貌。读者将学习以下一些内容:
bootloader 是什么?
bootloader 做了哪些事情?
xv6 是如何被加载并启动的?
xv6 的初始内存管理是如何实现的?
xv6 的初始中断管理是如何实现的?
xv6 如何实现内核态到用户态的转变的?
xv6 启动用户态进程前需要完成哪些事情?
xv6 如何创建并启动第一个用户态进程?
当计算机加电后,一般不直接执行操作系统,而是执行引导加载程序。简单地说,引导加
载程序就是在操作系统内核运行之前运行的一段小程序。通过这段小程序,我们可以初始化硬
件设备、建立系统的内存空间映射图,从而将系统的软硬件环境带到一个合适的状态,以便为
最终调用操作系统内核准备好正确的环境。最终引导加载程序把操作系统内核映像加载到 RAM
中,并将系统控制权传递给它。
对于绝大多数计算机系统而言,操作系统和应用软件是存放在磁盘(硬盘/软盘)、光盘、EPROM、
ROM、Flash 等可在掉电后继续保存数据的存储介质上。计算机启动后,CPU 一开始会到一个特
定的地址开始执行指令,这个特定的地址存放了系统软件(不仅是操作系统,还可能是引导加
载程序等)。
引导加载程序(bootloader)是系统加电后运行的第一段软件代码。对于 PC386 的体系结构而言,
PC 机中的引导加载程序由 BIOS (Basic Input Output System,即基本输入/输出系统,其本质
是一个固化在主板 Flash/CMOS 上的软件)和位于软盘/硬盘引导扇区中的 OS Boot Loader 一起
组成。BIOS 实际上是被固化在计算机 ROM(只读存储器)芯片上的一个特殊的软件,为上层软
件提供最底层的、最直接的硬件控制与支持。更形象地说,BIOS 就是 PC 计算机硬件与上层软
件程序之间的一个"桥梁",负责访问和控制硬件。
以 PC386 为例,计算机加电后,CPU 从物理地址 0xFFFFFFF0(由初始化的 CS:EIP 确定,此时
CS 和 IP 的值分别是 0xF000 和 0xFFF0))开始执行。在 0xFFFFFFF0 这里只是存放了一条跳转指
令,通过跳转指令跳到 BIOS 例行程序起始点。BIOS 做完计算机硬件自检和初始化后,会选择
一个启动设备(例如软盘、硬盘、光盘等),并且读取该设备的第一扇区(即启动扇区)到内存一
个特定的地址 0x7c00 处,然后 CPU 控制权会转移到那个地址继续执行。至此 BIOS 的初始化工
作做完了,进一步的工作交给了 xv6。
整个 xv6 系统的启动流程大致是这样的:做为多处理系统,启动是首先从一个 CPU 的启动
进行的。第一个 CPU 的启动过程与其他在单核上启动操作系统的过程是十分类似的。 首先
BIOS 将把 OS 的 Boot Loader 从磁盘上(一般是位于第一个扇区)拷贝到内存当中。当 BIOS
将基本的初始化程序完成后,将跳转到 Boot Loader 所在内存的位置继续执行。 Boot Loader
将把 OS 的内核从磁盘上拷贝到然后运行。这样第一个 CPU 就完成了启动。那么第一个 CPU
将把启动代码拷贝到内存中,然后唤起其他 CPU 执行这一段代码, 完成它们的初始化过程。
在 xv6 的源码中,整个启动过程主要牵涉到如下几个文件:
bootloader
bootother.S
bootasm.S
bootmain.c
o
o
xv6 初始化模块
o main.c
o
2.代码分析
bootloader 代码分析
bootloader 的组成
在 makefile 中 50 行~56 行有如下语句
下面将针对这些文件进行分析,对启动过程分成两部分进行介绍。
$(CC) $(CFLAGS) -O -nostdinc -I. -c bootmain.c
$(CC) $(CFLAGS) -nostdinc -I. -c bootasm.S
50: bootblock: bootasm.S bootmain.c
51:
52:
53:
54:
55:
56:
$(LD) $(LDFLAGS) -N -e start -Ttext 0x7C00 -o bootblock.o bootasm.o bootmain.o
$(OBJDUMP) -S bootblock.o > bootblock.asm
$(OBJCOPY) -S -O binary bootblock.o bootblock
./sign.pl bootblock
从中可以看出 bootloader 包含两个文件,bootasm.S 和 bootmain.c。生成的 bootloader 会写
到一个主引导扇区上面。作为主引导扇区,其位置在软盘或硬盘的第一个扇区,其大小为 512
个字节,在此扇区的最后两个字节是一个主引导扇区特征码为”55AA”。Makefile 的 51 行和 52
行是通过 gcc 把 bootmain.c 和 bootasm.S 编译成目标文件 bootmain.o 和 bootasm.o。
Makefile 的 53 行是通过 ld 程序把目标文件 bootmain.o 和 bootasm.o 链接成目标文件
bootblock.o,且定义了起始执行的点(也称入口点)为 start 函数,具体的代码段起始地址为
0x7C00。[Q]大家还记得 0x7C00 这个特殊的地址的含义吗?Makefile 的 54 行是通过
objdump 程序把 bootblock.o 反汇编成 bootlock.asm。Makefile 的 55 行是通过 objcopy 程序把
bootblock.o 变成二进制码 bootlock。[Q]bootlock 的大小可以大于 512 字节吗?Makefile 的 56
行是通过 sign.pl 程序把 bootlock 扩展到 512 个字节,并把最后两个字节写成”55AA”。
[小实验]把最后的 xv6.img 的前 512 个字节取出来,反汇编它的内容,并与 bootasm.S 和
bootmain.c 的内容(可以用 bootblock.asm)进行比较,观察前 512 个字节的最后两个字节的内
容是否是“55AA”
代码分析
bootloader 的启动主要涉及到 bootasm.S、bootmain.c。其中 bootasm.S 的主要作用是从实模
式转化到保护模式。 bootmain 的作用是把内核从磁盘拷贝到内存中。
bootasm.S
在进入实模式向保护模式切换之前,首先需要把中断关闭("cli" at line 15),保证转换过程不被
硬件中断打断。
在 19~22 行中,将 DS, ES, SS 进行清零。
在 20~42 行 (打开 A20 地址线)
[历史]
在 8086 年代,8086 提供了 20 跟地址线,那么提供的可寻址空间范围即 0~2^20(00000H~FFFFFH)
的 1M 空间,而由于 8086 的数据处理位宽位 16 位,所以 8086 提供了段地址加偏移地址的地址转
换机制,就是我们常见的”段地址:偏移地址(或有效地址)”,实际的计算方法为:”段地址*10H+偏移
地址”,作为段地址的数据是放在段寄存器中的(16 位),而座位偏移地址的数据则是通过 8086
提供的寻址方式来计算而来的(16 位)。而“段值:偏移”这种表示法能够表示的最大内存为
10FFEEh(FFFF0 + FFFF),所以当寻址到超过 1MB 的内存时,会发生“回卷”(不会发生异
常)。但是到了 80286 提供了 24 根地址线,cpu 的寻址范围变为 2^24=16M,同时也提供了保护模
式,真的可以访问到 1MB 以上的内存了,此时如果遇到“寻址超过 1MB”的情况,系统不会再
“回卷”了,这就造成了向上不兼容。为了保持完全的兼容性,IBM 决定在 PC AT 系统上加个逻
辑,来模仿以上的回绕特征。他们的方法就是把 A20 和键盘控制器的一个输出进行 AND,这
样来控制 A20 的打开和关闭。一开始时 A20 是被屏蔽的(总为 0),直到系统软件去打开它。
注意 A20 而非 A20~A31 被控制,所以在 A20 关闭时会发生一些有趣的副作用。就是在访问奇
数 M 地址空间的时候,实际的地址会减少 1M。例如访问 1M~2M-d1 时实际访问的是 0~1M-1;
访问 3M~4M-1 时为 2M~3M-1,等等。
当 A20 Gate 禁止时,则程序就像在 8086 中运行,100000h~100FFEFh 的地是不可访问的。
在保护模式下 A20 Gate 是要打开的。
为了使能所有地址位的寻址能力,必须向键盘控制器 8042 发送一个命令。键盘控制器 8042 将
会将它的的某个输出引脚的输出置高电平,作为 A20 门的输入。一旦设置成功之后,内存将不会
再被绕回(memory wrapping),这样我们就可以寻址整个 286 的 16M 内存,或者是寻址
80386 级别机器的所有 4G 内存了。
8042 键盘控制器的 IO 端口是 0x60~0x6f,实际上 IBM PC/AT 使用的只有 0x60 和 0x64
两个端口(0x61、0x62 和 0x63 用于与 XT 兼容目的)。8042 通过这些端口给键盘控制器或
键盘发送命令或读取状态。输出端口 P2 用于特定目的。位 0(P20 引脚)用于实现 CPU 复位
操作,位 1(P21 引脚)用户控制 A20 信号线的开启与否。系统向输入缓冲(端口 0x64)写
入一个字节,即发送一个键盘控制器命令。可以带一个参数。参数是通过 0x60 端口发送
的。 命令的返回值也从端口 0x60 去读。
图 Intel8042 芯片或其兼容芯片的逻辑示意图
28~31 行,等待 I/O 端口 0x64 空闲,读 I/O 端口 0x64, 如果返回值的第 1 位(最低位为第 0
位)的值不为 0,表示端口 0x64 为 busy,需要再次重复测试,直到第 1 位为 0 为止。
33~34 行,把 0xd1 写入 I/O 端口 0x64;0xd1 命令是写输出端口,bit 0 是复位,bit 1 是 Gate
A20.
36~39 行,等待 I/O 端口 0x64 空闲,即读 I/O 端口 0x64, 如果返回值的第 1 位(最低位为第
0 位)的值不为 0,表示端口 0x64 为 busy,需要再次重复测试,直到第 1 位为 0 为止。
41~42 行,把 0xdf 写入 I/O 端口 0x60;0xdf 命令是使能 A20
至此,A20 地址线已经使能。
在第 48 行,"lgdt gdtdesc" 将新的全局段描述符表进行加载。注意到 gdtdesc 中给出了新段表
有效大小,和所在地址(gdt)。 在 gdt 中给出了三个段的描述, 第 0 段默认是空段, 第 1 段是
代码段,第 2 段是数据段。由于现在只是做模式切换之用,因此第 1、2 段的范围都是
0x0~0xffffffff。
在第 49~51 行中,通过将 CR0 的第 0 位置 1,把保护模式设置为打开。但此时段模式并没有
真正运行。只有当执行完 55 行的 ljmp 后,段模式才真正的启动。 此时 cs 变成
$PROT_MODE_CSEG 所指向的段(即 8>>3=1, 为 gdt 的第 1 段,即代码段)。在完成 ljmp 后,
机器进入 32 位模式。
在 59~65 行在将其他段寄存器置成数据段即 gdt 中的第 2 段,即数据段。
在第 68 行,将栈顶指针指向$start 坐在位置即(0x7c00)。然后在第 69 调用 bootmain 过程,进
行内核的加载。
bootmain.c
在这个文件中主要有四个函数:bootmain、waitdisk、readsect 和 readseg。其中 bootmain 是
加载内核,其余三个都是对磁盘进行访问的程序。
首先来看一下 waitdisk、readsect 和 readseg。 readseg 函数的作用是从磁盘的 offset 处开始
读取 count 个字符到 va 处。在读取数据时是通过调用 readsect 以扇区为单位进行的。因此在
88 行保证 va 是从一个扇区起始位置开始,因此要对 va 进行对齐。readsect 是对磁盘进行读取,
在读取之前每次调用 waitdisk 等待磁盘的准备过程,一旦磁盘准备好后就可以进行读取了。
然后看一下 bootmain 过程。bootmain 的目的是从磁盘中加载内核到内存中,其中内核是以
ELF 执行文件格式存在磁盘上的。首先将从磁盘读取一页大小(8*512B=4KB)的信息,其中包含
了 ELF 执行文件格式的头。从中可以知道读取镜像的大小以及存放的位置 (见 34~37 行)。当
完成拷贝后,bootmain 获取内核入口程序的地址(见 40~41 行),然后进入该入口 (即 main.c
中的 main 函数)。
操作系统初始化模块代码分析
操作系统的启动部分包含如下文件
main.c
bootother.S
main.c
main.c 的作用是进行对系统各方面的初始化工作,然后唤起其他 CPU 的初始化。 首先我们看
一下 main 过程。 在这个过程将进行一系列的初始化过程。第一步是对 BSS 段进行初始化 (18
行)。在 20 行将调用函数 mp_init 将获取所有 cpu 的信息,其中 bcpu 将指定 BOOTSTRAP
CPU 的编号(即第一个启动的 CPU 的编号)。接下 24~36 行是一系列的初始化过程,涉及
process table
buffer cache
PIC interrupt controller
IOAPIC interrupt controller
physical memory allocator
trap vectors
file table
inode cache
console device & interrupt
IDE device & interrupt
timer (only for uniprocessor)
first user process
这些将在后面的文章具体介绍。在初始话内存、中断表、文件系统、I/O 设备等之后,第一个
CPU 将启动调用 bootothers()去启动其他的 CPU。 在调用函数 bootothers()之前,第一个用户
进程将通过 userint()进行初始化。在初始化其他 AP 后,将进入 scheduler()过程。scheduler
过程是对单个 CPU 的进行进程调度的,这将在以后进行讨论。
然后,我们看一下 bootothers()过程。 在这个过程中将对除 bootstrap CPU 之外每个 CPU 进
行启动。 启动时这样进行的,首先把 bootother.S 的代码拷贝到 0x7000 起始的这块内存里。
然后在 0x7000-4、0x7000-8 两个内存单元记录下 bootother.S 中将要进行跳转的内核栈位置以
及 mpmain 的入口地址。 这样当 CPU 运行完 bootother.S 中的代码之后将进入 mpmain 过程。
在 mpmain 中,每个 CPU 将进行中断表和段表的初始化,然后打开中断进入 scheduler()过程。
bootother.S
bootother.S 完成启动其他 CPU 的启动工作,根据 Makefile 的 58
62 行:
58: bootother: bootother.S
59:
60:
61:
62:
$(CC) $(CFLAGS) -nostdinc -I. -c bootother.S
$(LD) $(LDFLAGS) -N -e start -Ttext 0x7000 -o bootother.out bootother.o
$(OBJCOPY) -S -O binary bootother.out bootother
$(OBJDUMP) -S bootother.o > bootother.asm
可以了解到:Makefile 的 59 行是通过 gcc 把 bootother.S 编译成目标文件 bootother.o。
Makefile 的 60 行是通过 ld 把 bootother.o 进行地址重定位,设定其起始入口点为 start,起始地
址位 0x7000,并生成执行文件 bootother.out。Makefile 的 61 行是通过 objcopy 把
bootother.out 转变成二进制代码 bootother。Makefile 的 62 行是通过 objdump 把 bootother.o
反汇编成 bootother.asm。
bootother.S 的执行内容十分类似之前的 bootasm.S。在这个文件中晚启动的 CPU 将会进行从
实模式到保护模式的转化(42
49 行)。然后重设段寄存器(54
59 行)。完成后,便设置 kernel 栈,跳转进入 mpmain 过程 (61-63 行)。[Q]这个栈的内容
是什么?
第三章 同步互斥与锁机制 (spinlock)
1.概述
本章将给出 xv6 同步互斥实现的概貌。读者将学习以下一些内容:
什么是竞争状态?
什么是互斥?
什么是同步?[NUD]
xv6 中的临界区代码是什么?
xv6 是怎样处理临界区代码的?
当两个或多个线程在执行一些关键性的临界区代码时(如对共享资源的访问),如何确保它
们不会相互妨碍?当线程之间存在着某种依存关系时,如何来调整它们的运行次序?当线程经
常需要与其它线程进行通信,那么如何根据需要提供有效的通信手段?这实际上需要操作系统
提供同步互斥与通信的手段才能解决上述问题。
任何为进程所占用的实体都可称为资源。资源可以是 CPU、内存,也可以是 I/O 设备,还可
以是一个变量,一个结构或一个数组等。可以被一个以上进程使用的资源叫做共享资源。为了
防止数据被随意访问(特别是执行写操作),每个进程在与共享资源打交道时,必须独占该资源。
这叫做互斥(mutual exclusion)。需要互斥访问的共享资源称为临界资源。
如果两个或多个进程对同一共享资源同时进行读写操作,而最后的结果是不可预测的,该结
果取决于各个进程具体运行情况。则称此状态为竞争状态(race condition)。对共享资源的访
问,可能导致竞争状态的出现。我们把可能出现竞争态的程序片断称为程序临界区。程序临界
区在处理时不可以被中断,要保证其操作的原子性。为确保临界区程序执行过程中不被中断,
在进入临界区之前要屏蔽中断,而临界区代码执行完以后要立即使能中断,以减少对中断处理
延迟的影响。
Spinlock 的引入是为了进行资源的互斥访问。在 SMP 架构下,每个 CPU 的权限都是相同的,
但是某些情况下,一个 CPU 需要对资源进行独占,此时就可以通过 spinlock 来进行。spinlock
是通过一条 CPU 原子指令 xchg 完成的。具体的实现方法如下。