mit6.828筆記 - lab5(下)- Spawn and Shell

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

Spawning Process 有了文件系統了,我們終於可以方便地讀取磁碟中的文件了。到目前為止,我們創建進程的方法一直都是在編譯內核的時候將程式鏈接到數據段,在 i386_init 通過 ENV_CREATE 巨集創建。 現在我們應該考慮通過文件系統直接將用戶程式從硬碟中讀取出來,spawn 就是 ...


Spawning Process

有了文件系統了,我們終於可以方便地讀取磁碟中的文件了。到目前為止,我們創建進程的方法一直都是在編譯內核的時候將程式鏈接到數據段,在 i386_init 通過 ENV_CREATE 巨集創建。
現在我們應該考慮通過文件系統直接將用戶程式從硬碟中讀取出來,spawn 就是這樣的東西。
spawn和unix中的exec不同,spawn 在用戶空間實現,不需要內核的特殊幫助,讀取文件、創建進程完全通過 syscall。
spawn 已經實現好了,位於 lib/spawn.c 中。很有必要學習一下其中的代碼。

spawn.c

spawn 很像 icode_load,但是他需要通過文件的方式讀取數據。
而且棧的創建、子進程狀態的設置,記憶體映射都需要以syscall的方式實現。

// 從文件系統載入的程式映像中生成一個子進程。
// prog:要運行的程式的路徑名。
// argv: 字元串指針數組的空端指針,這些字元串將作為命令行參數傳遞給子進程。
// 成功時返回子程式 envid,失敗時返回 <0。
int
spawn(const char *prog, const char **argv)
{
	unsigned char elf_buf[512];
	struct Trapframe child_tf;
	envid_t child;

	int fd, i, r;
	struct Elf *elf;
	struct Proghdr *ph;
	int perm;

	// 打開 elf 文件
	if ((r = open(prog, O_RDONLY)) < 0)
		return r;
	fd = r;

	// 讀取 elf文件頭
	elf = (struct Elf*) elf_buf;
	if (readn(fd, elf_buf, sizeof(elf_buf)) != sizeof(elf_buf)
	    || elf->e_magic != ELF_MAGIC) {
		close(fd);
		cprintf("elf magic %08x want %08x\n", elf->e_magic, ELF_MAGIC);
		return -E_NOT_EXEC;
	}

	// 創建子進程
	if ((r = sys_exofork()) < 0)
		return r;
	child = r;

	// Set up trap frame, including initial stack.
	// 將子進程的 eip 設置為 elf 的入口點
	child_tf = envs[ENVX(child)].env_tf;
	child_tf.tf_eip = elf->e_entry;

	// 為子進程設置棧
	if ((r = init_stack(child, argv, &child_tf.tf_esp)) < 0)
		return r;

	// Set up program segments as defined in ELF header.
	// 將 elf 的程式段載入記憶體
	ph = (struct Proghdr*) (elf_buf + elf->e_phoff);
	for (i = 0; i < elf->e_phnum; i++, ph++) {
		// 使用 Proghdr 中每個程式段的 p_flags 欄位來確定如何映射程式段: 
		if (ph->p_type != ELF_PROG_LOAD)
			continue;
		perm = PTE_P | PTE_U;
		// 如果 ELF 標誌不包括 ELF_PROG_FLAG_WRITE,則段包含文本和只讀數據。
		// 如果 ELF 段標誌包含 ELF_PROG_FLAG_WRITE,則該段包含讀/寫數據和 bss。
		if (ph->p_flags & ELF_PROG_FLAG_WRITE)
			perm |= PTE_W;
		if ((r = map_segment(child, ph->p_va, ph->p_memsz,
				     fd, ph->p_filesz, ph->p_offset, perm)) < 0)
			goto error;
	}
	close(fd);
	fd = -1;

	// Copy shared library state.
	if ((r = copy_shared_pages(child)) < 0)
		panic("copy_shared_pages: %e", r);

	child_tf.tf_eflags |= FL_IOPL_3;   // devious: see user/faultio.c
	if ((r = sys_env_set_trapframe(child, &child_tf)) < 0)
		panic("sys_env_set_trapframe: %e", r);

	if ((r = sys_env_set_status(child, ENV_RUNNABLE)) < 0)
		panic("sys_env_set_status: %e", r);

	return child;

error:
	sys_env_destroy(child);
	close(fd);
	return r;
}

spawn的步驟:

  • 打開程式文件。
  • 像以前一樣讀取 ELF 頭文件,並檢查其神奇數字是否正確。 (檢查你的 load_icode!)。
  • 使用 sys_exofork() 創建一個新環境。
  • 將 child_tf 設置為子程式的初始 struct Trapframe。
  • 調用上面的 init_stack() 函數,為子環境設置初始堆棧頁面。
  • 將所有 p_type ELF_PROG_LOAD 類型的程式段映射到新環境的地址空間。

init_stack 則是先在父進程的 UTMP 上將子進程的用戶棧佈局好,然後通過 sys_page_map 將物理頁映射到子進程中。佈局情況如下:

//下麵的argv[n]指的是字元串首地址,也是這個棧中對應條目的虛擬地址
		argv_2 -->			|		"initarg2"		| 	<--  USTACKTOP 
		argv_1 -->			|		"initarg1"		|
		argv_0 -->			|		"init"			|
							|		 0(NULL)		|
							|		 &argv_2		|
							|		 &argv_1		|
		&argv  -->			|		 &argv_0		|
							|	  	 &argv		    |
 child->esp(往上是出棧) -->  |		   3		  	|
————————————————

練習7

練習 7. `spawn` 依靠新的系統調用 `sys_env_set_trapframe` 來初始化新創建環境的狀態。在 `kern/syscall.c` 中實現 `sys_env_set_trapframe`(別忘了在 `syscall()` 中調度新的系統調用)。

運行 `kern/init.c` 中的 `user/spawnhello` 程式來測試代碼,該程式將嘗試從文件系統中生成 `/hello`。

使用 `make grade` 測試代碼。
// 將 envid 的陷阱框架設置為 “tf”。
// 修改 tf 是為了確保用戶環境始終運行在代碼
// 保護級別 3(CPL 3),啟用中斷,IOPL 為 0。
//
// 成功時返回 0,錯誤時返回 <0。 錯誤是
// -E_BAD_ENV 如果環境 envid 當前不存在、
// 或調用者沒有許可權更改 envid。
static int
sys_env_set_trapframe(envid_t envid, struct Trapframe *tf)
{
	// LAB 5: Your code here.
	// Remember to check whether the user has supplied us with a good
	// address!
	// panic("sys_env_set_trapframe not implemented");
	struct Env * e;
	if(envid2env(envid, &e, true) < 0)
		return -E_BAD_ENV;
	tf->tf_eflags = FL_IF;				//允許中斷
	tf->tf_eflags &= ~FL_IOPL_MASK;		//IOPL為0
	tf->tf_cs = GD_UT | 3;				//保護級別 3
	e->env_tf = *tf;
	return 0;
}

然後在 kern/syscall.c : syscall 中補充:

case SYS_env_set_trapframe:
			ret = sys_env_set_trapframe((envid_t) a1, (struct Trapframe *)a2);
			return ret;

為了測試效果,在 kern/init.c : i386_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_icode, ENV_TYPE_USER);
	ENV_CREATE(user_spawnhello, ENV_TYPE_USER);
#endif // TEST*

測試效果 make qemu:

image.png

跨 fork 和 spawn 共用庫狀態

我們希望在 forkspawn 之間共用文件描述符狀態,但文件描述符狀態保存在用戶空間記憶體中。
現在,fork 時,記憶體將被標記為寫入時複製,因此狀態將被覆制而非共用(這意味著進程無法在自己未打開的文件中定址,管道也無法在 fork 時工作)。
spawn時,記憶體將被留下,根本不會被覆制。(實際上,生成(spawned)的進程一開始並沒有打開文件描述符)。

我們將修改 fork,使其知道某些記憶體區域被 "庫操作系統 "使用,並應始終共用。
我們將在頁表項中設置一個未使用的位,而不是在某個地方硬編碼一個區域列表(就像我們在 fork 中設置 PTE_COW 位一樣)。

我們在 inc/lib.h 中定義了一個新的 PTE_SHARE 位。
該位是 Intel 和 AMD 手冊中標明 "可用於軟體 "的三個 PTE 位之一。
我們將建立一個慣例,即如果頁表項設置了該位,則 PTE 應在 forkspawn 中直接從父節點複製到子節點。
請註意,這與 "寫入時複製 "不同:如第一段所述,我們要確保共用頁面更新。

練習 8. 修改 `lib/fork.c` 中的 `duppage`,以遵循新的約定。如果頁表項設置了 `PTE_SHARE` 位,則直接複製映射即可。(您應該使用 `PTE_SYSCALL`,而不是 0xfff 來屏蔽掉頁表項中的相關位。0xfff 還會拾取訪問位和臟位)。

同樣,在 `lib/spawn.c` 中實現 `copy_shared_pages`。它應該迴圈查看當前進程中的所有頁表項(就像 fork 所做的),將任何設置了 `PTE_SHARE` 位的頁面映射複製到子進程中。

lib/fork.c : duppage

// 將當前進程(父進程)的記憶體映射(頁表)複製給子進程,同時標記COW
static int
duppage(envid_t envid, unsigned pn)
{
	int r;

	// LAB 4: Your code here.
	// panic("duppage not implemented");
	void *addr = (void *)(pn * PGSIZE);
	//如果頁表項設置了 `PTE_SHARE` 位,則直接複製映射即可。
	if(uvpt[pn] & PTE_SHARE){
		sys_page_map(0, addr, envid, addr, PTE_SYSCALL);	
	}
	//對父進程所有可寫頁或COW頁,標記COW
	else if ((uvpt[pn]&PTE_W)|| (uvpt[pn] & PTE_COW)){
		if ((r = sys_page_map(0, addr, envid, addr, PTE_COW|PTE_U|PTE_P)) < 0)
			panic("duppage:sys_page_map:%e", r);
		if ((r = sys_page_map(0, addr, 0, addr, PTE_COW|PTE_U|PTE_P)) < 0)
			panic("duppage:sys_page_map:%e", r);
	}
	//對於父進程的只讀頁不標記COW
	else{
		sys_page_map(0, addr, envid, addr, PTE_U|PTE_P);	
	}
	return 0;
}

lib/spawn.c : copy_shared_pages

// 將共用頁面的映射複製到子地址空間。
static int
copy_shared_pages(envid_t child)
{
	// LAB 5: Your code here.
	uintptr_t addr;
	for (addr = 0; addr < UTOP; addr += PGSIZE) {
		if ((uvpd[PDX(addr)] & PTE_P) && (uvpt[PGNUM(addr)] & PTE_P) &&
				(uvpt[PGNUM(addr)] & PTE_U) && (uvpt[PGNUM(addr)] & PTE_SHARE)) {
            sys_page_map(0, (void*)addr, child, (void*)addr, (uvpt[PGNUM(addr)] & PTE_SYSCALL));
        }
	}
	return 0;
}

話說,我們是在什麼時候將文件描述符標記為 PTE_SHARE 的呢?vscode搜索一下:
答案是在 serve_open 的末尾,文件的主迴圈在處理open請求時,調用serve_open,然後serve_open 申請一個新的openfile,代表打開的文件。然後將該openfile關聯的 FD 所在物理頁,以及該物理頁許可權返回給serve,如下圖

image.png

緊接著 serve 調用 ipc_send 將 FD 物理頁發送給客戶端,並以帶有 PTE_SHARE 的許可權,將FD安裝在客戶端調用 ipc_recv 時指定的地址。

因此,所有通過open打開的文件描述符,都是 PTE_SHARE 的。經過 fork 或 spawn 後,父子進程共用。

鍵盤介面

為了讓 shell 正常工作,我們需要一種輸入方式。QEMU 一直在顯示我們寫入 CGA 顯示屏和串列埠的輸出,但到目前為止,我們只在內核監視器中輸入了內容。在 QEMU 中,在圖形視窗中輸入的內容會以鍵盤輸入的形式顯示在 JOS 上,而輸入到控制台的內容則會以串列埠上的字元形式顯示。kern/console.c 已經包含了內核監視器從實驗一開始就使用的鍵盤和串列驅動程式,但現在你需要將它們連接到系統的其他部分。

練習 9. 在 kern/trap.c 中,調用 kbd_intr 處理陷阱 IRQ_OFFSET+IRQ_KBD,調用 serial_intr 處理陷阱 IRQ_OFFSET+IRQ_SERIAL。

我們在 lib/console.c 中為您實現了控制台輸入/輸出文件類型。kbd_intr 和 serial_intr 會將最近讀取的輸入內容填入緩衝區,而控制台文件類型則會耗盡緩衝區(除非用戶重定向,否則預設情況下控制台文件類型用於 stdin/stdout)。

運行 make run-testkbd 並鍵入幾行代碼,測試你的代碼。當你輸入完畢時,系統會回聲提示。如果控制台和圖形視窗都可用,請嘗試同時在控制台和圖形視窗中鍵入。

kern/trap.c : trap_dispatch 中添加:

// Handle keyboard and serial interrupts.
	// LAB 5: Your code here.
	if (tf->tf_trapno == IRQ_OFFSET + IRQ_KBD){
		kbd_intr();
  		return;
	}
	if (tf->tf_trapno == IRQ_OFFSET + IRQ_SERIAL){
		serial_intr();
  		return;
	}

然後 make run-testkbd

image.png

看起來只是簡單的回顯了輸入,來看看代碼
user/testkbd

#include <inc/lib.h>

void
umain(int argc, char **argv)
{
	int i, r;

	// Spin for a bit to let the console quiet
	for (i = 0; i < 10; ++i)
		sys_yield();

	close(0);
	// 打開一個文件,這個文件的設備類型是終端
	if ((r = opencons()) < 0)
		panic("opencons: %e", r);
	// 由於是第一個打開的,fd應該是0
	if (r != 0)
		panic("first opencons used fd %d", r);
	// 複製一個文件描述符
	if ((r = dup(0, 1)) < 0)
		panic("dup: %e", r);

	for(;;){
		char *buf;

		buf = readline("Type a line: ");
		if (buf != NULL)
			// fprintf 最終會調用write向fd1寫入數據
			// 此時會將內容顯示在終端上
			fprintf(1, "%s\n", buf);
		else
			fprintf(1, "(end of file received)\n");
	}
}

首先打開了文件描述符0,其設備類型為終端devcons(可以通過 opencons 看到)。
然後複製了文件描述符0得到文件描述符1,並向文件描述符1寫入我們輸入的字元串。
從而使得終端顯示了我們的輸入。
image.png

The Shell

運行 make run-icode 或 make run-icode-nox。這將運行內核並啟動用戶/icode。icode 會執行 init,將控制台設置為文件描述符 0 和 1(標準輸入和標準輸出)。然後會生成 shell sh。你應該可以運行以下命令:

	echo hello world | cat
	cat lorem |cat
	cat lorem |num
	cat lorem |num |num |num |num |num
	lsfd

請註意,用戶庫常式 cprintf 直接列印到控制台,而不使用文件描述符代碼。這非常適合調試,但不適合在其他程式中使用。printf("...", ...) 是列印到 FD 1 的快捷方式。有關示例,請參見 user/lsfd.c。

練習 10.

shell 不支持 I/O 重定向。如果能運行 sh <script 就好了,而不必像上面那樣手寫輸入腳本中的所有命令。將 < 的 I/O 重定向添加到 user/sh.c。

在 shell 中鍵入 sh <script 測試你的實現

運行 make run-testshell 測試你的 shell。testshell 只需將上述命令(也可在 fs/testshell.sh 中找到)輸入 shell,然後檢查輸出是否與 fs/testshell.key 一致。

熟悉 linux 的 bash 的話,應該知道IO重定向的概念。 < 用於重定向標準輸入。比如說 [命令a] < [文件b] 的含義就是,將命令a的標準輸入改為文件b。
標準輸入的文件描述符編號是0,所以我們要做的就是將,打開 文件b,然後將 文件描述符0 改為文件b:


// LAB 5: Your code here.
// panic("< redirection not implemented");
// t是gettoken得到的當前短語,即文件b的文件名,打開文件b
if ((fd = open(t, O_RDONLY)) < 0) {
	cprintf("open %s for read: %e", t, fd);
	exit();
}
if (fd != 0) {
	// 將文件描述符0 變為文件b的副本
	dup(fd, 0);
	// 關閉文件b
	close(fd);
}
break;

image.png

關於 /lib/sh.c
sh.c 實現了一個 shell,其核心函數式 runcmd。邏輯其實也很簡單,通過迴圈調用gettoken 解析命令。然後根據重定向的需求修改輸入輸出,最後通過 spawn 運行相關程式。

image.png

管道部分比較有意思:
image.png

管道左側直接跳轉到 runit 運行命令,右側則重新解析命令。

管道右側需要等待管道左側運行完畢後,再運行:

image.png


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

-Advertisement-
Play Games
更多相關文章
  • SpringBoot筆記 SpringBoot文檔 官網: https://spring.io/projects/spring-boot 學習文檔: https://docs.spring.io/spring-boot/docs/current/reference/html/ 線上API: http ...
  • 一、背景說明 1.1 效果演示 用python開發的爬蟲採集軟體,可自動抓取抖音評論數據,並且含二級評論! 為什麼有了源碼還開發界面軟體呢?方便不懂編程代碼的小白用戶使用,無需安裝python、無需懂代碼,雙擊打開即用! 軟體界面截圖: 爬取結果截圖: 以上。 1.2 演示視頻 軟體運行演示視頻:見 ...
  • @DateTimeFormat 和 @JsonFormat 是 Spring 和 Jackson 中用於處理日期時間格式的註解,它們有不同的作用: @DateTimeFormat @DateTimeFormat 是 Spring 框架提供的註解,用於指定字元串如何轉換為日期時間類型,以及如何格式化日 ...
  • OpenAPI 規範是用於描述 HTTP API 的標準。該標準允許開發人員定義 API 的形狀,這些 API 可以插入到客戶端生成器、伺服器生成器、測試工具、文檔等中。儘管該標準具有普遍性和普遍性,但 ASP.NET Core 在框架內預設不提供對 OpenAPI 的支持。 當前 ASP.NET ...
  • 不廢話,直接代碼 private Stack<Action> actionStack = new Stack<Action>(); private void SetCellValues() { var worksheet = Globals.ThisAddIn.Application.ActiveS ...
  • 大家好,我是痞子衡,是正經搞技術的痞子。今天痞子衡給大家介紹的是不同J-Link版本對於i.MXRT1170連接複位後處理行為。 痞子衡之前寫過一篇舊文 《i.MXRT1170上用J-Link連接複位後PC總是停在0x223104的原因》,這篇文章詳細解釋了 RT1170 BootROM 代碼里軟體 ...
  • 目錄題目思路代碼展示進程A進程B結果展示 題目 要求進程A創建一條消息隊列之後向進程B發送SIGUSR1信號,進程B收到該信號之後打開消息隊列並寫入一段信息作為消息寫入到消息隊列中,要求進程B在寫入消息之後,發SIGUSR2信號給進程A,進程A收到該信號則從消息隊列中讀取消息並輸出消息正文的內容。 ...
  • 目錄一、創建百萬級小文件1、單核CPU情況2、多核CPU情況3、執行效率對比3.1、單核的順序執行3.2、多核的併發執行二、如何列出/瀏覽這些文件1、查看目錄下文件的數量2、列出?3、ls -f(關閉排序功能)3.1、執行效率對比4、通過重定嚮導入到文件中瀏覽對應的文件名三、如何快速刪除目錄下所有文 ...
一周排行
    -Advertisement-
    Play Games
  • 示例項目結構 在 Visual Studio 中創建一個 WinForms 應用程式後,項目結構如下所示: MyWinFormsApp/ │ ├───Properties/ │ └───Settings.settings │ ├───bin/ │ ├───Debug/ │ └───Release/ ...
  • [STAThread] 特性用於需要與 COM 組件交互的應用程式,尤其是依賴單線程模型(如 Windows Forms 應用程式)的組件。在 STA 模式下,線程擁有自己的消息迴圈,這對於處理用戶界面和某些 COM 組件是必要的。 [STAThread] static void Main(stri ...
  • 在WinForm中使用全局異常捕獲處理 在WinForm應用程式中,全局異常捕獲是確保程式穩定性的關鍵。通過在Program類的Main方法中設置全局異常處理,可以有效地捕獲並處理未預見的異常,從而避免程式崩潰。 註冊全局異常事件 [STAThread] static void Main() { / ...
  • 前言 給大家推薦一款開源的 Winform 控制項庫,可以幫助我們開發更加美觀、漂亮的 WinForm 界面。 項目介紹 SunnyUI.NET 是一個基於 .NET Framework 4.0+、.NET 6、.NET 7 和 .NET 8 的 WinForm 開源控制項庫,同時也提供了工具類庫、擴展 ...
  • 說明 該文章是屬於OverallAuth2.0系列文章,每周更新一篇該系列文章(從0到1完成系統開發)。 該系統文章,我會儘量說的非常詳細,做到不管新手、老手都能看懂。 說明:OverallAuth2.0 是一個簡單、易懂、功能強大的許可權+可視化流程管理系統。 有興趣的朋友,請關註我吧(*^▽^*) ...
  • 一、下載安裝 1.下載git 必須先下載並安裝git,再TortoiseGit下載安裝 git安裝參考教程:https://blog.csdn.net/mukes/article/details/115693833 2.TortoiseGit下載與安裝 TortoiseGit,Git客戶端,32/6 ...
  • 前言 在項目開發過程中,理解數據結構和演算法如同掌握蓋房子的秘訣。演算法不僅能幫助我們編寫高效、優質的代碼,還能解決項目中遇到的各種難題。 給大家推薦一個支持C#的開源免費、新手友好的數據結構與演算法入門教程:Hello演算法。 項目介紹 《Hello Algo》是一本開源免費、新手友好的數據結構與演算法入門 ...
  • 1.生成單個Proto.bat內容 @rem Copyright 2016, Google Inc. @rem All rights reserved. @rem @rem Redistribution and use in source and binary forms, with or with ...
  • 一:背景 1. 講故事 前段時間有位朋友找到我,說他的窗體程式在客戶這邊出現了卡死,讓我幫忙看下怎麼回事?dump也生成了,既然有dump了那就上 windbg 分析吧。 二:WinDbg 分析 1. 為什麼會卡死 窗體程式的卡死,入口門檻很低,後續往下分析就不一定了,不管怎麼說先用 !clrsta ...
  • 前言 人工智慧時代,人臉識別技術已成為安全驗證、身份識別和用戶交互的關鍵工具。 給大家推薦一款.NET 開源提供了強大的人臉識別 API,工具不僅易於集成,還具備高效處理能力。 本文將介紹一款如何利用這些API,為我們的項目添加智能識別的亮點。 項目介紹 GitHub 上擁有 1.2k 星標的 C# ...