湖南大学操作系统实验ucore-lab8文件系统(md)

  • 专业班级:
  • 学号:
  • 姓名:

目录

  • 实验目的
  • 实验内容
  • 实验步骤
    • 练习0
    • 练习1:完成读文件操作的实现
    • 练习2:完成基于文件系统的执行程序机制的实现
  • 实验总结

实验目的

通过完成本次实验,希望能达到以下目标

  • 了解基本的文件系统系统调用的实现方法;
  • 了解一个基于索引节点组织方式的Simple FS文件系统的设计与实现;
  • 了解文件系统抽象层-VFS的设计与实现;

实验内容

实验七完成了在内核中的同步互斥实验。本次实验涉及的是文件系统,通过分析了解ucore文件系统的总体架构计,完善读写文件操作,从新实现基于文件系统的执行程序机制(即改写do_execve),从而可以完成执行存储在磁盘上的文件和实现文件读写等功能。

实验步骤

练习0:

练习1: 完成读文件操作的实现(需要编码)

首先了解打开文件的处理流程,然后参考本实验后续的文件读写操作的过程分析,编写在sfs_inode.csfs_io_nolock读文件中数据的实现代码。

请在实验报告中给出设计实现”UNIX的PIPE机制“的概要设方案,鼓励给出详细设计方案

解析

根据实验指导书,我们可以了解到,ucore的文件系统架构主要由四部分组成:

  • 通用文件系统访问接口层:该层提供了一个从用户空间到文件系统的标准访问接口。这一层访问接口让应用程序能够通过一个简单的接口获得ucore内核的文件系统服务。
  • 文件系统抽象层:向上提供一个一致的接口给内核其他部分(文件系统相关的系统调用实现模块和其他内核功能模块)访问。向下提供一个抽象函数指针列表和数据结构来屏蔽不同文件系统的实现细节。
  • Simple FS文件系统层:一个基于索引方式的简单文件系统实例。向上通过各种具体函数实现以对应文件系统抽象层提出的抽象函数。向下访问外设接口
  • 外设接口层:向上提供device访问接口屏蔽不同硬件细节。向下实现访问各种具体设备驱动的接口,比如disk设备接口/串口设备接口/键盘设备接口等。

对照上面的层次有如下大致的文件系统的访问处理过程:

假如应用程序操作文件(打开/创建/删除/读写),首先需要通过文件系统的通用文件系统访问接口层给用户空间提供的访问接口进入文件系统内部,接着由文件系统抽象层把访问请求转发给某一具体文件系统(比如SFS文件系统),具体文件系统(Simple FS文件系统层)把应用程序的访问请求转化为对磁盘上的block的处理请求,并通过外设接口层交给磁盘驱动例程来完成具体的磁盘操作。

ucore中的文件系统架构包含四类主要的数据结构, 它们分别是:
- 超级块(SuperBlock),它主要从文件系统的全局角度描述特定文件系统的全局信息。它的作用范围是整个OS空间。
- 索引节点(inode):它主要从文件系统的单个文件的角度它描述了文件的各种属性和数据所在位置。它的作用范围是整个OS空间。
- 目录项(dentry):它主要从文件系统的文件路径的角度描述了文件路径中的特定目录。它的作用范围是整个OS空间。
- 文件(file),它主要从进程的角度描述了一个进程在访问文件时需要了解的文件标识,文件读写的位置,文件引用情况等信息。它的作用范围是某一具体进程。

file数据结构:

struct file {
    enum {
    FD_NONE, FD_INIT, FD_OPENED, FD_CLOSED,
    } status;       //访问文件的执行状态
    bool readable; //文件是否可读
    bool writable; //文件是否可写
    int fd;        //文件在filemap中的索引值
    off_t pos;    //访问文件的当前位置
    struct inode *node;//该文件对应的内存inode指针
    atomic_t open_count;//打开此文件的次数
};

inode数据结构,它是位于内存的索引节点,把不同文件系统的特定索引节点信息(甚至不能算是一个索引节点)统一封装起来,避免了进程直接访问具体文件系统

struct inode {
    union { //包含不同文件系统特定inode信息的union域
    struct device __device_info;  //设备文件系统内存inode信息
    struct sfs_inode __sfs_inode_info; //SFS文件系统内存inode信息
    } in_info;
    enum {
    inode_type_device_info = 0x1234,
    inode_type_sfs_inode_info,
    } in_type;  //此inode所属文件系统类型
    atomic_t ref_count;   //此inode的引用计数
    atomic_t open_count;  //打开此inode对应文件的个数
    struct fs *in_fs;     //抽象的文件系统,包含访问文件系统的函数指针
    const struct inode_ops *in_ops;   //抽象的inode操作,包含访问inode的函数指针
};

对应到我们的ucore上,具体的过程如下:

先进入通用文件访问接口层的处理流程,即进一步调用如下用户态函数:read->sys_read->syscall,从而引起系统调用进入到内核态。到了内核态以后,通过中断处理例程,会调用到sys_read内核函数,并进一步调用sysfile_read内核函数,进入到文件系统抽象层处理流程完成进一步读文件的操作。

文件系统抽象层的处理流程

  1. 检查错误,即检查读取长度是否为0和文件是否可读。
  2. 分配buffer空间,即调用kmalloc函数分配4096字节的buffer空间。
  3. 读文件过程

[1] 实际读文件
循环读取文件,每次读取buffer大小。每次循环中,先检查剩余部分大小,若其小于4096字节,则只读取剩余部分的大小。然后调用file_read函数(详细分析见后)将文件内容读取到buffer中,alen为实际大小。调用copy_to_user函数将读到的内容拷贝到用户的内存空间中,调整各变量以进行下一次循环读取,直至指定长度读取完成。最后函数调用层层返回至用户程序,用户程序收到了读到的文件内容。

[2] file_read函数
这个函数是读文件的核心函数。函数有4个参数,fd是文件描述符,base是缓存的基地址,len是要读取的长度,copied_store存放实际读取的长度。函数首先调用fd2file函数找到对应的file结构,并检查是否可读。调用filemap_acquire函数使打开这个文件的计数加1。调用vop_read函数将文件内容读到iob中(详细分析见后)。调整文件指针偏移量pos的值,使其向后移动实际读到的字节数iobuf_used(iob)。最后调用filemap_release函数使打开这个文件的计数减1,若打开计数为0,则释放file

SFS文件系统层的处理流程

vop_read函数实际上是对sfs_read的包装。在sfs_inode.csfs_node_fileops变量定义了.vop_read = sfs_read,所以下面来
分析sfs_read函数的实现。sfs_read函数调用sfs_io函数。它有三个参数,node是对应文件的inodeiob是缓存,write表示是读还是写的布尔值(0表示读,1表示写),这里是0。函数先找到inode对应sfssin,然后调用sfs_io_nolock函数进行读取文件操作,最后调用iobuf_skip函数调整iobuf的指针。

sfs_io_nolock函数中,先计算一些辅助变量,并处理一些特殊情况(比如越界),然后有sfs_buf_op =sfs_rbuf,sfs_block_op = sfs_rblock,设置读取的函数操作。接着进行实际操作,先处理起始的没有对齐到块的部分,再以块为单位循环处理中间的部分,最后处理末尾剩余的部分。每部分中都调用sfs_bmap_load_nolock函数得到blkno对应的inode编号,并调用sfs_rbufsfs_rblock函数读取数据(中间部分调用sfs_rblock,起始和末尾部分调用sfs_rbuf),调整相关变量。完成后如果offset + alen > din->fileinfo.size(写文件时会出现这种情况,读文件时不会出现这种情况,alen为实际读写的长度),则调整文件大小为offset + alen并设置dirty变量。

sfs_io_nolock函数中添加如下代码:

if ((blkoff = offset % SFS_BLKSIZE) != 0) {                   //读取第一部分的数据
size = (nblks != 0) ? (SFS_BLKSIZE - blkoff) : (endpos - offset); //计算第一个数据块的大小
if ((ret = sfs_bmap_load_nolock(sfs, sin, blkno, &ino)) != 0) {   //找到内存文件索引对应的block的编号ino
    goto out;
}

if ((ret = sfs_buf_op(sfs, buf, size, ino, blkoff)) != 0) {   
    goto out;
}
//完成实际的读写操作
alen += size;
if (nblks == 0) {
    goto out;
}
buf += size, blkno ++, nblks --;
}

//读取中间部分的数据,将其分为size大学的块,然后一次读一块直至读完
size = SFS_BLKSIZE;
while (nblks != 0) {
if ((ret = sfs_bmap_load_nolock(sfs, sin, blkno, &ino)) != 0) {
    goto out;
}
if ((ret = sfs_block_op(sfs, buf, ino, 1)) != 0) {
    goto out;
}
alen += size, buf += size, blkno ++, nblks --;
}
//读取第三部分的数据
if ((size = endpos % SFS_BLKSIZE) != 0) {
if ((ret = sfs_bmap_load_nolock(sfs, sin, blkno, &ino)) != 0) {
    goto out;
}
if ((ret = sfs_buf_op(sfs, buf, size, ino, 0)) != 0) {
    goto out;
}
alen += size;
}

练习2: 完成基于文件系统的执行程序机制的实现(需要编码)

改写proc.c中load_icode函数和其他相关函数,实现基于文件系统的执行程序机制。执行:make qemu。如果能看到sh用户程序的执行界面,则基本成功了。如果在sh用户界面上可以执行”ls”,”hello”等其他放置在sfs文件系统中的其他执行程序,则可以认为本实验基本成功。

请在实验报告中给出设计实现基于”UNIX的硬链接和软链接机制“的概要设方案,鼓励给出详细设计方案

实验要求改写proc.c中的 load_icode 函数和其他相关函数,实现基于文件系统的执行程序机制。
proc.c中,根据注释我们需要先初始化fs中的进程控制结构,即在alloc_proc函数中我们需要做一下修改,加上一句proc->filesp = NULL;从而完成初始化。

修改之后alloc_proc函数如下:

static struct proc_struct * alloc_proc(void) {  
struct proc_struct *proc = kmalloc(sizeof(struct proc_struct));  
if (proc != NULL) {  
    proc->state = PROC_UNINIT;  
    proc->pid = -1;  
    proc->runs = 0;  
    proc->kstack = 0;  
    proc->need_resched = 0;  
    proc->parent = NULL;  
    proc->mm = NULL;  
    memset(&(proc->context), 0, sizeof(struct context));  
    proc->tf = NULL;  
    proc->cr3 = boot_cr3;  
    proc->flags = 0;  
    memset(proc->name, 0, PROC_NAME_LEN);  
    proc->wait_state = 0;  
    proc->cptr = proc->optr = proc->yptr = NULL;  
    proc->rq = NULL;  
    proc->run_link.prev = proc->run_link.next = NULL;  
    proc->time_slice = 0;  
    proc->lab6_run_pool.left = proc->lab6_run_pool.right = proc->lab6_run_pool.parent = NULL;  
    proc->lab6_stride = 0;  
    proc->lab6_priority = 0;  

    proc->filesp = NULL;     //初始化fs中的进程控制结构
}  
return proc;  
}  

然后就是要实现load_icode函数,具体的实现及注释如下所示:

static int
load_icode(int fd, int argc, char **kargv) {
assert(argc >= 0 && argc <= EXEC_MAX_ARG_NUM);

//(1)建立内存管理器
if (current->mm != NULL) {    //要求当前内存管理器为空
    panic("load_icode: current->mm must be empty.\n");
}

int ret = -E_NO_MEM;    // E_NO_MEM代表因为存储设备产生的请求错误
struct mm_struct *mm;  //建立内存管理器
if ((mm = mm_create()) == NULL) {
    goto bad_mm;
}

//(2)建立页目录
if (setup_pgdir(mm) != 0) {
    goto bad_pgdir_cleanup_mm;
}
struct Page *page;//建立页表

//(3)从文件加载程序到内存
struct elfhdr __elf, *elf = &__elf;
if ((ret = load_icode_read(fd, elf, sizeof(struct elfhdr), 0)) != 0) {//读取elf文件头
    goto bad_elf_cleanup_pgdir;           
}

if (elf->e_magic != ELF_MAGIC) {
    ret = -E_INVAL_ELF;
    goto bad_elf_cleanup_pgdir;
}

struct proghdr __ph, *ph = &__ph;
uint32_t vm_flags, perm, phnum;
for (phnum = 0; phnum < elf->e_phnum; phnum ++) {  //e_phnum代表程序段入口地址数目,即多少各段
    off_t phoff = elf->e_phoff + sizeof(struct proghdr) * phnum;  //循环读取程序的每个段的头部   
    if ((ret = load_icode_read(fd, ph, sizeof(struct proghdr), phoff)) != 0) {
        goto bad_cleanup_mmap;
    }
    if (ph->p_type != ELF_PT_LOAD) {
        continue ;
    }
    if (ph->p_filesz > ph->p_memsz) {
        ret = -E_INVAL_ELF;
        goto bad_cleanup_mmap;
    }
    if (ph->p_filesz == 0) {
        continue ;
    }
    vm_flags = 0, perm = PTE_U;//建立虚拟地址与物理地址之间的映射
    if (ph->p_flags & ELF_PF_X) vm_flags |= VM_EXEC;
    if (ph->p_flags & ELF_PF_W) vm_flags |= VM_WRITE;
    if (ph->p_flags & ELF_PF_R) vm_flags |= VM_READ;
    if (vm_flags & VM_WRITE) perm |= PTE_W;
    if ((ret = mm_map(mm, ph->p_va, ph->p_memsz, vm_flags, NULL)) != 0) {
        goto bad_cleanup_mmap;
    }
    off_t offset = ph->p_offset;
    size_t off, size;
    uintptr_t start = ph->p_va, end, la = ROUNDDOWN(start, PGSIZE);


    ret = -E_NO_MEM;

    //复制数据段和代码段
    end = ph->p_va + ph->p_filesz;      //计算数据段和代码段终止地址
    while (start < end) {               
        if ((page = pgdir_alloc_page(mm->pgdir, la, perm)) == NULL) {
            ret = -E_NO_MEM;
            goto bad_cleanup_mmap;
        }
        off = start - la, size = PGSIZE - off, la += PGSIZE;
        if (end < la) {
            size -= la - end;
        }
        //每次读取size大小的块,直至全部读完
        if ((ret = load_icode_read(fd, page2kva(page) + off, size, offset)) != 0) {       //load_icode_read通过sysfile_read函数实现文件读取
            goto bad_cleanup_mmap;
        }
        start += size, offset += size;
    }
    //建立BSS段
    end = ph->p_va + ph->p_memsz;   //同样计算终止地址

    if (start < la) {     
        if (start == end) {   
            continue ;
        }
        off = start + PGSIZE - la, size = PGSIZE - off;
        if (end < la) {
            size -= la - end;
        }
        memset(page2kva(page) + off, 0, size);
        start += size;
        assert((end < la && start == end) || (end >= la && start == la));
    }

    while (start < end) {
        if ((page = pgdir_alloc_page(mm->pgdir, la, perm)) == NULL) {
            ret = -E_NO_MEM;
            goto bad_cleanup_mmap;
        }
        off = start - la, size = PGSIZE - off, la += PGSIZE;
        if (end < la) {
            size -= la - end;
        }
        //每次操作size大小的块
        memset(page2kva(page) + off, 0, size);
        start += size;
    }
}
sysfile_close(fd);//关闭文件,加载程序结束

//(4)建立相应的虚拟内存映射表
vm_flags = VM_READ | VM_WRITE | VM_STACK;
if ((ret = mm_map(mm, USTACKTOP - USTACKSIZE, USTACKSIZE, vm_flags, NULL)) != 0) {
    goto bad_cleanup_mmap;
}
assert(pgdir_alloc_page(mm->pgdir, USTACKTOP-PGSIZE , PTE_USER) != NULL);
assert(pgdir_alloc_page(mm->pgdir, USTACKTOP-2*PGSIZE , PTE_USER) != NULL);
assert(pgdir_alloc_page(mm->pgdir, USTACKTOP-3*PGSIZE , PTE_USER) != NULL);
assert(pgdir_alloc_page(mm->pgdir, USTACKTOP-4*PGSIZE , PTE_USER) != NULL);
//(5)设置用户栈
mm_count_inc(mm);
current->mm = mm;
current->cr3 = PADDR(mm->pgdir);
lcr3(PADDR(mm->pgdir));

//(6)处理用户栈中传入的参数,其中argc对应参数个数,uargv[]对应参数的具体内容的地址
uint32_t argv_size=0, i;
for (i = 0; i < argc; i ++) {
    argv_size += strnlen(kargv[i],EXEC_MAX_ARG_LEN + 1)+1;
}

uintptr_t stacktop = USTACKTOP - (argv_size/sizeof(long)+1)*sizeof(long);
char** uargv=(char **)(stacktop  - argc * sizeof(char *));

argv_size = 0;
for (i = 0; i < argc; i ++) {         //将所有参数取出来放置uargv
    uargv[i] = strcpy((char *)(stacktop + argv_size ), kargv[i]);
    argv_size +=  strnlen(kargv[i],EXEC_MAX_ARG_LEN + 1)+1;
}

stacktop = (uintptr_t)uargv - sizeof(int);   //计算当前用户栈顶
*(int *)stacktop = argc;              
//(7)设置进程的中断帧   
struct trapframe *tf = current->tf;     
memset(tf, 0, sizeof(struct trapframe));//初始化tf,设置中断帧
tf->tf_cs = USER_CS;      
tf->tf_ds = tf->tf_es = tf->tf_ss = USER_DS;
tf->tf_esp = stacktop;
tf->tf_eip = elf->e_entry;
tf->tf_eflags = FL_IF;
ret = 0;
//(8)错误处理部分
out:
return ret;           //返回
    bad_cleanup_mmap:
exit_mmap(mm);
bad_elf_cleanup_pgdir:
put_pgdir(mm);
bad_pgdir_cleanup_mm:
mm_destroy(mm);
bad_mm:
goto out;
}

详细的注释已经在代码中标注出来了。
load_icode主要是将文件加载到内存中执行,根据注释的提示分为了一共七个步骤:

  1. 建立内存管理器
  2. 建立页目录
  3. 将文件逐个段加载到内存中,这里要注意设置虚拟地址与物理地址之间的映射
  4. 建立相应的虚拟内存映射表
  5. 建立并初始化用户堆栈
  6. 处理用户栈中传入的参数
  7. 最后很关键的一步是设置用户进程的中断帧

当然一旦发生错误还需要进行错误处理。

实验结果与分析

实验总结

本次实验收获颇大。

你可能感兴趣的:(机器学习)