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
  • 移動開發(一):使用.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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...