手写操作系统(五十五)-实现文件删除功能

代码、内容参考来自于包括《操作系统真象还原》、《一个64位操作系统的设计与实现》以及《ORANGE’S:一个操作系统的实现》。

删除文件不止删除文件,还会涉及到inode、inode位图、目录inode中的i-size、目录项、数据块及数据块位图的回收操作。

1.回收inode

第一步先要回收inode

也就是,要回收inode 位图,inode_table, inode中i_sectors[0~11]中的直接块和一级间接索引块表i_sectors[12]中的间接块,一级间接索引块表本身的扇区地址。

修改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);

    /******     以下inode_delete是调试用的    ******
    * 此函数会在inode_table中将此inode清0,
    * 但实际上是不需要的,inode分配是由inode位图控制的,
    * 硬盘上的数据不需要清0,可以直接覆盖*/
    void* io_buf = sys_malloc(1024);
    inode_delete(part, inode_no, io_buf);
    sys_free(io_buf);
    /***********************************************/

    inode_close(inode_to_del);
}

inode 的使用情况是由 inode位图来控制的,从 inode位图中把 inode 分配出去后,无论该 inode 中原来是否有数据,创建后一律会被新数据覆盖,因此在回收inode时只要在inode位图中的相应位置0就可以了,没必要在inode_table中真正擦除该inode,就像删除文件时不需要真正把文件数据块中的数据擦除一样。

inode_delete接受3个参数,分区part、 inode编号 inode_no及缓冲区io_buf,功能是把inode从inode_table 中擦除,也就是在 inode_table中把该 inode对应的空间清0。 函数开头定义的变量inode_pos用于存储inode的位置信息,下面将它作为参数调用函数inode_locate以定位编号为 inode_no的 inode,之后 inode_pos中便有了该inode的坐标。

之后开始判断该inode是否跨扇区,并针对这两种情况分别处理。 这两种情况的区别是从硬盘上读入1个扇区,还是读入2个扇区到io_buf,共性把该inode从硬盘所在扇区读入到io_buf后,调用memset函数将io_buf 中该 inode 所在的内存空间清0,然后将该 io_buf 重新写回到硬盘,从而实现了将 inode 擦除的目的。

函数inode_release接受2个参数,分区part和inode编号inode_no,功能是回收inode,这包括 inode中的数据块和 inode本身在 inode位图中的 bit。先将直接块收集到all_blocks。if (inode_to_del->i_sectors[12] != 0)判断,如果一级间接块表存在,将表中128个间接块读到all_blocks中第12块以后的其余空间中。 一级间接块表本身占据 1 扇区,因此将该扇区回收。

下面while (block_idx < block_cnt)通过while循环把块逐个回收,核心操作是调用bitmap_set将内存中位图的block_bitmap_idx位置为0,再调用bitmap_sync将内存中的位图同步到硬盘。 关于块回收工作,这里有一点说明。 如果该inode是普通文件,不需要遍历140个块,因为文件的数据是一个整体,存储数据时是把数据挨个、连续存放在块中的,相当于以all_blocks[0]~all_blocks[139]的顺序依次写入,不会出现中间某个块地址为空(0)的情况,因此在while循环中可以把块地址为0作为结束条件。 但如果该inode是目录的话,目录中的数据是目录项,它们都是单独的个体,每一个目录项都可以单独操作。 ,在删除文件中有一项工作就是擦除目录项,倘若该目录项单独占用1个块,为节约块资源,在删除此类目录项时我们会回收目录项所占的块(除了根目录中只剩下一个块的情况),也就是该块地址会被置为0。 该块很少是最后一个可用块,大部分情况下是位于140个块的中间某个块,因此对于目录要完整遍历 140个块。假设目录a中我们顺序创建了90个文件,恰好是每 30个文件占1个块,前 30个文件占用块a0,即i_sector[0]=a0,中间 30 个文件占用块a1,即i_sector[1]=a1,后30个文件占用块 a2,即 i_sector[2]=a2,这时我们先把中间30个文件都删除了,原本这30个文件所占的块就要被回收,因此出现i_sector[1]=0的情况。为实现简单,这里就不单独判断inode是目录,还是普通文件了,一律按最大块数140遍历。

接下来回收该inode在inode位图中所占用的位,依然通过bitmap_set和bitmap_sync两步完成。

接着调用inode_delete函数把该inode在inode_table擦除。不过实际inode 本身所在的空间没必要真正擦除,我们申请了1024字节的io_buf作为inode_delete的参数,理由是函数inode_delete中根据inode是否跨扇区的情况有可能要读入2个扇区。最后再调用 inode_close(inode_to_del)将 inode 关闭。

 

对应inode.h加入函数声明

void inode_release(struct partition* part, uint32_t inode_no);
void inode_delete(struct partition* part, uint32_t inode_no, void* io_buf);

 

 

2.删除目录项

文件名是以目录项的形式存在的,删除文件必须在目录中将其目录项擦除。

删除目录项相关的工作:

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

修改fs/dir.c

/* 把分区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) {      // 如果找到此i结点,就将其记录在dir_entry_found
                        ASSERT(dir_entry_found == NULL);  // 确保目录中只有一个编号为inode_no的inode,找到一次后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);
        }

        /* 更新i结点信息并同步到硬盘 */
        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,若出现这种情况应该是serarch_file出错了 */
    return false;
}

delete_dir_entry 接受 4 个参数,分区 part、目录 pdir、inode 编号 inode_no、缓冲区 io_buf,功能是把分区part目录pdir中编号为inode_no的目录项删除。

代码while (block_idx < 12)开始收集目录的inode占用的所有块,

接下来while (block_idx < 140) 开始遍历所有块,在其中查找目录项。

变量is_dir_first_block表示当前的块(待删除的目录项所在的块)是否是目录中最初的那个块,若在目录中创建很多文件或子目录后,目录会扩展到多个块中。根目录最初的块是在格式化分区时创建的,目录”.”和”..”都在这个块中,因此目录项的名称若为”.”,该块便是目录的最初的块。当删除一个目录项时,若该目录项所在的块上没有其他目录项了,或者是除了”.”和”.”之外没有其他目录项,我们就将该块回收了。

先读取目录的块,获取该块中的目录项。接着在while (dir_entry_idx < dir_entrys_per_sec)开始遍历该块,寻找待删除的目录项。目录项成员f_type的值只要不是 FT_UNKNOWN 就表示该目录项有意义,if (!strcmp((dir_e + dir_entry_idx)->filename, “.”))若判断出目录项的filename为”.”,就表示当前的块是目录最初的块,因此将变量is_dir_first_block置为true,否则统计目录中除”.”和”..”之外的所有目录项, 目录项总数存储在变量dir_entry_ent中。统计目录项个数的目的是判断删除目录项后是否回收该块,理由是若dir_entry_cnt为1,就表示该目录项独占一个块,后面会根据是否为目录的最初块判断是否将该块回收。

if (!strcmp((dir_e + dir_entry_idx)->filename, “.”))判断如果目录项与待删除的 inode 编号相同,这说明找到了,用指针变量dir_entry_found记录其在io_buf中的地址,即dir_e+dir_entry_idx,后面会用它来擦除目录项。

if (dir_entry_found == NULL)判断若此块中没找到该目录项,继续下一轮循环查找。如果找到了,就准备擦除该目录项并判断是否回收目录项所在的块。

if (dir_entry_cnt == 1 && !is_dir_first_block)判断如果当前块中目录项个数 dir_entry_cnt 为 1,并且当前块并不是根目录最初的那个块,那么就不需要擦除目录项,把当前块直接回收一了百了。工作分为两部分,先在在块位图中回收当前块,然后将块地址从数组 i_sectors 或索引表中去掉。这部分工作涉及到索引表中的间接块,因此有可能涉及到索引表所在块的回收。if (block_idx < 12)判断当前块若是直接块,就在i_sectors数组中将相应位置为0,这是最简单的情况。否则当前块是间接块,先判断一级间接索引表统计间接块的数量。if (indirect_blocks > 1)判断如果表中有多个间接块,间接索引表不能回收,就将该间接块地址清 0,同步到硬盘上的一级间接块索引表中。

else语句判断如果表中仅有这1个间接块,就在把间接索引表所在的块一同回收,随后把i_sector[12]置为0,擦除间接块索引表地址。

回到if (dir_entry_cnt == 1 && !is_dir_first_block),若判断不成立,也就是如果当前块中的目录项个数为多个或者当前块是目录的最初块,此种情况不能将块回收,进入else擦除目录项,然后同步到硬盘。

最后将目录的inode->i_size减少一个目录项大小,然后同步 inode 到硬盘,返回true。如果目录所有的块中都未找到待删除的目录项,就返回 false。

对应dir.h加入声明

bool delete_dir_entry(struct partition* part, struct dir* pdir, uint32_t inode_no, void* io_buf);

 

 

3.实现函数

接下来就是删除函数了

修改fs/fs.c

添加sys_unlink函数

/* 删除文件(非目录),成功返回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 direcotry 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;   // 成功删除文件 
}

sys_unlink函数接受1个参数,文件绝对路径名pathname,删除文件(非目录),成功返回0,失败返回-1。

声明 path_search_record 结构并调用search_file检查文件 pathname是否存在,如果不存在,则输出提示并返回-1。若只存在同名的目录,提示不能用unlink删除目录,只能用rmdir函数(将来实现)并返回-1。

while (file_idx < MAX_FILE_OPEN)在文件表中检索待删除的文件,如果文件在文件表中存在,这说明该文件正被打开,不能删除。if (file_idx < MAX_FILE_OPEN)判断如果文件已位于文件表中被打开了,输出提示该文件正处于使用中,不允许删除,返回-1。if (io_buf == NULL)为即将调用的delete_dir_entry函数申请缓冲区,这里为其申请的缓冲区大小是两个扇区。下面调用函数delete_dir_entry 删除目录项,调用inode_release释放inode,调用dir_close关闭pathname 所在的目录后,返回0,函数结束。

 

同样修改main.c测试一下:

int main(void) {
    put_str("I am kernel\n");
    init_all();
    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;
}

 

执行结果如下:

 

4.参考

郑钢著操作系统真象还原

田宇著一个64位操作系统的设计与实现

丁渊著ORANGE’S:一个操作系统的实现

暂无评论

发送评论 编辑评论

|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇