“回卷”了,这就造成了向上不兼容。为了保持完全的兼容性,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行,\将新的全局段描述符表进行加载。注意到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: $(CC) $(CFLAGS) -nostdinc -I. -c bootother.S
60: $(LD) $(LDFLAGS) -N -e start -Ttext 0x7000 -o bootother.out bootother.o 61: $(OBJCOPY) -S -O binary bootother.out bootother 62: $(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完成的。具体的实现方法如下。
2.代码分析
spinlock.h
文件中给出了spinlock结构的定义如下:
struct spinlock {
uint locked; // Is the lock held?
// For debugging:
char *name; // Name of lock.
int cpu; // The number of the cpu holding the lock.
uint pcs[10]; // The call stack (an array of program counters) // that locked the lock. };
可以看到,起作用的主要是locked变量,这个用来表示当前锁是否被锁上。其他的变量name, cpu 和 pcs都是其调试作用的。其中name记录锁的用途,cpu记录了那个cpu取得了这把锁,pcs记录了获得这把锁时的栈的内容。
spinlock.c
spinlock.c文件中包含了对spinlock的各种操作:初始化、获取锁,释放锁等。我们依次阅读文件中的每个过程。
初始化锁
initlock过程是对所进行初始化,即初始化name、locked和cpu三个域。cpu被置成0xffffffff,表示不被任何cpu所有。
获取锁
acquire过程是获取锁(变量lock)的过程。 在28~29行,通过pushcli屏蔽中断,检查当前锁是否已经被当前CPU所占有。此时,用cli指令把当前CPU的中断关闭(注意其他CPU的中断并不受到影响,而且其他软中断也并没有被关闭)。在程序35行的while将直到成功获取锁为止。在35行通过cmpxchg去获取锁。其指令格式为:xchg(va , newvalue)。作用是交换内存地址va中的值和newvalue,返回值为内存地址va中运行xchg之前的值。因为locked=0表示这把锁没有人获取,此时xchg(va,1)便能返回0,跳出while循环,否则在while循环中打转。跳出while表示获得了锁,42行是记录获得锁的CPU ID。43行是调用函数getcallerpcs记录获得锁时的函数调用栈。
释放锁
release过程十分简单,首先判断是否当前CPU拿到这把锁,如果不是,则报错;如果是,那么就把锁进行释放,并清除获得锁的CPU记录和函数调用栈。同时如果当前CPU已经没有拿任何锁,那么通过popcli将中断打开。
第四章 内存管理
1.概述
本章将给出xv6内存管理实现的概貌。读者将学习以下一些内容:
? ? ?
xv6如何进行内存管理初始化的? xv6的静态内存分配的空间包括什么? xv6的动态内存分配的空间包括什么?
? ? xv6的动态内存分配是如何实现的? 如何使用xv6的动态内存分配的空间? 内存管理机制是实时操作系统的重要组成部分。xv6不支持虚拟存储管理,不支持复杂的段页式的保护机制,而采用线性编址方式,即逻辑地址和物理地址一一对应的平面模式。这样没有虚拟存储管理提供的不受限于物理内存大小的大地址空间、地址保护等功能。 xv6同时支持静态内存分配和动态内存分配两种管理方式。静态内存分配是指在编译或链接时将应用所需的内存空间分配好。采用这种分配方案的xv6内核映像所占内存空间(代码段和数据段等)的大小一般在编译时就能够确定,中断向量表等其它区域所占用的内存空间大小是个定值。这样采用静态的内存分配机制,在编译时就可以确定xv6所需内存的大小。而动态内存分配是指系统运行时根据应用需要动态地分配内存。动态内存分配的实现机制灵活,给程序实现带来极大的方便,有的应用环境中动态内存分配甚至是必不可少的。 xv6目前采用的是基于段模式的寻址方式。首先在xv6.img所在内存的末尾开始,增加一块1MB的空闲动态内存空间。然后通过一种链表的方式来管理这1MB内存空间的在xv6中动态分配和释放。在初始化完动态内存空间后的内存布局如下: 2.代码分析 kalloc.c 动态内存管理初始化 内存空间初始化工作由函数kinit完成,执行完kinit后,内存布局如下所示: 内核首先会被bootloader加载到x0x10000,然后main初始化总控函数通过kalloc.c文件中的kinit函数对内存进行初始化。其执行流程如下: 1.调用initlock函数初始化用于内存管理互斥的kalloc_lock(32行) 2.得到代码段数据段之后的最后地址指针end,并把它赋值给start指针变量,并把start指针页对齐,把start指针指向的地址作为xv6所管理的空闲区域的起始地址(33 34行) 3.设置可使用的内存空间为256个4K大小的页,并调用kfree函数对start指针指向的地址为首地址,大小为256*4*1024个字节的空间进行空闲设置处理。(35