本文是前 3 篇博文 Linux 内核学习笔记:初始化程序(第 1 部分)、Linux 内核学习笔记:初始化程序(第 2 部分)和 Linux 内核学习笔记:初始化程序(第 3 部分)的续篇。
缓冲区管理结构初始化
注意:缓冲区及缓冲区管理结构跟之前博文 Linux 内核学习笔记:初始化程序(第 2 部分)提到的块设备请求项有关系但不一样:
- 缓冲区管理结构(buffer_head)主要负责
进程与缓冲区
中缓冲块的数据交互,在确保数据交互正确的前提下,让数据在缓冲区中停留的时间尽可能长 - 块设备请求项(request)主要负责
块设备与缓冲区
之间的数据交互,在确保数据交互正确的前提下,尽可能地及时将进程修改过的缓冲块中的数据同步到块设备中
进程、块设备与缓冲区之间的关系如下:
图片来源:《Linux 内核设计的艺术》
缓冲区
《Linux 内核设计的艺术》对缓冲区有一个简要的介绍:
缓冲区是内存与外设(如硬盘)进行数据交互的媒介。内存与硬盘最大的区别在于,硬盘的作用仅仅是对数据信息以很低的成本做大量数据的断电保存,并不参与运算(因为
CPU 无法到硬盘上寻址
),而内存除了需要对数据进行保存以外,更重要的是与 CPU、总线配合进行数据运算。缓冲区则介于两者之间,它既对数据信息进行保存,也能够参与一些像查找、组织之类的间接、辅助性运算。有了缓冲区这个媒介后,对外设而言,它仅需要考虑与缓冲区进行数据交互是否符合要求,而不需要考虑内存如何使用这些交互的数据;对内存而言,它也仅需要考虑与缓冲区交互的条件是否成熟,而不需要关心此时外设对缓冲区的交互情况。
两者的组织、管理和协调将由操作系统统一操作。
缓冲区在内存中的位置如下:
图片来源:《Linux 内核完全注释》
注意:
- 显存和 BIOS ROM 在缓冲区内部
end
表示内核模块的末端,也是缓冲区的开端。end 是内核模块(system)链接期间由链接程序 ld 设置的一个外部变量,内核代码中没有定义这个符号。当在生成 system 模块时,ld 设置了 end 的地址,它等于 data_start+datasize+bss_size,即 bss 段结束后的第 1 个有效地址,也即内核模块的末端。另外,ld 还设置了 etext 和 edata 两个外部变量,分别表示代码段后第 1 个地址和数据段后第 1 个地址。
缓冲区管理
缓冲区从高地址端向低地址端划分成一个个 1024 字节大小的缓冲块
,与块设备上的逻辑块大小相同(逻辑块是由设备驱动程序拼接
而成的,而驱动程序为了拼接好这个逻辑块,可能需要多次读写外设。如从硬盘读两个扇区(512B)才能拼接成一个逻辑块)。而从缓冲区低地址端向高地址端方向,需要建立缓冲区管理结构,以管理这些划分出来的缓冲块。缓冲区具体划分如下图所示:
图片来源:《Linux 内核完全注释》
每一个缓冲块
都用这样一个结构体来管理:
1 | // include/linux/fs.h -------------------------- |
Linux 0.11 对缓冲区的管理分为两个部分:
针对
所有缓冲块
的双向链表结构
所有的缓冲块的管理结构 buffer_head 被链接成一个双向链表结构,如下图所示。图中 free_list 指针是该链表的头指针,指向空闲块链表中第一个“最为空闲”的缓冲块,即近期最少使用的缓冲块。而该缓冲块的 buffer_head 的反向指针 b_prev_free 则指向缓冲块链表中最后一个缓冲块,即最近刚使用的缓冲块。
图片来源:《Linux 内核完全注释》针对
已读入数据的缓冲块
的 Hash 表
为了能够快速而有效地在缓冲区中判断所请求的数据块是否已经被读入到缓冲区中,buffer.c 程序使用了具有 307 个 buffer_head 指针项的 Hash 表。Hash 表所使用的 Hash 函数由设备号和逻辑号通过异或操作组合而成,即(设备号^逻辑块号)Mod 307
。某一时刻,动态变化的 Hash 表结构可能如下:
图片来源:《Linux 内核完全注释》
注:上图中双箭头横线表示散列在同一 Hash 表项中缓冲块头结构 buffer_head 之间的双向链接指针。
缓冲区管理结构初始化
这部分代码如下:
1 | // init/main.c --------------------------------- |
上述代码执行结果如下图所示:
图片来源:《Linux 内核设计的艺术》
硬盘初始化
硬盘初始化代码为:
1 | // init/main.c --------------------------------- |
这段代码跟之前博文 Linux 内核学习笔记:初始化程序(第 1 部分)中“设置虚拟盘空间并初始化”部分很类似。
在 hd_init() 函数中,将硬盘请求项服务程序 do_hd_request() 与 blk_dev 控制结构相挂接,硬盘与请求项的交互工作将由 do_hd_request()函数(由 kenel/blk_dev/ll_rw_block.c -> ll_rw_block()函数调用)来处理。然后将硬盘中断服务程序 hd_interrupt 与 IDT 相挂接。最后,复位主 8259A int2 的屏蔽位,允许从片发出中断请求信号;复位硬盘的中断请求屏蔽位(在从片上),允许硬盘控制器发送中断请求信号。
硬盘初始化结果如下图所示:
图片来源:《Linux 内核设计的艺术》
软盘初始化
软盘初始化代码为:
1 | // init/main.c --------------------------------- |
软盘跟硬盘的初始化过程基本是一样的,除了软盘请求项处理函数是 do_fd_request()。
软盘初始化结果如下图所示:
图片来源:《Linux 内核设计的艺术》
开中断
到此,系统中所有中断服务程序都已经和 IDT 正常挂接。这意味着中断服务体系已经构建完毕,系统现在可以在 32 位保护模式下处理中断,重要意义之一是可以使用系统调用。
现在可以开启中断了:
1 | // init/main.c --------------------------------- |
代码执行结果如下图所示:
图片来源:《Linux 内核设计的艺术》
进程 0 特权级翻转
《Linux 内核设计的艺术》对进程 0 特权级翻转有一简要描述:
Linux 操作系统规定,
除进程 0 之外,所有进程都要由一个已有进程在 3 特权级下创建。
在 Linux 0.11 中,进程 0 的代码和数据都是由操作系统的设计者写在内核代码、数据区,并且,进程 0 此前处在 0 特权级,严格说还不是真正意义上的进程。为了遵守规则,在进程 0 创建进程 1 之前,要将进程 0 由 0 特权级转变为 3 特权级。方法是调用 move_to_user_mode() 函数,模仿中断返回动作,实现进程 0 的特权级从 0 转变为 3。
执行代码如下:
1 | // init/main.c --------------------------------- |
对该程序,《Linux 内核设计的艺术》有一个很详细的解释:
图片来源:《Linux 内核设计的艺术》