Linux0.11 通过文件来使用磁盘的过程涉及到了许多概念,以及概念之间复杂错乱的关系,容易让人困惑,本章主要是对 Linux0.11 通过文件来使用磁盘的过程进行梳理。《Linux内核完全剖析——基于0.12内核》的第12章:文件系统,对这些概念及其之间的关系进行了相当精彩的分析,推荐大家阅读。当然本章分析的也还算精彩吧,也推荐大家阅读 ( ̄▽ ̄)~*
1 从生磁盘到文件
上一章分析了生磁盘的使用方法,即通过盘块号使用磁盘。但许多人连扇区都不知道是什么? 要求他们根据盘块号来访问磁盘显然是不合适的。我们需要在盘块上引入更高一层次的抽象概念——文件,人们通过文件来访问磁盘中的字符序列就会感觉自然多了。用户在打开一个文件前,先向操作系统提供该文件的文件名,然后操作系统根据文件名找到文件存放的盘块位置,最后操作系统将文件内容(字符序列)显示在用户的眼前。
至此用户眼中的文件就变成了一串字符序列,用户可以随意访问修改这串字符序列,并且用户也不用去关心这串字符序列是怎么存放的。而操作系统眼中的文件就变成了字符序列到盘块集合的映射,即文件建立起了字符序列到盘块集合的映射关系。
2 文件名与盘块的映射
引入文件是对使用磁盘的抽象。为了实现这层抽象,操作系统需要根据用户提供的文件名找到文件存放的盘块位置,因此操作系统需要建立一个能够完成从文件名到盘块映射的数据结构。暂且把这个数据结构叫做 FCB 吧。
2.1 连续结构
连续结构是一种最自然想到的结构,就像需要存放一堆数据的时候,我们首先会想到用数组是一样的。顺序结构如下:
从图中可以看出用户要打开文件名为 test.c 的文件,该文件存放在6、7、8号盘块,共占用3个盘块。建立这样的结构体也不复杂,这里不再多说。
2.2 索引结构
同样是打开 test.c ,索引结构如下:
索引结构需要在磁盘中留出一个单独的索引块,用于存放索引,而这些索引指向的就是文件存放在磁盘中的盘块位置。从图中可以看出 test.c 文件按顺序存放在磁盘的第9号、17号、1号、10号盘块中。建立这样的FCB的结构体同样也不复杂。
2.3 多级索引结构
多级索引结构是 Linux0.11 实际使用的一种结构。这里首先需要介绍一个“鼎鼎大名”的数据结构——inode(index node)。inode 是操作系统中定义的一个结构体,inode 中有一个数组成员 short i_zone[9]
里面记录了文件所占用的盘块号的信息,操作系统会将 inode 的前32个字节存放磁盘中, short i_zone[9]
就在这32个字节之中:
从图2.3中可以看出,i_zone[0 - 6] 为直接块号,它们直接指向数据块(这里我们暂时把数据块理解为真正存放着文件数据的盘块吧,把数据块与索引块区分开来)。i_zone[7] 为一次间接块号,它指向了一个一次间接块(没错就是索引结构中的那个索引块),一次间接块又指向了一堆数据块。可以猜一下一次间接块可以存放多少个索引呢?因为 i_zone[n] 为 short 类型占2字节,而一个盘块的大小为 1kB, 因此一次间接块可以存放 512 个索引。i_zone[8] 为二次间接块号,原理与 i_zone[7] 差不多,只不过多了一级。当文件的大小小于7个盘块时,只需要使用 i_zone[0 - 6] 就好,若大于则需要使用 i_zone[7] 甚至 i_zone[8] 了。这样我们就可以表示很大的文件了,并且对于很小的文件也可以高效访问,对于中等大小的文件访问速度也不慢。这里只是粗浅的介绍了一下 inode ,关于 inode 更多的介绍可以参考《Linux内核完全剖析——基于0.12内核》的第12章:文件系统。
的确 i_zone[ ] 就已经形成多级索引结构了,但我们需要通过文件名来获取文件。我们的 FCB 还没有建立。多级索引结构已经有了,我们只需要把多级索引结构和文件名关联起来,我们的 FCB 就建好了:
把图2.4做成结构体:
struct dir_entry {
unsigned short inode; //i节点号
char name[NAME_LEN]; //文件名
};
没错,这个就是“目录项”结构体!
再次回顾一下基于多级索引结构通过文件使用磁盘的过程。用户在打开一个文件前,先向操作系统提供该文件的文件名,然后操作系统根据文件名找文件对应的目录项。目录项中存放着 i 节点号,因此操作系统可以从磁盘中将文件的 inode 读入内存,然后通过 inode 中的 i_zone[] 字段找到文件对应的盘块号,最后通过盘块号访问磁盘,获取文件内容,并将文件内容显示在用户眼前。
3 文件使用磁盘的实现
本节以对文件进行写操作为线索,分析 Linux0.11 通过文件使用磁盘部分程序。
(1)调用 open() 打开文件
在读写一个文件前,都需要先调用 open() 打开文件,open() 的核心是根据文件名,找到对应的 inode ,并读入 inode。
int open(const char * filename, int flag, ...)
内核使用文件结构 file ,文件表 file_table,和内存中 i 节点表 inode_table 来管理对文件的访问操作。open() 会找出 inode_table[NR_INODE]
的一个空闲项,并将读入的 inode 存放在空闲项中 。然后在找出 file_table[NR_FILE]
的空闲项,填充该空闲项,并让空闲项的 f_inode 字段指向读入的 inode 。最后寻找 filp[NR_OPEN]
中的空闲项,并让该空闲项指向前面刚填充好的file_table[NR_FILE]
中的那个项。open() 的返回值就是刚刚那个 filp[NR_OPEN]
的空闲项的索引。
struct file {
unsigned short f_mode;
unsigned short f_flags;
unsigned short f_count;
struct m_inode * f_inode; //文件 inode
off_t f_pos; // 文件当前读写指针位置(文件光标位置)
};
struct task_struct {
......
struct file * filp[NR_OPEN]; // PCB中的文件指针数组,其中 NR_OPEN = 20
......
};
struct file file_table[NR_FILE]; // 文件表,NR_FILE = 64
struct m_inode inode_table[NR_INODE]; // i 节点表, NR_INODE = 32
进程打开文件时使用的内核数据结构如下:
到此 open() 的工作就结束了。
(2)调用 write()向文件写
根据系统调用,实际上 write() 的功能时通过 sys_write() 实现的。sys_write() 将利用前面 open() 的返回值,得到文件的 inode。
//参数 fd 是 open() 的返回值
int sys_write(unsigned int fd,char * buf,int count)
{
struct file * file;
struct m_inode * inode;
if (fd>=NR_OPEN || count <0 || !(file=current->filp[fd])) //获取 file
return -EINVAL;
if (!count)
return 0;
inode=file->f_inode; //获取文件的 inode
......
if (S_ISREG(inode->i_mode)) //判断文件类型是否是普通文件
return file_write(inode,file,buf,count); //该文件为普通文件,调用 file_write() 进行处理
printk("(Write)inode->i_mode=%06o\n\r",inode->i_mode);
return -EINVAL;
}
sys_write() 并没有直接处理普通文件,而是交给了 file_write() 进行处理。
(3)调用 file_write() 继续解析
file_write() 共有4个参数,其中 inode 中有最重要的文件所在的盘块位置信息,filp 中存放着文件光标位置,通过它可以确定文件修改的位置,buf 为写入的内容,count 为写入内容的字节数。通过 filp 和 count 可以确认文件修改的范围。需要注意的是,file_write() 并不是直接将数据写入盘块中,而是写入缓冲区中。
int file_write(struct m_inode * inode, struct file * filp, char * buf, int count)
{
off_t pos;
int block,c;
struct buffer_head * bh;// struct buffer_head 是缓冲开头结构体
char * p;
int i=0;
if (filp->f_flags & O_APPEND)
pos = inode->i_size; //如果是“追加” 则从文件的末尾开始写入
else
pos = filp->f_pos; //否则从文件光标位置开始修改,filp->f_pos中记录了文件光标的位置
while (i<count) {
//BLOCK_SIZE = 1024(一个盘块的大小),pos/BLOCK_SIZE 就是当前修改位置所在的盘块相对于文件第一个盘块的盘块位置
//create_block() 用于获取盘块号,create_block() 的返回值是 pos 所在盘块的盘块号
if (!(block = create_block(inode,pos/BLOCK_SIZE)))
break;
// bread() 会调用 ll_rw_block(), 而 ll_rw_block()又会调用 make_request() ,然后就是上章讲的内容了。
if (!(bh=bread(inode->i_dev,block)))
break;
c = pos % BLOCK_SIZE;//计算出 pos 在盘块中的相对位置
p = c + bh->b_data; //p指向缓冲块中开始写入数据的位置
bh->b_dirt = 1;
c = BLOCK_SIZE-c;
if (c > count-i) c = count-i;
pos += c; //修改 pos
if (pos > inode->i_size) {
inode->i_size = pos;
inode->i_dirt = 1;
}
i += c;
while (c-->0)
*(p++) = get_fs_byte(buf++); //获取要写入的内容
brelse(bh);
}
......
}
(4)create_block() 分析
create_block() 中调用了 _bmap() 。_bmap() 的第3个参数为 1 时表示没有映射时则创建映射,这里的映射是指缓冲区。
int create_block(struct m_inode * inode, int block)
{
return _bmap(inode,block,1);
}
static int _bmap(struct m_inode * inode,int block,int create)
{
struct buffer_head * bh;
int i;
......
if (block<7) { //若block< 7 直接获得一个数据块
if (create && !inode->i_zone[block])
if ((inode->i_zone[block]=new_block(inode->i_dev))) {
inode->i_ctime=CURRENT_TIME;
inode->i_dirt=1;
}
return inode->i_zone[block];
}
//若减去7个数据块后剩余的数据块数小于512,则说明该文件只有一次间接块号。inode->i_zone[7]是一次间接块号
block -= 7;
if (block<512) {
if (create && !inode->i_zone[7])
if ((inode->i_zone[7]=new_block(inode->i_dev))) {
inode->i_dirt=1;
inode->i_ctime=CURRENT_TIME;
}
......
}
//存在二次间接块号,那么继续处理
block -= 512;
if (create && !inode->i_zone[8])
......
}
至此,可能还会有些疑问,比如操作系统如何通过 inode 号找到 磁盘中文件的 inode 的呢?目录项是存在磁盘中还是在系统启动后操作系统在内存中建立目录项的呢?如果是在内存中建立的那么 OS 启动时是怎么知道磁盘中有哪些文件的呢?还有写文件时写入的缓冲区是怎么回事?等等一系列的细节问题。其实不必纠结于操作系统对这些细节是如何实现的,正如李治军老师课程中提到的一种思想:我们能解决这些细节问题吗?我们可以猜一下操作系统如何实现这些细节。
下一章将分析目录与文件系统。
参考
本章内容中的图2.4为自己所画,图2.1、图2.2截取自哈工大操作系统课程的课件,图2.3、图3.1截取自《Linux内核完全剖析——基于0.12内核》。
[1]操作系统_哈尔滨工业大学_中国大学MOOC
[2]《Linux内核完全剖析——基于0.12内核》