動機

What I can not create I do not understand.

6.s081是個了解unix與c語言的超讚課程

有許多符合自修性質

  • 有實作
  • 有test
  • 有解答可以參考

這篇是我讀pdf(有人翻譯2020的pdf,所以有些句子可能看起來怪怪的)與lab的筆記 lab是2020版,6.s081有經歷幾次改版,lab的內容會換一下,尤其是2019到2020差有點多

裝環境

我是在win11 WSL2的ubuntu 20.04跑

sudo apt-get install git build-essential gdb-multiarch qemu-system-misc gcc-riscv64-linux-gnu binutils-riscv64-linux-gnu gcc-riscv64-unknown-elf

需要參考的話,我的lab code在

怎麼在lab使用gdb

  1. ~/.gdbinitadd-auto-load-safe-path ~/xv6-labs/.gdbinit
  2. make qemu-gdb
  3. 在另一個視窗跑gdb-multiarch

如何debug

  1. 在panic打上breakpoint
  2. 利用make產生的xxx.asm,可以用addr去對,找到哪一行出事了(addr2line -e kernel/kernel pc-value)
  3. qemu的Ctrl-a x是關閉,Ctrl-a c是類似gdb可以info mem看pagetable
  4. 一點一點的寫,可以用panic去停下cpu看狀態對不對
  5. 在沒有動過的地方掛了、一開始就動不了
    • 可能理由
      • mem不知道寫到哪了
        • 腦袋要清楚mem到底要怎麼寫
          • 這個是va, pa, pte?
          • 這裡是page, stack的終點還是起點?
          • 資料往哪邊長?
            • stack是高往低
            • 一般資料是低往高
      • concurrent沒處理好
        • deadlock
          • 這個去trace中間用到的function應該可以看到一些東西
            • 有人跟你用一樣的lock
          • 拿lock的順序對嗎
        • 沒有用lock包好
          • 思考有哪些資料是要一起動的,思考在lock結束後有什麼性質要有
            • reference counter
            • freelist

ch1

在riscv中,CPU == hart

fd

因為綁定0,1成stdin, stdout 所以會需要close,之後再開新的file完成redirect 因為fd是從小的開始分配

if(fork() == 0) {
    close(0);
    open("input.txt", O_RDONLY);
    exec("cat", argv);
}

dup做soft copy,所以下面的file會是hello world

fd = dup(1);
write(1, "hello", 6);
write(fd, "world\n", 6);

這裡想完成的事就是dynamic scope或是Parameterize

pipe

pipe會產生一個file(in mem),之後開2個fd,下面是redirect stdin到pipe

pipe(p);
if(fork() == 0) {
    close(0);
    dup(p[0]);
    close(p[0]);
    close(p[1]);
    exec("/bin/wc", argv);
} else {
    write(p[1], "hello world\n", 12);
    close(p[0]);
    close(p[1]);
}

這等於就是把ref裡面的東西暴露給user阿 (因為綁定0,1成stdin, stdout)

trace: pipe

  • pipe是由兩個file控制的ring buffer
  • 由寫到哪(nwrite)與讀到哪(nread)控制sleep與wakeup

alloc pipe

  • sys_pipe
    • alloc 兩個struct file
    • pipealloc設定兩個file
      • kalloc一塊page,作為struct pipe
      • 讓file能指到struct pipe
    • 把fd寫回去
      • copyout留到pagetable那章談

write/read syscall

  • sys_write/sys_write
    • filewrite/fileread根據struct file的type跑到pipe去
      • piperead
        • 空了 AND 對面還想要寫
          • pi->nread == pi->nwrite && pi->writeopen
          • 先去睡覺
        • 開始寫到addr (increase nread)
          • 注意到這是個ring buffer!!
        • 都好了就wakeup對面 (用nwrite去認)
          • sleeplock留到lock那章談
      • pipewrite
        • 對面不想要讀
          • return
        • 如果
          • 滿了
            • pi->nwrite == pi->nread + PIPESIZE
            • wakeup對面 (用nread去認)
            • 先去睡覺
          • 沒滿
            • 從addr讀到struct pipe中 (increase nwrite)

close syscall

  • sys_close
    • pipeclose
      • 根據fd來看是不是用來寫的
      • 之後關對應的狀態
      • wakeup另外一邊

Lab Utilities

sleep

練手用

#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"

int
main(int argc, char *argv[])
{
  if (argc != 2)
    fprintf(2, "sleep: 2 args\n");

  sleep(atoi(argv[1]));
  exit(0);
}

pingpong

這裡開始寫簡單的pipe

從前面的trace可以看出,pipe只有三個狀態下會換手

  1. read/write完成
  2. 空了/滿了
  3. close

所以寫pipe時要記得把所有read/write該關的都關一關

#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"

void closeR(int *p) { close(p[0]); }
void closeW(int *p) { close(p[1]); }

int
main(int argc, char *argv[])
{
  int p2c[2], c2p[2];
  pipe(p2c), pipe(c2p);

  if (fork()) {
    int tmp = 0;
    closeR(p2c);
    closeW(c2p);
    write(p2c[1], &tmp, sizeof(int));
    read(c2p[0], &tmp, sizeof(int));
    printf("%d: received pong\n", getpid());
    closeW(p2c);
    closeR(c2p);
  } else {
    int tmp = 1;
    closeW(p2c);
    closeR(c2p);
    read(p2c[0], &tmp, sizeof(int));
    printf("%d: received ping\n", getpid());
    write(c2p[1], &tmp, sizeof(int));
    closeR(p2c);
    closeW(c2p);
  }
  exit(0);
}

find

主要是練怎麼用file stat,以及認識到c處理string是多麼麻煩

#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
#include "kernel/fs.h"

char* getFilename(char *path)
{
  char *start = path;
  int len = strlen(path);
  path += len;
  for(;path != start && *path != '/';path--) ;
  if (*path == '/')
    path++;
  return path;
}

int isSubstring(char* s1, char* s2)
{
    int M = strlen(s1);
    int N = strlen(s2);

    /* A loop to slide pat[] one by one */
    for (int i = 0; i <= N - M; i++) {
        int j;
        for (j = 0; j < M; j++)
            if (s2[i + j] != s1[j])
                break;

        if (j == M)
            return 1;
    }

    return 0;
}

int isNotDots(char* name) {
  int len = strlen(name);
  return len >= 3 || (len == 2 && name[0] != '.' && name[1] != '.') || (len == 1 && name[0] != '.');
}

void
find(char *path, char *pat, int has)
{
  char buf[512], *p;
  int fd;
  struct dirent de;
  struct stat st;

  if((fd = open(path, 0)) < 0){
    fprintf(2, "ls: cannot open %s\n", path);
    return;
  }

  if(fstat(fd, &st) < 0){
    fprintf(2, "ls: cannot stat %s\n", path);
    close(fd);
    return;
  }

  switch(st.type){
  case T_FILE:
    if (has)
      printf("%s\n", path);
    break;
  case T_DIR:
    if(strlen(path) + 1 + DIRSIZ + 1 > sizeof buf){
      printf("ls: path too long\n");
      break;
    }
    strcpy(buf, path);
    p = buf+strlen(buf);
    *p++ = '/';
    while(read(fd, &de, sizeof(de)) == sizeof(de)){
      if(de.inum == 0)
        continue;
      memmove(p, de.name, DIRSIZ);
      p[DIRSIZ] = 0;
      if(stat(buf, &st) < 0){
        printf("ls: cannot stat %s\n", buf);
        continue;
      }
      char* filename = getFilename(buf);

      if (isNotDots(filename)) {
        find(buf, pat, has || isSubstring(pat, filename));
      }

    }
    break;
  }
  close(fd);
}

int
main(int argc, char *argv[])
{
  find(argv[1], argv[2], 0);
  exit(0);
}

xargs

因為我從沒用過xargs所以一開始寫根本不知道這要幹嘛

xargs就是讀stdout,用空格或斷行當成分隔,去invoke指令

不過這裡在讀的時候要一直loop,就算你知道test data基本上一次就讀的完

可以順便說說虛假喚醒的原因

  • 喚醒會把所有proc設定成可以跑(runnable)
  • scheduler只會挑出一個proc跑
  • 如果這個proc做一下改變,就直接被切走…
    • 但其他proc還是runnable!!
    • 這樣其他proc還是被wakeup的!!

虛假喚醒是來自preemptive schedule,所以只能在每次wakeup時確認需要的前提有沒有對,才繼續跑

#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"

char*
strncpy(char *s, const char *t, int n)
{
  char *os;

  os = s;
  while(n-- > 0 && (*s++ = *t++) != 0)
    ;
  while(n-- > 0)
    *s++ = 0;
  return os;
}

int
main(int argc, char *argv[])
{
  char buf[10][32];
  char tmp[32];
  char *cmd[10];
  for (int i=1;i<argc;i++)
     strncpy(buf[i-1], argv[i], strlen(argv[i]));

  while (read(0, tmp, 32) > 0) { // 虛假喚醒!!
    char *start = tmp;
    int end = argc-1;
    for(int len=strlen(tmp),i=0;i<len;i++)
      if (tmp[i] == ' ' || tmp[i] == '\n') {
        tmp[i] = 0;
        strcpy(buf[end++], start);
        start = tmp+i+1;
      }
    for (int i=0;i<end;i++)
      cmd[i] = (char*)&(buf[i]);
    if (fork())
      wait(0);
    else
      exec(buf[0], cmd);
  }

  exit(0);
}

primes

全部裡面最有趣的,也是考驗會不會用pipe

這裡的做法是,每個stage(go)

  • 取第一個數字作為這邊的質數
  • 剩下塞到新的pipe,產生下一個stage
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"

void put(int *p, int n) {
	write(p[1], &n, sizeof(int));
}

int get(int *p) {
	int ret;
	int state = read(p[0], &ret, sizeof(int));
	if (state <= 0)
		return state;
	else
		return ret;
}

void endWrite(int *p) {
	close(p[1]);
}

void endRead(int *p) {
	close(p[0]);
}

void go(int *p) {
	// WARN: get a item one time!!
	int b = get(p);
	if (b > 0) {
		printf("prime %d\n", b);
		int n = get(p);
		if (n > 0) {
			int pp[2];
			pipe(pp);
			if (fork() == 0) {
				endWrite(pp);
				go(pp);
			} else {
				endRead(pp);
				for(;n > 0;n=get(p))
					if (n % b != 0)
						put(pp,n);
				endWrite(pp);
				wait(0);
			}

		}
	}
	endRead(p);
}

int main() {
	int p[2];
	pipe(p);
	if (fork() == 0) {
		endWrite(p);
		go(p);
	}
	else {
		endRead(p);
		for (int n=2;n<36;n++)
			put(p,n);
		endWrite(p);
		wait(0);
	}
	exit(0);
}

ch2

Isolation

隔離是由下面兩個東西提供保證的

  • 硬體
    • 執行模式
  • OS
    • process (使用不同的stack)
    • 其他 (cgroup, 權限管理…)

先看執行模式

執行模式

RISC-V 有三種模式,CPU 可以執行指令:

  • 機器模式
  • 監督者(supervisor)模式
    • CPU 被允許執行特權指令:例如,啟用和禁用中斷,讀寫保存頁表地址的寄存器等
  • 用戶模式

CPU提供了一個特殊的指令(ecall),可以將 CPU 從用戶模式切換到監督模式,並在內核指定的入口處進入內核。

一個關鍵的設計問題是操作系統的哪一部分應該在監督者模式下運行。

  • 宏內核
    • 整個操作系統駐留在內核中,這樣所有系統調用的實現都在監督者模式下運行
  • 微內核
    • 減少在監督者模式下運行的操作系統代碼量,而在用戶模式下執行操作系統的大部分代碼

process

process 就是 一台電腦

  • 硬體
    • kernel syscall
  • CPU
    • concurrent mechnism
      • process state
  • mem
    • page table
      • stack
        • kernel stack (kstack)
        • user stack

透過assign不同的pagetable讓process只能看到與使用一部份的mem,這樣就算把自己的搞壞也沒關係

process的mem layout

xv6 只使用 39 位中的 38 位。因此,最大地址是 2^38-1 = 0x3fffffffff,也就是 MAXVA

在地址空間的頂端,xv6 保留了一頁,用於 trampoline 和映射進程trapframe 的頁,以便切換到內核

trace: init

在kernel load完後會call init去setup shell 這裡主要是看怎麼從kernel mode變成user mode

  1. loader 将 xv6 内核加载到物理地址 0x80000000 的内存中
    • 0x80000000 而不是 0x0,是因为地址范围 0x0-0x80000000 包含 I/O 设备。
  2. _entry处的指令设置了一个栈(stack0),这样xv6就可以运行C代码
    • 注意
      • stack0被宣告在start.c
      • riscv的stack是往下長的!!
        • 所以,entry.S做的事用一句話來說
          • sp = stack0+PGSIZE
  3. 跑到start
    • 這裡是machine mode
      • 設定切hyperviser mode
      • 關中斷
      • 關paging
      • 設定pc成main
      • 設定timer的東西 (timerinit)
      • 用mret跳去main,同時切成hyperviser mode
  4. 跑main
    • 設定各種設定
    • 最後跑userinit,跑kernel的第一個程式
  5. 在main跑userinit,就會去帶init
    • alloc proc之後設定一些基本訊息
    • 把跑init的binary (exec("/init")),copy到proc的記憶體中
      • binary的asm在user的initcode.S
        • 就是透過a7去打exec
        • exec會把記憶體換掉,所以變成init
          • init(init.c)做兩件事
            • fork: 開sh
            • main: 一直wait,zombie或是shell之類的proc
        • 同時之後init跑完就會變成user mode
          • 為什麼會變成user mode??
            • 看到exec
              • 先讀elf
              • 用uvmalloc設定pagetable
                • uvmalloc設定PTE時會代PTE_U (usermode記憶體)
            • 之後到syscall的流程 (usertrapret)
              • 設定user mode
              • 設定user pagetable
              • userret做ctx switch+trap的switch

trace: syscall

user mode的syscall

  • usys.pl
    • include syscall.h拿syscall編號
    • 會產生一段設定a7的asm
    • 這就是syscall

kernel mode的syscall

  • syscall.h
    • 這裡有所有syscall的編號
  • usys.pl的ecall觸發trap,切到kernel mode
    • trap之後會提,反正會到usertrap
    • 看mstatus,之後跑syscall
    • 根據num,跑對應的syscal
    • return code寫到a0,之後透過剩下的trap流程
      • 回到usertrap
      • 跑usertrapret(切pagetable與user mode),userret

user的參數怎麼pass到syscall的?

以argint為例

  • call argraw
    • argraw直接拿trapframe的value
    • 在此如果是addr也是這樣拿到addr
      • 但是之後要處理pagetable的copyin, copyout接手去複製資料

Lab System calls

trace

就是在proc上設定mask,之後只要syscall時就看mask決定要不要print

// ...
extern uint64 sys_trace(void);
extern uint64 sys_sysinfo(void);

static uint64 (*syscalls[])(void) = {
    // ...
    [SYS_trace]   sys_trace,
    [SYS_sysinfo]   sys_sysinfo,
};

static char* syscallnames[24] = {
  "",
  "fork",
  "exit",
  "wait",
  "pipe",
  "read",
  "kill",
  "exec",
  "fstat",
  "chdir",
  "dup",
  "getpid",
  "sbrk",
  "sleep",
  "uptime",
  "open",
  "write",
  "mknod",
  "unlink",
  "link",
  "mkdir",
  "close",
  "trace",
  "sysinfo",
};

void
syscall(void)
{
  // ...
  struct proc *p = myproc();
  // ...
  if(num > 0 && num < NELEM(syscalls) && syscalls[num]) {
    p->trapframe->a0 = syscalls[num]();
    if ((1 << num) & p->tracemask)
      printf("%d: syscall %s -> %d\n", p->pid, syscallnames[num], p->trapframe->a0); // 
  } else {
    // ...
  }
}

sysinfo

這裡的重點是

  • 數freemem
  • 數unused procs

數unused procs就是從proc表去數

int getAllocedProcsCount(void) {
  int ret = 0;
  for(struct proc *p = proc; p < &proc[NPROC]; p++)
    if (p->state != UNUSED)
      ret++;
  return ret;
}

數freemem從freelist去數(幸好是用page去分不然會很麻煩) 4096是page的大小(PGSIZE)

int getFreeMemAmount(void) {
  int ret = 0;
  for(struct run* ptr = kmem.freelist; ptr; ptr=ptr->next)
    ret += 4096;
  return ret;
}

剩下的問題是怎麼copy struct過去 所以我們需要copy addr過去user space的struct

uint64
sys_sysinfo(void)
{
  uint64 infoAddr; // user pointer to struct stat
  if(argaddr(0, &infoAddr) < 0)
    return -1;
  struct sysinfo info = {
    .freemem = getFreeMemAmount(),
    .nproc = getAllocedProcsCount(),
  };
  struct proc* p = myproc();
  if(copyout(p->pagetable, infoAddr, (char *)&info, sizeof(info)) < 0)
    return -1;
  return 0;
} 

ch4&5

trap

trap有3種

  • from user mode (syscall)
  • from device (device interrupt)
  • from cpu (exception)

trap通常的順序是

  • trap 迫使控制權轉移到內核
  • 內核保存寄存器和其他狀態,以便恢復執行
  • 內核執行適當的處理程序代碼(例如,系統調用實現或設備驅動程序)
  • 內核恢復保存的狀態,並從 trap 中返回
  • 代碼從原來的地方恢復。

Xv6 trap 處理分為四個階段

  • RISC-V CPU 採取的硬件行為
  • 為內核 C 代碼準備的彙編入口
  • 處理 trap 的 C 處理程序
  • 系統調用或設備驅動服務

RISC-V trap mechinism

重要的reg

stvec:內核在這裡寫下 trap 處理程序的地址;RISC-V 到這裡來處理 trap。 sepc:當 trap 發生時,RISC-V 會將程序計數器保存在這裡(因為 PC 會被 stvec 覆蓋)。 sret: 從 trap 中返回 scause:RISC -V 在這裡放了一個數字,描述了 trap 的原因。 sscratch:內核在這裡放置了一個值,這個值會方便 trap 恢復/儲存用戶上下文。 sstatus: 類似attr,SIE 位控制設備中斷是否被啟用,SPP 位表示 trap 是來自用戶模式還是監督者模式,並控制sret 返回到什麼模式

RISC-V 硬件對所有的 trap 類型(除定時器中斷外)進行以下操作

  • 如果該 trap 是設備中斷,且 sstatus SIE 位為 1
    • 通過清除 SIE 來禁用中斷
    • 複製 pc 到 sepc
    • 將當前模式(用戶或監督者)保存在 sstatus 的 SPP 位
    • 在 scause 設置該次 trap 的原因
    • 將模式轉換為監督者
    • 將 stvec 複製到 pc
    • 執行新的 pc

CPU 不會切換到內核頁表,不會切換到內核中的棧,也不會保存 pc 以外的任何寄存器!! 內核軟件必須執行這些任務!!

trace: trap from user space

  • 從proc的trampoline開始

    • 跑uservec (kernel mode,因為是中斷)
      • 保存狀態到trapframe
        • trapframe可以
          • 保存所有用户寄存器
          • 指向当前进程的内核栈
          • 当前 CPU 的 hartid
          • usertrap 的地址和内核页表的地址的指针
      • 換kernel pagetable (透過設定satp)
      • 跳usertrap (kernel mode)
        • 設定stvec成kernelvec
        • 保存pc
        • syscall或是device interrupt
  • usertrap完到usertrapret (kernel mode)

    • 設定成stvec,要到uservec
    • 把kernel資訊寫到trapframe
    • 設定pc (trap的重點!!!)
    • 設定user pagetable
    • 設定user mode
    • 跳到userret (user mode)
      • 把trapframe載回去
      • return 到原本的位置

trace: exec syscall

  • 用戶代碼將 exec 的參數放在寄存器 a0 和 a1 中,並將系統調用號放在 a7 中
  • 系統調用號與函數指針表 syscalls 數組(kernel/syscall.c:108)中的項匹配 (from trapframe的a7)
  • ecall 指令進入內核,執行uservec、usertrap,然後執行 syscall
  • 當系統調用函數返回時,syscall 將其返回值記錄在 p->trapframe->a0 中
如果有pointer?

透過kernel function去load 使用 fetchstr 從用戶空間中檢索字符串文件名參數,fetchstr 調用 copyinstr 來做這些困難的工作

trap from kernel space

kernel的trap因為在kernel所以不用換pagetable、stvec 同時因為大家都有自己的kstack,所以可以把registrer存在stack上

  • kernelvec
    • 保存狀態到kstack
    • 跳到kerneltrap (會回來kernelvec)
      • 保存pc, sstatus, scause
        • pc很正常,但sstatus,scause!?
          • 如果是timer interupt會yield
          • 等回來,會需要原本的sstatus,scause
      • 做該做的事
        • timer的preemptive切換在這裡實現
      • 回復sstatus,scause之後return
    • 從kstack回復狀態

trap from device

許多設備驅動程序在兩個 context 中執行代碼: 上半部分(top half)在進程的內核線程中運行 下半部分(bottom half)在中斷時執行

上半部分是通過系統調用,如希望執行 I/O 的read 和 write。 這段代碼可能會要求硬件開始一個操作(比如要求磁盤讀取一個塊);然後代碼等待操作完成。 最終設備完成操作並引發一個中斷。 驅動程序的中斷處理程序,作為下半部分,推算出什麼操作已經完成,如果合適的話,喚醒一個等待該操作的進程,並告訴硬件執行下一個操作。

trace: Console input

UART 硬件在軟件看來是一組內存映射的控制寄存器 (不用port操作啦)

當 UART 接收到一個字節的輸入時,就產生一個接收中斷,當 UART 每次完成發送一個字節的輸出時產生一個傳輸完成(transmit complete)中斷(kernel/uart.c:53)。

trap 處理程序調用 devintr(kernel/trap.c:177),它查看 RISC-V 的 scause 寄存器,發現中斷來自一個外部設備。 然後它向一個叫做 PLIC的硬件單元詢問哪個設備中斷了(kernel/trap.c:186)。 如果是 UART,devintr 調用 uartintr。

uartintr (kernel/uart.c:180) 從 UART 硬件中讀取在等待的輸入字符,並將它們交給consoleintr (kernel/console.c:138); 它不會等待輸入字符,因為以後的輸入會引發一個新的中斷。 consoleintr 的工作是將中輸入字符積累 cons.buf 中,直到有一行字符

一旦被喚醒,consoleread 將會注意到 cons.buf 中的完整行,並將其將其複製到用戶空間,並返回(通過系統調用)到用戶空間。

trace: Console output

write 系統調用最終會到達 uartputc(kernel/uart.c:87)。 設備驅動維護了一個輸出緩衝區(uart_tx_buf),uartputc 將每個字符追加到緩衝區調用 uartstart 來啟動設備發送(如果還沒有的話),然後返回

每次 UART 發送完成一個字節,它都會產生一個中斷。 uartintr 調用 uartstart,uartintr檢查設備是否真的發送完畢,並將下一個緩衝輸出字符交給設備,每當 UART 發送完一個字節,就會產生一個中斷

Timer interrupts

RISC-V 要求在機器模式下處理定時器中斷,而不是監督者模式。 因此,xv6 對定時器中斷的處理與上面談到的 trap 機製完全分離了。

所以都在start中設定

  • 對 CLINT 硬件(core-local interruptor)進行編程,使其每隔一定時間產生一次中斷
  • 設置一個類似於 trapframe 的 scratch 區域,幫助定時器中斷處理程序保存寄存器和 CLINT 寄存器的地址
    • 所以前面trap要保留scratch的內容
  • 將 mtvec 設置為 timervec,啟用定時器中斷
    • 中斷之後由clockintr處理 (tick++)

interrupt還是要處理concurent

內核代碼需要注意它可能會被暫停(由於定時器中斷),然後在不同的 CPU 上恢復

等等,一直發trap?

UART 驅動器通過讀取 UART 控制寄存器,一次檢索一個字節的數據 這種模式被稱為編程 I/O,因為軟件在驅動數據移動。

程序化 I/O 簡單,但速度太慢,無法在高數據速率下使用。

需要高速移動大量數據的設備通常使用直接內存訪問(DMA) DMA 設備硬件直接將傳入數據寫入 RAM,並從 RAM 中讀取傳出數據

當設備在不可預知的時間需要關注時,中斷是很有用的,而且不會太頻繁。 但中斷對 CPU的開銷很大。 因此,高速設備,如網絡和磁盤控制器,使用了減少對中斷需求的技巧。

其中一個技巧是對整批傳入或傳出的請求提出一個單一的中斷。 另一個技巧是讓驅動程序完全禁用中斷,並定期檢查設備是否需要關注。 這種技術稱為輪詢(polling)。 如果設備執行操作的速度非常快,輪詢是有意義的 但如果設備大部分時間處於空閒狀態,則會浪費 CPU 時間 一些驅動程序會根據當前設備的負載情況,在輪詢和中斷之間動態切換

Lab Traps

backtrace

defs.h加

void            backtrace(void);

接著hint有提到sp與fp,所以就直接用吧 但這裡用struct讓code好看一點

注意到riscv是小頭,所以mem addr小的會被放到struct最前面 這段我是加在printf.c

struct stk_frame
{
  void * prev_frame_plus_16;
  uint64 ret_addr;
};

static inline uint64
r_fp()
{
  uint64 x;
  asm volatile("mv %0, s0" : "=r" (x) );
  return x;
}

/*
riscv是小頭
所以addr越小,在c struct會被往上面放
stack是往下長 (addr-8)
所以寫struct要把下面的往struct的上面放 (stack倒著放)
*/

void backtrace(void) {
  struct stk_frame* now = (struct stk_frame*)(r_fp()-16);
  uint64 start = PGROUNDDOWN(now->ret_addr);
  for (struct stk_frame* now = (struct stk_frame*)(r_fp()-16); now->ret_addr > start; now=now->prev_frame_plus_16-16)
    printf("%p\n", now->ret_addr);
} 

最後就直接call

uint64
sys_sleep(void)
{
  int n;
  uint ticks0;

  backtrace();
  if(argint(0, &n) < 0)
    return -1;
// ...
}
fp & sp

from hint sp就是目前stack的top fp就是保存caller訊息的addr

caller reg: caller要存,換言之,在call完function後可能會被破壞掉 callee reg: callee要還原,換言之,在call完function後他們的值還是對的

alarm

如果timer動了就call一下callback,透過兩個syscall,sigalarm與sigreturn 但要怎麼到callback去,是要在kernel mode跑??

timer interupt最後會回到user mode,所以只要讓他不要回到原本的位置就好!! 這手法我們看過了,usertrapret與usertrap與kerneltrap都是透過改pc完成的

所以我們也改pc到callback,但是執行時的狀態怎麼辦,要怎麼回去原本的狀態? 再多一個trapframe存原本的trapframe,在alarmreturn把原本的frame設回去

sysproc.c

uint64 sys_sigreturn(void) {
  struct proc * p = myproc();
  p->acc = 0;
  memmove(p->trapframe, p->trapframe2, sizeof(struct trapframe));
  return 0;
}

uint64 sys_sigalarm(void) {
  struct proc* p = myproc();
  if(argint(0, &p->cnt) < 0)
    return -1;
  if(argaddr(1, &p->cb) < 0)
    return -1;
  return 0;
}

trap.c

// ...
  if(which_dev == 2) {
    if (p->cnt) {
      if (++p->acc == p->cnt) {
        memmove(p->trapframe2, p->trapframe, sizeof(struct trapframe));
        p->trapframe->epc = p->cb;
      }
    }
    yield();
  }
// ...

Lab network driver

在2019的版本是要實現stack與driver的,所以很累,但2020就不用實現stack

driver

driver的重點是怎麼與device溝通

iface都是用ring buffer去存資料

xx_ring就是device的buffer的狀態,然後他們是array 那是要取哪一個?? write: 取E1000_TDT,這是接下去dirver要寫的位置 read: 取E1000_RDT,這是iface已經讀完的位置

xx_mbuf是ring buffer對應到的memory

所以 write: 把tx_ring[regs[E1000_TDT]]的狀態設定好,把addr指向第一個位置 read: 先拉regs[E1000_TDT]+1的mbuf,之後用新的mbuf蓋掉原本的

int inc(int i, int len) { return (i+1) % len; }

int
e1000_transmit(struct mbuf *m)
{
  acquire(&e1000_lock);
  int i = regs[E1000_TDT], ret;
  if ((tx_ring[i].status & E1000_TXD_STAT_DD) == 0) {
    printf("not ready for trasmit");
    ret = -1;
  } else {
    // free previous mbuf
    if (tx_mbufs[i])
      mbuffree(tx_mbufs[i]);
    tx_ring[i].addr = (uint64)m->head;
    tx_ring[i].length = m->len;
    tx_ring[i].cmd = E1000_TXD_CMD_EOP | E1000_TXD_CMD_RS;
    tx_mbufs[i] = m;
    regs[E1000_TDT] = inc(i, TX_RING_SIZE);
    ret = 0;
  }
  release(&e1000_lock);
  return ret;
}

// allocate mbuf for iface
static void
e1000_recv(void)
{
  for (int i = inc(regs[E1000_RDT], RX_RING_SIZE);rx_ring[i].status & E1000_RXD_STAT_DD;i = inc(regs[E1000_RDT], RX_RING_SIZE)) {
    acquire(&e1000_lock);
    struct mbuf* pkt = *(rx_mbufs+i);
    mbufput(pkt, rx_ring[i].length);
    rx_mbufs[i] = mbufalloc(0);
    // 從哪邊塞
    rx_ring[i].addr = (uint64)rx_mbufs[i]->head;
    rx_ring[i].status = 0;
    regs[E1000_RDT] = i;
    release(&e1000_lock);
    net_rx(pkt);
  } 
}

network stack

雖然說不用實現一個network stack,但我們可以trace看看

rx
  • net_rx
    • 拉eth_header
    • 判斷type決定要去哪
      • ntohs去轉數字
  • net_rx_ip
    • 拉ip_header
    • 各種判斷與check
      • cksum
      • routing
        • 這裡只有看是不是給我們,不是就丟了
    • 算udp長度
    • 送去udp
  • net_rx_udp
    • 拉udp_header
    • 各種判斷與check
    • 把sip, sport, dport抓出來,送到sockrecvudp
  • sockrecvudp
    • 找到對的socket
    • 把pkt塞到socket的queue
tx
  • sockwrite
    • alloc mbuf
    • copy data
    • 送到net_tx_udp
  • net_tx_udp
    • 把sip, sport, dport轉成network order
    • 設定udp header,並加在mbuf上
    • 送到net_tx_ip
  • net_tx_ip
    • 與net_tx_udp很像,轉資料,加在mbuf上
    • 送到net_tx_eth
  • 不存在的routing
    • 一般來說從ip到eth或是eth到ip之間要過routing
    • 決定要繼續往上還是直接轉出去
      • 以linux的netfilter為例
  • net_tx_eth
    • 塞ethaddr,加在mbuf上
      • 這裡是直接boardcast,所以迴避了arp
    • call e1000_transmit

ch3

來了,最難的部分,撐過去就會有全新的方式看待c了!!

為什麼會難?

  1. 很難debug,一個是要知道結構,也要只到這個數字對應到什麼,還有concurrent要處理
  2. 都是uint64
  3. 之後會在va, pte, pa一直轉來轉去

kernel mem layout

  • 当内核通过高地址映射使用 stack 时,它们也可以通过直接映射的地址被内核访问
    • 内核使用“直接映射”RAM 和内存映射设备寄存器,也就是在虚拟地址上映射硬件资源,这些地址与物理地址相等。
      • 例如,内核本身在虚拟地址空间和物理内存中的位置都是KERNBASE=0x80000000。直接映射简化了读/写物理内存的内核代码。

pagetable

page table就是把virtual address(va),丟到table換出physical address(pa)

硬體上是分成3層,也因為多了一層抽象,所以可以多點attr

像後面就是看page有沒有PTE_V(Page Table Entry, PTE),決定這個page是不是free

要告訴硬件使用頁表,內核必須將根頁表頁的物理地址寫入 satp 寄存器中

pte, pagetable都是存在pa中!!

如何map & 如何把va換成pa

pagetable就是hashtable,所以要先知道key,value到底要什麼?

key: va,在程式中跑的數字;pte,可以換出pagetable或是pa的數字,可以把pte當成page開頭的pa(見walk) value: pa,真實mem的addr

trace: free mem怎麼產生的

把kernel的memory layout打開來

中間的free memory就是我們需要的東西

接著就是怎麼讓free memory可以被分配,要先切塊

  • kinit
    • freerange
      • 從end到PHYSTOP跑kfree
        • kfree就是加linked list

如何把va換成pa: walk

就是模擬在table跳的過程,一層一層換pte,到最後就可以用kalloc拿pa,利用pa設定對應的pte 之後回傳對應的pa

那怎麼從va拿pte? 再看回去這張圖

注意到va最右手邊就是12個offset,之後看到pte的右手邊是10位flag,所以轉成pte就是把12位拿掉再把10位補回去

#define PA2PTE(pa) ((((uint64)pa) >> 12) << 10)
#define PTE2PA(pte) (((pte) >> 10) << 12)
#define PTE_FLAGS(pte) ((pte) & 0x3FF)

剩下就是walk三層table了

// extract the three 9-bit page table indices from a virtual address.
#define PXMASK          0x1FF // 9 bits
#define PXSHIFT(level)  (PGSHIFT+(9*(level)))
#define PX(level, va) ((((uint64) (va)) >> PXSHIFT(level)) & PXMASK)

// Return the address of the PTE in page table pagetable
// that corresponds to virtual address va.  If alloc!=0,
// create any required page-table pages.
//
// The risc-v Sv39 scheme has three levels of page-table
// pages. A page-table page contains 512 64-bit PTEs.
// A 64-bit virtual address is split into five fields:
//   39..63 -- must be zero.
//   30..38 -- 9 bits of level-2 index.
//   21..29 -- 9 bits of level-1 index.
//   12..20 -- 9 bits of level-0 index.
//    0..11 -- 12 bits of byte offset within the page.
pte_t *
walk(pagetable_t pagetable, uint64 va, int alloc)
{
  if(va >= MAXVA)
    panic("walk");

  for(int level = 2; level > 0; level--) {
    pte_t *pte = &pagetable[PX(level, va)];
    if(*pte & PTE_V) {
      pagetable = (pagetable_t)PTE2PA(*pte);
    } else {
      if(!alloc || (pagetable = (pde_t*)kalloc()) == 0)
        return 0;
      memset(pagetable, 0, PGSIZE);
      *pte = PA2PTE(pagetable) | PTE_V;
    }
  }
  return &pagetable[PX(0, va)];
}

如何map: mappages

mappages就是walk很多次,一次一個page

如果完成map就可以用c的*做操作!! 十分神奇,前面還是當成數字操作,之後就可以直接dereference

trace: 怎麼做mem alloc

mem alloc,就是

  1. kalloc拿一塊page,拿到一個pa
  2. mappages在pagetable中設定va到pa

trace: 怎麼free mem/pagetable

free mem就是

  1. uvmunmap
    • 會walk拉出pa
    • 檢查有沒有被map過(有PTE_V)
    • 如果有要free,就kfree

free pagetable比較麻煩

  • uvmfree
    • uvmunmap把所有pa去掉
    • walkfree把pte去掉

trace: how to create kernel page table

  • kvminit設定kernel_pagetable
    • 跑kvmmake會生出kernel pagetable
      • 先kalloc一塊放pagetable
      • 之後照layout設定
  • kvminithart
    • 設定satp
  • how to access a page table:
    • pagetable_t,它實際上是一個指向 RISC-V 根頁表頁的指針 (pa)

process mem layout

當一個進程要求 xv6 提供更多的用戶內存時,xv6 首先使用 kalloc 來分配物理頁,然後將指向新物理頁的 PTE 添加到進程的頁表中。

xv6 使用 PTE_V 來清除不使用的 PTE

trampoline是負責跳到hypervisor mode的code trapframe是在跳之前保存process狀態的地方

trace: sbrk

回傳目前的終點(sz),之後算與原本size的差,之後調用uvmalloc 或 uvmdealloc縮放自己的大小

trace: exec

  1. open binary exe (namei)
  2. parse ELF
  3. 從proc_pagetable分配一個page,之後用uvmalloc為剩下的proc配置page

exec配出來的mem layout可以看process mem layout的圖

Before labs

在做lab之前先看看要怎麼為某個va做map,會做兩件事

  1. 拿一塊page
  2. 把va指(map)過去
    • 這裡的va不一定是剛好在page的起點上!!
    • 記得,所有mem都要以page為單位

故要先介紹兩個macro,因為之後很常用到

  • PGROUNDUP: page的終點
    • usage: exec在alloc elf的執行檔後要alloc stack,就是先取PGROUNDUP,之後stack從PGROUNDUP開始alloc
  • PGROUNDDOWN: page的起點
    • usage: mappages在一開始就先對傳進來的va做PGROUNDDOWN,之後才開始alloc

Lab Page tables

vmprint

小試身手,抄freewalk,走過每個pte即可

void vmprint_dfs(pagetable_t pagetable, int dep) {
  if (*pagetable < MAXVA)
    for(int i = 0; i < 512; i++){
      pte_t pte = pagetable[i];
      if((pte & PTE_V) && pte < MAXVA){
        uint64 child = PTE2PA(pte);
        for (int j=0;j<dep;j++)
          printf(".. ");
        printf("..%d: ptr %p pa %p\n", i, pte, child);
        vmprint_dfs((pagetable_t)child, dep+1);
      }
    }
}

void vmprint(pagetable_t pagetable) {
  printf("page table %p\n", pagetable);
  vmprint_dfs(pagetable, 0);
}

new copyin, copyinstr

現在想把user mode的資料加到kernel mode的表,這樣就不用copy來copy去

先要有產生kernel pagetable的函數,並在struct proc中多加一個kernel pagetable

void kpgtinit(pagetable_t t) {
  memset(t, 0, PGSIZE);

  // uart registers
  if (mappages(t, UART0, PGSIZE, UART0, PTE_R | PTE_W) < 0)
    panic("kpgtinit: uart0\n");

  // virtio mmio disk interface
  if (mappages(t, VIRTIO0, PGSIZE, VIRTIO0, PTE_R | PTE_W) < 0)
    panic("kpgtinit: virtio0\n");

  // CLINT
  if (mappages(t, CLINT, 0x10000, CLINT, PTE_R | PTE_W) < 0)
    panic("kpgtinit: clint\n");

  // PLIC
  if (mappages(t, PLIC, 0x400000, PLIC, PTE_R | PTE_W) < 0)
    panic("kpgtinit: plic\n");

  // map kernel text executable and read-only.
  if (mappages(t, KERNBASE, (uint64)etext-KERNBASE, KERNBASE, PTE_R | PTE_X) < 0)
    panic("kpgtinit: kernel base\n");

  // map kernel data and the physical RAM we'll make use of.
  if (mappages(t, (uint64)etext, PHYSTOP-(uint64)etext, (uint64)etext, PTE_R | PTE_W) < 0)
    panic("kpgtinit: kernel data\n");

  // map the trampoline for trap entry/exit to
  // the highest virtual address in the kernel.
  if (mappages(t, TRAMPOLINE, PGSIZE, (uint64)trampoline, PTE_R | PTE_X) < 0)
    panic("kpgtinit: trampoline\n");
}
void switchPGT(pagetable_t t) {
  w_satp(MAKE_SATP(t));
  sfence_vma();
}

因為每個process都有kernel table,所以可以把kstack分到每個kernel pagetable去,把procinit的kstack拿掉

static struct proc*
allocproc(void)
{
  // ...

  p->kpagetable = (pagetable_t) kalloc();
  kpgtinit(p->kpagetable);

  char *pa = kalloc();
  if(pa == 0)
    panic("kalloc");
  uint64 va = KSTACK(0);
  mappages(p->kpagetable, va, PGSIZE, (uint64)pa, PTE_R | PTE_W);
  p->kstack = va;

  // ...
}

所以現在每個process都有一個kernel pagetable,需要的時候會把user mode pagetable的map到在kernel pagetable也map一下

void
includeInto(pagetable_t pagetable, pagetable_t kpagetable, uint64 oldsz, uint64 newsz)
{
  pte_t *pte_from, *pte_to;
  uint64 a, pa;
  uint flags;

  if (newsz < oldsz)
    return;

  oldsz = PGROUNDUP(oldsz);
  for (a = oldsz; a < newsz; a += PGSIZE)
  {
    if ((pte_from = walk(pagetable, a, 0)) == 0)
      panic("includeInto: pte should exist");
    if ((pte_to = walk(kpagetable, a, 1)) == 0)
      panic("includeInto: walk fails");
    pa = PTE2PA(*pte_from);
    flags = (PTE_FLAGS(*pte_from) & (~PTE_U));
    *pte_to = PA2PTE(pa) | flags;
  }
}

需要的時候? 就是程式在kernel mode中時動到或是需要user mode的資料時

  • userinit
  • growproc
    • 這裡要看kernel的layout,PILC上面的其實不能用,因為已經被map了
  • fork
  • exec
void
userinit(void)
{
  // ...
  includeInto(p->pagetable, p->kpagetable, 0, p->sz);
  // prepare for the very first "return" from kernel to user.
  p->trapframe->epc = 0;      // user program counter
  // ...
}
// Grow or shrink user memory by n bytes.
// Return 0 on success, -1 on failure.
int
growproc(int n)
{
  // ...
  if(n > 0){
    if (PGROUNDUP(sz + n) >= PLIC)
      return -1;
    // ...
    includeInto(p->pagetable, p->kpagetable, sz-n, sz);
  } else if(n < 0){
    sz = uvmdealloc(p->pagetable, sz, sz + n);
  }
  p->sz = sz;
  return 0;
}
// Create a new process, copying the parent.
// Sets up child kernel stack to return as if from fork() system call.
int
fork(void)
{
    // ...
  includeInto(np->pagetable, np->kpagetable, 0, np->sz);
  safestrcpy(np->name, p->name, sizeof(p->name));

  pid = np->pid;
  np->state = RUNNABLE;
  release(&np->lock);
  return pid;
}

freeproc也把kernel pagetable也free掉,但是不能把kernel的項目的pa給free掉

void
freewalk_keepleaf(pagetable_t pagetable)
{
  // there are 2^9 = 512 PTEs in a page table.
  for(int i = 0; i < 512; i++){
    pte_t pte = pagetable[i];
    if((pte & PTE_V)) {
      pagetable[i] = 0;
      if((pte & (PTE_R|PTE_W|PTE_X)) == 0){
        // this PTE points to a lower-level page table.
        uint64 child = PTE2PA(pte);
        freewalk_keepleaf((pagetable_t)child);
      }
    }

  }
  kfree((void*)pagetable);
}

void 
proc_freekpt(pagetable_t pagetable)
{
  // there are 2^9 = 512 PTEs in a page table.
  for(int i = 0; i < 512; i++){
    pte_t pte = pagetable[i];
    if((pte & PTE_V)){
      pagetable[i] = 0;
      if ((pte & (PTE_R|PTE_W|PTE_X)) == 0)
      {
        uint64 child = PTE2PA(pte);
        proc_freekpt((pagetable_t)child);
      }
    } else if(pte & PTE_V){
      panic("proc free kpt: leaf");
    }
  }
  kfree((void*)pagetable);
}

static void
freeproc(struct proc *p)
{
  // ...
  if(p->pagetable)
    proc_freepagetable(p->pagetable, p->sz);
  if (p->kstack)
  {
    uvmunmap(p->kpagetable, p->kstack, 1,1);
    p->kstack = 0;
  }
  if(p->kpagetable)
    freewalk_keepleaf(p->kpagetable);
  p->kpagetable = 0;
  // ...
}

最後就可以切到新的copyin, copyinstr了

Lab Lazy allocation

在sbrk不分配mem

uint64
sys_sbrk(void)
{
  int addr;
  int n;
  if(argint(0, &n) < 0)
    return -1;
  addr = myproc()->sz;
   if(n < 0)
     growproc(n);
   else
     myproc()->sz += n;
  return addr; // !!!
}

在page fault時做分配

int do_lazy(uint64 addr) {
  struct proc* p = myproc();
  int stkOverFlow = (addr < p->trapframe->sp);
  // page-faults on a virtual memory address higher than any allocated with sbrk()
  // this should be >= not > !!!
  int addrOutOfBound = (addr >= p->sz);
  void *mem;
  if (stkOverFlow)
    ;//printf("lazy: stack overflow\n");
  else if(addrOutOfBound)
    ;//printf("lazy: addr over of bound\n");
  else if((mem = kalloc()) == 0)
    ;//printf("out of pa\n");
  else {
    memset(mem, 0, PGSIZE);
    if (mappages(p->pagetable, PGROUNDDOWN(addr), PGSIZE, (uint64)mem, PTE_W|PTE_X|PTE_R|PTE_U) < 0)// ???
      kfree(mem);
    else 
      return 0;
  }
  return -1;
}

void
usertrap(void)
{
  // ...
  if(r_scause() == 8){
    // ...
    intr_on();

    syscall();
  } else if(r_scause() == 13 || r_scause() == 15) {
     if (do_lazy(r_stval()) < 0)
      p->killed = 1;
  } else if((which_dev = devintr()) != 0){
    // ok
  }
  // ...
}

剩下要在走訪pte時忽略沒有PTE_V的page,這邊就看hint就知道要改哪

Lab Copy on-write

這裡會牽涉到reference counter,導致要處理concurrent!! 如果沒有處理好,連怎麼出事的都不知道

先refcnt,代表有多少process有map到這個page

struct __refcnt{
  struct spinlock lock;
  uint counter[(PHYSTOP - KERNBASE) / PGSIZE];
};

struct __refcnt refcnt;

void refcnt_lock() {
  acquire(&refcnt.lock);
}

void refcnt_unlock() {
  release(&refcnt.lock);
}

void refcnt_create() {
  initlock(&refcnt.lock, "refcnt");
  refcnt_lock();
  for(int i=0,goal=(PHYSTOP - KERNBASE) / PGSIZE;i<goal;i++)
    refcnt.counter[i] = 0;
  refcnt_unlock();
}

inline
uint64
refcnt_index(uint64 pa){
  return (pa - KERNBASE) / PGSIZE;
}

void refcnt_set(uint64 pa, int n) {
  refcnt.counter[refcnt_index(pa)] = n;
}

inline
uint
refcnt_get(uint64 pa){
  return refcnt.counter[refcnt_index(pa)];
}

void
refcnt_incr(uint64 pa){
  refcnt.counter[refcnt_index(pa)]++;
}

void
refcnt_desc(uint64 pa){
  if (refcnt.counter[refcnt_index(pa)] > 0)
    refcnt.counter[refcnt_index(pa)]--;
  else
    panic("wtf");
}

之後page要會fork 兩個case

  1. refcnt大於1: 產生新的page,原本的page的refcnt減1
  2. refcnt等於1: 直接拿去用
int page_fork(uint64 va) {
  int ret;
  pagetable_t pgt = myproc()->pagetable;
  va = PGROUNDDOWN(va);
  pte_t * pte = walk(pgt, va, 0);
  uint64 pa = PTE2PA(*pte);
  uint flags = PTE_FLAGS(*pte);
  if ((*pte) & PTE_COW) {
    refcnt_lock(); // MUST BE DONE TOGETHER!!
    if (refcnt_get(pa) > 1) {
      void *mem = kalloc_cow();
      if (mem != 0) {
        memmove(mem, (char*)pa, PGSIZE);
        if(mappages(pgt, va, PGSIZE, (uint64)mem, (flags & (~PTE_COW)) | PTE_W) != 0)
          ret = -1, kfree(mem);
        else
          ret = 0, refcnt_desc(pa);
      } else
        ret = -3;
    } else
      ret = 0, *pte = (*pte & ~PTE_COW) | PTE_W;
    refcnt_unlock();
  } else 
    ret = -2;
  return ret;
}

與之相對,kfree只有在refcnt小於等於1時才free,其他都是decrease refcnt

void
kfree(void *pa)
{
  struct run *r;

  refcnt_lock(); //premise: counter == 0  <=> this page is in freelist
  if (refcnt_get((uint64)pa) <= 1) {
    if(((uint64)pa % PGSIZE) != 0 || (char*)pa < end || (uint64)pa >= PHYSTOP)
        panic("kfree");

    // Fill with junk to catch dangling refs.
    memset(pa, 1, PGSIZE);
    refcnt_set((uint64)pa, 0);
    r = (struct run*)pa;

    acquire(&kmem.lock);
    r->next = kmem.freelist;
    kmem.freelist = r;
    release(&kmem.lock);
  } else
    refcnt_desc((uint64)pa);
  refcnt_unlock();
}

另外,從kernel mode複製到user mode也要fork page 如果都指向同一個page要分開

int
uvmcopy(pagetable_t old, pagetable_t new, uint64 sz)
{
  // 從copy變成map
  // ...

  for(i = 0; i < sz; i += PGSIZE){
    // ...
    *pte = ((*pte) & (~PTE_W)) | PTE_COW;
    if(mappages(new, i, PGSIZE, (uint64)pa, (flags & (~PTE_W)) | PTE_COW) != 0)
      goto err;
    else {
      refcnt_lock();
      refcnt_incr(pa);
      refcnt_unlock();
    }
  }
  return 0;
 err:
  uvmunmap(new, 0, i / PGSIZE, 1);
  return -1;
}

int
copyout(pagetable_t pagetable, uint64 dstva, char *src, uint64 len)
{
  uint64 n, va0, pa0;

  while(len > 0){
    // ...
    // 如果都指向同一個page要分開
    pte_t* pte = walk(pagetable, va0, 0);
    if (pte && (*pte & PTE_COW))
      if (page_fork(va0) != 0)
        return -1;
    // ...
  }
}

之後把剩下的線串完

void
usertrap(void) {
    // ...
  } else if((r_scause() == 15)) {
    if (page_fork(r_stval()))
      p->killed = 1;
  } else {
    // ...
}

這邊有一個要注意的點 refcnt是跟著page走,所以直到page與refcnt設定好之前任何人都不該動refcnt 這導致kalloc與kalloc_cow的產生,因為refcnt會鎖,但是做page_fork時會動到兩個page,所以kalloc_cow不鎖,交給page_fork鎖

void *
kalloc(void)
{
  // ...
  if(r) {
    refcnt_lock();
    refcnt_incr((uint64)r);
    refcnt_unlock();
    //printf("init: %p %d\n", r, refcnt_get((uint64)r));
    memset((char*)r, 5, PGSIZE); // fill with junk
  }

  return (void*)r;
}

void *
kalloc_cow(void)
{
  // ...
  if(r) {
    refcnt_set((uint64)r, 1);
    //printf("init: %p %d\n", r, refcnt_get((uint64)r));
    memset((char*)r, 5, PGSIZE); // fill with junk
  }

  return (void*)r;
}

最後就像,lazy alloc做的一樣,要處理走訪pte時忽略沒有PTE_V的page

lock心得 0. 上鎖

  1. 先存狀態
  2. 都用同一個狀態延伸
  3. 把所有動作用同一個鎖包 (這會導致在不同branch要一直release)
  4. 確保假設被打破時可以馬上停下程式 (ex: panic(“wtf”))

Lab mmap

現在user可以自己設定自己的addr了!!

Q: 這樣怎麼區分user設定的資料與程式設定的(原本活在pagetable中的)資料? A: 多一個vma去trace

Q: 怎麼分配位置? A: 這隨便,這裡從TRAPFRAME之後開始

Q: 如果alloc很多塊,卻只free其中幾塊,我們還有方法再利用那些mem嗎? A: 這要做compact,但我懶,沒做test會過

先加vma,與sbrk很像,用vma_end紀錄最後的位置 記住,riscv是往addr小的地方開始填資料

struct entry {
  uint64 va_end;
  int size;
  int prot;
  int flag;
  struct file* f;
};

#define VMA_SIZE 16
#define VMA_BASE (TRAPFRAME - PGSIZE)

struct proc {
    // ...
    struct entry vma[VMA_SIZE];
    uint64 vma_end;
};

之後要可以alloc vma,與get vma

struct entry* allocvma() {
  struct proc *p = myproc();
  struct entry* ret = 0;
  for (int i = 0;i<VMA_SIZE;i++) {
    if (p->vma[i].size == 0) {
      ret = &(p->vma[i]);
      break;
    }
  }
  return ret;
}

struct entry* getvma(uint64 addr) {
  int i;
  struct proc *p = myproc();
  for (i = 0;i<VMA_SIZE;i++)
    if (p->vma[i].size > 0 && p->vma[i].va_end > addr && addr >= p->vma[i].va_end-p->vma[i].size)
      break;
  return i < VMA_SIZE ? &(p->vma[i]) : 0;
}

把mmap, munmap加進去 這裡做lazy,只有在read/write才map與讀檔案

uint64
sys_mmap(void)
{
  struct file* f;
  uint64 ret = -1;
  int size, prot, flags, fd;
  struct proc *p = myproc();
  int goodargs = (argint(1, &size) < 0 || argint(2, &prot) < 0 || argint(3, &flags) < 0 || argfd(4, &fd, &f) < 0);
  if(goodargs || (!f->writable && (prot & PROT_WRITE) && (flags & MAP_SHARED)))
    return ret;
  acquire(&p->lock);
  struct entry *vma = allocvma();
  if (vma) {
    int pte_prot = 0;
    if (prot & PROT_READ)
      pte_prot |= PTE_R;
    if (prot & PROT_WRITE)
      pte_prot |= PTE_W;
    filedup(f);
    vma->va_end = p->vma_end; // start在小 end在大 mmap回傳的addr是小的!!
    ret = vma->va_end-size;
    vma->size = size;
    vma->f = f;
    vma->prot = pte_prot;
    vma->flag = flags;
    p->vma_end -= size;
  }
  release(&p->lock);
  return ret;
}

uint64 do_mummap(uint64 addr, int len) {
  struct proc* p = myproc();
  struct entry *vma = getvma(addr);
  if (!vma || addr + len > vma->va_end) {
    return -1;
  }
  //printf("??: %d start:%p va:%p va_end:%p end:%p\n", vma->size, vma->va_end-vma->size, addr, addr+len, vma->va_end);
  for (uint64 va=addr,end=addr+len;va < end;va+=PGSIZE) {
      if (walkaddr(p->pagetable, va)) {
        if (vma->flag & MAP_SHARED)
          filewrite(vma->f, va, PGSIZE);
        uvmunmap(p->pagetable, va, 1, 1);
      }   
  }

  if (addr == vma->va_end-vma->size && addr+len == vma->va_end) {
    vma->size = 0;
    //fileclose(vma->f);
    vma->f->ref--;
  } else {
    vma->size -= len;
    if (addr+len == vma->va_end)
      vma->va_end -= len;
  }

  return 0;
}

uint64
sys_munmap(void)
{
  uint64 addr;
  int len;

  if (argaddr(0, &addr) < 0 || argint(1, &len) < 0)
    return -1;
  return do_mummap(addr, len);
}

在page fault才map與讀檔案

int do_mmap(uint64 addr) {
  struct proc* p = myproc();
  uint64 base = PGROUNDDOWN(addr);
  void *mem;
  if((mem = kalloc()) == 0)
    printf("out of pa\n");
  else {
    struct entry* vma = getvma(addr);
    if (vma) {
      memset(mem, 0, PGSIZE);
      if (mappages(p->pagetable, base, PGSIZE, (uint64)mem, vma->prot|PTE_U|PTE_X) < 0)
        kfree(mem), printf("map fail\n");
      else {
        ilock(vma->f->ip);
        int offset = base - (vma->va_end-vma->size);
        readi(vma->f->ip, 1, base, offset, PGSIZE);
        iunlock(vma->f->ip);
        return 0;
      }
    } else {
      //printf("vma no found\n");
      kfree(mem);
    }
  }
  return -1;
}

void
usertrap(void) {
    // ...
    } else if(r_scause() == 13 || r_scause() == 15) {
        if (do_mmap(r_stval()) < 0)
            p->killed = 1;
    } else {
    // ...
}

剩下就是初始化與在fork時vma用到的file要記得refcnt要遞增

Linux crash tool

經過前面lab的洗禮,相信大家也對va,pa,pte的關係有深刻的了解 這樣可以來看看,在linux中怎麼改mem

  1. crash tool: 可以分析kerneldump看ctx與kernel的struct
  2. /dev/mem: 整個mem的視圖,就是ch3第一張圖的所有東西都會在這裡看到

下面借用這裡的內容來看看這兩個怎麼一起用 先預設/dev/mem可以寫,pagetable沒有任何限制

這裡可以看crash的指令

  • vtop: show va的訊息
  • wr: 改寫 mem
  • set: 把mem view改成該pid的process的view
  • ps: 就是ps

改pte的map

兩個process,兩個page,兩個addr(0x34000000, 0x34004000)

之後在crash中改pte指到的地方

#include <stdio.h>
#include <unistd.h>
#include <sys/mman.h>
#include <string.h>
#include <fcntl.h>

int main(int argc, char **argv)
{
    int fd;
    unsigned long *addr;

    fd = open("/dev/mem", O_RDWR);

    // 建立一個分頁 P1 映射到保留記憶體
    addr = mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0x34000000);

    // 修改 P1 的内容
    *addr = 0x1122334455667788;
    
    printf("address at: %p   content is: 0x%lx\n", addr, addr[0]);

    // 等待分頁交換
    getchar();
    
    printf("address at: %p   content is: 0x%lx\n", addr, addr[0]);

    close(fd);
    munmap(addr, 4096);
    return 1;
}
#include <stdio.h>
#include <unistd.h>
#include <sys/mman.h>
#include <string.h>
#include <fcntl.h>

int main(int argc, char **argv)
{
    int fd;
    unsigned long *addr;

    fd = open("/dev/mem", O_RDWR);

    // 建立分頁 P2 映射到保留的記憶體
    addr = mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0x34004000);
    
    // 修改 P2 的内容
    *addr = 0x8877665544332211;

    printf("address at: %p   content is: 0x%lx\n", addr, addr[0]);  
    // 等待分頁交換
    getchar();
    printf("address at: %p   content is: 0x%lx\n", addr, addr[0]);

    close(fd);
    munmap(addr, 4096);
    return 1;
}

接著要找pte,找對面對到的pa

crash> ps | grep master                                                                                                                                                                [8/287]
  32334  32333   6  ffff93d0ed35c680  IN   0.0    4512   1384  master
crash> set 32334
    PID: 32334
COMMAND: "master"
   TASK: ffff93d0ed35c680  [THREAD_INFO: ffff93d0ed35c680]
    CPU: 6
  STATE: TASK_INTERRUPTIBLE
crash> vtop 0x7f8f3ba6a000
VIRTUAL     PHYSICAL
7f8f3ba6a000  2c0000000 <= va 與 pa

   PGD: 2ae2f87f8 => 80000002af219067
   PUD: 2af2191e0 => 2a9d3c067
   PMD: 2a9d3cee8 => 2ac34b067
   PTE: 2ac34b350 => 80000002c0000267 <= pte 與 對到的pa
  PAGE: 2c0000000

      PTE         PHYSICAL   FLAGS
80000002c0000267  2c0000000  (PRESENT|RW|USER|ACCESSED|DIRTY|NX)

      VMA           START       END     FLAGS FILE
ffff93d0e894e000 7f8f3ba6a000 7f8f3ba6b000 d0444fb /dev/mem

crash> wr -64 -p 2ac34b350 80000002c0004267 <= pte 與 對面對到的pa
crash> ps | grep slave
  32348  32347   1  ffff93d0ed359780  IN   0.0    4512   1416  slave
crash> set 32348
    PID: 32348
COMMAND: "slave"
   TASK: ffff93d0ed359780  [THREAD_INFO: ffff93d0ed359780]
    CPU: 1
  STATE: TASK_INTERRUPTIBLE
crash> vtop 0x7f269fba3000
VIRTUAL     PHYSICAL
7f269fba3000  2c0004000

   PGD: 2ae2ca7f0 => 80000002ac354067
   PUD: 2ac3544d0 => 2b45f6067
   PMD: 2b45f67e8 => 2ac7db067
   PTE: 2ac7dbd18 => 80000002c0004267
  PAGE: 2c0004000
  
      PTE         PHYSICAL   FLAGS
80000002c0004267  2c0004000  (PRESENT|RW|USER|ACCESSED|DIRTY|NX)

      VMA           START       END     FLAGS FILE
ffff93d0eea18820 7f269fba3000 7f269fba4000 d0444fb /dev/mem

crash> wr -64 -p 2ac7dbd18 80000002c0000267

改pa的值

先給個程式,印出va(不然無法知道pa),之後直接改pa的值

#include <stdio.h>
#include <unistd.h>
#include <sys/mman.h>
#include <string.h>

int main(int argc, char **argv)
{
    unsigned char *addr;

    // 匿名映射一段記憶體空間
    addr = mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_ANONYMOUS|MAP_SHARED, -1, 0);

    // 修改內容
    strcpy(addr, "浙江溫州皮鞋濕");

    // 只是範例,所以直接顯示 address 實際操作時需要手工 hack 記憶體位置
    printf("address at: %p   content is: %s\n", addr, addr);
    getchar();
    printf("address at: %p   content is: %s\n", addr, addr);

    munmap(addr, 4096);
    return 1;
}

用crash找pa

crash> ps | grep test
  11608  11607   1  ffff93d0ed378000  IN   0.0    4512   1408  test
  
crash> set 11608
    PID: 11608
COMMAND: "test"
   TASK: ffff93d0ed378000  [THREAD_INFO: ffff93d0ed378000]
    CPU: 1
  STATE: TASK_INTERRUPTIBLE
  
crash> vtop 0x7f7d88693000
VIRTUAL     PHYSICAL
7f7d88693000  1f83ed000

   PGD: 2a73ee7f0 => 8000000220a30067
   PUD: 220a30fb0 => 2ae1f4067
   PMD: 2ae1f4218 => 2b0c7e067
   PTE: 2b0c7e498 => 80000001f83ed867
  PAGE: 1f83ed000

      PTE         PHYSICAL   FLAGS
80000001f83ed867  1f83ed000  (PRESENT|RW|USER|ACCESSED|DIRTY|NX)

      VMA           START       END     FLAGS FILE
ffff93d0ec033450 7f7d88693000 7f7d88694000 80000fb dev/zero

      PAGE        PHYSICAL      MAPPING       INDEX CNT FLAGS
ffffd377c7e0fb40 1f83ed000 ffff93d0f01a9290        0  2 17ffffc0040038 uptodate,dirty,lru,swapbacked

現在我們知道0x1f83ed000就是我們要的位置!! 就改吧 mmap到pa去

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/mman.h>
#include <string.h>
#include <fcntl.h>

int main(int argc, char **argv)
{
    int fd;
    unsigned char *addr;
    unsigned long off = 0x1f83ed000;

    fd = open("/dev/mem", O_RDWR);

    addr = mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_SHARED, fd, off);

    strcpy(addr, "下雨進水不會胖");
    close(fd);

    munmap(addr, 4096);

    return 1;
}

小結

這裡的例子很簡單,但是很可怕,基本上只要知道pa就什麼都擋不住了 同時如果知道struct的大小與偏移量,基本上就是可以操作任何東西了

像是在task_struct遊走,改常數等等… 可以看上面的文章與這裡都有一些使用範例

Heap Coruption

剛好提到記憶體,可以看看關於記憶體的問題,下面內容主要處自這裡

  1. 在第一次的dump,backtrace沒有印出正確的行號
    • 文中提到懷疑是Heap Coruption的理由是
      • 崩潰是發生在存取非法位址
      • 這個行號所在的程式碼單純到沒辦法找到造成問題的部分在哪
    • 可以猜猜看why
      • 亂序執行
        • concurrent,但明顯不是
      • 之前在不對的地方沒有停下來
        • 可以試試把array超過length的地方print出來: print的出來!!
        • 如果這是迴圈中出事就還好,但如果出了loop…
  2. 第二次dump用了大招,上heap保護
    • 這與xv6的stack的guard page一樣,出錯直接panic
    • 這樣就可以直接看到對的地方,index出界,在8爆了
      1. 不是在5爆?
        • 為了好分配mem,所以會對齊
      2. 回頭來看c的type到底是什麼?
        • 一次要跳幾格
          • char是1
          • uint64是8
          • etc
        • 所以c其實就是一直幫忙算這個addr一次跳幾格

ch6

當我們說鎖保護數據時,我們真正的意思是鎖保護了一些適用於數據的不變式(invariant)集合

你可以把鎖看成是把並發的臨界區串行化(serializing)的一種工具,使它們同時只運行一個,從而保護 invariant(假設臨界區是獨立的)。

正確地使用鎖可以保證一次只能有一個 CPU 對關鍵部分的數據結構進行操作,所以當數據結構的 invariant 不成立時,沒有 CPU 會執行數據結構操作

如果一個穿過內核的代碼路徑必須同時持有多個鎖,那麼所有的代碼路徑以相同的順序獲取這些鎖是很重要的 * 有時鎖的身份並不是事先知道的,也許是因為必須持有一個鎖才能發現接下來要獲取的鎖的身份

CPU 的 ordering 規則稱為內存模型!! (目前看過最精練的解釋)

自旋锁

  1. 關中斷
  2. atomic cas while looping => lock
  3. mem barrier => mem barrier

一個中斷處理程序使用了自旋鎖,CPU 決不能在啟用中斷的情況下持有該鎖。 Xv6 比較保守:當一個 CPU 獲取任何鎖時,xv6 總是禁用該 CPU 上的中斷。

xv6 在 CPU 沒有持有自旋鎖時重新啟用中斷;它必須做一點記錄來應對嵌套的臨界區。

這個spinlock可以recursive!! 因為有紀錄cpu id所以可以處理這一段

trace: spinlock

我們把debug有關的部分skip掉

// Mutual exclusion lock.
struct spinlock {
  uint locked;       // Is the lock held?
};

acquire對數字做test_and_set,不成功就一直轉

  • 這裡要用riscv提供的指令去換,不然被reorder就出事了
  • 因為原本有設定cpuid的部分,導致還需要memory barrier
    • 但這裡就先跳掉
void
acquire(struct spinlock *lk)
{
  push_off();

  while(__sync_lock_test_and_set(&lk->locked, 1) != 0)
    ;
}

release就是acquire反著做

void
release(struct spinlock *lk)
{
  __sync_lock_release(&lk->locked);

  pop_off();
}

睡眠锁

擴展自旋锁,多了sleep

  1. 上spinlock
  2. 直到拿到鎖之前,一直sleep
    • 處理虛假喚醒

這裡是把sleep多傳一個lock,保證sleep後lock會被釋放 pthread也是,但是叫condition var

trace: sleeplock

struct sleeplock {
  uint locked;       // Is the lock held?
  struct spinlock lk; // spinlock protecting this sleep lock
};

acquire與release其實很簡單,用spinlock保護locked 如果只有保護locked能用atomic?

答案是不行,因為還要保護成功sleep

void
acquiresleep(struct sleeplock *lk)
{
  acquire(&lk->lk);
  while (lk->locked) {
    sleep(lk, &lk->lk);
  }
  lk->locked = 1;
  release(&lk->lk);
}

void
releasesleep(struct sleeplock *lk)
{
  acquire(&lk->lk);
  lk->locked = 0;
  wakeup(lk);
  release(&lk->lk);
}

為了可以在有lock當狀態下sleep,變成sleep要解上面lock,之後再鎖自己的lock 可以看到這邊lock涵蓋的範圍有overlay

如果有用過pthread的cond var就會看到一樣的東西!!

void
sleep(void *chan, struct spinlock *lk)
{
  struct proc *p = myproc();
  
  if(lk != &p->lock){  //DOC: sleeplock0
    acquire(&p->lock);  //DOC: sleeplock1
    release(lk);
  }

  // ...

  // Reacquire original lock.
  if(lk != &p->lock){
    release(&p->lock);
    acquire(lk);
  }
}

用什麼鎖

因為睡眠鎖會使中斷處於啟用狀態,所以不能在中斷處理程序中使用睡眠鎖

自旋鎖最適合短的臨界區,因為等待它們會浪費 CPU 時間 睡眠鎖對長時間的操作很有效

Lab Lock

Memory allocator

把freelist放到各個cpu中

struct cpu {
  // ...
  struct run *freelist;
  struct spinlock lock;
};

所以kalloc要從自己的list中找,如果沒有就去偷 kfree就直接塞回自己的list

void* kalloc(void)
{
  // ...
  push_off();
  int i = cpuid();
  acquire(&cpus[i].lock);
  if (!cpus[i].freelist) {
    r = steal_page(i);
  } else {
    r = cpus[i].freelist;
    cpus[i].freelist = r->next;
  }

  release(&cpus[i].lock);
  pop_off();
  // ...
}

void kfree(void *pa) {
  // ...
  int i = cpuid();
  acquire(&cpus[i].lock);
  r->next = cpus[i].freelist;
  cpus[i].freelist = r;
  release(&cpus[i].lock);
  pop_off();
}

偷就是走訪其他cpu看有沒有free的

void* steal_page(int i) {
  // interrupt should be disabled
  void *ret = 0;
  for (int j=((i+1)%NCPU); !ret && j!=i; j=((j+1)%NCPU)) {
    acquire(&cpus[j].lock);
    if (cpus[j].freelist) {
      ret = cpus[j].freelist;
      cpus[j].freelist = cpus[j].freelist->next;
    }
    release(&cpus[j].lock);
  }
  return ret;
}

Buffer cache

其實可以用前面的想法,把list打散

#define TBL_SIZE 7
struct entry {
  struct buf head;
  struct spinlock lock;
};

struct entry tbl[TBL_SIZE];

void insert_buf(struct buf* head, struct buf* b) {
  b->next = head->next, b->prev = head;
  head->next->prev = b, head->next = b;
}

拿不到就去偷

bget(uint dev, uint blockno)
{
  struct buf *b;
  //acquire(&bcache.lock);

  // Is the block already cached?
  int i = tbl_index(dev, blockno);
  acquire(&tbl[i].lock);
  // check cache
  for(b = tbl[i].head.next; b != &tbl[i].head; b = b->next)
    if(b->dev == dev && b->blockno == blockno){
        b->refcnt++;
        release(&tbl[i].lock);
        acquiresleep(&b->lock);
        return b;
      }

  // steal free buf
  for (int j=i, cnt=0;cnt < TBL_SIZE;j=((j+1)%TBL_SIZE),cnt++) {
    if (i != j)
      acquire(&tbl[j].lock);
    for(b = tbl[j].head.next; b != &tbl[j].head; b = b->next){
      if(b->refcnt == 0){
        b->dev = dev;
        b->blockno = blockno;
        b->valid = 0;
        b->refcnt = 1;
        b->tbl_index = i;
        struct buf *prev = b->prev, *next = b->next;
        if (prev) {
          prev->next = next;
        } else {
          tbl[j].head.next = next;
        }

        if (next) {
          next->prev = prev;
        }
        insert_buf(&tbl[i].head, b);
        if (i != j)
          release(&tbl[j].lock);
        release(&tbl[i].lock);
        acquiresleep(&b->lock);
        return b;
      }
    }
    if (i != j)
      release(&tbl[j].lock);
  }
  release(&tbl[i].lock);
  panic("bget: no buffers");
}

剩下的就是把bcache改成用到對的list

struct buf {
  int tbl_index;
  // ...
};

ch7

xv6 的sleep 和 wakeup 機制會進行切換 這會發生在進程等待設備或管道 I/O 等待子進程退出 在 sleep 系統調用中等待

xv6 週期性地強制切換,以應對長時間的計算進程。

首先,如何從一個進程切換到另一個進程? 第二,如何對用戶進程透明的強制切換? (用定時器中斷來驅動上下文切換) 第三,許多 CPU 可能會在進程間並發切換,需要設計一個鎖來避免競爭。 第四,當進程退出時,必須釋放進程的內存和其他資源,但它自己不能做到這一切,因為它不能釋放自己的內核棧,同時又在使用內核棧。 第五,多核機器的每個內核必須記住它正在執行的進程,這樣系統調用就會修改相應進程的內核狀態。 最後,sleep 和 wakeup 允許一個進程放棄 CPU,並睡眠等待事件,並允許另一個進程喚醒第一個進程。

需要注意一些競爭可能會使喚醒丟失!!

coperative thread: sleep, wakeup, ctx switch, scheduler

ctx switch

xv6 調度器在每個 CPU 上有一個專門的線程(保存的寄存器和棧),因為調度器在舊進程的內核棧上執行是不安全的

棧指針和 pc 被保存和恢復,意味著 CPU 將切換棧和正在執行的代碼

Swtch(kernel/swtch.S:3)只保存 callee-saved 寄存器,caller-saved 寄存器由調用的 C 代碼保存在堆棧上(如果需要)

它不保存 pc。 當swtch 返回時,它返回到被恢復的 ra 寄存器所指向的指令,也就是新線程之前調用 swtch的指令。 此外,它還會在新線程的棧上返回。

scheduler

其實就是在沒有process在跑的時候選一個跑

  1. main會call,scheduler
  2. 之後scheduler挑一個proc (RR)
  3. ctx switch
    • 到ctx switch時還沒release lock!!
      • 對於上下文切換來說,有必要打破這個約定,因為 p->lock 保護了進程的狀態和 context 字段上的不變式(invariant),而這些不變式在 swtch 中執行時為 false。
    • 都是與mycpu換ctx!!
      • 所以可以看成mycpu()->context就是正在跑的cpu的state,也是proc的state(不是RUNNABLE之類的,是執行的狀態)
p->lock??

可以這樣理解調度代碼結構,它執行一組關於進程的不變式,並且每當這些不變式為False 時,就持有 p->lock。

一個不變式是,

  • 如果一個進程正在運行,定時器中斷的 yield 必須能夠安全地切換進程;
    • 這意味著 CPU 寄存器必須持有進程的寄存器值(即 swtch 沒有將它們移到上下文中)
    • 並且 c->proc 必須指向該進程。 另一個不變式是,
  • 如果一個進程是RUNNABLE 的,那麼對於一個空閒的 CPU 調度器來說,運行它必須是安全的
    • 這意味著
      • (1)p->context 必須擁有進程的寄存器(i.e., 它們實際上並不在真實的寄存器中)
      • (2)沒有 CPU 在進程的內核棧上執行
      • (3)也沒有 CPU 的 c->proc 指向進程
        • 請注意,當 p->lock被持有時,這些屬性往往不為真。

維護上述不變式的原因:xv6 經常在一個線程中獲取 p->lock,然後在另一個線程中釋放

直到成功轉移state之前都要hold lock

mycpu and myproc

Xv6 為每個 CPU 維護了一個 cpu 結構體(kernel/proc.h:22),它記錄了當前在該 CPU 上 運行的進程(如果有的話),為 CPU 的調度線程保存的寄存器,以及管理中斷禁用所需的嵌套 自旋鎖的計數。

Xv6 確保每個 CPU 的 hartid 在內核中被存儲在該 CPU 的 tp 寄存器中

Usertrapret 將 tp 寄存器保存在 trampoline 頁中,因為用戶進程可能會修改 tp 寄存器

當從用戶空間進入內核時,uservec 會恢復保存的 tp(kernel/trampoline.S:70)。編譯器保證永遠不使用 tp 寄存器。

如果 RISC-V 允許 xv6 直接讀取當前的 hartid 會更方便

cpuid 和 mycpu 的返回值很容易錯: 如果定時器中斷,導致線程讓出 CPU,然後轉移到不同的 CPU 上,之前返回的值將不再正確。 為了避免這個問題,xv6 要求調用者禁用中斷,只有在使用完返回的 cpu 結構後才啟用中斷

myproc(kernel/proc.c:68)函數返回當前 CPU 上運行的進程的 proc 指針。 myproc 禁用中斷,調用 mycpu,從 cpu 中獲取當前進程指針(c->proc),然後啟用中斷。

myproc不用lock防嗎?? 當下指到的proc是對的

Sleep

為了確保進入sleep前的state改變不會被打斷 (eg: semaphore的P)

sleep會先hold proc的lock,才去

  1. release 前一個lock
  2. 改proc狀態
    • p->chan = chan;
    • p->state = SLEEPING;
  3. sched

這邊會看到一個有趣的事,與scheduler一樣,p->lock沒有release!!

wakeup

所有聽在同一個lock(semaphore)的proc的state設定成runnable

虛假喚醒

wakeup會把所有proc,叫醒,但只有一個proc可以拿到lock

所以對其他proc而言,這是虛假喚醒!!

exit & wait

exit: 把file close,把proc設定成ZOMBIE,sched wait: 掃child proc,看有沒有ZOMBIE,有就設定成UNUSED;沒有就sleep

父進程和子進程的 wait 和 exit,以及 exit 和 exit 之間可能出現競爭和死鎖的情況

此 xv6 的所有鎖都必須遵守相同的鎖順序(父進程的鎖,然後是子進程的鎖),以避免死鎖

preemptive thread

time interrupt時做sched

void
usertrap(void)
{
    // ...
    // give up the CPU if this is a timer interrupt.
    if(which_dev == 2)
        yield();
}

trace: trapframe & context

看回來這張圖

sched都是在kernel mode發生,所以不用換pagetable

sched是換手,也就是我已經做了差不多了,所以會主動call函數,之後只要回來我這邊就好, 但是換回來函數的執行環境(callee-save)會壞掉,所以要存; 用於計算用的參數就,沒有用(做了差不多了,或是說已經存在stack上了(會call函數c會處理)),就不用管

trap是中斷,所以之後要在原本的地方跑(還沒做完),因此要保留原本的pc,不然沒辦法接回去; 同時要保留caller-save,trap可以看成call函數,但沒有c的幫忙,所以這要自己存; 剩下是ctx switch,所以callee-save也要存

trace: exit & wait

  • exit:

    • 把opend file關一關
    • 把parent設定成init
    • 把自己state設定成ZOMBIE
  • wait

    • 掃過整個proc,找符合下面兩個條件的proc
      • parent是caller的proc
      • state是ZOMBIE
    • 找到就
      • acquire child的lock (wait!!)
      • 拉proc的return值(xstate)
      • freeproc
    • 沒找到
      1. state不是ZOMBIE
        • parent sleep
      2. 沒有proc認caller做parent
        • 報錯

這裡就可以回答一個經典問題,為什麼要有ZOMBIE? 因為把proc回收分成

  1. 關file: exit
  2. free mem: wait

而在wait可以拿到proc的return值(所以不能free mem) 但我們需要一個方式表示proc準備好被回收,所以有ZOMBIE

但為什麼叫ZOMBIE? 這我真的不懂,不能叫EXITED嗎?

Lab Multithreading

ph

練手用

pthread_mutex_t table_locks[NBUCKET];
static void 
insert(int key, int value, struct entry **p, struct entry *n)
{
  // ...

  // is the key already present?
  struct entry *e = 0;
  pthread_mutex_lock(table_locks+i);
  for (e = table[i]; e != 0; e = e->next) {
    // ...
    insert(key, value, &table[i], table[i]);
  }
  pthread_mutex_unlock(table_locks+i);
}

barrier

這個就有趣了

一開始會想只要一個出去把counter設成0就好,但這樣while的部分就會出事,可能有人出不來

這樣我們挑最後一個去reset計數器,但這樣也會有問題,如果說最後一個一直被hang住,之後中間有跑比較快的進來,這樣就不是barrier

所以鎖要保護兩個條件

  1. 進來時要確認上一輪的不在這裡了
  2. 人數到了就能release,最後一個要去reset計數器
static void 
barrier()
{
  static int no_one_here = 1, in_room = 0;
  pthread_mutex_lock(&bstate.barrier_mutex);
  while (!no_one_here)
    pthread_cond_wait(&bstate.barrier_cond, &bstate.barrier_mutex);
  pthread_cond_broadcast(&bstate.barrier_cond);

  bstate.nthread++, in_room++;
  while (bstate.nthread != nthread)
    pthread_cond_wait(&bstate.barrier_cond, &bstate.barrier_mutex);
  if (in_room-- == nthread)
    no_one_here = 0, bstate.round++;
  if (in_room == 0)
    no_one_here = 1, bstate.nthread = 0;
  pthread_cond_broadcast(&bstate.barrier_cond);
  pthread_mutex_unlock(&bstate.barrier_mutex);
}

uthread

這題就去抄context與swtch就好

但重點是,

  1. 要怎麼到thread的function去?

trap是改pc,因為我們不會再回來trap中 但thread可以多次來回,所以需要一個自動回到對的位置的機制,ra

  1. thread的記憶體要放在哪? 回想當初init怎麼做,讓之後的c可以跑? 從stack0放一個PGSIZE,指到sp
void thread_create(void (*func)()) {
  // ...
  t->ctx.ra = (uint64)func; // HERE
  t->ctx.sp = (uint64)t->stack + STACK_SIZE-1;
}

ch8

  • 磁盤層在 virtio 磁盤上讀寫塊

  • 緩存層(bio.c)緩存磁盤塊,並同步訪問它們,確保一個塊只能同時被內核中的一個進程訪問

    • buffer 緩存是一個由 buffer 組成的雙端鍊錶
    • bget根據devid、sector找buffer
    • bread/bwrite讀寫buffer
    • brelse釋放sleep lock
      • bread拿鎖
  • 日誌層(log.c)允許上層通過事務更新多個磁盤塊,並確保在崩潰時,磁盤塊是原子更新的(即全部更新或不更新)

    • 日誌由一個 header 塊組成,後面是一連串的更新塊副本(日誌塊)。
      • header 塊包含一個扇區號數組,
        • 每個扇區號都對應一個日誌塊,header 還包含日誌塊的數量
    • 日誌系統可以將多個系統調用的寫操作累積到一個事務中
      • 一次提交可能涉及多個完整系統調用的寫入
      • 為了避免一個系統調用被分裂到不同的事務中,只有在沒有文件系統相關的系統調用正在進行時,日誌系統才會提交
    • Xv6 在磁盤上劃出固定的空間來存放日誌。在一個事務中,系統調用所寫的塊總數必須 適應這個空間的大小
      • 系統調用寫入的日誌大小必須小於日誌空間的大小
        • Xv6 的 write 系統調用將大的寫操作分解成多個小的寫操作,以適應在日誌空間的大小
        • unlink 不會引起問題,因為 xv6 文件系統只使用一個位圖塊
      • 日誌系統只會在確定了系統調用的寫操作可以適應剩餘日誌空間之後,才會開始執行該系統調用
  • inode 層(fs.c)將一個文件都表示為一個 inode,每個文件包含一個唯一的 i-number 和一些存放文件數據的塊

    • balloc 申請一個新的磁盤塊: iterate bitmap
    • bfree 釋放一個塊: clear flag on block
    • 磁盤上的inode
      • 文件的大小和數據塊號的列表
      • 磁盤上的 inode 被放置磁盤的一個連續區域
        • 每一個 inode 的大小都是一樣的
          • 所以,給定一個數字 n,很容易找到磁盤上的第 n 個 inode
        • dinode定義了磁盤上的 inode
          • 包含一個 size 和一個塊號數組
            • 開始的 NDIRECT 個數據塊放置在數組中的前NDIRECT 個條目中,這些塊被稱為直接塊
            • 接下來的 NINDIRECT 個數據塊並沒有放置在inode 中,而是被存放在叫做間接塊的數據塊中
          • Bmap 返回 inode ip 的第 bn 個數據塊的磁盤塊號。如果 ip 沒有第 bn 個的數據塊,bmap 就會分配一個 (mmap!!)
    • mem中的inode
      • 了磁盤上 inode 的副本以及內核中需要的其他信息
      • 結構體 inode (kernel/file.h:17)是磁盤 dinode 的拷貝
        • ref 字段為指向 inode 的指針的數量,如果引用數量減少到零,內核就會從內存中丟棄這個 inode
        • iget 和 iput 函數引用和釋放 inode,並修改引用計數
        • 四種鎖
          • icache.lock 保證了一個 inode 在緩 存只有一個副本,以及緩存 inode 的 ref 字段計數正確
          • 每個內存中的 inode 都有一個包含 sleep-lock 的鎖字段,它保證了可以獨占訪問 inode 的其他字段(如文件長度)以及 inode 的文件或目錄內容塊的
          • 一個 inode 的 ref 如果大於 0,則會使系統將該 inode 保留在緩存 中,而不會重用該 inode
          • 每個 inode 都包含一個 nlink 字段(在磁盤上,緩存時會復 製到內存中),該字段統計鏈接該 inode 的目錄項的數量;如果一個 inode 的鏈接數大於零, xv6 不會釋放它
  • 目錄層(fs.c)將實現了一種特殊的 inode,被稱為目錄,其包含一個目錄項序列,每個目錄項由文件名稱和 i-number 組成

    • 函數 dirlookup 在一個目錄中搜索一個帶有給定名稱的條目
    • 函數 dirlink 會在當前目錄 dp 中創建一個新的目錄項
    • 查找路徑名會對每一個節點調用一次 dirlookup
    • Namex首先確定路徑解析從哪裡開始
      • 如果路徑以斜線開頭,則從根目錄開始解析
      • 否則,從當前目錄開始解析。
    • 然後它使用 skipelem 來遍歷路徑中的每個元素
  • file(file.c)

    • 系統中所有打開的文件都保存在一個全局文件表中,即 ftable
      • 文件表的功能有:
        • 分配文件(filealloc)
        • 創建重複引用(fileup)
        • 釋放引用(fileclose)
        • 讀寫數據(fileeread和filewrite)。

superblock, 它包含了文件系統的元數據(以塊為單位的文件系統大小、數據塊的數量、inode 的數量和日誌中的塊數)

位圖塊(bitmap),記錄哪些數據塊在使用

balloc與kalloc與malloc的差別

kalloc: 直接丟一個page malloc: 從現有的mem拉一段va出來,如果沒空間了就sbrk加大 * sbrk: growproc -> uvmalloc -> kalloc -> mappages * mappages: 把從kalloc拿到的page與加到pagetable balloc: 與kalloc很像,就是直接丟一個block出來 * 但要處理log!! * 去trace誰是空的 * mem用list(或AVL) * disk用bitmap

trace: how to read/write a file

  • fileread
    • readi
      • bread拿buf,bmap算偏移(在hdd上的addr)
      • copyout
  • filewrite
    • begin_op
      • writei
        • bread拿buf,bmap算偏移(在hdd上的addr)
        • copyin
        • log_write
          • 記錄到log
    • end_op
      • commit
        • write_log
          • 寫buf到disk
        • write_head
          • commit log

Lab File system

big file

就是在block再放一個table,之後再多做一次

static uint
bmap(struct inode *ip, uint bn)
{
  uint addr, *a;
  struct buf *bp;
  if(bn < NDIRECT){
    if((addr = ip->addrs[bn]) == 0)
      ip->addrs[bn] = addr = balloc(ip->dev);
    return addr;
  }
  bn -= NDIRECT;

  if (bn < BLOCKS) {
    // Load indirect block, allocating if necessary.
    if((addr = ip->addrs[NDIRECT]) == 0)
      ip->addrs[NDIRECT] = addr = balloc(ip->dev);
	@@ -400,6 +401,30 @@ bmap(struct inode *ip, uint bn)
    brelse(bp);
    return addr;
  }
  bn -= BLOCKS;
  if (bn < NINDIRECT) { 
    // double indirect
    int i = bn/BLOCKS, j = bn%BLOCKS;
    if((addr = ip->addrs[NDIRECT+1]) == 0)
      ip->addrs[NDIRECT+1] = addr = balloc(ip->dev);
    bp = bread(ip->dev, addr);
    a = (uint*)bp->data;
    if((addr = a[i]) == 0){
      a[i] = addr = balloc(ip->dev);
      log_write(bp);
    }
    brelse(bp);

    // query addr from int[]'s data
    bp = bread(ip->dev, addr);
    a = (uint*)bp->data;
    if((addr = a[j]) == 0) {
      a[j] = addr = balloc(ip->dev);
      log_write(bp);
    }
    brelse(bp);
    return addr;
  }

  panic("bmap: out of range");
}
void
itrunc(struct inode *ip)
{
  int i, j;
  struct buf *bp;
  uint *a;
  for(i = 0; i < NDIRECT; i++){
    if(ip->addrs[i]){
      bfree(ip->dev, ip->addrs[i]);
      ip->addrs[i] = 0;
    }
  }
  if(ip->addrs[NDIRECT]){
    bp = bread(ip->dev, ip->addrs[NDIRECT]);
    a = (uint*)bp->data;
    for(j = 0; j < BLOCKS; j++){
      if(a[j])
        bfree(ip->dev, a[j]);
    }
    brelse(bp);
    bfree(ip->dev, ip->addrs[NDIRECT]);
    ip->addrs[NDIRECT] = 0;
  }

  uint *b;
  struct buf *bp2;
  if(ip->addrs[NDIRECT+1]){
    bp = bread(ip->dev, ip->addrs[NDIRECT+1]);
    a = (uint*)(bp->data); // int[]
    for(i = 0; i < BLOCKS; i++){
      if (a[i]) {
        bp2 = bread(ip->dev, a[i]);
        b = (uint*)(bp2->data);
        for (j = 0; j < BLOCKS; j++)
          if(b[j])
            bfree(ip->dev, b[j]);
        brelse(bp2);
        bfree(ip->dev, a[i]);
      }
    }
    brelse(bp);
    bfree(ip->dev, ip->addrs[NDIRECT+1]);
    ip->addrs[NDIRECT+1] = 0;
  }

  ip->size = 0;
  iupdate(ip);
}

一個有path的檔案

syscall要先生出檔案,把path寫進去

uint64
sys_symlink(void)
{
  int n;
  char target[MAXPATH], path[MAXPATH];
  struct inode *ip;

  if((n = argstr(0, target, MAXPATH)) < 0 || argstr(1, path, MAXPATH) < 0)
    return -1;
  begin_op();
  ip = create(path, T_SYMLINK, 0, 0);

  for (int i=0, r = 0, n1 = 0, off = 0;i < n && r == n1; i += r){
    n1 = n - i;
    if ((r = writei(ip, 0, (uint64)target + i, off, n1)) > 0)
      off += r;
  }
  ip->nlink = 1;
  iunlockput(ip);
  end_op();
  return 0;
}

open要能處理symbol link,與原本的檔案 如果遇到其他symbol link還要繼續follow

抄原本的open,加symbol link的處理,最後遞迴

uint64 real_open(char path[MAXPATH], int omode, int follow) {
  int fd;
  struct file *f;
  struct inode *ip;

  if(omode & O_CREATE){
    ip = create(path, T_FILE, 0, 0);
    if(ip == 0){
      return -1;
    }
  } else {
    if((ip = namei(path)) == 0){
      return -1;
    }
    ilock(ip);
    if(ip->type == T_DIR && omode != O_RDONLY){
      iunlockput(ip);
      return -1;
    }
  }

  if(ip->type == T_DEVICE && (ip->major < 0 || ip->major >= NDEV)){
    iunlockput(ip);
    return -1;
  }

  if (ip->type == T_SYMLINK) {
    if (!(omode & O_NOFOLLOW)) {
      char target[MAXPATH];
      if (follow > FOLLOW_DEPS || readi(ip, 0, (uint64)target, 0, MAXPATH) < 0) {
        iunlockput(ip);
        return -1;
      }

      iunlock(ip);
      return real_open(target, omode, follow+1);
    }
  }

  if((f = filealloc()) == 0 || (fd = fdalloc(f)) < 0){
    if(f)
      fileclose(f);
    iunlockput(ip);
    return -1;
  }

  if(ip->type == T_DEVICE){
    f->type = FD_DEVICE;
    f->major = ip->major;
  } else {
    f->type = FD_INODE;
    f->off = 0;
  }
  f->ip = ip;
  f->readable = !(omode & O_WRONLY);
  f->writable = (omode & O_WRONLY) || (omode & O_RDWR);
  if((omode & O_TRUNC) && ip->type == T_FILE){
    itrunc(ip);
  }

  iunlock(ip);

  return fd;
}

uint64
sys_open(void)
{
  char path[MAXPATH];
  int omode;
  int n;

  if((n = argstr(0, path, MAXPATH)) < 0 || argint(1, &omode) < 0)
    return -1;
  begin_op();
  uint64 ret = real_open(path, omode, 0);
  end_op();
  return ret;
}

Ref

可以在google找到很多其他人的做法,都值得參考

MIT6.S081 操作系统工程中文翻译 xv6-book-2020-Chinese