代码、内容参考来自于包括《操作系统真象还原》、《一个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:一个操作系统的实现


