本文是上一博文 Linux 内核学习笔记:初始化程序(第 1 部分)的续篇。
初始化块设备请求项结构
块设备与字符设备
《Linux 内核完全注释》对块设备(block device)和字符设备(character device)有一个简要的阐述:
- 操作系统的主要功能之一就是与周边的输入输出设备进行通信,采用统一的接口来控制这些外围设备。操作系统的所有设备可以粗略地分成两种类型:块设备和字符设备。
块设备是一种可以以固定大小的数据块(1024B)为单位进行寻址和访问的设备
,如硬盘和软盘设备。字符设备是一种以字符流为操作对象的设备,不能进行寻址操作
,如打印机、网络接口、终端设备。- 为了便于管理和访问,操作系统将这些设备统一地以设备号进行分类。在 Linux 0.11 内核中设备被分成 7 类,即共有 7 个设备号(0 ~ 6),如下图所示。每个类型的设备可在根据次设备号来加以进一步区别。
图片来源:《Linux 内核完全注释》
块设备请求项
进程要想与块设备进行通信,必须经过主机内存中的缓冲区。块设备请求项管理结构 request[32] 就是操作系统管理缓冲区块与块设备上逻辑块之间读写关系的数据结构。
其中块设备请求项定义如下:
1 | // kenel/blk_dev/blk.h ------------------------- |
下边以进程从硬盘读数据为例来说明它们之间的通信过程:
- 当进程需要读取硬盘上的一个逻辑块时,就会向缓冲区管理程序提出申请,而该进程则进入睡眠等待状态。
- 缓冲区管理程序首先在缓冲区中寻找以前是否已经读取过这块数据。如果缓冲区中已经有了,就直接将对应的缓冲区块头指针返回给进程并唤醒该进程。
- 如果缓冲区还不存在所要求的数据块,则缓冲区管理程序就会调用低级块读写函数 ll_rw_block()(定义在 kenel/blk_dev/ll_rw_block.c 文件,实际读写调用的是块设备的请求项处理函数,如虚拟盘的 do_rd_request() 函数),向相应的块设备发出一个读数据块的操作请求。该函数会为此创建一个请求结构项,并插入到请求队列中(ll_rw_block() 调用的子函数 make_request() )。
- 此时,若相应块设备的请求项队列为空,则表明块设备不忙。于是内核就会立刻向该块设备发出读数据命令。当块设备的数据被读入到指定的缓冲块后,就会发出中断请求信号,并调用相应的读命令后处理函数,处理继续读扇区或结束本次请求读过程。
注:
- 上面过程,块设备实际是由其驱动程序来管理的。直接说成块设备是为了便于陈述。
- ll_rw_block 函数定义如下:
1
2
3
4
5
6
7
8
9
10
11
12
13// kenel/blk_dev/ll_rw_block.c -----------------
void ll_rw_block(int rw, struct buffer_head * bh)
{
unsigned int major;
// 如果设备的主设备号不存在或者该设备的读写操作函数不存在,则显示出错信息,返回
if ((major=MAJOR(bh->b_dev)) >= NR_BLK_DEV ||
!(blk_dev[major].request_fn)) { // 注意:这里并不是调用 request_fn() 函数。request_fn 和 request_fn() 是不一样的
printk("Trying to read nonexistent block-device\n\r");
return;
}
make_request(major,rw,bh); // 创建一个请求结构项,可能执行块设备的请求函数,并将该请求结构项插入到该设备的请求队列中
} - 关于 ll_rw_block 详细调用,请参考博文 Linux 内核学习笔记:进程 1 创建及执行(第 3 部分)。
对块设备请求项数组的初始化代码如下:
1 | // init/main.c --------------------------------- |
代码执行结果如下:
图片来源:《Linux 内核设计的艺术》
块设备请求项处理函数
在上一博文 Linux 内核学习笔记:初始化程序(第 1 部分)“设置虚拟盘空间并初始化”小节,我们知道 Linux 0.11 用一个 blk_dev_struct
结构体数组 blk_dev[NR_BLK_DEV]
来管理所有的块设备。其中结构体 blk_dev_struct
定义如下:
1 | // kenel/blk_dev/blk.h ------------------------- |
该结构体的第一个字段是一个函数指针,用于处理相应块设备的请求项(request
)。在 Linux 0.11 中,对应设备的请求项操作函数如下表所示:
图片来源:《Linux 内核完全注释》
块设备表项与请求项的关系的一个例子如下图所示:
图片来源:《Linux 内核完全注释》
人机交互外设的中断服务程序挂接
main 函数中的字符设备初始化函数 chr_dev_init()
为空。而其后的 tty_init()
就是用于初始化字符设备。
字符设备初始化为进程与串行口、显示器以及键盘进行 I/O 通信准备工作环境,主要是对这 3 者进行初始化,以及将与这 3 者相关的中断服务程序与 IDT 相挂接。
字符设备初始化代码如下:
1 | // init/main.c --------------------------------- |
因为这部分不是重点,所以在这里就不详述了。想了解详细的童鞋可参考《Linux 内核设计的艺术》(第 2 版)第 2.7 节。
开机启动时间设置
CMOS 是主板上的一个小存储芯片,用于保存时钟和日期信息,存放的格式是 BCD 码。CMOS 的地址空间在基本地址空间之外,因此其中不包括可执行代码。
要访问它需要通过端口 0x70、0x71 进行。0x70 是地址端口,0x71 是数据端口。为了读取指定偏移位置的字节,必须首先使用 out 指令向地址端口 0x70 发送指定字节的偏移位置值,然后使用 in 指令从数据端口 0x71 读取指定的字节信息。同样,对于写操作也需要首先向地址端口 0x70 发送指定字节的偏移值,然后把数据写到数据端口 0x71 去。
CMOS 64 字节信息简表如下所示:
图片来源:《Linux 内核完全注释》
系统通过调用 time_init()
函数,先对它上面记录的时间数据进行采集,提取不同等级的时间要素,如秒、分等,然后对这些要素进行整合,并最终得到开机启动时间(startup_time)。
执行代码如下:
1 | // init/main.c --------------------------------- |