Introduction
在這次實驗中將會實現建立進程並調用庫函數裝載和運行磁碟上的可執行檔。同時實現在作業系統核心的console上運行shell。這些特點都需要實現檔案系統,在這裡我們將實現1個簡單可讀寫的檔案系統。
本次實驗新增加的檔案如下:
fs/fs.c 操作檔案系統在磁碟上的結構。
fs/bc.c 基於使用者級頁錯誤處理機制的塊緩衝。
fs/ide.c 最簡單基於PIO的IDE磁碟驅動。
fs/serv.c 檔案系統與用戶端進程進行互動的服務端代碼
lib/fd.c 實現傳統UNIX檔案描述符介面。
lib/file.c 磁碟類型的檔案系統驅動
lib/console.c console類型的檔案系統驅動
lib/spawn.c spawn系統調用實現
File system preliminaries
這部分內容主要介紹了一般檔案系統的結構,包括扇區、塊、超級塊、塊位元影像、檔案中繼資料和目錄的概念。後面JOS實現的檔案系統設計到了這些東西,需要仔細閱讀。
The File System
本次實驗的目的不是讓你實現整個檔案系統,而是只要實現關鍵區段。尤其是如何讀取塊到塊緩衝並寫回磁碟;對應檔位移到磁碟塊;實現檔案的讀取、寫入和開啟IPC介面調用。
Disk Access
JOS的檔案系統需要能夠訪問磁碟,但是我們現在還沒在核心實現訪問磁碟。為了簡化,這裡我們拋棄傳統單核心作業系統將磁碟驅動作為系統調用的實現方式,將磁碟驅動作為使用者進程訪問磁碟來實現。
這將很簡單,通過輪詢而不是中斷來實現在使用者空間進行磁碟訪問。在x86處理器中可以通過設定EFLAGS寄存器中的IOPL位來允許使用者態進程執行IO指令比如in和out。
Exercise 1:
i386_init函數中會建立1個檔案系統進程,通過傳遞ENV_TYPE_FS標誌給env_create函數,需要在該函數中允許檔案系統進程執行IO指令。
回答:
在env_create函數中修改進程的eflag值。
if (type == ENV_TYPE_FS) e->env_tf.tf_eflags |= FL_IOPL_MASK;
Question 1:
在進程切換時如何保證IO特權設定被儲存和重載。
回答:
在進程切換時調用了env.pop_tf函數,其中進行了寄存器的恢複,在iret指令中恢複了eip,cs,eflags等寄存器。
The Block Cache
在JOS中,實現了1個簡單的磁碟塊緩衝機制。該機制支援的磁碟大小最大為3GB,可以使用類似Lab 4中實現fork的COW頁面機制。
其實現機制如下:
1、用檔案系統服務進程的虛擬位址空間(0x10000000 (DISKMAP)到0xD0000000 (DISKMAP+DISKMAX))對應到磁碟的地址空間(3GB)。
2、初始檔案系統服務進程不映射頁面,如果要訪問1個磁碟的地址空間,則發生頁錯誤。
3、在頁錯誤處理程式中,在記憶體中申請一個塊的空間映射到相應的檔案系統虛擬位址,然後去實際的物理磁碟上讀取這個地區的資料到該記憶體地區,最後恢複檔案系統服務進程。
Exercise2:
實現bc_pgfault和flush_block函數,其中bc_pgfault是頁錯誤處理程式,作用是從磁碟上裝載頁。
回答:
主要是實現磁碟塊緩衝的頁面處理和寫回部分,主要用到跟磁碟直接互動的IDE驅動函數。
int ide_read(uint32_t secno, void *dst, size_t nsecs)int ide_write(uint32_t secno, void *dst, size_t nsecs)
secno對應IDE磁碟上的扇區編號,dst為當前檔案系統服務程式空間中的對應地址,nsecs為讀寫的扇區數。
static voidbc_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. addr = ROUNDDOWN(addr, PGSIZE); if ((r = sys_page_alloc(0, addr, PTE_U | PTE_P | PTE_W)) < 0) panic("in bc_pgfault, sys_page_alloc: %e", r); if ((r = ide_read(blockno * BLKSECTS, addr, BLKSECTS)) < 0) panic("in bc_pgfault, ide_read: %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("in bc_pgfault, sys_page_map: %e", r); if (bitmap && block_is_free(blockno)) panic("reading free block %08x\n", blockno);}
先根據地址計算出對應的blockno,然後檢查正確性包括地址是否在對應範圍內、對應的block是否存在等。
voidflush_block(void *addr){ uint32_t blockno = ((uint32_t)addr - DISKMAP) / BLKSIZE; if (addr < (void*)DISKMAP || addr >= (void*)(DISKMAP + DISKSIZE)) panic("flush_block of bad va %08x", addr); int r; addr = ROUNDDOWN(addr, PGSIZE); if (va_is_mapped(addr) && va_is_dirty(addr)) { 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("in flush_block, sys_page_map: %e", r); }}
先根據地址計算對應的blockno,然後然後檢查正確性,最後判斷是否是髒塊,如果是則寫回磁碟並清除dirty位。
The Block Bitmap
在fs_init函數設定塊位元影像之後,我們就能將位元影像當做位元組來對待。
Exercise 3:
以free_block為參考實現alloc_block,功能是在位元影像中尋找1個空閑磁碟塊,標記為佔用並返回塊序號。當你分配1個塊時,為了維護檔案系統的一致性,你需要快速地使用flush_block函數寫回你對位元影像的修改。
回答:
這部分比較簡單,參考free_block函數的實現即可。
intalloc_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. uint32_t blockno; for (blockno = 0; blockno < super->s_nblocks; blockno++) { if (block_is_free(blockno)) { bitmap[blockno/32] ^= 1<<(blockno%32); flush_block(bitmap); return blockno; } } return -E_NO_DISK;}
File Operations
JOS在fs/fs.c中已經提供了1系列函數來操作和管理檔案結構,瀏覽和管理目錄,解析檔案名稱。具體各個函數的功能如下:
file_block_walk(struct File *f, uint32_t filebno, uint32_t **ppdiskbno, bool alloc):
尋找一個檔案結構f中的第fileno個塊指向的磁碟塊編號放入ppdiskbno。如果filebno小於NDIRECT,則返回屬於f.direct[NDIRECT]中的相應連結,否則返回f_indirect中尋找的塊。如果alloc為真且相應磁碟塊不存在,則分配1個。
dir_lookup(struct File *dir, const char *name, struct File **file):
在目錄dir中尋找名字為name的檔案,如果找到則讓file指向該檔案結構體。
dir_alloc_file(struct File *dir, struct File **file):
在dir對應的File結構體中分配1個File的指標串連給file,用於添加檔案的操作。
skip_slash(const char *p):
用於路徑中的字串處理,跳過斜杠。
walk_path(const char *path, struct File **pdir, struct File **pf, char *lastelem):
path為從絕對路徑的檔案名稱,如果成功找到該檔案,則把相應的檔案結構體賦值給pf,其所在目錄的檔案結構體賦值給pdir,lastlem為失效時最後剩下的檔案名稱。
file_free_block(struct File *f, uint32_t filebno):
釋放1個檔案中的第filebno個磁碟塊。此函數在file_truncate_blocks中被調用。
file_truncate_blocks(struct File *f, off_t newsize):
將檔案設定為縮小後的新大小,清空那些被釋放的物理塊。
Exercise 4:
實現file_block_walk函數和file_get_block函數。
回答:
file_block_walk函數尋找一個檔案結構f中的第fileno個塊指向的磁碟塊編號放入ppdiskbno。
static intfile_block_walk(struct File *f, uint32_t filebno, uint32_t **ppdiskbno, bool alloc){ int r; if (filebno >= NDIRECT + NINDIRECT) return -E_INVAL; if (filebno < NDIRECT) { if (ppdiskbno) *ppdiskbno = f->f_direct + filebno; return 0; } if (!alloc && !f->f_indirect) return -E_NOT_FOUND; if (!f->f_indirect) { if ((r = alloc_block()) < 0) return -E_NO_DISK; f->f_indirect = r; memset(diskaddr(r), 0, BLKSIZE); flush_block(diskaddr(r)); } if (ppdiskbno) *ppdiskbno = (uint32_t*)diskaddr(f->f_indirect) + filebno - NDIRECT; return 0;}
file_get_block函數先調用file_walk_block函數找到檔案中的目標塊,然後將其轉換為地址空間中的地址賦值給blk。
intfile_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; if (*ppdiskbno == 0) { if ((r = alloc_block()) < 0) return -E_NO_DISK; *ppdiskbno = r; memset(diskaddr(r), 0, BLKSIZE); flush_block(diskaddr(r)); } *blk = diskaddr(*ppdiskbno); return 0;}
The file system interface
現在我們已經實現了檔案系統必要的函數,需要讓它能被其它進程調用使用。我們將使用Lab4中實現的IPC方式來讓其它進程來與檔案系統服務進程互動來進行檔案操作。
在虛線下的部分是普通進程如何發送一個讀請求到檔案系統服務進程的機制。首先read操作檔案描述符,分發給合適的裝置讀函數devfile_read 。devfile_read函數實現讀取磁碟檔案,作為用戶端檔案操作函數。然後建立請求結構的參數,調用fsipc函數來發送IPC請求並解析返回的結果。
檔案系統服務端的代碼在fs/serv.c中,服務進程迴圈等待直到收到1個IPC請求。然後分發給合適的處理函數,最後通過IPC發回結果。對於讀請求,服務端會分發給serve_read函數
在JOS實現的IPC機制中,允許進程發送1個32位元和1個頁。為了實現發送1個請求從用戶端到服務端,我們使用32位元來表示請求類型,儲存參數在聯合Fsipc位於共用頁中。在用戶端我們一直共用fsipcbuf所在頁,在服務端我們映射請求頁到fsreq地址(0x0ffff000)。
服務端也會通過IPC發送結果。我們使用32位元作為函數的返回碼。FSREQ_READ 和FSREQ_STAT函數也會返回資料,它們將資料寫入共用頁返回給用戶端。
union Fsipc { struct Fsreq_open { char req_path[MAXPATHLEN]; int req_omode; } open; struct Fsreq_set_size { int req_fileid; off_t req_size; } set_size; struct Fsreq_read { int req_fileid; size_t req_n; } read; struct Fsret_read { char ret_buf[PGSIZE]; } readRet; struct Fsreq_write { int req_fileid; size_t req_n; char req_buf[PGSIZE - (sizeof(int) + sizeof(size_t))]; } write; struct Fsreq_stat { int req_fileid; } stat; struct Fsret_stat { char ret_name[MAXNAMELEN]; off_t ret_size; int ret_isdir; } statRet; struct Fsreq_flush { int req_fileid; } flush; struct Fsreq_remove { char req_path[MAXPATHLEN]; } remove; // Ensure Fsipc is one page char _pad[PGSIZE];};
這裡需要瞭解一下union Fsipc,檔案系統中用戶端和服務端通過IPC進行通訊,通訊的資料格式就是union Fsipc,它裡面的每一個成員對應一種檔案系統的操作請求。每次用戶端發來請求,都會將參數放入一個union Fsipc映射的物理頁到服務端。同時服務端還會將處理後的結果放入到Fsipc內,傳遞給用戶端。檔案服務端進行的地址空間布局如下:
OpenFile結構是服務端進程維護的一個映射,它將一個真實檔案struct File和使用者用戶端開啟的檔案描述符struct Fd對應到一起。每個被開啟檔案對應的struct Fd都被映射到FILEEVA(0xd0000000)往上的1個物理頁,服務端和開啟這個檔案的用戶端進程共用這個物理頁。用戶端進程和檔案系統服務端通訊時使用0_fileid來指定要操作的檔案。
struct OpenFile { uint32_t o_fileid; // file id struct File *o_file; // mapped descriptor for open file int o_mode; // open mode struct Fd *o_fd; // Fd page};
檔案系統預設最大同時可以開啟的檔案個數為1024,所以有1024個strcut Openfile,對應著服務端進程地址空間0xd0000000往上的1024個物理頁,用於映射這些對應的struct Fd。
struct Fd是1個抽象層,JOS和Linux一樣,所有的IO都是檔案,所以使用者看到的都是Fd代表的檔案。但是Fd會記錄其對應的具體對象,比如真實檔案、Socket和管道等等。現在只用檔案,所以union中只有1個FdFile。
struct Fd { int fd_dev_id; off_t fd_offset; int fd_omode; union { // File server files struct FdFile fd_file; };};
Exercise 5:
實現fs/serv.c檔案中的serve_read函數。
回答:
首先需要弄清楚服務端進程的內部結構,工作機制。
voidserve(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 < NHANDLERS && 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); }}
服務端主迴圈會使用輪詢的方式接受用戶端進程的檔案操作請求。每次操作如下:
1、從IPC接受1個請求類型req以及資料頁fsreq
2、然後根據req來執行相應的服務端處理函數
3、將相應服務端函數的執行結果(如果產生了資料也則有pg)通過IPC發送回調用進程
4、將映射好的物理頁fsreq取消映射
服務端函數定義在handler數組,通過請求號進行調用。
typedef int (*fshandler)(envid_t envid, union Fsipc *req);fshandler handlers[] = { // Open is handled specially because it passes pages /* [FSREQ_OPEN] = (fshandler)serve_open, */ [FSREQ_READ] = serve_read, [FSREQ_STAT] = serve_stat, [FSREQ_FLUSH] = (fshandler)serve_flush, [FSREQ_WRITE] = (fshandler)serve_write, [FSREQ_SET_SIZE] = (fshandler)serve_set_size, [FSREQ_SYNC] = serve_sync};#define NHANDLERS (sizeof(handlers)/sizeof(handlers[0]))
對於讀檔案請求,調用serve_read函數來處理。
intserve_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); struct OpenFile *o; int r, req_n; if ((r = openfile_lookup(envid, req->req_fileid, &o)) < 0) return r; req_n = req->req_n > PGSIZE ? PGSIZE : req->req_n; if ((r = file_read(o->o_file, ret->ret_buf, req_n, o->o_fd->fd_offset)) < 0) return r; o->o_fd->fd_offset += r; return r;}
先從Fsipc中擷取讀請求的結構體,然後在openfile中尋找fileid對應的Openfile結構體,緊接著從openfile長相的o_file中讀取內容到儲存返回結果的ret_buf中,並移動檔案位移指標。
然後我們可以看一下使用者進程發送讀取請求的函數devfile_read,主要操作是封裝Fsipc佈建要求類型為FSREQ_READ,在接受到返回後,將返回結果拷貝到自己的buf中。
static ssize_tdevfile_read(struct Fd *fd, void *buf, size_t n){ 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;}
Exercise 6:
模仿read請求實現serve_write函數和devfile_write函數。
回答:
實現與read請求類似。
// fs/serv.cintserve_write(envid_t envid, struct Fsreq_write *req){ if (debug) cprintf("serve_write %08x %08x %08x\n", envid, req->req_fileid, req->req_n); struct OpenFile *o; int r, req_n; if ((r = openfile_lookup(envid, req->req_fileid, &o)) < 0) return r; req_n = req->req_n > PGSIZE ? PGSIZE : req->req_n; 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;}// lib/file.cstatic ssize_tdevfile_write(struct Fd *fd, const void *buf, size_t n){ int r; 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); if ((r = fsipc(FSREQ_WRITE, NULL)) < 0) return r; return r;}
Spawning Processes
在lib/spawn.c中已經實現了spawn函數來建立1個新進程並從檔案系統中裝載1個程式運行,然後父進程繼續執行。這與UNIX的fork函數類似在建立新進程後馬上執行exec。
Exercise 7:
spawn函數依賴於新的系統調用sys_env_set_trapframe來初始化新建立進程的狀態。實現sys_env_set_trapframe函數(不要忘記在syscall中分發對應的調用號)。
回答:
sys_env_set_trapframe函數實現簡單,主要是用來拷貝父進程的寄存器。
static intsys_env_set_trapframe(envid_t envid, struct Trapframe *tf){ struct Env *e; int r; if ((r = envid2env(envid, &e, true)) < 0) return -E_BAD_ENV; user_mem_assert(e, tf, sizeof(struct Trapframe), PTE_U); e->env_tf = *tf; e->env_tf.tf_cs |= 3; e->env_tf.tf_eflags |= FL_IF; return 0;}
Sharing library state across fork and spawn
UNIX檔案描述符包括pipe,console I/O。 在JOS中,這些裝置類型都有1個與與它關聯的struct Dev,裡面有實現read/write等檔案操作的函數指標。在lib/fd.c中實現了傳統UNIX的檔案描述符介面。
在lib/fd.c中也包括每個客戶進程的檔案描述符表布局,開始於FSTABLE。這塊空間為每個描述符保留了1個頁的地址空間。 在任何時候,只有當檔案描述符在使用中才在檔案描述符表中映射頁。
我們想要共用檔案描述符狀態在調用fork和spawn建立新進程。當下,fork函數使用COW會將狀態複製1份而不是共用。在spawn中,狀態則不會被拷貝而是完全捨棄。
所以我們將改變fork來共用狀態。在inc/lib.h中新定義了PTE_SHARE位來標識頁共用。當頁表入口中設定了該位,則應該從父進程中拷貝PTE映射到子進程在fork和spawn時。
Exercise 8:
改變duppage函數實現上述變化,如果頁表入口有設定PTE_SHARE位,那麼直接拷貝映射。類似地,實現copy_shared_pages函數。
回答:
static intduppage(envid_t envid, unsigned pn){ int r; void *addr; pte_t pte; int perm; addr = (void *)((uint32_t)pn * PGSIZE); pte = uvpt[pn]; if (pte & PTE_SHARE) { if ((r = sys_page_map(sys_getenvid(), addr, envid, addr, pte & PTE_SYSCALL)) < 0) { panic("duppage: page mapping failed %e", r); return r; } } else { perm = PTE_P | PTE_U; if ((pte & PTE_W) || (pte & PTE_COW)) perm |= PTE_COW; if ((r = sys_page_map(thisenv->env_id, addr, envid, addr, perm)) < 0) { panic("duppage: page remapping failed %e", r); return r; } if (perm & PTE_COW) { if ((r = sys_page_map(thisenv->env_id, addr, thisenv->env_id, addr, perm)) < 0) { panic("duppage: page remapping failed %e", r); return r; } } } return 0;}
在原先的基礎上新添加PTE_SHARE位判斷。
copy_shared_pages(envid_t child){ int i, j, pn, r; for (i = PDX(UTEXT); i < PDX(UXSTACKTOP); i++) { if (uvpd[i] & PTE_P) { for (j = 0; j < NPTENTRIES; j++) { pn = PGNUM(P