【操作系统】xv6源代码分析
考试内容(?)
解析题
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; // 返回物理块号}// 如果块号超出了直接块和间接块的范围,抛出panicpanic("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,%aljnz seta20.1movb $0xd1,%al # 0xd1 -> 端口0x64outb %al,$0x64seta20.2:inb $0x64,%al # 等待非忙状态testb $0x2,%aljnz seta20.2movb $0xdf,%al # 0xdf -> 端口0x60outb %al,$0x60# 从实模式切换到保护模式。使用引导GDT,使虚拟地址直接映射到物理地址lgdt gdtdescmovl %cr0, %eaxorl $CR0_PE, %eaxmovl %eax, %cr0//PAGEBREAK!# 通过长跳转来完成到32位保护模式的过渡,重新加载%cs和%eipljmp $(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 # -> FSmovw %ax, %gs # -> GS# 设置堆栈指针并调用C语言代码movl $start, %espcall bootmain# 如果bootmain返回(它不应该),在Bochs下触发断点,然后循环movw $0x8a00, %ax # 0x8a00 -> 端口0x8a00movw %ax, %dxoutw %ax, %dxmovw $0x8ae0, %ax # 0x8ae0 -> 端口0x8a00outw %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 = 1outb(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寄存器到EAXorl $(CR4_PSE), %eax # 开启页大小扩展位movl %eax, %cr4 # 将修改后的值写回CR4这里设置了页目录的地址。`entrypgdir` 是页目录的开始,地址转换为物理地址,并存放在控制寄存器 `CR3` 中。movl $(V2P_WO(entrypgdir)), %eax # 将entrypgdir的物理地址加载到EAXmovl %eax, %cr3 # 设置CR3寄存器为页目录的地址启用分页机制。通过设置控制寄存器 CR0 中的 PG 位来启动分页。movl %cr0, %eax # 读取CR0寄存器到EAXorl $(CR0_PG|CR0_WP), %eax # 开启分页和写保护movl %eax, %cr0 # 将修改后的值写回CR0设置堆栈指针。stack 是内核堆栈的开始,KSTACKSIZE 是其大小。这行代码将堆栈指针 ESP 设置为堆栈的顶部。movl $(stack + KSTACKSIZE), %esp # 设置堆栈指针跳转到内核的主函数 main()。由于直接跳转会产生 PC 相对指令,因此这里使用间接跳转。mov $main, %eax # 将main函数的地址加载到EAXjmp *%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(); // 禁用PICioapicinit(); // 另一个中断控制器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!
// 空白页。