iT邦幫忙

2022 iThome 鐵人賽

DAY 9
0

前言

今日希望通過看到整個 xv6 的啟動過程,來一步步地看到作業系統相關的設計議題,如 memory page 機制,lock,process 等等。

啟動xv6

整個xv6的啟動大致上可以使用下圖進行表示

  • entry.s : 在entry.s中,會初始化 stack pointer,初始化 sp 暫存器,接著會初始化 tp 暫存器 (儲存每一個核心編號,xv6中有8核心,因此編號為 0 ~ 7),每個核心有各自的 stack,在 entry.s 中初始化,初始化完成後,會進入到 start.c 中。
  • start.c : 在 start.c 中,xv6 會處於 machine mode 底下,而下方將會進行詳細介紹。
  • main.c : 在 main.c 中,xv6 會處於 supervisor mode,下方將會對 main.c 進行介紹。

entry.S

當 RISC-V 架構的電腦啟動時,會自動執行儲存在 ROM (Read Only Memory) 中的引導載入程式 (boot loader),boot loader 會將 xv6 kernel 載入到記憶體中。接著在 machine mode 底下,CPU 會從kernel/entry.S第6行開始執行 xv6,從_entry標籤處開始。

# qemu -kernel loads the kernel at 0x80000000
    # and causes each CPU to jump there.
    # kernel.ld causes the following code to
    # be placed at 0x80000000.
.section .text
.global _entry
_entry:
	# set up a stack for C.
        # stack0 is declared in start.c,
        # with a 4096-byte stack per CPU.
        # sp = stack0 + (hartid * 4096)
        la sp, stack0
        li a0, 1024*4
	csrr a1, mhartid
        addi a1, a1, 1
        mul a0, a0, a1
        add sp, sp, a0
	# jump to start() in start.c
        call start
spin:
        j spin

bootloader 會將 xv6 kernel 載入到實體記憶體地址0x80000000中,原因為0x0 ~ 0x80000000包含 I/O 設備。

entry.S 中可以看到配置了一塊 stack 用來執行 C 的程式。而在 kernel/start.c 中可以看到為 stack0 初始化了一段記憶體空間。而整個程式碼運作如下:

第12行: la sp, stack0 將 stack0 的記憶體地址存放到 sp 暫存器中,sp 暫存器即為 RISC-V 中用來存放 stack pointer 的暫存器。
第13行: li 為 load immediate,是一條偽指令,li a0, 1024 * 4,將1024 * 4載入到 a0 暫存器中。
第14行: 將 mhartid CSR (其中的值為0~7 ,表示8個核心的編號,根據 NCPU 知道最大支援核心數為8) 寫入到 a1 暫存器中。

第15行: 將 a1 暫存器+1並且寫回,因此核心代號由0~7變成1~8。

第16行: 將 a0a1 相乘,也就是核心編號乘上4096 bytes,並存放回 a0

第17行: 將 sp 加上 a0,並存回 sp,也就是註解上方的 sp = stack0 + (hartid * 4096)

第19行的地方跳到 start.cstart() 函式的記憶體位置。

start.c

start.c 中有兩個函式,分別為 start()timerinit()

#include "types.h"
#include "param.h"
#include "memlayout.h"
#include "riscv.h"
#include "defs.h"

void main();
void timerinit();

// entry.S needs one stack per CPU.
__attribute__ ((aligned (16))) char stack0[4096 * NCPU];

// a scratch area per CPU for machine-mode timer interrupts.
uint64 timer_scratch[NCPU][5];

// assembly code in kernelvec.S for machine-mode timer interrupt.
extern void timervec();

第12行: 為 stack0 配置了一塊記憶體空間,而後面的 NCPU 則為巨集定義,定義位於 param.h 中,表示處理器的核心數 (hart),每一個核心擁有自己的 stack,大小為4096 byte,這裡使用到了 __attribute__ 這個關鍵字。

第14行 : 總共有8個核心,為每一個核心配置5格大,元素為 uint64 的陣列,而每一個 uint64 的使用可以從 timerinit() 看到。

// scratch[0..2] : space for timervec to save registers.
// scratch[3] : address of CLINT MTIMECMP register.
// scratch[4] : desired interval (in cycles) between timer interrupts.

__attribute__ 關鍵字

__attribute__不是 C 的一部分,__attribute__ 為 gcc 的擴充功能 (GNU extension),用於傳遞一些訊息給編譯器 (compiler)。
__attribute__ 可以設定函式屬性 (function attribute),變數屬性 (variable attribute),型別屬性 (type attribute) 等等,而語法格式為以下。

__attribute__((???))

總共有以下參數值可以設定,分別為

  • aligned
  • packed
  • transparent_union
  • deprecated
  • may_alias

而在 start.c 中第11行的部分,使用了 aligned 作為參數,該參數用於告訴編譯器將結構 (struct) 以及函式 (function) 以16-bit 作為 stack 邊界進行對齊。如果僅僅使用 __attribute__ ((aligned)),則編譯器會根據目標機器最有效率的方式進行對齊。

Example:

#include <stdio.h>

struct a{
    char arr[3];
};

struct b{
    char arr[3];
}__attribute__((aligned(8)));

int main(void)
{
    struct a A;
    struct b B;
    printf("sizeof(A) = %ld\n", sizeof(A));
    printf("sizeof(B) = %ld", sizeof(B));
}

output

sizeof(A) = 3
sizeof(B) = 8

aligned 能夠對齊的大小和 linker 所支援的最大大小有關。

記憶體對齊目的為由於 CPU 對於記憶體的讀取並非是連續的,而是一塊一塊的以1,2,4,8 byte 進行讀取,而不是一次只抓取1 byte,假設有一筆資料為 int,int 大小為4 byte,如果一次只抓取1 byte,那麼我們需要4次週期,效率十分低落,因此 CPU 在讀取資料時會一次抓取 (fetch) 一塊的大小。

當有一些資料沒有進行對齊,可能我們需要兩次存取週期才能夠存取資料,使得效率下降。
為了增加資料的讀取速度,進行記憶體對齊將會帶來效率上的提升。在 C 語言中 struct 會自動進行記憶體對齊 (memory alignment)。可以在一個 struct 中,放入兩個成員 (member),分別為 char 和 int ,可以發現該 struct 大小為 8 byte,而非 5 byte,這是因為進行了記憶體對齊的緣故。

start()

// entry.S jumps here in machine mode on stack0.
void
start()
{
  // set M Previous Privilege mode to Supervisor, for mret.
  unsigned long x = r_mstatus();
  x &= ~MSTATUS_MPP_MASK;
  x |= MSTATUS_MPP_S;
  w_mstatus(x);

  // set M Exception Program Counter to main, for mret.
  // requires gcc -mcmodel=medany
  w_mepc((uint64)main);

  // disable paging for now.
  w_satp(0);

  // delegate all interrupts and exceptions to supervisor mode.
  w_medeleg(0xffff);
  w_mideleg(0xffff);
  w_sie(r_sie() | SIE_SEIE | SIE_STIE | SIE_SSIE);

  // configure Physical Memory Protection to give supervisor mode
  // access to all of physical memory.
  w_pmpaddr0(0x3fffffffffffffull);
  w_pmpcfg0(0xf);

  // ask for clock interrupts.
  timerinit();

  // keep each CPU's hartid in its tp register, for cpuid().
  int id = r_mhartid();
  w_tp(id);

  // switch to supervisor mode and jump to main().
  asm volatile("mret");
}

entry.S 跳轉到 start.cstart.c 在 Machine Mode 底下執行,執行一些初始化的設定,而具體設定為以下

第6~9行: 讀取 mstatus 暫存器的內容到變數 x 中, MSTATUS_MPP_MASK 位於 kernel/riscv.h 中,我們將 MPP 欄位的兩個 bit 歸零,接著將這兩個歸零為加上 MSTATUS_MPP_S,也就是切換到 supervisor mode,接著通過 w_mstatus 將值寫入到 mstatus 暫存器中,下圖為 mstatus 暫存器的資料欄位圖。

第12行: 將 main 的記憶體地址寫入到 mepc 暫存器中,該暫存器用途為當 trap 發生時所使用的 Program counter。

第16行: 將 satp 暫存器設置為0,satp 暫存器表示是否需要 pagging,0表示禁用。

第19~20行: 將 medelegmideleg 全部寫入成1,表示當 interrupts (中斷) 或是 exceptions (例外) 發生時,不會在 machine mode 底下進行處理,而是由 supervisor mode 進行處理。

第21行: 讀取 sie 暫存器,用於決定是否 interrupt (中斷),對 sie 暫存器寫入一些值,並且將它存放回 sie 暫存器中,而這一些值表示允許在 supervisor mode 底下,I/O 裝置 (外部裝置 externel) interrupt,timer interrupt,software interrupt。SEIE 為 Supervisor mode externel interrupt enable 的縮寫。

第25~26行: 作用為希望在 supervisor mode 能夠存取所有的實體記憶體位置。

第29行: 對 timer 進行初始化,為了後續的 timer interrupt。

第32~33行: 讀取 mhartid 暫存器並將值寫入到 tp 暫存器中,tp 暫存器存放 thread pointer。這決定等一下程式碼會在哪一個處理器核心上執行。

第36行: 執行由 RISC-V 所提供了 mret 指令,表示當處在 machine mode 底下,用於退出 trap 的指令,而退出時硬體會做兩件事情

  • mepc 指向的記憶體地址開始執行,這裡我們在第12行的地方將 mepc 指向到 main 函式,因此當 machine mode 退出 trap 時,會從 main 函式開始執行。
  • 更新 mstatus 暫存器,MIE 更新成 MPIE 的值,MPIE 更新成1,MPP 功用為在 trap 結束後,恢復到先前的模式,而在第6~9行模式設定成 supervisor mode,因此 trap 結束後會切換到 supervisor mode 中。

timerinit()

void
timerinit()
{
  // each CPU has a separate source of timer interrupts.
  int id = r_mhartid();

  // ask the CLINT for a timer interrupt.
  int interval = 1000000; // cycles; about 1/10th second in qemu.
  *(uint64*)CLINT_MTIMECMP(id) = *(uint64*)CLINT_MTIME + interval;

  // prepare information in scratch[] for timervec.
  // scratch[0..2] : space for timervec to save registers.
  // scratch[3] : address of CLINT MTIMECMP register.
  // scratch[4] : desired interval (in cycles) between timer interrupts.
  uint64 *scratch = &timer_scratch[id][0];
  scratch[3] = CLINT_MTIMECMP(id);
  scratch[4] = interval;
  w_mscratch((uint64)scratch);

  // set the machine-mode trap handler.
  w_mtvec((uint64)timervec);

  // enable machine-mode interrupts.
  w_mstatus(r_mstatus() | MSTATUS_MIE);

  // enable machine-mode timer interrupts.
  w_mie(r_mie() | MIE_MTIE);
}

第5行: 讀取核心編號,得到目前程式在哪一個核心上面執行

第9行: CLINT_MTIMECMP() 為巨集,定義在 kernel/memlayout.h 中,memlayout.h 中定義了實體記憶體每一個區段的意義與排列方式,展開為 (CLINT + 0x4000 + 8*(hartid)),其中 CLINT 也是巨集,也是定義在 kernel/memlayout.h#deifne CLINT 0x2000000LCLINT 為 core local interruptor 的簡寫,其中包含 timer,而每一個核心都有各自的 CLINTCLINT 包含許多暫存器 (如下表),而 CLINT_MTIMECMP() 作用為取得每一個核心的 CLINTMTIMECMP 暫存器的記憶體地址。

source

而我們會取得 CLINT_MTIME 暫存器中存放的值,得到目前的時間,加上 interval,interval 代表每 interval cycles 都會發生一次 timer interrupt。

而當 timer interrupt 發生時,在 interrupt 期間我們就需要使用到 timer_scratch[id] 中所存放的內容。而根據 timerinit() 中的註解內容,我們可以將 scratch 的內容進行以下表示

而在第15行我們取得指向每一個核心對應到的 scratch 陣列的指標,以便後續對每一個核心對應到的 scratch 陣列的內容進行更動。

在第16行到第17行將內容寫入到 scratch 中對應到的欄位,並將其寫入到 mscratch 暫存器中 (每一個核心有對應的 mscratch 暫存器,即是 CSR),因此 mscratch 暫存器指向的即為每一個核心的 scratch,而從前面的內容我們可以知道只有在 timer interrupt 時,我們才會使用到 scratch 中的內容,因此我們可以知道只有在 timer interrupt 的時候 (且是位在 machine mode 底下),才會使用到 mscratch CSR。

在第21行,我們對 mtvec 暫存器進行寫入,mtvec 存放在 machine mode 當 trap 發生時,我們需要跳轉 (jumping) 到的記憶體地址,mtvec 為 machine mode trap vector 的簡寫,這裡我們可以知道當 trap 發生時,我們需要跳轉到 timervec 的記憶體地址。

第24行,讀取 mstatus 暫存器,並 machine-mode interrupts 啟用,接著寫入到 mstatus 暫存器中。

第26行,讀取 mie 暫存器,啟用 timer interrupt,並且寫入到 mie 暫存器中。

而第21行時,提及當 timer interrupt 發生時,我們會跳轉到 timervec 的記憶體地址

main.c

#include "types.h"
#include "param.h"
#include "memlayout.h"
#include "riscv.h"
#include "defs.h"

volatile static int started = 0;

// start() jumps here in supervisor mode on all CPUs.
void
main()
{
  if(cpuid() == 0){
    consoleinit();
    printfinit();
    printf("\n");
    printf("xv6 kernel is booting\n");
    printf("\n");
    kinit();         // physical page allocator
    kvminit();       // create kernel page table
    kvminithart();   // turn on paging
    procinit();      // process table
    trapinit();      // trap vectors
    trapinithart();  // install kernel trap vector
    plicinit();      // set up interrupt controller
    plicinithart();  // ask PLIC for device interrupts
    binit();         // buffer cache
    iinit();         // inode cache
    fileinit();      // file table
    virtio_disk_init(); // emulated hard disk
    userinit();      // first user process
    __sync_synchronize();
    started = 1;
  } else {
    while(started == 0)
      ;
    __sync_synchronize();
    printf("hart %d starting\n", cpuid());
    kvminithart();    // turn on paging
    trapinithart();   // install kernel trap vector
    plicinithart();   // ask PLIC for device interrupts
  }

  scheduler();        
}

main.c 中可以看到許多初始化的操作,以及各種專有名詞,包含 paging, trap, interrupt 等等,這裡我們帶著這一些疑問去閱讀這一些程式碼,之後再回來看會更加清楚一些,這邊大略的介紹 main.c 的行為

每一個核心都會平行 (parallel) 的去執行 main.c ,在第13行的地方有一個 cpuid() 的函式,我們進入其中來查看其行為, cpuid() 實作位於 kernel/proc.c 中。

int
cpuid()
{
  int id = r_tp();
  return id;
}

我們可以看到讀取 tp 暫存器的值, tp 暫存器我們知道這是存放核心編號的暫存器,因此得知 cpuid() 為回傳核心編號的函式。0號核心執行 if 內述句,其餘核心執行 else。

我們可以在 main.c 中看到一個全域變數 started ,而帶有 volatile 這一個關鍵字(keyword),以下說明

volatile

volatile 為 C 語言中的關鍵字,volatile 表示該物件或是變數據有一些最佳化或是多執行續的特性,在本例子中即是多執行續的特性,volatile 告知編譯器該變數可能會隨時進行變動,因此在讀取或是儲存該變數時,應該對其變數地址進行操作,如果沒有 volatile 關鍵字修飾變數或是物件,可能在編譯器最佳化的過程,讀取該變數不是從變數的記憶體地址,而是從暫存器中進行讀取,導致一些不一致的情況發生,例如有一個外部硬體或是程式對某一個變數進行更動,而由於程式讀取暫存器內容,因此沒有讀取到該更動。

在進行了一些初始化之後,會執行userinit()建立第一個 process,而要深入探討 userinit() 會牽扯到 lock 以及 memory page, trapframe 等等機制,這一些將會在後續進行探討,現在先抽象的理解 userinit() 會建立第一個 process。我們進入到 userinit() 中可以發現一些事情

uchar initcode[] = {
  0x17, 0x05, 0x00, 0x00, 0x13, 0x05, 0x45, 0x02,
  0x97, 0x05, 0x00, 0x00, 0x93, 0x85, 0x35, 0x02,
  0x93, 0x08, 0x70, 0x00, 0x73, 0x00, 0x00, 0x00,
  0x93, 0x08, 0x20, 0x00, 0x73, 0x00, 0x00, 0x00,
  0xef, 0xf0, 0x9f, 0xff, 0x2f, 0x69, 0x6e, 0x69,
  0x74, 0x00, 0x00, 0x24, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00
};

// Set up first user process.
void
userinit(void)
{
  struct proc *p;

  p = allocproc();
  initproc = p;
  
  // allocate one user page and copy init's instructions
  // and data into it.
  uvminit(p->pagetable, initcode, sizeof(initcode));
  p->sz = PGSIZE;

  // prepare for the very first "return" from kernel to user.
  p->trapframe->epc = 0;      // user program counter
  p->trapframe->sp = PGSIZE;  // user stack pointer

  safestrcpy(p->name, "initcode", sizeof(p->name));
  p->cwd = namei("/");

  p->state = RUNNABLE;

  release(&p->lock);
}

可以發現會執行一段 initcode,而這一些看起來像是組合語言轉變成機器語言的16進位表示法,而其組合語言的表示法可以在 user/initcode.S 中看見

# Initial process that execs /init.
# This code runs in user space.

#include "syscall.h"

# exec(init, argv)
.globl start
start:
        la a0, init
        la a1, argv
        li a7, SYS_exec
        ecall

# for(;;) exit();
exit:
        li a7, SYS_exit
        ecall
        jal exit

# char init[] = "/init\0";
init:
  .string "/init\0"

# char *argv[] = { init, 0 };
.p2align 2
argv:
  .long init
  .long 0

可以看到使用了 System call exec() 執行了init(),而 init() 會替換掉目前的記憶體內容以及暫存器 (在 System call 的 exec() 中有提及此部分),kernel 完成 exec() 後,就會回到 init() 中,init() 位於 user/init.c,可以看到他會開啟 console ,console 會和檔案描述子 0,1,2 關聯在一起,我們就能夠在 console 上面進行輸入,並且在 console 上面看到輸出,接著在 console 上面開啟 Shell,到這裡整個開機流程就完成了。

而在 main.c 中,我們看到了一段程式碼,__sync_synchronize(),此為 GNU C Extension,用於擴展標準 C ,我們將在下一篇文章針對這個議題進行討論,研究同步與非同步,以及一些記憶體保護,最後切入到作業系統 lock 的機制

而整個開機中有一些記憶體分頁,lock,trap 等等細節,將會在後續提及。

reference

SiFive FU540-C000 Manual v1p0
xv6-riscv
Operating System Concepts, 9/e
RISC-V xv6 Book
xv6 Kernel-13: entry.S + start.c


上一篇
Day-07 Linker Script File: kernel.ld
下一篇
Day-09 xv6 Paging, Page Table
系列文
與作業系統的第一類接觸 : 探索 xv631
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言