You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
pke-doc/chapter6_filesystem.md

126 KiB

第六章实验4文件系统

目录

6.1 实验4的基础知识

本章我们将首先以Linux的文件系统为例介绍文件系统的基础知识接着讲述 riscv-pke操作系统内核的文件系统设计然后开始对PKE实验4的基础实验进行讲解以加深读者对文件系统底层逻辑的理解。

6.1.1 文件系统概述

文件系统是操作系统用于存储、组织和访问计算机数据的方法它使得用户可以通过一套便利的文件访问接口来访问存储设备常见的是磁盘也有基于NAND Flash的固态硬盘或分区上的文件。操作系统中负责管理和存储文件信息的软件机构称为文件管理系统,简称文件系统。 文件系统所管理的最基本的单位,是有具体且完整逻辑意义的“文件”,文件系统实现了对文件的按名(即文件名)存取。为避免“重名”问题,文件系统的常用方法是采用树型目录,来辅助(通过加路径)实现文件的按名存取。存储在磁盘上,实现树型目录的数据结构(如目录文件等),往往被称为“元数据meta-data。实际上文件系统中辅助实现对文件进行检索和存储的数据都被称为元数据。从这个角度来看文件系统中对磁盘空间进行管理的数据以及可能的缓存数据也都是重要的元数据。

不同的文件系统往往采用不同种类的元数据来完成既定功能。对不同类型元数据的采用往往也区分了不同类型的文件系统。例如Linux中的ext系列文件系统Windows中广泛采用的fat系列文件系统、NTFS文件系统等。虽然完成的功能一样但它们在具体实现上却也存在巨大差异。由于PKE实验采用了Spike模拟器它具有和主机进行交互的HTIF接口所以在PKE实验4我们将自然接触到一类很特殊的文件系统hostfsHost File System。它实际上是位于主机上的文件系统PKE通过定义一组接口使得在Spike所构造的虚拟RISC-V机器上运行的应用能够对主机上文件进行访问。另外我们将分配一段内存作为我们的“磁盘”即RAM Disk并在该磁盘上创建一个简单的名称为RFSRamdisk File System的文件系统。

由于有多个文件系统的存在且PKE需要同时对这两类文件系统进行支持。所以在PKE的实验四我们引入了虚拟文件系统Virtual File System有时也被称为Virtual Filesystem Switch简称都是VFS的概念。VFS也是构建现代操作系统中文件系统的重要概念通过实验四读者将对这一重要概念进行学习。在后面的讨论中我们将依次介绍PKE的文件系统架构、了解PKE文件系统对进程所提供的接口虚拟文件系统的构造,以及我们自己定义的RFS文件系统。目标是辅助读者构建对PKE文件系统代码的理解为完成后续的基础和挑战实验做好准备。

6.1.2 PKE的文件系统架构

PKE文件系统架构如下图所示图中的RAM DISK在文件系统中的地位等价于磁盘设备在其上“安装”的文件系统就是RFS。特别要注意的一点是在后文讨论RFS时会存在“某种数据结构被保存在磁盘中”这样的表述这通常意味着这种数据结构实际上被保存在上图的RAM DISK中。除了RFS外PKE文件系统通过虚拟文件系统对主机文件系统hostfs进行了支持在riscv-pke操作系统内核上运行的应用程序能够对这两个文件系统中的文件进行访问。

1660396457538

在PKE系统启动时会在初始化阶段将这两个文件系统进行挂载见kernel/kernel.c文件中对S模式的启动代码的修改

 53 int s_start(void) {
 54   sprint("Enter supervisor mode...\n");
 55   // in the beginning, we use Bare mode (direct) memory mapping as in lab1.
 56   // but now, we are going to switch to the paging mode @lab2_1.
 57   // note, the code still works in Bare mode when calling pmm_init() and kern_vm_init().
 58   write_csr(satp, 0);
 59
 60   // init phisical memory manager
 61   pmm_init();
 62
 63   // build the kernel page table
 64   kern_vm_init();
 65
 66   // now, switch to paging mode by turning on paging (SV39)
 67   enable_paging();
 68   // the code now formally works in paging mode, meaning the page table is now in use.
 69   sprint("kernel page table is on \n");
 70
 71   // added @lab3_1
 72   init_proc_pool();
 73
 74   // init file system, added @lab4_1
 75   fs_init();
 76
 77   sprint("Switch to user mode...\n");
 78   // the application code (elf) is first loaded into memory, and then put into execution
 79   // added @lab3_1
 80   insert_to_ready_queue( load_user_program() );
 81   schedule();
 82
 83   // we should never reach here.
 84   return 0;
 85 }

我们看到在第75行增加了对函数fs_init()的调用。fs_init()的定义在kernel/proc_file.c文件中

21 void fs_init(void) {
22   // initialize the vfs
23   vfs_init();
24 
25   // register hostfs and mount it as the root
26   if( register_hostfs() < 0 ) panic( "fs_init: cannot register hostfs.\n" );
27   struct device *hostdev = init_host_device("HOSTDEV");
28   vfs_mount("HOSTDEV", MOUNT_AS_ROOT);
29 
30   // register and mount rfs
31   if( register_rfs() < 0 ) panic( "fs_init: cannot register rfs.\n" );
32   struct device *ramdisk0 = init_rfs_device("RAMDISK0");
33   rfs_format_dev(ramdisk0);
34   vfs_mount("RAMDISK0", MOUNT_DEFAULT);
35 }

我们看到初始化过程首先初始化了VFS层的两个哈希表结构23行然后对hostfs文件系统进行了初始化26--28行接下来对RFS文件系统进行了初始化31--34。对于RFS的初始化甚至包含了对RAM DISK的格式化33行。初始化过程会通过vfs_mount函数把这两种不同类型的文件系统挂载到PKE的虚拟文件系统的不同目录下。

1660396457538

从上图中我们看到启动阶段PKE会把hostfs挂载到根目录而把RFS文件系统挂载到/RAMDISK0目录下。由于hostfs的本质是通过spike的HTIF接口对主机上的文件进行访问hostfs在挂载时会将主机上源代码目录下的hostfs_root目录做为“根目录”加载到PKE文件系统中做为后者的“根目录”。实际上这个主机上被加载为根目录的目录是可以更改的见kernel/hostfs.h文件中的定义

  1 #ifndef _HOSTFS_H_
  2 #define _HOSTFS_H_
  3 #include "vfs.h"
  4
  5 #define HOSTFS_TYPE 1
  6
  7 // dinode type
  8 #define H_FILE FILE_I
  9 #define H_DIR DIR_I
 10
 11 // root directory
 12 #define H_ROOT_DIR "./hostfs_root"
 13
 14 // hostfs utility functin declarations
 15 int register_hostfs();
 16 struct device *init_host_device(char *name);
 17 void get_path_string(char *path, struct dentry *dentry);
 18 struct vinode *hostfs_alloc_vinode(struct super_block *sb);
 19 int hostfs_write_back_vinode(struct vinode *vinode);
 20 int hostfs_update_vinode(struct vinode *vinode);
 21
 22 // hostfs interface function declarations
 23 ssize_t hostfs_read(struct vinode *f_inode, char *r_buf, ssize_t len,
 24                     int *offset);
 25 ssize_t hostfs_write(struct vinode *f_inode, const char *w_buf, ssize_t len,
 26                      int *offset);
 27 struct vinode *hostfs_lookup(struct vinode *parent, struct dentry *sub_dentry);
 28 struct vinode *hostfs_create(struct vinode *parent, struct dentry *sub_dentry);
 29 int hostfs_lseek(struct vinode *f_inode, ssize_t new_offset, int whence,
 30                   int *offset);
 31 int hostfs_hook_open(struct vinode *f_inode, struct dentry *f_dentry);
 32 int hostfs_hook_close(struct vinode *f_inode, struct dentry *dentry);
 33 struct super_block *hostfs_get_superblock(struct device *dev);
 34
 35 extern const struct vinode_ops hostfs_node_ops;
 36
 37 #endif

注意kernel/hostfs.h文件中第12行的定义H_ROOT_DIR的定义其实是可以修改的感兴趣的读者可以在完成实验后自行修改这个定义把PKE文件系统的根目录定向到自己选择的主机目录下。但是需要注意的是如果更改了这个设置PKE文件系统的根目录下就具有不同的初始文件和目录了。对于RFS文件系统它的根目录被加载到了PKE文件系统的/RAMDISK0目录下。对于RFS实现细节的讨论我们将在6.1.5节中进行。

在应用对于文件或者子目录进行访问时,会通过被访问的文件或子目录的路径进行判断,来决定被访问的文件的具体放置位置。例如:

  • 如果访问的路径是/dir1/file1则实际被访问的文件位于hostfs中且被访问的文件在主机上的存放位置为[源代码目录]/hostfs_root/dir1/file1
  • 如果访问的路径是/RAMDISK0/dir2/file2则该文件就应该位于RFS中且文件实际存放的位置为[RFS的根目录]/dir2/file2。

6.1.3 文件系统提供的接口

在PKE文件系统架构下进程的读写文件函数调用关系如下

1660396457538

从用户程序发出的文件读写操作如open、u_read、u_write等将会首先通过syscall传递到内核态空间紧接着syscall将调用proc_file.c文件中定义的文件读写动作如do_open、do_read、do_write等。而proc_file.c中的动作又会调用vfs中定义的数据结构将动作进一步传导到具体的文件系统主机文件系统或者RFS文件系统传递到相应的动作函数最后再传导到不同的设备上HTIF接口或者RAM DISK。在下面的讨论中我们将按照自顶向下的顺序对PKE文件系统进行讨论。

在lab4中PKE为进程定义了一个“打开文件表”并用一个管理数据结构proc_file_management对一个进程所打开的文件进行管理。见kernel/proc_file.h中的定义

21 // data structure that manages all openned files in a PCB
22 typedef struct proc_file_management_t {
23   struct dentry *cwd;  // vfs dentry of current working directory
24   struct file opened_files[MAX_FILES];  // opened files array
25   int nfiles;  // the number of files opened by a process
26 } proc_file_management;

我们看到proc_file_management结构保存了一个当前目录的dentry以及一个“打开文件”数组。该结构是每个PKE进程都有的这一点可以参考kernel/process.h中对process结构的修改

66 typedef struct process_t {
67   // pointing to the stack used in trap handling.
68   uint64 kstack;
69   // user page table
70   pagetable_t pagetable;
71   // trapframe storing the context of a (User mode) process.
72   trapframe* trapframe;
73 
74   // points to a page that contains mapped_regions. below are added @lab3_1
75   mapped_region *mapped_info;
76   // next free mapped region in mapped_info
77   int total_mapped_region;
78 
79   // heap management
80   process_heap_manager user_heap;
81 
82   // process id
83   uint64 pid;
84   // process status
85   int status;
86   // parent process
87   struct process_t *parent;
88   // next queue element
89   struct process_t *queue_next;
90 
91   // accounting. added @lab3_3
92   int tick_count;
93 
94   // file system. added @lab4_1
95   proc_file_management *pfiles;
96 }process;

我们看到在进程定义的95行增加了一个proc_file_management指针类型的成员pfile。这样每当创建一个进程时都会调用kernel/proc_file.c中定义的函数init_proc_file_management(),来对该进程(将来)要打开的文件进行管理。该函数的定义如下:

41 proc_file_management *init_proc_file_management(void) {
42   proc_file_management *pfiles = (proc_file_management *)alloc_page();
43   pfiles->cwd = vfs_root_dentry; // by default, cwd is the root
44   pfiles->nfiles = 0;
45 
46   for (int fd = 0; fd < MAX_FILES; ++fd)
47     pfiles->opened_files[fd].status = FD_NONE;
48 
49   sprint("FS: created a file management struct for a process.\n");
50   return pfiles;
51 }

该函数的作用是为将要管理的“打开文件”分配一个物理页面的空间初始当前目录cwd置为根目录初始化打开文件计数42--44行然后初始化所有“已打开”的文件的文件描述符fd46--47行。kernel/proc_file.c文件中还定义了一组接口用于进程对文件的一系列操作。这些接口包括文件打开do_open、文件关闭do_close、文件读取do_read、文件写do_write、文件读写定位do_lseek、获取文件状态do_stat甚至获取磁盘状态do_disk_stat。这些接口都是在应用程序发出对应的文件操作如open、close、read_u等通过user lib到达do_syscall并最后被调用的。

这里我们对其中比较典型的文件操作如打开和文件读进行分析。我们先来观察do_open的实现见kernel/proc_file.c

 82 int do_open(char *pathname, int flags) {
 83   struct file *opened_file = NULL;
 84   if ((opened_file = vfs_open(pathname, flags)) == NULL) return -1;
 85 
 86   int fd = 0;
 87   if (current->pfiles->nfiles >= MAX_FILES) {
 88     panic("do_open: no file entry for current process!\n");
 89   }
 90   struct file *pfile;
 91   for (fd = 0; fd < MAX_FILES; ++fd) {
 92     pfile = &(current->pfiles->opened_files[fd]);
 93     if (pfile->status == FD_NONE) break;
 94   }
 95 
 96   // initialize this file structure
 97   memcpy(pfile, opened_file, sizeof(struct file));
 98 
 99   ++current->pfiles->nfiles;
100   return fd;
101 }

我们看到打开文件时PKE内核是通过调用vfs_open函数来实现对一个文件的打开动作的第84行。在文件打开后会在进程的“打开文件表”中寻找一个未被使用的表项将其加入拷贝到该表项中第86--99行。我们再来观察另一个典型函数do_read的实现见kernel/proc_file.c

107 int do_read(int fd, char *buf, uint64 count) {
108   struct file *pfile = get_opened_file(fd);
109 
110   if (pfile->readable == 0) panic("do_read: no readable file!\n");
111 
112   char buffer[count + 1];
113   int len = vfs_read(pfile, buffer, count);
114   buffer[count] = '\0';
115   strcpy(buf, buffer);
116   return len;
117 }

我们看到这个函数会首先对发起进程读取文件的权限进行判断通过判断后会继续通过调用vfs_read来实现对文件的真正的读取动作。最后再将读取到的数据拷贝到参数buf中。

实际上PKE文件系统提供给进程的所有操作最终都是通过调用VFS这一层的功能来最终完成它们的功能的。这里我们鼓励读者继续阅读其他操作如关闭文件、写文件等来观察PKE文件系统的这一特点。

6.1.4 虚拟文件系统

虚拟文件系统VFS为上层应用程序提供了一套统一的文件系统访问接口屏蔽了具体文件系统在文件组织方式、文件操作方式上的差异。由于VFS的存在用户在访问文件系统中的文件时并不需要关注该文件具体是保存在何种文件系统中的只需要通过VFS层提供的统一的访问接口来进行文件访问这大大降低了用户使用文件系统的复杂性。

6.1.4.1 VFS的功能接口

在PKE中VFS层对上层应用提供了如下一些文件系统访问接口包括设备接口、文件接口和目录接口在kernel/vfs.h中可以找到他们的声明

21 // device interfaces
22 struct super_block *vfs_mount(const char *dev_name, int mnt_type);
23 
24 // file interfaces
25 struct file *vfs_open(const char *path, int flags);
26 ssize_t vfs_read(struct file *file, char *buf, size_t count);
27 ssize_t vfs_write(struct file *file, const char *buf, size_t count);
28 ssize_t vfs_lseek(struct file *file, ssize_t offset, int whence);
29 int vfs_stat(struct file *file, struct istat *istat);
30 int vfs_disk_stat(struct file *file, struct istat *istat);
31 int vfs_link(const char *oldpath, const char *newpath);
32 int vfs_unlink(const char *path);
33 int vfs_close(struct file *file);
34 
35 // directory interfaces
36 struct file *vfs_opendir(const char *path);
37 int vfs_readdir(struct file *file, struct dir *dir);
38 int vfs_mkdir(const char *path);
39 int vfs_closedir(struct file *file);

上述接口函数可以用来完成文件系统挂载、读写文件和目录、创建文件硬链接等功能。系统内的其他模块通过调用这些接口函数来完成对文件系统的访问。在上一节中提到的面向进程的一组接口do_open、do_read等便是通过调用VFS层提供的这些文件系统访问接口来实现对文件系统的访问的。在实现上这些vfs函数大部分会通过调用“viop”开始函数来完成对具体文件系统的访问而viop函数则由具体文件系统RFS或hostfs自行定义这也是VFS能支持多种文件系统的关键。在下文6.1.4.3节中会对viop函数进行更详细的介绍。

6.1.4.2 VFS的重要数据结构

VFS对具体的文件系统进行了抽象构建出一个通用的文件系统模型。这个抽象的文件系统模型由几种VFS层的抽象数据类型组成下面分别介绍VFS中的这些抽象数据类型。

  • vinode VFS对具体文件系统中的inode进行了抽象构建出一个通用的vinode对象。vinode对象包含操作系统操作一个文件或目录所需要的全部信息对文件的各种操作都是围绕着vinode来进行的。由此可见vinode是文件系统中文件访问和操作的核心对象。在PKE中为了与RFS存储在“磁盘”上的inode进行区分我们将VFS层的抽象inode对象命名为vinode而实际存储在磁盘上的inode则被称为disk inode 简写为dinode。除了起到区分作用该名称同时强调了vinode对象是仅存在于内存中的这一事实。vinode会在一个文件被打开时创建在不被任何dentry见下文dentry引用时释放。另外RFS对应的vinode都会被存储在一个哈希链表中vinode_hash_table。设置该表的目的主要是为了避免同一个文件被多次打开时可能产生多个与之对应的vinode的情况出现。因为多个vinode在回写到磁盘时可能发生数据丢失后写入的vinode将先写入的vinode覆盖。vinode的结构如下见kernel/vfs.h

    115 struct vinode {
    116   int inum;                  // inode number of the disk inode
    117   int ref;                   // reference count
    118   int size;                  // size of the file (in bytes)
    119   int type;                  // one of FILE_I, DIR_I
    120   int nlinks;                // number of hard links to this file
    121   int blocks;                // number of blocks
    122   int addrs[DIRECT_BLKNUM];  // direct blocks
    123   void *i_fs_info;           // filesystem-specific info (see s_fs_info)
    124   struct super_block *sb;          // super block of the vfs inode
    125   const struct vinode_ops *i_ops;  // vfs inode operations
    126 };
    

    其中需要重点关注的成员是inum、sb以及i_ops。下面分别介绍这几个字段。

    inuminum用于保存一个vinode所对应的存在于磁盘上的disk inode序号。对于存在disk inode的文件系统如RFS而言inum唯一确定了该文件系统中的一个实际的文件。而对于不存在disk inode的文件系统如hostfsinum字段则不会被使用且该文件系统需要自行提供并维护一个vinode所对应的文件在该文件系统中的“定位信息”VFS必须能够根据该“定位信息”唯一确定此文件系统中的一个文件。这类“定位信息”由具体文件系统创建、使用通过viop函数后文会介绍和释放。实现上PKE为一个hostfs所设计的 “定位信息”就是一个spike_file_t结构体定义在spike_interface/spike_file.h中

    09 typedef struct file_t {
    10   int kfd;  // file descriptor of the host file
    11   uint32 refcnt;
    12 } spike_file_t;
    

    该结构体保存了宿主机中一个打开文件的文件描述符结构体的首地址会被保存在vinode的i_fs_info字段中。

    sbvinode中的sb字段是struct super_block类型它用来记录inode所在的文件系统对应的超级块超级块唯一确定了vinode对应的文件系统。

    i_opsi_ops字段是struct vinode_ops类型它是一组对vinode进行操作的函数调用。需要注意的是在VFS中仅仅提供了vinode的操作函数接口这些函数的具体实现会由下层的文件系统提供。

    对于RFS而言vinode中的其他信息inum、size、type、nlinks、blocks以及addrs都是在VFS首次访问到某个磁盘文件或目录时基于对应的disk inode内容产生的。而对于hostfsvinode中的这些数据并不会被使用而只是会设置i_fs_info字段。

  • dentry

    dentry即directory entry是VFS中对目录项的抽象该对象在VFS承载了多种功能包括对vinode进行组织、提供目录项缓存以及快速索引支持它的定义在vfs.h文件中

    38 struct dentry {
    39   char name[MAX_DENTRY_NAME_LEN];
    40   int d_ref;
    41   struct vinode *dentry_inode;
    42   struct dentry *parent;
    43   struct super_block *sb;
    45 };
    

    与vinode类似dentry同样是仅存在于内存中一种抽象数据类型且一个dentry对应一个vinode由dentry_inode成员记录。由于硬链接的存在一个vinode可能同时被多个dentry引用。另外需要注意的是在VFS中无论是普通文件还是目录文件都会存在相应的dentry。

    为了支撑dentry的功能内存中的每一个dentry都存在于目录树与路径哈希表两种结构之中。首先所有的dentry会按文件的树形目录结构组织成一颗目录树每个dentry中的parent数据项会指向其父dentry。该目录树结构将内存中存在的dentry及对应的vinode进行了组织便于文件的访问。其次所有的dentry都存在于一个以父目录项地址和目录项名为索引的哈希链表dentry_hash_table将dentry存放于哈希链表中可以加快对目录的查询操作。

  • super_block

    super_block结构用来保存一个已挂载文件系统的相关信息。在一个文件系统被挂载到系统中时其对应的super_block会被创建并读取磁盘上的super block信息若存在来对super_block对象进行初始化。对于RFS文件系统而言在访问其中的文件时需要从super_block中获取文件系统所在的设备信息且RFS中数据盘块的分配和释放需要借助保存在s_fs_info成员中的bitmap来完成。而对于hostfs而言它并不存在物理上的super block其VFS层的super_block结构主要用来维护hostfs根目录信息。super_block结构定义在vfs.h文件中

    104 struct super_block {
    105   int magic;              // magic number of the file system
    106   int size;               // size of file system image (blocks)
    107   int nblocks;            // number of data blocks
    108   int ninodes;            // number of inodes.
    109   struct dentry *s_root;  // root dentry of inode
    110   struct device *s_dev;   // device of the superblock
    111   void *s_fs_info;        // filesystem-specific info. for rfs, it points bitmap
    112 };
    

    其中最重要的数据项是s_root与s_dev。s_root指向该文件系统根目录所对应的dentrys_dev则指向该文件系统所在的设备。

  • file

    在用户进程的眼里磁盘上记录的信息是以文件的形式呈现的。因此VFS使用file对象为进程提供一个文件抽象。file结构体需要记录文件的状态、文件的读写权限、文件读写偏移量以及文件对应的dentry。该结构体定义在vfs.h中

    71 struct file {
    72   int status;
    73   int readable;
    74   int writable;
    75   int offset;
    76   struct dentry *f_dentry;
    77 };
    

    显然VFS层的file结构体只包含了一个文件的基本信息。

6.1.4.3 viop函数的定义和使用

viop函数是VFS与具体文件系统进行交互的重要部分也是VFS能够同时支持多种具体文件系统的关键。这些viop函数是在vinode上执行的一系列操作函数操作函数接口的定义可以在kernel/vfs.h文件中找到在lab4_1_file与lab4_2_directory这两个分支做实验时代码的vinode操作函数接口并不完整更完整的代码来自lab4_3_hardlink这个分支

136 struct vinode_ops {
137   // file operations
138   ssize_t (*viop_read)(struct vinode *node, char *buf, ssize_t len,
139                        int *offset);
140   ssize_t (*viop_write)(struct vinode *node, const char *buf, ssize_t len,
141                         int *offset);
142   struct vinode *(*viop_create)(struct vinode *parent, struct dentry *sub_dentry);
143   int (*viop_lseek)(struct vinode *node, ssize_t new_off, int whence, int *off);
144   int (*viop_disk_stat)(struct vinode *node, struct istat *istat);
145   int (*viop_link)(struct vinode *parent, struct dentry *sub_dentry,
146                    struct vinode *link_node);
147   int (*viop_unlink)(struct vinode *parent, struct dentry *sub_dentry,
148                      struct vinode *unlink_node);
149   struct vinode *(*viop_lookup)(struct vinode *parent,
150                                 struct dentry *sub_dentry);
151 
152   // directory operations
153   int (*viop_readdir)(struct vinode *dir_vinode, struct dir *dir, int *offset);
154   struct vinode *(*viop_mkdir)(struct vinode *parent, struct dentry *sub_dentry);
155 
156   // write back inode to disk
157   int (*viop_write_back_vinode)(struct vinode *node);
158 
159   // hook functions
160   // In the vfs layer, we do not assume that hook functions will do anything,
161   // but simply call them (when they are defined) at the appropriate time.
162   // Hook functions exist because the fs layer may need to do some additional
163   // operations (such as allocating additional data structures) at some critical
164   // times.
165   int (*viop_hook_open)(struct vinode *node, struct dentry *dentry);
166   int (*viop_hook_close)(struct vinode *node, struct dentry *dentry);
167   int (*viop_hook_opendir)(struct vinode *node, struct dentry *dentry);
168   int (*viop_hook_closedir)(struct vinode *node, struct dentry *dentry);
169 };

上述函数接口实际上是一系列的函数指针在VFS中的vinode_ops结构体中限定了这些函数的参数类型和返回值但并没有给出函数的实际定义。而在具体文件系统中需要根据vinode_ops中提供的函数接口来定义具体的函数实现并将函数地址赋值给vinode_ops结构中相应的函数指针。在VFS中则通过vinode_ops的函数指针调用实际的文件系统操作函数。以RFS为例我们可以在kernel/rfs.c中找到如下一个vinode_ops类型变量rfs_i_ops的定义以下完整代码来自lab4_3_hardlink分支

22 const struct vinode_ops rfs_i_ops = {
23     .viop_read = rfs_read,
24     .viop_write = rfs_write,
25     .viop_create = rfs_create,
26     .viop_lseek = rfs_lseek,
27     .viop_disk_stat = rfs_disk_stat,
28     .viop_link = rfs_link,
29     .viop_unlink = rfs_unlink,
30     .viop_lookup = rfs_lookup,
31 
32     .viop_readdir = rfs_readdir,
33     .viop_mkdir = rfs_mkdir,
34 
35     .viop_write_back_vinode = rfs_write_back_vinode,
36 
37     .viop_hook_opendir = rfs_hook_opendir,
38     .viop_hook_closedir = rfs_hook_closedir,
39 };

从该结构中我们可以清晰的看出inode操作函数接口viop_read、viop_write等与它们的具体实现rfs_read、rfs_write等之间的对应关系。RFS中inode操作函数的具体实现都定义在kernel/rfs.c中读者可以根据上述rfs_i_ops定义找到它们与VFS中接口的对应关系并阅读其函数实现这里不再一一列举。

继续回到vinode_ops我们可以发现其内部定义了四类接口函数分别是file operations文件操作、directory operations目录操作、write back inode to disk回写vinode以及hook functions钩子函数。其中前两种的功能是显而易见的它们主要是一些用来对文件和目录进行访问和操作的接口函数如读写文件、读目录等。对于“write back inode to disk”根据其注释名称我们也容易得知其功能将内存中保存的vinode数据回写到磁盘该函数一般会在关闭文件时调用。

而对于VFS层为vinode提供的钩子函数接口这里需要进行一些额外的说明首先钩子函数与其他三类接口函数一样同样是由具体文件系统所实现并提供的在VFS层仅仅定义了它们的参数和返回值。但钩子函数与其他三类接口函数不同的地方在于VFS层的调用者并不会“期待”这些函数完成任何的工作而仅仅只是在预先规定好的时机对这类钩子函数进行调用。下面举一个具体的例子即vfs_open见kernel/vfs.c

111 struct file *vfs_open(const char *path, int flags) {
112   struct dentry *parent = vfs_root_dentry; // we start the path lookup from root.
113   char miss_name[MAX_PATH_LEN];
114 
115   // path lookup.
116   struct dentry *file_dentry = lookup_final_dentry(path, &parent, miss_name);
117 
118   // file does not exist
119   if (!file_dentry) {
120     int creatable = flags & O_CREAT;
121 
122     // create the file if O_CREAT bit is set
123     if (creatable) {
124       char basename[MAX_PATH_LEN];
125       get_base_name(path, basename);
126 
127       // a missing directory exists in the path
128       if (strcmp(miss_name, basename) != 0) {
129         sprint("vfs_open: cannot create file in a non-exist directory!\n");
130         return NULL;
131       }
132 
133       // create the file
134       file_dentry = alloc_vfs_dentry(basename, NULL, parent);
135       struct vinode *new_inode = viop_create(parent->dentry_inode, file_dentry);
136       if (!new_inode) panic("vfs_open: cannot create file!\n");
137 
138       file_dentry->dentry_inode = new_inode;
139       new_inode->ref++;
140       hash_put_dentry(file_dentry);
141       hash_put_vinode(new_inode); 
142     } else {
143       sprint("vfs_open: cannot find the file!\n");
144       return NULL;
145     }
146   }
147 
148   if (file_dentry->dentry_inode->type != FILE_I) {
149     sprint("vfs_open: cannot open a directory!\n");
150     return NULL;
151   }
152 
153   // get writable and readable flags
154   int writable = 0;
155   int readable = 0;
156   switch (flags & MASK_FILEMODE) {
157     case O_RDONLY:
158       writable = 0;
159       readable = 1;
160       break;
161     case O_WRONLY:
162       writable = 1;
163       readable = 0;
164       break;
165     case O_RDWR:
166       writable = 1;
167       readable = 1;
168       break;
169     default:
170       panic("fs_open: invalid open flags!\n");
171   }
172 
173   struct file *file = alloc_vfs_file(file_dentry, readable, writable, 0);
174 
175   // additional open operations for a specific file system
176   // hostfs needs to conduct actual file open.
177   if (file_dentry->dentry_inode->i_ops->viop_hook_open) {
178     if (file_dentry->dentry_inode->i_ops->
179         viop_hook_open(file_dentry->dentry_inode, file_dentry) < 0) {
180       sprint("vfs_open: hook_open failed!\n");
181     }
182   }
183 
184   return file;
185 }

vfs_open函数即为前文中提到的VFS层对上层应用提供的接口之一该函数完成根据文件路径和访问标志位打开一个文件的功能。vfs_open在第135行和179行分别调用了两个viop接口函数viop_create“viop_create(node, name)”形式是为了调用方便而定义的宏其展开为“node->i_ops->viop_create(node, name)”其他viop接口函数也有类似的宏定义见kernel/vfs.h和viop_hook_open。通过阅读该函数实现我们可以发现vfs_open函数在发现打开的文件不存在且调用时具有creatable标志位时会调用viop_create对该文件进行创建。而该viop_create则由具体文件系统提供实现比如RFS中的rfs_create。在这个过程中我们注意到vfs_open函数要想完成打开文件的操作其必须依赖底层文件系统提供一个能够完成“创建新文件并返回其vinode”功能的viop_create函数。换句话说除了钩子函数以外的其他viop函数如viop_create在其具体实现中需要完成什么样的功能是在VFS层规定好的底层文件系统提供的这些viop函数实现必须完成VFS层所预期的功能如在磁盘中创建一个文件、读写文件或创建硬链接等

接下来我们再来看vfs_open的177--182行对钩子函数即viop_hook_open的调用。代码实现的功能是若viop_hook_open函数指针不为NULL则调用它通过条件判断。可以看到从VFS层的角度来说即使viop_hook_open不完成任何实际功能甚至没有定义则不会被调用也不会影响vfs_open函数完成打开文件的功能。换句话来讲VFS层对这些钩子函数没有任何的“期待”仅仅只是在正确的位置打开文件、关闭文件、打开目录和关闭目录调用它们而已。既然从VFS层的角度来看这类钩子函数不需要完成任何功能为什么还要有钩子函数的存在呢实际上在VFS运行过程中的一些关键时刻钩子函数为具体文件系统提供一个执行自定义代码的“机会”。比如上文中提到过hostfs中的spike_file_t结构体包含主机端打开文件描述符hostfs需要将其地址保存在文件对应vinode的i_fs_info中而该操作显然需要在打开文件时完成否则后续对该文件的读写操作则无法顺利进行。但是这段与hostfs自身细节高度相关的代码显然不应该直接放入vfs_open函数中而是应该由hostfs自己提供vfs_open负责为其提供一个执行“机会”。

为了验证这一点我们可以查看kernel/hostfs.c中hostfs_i_ops的定义以下完整代码来自lab4_3_hardlink分支

14 const struct vinode_ops hostfs_i_ops = {
15     .viop_read = hostfs_read,
16     .viop_write = hostfs_write,
17     .viop_create = hostfs_create,
18     .viop_lseek = hostfs_lseek,
19     .viop_lookup = hostfs_lookup,
20 
21     .viop_hook_open = hostfs_hook_open,
22     .viop_hook_close = hostfs_hook_close,
23 
24     .viop_write_back_vinode = hostfs_write_back_vinode,
25 
26     // not implemented
27     .viop_link = hostfs_link,
28     .viop_unlink = hostfs_unlink,
29     .viop_readdir = hostfs_readdir,
30     .viop_mkdir = hostfs_mkdir,
31 };

在第21行可以看到hostfs确实为viop_hook_open提供了一个函数实现hostfs_hook_open。在kernel/hostfs.c中我们同样可以找到hostfs_hook_open的定义

236 int hostfs_hook_open(struct vinode *f_inode, struct dentry *f_dentry) {
237   if (f_inode->i_fs_info != NULL) return 0;
238 
239   char path[MAX_PATH_LEN];
240   get_path_string(path, f_dentry);
241   spike_file_t *f = spike_file_open(path, O_RDWR, 0);
242   if ((int64)f < 0) {
243     sprint("hostfs_hook_open cannot open the given file.\n");
244     return -1;
245   }
246 
247   f_inode->i_fs_info = f;
248   return 0;
249 }

由此可见hostfs_hook_open函数确实完成了打开一个主机端文件并将其spike_file_t结构地址保存在i_fs_info的功能。VFS层一共定义了四个钩子函数分别是viop_hook_open、viop_hook_close、viop_hook_opendir和viop_hook_closedir它们分别在打开文件vfs_open、关闭文件vfs_close、打开目录vfs_opendir和关闭目录vfs_closedir时被调用。

hostfs实现了前两种钩子函数函数名称以及完成的功能如下

  • hostfs_hook_openkernel/hostfs.c#236行通过HTIF接口在宿主机打开文件并保存其文件描述符到vinode的i_fs_info中。
  • hostfs_hook_closekernel/hostfs.c#254行通过HTIF接口在宿主机关闭文件。

RFS实现了后两种钩子函数函数名称以及完成的功能如下代码来自lab4_3_hardlink分支

  • rfs_hook_opendirkernel/rfc.c#719行读取目录文件内容到目录缓存有关目录缓存的内容在实验二中会详细介绍将目录缓存的地址保存到vinode的i_fs_info中。
  • rfs_hook_closedirkernel/rfs.c#751行释放目录缓存。

6.1.4.4 VFS层的目录组织

类似于底层文件系统中保存在磁盘上的目录结构VFS会在内存中同样会构建一颗目录树。任何具体文件系统中的目录树都需要先挂载到这颗VFS的目录树上才能够被使用。下图展示了将两个文件系统挂载到VFS目录树后的情形其中/dir1/file1和/RAMDISK0/dir2/file2分别位于不同的文件系统中

1660396457538

这里我们回过头来观察kernel/vfs.c中vfs_mount函数的定义

 49 struct super_block *vfs_mount(const char *dev_name, int mnt_type) {
 50   // device pointer
 51   struct device *p_device = NULL;
 52 
 53   // find the device entry in vfs_device_list named as dev_name
 54   for (int i = 0; i < MAX_VFS_DEV; ++i) {
 55     p_device = vfs_dev_list[i];
 56     if (p_device && strcmp(p_device->dev_name, dev_name) == 0) break;
 57   }
 58   if (p_device == NULL) panic("vfs_mount: cannot find the device entry!\n");
 59 
 60   // add the super block into vfs_sb_list
 61   struct file_system_type *fs_type = p_device->fs_type;
 62   struct super_block *sb = fs_type->get_superblock(p_device);
 63 
 64   // add the root vinode into vinode_hash_table
 65   hash_put_vinode(sb->s_root->dentry_inode);
 66 
 67   int err = 1;
 68   for (int i = 0; i < MAX_MOUNTS; ++i) {
 69     if (vfs_sb_list[i] == NULL) {
 70       vfs_sb_list[i] = sb;
 71       err = 0;
 72       break;
 73     }
 74   }
 75   if (err) panic("vfs_mount: too many mounts!\n");
 76 
 77   // mount the root dentry of the file system to right place
 78   if (mnt_type == MOUNT_AS_ROOT) {
 79     vfs_root_dentry = sb->s_root;
 80 
 81     // insert the mount point into hash table
 82     hash_put_dentry(sb->s_root);
 83   } else if (mnt_type == MOUNT_DEFAULT) {
 84     if (!vfs_root_dentry)
 85       panic("vfs_mount: root dentry not found, please mount the root device first!\n");
 86 
 87     struct dentry *mnt_point = sb->s_root;
 88 
 89     // set the mount point directory's name to device name
 90     char *dev_name = p_device->dev_name;
 91     strcpy(mnt_point->name, dev_name);
 92 
 93     // by default, it is mounted under the vfs root directory
 94     mnt_point->parent = vfs_root_dentry;
 95 
 96     // insert the mount point into hash table
 97     hash_put_dentry(sb->s_root);
 98   } else {
 99     panic("vfs_mount: unknown mount type!\n");
100   }
101 
102   return sb;
103 }

为了简化文件系统挂载过程PKE的文件系统并不支持将一个设备中的文件系统挂载到任意目录下而是提供了两种固定的挂载方式“挂载为根目录”或“挂载为根目录下的子目录”。vfs_mount的第78--83行对应将一个设备挂载为根目录的情况。第79行将VFS目录树的根目录指向该设备上文件系统的根目录第82行则将根目录的dentry加入哈希链表中加快后续的目录搜索过程。第83--98行对应将一个设备挂载为根目录下的子目录的情况。第91行将设备上文件系统的根目录对应的dentry名称修改为设备名第94行将其链接到VFS根目录下作为VFS根目录下的一个子目录。注意该步骤相当于在VFS的根目录下创建了一个虚拟目录虚拟目录的名称为被挂载设备的设备名。例如若将设备“DEVICE0”以MOUNT_DEFAULT方式挂载则会在VFS根目录下创建一个虚拟的子目录仅存在于内存中不会在磁盘中创建目录文件/DEVICE0作为所挂载设备的挂载点。在挂载完成后/DEVICE0在逻辑上等价于所挂载文件系统的根目录。最后第97行同样将这个新加入VFS目录树的dentry插入到哈希链表中加快后续的目录搜索。实际上任何新加入VFS目录树的dentry都会被同步插入哈希链表中。

在上述VFS层文件系统挂载过程中我们应当注意到另一个事实当一个新文件系统被挂载时并不会立刻读取其磁盘上保存的完整目录结构到VFS目录树中而只是将具体文件系统的根目录添加到VFS目录树中。显然这样做既能节约时间和内存消耗也更加贴近于真实的文件系统运行情况。但是VFS如何处理后续对该新挂载文件系统下子目录或子文件的访问呢通过阅读kernel/vfs.c中定义的lookup_final_dentry函数可以找到答案

304 struct dentry *lookup_final_dentry(const char *path, struct dentry **parent,
305                                    char *miss_name) {
306   char path_copy[MAX_PATH_LEN];
307   strcpy(path_copy, path);
308 
309   // split the path, and retrieves a token at a time.
310   // note: strtok() uses a static (local) variable to store the input path
311   // string at the first time it is called. thus it can out a token each time.
312   // for example, when input path is: /RAMDISK0/test_dir/ramfile2
313   // strtok() outputs three tokens: 1)RAMDISK0, 2)test_dir and 3)ramfile2
314   // at its three continuous invocations.
315   char *token = strtok(path_copy, "/");
316   struct dentry *this = *parent;
317 
318   while (token != NULL) {
319     *parent = this;
320     this = hash_get_dentry((*parent), token);  // try hash first
321     if (this == NULL) {
322       // if not found in hash, try to find it in the directory
323       this = alloc_vfs_dentry(token, NULL, *parent);
324       // lookup subfolder/file in its parent directory. note:
325       // hostfs and rfs will take different procedures for lookup.
326       struct vinode *found_vinode = viop_lookup((*parent)->dentry_inode, this);
327       if (found_vinode == NULL) {
328         // not found in both hash table and directory file on disk.
329         free_page(this);
330         strcpy(miss_name, token);
331         return NULL;
332       }
333 
334       struct vinode *same_inode = hash_get_vinode(found_vinode->sb, found_vinode->inum);
335       if (same_inode != NULL) {
336         // the vinode is already in the hash table (i.e. we are opening another hard link)
337         this->dentry_inode = same_inode;
338         same_inode->ref++;
339         free_page(found_vinode);
340       } else {
341         // the vinode is not in the hash table
342         this->dentry_inode = found_vinode;
343         found_vinode->ref++;
344         hash_put_vinode(found_vinode);
345       }
346 
347       hash_put_dentry(this);
348     }
349 
350     // get next token
351     token = strtok(NULL, "/");
352   }
353   return this;
354 }

该函数用来对一个路径字符串进行目录查询并返回路径基地址对应的文件或目录的dentry在vfs_open函数中可以找到对其的调用。当通过vfs_open打开/home/user/file1文件时path参数为/home/user/file1parent参数为/。该函数会从VFS目录树根目录开始逐级访问其目录文件进行目录查询。若home、user和file1都存在则函数会返回file1对应的dentryparent会被修改为指向file1的父目录这个例子中就是user。第318--352行是其核心功能的实现该循环会依次处理路径字符串中的每个token上述例子中是home、user、file1。在320行先尝试在哈希链表中查询。若当前token已经在VFS目录树中则能直接获取到该token对应的dentry不需要访问底层文件系统若当前token不在VFS目录树中则会执行第321--348行代码。其中最关键的是第326行该行通过父目录的vinode调用了viop_lookup函数而该函数是由具体文件系统所提供的。在RFS中viop_lookup对应的实现是rfs_lookup函数其定义在kernel/rfs.c中

429 struct vinode *rfs_lookup(struct vinode *parent, struct dentry *sub_dentry) {
430   struct rfs_direntry *p_direntry = NULL;
431   struct vinode *child_vinode = NULL;
432 
433   int total_direntrys = parent->size / sizeof(struct rfs_direntry);
434   int one_block_direntrys = RFS_BLKSIZE / sizeof(struct rfs_direntry);
435 
436   struct rfs_device *rdev = rfs_device_list[parent->sb->s_dev->dev_id];
437 
438   // browse the dir entries contained in a directory file
439   for (int i = 0; i < total_direntrys; ++i) {
440     if (i % one_block_direntrys == 0) {  // read in the disk block at boundary
441       rfs_r1block(rdev, parent->addrs[i / one_block_direntrys]);
442       p_direntry = (struct rfs_direntry *)rdev->iobuffer;
443     }
444     if (strcmp(p_direntry->name, sub_dentry->name) == 0) {  // found
445       child_vinode = rfs_alloc_vinode(parent->sb);
446       child_vinode->inum = p_direntry->inum;
447       if (rfs_update_vinode(child_vinode) != 0)
448         panic("rfs_lookup: read inode failed!");
449       break;
450     }
451     ++p_direntry;
452   }
453   return child_vinode;
454 }

重点关注第439--452行这部分代码逐个遍历parent目录文件中的所有目录项保存在RAM DISK中将parent下目录项的名称与传入的待查询子目录的名称进行比较第444行并在找到目标子目录时为其分配vinode第445行最后将其返回。

再回到lookup_final_dentry函数若viop_lookup成功在底层文件系统中查询当前token则在第334--345行检查该token对应的vinode是否已经在之前被创建过且存入了vinode哈希链表中。若已存在则直接将token对应的dentry指向从哈希链表中查询到的vinode并将所找到的vinode释放第337--339行若不存在则将dentry指向该新找到的vinode并将其放入哈希链表中第342--344行。由上述过程可见在搜索路径的过程中会遇到不在VFS目录树中的节点而这些节点会“按需”从磁盘中读出并被加入到VFS目录树中。这便是VFS目录树的构造过程。

需要说明的是PKE中的VFS层为了降低代码的复杂性并没有加入VFS目录树中dentry的淘汰和回收机制。虽然从理论上来说VFS目录树中的节点越多越有利于后续对文件系统的快速访问但在真实的文件系统中VFS目录树中的节点数量受制于有限的内存资源不可能无限制的增长当节点数量达到上限时应当从VFS目录树中挑选一些旧的dentry进行回收以便新节点的加入。

6.1.4.5 VFS层的哈希缓存

VFS通过哈希链表的形式实现了对dentry与vinode两种结构的缓存cache和快速索引它们都采用util/hash_table.h中定义的通用哈希链表类型hash_table实现并提供各自的key类型、哈希函数以及key的等值判断函数。在kernel/vfs.c中能找到这两个哈希链表的定义

16 struct hash_table dentry_hash_table;
17 struct hash_table vinode_hash_table;

首先讨论dentry的哈希缓存。dentry哈希缓存的主要作用是加快VFS中的路径查找过程。下面是其key类型定义kernel/vfs.h

64 struct dentry_key {
65   struct dentry *parent;
66   char *name;
67 };

从中可以看出dentry通过其父dentry指针与dentry名称进行索引。之所以不直接使用name字段对dentry索引是因为不同目录下存在同名子目录或子文件的情况非常常见这样会导致太多的哈希冲突而加入父dentry指针后冲突率会大大降低。下面是根据dentry_key类型定义的dentry哈希函数kernel/vfs.c

633 size_t dentry_hash_func(void *key) {
634   struct dentry_key *dentry_key = key;
635   char *name = dentry_key->name;
636 
637   size_t hash = 5381;
638   int c;
639 
640   while ((c = *name++)) hash = ((hash << 5) + hash) + c;  // hash * 33 + c
641 
642   hash = ((hash << 5) + hash) + (size_t)dentry_key->parent;
643   return hash % HASH_TABLE_SIZE;
644 }

当查询dentry时发生哈希冲突时hash_table会调用dentry_key的等值判断函数将冲突dentry的key依次与目标key进行比较找出与目标key值匹配的dentry。下面是这个比较函数的定义kernel/vfs.c

623 int dentry_hash_equal(void *key1, void *key2) {
624   struct dentry_key *dentry_key1 = key1;
625   struct dentry_key *dentry_key2 = key2;
626   if (strcmp(dentry_key1->name, dentry_key2->name) == 0 &&
627       dentry_key1->parent == dentry_key2->parent) {
628     return 1;
629   }
630   return 0;
631 }

上述代码表明当parent字段与name字段都匹配时认为两个dentry的key相等。由于一个目录下不会存在同名的子文件或目录在PKE中我们不允许目录下子文件与子目录同名因此该函数能够用来从dentry哈希链表中唯一确定一个dentry。

对于vinode哈希缓存其主要作用与dentry哈希缓存不同。首先需要总结一下在VFS中访问一个文件的过程

  1. 从文件路径字符串找到文件对应的dentry。在这个过程中dentry哈希缓存起到了加速作用。
  2. 从文件dentry找到文件的vinode。由于每个dentry中都记录了对应vinode所以通过dentry可以直接找到对应的vinode不需要额外的过程。
  3. 通过vinode访问文件。

从这个过程中可以看出vinode的哈希缓存并没有在访问文件时起到作用。实际上vindoe的哈希缓存是在创建新的vinode时使用用来保证vinode的唯一性避免出现多个vinode对应于同一个文件的情况。具体来讲由于RFS中硬链接的存在应用程序可以通过不同的文件别名打开同一个文件。当同时通过文件的多个别名打开一个文件时文件系统正确的行为应该是让不同别名对应的dentry指向同一个vinode。这时便存在一个问题若先用某一个文件别名打开了文件此时文件的vinode被创建在关闭这个文件之前又用该文件的其他别名再次打开了这个文件。这时文件系统需要找到对应该文件的已有vinode并将第二次打开文件时所用别名对应的dentry指向该已有vinode而不是重新创建。在这个过程中需要vinode的哈希缓存辅助完成两件事

  1. 判断新创建的vinode是否已经在VFS中存在了。
  2. 对于1若已存在相同的vinode则要能获取到这个已存在的vinode。

在VFS中当一个文件对应的vinode被创建如打开文件首先通过其key值对vinode哈希缓存进行查询若发现哈希表中已存在相同key的vinode则将新创建的vinode废弃使用从哈希表中找到的vinode继续完成后续操作若不存在则将新创建的vinode加入哈希表中并用该vinode完成后续操作。前文中介绍的lookup_final_dentry函数便存在该过程的一个例子

下面同样给出vinode哈希表对应的key类型kernel/vfs.h

191 struct vinode_key {
192   int inum;
193   struct super_block *sb;
194 };

vinode的key值由disk inode号与文件系统对应的超级块构成。disk inode号在一个文件系统中具有唯一性因此一个disk inode号加上一个文件系统超级块便能够唯一确定一个vinode。vinode的等值判断函数与哈希函数同样定义在kernel/vfs.c中这里不再罗列请读者自行阅读。另外由于hostfs对应的vinode不存在disk inode号所以其不会被加入到上述vinode哈希链表中hostfs不存在硬链接也不需要回写vinode数据因此无需担心上述异常情况

6.1.5 RFS文件系统

在本实验中为了让读者更详细地了解文件系统的组织结构我们以块设备上的传统文件系统为模板在RAM DISK上实现了RFS。RFS在简化了操作系统与外存硬件交互有关细节的同时清晰地展示了文件系统中的一些关键数据结构如superblock、inode、bitmap和目录项等以及建立在这些数据结构之上的相关操作的实现。

6.1.5.1 RFS的磁盘结构和格式化过程

下图给出了 PKE的RFS 整个文件系统的结构和块组的内容文件系统都由大量块组组成在硬盘上相继排布在rfs.c中对RAM DISK的布局进行了注释

/*
 * RFS (Ramdisk File System) is a customized simple file system installed in the
 * RAM disk. added @lab4_1.
 * Layout of the file system:
 *
 * ******** RFS MEM LAYOUT (112 BLOCKS) ****************
 *   superblock  |  disk inodes  |  bitmap  |  free blocks  *
 *     1 block   |   10 blocks   |     1    |     100       *
 * *****************************************************
 *
 * The disk layout of rfs is similar to the fs in xv6.
 */

RFS采用了类Linux文件系统如ext文件系统的磁盘布局在磁盘上存储的内容如下

  • superblock包含的是文件系统的重要信息比如disk inode 总个数、块总个数、数据块总个数等等。其保存的数据结构如下:

    29 struct rfs_superblock {
    30   int magic;    // magic number of the
    31   int size;     // size of file system image (blocks)
    32   int nblocks;  // number of data blocks
    33   int ninodes;  // number of inodes.
    34 };
    
  • disk inodesdinode是在磁盘上存储的索引节点用来记录文件的元信息比如文件大小、文件类型、文件占用的块数、数据在磁盘的位置等等。索引节点是文件的唯一标识。inodes部分分配的磁盘块用来保存所有磁盘文件的dinode每一个磁盘块可保存32个dinode。dinode的数据结构如下其中addrs字段会按顺序记录文件占用的块号直接索引

    37 struct rfs_dinode {
    38   int size;                      // size of the file (in bytes)
    39   int type;                      // one of R_FREE, R_FILE, R_DIR
    40   int nlinks;                    // number of hard links to this file
    41   int blocks;                    // number of blocks
    42   int addrs[RFS_DIRECT_BLKNUM];  // direct blocks
    43 };
    
  • bitmap 用于表示对应的数据块是否空闲。在RFS被实现为一个简单的一维数组

    int * freemap;
    

    freemap[blkno]=1代表第blkno个数据块被使用了=0则代表空闲。

    1660396457538
  • free blocks,磁盘上实际可以存储用户数据的区域。

PKE系统启动时会调用格式化函数对RAM DISK进行格式化将上述磁盘布局写入到RAM DISK中。在kernel/rfs.c中我们可以找到这个格式化函数

054 int rfs_format_dev(struct device *dev) {
055   struct rfs_device *rdev = rfs_device_list[dev->dev_id];
056 
057   // ** first, format the superblock
058   // build a new superblock
059   struct super_block *super = (struct super_block *)rdev->iobuffer;
060   super->magic = RFS_MAGIC;
061   super->size =
062       1 + RFS_MAX_INODE_BLKNUM + 1 + RFS_MAX_INODE_BLKNUM * RFS_DIRECT_BLKNUM;
063   // only direct index blocks
064   super->nblocks = RFS_MAX_INODE_BLKNUM * RFS_DIRECT_BLKNUM;
065   super->ninodes = RFS_BLKSIZE / RFS_INODESIZE * RFS_MAX_INODE_BLKNUM;
066 
067   // write the superblock to RAM Disk0
068   if (rfs_w1block(rdev, RFS_BLK_OFFSET_SUPER) != 0)  // write to device
069     panic("RFS: failed to write superblock!\n");
070 
071   // ** second, set up the inodes and write them to RAM disk
072   // build an empty inode disk block which has RFS_BLKSIZE/RFS_INODESIZE(=32)
073   // disk inodes
074   struct rfs_dinode *p_dinode = (struct rfs_dinode *)rdev->iobuffer;
075   for (int i = 0; i < RFS_BLKSIZE / RFS_INODESIZE; ++i) {
076     p_dinode->size = 0;
077     p_dinode->type = R_FREE;
078     p_dinode->nlinks = 0;
079     p_dinode->blocks = 0;
080     p_dinode = (struct rfs_dinode *)((char *)p_dinode + RFS_INODESIZE);
081   }
082 
083   // write RFS_MAX_INODE_BLKNUM(=10) empty inode disk blocks to RAM Disk0
084   for (int inode_block = 0; inode_block < RFS_MAX_INODE_BLKNUM; ++inode_block) {
085     if (rfs_w1block(rdev, RFS_BLK_OFFSET_INODE + inode_block) != 0)
086       panic("RFS: failed to initialize empty inodes!\n");
087   }
088 
089   // build root directory inode (ino = 0)
090   struct rfs_dinode root_dinode;
091   root_dinode.size = 0;
092   root_dinode.type = R_DIR;
093   root_dinode.nlinks = 1;
094   root_dinode.blocks = 1;
095   root_dinode.addrs[0] = RFS_BLK_OFFSET_FREE;
096 
097   // write root directory inode to RAM Disk0 (ino = 0)
098   if (rfs_write_dinode(rdev, &root_dinode, 0) != 0) {
099     sprint("RFS: failed to write root inode!\n");
100     return -1;
101   }
102 
103   // ** third, write freemap to disk
104   int *freemap = (int *)rdev->iobuffer;
105   memset(freemap, 0, RFS_BLKSIZE);
106   freemap[0] = 1;  // the first data block is used for root directory
107 
108   // write the bitmap to RAM Disk0
109   if (rfs_w1block(rdev, RFS_BLK_OFFSET_BITMAP) != 0) {  // write to device
110     sprint("RFS: failed to write bitmap!\n");
111     return -1;
112   }
113 
114   sprint("RFS: format %s done!\n", dev->dev_name);
115   return 0;
116 }

rfs_format_dev函数在第57--69行构建了RFS的超级块并将其写入RAM DISK的第RFS_BLK_OFFSET_SUPER=0块盘块中第71--87行初始化了RAM DISK的32*10个inode第89--101行将根目录inode写入到第0号inode中第103--112行将bitmap进行初始化并写入第RFS_BLK_OFFSET_BITMAP=11块盘块中。注意第106行由于第0块数据盘块已经被根目录所占用在第95行分配所以freemap[0] = 1。

6.1.5.2 RFS的dinode和目录项

文件系统除了要保存文件的内容还需要保存文件系统的元数据信息例如文件的大小、类型以及创建时间等信息。在RFS中文件的元信息作为一个dinode单独存放在RAM DISK的disk inodes块组区域中。另外由于文件的元信息与文件内容分开存放元信息中显然应该记录下文件内容所在的物理地址这样才能够根据文件的dinode访问到文件的内容。RFS中的dinode结构rfs_dinode已在上一小节中给出与之前VFS中介绍的抽象结构vinode不同RFS中的dinode是实际保存在RAM DISK上的数据结构其名称前的字母ddisk便是对这一点的强调读者在阅读代码时应注意与vinode进行区分。

值得注意的是rfs_dinode结构中包含了文件的大小、类型、硬链接数、盘块数以及文件内容的物理地址但是并没有记录文件的文件名。实际上文件的文件名同样是一个文件的重要属性其保存在目录文件中。这里需要引入RFS中的另外一个数据结构RFS目录项它的定义在kernel/rfs.h文件中

46 struct rfs_direntry {
47   int inum;                          // inode number
48   char name[RFS_MAX_FILE_NAME_LEN];  // file name
49 };

注意rfs_direntry结构与VFS中的dentry完全不同它是实际保存在RAM DISK上的结构其功能更加单一一个rfs_direntry仅包含一个文件或目录的名称与其对应的dinode号。RFS文件系统中的目录文件就是由一个接一个的rfs_direntry构成的如下图所示

1660396457538

rfs_direntry实际上起到了将文件名与文件dinode关联起来的作用当用户通过文件名访问文件内容时首先会遍历文件父目录中的rfs_direntry找到name字段与文件名相匹配的rfs_direntry再通过其对应的inumdinode序号从RAM DISK的inodes块组中读出文件对应的dinode进一步根据dinode中保存的文件物理地址读取文件内容。由于RFS采用树型目录的组织形式因此该过程实际上会重复进行多次读者可参考我们在6.1.4.4节对lookup_final_dentry函数的讨论。

6.1.5.3 硬链接

硬链接是指多个目录项中的文件名不同但索引节点号相同指向同一个物理文件。索引结点是不可能跨越文件系统的每个文件系统都有各自的索引结点数据结构和列表所以硬链接是不可用于跨文件系统的。在文件系统中允许多个目录项指向同一个索引节点并在磁盘上文件对应的索引节点中记录文件的硬链接数文件的硬链接数等于指向该文件索引节点的目录项数。为文件创建新的硬链接会使硬链接数加1删除文件只是对硬链接数减1直到硬链接数减为0时系统才会真正删除该文件。

RFS给每个文件都分配了一个硬链接数nlinks在rfs_dinode的定义中可以找到见kernel/rfs.h

37 struct rfs_dinode {
38   int size;                      // size of the file (in bytes)
39   int type;                      // one of R_FREE, R_FILE, R_DIR
40   int nlinks;                    // number of hard links to this file
41   int blocks;                    // number of blocks
42   int addrs[RFS_DIRECT_BLKNUM];  // direct blocks
43 };

文件的硬链接数等于引用这个文件的目录项数。当一个文件刚刚被创建时其硬链接数为1。在完成实验三后我们可以通过系统调用为一个已存在的文件创建硬链接每次为文件创建一个新的硬链接该文件的硬链接便加一。类似的当删除文件的一个硬链接时文件的硬链接数会减一。当硬链接数减为0时文件会从磁盘上删除也就是释放文件的dinode和所有的数据盘块。下图是一个硬链接的例子

1660396457538

上图中虽然file1与file2文件名不同且位于不同的目录下但是它们实际上对应于同一个文件因为它们对应同一个dinode图中的dinode1。下面从目录文件和dinode两方面来分析这个例子。首先对于目录文件而言目录dir1的文件内容包含这样一个目录条目类型为rfs_direntry"file1":1我们假设dinode1的inode号为1同样的目录dir3会包含"file2":1。由此可见两个目录条目对应的dinode号是相同的都对应dinode1。其次对于dinode1而言因为其被上文中的两个目录项所引用所以其硬链接数nlinks为2。这里可以对比dinode2该dinode仅被file2一个目录条目所引用所以它的硬链接数为1。

另外在进行lab4_3时读者需要完成与硬链接有关的代码实现。

6.2 lab4_1 文件

给定应用

  • user/app_file.c
01 #include "user_lib.h"
02 #include "util/string.h"
03 #include "util/types.h"
04 
05 int main(int argc, char *argv[]) {
06   int fd;
07   int MAXBUF = 512;
08   char buf[MAXBUF];
09   char str[] = "hello world";
10   int fd1, fd2;
11 
12   printu("\n======== Test 1: read host file  ========\n");
13   printu("read: /hostfile.txt\n");
14 
15   fd = open("/hostfile.txt", O_RDONLY);
16   printu("file descriptor fd: %d\n", fd);
17 
18   read_u(fd, buf, MAXBUF);
19   printu("read content: \n%s\n", buf);
20 
21   close(fd);
22 
23   printu("\n======== Test 2: create/write rfs file ========\n");
24   printu("write: /RAMDISK0/ramfile\n");
25 
26   fd = open("/RAMDISK0/ramfile", O_RDWR | O_CREAT);
27   printu("file descriptor fd: %d\n", fd);
28 
29   write_u(fd, buf, strlen(buf));
30   printu("write content: \n%s\n", buf);
31   close(fd);
32 
33   printu("\n======== Test 3: read rfs file ========\n");
34   printu("read: /RAMDISK0/ramfile\n");
35 
36   fd = open("/RAMDISK0/ramfile", O_RDWR);
37   printu("file descriptor fd: %d\n", fd);
38 
39   read_u(fd, buf, MAXBUF);
40   printu("read content: \n%s\n", buf);
41   close(fd);
42 
43   printu("\n======== Test 4: open twice ========\n");
44 
45   fd1 = open("/RAMDISK0/ramfile", O_RDWR | O_CREAT);
46   fd2 = open("/RAMDISK0/ramfile", O_RDWR | O_CREAT);
47 
48   printu("file descriptor fd1(ramfile): %d\n", fd1);
49   printu("file descriptor fd2(ramfile): %d\n", fd2);
50 
51   write_u(fd1, str, strlen(str));
52   printu("write content: \n%s\n", str);
53 
54   read_u(fd2, buf, MAXBUF);
55   printu("read content: \n%s\n", buf);
56 
57   close(fd1);
58   close(fd2);
59 
60   printu("\nAll tests passed!\n\n");
61   exit(0);
62   return 0;
63 }

在这个应用中,我们打开了一个宿主机文件/hostfile.txt和一个RAM Disk文件/RAMDISK0/ramfile,从宿主机读入hostfile.txt的内容,写入ramfile,再从ramfile中读出,检查对应功能是否正确实现。

  • 先提交lab3_3的答案然后切换到lab4_1继承lab3_3中所做的修改并make后的直接运行结果
//切换到lab4_1
$ git checkout lab4_1_file

//继承lab4_1以及之前的答案
$ git merge lab3_3_rrsched -m "continue to work on lab4_1"

//重新构造
$ make clean; make

//运行构造结果
$ spike ./obj/riscv-pke ./obj/app_file
In m_start, hartid:0
HTIF is available!
(Emulated) memory size: 2048 MB
Enter supervisor mode...
PKE kernel start 0x0000000080000000, PKE kernel end: 0x000000008000d000, PKE kernel size: 0x000000000000d000 .
free physical memory address: [0x000000008000d000, 0x0000000087ffffff] 
kernel memory manager is initializing ...
KERN_BASE 0x0000000080000000
physical address of _etext is: 0x0000000080007000
kernel page table is on 
RAMDISK0: base address of RAMDISK0 is: 0x0000000087f37000
RFS: format RAMDISK0 done!
Switch to user mode...
in alloc_proc. user frame 0x0000000087f2f000, user stack 0x000000007ffff000, user kstack 0x0000000087f2e000 
FS: created a file management struct for a process.
in alloc_proc. build proc_file_management successfully.
User application is loading.
Application: obj/app_file
CODE_SEGMENT added at mapped info offset:3
DATA_SEGMENT added at mapped info offset:4
Application program entry point (virtual address): 0x00000000000100b0
going to insert process 0 to ready queue.
going to schedule process 0 to run.

======== Test 1: read host file  ========
read: /hostfile.txt
file descriptor fd: 0
read content: 
This is an apple. 
Apples are good for our health. 


======== Test 2: create/write rfs file ========
write: /RAMDISK0/ramfile
You need to implement the code of populating a disk inode in lab4_1.

System is shutting down with exit code -1.

从以上运行结果来看我们的应用app_file.c并未如愿地完成文件的读写操作。进一步分析程序的运行结果我们可以发现在“Test 1”中程序顺利完成了对位于宿主机文件系统的hostfile.txt文件的读操作。而在“Test 2”中向位于RFS中的ramfile执行的写操作并未顺利完成。

在完成实验的过程中我们需要关注RFS中的4个基础文件操作包括open(create)readwriteclose

实验内容

在PKE操作系统内核中完善对文件的操作使得它能够正确处理用户进程的打开、创建、读写文件请求。

实验完成后的运行结果:

$ spike ./obj/riscv-pke ./obj/app_file
In m_start, hartid:0
HTIF is available!
(Emulated) memory size: 2048 MB
Enter supervisor mode...
PKE kernel start 0x0000000080000000, PKE kernel end: 0x000000008000e000, PKE kernel size: 0x000000000000e000 .
free physical memory address: [0x000000008000e000, 0x0000000087ffffff] 
kernel memory manager is initializing ...
KERN_BASE 0x0000000080000000
physical address of _etext is: 0x0000000080008000
kernel page table is on 
RAMDISK0: base address of RAMDISK0 is: 0x0000000087f37000
RFS: format RAMDISK0 done!
Switch to user mode...
in alloc_proc. user frame 0x0000000087f2f000, user stack 0x000000007ffff000, user kstack 0x0000000087f2e000 
FS: created a file management struct for a process.
in alloc_proc. build proc_file_management successfully.
User application is loading.
Application: obj/app_file
CODE_SEGMENT added at mapped info offset:3
DATA_SEGMENT added at mapped info offset:4
Application program entry point (virtual address): 0x00000000000100b0
going to insert process 0 to ready queue.
going to schedule process 0 to run.

======== Test 1: read host file  ========
read: /hostfile.txt
file descriptor fd: 0
read content: 
This is an apple. 
Apples are good for our health. 


======== Test 2: create/write rfs file ========
write: /RAMDISK0/ramfile
file descriptor fd: 0
write content: 
This is an apple. 
Apples are good for our health. 


======== Test 3: read rfs file ========
read: /RAMDISK0/ramfile
file descriptor fd: 0
read content: 
This is an apple. 
Apples are good for our health. 


======== Test 4: open twice ========
file descriptor fd1(ramfile): 0
file descriptor fd2(ramfile): 1
write content: 
hello world
read content: 
hello world

All tests passed!

User exit with code:0.
no more ready processes, system shutdown now.
System is shutting down with exit code 0.

实验指导

从程序的输出可以看出Test 2中对RFS文件的写操作并未顺利返回。通过阅读app_file.c中的代码我们可以发现Test 2执行了文件的打开、写入和关闭操作其中文件的打开函数write_u并未顺利返回。该函数定义在user_lib.c文件中从函数定义可以看出打开文件操作对应着SYS_user_open系统调用。与之前实验中涉及到的系统调用一样打开文件系统调用的处理函数同样被定义在syscall.c文件中

101 ssize_t sys_user_open(char *pathva, int flags) {
102   char* pathpa = (char*)user_va_to_pa((pagetable_t)(current->pagetable), pathva);
103   return do_open(pathpa, flags);
104 }

该函数在对文件路径字符串地址进行虚地址到物理地址的转换后会调用do_open函数完成余下的打开文件操作。do_open函数在前文中已经进行过介绍其通过调用vfs_open函数打开文件并获取该文件的file对象。vfs_open函数的完整功能也已在前文虚拟文件系统小节中进行了介绍。由于在Test 2中打开的文件/RAMDISK0/ramfile是不存在的所以vfs_open函数会调用viop_create函数进行创建。viop_create在RFS中的实现为rfs_create其定义在kernel/rfs.c文件中

456 //
457 // create a file with "sub_dentry->name" at directory "parent" in rfs.
458 // return the vfs inode of the file being created.
459 //
460 struct vinode *rfs_create(struct vinode *parent, struct dentry *sub_dentry) {
461   struct rfs_device *rdev = rfs_device_list[parent->sb->s_dev->dev_id];
462 
463   // ** find a free disk inode to store the file that is going to be created
464   struct rfs_dinode *free_dinode = NULL;
465   int free_inum = 0;
466   for (int i = 0; i < (RFS_BLKSIZE / RFS_INODESIZE * RFS_MAX_INODE_BLKNUM);
467        ++i) {
468     free_dinode = rfs_read_dinode(rdev, i);
469     if (free_dinode->type == R_FREE) {  // found
470       free_inum = i;
471       break;
472     }
473     free_page(free_dinode);
474   }
475 
476   if (free_dinode == NULL)
477     panic("rfs_create: no more free disk inode, we cannot create file.\n" );
478 
479   // initialize the states of the file being created
480 
481   // TODO (lab4_1): implement the code for populating the disk inode (free_dinode) 
482   // of a new file being created.
483   // hint:  members of free_dinode to be filled are:
484   // size, should be zero for a new file.
485   // type, see kernel/rfs.h and find the type for a rfs file.
486   // nlinks, i.e., the number of links.
487   // blocks, i.e., its block count.
488   // Note: DO NOT DELETE CODE BELOW PANIC.
489   panic("You need to implement the code of populating a disk inode in lab4_1.\n" );
490 
491   // DO NOT REMOVE ANY CODE BELOW.
492   // allocate a free block for the file
493   free_dinode->addrs[0] = rfs_alloc_block(parent->sb);
494 
495   // **  write the disk inode of file being created to disk
496   rfs_write_dinode(rdev, free_dinode, free_inum);
497   free_page(free_dinode);
498 
499   // ** build vfs inode according to dinode
500   struct vinode *new_vinode = rfs_alloc_vinode(parent->sb);
501   new_vinode->inum = free_inum;
502   rfs_update_vinode(new_vinode);
503 
504   // ** append the new file as a direntry to its parent dir
505   int result = rfs_add_direntry(parent, sub_dentry->name, free_inum);
506   if (result == -1) {
507     sprint("rfs_create: rfs_add_direntry failed");
508     return NULL;
509   }
510 
511   return new_vinode;
512 }

第463--474行在RFS中查找一个空闲的dinode保存到free_dinode中并将其dinode号记录到free_inum在第470--489行我们找到了本实验中缺失的代码块。根据注释提示可以看出读者需要完成新文件dinode的初始化操作。

继续阅读剩下的代码第493行为新文件分配了一块数据盘块第495--497行将初始化完成的dinode写回到RAM DISK中第499--502行为新文件创建了一个vinode并从RAM DISK读入相关数据项第504--509行在新文件的父目录文件中添加了一个新的目录项。

实验完毕后,记得提交修改(命令行中-m后的字符串可自行确定以便在后续实验中继承lab4_1中所做的工作

$ git commit -a -m "my work on lab4_1 is done."

6.3 lab4_2 目录文件

给定应用

  • user/app_directory.c
01 #include "user_lib.h"
02 #include "util/string.h"
03 #include "util/types.h"
04 
05 void ls(char *path) {
06   int dir_fd = opendir_u(path);
07   printu("------------------------------\n");
08   printu("ls \"%s\":\n", path);
09   printu("[name]               [inode_num]\n");
10   struct dir dir;
11   int width = 20;
12   while(readdir_u(dir_fd, &dir) == 0) {
13     // we do not have %ms :(
14     char name[width + 1];
15     memset(name, ' ', width + 1);
16     name[width] = '\0';
17     if (strlen(dir.name) < width) {
18       strcpy(name, dir.name);
19       name[strlen(dir.name)] = ' ';
20       printu("%s %d\n", name, dir.inum);
21     }
22     else
23       printu("%s %d\n", dir.name, dir.inum);
24   }
25   printu("------------------------------\n");
26   closedir_u(dir_fd);
27 }
28 
29 int main(int argc, char *argv[]) {
30   char str[] = "hello world";
31   int fd;
32   
33   printu("\n======== Test 1: open and read dir ========\n");
34 
35   ls("/RAMDISK0");
36   
37   printu("\n======== Test 2: make dir ========\n");
38 
39   mkdir_u("/RAMDISK0/sub_dir");
40   printu("make: /RAMDISK0/sub_dir\n");
41 
42   ls("/RAMDISK0");
43 
44   // try to write a file in the new dir
45   printu("write: /RAMDISK0/sub_dir/ramfile\n");
46 
47   fd = open("/RAMDISK0/sub_dir/ramfile", O_RDWR | O_CREAT);
48   printu("file descriptor fd: %d\n", fd);
49 
50   write_u(fd, str, strlen(str));
51   printu("write content: \n%s\n", str);
52 
53   close(fd);
54 
55   ls("/RAMDISK0/sub_dir");
56 
57   printu("\nAll tests passed!\n\n");
58   exit(0);
59   return 0;
60 }

该应用在Test 1中完成对目录文件的读取操作。目录文件的读取在ls函数中实现该函数会读取并显示一个目录下的所有目录项其主要执行步骤是第6行调用opendir_u打开一个目录文件第12行调用readdir_u函数读取目录文件中的下一个目录项该过程循环执行直到读取完最后一个目录项最后在第26行将目录文件关闭。Test 2完成在目录下创建子目录的功能。第39行调用mkdir_u在/RAMDISK0下创建子目录:sub_dir第44--53行尝试在新目录下创建并写入一个文件。

  • 先提交lab4_1的答案然后切换到lab4_2继承lab4_1以及之前实验所做的修改并make后的直接运行结果
//切换到lab4_2
$ git checkout lab4_2_directory

//继承lab4_2以及之前的答案
$ git merge lab4_1_file -m "continue to work on lab4_2"

//重新构造
$ make clean; make

//运行构造结果
$ spike ./obj/riscv-pke ./obj/app_directory
In m_start, hartid:0
HTIF is available!
(Emulated) memory size: 2048 MB
Enter supervisor mode...
PKE kernel start 0x0000000080000000, PKE kernel end: 0x000000008000e000, PKE kernel size: 0x000000000000e000 .
free physical memory address: [0x000000008000e000, 0x0000000087ffffff] 
kernel memory manager is initializing ...
KERN_BASE 0x0000000080000000
physical address of _etext is: 0x0000000080008000
kernel page table is on 
RAMDISK0: base address of RAMDISK0 is: 0x0000000087f37000
RFS: format RAMDISK0 done!
Switch to user mode...
in alloc_proc. user frame 0x0000000087f2f000, user stack 0x000000007ffff000, user kstack 0x0000000087f2e000 
FS: created a file management struct for a process.
in alloc_proc. build proc_file_management successfully.
User application is loading.
Application: obj/app_directory
CODE_SEGMENT added at mapped info offset:3
DATA_SEGMENT added at mapped info offset:4
Application program entry point (virtual address): 0x0000000000010186
going to insert process 0 to ready queue.
going to schedule process 0 to run.

======== Test 1: open and read dir ========
------------------------------
ls "/RAMDISK0":
[name]               [inode_num]
------------------------------

======== Test 2: make dir ========
make: /RAMDISK0/sub_dir
------------------------------
ls "/RAMDISK0":
[name]               [inode_num]
You need to implement the code for reading a directory entry of rfs in lab4_2.

System is shutting down with exit code -1.

从以上运行结果来看虽然在Test 1中读取/RAMDISK0顺利完成了,但这是由于/RAMDISK0目录为空并没有实际发生目录文件的读取。在Test 2中mkdir_u函数顺利创建了子目录sub_dir当再次读取/RAMDISK0目录文件的内容时,程序产生了错误。

实验内容

在PKE操作系统内核中完善对目录文件的访问操作使得它能够正确处理用户进程的打开、读取目录文件请求。

实验完成后的运行结果:

$ spike ./obj/riscv-pke ./obj/app_directory
In m_start, hartid:0
HTIF is available!
(Emulated) memory size: 2048 MB
Enter supervisor mode...
PKE kernel start 0x0000000080000000, PKE kernel end: 0x000000008000e000, PKE kernel size: 0x000000000000e000 .
free physical memory address: [0x000000008000e000, 0x0000000087ffffff] 
kernel memory manager is initializing ...
KERN_BASE 0x0000000080000000
physical address of _etext is: 0x0000000080008000
kernel page table is on 
RAMDISK0: base address of RAMDISK0 is: 0x0000000087f37000
RFS: format RAMDISK0 done!
Switch to user mode...
in alloc_proc. user frame 0x0000000087f2f000, user stack 0x000000007ffff000, user kstack 0x0000000087f2e000 
FS: created a file management struct for a process.
in alloc_proc. build proc_file_management successfully.
User application is loading.
Application: obj/app_directory
CODE_SEGMENT added at mapped info offset:3
DATA_SEGMENT added at mapped info offset:4
Application program entry point (virtual address): 0x0000000000010186
going to insert process 0 to ready queue.
going to schedule process 0 to run.

======== Test 1: open and read dir ========
------------------------------
ls "/RAMDISK0":
[name]               [inode_num]
------------------------------

======== Test 2: make dir ========
make: /RAMDISK0/sub_dir
------------------------------
ls "/RAMDISK0":
[name]               [inode_num]
sub_dir              1
------------------------------
write: /RAMDISK0/sub_dir/ramfile
file descriptor fd: 0
write content: 
hello world
------------------------------
ls "/RAMDISK0/sub_dir":
[name]               [inode_num]
ramfile              2
------------------------------

All tests passed!

User exit with code:0.
no more ready processes, system shutdown now.
System is shutting down with exit code 0.

实验指导

首先需要了解一下RFS读取目录文件的方式。RFS读取目录文件的方式与读取普通文件有很大的不同读取一个目录分为三步打开目录文件、读取目录文件和关闭目录文件。其中打开目录文件对应于函数opendir_u定义在user/user_lib.c文件中

125 int opendir_u(const char *dirname) {
126   return do_user_call(SYS_user_opendir, (uint64)dirname, 0, 0, 0, 0, 0, 0);
127 }

可见其对应SYS_user_opendir系统调用在kernel/syscall.c中找到该系统调用的定义

171 ssize_t sys_user_opendir(char * pathva){
172   char * pathpa = (char*)user_va_to_pa((pagetable_t)(current->pagetable), pathva);
173   return do_opendir(pathpa);
174 }

该函数完成虚拟地址到物理地址的转换后调用do_opendir完成打开目录文件的功能。do_opendir定义在kernel/proc_file.c中

168 int do_opendir(char *pathname) {
169   struct file *opened_file = NULL;
170   if ((opened_file = vfs_opendir(pathname)) == NULL) return -1;
171 
172   int fd = 0;
173   struct file *pfile;
174   for (fd = 0; fd < MAX_FILES; ++fd) {
175     pfile = &(current->pfiles->opened_files[fd]);
176     if (pfile->status == FD_NONE) break;
177   }
178   if (pfile->status != FD_NONE)  // no free entry
179     panic("do_opendir: no file entry for current process!\n");
180 
181   // initialize this file structure
182   memcpy(pfile, opened_file, sizeof(struct file));
183 
184   ++current->pfiles->nfiles;
185   return fd;
186 }

该函数主要在第170行调用vfs_opendir函数完成打开目录文件的功能其余部分与前文中do_open基本一致。继续在kernel/vfs.c中找到vfs_opendir函数

302 struct file *vfs_opendir(const char *path) {
303   struct dentry *parent = vfs_root_dentry;
304   char miss_name[MAX_PATH_LEN];
305 
306   // lookup the dir
307   struct dentry *file_dentry = lookup_final_dentry(path, &parent, miss_name);
308 
309   if (!file_dentry || file_dentry->dentry_inode->type != DIR_I) {
310     sprint("vfs_opendir: cannot find the direntry!\n");
311     return NULL;
312   }
313 
314   // allocate a vfs file with readable/non-writable flag.
315   struct file *file = alloc_vfs_file(file_dentry, 1, 0, 0);
316 
317   // additional open direntry operations for a specific file system
318   // rfs needs duild dir cache.
319   if (file_dentry->dentry_inode->i_ops->viop_hook_opendir) {
320     if (file_dentry->dentry_inode->i_ops->
321         viop_hook_opendir(file_dentry->dentry_inode, file_dentry) != 0) {
322       sprint("vfs_opendir: hook opendir failed!\n");
323     }
324   }
325 
326   return file;
327 }

该函数在第307行调用lookup_final_dentry在目录树中查找目标目录文件获得其对应的dentry之后在第315行分配一个关联该dentry的file对象返回。这两步其实与打开一个已存在的普通文件的过程相同。值得注意的是在vfs_opendir函数的最后第319--324行调用了钩子函数viop_hook_opendir。根据前文虚拟文件系统小节中对钩子函数的解释RFS定义了viop_hook_opendir的具体函数实现rfs_hook_opendir因此rfs_hook_opendir函数会在vfs_opendir的最后被调用。rfs_hook_opendir定义在kernel/rfs.c文件中

574 //
575 // when a directory is opened, the contents of the directory file are read
576 // into the memory for directory read operations
577 //
578 int rfs_hook_opendir(struct vinode *dir_vinode, struct dentry *dentry) {
579   // allocate space and read the contents of the dir block into memory
580   void *pdire = NULL;
581   void *previous = NULL;
582   struct rfs_device *rdev = rfs_device_list[dir_vinode->sb->s_dev->dev_id];
583 
584   // read-in the directory file, store all direntries in dir cache.
585   for (int i = dir_vinode->blocks - 1; i >= 0; i--) {
586     previous = pdire;
587     pdire = alloc_page();
588 
589     if (previous != NULL && previous - pdire != RFS_BLKSIZE)
590       panic("rfs_hook_opendir: memory discontinuity");
591 
592     rfs_r1block(rdev, dir_vinode->addrs[i]);
593     memcpy(pdire, rdev->iobuffer, RFS_BLKSIZE);
594   }
595 
596   // save the pointer to the directory block in the vinode
597   struct rfs_dir_cache *dir_cache = (struct rfs_dir_cache *)alloc_page();
598   dir_cache->block_count = dir_vinode->blocks;
599   dir_cache->dir_base_addr = (struct rfs_direntry *)pdire;
600 
601   dir_vinode->i_fs_info = dir_cache;
602 
603   return 0;
604 }

在第585--594行该函数读取了目录文件的所有数据块由于alloc_page会从高地址向低地址分配内存因此对目录文件数据块从后往前读取第597--599行将目录文件内容的地址和块数保存在dir_cache结构体中第601行将dir_cache的地址保存在目录文件vinode的i_fs_info数据项中。该函数实际上将目录文件内的全部目录条目读入了dir_cache中后续的读目录操作则直接从dir_cache中读取目录项并返回。

RFS的读目录函数由rfs_readdir实现按照上文跟踪opendir_u的过程读者可以自行跟踪readdir_u函数的调用过程最终会跟踪到rfs_readdir函数这里不再赘述该函数在kernel/rfs.c中实现

621 //
622 // read a directory entry from the directory "dir", and the "offset" indicate
623 // the position of the entry to be read. if offset is 0, the first entry is read,
624 // if offset is 1, the second entry is read, and so on.
625 // return: 0 on success, -1 when there are no more entry (end of the list).
626 //
627 int rfs_readdir(struct vinode *dir_vinode, struct dir *dir, int *offset) {
628   int total_direntrys = dir_vinode->size / sizeof(struct rfs_direntry);
629   int one_block_direntrys = RFS_BLKSIZE / sizeof(struct rfs_direntry);
630 
631   int direntry_index = *offset;
632   if (direntry_index >= total_direntrys) {
633     // no more direntry
634     return -1;
635   }
636 
637   // reads a directory entry from the directory cache stored in vfs inode.
638   struct rfs_dir_cache *dir_cache =
639       (struct rfs_dir_cache *)dir_vinode->i_fs_info;
640   struct rfs_direntry *p_direntry = dir_cache->dir_base_addr + direntry_index;
641 
642   // TODO (lab4_2): implement the code to read a directory entry.
643   // hint: in the above code, we had found the directory entry that located at the
644   // *offset, and used p_direntry to point it.
645   // in the remaining processing, we need to return our discovery.
646   // the method of returning is to popular proper members of "dir", more specifically,
647   // dir->name and dir->inum.
648   // note: DO NOT DELETE CODE BELOW PANIC.
649   panic("You need to implement the code for reading a directory entry of rfs in lab4_2.\n" );
650 
651   // DO NOT DELETE CODE BELOW.
652   (*offset)++;
653   return 0;
654 }

在第638行函数从目录文件vinode的i_fs_info中获取到dir_cache的地址第640行在预先读入的目录文件中通过偏移量找到需要读取的rfs_direntry地址p_direntry第642--649则是读者需要进行补全的代码。根据注释内容的提示读者需要从dir_cache中读取目标rfs_direntry并将其名称与对应的dinode号复制到dir中。

完成目录的读取后用户程序需要调用closedir_u函数关闭文件该函数的调用过程由读者自行阅读。

实验完毕后,记得提交修改(命令行中-m后的字符串可自行确定以便在后续实验中继承lab4_2中所做的工作

$ git commit -a -m "my work on lab4_2 is done."

6.4 lab4_3 硬链接

给定应用

  • user/app_hardlink.c
01 #include "user_lib.h"
02 #include "util/string.h"
03 #include "util/types.h"
04 
05 void ls(char *path) {
06   int dir_fd = opendir_u(path);
07   printu("------------------------------\n");
08   printu("ls \"%s\":\n", path);
09   printu("[name]               [inode_num]\n");
10   struct dir dir;
11   int width = 20;
12   while(readdir_u(dir_fd, &dir) == 0) {
13     // we do not have %ms :(
14     char name[width + 1];
15     memset(name, ' ', width + 1);
16     name[width] = '\0';
17     if (strlen(dir.name) < width) {
18       strcpy(name, dir.name);
19       name[strlen(dir.name)] = ' ';
20       printu("%s %d\n", name, dir.inum);
21     }
22     else
23       printu("%s %d\n", dir.name, dir.inum);
24   }
25   printu("------------------------------\n");
26   closedir_u(dir_fd);
27 }
28 
29 int main(int argc, char *argv[]) {
30   int MAXBUF = 512;
31   char str[] = "hello world";
32   char buf[MAXBUF];
33   int fd1, fd2;
34   
35   printu("\n======== establish the file ========\n");
36 
37   fd1 = open("/RAMDISK0/ramfile", O_RDWR | O_CREAT);
38   printu("create file: /RAMDISK0/ramfile\n");
39   close(fd1);
40 
41   printu("\n======== Test 1: hard link ========\n");
42 
43   link_u("/RAMDISK0/ramfile", "/RAMDISK0/ramfile2");
44   printu("create hard link: /RAMDISK0/ramfile2 -> /RAMDISK0/ramfile\n");
45 
46   fd1 = open("/RAMDISK0/ramfile", O_RDWR);
47   fd2 = open("/RAMDISK0/ramfile2", O_RDWR);
48   
49   printu("file descriptor fd1 (ramfile): %d\n", fd1);
50   printu("file descriptor fd2 (ramfile2): %d\n", fd2);
51 
52   // check the number of hard links to ramfile on disk
53   struct istat st;
54   disk_stat_u(fd1, &st);
55   printu("ramfile hard links: %d\n", st.st_nlinks);
56   if (st.st_nlinks != 2) {
57     printu("ERROR: the number of hard links to ramfile should be 2, but it is %d\n",
58              st.st_nlinks);
59     exit(-1);
60   }
61   
62   write_u(fd1, str, strlen(str));
63   printu("/RAMDISK0/ramfile write content: \n%s\n", str);
64 
65   read_u(fd2, buf, MAXBUF);
66   printu("/RAMDISK0/ramfile2 read content: \n%s\n", buf);
67 
68   close(fd1);
69   close(fd2);
70 
71   printu("\n======== Test 2: unlink ========\n");
72 
73   ls("/RAMDISK0");
74 
75   unlink_u("/RAMDISK0/ramfile");
76   printu("unlink: /RAMDISK0/ramfile\n");
77 
78   ls("/RAMDISK0");
79 
80   // check the number of hard links to ramfile2 on disk
81   fd2 = open("/RAMDISK0/ramfile2", O_RDWR);
82   disk_stat_u(fd2, &st);
83   printu("ramfile2 hard links: %d\n", st.st_nlinks);
84   if (st.st_nlinks != 1) {
85     printu("ERROR: the number of hard links to ramfile should be 1, but it is %d\n",
86              st.st_nlinks);
87     exit(-1);
88   }
89   close(fd2);
90 
91   printu("\nAll tests passed!\n\n");
92   exit(0);
93   return 0;
94 }

该应用调用open创建一个文件 /RAMDISK0/ramfile然后在Test 1中为/RAMDISK0/ramfile创建一个硬链接/RAMDISK0/ramfile2。链接完成后我们先检查RAM DISK中 ramfile文件的硬链接数是否为2然后向 ramfile写入一段内容,再读取 ramfile2 查看ramfile2是否得到了更新。

在Test 2中我们调用unlink删除ramfile查看ramfile2硬链接数的变化情况。

  • 先提交lab4_2的答案然后切换到lab4_3继承lab4_2以及之前实验所做的修改并make后的直接运行结果
//切换到lab4_3
$ git checkout lab4_3_hardlink

//继承lab4_2以及之前的答案
$ git merge lab4_2_directory -m "continue to work on lab4_3"

//重新构造
$ make clean; make

//运行构造结果
$ spike ./obj/riscv-pke ./obj/app_hardlink
In m_start, hartid:0
HTIF is available!
(Emulated) memory size: 2048 MB
Enter supervisor mode...
PKE kernel start 0x0000000080000000, PKE kernel end: 0x000000008000e000, PKE kernel size: 0x000000000000e000 .
free physical memory address: [0x000000008000e000, 0x0000000087ffffff] 
kernel memory manager is initializing ...
KERN_BASE 0x0000000080000000
physical address of _etext is: 0x0000000080008000
kernel page table is on 
RAMDISK0: base address of RAMDISK0 is: 0x0000000087f37000
RFS: format RAMDISK0 done!
Switch to user mode...
in alloc_proc. user frame 0x0000000087f2f000, user stack 0x000000007ffff000, user kstack 0x0000000087f2e000 
FS: created a file management struct for a process.
in alloc_proc. build proc_file_management successfully.
User application is loading.
Application: obj/app_hardlink
CODE_SEGMENT added at mapped info offset:3
DATA_SEGMENT added at mapped info offset:4
Application program entry point (virtual address): 0x0000000000010186
going to insert process 0 to ready queue.
going to schedule process 0 to run.

======== establish the file ========
create file: /RAMDISK0/ramfile

======== Test 1: hard link ========
You need to implement the code for creating a hard link in lab4_3.

System is shutting down with exit code -1.

从以上运行结果来看为ramfile创建硬链接的函数link_u并未顺利完成。

实验内容

在PKE操作系统内核中完善对文件的访问操作使得它能够正确处理用户进程的创建、删除硬链接的请求。

实验完成后的运行结果:

$ spike ./obj/riscv-pke ./obj/app_hardlink
In m_start, hartid:0
HTIF is available!
(Emulated) memory size: 2048 MB
Enter supervisor mode...
PKE kernel start 0x0000000080000000, PKE kernel end: 0x000000008000e000, PKE kernel size: 0x000000000000e000 .
free physical memory address: [0x000000008000e000, 0x0000000087ffffff] 
kernel memory manager is initializing ...
KERN_BASE 0x0000000080000000
physical address of _etext is: 0x0000000080008000
kernel page table is on 
RAMDISK0: base address of RAMDISK0 is: 0x0000000087f37000
RFS: format RAMDISK0 done!
Switch to user mode...
in alloc_proc. user frame 0x0000000087f2f000, user stack 0x000000007ffff000, user kstack 0x0000000087f2e000 
FS: created a file management struct for a process.
in alloc_proc. build proc_file_management successfully.
User application is loading.
Application: obj/app_hardlink
CODE_SEGMENT added at mapped info offset:3
DATA_SEGMENT added at mapped info offset:4
Application program entry point (virtual address): 0x0000000000010186
going to insert process 0 to ready queue.
going to schedule process 0 to run.

======== establish the file ========
create file: /RAMDISK0/ramfile

======== Test 1: hard link ========
create hard link: /RAMDISK0/ramfile2 -> /RAMDISK0/ramfile
file descriptor fd1 (ramfile): 0
file descriptor fd2 (ramfile2): 1
ramfile hard links: 2
/RAMDISK0/ramfile write content: 
hello world
/RAMDISK0/ramfile2 read content: 
hello world

======== Test 2: unlink ========
------------------------------
ls "/RAMDISK0":
[name]               [inode_num]
ramfile              1
ramfile2             1
------------------------------
unlink: /RAMDISK0/ramfile
------------------------------
ls "/RAMDISK0":
[name]               [inode_num]
ramfile2             1
------------------------------
ramfile2 hard links: 1

All tests passed!

User exit with code:0.
no more ready processes, system shutdown now.
System is shutting down with exit code 0.

通过上述输出我们可以发现ramfile和ramfile2实际对应同一个文件更新其中一个会影响另一个。而删除ramfile只是将inode对应的nlinks减一并将其目录项从父目录文件中移除。

实验指导

为了补全PKE文件系统的硬链接功能我们可以从应用app_hardlinks.c开始查看link_u函数的调用关系。同之前实验的跟踪过程一样我们可以在kernel/proc_file.c下找到link_u函数在文件系统中的实现

214 int do_link(char *oldpath, char *newpath) {
215   return vfs_link(oldpath, newpath);
216 }

该函数直接调用了vfs_link在kernel/vfs.c中找到它的实现

236 //
237 // make hard link to the file specified by "oldpath" with the name "newpath"
238 // return: -1 on failure, 0 on success.
239 //
240 int vfs_link(const char *oldpath, const char *newpath) {
241   struct dentry *parent = vfs_root_dentry;
242   char miss_name[MAX_PATH_LEN];
243 
244   // lookup oldpath
245   struct dentry *old_file_dentry =
246       lookup_final_dentry(oldpath, &parent, miss_name);
247   if (!old_file_dentry) {
248     sprint("vfs_link: cannot find the file!\n");
249     return -1;
250   }
251 
252   if (old_file_dentry->dentry_inode->type != FILE_I) {
253     sprint("vfs_link: cannot link a directory!\n");
254     return -1;
255   }
256 
257   parent = vfs_root_dentry;
258   // lookup the newpath
259   // note that parent is changed to be the last directory entry to be accessed
260   struct dentry *new_file_dentry =
261       lookup_final_dentry(newpath, &parent, miss_name);
262   if (new_file_dentry) {
263     sprint("vfs_link: the new file already exists!\n");
264     return -1;
265   }
266 
267   char basename[MAX_PATH_LEN];
268   get_base_name(newpath, basename);
269   if (strcmp(miss_name, basename) != 0) {
270     sprint("vfs_link: cannot create file in a non-exist directory!\n");
271     return -1;
272   }
273 
274   // do the real hard-link
275   new_file_dentry = alloc_vfs_dentry(basename, old_file_dentry->dentry_inode, parent);
276   int err =
277       viop_link(parent->dentry_inode, new_file_dentry, old_file_dentry->dentry_inode);
278   if (err) return -1;
279 
280   // make a new dentry for the new link
281   hash_insert(new_file_dentry);
282 
283   return 0;
284 }

第267--268行在目录树中查找被链接的文件第282--294行在目录树中查找将要创建的硬链接确保其不与已存在的文件或目录名冲突第298--299行调用viop_link函数来实现真正的创建硬链接操作。viop_link函数在RFS中的实现是rfs_link函数其定义在kernel/rfs.c中

576 //
577 // create a hard link under a direntry "parent" for an existing file of "link_node"
578 //
579 int rfs_link(struct vinode *parent, struct dentry *sub_dentry, struct vinode *link_node) {
580   // TODO (lab4_3): we now need to establish a hard link to an existing file whose vfs
581   // inode is "link_node". To do that, we need first to know the name of the new (link)
582   // file, and then, we need to increase the link count of the existing file. Lastly, 
583   // we need to make the changes persistent to disk. To know the name of the new (link)
584   // file, you need to stuty the structure of dentry, that contains the name member;
585   // To incease the link count of the existing file, you need to study the structure of
586   // vfs inode, since it contains the inode information of the existing file.
587   //
588   // hint: to accomplish this experiment, you need to:
589   // 1) increase the link count of the file to be hard-linked;
590   // 2) append the new (link) file as a dentry to its parent directory; you can use 
591   //    rfs_add_direntry here.
592   // 3) persistent the changes to disk. you can use rfs_write_back_vinode here.
593   //
594   panic("You need to implement the code for creating a hard link in lab4_3.\n" );
595 }

此函数需要读者进行实现。根据注释的提示在rfs_link函数中完成创建硬链接需要三步

  1. 将旧文件vinode的硬链接数加一
  2. 新创建硬链接的父目录文件中添加一个目录项“新文件名旧dinode号”
  3. 将vinode写回到RAM DISK中。

实验完毕后,记得提交修改(命令行中-m后的字符串可自行确定以便在后续实验中继承lab4_2中所做的工作

$ git commit -a -m "my work on lab4_3 is done."

6.5 lab4_challenge1 相对路径(难度:★★★☆☆)

给定应用

  • user/app_relativepath.c

      1 #include "user_lib.h"
      2 #include "util/string.h"
      3 #include "util/types.h"
      4
      5 void pwd() {
      6   char path[30];
      7   read_cwd(path);
      8   printu("cwd:%s\n", path);
      9 }
     10
     11 void cd(const char *path) {
     12   if (change_cwd(path) != 0)
     13     printu("cd failed\n");
     14 }
     15
     16 int main(int argc, char *argv[]) {
     17   int fd;
     18   int MAXBUF = 512;
     19   char buf[MAXBUF];
     20   char str[] = "hello world";
     21   int fd1, fd2;
     22
     23   printu("\n======== Test 1: change current directory  ========\n");
     24
     25   pwd();
     26   cd("./RAMDISK0");
     27   printu("change current directory to ./RAMDISK0\n");
     28   pwd();
     29
     30   printu("\n======== Test 2: write/read file by relative path  ========\n");
     31   printu("write: ./ramfile\n");
     32
     33   fd = open("./ramfile", O_RDWR | O_CREAT);
     34   printu("file descriptor fd: %d\n", fd);
     35
     36   write_u(fd, str, strlen(str));
     37   printu("write content: \n%s\n", str);
     38   close(fd);
     39
     40   fd = open("./ramfile", O_RDWR);
     41   printu("read: ./ramfile\n");
     42
     43   read_u(fd, buf, MAXBUF);
     44   printu("read content: \n%s\n", buf);
     45   close(fd);
     46
     47   printu("\n======== Test 3: Go to parent directory  ========\n");
     48
     49   pwd();
     50   cd("..");
     51   printu("change current directory to ..\n");
     52   pwd();
     53
     54   printu("read: ./hostfile.txt\n");
     55
     56   fd = open("./hostfile.txt", O_RDONLY);
     57   printu("file descriptor fd: %d\n", fd);
     58
     59   read_u(fd, buf, MAXBUF);
     60   printu("read content: \n%s\n", buf);
     61
     62   close(fd);
     63
     64   printu("\nAll tests passed!\n\n");
     65   exit(0);
     66   return 0;
     67 }
    

    相对路径即形如:“./file”、“./dir/file”以及“../dir/file”的路径形式。与绝对路径总是从根目录开始逐级指定文件或目录在目录树中的位置不同,相对路径从进程的“当前工作目录“开始,对一个文件或目录在目录树中的位置进行描述。

    从路径字符串形式的角度来看,相对路径的起始处总是为以下两种特殊的目录之一:”.“,”..“。其中”.“代指进程的当前工作目录,“..”代指进程当前工作目录的父目录。例如,“./file”的含义为位于进程当前工作目录下的file文件../dir/file”的含义为当前进程工作目录父目录下的dir目录下的file文件。

    从文件系统的角度来说,相对路径的实现首先依赖于每个进程中所维护的当前工作目录。其次,具体文件系统需要对相对路径访问方式提供支持。

    如上的应用预期输出如下:

    In m_start, hartid:0
    HTIF is available!
    (Emulated) memory size: 2048 MB
    Enter supervisor mode...
    PKE kernel start 0x0000000080000000, PKE kernel end: 0x000000008000d000, PKE kernel size: 0x000000000000d000 .
    free physical memory address: [0x000000008000d000, 0x0000000087ffffff] 
    kernel memory manager is initializing ...
    KERN_BASE 0x0000000080000000
    physical address of _etext is: 0x0000000080008000
    kernel page table is on 
    RAMDISK0: base address of RAMDISK0 is: 0x0000000087f35000
    RFS: format RAMDISK0 done!
    Switch to user mode...
    in alloc_proc. user frame 0x0000000087f29000, user stack 0x000000007ffff000, user kstack 0x0000000087f28000 
    FS: created a file management struct for a process.
    in alloc_proc. build proc_file_management successfully.
    User application is loading.
    Application: obj/app_relativepath
    CODE_SEGMENT added at mapped info offset:3
    DATA_SEGMENT added at mapped info offset:4
    Application program entry point (virtual address): 0x00000000000100ec
    going to insert process 0 to ready queue.
    going to schedule process 0 to run.
    
    ======== Test 1: change current directory  ========
    cwd:/
    change current directory to ./RAMDISK0
    cwd:/RAMDISK0
    
    ======== Test 2: write/read file by relative path  ========
    write: ./ramfile
    file descriptor fd: 0
    write content: 
    hello world
    read: ./ramfile
    read content: 
    hello world
    
    ======== Test 3: Go to parent directory  ========
    cwd:/RAMDISK0
    change current directory to ..
    cwd:/
    read: ./hostfile.txt
    file descriptor fd: 0
    read content: 
    This is an apple. 
    Apples are good for our health. 
    
    
    All tests passed!
    
    User exit with code:0.
    no more ready processes, system shutdown now.
    System is shutting down with exit code 0.
    

实验内容

本实验为挑战实验基础代码将继承和使用lab4_3完成后的代码

  • 先提交lab4_3的答案然后切换到lab4_challenge1_relativepath、继承lab4_3中所做修改

    //切换到lab4_challenge1_relativepath
    $ git checkout lab4_challenge1_relativepath
    
    //继承lab3_3以及之前的答案
    $ git merge lab4_3_hardlink -m "continue to work on lab4_challenge1"
    
    //在完成代码修改后进行编译
    $ make clean; make
    
    //运行程序
    $ spike ./obj/riscv-pke ./obj/app_relativepath
    

    注意:**不同于基础实验,挑战实验的基础代码具有更大的不完整性,可能无法直接通过构造过程。**同样,不同于基础实验,我们在代码中也并未专门地哪些地方的代码需要填写,哪些地方的代码无须填写。这样,我们留给读者更大的“想象空间”。

  • 本实验的具体要求为:

    • 通过修改PKE文件系统代码提供解析相对路径的支持。具体来说用户应用程序在使用任何需要传递路径字符串的函数时都可以传递相对路径而不会影响程序的功能。
    • 完成用户层pwd函数显示进程当前工作目录、cd函数切换进程当前工作目录
  • 注意:最终测试程序可能和给出的用户程序不同,但都涉及工作目录的显示、工作目录切换以及相对路径的使用等相关操作。

实验指导

  • 你对内核代码的修改可能包含以下内容:
    • 添加系统调用完成进程cwd的读取和修改。
    • 修改文件系统对路径字符串的解析过程,使其能够正确处理相对路径。
    • 对RFS进行适当的修改。

注意:完成实验内容后,请读者另外编写应用,对自己的实现进行检测。

另外,后续的基础实验代码并不依赖挑战实验,所以读者可自行决定是否将自己的工作提交到本地代码仓库中(当然,提交到本地仓库是个好习惯,至少能保存自己的“作品”)。

6.6 lab4_challenge2 重载执行(难度:★★★★☆)

给定应用

  • user/app_exec.c

    01 #include "user_lib.h"
    02 #include "util/types.h"
    03 
    04 int main(int argc, char *argv[]) {
    05   printu("\n======== exec /bin/app_ls in app_exec ========\n");
    06   int ret = exec("/bin/app_ls");
    07   if (ret == -1)
    08     printu("exec failed!\n");
    09 
    10   exit(0);
    11   return 0;
    12 }
    
  • user/app_ls.c

    01 #include "user_lib.h"
    02 #include "util/string.h"
    03 #include "util/types.h"
    04 
    05 int main(int argc, char *argv[]) {
    06   char path[] = "/RAMDISK0";
    07   int dir_fd = opendir_u(path);
    08   printu("------------------------------\n");
    09   printu("ls \"%s\":\n", path);
    10   printu("[name]               [inode_num]\n");
    11   struct dir dir;
    12   int width = 20;
    13   while(readdir_u(dir_fd, &dir) == 0) {
    14     // we do not have %ms :(
    15     char name[width + 1];
    16     memset(name, ' ', width + 1);
    17     name[width] = '\0';
    18     if (strlen(dir.name) < width) {
    19       strcpy(name, dir.name);
    20       name[strlen(dir.name)] = ' ';
    21       printu("%s %d\n", name, dir.inum);
    22     }
    23     else
    24       printu("%s %d\n", dir.name, dir.inum);
    25   }
    26   printu("------------------------------\n");
    27   closedir_u(dir_fd);
    28 
    29   exit(0);
    30   return 0;
    31 }
    

    以上给出了app_exec和app_ls两个程序。其中app_exec作为主程序会在PKE启动后被运行。user/app_ls.c被编译成可执行文件app_ls,保存在./hostfs_root/bin/目录(挂载为/bin下。在app_exec执行过程中会调用exec系统调用执行/bin目录下的app_ls程序。

    exec函数用来在一个进程中启动另一个程序执行。其中可执行文件通过文件的路径名指定从文件系统中读出。exec会根据读入的可执行文件将原进程的数据段、代码段和堆栈段替换。

    如上的应用预期输出如下:

    In m_start, hartid:0
    HTIF is available!
    (Emulated) memory size: 2048 MB
    Enter supervisor mode...
    PKE kernel start 0x0000000080000000, PKE kernel end: 0x000000008000d000, PKE kernel size: 0x000000000000d000 .
    free physical memory address: [0x000000008000d000, 0x0000000087ffffff] 
    kernel memory manager is initializing ...
    KERN_BASE 0x0000000080000000
    physical address of _etext is: 0x0000000080008000
    kernel page table is on 
    RAMDISK0: base address of RAMDISK0 is: 0x0000000087f35000
    RFS: format RAMDISK0 done!
    Switch to user mode...
    in alloc_proc. user frame 0x0000000087f29000, user stack 0x000000007ffff000, user kstack 0x0000000087f28000 
    FS: created a file management struct for a process.
    in alloc_proc. build proc_file_management successfully.
    User application is loading.
    Application: /bin/app_exec
    CODE_SEGMENT added at mapped info offset:3
    Application program entry point (virtual address): 0x0000000000010078
    going to insert process 0 to ready queue.
    going to schedule process 0 to run.
    
    ======== exec /bin/app_ls in app_exec ========
    Application: /bin/app_ls
    CODE_SEGMENT added at mapped info offset:3
    DATA_SEGMENT added at mapped info offset:4
    Application program entry point (virtual address): 0x00000000000100b0
    ------------------------------
    ls "/RAMDISK0":
    [name]               [inode_num]
    ------------------------------
    User exit with code:0.
    no more ready processes, system shutdown now.
    System is shutting down with exit code 0.
    

    根据结果可以看出app_exec程序成功读取并运行了保存在hostfs上的app_ls程序。

实验内容

本实验为挑战实验基础代码将继承和使用lab4_3完成后的代码

  • 先提交lab4_3的答案然后切换到lab4_challenge2_exec、继承lab4_3中所做修改

    //切换到lab4_challenge2_exec
    $ git checkout lab4_challenge2_exec
    
    //继承lab4_3以及之前的答案
    $ git merge lab4_3_hardlink -m "continue to work on lab4_challenge2"
    
    //在完成代码修改后进行编译
    $ make clean; make
    
    //运行程序(根据实现方式的不同,运行命令可能有以下两种)
    //1. 修改了PKE基础代码中加载主程序ELF文件的方式改为通过VFS文件系统读取
    $ spike ./obj/riscv-pke /bin/app_exec
    
    //2. 未对PKE基础代码中加载主程序ELF文件的方式进行修改
    $ spike ./obj/riscv-pke ./obj/app_exec
    

    注意:**不同于基础实验,挑战实验的基础代码具有更大的不完整性,可能无法直接通过构造过程。**同样,不同于基础实验,我们在代码中也并未专门地哪些地方的代码需要填写,哪些地方的代码无须填写。这样,我们留给读者更大的“想象空间”。

  • 本实验的具体要求为:

    • 通过修改PKE内核和系统调用为用户程序提供exec函数的功能。exec接受一个可执行程序的路径名作为参数表示要重新载入的elf文件。exec函数在执行成功时不会返回执行失败时返回-1。

实验指导

  • 你对内核代码的修改可能包含添加系统调用、在内核中实现exec函数的功能。

注意完成实验内容后若读者同时完成了之前的“lab3_challenge1 进程等待和数据段复制”挑战实验则可以自行对app_exec程序进行修改将其改为一个简单的“shell”程序。该“shell”程序应该首先通过fork系统调用创建一个新的子进程并在子进程中调用exec执行一个新的程序如app_ls同时父进程会调用wait系统调用等待子进程的结束并继续执行后续代码。

另外,后续的基础实验代码并不依赖挑战实验,所以读者可自行决定是否将自己的工作提交到本地代码仓库中(当然,提交到本地仓库是个好习惯,至少能保存自己的“作品”)。

6.7 lab4_challenge3 简易Shell难度★★★★★

给定应用

  • user/app_shell.c
/*
 * This app starts a very simple shell and executes some simple commands.
 * The commands are stored in the hostfs_root/shellrc
 * The shell loads the file and executes the command line by line.                 
 */
#include "user_lib.h"
#include "string.h"
#include "util/types.h"

int main(int argc, char *argv[]) {
  printu("\n======== Shell Start ========\n\n");
  int fd;
  int MAXBUF = 1024;
  char buf[MAXBUF];
  char *token;
  char delim[3] = " \n";
  fd = open("/shellrc", O_RDONLY);

  read_u(fd, buf, MAXBUF);
  close(fd);
  char *command = naive_malloc();
  char *para = naive_malloc();
  int start = 0;
  while (1)
  {
    if(!start) {
      token = strtok(buf, delim);
      start = 1;
    }
    else 
      token = strtok(NULL, delim);
    strcpy(command, token);
    token = strtok(NULL, delim);
    strcpy(para, token);
    if(strcmp(command, "END") == 0 && strcmp(para, "END") == 0)
      break;
    printu("Next command: %s %s\n\n", command, para);
    printu("==========Command Start============\n\n");
    int pid = fork();
    if(pid == 0) {
      int ret = exec(command, para);
      if (ret == -1)
      printu("exec failed!\n");
    }
    else
    {
      wait(pid);
      printu("==========Command End============\n\n");
    }
  }
  exit(0);
  return 0;
}

该应用程序从位于hostfs_root下的shellrc文件中读取若干条命令对于每条命令我们都会fork一个新的进程并exec对应的程序来完成命令。exec将用第一个参数对应的程序来替换fork出来的新进程并将第二个参数的内容传入新的进程。

给出的shellrc文件如下

/bin/app_mkdir /RAMDISK0/sub_dir
/bin/app_touch /RAMDISK0/sub_dir/ramfile1
/bin/app_touch /RAMDISK0/sub_dir/ramfile2
/bin/app_echo /RAMDISK0/sub_dir/ramfile1
/bin/app_cat /RAMDISK0/sub_dir/ramfile1
/bin/app_ls /RAMDISK0/sub_dir
/bin/app_ls /RAMDISK0
END END

可以看到我们提供了五种简化版的命令分别是lsmkdirtouchechocat。命令的对应功能参考Linux下的命令。echo命令的内容有所不同为了简单起见提供的echo命令功能为向指定的文件写入hello world。比如shellrc中的echo命令实际上是向/RAMDISK0/sub_dir/ramfile1文件中写入了hello world

同样是为了简单起见我们规定在遇到两个END时结束程序。

shellrc中的其余命令实现均位于user目录下这里不一一列出请感兴趣的读者自行查看。

shell程序将会依次执行上述命令如果你完成了本实验你应当看到的输出如下

$ spike obj/riscv-pke /bin/app_shell
In m_start, hartid:0
HTIF is available!
(Emulated) memory size: 2048 MB
Enter supervisor mode...
PKE kernel start 0x0000000080000000, PKE kernel end: 0x0000000080011000, PKE kernel size: 0x0000000000011000 .
free physical memory address: [0x0000000080011000, 0x0000000087ffffff] 
kernel memory manager is initializing ...
KERN_BASE 0x0000000080000000
physical address of _etext is: 0x0000000080009000
kernel page table is on 
RAMDISK0: base address of RAMDISK0 is: 0x0000000087f35000
RFS: format RAMDISK0 done!
Switch to user mode...
in alloc_proc. user frame 0x0000000087f29000, user stack 0x000000007ffff000, user kstack 0x0000000087f28000 
FS: created a file management struct for a process.
in alloc_proc. build proc_file_management successfully.
User application is loading.
Application: /bin/app_shell
CODE_SEGMENT added at mapped info offset:4
DATA_SEGMENT added at mapped info offset:5
Application program entry point (virtual address): 0x00000000000100b0
going to insert process 0 to ready queue.
going to schedule process 0 to run.

======== Shell Start ========

Next command: /bin/app_mkdir /RAMDISK0/sub_dir

==========Command Start============

User call fork.
will fork a child from parent 0.
in alloc_proc. user frame 0x0000000087f0e000, user stack 0x000000007ffff000, user kstack 0x0000000087f0c000 
FS: created a file management struct for a process.
in alloc_proc. build proc_file_management successfully.
do_fork map code segment at pa:0000000087f13000 of parent to child at va:0000000000010000.
going to insert process 1 to ready queue.
going to schedule process 1 to run.
Application: /bin/app_mkdir
CODE_SEGMENT added at mapped info offset:4
Application program entry point (virtual address): 0x0000000000010078

======== mkdir command ========
mkdir: /RAMDISK0/sub_dir
User exit with code:0.
going to insert process 0 to ready queue.
going to schedule process 0 to run.
==========Command End============

Next command: /bin/app_touch /RAMDISK0/sub_dir/ramfile1

==========Command Start============

User call fork.
will fork a child from parent 0.
in alloc_proc. user frame 0x0000000087ef3000, user stack 0x000000007ffff000, user kstack 0x0000000087ef2000 
FS: created a file management struct for a process.
in alloc_proc. build proc_file_management successfully.
do_fork map code segment at pa:0000000087f13000 of parent to child at va:0000000000010000.
going to insert process 2 to ready queue.
going to schedule process 2 to run.
Application: /bin/app_touch
CODE_SEGMENT added at mapped info offset:4
Application program entry point (virtual address): 0x0000000000010078

======== touch command ========
touch: /RAMDISK0/sub_dir/ramfile1
file descriptor fd: 0
User exit with code:0.
going to insert process 0 to ready queue.
going to schedule process 0 to run.
==========Command End============

Next command: /bin/app_touch /RAMDISK0/sub_dir/ramfile2

==========Command Start============

User call fork.
will fork a child from parent 0.
in alloc_proc. user frame 0x0000000087ee8000, user stack 0x000000007ffff000, user kstack 0x0000000087ee6000 
FS: created a file management struct for a process.
in alloc_proc. build proc_file_management successfully.
do_fork map code segment at pa:0000000087f13000 of parent to child at va:0000000000010000.
going to insert process 3 to ready queue.
going to schedule process 3 to run.
Application: /bin/app_touch
CODE_SEGMENT added at mapped info offset:4
Application program entry point (virtual address): 0x0000000000010078

======== touch command ========
touch: /RAMDISK0/sub_dir/ramfile2
file descriptor fd: 0
User exit with code:0.
going to insert process 0 to ready queue.
going to schedule process 0 to run.
==========Command End============

Next command: /bin/app_echo /RAMDISK0/sub_dir/ramfile1

==========Command Start============

User call fork.
will fork a child from parent 0.
in alloc_proc. user frame 0x0000000087ed1000, user stack 0x000000007ffff000, user kstack 0x0000000087ecf000 
FS: created a file management struct for a process.
in alloc_proc. build proc_file_management successfully.
do_fork map code segment at pa:0000000087f13000 of parent to child at va:0000000000010000.
going to insert process 4 to ready queue.
going to schedule process 4 to run.
Application: /bin/app_echo
CODE_SEGMENT added at mapped info offset:4
DATA_SEGMENT added at mapped info offset:5
Application program entry point (virtual address): 0x00000000000100b0

======== echo command ========
echo: /RAMDISK0/sub_dir/ramfile1
file descriptor fd: 0
write content: 
hello world
User exit with code:0.
going to insert process 0 to ready queue.
going to schedule process 0 to run.
==========Command End============

Next command: /bin/app_cat /RAMDISK0/sub_dir/ramfile1

==========Command Start============

User call fork.
will fork a child from parent 0.
in alloc_proc. user frame 0x0000000087eba000, user stack 0x000000007ffff000, user kstack 0x0000000087eb8000 
FS: created a file management struct for a process.
in alloc_proc. build proc_file_management successfully.
do_fork map code segment at pa:0000000087f13000 of parent to child at va:0000000000010000.
going to insert process 5 to ready queue.
going to schedule process 5 to run.
Application: /bin/app_cat
CODE_SEGMENT added at mapped info offset:4
Application program entry point (virtual address): 0x0000000000010078

======== cat command ========
cat: /RAMDISK0/sub_dir/ramfile1
file descriptor fd: 0
read content: 
hello world
User exit with code:0.
going to insert process 0 to ready queue.
going to schedule process 0 to run.
==========Command End============

Next command: /bin/app_ls /RAMDISK0/sub_dir

==========Command Start============

User call fork.
will fork a child from parent 0.
in alloc_proc. user frame 0x0000000087ea2000, user stack 0x000000007ffff000, user kstack 0x0000000087ea0000 
FS: created a file management struct for a process.
in alloc_proc. build proc_file_management successfully.
do_fork map code segment at pa:0000000087f13000 of parent to child at va:0000000000010000.
going to insert process 6 to ready queue.
going to schedule process 6 to run.
Application: /bin/app_ls
CODE_SEGMENT added at mapped info offset:4
DATA_SEGMENT added at mapped info offset:5
Application program entry point (virtual address): 0x00000000000100b0
---------- ls command -----------
ls "/RAMDISK0/sub_dir":
[name]               [inode_num]
ramfile1             2
ramfile2             3
------------------------------
User exit with code:0.
going to insert process 0 to ready queue.
going to schedule process 0 to run.
==========Command End============

Next command: /bin/app_ls /RAMDISK0

==========Command Start============

User call fork.
will fork a child from parent 0.
in alloc_proc. user frame 0x0000000087e8b000, user stack 0x000000007ffff000, user kstack 0x0000000087f14000 
FS: created a file management struct for a process.
in alloc_proc. build proc_file_management successfully.
do_fork map code segment at pa:0000000087f13000 of parent to child at va:0000000000010000.
going to insert process 7 to ready queue.
going to schedule process 7 to run.
Application: /bin/app_ls
CODE_SEGMENT added at mapped info offset:4
DATA_SEGMENT added at mapped info offset:5
Application program entry point (virtual address): 0x00000000000100b0
---------- ls command -----------
ls "/RAMDISK0":
[name]               [inode_num]
sub_dir              1
------------------------------
User exit with code:0.
going to insert process 0 to ready queue.
going to schedule process 0 to run.
==========Command End============

User exit with code:0.
no more ready processes, system shutdown now.
System is shutting down with exit code 0.

实验内容

本实验为挑战实验基础代码将继承和使用lab4_3_hardlink完成后的代码

  • 先提交lab4_3_hardlink的答案然后切换到lab4_challenge3_shell、继承lab4_3_hardlink中所做修改
//切换到lab4_challenge3_shell
$ git checkout lab4_challenge3_shell

//继承lab4_3_hardlink以及之前的答案
$ git merge lab4_3_hardlink -m "continue to work on lab4_challenge3"

注意:不同于基础实验,挑战实验的基础代码具有更大的不完整性,可能无法直接通过构造过程。 同样,不同于基础实验,我们在代码中也并未专门地哪些地方的代码需要填写,哪些地方的代码无须填写。这样,我们留给读者更大的“想象空间”。

  • 本实验的具体要求为:

    • 通过修改PKE内核和系统调用为用户程序提供exec函数的功能。exec接受一个可执行程序的路径名以及一个字符串作为参数。实际上exec系统调用的第二个参数本应是一个参数数组即char** 类型这里为简单起见仅需传入一个参数即可。如果读者学有余力可以自行实现多个参数传入版本的exec
    • 此外为了保证shell的正规性你需要额外实现wait功能来保证程序的执行顺序。如果你已经完成了lab3_challenge1那么可以直接使用对应成果。

实验指导

shell是Linux中的一个重要概念主要用于和用户的交互。其基本流程为用户输入命令->shell创建一个新进程并使用exec函数加载命令对应的程序->将结果通过shell返回用户。其中fork函数的功能已在前面的实验中实现本实验主要实现exec函数的功能。如果你已完成lab4_challenge2的话对本实验会非常有帮助

不考虑参数传递问题的话exec要做的工作基本为替换elf内容和重置堆栈等地址空间。但是因为存在新进程需要得知命令参数的问题exec除了提到的工作之外还需要考虑以下问题

  • main函数中的argcargv数组的含义以及main函数是从哪里得到它们的。提示main函数也是函数函数的参数一般保存在哪里呢
  • 在lab1中我们初步了解了系统调用可以发现系统调用使用了a0~a7寄存器。请读者思考处理系统调用的函数的返回值保存在哪里,有什么作用呢?
  • 在exec替换完elf后我们将堆栈重置。此时的sp指向什么位置是否可以修改以达到传递参数的效果。
  • 注意地址对齐。读者可自行参阅RISC-V的相关资料了解该指令集下的地址对齐规则。

注意完成实验内容后若读者同时完成了之前的“lab3_challenge3 写时复制”挑战实验则可以将fork添加写时复制功能来减少不必要的开销。此外本挑战实验具有很强的扩展性读者可通过自行比对与真实shell的差别为pke-shell添加更完善的功能。

另外,后续的基础实验代码并不依赖挑战实验,所以读者可自行决定是否将自己的工作提交到本地代码仓库中(当然,提交到本地仓库是个好习惯,至少能保存自己的“作品”)。