考试内容(?)
解析题
fs.c
entry.S xv6初始化代码
编程题
fs.c 文件逻辑地址向物理地址转换 bmap
fs.c 查找磁盘块的位图的算法 balloc
代码分析
磁盘块分配 balloc
// 分配一个新的磁盘块,并将其清零。该函数会从设备dev上寻找一个未被使用的磁盘块,并返回其块号。
static uint
balloc(uint dev)//dev:设备号
{
int m;//m:一个二进制数,只有一位是1,其余位都是0。用于检查和设置位图中的特定位。
struct buf *bp; //缓冲区指针
bp = 0;
// 遍历所有的磁盘块,每次步进BPB(一个块中的位数)
for(int b = 0; b < sb.size; b += BPB){//sb是全局变量superblock超级块
// 读取当前块的位图块到缓冲区
bp = bread(dev, BBLOCK(b, sb));//BBLOCK:给定一个块号b和超级块信息sb,计算该块在位图中对应的块位置。
// 遍历当前位图块中的所有位,查找未使用的块。b+bi是当前正在检查的磁盘块的块号,确保在分配磁盘块时不会超出文件系统的总大小。
for(int bi = 0; bi < BPB && b + bi < sb.size; bi++){
// 计算当前位的掩码,把1挪到位图块的特定位。
//bi%8:bi在它所在的字节中的具体位置
m = 1 << (bi % 8);
// 如果当前块未被使用(位图中对应的位为0)
//bi/8:此处拿出所在的那个字节来运算
if((bp->data[bi/8] & m) == 0){
// 标记当前块为已使用(在位图中将对应的位设置为1)
bp->data[bi/8] |= m;
// 将改动写入日志
log_write(bp);
// 释放缓冲区
brelse(bp);
// 将新分配的块清零
bzero(dev, b + bi);
// 返回新分配的块号
return b + bi;
}
}
// 释放缓冲区
brelse(bp);
}
}
块号映射 bmap
映射一个inode中的逻辑块号到对应的物理块号
// 与每个inode关联的内容(数据)存储在磁盘的块中。
// 前NDIRECT个块号列在ip->addrs[]中。
// 接下来的NINDIRECT块列在块ip->addrs[NDIRECT]中。
// 返回inode ip中第bn个块的磁盘块地址。
// 如果没有这样的块,bmap会分配一个。
static uint bmap(struct inode *ip, uint bn)
{
uint addr, *a;//a:间接块的指针,通过a[]来访问间接块位置
struct buf *bp;
// 处理直接块
// 如果请求的块号在直接块的范围内(即小于NDIRECT)
//NDIRECT:直接映射的块数,非直接映射起始的下标
if(bn < NDIRECT){
// 获取inode中对应的磁盘块地址
// 如果该地址为0,表示这个直接块还没有被分配
if((addr = ip->addrs[bn]) == 0)
// 为该直接块分配一个新的物理块
ip->addrs[bn] = addr = balloc(ip->dev);
return addr; // 返回物理块号
}
bn -= NDIRECT;
// 处理间接块
// 如果请求的块号在间接块的范围内(即在NDIRECT到NDIRECT+NINDIRECT之间)
//NINDIRECT:一个磁盘块可以存储多少块地址
if(bn < NINDIRECT){
// 获取inode中间接块的地址
// 如果该地址为0,表示间接块还没有被分配
if((addr = ip->addrs[NDIRECT]) == 0)
// 为间接块分配一个新的物理块
ip->addrs[NDIRECT] = addr = balloc(ip->dev);
// 读取间接块,间接块存放了指向实际数据块的指针
bp = bread(ip->dev, addr);
a = (uint*)bp->data;
// 获取间接块中对应的物理块地址
// 如果该地址为0,表示数据块还没有被分配
if((addr = a[bn]) == 0){
// 为数据块分配一个新的物理块,并更新间接块
a[bn] = addr = balloc(ip->dev);
// 将更新后的间接块写回磁盘
log_write(bp);
}
// 释放缓冲区
brelse(bp);
return addr; // 返回物理块号
}
// 如果块号超出了直接块和间接块的范围,抛出panic
panic("bmap: out of range");
}
启动 bootasm.S
将 CPU 从 BIOS 提供的 16 位实模式切换到 32 位的保护模式。在这个过程中,首先关闭中断,清零数据段寄存器,解除物理地址线 A20 的绑定以访问更多的内存。接着,代码加载全局描述符表(GDT),设置 CR0 寄存器来启用保护模式,并通过长跳转来完成模式的切换。进入保护模式后,它设置了新的数据段寄存器,并初始化堆栈指针,然后调用 bootmain
函数。bootmain
函数负责从硬盘加载 ELF 格式的操作系统内核到内存,并将控制权交给内核。如果 bootmain
返回(理论上不应该发生),代码将进入无限循环,这在某些模拟器(如 Bochs)中用于触发断点。整个过程为操作系统的进一步启动和运行做好了准备,实现了从基础的 BIOS 服务向完全由操作系统控制的环境的转变。
#include "asm.h" // 包含汇编语言相关的定义
#include "memlayout.h" // 包含内存布局的定义
#include "mmu.h" // 包含内存管理单元(MMU)的定义
# 启动第一个CPU:切换到32位保护模式,跳转到C语言代码
# BIOS将此代码从硬盘的第一个扇区加载到物理地址0x7c00,并在实模式下开始执行
.code16 # 为16位模式组装
.globl start
start:
cli # BIOS禁用外部中断
# 处理器将不再响应外部中断请求,只有非屏蔽中断和软件中断仍然可以被执行。通常用于关键代码段,保护它们免受中断处理的干扰,从而保证了代码段的原子性。
# 将数据段寄存器DS, ES和SS清零
xorw %ax,%ax # 将%ax设置为零
movw %ax,%ds # -> 数据段
movw %ax,%es # -> 扩展段
movw %ax,%ss # -> 栈段
# 解除物理地址线A20的绑定
seta20.1:
inb $0x64,%al # 等待非忙状态
testb $0x2,%al
jnz seta20.1
movb $0xd1,%al # 0xd1 -> 端口0x64
outb %al,$0x64
seta20.2:
inb $0x64,%al # 等待非忙状态
testb $0x2,%al
jnz seta20.2
movb $0xdf,%al # 0xdf -> 端口0x60
outb %al,$0x60
# 从实模式切换到保护模式。使用引导GDT,使虚拟地址直接映射到物理地址
lgdt gdtdesc
movl %cr0, %eax
orl $CR0_PE, %eax
movl %eax, %cr0
//PAGEBREAK!
# 通过长跳转来完成到32位保护模式的过渡,重新加载%cs和%eip
ljmp $(SEG_KCODE<<3), $start32
.code32 # 现在通知汇编器生成32位代码
start32:
# 设置保护模式下的数据段寄存器
movw $(SEG_KDATA<<3), %ax # 我们的数据段选择器
movw %ax, %ds # -> DS: 数据段
movw %ax, %es # -> ES: 扩展段
movw %ax, %ss # -> SS: 栈段
movw $0, %ax # 清零尚未准备使用的段
movw %ax, %fs # -> FS
movw %ax, %gs # -> GS
# 设置堆栈指针并调用C语言代码
movl $start, %esp
call bootmain
# 如果bootmain返回(它不应该),在Bochs下触发断点,然后循环
movw $0x8a00, %ax # 0x8a00 -> 端口0x8a00
movw %ax, %dx
outw %ax, %dx
movw $0x8ae0, %ax # 0x8ae0 -> 端口0x8a00
outw %ax, %dx
spin:
jmp spin
# 引导GDT
.p2align 2 # 强制4字节对齐
# 定义了引导时使用的全局描述符表GDT
gdt:
SEG_NULLASM # 空段
SEG_ASM(STA_X|STA_R, 0x0, 0xffffffff) # 代码段
SEG_ASM(STA_W, 0x0, 0xffffffff) # 数据段
# 定义了GDT的大小和地址
gdtdesc:
.word (gdtdesc - gdt - 1) # sizeof(gdt) - 1
.long gdt # gdt的地址
启动 bootmain.c
- 初始化:代码包含了必要的头文件,如基本类型定义、ELF(Executable and Linkable Format)文件格式相关定义、x86架构相关定义和内存布局定义。
- 读取内核:
bootmain
函数首先从硬盘的第一个扇区(跳过引导扇区)读取 ELF 格式的内核映像。它检查 ELF 头部的魔数(Magic Number)以确认文件格式,并根据 ELF 头部信息找到各个程序段(Program Header)。 - 加载程序段:对于 ELF 文件中的每个程序段,函数根据其在文件中的偏移量将其内容加载到相应的物理地址。如果程序段在内存中的大小大于其在文件中的大小,剩余的部分会被清零。
- 跳转到内核入口:加载完所有必要的程序段后,函数通过 ELF 头部指定的入口地址跳转到内核代码。这是通过将入口地址转换为函数指针并调用它来实现的。
- 磁盘读取函数:
waitdisk
函数等待磁盘就绪,readsect
函数从指定偏移处读取一个扇区到目标内存地址,readseg
函数则是在较高层次上读取连续的多个扇区数据到内存中。
#include "types.h" // 包含基本类型定义
#include "elf.h" // 包含ELF格式相关定义
#include "x86.h" // 包含x86架构相关定义
#include "memlayout.h" // 包含内存布局定义
#define SECTSIZE 512 // 定义扇区大小为512字节
void readseg(uchar*, uint, uint); // 声明readseg函数
void
bootmain(void)
{
struct elfhdr *elf; // ELF格式头指针
struct proghdr *ph, *eph; // 指向程序头部的指针
void (*entry)(void); // 指向入口函数的指针
uchar* pa; // 物理地址指针
elf = (struct elfhdr*)0x10000; // 分配临时空间用于存放ELF头
// 从磁盘读取第一页
readseg((uchar*)elf, 4096, 0);
// 检查是否为ELF可执行文件
if(elf->magic != ELF_MAGIC)
return; // 如果不是,则返回错误
// 加载每个程序段(忽略ph标志)
ph = (struct proghdr*)((uchar*)elf + elf->phoff);
eph = ph + elf->phnum;
for(; ph < eph; ph++){
pa = (uchar*)ph->paddr;
readseg(pa, ph->filesz, ph->off);
if(ph->memsz > ph->filesz)
stosb(pa + ph->filesz, 0, ph->memsz - ph->filesz);
}
// 从ELF头部调用入口点
// 调用后不返回
entry = (void(*)(void))(elf->entry);
entry();
}
void
waitdisk(void)
{
// 等待磁盘就绪
while((inb(0x1F7) & 0xC0) != 0x40)
;
}
// 在offset偏移处读取单个扇区到dst
void
readsect(void *dst, uint offset)
{
// 发出命令
waitdisk();
outb(0x1F2, 1); // count = 1
outb(0x1F3, offset);
outb(0x1F4, offset >> 8);
outb(0x1F5, offset >> 16);
outb(0x1F6, (offset >> 24) | 0xE0);
outb(0x1F7, 0x20); // cmd 0x20 - 读取扇区
// 读取数据
waitdisk();
insl(0x1F0, dst, SECTSIZE/4);
}
// 从内核将'count'字节在'offset'偏移处读到物理地址'pa'
// 可能会复制比请求更多的数据
void
readseg(uchar* pa, uint count, uint offset)
{
uchar* epa;
epa = pa + count;
// 向下舍入到扇区边界
pa -= offset % SECTSIZE;
// 从字节转换为扇区;内核从扇区1开始
offset = (offset / SECTSIZE) + 1;
// 如果太慢,我们可以一次读取很多扇区
// 我们写入的内存会超过请求的量,但这不重要 --
// 我们按递增顺序加载
for(; pa < epa; pa += SECTSIZE, offset++)
readsect(pa, offset);
}
启动 entry.S
这个文件包含了在多引导环境下,xv6 操作系统从实模式切换到保护模式并开始执行的代码。这里的代码主要设置了 CPU 的工作模式、内存分页、堆栈初始化,以及跳转到内核的主函数 main()
# 多引导头部,用于指示多引导加载器如何加载 xv6。它定义了特定的魔数、标志和校验和,确保加载器正确识别和加载 xv6。
.p2align 2 # 内存对齐,确保Multiboot头部正确放置
.text # 指示接下来的部分是代码段
.globl multiboot_header # 定义全局符号multiboot_header
multiboot_header:
#define magic 0x1badb002 # Multiboot头部的魔数
#define flags 0 # Multiboot头部的标志位
.long magic # 写入魔数
.long flags # 写入标志位
.long (-magic-flags) # 写入校验和
定义了 `_start` 符号作为 ELF 的入口点。在 xv6 的上下文中,此时还未设置虚拟内存,因此入口点是 `entry` 的物理地址。
.globl _start
_start = V2P_WO(entry) # 设置ELF入口点为'entry'的物理地址
进入 xv6 的启动处理器时,分页尚未开启。这几行代码开启了页大小扩展,以支持 4MB 大小的页面。
.globl entry
entry:
movl %cr4, %eax # 读取CR4寄存器到EAX
orl $(CR4_PSE), %eax # 开启页大小扩展位
movl %eax, %cr4 # 将修改后的值写回CR4
这里设置了页目录的地址。`entrypgdir` 是页目录的开始,地址转换为物理地址,并存放在控制寄存器 `CR3` 中。
movl $(V2P_WO(entrypgdir)), %eax # 将entrypgdir的物理地址加载到EAX
movl %eax, %cr3 # 设置CR3寄存器为页目录的地址
启用分页机制。通过设置控制寄存器 CR0 中的 PG 位来启动分页。
movl %cr0, %eax # 读取CR0寄存器到EAX
orl $(CR0_PG|CR0_WP), %eax # 开启分页和写保护
movl %eax, %cr0 # 将修改后的值写回CR0
设置堆栈指针。stack 是内核堆栈的开始,KSTACKSIZE 是其大小。这行代码将堆栈指针 ESP 设置为堆栈的顶部。
movl $(stack + KSTACKSIZE), %esp # 设置堆栈指针
跳转到内核的主函数 main()。由于直接跳转会产生 PC 相对指令,因此这里使用间接跳转。
mov $main, %eax # 将main函数的地址加载到EAX
jmp *%eax # 间接跳转到main
声明内核堆栈的大小
.comm stack, KSTACKSIZE # 声明名为stack的内核堆栈
启动 main.c
初始化内存分配器、设置内核页表、检测和初始化其他处理器、设置中断控制器和设备、初始化各种内核数据结构(如进程表、文件表、缓冲区缓存等),并最终启动调度器开始运行进程。此外,还包括了为多处理器环境启动非引导处理器。
#include "types.h" // 基本类型定义
#include "defs.h" // 函数声明和宏定义
#include "param.h" // 系统参数
#include "memlayout.h" // 内存布局
#include "mmu.h" // 内存管理单元
#include "proc.h" // 进程管理
#include "x86.h" // x86架构相关定义
static void startothers(void);
static void mpmain(void) __attribute__((noreturn));
extern pde_t *kpgdir;
extern char end[]; // 内核加载后的第一个地址
// 引导处理器开始运行C代码的入口。
// 分配一个真正的栈并切换到它,首先执行内存分配器所需的一些设置。
int main(void) {
kinit1(end, P2V(4*1024*1024)); // 物理页面分配器
kvmalloc(); // 内核页表
mpinit(); // 检测其他处理器
lapicinit(); // 中断控制器
seginit(); // 段描述符
picinit(); // 禁用PIC
ioapicinit(); // 另一个中断控制器
consoleinit(); // 控制台硬件
uartinit(); // 串行端口
pinit(); // 进程表
tvinit(); // 陷阱向量
binit(); // 缓冲区缓存
fileinit(); // 文件表
ideinit(); // 磁盘
startothers(); // 启动其他处理器
kinit2(P2V(4*1024*1024), P2V(PHYSTOP)); // 在startothers()之后调用
userinit(); // 第一个用户进程
mpmain(); // 完成此处理器的设置
}
// 其他CPU从entryother.S跳转到此处。
static void mpenter(void) {
switchkvm();
seginit();
lapicinit();
mpmain();
}
// 公共CPU设置代码。
static void mpmain(void) {
cprintf("cpu%d: starting %d\n", cpuid(), cpuid());
idtinit(); // 加载IDT寄存器
xchg(&(mycpu()->started), 1); // 通知startothers()我们已启动
scheduler(); // 开始运行进程
}
pde_t entrypgdir[]; // entry.S的引导页表
// 启动非引导(AP)处理器。
static void startothers(void) {
extern uchar _binary_entryother_start[], _binary_entryother_size[];
uchar *code;
struct cpu *c;
char *stack;
// 将entryother.S的代码写入未使用的内存0x7000处。
// 链接器已经将entryother.S的镜像放在_binary_entryother_start中。
code = P2V(0x7000);
memmove(code, _binary_entryother_start, (uint)_binary_entryother_size);
for(c = cpus; c < cpus+ncpu; c++){
if(c == mycpu()) // 我们已经启动。
continue;
// 告诉entryother.S使用哪个栈,从哪里进入,以及使用哪个页目录。
// 我们还不能使用kpgdir,因为AP处理器在低内存中运行,
// 所以我们也为APs使用entrypgdir。
stack = kalloc();
*(void**)(code-4) = stack + KSTACKSIZE;
*(void(**)(void))(code-8) = mpenter;
*(int**)(code-12) = (void *) V2P(entrypgdir);
lapicstartap(c->apicid, V2P(code));
// 等待CPU完成mpmain()
while(c->started == 0)
;
}
}
// entry.S和entryother.S中使用的引导页表。
// 页目录(和页表)必须从页面边界开始,因此需要__aligned__属性。
// PTE_PS在页目录项中启用4MB页面。
__attribute__((__aligned__(PGSIZE)))
pde_t entrypgdir[NPDENTRIES] = {
// 将虚拟地址[0, 4MB)映射到物理地址[0, 4MB)
[0] = (0) | PTE_P | PTE_W | PTE_PS,
// 将虚拟地址[KERNBASE, KERNBASE+4MB)映射到物理地址[0, 4MB)
[KERNBASE>>PDXSHIFT] = (0) | PTE_P | PTE_W | PTE_PS,
};
//PAGEBREAK!
// 空白页。
//PAGEBREAK!
// 空白页。
//PAGEBREAK!
// 空白页。