手写操作系统(四十九)-inode相关函数

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

既然文件、目录本质上都是inode,先实现inode。

实现相关的代码在fs/inode.c中

新建fs/inode.c

#include "inode.h"
#include "fs.h"
#include "file.h"
#include "global.h"
#include "debug.h"
#include "memory.h"
#include "interrupt.h"
#include "list.h"
#include "stdio-kernel.h"
#include "string.h"
#include "super_block.h"

/* 用来存储inode位置 */
struct inode_position {
   bool  two_sec;  // inode是否跨扇区
   uint32_t sec_lba;    // inode所在的扇区号
   uint32_t off_size;   // inode在扇区内的字节偏移量
};

/* 获取inode所在的扇区和扇区内的偏移量 */
static void inode_locate(struct partition* part, uint32_t inode_no, struct inode_position* inode_pos) {
   /* inode_table在硬盘上是连续的 */
   ASSERT(inode_no < 4096);
   uint32_t inode_table_lba = part->sb->inode_table_lba;

   uint32_t inode_size = sizeof(struct inode);
   uint32_t off_size = inode_no * inode_size;       // 第inode_no号I结点相对于inode_table_lba的字节偏移量
   uint32_t off_sec  = off_size / 512;         // 第inode_no号I结点相对于inode_table_lba的扇区偏移量
   uint32_t off_size_in_sec = off_size % 512;       // 待查找的inode所在扇区中的起始地址

   /* 判断此i结点是否跨越2个扇区 */
   uint32_t left_in_sec = 512 - off_size_in_sec;
   if (left_in_sec < inode_size ) {   // 若扇区内剩下的空间不足以容纳一个inode,必然是I结点跨越了2个扇区
      inode_pos->two_sec = true;
   } else {            // 否则,所查找的inode未跨扇区
      inode_pos->two_sec = false;
   }
   inode_pos->sec_lba = inode_table_lba + off_sec;
   inode_pos->off_size = off_size_in_sec;
}

/* 将inode写入到分区part */
void inode_sync(struct partition* part, struct inode* inode, void* io_buf) {     // io_buf是用于硬盘io的缓冲区
   uint8_t inode_no = inode->i_no;
   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));
   
   /* 硬盘中的inode中的成员inode_tag和i_open_cnts是不需要的,
    * 它们只在内存中记录链表位置和被多少进程共享 */
   struct inode pure_inode;
   memcpy(&pure_inode, inode, sizeof(struct inode));

   /* 以下inode的三个成员只存在于内存中,现在将inode同步到硬盘,清掉这三项即可 */
   pure_inode.i_open_cnts = 0;
   pure_inode.write_deny = false;    // 置为false,以保证在硬盘中读出时为可写
   pure_inode.inode_tag.prev = pure_inode.inode_tag.next = NULL;

   char* inode_buf = (char*)io_buf;
   if (inode_pos.two_sec) {     // 若是跨了两个扇区,就要读出两个扇区再写入两个扇区
   /* 读写硬盘是以扇区为单位,若写入的数据小于一扇区,要将原硬盘上的内容先读出来再和新数据拼成一扇区后再写入  */
      ide_read(part->my_disk, inode_pos.sec_lba, inode_buf, 2); // inode在format中写入硬盘时是连续写入的,所以读入2块扇区

   /* 开始将待写入的inode拼入到这2个扇区中的相应位置 */
      memcpy((inode_buf + inode_pos.off_size), &pure_inode, sizeof(struct inode));
   
   /* 将拼接好的数据再写入磁盘 */
      ide_write(part->my_disk, inode_pos.sec_lba, inode_buf, 2);
   } else {           // 若只是一个扇区
      ide_read(part->my_disk, inode_pos.sec_lba, inode_buf, 1);
      memcpy((inode_buf + inode_pos.off_size), &pure_inode, sizeof(struct inode));
      ide_write(part->my_disk, inode_pos.sec_lba, inode_buf, 1);
   }
}

struct inode_position用于记录 inode所在的扇区地址及在扇区内的偏移量,也就是用于定位inode在磁盘上的位置,其中two_sec用于标识inode是否跨扇区,即占用2个扇区的情况,这种情况通常出现在 inode在扇区末创建,且扇区末尾的空间又不足矣容纳完整inode的时候。 sec_lba是inode的扇区地 址,off_size是inode在扇区内的偏移字节。

函数inode_locate接受3个参数,分区part、 inode编号inode_no及inode_pos,inode_pos类型是上面提到的struct inode_position,用于记录inode在硬盘上的位置,函数功能是定位inode所在的扇区和扇区内的偏移量,将其写入inode_pos中。

我们已经在函数partition_format中把inode_table连续地写入到磁盘上,在这之前,我们已经将分区通过函数mount_partition挂载了,因此可以在uint32_t inode_table_lba = part->sb->inode_table_lba从分区超级块的inode_table_lba中获取inode_table 的扇区地址,存入到同名变量inode_table_lba中。

接着获取编号为inode_no对应的inode的位置,off_sec是该inode偏移扇区地址, off_size_in_sec是该inode在扇区中的偏移字节,off_sec是相 对于inode_table_lba的扇区偏移量。

对应代码如下:

uint32_t off_size = inode_no * inode_size;      // 第inode_no号I结点相对于inode_table_lba的字节偏移量
uint32_t off_sec  = off_size / 512;        // 第inode_no号I结点相对于inode_table_lba的扇区偏移量
uint32_t off_size_in_sec = off_size % 512;      // 待查找的inode所在扇区中的起始地址

 

inode_table是连续写入多个扇区中的,因此在扇区的结尾处,有可能存在inode跨扇区的情况,即Inode的一部分在当前扇区末尾,另一部分在下一扇区的开头处。 在uint32_t left_in_sec = 512 – off_size_in_sec判断此inode是否跨越扇区,逻辑很简单,只要判断出该inode所在扇区的起始地址到扇区结束之间的剩余空间是否大于inode尺寸就行了,这个剩余空间等于扇区大小512减去inode在扇区内的偏移字节off_size_in_sec的差,也就是变量left_in_sec的值。 if (left_in_sec < inode_size )判断若left_in_sec小于inode_size,则将inode_pos->two_sec置为true,否则置为 false。

off_sec是相对于inode_table的扇区偏移量,因此inode的绝对扇区地址inode_pos->sec_lba等于 inode_table_lba加上off_sec,而inode扇区内的字节偏移量 inode_pos->off_size仍然等于off_size_in_sec。

inode_sync函数接受3个参数,分区part、待同步的inode指针、操作缓冲区io_buf,函数功能是将inode写入到磁盘分区part。

io_buf是主调函数提供的缓冲区,原因是必须要保证把内存数据成功同步到硬盘,同步数据需要缓冲区,一般情况下把内存中的数据同步到硬盘都是最后的操作,其前肯定已经做了大量工作,若到这最后一步(也就是执行到此函数)时才申请内存失败,前面的所有操作都白费了不说,还要回滚到之前的旧状态,代价太大,因此io_buf必须由主调函数提前申请好。 inode_sync的内容看上去有些多,但实际上很简单,要想往硬盘上写入inode,必须得知道inode的扇区地址,也就是说得知道往硬盘哪里写,因此在inode_locate(part, inode_no, &inode_pos)先通过函数inode_locate定位该inode的位置,位置信息在 inode_pos 中保存。 inode中的三个成员i_open_ents、 write_deny和inode_tag,它们用于统计inode操作状态,只在内存中有意义,为避免下次将该inode加载到内存时出现混乱的情况,最好是把它们写入到硬盘之前就将这三个成员的值清掉。 原inode就不动了,这里新建一局部变量pure_inode,memcpy(&pure_inode, inode, sizeof(struct inode))将原inode的值拷贝到pure_inode中,然后清空pure_node中的那3个变量,后续操作的都是pure_inode。

char* inode_buf = (char*)io_buf将io_buf转换为inode_buf,此缓冲区用于拼接同步的inode数据。

if (inode_pos.two_sec)判断inode是否跨扇区,如果(inode_pos.two_sec为true,说明该inode横跨两个扇区,因此在ide_read(part->my_disk, inode_pos.sec_lba, inode_buf, 2)连续读入2个扇区到inode_buf,memcpy((inode_buf + inode_pos.off_size), &pure_inode, sizeof(struct inode))通过memcpy函数将pure_inode拷贝到inode_buf中的相应位置,ide_write(part->my_disk, inode_pos.sec_lba, inode_buf, 2)将inode_buf中两个扇区大小的数据写入硬盘。

后面就是是处理未跨扇区的情况,主要区别是读写1个扇区的大小。

 

继续修改/fs/inode.c

/* 根据i结点号返回相应的i结点 */
struct inode* inode_open(struct partition* part, uint32_t inode_no) {
   /* 先在已打开inode链表中找inode,此链表是为提速创建的缓冲区 */
   struct list_elem* elem = part->open_inodes.head.next;
   struct inode* inode_found;
   while (elem != &part->open_inodes.tail) {
      inode_found = elem2entry(struct inode, inode_tag, elem);
      if (inode_found->i_no == inode_no) {
     inode_found->i_open_cnts++;
     return inode_found;
      }
      elem = elem->next;
   }

   /*由于open_inodes链表中找不到,下面从硬盘上读入此inode并加入到此链表 */
   struct inode_position inode_pos;

   /* inode位置信息会存入inode_pos, 包括inode所在扇区地址和扇区内的字节偏移量 */
   inode_locate(part, inode_no, &inode_pos);

/* 为使通过sys_malloc创建的新inode被所有任务共享,
 * 需要将inode置于内核空间,故需要临时
 * 将cur_pbc->pgdir置为NULL */
   struct task_struct* cur = running_thread();
   uint32_t* cur_pagedir_bak = cur->pgdir;
   cur->pgdir = NULL;
   /* 以上三行代码完成后下面分配的内存将位于内核区 */
   inode_found = (struct inode*)sys_malloc(sizeof(struct inode));
   /* 恢复pgdir */
   cur->pgdir = cur_pagedir_bak;

   char* inode_buf;
   if (inode_pos.two_sec) { // 考虑跨扇区的情况
      inode_buf = (char*)sys_malloc(1024);

   /* i结点表是被partition_format函数连续写入扇区的,
    * 所以下面可以连续读出来 */
      ide_read(part->my_disk, inode_pos.sec_lba, inode_buf, 2);
   } else { // 否则,所查找的inode未跨扇区,一个扇区大小的缓冲区足够
      inode_buf = (char*)sys_malloc(512);
      ide_read(part->my_disk, inode_pos.sec_lba, inode_buf, 1);
   }
   memcpy(inode_found, inode_buf + inode_pos.off_size, sizeof(struct inode));

   /* 因为一会很可能要用到此inode,故将其插入到队首便于提前检索到 */
   list_push(&part->open_inodes, &inode_found->inode_tag);
   inode_found->i_open_cnts = 1;

   sys_free(inode_buf);
   return inode_found;
}

/* 关闭inode或减少inode的打开数 */
void inode_close(struct inode* inode) {
   /* 若没有进程再打开此文件,将此inode去掉并释放空间 */
   enum intr_status old_status = intr_disable();
   if (--inode->i_open_cnts == 0) {
      list_remove(&inode->inode_tag);     // 将I结点从part->open_inodes中去掉
   /* inode_open时为实现inode被所有进程共享,
    * 已经在sys_malloc为inode分配了内核空间,
    * 释放inode时也要确保释放的是内核内存池 */
      struct task_struct* cur = running_thread();
      uint32_t* cur_pagedir_bak = cur->pgdir;
      cur->pgdir = NULL;
      sys_free(inode);
      cur->pgdir = cur_pagedir_bak;
   }
   intr_set_status(old_status);
}

/* 初始化new_inode */
void inode_init(uint32_t inode_no, struct inode* new_inode) {
   new_inode->i_no = inode_no;
   new_inode->i_size = 0;
   new_inode->i_open_cnts = 0;
   new_inode->write_deny = false;

   /* 初始化块索引数组i_sector */
   uint8_t sec_idx = 0;
   while (sec_idx < 13) {
   /* i_sectors[12]为一级间接块地址 */
      new_inode->i_sectors[sec_idx] = 0;
      sec_idx++;
   }
}

inode_open函数接受两个参数,分区part及inode编号inode_no,函数功能是根据inode_no返回相应的i结点指针。

磁盘是一种低速设备,因此文件系统的设计原则是尽量减少硬盘操作。inode是存储在磁盘上的,为减少频繁访问磁盘,我们早已在内存中为各分区创建了inode队列,即part->open_inodes。这个队列应该称为已打开的inode队列,它是inode的缓存。

以后每打开1个inode时,先在此缓存中查找该inode,找到后则直接返回inode指针,若未找到,再从磁盘上加载该inode到此缓存中,然后再返回其指针。查找过程就是遍历part->open_inodes,对应的代码是while (elem != &part->open_inodes.tail),如果找到后就执行”return inode_found”返回找到的 inode 指针。

如果inode队列中没有该inode,下面开始从硬盘上读取。先在struct inode_position inode_pos创建inode_pos,接着在inode_locate(part, inode_no, &inode_pos)调用 inode_locate 去定位该 inode,位置存储到 inode_pos 中。

inode队列中的所有inode应该被所有任务共享,包括内核线程和进程,这样的缓存才有意义。各进程都有独立的页表,因此为保证所有任务都共享inode队列,需要将整个inode队列创建在内核空间中。还有,为了使inode长久存在,inode队列中的所有结点必须在内核的堆空间中创建,而不是简单使用栈中的局部变量。故下面我们从硬盘上获取到的inode,其所占的内存是我们用sys_malloc从堆中分配的。由于用户进程有自己的页表,即peb->pgdir的值不为NULL,按照sys_malloc的规则,这会从该用户进程自己的堆中分配内存,显然,这样做的话该inode只会被该进程自己访问到,失去了共享的意义。因此,为了使inode置于内核空间被所有任务共享,,需要临时将当前任务peb->pgdir置为NULL,待为inode完成内存分配后再将任务的pgdir恢复。为简单省事,这里并未判断当前任务是内核线程,还是用户进程,采取了统一处理,因为此处判断的代价和直接赋值的代价是一样的。先在uint32_t* cur_pagedir_bak = cur->pgdir将当前任务的页表地址备份到变量cur_pagedir_bak中,在cur->pgdir = NULL将pgdir置为NULL后,接着在inode_found = (struct inode*)sys_malloc(sizeof(struct inode))调用sys_malloc分配1个inode大小的内存,指针存入变量inode_found,它是我们为磁盘上的inode所分配的内存变量。然后在cur->pgdir = cur_pagedir_bak将pgdir恢复为cur_pagedir_bak。注意,整个操作过程不是在更换页表,页表在寄存器CR3中呢,它可一直没有变动。这里仅仅是修改了 peb->pgdir 而已。

inode_found的内存分配好之后,下面开始读取硬盘了,if (inode_pos.two_sec)先判断该inode是否跨扇区,如果是的话,就涉及到2个扇区的读写,因此inode_buf = (char*)sys_malloc(1024)申请了2个扇区大小的缓冲区赋值给inode_buf,之后在ide_read(part->my_disk, inode_pos.sec_lba, inode_buf, 2)读取2个扇区的数据到inode_buf中。 否则inode未跨扇区的话,就在inode_buf = (char*)sys_malloc(512)只申请1个扇区大小的内存做缓冲区,然后将1扇区的数据读入到该缓冲区中。

硬盘的读写单位是扇区,我们想要的inode还混在这些扇区中,具体地址是在inode_buf+inodepos.off_size处,因此在memcpy(inode_found, inode_buf + inode_pos.off_size, sizeof(struct inode))调用memcpy函数将扇区中的inode复制到inode_found中。

通常情况下此inode会被再次使用到,因此在 list_push(&part->open_inodes, &inode_found->inode_tag)通过list_push将它插入 到inode队列的最前面,以便下次更快被找到。 inode_found->i_open_cnts = 1将它的i_open_cnts置为1,表示目前此inode仅被打开1次。 然后释放缓冲区inode_buf,返回inode_found指针。

inode_close函数接受1个参数,inode指针inode,功能是关闭inode。 关闭inode的思路是将inode的i_open_ents减1,若其值为0,则说明此inode未被打开,此时可以将其从inode队列中去掉并回收空间了。 相关代码是if (–inode->i_open_cnts == 0)。前面在用inode_open打开inode时,为了确保inode被所有进程共享,特意把页表置为NULL使内存分配函数sys_malloc在内核空间中为inode分配内存,现在释放inode的内存时,当前任务有可能是进程,因此会导致将内核地址在用户内存池中释放,这肯定是错的,所以又将页表置为NULL,使sys_free正确释放内核中的 inode。 sys_free之后,再把页表换回来。

inode_init函数接受2个参数,inode编号inode_no及待初始化的inode指针new_inode,功能是初始化 new_inode。先初始化 inode 中的 i_no 为参数 inode_no,i_size和 i_open_cnt 为 0,write_deny 为 false,接下来是初始化i_sectors 数组,该数组大小是 13个元素,前12个是直接块地址,第13 个一级间接 块索引表地址,在此统统置为0。 inode是由sys_malloc分配的,其实经sys_malloc返回的内存内容已被置为 0,这里为了可读性好一些还是显示初始化。

创建Inode时不为其分配扇区,只有在真正写文件时才分配扇区,理由是首先不知道文件大小,因此不知道分配多少个扇区合适,提前分配的扇区都是浪费空间。其次,文件创建后未必马上会写数据,真正需要往文件中写数据时根据实际数据量大小再分配扇区,效率更高一些。

对应/fs/inode.h

#ifndef __FS_INODE_H
#define __FS_INODE_H
#include "stdint.h"
#include "list.h"
#include "ide.h"

/* inode结构 */
struct inode {
   uint32_t i_no;    // inode编号

    /* 当此inode是文件时,i_size是指文件大小,
    若此inode是目录,i_size是指该目录下所有目录项大小之和*/
   uint32_t i_size;

   uint32_t i_open_cnts;   // 记录此文件被打开的次数
   bool write_deny;    // 写文件不能并行,进程写文件前检查此标识

    /* i_sectors[0-11]是直接块, i_sectors[12]用来存储一级间接块指针 */
   uint32_t i_sectors[13];
   struct list_elem inode_tag;
};

struct inode* inode_open(struct partition* part, uint32_t inode_no);
void inode_sync(struct partition* part, struct inode* inode, void* io_buf);
void inode_init(uint32_t inode_no, struct inode* new_inode);
void inode_close(struct inode* inode);
#endif

 

参考

郑钢著操作系统真象还原

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

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

暂无评论

发送评论 编辑评论

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