操作系统真象还原 学习笔记16--实现文件目录操作

selph
selph
发布于 2021-02-27 / 679 阅读
1
0

操作系统真象还原 学习笔记16--实现文件目录操作

本篇对应书籍第十四章14.5--14.15的内容

本章分成了两部分来记录,上一部分完成了文件系统原理的介绍,文件系统的创建,和基础设施的建设,这里该实现文件系统相关的文件操作了

创建文件

实现 file_create

创建文件需要考虑实现如下工作:

  1. 文件需要inode来描述大小、 位置等属性, 所以创建文件就要创建其inode。 这就涉及到向inode _ bitmap申请位图来获得inode号,因此inode_ bitmap会被更新,inode_table数组中的某项也会由新的inode填充。
  2. inode->i_sectors是文件具体存储的扇区地址,这需要向block_bitmap申请可用位来获得可用的块,因此block_bitmap会被更新,分区的数据区data_ start_Iba 以后的某个扇区会被分配。
  3. 新增加的文件必然存在于某个目录,所以该目录的 inode->i_ size 会增加个目录项的大小。 此新增加的文件对应的目录项需要写入该目录的 inode->i_ sectors[]中的某个扇区, 原有扇区可能已满,所以有可能要申请新扇区来存储目录项。
  4. 若其中某步操作失败, 需要回滚之前已成功的操作。
  5. inode _ bitmap、 block—bitmap、新文件的 inode 及文件所在目录的 inode, 这些位千内存中已经被改变的数据要同步到硬盘。

fs/file.c

// 创建文件, 若成功则返回文件描述符, 否则返回 -1
int32_t file_create(struct dir* parent_dir, char* filename, uint8_t flag) {
    // 后续操作的公共缓冲区
    void* io_buf = sys_malloc(1024);
    if (io_buf == NULL) {
        printk("in file_creat: sys_malloc for io_buf failed\n");
        return -1;
    }

    uint8_t rollback_step = 0; // 用于操作失败时回滚各资源状态

    // 为新文件分配 inode
    int32_t inode_no = inode_bitmap_alloc(cur_part);
    if (inode_no == -1) {
        printk("in file_creat: allocate inode failed\n");
        return -1;
    }

    // 此 inode 要从堆中申请内存, 不可生成局部变量(函数退出时会释放)
    // 因为 file_table 数组中的文件描述符的 inode 指针要指向它
    struct inode* new_file_inode = (struct inode*)sys_malloc(sizeof(struct inode));
    if (new_file_inode == NULL) {
        printk("file_create: sys_malloc for inode failed\n");
        rollback_step = 1;
        goto rollback;
    }
    inode_init(inode_no, new_file_inode); // 初始化 inode

    // 返回的是 file_table 数组的下标
    int fd_idx = get_free_slot_in_global();
    if (fd_idx == -1) {
        printk("exceed max open files\n");
        rollback_step = 2;
        goto rollback;
    }

    file_table[fd_idx].fd_inode = new_file_inode;
    file_table[fd_idx].fd_pos = 0;
    file_table[fd_idx].fd_flag = flag;
    file_table[fd_idx].fd_inode->write_deny = false;

    struct dir_entry new_dir_entry;
    memset(&new_dir_entry, 0, sizeof(struct dir_entry));

    create_dir_entry(filename, inode_no, FT_REGULAR, &new_dir_entry);

    // 同步内存数据到硬盘
    // a 在目录 parent_dir 下安装目录项 new_dir_entry
    // 写入硬盘后返回 true, 否则 false
    if (!sync_dir_entry(parent_dir, &new_dir_entry, io_buf)) {
        printk("sync dir_entry to disk failed\n");
        rollback_step = 3;
        goto rollback;
    }

    memset(io_buf, 0, 1024);
    // b 将父目录 inode 的内容同步到硬盘
    inode_sync(cur_part, parent_dir->inode, io_buf);

    memset(io_buf, 0, 1024);
    // c 将新创建文件的 inode 内容同步到硬盘
    inode_sync(cur_part, new_file_inode, io_buf);

    // d 将 inode_bitmap 位图同步到硬盘
    bitmap_sync(cur_part, inode_no, INODE_BITMAP);

    // e 将创建的文件 inode 添加到 open_inodes 链表
    list_push(&cur_part->open_inodes, &new_file_inode->inode_tag);
    new_file_inode->i_open_cnts = 1;

    sys_free(io_buf);
    return pcb_fd_install(fd_idx);

// 创建文件需要创建相关的多个资源
// 若某步失败则会执行到下面的回滚步骤
rollback:
    switch (rollback_step) {
        case 3:
            // 失败时, 将 file_table 中的相应位清空
            memset(&file_table[fd_idx], 0, sizeof(struct file));
        case 2:
            sys_free(new_file_inode);
        case 1:
            // 如果新文件的 inode 创建失败
            // 之前位图中分配的 inode_no 也要恢复
            bitmap_set(&cur_part->inode_bitmap, inode_no, 0);
            break;
    }
    sys_free(io_buf);
    return -1;
}

这里使用了goto和switch实现了累加回滚操作执行过程

函数执行先申请缓冲区,以防最后环节缓冲区空间不够导致瞎操作了半天

接着申请file_table数组下标,向数组中填充结构信息,接着创建目录项,将目录项填充,最后再将文件对硬盘的修改写入硬盘

创建新文件的顺序是:创建i节点,文件描述符fs,目录项

实现 sys_create

fs/fs.c

// 打开或创建文件成功后, 返回文件描述符, 否则返回 -1
int32_t sys_open(const char* pathname, uint8_t flags) {
    // 对目录要用 dir_open, 这里只有 open 文件
    if (pathname[strlen(pathname) - 1] == '/') {
        printk("can`t open a directory %s\n", pathname);
        return -1;
    }
    ASSERT(flags <= 7);
    int32_t fd = -1; // 默认为找不到

    struct path_search_record searched_record;
    memset(&searched_record, 0, sizeof(struct path_search_record));

    // 记录目录深度, 帮助判断中间某个目录不存在的情况
    uint32_t pathname_depth = path_depth_cnt((char*)pathname);
    
    // 先检查文件是否存在
    int inode_no = search_file(pathname, &searched_record);
    bool found = inode_no != -1 ? true : false;

    if (searched_record.file_type == FT_DIRECTORY) {
        printk("can`t open a directory with open(), use opendir() to instead\n");
        dir_close(searched_record.parent_dir);
        return -1;
    }

    uint32_t path_searched_depth = path_depth_cnt(searched_record.searched_path);

    // 先判断是否把 pathname 的各层目录都访问到了, 即是否在某个中间目录就失败了
    if (pathname_depth != path_searched_depth) {
        printk("cannot access %s: Not a directory, subpath %s is`t exist\n", pathname, searched_record.searched_path);
        dir_close(searched_record.parent_dir);
        return -1;
    }

    // 若是在最后一个路径上没找到, 并且并不是要创建文件, 直接返回 -1
    if (!found && !(flags & O_CREAT)) {
        printk("in path %s, file %s is`t exist\n", searched_record.searched_path, (strrchr(searched_record.searched_path, '/') + 1));
        dir_close(searched_record.parent_dir);
        return -1;
    } else if (found && flags & O_CREAT) { // 若要创建的文件已存在
        printk("%s has already exist!\n", pathname);
        dir_close(searched_record.parent_dir);
        return -1;
    }

    switch (flags & O_CREAT) {
        case O_CREAT:
            printk("creating file\n");
            fd = file_create(searched_record.parent_dir, (strrchr(pathname, '/') + 1), flags);
            dir_close(searched_record.parent_dir);
            break;
        // 其余为打开文件

    }

    // 此 fd 是指任务 pcb->fd_table 数组中的元素下标
    // 并不是指全局 file_table 中的下标
    return fd;
}

// 在磁盘上搜索文件系统, 若没有则格式化分区创建文件系统
void filesys_init() {
...
    // 确定默认操作的分区
    char default_part[8] = "sdb1";
    // 挂载分区
    list_traversal(&partition_list, mount_partition, (int)default_part);

    // 将当前分区的根目录打开
    open_root_dir(cur_part);

    // 初始化文件表
    uint32_t fd_idx = 0;
    while (fd_idx < MAX_FILE_OPEN) {
        file_table[fd_idx++].fd_inode = NULL;
    }
}

创建第一个文件

kernel/main.c

int main(void) {
    put_str("I am kernel\n");
    init_all();
    intr_enable(); // 打开中断
    process_execute(u_prog_a, "u_prog_a");
    process_execute(u_prog_b, "u_prog_b");
    thread_start("k_thread_a", 31, k_thread_a, "I am thread_a");
    thread_start("k_thread_b", 31, k_thread_b, "I am thread_b");
    sys_open("/file1",O_CREAT);
    while(1);
    return 0;
}

运行 Bochs

编译,第一次运行:

image-20210224203200539

第二次运行:

image-20210224203225246

文件的打开与关闭

有关文件读写的操作都要用到文件描述符,这里要改进一下sys_open来支持更多功能

文件的打开

fs/file.c

// 打开编号为 inode_no 的 inode 对应的文件, 若成功则返回文件描述符, 否则返回 -1
int32_t file_open(uint32_t inode_no, uint8_t flag) {
    int fd_idx = get_free_slot_in_global();
    if (fd_idx == -1) {
        printk("exceed max open files\n");
        return -1;
    }
    file_table[fd_idx].fd_inode = inode_open(cur_part, inode_no);
    file_table[fd_idx].fd_pos = 0; // 每次打开文件, 要将 fd_pos 还原为 0, 即让文件内的指针指向开头
    file_table[fd_idx].fd_flag = flag;
    bool* write_deny = &file_table[fd_idx].fd_inode->write_deny;

    // 只要是关于写文件, 判断是否有其它进程正写此文件
    if (flag & O_WRONLY || flag & O_RDWR) {
        // 进入临界区要关中断
        enum intr_status old_status = intr_disable();
        // 若当前没有其它进程写该文件, 将其占用
        if (!(*write_deny)) {
            *write_deny = true; // 置为 true, 避免多个进程同时写此文件
            intr_set_status(old_status);
        } else {
            intr_set_status(old_status);
            printk("file can`t be write now, try again later\n");
            return -1;
        }
    }
    return pcb_fd_install(fd_idx);
}

fs/fs.c

// 打开或创建文件成功后, 返回文件描述符, 否则返回 -1
int32_t sys_open(const char* pathname, uint8_t flags) {
...
    switch (flags & O_CREAT) {
        case O_CREAT:
            printk("creating file\n");
            fd = file_create(searched_record.parent_dir, (strrchr(pathname, '/') + 1), flags);
            dir_close(searched_record.parent_dir);
            break;
        // 其余为打开文件
        default:
            fd = file_open(inode_no, flags);
    }

    // 此 fd 是指任务 pcb->fd_table 数组中的元素下标
    // 并不是指全局 file_table 中的下标
    return fd;
}

这里是改进sys_open函数,使其拥有打开文件的功能

文件的关闭

fs/file.c

// 关闭文件
int32_t file_close(struct file* file) {
    if (file == NULL) {
        return -1;
    }
    file->fd_inode->write_deny = false;
    inode_close(file->fd_inode);
    file->fd_inode = NULL; // 使文件结构可用
    return 0;
}

就是关闭inode,恢复inode为未使用的状态

fs/fs.c

// 将文件描述符转化为文件表的下标
static uint32_t fd_local2global(uint32_t local_fd) {
    struct task_struct* cur = running_thread();
    int32_t global_fd = cur->fd_table[local_fd];
    ASSERT(global_fd >= 0 && global_fd < MAX_FILE_OPEN);
    return (uint32_t)global_fd;
}

// 关闭文件描述符 fd 指向的文件, 成功返回 0, 否则返回 -1
int32_t sys_close(int32_t fd) {
    int32_t ret = -1;
    if (fd > 2) {
        uint32_t _fd = fd_local2global(fd);
        ret = file_close(&file_table[_fd]);
        running_thread()->fd_table[fd] = -1; // 使该文件描述符位可用
    }
    return ret;
}

文件描述符数组的值就是文件表对应的文件结构的下标

sys_close 函数实际上就是上面两个函数的封装,关闭文件实际上就是获取该文件结构(通过文件表下标获取,知道文件描述符数组下标即可实现),然后将该文件结构中的inode信息还原

运行 Bochs

kernel/main.c

int main(void) {
    put_str("I am kernel\n");
    init_all();
    intr_enable(); // 打开中断
    process_execute(u_prog_a, "u_prog_a");
    process_execute(u_prog_b, "u_prog_b");
    thread_start("k_thread_a", 31, k_thread_a, "I am thread_a");
    thread_start("k_thread_b", 31, k_thread_b, "I am thread_b");
    uint32_t fd = sys_open("/file1",O_RDONLY);
    printf("fd:%d\n",fd);
    sys_close(fs);
    printf("%d closed now \n",fd);
    while(1);
    return 0;
}

编译,运行:

image-20210225025601386

实现文件写入

本节要实现系统调用sys_write的内核实现,让write支持文件描述符

实现 file_write

实现文件系统的套路:在file.c中添加file_xxx的核心功能函数,然后再fs.c中套个壳(封装)

fs/file.c

// 把 buf 中的 count 个字节写入 file, 成功则返回写入的字节数, 失败则返回 -1
int32_t file_write(struct file* file, const void* buf, uint32_t count) {
    if ((file->fd_inode->i_size + count) > (BLOCK_SIZE * 140)) { // 文件目前最大只支持 512*140=71680 字节
        printk("exceed max file_size 71680 bytes, write file failed\n");
        return -1;
    }
    uint8_t* io_buf = sys_malloc(BLOCK_SIZE);
    if (io_buf == NULL) {
        printk("file_write: sys_malloc for io_buf failed\n");
        return -1;
    }
    uint32_t* all_blocks = (uint32_t*)sys_malloc(BLOCK_SIZE+48); // 用来记录文件所有的块地址
    if (all_blocks == NULL) {
        printk("file_write: sys_malloc for all_blocks failed\n");
        return -1;
    }

    const uint8_t* src = buf;       // 用 src 指向 buf 中待写入的数据
    uint32_t bytes_written = 0;     // 用来记录已写入数据大小
    uint32_t size_left = count;	    // 用来记录未写入数据大小
    int32_t block_lba = -1;	        // 块地址
    uint32_t block_bitmap_idx = 0;  // 用来记录block对应于block_bitmap中的索引,做为参数传给bitmap_sync
    uint32_t sec_idx;	            // 用来索引扇区
    uint32_t sec_lba;	            // 扇区地址
    uint32_t sec_off_bytes;         // 扇区内字节偏移量
    uint32_t sec_left_bytes;        // 扇区内剩余字节量
    uint32_t chunk_size;	        // 每次写入硬盘的数据块大小
    int32_t indirect_block_table;   // 用来获取一级间接表地址
    uint32_t block_idx;		        // 块索引

    // 判断文件是否是第一次写, 如果是, 先为其分配一个块
    if (file->fd_inode->i_sectors[0] == 0) {
        block_lba = block_bitmap_alloc(cur_part);
        if (block_lba == -1) {
            printk("file_write: block_bitmap_alloc failed\n");
            return -1;
        }
        file->fd_inode->i_sectors[0] = block_lba;

        // 每分配一个块就将位图同步到硬盘
        block_bitmap_idx = block_lba - cur_part->sb->data_start_lba;
        ASSERT(block_bitmap_idx != 0);
        bitmap_sync(cur_part, block_bitmap_idx, BLOCK_BITMAP);
    }
    
    // 写入 count 个字节前, 该文件已经占用的块数
    uint32_t file_has_used_blocks = file->fd_inode->i_size / BLOCK_SIZE + 1;

    // 存储 count 字节后该文件将占用的块数
    uint32_t file_will_use_blocks = (file->fd_inode->i_size + count) / BLOCK_SIZE + 1;
    ASSERT(file_will_use_blocks <= 140);

    // 通过此增量判断是否需要分配扇区, 如增量为 0, 表示原扇区够用
    uint32_t add_blocks = file_will_use_blocks - file_has_used_blocks;

    // 开始将所有块地址收集到 all_blocks (系统中块大小等于扇区大小)
    // 后面都统一在 all_blocks 中获取写入扇区地址
    if (add_blocks == 0) {
        if (file_has_used_blocks <= 12) {
            // 在同一扇区内写入数据, 不涉及到分配新扇区
            block_idx = file_has_used_blocks - 1;
            all_blocks[block_idx] = file->fd_inode->i_sectors[block_idx];
        } else {
            // 未写入新数据之前已经占用了间接块, 需要将间接块地址读进来
            ASSERT(file->fd_inode->i_sectors[12] != 0);
            indirect_block_table = file->fd_inode->i_sectors[12];
            ide_read(cur_part->my_disk, indirect_block_table, all_blocks+12, 1);
        }
    } else {
        // 有增量, 涉及到分配新扇区及是否分配一级间接块表
        // 第一种情况, 12 个直接块够用
        if (file_will_use_blocks <= 12) {
            block_idx = file_has_used_blocks - 1;
            ASSERT(file->fd_inode->i_sectors[block_idx] != 0);
            all_blocks[block_idx] = file->fd_inode->i_sectors[block_idx];

            // 再将未来要用的扇区分配好后写入 all_blocks
            block_idx = file_has_used_blocks; // 指向第一个要分配的新扇区
            while (block_idx < file_will_use_blocks) {
                block_lba = block_bitmap_alloc(cur_part);
                if (block_lba == -1) {
                    printk("file_write: block_bitmap_alloc for situation 1 failed\n");
                    return -1;
                }

                ASSERT(file->fd_inode->i_sectors[block_idx] == 0);
                file->fd_inode->i_sectors[block_idx] = all_blocks[block_idx] = block_lba;

                // 每分配一个块就将位图同步到硬盘
                block_bitmap_idx = block_lba - cur_part->sb->data_start_lba;
                bitmap_sync(cur_part, block_bitmap_idx, BLOCK_BITMAP);

                block_idx++; // 下一个分配的新扇区
            }
        } else if (file_has_used_blocks <= 12 && file_will_use_blocks > 12) {
            // 第二种情况: 旧数据在 12 个直接块内, 新数据将使用间接块
            
            // 先将有剩余空间的可继续用的扇区地址收集到 all_blocks
            block_idx = file_has_used_blocks - 1; // 指向旧数据所在的最后一个扇区
            all_blocks[block_idx] = file->fd_inode->i_sectors[block_idx];

            // 创建一级间接块表
            block_lba = block_bitmap_alloc(cur_part);
            if (block_lba == -1) {
                printk("file_write: block_bitmap_alloc for situation 2 failed\n");
                return -1;
            }

            ASSERT(file->fd_inode->i_sectors[12] == 0);
            // 分配一级间接块索引表
            indirect_block_table = file->fd_inode->i_sectors[12] = block_lba;

            block_idx = file_has_used_blocks;
            while (block_idx < file_will_use_blocks) {
                block_lba = block_bitmap_alloc(cur_part);
                if (block_lba == -1) {
                    printk("file_write: block_bitmap_alloc for situation 2 failed\n");
                    return -1;
                }

                if (block_idx < 12) {
                    ASSERT(file->fd_inode->i_sectors[block_idx] == 0);
                    file->fd_inode->i_sectors[block_idx] = all_blocks[block_idx] = block_lba;
                } else { // 间接块只写入到 all_block 数组中, 待全部分配完成后一次性同步到硬盘
                    all_blocks[block_idx] = block_lba;
                }

                // 每分配一个块就将位图同步到硬盘
                block_bitmap_idx = block_lba - cur_part->sb->data_start_lba;
                bitmap_sync(cur_part, block_bitmap_idx, BLOCK_BITMAP);

                block_idx++; // 下一个扇区
            }
            ide_write(cur_part->my_disk, indirect_block_table, all_blocks+12, 1); // 同步一级间接块表到硬盘
        } else if (file_has_used_blocks > 12) {
            // 第三种情况: 新数据占据间接块
            ASSERT(file->fd_inode->i_sectors[12] != 0);
            indirect_block_table = file->fd_inode->i_sectors[12];

            // 已使用的间接块也将被读入 all_blocks, 无须单独收录
            ide_read(cur_part->my_disk, indirect_block_table, all_blocks+12, 1); // 获取所有间接块地址

            block_idx = file_has_used_blocks;
            while (block_idx < file_will_use_blocks) {
                block_lba = block_bitmap_alloc(cur_part);
                if (block_lba == -1) {
                    printk("file_write: block_bitmap_alloc for situation 3 failed\n");
                    return -1;
                }
                all_blocks[block_idx++] = block_lba;

                // 每分配一个块就将位图同步到硬盘
                block_bitmap_idx = block_lba - cur_part->sb->data_start_lba;
                bitmap_sync(cur_part, block_bitmap_idx, BLOCK_BITMAP);
            }
            ide_write(cur_part->my_disk, indirect_block_table, all_blocks+12, 1); 
        }
    }

    bool first_write_block = true; // 含有剩余空间的扇区标识
    // 块地址已经收集到 all_blocks 中, 下面开始写数据
    file->fd_pos = file->fd_inode->i_size - 1;
    while (bytes_written < count) {
        memset(io_buf, 0, BLOCK_SIZE);
        sec_idx = file->fd_inode->i_size / BLOCK_SIZE;
        sec_lba = all_blocks[sec_idx];
        sec_off_bytes = file->fd_inode->i_size % BLOCK_SIZE;
        sec_left_bytes = BLOCK_SIZE - sec_off_bytes;

        // 判断此次写入硬盘的数据大小
        chunk_size = size_left < sec_left_bytes ? size_left : sec_left_bytes;
        if (first_write_block) {
            ide_read(cur_part->my_disk, sec_lba, io_buf, 1);
            first_write_block = false;
        }
        memcpy(io_buf+sec_off_bytes, src, chunk_size);
        ide_write(cur_part->my_disk, sec_lba, io_buf, 1);
        printk("file write at lba 0x%x\n", sec_lba);

        src += chunk_size; // 将指针推移到下个新数据
        file->fd_inode->i_size += chunk_size; // 更新文件大小
        file->fd_pos += chunk_size;
        bytes_written += chunk_size;
        size_left -= chunk_size;
    }
    inode_sync(cur_part, file->fd_inode, io_buf);
    sys_free(all_blocks);
    sys_free(io_buf);
    return bytes_written;
}

先判断是否是第一次写入文件,如果是,则块地址未分配,为0,则申请块地址,填充到inode信息中去,将位图同步到硬盘中

接着判断写入前后占用块数,也就是写入前后扇区数有没有变化:(这里将数据收集到all_block中,收集完毕之后再进行写入)

  • 如果无,则判断是否使用到了间接块

    • 未使用间接块,则把使用的块记录到all_block中
    • 使用到了间接块,则需要将间接块地址读进来添加到all_block中
  • 如果有,分三种情况

    1. 若已经使用的扇区数在12块之内,新增了若干块后,文件大小还在12块之内,直接分配所需的块并把块地址写入i_sectors数组中即可。

      先将原有地址收录到all_block中,在将未来要用的扇区分配好写入all_block

    2. 若已经使用的块数在12块之内,新增了若干块后,文件大小超过了12块,这种情况下所申请的块除了要写入i_sector 数组,还要创建一级间接块表并写入块地址。

      先将原有地址收录到all_block中,分配一个块作为一级间接索引表,然后在将未来要用的扇区分配好写入all_block

    3. 若已经使用的扇区数超过了12块,这种情况下要在一级间接块表中创建间接块项,间接块项就是在一级间接块表中记录间接块地址的条目。

      直接将间接块索引表读取到all_block中即可,新分配的块地址也直接写入间接索引表

实现 sys_write

fs/fs.c

// 将 buf 中连续 count 个字节写入文件描述符 fd, 成功则返回写入的字节数, 失败返回 -1
int32_t sys_write(int32_t fd, const void* buf, uint32_t count) {
    if (fd < 0) {
        printk("sys_write: fd error\n");
        return -1;
    }
    if (fd == stdout_no) {
        char tmp_buf[1024] = {0};
        memcpy(tmp_buf, buf, count);
        console_put_str(tmp_buf);
        return count;
    }
    uint32_t _fd = fd_local2global(fd);
    struct file* wr_file = &file_table[_fd];
    if (wr_file->fd_flag & O_WRONLY || wr_file->fd_flag & O_RDWR) {
        uint32_t bytes_written = file_write(wr_file, buf, count);
        return bytes_written;
    } else {
        console_put_str("sys_write: not allowed to write file without flag O_RDWR or O_WRONLY\n");
        return -1;
    }
}

这里比较好看懂,如果是输出到标准输出,则用缓冲区存一下要输出的内容,console_put_str直接打印在终端上了,不然就输出到文件里

这里修改了系统调用sys_write,所以系统调用相关文件也需要进行修改

lib/user/syscall.c

/* 打印字符串str */
uint32_t write(int32_t fd, const void* buf, uint32_t count) {
   return _syscall3(SYS_WRITE, fd, buf, count);
}

就是把参数数量变了,记得在syscall.h把声明也改一下

lib/stdio.c

/* 格式化输出字符串format */
uint32_t printf(const char* format, ...) {
   va_list args;
   va_start(args, format);	       // 使args指向format
   char buf[1024] = {0};	       // 用于存储拼接后的字符串
   vsprintf(buf, format, args);
   va_end(args);
   return write(1, buf, strlen(buf)); 
}

这里把write参数给改了

接着把userprog/syscall-init.c中的sys-write给去掉即可

运行 Bochs

kernel/main.c

int main(void) {
    put_str("I am kernel\n");
    init_all();
    intr_enable(); // 打开中断
    process_execute(u_prog_a, "u_prog_a");
    process_execute(u_prog_b, "u_prog_b");
    thread_start("k_thread_a", 31, k_thread_a, "I am thread_a");
    thread_start("k_thread_b", 31, k_thread_b, "I am thread_b");
    uint32_t fd = sys_open("/file1",O_RDWR);
    printf("fd:%d\n",fd);
    sys_write(fd, "hello world\n",12);
    sys_close(fd);
    printf("%d closed now \n",fd);
    while(1);
    return 0;
}

编译,运行:

image-20210225034207856

读取文件

实现 file_read

fs/file.c

// 从文件 file 中读取 count 个字节写入 buf, 返回读出的字节数, 若到文件尾则返回 -1
int32_t file_read(struct file* file, void* buf, uint32_t count) {
    uint8_t* buf_dst = (uint8_t*)buf;
    uint32_t size = count, size_left = size;

    // 若要读取的字节数超过了文件可读的剩余量, 就用剩余量作为待读取的字节数
    if ((file->fd_pos + count) > file->fd_inode->i_size) {
        size = file->fd_inode->i_size - file->fd_pos;
        size_left = size;
        if (size == 0) { // 若到文件尾则返回 -1
            return -1;
        }
    }

    uint8_t* io_buf = sys_malloc(BLOCK_SIZE);
    if (io_buf == NULL) {
        printk("file_read: sys_malloc for io_buf failed\n");
    }
    uint32_t* all_blocks = (uint32_t*)sys_malloc(BLOCK_SIZE+48); // 用来记录文件所有的块地址
    if (all_blocks == NULL) {
        printk("file_read: sys_malloc for all_blocks failed\n");
        return -1;
    }

    uint32_t block_read_start_idx = file->fd_pos / BLOCK_SIZE; // 数据所在块的起始地址
    uint32_t block_read_end_idx = (file->fd_pos + size) / BLOCK_SIZE; // 数据所在块的终止地址
    uint32_t read_blocks = block_read_start_idx - block_read_end_idx; // 如增量为 0, 表示数据在同一扇区
    ASSERT(block_read_start_idx < 139 && block_read_end_idx < 139);

    int32_t indirect_block_table; // 用来获取一级间接表地址
    uint32_t block_idx; // 获取待读的块地址

    // 开始构建 all_blocks 块地址数组, 专门存储用到的块地址
    if (read_blocks == 0) {
        ASSERT(block_read_end_idx == block_read_start_idx);
        if (block_read_end_idx < 12) { // 待读的数据在 12 个直接块之内
            block_idx = block_read_end_idx;
            all_blocks[block_idx] = file->fd_inode->i_sectors[block_idx];
        } else {
            indirect_block_table = file->fd_inode->i_sectors[12];
            ide_read(cur_part->my_disk, indirect_block_table, all_blocks+12, 1);
        }
    } else { // 若要读多个块
        if (block_read_end_idx < 12) { // 数据结束所在的块属于直接块
            // 第一种情况, 起始块和终止块属于直接块
            block_idx = block_read_start_idx;
            while (block_idx <= block_read_end_idx) {
                all_blocks[block_idx] = file->fd_inode->i_sectors[block_idx];
                block_idx++;
            }
        } else if (block_read_start_idx < 12 && block_read_end_idx >= 12) {
            // 第二种情况, 待读入的数据跨越直接块和间接块两类
            // 先将直接块地址写入 all_blocks
            block_idx = block_read_start_idx;
            while (block_idx < 12) {
                all_blocks[block_idx] = file->fd_inode->i_sectors[block_idx];
                block_idx++;
            }
            ASSERT(file->fd_inode->i_sectors[12] != 0); // 确保已经分配了一级间接块表

            // 再将间接块地址写入 all_blocks
            indirect_block_table = file->fd_inode->i_sectors[12];
            ide_read(cur_part->my_disk, indirect_block_table, all_blocks+12, 1); // 将一级间接块表读进来写入到第 13 个块的位置之后
        } else {
            // 第三种情况, 数据在间接块中
            ASSERT(file->fd_inode->i_sectors[12] != 0);
            indirect_block_table = file->fd_inode->i_sectors[12]; // 获取一级间接表地址
            // 将一级间接块表读进来写入到第 13 个块的位置之后
            ide_read(cur_part->my_disk, indirect_block_table, all_blocks+12, 1);
        }
    }

    // 用到的块地址已经收集到 all_blocks 中, 下面开始读数据
    uint32_t sec_idx, sec_lba, sec_off_bytes, sec_left_bytes, chunk_size;
    uint32_t bytes_read = 0;
    while (bytes_read < size) {
        sec_idx = file->fd_pos / BLOCK_SIZE;
        sec_lba = all_blocks[sec_idx];
        sec_off_bytes = file->fd_pos % BLOCK_SIZE;
        sec_left_bytes = BLOCK_SIZE - sec_off_bytes;
        chunk_size = size_left < sec_left_bytes ? size_left : sec_left_bytes; // 待读入的数据大小

        memset(io_buf, 0, BLOCK_SIZE);
        ide_read(cur_part->my_disk, sec_lba, io_buf, 1);
        memcpy(buf_dst, io_buf+sec_off_bytes, chunk_size);

        buf_dst += chunk_size;
        file->fd_pos += chunk_size;
        bytes_read += chunk_size;
        size_left -= chunk_size;
    }
    sys_free(all_blocks);
    sys_free(io_buf);
    return bytes_read;
}

读取文件同写入文件类似,先创建缓冲区,将用到的块地址都收集起来(三种情况),然后再将数据读取到缓冲区中

收集的时候分三种情况与写入的时候一样:

  1. 若起始块和终止块都在 12 块之内,直接读入 i_sectors 数组中即可。
  2. 若起始块在 12 块之内,结束块超过了 12 块,除了要读入 i_sector 数组,还要从一级间接块索引表中读取间接块地址。
  3. 若起始块超过了 12 块,这种情况下要在 级间接块索引表中读取间接块。

实现 sys_read

fs/fs.c

// 从文件描述符 fd 指向的文件中读取 count 个字节到 buf, 若成功则返回读出的字节数, 到文件尾则返回 -1
int32_t sys_read(int32_t fd, void* buf, uint32_t count) {
    if (fd < 0) {
        printk("sys_read: fd error\n");
        return -1;
    }
    ASSERT(buf != NULL);
    uint32_t _fd = fd_local2global(fd);
    return file_read(&file_table[_fd], buf, count);
}

运行 Bochs

kernel/main.c

int main(void) {
    put_str("I am kernel\n");
    init_all();
    intr_enable(); // 打开中断

    uint32_t fd = sys_open("/file1",O_RDWR);
    printf("open /file1 fd:%d\n",fd);

    char buf[64] = { 0 };
    int read_bytes = sys_read(fd,buf,6);
    printf("1_ read %d byte:\n%s\n",read_bytes,buf);
    
    memset(buf,0,64);
    read_bytes = sys_read(fd,buf,12);
    printf("2_ read %d byte:\n%s\n",read_bytes,buf);

    sys_close(fd);
    printf("%d closed now and reopen \n",fd);
    
    fd = sys_open("/file1",O_RDWR);
    memset(buf,0,64);
    read_bytes = sys_read(fd,buf,10);
    printf("3_ read %d byte:\n%s\n",read_bytes,buf);
    
    while(1);
    return 0;
}

编译,运行:

image-20210227190157338

实现文件读写指针定位

实现 sys_lseek

fs/fs.h

// 文件属性结构体
struct stat {
    uint32_t st_ino; // inode 编号
    uint32_t st_size; // 尺寸
    enum file_types st_filetype; // 文件类型
};

fs/fs.c

// 重置用于文件读写操作的偏移指针, 成功时返回新的偏移量, 出错时返回 -1
int32_t sys_lseek(int32_t fd, int32_t offset, uint8_t whence) {
    if (fd < 0) {
        printk("sys_lseek: fd error\n");
        return -1;
    }
    ASSERT(whence > 0 && whence < 4);
    uint32_t _fd = fd_local2global(fd);
    struct file* pf = &file_table[_fd];
    int32_t new_pos = 0; // 新的偏移量必须位于文件大小之内
    int32_t file_size = (int32_t)pf->fd_inode->i_size;
    switch (whence) {
        // SEEK_SET 新的读写位置是相对于文件开头再增加 offset 个位移量
        case SEEK_SET:
            new_pos = offset;
            break;
        // SEEK_CUR 新的读写位置是相对于当前的位置增加 offset 个位移量
        case SEEK_CUR:
            new_pos = (int32_t)pf->fd_pos + offset;
            break;
        // SEEK_END 新的读写位置是相对于文件尺寸再增加 offset 个位移量
        case SEEK_END:
            new_pos = file_size + offset;
    }
    if (new_pos < 0 || new_pos > (file_size - 1)) {
        return -1;
    }
    pf->fd_pos = new_pos;
    return pf->fd_pos;
}

通过参照物和偏移量移动指向文件的位置,原理很简单

运行 Bochs

kernel/main.c

int main(void) {
    put_str("I am kernel\n");
    init_all();
    intr_enable(); // 打开中断

    uint32_t fd = sys_open("/file1",O_RDWR);
    printf("open /file1 fd:%d\n",fd);

    char buf[64] = { 0 };
    int read_bytes = sys_read(fd,buf,6);
    printf("1_ read %d byte:\n%s\n",read_bytes,buf);
    
    memset(buf,0,64);
    sys_lseek(fd,0,SEEK_SET);
    read_bytes = sys_read(fd,buf,12);
    printf("2_ read %d byte:\n%s\n",read_bytes,buf);

    sys_close(fd);
    printf("%d closed now \n",fd);
    while(1);
    return 0;
}

运行,编译:

image-20210227192137292

之前这里第二次读取,读出来的是两个hello world中间的部分,即world\nhello

这里通过sys_lseek将指针重置到文件头,重新读取,即是hello world

实现文件删除功能

文件删除是文件创建的逆过程

涉及inode,inode位图,目录inode的size,目录项,数据块,数据块位图的回收

回收 inode

inode是文件系统的灵魂,删除文件最重要的就是回收文件对应的inode了,与inode相关资源有:

  1. inode 位图
  2. inode_table 文件表
  3. inode中的数据块
  4. 一级间接索引表本身扇区地址

fs/inode.c

// 将硬盘分区 part 上的 inode 清空
void inode_delete(struct partition* part, uint32_t inode_no, void* io_buf) {
    ASSERT(inode_no < 4096);
    struct inode_position inode_pos;
    inode_locate(part, inode_no, &inode_pos); // inode 位置信息会存入 inode_pos
    ASSERT(inode_pos.sec_lba <= (part->start_lba + part->sec_cnt));

    char* inode_buf = (char*)io_buf;
    if (inode_pos.two_sec) { // inode 跨扇区, 读入 2 个扇区
        // 将原硬盘上的内容先读出来
        ide_read(part->my_disk, inode_pos.sec_lba, inode_buf, 2);
        // 将 inode_buf 清 0
        memset((inode_buf + inode_pos.off_size), 0, sizeof(struct inode));
        // 用清 0 的内存数据覆盖磁盘
        ide_write(part->my_disk, inode_pos.sec_lba, inode_buf, 2);
    } else { // 未跨扇区, 只读入 1 个扇区就好
        // 将原硬盘上的内容先读出来
        ide_read(part->my_disk, inode_pos.sec_lba, inode_buf, 1);
        // 将 inode_buf 清 0
        memset((inode_buf + inode_pos.off_size), 0, sizeof(struct inode));
        // 用清 0 的内存数据覆盖磁盘
        ide_write(part->my_disk, inode_pos.sec_lba, inode_buf, 1);
    }
}

// 回收 inode 的数据块和 inode 本身
void inode_release(struct partition* part, uint32_t inode_no) {
    struct inode* inode_to_del = inode_open(part, inode_no);
    ASSERT(inode_to_del->i_no == inode_no);

// 1 回收 inode 占用的所有块
    uint8_t block_idx = 0, block_cnt = 12;
    uint32_t block_bitmap_idx;
    uint32_t all_blocks[140] = {0}; // 12 个直接块 + 128 个间接块

    // a 先将前 12 个直接块存入 all_blocks
    while (block_idx < 12) {
        all_blocks[block_idx] = inode_to_del->i_sectors[block_idx];
        block_idx++;
    }

    // b 如果一级间接块表存在, 将其 128 个间接块读到 all_blocks[12~], 并释放一级间接块表所占的扇区
    if (inode_to_del->i_sectors[12] != 0) {
        ide_read(part->my_disk, inode_to_del->i_sectors[12], all_blocks+12, 1);
        block_cnt = 140;

        // 回收一级间接块表占用的扇区
        block_bitmap_idx = inode_to_del->i_sectors[12] - part->sb->data_start_lba;
        ASSERT(block_bitmap_idx > 0);
        bitmap_set(&part->block_bitmap, block_bitmap_idx, 0);
        bitmap_sync(cur_part, block_bitmap_idx, BLOCK_BITMAP);
    }

    // c inode 所有的块地址已经收集到 all_blocks 中, 下面逐个回收
    block_idx = 0;
    while (block_idx < block_cnt) {
        if (all_blocks[block_idx] != 0) {
            block_bitmap_idx = 0;
            block_bitmap_idx = all_blocks[block_idx] - part->sb->data_start_lba;
            ASSERT(block_bitmap_idx > 0);
            bitmap_set(&part->block_bitmap, block_bitmap_idx, 0);
            bitmap_sync(cur_part, block_bitmap_idx, BLOCK_BITMAP);
        }
        block_idx++;
    }

// 2 回收该 inode 所占用的 inode
    bitmap_set(&part->inode_bitmap, inode_no, 0);
    bitmap_sync(cur_part, inode_no, INODE_BITMAP);

    void* io_buf = sys_malloc(1024);
    inode_delete(part, inode_no, io_buf);
    sys_free(io_buf);

    inode_close(inode_to_del);
}

删除目录项

与删除目录项相关的工作:

  1. 在文件所在的目录中擦除该文件的目录项,使其为 0。
  2. 根目录是必须存在的, 它是文件读写的根基, 不应该被清空,它至少要保留1个块。 如果目录项独占1个块, 并且该块不是根目录最后一个块的话, 将其回收。
  3. 目录inode的i_size是目录项大小的总和, 因此还要将i_size减去一个目录项的单位大小。
  4. 目录inode改变后, 要同步到硬盘。

fs/dir.h

// 把分区 part 目录 pdir 中编号为 inode_no 的目录项删除
bool delete_dir_entry(struct partition* part, struct dir* pdir, uint32_t inode_no, void* io_buf) {
    struct inode* dir_inode = pdir->inode;
    uint32_t block_idx = 0, all_blocks[140] = {0};
    // 收集目录全部块地址
    while (block_idx < 12) {
        all_blocks[block_idx] = dir_inode->i_sectors[block_idx];
        block_idx++;
    }
    if (dir_inode->i_sectors[12]) {
        ide_read(part->my_disk, dir_inode->i_sectors[12], all_blocks+12, 1);
    }

    // 目录项在存储时保证不会跨扇区
    uint32_t dir_entry_size = part->sb->dir_entry_size;
    uint32_t dir_entrys_per_sec = (SECTOR_SIZE / dir_entry_size); // 每扇区最大的目录项数目
    struct dir_entry* dir_e = (struct dir_entry*)io_buf;
    struct dir_entry* dir_entry_found = NULL;
    uint8_t dir_entry_idx, dir_entry_cnt;
    bool is_dir_first_block = false; // 目录的第 1 个块

    // 遍历所有块, 寻找目录项
    block_idx = 0;
    while (block_idx < 140) {
        is_dir_first_block = false;
        if (all_blocks[block_idx] == 0) {
            block_idx++;
            continue;
        }
        dir_entry_idx = dir_entry_cnt = 0;
        memset(io_buf, 0, SECTOR_SIZE);
        // 读取扇区, 获得目录项
        ide_read(part->my_disk, all_blocks[block_idx], io_buf, 1);

        // 遍历所有的目录项, 统计该扇区的目录项数量及是否有待删除的目录项
        while (dir_entry_idx < dir_entrys_per_sec) {
            if ((dir_e + dir_entry_idx)->f_type != FT_UNKNOWN) {
                if (!strcmp((dir_e + dir_entry_idx)->filename, ".")) {
                    is_dir_first_block = true;
                } else if (strcmp((dir_e + dir_entry_idx)->filename, ".") && 
                           strcmp((dir_e + dir_entry_idx)->filename, "..")) {
                    dir_entry_cnt++; // 统计此扇区内的目录项个数, 用来判断删除目录项后是否回收该扇区
                    if ((dir_e + dir_entry_idx)->i_no == inode_no) {
                        ASSERT(dir_entry_found == NULL);
                        dir_entry_found = dir_e + dir_entry_idx;
                        // 找到后也继续遍历, 统计总共的目录项数
                    }
                }
            }
            dir_entry_idx++;
        }
        // 若此扇区未找到该目录项, 继续在下个扇区中找
        if (dir_entry_found == NULL) {
            block_idx++;
            continue;
        }

        // 在此扇区中找到目录项后, 清除该目录项并判断是否回收扇区, 随后退出循环直接返回
        ASSERT(dir_entry_cnt >= 1);
        // 除目录第 1 个扇区外, 若该扇区上只有该目录项自己, 则将整个扇区回收
        if (dir_entry_cnt == 1 && !is_dir_first_block) {
            // a 在块位图中回收该块
            uint32_t block_bitmap_idx = all_blocks[block_idx] - part->sb->data_start_lba;
            bitmap_set(&part->block_bitmap, block_bitmap_idx, 0);
            bitmap_sync(cur_part, block_bitmap_idx, BLOCK_BITMAP);

            // b 将块地址从数组 i_sectors 或索引表中去掉
            if (block_idx < 12) {
                dir_inode->i_sectors[block_idx] = 0;
            } else { // 在一级间接索引表中擦除该间接块地址
                // 先判断一级间接索引表中间接块的数量, 如果仅有这 1 个间接块, 连同间接索引表所在的块一同回收
                uint32_t indirect_blocks = 0;
                uint32_t indirect_block_idx = 12;
                while (indirect_block_idx < 140) {
                    if (all_blocks[indirect_block_idx] != 0) {
                        indirect_blocks++;
                    }
                }
                ASSERT(indirect_blocks >= 1);

                // 间接索引表中还包括其它间接块, 仅在索引表中擦除当前这个间接块地址
                if (indirect_blocks > 1) {
                    all_blocks[block_idx] = 0;
                    ide_write(part->my_disk, dir_inode->i_sectors[12], all_blocks+12, 1);
                } else { // 间接索引表中就当前这 1 个间接块, 直接把间接块索引表所在的块回收, 然后擦除间接索引表块地址
                    // 回收间接索引表所在的块
                    block_bitmap_idx = dir_inode->i_sectors[12] - part->sb->data_start_lba;
                    bitmap_set(&part->block_bitmap, block_bitmap_idx, 0);
                    bitmap_sync(cur_part, block_bitmap_idx, BLOCK_BITMAP);

                    // 将间接索引表地址清 0
                    dir_inode->i_sectors[12] = 0;
                }
            }
        } else { // 仅将该目录项清空
            memset(dir_entry_found, 0, dir_entry_size);
            ide_write(part->my_disk, all_blocks[block_idx], io_buf, 1);
        }

        // 更新 inode 信息并同步到硬盘
        ASSERT(dir_inode->i_size >= dir_entry_size);
        dir_inode->i_size -= dir_entry_size;
        memset(io_buf, 0, SECTOR_SIZE*2);
        inode_sync(part, dir_inode, io_buf);

        return true;
    }
    // 所有块中未找到则返回 false, 若出现这种情况应该是 search_file 出错了
    return false;
}

fs/fs.c

// 删除文件(非目录), 成功返回 0, 失败返回 -1
int32_t sys_unlink(const char* pathname) {
    ASSERT(strlen(pathname) < MAX_PATH_LEN);

    // 先检查待删除的文件是否存在
    struct path_search_record searched_record;
    memset(&searched_record, 0, sizeof(struct path_search_record));
    int inode_no = search_file(pathname, &searched_record);
    ASSERT(inode_no != 0);
    if (inode_no == -1) {
        printk("file %s not found!\n", pathname);
        dir_close(searched_record.parent_dir);
        return -1;
    }
    if (searched_record.file_type == FT_DIRECTORY) {
        printk("can`t delete a directory with unlink(), use rmdir() to instead\n");
        dir_close(searched_record.parent_dir);
        return -1;
    }
    // 检查是否在已打开文件列表(文件表)中
    uint32_t file_idx = 0;
    while (file_idx < MAX_FILE_OPEN) {
        if (file_table[file_idx].fd_inode != NULL && (uint32_t)inode_no == file_table[file_idx].fd_inode->i_no) {
            break;
        }
        file_idx++;
    }
    if (file_idx < MAX_FILE_OPEN) {
        dir_close(searched_record.parent_dir);
        printk("file %s is in use, not allow to delete!\n", pathname);
        return -1;
    }
    ASSERT(file_idx == MAX_FILE_OPEN);

    // 为 delete_dir_entry 申请缓冲区
    void* io_buf = sys_malloc(SECTOR_SIZE+SECTOR_SIZE);
    if (io_buf == NULL) {
        dir_close(searched_record.parent_dir);
        printk("sys_unlink: malloc for io_buf failed\n");
        return -1;
    }

    struct dir* parent_dir = searched_record.parent_dir;
    delete_dir_entry(cur_part, parent_dir, inode_no, io_buf);
    inode_release(cur_part, inode_no);
    sys_free(io_buf);
    dir_close(searched_record.parent_dir);
    return 0; // 成功删除文件
}

运行 Bochs

kernel/main.c

int main(void) {
    put_str("I am kernel\n");
    init_all();
    intr_enable(); // 打开中断
    process_execute(u_prog_a, "u_prog_a");
    process_execute(u_prog_b, "u_prog_b");
    thread_start("k_thread_a", 31, k_thread_a, "I am thread_a");
    thread_start("k_thread_b", 31, k_thread_b, "I am thread_b");

    printf("/file1 delete %s!\n",sys_unlink("/file1") == 0?"done":"fail");
    
    while(1);
    return 0;
}

image-20210227194230011

再一次运行,就会显示fail!

创建目录

实现 sys_mkdir 创建目录

创建目录的工作是:

  1. 确认待创建的新目录在文件系统上不存在。
  2. 为新目录创建inode。
  3. 为新目录分配 1 个块存储该目录中的目录项。
  4. 在新目录中创建两个目录项 “.“和 “ ..“, 这是每个目录都必须存在的两个目录项。
  5. 在新目录的父目录中添加新目录的目录项。
  6. 将以上资源的变更同步到硬盘。

fs/fs.c

// 创建目录 pathname, 成功返回 0, 失败返回 -1
int32_t sys_mkdir(const char* pathname) {
    uint8_t rollback_step = 0; // 用于操作失败时回滚各资源状态
    void* io_buf = sys_malloc(SECTOR_SIZE*2);
    if (io_buf == NULL) {
        printk("sys_mkdir: sys_malloc for io_buf failed\n");
        return -1;
    }

    struct path_search_record searched_record;
    memset(&searched_record, 0, sizeof(struct path_search_record));
    int inode_no = -1;
    inode_no = search_file(pathname, &searched_record);
    if (inode_no != -1) { // 如果找到了同名目录或文件, 失败返回
        printk("sys_mkdir: file or directory %s exist!\n", pathname);
        rollback_step = 1;
        goto rollback;
    } else { // 若未找到, 也要判断是在最终目录没找到还是某个中间目录不存在
        uint32_t pathname_depth = path_depth_cnt((char*)pathname);
        uint32_t path_searched_depth = path_depth_cnt(searched_record.searched_path);
        // 先判断是否把 pathname 的各层目录都访问到了, 即是否在某个中间目录就失败了
        if (pathname_depth != path_searched_depth) { // 说明并没有访问到全部的路径, 某个中间目录是不存在的
            printk("sys_mkdir: can`t access %s, subpath %s is not exist\n", pathname, searched_record.searched_path);
            rollback_step = 1;
            goto rollback;
        }
    }

    struct dir* parent_dir = searched_record.parent_dir;
    // 目录名称后可能会有字符 '/', 所以最好直接用 searched_record.searched_path, 无 '/'
    char* dirname = strrchr(searched_record.searched_path, '/') + 1;

    inode_no = inode_bitmap_alloc(cur_part);
    if (inode_no == -1) {
        printk("sys_mkdir: allocate inode failed\n");
        rollback_step = 1;
        goto rollback;
    }

    struct inode new_dir_inode;
    inode_init(inode_no, &new_dir_inode); // 初始化 inode

    uint32_t block_bitmap_idx = 0; // 用来记录 block 对应于 block_bitmap 中的索引
    int32_t block_lba = -1;
    // 为目录分配一个块, 用来写入目录 . 和 ..
    block_lba = block_bitmap_alloc(cur_part);
    if (block_lba == -1) {
        printk("sys_mkdir: block_bitmap_alloc for create directory failed\n");
        rollback_step = 2;
        goto rollback;
    }
    new_dir_inode.i_sectors[0] = block_lba;
    // 每分配一个块就将位图同步到硬盘
    block_bitmap_idx = block_lba - cur_part->sb->data_start_lba;
    ASSERT(block_bitmap_idx != 0);
    bitmap_sync(cur_part, block_bitmap_idx, BLOCK_BITMAP);

    // 将当前目录的目录项 '.' 和 '..' 写入目录
    memset(io_buf, 0, SECTOR_SIZE*2); // 清空 io_buf
    struct dir_entry* p_de = (struct dir_entry*)io_buf;

    // 初始化当前目录 '.'
    memcpy(p_de->filename, ".", 1);
    p_de->i_no = inode_no;
    p_de->f_type = FT_DIRECTORY;

    p_de++;
    // 初始化当前目录 '..'
    memcpy(p_de->filename, "..", 2);
    p_de->i_no = parent_dir->inode->i_no;
    p_de->f_type = FT_DIRECTORY;
    ide_write(cur_part->my_disk, new_dir_inode.i_sectors[0], io_buf, 1);

    new_dir_inode.i_size = 2 * cur_part->sb->dir_entry_size;

    // 在父目录中添加自己的目录项
    struct dir_entry new_dir_entry;
    memset(&new_dir_entry, 0, sizeof(struct dir_entry));
    create_dir_entry(dirname, inode_no, FT_DIRECTORY, &new_dir_entry);
    memset(io_buf, 0, SECTOR_SIZE*2); // 清空 io_buf
    if (!sync_dir_entry(parent_dir, &new_dir_entry, io_buf)) {
        printk("sys_mkdir: sync_dir_entry to disk failed\n");
        rollback_step = 2;
        goto rollback;
    }

    // 父目录的 inode 同步到硬盘
    memset(io_buf, 0, SECTOR_SIZE*2);
    inode_sync(cur_part, parent_dir->inode, io_buf);
    
    // 将新创建目录的 inode 同步到硬盘
    memset(io_buf, 0, SECTOR_SIZE*2);
    inode_sync(cur_part, &new_dir_inode, io_buf);

    // 将 inode 位图同步到硬盘
    bitmap_sync(cur_part, inode_no, INODE_BITMAP);

    sys_free(io_buf);

    // 关闭所创建目录的父目录
    dir_close(searched_record.parent_dir);
    return 0;

// 创建文件或目录需要创建相关的多个资源, 若某步失败则会执行到下面的回滚步骤
rollback:
    switch (rollback_step) {
        case 2:
            bitmap_set(&cur_part->inode_bitmap, inode_no, 0);
        case 1:
            dir_close(searched_record.parent_dir);
            break;
    }
    sys_free(io_buf);
    return -1;
}

和创建文件有点相似

运行 Bochs 功能验证

kernel/main.c

int main(void) {
   put_str("I am kernel\n");
   init_all();
   intr_enable(); // 打开中断
   process_execute(u_prog_a, "u_prog_a");
   process_execute(u_prog_b, "u_prog_b");
   thread_start("k_thread_a", 31, k_thread_a, "I am thread_a");
   thread_start("k_thread_b", 31, k_thread_b, "I am thread_b");

   printf("/dirl/subdirl create %s!\n",sys_mkdir("/dirl/subdirl") == 0 ? "done" : "fail"); 
   printf("/dirl create %s!\n", sys_mkdir("/dirl") == 0? "done" : "fail"); 
   printf ("now, /dirl/subdirl create %s ! \n", sys_mkdir("/dirl/subdirl") == 0 ? "done" : "fail"); 
   int fd = sys_open ("/dirl/subdirl/file2", O_CREAT | O_RDWR); 
   if (fd != -1) { 
      printf("/dirl/subdirl/file2 create done!\n"); 
      sys_write(fd, "Catch me if you can!\n", 21); 
      sys_lseek (fd, 0, SEEK_SET); 
      char buf [32] = {0}; 
      sys_read(fd, buf, 21); 
      printf ("/dirl'/subdirl/file2 says: \n%s", buf); 
      sys_close (fd); 
}
    while(1);
    return 0;
}

编译,运行:

image-20210227195651101

遍历目录

打开和关闭目录

fs/fs.c

// 目录打开成功后返回目录指针, 失败返回 NULL
struct dir* sys_opendir(const char* name) {
    ASSERT(strlen(name) < MAX_PATH_LEN);
    // 如果是根目录 '/', 直接返回 &root_dir
    if (name[0] == '/' && (name[1] == 0 || name[0] == '.')) {
        return &root_dir;
    }

    // 先检查待打开的目录是否存在
    struct path_search_record searched_record;
    memset(&searched_record, 0, sizeof(struct path_search_record));
    int inode_no = search_file(name, &searched_record);
    struct dir* ret = NULL;
    if (inode_no == -1) { // 找不到目录
        printk("In %s, sub path %s not exist\n", name, searched_record.searched_path);
    } else {
        if (searched_record.file_type == FT_REGULAR) {
            printk("%s is regular file!\n", name);
        } else if (searched_record.file_type == FT_DIRECTORY) {
            ret = dir_open(cur_part, inode_no);
        }
    }
    dir_close(searched_record.parent_dir);
    return ret;
}

// 成功关闭目录 dir 返回 0, 失败返回 -1
int32_t sys_closedir(struct dir* dir) {
    int32_t ret = -1;
    if (dir != NULL) {
        dir_close(dir);
        ret = 0;
    }
    return ret;
}

读取1个目录项

fs/dir.c

// 读取目录, 成功返回 1 个目录项, 失败返回 NULL
struct dir_entry* dir_read(struct dir* dir) {
    struct dir_entry* dir_e = (struct dir_entry*)dir->dir_buf;
    struct inode* dir_inode = dir->inode;
    uint32_t all_blocks[140] = {0}, block_cnt = 12;
    uint32_t block_idx = 0, dir_entry_idx = 0;
    while (block_idx < 12) {
        all_blocks[block_idx] = dir_inode->i_sectors[block_idx];
        block_idx++;
    }
    if (dir_inode->i_sectors[12] != 0) { // 若含有一级间接块表
        ide_read(cur_part->my_disk, dir_inode->i_sectors[12], all_blocks+12, 1);
        block_cnt = 140;
    }
    block_idx = 0;

    uint32_t cur_dir_entry_pos = 0; // 当前目录项的偏移, 此项用来判断是否是之前已经返回过的目录项
    uint32_t dir_entry_size = cur_part->sb->dir_entry_size;
    uint32_t dir_entrys_per_sec = SECTOR_SIZE / dir_entry_size; // 1 扇区内可容纳的目录项个数
    // 因为此目录内可能删除了某些文件或子目录, 所以要遍历所有块
    while (block_idx < block_cnt) {
        if (dir->dir_pos >= dir_inode->i_size) {
            return NULL;
        }
        if (all_blocks[block_idx] == 0) {
            block_idx++;
            continue;
        }
        memset(dir_e, 0, SECTOR_SIZE);
        ide_read(cur_part->my_disk, all_blocks[block_idx], dir_e, 1);
        dir_entry_idx = 0;
        // 遍历扇区内所有目录项
        while (dir_entry_idx < dir_entrys_per_sec) {
            if ((dir_e + dir_entry_idx)->f_type) { // f_type != FT_UNKNOWN
                // 判断是不是最新的目录项, 避免返回曾经已经返回过的目录项
                if (cur_dir_entry_pos < dir->dir_pos) {
                    cur_dir_entry_pos += dir_entry_size;
                    dir_entry_idx++;
                    continue;
                }
                ASSERT(cur_dir_entry_pos == dir->dir_pos);
                dir->dir_pos += dir_entry_size; // 更新为新位置, 即下一个返回的目录项地址
                return dir_e + dir_entry_idx;
            }
            dir_entry_idx++;
        }
        block_idx++;
    }
    return NULL;
}

实现 sys_readdir 和 sys_rewinddir

fs/fs.c

// 读取目录 dir 的 1 个目录项, 成功后返回其目录项地址, 到目录尾时或出错时返回 NULL
struct dir_entry* sys_readdir(struct dir* dir) {
    ASSERT(dir != NULL);
    return dir_read(dir);
}

// 把目录 dir 的指针 dir_pos 置 0
void sys_rewinddir(struct dir* dir) {
    dir->dir_pos = 0;
}

运行 Bochs 功能验证

kernel/main.c

int main(void) {
   put_str("I am kernel\n");
   init_all();
   intr_enable(); // 打开中断

   struct dir* p_dir = sys_opendir("/dirl/subdirl"); 
   if (p_dir) { 
      printf("/dirl/subdirl open done!\ncontent:\n"); 
      char* type = NULL; 
      struct dir_entry* dir_e = NULL; 
      while ((dir_e = sys_readdir(p_dir))) { 
         if (dir_e->f_type == FT_REGULAR) {
           type = "regular";
         }else{
            type = "directory";
         }
         printf(" %s %s\n",type,dir_e->filename);
      }
      if (sys_closedir (p_dir) == 0) {
         printf("/dirl/subdirl close done!\n"); 
      } else { 
         printf("/dirl/subdirl close fail!\n"); 
      }
   }else{   
      printf("/dirl/subdirl open fail!\n"); 
   }
   while(1);
   return 0;
}

编译,运行:

image-20210227201538961

删除目录

删除目录与判断空目录

在 Linux 中删除文件的时候,会提示目录非空,需要删除目录下的文件才能删除目录,删除非空目录实际上是采用递归的方式rm -r

fs/dir.c

// 判断目录是否为空
bool dir_is_empty(struct dir* dir) {
    struct inode* dir_inode = dir->inode;
    // 若目录下只有 . 和 .. 这两个目录项则目录为空
    return (dir_inode->i_size == cur_part->sb->dir_entry_size * 2);
}

// 在父目录 parent_dir 中删除 child_dir
int32_t dir_remove(struct dir* parent_dir, struct dir* child_dir) {
    struct inode* child_dir_inode = child_dir->inode;
    // 空目录只在 inode->i_sectors[0] 中有扇区, 其它扇区都应该为空
    int32_t block_idx = 1;
    while (block_idx < 13) {
        ASSERT(child_dir_inode->i_sectors[block_idx] == 0);
        block_idx++;
    }
    void* io_buf = sys_malloc(SECTOR_SIZE*2);
    if (io_buf == NULL) {
        printk("dir_remove: malloc for io_buf failed\n");
        return -1;
    }

    // 在父目录 parent_dir 中删除子目录 child_dir 对应的目录项
    delete_dir_entry(cur_part, parent_dir, child_dir_inode->i_no, io_buf);

    // 回收 inode 中 i_sectors 中所占用的扇区, 并同步 inode_bitmap 和 block_bitmap
    inode_release(cur_part, child_dir_inode->i_no);
    sys_free(io_buf);
    return 0;
}

实现 sys_rmdir

fs/fs.c

// 删除空目录, 成功时返回 0, 失败时返回 -1
int32_t sys_rmdir(const char* pathname) {
    // 先检查待删除的文件是否存在
    struct path_search_record searched_record;
    memset(&searched_record, 0, sizeof(struct path_search_record));
    int inode_no = search_file(pathname, &searched_record);
    ASSERT(inode_no != 0);
    int retval = -1; // 默认返回值
    if (inode_no == -1) {
        printk("In %s, sub path %s not exist\n", pathname, searched_record.searched_path);
    } else {
        if (searched_record.file_type == FT_REGULAR) {
            printk("%s is regular file!\n", pathname);
        } else {
            struct dir* dir = dir_open(cur_part, inode_no);
            if (!dir_is_empty(dir)) { // 非空目录不可删除
                printk("dir %s is not empty, it is not allowed to delete a nonempty directory!\n", pathname);
            } else {
                if (!dir_remove(searched_record.parent_dir, dir)) {
                    retval = 0;
                }
            }
            dir_close(dir);
        }
    }
    dir_close(searched_record.parent_dir);
    return retval;
}

任务的工作目录

显示当前工作目录的原理和基础代码

每个目录中都有..目录项,这个目录项表示父目录,那就进入父目录找有没有..,这样依次找下去,最终找到根目录,这个过程中获取到的每个目录名加起来就是当前工作目录

fs/fs.c

// 获得父目录的 inode 编号
static uint32_t get_parent_dir_inode_nr(uint32_t child_inode_nr, void* io_buf) {
    struct inode* child_dir_inode = inode_open(cur_part, child_inode_nr);
    // 目录中的目录项 ".." 中包括父目录 inode 编号, ".." 位于目录的第 0 块
    uint32_t block_lba = child_dir_inode->i_sectors[0];
    ASSERT(block_lba >= cur_part->sb->data_start_lba);
    inode_close(child_dir_inode);
    ide_read(cur_part->my_disk, block_lba, io_buf, 1);
    struct dir_entry* dir_e = (struct dir_entry*)io_buf;
    // 第 0 个目录项是 ".", 第 1 个目录项是 ".."
    ASSERT(dir_e[1].i_no < 4096 && dir_e[1].f_type == FT_DIRECTORY);
    return dir_e[1].i_no; // 返回父目录的 inode 编号
}

// 在 inode 编号为 p_inode_nr 的目录中查找 indoe 编号为 c_inode_nr 的子目录的名字
// 将名字存入缓冲区 path
// 成功返回 0, 失败返回 -1
static int get_child_dir_name(uint32_t p_inode_nr, uint32_t c_inode_nr, char* path, void* io_buf) {
    struct inode* parent_dir_inode = inode_open(cur_part, p_inode_nr);
    // 填充 all_blocks, 将该目录的所占扇区地址全部写入 all_blocks
    uint8_t block_idx = 0;
    uint32_t all_blocks[140] = {0}, block_cnt = 12;
    while (block_idx < 12) {
        all_blocks[block_idx] = parent_dir_inode->i_sectors[block_idx];
        block_idx++;
    }
    if (parent_dir_inode->i_sectors[12]) {
        ide_read(cur_part->my_disk, parent_dir_inode->i_sectors[12], all_blocks+12, 1);
        block_cnt = 140;
    }
    inode_close(parent_dir_inode);

    struct dir_entry* dir_e = (struct dir_entry*)io_buf;
    uint32_t dir_entry_size = cur_part->sb->dir_entry_size;
    uint32_t dir_entrys_per_sec = (512 / dir_entry_size);
    block_idx = 0;
    // 遍历所有块
    while (block_idx < block_cnt) {
        if (all_blocks[block_idx]) {
            ide_read(cur_part->my_disk, all_blocks[block_idx], io_buf, 1);
            uint8_t dir_e_idx = 0;
            // 遍历每个目录项
            while (dir_e_idx < dir_entrys_per_sec) {
                if ((dir_e + dir_e_idx)->i_no == c_inode_nr) {
                    strcat(path, "/");
                    strcat(path, (dir_e+dir_e_idx)->filename);
                    return 0;
                }
                dir_e_idx++;
            }
        }
        block_idx++;
    }
    return -1;
}

实现 sys_getcwd

为了实现当前工作目录,需要在进程PCB中添加成员来记录当前工作目录

thread/thread.h

    uint32_t cwd_inode_nr;          // 进程所在工作目录的inode编号
    uint32_t stack_magic;           // 栈的边界标记, 用于检测栈的溢出

thread/thread.c

    pthread->cwd_inode_nr = 0;
    pthread->stack_magic = 0x19870916; // 自定义魔数

fs/fs.c

// 把当前工作目录绝对路径写入 buf, size 是 buf 的大小
// 当 buf 为 NULL 时, 由操作系统分配存储工作路径的空间并返回地址
// 失败则返回 NULL
char* sys_getcwd(char* buf, uint32_t size) {
    ASSERT(buf != NULL);
    void* io_buf = sys_malloc(SECTOR_SIZE);
    if (io_buf == NULL) {
        return NULL;
    }

    struct task_struct* cur_thread = running_thread();
    int32_t parent_inode_nr = 0;
    int32_t child_inode_nr = cur_thread->cwd_inode_nr;
    ASSERT(child_inode_nr >= 0 && child_inode_nr < 4096); // 最大支持 4096 个 inode
    // 若当前目录是根目录, 直接返回 '/'
    if (child_inode_nr == 0) {
        buf[0] = '/';
        buf[1] = 0;
        return buf;
    }

    memset(buf, 0, size);
    char full_path_reverse[MAX_PATH_LEN] = {0}; // 用来做全路径缓冲区

    // 从下往上逐层找父目录, 直到找到根目录为止
    // 当 child_inode_nr 为 0 时停止
    while (child_inode_nr) {
        parent_inode_nr = get_parent_dir_inode_nr(child_inode_nr, io_buf);
        if (get_child_dir_name(parent_inode_nr, child_inode_nr, full_path_reverse, io_buf) == -1) {
            sys_free(io_buf);
            return NULL;
        }
        child_inode_nr = parent_inode_nr;
    }
    ASSERT(strlen(full_path_reverse) <= size);
    // 至此 full_path_reverse 中的路径是反着的
    char* last_slash; // 字符串中最后一个斜杆地址
    while ((last_slash = strrchr(full_path_reverse, '/'))) {
        uint16_t len =strlen(buf);
        strcpy(buf+len, last_slash);
        // 在 full_path_reverse 中添加结束字符, 作为下一次执行 strcpy 中 last_slash 的边界
        *last_slash = 0;
    }
    sys_free(io_buf);
    return buf;
}

实现 sys_chdir 改变工作目录

fs/fs.c

// 更改当前工作目录为绝对路径 path, 成功则返回 0, 失败返回 -1
int32_t sys_chdir(const char* path) {
    int32_t ret = -1;
    struct path_search_record searched_record;
    memset(&searched_record, 0, sizeof(struct path_search_record));
    int inode_no = search_file(path, &searched_record);
    if (inode_no != -1) {
        if (searched_record.file_type == FT_DIRECTORY) {
            running_thread()->cwd_inode_nr = inode_no;
            ret = 0;
        } else {
            printk("sys_chdir: %s is regular file or other!\n", path);
        }
    }
    dir_close(searched_record.parent_dir);
    return ret;
}

运行 Bochs 功能验证

kernel/main.c

int main(void) {
   put_str("I am kernel\n");
   init_all();
   intr_enable(); // 打开中断

   char cwd_buf[32]={0};
   sys_getcwd(cwd_buf, 32);
   printf("cwd:%s\n",cwd_buf);
   sys_chdir("/dirl");
   printf("change cwd now\n");
   sys_getcwd(cwd_buf, 32);
   printf("cwd:%s\n",cwd_buf);
   while(1);
   return 0;
}

运行,编译:

image-20210227224850801

获取文件属性

ls命令查看文件属性的时候,会大量调用系统调用stat64,这是64位版本的函数,这里实现一个建议的stat

实现sys_stat

fs/fs.h

// 文件属性结构体
struct stat {
    uint32_t st_ino; // inode 编号
    uint32_t st_size; // 尺寸
    enum file_types st_filetype; // 文件类型
};

fs/fs.c

// 在 buf 中填充文件结构相关信息, 成功时返回 0, 失败返回 -1
int32_t sys_stat(const char* path, struct stat* buf) {
    // 若直接查看根目录 '/'
    if (!strcmp(path, "/") || !strcmp(path, "/.") || !strcmp(path, "/..")) {
        buf->st_filetype = FT_DIRECTORY;
        buf->st_ino = 0;
        buf->st_size = root_dir.inode->i_size;
        return 0;
    }

    int32_t ret = -1; // 默认返回值
    struct path_search_record searched_record;
    memset(&searched_record, 0, sizeof(struct path_search_record));
    int inode_no = search_file(path, &searched_record);
    if (inode_no != -1) {
        struct inode* obj_inode = inode_open(cur_part, inode_no);
        buf->st_size = obj_inode->i_size;
        inode_close(obj_inode);
        buf->st_filetype = searched_record.file_type;
        buf->st_ino = inode_no;
        ret = 0;
    } else {
        printk("sys_stat: %s not found\n", path);
    }
    dir_close(searched_record.parent_dir);
    return ret;
}

运行 Bochs 功能验证

kernel/main.c

int main(void) {
   put_str("I am kernel\n");
   init_all();
   intr_enable(); // 打开中断

    struct stat obj_stat;
    sys_stat("/", &obj_stat);
    printf("/`s info\n   i_no:%d\n   size:%d\n   filetype:%s\n", \
        obj_stat.st_ino, obj_stat.st_size, \
        obj_stat.st_filetype == 2 ? "directory" : "regular");
    sys_stat("/dirl", &obj_stat);
    printf("/dirl`s info\n   i_no:%d\n   size:%d\n   filetype:%s\n", \
        obj_stat.st_ino, obj_stat.st_size, \
        obj_stat.st_filetype == 2 ? "directory" : "regular");

   while(1);
   return 0;
}

编译,运行:

image-20210227225358882

参考资料

  • 《操作系统真象还原》Chapter 14

评论