【操作系统】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; // 返回物理块号
  }
  // 如果块号超出了直接块和间接块的范围,抛出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

  1. 初始化:代码包含了必要的头文件,如基本类型定义、ELF(Executable and Linkable Format)文件格式相关定义、x86架构相关定义和内存布局定义。
  2. 读取内核bootmain 函数首先从硬盘的第一个扇区(跳过引导扇区)读取 ELF 格式的内核映像。它检查 ELF 头部的魔数(Magic Number)以确认文件格式,并根据 ELF 头部信息找到各个程序段(Program Header)。
  3. 加载程序段:对于 ELF 文件中的每个程序段,函数根据其在文件中的偏移量将其内容加载到相应的物理地址。如果程序段在内存中的大小大于其在文件中的大小,剩余的部分会被清零。
  4. 跳转到内核入口:加载完所有必要的程序段后,函数通过 ELF 头部指定的入口地址跳转到内核代码。这是通过将入口地址转换为函数指针并调用它来实现的。
  5. 磁盘读取函数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!
// 空白页。

  • 12
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值