注:源代码参见我的github: https://github.com/YaoZengzeng/jos
Part A : User Environments and Exception Handling
User Exception:
1、kernel维护了以下三个全局变量和environment有关的全局变量
struct Env *envs = NULL; // All environments
struct Env *curenv = NULL; // The current env -->在启动过程中,curenv被初始化为NULL
static struct Env *env_free_list; // Free environment list 数据结构Env定义在inc/env.h中,如下所示:
struct Env {
struct Trapframe env_tf; // Saved registers
struct Env *env_link; // Next free Env
envid_t env_id; // Unique environment identifier
envid_t env_parent_id; // env_id of this env's parent
enum EnvType env_type; // Indicates special system environments
unsigned env_status; // Status of the environment
uint32_t env_runs; // Number of times environment has run // Address space
pde_t *env_pgdir; // Kernel virtual address of page dir
}; env_id:Kernel用一个唯一的id来标示当前正在使用这个Env数据结构的进程(或者说正在使用envs中这个特定的slot的environment)。当一个用户进程终止的时候,kernel可能会将同一个Env数据结构
重新分配给另一个不同的environment。但是,新的environment仍然会有一个完全不同的env_id,虽然它和旧的environment使用在envs中相同的slot。
env_status:ENV_FREE
:该Env inactive,该结构正在env_free_list中ENV_RUNNABLE
:该Env正在等待CPU运行ENV_RUNNING
:该Env正在CPU上运行ENV_NOT_RUNNABLE
:该Env是active的,但是并不能运行,原因可能是它在等待来自其他environment的IPCENV_DYING
:僵尸进程,它在下一次陷入kernel的时候会被释放 2、和Unix process类似,JOS的environment结合了"thread"和"address space"。thread由保存在env_tf中的寄存器定义,而address space则由env_pgdir指向的page directory和page table 定义。
在JOS中,不像xv6,每个进程都有单独的kernel stacks。因为在JOS中,每次仅仅只能有一个environment处于kernel中,因此JOS只需要一个kernel stack。 3、
(1)void env_init(void):将envs中的所有environment设置为free,将它们的env_ids设置为0,并且将它们插入到env_free_list
(2)static int env_setup_vm(struct Env *e):初始化environment e的内核部分的virtual memory,分配一个page作为e->env_pgdir的page directory,因为每个environment的kernel部分的
(3)virtual memmory的布局是一致的,因此只需要将kern_pgdir索引高于PDX(UTOP)的部分直接拷贝到e->env_pgdir的相应部分即可。
(4)static void region_alloc(struct Env *e, void *va, size_t len): 为environment e分配len个字节的physical memory,并且将它映射到该environment address space的va处。并且
将va向下对齐,(va+len)向上对齐(对齐即能被PGSIZE整除)
(5)static void load_icode(struct Env *e, uint8_t *binary):为environment e初始化程序,堆栈和处理器标志,简单地说就是将kernel中硬编码写入的程序加载到environment e的地址空间内。
并且为该程序映射一个one page大小的初始栈。
注:类似于boot loader从磁盘中加载加载内核,首先需要读取ELF header,这里将binary做强制类型转换即可
接着将类型为ELF_PROG_LOAD的segment载入内存,其实最快的方法是直接利用memcpy的方法进行内存的拷贝,但是这里存在一个问题,因为此时的page directory依旧是kernel的kern_pgdir,而我们需要将
数据拷贝到environment e自己的address space中,所以这里的操作比较tricky,需要先执行指令"lcr3(PADDR(e->env_pgdir));"进入e的address space,再进行memcpy,之后再"lcr3(PADDR(kern_pgdir));"
转换回来即可。最后,我们需要制定environment e的执行入口,其实就是初始化e->env_tf.tf_eip,一般该值为0x800020。
(6)void env_create(uint8_t *binary, enum EnvType type): 利用env_alloc获取一个新的environment,并且利用load_inode将binary载入该env的address space,最后设置environment的type。
(7)void env_run(struct Env *e):切换上下文,将当前运行的environment设置为e。
(8)env_pop_tf(struct Trapframe *tf): 将tf的内容载入寄存器,当'iret'指令执行时,离开kernel,开始执行environment的代码。 Exception Handling:
1、exception和interrupt都是受保护的控制转换(protected control transfer),它能让处理器从用户模式转换到内核模式(CPL=0),从而能够避免用户态的代码影响到kernel或者其他environment的代码。
在x86体系中,interrupt是由处理器之外异步的事件导致的控制转换,例如外设IO的消息等等,而exception是由当前正在运行的代码导致的控制转换,例如访问非法内存等等。 2、为了保证exception/interrupt导致的控制转换确实是受到保护的,那么处理器必须保证在严格控制的条件下才能进入kernel。在x86中,下面两种机制确保了这一切:
(1)The Interrupt Descriptor Table:x86规定了256种不同的interrupt和exception能进入kernel,它们每一个都有自己的interrupt vector。一个vector是一个0到255的数字。CPU使用vector作为
处理器interrupt descriptor table (IDX)的索引,而IDX存放在内核私有的内存里,通过IDX的每个表项,处理器能够得到:
一、EIP:指向处理该类型exception的内核代码。二、CS寄存器的值,其中的0-1位指定了exception handler运行的级别。
(2)The Task State Segment:在处理interrupt和exception之前,处理器需要将当前正在运行代码的状态保存起来,例如寄存器EIP和CS的值,并且为了防止恶意代码的攻击这些状态值存放的地方不能被用户态的代码访问。
因此,当x86执行interrupt或trap之前,运行等级会从用户态转化为内核态,并且会切换到内核内存中的一个stack。一个叫做task state segment(TSS)的数据结构标示了这个stack的segment selector和address。
当执行异常处理的时候,处理器先将SS,ESP,EFLAGS,CS,EIP以及一个可选的error code压栈,接着从interrupt descriptor中加载CS和EIP,最后将ESP和SS指向上述的stack。 3、在x86中所有的synchronous exception使用0到31的interrupt vectors,对应IDT的0到31,例如缺页异常就是vector 14。interrupt vector大于31的,只能被software interrupts,通常由int指令引起,或者
其他一些asynchronous hardware interrupts引起。 4、
(1)、处理器切换到TSS中SS0和ESP0指定stack。在JOS它们的值分别为GD_KD,KSTAKTOP
(2)、将SS,ESP,EFLAGS,CS,EIP一次入栈(有些exception可能会将error code入栈)
(3)、处理器从相应的IDT中获取CS:EIP,执行相应异常处理的代码 5、当系统执行一个嵌套的异常时,此时已经处在内核模式,因此它不需要切换堆栈,并且不用保存老的SS和ESP寄存器,只需要将老的EFLAGS,CS,EIP(如果还有error code)入栈即可。