MIT6.828 La5 File system, Spawn and Shell

周小倫發表於2021-08-14

Lab 5: File system, Spawn and Shell

1. File system preliminaries

在lab中我們要使用的檔案系統比大多數“真實”檔案系統更簡單,包括XV6 UNIX的檔案系統,但它足以提供基本功能:建立,讀取,寫入和刪除在分層目錄結構中組織的檔案。

我們僅開發一個單使用者作業系統, 因此,我們的檔案系統不支援檔案所有權或許可權。 我們的檔案系統目前也不支援硬連結,符號連結,時間戳或大多數UNIX檔案系統的特殊裝置檔案。

1. On-Disk File System Structure

大多數UNIX檔案系統將可用磁碟空間分為兩種主要型別的區域:inode區域和資料區域。 UNIX檔案系統為檔案系統中的每個檔案分配一個inode;檔案的inode儲存關於檔案的關鍵後設資料,例如其stat屬性和指向其資料塊的指標。資料區域被劃分成更大(通常為8KB或更多)的資料塊,檔案系統在其中儲存檔案資料和目錄後設資料。目錄條目包含檔名和指向inode的指標;如果檔案系統中的多個目錄條目引用該檔案的inode,則檔案被稱為硬連結。由於我們的檔案系統不支援硬連結,所以我們不需要這種級別的重定向,因此可以方便的簡化:我們的檔案系統根本不會使用inode,而只是在(唯一)的目錄條目中儲存所有的檔案(或子目錄)的後設資料。

檔案和目錄邏輯上都是由一系列資料塊組成的,這些資料塊可能散佈在整個磁碟上,就像使用者環境的虛擬地址空間的頁面可以分散在整個實體記憶體中一樣。檔案系統環境隱藏資料塊佈局的細節,僅呈現在檔案任意偏移量處讀/寫位元組序列的介面。檔案系統環境將對目錄的所有修改作為檔案建立和刪除等操作內部處理的一部分。我們的檔案系統允許使用者環境直接讀取目錄後設資料(例如,read),這意味著使用者環境可以自己執行目錄掃描操作(例如,實現ls程式),而不必依賴額外特殊的對檔案系統的呼叫。對目錄掃描方法的缺點,以及大多數現代UNIX變體阻止它的原因在於它使應用程式依賴於目錄後設資料的格式,使得在不更改或至少重新編譯應用程式的情況下難以更改檔案系統的內部佈局。

簡單來講,我們檔案系統就只有一個資料結構儲存檔案,沒有索引。

1.1 Sectors and Blocks

大多數磁碟不能以位元組粒度執行讀取和寫入,而是以扇區為單位執行讀取和寫入操作。在JOS中,扇區為512位元組。檔案系統實際上以塊為單位分配和使用磁碟儲存。請注意兩個術語之間的區別:扇區大小是磁碟硬體的屬性,而塊大小是作業系統使用磁碟的一個方面。檔案系統的塊大小必須是底層磁碟扇區大小的倍數。

UNIX xv6檔案系統使用512位元組的塊大小,與底層磁碟的扇區大小相同。然而,大多數現代檔案系統使用更大的塊大小,因為儲存空間已經變得更便宜,並且以更大的粒度來管理儲存效率更高。我們的檔案系統將使用4096位元組的塊大小,方便地匹配處理器的頁面大小。

簡單來講,磁碟預設512位元組是一個扇區,我們系統4096位元組一個塊,也就是8個扇區一個塊。

1.2 Superblocks

檔案系統通常將某些磁碟塊保留在磁碟上的“易於查詢”位置(例如起始或最後),以儲存描述整個檔案系統屬性的後設資料,例如塊大小,磁碟大小,找到根目錄所需的任何後設資料,檔案系統上次掛載的時間,檔案系統上次檢查錯誤的時間等等。這些特殊塊稱為超級塊。

我們的檔案系統將只有一個超級塊,它將始終位於磁碟上的塊1。它的佈局由struct Super在inc/fs.h中定義。塊0通常保留用於儲存引導載入程式和分割槽表,因此檔案系統通常不使用第一個磁碟塊。許多“真正的”檔案系統具有多個超級塊,這幾個副本在磁碟的幾個廣泛間隔的區域,以便如果其中一個被損壞或磁碟在該區域中產生媒體錯誤,則仍然可以找到其他超級塊,並將其用於訪問檔案系統。

其中具體塊的資料結構如下

struct Super {
	uint32_t s_magic;		// Magic number: FS_MAGIC
	uint32_t s_nblocks;		// Total number of blocks on disk
	struct File s_root;		// Root directory node
};

1.3 File Meta-data

描述檔案系統中的檔案的後設資料的佈局由inc/fs.h中的struct File定義。該後設資料包括檔案的名稱,大小,型別(常規檔案或目錄)以及指向包含該檔案的塊的指標。如上所述,我們沒有inode,所以後設資料儲存在磁碟上的目錄條目中。與大多數“真實”檔案系統不同,為簡單起見,我們將使用這個struct File來表示在磁碟和記憶體中出現的檔案後設資料。

struct File中的f_direct陣列包含儲存檔案前10個(NDIRECT)塊的塊號的空間,這前10個塊被稱之為檔案的直接塊。對於大小為10 * 4096 = 40KB的小檔案,這意味著所有檔案塊的塊號將直接適用於struct File本身。然而,對於較大的檔案,我們需要一個地方來儲存檔案的其他塊號。因此,對於大於40KB的任何檔案,我們分配一個額外的磁碟塊,稱為檔案的間接塊,最多容納4096/4 = 1024個附加塊號。因此,我們的檔案系統允許檔案的大小可達1034個塊,或者剛剛超過四兆位元組大小。為了支援更大的檔案,“真實”檔案系統通常也支援雙重和三重間接塊。

struct File {
	char f_name[MAXNAMELEN];	// filename  檔名
	off_t f_size;			// file size in bytes 檔案大小
	uint32_t f_type;		// file type 檔案型別

	// Block pointers.
	// A block is allocated iff its value is != 0.
	uint32_t f_direct[NDIRECT];	// direct blocks 直接塊
	uint32_t f_indirect;		// indirect block 間接塊

	// Pad out to 256 bytes; must do arithmetic in case we're compiling
	// fsformat on a 64-bit machine.
	uint8_t f_pad[256 - MAXNAMELEN - 8 - 4*NDIRECT - 4];
} __attribute__((packed));	// required only on some 64-bit machines

1.4 Directories versus Regular Files

我們的檔案系統中的struct File可以表示常規檔案或目錄;這兩種型別的“檔案”通過struct File中的型別欄位進行區分。檔案系統以完全相同的方式管理常規檔案和目錄檔案,除了它不解釋與常規檔案相關聯的資料塊的內容,而檔案系統將目錄檔案的內容解釋為一系列描述目錄中的檔案和子目錄的struct File

// File types
#define FTYPE_REG	0	// Regular file 檔案
#define FTYPE_DIR	1	// Directory 目錄

我們的檔案系統中的超級塊包含一個struct File(其實struct Super中的根欄位),它儲存檔案系統根目錄的後設資料。根目錄檔案的內容是描述位於檔案系統根目錄下的檔案和目錄的struct File序列。根目錄中的任何子目錄可以依次包含表示子子目錄的更多的struct File,依此類推。

2. The File System

lab的目標不是實現整個檔案系統,而是僅實現某些關鍵元件。特別是,需要實現將塊讀入塊快取記憶體並將其重新整理回磁碟;分配磁碟塊;將檔案偏移對映到磁碟塊;並在IPC介面中中實現讀,寫和開啟。因為你不會自己實現所有的檔案系統,所以你需要熟悉提供的程式碼和各種檔案系統介面。

2.1 Disk Access

x86處理器使用EFLAGS暫存器中的IOPL位來確定是否允許保護模式程式碼執行特殊的裝置I/O指令,如IN和OUT指令。由於我們需要訪問的所有IDE磁碟暫存器位於x86的I/O空間中,而不是記憶體對映,因此為檔案系統環境提供“I/O特權”是我們唯一需要做的,以便允許檔案系統訪問這些暫存器。實際上,EFLAGS暫存器中的IOPL位為核心提供了一種簡單的“全或無”方法來控制使用者態程式碼能否訪問I/O空間。在我們的實現中,我們希望檔案系統環境能夠訪問I/O空間,但是我們不希望任何其他環境能夠訪問I/O空間。

所以對於第一個Exercise1是非常簡單的,只需要一行程式碼。

不過這裡我遇到了一個問題

這個問題我修了一個小時。。fuck

後面發現是make引數的問題。本來的makefile檔案設定了Werror引數。這表示會把warning當作error來處理。後面把它刪掉就沒問題了

// If this is the file server (type == ENV_TYPE_FS) give it I/O privileges.
	// LAB 5: Your code here.
	if (type == ENV_TYPE_FS) {
		env->env_tf.tf_eflags |= FL_IOPL_MASK;
	}

3. The Block Cache

在我們的檔案系統中,我們將在處理器的虛擬記憶體系統的幫助下實現一個簡單的“緩衝區快取”(真正只是一個塊快取)。 塊快取的程式碼在fs/bc.c中。

我們的檔案系統將僅限於處理小於等於3GB的磁碟。 我們為檔案系統預留了固定的3GB區域。從0x10000000(diskmap)到0xd0000000(diskmap + diskmax),作為磁碟的“記憶體對映”版本。 例如,磁碟塊0對映在虛擬地址0x10000000,磁碟塊1對映在虛擬地址0x10001000等等。

Exercise 2

實現bc_pgfault()flush_block()
bc_pgfault()是檔案系統中的程式缺頁處理函式,負責將資料從磁碟讀取到對應的記憶體

參考註釋的提示也是不難實現

bc_pgfault()實現

static void
bc_pgfault(struct UTrapframe *utf)
{
	void *addr = (void *) utf->utf_fault_va;
	uint32_t blockno = ((uint32_t)addr - DISKMAP) / BLKSIZE;
	int r;

	// Check that the fault was within the block cache region
	if (addr < (void*)DISKMAP || addr >= (void*)(DISKMAP + DISKSIZE))
		panic("page fault in FS: eip %08x, va %08x, err %04x",
		      utf->utf_eip, addr, utf->utf_err);

	// Sanity check the block number.
	if (super && blockno >= super->s_nblocks)
		panic("reading non-existent block %08x\n", blockno);

	// Allocate a page in the disk map region, read the contents
	// of the block from the disk into that page.
	// Hint: first round addr to page boundary. fs/ide.c has code to read
	// the disk.
	//
	// LAB 5: you code here:
	addr = ROUNDDOWN(addr,PGSIZE);
	sys_page_alloc(0,addr,PTE_U | PTE_W | PTE_P);
	if ((r = ide_read(blockno * BLKSECTS,addr,BLKSECTS)) < 0) {
		panic("ide_read error : %e",r);
	}
	// Clear the dirty bit for the disk block page since we just read the
	// block from disk
	if ((r = sys_page_map(0,addr,0,addr,uvpt[PGNUM(addr)] & PTE_SYSCALL)) < 0) {
		panic("sys_page_map error : %e",r);
	}
	if (bitmap && block_is_free(blockno)) {
		panic("reading free block %08x\n",blockno);
	}
	
}

flush_block()實現

void
flush_block(void *addr)
{
	uint32_t blockno = ((uint32_t)addr - DISKMAP) / BLKSIZE;
	int r;
	if (addr < (void*)DISKMAP || addr >= (void*)(DISKMAP + DISKSIZE))
		panic("flush_block of bad va %08x", addr);

	// LAB 5: Your code here.
	// round addr down
	addr = ROUNDDOWN(addr,PGSIZE);
	if (!va_is_mapped(addr) || ! va_is_dirty(addr)) {
		return ;
	}
	if ((r = ide_write(blockno * BLKSECTS,addr,BLKSECTS)) < 0 ) {
		panic("in flush block ,ide_write() :%e",r);
	}
	if ((r = sys_page_map(0,addr,0,addr,uvpt[PGNUM(addr)] & PTE_SYSCALL) ) < 0 ) {
		panic("sys_page_map error : %e",r);
	}
}

4. The Block Bitmap

fs_init()中已經初始化了bitmap,我們能通過bitmap訪問磁碟的block 1,也就是位陣列,每一位代表一個block,1表示該block未被使用,0表示已被使用。我們實現一系列管理函式來管理這個位陣列。

Exercise 3

實現fs/fs.c中的alloc_block(),該函式搜尋bitmap位陣列,返回一個未使用的block,並將其標記為已使用。

這裡主要仿照free_block.c函式來寫.

根據blockno來清楚bitmap陣列。因此當我們設定bitmap陣列的時候,只需要做取反操作即可

// Mark a block free in the bitmap
void
free_block(uint32_t blockno)
{
	// Blockno zero is the null pointer of block numbers.
	if (blockno == 0)
		panic("attempt to free zero block");
	bitmap[blockno/32] |= 1<<(blockno%32);
}

因此alloc_block函式就非常好實現了

int
alloc_block(void)
{
	// The bitmap consists of one or more blocks.  A single bitmap block
	// contains the in-use bits for BLKBITSIZE blocks.  There are
	// super->s_nblocks blocks in the disk altogether.

	// LAB 5: Your code here.
	for (int i = 3; i < super->s_nblocks; i++) {
		if (block_is_free(i)) {
			bitmap[i/32] &= ~(1<<(i%32));
			return i;
		}
	}

	return -E_NO_DISK;
}

5. File Operations

fs/fs.c檔案提供了一系列函式用於管理File結構,掃描和管理目錄檔案,解析絕對路徑。
基本的檔案系統操作:

1. static int file_block_walk(struct File *f, uint32_t filebno, uint32_t **ppdiskbno, bool alloc){}
2. int file_get_block(struct File *f, uint32_t filebno, char **blk)

解析路徑path,填充pdir和pf地址處的File結構。比如/aa/bb/cc.c那麼pdir指向代表bb目錄的File結構,pf指向代表cc.c檔案的File結構。又比如/aa/bb/cc.c,但是cc.c此時還不存在,那麼pdir依舊指向代表bb目錄的File結構,但是pf地址處應該為0,lastelem指向的字串應該是cc.c。

3. static int walk_path(const char **path*, struct *File* ***pdir*, struct *File* ***pf*, char **lastelem*)

該函式尋找dir指向的檔案內容。並尋找制定name的file結構儲存到file指標處

4. static int dir_lookup(struct File *dir, const char *name, struct File **file)

在dir目錄檔案的內容中尋找一個未被使用的File結構,將其地址儲存到file的地址處

5. static int dir_alloc_file(struct File *dir, struct File **file)

基本的檔案操作

在給定path建立file,如果建立成功pf指向新建立的File指標

1. int file_create(const char *path, struct File **pf)

尋找path對應的File結構地址,儲存到pf地址處。

2. int file_open(const char *path, struct File **pf)

從檔案f的offset位元組處讀取count位元組到buf處

3. size_t* file_read(struct *File* **f*, void **buf*, *size_t* *count*, *off_t* *offset*)

將buf處的count位元組寫到檔案f的offset開始的位置。

4. int file_write(struct File *f, const void *buf, size_t count, off_t offset)

Exercise 4

實現file_block_walk()和file_get_block()。
file_block_walk():

該函式查詢f指標指向檔案結構的第filebnoblock的儲存地址,儲存到ppdiskbno中。

  1. 如果filebno > NDIRECT 則需要去checkf_indirect。如果還未建立indirect塊。但是alloc為真,那麼將分配要給新的block作為該檔案的f->f_indirect。類比頁表管理的pgdir_walk()。
  2. 最簡單的情況是可以在NDIRECT中獲取到對應的塊
static int
file_block_walk(struct File *f, uint32_t filebno, uint32_t **ppdiskbno, bool alloc)
{
    // LAB 5: Your code here.
	// first do sanity check
	if (filebno >= NDIRECT + NINDIRECT) {
		return -E_INVAL;
	}
	uintptr_t *block_addr = NULL;
	if (filebno < NDIRECT) {
		block_addr = &f->f_direct[filebno];
	} else {
		int r;
		if (f->f_indirect == 0) {
			if (alloc) {
				r = alloc_block();
				if (r < 0) {
					return r;
				}
				memset(diskaddr(r),0,BLKSIZE);
				f->f_indirect = r;
			} else {
				return -E_NOT_FOUND;
			}
		}
		uint32_t *indir = (uint32_t *)diskaddr(f->f_indirect);
		block_addr = indir[filebno - NDIRECT];
	}
	*ppdiskbno = block_addr;
	return 0;
}

file_get_block

  1. 這個就是獲取制定的block
  2. 首先呼叫我們之前實現的file_block_walk函式。如果沒問題則直接ok
  3. 否則的話會分配一個新的block,注意這裡要把它flush到磁碟裡
int
file_get_block(struct File *f, uint32_t filebno, char **blk)
{
    // LAB 5: Your code here.
	int r;
	uint32_t *ppdiskbno;
	if ((r = file_block_walk(f, filebno, &ppdiskbno, 1)) < 0) {
		return r;
	}

	int blockno;
	if (*ppdiskbno == 0) {
		if ((blockno = alloc_block()) < 0) {
			return blockno;
		}

		*ppdiskbno = blockno;
		flush_block(diskaddr(blockno));
	}

	*blk = diskaddr(*ppdiskbno);
	return 0;
}

6. The file system interface

當然其他env也有對於檔案系統環境的請求。這時候我們需要有類似下面的機制

Exercise 5

Implement serve_read in fs/serv.c.

serve_read 的繁重工作將由 fs/fs.c 中已經實現的 file_read 完成(反過來,它只是對 file_get_block 的一堆呼叫)。 serve_read 只需要提供用於檔案讀取的 RPC 介面。 檢視 serve_set_size 中的註釋和程式碼,以大致瞭解伺服器功能的結構。

那我們在寫這個程式碼之前首先就要剖析一下整個ipc的發生過程

  1. regular_env中呼叫read函式

    這個函式首先根據fd_lookup找到對應fdnum的fd結構

    隨後根據dev_lookup找到對應的dev資訊

    然後呼叫dev_read(fd,buf,n)

    ssize_t
    read(int fdnum, void *buf, size_t n)
    {
    	int r;
    	struct Dev *dev;
    	struct Fd *fd;
    
    	if ((r = fd_lookup(fdnum, &fd)) < 0
    	    || (r = dev_lookup(fd->fd_dev_id, &dev)) < 0)
    		return r;
    	if ((fd->fd_omode & O_ACCMODE) == O_WRONLY) {
    		cprintf("[%08x] read %d -- bad mode\n", thisenv->env_id, fdnum);
    		return -E_INVAL;
    	}
    	if (!dev->dev_read)
    		return -E_NOT_SUPP;
    	return (*dev->dev_read)(fd, buf, n);
    }
    // 涉及到的三個不同的dev;
    static struct Dev *devtab[] =
    {
    	&devfile,
    	&devpipe,
    	&devcons,
    	0
    };
    
  2. 隨後是呼叫lib/file.c

    1. 這裡是把請求引數儲存在fsipcbuf.read中
    2. 然後呼叫fsipc去向伺服器端傳送read請求。請求成功後結果也是儲存在共享頁面fsipcbuf中,然後讀到指定的buf就行。
    static ssize_t
    devfile_read(struct Fd *fd, void *buf, size_t n)
    {
    	// Make an FSREQ_READ request to the file system server after
    	// filling fsipcbuf.read with the request arguments.  The
    	// bytes read will be written back to fsipcbuf by the file
    	// system server.
    	int r;
    
    	fsipcbuf.read.req_fileid = fd->fd_file.id;
    	fsipcbuf.read.req_n = n;
    	if ((r = fsipc(FSREQ_READ, NULL)) < 0)
    		return r;
    	assert(r <= n);
    	assert(r <= PGSIZE);
    	memmove(buf, fsipcbuf.readRet.ret_buf, r);
    	return r;
    }
    
  3. lib/file.c/fsipc()函式

    1. 找到第一個fs型別的env
    2. 然後呼叫ipc_send傳送ipc訊號
    3. 利用ipc_recv得到返回結果
    static int
    fsipc(unsigned type, void *dstva)
    {
    	static envid_t fsenv;
    	if (fsenv == 0)
    		fsenv = ipc_find_env(ENV_TYPE_FS);
    
    	static_assert(sizeof(fsipcbuf) == PGSIZE);
    
    	if (debug)
    		cprintf("[%08x] fsipc %d %08x\n", thisenv->env_id, type, *(uint32_t *)&fsipcbuf);
    
    	ipc_send(fsenv, type, &fsipcbuf, PTE_P | PTE_W | PTE_U);
    	return ipc_recv(NULL, dstva, NULL);
    }
    
  4. 接下來看fs/serv.c/server函式

    這裡有一個while迴圈等到請求的接受,對於上面傳送的ipc請求這裡會被接收到

    這裡的邏輯還是非常簡單的

    1. 當ipc_recv得到結果之後,就會往下執行
    2. 然後根據請求的型別做出不同的處理。對於read操作而言的話
    3. 就會執行server_read函式來處理這個ipc請求
    void
    serve(void)
    {
    	uint32_t req, whom;
    	int perm, r;
    	void *pg;
    
    	while (1) {
    		perm = 0;
    		req = ipc_recv((int32_t *) &whom, fsreq, &perm);
    		if (debug)
    			cprintf("fs req %d from %08x [page %08x: %s]\n",
    				req, whom, uvpt[PGNUM(fsreq)], fsreq);
    
    		// All requests must contain an argument page
    		if (!(perm & PTE_P)) {
    			cprintf("Invalid request from %08x: no argument page\n",
    				whom);
    			continue; // just leave it hanging...
    		}
    
    		pg = NULL;
    		if (req == FSREQ_OPEN) {
    			r = serve_open(whom, (struct Fsreq_open*)fsreq, &pg, &perm);
    		} else if (req < ARRAY_SIZE(handlers) && handlers[req]) {
    			r = handlers[req](whom, fsreq);
    		} else {
    			cprintf("Invalid request code %d from %08x\n", req, whom);
    			r = -E_INVAL;
    		}
    		ipc_send(whom, r, pg, perm);
    		sys_page_unmap(0, fsreq);
    	}
    }
    
  5. server_read函式

    這個函式就是我們要實現的函式。根據提示我們來實現一下

    1. 首先找到ipc->read->req_fileid對應的OpenFile。
    2. 然後呼叫file_read去讀內容到ipc->readRet->ret_buf
    int
    serve_read(envid_t envid, union Fsipc *ipc)
    {
    	struct Fsreq_read *req = &ipc->read;
    	struct Fsret_read *ret = &ipc->readRet;
    
    	if (debug)
    		cprintf("serve_read %08x %08x %08x\n", envid, req->req_fileid, req->req_n);
    
    	// Lab 5: Your code here:
    	struct OpenFile *o;
    	int r;
    	if ((r = openfile_lookup(envid, req->req_fileid, &o)) < 0)
    		return r;
    	if ((r = file_read(o->o_file, ret->ret_buf, req->req_n, o->o_fd->fd_offset)) < 0)
    		return r;
    	o->o_fd->fd_offset += r;
    	return r;
    }
    

Exercise6

Implement serve_write in fs/serv.c and devfile_write in lib/file.c.

這個的呼叫邏輯是和上面一樣的,所以就不分析了。那我們直接看寫操作是如何實現的

首先實現server_write這個和server_read基本上完全一致的

int
serve_write(envid_t envid, struct Fsreq_write *req)
{
	if (debug)
		cprintf("serve_write %08x %08x %08x\n", envid, req->req_fileid, req->req_n);

	// LAB 5: Your code here.
	struct OpenFile *o;
	int req_n = req->req_n > PGSIZE ? PGSIZE : req->req_n;
	int r;
	if ((r = openfile_lookup(envid, req->req_fileid, &o)) < 0)
		return r;
	if ((r = file_write(o->o_file, req->req_buf, req_n, o->o_fd->fd_offset)) < 0)
		return r;
	o->o_fd->fd_offset += r;
	return r;
}

然後實現devfile_write函式

static ssize_t
devfile_write(struct Fd *fd, const void *buf, size_t n)
{
	// Make an FSREQ_WRITE request to the file system server.  Be
	// careful: fsipcbuf.write.req_buf is only so large, but
	// remember that write is always allowed to write *fewer*
	// bytes than requested.
	// LAB 5: Your csode here
	if (n > sizeof(fsipcbuf.write.req_buf))
		n = sizeof(fsipcbuf.write.req_buf);
	fsipcbuf.write.req_fileid = fd->fd_file.id;
	fsipcbuf.write.req_n = n;
	memmove(fsipcbuf.write.req_buf, buf, n);
	return fsipc(FSREQ_WRITE, NULL);
}

7. Spawning Processes

我們已經為您提供了 spawn 的程式碼(參見 lib/spawn.c),它建立一個新環境,將檔案系統中的程式映像載入到其中,然後啟動執行該程式的子環境。 然後父程式獨立於子程式繼續執行。 spawn 函式的作用類似於 UNIX 中的 fork,然後是子程式中的 exec。

我們實現了 spawn 而不是 UNIX 風格的 exec,因為 spawn 更容易以“外核心方式”從使用者空間實現,而無需核心的特殊幫助。 考慮一下為了在使用者空間中實現 exec 必須做什麼。

Exercise7

spawn 依賴於新的系統呼叫 sys_env_set_trapframe 來初始化新建立環境的狀態。 在 kern/syscall.c 中實現 sys_env_set_trapframe(不要忘記在 syscall() 中排程新的系統呼叫)。

這個系統呼叫其實也不難實現.可以參考env_alloc()

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!
	struct Env *e;
	int32_t ret;
	if ((ret = envid2env(envid, &e, 1)) < 0) {
		return ret; // -E_BAD_ENV
	}
	if ((ret = user_mem_check(e, tf, sizeof(struct Trapframe), PTE_U)) < 0) {
		return ret;
	}
	
	memmove(&e->env_tf, tf, sizeof(struct Trapframe));
	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_cs = GD_UT | 3;
	e->env_tf.tf_eflags |= FL_IF;
	e->env_tf.tf_eflags &= ~FL_IOPL_MASK; //普通程式不能有IO許可權
	return 0;
}

8. Sharing library state across fork and spawn

我們希望在 fork 和 spawn 之間共享檔案描述符狀態,但檔案描述符狀態儲存在使用者空間記憶體中。在 fork 上,記憶體將被標記為 copy-on-write,因此狀態將被複制而不是共享。 (這意味著環境將無法在它們自己沒有開啟的檔案中查詢,並且管道無法跨分支工作。)在spawn中,記憶體將被共享,根本不會被複制。 (有效地,spawn的environment不會開啟任何檔案描述符.)

我們將更改fork以瞭解某些記憶體區域已由“作業系統庫”使用,並且應始終共享。 與其在某處硬編碼區域列表,不如在頁表條項設定一個otherwise-unused的位(就像我們對fork中的PTE_COW位所做的那樣)。

我們在inc / lib.h中定義了一個新的PTE_SHARE位。 該位是Intel和AMD手冊中標記為“可用於軟體使用”的三個PTE位之一。 我們將建立一個約定,如果頁表項設定了該位,則應該在fork和spawn中將PTE直接從父級複製到子級。 請注意,這不同於將其標記為“copy-on-write”:如第一段所述,我們要確保共享頁面更新。

Exercise8

更改 lib/fork.c 中的 duppage 以遵循新約定。 如果頁表條目設定了 PTE_SHARE 位,則直接複製對映。 (您應該使用 PTE_SYSCALL,而不是 0xfff,來遮蔽頁表條目中的相關位。0xfff 也拾取訪問的位和髒位。)

同樣,在lib/spawn.c 中實現 copy_shared_pages。 它應該遍歷當前程式中的所有頁表條目(就像 fork 所做的那樣),將任何設定了 PTE_SHARE 位的頁對映覆制到子程式中。

1. 修改 lib/fork.c

加入針對於PTE_SHARE的處理

	perm = pte & PTE_SYSCALL;
		if ((r = sys_page_map(srcid, (void*)addr, envid, (void*)addr, perm)) < 0) {
			panic("sys_page_map: %e\n", r);
		}

2. 實現copy_shared_pages

static int
copy_shared_pages(envid_t child)
{
	// LAB 5: Your code here.
	int r,i;
	for (i = 0; i < PGNUM(USTACKTOP); i ++){ 
    // Attention! i跟pte一一對應,而i/1024就是該pte所在的頁表
		if((uvpd[i/1024] & PTE_P) && (uvpt[i] & PTE_P) && (uvpt[i] & PTE_SHARE)){ 
			if ((r = sys_page_map(0, PGADDR(i/1024, i%1024, 0), child,PGADDR(i/1024, i%1024, 0), uvpt[i] & PTE_SYSCALL)) < 0)
				return r;
		}
	}
	return 0;
	
}

9. The keyboard interface

要讓shell工作,我們需要一種方法來鍵入它。QEMU一直在顯示我們寫入到CGA顯示器和串列埠的輸出,但到目前為止,我們只在核心監視器中接受輸入。在QEMU中,在圖形化視窗中鍵入的輸入顯示為從鍵盤到JOS的輸入,而在控制檯中鍵入的輸入顯示為串列埠上的字元。kern/console.c已經包含了自lab 1以來核心監視器一直使用的鍵盤和序列驅動程式,但是現在您需要將它們附加到系統的其他部分。

Exercise 9.

在你的kern/trap.c,呼叫kbd_intr處理trap`` IRQ_OFFSET+IRQ_KBD,呼叫serial_intr處理trap IRQ_OFFSET+IRQ_SERIAL。

我們在lib/console.c中為您實現了控制檯輸入/輸出檔案型別。kbd_intr和serial_intr用最近讀取的輸入填充緩衝區,而控制檯檔案型別耗盡緩衝區(控制檯檔案型別預設用於stdin/stdout,除非使用者重定向它們)。

其實這裡非常簡單就是新增兩個新的trap處理函式

//kern/trap.c/trap_dispatch()
if (tf->tf_trapno == IRQ_OFFSET + IRQ_KBD){
	kbd_intr();
	return;
} 
else if (tf->tf_trapno == IRQ_OFFSET + IRQ_SERIAL){
	serial_intr();
	return;
}

然後我們來看一下這兩個陷阱處理函式是怎麼做的

  1. kbd_intr()

    kbd_proc_data()是從鍵盤讀入a character就返回,如果沒輸入就返回-1*

    void kbd_intr(void)
    {
      cons_intr(kbd_proc_data);
    } 
    
  2. cons_intr

    serial_proc_data()很明顯就是從串列埠讀一個

    void serial_intr(void){
    	if (serial_exists)
    		cons_intr(serial_proc_data);
    }
    

    這兩個函式都呼叫了cons_intr函式

  3. cons_intr函式

    將從鍵盤讀入的一行填充到cons.buf

    static void cons_intr(int (*proc)(void)) 
    {
    	int c;
    
    	while ((c = (*proc)()) != -1) {
    		if (c == 0)
    			continue;
    		cons.buf[cons.wpos++] = c;
    		if (cons.wpos == CONSBUFSIZE)
    			cons.wpos = 0;
    	}
    
    

10. The Shell

Run make run-icode or make run-icode-nox。這將執行核心並啟動user/icode。icode執行init,它將把控制檯設定為檔案描述符0和1(標準輸入和標準輸出)。然後它會spawn sh,也就是shell。你應該能夠執行以下命令:

echo hello world | cat

cat lorem |cat

cat lorem |num

cat lorem |num |num

|num |num |num lsfd

注意,使用者庫例程cprintf直接列印到控制檯,而不使用檔案描述符程式碼。這對於除錯非常有用,但是對於piping into other programs卻不是很有用。要將輸出列印到特定的檔案描述符(例如,1,標準輸出),請使用fprintf(1, “…”, …)。 printf("…", …)是列印到FD 1的捷徑。有關示例,請參見user/lsfd.c。

Exercise 10.

shell不支援I/O重定向。如果能執行sh <script就更好,而不是像上面那樣手工輸入script中的所有命令。將<的I/O重定向新增到user/sh.c

	case '<':	// Input redirection
		// Grab the filename from the argument list
		if (gettoken(0, &t) != 'w') {
			cprintf("syntax error: < not followed by word\n");
			exit();
		}
		// LAB 5: Your code here.
		if ((fd = open(t, O_RDONLY)) < 0) {
			cprintf("open %s for read: %e", t, fd);
			exit();
		}
		if (fd != 0) {
			dup(fd, 0); 
			close(fd);
		}
		break;

11. Summary

好了828暫時也告一段落了,lab6就不寫了。。我認識的好多人都沒寫,我也就暫時不寫了。(lab5寫的確實有點草率,主要是中間間隔了太久。。。。趕上暑假了)後面應該是會去看一下xv6的原始碼。然後看完leveldb原始碼。後面會寫一個DDIA的閱讀筆記,然後就是偶爾會更新刷題的總結部落格啦。

相關文章