《操作系统真象还原》chapter14/文件系统与遗憾

First Post:

Last Update:

写在前面

本章没能看完,有些可惜。主要是因为寒假结束,没有那种能够安静看书的时间了,所以最后两章我的阅读效率下降的很快;另外还是因为本书已经快要看完了,心态有点浮躁,实在不适合继续看下去了,于是本章笔记只对本章前半部分做了相对详细的笔记,但后半部分笔者没能读下去,所以肯定是不足的。

以后若有时间的话,希望能把这本书完整读完吧。

勘误

本书P600页存在一个表述错误,特此摘出:

“它被固定储存在各分区的第2个扇区,通常在占用一个扇区的大小。”

此处“它”是指超级块。

该表述不够严谨,在上一章中我们曾留意到:

  • 对于主分区,其开始的第一个磁道会被OBR占用,而OBR的大小不一定只占用一个扇区。在EXT4文件系统中,该OBR会占用两个扇区,所以该文件系统中的超级块存在于1024偏移处,也就是从第三个扇区开始
  • 对于总拓展分区,每个子拓展分区的开始是EBR,EBR和MBR是同构的。子拓展分区里的每个逻辑分区就相当于主分区,它们也都在相似的地方存在OBR,在EXT4文件系统中,超级块也都在1024偏移处

综上,超级块的具体位置应该是和文件组织结构本身有关的,EXT4和FAT32等等各不相同的结构有各不相同的结果,本书在这方面没有表述清楚(注:从EXT2开始,引导块就占两个扇区1Kb大小了,至于FAT32是不是这样,笔者并没有查过)。

不过本书的实现中,接下来会默认引导块只占用一个扇区,超级块从第二个扇区开始。

https://akaedu.github.io/book/ch29s02.html

前言

本章虽然实现了一个简易的文件系统,不过它并没有实用性,只能用作理解掌握文件系统根本原理。最终完成格式化的硬盘并没有泛用性,属于是专属于该操作系统的硬盘了(当然,我没有说这样做不好,倒不如说这样做帮大忙了。所以只是提个醒,不要以本章实现的文件系统为准,只需要理解其原理即可)。

文件系统

总结一下文件系统的几个要点吧:

  • 操作系统为整个文件系统提供了inode结构体,每个文件对应一个inode。该结构体标识了文件属性和文件数据指针等内容。
  • 操作系统为所有文件维护了一个inode数组,访问文件的具体数据通过inode编号的下表直接寻址
  • 同时,对于任何一个文件,操作系统为其确定固定的结构体条目,该结构体中包含了文件名、文件大小、文件类型、inode编号等
  • 对于“目录文件”类型,这类文件的inode文件的数据指针处存放的是目录下其他文件的文件结构条目
  • 所有的文件都会被挂载在根目录下

然后是操作系统的文件访问逻辑:假定目前文件系统已经完全初始化完成了

  • 首先由用户提供文件名
  • 操作系统根据该文件从根目录开始递归查询
  • 首先会访问根目录的数据区,该数据区存放了根目录下其他文件的结构条目,对比每个条目中的文件名和请求文件名
  • 若存在该文件,那么直接从条目中获取inode编号,通过inode编号得到文件对于数据区的指针
  • 若不存在该文件,则通过该目录下其他“目录类型”的文件继续递归查找,直到全目录搜索完毕或找到同名文件为止

当然,上面描述的寻址有些简单粗暴,因为我们一般都会界定寻址的范围和开始的目录,很少从根目录就开始查询。并且,一般的系统都支持在不同的目录下运行同名文件出现,并且我们往往只在一层目录中寻找文件。

不过概念上有些不同的是,现在我们通常描述的“文件名”其实是包括了父目录以后的文件名,比如“C:/file.txt”;而本书中所说的文件名就是单独所指的文件名,比如“file.txt”。前者属于更高级一点的概念,还是要做一点区分的。

上述的两个结构体如下:有一定删减

1
2
3
4
5
6
//文件结构条目(有删减)
struct dir_entry {
char filename[MAX_FILE_NAME_LEN]; // 普通文件或目录名称
uint32_t i_no; // 普通文件或目录对应的inode编号
enum file_types f_type; // 文件类型
};
1
2
3
4
5
6
7
8
9
10
11
/* 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;
};

其中,i_sectors指针是针对文件较大需要分散存放的文件设计的。硬盘的储存单位是“块”,对于一个块存放不下的文件,会指定其他块进行存放,为了寻址其他块,在inode结构体中通过一系列数组来记录每个块的指针。

然后就是构建文件系统了。格式化硬盘的函数就在本段下面,不过在看之前还是先听笔者唠叨几句吧。

一般来讲,我们现在装Linux都是先把硬盘(当然一般是U盘)格式化以后,写入操作系统镜像的。这个格式化其实就是在为硬盘创建文件系统。本书也说明了,现代的操作系统一般是先格式化硬盘,然后再初始化操作系统自己的,所以笔者认为,本来的话,文件系统是不需要操作系统的范畴的,因为操作系统不负责文件系统的构建,那是在制作启动盘的时候就完成的事情。

本章的作者是自己去创建文件系统,而不是通过工具生成一块具有泛用性的磁盘文件,而是自己去仿造了一个类似的文件系统。虽然略感可惜,但对于笔者这样的初学者来说确实是帮大忙了。唠叨就到这里,下面是格式化函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
/* 格式化分区,也就是初始化分区的元信息,创建文件系统 */
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);
}

磁盘内容分布如下:

第一扇区中存在一个Boot Block,也就是EBR或者MBR,然后紧跟着的是占用一个扇区的超级块,其结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* 超级块 */
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

该超级块中表明了分区的扇区数、inode数、根目录位置等信息。通过magic来确定文件系统类型或判断是否存在文件系统。

在ide通道初始化完成以后,操作系统就已经获得了有关磁盘和分区的主要信息。但分区并没有建立文件系统。

filesys_init函数负责从ide通道中获取每个分区,然后通过partition_format函数初始化每个分区。

partition_format函数首先初始化超级块,然后是块位图以及inode位图,再之后初始化inode数组和根目录。

  • 这里插入一点关于rootfs的内容。其实现在所实现的根目录就是一个简化版的rootfs。
  • 真正的rootfs在内核开启时第一个被挂载,由它提供根目录‘/’,并从该目录下会加载出一些初始化脚本和服务到内存,init进程也运行在根目录文件系统上。
  • https://cloud.tencent.com/developer/article/1791275

插画ID:1134778859747008514(tw)