本文接着上一博文 Linux 内核学习笔记:进程 1 创建及执行(第 3 部分)最后的“进程 1 格式化虚拟盘并更换根设备为虚拟盘”继续写下去。
进程 1 格式化虚拟盘并更换根设备为虚拟盘
注意:现在系统已经切换到进程 1 执行。
在之前博文 Linux 内核学习笔记:初始化程序(第 1 部分)中,我们提到 Linux 0.11 设置了虚拟盘并对其进行初始化。但那时的虚拟盘只是一块“白盘”,尚未经过类似“格式化”的处理,还不能当做一个块设备使用。格式化所用的信息就在 boot 操作系统的软盘上。这个软盘的第 1 个扇区是 bootsect,后面 4 个扇区是 setup,接下来大约 240 个扇区是包含 head 的 system 模块。“格式化”虚拟盘的信息从 256 块开始。
下面,进程 1 调用 rd_load 函数,用软盘上 256 块以后扇区中的信息“格式化”虚拟盘,使之成为一个块设备:
1 | // kenel/blk_drv/hd.c -------------------------- |
调用 rd_load
rd_load 函数定义如下:
1 | // kenel/blk_drv/ramdisk.c --------------------- |
**breada **函数的定义如下:
1 | // linux/buffer.c ------------------------------ |
至此,整个文件系统已经完全读入虚拟盘。
MINIX 文件系统
Linux 0.11 内核使用的是 MINIX 文件系统 1.0 版本。
MINIX 文件系统与标准 UNIX 的文件系统基本相同。它由 6 个部分组成。对于一个完整的文件系统,其各部分在软盘上的分布如下图所示:
图片来源:《Linux 内核完全注释》
上图中,整个磁盘被划分为以 1KB 为单位的磁盘块。如果整个文件系统占用了 360KB 的空间,那意味着该文件系统共占用了 360 个磁盘块。图中每个方格表示一个磁盘块。在 MINIX 1.0 文件系统中,其磁盘块大小与逻辑块大小正好是一样的,也是 1KB。因此也可以说该文件系统占用了 360 个逻辑块。从这个角度上来看,逻辑块指的就是磁盘块。
对于引导块(可作为操作系统引导块,在这里可以不管。另外,一般只有引导块的第 1 个扇区,即引导扇区有用),MINIX 文件系统需要在块设备上空出一个逻辑块用以存放它,以保持 MINIX 文件系统格式的统一。引导块是第 1 块,编号为 0,其他磁盘块编号从左到右依次递增。
对于硬盘块设备,通常在其上会划分出几个分区,每个分区都可当作一个设备,并且每个分区都可以存放一个不同的完整文件系统
,如下图所示。硬盘的第 1 个扇区是主引导扇区,其中存放着硬盘引导程序和分区表信息。分区表中的信息指明了硬盘上每个分区的类型、在硬盘中起始位置参数和结束位置参数以及占用的扇区总数。
图片来源:《Linux 内核完全注释》(编辑过)
超级块结构
MINIX 文件系统超级块结构 super_block 用于表示文件系统结构,其定义如下:
1 | // include/linux/fs.h -------------------------- |
关于超级块结构详细解释如下:
图片来源:《Linux 内核完全注释》
逻辑块位图
逻辑块位图用于描述软盘上文件系统"数据区"的每个磁盘块(也即逻辑块)的使用情况。
除第 1 个比特位(位 0)以外,逻辑块位图中每个比特位依次代表”数据区”中的一个逻辑块。当一个”数据区”的逻辑块被占用时,则逻辑块位图中相应的比特位被置 1。
一个逻辑块位图占用一个逻辑块,共 8*1024=8192 位,共可描述 8192-1=8191 个”数据区”的逻辑块。
逻辑块位图可占用多个逻辑块,由 s_zmap_blocks
确定。
i 节点位图
i 节点位图用于说明 i 节点是否被使用,同样是每个比特位代表一个 i 节点。
第 1 个比特位不用,共可表示 8191 个 i 节点的使用情况。
i 节点位图可占用多个逻辑块,由 s_imap_blocks
确定。
i 节点
i 节点英文是 inode。i 节点是储存文件元信息的结构,每一个文件都有对应的 i 节点。
目录也是文件,也有对应的 i 节点。
i 节点结构体
i 节点定义如下:
1 | // include/linux/fs.h -------------------------- |
关于 i 节点结构详细解释如下:
图片来源:《Linux 内核完全注释》
注:
- 当所有 i 节点都被使用时,查找空闲 i 节点的函数会返回 0,因此 i 节点位图最低比特位和
i 节点 0 都不使用
。 - 存在于磁盘的 i 节点(d_inode)长度为 16 字节,所以
一个逻辑块可以存放 1024/16 = 64 个 i 节点
。属性字段 i_mode
i 节点属性字段 i_mode 内容如下:
图片来源:《Linux 内核完全注释》
逻辑块号数组 i_zone
文件中的数据是放在磁盘块中的数据区
中的,而一个文件名则通过对应的 i 节点与这些(数据区中的)磁盘块相联系。盘块号的编号从 0 号(引导块)开始
,数据区的盘块编号从“引导块所占用盘块数 + 超级块所占用盘块数 + i 节点位图所占用盘块数 + 逻辑块位图所占用盘块数 + i 节点所占用盘块数”开始
。
i_zone[9] 数组用于存放 i 节点对应文件所在盘块的盘块号
:
- i_zone[0]
i_zone[6] 共能指向 7 个盘块(这些盘块也称为i_zone[6] 就足以表示了。直接块
),所以如果一个文件的大小小于等于 7KB,i_zone[0] - 如果文件大小大于 7KB,那就要用到
一次间接块
,这个间接块由 i_zone[7] 指向。一次间接块存放的并不是文件的数据,而是指向其他盘块的编号。在 MINIX 文件系统,一次间接块可以存放 1024/2 = 512 个盘块编号,也就是说通过这个一次间接块,可以涵盖的数据大小是 512 * 1KB = 512KB。 - 如果文件还要更大(大于 7KB + 512KB = 519KB),就要用到
二次间接块
,这个间接块由 i_zone[8] 指向。二次间接块存放的也不是文件的数据,而是一次间接块的编号。在 MINIX 文件系统,二次间接块可以存放 512 个一次间接块编号。这样,通过二次间接块,可以涵盖的数据达到 512 * 512 * 1KB = 262,144KB。
以上 3 点可用图表示如下:
图片来源:《Linux 内核完全注释》
根 i 节点
文件路径
在操作系统中由目录文件中的目录项管理,一个目录项对应一级路径。目录文件也是文件,也由 i 节点管理。一个文件挂在一个目录文件的目录项上,这个目录文件根据实际路径的不同,又可能挂在另一个目录文件的目录项上。一个目录文件有多个目录项,可以形成不同的路径。效果如下图所示:
图片来源:《Linux 内核设计的艺术》
所有的文件(包括目录文件)的 i 节点最终挂接成一个树形结构,树根 i 节点就叫这个文件系统的根 i 节点,也即上图中的根目录文件 i 节点
。一个逻辑设备(一个物理设备可以分成多个逻辑设备,比如物理硬盘可以分成多个逻辑硬盘)只有一个文件系统,一个文件系统只能包括一个这样的树形结构,也就是说,一个逻辑设备只能有一个根 i 节点。
题外:文件相关知识点
文件结构体
文件结构体定义如下:
1 | // include/linux/fs.h -------------------------- |
文件类型和属性
如果我们在 shell 中键入 ls -l 并回车就可以看到文件的类型和属性,如下图所示:
图片来源:《Linux 内核完全注释》(编辑过)
文件目录项结构
文件目录项存在于目录文件中,它结构定义如下:
1 | // 文件目录项 |
在这里,需要特别清楚的就是目录也是一个文件,有自己的 i 节点,有自己的存储空间,只不过存储的是目录项。
每个目录项长度为 2 + 14 = 16 个字节,所以一个逻辑块可以存放 1024/16 = 64 个目录项。
以打开绝对路径文件 /usr/bin/vi 为例,
- 文件系统首先从根(目录) i 节点(i 节点号为 1)开始搜索,找出文件名为 usr 的目录项,从而得到文件 /usr 的 i 节点
- 因为 /usr 是个目录文件,所以在该目录文件查找文件名为 bin 的目录项,从而得到文件 /usr/bin 的 i 节点号
- /usr/bin 还是一个目录文件,在该目录文件查找文件名为 vi 的目录项,最终得到 /usr/bin/vi 文件的 i 节点号
- 根据 /usr/bin/vi 节点号,找到 vi 文件所在的逻辑块
上述过程可用图表示如下:
对于上图,节点号为 0 的 i 节点应该是不占内存的,画出来只有表示意义。i 节点表实际存放的节点编号应该是:第一个逻辑块 1~64;第 2 个逻辑块 65~128……
因为这可以从 read_node 函数判断出来:
1 | static void read_inode(struct m_inode * inode) |
另外,磁盘与文件系统关系的一个例子如下:
图片来源
硬链接和软链接
硬链接
一般情况下,文件名和 i 节点编号是”一一对应”关系,每个 i 节点编号对应一个文件名。但是,Linux 系统允许多个文件名指向同一个 i 节点编号。这意味着,可以用不同的文件名访问同样的内容;对文件内容进行修改,会影响到所有文件名;但是,删除一个文件名,不影响另一个文件名的访问。
这种情况就被称为硬链接(hard link),硬链接相当于文件的别名。
ln 命令可以创建硬链接:
1 | ln 源文件 目标文件 |
运行上面这条命令以后,源文件与目标文件的 i 节点编号相同,都指向同一个 i 节点。 i 节点结构中有一成员叫做链接数 i_nlinks
,记录指向该 i 节点的文件名总数。在这个例子中,链接数会增加 1。
反过来,删除一个文件名,就会使得 i 节点中的链接数减 1。当这个值减到 0,表明没有文件名指向这个 i 节点,系统就会回收这个 i 节点编号,以及其所对应的逻辑块区域。
这里顺便说一下目录文件的链接数。创建目录时,默认会生成两个目录项:.
和..
。前者的 i 节点编号就是当前目录的 i 节点编号,等同于当前目录的“硬链接”;后者的 i 节点编号就是当前目录的父目录的 i 节点编号,等同于父目录的”硬链接”。所以,任何一个目录的“硬链接”总数,总是等于 2 加上它的子目录总数(含隐藏目录)。
软链接
除了硬链接以外,还有一种特殊情况:文件 A 和文件 B 的 i 节点编号虽然不一样,但是文件 A 的内容是文件 B 的路径。读取文件 A 时,系统会自动将访问者导向文件 B。因此,无论打开哪一个文件,最终读取的都是文件 B。这时,文件 A 就称为文件 B 的软链接(soft link)
或者符号链接(symbolic link)
。这种情况下,文件 A 相当于文件 B 的快捷方式。
这意味着,文件 A 依赖于文件 B 而存在,如果删除了文件 B,打开文件 A 就会报错:”No such file or directory”。这是软链接与硬链接最大的不同:文件 A 指向文件 B 的文件名,而不是文件 B 的inode号码,文件 B 的 i 节点“链接数”不会因此发生变化。
ln -s 命令可以创建软链接。
1 | ln -s 源文文件或目录 目标文件或目录 |
软硬链接区别
由于硬链接是有着相同 i 节点编号、仅文件名不同的文件,因此硬链接存在以下几点特性:
- 文件有相同的 i 节点和数据块;
- 只能对已存在的文件进行创建;
- 不能交叉文件系统进行硬链接的创建;
- 不能对目录进行创建,只可对文件创建;
- 删除一个硬链接文件并不影响其他有相同 i 节点编号的文件。
软链接与硬链接不同,软链接本身就是一个普通文件,只是数据块内容有点特殊。软链接有着自己的 i 节点编号以及用户数据块(见下图)。因此软链接的创建与使用没有类似硬链接的诸多限制: - 软链接有自己的文件属性及权限等;
- 可对不存在的文件或目录创建软链接;
- 软链接可交叉文件系统;
- 软链接可对文件或目录创建;
- 创建软链接时,链接计数 i_nlinks 不会增加;
- 删除软链接并不影响被指向的文件,但若被指向的原文件被删除,则相关软连接被称为死链接(即 dangling link,若被指向路径文件被重新创建,死链接可恢复为正常的软链接)。
根文件系统
加载文件系统最重要的标志,就是把一个逻辑设备上的文件系统的根 i 节点,关联到另一个文件系统的 i 节点上。
具体是哪一个 i 节点,由操作系统的使用者通过 mount 命令决定。逻辑效果如下图所示:
图片来源:《Linux 内核设计的艺术》
另外,一个文件系统必须挂接在另一个文件系统上,按照这个设计,一定存在一个只被其他文件系统挂接的文件系统,这个文件系统就叫根文件系统。根文件系统所在的设备就叫根设备
。
别的文件系统可以挂接在根文件系统上,根文件系统挂接在 super_block[8] 上
:
1 | // fs/super.c ---------------------------------- |
Linux 0.11 操作系统中只有一个 super_block[8],每个元素是一个超级块,一个超级块管理一个逻辑设备(文件系统),也就是说操作系统最多只能管理 8 个逻辑设备(文件系统),其中只有一个是根设备
。加载根文件系统最重要的标志就是把根文件系统的跟 i 节点挂接在 super_block[8] 中根设备对应的超级块上。
加载根文件系统
加载根文件系统的 3 个主要步骤:
- 复制根设备的超级块到 super_block[8] 上,将根设备中的根 i 节点挂接在 super_block[8] 中根设备对应的超级块上。
- 将驻留缓冲区的 16 个缓冲块的根设备逻辑块位图、i 节点位图分别挂接在 super_block[8] 中根设备超级块的 s_zmap[8]、s_imap[8] 上。
- 将当前进程的 pwd、root 指针指向根设备的根 i 节点。
执行代码:
1 | // kenel/blk_drv/hd.c -------------------------- |
加载根文件系统和安装硬盘文件系统完成后的总体效果图如下:
图片来源:《Linux 内核设计的艺术》
复制根系统超级块
在 rd_road 函数中,我们已经将整个根文件系统拷贝进了虚拟盘。虚拟盘是一个特殊的“外设”。现在是要从虚拟盘将根系统超级块复制到 super_block[8] 中。
这部分执行代码如下:
1 | // fs/super.c ---------------------------------- |
上边程序中需要注意的一点是利用 bread 从虚拟盘这个“外设”读数据,跟从硬盘读数据是不一样的,毕竟虚拟盘是内存中的一段,跟硬盘这个真外设还是不一样的。两者的前半段数据读取流程(bread -> make_request -> add_request -> do_XX_request)是一样的,但从虚拟盘读取数据用这前半段过程就可以了,没有硬盘后续的如硬盘中断等步骤。我们看一下虚拟盘的请求项处理函数 do_rd_request 函数就很清楚了:
1 | // kenel/blk_drv/ramdisk.c --------------------- |
挂接根系统根 i 节点
这部分执行代码如下:
1 | // fs/super.c ---------------------------------- |
上述代码执行结果如下图所示:
图片来源:《Linux 内核设计的艺术》
最后,加载根文件系统标志性的一步是将根 i 节点挂接到根文件系统在 super_block[8] 中的超级块:
1 | // fs/super.c ---------------------------------- |
关联进程 1
接下来就是将根文件系统关联到进程 1:
1 | // fs/super.c ---------------------------------- |
得到了根文件系统的超级块,就可以根据超级块中“逻辑块位图”里记载的信息,计算出虚拟盘上逻辑块的占用和空闲情况,并将此信息记录在驻留在缓冲区中“装载逻辑块位图信息的缓冲块中”,执行代码如下:
1 | // fs/super.c ---------------------------------- |
返回 sys_setup
到此,sys_setup 函数也算是执行完毕了:
1 | // kenel/blk_drv/hd.c -------------------------- |
而之前 sys_setup 函数的调用流程是:main -> init -> setup -> _syscall1 -> _system_call -> call _sys_call_table(,%eax,4) -> sys_setup
。所以,sys_setup 函数返回后,之后会从 _system_call 的 ret_from_sys_call 处开始执行。因为当前进程是进程 1,所以之后会调用 do_signal 函数,对当前进程的信号位图进行检测。不过,当前进程进程 1 并没有接收到信号,调用 do_signal 函数并没有实际意义。这部分执行代码如下:
1 | // kenel/system_call.s ------------------------- |
返回 init
从 _system_call 返回后,再经 _syscall1 会返回到 init 函数:
1 | // init/main.c --------------------------------- |
至此,进程 0 创建进程 1,进程 1 为安装硬盘文件系统做准备、“格式化”虚拟盘并用虚拟盘取代软盘为根设备、在虚拟盘上加载根文件系统的内容阐述完毕。