清華大學作業系統課程 ucore Lab8 檔案系統 實驗報告
作業系統 Lab8 檔案系統 實驗報告
課程資訊所在網址: https://github.com/chyyuu/os_course_info
-
- 練習1:完成讀檔案操作的實現(需要編碼)
- 練習2: 完成基於檔案系統的執行程式機制的實現(需要編碼)
- 實驗中涉及的知識點列舉
- 實驗中未涉及的知識點列舉
實驗目的
- 瞭解基本的檔案系統系統呼叫的實現方法;
- 瞭解一個基於索引節點組織方式的Simple FS檔案系統的設計與實現;
- 瞭解檔案系統抽象層-VFS的設計與實現;
實驗內容
- 通過分析瞭解ucore檔案系統的總體架構設計,完善讀寫檔案操作;
- 實現基於檔案系統的執行程式機制(即改寫do_execve),從而可以完成執行儲存在磁碟上的檔案和實現檔案讀寫等功能;
基本練習
練習0:填寫已有實驗
在本練習中將LAB1/2/3/4/5/6/7的實驗內容移植到了LAB8的實驗框架內,由於手動進行內容移植比較煩雜,因此考慮使用diff和patch工具進行自動化的移植,具體使用的命令如下所示:(對於patch工具進行合併的時候產生衝突的少部分內容,則使用*.rej, *.orig檔案來手動解決衝突問題)
diff -r -u -P lab7_origin lab7 > lab7.patch cd lab8 patch -p1 -u < ../lab7.patch
練習1:完成讀檔案操作的實現(需要編碼)
首先了解開啟檔案的處理流程,然後參考本實驗後續的檔案讀寫操作的過程分析,編寫在sfs_inode.c中sfs_io_nolock讀檔案中資料的實現程式碼。
設計實現
- 在完成練習1之前,首先需要對先前LAB中填寫的程式碼進行更新,包括對程序控制塊中新增變數的初始化、以及在do_fork等函式中對這些變數進行相應的設定等,由於這些內容均比較瑣碎,因此在本報告中將不對其進行贅述;
- 在本練習中需要進行具體編碼實現的函式是sfs_node.c檔案中的sfs_io_nolock函式,因此不妨對該函式進行分析:
- 根據對該函式的觀察可以得知,該函式的功能為針對指定的檔案(檔案對應的記憶體中的inode資訊已經給出),從指定偏移量進行指定長度的讀或者寫操作,因此不妨分析系統呼叫的讀操作究竟是符合呼叫到這個函式的來了解這個函式在整個系統中的功能:
- 發起read系統呼叫後,通過正常的系統呼叫處理流程,進入sys_read函式,該函式進一步呼叫了sysfile_read函式,在這個函式中,建立了大小有限的緩衝區,用於從檔案讀取資料之後,進一步複製到使用者空間的指定位置去;具體用於從檔案讀取資料的函式是file_read,在file_read函式中,通過檔案描述符查詢到了相應檔案對應的記憶體中的inode資訊,然後轉交給vop_read進行讀取處理,事實上就是轉交到了sfs_read函式進行處理(通過函式指標),然後呼叫了sfs_io函式,再進一步呼叫了sfs_io_nolock函式,這就是我們在本次練習中需要完善的函式;
- 在sfs_io_nolock函式中,首先會進行一系列邊界檢查,檢查是否訪問合法,然後將具體的讀/寫操作使用函式指標統一起來,統一成針對整塊的操作,以及不需要針對整塊的操作兩個處理函式,接下來的部分就是在本次實驗中需要完成的部分了,這部分的主要功能為完成不落在整塊資料塊上的讀/寫操作,以及落在整塊資料塊上的讀寫,接下來將結合具體的程式碼來說明實際的實現過程:
if (offset % SFS_BLKSIZE != 0 || endpos / SFS_BLKSIZE == offset / SFS_BLKSIZE) { // 判斷被需要讀/寫的區域所覆蓋的資料塊中的第一塊是否是完全被覆蓋的,如果不是,則需要呼叫非整塊資料塊進行讀或寫的函式來完成相應操作 blkoff = offset % SFS_BLKSIZE; // 計算出在第一塊資料塊中進行讀或寫操作的偏移量 size = (nblks != 0) ? (SFS_BLKSIZE - blkoff) : (endpos - offset); // 計算出在第一塊資料塊中進行讀或寫操作需要的資料長度 if ((ret = sfs_bmap_load_nolock(sfs, sin, blkno, &ino)) != 0) goto out; // 獲取當前這個資料塊對應到的磁碟上的資料塊的編號 if ((ret = sfs_buf_op(sfs, buf, size, ino, blkoff)) != 0) goto out; // 將資料寫入到磁碟中 alen += size; // 維護已經讀寫成功的資料長度資訊 buf += size; } uint32_t my_nblks = nblks; if (offset % SFS_BLKSIZE != 0 && my_nblks > 0) my_nblks --; if (my_nblks > 0) { // 判斷是否存在被需要讀寫的區域完全覆蓋的資料塊 if ((ret = sfs_bmap_load_nolock(sfs, sin, (offset % SFS_BLKSIZE == 0) ? blkno: blkno + 1, &ino)) != 0) goto out; // 如果存在,首先獲取這些資料塊對應到磁碟上的資料塊的編號 if ((ret = sfs_block_op(sfs, buf, ino, my_nblks)) != 0) goto out; // 將這些磁碟上的這些資料塊進行讀或寫操作 size = SFS_BLKSIZE * my_nblks; alen += size; // 維護已經成功讀寫的資料長度 buf += size; // 維護緩衝區的偏移量 } if (endpos % SFS_BLKSIZE != 0 && endpos / SFS_BLKSIZE != offset / SFS_BLKSIZE) { // 判斷需要讀寫的最後一個數據塊是否被完全覆蓋(這裡還需要確保這個資料塊不是第一塊資料塊,因為第一塊資料塊已經操作過了) size = endpos % SFS_BLKSIZE; // 確定在這資料塊中需要讀寫的長度 if ((ret = sfs_bmap_load_nolock(sfs, sin, endpos / SFS_BLKSIZE, &ino) == 0) != 0) goto out; // 獲取該資料塊對應到磁碟上的資料塊的編號 if ((ret = sfs_buf_op(sfs, buf, size, ino, 0)) != 0) goto out; // 進行非整塊的讀或者寫操作 alen += size; buf += size; }
- 至此,練習1中的所有編碼工作完成,實現了讀檔案操作;
- 根據對該函式的觀察可以得知,該函式的功能為針對指定的檔案(檔案對應的記憶體中的inode資訊已經給出),從指定偏移量進行指定長度的讀或者寫操作,因此不妨分析系統呼叫的讀操作究竟是符合呼叫到這個函式的來了解這個函式在整個系統中的功能:
問題回答
- 請在實驗報告中給出設計實現”UNIX的PIPE機制“的概要設方案,鼓勵給出詳細設計方案。
- 為了實現UNIX的PIPE機制,可以考慮在磁碟上保留一部分空間或者是一個特定的檔案來作為pipe機制的緩衝區,接下來將說明如何完成對pipe機制的支援:
- 當某兩個程序之間要求建立管道,假定將程序A的標準輸出作為程序B的標準輸入,那麼可以在這兩個程序的程序控制塊上新增變數來記錄程序的這種屬性;並且同時生成一個臨時的檔案,並將其在程序A, B中開啟;
- 當程序A使用標準輸出進行write系統呼叫的時候,通過PCB中的變數可以知道,需要將這些標準輸出的資料輸出到先前提高的臨時檔案中去;
- 當程序B使用標準輸入的時候進行read系統呼叫的時候,根據其PCB中的資訊可以知道,需要從上述的臨時檔案中讀取資料;
- 至此完成了對pipe機制的設計;
- 事實上,由於在真實的檔案系統和使用者之間還由一層虛擬檔案系統,因此我們也可以不把資料緩衝在磁碟上,而是直接儲存在記憶體中,然後完成一個根據虛擬檔案系統的規範完成一個虛擬的pipe檔案,然後進行輸入輸出的時候只要對這個檔案進行操作即可;
- 為了實現UNIX的PIPE機制,可以考慮在磁碟上保留一部分空間或者是一個特定的檔案來作為pipe機制的緩衝區,接下來將說明如何完成對pipe機制的支援:
練習2: 完成基於檔案系統的執行程式機制的實現(需要編碼)
改寫proc.c中的load_icode函式和其他相關函式,實現基於檔案系統的執行程式機制。執行: make qemu。如果能看看到sh使用者程式的執行介面,則基本成功了。如果在sh使用者介面上可 以執行”ls”,”hello”等其他放置在sfs檔案系統中的其他執行程式,則可以認為本實驗基本成功。
設計實現
- 通過對實驗程式碼的分析可以得知最終用於從磁碟上讀取可執行檔案,並且載入到記憶體中,完成記憶體空間的初始化的函式是load_icode函式,該函式在本LAB中的具體實現與先前的LAB區別在於,先前的LAB僅僅將原先就載入到了核心記憶體空間中的ELF可執行檔案載入到使用者記憶體空間中,而沒有涉及從磁碟讀取資料的操作,而且先前的時候也沒有考慮到給需要執行的應用程度傳遞操作的可能性;
- 仿照先前lab中的load_icode實現,可以大致將該函式的實現流程分為以下幾個步驟:
- 給要執行的使用者程序建立一個新的記憶體管理結構mm,原先該程序的mm已經在do_execve中被釋放掉了;
- 建立使用者記憶體空間的新的頁目錄表;
- 將磁碟上的ELF檔案的TEXT/DATA/BSS段正確地載入到使用者空間中;
- 從磁碟中讀取elf檔案的header;
- 根據elfheader中的資訊,獲取到磁碟上的program header;
- 對於每一個program header:
- 為TEXT/DATA段在使用者記憶體空間上的儲存分配實體記憶體頁,同時建立物理頁和虛擬頁的對映關係;
- 從磁碟上讀取TEXT/DATA段,並且複製到使用者記憶體空間上去;
- 根據program header得知是否需要建立BBS段,如果是,則分配相應的記憶體空間,並且全部初始化成0,並且建立物理頁和虛擬頁的對映關係;
- 將使用者棧的虛擬空間設定為合法,並且為棧頂部分先分配4個物理頁,建立好對映關係;
- 切換到使用者地址空間;
- 設定好使用者棧上的資訊,即需要傳遞給執行程式的引數;
- 設定好中斷幀;
- 接下來結合具體的程式碼實現來說明本實驗中的實現:
static int load_icode(int fd, int argc, char **kargv) { if (current->mm != NULL) { // 判斷當前程序的mm是否已經被釋放掉了 panic("load_icode: current->mm must be empty.\n"); } int ret = -E_NO_MEM; struct mm_struct *mm; // (1) create a new mm for current process if ((mm = mm_create()) == NULL) { // 為程序建立一個新的mm goto bad_mm; } // (2) create a new PDT if ((ret = setup_pgdir(mm)) != 0) { // 進行頁表項的設定 goto bad_pgdir_cleanup_mm; } // (3) copy TEXT/DATA/BSS section // (3.1) resolve elf header struct elfhdr elf, *elfp = &elf; off_t offset = 0; load_icode_read(fd, (void *) elfp, sizeof(struct elfhdr), offset); // 從磁碟上讀取出ELF可執行檔案的elf-header offset += sizeof(struct elfhdr); if (elfp->e_magic != ELF_MAGIC) { // 判斷該ELF檔案是否合法 ret = -E_INVAL_ELF; goto bad_elf_cleanup_pgdir; } struct proghdr ph, *php = &ph; uint32_t vm_flags, perm; struct Page *page; for (int i = 0; i < elfp->e_phnum; ++ i) { // 根據elf-header中的資訊,找到每一個program header // (3.2) resolve prog header load_icode_read(fd, (void *) php, sizeof(struct proghdr), elfp->e_phoff + i * sizeof(struct proghdr)); // 讀取program header if (php->p_type != ELF_PT_LOAD) { continue; } if (php->p_filesz > php->p_memsz) { ret = -E_INVAL_ELF; goto bad_cleanup_mmap; } if (php->p_filesz == 0) { continue; } // (3.3) build vma vm_flags = 0, perm = PTE_U; if (php->p_flags & ELF_PF_X) vm_flags |= VM_EXEC; // 根據ELF檔案中的資訊,對各個段的許可權進行設定 if (php->p_flags & ELF_PF_W) vm_flags |= VM_WRITE; if (php->p_flags & ELF_PF_R) vm_flags |= VM_READ; if (vm_flags & VM_WRITE) perm |= PTE_W; if ((ret = mm_map(mm, php->p_va, php->p_memsz, vm_flags, NULL)) != 0) { // 將這些段的虛擬記憶體地址設定為合法的 goto bad_cleanup_mmap; } // (3.4) allocate pages for TEXT/DATA sections offset = php->p_offset; size_t off, size; uintptr_t start = php->p_va, end = php->p_va + php->p_filesz, la = ROUNDDOWN(start, PGSIZE); ret = -E_NO_MEM; while (start < end) { if ((page = pgdir_alloc_page(mm->pgdir, la, perm)) == NULL) { // 為TEXT/DATA段逐頁分配實體記憶體空間 goto bad_cleanup_mmap; } off = start - la, size = PGSIZE - off, la += PGSIZE; if (end < la) { size -= la - end; } load_icode_read(fd, page2kva(page) + off, size, offset);// 將磁碟上的TEXT/DATA段讀入到分配好的記憶體空間中去 //memcpy(page2kva(page) + off, page2kva(buff_page), size); start += size, offset += size; } // (3.5) allocate pages for BSS end = php->p_va + php->p_memsz; if (start < la) { // 如果存在BSS段,並且先前的TEXT/DATA段分配的最後一頁沒有被完全佔用,則剩餘的部分被BSS段佔用,因此進行清零初始化 if (start == end) { continue; } off = start + PGSIZE - la, size = PGSIZE - off; if (end < la) { size -= la - end; } memset(page2kva(page) + off, 0, size); // init all BSS data with 0 start += size; assert((end < la && start == end) || (end >= la && start == la)); } while (start < end) {// 如果BSS段還需要更多的記憶體空間的話,進一步進行分配 if ((page = pgdir_alloc_page(mm->pgdir, la, perm)) == NULL) { // 為BSS段分配新的實體記憶體頁 goto bad_cleanup_mmap; } off = start - la, size = PGSIZE - off, la += PGSIZE; if (end < la) { size -= la - end; } memset(page2kva(page), 0, size); // 將分配到的空間清零初始化 start += size; } } sysfile_close(fd); // 關閉傳入的檔案,因為在之後的操作中已經不需要讀檔案了 // (4) setup user stack vm_flags = VM_READ | VM_WRITE | VM_STACK; // 設定使用者棧的許可權 if ((ret = mm_map(mm, USTACKTOP - USTACKSIZE, USTACKSIZE, vm_flags, NULL)) != 0) { // 將使用者棧所在的虛擬記憶體區域設定為合法的 goto bad_cleanup_mmap; } // setup args uint32_t stacktop = USTACKTOP; uint32_t argsize = 0; for (int j = 0; j < argc; ++ j) {// 確定傳入給應用程式的引數具體應當佔用多少空間 argsize += (1 + strlen(kargv[j])); // includinng the ending '\0' } argsize = (argsize / sizeof(long) + 1) * sizeof(long); //alignment argsize += (2 + argc) * sizeof(long); stacktop = USTACKTOP - argsize; // 根據引數需要在棧上佔用的空間來推算出,傳遞了引數之後棧頂的位置 uint32_t pagen = argsize / PGSIZE + 4; for (int j = 1; j <= 4; ++ j) { // 首先給棧頂分配四個物理頁 assert(pgdir_alloc_page(mm->pgdir, USTACKTOP - PGSIZE * j, PTE_USER) != NULL); } // for convinience, setup mm (5) mm_count_inc(mm); // 切換到使用者的記憶體空間,這樣的話後文中在棧上設定引數部分的操作將大大簡化,因為具體因為空間不足而導致的分配物理頁的操作已經交由page fault處理了,是完全透明的 current->mm = mm; current->cr3 = PADDR(mm->pgdir); lcr3(PADDR(mm->pgdir)); // (6) setup args in user stack uint32_t now_pos = stacktop, argvp; *((uint32_t *) now_pos) = argc; // 設定好argc引數(壓入棧) now_pos += 4; *((uint32_t *) now_pos) = argvp = now_pos + 4; // 設定argv陣列的位置 now_pos += 4; now_pos += argc * 4; for (int j = 0; j < argc; ++ j) { argsize = strlen(kargv[j]) + 1;// 將argv[j]指向的資料拷貝到使用者棧中 memcpy((void *) now_pos, kargv[j], argsize); *((uint32_t *) (argvp + j * 4)) = now_pos; // 設定好使用者棧中argv[j]的數值 now_pos += argsize; } // (7) setup tf struct trapframe *tf = current->tf; // 設定中斷幀 memset(tf, 0, sizeof(struct trapframe)); tf->tf_cs = USER_CS; // 需要返回到使用者態,因此使用使用者態的資料段和程式碼段的選擇子 tf->tf_ds = tf->tf_es = tf->tf_ss = USER_DS; tf->tf_esp = stacktop; // 棧頂位置為先前計算過的棧頂位置,注意在C語言的函式呼叫規範中,棧頂指標指向的位置應該是返回地址而不是第一個引數,這裡讓棧頂指標指向了第一個引數的原因在於,在中斷返回之後,會跳轉到ELF可執行程式的入口處,在該入口處會進一步使用call命令呼叫主函式,這時候也就完成了將Return address入棧的功能,因此這裡無需畫蛇添足壓入返回地址 tf->tf_eip = elfp->e_entry; // 將返回地址設定為使用者程式的入口 tf->tf_eflags = 0x2 | FL_IF; // 允許中斷,根據IA32的規範,eflags的第1位需要恆為1 ret = 0; out: return ret; bad_cleanup_mmap: // 進行載入失敗的一系列清理操作 exit_mmap(mm); bad_elf_cleanup_pgdir: put_pgdir(mm); bad_pgdir_cleanup_mm: mm_destroy(mm); bad_mm: goto out; }
- 至此,完成了本練習中的所有編碼任務;
問題回答
- 請在實驗報告中給出設計實現基於”UNIX的硬連結和軟連結機制“的概要設方案,鼓勵給出詳細設計方案;
- 觀察到儲存在磁碟上的inode資訊均存在一個nlinks變數用於表示當前檔案的被連結的計數,因而支援實現硬連結和軟連結機制;
- 如果在磁碟上建立一個檔案A的軟連結B,那麼將B當成正常的檔案建立inode,然後將TYPE域設定為連結,然後使用剩餘的域中的一個,指向A的inode位置,然後再額外使用一個位來標記當前的連結是軟連結還是硬連結;
- 當訪問到檔案B(read,write等系統呼叫),判斷如果B是一個連結,則實際是將對B指向的檔案A(已經知道了A的inode位置)進行操作;
- 當刪除一個軟連結B的時候,直接將其在磁碟上的inode刪掉即可;
- 如果在磁碟上的檔案A建立一個硬連結B,那麼在按照軟連結的方法建立完B之後,還需要將A中的被連結的計數加1;
- 訪問硬連結的方式與訪問軟連結是一致的;
- 當刪除一個硬連結B的時候,除了需要刪除掉B的inode之外,還需要將B指向的檔案A的被連結計數減1,如果減到了0,則需要將A刪除掉;
- 觀察到儲存在磁碟上的inode資訊均存在一個nlinks變數用於表示當前檔案的被連結的計數,因而支援實現硬連結和軟連結機制;
實驗結果
最終的實驗結果符合預期,並且能夠通過make grade指令碼的檢查,如下圖所示:

result1.png

result2.png
參考答案分析
練習1
比較練習1的實現與參考答案的實現的區別在於一些細節方面的實現,主要體現在對完全被讀寫區域覆蓋的資料塊進行讀寫的時候,提供的函式事實上是可以完成連續若干塊資料塊的讀寫的,但是參考答案沒有利用這個特點,而是額外添加了一個迴圈,然後在迴圈中對每一個數據塊逐次進行讀取操作,這有可能會造成時間效率的降低;
練習2
本實驗在練習2中的實現與參考答案的實現大致一致,但是經過仔細的比較,觀察到一個細節,參考答案在確定引數的長度的時候使用的函式時strnlen,而本實驗中的實現使用了strlen,而後者是不安全的,有可能遭到棧溢位攻擊的,因此在這個區別上,參考答案的實現明顯優於本實驗的實驗,這也其實我在完成實際的程式設計任務的時候需要充分考慮到魯棒性、安全性等細節,這也是我自己覺得在本次作業系統實驗中做得有所欠缺的地方;
實驗中涉及的知識點列舉
-
在本次實驗中涉及到的知識點如下:
- 虛擬檔案系統;
- SFS檔案系統;
- 將裝置抽象為檔案的管理方式;
- 系統呼叫;
- 程序間的排程、管理;
- ELF檔案格式;
- ucore中使用者程序虛擬空間的劃分;
-
對應的OS中的知識點如下:
- 在ucore中檔案系統、虛擬檔案系統、以及SFS檔案系統的具體實現;
- 在ucore中將stdin,stdout抽象成檔案的機制;
- 在ucore中系統呼叫的機制;
- 在ucore中完成ELF檔案從磁碟到記憶體的載入的具體機制;
-
它們之間的關係為:
- 前者為後者提供了底層的支援,比如對SFS檔案系統的瞭解才能夠使得可以在OS中正確地實現對使用該檔案系統的磁碟的訪問;
- 前者給後者提供了必要的基礎知識,比如只有瞭解了ELF檔案的格式,以及瞭解了使用者程序空間的劃分之後,才能夠正確地在OS中實現將指定ELF檔案載入到記憶體中執行的操作(exec系統呼叫);
實驗中未涉及的知識點列舉
在本次實驗中未涉及到的知識點列舉如下:
- 程序之間的同步互斥機制;
- 作業系統的啟動機制;
- 作業系統對網路協議棧的支援;
實驗程式碼
https://github.com/AmadeusChan/ucore_os_lab/tree/master/lab8
參考文獻
- INTEL 80386 PROGRAMMER'S REFERENCE MANUAL 1986