代码、内容参考来自于包括《操作系统真象还原》、《一个64位操作系统的设计与实现》以及《ORANGE’S:一个操作系统的实现》。
1.创建超级块、i结点、目录项
我们先创建基本数据结构,超级块、i结点、目录项。
创建超级块,新建/fs/super_block.h
#ifndef __FS_SUPER_BLOCK_H
#define __FS_SUPER_BLOCK_H
#include "stdint.h"
/* 超级块 */
struct super_block {
uint32_t magic; // 用来标识文件系统类型,支持多文件系统的操作系统通过此标志来识别文件系统类型
uint32_t sec_cnt; // 本分区总共的扇区数
uint32_t inode_cnt; // 本分区中inode数量
uint32_t part_lba_base; // 本分区的起始lba地址
uint32_t block_bitmap_lba; // 块位图本身起始扇区地址
uint32_t block_bitmap_sects; // 扇区位图本身占用的扇区数量
uint32_t inode_bitmap_lba; // i结点位图起始扇区lba地址
uint32_t inode_bitmap_sects; // i结点位图占用的扇区数量
uint32_t inode_table_lba; // i结点表起始扇区lba地址
uint32_t inode_table_sects; // i结点表占用的扇区数量
uint32_t data_start_lba; // 数据区开始的第一个扇区号
uint32_t root_inode_no; // 根目录所在的I结点号
uint32_t dir_entry_size; // 目录项大小
uint8_t pad[460]; // 加上460字节,凑够512字节1扇区大小
} __attribute__ ((packed));
#endif为方便写程序,我们的数据块大小与扇区大小一致,即 1 块等于 1 扇区,磁盘操作要以扇区为单位,我们交给硬盘的数据必须是扇区大小的整数倍。为了凑足1扇区,即512字节,在超级块的最后定义了460字节的pad数组,仅仅是用来填充扇区用的。
接下来是inode结构,新建/fs/inode.h
#ifndef __FS_INODE_H
#define __FS_INODE_H
#include "stdint.h"
#include "list.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;
};
#endifinode 结构中,i_no 是 inode 编号,它是在 inode 数组中的下标。
i_size是此inode指向的文件的大小,目录也是用inode指代,因此当inode指向的是普通文件时, i_size表示普通文件的大小,当inode指向的是目录时,i_size表示目录中所有目录项的大小之和。注意i_size是以字节为单位的大小,并不是以数据块为单位,为方便编码,我们的数据块大小等于扇区大小。
i_open_ents表示此文件被打开的次数,它在关闭文件时,回收与之相关的资源时使用。write_deny用于限制文件的并行写操作,我们都有这样的常识,不能让多个用户同时写1个文件,这样后写入的内容会覆盖之前写入的内容,从而引起数据混乱,因此必须保证文件在执行写操作时,该文件不能再有其他并行的写操作,多个写操作应该以串行的方式进行。当write_deny为true时表示已经有任务在写该文件了,此文件的其他写操作应该被拒绝。
i_sectors是数据块的指针,我们的块大小就是1扇区,直接把块数组命名为i_sector。其中,数据的前12个块 i_sectors[0-11]是直接块,也就是它们中记录的是数据块的扇区地址,i_sectors[12]用来存储一级间接块索引表的扇区地址,目前我们只打算支持一级间接块,不过稍微不同的是扇区大小是512字节,并且块地址用4字节来表示,因此我们支持的一级间接块数量是128个,即总共支持128+12=140个块(扇区)。
inode_tag是此inode的标识,用于加入已打开的 inode列表,此列表将在以后定义。建立此列表的目的是由于inode是从硬盘上保存的,文件被打开时,肯定是先要从硬盘上载入其inode,但硬盘比较慢,为了避免下次再打开该文件时还要从硬盘上重复载入inode,应该在该文件第一次被打开时就将其inode加入到内存缓存中,每次打开一个文件时,先在此缓冲中查找相关的inode,如果有就直接使用,否则再从硬盘上读取inode,然后再加入此缓存。这个内存缓存就是已打开的inode队列。
下面咱们看目录项的定义,它定义在fs/dir.h中
#ifndef __FS_DIR_H
#define __FS_DIR_H
#include "stdint.h"
#include "inode.h"
#include "ide.h"
#include "global.h"
#define MAX_FILE_NAME_LEN 16 // 最大文件名长度
/* 目录结构 */
struct dir {
struct inode* inode;
uint32_t dir_pos; // 记录在目录内的偏移
uint8_t dir_buf[512]; // 目录的数据缓存
};
/* 目录项结构 */
struct dir_entry {
char filename[MAX_FILE_NAME_LEN]; // 普通文件或目录名称
uint32_t i_no; // 普通文件或目录对应的inode编号
enum file_types f_type; // 文件类型
};
#endif文件名要存储在目录项中,目录项大小是固定的,因此文件名的长度肯定要有个上限,代码开头定义的宏MAX_FILE_NAME_LEN便是文件名的最大长度,其值为16。 此宏是为目录项dir_entry准备的。
struct dir是目录结构,它并不在磁盘上存在,只用于与目录相关的操作时,在内存中创建的结构,用过之后就释放了,不会回写到磁盘中。其成员 inode 是指针,因此肯定是用于指向内存中 inode,该 inode 必然是在已打开的 inode 队列。 成员dir_pos用于遍历目录时记录游标在目录中的偏移,也就是目录项的偏移量,所以dir_pos大小应为目录项大小的整数倍,这与遍历目录的操作相关。 成员dir_buf用于目录的数据缓存,如读取目录时,用来存储返回的目录项。
下面是目录项结构struct dir_entry,它是连接文件名与inode的纽带,成员filename是文件名,这里只 支持最大 16 个字符的文件名。 成员 i_no 是文件 filename 对应的 inode 编号,无论filename是普通文件, 还是目录文件。 成员 f_type 是指 filename 的类型,具体类型定义在 fs/fs.h 中。
新建fs/fs.h
#ifndef __FS_FS_H
#define __FS_FS_H
#include "stdint.h"
#include "ide.h"
#define MAX_FILES_PER_PART 4096 // 每个分区所支持最大创建的文件数
#define BITS_PER_SECTOR 4096 // 每扇区的位数
#define SECTOR_SIZE 512 // 扇区字节大小
#define BLOCK_SIZE SECTOR_SIZE // 块字节大小
/* 文件类型 */
enum file_types {
FT_UNKNOWN, // 不支持的文件类型
FT_REGULAR, // 普通文件
FT_DIRECTORY // 目录
};
void filesys_init(void);
#endif枚举结构 file_types是文件类型, FT_UNKNOWN 的值为 0,表示未知的文件类型,FT_REGULAR 值为1,表示普通文件,FT_DIRECTORY 值为2,表示目录。
2.创建文件系统
开始创建文件系统先要完成格式化分区,对应的函数是partition_format,定义在fs/fs.c
完整代码
#include "fs.h"
#include "super_block.h"
#include "inode.h"
#include "dir.h"
#include "stdint.h"
#include "stdio-kernel.h"
#include "list.h"
#include "string.h"
#include "ide.h"
#include "global.h"
#include "debug.h"
#include "memory.h"
/* 格式化分区,也就是初始化分区的元信息,创建文件系统 */
static void partition_format(struct partition* part) {
/* 为方便实现,一个块大小是一扇区 */
uint32_t boot_sector_sects = 1;
uint32_t super_block_sects = 1;
uint32_t inode_bitmap_sects = DIV_ROUND_UP(MAX_FILES_PER_PART, BITS_PER_SECTOR); // I结点位图占用的扇区数.最多支持4096个文件
uint32_t inode_table_sects = DIV_ROUND_UP(((sizeof(struct inode) * MAX_FILES_PER_PART)), SECTOR_SIZE);
uint32_t used_sects = boot_sector_sects + super_block_sects + inode_bitmap_sects + inode_table_sects;
uint32_t free_sects = part->sec_cnt - used_sects;
/************** 简单处理块位图占据的扇区数 ***************/
uint32_t block_bitmap_sects;
block_bitmap_sects = DIV_ROUND_UP(free_sects, BITS_PER_SECTOR);
/* block_bitmap_bit_len是位图中位的长度,也是可用块的数量 */
uint32_t block_bitmap_bit_len = free_sects - block_bitmap_sects;
block_bitmap_sects = DIV_ROUND_UP(block_bitmap_bit_len, BITS_PER_SECTOR);
/*********************************************************/
/* 超级块初始化 */
struct super_block sb;
sb.magic = 0x19590318;
sb.sec_cnt = part->sec_cnt;
sb.inode_cnt = MAX_FILES_PER_PART;
sb.part_lba_base = part->start_lba;
sb.block_bitmap_lba = sb.part_lba_base + 2; // 第0块是引导块,第1块是超级块
sb.block_bitmap_sects = block_bitmap_sects;
sb.inode_bitmap_lba = sb.block_bitmap_lba + sb.block_bitmap_sects;
sb.inode_bitmap_sects = inode_bitmap_sects;
sb.inode_table_lba = sb.inode_bitmap_lba + sb.inode_bitmap_sects;
sb.inode_table_sects = inode_table_sects;
sb.data_start_lba = sb.inode_table_lba + sb.inode_table_sects;
sb.root_inode_no = 0;
sb.dir_entry_size = sizeof(struct dir_entry);
printk("%s info:\n", part->name);
printk(" magic:0x%x\n part_lba_base:0x%x\n all_sectors:0x%x\n inode_cnt:0x%x\n block_bitmap_lba:0x%x\n block_bitmap_sectors:0x%x\n inode_bitmap_lba:0x%x\n inode_bitmap_sectors:0x%x\n inode_table_lba:0x%x\n inode_table_sectors:0x%x\n data_start_lba:0x%x\n", sb.magic, sb.part_lba_base, sb.sec_cnt, sb.inode_cnt, sb.block_bitmap_lba, sb.block_bitmap_sects, sb.inode_bitmap_lba, sb.inode_bitmap_sects, sb.inode_table_lba, sb.inode_table_sects, sb.data_start_lba);
struct disk* hd = part->my_disk;
/*******************************
* 1 将超级块写入本分区的1扇区 *
******************************/
ide_write(hd, part->start_lba + 1, &sb, 1);
printk(" super_block_lba:0x%x\n", part->start_lba + 1);
/* 找出数据量最大的元信息,用其尺寸做存储缓冲区*/
uint32_t buf_size = (sb.block_bitmap_sects >= sb.inode_bitmap_sects ? sb.block_bitmap_sects : sb.inode_bitmap_sects);
buf_size = (buf_size >= sb.inode_table_sects ? buf_size : sb.inode_table_sects) * SECTOR_SIZE;
uint8_t* buf = (uint8_t*)sys_malloc(buf_size); // 申请的内存由内存管理系统清0后返回
/**************************************
* 2 将块位图初始化并写入sb.block_bitmap_lba *
*************************************/
/* 初始化块位图block_bitmap */
buf[0] |= 0x01; // 第0个块预留给根目录,位图中先占位
uint32_t block_bitmap_last_byte = block_bitmap_bit_len / 8;
uint8_t block_bitmap_last_bit = block_bitmap_bit_len % 8;
uint32_t last_size = SECTOR_SIZE - (block_bitmap_last_byte % SECTOR_SIZE); // last_size是位图所在最后一个扇区中,不足一扇区的其余部分
/* 1 先将位图最后一字节到其所在的扇区的结束全置为1,即超出实际块数的部分直接置为已占用*/
memset(&buf[block_bitmap_last_byte], 0xff, last_size);
/* 2 再将上一步中覆盖的最后一字节内的有效位重新置0 */
uint8_t bit_idx = 0;
while (bit_idx <= block_bitmap_last_bit) {
buf[block_bitmap_last_byte] &= ~(1 << bit_idx++);
}
ide_write(hd, sb.block_bitmap_lba, buf, sb.block_bitmap_sects);
/***************************************
* 3 将inode位图初始化并写入sb.inode_bitmap_lba *
***************************************/
/* 先清空缓冲区*/
memset(buf, 0, buf_size);
buf[0] |= 0x1; // 第0个inode分给了根目录
/* 由于inode_table中共4096个inode,位图inode_bitmap正好占用1扇区,
* 即inode_bitmap_sects等于1, 所以位图中的位全都代表inode_table中的inode,
* 无须再像block_bitmap那样单独处理最后一扇区的剩余部分,
* inode_bitmap所在的扇区中没有多余的无效位 */
ide_write(hd, sb.inode_bitmap_lba, buf, sb.inode_bitmap_sects);
/***************************************
* 4 将inode数组初始化并写入sb.inode_table_lba *
***************************************/
/* 准备写inode_table中的第0项,即根目录所在的inode */
memset(buf, 0, buf_size); // 先清空缓冲区buf
struct inode* i = (struct inode*)buf;
i->i_size = sb.dir_entry_size * 2; // .和..
i->i_no = 0; // 根目录占inode数组中第0个inode
i->i_sectors[0] = sb.data_start_lba; // 由于上面的memset,i_sectors数组的其它元素都初始化为0
ide_write(hd, sb.inode_table_lba, buf, sb.inode_table_sects);
/***************************************
* 5 将根目录初始化并写入sb.data_start_lba
***************************************/
/* 写入根目录的两个目录项.和.. */
memset(buf, 0, buf_size);
struct dir_entry* p_de = (struct dir_entry*)buf;
/* 初始化当前目录"." */
memcpy(p_de->filename, ".", 1);
p_de->i_no = 0;
p_de->f_type = FT_DIRECTORY;
p_de++;
/* 初始化当前目录父目录".." */
memcpy(p_de->filename, "..", 2);
p_de->i_no = 0; // 根目录的父目录依然是根目录自己
p_de->f_type = FT_DIRECTORY;
/* sb.data_start_lba已经分配给了根目录,里面是根目录的目录项 */
ide_write(hd, sb.data_start_lba, buf, 1);
printk(" root_dir_lba:0x%x\n", sb.data_start_lba);
printk("%s format done\n", part->name);
sys_free(buf);
}
/* 在磁盘上搜索文件系统,若没有则格式化分区创建文件系统 */
void filesys_init() {
uint8_t channel_no = 0, dev_no, part_idx = 0;
/* sb_buf用来存储从硬盘上读入的超级块 */
struct super_block* sb_buf = (struct super_block*)sys_malloc(SECTOR_SIZE);
if (sb_buf == NULL) {
PANIC("alloc memory failed!");
}
printk("searching filesystem......\n");
while (channel_no < channel_cnt) {
dev_no = 0;
while(dev_no < 2) {
if (dev_no == 0) { // 跨过裸盘hd60M.img
dev_no++;
continue;
}
struct disk* hd = &channels[channel_no].devices[dev_no];
struct partition* part = hd->prim_parts;
while(part_idx < 12) { // 4个主分区+8个逻辑
if (part_idx == 4) { // 开始处理逻辑分区
part = hd->logic_parts;
}
/* channels数组是全局变量,默认值为0,disk属于其嵌套结构,
* partition又为disk的嵌套结构,因此partition中的成员默认也为0.
* 若partition未初始化,则partition中的成员仍为0.
* 下面处理存在的分区. */
if (part->sec_cnt != 0) { // 如果分区存在
memset(sb_buf, 0, SECTOR_SIZE);
/* 读出分区的超级块,根据魔数是否正确来判断是否存在文件系统 */
ide_read(hd, part->start_lba + 1, sb_buf, 1);
/* 只支持自己的文件系统.若磁盘上已经有文件系统就不再格式化了 */
if (sb_buf->magic == 0x19590318) {
printk("%s has filesystem\n", part->name);
} else { // 其它文件系统不支持,一律按无文件系统处理
printk("formatting %s`s partition %s......\n", hd->name, part->name);
partition_format(part);
}
}
part_idx++;
part++; // 下一分区
}
dev_no++; // 下一磁盘
}
channel_no++; // 下一通道
}
sys_free(sb_buf);
}
partition_format函数
函数partition_format接受1个参数,待创建文件系统的分区part。 为方便实现,一个块大小是一扇区,但是相关术语中都是以块单位,因此下面说到块时请直接理解为扇区。
创建文件系统就是创建文件系统所需要的元信息,这包括超级块位置及大小、空闲块位图的位置及大小、inode位图的位置及大小、inode数组的位置及大小、空闲块起始地址、根目录起始地址。
创建步骤如下:
- 根据分区part大小,计算分区文件系统各元信息需要的扇区数及位置。
- 在内存中创建超级块,将以上步骤计算的元信息写入超级块。
- 将超级块写入磁盘。
- 将元信息写入磁盘上各自的位置。
- 将根目录写入磁盘。
下面的创建工作将按照这5个步骤依次完成。
代码uint32_t boot_sector_sects = 1和uint32_t super_block_sects = 1是为引导块和超级块占用的扇区数赋值,简单起见,它们均占用1 扇区大小。 我们的引导块未使用,因此无所谓大小,但依然要保留其占位。
inode_bitmap_sects表示inode位图占用的扇区数,MAX_FILES_PER_PART定义在fs.h中,表示分区 可创建的最大文件数,也就是inode数量,它的值为4096。BITS_PER_SECTOR同样定义在fs.h中,其值也为4096,经过宏DIV_ROUND_UP计算后 inode_bitmap_sects的值为1, inode位图占用1扇区。
inode_table_sects表示 inode数组占用的扇区数,这是由inode的尺寸和数量决定的。
目前已占用的磁盘空间包括引导块、超级块、inode位图及inode数组,现在还差空闲块位图大小未计算出来,它是由空闲块的数量决定的,因此在uint32_t free_sects = part->sec_cnt – used_sects先算出空闲块数量,空闲块(扇区)数量等于分区总扇区数减去使用的扇区数。
接着开始计算空闲块位图占用的扇区数。
/************** 简单处理块位图占据的扇区数 ***************/ uint32_t block_bitmap_sects; block_bitmap_sects = DIV_ROUND_UP(free_sects, BITS_PER_SECTOR); /* block_bitmap_bit_len是位图中位的长度,也是可用块的数量 */ uint32_t block_bitmap_bit_len = free_sects - block_bitmap_sects; block_bitmap_sects = DIV_ROUND_UP(block_bitmap_bit_len, BITS_PER_SECTOR); /*********************************************************/
由于空闲块位图大小和空闲块数量相互依赖,其总和为空闲块数free_sects,相当于两个互相决定对方大小的变量,这里的处理很简单,先用空闲块数free_sects除以每扇区的位数,这样便得到了空闲块位图block_bitmap占用的扇区数block_bitmapsects。 空闲块位图占用了一部分空闲扇区,因此现在真正的空闲块数得把block_bitmap_sects从free_sets中减去,其结果也是位图中位的个数,即我们把结果写入了变量block_bitmap_bit_len,然后再用变量block_bitmap_bit_len重新除以BITS_PER_SECTOR,这便是空闲块位图最终占用的扇区数block_bitmap_sects。
从struct super_block sb开始在内存中创建超级块,此处用局部变量生成超级块,用的是栈中的内存,不过还好,超级块是512字节,栈还够用。 代码”sb.rootinode_no=0″,这表示根目录的inode编号为0,也就是说inode数组中第0个inode我们留给了根目录。 sb.dir_entry_size = sizeof(struct dir_entry)为目录项尺寸 dir_entry_size 赋值。 后面是打印超级块中元信息,我们在运行时会看到它们。
struct disk* hd = part->my_disk先获取分区part自己所属的硬盘hd,hd将作为后续参数。
超级块已经构建完成,在ide_write(hd, part->start_lba + 1, &sb, 1)将其写到本分区开始的扇区加 1 的地方,即 part->start_1ba + 1,也就是跨过引导扇区,把超级块写入引导扇区后面的扇区中。 尽管此处的引导块没什么用,但也要将其位置空出来。 元信息最终是要写入硬盘的,但数据源是在内存中。 像超级块本身是1扇区大小,我们是用局部变量声明它的,栈还能将就对付。 可是空闲块位图、inode数组位图等占用的扇区数较大,所以这里不便用局部变量来保存它们了,应该从堆中申请内存获取缓冲区,最好是找出数据量最大的元信息,用其尺寸作为申请的内存大小。
选出占用空间最大的元信息,使其尺寸作为申请的缓冲区大小,在uint8_t* buf = (uint8_t*)sys_malloc(buf_size)申请内存返回给指针buf,buf是通用的缓冲区,接下来往磁盘中的数据写入操作都将buf作为数据源,通过不同的类型转换,使buf变成合适的缓冲区类型。
接下来是把块位图写入磁盘扇区 sb.block_bitmap_lba 的准备工作。
我们把第0个空闲块作为根目录,因此我们需要buf[0] |= 0x01在空闲块位图中将第0位置1。接下来直到while循环的功能就是把块位图最后一个扇区中不属于空闲块的位初始为1。
跟踪资源状态是通过位图来决定了,创建文件系统相当于创建各种资源的位图,位图在内存中创建好,然后再持久化到硬盘中去,此时创建的分区位图不是现在用,挂载分区前,内存里没有位图,挂载时就需要从分区中读取位图到内存中,因为现在创建的块位图大小是通过宏DIV_ROUND_UP把free_sects除以BITS_PER_SECTOR向上取整得到的,所以就难免有一些非位图资源的数据,若在位图操作中强行使用这些位的话,必然会损坏那些资源外的数据。因此现在必须将这些多余位初始为1,将来就不会再分配这些位对应的资源了,避免了出现错误的情况。
将来挂载分区时把位图从硬盘加载到内存后,内存中位图的btmp_bytes_len=位图占用的扇区数*每扇区的位数。因此在将位图持久化到硬盘之前,一定要将位图最后一扇区中的多余位初始为1,表示它们已被占用。
接下来就是开始准备将inode位图写入磁盘扇区sb.inode_bitmap_lba。
buf[0] |= 0x1;将inode位图(buf)中第0个inode置为1,原因是我们把inode数组中第0个inode分配给了根目录。
分区只支持4096个文件,也就是 inode数组inode_table中共4096个inode,inode位图inode_bitmap正好占用1扇区,即inode_bitmap_sects等于1, inode_bitmap所在的扇区中没有多余的无效位,因此无需再像块位图 block_bitmap那样单独处理最后一扇区的剩余部分。 最后在ide_write(hd, sb.inode_bitmap_lba, buf, sb.inode_bitmap_sects);将inode位图写入磁盘。
接下来准备把 inode 数组写入 sb.inode_table_lba。 inode_table_sects是通过宏DIV_ROUND_UP除法向上取整得到的结果,因此inode_table最终在磁盘上占据的全部扇区中,并不是所有空间都是inode_table的内容,大多数情况下在其所占据的最后不足一 扇区,剩余部分肯定不属于inode_table中的inode。不过由于inode数量是由inode_bitmap来控制的,只要保证 inode_bitmap不越界就行,而 inode_bitmap占用完整1 扇区,里面没有多余的位,因此肯定是正确的,不用再额外处理什么。
我们把第0个inode已经分配给了根目录,因此现在要初始化第0个inode为根目录的信息。 我们前面讨论过的,当inode指代的是普通文件时,i_size是文件大小,当inode指代的是目录时,成员i_size表示此目录下所有目录项的大小之和。 任何目录中默认都会有表示当前目录的”.”和上一级目录”.. “,根目录也不能例外,这两个目录项的大小之和便是此inode中i_size 的值,因此先在struct inode* i = (struct inode*)buf将buf转换为 inode结构struct inode型指针后,通过”i->i_size = sb.dir_entry_size *2”将 i_size 赋值为两个目录项的大小。 “i->i_no= 0”为 inode 编号赋值为0,表示此inode自己是inode数组中第0个inode。 “i->i_sectors[0] = sb.data_start_lba”使 此inode的第0个数据块指向sb.data_start_lba,也就是我们把根目录安排在最开始的空闲块中。
由于memset(buf, 0, buf_size)的memset清0工作,i_sectors数组的其他元素也都被初始化为0。 最后在ide_write(hd, sb.inode_table_lba, buf, sb.inode_table_sects)将inode数组写入硬盘。
最后一项工作是在根目录中写目录项”.”和”..”。 任何目录都有这两个目录项,”.”表示当前目录,”..”表示上一级目录。struct dir_entry* p_de = (struct dir_entry*)buf将 buf 转换为目录项struct dir_entry 型指针,此时p_de 指向 buf,接下来先对第1个目录项”. “初始化。在memcpy(p_de->filename, “.”, 1)通过memcpy函数把”. “写入目录项的filename成员,接下来的2行分别为目录项的i_no 赋值为0,使其指向根目录自己,为目录项的f_type赋值为FT_DIRECTORY,使其类型为目录。 然后p_de++执行过后,p_de指向下一目录项”..”。
接下来在ide_write(hd, sb.data_start_lba, buf, 1)将根目录写入磁盘。最后sys_free(buf)释放缓冲区 buf。
filesys_init函数
while循环开始在分区上扫描文件系统,我们这里只支持partition_format创建的文件系统,其魔数等于0x19590318,如果未发现魔数为0x19590318的文件系统就调用partition_format去创建。 这里面是通过三层循环完成的,最外层循环用来遍历通道,中间层循环用来遍历通道中的硬盘,最内层循环用来遍历硬盘上的所有分区。
我们对每个硬盘最多支持12个分区,即4个主分区和8个逻辑分区,因此在遍历硬盘分区时只需要循环12次。 part用于指向每一个分区,当分区索引变量part_idx等于4时,这表示全部主分区都处理完了,于是将part指向硬盘的逻辑分区数组,也就是第一个逻辑分区的地址。 虽然我们支持12个分区,但并不表示硬盘中存在12个分区,因此在进行格式化分区之前,先if (part->sec_cnt != 0) 判断分区是否存在,这是通过分区的sec_ent是否为0来判断的,之所以可以用此变量来判断,原因是分区part所在的硬盘作为全局数组channels的内嵌成员,全局变量会被初始化为0。 我们在扫描分区表的时候会把分区的信息写到part中,因此只要分区不存在,分区part中任意成员的值都会是0,只是我们这里用 sec_cnt 来判断而已。 然后开始读分区的超级块,目的是获取超级块中的魔数。 在if (sb_buf->magic == 0x19590318)若判断出魔数为 0×19590318,就表示已经有文件系统,不再去格式它。
修改/kernel/init.c,添加上文件的初始化
#include "init.h"
#include "print.h"
#include "interrupt.h"
#include "timer.h"
#include "memory.h"
#include "thread.h"
#include "console.h"
#include "keyboard.h"
#include "tss.h"
#include "syscall-init.h"
#include "ide.h"
#include "fs.h"
/* 负责初始化所有模块 */
void init_all() {
put_str("init_all\n");
idt_init(); // 初始化中断
mem_init(); // 初始化内存管理系统
thread_init(); // 初始化线程相关结构
timer_init(); // 初始化 PIT
console_init(); // 初始化终端
keyboard_init(); // 键盘初始化
tss_init(); // tss 初始化
syscall_init(); // 初始化系统调用
intr_enable(); // 后面的 ide_init 需要打开中断
ide_init(); // 初始化硬盘
filesys_init(); // 初始化文件系统
}
硬盘dreamsFS.img中分区较多,包括1个主分区、5个逻辑分区,输出信息挺多,这里展示最后两个分区格式化信息。

不过当下一次运行时,由于已经有了文件系统,因此不会再进行格式化工作。
运行结果如图所示:

3.挂载分区
Linux内核所在的分区是默认分区,自系统启动后就以该分区为默认分区,该分区的根目录是固定存在的,要想使用其他新分区的话,需要用mount命令手动把新的分区挂载到默认分区的某个目录下,尽管其他分区都有自己的根目录,但是默认分区的根目录才是所有分区的父目录,挂载分区之后,分区不用的时候还可以通过umount命令卸载。
其实要想把操作系统安装到文件系统上,必须在实现内核之前先实现文件系统模块,至少得完成写文件的功能,然后把操作系统通过写文件功能写到文件系统上,无论是安装Windows,还是Linux,安装过程中都是先选择安装到哪个外区上,然后选择以什么文件系统来格式化该分区,Windows系统通常是fat32或ntfs,Linux系统通常是ext2或ext3,在为该分区格式化出文件系统之后才开始正式的安装,最终操作系统就被安装到某种文件系统上了。
不过这里简单点,直接选择待操作的分区。挂载分区的实质是把该分区文件系统的元信息从硬盘上读出来加载到内存中,这样硬盘资源的变化都用内存中元信息来跟踪,如果有写操作,及时将内存中的元信息同步写入到硬盘以持久化。
修改/fs/fs.c
struct partition* cur_part; // 默认情况下操作的是哪个分区
/* 在分区链表中找到名为part_name的分区,并将其指针赋值给cur_part */
static bool mount_partition(struct list_elem* pelem, int arg) {
char* part_name = (char*)arg;
struct partition* part = elem2entry(struct partition, part_tag, pelem);
if (!strcmp(part->name, part_name)) {
cur_part = part;
struct disk* hd = cur_part->my_disk;
/* sb_buf用来存储从硬盘上读入的超级块 */
struct super_block* sb_buf = (struct super_block*)sys_malloc(SECTOR_SIZE);
/* 在内存中创建分区cur_part的超级块 */
cur_part->sb = (struct super_block*)sys_malloc(sizeof(struct super_block));
if (cur_part->sb == NULL) {
PANIC("alloc memory failed!");
}
/* 读入超级块 */
memset(sb_buf, 0, SECTOR_SIZE);
ide_read(hd, cur_part->start_lba + 1, sb_buf, 1);
/* 把sb_buf中超级块的信息复制到分区的超级块sb中。*/
memcpy(cur_part->sb, sb_buf, sizeof(struct super_block));
/********** 将硬盘上的块位图读入到内存 ****************/
cur_part->block_bitmap.bits = (uint8_t*)sys_malloc(sb_buf->block_bitmap_sects * SECTOR_SIZE);
if (cur_part->block_bitmap.bits == NULL) {
PANIC("alloc memory failed!");
}
cur_part->block_bitmap.btmp_bytes_len = sb_buf->block_bitmap_sects * SECTOR_SIZE;
/* 从硬盘上读入块位图到分区的block_bitmap.bits */
ide_read(hd, sb_buf->block_bitmap_lba, cur_part->block_bitmap.bits, sb_buf->block_bitmap_sects);
/*************************************************************/
/********** 将硬盘上的inode位图读入到内存 ************/
cur_part->inode_bitmap.bits = (uint8_t*)sys_malloc(sb_buf->inode_bitmap_sects * SECTOR_SIZE);
if (cur_part->inode_bitmap.bits == NULL) {
PANIC("alloc memory failed!");
}
cur_part->inode_bitmap.btmp_bytes_len = sb_buf->inode_bitmap_sects * SECTOR_SIZE;
/* 从硬盘上读入inode位图到分区的inode_bitmap.bits */
ide_read(hd, sb_buf->inode_bitmap_lba, cur_part->inode_bitmap.bits, sb_buf->inode_bitmap_sects);
/*************************************************************/
list_init(&cur_part->open_inodes);
printk("mount %s done!\n", part->name);
/* 此处返回true是为了迎合主调函数list_traversal的实现,与函数本身功能无关。
只有返回true时list_traversal才会停止遍历,减少了后面元素无意义的遍历.*/
return true;
}
return false; // 使list_traversal继续遍历
}函数moun_partition是list_traversal的回调函数,其接受两个参数,pelem是list_traversal传给它的列表中的元素,在此处pelem是分区partition中的part_tag的地址。arg是待比对的参数,此处是分区名。函数功能是在分区链表中找到名为part_name的分区,并将其指针赋值给cur_part。 cur_part用来记录默认操作的分区。
char* part_name = (char*)arg将arg还原为字符指针,赋值给part_name。
struct partition* part = elem2entry(struct partition, part_tag, pelem)将pelem通过宏elem2entry还原为分区part
if (!strcmp(part->name, part_name))通过strcmp比对part_name和part_name,如果相等则找到了该分区,接下来开始加载该分区的元信息。
首先将找到的分区指针part赋值给cur_part,也就是说找到了默认的分区。 然后struct disk* hd = cur_part->my_disk获得分区所在的硬盘hd,hd将作为后续硬盘操作的参数。
struct super_block* sb_buf = (struct super_block*)sys_malloc(SECTOR_SIZE)申请了一扇区大小的内存作为缓冲区sb_buf,sb_buf用于容纳从硬盘读取的1扇区大小的超级块。
cur_part->sb = (struct super_block*)sys_malloc(sizeof(struct super_block))创建分区 cur_part 的超级块 cur_part->sb,为此申请超级块大小的内存,然后赋值给 cur_part->sb。 内存申请成功后,在ide_read(hd, cur_part->start_lba + 1, sb_buf, 1)读入超级块到 sb_buf,memcpy(cur_part->sb, sb_buf, sizeof(struct super_block))将缓冲区sb_buf中超级块的信息复制到 cur_part->sb,这么做的原因是超级块虽然占一扇区,但我们的 struct super_block 结构并没有一扇区大小,所以只把超级块中有用的信息复制过来,那些用于填充的超级块的数组“uint8_t pad[460]”就不要了。
cur_part->block_bitmap.bits = (uint8_t*)sys_malloc(sb_buf->block_bitmap_sects * SECTOR_SIZE)为当前分区 cur_part 的块位图申请内存,内存大小是超级块中的 block_bitmap_sects 乘以SECTOR_SIZE。
cur_part->block_bitmap.btmp_bytes_len = sb_buf->block_bitmap_sects * SECTOR_SIZE初始化块位图的btmp_bytes_len,用块位图占用的扇区 数block_bitmap_sects乘以扇区大小SECTOR_SIZE的值作为位图btmp_bytes_len的值。
ide_read(hd, sb_buf->block_bitmap_lba, cur_part->block_bitmap.bits, sb_buf->block_bitmap_sects)将硬盘上的块位图读入到内存。也就是读到cur_part->block_bitmap.bits。完成了块位图的加载。 分区越大。块位图占用的内存就越多。因此物理内存一定要匹配。不要出现物理内存太小而分区太大。连块位图都容纳不了的情况
接着是将inode位图读入到内存,与载入块位图同理。
list_init(&cur_part->open_inodes)初始化分区的open_inodes列表。
继续修改/fs/fs.c
/* 在磁盘上搜索文件系统,若没有则格式化分区创建文件系统 */
void filesys_init() {
uint8_t channel_no = 0, dev_no, part_idx = 0;
/* sb_buf用来存储从硬盘上读入的超级块 */
struct super_block* sb_buf = (struct super_block*)sys_malloc(SECTOR_SIZE);
if (sb_buf == NULL) {
PANIC("alloc memory failed!");
}
printk("searching filesystem......\n");
while (channel_no < channel_cnt) {
dev_no = 0;
while(dev_no < 2) {
if (dev_no == 0) { // 跨过裸盘hd60M.img
dev_no++;
continue;
}
struct disk* hd = &channels[channel_no].devices[dev_no];
struct partition* part = hd->prim_parts;
while(part_idx < 12) { // 4个主分区+8个逻辑
if (part_idx == 4) { // 开始处理逻辑分区
part = hd->logic_parts;
}
/* channels数组是全局变量,默认值为0,disk属于其嵌套结构,
* partition又为disk的嵌套结构,因此partition中的成员默认也为0.
* 若partition未初始化,则partition中的成员仍为0.
* 下面处理存在的分区. */
if (part->sec_cnt != 0) { // 如果分区存在
memset(sb_buf, 0, SECTOR_SIZE);
/* 读出分区的超级块,根据魔数是否正确来判断是否存在文件系统 */
ide_read(hd, part->start_lba + 1, sb_buf, 1);
/* 只支持自己的文件系统.若磁盘上已经有文件系统就不再格式化了 */
if (sb_buf->magic == 0x19590318) {
printk("%s has filesystem\n", part->name);
} else { // 其它文件系统不支持,一律按无文件系统处理
printk("formatting %s`s partition %s......\n", hd->name, part->name);
partition_format(part);
}
}
part_idx++;
part++; // 下一分区
}
dev_no++; // 下一磁盘
}
channel_no++; // 下一通道
}
sys_free(sb_buf);
/* 确定默认操作的分区 */
char default_part[8] = "sdb1";
/* 挂载分区 */
list_traversal(&partition_list, mount_partition, (int)default_part);
}其实只是在最后添加了两行代码。
default_part为默认分区的名称,其值为sdb1,也就是说我们默认操作的分区 是sdb1。 分区挂载是借助函数list_traversal完成的,list_traversal(&partition_list, mount_partition, (int)default_part)功能相当于用mount_partition(default_part)处理每一个分区。 其中partition_list是所有分区的列表,(int)default_part将数组地址转换成整型作为mount_partition的参数,list_traversal其功能是遍历plist中所有元素,直到func(arg)返回true 或者列表元素全部遍历结束。
执行结果如下:
不过看不出什么变化,只在最下面打印出”mount sdbl done!”

4.参考
郑钢著操作系统真象还原
田宇著一个64位操作系统的设计与实现
丁渊著ORANGE’S:一个操作系统的实现


