操作系统真象还原实验记录之实验二十四:创建并挂载文件系统
1.文件系统基础知识
一个分区就对应着windows下的一个盘,
一开始购买的笔记本,第一次开机后会发现只有一个c盘,意味着这个盘有一块固定的地方永远存着操作系统,loader.s知道os的位置,整个盘只有一个分区,mbr、loader.s也已经被填入这个分区对应位置,同时这个分区文件系统已经创建同时被挂载。这个盘也被称为启动盘。
因此要把裸盘变成一个启动盘,里面的MBR.s、loader.s需要提前写入,os要提前刻在盘中,刻的位置要和loader.s配合好。然后分区表也要提前写好,也就是一个分区表项。
本操作系统启动盘是seven60.img,有MBR.S,loader.s,但是未分区。
另一个盘seven80.img有分区。
未分区意味着没法创建文件系统。所以seven60.img只能用来跑和存操作系统,创建seven80.img的文件系统。
对这个c盘分区成d盘、e盘、f盘,意味着修改了分区表,增加了三个分区,同时每个分区创建了文件系统,而且都挂载了,也就是它们的超级块都在内存。
所以c、d、e、f用的虽然是同一个文件系统windows,但是里面的实时数据是完全不一样的,也就是他们的超级块是一样的,操作inode的内核函数是一样的,但是各种位图,inode数组是不一样的。所以c盘里的文件和d盘是完全隔离的。
创建文件系统,也就是填写每个分区的超级块,以及根目录的两个目录项。
每个分区都可以有自己的操作系统、文件系统。
根目录文件地址规定死,记录在超级块中。
目录文件里有目录项,一个目录项对应一个文件,它的inode指针就是指向该文件。
这个文件可以是目录文件,里面继续装FCB,也可以是记事本这样的真正的文件。
inode里面有直接、一级、二级等索引指针,指向磁盘扇区这几乎是408必考。
2.创建文件系统
创建文件系统本质就是填写上述图的相关元信息,
本次实验创建了1个主分区,5个逻辑分区的文件系统,
也就是在内存写好了超级块结构体所有信息,
空闲块位图出了第一个位”置1“用作根目录,最后一个扇区多余的部分全部置1,其余全部置0,
inode位图只占一个扇区,共4096个inode结点,所以只需要第一位置1表0号inode结点指向根目录即可,
inode数组写好0号inode结点即可,
最后依次写入磁盘,
机完成创建文件系统。
2.1 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
2.2 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;
};
#endif
假如根目录下有一个文件一个文件夹
文件和文件夹(目录)都一定有一个它上一级的目录项dir_entry,一个与它对应的inode
dir_entry里面有它的名字、类型和它在inode数组的索引i_no,它的inode有具体存储它的扇区地址
当它是目录时,存的就是下一级的目录项dir_entry
初始化根目录占第一个扇区,里面存有两个下一级目录项“./”和". ./",均指向0号inode,也就是根目录本身。
根目录没有上一级目录项,但是有0号inode。
2.3 dir.h
#ifndef __FS_DIR_H
#define __FS_DIR_H
#include "stdint.h"
#include "inode.h"
#include "fs.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
2.4 fs.h
#ifndef __FS_FS_H
#define __FS_FS_H
#include "stdint.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 // 目录
};
#endif
2.5 fs.c之partition_format
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);
}
partition_format这个函数用于在内存填写超级块、空闲块位图、inode位图,inode数组。然后写入磁盘。
超级块、inode位图大小都是1个扇区,可以用作在partition_format里声明局部变量,这样他们占用的就是栈空间,勉强够用。
但是空闲块位图有几百扇区,所以要用sys_malloc来申请内存。
超级块结构体的填写中,空闲块位图大小计算逻辑如下:
一个分区最多4096个文件,所以支持4096个inode结点,这样inode位图大小就是1扇区、inode数组大小也固定了。
根目录属于空闲块区域。
所以剩余空闲块数a就等于分区总块数 - 1 - 1 - 1- inode数组块数。
以a再来计算空闲块位图大小A
A = a / 4096 向上取整。
这样A一定会有多余位。
再用a - A得到剩余空闲块数b
再利用b来计算空闲块位图大小B
B = b / 4096 向上取整。
B也会有多余位。
综上,
也就是说,把b当成可用空闲块数,把B当成空闲块位图。
显然b + B < a的,会有一点浪费,但是B仍然会有多余位需要处理成1。
处理完超级块,还初始化了inode位图第一位,inode数组第一个指向根目录起始扇区地址。然后填写了根目录两个
目录项 “.” 、“…”。
2.6 filesys_init
#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"
#include "file.h"
#include "console.h"
#include "keyboard.h"
#include "ioqueue.h"
/* 在磁盘上搜索文件系统,若没有则格式化分区创建文件系统 */
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);
}
遍历所有通道(本次实验就一个)
跳过Seven60.img主盘,遍历了Seven80.img的所有分区,分别建立了文件系统并打印。
每个分区先ide_read读出第二个扇区即超级块,如果超级块魔数存在,表示超级块存在,那么文件系统存在,否则调用partition_format来给分区创建文件系统,完成分区的格式化。
2.7 实验结果
第二次运行,读每个分区的超级块于内存,发现超级块已经有了魔数,
说明所有的分区信息已经写入磁盘,也就是文件系统已经创建。
3. 挂载分区
正常情况下安装操作系统,无论是Windows还是Linux,安装的过程都是先选择将os安装到哪一个分区上,然后选择以什么文件系统来格式化该分区。Windows通常是fat32、ntfs;Linux通常是ext2或ext3。该分区格式化出文件系统后才能安装操作系统。
我们实验的Seven.img没有文件系统,loader.s就开始安装os了。
因此这次实验挂载分区,只需要选择待挂载的分区,将该分区文件系统的元信息从硬盘上读到内存,如果有写操作,还要及时将元信息同步写回磁盘。
3.1 filesys_init增加
void filesys_init(){
...略
/* 确定默认操作的分区 */
char default_part[8] = "sdb1";
/* 挂载分区 */
list_traversal(&partition_list, mount_partition, (int)default_part);
}
首先回顾一下list_traversal
struct list_elem* list_traversal(struct list* plist, function func, int arg) {
struct list_elem* elem = plist->head.next;
/* 如果队列为空,就必然没有符合条件的结点,故直接返回NULL */
if (list_empty(plist)) {
return NULL;
}
while (elem != &plist->tail) {
if (func(elem, arg)) { // func返回ture则认为该元素在回调函数中符合条件,命中,故停止继续遍历
return elem;
} // 若回调函数func返回true,则继续遍历
elem = elem->next;
}
return NULL;
}
list_traversal,遍历plist中所有元素,直到func(elem, arg)返回true。
在这次试验,就是遍历5个分区,将每个分区指针作为elem,默认分区名"sdbl"作为mount_partition的参数。
3.2 mount_partition函数
struct partition* 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继续遍历
}
mount_partition函数
如果该分区pelem的分区名不是默认分区名,那么继续遍历,否则让全局变量cur_part这个分区指针指向该分区,并开始挂载。
回忆一下分区结构
/* 分区结构 */
struct partition {
uint32_t start_lba; // 起始扇区
uint32_t sec_cnt; // 扇区数
struct disk* my_disk; // 分区所属的硬盘
struct list_elem part_tag; // 用于队列中的标记
char name[8]; // 分区名称
struct super_block* sb; // 本分区的超级块
struct bitmap block_bitmap; // 块位图
struct bitmap inode_bitmap; // i结点位图
struct list open_inodes; // 本分区打开的i结点队列
};
我们知道,主板一共两个通道,一个通道两个硬盘。
我们的实验模拟了一个通道,主盘Seven.img未分区,从盘Seven80.img有一个主分区,5个逻辑分区。
然后,我们的内存是定义了通道、磁盘、分区的结构体的。并且有全局变量通道数组
上次实验,我们对内存的每个分区都调用了partition_scan,
partition_scan遍历了每个分区的分区表项,
将五个分区结构体里面的前五个成员变量进行了初始化。
所以剩下三个:
struct super_block* sb; // 本分区的超级块
struct bitmap block_bitmap; // 块位图
struct bitmap inode_bitmap; // i结点位图
还未填写。
其实上述三个信息,在创建文件系统中已经记录在了磁盘上每个分区中。
所以这次挂载文件系统,就是读出默认分区的信息,填写内存中默认分区结构体这三个元素。
也就是说,内存里六个分区结构体,哪一个分区结构体剩下三个信息被补充完整,哪个分区的文件系统就被挂载。
要补充这三个元素:
mount_partition申请了三块内存存储超级块、空闲块位图、inode结点位图。
最后被挂载的分区,是由全局变量cur_part指明的。