JOS學習筆記(八)

神說、內核要有自己的數據、使用戶不可訪問.事就這樣成了。 
神稱高地址爲內核空間、稱低地址爲用戶空間. 神看着是好的。 
神說、用戶要有自己的進程、和自己的頁表、並可以進行系統調用.事就這樣成了。  
有晚上、有早晨、是第三日。 


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,代碼如下:
  1. void  
  2. env_init(void)  
  3. {  
  4.     // Set up envs array  
  5.     // LAB 3: Your code here.  
  6.     int i=0;  
  7.     for(i=0;i<=NENV-1;i++)  
  8.     {  
  9.         envs[i].env_id=0;  
  10.         if(i!=NENV-1)  
  11.         {  
  12.         envs[i].env_link=&envs[i+1];  
  13.         }  
  14.     }  
  15.     env_free_list=envs;  
  16.     // Per-CPU part of the initialization  
  17.     env_init_percpu();  
  18. }  

(4)初始化進程虛擬地址空間
完成env_setup_vm函數。不同的進程有不同的虛擬地址空間,進而就必須有自己的頁目錄和頁表,該函數的任務就是初始化頁目錄。
首先分配一個空閒頁當做頁目錄,然後將這個頁目錄映射內核地址空間(UTOP之上的部分),不需要映射頁表因爲可以和內核共用頁表。
接着增加分配的物理頁的引用(JOS設計的一個很不美的地方),將此頁的虛擬地址賦值給進程的pgdir,然後使進程有權限操作自己的pgdir。
  1. static int  
  2. env_setup_vm(struct Env *e)  
  3. {  
  4.     int i;  
  5.     struct Page *p = NULL;  
  6.     cprintf("env_setup_vm\r\n");  
  7.     // Allocate a page for the page directory  
  8.     if (!(p = page_alloc(ALLOC_ZERO)))  
  9.         return -E_NO_MEM;  
  10.     // LAB 3: Your code here.  
  11.     e->env_pgdir=page2kva(p);  
  12.     for(i=PDX(UTOP);i<1024;i++)  
  13.     {  
  14.         e->env_pgdir[i]=kern_pgdir[i];  
  15.     }  
  16.     p->pp_ref++;  
  17.     // UVPT maps the env's own page table read-only.  
  18.     // Permissions: kernel R, user R  
  19.     e->env_pgdir[PDX(UVPT)] = PADDR(e->env_pgdir) | PTE_P | PTE_U;  
  20.   
  21.     return 0;  
  22. }

(5)輔助映射函數
當進行加載二進制代碼的時候,需要一個輔助函數region_alloc,其作用是映射虛擬地址va及之後的len字節到進程e的虛擬地址空間裏。
實現比較簡單,首先將va向下4K對齊,va+len向上4k對齊,以保證分配的是整數頁面。之後一個頁面一個頁面分配即可。
爲了使頁面用戶態可寫,需要權限PTE_W|PTE_U。
  1. static void  
  2. region_alloc(struct Env *e, void *va, size_t len)  
  3. {  
  4.     // LAB 3: Your code here.  
  5.     void* i;  
  6.     cprintf("region_alloc %x,%d\r\n",e->env_pgdir,len);  
  7.     for(i=ROUNDDOWN(va,PGSIZE);i<ROUNDUP(va+len,PGSIZE);i+=PGSIZE)  
  8.     {  
  9.         struct Page* p=(struct Page*)page_alloc(1);  
  10.         if(p==NULL)  
  11.             panic("Memory out!");  
  12.         page_insert(e->env_pgdir,p,i,PTE_W|PTE_U);  
  13.     }  
  14.   
  15. }  

(6)進程代碼加載
通過load_icode給相應的進程加載可執行代碼。可執行代碼的格式是elf格式,因此使用類似加載kernel的方式將代碼加載到相應內存中。
因爲當前JOS沒有文件系統,所以用戶態的程序是編譯在kernel裏面的,通過編譯器的導出符號來進行訪問,通過追蹤init.c裏的相關代碼也可以說明這一點。當然在這裏我們無需更多的關注這個可執行代碼目前在哪裏,只需要完成其加載機制即可。
爲了往進程對應的虛擬空間映射到的物理內存中寫數據,首先必須要加載進程相應的頁目錄,當然必須使用此函數外的邏輯來保證在調用此函數之前頁目錄是已經初始化過的。接着仿照kernel的方式去分析elf文件,加載類型爲ELF_PROG_LOAD的段到其要求的虛擬地址中,使用之前完成的輔助函數來方便的進行地址的映射。最後將程序入口放入進程的eip中(後面會有解釋),並映射進程堆棧(個人認爲堆棧映射應該放在env_setup_vm會更合乎邏輯一些),然後重新加載kern_pgdir,函數返回。
  1. static void  
  2. load_icode(struct Env *e, uint8_t *binary, size_t size)  
  3. {  
  4.     // LAB 3: Your code here.  
  5.     lcr3(PADDR(e->env_pgdir));  
  6.     cprintf("load_icode\r\n");  
  7.     struct Elf * ELFHDR=(struct Elf *)binary;  
  8.     struct Proghdr *ph, *eph;  
  9.     int i;  
  10.     if (ELFHDR->e_magic != ELF_MAGIC)  
  11.             panic("Not a elf binary");  
  12.   
  13.   
  14.         ph = (struct Proghdr *) ((uint8_t *) ELFHDR + ELFHDR->e_phoff);  
  15.         eph = ph + ELFHDR->e_phnum;  
  16.         for (; ph < eph; ph++)  
  17.         {  
  18.             // p_pa is the load address of this segment (as well  
  19.             // as the physical address)  
  20.             if(ph->p_type==ELF_PROG_LOAD)  
  21.             {  
  22.             cprintf("load_prog %d\r\n",ph->p_filesz);  
  23.             region_alloc(e,(void*)ph->p_va,ph->p_filesz);  
  24.             char* va=(char*)ph->p_va;  
  25.             for(i=0;i<ph->p_filesz;i++)  
  26.             {  
  27.   
  28.                 va[i]=binary[ph->p_offset+i];  
  29.             }  
  30.   
  31.             }  
  32.         }  
  33.         e->env_tf.tf_eip=ELFHDR->e_entry;  
  34.   
  35.     // Now map one page for the program's initial stack  
  36.     // at virtual address USTACKTOP - PGSIZE.  
  37.   
  38.     // LAB 3: Your code here.  
  39.         struct Page* p=(struct Page*)page_alloc(1);  
  40.      if(p==NULL)  
  41.          panic("Not enough mem for user stack!");  
  42.      page_insert(e->env_pgdir,p,(void*)(USTACKTOP-PGSIZE),PTE_W|PTE_U);  
  43.      cprintf("load_icode finish!\r\n");  
  44.      lcr3(PADDR(kern_pgdir));  

(7)進程建立
使用上述完成的函數來建立進程。
完成env_create函數,首先分配一個進程描述符,然後加載可執行代碼,邏輯很簡單。
  1. void  
  2. env_create(uint8_t *binary, size_t size, enum EnvType type)  
  3. {  
  4.     // LAB 3: Your code here.  
  5.   
  6.     struct Env* env;  
  7.   
  8.     if(env_alloc(&env,0)==0)  
  9.     {  
  10.         env->env_type=type;  
  11.         load_icode(env, binary,size);  
  12.     }  
  13.   
  14. }  

(8)進程執行
完成env_run函數來運行進程。
首先設置當前進程的一些信息,然後更改當前進程指針指向要運行的進程,之後加載進程頁目錄跳轉到使用env_pop_tf真正使進程執行。
  1. void  
  2. env_run(struct Env *e)  
  3. {  
  4.    
  5.     // LAB 3: Your code here.  
  6.     cprintf("Run env!\r\n");  
  7.     if(curenv!=NULL)  
  8.     {  
  9.         if(curenv->env_status==ENV_RUNNING)  
  10.         {  
  11.             curenv->env_status=ENV_RUNNABLE;  
  12.         }  
  13.     }  
  14.     curenv=e;  
  15.     e->env_status=ENV_RUNNING;  
  16.     e->env_runs++;  
  17.     lcr3(PADDR(e->env_pgdir));  
  18.     env_pop_tf(&e->env_tf);  
  19. }  
接着分析env_pop_tf。
env_pop_tf首先將傳入的trapframe,包含所有的寄存器信息壓棧,然後使用iret,即中斷返回來執行。
  1. void  
  2. env_pop_tf(struct Trapframe *tf)  
  3. {  
  4.     __asm __volatile("movl %0,%%esp\n"  
  5.         "\tpopal\n"  
  6.         "\tpopl %%es\n"  
  7.         "\tpopl %%ds\n"  
  8.         "\taddl $0x8,%%esp\n" /* skip tf_trapno and tf_errcode */  
  9.         "\tiret"  
  10.         : : "g" (tf) : "memory");  
  11.     panic("iret failed");  /* mostly to placate the compiler */  
  12. }  

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放入進程代碼入口地址的原因。


做完這些之後,第一個進程就能運行起來了,但立刻又崩潰了,因爲沒有處理系統調用,而系統調用相關的實現就是下篇日誌的內容了。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章