MIT6828 学习笔记 004 (read code 00)

MIT6828 学习笔记 004 (read code 00)

RayAlto OP

目前只是看代码里的注释试图理解代码,后面如果有了更深的理解再来改。

1. kernel/proc.h

hart (RISC-V): hardware thread (The RISC-V Instruction Set Manual Sec 1.2)

1.1. struct context

为内核 context 切换保存的寄存器

大概用在 CPU 在进程和内核的 scheduler 之间切换。大概过程是先把当前寄存器值放进旧 struct context 里,然后把新的 struct context 放进寄存器里,简单来说就是先把当前寄存器值放进某块内存里,在把新的值从另一块内存里读进寄存器。

1
2
3
4
5
6
7
8
9
10
// Saved registers for kernel context switches.
struct context {
uint64 ra; // return address
uint64 sp; // stack pointer

// callee-saved
uint64 s0;
// uint64 s0 ... s11;
uint64 s11;
};

1.2. struct cpu

每个 CPU

1
2
3
4
5
6
7
// Per-CPU state.
struct cpu {
struct proc *proc; // 指向跑在这个 CPU 的进程或 `NULL`
struct context context; // swtch() here to enter scheduler().
int noff; // Depth of push_off() nesting.
int intena; // Were interrupts enabled before push_off()?
};

1.3. struct trapframe

003:5 里首次出现,处理 trap 的代码 (kernel/trampoline.S) 要用到的每个进程的数据?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// per-process data for the trap handling code in trampoline.S.
// sits in a page by itself just under the trampoline page in the
// user page table. not specially mapped in the kernel page table.
// uservec in trampoline.S saves user registers in the trapframe,
// then initializes registers from the trapframe's
// kernel_sp, kernel_hartid, kernel_satp, and jumps to kernel_trap.
// usertrapret() and userret in trampoline.S set up
// the trapframe's kernel_*, restore user registers from the
// trapframe, switch to the user page table, and enter user space.
// the trapframe includes callee-saved user registers like s0-s11 because the
// return-to-user path via usertrapret() doesn't return through
// the entire kernel call stack.
struct trapframe {
/* 0 */ uint64 kernel_satp; // 内核 page table
/* 8 */ uint64 kernel_sp; // 进程内核栈的栈顶
/* 16 */ uint64 kernel_trap; // usertrap()
/* 24 */ uint64 epc; // 用户 PC
/* 32 */ uint64 kernel_hartid; // saved kernel tp
/* 40 */ uint64 ra;
/* ... uint64 一堆寄存器 */
/* 280 */ uint64 t6;
};

1.4. enum procstate

进程状态

1
enum procstate { UNUSED, USED, SLEEPING, RUNNABLE, RUNNING, ZOMBIE };
  • UNUSED: allocproc 时会去进程列表里找这种状态的进程并占用
  • USED: allocproc 产生的新进程首先会进入这个状态
  • SLEEPING: 调用 wait 时有子进程但没有 ZOMBIE 状态时进程会调用 sleep 进入这个状态让出 CPU
  • RUNNABLE: fork 出的新进程准备就绪后会进入这个状态,内核的 scheduler 会不断让 CPU 寻找这种状态的进程去执行
  • RUNNING: scheduler 中 CPU 找到了 RUNNABLE 的进程后会把这个进程标为 RUNNING
  • ZOMBIE: 进程调用 exit 后会进行一定的清理,然后进入这个模式等待父进程调用 wait

1.5. struct proc

每个进程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// Per-process state
struct proc {
struct spinlock lock;

// 使用下面这些成员必须拿到上面的 lock
enum procstate state; // 进程状态
void *chan; // 标记因为什么进入 SLEEPING 状态
int killed; // 标记是否被 kill()
int xstate; // 返回给父进程的 wait() 的 exit status
int pid; // 进程 ID

// 使用这个必须拿到 wait_lock
struct proc *parent; // 指向父进程

// these are private to the process, so p->lock need not be held.
// 下面这些成员是进程私有的,使用时要拿到上面的 lock
uint64 kstack; // 内核栈的虚拟地址
uint64 sz; // 进程存储空间大小,单位: byte
pagetable_t pagetable; // 用户 page table
struct trapframe *trapframe; // trampoline.S 要用到的数据 page
struct context context; // swtch() here to run process
struct file *ofile[NOFILE]; // 打开的文件, NOFILE=16 (user/param.h:3),
// 一个进程最多16个
struct inode *cwd; // 当前目录
char name[16]; // 进程名 (debugging)
};

2. kernel/defs.h

里面是源文件 bio.c, console.c, exec.c, file.c, fs.c, ramdisk.c, kalloc.c, log.c, pipe.c, printf.c, proc.c, swtch.c, spinlock.c, sleeplock.c, string.c, syscall.c, trap.c, uart.c, vm.c, plic.c, virtio_disk.c的声明

3. kernel/entry.S

  • CSR: Control and Status Register
  • mhartid: hart ID
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
        # qemu -kernel 把内核加载到 0x80000000 并使每个 hart 都跳转到这
# kernel/kernel.ld 使下面的代码被放在 0x80000000
.section .text
.global _entry
_entry:
# 创建一个栈给 C 代码使用(对应 kernel/start.c:11 的 stack0 )
# 每个 CPU 要 4096 byte 的栈
# sp = stack0 + (hartid * 4096)
la sp, stack0 # load address , sp = stack0
li a0, 1024*4 # load immediate, a0 = 1024 * 4
csrr a1, mhartid # CSR read? , a1 = mhartid
addi a1, a1, 1 # add immediate , a1 = a1 + 1
mul a0, a0, a1 # multiply , a0 = a0 * a1
add sp, sp, a0 # add , sp = sp + a0
# jump to start() in start.c
call start
spin:
j spin

4. kernel/main.c

  • PLIC: the riscv Platform Level Interrupt Controller
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
/* includes */

volatile static int started = 0;

// start() jumps here in supervisor mode on all CPUs.
void main() {
if (cpuid() == 0) { // ===== 第一个 CPU =====
consoleinit();
printfinit();
printf("\n");
printf("xv6 kernel is booting\n");
printf("\n");
kinit(); // 物理 page 分配器
kvminit(); // 创建内核 page table
kvminithart(); // 启用 paging
procinit(); // 初始化进程 table
trapinit(); // 初始化 trap vectors
trapinithart(); // 初始化内核 trap vector ,使内核接受 exceptions 和 traps
plicinit(); // 初始化 interrupt 控制器
plicinithart(); // 向 PLIC 请求设备 interrupts
binit(); // 初始化缓冲区
iinit(); // 初始化 inode 表
fileinit(); // 初始化文件表
virtio_disk_init(); // 初始化 qemu 的 virtio 磁盘
userinit(); // 第一个用户进程 (kernel/proc.c:233),
// 结果是执行由 (user/initcode.S) 汇编而来的程序
__sync_synchronize();
started = 1;
} else { // ===== 其他 CPU =====
while (started == 0);
__sync_synchronize();
printf("hart %d starting\n", cpuid());
kvminithart(); // 启用 paging
trapinithart(); // 初始化内核 trap vector ,使内核接受 exceptions 和 traps
plicinithart(); // 向 PLIC 请求设备 interrupts
}

scheduler(); /** 每个 CPU 初始化后都进入 scheduler 不会返回:
* - 挑一个进程来运行
* - 调用 swtch 来开始运行那个进程
* - 那个进程最后会通过 swtch 把控制权还给 scheduler
*/
}

5. user/initcode.S

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# 最初的进程,它 exec /init (运行在 user mode )

#include "syscall.h"
# 在 Makefile 里这个是用 CC 编译的,所以会执行预处理
# 这也是为什么下面 SYS_exec, SYS_exit 符号有效

# exec(init, argv)
.globl start
start:
la a0, init # a0 = init (initcode.S:22, "/init\0")
la a1, argv # a1 = argv (initcode.S:27, {init, NULL})
li a7, SYS_exec # a7 = SYS_exec (kernel/syscall.h:8, 7)
ecall # environment call, transfer control to OS

# for(;;) exit();
exit:
li a7, SYS_exit # a7 = SYS_exit (kernel/syscall.h:3, 2)
ecall # environment call, transfer control to OS
jal exit # jump and link

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

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

6. user/init.c

最初的用户进程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
/* includes */

char *argv[] = {"sh", 0};

int main(void) {
int pid, wpid;
// 打开 /console ,如果不存在就创建
if (open("console", O_RDWR) < 0) {
mknod("console", CONSOLE, 0);
open("console", O_RDWR); // fd = 0, stdin
}
dup(0); // stdout
dup(0); // stderr
// 确保有一个子进程运行 /sh
for (;;) {
printf("init: starting sh\n");
pid = fork();
if (pid < 0) {
printf("init: fork failed\n");
exit(1);
}
if (pid == 0) {
exec("sh", argv);
printf("init: exec sh failed\n");
exit(1);
}

for (;;) {
// 子进程或无父进程的进程退出时 wait 会返回
wpid = wait((int *)0);
if (wpid == pid) {
// 运行 /sh 的子进程返回了,重新打开
break;
}
else if (wpid < 0) {
printf("init: wait returned an error\n");
exit(1);
}
else {
// 无父进程的进程退出,不用管
}
}
}
}

7. 简单看一眼

7.1. kernel/proc.c

1
2
struct cpu cpus[NCPU];
struct proc proc[NPROC];

原来 CPU 和进程的存储方式这么硬核

函数格式实现
65int cpuid();从寄存器 tp 读取
74struct cpu* mycpu();cpuid()struct cpu cpus[NCPU] 里拿
83struct proc* myproc();通过 cpuid 拿到 struct cpu 里面的 proc 就是当前进程
93int allocpid();1 开始递增分配
110static struct proc* allocproc();遍历 struct proc proc[NPROC]UNUSED 进程,没有则返回 0 ,否则给这个进程分配一个 pid ,然后设置成 UNUSED 状态,并给各个成员分配空间,然后使这个新进程的入口为 forkret
280int fork();读取当前的 struct proc 复制出一个新的进入 RUNNABLE 状态
331void reparent(struct proc* p);p 的所有子进程交给 init 进程
347void exit(int status);拿到当前进程(拿到 init 时 panic ),关闭所有打开的文件,把所有子进程交给 init ,进入 ZOMBIE 状态,最后 sched() 回到 scheduler
391int wait(uint64 addr);循环遍历 struct proc proc[NPROC] 找到当前进程的状态为 ZOMBIE 的子进程,释放并返回它的 pid 。如果当前进程没有子进程则返回 -1 。如果没有处于 ZOMBIE 状态的子进程则把当前进程设置为 SLEEPING 后通过 sched() 使当前 CPU 回到 scheduler
445void scheduler();每个 CPU 都进入 scheduler 不返回,不断寻找下一个 RUNNABLE 进程,设置为 RUNNINGswtch 进去执行
482void sched();暂存当前 CPU 的 intena (interrupt enabled, bool) , swtch 到当前 CPU (回到 scheduler ),恢复 intena
503void yield();把自己设置为 RUNNABLEsched()scheduler
536void sleep(void* chan, struct spinlock* lk);把自己设为 SLEEPING 后回到 scheduler

7.2. kernel/exec.c

主要是 exec 的实现。首先读取应用程序的 ELF header (struct elfhdr, kernel/elf.h:6) ,比如:

1
head -c 4 user/_sh | hexdump -C

可以看到应用程序的 magic bytes :

1
2
00000000  7f 45 4c 46                                       |.ELF|
00000004

然后把程序加载进内存,分配一个用户栈,把参数压进去,然后把 argv 放进 spargc 放进 a0