mit6.828筆記 - lab3 Part A:用戶進程和異常處理

来源:https://www.cnblogs.com/toso/p/17810689.html
-Advertisement-
Play Games

簡單回顧 在開始 lab3 的學習之前,我們先簡單回顧下 到目前為止,我們的內核能做了什麼: lab1中,我們學習了 PC啟動的過程,看到BIOS將我們編寫的boot loader 載入記憶體,然後通過bootloader 將內核載入記憶體。同時,使用了一個寫死的臨時頁表(entry_pgdir)完成了 ...


簡單回顧

在開始 lab3 的學習之前,我們先簡單回顧下 到目前為止,我們的內核能做了什麼:

lab1中,我們學習了 PC啟動的過程,看到BIOS將我們編寫的boot loader 載入記憶體,然後通過bootloader 將內核載入記憶體。同時,使用了一個寫死的臨時頁表(entry_pgdir)完成了簡單的地址映射;我們的內核最後執行monitor函數(一個簡單的shell),這是個看起來像是xxx管理系統的C語言課程設計程式,他接收命令行輸入,將輸入解析成命令,並逐個調用相關函數。

但是,問題在於,這樣簡單的頁表,只能映射4MB大小的物理記憶體,如果我們的內核代碼增加了(更不用說載入用戶進程了),4MB不夠用了,就直接G了,因此發展內核的當務之急就是解決記憶體的生存空間危機。因此lab2中,我們學習瞭如何通過 pageinfo 構成的數組pages 和 鏈表 page_free_list 來管理物理記憶體;然後學習了頁表的映射原理,並編寫代碼實現了增刪查改頁表,達到pageinfo和pte之間的映射和取消映射。擁有了這樣的基礎設施,我們可以將所有物理記憶體全部利用起來。

但是到現在為止,我們的JOS的功能還是只有一個簡單的monitor,無法載入用戶進程(或者說,載入運行其他的可執行文件)。為了能夠實現載入用戶進程,在lab3中,我們要實現進程載入、調度的基礎設施。

lab3主要內容是

  1. 完成進程管理的初始化
  2. 完成中斷管理的初始化
  3. 完成系統調用的中斷處理
  4. 完成記憶體保護

lab3 新增的代碼源文件如下,沒必要一開始就全看,跟著手冊遇到什麼看什麼,最後自然就看完了。

目錄 文件 備註
inc/ env.h Public definitions for user-mode environments
trap.h Public definitions for trap handling
syscall.h Public definitions for system calls from user environments to the kernel
lib.h Public definitions for the user-mode support library
kern/ env.h Kernel-private definitions for user-mode environments
env.c Kernel code implementing user-mode environments
trap.h Kernel-private trap handling definitions
trap.c Trap handling code
trapentry.S Assembly-language trap handler entry-points
syscall.h Kernel-private definitions for system call handling
syscall.c System call implementation code
lib/ Makefrag Makefile fragment to build user-mode library, obj/lib/libjos.a
entry.S Assembly-language entry-point for user environments
libmain.c User-mode library setup code called from entry.S
syscall.c User-mode system call stub functions
console.c User-mode implementations of putchar and getchar, providing console I/O
exit.c User-mode implementation of exit
panic.c User-mode implementation of panic
user/ * Various test programs to check kernel lab 3 code

Part A 用戶進程和異常處理

就像使用 pages 數組管理物理記憶體一樣,JOS 使用 envs 數組管理所有的進程。在 lab3 中,我們的目標是載入、運行一個用戶環境,但是一個操作系統當然要處理多個進程了,不過這是 lab4要做的事情了,現在我們要做的是熟悉JOS 維護進程的數據結構和相應的函數。

kern/env.c 中看到的,內核維護著三個與環境有關的主要全局變數:

struct Env *envs = NULL;		// All environments
struct Env *curenv = NULL;		// The current env
static struct Env *env_free_list;	// Free environment list

一旦 JOS 啟動並運行,envs 指針就會指向一個代表系統中所有環境的 Env 結構數組。在我們的設計中,JOS 內核最多可同時支持 NENV 個活動環境,不過在任何時候運行的環境通常都要少得多。(NENV 是一個在 inc/env.h#define 的常量。)分配完畢後,envs 數組將包含一個 Env 數據結構實例,用於表示每個 NENV 可能的環境。

JOS 內核會將所有不活動的 Env 結構保存在 env_free_list 中。這種設計可以方便地分配和取消分配環境,因為只需將它們添加到空閑列表或從空閑列表中移除即可。這和 page_free_list 異曲同工。

核使用 curenv 符號隨時跟蹤當前正在執行的環境。在啟動過程中,在第一個環境開始運行之前,curenv 初始化為 NULL

現在我們要先熟悉 env 結構體,其位於 inc/env.h

struct Env {
	struct Trapframe env_tf;	// 保存的寄存器
	struct Env *env_link;		// 下一個空閑的進程
	envid_t env_id;			// 進程的唯一標識符
	envid_t env_parent_id;		// 該進程的父進程的 env_id
	enum EnvType env_type;		// 用於標識是否是特殊的系統進程
	unsigned env_status;		// 進程狀態
	uint32_t env_runs;		// 進程運行次數

	// Address space
	pde_t *env_pgdir;		// Kernel virtual address of page dir
};

可以看到,比較關鍵的有進程id、進程狀態、特殊進程標識,這些在 inc/env.h 中都有定義。比較令人迷惑的是這個運行次數,暫時不知道是什麼含義。先來看看進程ID的定義:

typedef int32_t envid_t;

// An environment ID 'envid_t' has three parts:
//
// +1+---------------21-----------------+--------10--------+
// |0|          Uniqueifier             |   Environment    |
// | |                                  |      Index       |
// +------------------------------------+------------------+
//                                       \--- ENVX(eid) --/
//
// The environment index ENVX(eid) equals the environment's index in the
// 'envs[]' array.  The uniqueifier distinguishes environments that were
// created at different times, but share the same environment index.
//
// All real environments are greater than 0 (so the sign bit is zero).
// envid_ts less than 0 signify errors.  The envid_t == 0 is special, and
// stands for the current environment.

#define LOG2NENV		10
#define NENV			(1 << LOG2NENV)
#define ENVX(envid)		((envid) & (NENV - 1))

可以看到 env_id 本質上就是 32位整數,然後低10位代表其在envs中的下標,之後21位用來區分不同時間被創建的進程。這裡還是有點迷惑,一個進程被多次載入後,為啥要共用相同的 environment index

進程 status

這裡直接看手冊就行了,手冊帶著介紹了下 struct env以及相關知識。

分配進程數組

Exercise 1

練習 1. 修改 `kern/pmap.c` 中的 `mem_init()` ,分配並映射 `envs` 數組。該數組由 `Env` 結構的 `NENV` 實例組成,分配方式與分配頁面數組類似。與頁面數組一樣,支持 `envs` 的記憶體也應在 `UENVS`(定義於 `inc/mlayout.h` )處映射為用戶只讀,這樣用戶進程才能讀取該數組。

你應該運行代碼並確保 `check_kern_pgdir()` 成功。
 [[lab3 - 翻譯#^988084]]

這裡和 lab2 的對pages的處理是一樣的:

envs = (struct Env *)boot_alloc(sizeof(struct Env) * NENV);
memset(envs, 0, sizeof(struct Env) * NENV);
//...
boot_map_region(kern_pgdir, UENVS, PTSIZE, PADDR(envs), PTE_U | PTE_P);

經過 mem_init 之後記憶體映射情況如下:

image.png


創建並運行進程

經過 練習1,我們有了進程管理的基本數據結構,但是沒有初始化和維護這些數據結構的代碼,接下來就是實現這一切的重頭戲,kern/env.c 中編寫運行用戶環境所需的代碼

因為我們還沒有文件系統,所以我們 將設置內核來載入嵌入在內核本身中的靜態二進位映像 。JOS將該二進位文件作為ELF可執行映像嵌入內核。但是,如何將可執行文件嵌入到內核里呢?這種活當然是鏈接器來幹了,看看kern/Makefrag鏈接器命令行上的 -b 二進位選項會將這些文件作為 "原始 "未解釋的二進位文件鏈接進去,而不是編譯器生成的普通 .o 文件。

image.png

Paart A 的目標就是將用戶的進程運行起來,之前,我們的內核在最終會迴圈調用monitor。現在發生了一些變化:

void
i386_init(void)
{
	extern char edata[], end[];

	// Before doing anything else, complete the ELF loading process.
	// Clear the uninitialized global data (BSS) section of our program.
	// This ensures that all static/global variables start out zero.
	memset(edata, 0, end - edata);

	// Initialize the console.
	// Can't call cprintf until after we do this!
	cons_init();

	cprintf("6828 decimal is %o octal!\n", 6828);

	// Lab 2 memory management initialization functions
	mem_init();

	// Lab 3 user environment initialization functions
	env_init();
	trap_init();

#if defined(TEST)
	// Don't touch -- used by grading script!
	ENV_CREATE(TEST, ENV_TYPE_USER);
#else
	// Touch all you want.
	ENV_CREATE(user_hello, ENV_TYPE_USER);
#endif // TEST*

	// We only have one user environment for now, so just run it.
	env_run(&envs[0]);
}

在mem_init後又出現了 env_init、trap_init,然後就是用於創建進程的 ENV_CREATE,最後由 env_run將用戶進程執行起來,monitor被去掉了。
ENV_CREATE是個巨集,在這裡他的含義對 user_hello 這個用戶進程調用 env_create。
env_create(_binary_obj_user_hello_start, ENV_TYPE_USER)
該巨集定義於 kern/env.h :

#define ENV_PASTE3(x, y, z) x ## y ## z

#define ENV_CREATE(x, type)						\
	do {								\
		extern uint8_t ENV_PASTE3(_binary_obj_, x, _start)[];	\
		env_create(ENV_PASTE3(_binary_obj_, x, _start),		\
			   type);					\
	} while (0)

#endif // !JOS_KERN_ENV_H

Excercise 2

練習 2.在 env.c 文件中,完成下列函數的編碼:

env_init()
初始化 envs 數組中的所有 Env 結構,並將它們添加到 env_free_list 中。同時調用 env_init_percpu ,該函數用於為許可權級別 0(內核)和許可權級別 3(用戶)配置獨立的分段硬體。
env_setup_vm()
為新環境分配頁面目錄,並初始化新環境地址空間的內核部分。
region_alloc()
為環境分配和映射物理記憶體
load_icode()
您需要解析 ELF 二進位映像,就像 Boot Loader 已經做的那樣,並將其內容載入到新環境的用戶地址空間。
env_create()
用 env_alloc 分配一個環境,然後調用 load_icode 將 ELF 二進位文件載入到該環境中。
env_run()
啟動以用戶模式運行的給定環境。
在編寫這些函數時,您可能會發現新的 cprintf verb %e 非常有用--它會列印出與錯誤代碼相對應的描述。例如

r = -E_NO_MEM;
panic("env_alloc: %e", r);
就會出現 "env_alloc: 記憶體不足 "的提示。

env_init

就像 page_init 一樣將 envs 數組初始化

// 將 “envs ”中的所有環境標記為空閑環境,
// 將它們的 env_ids 設置為 0,並將它們插入 env_free_list 中。
// 確保環境在空閑列表中的順序與它們在 envs 數組中的順序一致(
// 也就是說,這樣第一次調用 env_alloc()時就會返回 envs[0])。
//
void
env_init(void)
{
	// Set up envs array
	// LAB 3: Your code here.
	env_free_list = NULL;
	for(int i = NENV-1;i>=0;--i){
		envs[i].env_status = ENV_FREE;
		envs[i].env_id = 0;
		envs[i].env_link = env_free_list;
		env_free_list = &envs[i];
	}
	// Per-CPU part of the initialization
	env_init_percpu();  //載入 GDT 和段描述符。
}

註意 for 迴圈必須是從 NENV 至 0 進行遍歷。這樣才能保證 env_free_list 的循序和 envs 數組中的順序一致。

這裡看一眼 env_init_percpu()是乾什麼的

// 載入 GDT 和段描述符。
void
env_init_percpu(void)
{
	lgdt(&gdt_pd);
	// 內核從不使用 GS 或 FS,因此我們將其設置為用戶數據段。
	asm volatile("movw %%ax,%%gs" : : "a" (GD_UD|3));
	asm volatile("movw %%ax,%%fs" : : "a" (GD_UD|3));
	// 內核會使用 ES、DS 和 SS。 我們將根據需要在內核和用戶數據段之間進行切換。
	asm volatile("movw %%ax,%%es" : : "a" (GD_KD));
	asm volatile("movw %%ax,%%ds" : : "a" (GD_KD));
	asm volatile("movw %%ax,%%ss" : : "a" (GD_KD));
	// 將內核文本段載入 CS。
	asm volatile("ljmp %0,$1f\n 1:\n" : : "i" (GD_KT));
	// 為了穩妥起見,清除本地描述符表(LDT),因為我們不使用它。
	lldt(0);
}

env_setup_vm

// 為環境 e 初始化內核虛擬記憶體佈局。
// 分配一個頁面目錄,相應設置 e->env_pgdir,並初始化新進程地址空間的內核部分。
// 暫時不要將任何內容映射到環境虛擬地址空間的用戶部分。
//
// 成功時返回 0,錯誤時返回 <0。 錯誤包括
//	-E_NO_MEM 如果頁面目錄或表無法分配。
//
static int
env_setup_vm(struct Env *e)
{
	int i;
	struct PageInfo *p = NULL;

	// 為頁面目錄分配頁面
    // 由於我們在構造一個新的pgdir,而不是向已經存在的kern_pgdir中插入 pde,或插入增加映射
    // 我們不能使用 page_insert、pgdir_walk等用於頁表管理的方法,
    // 只能通過 物理記憶體管理的方法,申請物理頁,並手動調整七計數
	if (!(p = page_alloc(ALLOC_ZERO)))
		return -E_NO_MEM;

	// 現在,設置 e->env_pgdir 並初始化頁面目錄。
	//
	// 提示:
	// 所有環境的 VA 空間在 UTOP 以上是相同的(UVPT 除外,我們在下麵設置)。
	// 有關許可權和佈局,請參見 inc/memlayout.h。
	// 能否將 kern_pgdir 用作模板? 提示:可以。
	// (確保您在lab 2 中正確設置了許可權)。
	// - UTOP 下麵的初始 VA 是空的。
	// - 你不需要再調用 page_alloc。
	// - 註意:一般情況下,pp_ref 不會被維護。
	// 但 env_pgdir 是個例外 -- 你需要增加 env_pgdir 的 pp_ref 才能使 env_free 正常工作。
	// - kern/pmap.h 中的函數非常方便。

	// LAB 3: Your code here.
	
	e->env_pgdir = page2kva(p);
	memcpy(e->env_pgdir, kern_pgdir, PGSIZE);
	// UVPT 將環境自身的頁表映射為只讀。
	p->pp_ref ++;
	// Permissions: kernel R, user R
	e->env_pgdir[PDX(UVPT)] = PADDR(e->env_pgdir) | PTE_P | PTE_U;// p在此時被映射到UVPT
	p->pp_ref ++;//由於直接調用了page_alloc,需要手動計數
	return 0;
}

env_setup_vm 會在初始化一個 env 的時候發揮作用,這裡看看已經寫好的 env_alloc

env_alloc
 // 分配並初始化一個新環境。
// 成功後,新環境將存儲在 *newenv_store 中。
//
// 成功時返回 0,失敗時返回 <0。 錯誤包括
// -E_NO_FREE_ENV 如果所有 NENV 環境都已分配完畢
// 記憶體耗盡時返回 E_NO_MEM
//
int
env_alloc(struct Env **newenv_store, envid_t parent_id)
{
	int32_t generation;
	int r;
	struct Env *e;

	if (!(e = env_free_list))//從空閑列表中取下一個 env
		return -E_NO_FREE_ENV;

	// 為該環境分配和設置頁面目錄。
	if ((r = env_setup_vm(e)) < 0)
		return r;

	// 為該環境生成 env_id。
	generation = (e->env_id + (1 << ENVGENSHIFT)) & ~(NENV - 1);
	if (generation <= 0)	// Don't create a negative env_id.
		generation = 1 << ENVGENSHIFT;
	e->env_id = generation | (e - envs);

	// 設置基本狀態變數。
	e->env_parent_id = parent_id;
	e->env_type = ENV_TYPE_USER;
	e->env_status = ENV_RUNNABLE;
	e->env_runs = 0;

	// 清除所有已保存的寄存器狀態,
	// 以防止該 Env 結構中先前環境的寄存器值 “泄漏 ”到我們的新環境中。
	memset(&e->env_tf, 0, sizeof(e->env_tf));

	// 為段寄存器設置適當的初始值。
	// GD_UD 是 GDT 中的用戶數據段選擇器,
	// GD_UT 是用戶文本段選擇器(參見 inc/memlayout.h)。
	// 每個段寄存器的低 2 位包含請求者許可權級別(RPL);3 表示用戶模式。 
	// 當我們切換許可權級別時,硬體會對 RPL 和存儲在描述符中的
	// 描述符許可權級別(DPL)進行各種檢查。
	e->env_tf.tf_ds = GD_UD | 3;
	e->env_tf.tf_es = GD_UD | 3;
	e->env_tf.tf_ss = GD_UD | 3;
	e->env_tf.tf_esp = USTACKTOP;
	e->env_tf.tf_cs = GD_UT | 3;
	// You will set e->env_tf.tf_eip later.

	// commit the allocation
	env_free_list = e->env_link;
	*newenv_store = e;

	cprintf("[%08x] new env %08x\n", curenv ? curenv->env_id : 0, e->env_id);
	return 0;
}

region_alloc()

//
// 為環境 env 分配 len 位元組的物理記憶體,並將其映射到環境地址空間中的虛擬地址 va。
// 不會以任何方式將映射頁清零或初始化。
// 頁面應可被用戶和內核寫入。
// 如果任何分配嘗試失敗,就會panic。
//
static void
region_alloc(struct Env *e, void *va, size_t len)
{
	// 實驗 3:此處為您的代碼。
	// (但僅限於 load_icode 需要時)。
	// 提示:如果調用者可以傳遞非頁面對齊的'va'和'len'值,則使用 region_alloc 會更容易。
	// 應該將 va 向下舍入,將 (va + len) 向上舍入。
	// (註意拐角情況!)。
	void *begin = ROUNDDOWN(va, PGSIZE), *end = ROUNDUP(va+len, PGSIZE);
	while(begin < end){
		struct PageInfo *pp = page_alloc(ALLOC_ZERO);
		if(!pp){
			panic("region_alloc failed\n");
		}
		page_insert(e->env_pgdir, pp, begin, PTE_P | PTE_W | PTE_U);
		begin += PGSIZE;
	}
}

load_icode

細節看註釋,需要強調的一點是:

在載入各個段時需要切換到這個進程的頁表目前進程的頁表內容和 kern_pgdir 是一樣的(UVPT除外)
因此,使用進程的頁表一樣能訪問到鏈接的二進位文件(這裡的 elfhdr)
為什麼要切換呢?每個進程都有自己的頁表,將物理記憶體中的代碼數據映射到自己獨立的地址空間

//
// 為用戶進程設置初始程式二進位文件、堆棧和處理器標誌。
// 該函數只在內核初始化期間,即運行第一個用戶模式環境之前調用。
//
// 該函數將 ELF 二進位映像中的所有可載入段載入到環境的用戶記憶體中,從 ELF 程式頭中指示的相應虛擬地址開始。
// 同時,它將程式頭中標記為映射但實際上不存在於 ELF 文件中的任何部分(即程式的 bss 部分)清零。
// 除了 Boot Loader 還需要從磁碟讀取代碼外,所有這些都與 Boot Loader 的工作非常相似。 看看 boot/main.c 就會明白。
//
// 最後,這個函數為程式的初始堆棧映射了一個頁面。
//
// load_icode 在遇到問題時會panic
// - load_icode 怎麼會失敗? 給定的輸入可能有什麼問題?

static void
load_icode(struct Env *e, uint8_t *binary)
{
	// 提示:
	// 按照 ELF 程式段頭指定的地址將每個程式段載入到虛擬記憶體中。
	// 只應載入 ph->p_type == ELF_PROG_LOAD 的程式段。
	// 每個程式段的虛擬地址可以在 ph->p_va 中找到,其在記憶體中的大小可以在 ph->p_memsz 中找到。
	// ELF 二進位文件中從 “binary + ph->p_offset ”開始的 ph->p_filesz 位元組應複製到虛擬地址 ph->p_va。 剩餘的記憶體位元組應清零。
	// (ELF 頭應該是 ph->p_filesz <= ph->p_memsz。)
	// 使用前一個lab中的函數分配和映射頁面。
	//
	// 目前所有頁面保護位都應為用戶讀/寫。
	// ELF 程式段不一定是頁面對齊的,但在本函數中可以假設沒有兩個程式段會接觸同一個虛擬頁面。
	//
	// 你可能會發現 region_alloc 這樣的函數很有用。
	//
	// 如果能直接將數據移動到存儲在 ELF 二進位文件中的虛擬地址,載入段就會簡單得多。
	// 那麼在執行此函數時,哪個頁面目錄應該有效呢?
	//
	// 你還必須對程式的入口點做一些處理,以確保環境在那裡開始執行。
	// 參見下麵的 env_run() 和 env_pop_tf())。

	// 實驗 3:你的代碼在這裡。
	struct Elf  * elfhdr = (struct Elf *) binary;
	struct Proghdr *ph = (struct Proghdr *) ((uint8_t *) elfhdr + elfhdr->e_phoff);

	if (elfhdr->e_magic != ELF_MAGIC) {
		panic("binary is not ELF format\n");
	}

	ph = (struct Proghdr *) ((uint8_t *) elfhdr + elfhdr->e_phoff);
	int ph_num = elfhdr->e_phnum;
	// 接下來需要切換到這個 進程的頁表,目前進程的頁表內容和 kern_pgdir 是一樣的(UVPT除外)
	// 因此,使用進程的頁表一樣能訪問到鏈接的二進位文件(這裡的 elfhdr)
	// 為什麼要切換呢?每個進程都有自己的頁表,將物理記憶體中的代碼數據映射到自己獨立的地址空間
	lcr3(PADDR(e->env_pgdir));

	for(int i = 0; i < ph_num; i++){
		if(ph[i].p_type == ELF_PROG_LOAD){
			region_alloc(e, (void *)ph[i].p_va, ph[i].p_memsz);
			memset((void*) ph[i].p_va, 0, ph[i].p_memsz);
			memcpy((void*) ph[i].p_va, binary + ph[i].p_offset, ph[i].p_filesz);
		}
	}
	
	lcr3(PADDR(kern_pgdir));

	e->env_tf.tf_eip = elfhdr->e_entry;//在 env_alloc 中,唯獨 eip 沒有初始化

	// 現在為程式的初始堆棧映射一個頁面
	// 虛擬地址 USTACKTOP - PGSIZE.
	region_alloc(e, (void *)(USTACKTOP  - PGSIZE), PGSIZE);
}

關於 Elf 結構體的 p_memsz 和 p_filesz:

如果一個段是可裝載段PT_LOADProgram Header Type-Loadable):表明本程式頭指向一個可裝載的段。段的內容會被從文件中拷貝到記憶體中。段在文件中的大小是 p_filesz,在記憶體中的大小是 p_memsz。如果 p_memsz 大於 p_filesz,在記憶體中多出的存儲空間應填 0 補充,也就是說,段在記憶體中可以比在文件中占用空間更大;而相反,p_filesz 永遠不應該比 p_memsz 大,因為這樣的話,記憶體中就將無法完整地映射段的內容。

env_create()

// 使用 env_alloc 分配一個新環境,
// 使用 load_icode 將命名的精靈二進位文件載入其中,並設置其 env_type。
// 只有在運行第一個用戶模式環境之前,內核初始化過程中才會調用該函數。
// 新環境的父 ID 被設置為 0。
void env_create(uint8_t *binary, enum EnvType type)
{
	// LAB 3: Your code here.
	struct Env * new_env;
	envid_t parent_id = 0;
	int r = env_alloc(&new_env, parent_id);
	if(r< 0)
		panic("env_create error: %e", r);
	load_icode(new_env, binary);

	new_env->env_type = type;//順帶一體,env_alloc 預設已將type設為 ENV_TYPE_USER
}

然後看一看已經寫好的 env_free()

env_free

釋放 env e 及其使用的所有記憶體。
在看代碼之前,我們想一想,一個已經 env_create 的 env 占用了哪些物理記憶體?

  • 首先是e env 的 env_pgdir 自身占用的頁面
  • 然後是代碼、數據、棧占用的頁面
    為了釋放掉所有這些記憶體,首先應該處理後者(因為這個過程依賴前者)
void
env_free(struct Env *e)
{
	pte_t *pt;
	uint32_t pdeno, pteno;
	physaddr_t pa;

	//  如果釋放當前環境,則在釋放頁面目錄之前切換到 kern_pgdir,以防頁面被重覆使用。
	if (e == curenv)
		lcr3(PADDR(kern_pgdir));

	// Note the environment's demise.
	cprintf("[%08x] free env %08x\n", curenv ? curenv->env_id : 0, e->env_id);

	// 清空地址空間用戶部分的所有映射頁
	static_assert(UTOP % PTSIZE == 0);
	for (pdeno = 0; pdeno < PDX(UTOP); pdeno++) {

		// only look at mapped page tables
		if (!(e->env_pgdir[pdeno] & PTE_P))
			continue;

		// 查找頁表的 pa 和 va
		pa = PTE_ADDR(e->env_pgdir[pdeno]);
		pt = (pte_t*) KADDR(pa);

		// unmap all PTEs in this page table
		for (pteno = 0; pteno <= PTX(~0); pteno++) {
			if (pt[pteno] & PTE_P)
				page_remove(e->env_pgdir, PGADDR(pdeno, pteno, 0));
		}

		// free the page table itself
		// 一般來說,我們使用 page_remove,但是這裡是對頁表占用的記憶體頁進行釋放
		// 因此不能使用第一參數需要 pgdir 的 page_remove,需要手動處理。
		// 這裡是 env_setup_vm 中的逆過程,env_setup_vm 由於需要構建頁表,
		// 也需要手動調用 page_alloc,並處理計數
		e->env_pgdir[pdeno] = 0;
		page_decref(pa2page(pa));
	}

	// 釋放頁目錄
	pa = PADDR(e->env_pgdir);
	e->env_pgdir = 0;
	page_decref(pa2page(pa));

	// return the environment to the free list
	e->env_status = ENV_FREE;
	e->env_link = env_free_list;
	env_free_list = e;
}

需要註意一點,在執行 env_free 代碼時,應該使用kern_pgdir,
但是 kern_pgdir 中沒有 env e 的映射,所以需要先用 env e 的 env_pgdir 來獲取物理地址,
然後利用 KADDR 巨集從地址空間頂部的 物理記憶體映射區訪問。
回憶一下lab2中,我們在mem_init里寫過:

//////////////////////////////////////////////////////////////////////
// 將所有物理記憶體映射到 KERNBASE。
// 也就是說,VA 範圍 [KERNBASE, 2^32) 應該映射到 PA 範圍 [0, 2^32 - KERNBASE)
// 我們可能沒有 2^32 - KERNBASE 位元組的物理記憶體,但我們還是設置了映射。
// 許可權:內核 RW,用戶 NONE
// Your code goes here:
boot_map_region(kern_pgdir, KERNBASE, 0xffffffff - KERNBASE, 0, PTE_W);

註意,雖然每個進程都有頂部的 物理記憶體映射區 的頁表,但是他們沒有許可權讀寫。(這麼方便的功能當然只該有內核有許可權,這也是執行env_free時,使用kern_pgdir的原因之一)

除此之外,我們需要註意 env_free 中需要手動調用 page_decref。lab2 和 lab3 代碼中更多的是調用封裝好的 page_insert(不用處理 計數遞增)和 page_remove (不用處理 計數遞減)。但是一旦涉及到對頁表本身占用的物理頁做增刪處理時,就需要手動調用 page_allocpage_decref 處理計數。
(比如 pgdir_walk(頁表二級遍歷時,可能需要訪問尚未分配的 pte_table)、env_create(用戶進程創建頁表)和這裡的 env_free )

env_run

在看代碼之前,我們先想一想,讓一個進程 Env e 運行起來,大概需要做那些事呢?
首先,想到的就是恢復這個進程的寄存器,讓EIP指向繼續執行的代碼,
在那之前還要恢復cr3寄存器,載入 Env e 的頁表

除此之外,我們還要考慮,env_run 是什麼時候調用的,也許此時有一個進程正在運行,curenv指向其他進程。所以要考慮修改 curenv 、以及 Env e 的 status、runs等變數。

// 從 curenv 到 env e 的上下文切換。
// 註意:如果這是第一次調用 env_run,則 curenv 為空。
// 此函數不返回
//
void
env_run(struct Env *e)
{
	// 第 1 步: 如果這是一個上下文切換(一個新環境正在運行):
	// 1.如果當前環境是 ENV_RUNNING,則將當前環境(如果有的話)設置回 ENV_RUNNABLE(想想它還能處於什麼狀態)、
	// 2. 將 “curenv ”設置為新環境、
	// 3. 將其狀態設置為 ENV_RUNNING; // 4、
	// 4. 更新 “env_runs ”計數器、
	// 5.使用 lcr3() 切換到它的地址空間。
	// 第 2 步:使用 env_pop_tf()恢復環境的 // 寄存器,並切換到用戶模式。
	// 寄存器,併進入環境中的用戶模式。

	// 提示:該函數從 e->env_tf 載入新環境的狀態。 回過頭來看看你上面寫的代碼,確保已經將 e->env_tf 的相關部分設置為合理的值。

	// 實驗 3:此處為您的代碼。

	if(curenv != NULL){	//如果這不是第一次調用 env_run ,即,這是一個上下文切換
		if(curenv->env_status == ENV_RUNNING){
			curenv->env_status = ENV_RUNNABLE;
		}
	}

	curenv = e;
	e->env_status = ENV_RUNNING;
	lcr3(e->env_pgdir);
	// 恢復寄存器,這裡直接將覆蓋了當前寄存器狀態
	// 可以想到,當原進程的寄存器狀態,應該已經妥當保存好了
	// 並且在進程切換時負責進行保存,畢竟這裡沒有處理。
	env_pop_tf(&e->env_tf);
}

正如註釋中說的,env_run 如果不是首次調用,說明這是一次進程切換。那麼什麼時候需要切換進程呢?
大概能想到,發生異常、進程調度等,如果是進程調度這種比較平和的方式,那麼curenv 肯定是 ENV_RUNNING了,其他情況暫時等 lab4 學完才能瞭解到。

註意最後一句 env_pop_tf(&e->env_tf); 寄存器狀態恢復,就意味著進程正式運行了,因為eip被改變了。

總結:env.c中的函數關係

image.png
此時, make qemu ,內核就會將 user/hello.c 編譯出來的可執行文件,通過 env_create 創建出來,並通過 env_run 運行起來。在這之前,我們來看一眼這個程式:

// hello, world
#include <inc/lib.h>

void
umain(int argc, char **argv)
{
	cprintf("hello, world\n");
	cprintf("i am environment %08x\n", thisenv->env_id);
}

註意,用戶程式的cprintf的聲明雖然和我們剛剛編程時用的cprintf相同,都來自 inc/stdio.h,但是他們的實現不同:
用vscode 搜索可以發現,有兩個定義
image.png
內核使用的是 kern/printf.c 而 user/hello.c 使用的卻是 lib/printf.c,用戶的 cprintf 會調用到 lib/syscall.c 中的 sys_cputs

image.png

syscall 這個函數的定義如下:

image.png

實際上就是使用了 int 0x30 系統調用,這是一個中斷。這個中斷只能在內核態調用,但是XXX所以 make qemu 會導致三重故障,即:

如果一切順利,系統將進入用戶空間並執行 hello 二進位文件,直到使用 int 指令進行系統調用這時就會出現問題,因為 JOS 沒有設置允許從用戶空間過渡到內核的硬體當中央處理器發現自己的設置不允許處理這個系統調用中斷時,它就會產生一個一般保護異常,發現自己無法處理這個異常後,又會產生一個雙重故障異常,發現自己也無法處理這個異常後,最後就會放棄,這就是所謂的 "三重故障" 。通常情況下,CPU 會重置,系統會重啟。雖然這對傳統應用程式很重要(請參閱本博文中的原因解釋)

就像這個樣子:

image.png

我們用 GDB 在 env_pop_tf() 函數設置斷點,然後通過指令 si,單步調試,觀察 iret 指令前後寄存器的變化。

image.png

image.png

為了能讓用戶進程有能力處理異常,學習如何處理中斷和異常

處理中斷和異常

在這之前,我們需要徹底摸頭 x86中斷和異常機制。練習3的任務就是學習80386的手冊。

Exercise 3

練習 3. 如果還沒有,請閱讀《80386 程式員手冊》第 9 章 "異常和中斷"[Chapter 9, Exceptions and Interrupts](https://pdos.csail.mit.edu/6.828/2018/readings/i386/c09.htm)(或《IA-32 開發人員手冊》第 5 章 [IA-32 Developer's Manual](https://pdos.csail.mit.edu/6.828/2018/readings/ia32/IA32-3A.pdf))。

觀察下TSS

image.png

建立中斷描述符表

JOS 在 trapentry.S 中,為每個異常或中斷設置處理程式,
trap_init() 中用這些處理程式的地址建立 IDT

那這些處理程式具體要做什麼呢?手冊提示我們:

  1. 每個處理程式都應在堆棧上建立一個 struct Trapframe(參見 inc/trap.h
    2.將Trapframe 的地址作為參數調用 trap()(在 trap.c 中)。

先來看看 trapentry.S

trapentry.S

/* TRAPHANDLER 定義了一個全局可見的陷阱處理函數。
 * 它將一個陷阱編號推入堆棧,然後跳轉到 _alltraps。
 * 使用 TRAPHANDLER 來處理 CPU 自動推送錯誤代碼的陷阱。
 *
 * 你不應該在 C 語言中調用 TRAPHANDLER 函數,但你可能需要在 C 語言中聲明一個函數(例如,在 IDT 設置過程中獲取函數指針)。 您可以用以下方式聲明函數
 * void NAME();
 * 其中 NAME 是傳遞給 TRAPHANDLER 的參數。
 */
 
#define TRAPHANDLER(name, num)						\
	.globl name;		/* define global symbol for 'name' */	\
	.type name, @function;	/* symbol type is function */		\
	.align 2;		/* align function definition */		\
	name:			/* function starts here */		\
	pushl $(num);							\
	jmp _alltraps

/* 對於 CPU 不推送錯誤代碼的陷阱,使用 TRAPHANDLER_NOEC。
 * 它會推送一個 0 來代替錯誤代碼,因此陷阱幀在這兩種情況下的格式都是一樣的。
 * 格式。
 */
#define TRAPHANDLER_NOEC(name, num)					\
	.globl name;							\
	.type name, @function;						\
	.align 2;							\
	name:								\
	pushl $0;							\
	pushl $(num);							\
	jmp _alltraps

.text
/*
 * Lab 3: Your code here for generating entry points for the different traps.
 */
// 按照手冊提示,為 /inc/trap.h 中的每一個(lab3是0~31)異常創建處理程式
// 註意,有的異常會在堆棧上推入ErrorCode,有的則沒有,需要參照 x86手冊

	TRAPHANDLER_NOEC(handler_0, T_DIVIDE)
	TRAPHANDLER_NOEC(handler_1, T_DEBUG)
	TRAPHANDLER_NOEC(handler_2, T_NMI)
	TRAPHANDLER_NOEC(handler_3, T_BRKPT)
	TRAPHANDLER_NOEC(handler_4, T_OFLOW)
	TRAPHANDLER_NOEC(handler_5, T_BOUND)
	TRAPHANDLER_NOEC(handler_6, T_ILLOP)
	TRAPHANDLER_NOEC(handler_7, T_DEVICE)
	TRAPHANDLER(handler_8, T_DBLFLT)
	TRAPHANDLER(handler_10, T_TSS)
	TRAPHANDLER(handler_11, T_SEGNP)
	TRAPHANDLER(handler_12, T_STACK)
	TRAPHANDLER(handler_13, T_GPFLT)
	TRAPHANDLER(handler_14, T_PGFLT)
	TRAPHANDLER_NOEC(handler_16, T_FPERR)
	TRAPHANDLER(handler_17, T_ALIGN)
	TRAPHANDLER_NOEC(handler_18, T_MCHK)
	TRAPHANDLER_NOEC(handler_19, T_SIMDERR)
/*
 * Lab 3: Your code here for _alltraps
 */
/*
	按照手冊的提示:
	你的 `_alltraps` 應該
	1. 推值,使堆棧看起來像結構 Trapframe
	2. 將 `GD_KD` 載入到 `%ds` 和 `%es` 中
	3. 推送 `%esp` 以傳遞一個指向 `Trapframe` 的指針作為 `trap()` 的參數
	4. `call trap` ( `trap` 會返回嗎?)
*/
	_alltraps:
	pushl %ds
	pushl %es
	pushal
	pushl $GD_KD
	popl %ds
	pushl $GD_KD
	popl %es
	pushl %esp		
	call trap       

trap_init()

void
trap_init(void)
{
	extern struct Segdesc gdt[];

	// LAB 3: Your code here.
	// 聲明 異常處理函數
	void handler_0();
	void handler_1();
	void handler_2();
	void handler_3();
	void handler_4();
	void handler_5();
	void handler_6();
	void handler_7();
	void handler_8();
	void handler_10();
	void handler_11();
	void handler_12();
	void handler_13();
	void handler_14();
	void handler_16();
	void handler_17();
	void handler_18();
	void handler_19();
	// 通過異常處理函數建立IDT
	SETGATE(idt[0], 0, GD_KT, handler_0, 0);		
	SETGATE(idt[1], 0, GD_KT, handler_1, 0); 
	SETGATE(idt[2], 0, GD_KT, handler_2, 0); 
	SETGATE(idt[3], 0, GD_KT, handler_3, 3);
	SETGATE(idt[4], 0, GD_KT, handler_4, 0);
	SETGATE(idt[5], 0, GD_KT, handler_5, 0);
	SETGATE(idt[6], 0, GD_KT, handler_6, 0);
	SETGATE(idt[7], 0, GD_KT, handler_7, 0);
	SETGATE(idt[8], 0, GD_KT, handler_8, 0);
	SETGATE(idt[10], 0, GD_KT, handler_10, 0);
	SETGATE(idt[11], 0, GD_KT, handler_11, 0);
	SETGATE(idt[12], 0, GD_KT, handler_12, 0);
	SETGATE(idt[13], 0, GD_KT, handler_13, 0);
	SETGATE(idt[14], 0, GD_KT, handler_14, 0);
	SETGATE(idt[16], 0, GD_KT, handler_16, 0);
	SETGATE(idt[17], 0, GD_KT, handler_17, 0);
	SETGATE(idt[18], 0, GD_KT, handler_18, 0);
	SETGATE(idt[19], 0, GD_KT, handler_19, 0);

	// Per-CPU setup 
	trap_init_percpu();
}

來看看這個最後調用的 trap_init_percpu

// 初始化並載入每個 CPU 的 TSS 和 IDT
void
trap_init_percpu(void)
{
	// 設置 TSS,以便在向內核發送陷阱時獲得正確的堆棧。
	// 當我們向內核發送陷阱時。
	ts.ts_esp0 = KSTACKTOP;
	ts.ts_ss0 = GD_KD;
	ts.ts_iomb = sizeof(struct Taskstate);

	// 初始化 gdt 的 TSS 插槽。
	gdt[GD_TSS0 >> 3] = SEG16(STS_T32A, (uint32_t) (&ts),
					sizeof(struct Taskstate) - 1, 0);
	gdt[GD_TSS0 >> 3].sd_s = 0;

	// 載入 TSS 選擇器(與其他分段選擇器一樣,最下麵的三個比特是特殊的,我們將其置 0)
	ltr(GD_TSS0);

	// Load the IDT
	lidt(&idt_pd);
}

這裡的 ts 是用來存儲 TSS 數據的結構體,位於 inc/mmu.h。

實際上,到了這裡,Part A的練習已經完成,簡單小結一下,異常發生後的處理過程

image.png

所以說,trap,帶著 Trapframe 究竟做了什麼呢,在進入 Part B 之前必要將這一切弄明白

JOS 的中斷處理過程

先來看看 trap()做了什麼

trap

void
trap(struct Trapframe *tf)
{
	// 環境可能已經設置了 DF,
	// 某些版本的 GCC 依賴於 DF 的明確性
	asm volatile("cld" ::: "cc");

	// 檢查中斷是否被禁用。 
	// 如果斷言失敗,切勿試圖通過在中斷路徑中插入一個 “cli ”來修複。
	assert(!(read_eflags() & FL_IF));

	cprintf("Incoming TRAP frame at %p\n", tf);

	if ((tf->tf_cs & 3) == 3) {
		// 從用戶模式捕獲。
		assert(curenv);

		// 將陷阱幀(當前在堆棧上)複製到 “curenv->env_tf ”中,以便在陷阱點重新開始運行環境。
		curenv->env_tf = *tf;
		// 從現在起,堆棧上的陷阱框架應被忽略。
		tf = &curenv->env_tf;
	}

	// 記錄 tf 是最後一個真正的陷阱幀,以便 print_trapframe 可以列印一些附加信息。
	last_tf = tf;

	// 根據陷阱類型進行調度
	trap_dispatch(tf);

	// 返回當前運行環境。
	assert(curenv && curenv->env_status == ENV_RUNNING);
	env_run(curenv);
}

可以看到,trap 的主要工作就是調用 trap_dispatch 處理tf,然後調用 env_run 將控制交換給用戶進程。trapdispatch是個需要我們後期補全的函數。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 委托與事件 委托 委托的定義 委托是C#中的一種類型,用於存儲對方法的引用。它允許將方法作為參數傳遞給其他方法,實現回調、事件處理和動態調用等功能。通俗來講,就是委托包含方法的記憶體地址,方法匹配與委托相同的簽名,因此通過使用正確的參數類型來調用方法。 委托的特性 引用方法:委托允許存儲對方法的引用, ...
  • C#TMS系統代碼-基礎頁面BaseCity學習 本人純新手,剛進公司跟領導報道,我說我是java全棧,他問我會不會C#,我說大學學過,他說這個TMS系統就給你來管了。外包已經把代碼給我了,這幾天先把增刪改查的代碼背一下,說不定後面就要趕鴨子上架了 Service頁面 //using => impo ...
  • 最小Linux系統,安裝Java環境 想想就生氣,去面試個運維,面試官讓我上機裝個centos7,還是個最小安裝包連界面都沒有,只有命令行模式,我都哭了,然後讓把一些環境裝一下,然後再部署個springboot項目,我他媽都多久沒用沒有界面的東西了,最後卡在安裝MySQL上,真想扇自己個 ...
  • 公司給的伺服器上面啥都沒有,自己動手吧,這種活屬於是乾一次以後都不管的 首先檢查Linux系統是多少位 uname -m 下載JDK Linux版本,打不開官網可以點下麵去鏡像網站 https://repo.huaweicloud.com/java/jdk/8u181-b13/ 下載之後傳到伺服器, ...
  • 「Pygors系列」一句話導讀: MinGW-w64只有編譯器,MSYS2帶著更新環境,WSL2實用性比較高 歷史與淵源 Windows平臺 Linux平臺 二進位相容 WSL2:運行Linux程式 Wine:運行Windows程式 介面相容 CygWin:編譯Linux程式 Winelib:編譯W ...
  • 文章目錄 一、進程 二、線程 三、進程和線程的區別與聯繫 四、一個形象的例子解釋進程和線程的區別 五、進程/線程之間的親緣性 六、協程 一、進程 進程,直觀點說,保存在硬碟上的程式運行以後,會在記憶體空間里形成一個獨立的記憶體體,這個記憶體體有自己獨立的地址空間,有自己的堆,上級掛靠單位是操作系統。操作系 ...
  • 使用Ansible來部署Apache服務是一個很好的選擇,因為它可以自動化部署過程,確保所有的伺服器上都有相同的配置。以下是一個簡單的步驟指南,展示如何使用Ansible來部署Apache服務: 1 安裝ansible 在基於Debian的系統中,你可以使用以下命令來安裝Ansible: sudo ...
  • 設置SSH免密登錄本機主要涉及生成密鑰對、將公鑰複製到本地(或遠程伺服器,如果是雙向免密)以及測試免密登錄等步驟。以下是一個基本的設置流程: 生成密鑰對: 打開終端或命令提示符,並執行以下命令來生成RSA密鑰對:ssh-keygen -t rsa 系統將會提示你指定保存密鑰文件的路徑和文件名。預設情 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...