mit6.828-lab5 文件系統 1 文件系統初步 2 文件系統實現 3 Spawning 進程 4 鍵盤接口 5 Shell

lab5是實現文件系統相關功能,exercize實現代碼見 這裏

1 文件系統初步

JOS文件系統設計相比Linux等系統的文件系統如ext2,ext3等,要簡化不少。它不支持用戶和權限特性,也不支持硬鏈接,符號鏈接,時間戳以及特殊設備文件等。

1.1 磁盤文件系統結構

大部分Unix文件系統會將磁盤空間分爲inode和data兩個部分,如linux就是這樣的,其中inode用於存儲文件的元數據,比如文件類型(常規、目錄、符號鏈接等),權限,文件大小,創建/修改/訪問時間,文件數據塊信息等,我們運行的ls -l看到的內容,都是存儲在inode而不是數據塊中的。數據部分通常分爲很多數據塊,數據塊用於存儲文件的數據信息以及目錄的元數據(目錄元數據包括目錄下文件的inode,文件名,文件類型等)。

文件和目錄邏輯上都是由一系列數據塊構成,可以像進程那樣將虛擬地址空間映射到物理內存,文件系統需要隱藏數據塊分佈細節,對外只需要提供文件操作方法即可,如open,read,write,close等。JOS文件系統的實現跟Linux的不同,它沒有使用系統調用實現,而是通過我們之前完成的IPC功能來實現文件操作的。系統會在啓動時運行一個文件系統進程,該進程接收用戶進程的IPC請求並完成文件的各種操作。

1.2 磁盤扇區、數據塊、超級塊

扇區是磁盤的物理屬性,通常一個扇區大小爲512字節,而數據塊則是操作系統使用磁盤的一個邏輯屬性,一個塊大小通常是扇區的整數倍,在JOS中一個塊大小爲4KB,跟我們物理內存的頁大小一致。

文件系統通常會保留一些磁盤上很容易找到的數據塊用於存儲磁盤的元數據,這些特殊的塊叫超級塊(superblock)。JOS中有一個超級塊,用的塊1用作超級塊。塊0通常保留給啓動塊和分區,所以文件系統沒有使用塊0做超級塊。

1.3 文件元數據

JOS的文件元數據存儲在 inc/fs.hstruct File中。元數據包括文件名,文件大小,文件類型以及指向的數據塊。因爲JOS沒有使用inode,元數據信息就存儲在磁盤的目錄項中,不像Linux那樣,JOS使用同一個 File 結構存儲了磁盤和內存中的文件元數據。

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

struct File中的f_direct數組存儲了前10個數據塊的塊號,這10個數據塊是直接塊。每個塊爲4KB,所以直接塊可以存儲40KB內的小文件。而對於大文件,File中還支持一個間接塊,間接塊可以存儲 4096/4 = 1024 個塊號,即JOS中最大可以存儲1034個塊大小的文件,即最大支持4MB左右的文件。在Linux中,還有二級間接塊以及三級間接塊等,用於存儲更大的文件。

1.4 文件和目錄

JOS文件系統中的struct File 可以代表一個常規文件或者目錄,通過 f_type 來區分。文件系統管理常規文件和目錄文件採用的是一樣的方式,唯一區別是對常規文件,文件系統並不解析數據塊內容,而對於目錄,則會將數據內容解析爲 struct File 的格式。

JOS文件系統中的超級塊包含了一個 struct File結構的字段root,用於存儲文件系統的根目錄元數據。根目錄文件的數據塊存儲的則是該目錄下的文件的元數據,如果根目錄下有子目錄,則這裏會存儲子目錄的元數據。

2 文件系統實現

本實驗我們要完成的功能包括:

  • 讀取磁盤中的數據塊到塊緩存以及將塊緩存中的數據刷回磁盤。
  • 分配數據塊。
  • 映射文件偏移到磁盤數據塊。
  • 在IPC接口實現文件的open,read,write,close。

文件系統鏡像是在 fs/fsformat.c 中創建的,最終在QEMU中加載的文件系統鏡像文件爲 obj/fs/fs.img,其中內核鏡像在磁盤0,文件系統鏡像在磁盤1。文件系統的第0,1,2數據塊分別用於啓動塊,超級塊,以及塊位圖。而因爲在文件系統中初始加入了 user 目錄和fs 目錄的一些文件,一共用掉了0-110塊,所以空閒塊從111開始。

qemu-system-i386 -drive file=obj/kern/kernel.img,index=0,media=disk,format=raw -serial 
mon:stdio -gdb tcp::26000 -D qemu.log -smp 1 -drive 
file=obj/fs/fs.img,index=1,media=disk,format=raw 

2.1 磁盤訪問

不同於Linux等系統那樣增加一個磁盤驅動並添加相關係統調用實現磁盤訪問,JOS的磁盤驅動是用用戶級程序實現的,當然還是要對內核做一點修改,以支持文件系統進程(用戶級進程)有權限訪問磁盤。

在用戶空間訪問磁盤可以通過輪詢的方式實現,而不是使用磁盤中斷的方式,因爲使用中斷的方式會複雜不少。x86處理器使用 EFLAGS 寄存器的 IOPL 位來控制磁盤訪問權限(即IN和OUT指令),用戶代碼能否訪問IO空間就通過該標誌來設置。JOS在i386_init()中運行了一個用戶級的文件系統進程,該進程需要有磁盤訪問權限。因此作業1就是在 env_create 中對 文件系統進程 這個特殊的運行在用戶級的進程設置 IOPL 權限,而其他的用戶進程不能設置該權限,根據進程類型設置權限即可。

ENV_CREATE(fs_fs, ENV_TYPE_FS);

特殊的文件系統進程代碼在 fs/fs.c,它提供了file_open,file_read,file_write,file_flush文件操作函數以及file_get_block, file_block_walk數據塊操作函數等。

2.2 塊緩存

JOS文件系統將 0x10000000(DISKMAP) 到 0xD0000000(DISKMAP+DISKMAX)這個區間的地址空間映射到磁盤,即JOS可以處理3GB的磁盤文件。如0x1000000 映射到數據塊0,0x10001000 映射到數據庫1。塊緩存代碼在 fs/bc.c 中,其中 diskaddr 函數可以完成數據塊號到虛擬地址的轉換。

因爲文件系統進程自己有地理的虛擬地址空間,所以讓它保留3GB虛擬空間地址用於映射文件是沒問題的。當然我們不會一次將文件全部讀到內存中,JOS採用的是demand paging,即訪問對應的磁盤塊發生了頁錯誤時才分配物理頁。具體實現在 bc_pgfault 函數中,有點類似COW fork()的實現,ide_read() 的單位是扇區,不是磁盤塊,通過 outb 指令設置讀取的扇區數,通過insl指令讀取磁盤數據到對應的虛擬地址addr處。bc_pgfault 中分配了一頁物理頁,然後從磁盤中讀取出錯的addr那一塊數據(8個扇區)到分配的物理頁中,然後清除分配頁的dirty標記,最後調用 block_is_free 檢查對應磁盤塊確保磁盤塊已經分配。注意這裏檢查磁盤塊是否已經分配要在最後檢查,是因爲bitmap的值是在fs_init時指定的爲diskaddr(2),即0x10002000,在準備讀取第二個磁盤塊發生頁錯誤進入bgfault時,此時bitmap對應塊還沒有從磁盤讀取並映射好,所以要在最後檢查。

flush_block()函數用於在寫入磁盤數據到塊緩存後,調用 ide_write() 寫入塊緩存數據到磁盤中。寫入完成後,也要通過 sys_page_map() 清除塊緩存的 dirty 標記(每次寫入物理頁的時候,處理器會自動標記該頁爲 dirty,即設置PTE_D標記)。注意,在flush_block()中,如果該地址並沒有映射或者並沒有dirty,則不需要做任何事情。

bc.c中的bc_init用於完成塊緩存初始化,它完成下面幾件事:

  • 1)設置頁錯誤處理函數爲 bc_pgfault。
  • 2)調用 check_bc() 檢查塊緩存設置是否正確。
  • 3)讀取磁盤塊1的數據到函數局部變量super對應的地址中。(這一步沒有什麼作用,super變量也沒有用到過,應該是老代碼遺留問題)

2.3 塊位圖

在fs_init設置bitmap指針後,可以認爲bitmap就是一個位數組,每個塊佔據一位。可以通過 block_is_free 檢查塊位圖中的對應塊是否空閒,如果爲1表示空閒,爲0已經使用。JOS中第0,1,2塊分別給bootloader,superblock以及bitmap使用了。此外,因爲在文件系統中加入了user目錄和fs目錄的文件,導致JOS文件系統一共用掉了0-110這111個文件塊,下一個空閒文件塊從111開始。

2.4 文件操作

在 fs/fs.c 中有很多文件操作相關的函數,這裏的主要幾個結構體要說明下:

  • struct File 用於存儲文件元數據,前面提到過。

  • struct Fd 用於文件模擬層,類似文件描述符,如文件ID,文件打開模式,文件偏移都存儲在Fd中。一個進程同時最多打開 MAXFD(32) 個文件。

  • 文件系統進程還維護了一個打開文件的描述符表,即opentab數組,數組元素爲 struct OpenFile。OpenFile結構體用於存儲打開文件信息,包括文件ID,struct File以及struct Fd。JOS同時打開的文件數一共爲 MAXOPEN(1024) 個。

    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                                             
    };    
    
    struct Fd {
        int fd_dev_id;
        off_t fd_offset;
        int fd_omode;
        union {
            // File server files
            struct FdFile fd_file;
        };  
    }; 
    

文件操作函數如下:

  • file_block_walk(struct File *f, uint32_t filebno, uint32_t **ppdiskbno, bool alloc)

    這個函數是查找文件第filebno塊的數據塊的地址,查到的地址存儲在 ppdiskbno 中。注意這裏要檢查間接塊,如果alloc爲1且尋址的塊號>=NDIRECT,而間接塊沒有分配的話需要分配一個間接塊。

  • file_get_block(struct File *f, uint32_t filebno, char **blk)

    查找文件第filebno塊的塊地址,並將塊地址在虛擬內存中映射的地址存儲在 blk 中(即將diskaddr(blockno)存到blk中)。

  • dir_lookup(struct File *dir, const char *name, struct File **file)

    在目錄dir中查找名爲name的文件,如果找到了設置*file爲找到的文件。因爲目錄的數據塊存儲的是struct File列表,可以據此來查找文件。

  • file_open(const char *path, struct File **pf)

    打開文件,設置*pf爲查找到的文件指針。

  • file_create(const char *path, struct File *pf)
    創建路徑/文件,在
    pf存儲創建好的文件指針。

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

    從文件的offset處開始讀取count個字節到buf中,返回實際讀取的字節數。

  • file_write(struct File *f, const void *buf, size_t count, off_t offset)

    從文件offset處開始寫入buf中的count字節,返回實際寫入的字節數。

2.5 文件系統接口

完成了基本函數後,現在可以通過IPC來實現JOS的文件系統操作了。流程圖如下所示:

 Regular env           FS env
   +---------------+   +---------------+
   |      read     |   |   file_read   |
   |   (lib/fd.c)  |   |   (fs/fs.c)   |
...|.......|.......|...|.......^.......|...............
   |       v       |   |       |       | RPC mechanism
   |  devfile_read |   |  serve_read   |
   |  (lib/file.c) |   |  (fs/serv.c)  |
   |       |       |   |       ^       |
   |       v       |   |       |       |
   |     fsipc     |   |     serve     |
   |  (lib/file.c) |   |  (fs/serv.c)  |
   |       |       |   |       ^       |
   |       v       |   |       |       |
   |   ipc_send    |   |   ipc_recv    |
   |       |       |   |       ^       |
   +-------|-------+   +-------|-------+
           |                   |
           +-------------------+

寫文件過程類似,流程是devfile_write -> serve_write -> file_write。這裏分析幾個例子看下JOS讀寫文件流程:

直接讀文件

這裏跳過文件描述符層,直接打開文件並讀取

if ((r = xopen("/not-found", O_RDONLY)) < 0 && r != -E_NOT_FOUND)
    panic("serve_open /not-found: %e", r); 
else if (r >= 0)
    panic("serve_open /not-found succeeded!");

if ((r = xopen("/newmotd", O_RDONLY)) < 0)
    panic("serve_open /newmotd: %e", r); 
if (FVA->fd_dev_id != 'f' || FVA->fd_offset != 0 || FVA->fd_omode != O_RDONLY)
    panic("serve_open did not fill struct Fd correctly\n");
cprintf("serve_open is good\n");
    
memset(buf, 0, sizeof buf);
if ((r = devfile.dev_read(FVA, buf, sizeof buf)) < 0)
    panic("file_read: %e", r); 
if (strcmp(buf, msg) != 0)
    panic("file_read returned wrong data");
cprintf("file_read is good\n");
    1. fs 進程首先調用 serve_init 完成opentab的初始化,然後在 地址 0x0ffff000 處 接收IPC的頁。
  • 2)測試進程通過 IPC 發送FSREQ_OPEN請求,請求參數在 fsipcbuf所在頁中,然後在 FVA (0xCCCCC000)處接收fs進程的IPC頁。
  • 3)fs進程的serve() 接收到 FSREQ_OPEN 請求,調用 serve_open() 處理該請求。會先分配一個OpenFile結構給文件,設置o_file爲文件指針,o_fd爲文件描述符等,IPC映射的頁的權限爲 PTE_SHARE 等,然後將文件描述符所在的頁作爲參數發送IPC請求給測試進程。
    1. 測試進程在 FVA 處讀取打開的文件描述符信息,然後返回。

直接讀取文件

  • 1)調用devfile_read發送fsipc到文件系統進程。
  • 2)fs進程通過ipc_recv接收fsipc請求,然後傳給serve函數處理。serve函數根據fspic請求類型,調用 serve_read 處理請求。
  • 3)fs系統進程最終通過 file_read 完成文件讀取。文件讀取結果存儲到了fsipcbuf中的readRet中,恰好是一頁的大小,而且這個是測試進程一開始就映射了的頁面,可以直接讀取。

直接寫入文件

與讀取文件類似,只是不用返回讀取結果了,在IPC中返回寫入字節數即可。路徑是serve()->serve_write()->devfile_write()->file_write()

通過文件描述符打開/讀取/寫入文件

  • 通過文件描述符打開文件時,測試進程會先通過 fd_alloc() 分配一個文件描述符,然後在文件描述符fd 處接收fs進程的IPC頁,分配的fd的地址爲 (0xD0000000 + i * PGSIZE)。後面的流程跟之前直接操作類似。
  • 通過文件描述符讀取寫入文件,會先通過 fd_lookup() 找到文件描述符對應的文件信息,然後再根據設備類型調用相應的讀寫操作。如文件就是 devfile_read/devfile_write,console就是devcons_read/devcons_write。

3 Spawning 進程

spawn代碼用於創建一個子進程,然後從磁盤中加載一個程序代碼鏡像並在子進程運行加載的程序。這有點類似Unix的fork+exec,但是又有所不同,因爲我們的spawn進程運行在用戶空間,我們通過一個新的系統調用 sys_env_set_trapframe簡化了一些操作。

在fork和spawn中,JOS需要實現文件描述符的共享,這裏引入了一個新的 PTE_SHARE 標識,用於標識共享頁,這樣在拷貝時可以進行統一處理,不再類似fork那樣用COW,而是直接共享。因爲用fork的話,如子進程修改了文件數據,此時會新分配一個頁來保存修改數據,而父進程裏面對應頁面是沒有變化的,這樣無法在父子進程共享文件的變化。

spawn的流程如下:

  • 打開文件,獲取文件描述符fd。
  • 讀取ELF頭部,檢查ELF文件魔數。
  • 調用 sys_exofork() 創建一個子進程。
  • child_tf 設置,主要是設置了eip爲ELF文件的入口點e_entry,設置esp爲init_stack()分配的棧空間。
  • 最後將 ELF 文件映射到子進程的地址空間,並根據ELF的讀寫段來設置讀寫權限。
  • 拷貝共享的頁。
  • 調用sys_env_set_trapframe()設置子進程的env_tf位child_tf。
  • 調用 sys_env_set_status() 設置子進程爲RUNNABLE狀態。

看過之前實驗的朋友可能發現了,我之前的fork在duppage時拷貝代碼空間是以程序代碼的 end 爲結束的,現在看來這是有問題的,因爲文件系統映射的地址並不在其中,需要將end改爲 USTACKTOP-PGSIZE。此外,在duppage和spawn.c中的copy_shared_pages中要對 PTE_SHARE 做處理,直接映射即可,權限要用 PTE_SYSCALL,因爲文件系統相關的頁權限都是用的PTE_SYSCALL,否則會檢查失敗。

4 鍵盤接口

用戶鍵盤輸入會產生鍵盤中斷IRQ_KBD(通過QEMU圖形界面輸入觸發)或者串口中斷IRQ_SERIAL(通過console觸發),爲此要在trap.c中處理這兩個中斷,分別調用 kbd_intr() 和 serial_intr() 即可。

這裏有個地方注意下,測試進程 user/testkbd.c 中用的是 readline() 來讀取用戶輸入的,它的流程如下:

readline() -> lib/console.c getchar() -> read() -> devcons_read() 
           -> lib/syscall.c sys_cgetc() -> kern/syscall.c sys_cgetc()
           -> kern/console.c cons_getc()

其實在cons_getc()中調用了 kbd_intr() 和 serial_intr() 這兩個函數,因此在我們之前的實驗內核的 monitor 中已經屏蔽了中斷,一樣可以讀取到鍵盤輸入。

5 Shell

JOS的Shell實現了管道,IO重定向等功能,具體實現在 user/sh.c 中,在make run-icode後,我們可以運行下面的代碼來測試:

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

這裏有幾個小程序如cat,echo等,管道和IO重定向具體實現如下:

輸入/輸出重定向

輸入重定向:跟Linux一樣,使用 < 語法。實現原理就是使用 dup(fd,0) 將文件描述符拷貝到標準輸入0,然後關閉fd,最後從標準輸入中讀取文件內容即可。如 cat < script便是先將script文件重定向到標準輸入0,最後spawn執行cat從標準輸入讀取內容並輸出到標準輸出。而如果使用 sh < script,又是不同的,此時spawn進程執行的是sh進程,它會先讀取script文件內容,然後對script文件內容一行行命令spawn執行。

輸出重定向:使用 > 語法。實現原理就是使用 dup(fd, 1)將文件描述符拷貝到標準輸出1,然後關閉fd,這樣輸出到標準輸出就相當於輸出到文件了。如 echo haha > motd,會將motd文件內容改爲 haha。

管道

JOS管道實現在lib/pipe.c,它分配兩個文件描述符作爲管道輸入輸出端,設備類型爲管道,對應的數據頁部分映射到了同樣的物理頁,只是設置的文件描述符的權限不同,pipe[0]對應的文件描述符爲只讀,而pipe[1]可寫。然後fork()創建一個子進程,子進程中將 pipe[0] 拷貝到標準輸入,然後重新讀取輸入運行管道右邊的命令。父進程中則是將 pipe[1] 拷貝到標準輸出。父進程會先 spawn運行左邊命令,輸出會重定向到標準輸出,即pipe[1]這個fd。而子進程接着從標準輸入讀取輸入,也就是從pipe[0]這個fd讀取輸入,然後輸出結果。管道讀寫使用方法是 devpipe_read和devpipe_write,如果管道沒有數據可讀,則會sys_yield() 調度其他進程先運行。

如運行命令 echo haha|cat,則先父進程先spawn一個進程運行 echo haha,並將輸出haha重定向到 pipe[1],而子進程接着spawn一個進程運行 cat,它從pipe[0]讀取輸入,而因爲 pipe[0] 和 pipe[1] 映射的是同樣的物理頁面,所以可以讀取到pipe[1]中的內容,從而實現了管道功能。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章