# 八、
來源:[JOS學習筆記(八)](http://blog.csdn.net/roger__wong/article/details/8734349)
> 神說、內核要有自己的數據、使用戶不可訪問.事就這樣成了。
> 神稱高地址為內核空間、稱低地址為用戶空間. 神看著是好的。
> 神說、用戶要有自己的進程、和自己的頁表、并可以進行系統調用.事就這樣成了。
> 有晚上、有早晨、是第三日。
## 1、lab3概述
lab3大體分為兩部分,第一部分包括執行環境(可以簡單的理解為進程,下文也用進程代替執行環境)的建立和運行第一個進程,第二個部分初始化并完成中斷和異常以及系統調用相關機制,本文只描述第一部分的做法。
## 2、原理
在建立進程之前首先要建立進程的管理機制,包括分配進程描述符的空間、建立空閑進程列表、把進程描述符數組相應內存進行映射等。這是進程描述符管理的一些工作。
其次初始化進程的虛擬地址空間,也就是給其頁目錄賦值過程,主要是將內核空間映射到虛擬地址的高位上。
然后加載進程的代碼,代碼以二進制形式和內核編譯到一起了,所謂“加載”就是將這段代碼從內存(內核區往上一點的位置)復制到某個物理位置,接著將這個位置和相應虛擬地址進行映射。
最后將進程的頁目錄加載進cr3,然后將各寄存器壓棧,通過iret跳到用戶態執行。
大體上講就是這樣。
## 3、具體實現與代碼
### (1)Env數據結構
Env數據結構代表一個進程描述符,定義在env.h中,包括進程的id,父進程的id,執行狀態,該進程的寄存器狀態,執行的次數等,并使用env_link指向下一個空閑的Env。
所有Env對象存儲在envs數組中,該數組定義在env.c的開頭。
除此之外curenv代表當前正在執行的進程,env_free_list指向空閑的進程描述符,組成鏈表,鏈表的添加與刪除均在表頭執行。
### (2)進程描述符空間的分配
首先在pmap.c里給數組envs分配空間,然后將分配的頁面從free_page_list里剔除掉,最后將UENVS映射到envs,整個過程跟Page數組的處理完全一樣,不再贅述。通過檢查函數即代表完成。
### (3)進程描述符數組初始化
完成env_init函數,將數組中所有的進程id置0,同時將各元素串起來,并把數組地址賦給env_free_list,代碼如下:
```
void
env_init(void)
{
// Set up envs array
// LAB 3: Your code here.
int i=0;
for(i=0;i<=NENV-1;i++)
{
envs[i].env_id=0;
if(i!=NENV-1)
{
envs[i].env_link=&envs[i+1];
}
}
env_free_list=envs;
// Per-CPU part of the initialization
env_init_percpu();
}
```
### (4)初始化進程虛擬地址空間
完成env_setup_vm函數。不同的進程有不同的虛擬地址空間,進而就必須有自己的頁目錄和頁表,該函數的任務就是初始化頁目錄。
首先分配一個空閑頁當做頁目錄,然后將這個頁目錄映射內核地址空間(UTOP之上的部分),不需要映射頁表因為可以和內核共用頁表。
接著增加分配的物理頁的引用(JOS設計的一個很不美的地方),將此頁的虛擬地址賦值給進程的pgdir,然后使進程有權限操作自己的pgdir。
```
static int
env_setup_vm(struct Env *e)
{
int i;
struct Page *p = NULL;
cprintf("env_setup_vm\r\n");
// Allocate a page for the page directory
if (!(p = page_alloc(ALLOC_ZERO)))
return -E_NO_MEM;
// LAB 3: Your code here.
e->env_pgdir=page2kva(p);
for(i=PDX(UTOP);i<1024;i++)
{
e->env_pgdir[i]=kern_pgdir[i];
}
p->pp_ref++;
// UVPT maps the env's own page table read-only.
// Permissions: kernel R, user R
e->env_pgdir[PDX(UVPT)] = PADDR(e->env_pgdir) | PTE_P | PTE_U;
return 0;
}
```
### (5)輔助映射函數
當進行加載二進制代碼的時候,需要一個輔助函數region_alloc,其作用是映射虛擬地址va及之后的len字節到進程e的虛擬地址空間里。
實現比較簡單,首先將va向下4K對齊,va+len向上4k對齊,以保證分配的是整數頁面。之后一個頁面一個頁面分配即可。
為了使頁面用戶態可寫,需要權限PTE_W|PTE_U。
```
static void
region_alloc(struct Env *e, void *va, size_t len)
{
// LAB 3: Your code here.
void* i;
cprintf("region_alloc %x,%d\r\n",e->env_pgdir,len);
for(i=ROUNDDOWN(va,PGSIZE);i<ROUNDUP(va+len,PGSIZE);i+=PGSIZE)
{
struct Page* p=(struct Page*)page_alloc(1);
if(p==NULL)
panic("Memory out!");
page_insert(e->env_pgdir,p,i,PTE_W|PTE_U);
}
}
```
### (6)進程代碼加載
通過load_icode給相應的進程加載可執行代碼。可執行代碼的格式是elf格式,因此使用類似加載kernel的方式將代碼加載到相應內存中。
因為當前JOS沒有文件系統,所以用戶態的程序是編譯在kernel里面的,通過編譯器的導出符號來進行訪問,通過追蹤init.c里的相關代碼也可以說明這一點。當然在這里我們無需更多的關注這個可執行代碼目前在哪里,只需要完成其加載機制即可。
為了往進程對應的虛擬空間映射到的物理內存中寫數據,首先必須要加載進程相應的頁目錄,當然必須使用此函數外的邏輯來保證在調用此函數之前頁目錄是已經初始化過的。接著仿照kernel的方式去分析elf文件,加載類型為ELF_PROG_LOAD的段到其要求的虛擬地址中,使用之前完成的輔助函數來方便的進行地址的映射。最后將程序入口放入進程的eip中(后面會有解釋),并映射進程堆棧(個人認為堆棧映射應該放在env_setup_vm會更合乎邏輯一些),然后重新加載kern_pgdir,函數返回。
```
static void
load_icode(struct Env *e, uint8_t *binary, size_t size)
{
// LAB 3: Your code here.
lcr3(PADDR(e->env_pgdir));
cprintf("load_icode\r\n");
struct Elf * ELFHDR=(struct Elf *)binary;
struct Proghdr *ph, *eph;
int i;
if (ELFHDR->e_magic != ELF_MAGIC)
panic("Not a elf binary");
ph = (struct Proghdr *) ((uint8_t *) ELFHDR + ELFHDR->e_phoff);
eph = ph + ELFHDR->e_phnum;
for (; ph < eph; ph++)
{
// p_pa is the load address of this segment (as well
// as the physical address)
if(ph->p_type==ELF_PROG_LOAD)
{
cprintf("load_prog %d\r\n",ph->p_filesz);
region_alloc(e,(void*)ph->p_va,ph->p_filesz);
char* va=(char*)ph->p_va;
for(i=0;i<ph->p_filesz;i++)
{
va[i]=binary[ph->p_offset+i];
}
}
}
e->env_tf.tf_eip=ELFHDR->e_entry;
// Now map one page for the program's initial stack
// at virtual address USTACKTOP - PGSIZE.
// LAB 3: Your code here.
struct Page* p=(struct Page*)page_alloc(1);
if(p==NULL)
panic("Not enough mem for user stack!");
page_insert(e->env_pgdir,p,(void*)(USTACKTOP-PGSIZE),PTE_W|PTE_U);
cprintf("load_icode finish!\r\n");
lcr3(PADDR(kern_pgdir));
}
```
### (7)進程建立
使用上述完成的函數來建立進程。
完成env_create函數,首先分配一個進程描述符,然后加載可執行代碼,邏輯很簡單。
```
void
env_create(uint8_t *binary, size_t size, enum EnvType type)
{
// LAB 3: Your code here.
struct Env* env;
if(env_alloc(&env,0)==0)
{
env->env_type=type;
load_icode(env, binary,size);
}
}
```
### (8)進程執行
完成env_run函數來運行進程。
首先設置當前進程的一些信息,然后更改當前進程指針指向要運行的進程,之后加載進程頁目錄跳轉到使用env_pop_tf真正使進程執行。
```
void
env_run(struct Env *e)
{
// LAB 3: Your code here.
cprintf("Run env!\r\n");
if(curenv!=NULL)
{
if(curenv->env_status==ENV_RUNNING)
{
curenv->env_status=ENV_RUNNABLE;
}
}
curenv=e;
e->env_status=ENV_RUNNING;
e->env_runs++;
lcr3(PADDR(e->env_pgdir));
env_pop_tf(&e->env_tf);
}
```
接著分析env_pop_tf。
env_pop_tf首先將傳入的trapframe,包含所有的寄存器信息壓棧,然后使用iret,即中斷返回來執行。
```
void
env_pop_tf(struct Trapframe *tf)
{
__asm __volatile("movl %0,%%esp\n"
"\tpopal\n"
"\tpopl %%es\n"
"\tpopl %%ds\n"
"\taddl $0x8,%%esp\n" /* skip tf_trapno and tf_errcode */
"\tiret"
: : "g" (tf) : "memory");
panic("iret failed"); /* mostly to placate the compiler */
}
```
iret到底是什么,這里引用一段IA-32手冊上的話:
the IRET instruction pops the return instruction pointer, return code segment selector, and EFLAGS image from the stack to the EIP, CS, and EFLAGS registers, respectively, and then resumes execution of the interrupted program or procedure. If the return is to another privilege level, the IRET instruction also pops the stack pointer and SS from the stack, before resuming program execution。
為什么要使用這種方式來運行第一個進程,而不是直接根據該進程入口執行,主要是因為當前運行在內核態(CPL,也就是CS等寄存器的后兩位,見圖),要使進程運行在用戶態必須改變各段寄存器的CPL,但又不能直接給諸如CS等寄存器賦值,所以必須使用iret從堆棧里彈出相應寄存器的值,這也是需要事先將tf的eip放入進程代碼入口地址的原因。

做完這些之后,第一個進程就能運行起來了,但立刻又崩潰了,因為沒有處理系統調用,而系統調用相關的實現就是下篇日志的內容了。