xv6源码分析 007
今晚我们看看这两个文件
proc.h
和proc.c
首先看看proc.h
在这个文件中定义了一些硬件资源的抽象,比如cpu,进程,上下文(context):
struct context
:进程切换时的上下文struct cpu
:cpustruct trapframe
:用于用户态和内核态切换时,保存一部分寄存器的数据结构struct proc
:进程
struct context
// Saved registers for kernel context switches.
struct context {
uint64 ra;
uint64 sp;
// callee-saved
uint64 s0;
uint64 s1;
uint64 s2;
uint64 s3;
uint64 s4;
uint64 s5;
uint64 s6;
uint64 s7;
uint64 s8;
uint64 s9;
uint64 s10;
uint64 s11;
};
context 中都是一些寄存器的状态,我们主要看的ra
和sp
-
ra
:线程被切换时的执行的指令的地址,这个和我们平时的函数调用时的ra
有类似的地方,所以在线程切换和协程切换的这个过程我们都可以看成是一个函数的调用的过程。在mits6.s081的课程中我记得教授问过一个问题:为什么保存的是ra的值,而不是保存pc的值?
我们看看ra和pc的差别:
首先是功能的不同:RA寄存器用于存储函数调用的返回地址,即函数执行完毕之后需要返回到那个地址继续执行。PC寄存器用于存储当前执行的地址,即下一条需要执行的指令。
使用方式的不同:RA寄存器的值是由函数调用指令自动保存和恢复的,当函数调用时,返回地址会被自动保存到RA寄存器中;当函数执行完毕之后,会从RA寄存器中取出返回地址并跳转到该地址。PC寄存器的值是由计算机硬件自动更细的,每执行一条指令,PC寄存器的值会自动增减,指向下一条将要执行的指令
存储内容的不同:RA寄存器存储的是一个具体的地址值,表示函数调用之后需要返回到那个地址。PC寄存器存储的是一个指令的地址,表示下一条将要执行的指令在内存中的位置。
使用场景的不同:RA寄存器主要用于函数调用和返回过程中,用于保存和恢复返回地址,确保函数能够正确返回到调用点。PC寄存器在整个程序执行中都会被使用,用于指示吓一跳将要执行的指令的地址。
上面时gpt说的,所以答案就是,在线程再次被切换回来的时候,如果只是保存PC寄存器的话,那么它只是回到了原来的指令的地址,但是执行这条指令所需要的上下文都丢失了,因为没有保存RA。
-
sp
:栈指针寄存器,线程的调用栈
struct tramframe
struct trapframe {
/* 0 */ uint64 kernel_satp; // kernel page table
/* 8 */ uint64 kernel_sp; // top of process's kernel stack
/* 16 */ uint64 kernel_trap; // usertrap()
/* 24 */ uint64 epc; // saved user program counter
/* 32 */ uint64 kernel_hartid; // saved kernel tp
/* 40 */ uint64 ra;
/* 48 */ uint64 sp;
/* 56 */ uint64 gp;
/* 64 */ uint64 tp;
/* 72 */ uint64 t0;
/* 80 */ uint64 t1;
/* 88 */ uint64 t2;
/* 96 */ uint64 s0;
/* 104 */ uint64 s1;
/* 112 */ uint64 a0;
/* 120 */ uint64 a1;
/* 128 */ uint64 a2;
/* 136 */ uint64 a3;
/* 144 */ uint64 a4;
/* 152 */ uint64 a5;
/* 160 */ uint64 a6;
/* 168 */ uint64 a7;
/* 176 */ uint64 s2;
/* 184 */ uint64 s3;
/* 192 */ uint64 s4;
/* 200 */ uint64 s5;
/* 208 */ uint64 s6;
/* 216 */ uint64 s7;
/* 224 */ uint64 s8;
/* 232 */ uint64 s9;
/* 240 */ uint64 s10;
/* 248 */ uint64 s11;
/* 256 */ uint64 t3;
/* 264 */ uint64 t4;
/* 272 */ uint64 t5;
/* 280 */ uint64 t6;
};
这个太长了,而且也没什么好看,浏览一下即可。
struct cpu
// Per-CPU state.
struct cpu {
struct proc *proc; // The process running on this cpu, or null.
struct context context; // swtch() here to enter scheduler().
int noff; // Depth of push_off() nesting.
int intena; // Were interrupts enabled before push_off()?
};
主要是context:这个context是保存着该cpu内核线程调度线程的上下文,用于线程切换。
intena:表示这个cpu是否禁止了中断。
noff:这个不太懂,应该是表示临界区嵌套的深度
struct proc
enum procstate { UNUSED, SLEEPING, RUNNABLE, RUNNING, ZOMBIE };
// Per-process state
struct proc {
struct spinlock lock;
// p->lock must be held when using these:
enum procstate state; // Process state
struct proc *parent; // Parent process
void *chan; // If non-zero, sleeping on chan
int killed; // If non-zero, have been killed
int xstate; // Exit status to be returned to parent's wait
int pid; // Process ID
// these are private to the process, so p->lock need not be held.
uint64 kstack; // Virtual address of kernel stack
uint64 sz; // Size of process memory (bytes)
pagetable_t pagetable; // User page table
struct trapframe *trapframe; // data page for trampoline.S
struct context context; // swtch() here to run process
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
char name[16]; // Process name (debugging)
};
大家可能会对struct proc::chan
有疑惑:chan就类似于我们所说的条件变量,当进程获取锁失败的时候就会在这个chan上睡眠(仅针对睡眠锁)
struct context
:每一个进程都有一个context,一般在没有进程或线程调度的时候,context保存的是这个进程的内核线程的上下文。在xv6中,线程调度需要先切换到内核的线程,然后这个内核线程的上下文和cpu中保存的上下文(cpu调度线程的上下文)交换,来回到到调度器线程中,在来选择下一个处于就绪状态的进程。然后将调度器线程的上下文保存会cpu中的context结构中,再进行上下文切换。
struct trapframe
:用于实现trap机制,即在用户进程陷入内核态的时候,内核需要将该cpu上的该用户态进程的寄存器的状态保存在trapframe中,这样在恢复trap,返回用户空间的时候我们才能够继续正常地执行我们的指令。 trapframe 和 trapoline 其实和我们昨天说的 kernelpage 是一样的,都是所有进程共享,所以在进程被初始化的时候,会将这两个页映射到进程地址空间的最高地址处。
struct file
:记录该进程打开的文件
struct inode
:当前正在使用的i节点(文件系统相关的内容)。
今晚先坐下准备工作,我们明天再看源文件。