Linux0.12内核源码解读(9)-blk_dev_init和chr_dev_init

作者:小牛呼噜噜 | https://xiaoniuhululu.github.io
计算机内功、源码解析、科技故事、项目实战、面试八股等更多硬核文章,首发于公众号「小牛呼噜噜

哈喽,大家好呀,我是呼噜噜,好久没有更新old linux了,本文接着上一篇文章图解计算机中断,继续我们的内核探索之路

我们来接着看看main.c后面的几个初始化函数blk_dev_init()chr_dev_init()

1
2
3
4
5
6
7
8
9
10
11
12
// init/main.c
void main(void) {
...
blk_dev_init(); // 块设备初始化
chr_dev_init(); //字符设备初始化

tty_init(); // tty 初始化
time_init(); //设置开机启动时间
sched_init(); //调度程序初始化(加载进程0 的 tr,ldtr)
buffer_init(buffer_memory_end); // 缓冲管理初始化,建内存链表等
...
}

blk_dev_init

blk_dev_init()块设备初始化函数,那什么是块设备呢?

所谓的块设备block device,是计算机I/O设备的一种,就是以固定大小的数据块为单位来存储数据的设备,其传输速率较高,通常为每秒钟几兆位;数据块的大小通常在512字节到32768字节之间

每个块都有自己的地址,可以进行寻址操作和随机访问操作,而且每个块都能独立于其它块而进行读写,比较常见的有硬盘、软盘设备和闪存等

与之对应的就是字符设备character device,字符设备是一种只能以字节为最小单位,进行数据输入和输出的设备,其传输速率较低,通常为几个字节至数千字节

它无法进行寻址操作,只能按照字节流的方式被有序访问,字符设备的种类繁多,比如打印机、键盘、串口、网络接口和终端设备等。另外字符设备在输入输出时,常采用中断来驱动。

我们先来看下它源码:

1
2
3
4
5
6
7
8
9
10
//    /kernel/blk_drv/ll_rw_blk.c
void blk_dev_init(void)
{
int i;

for (i=0 ; i<NR_REQUEST ; i++) {
request[i].dev = -1;
request[i].next = NULL;
}
}

NR_REQUEST其实定义在kernel/blk_drv/blk.h#defineNR_REQUEST 32,表示request数组有32项,也是有讲究的,能够较好地平衡电梯调度算法的效率(电梯调度算法我们下文再介绍)

我们再来看下request结构体的定义:

1
2
3
4
5
6
7
8
9
10
11
struct request {
int dev; /* 使用的设备号,-1 就表示空闲*/
int cmd; /* READ or WRITE 命令 */
int errors; /*操作时产生的错误次数。*/
unsigned long sector; /*起始扇区。(1 块 = 2 扇区)*/
unsigned long nr_sectors; /*读/写扇区数。*/
char * buffer; /*数据缓冲区*/
struct task_struct * waiting; /*任务等待操作执行完成的地方*/
struct buffer_head * bh; /*缓冲区头指针(include/linux/fs.h)*/
struct request * next; /*指向下一请求项*/
};

那这段代码的作用,是给request数组的所有项,dev和next元素赋值,其实就是进行初始化;

然后就没了,许多人看到这会感觉很懵逼,这都是啥呀?

要想知道blk_dev_init()是如何初始化块设备的?那么得先了解最典型的块设备硬盘的构造了,比如常见的机械硬盘的组成部分:

我们都知道计算机世界中,只有0和1,在之前的文章漫谈从RS触发器到D触发器的发展历程我们介绍过一种储存数据的方式:RS触发器,让时序逻辑电路具有储存记忆的能力,它是内存用来储存数据的核心原理

而机械硬盘则是另一种方式,机械硬盘中最重要的结构就是磁盘,在工作时会高速旋转;一块磁盘一般有多个盘片组成,盘片的表面涂有磁性物质,可以被磁化以及改变磁极,这两个磁极就分别表示了计算机二进制中的0和1

上图来源于:Magnetic storage

写入数据时,磁头产生局部强磁场来磁化盘片上磁性物质的极性,来储存数据;当读出数据时,则通过读取元件来识别磁性物质的极性,并将其还原成0和1。也就是说这些磁性物质可以用来记录二进制数据,并通过电磁转换完成读、写操作

因为正反两面都可涂上磁性物质,故一个盘片可能会有两个盘面。每个盘面都会有对应的磁头,所有磁头”共进共退”,因为会有一个磁臂固定它们。

每个盘面划分成了一圈一圈的磁道track,最外圈是0磁盘。然后每个磁盘又划分为了一个个小块,小块叫做扇区sector扇区大小固定,是512个字节。另外由于最内侧磁道上的扇区面积最小,因此数据密度最大;最后所有盘面中相对位置相同的磁道组成柱面cylinder

扇区大小为啥固定512字节?这是1956年由industry trade organization, International Disk Drive Equipment和Materials Association三家机构确定的行业标准, 大家都默认会遵守的标准,久而久之,如果改变的话,兼容各种情况的代价非常大。

而有了柱面,磁头,扇区这3者,就能够定位磁盘上任意一块数据,其中如果知道哪个磁头,就可以知道数据在哪个盘面上,然后读取哪个柱面上的第几扇区,这也叫CHS模式

由此我们可知request这个结构体是非常重要的,是和块设备打交道的核心数据结构

本文以硬盘来举例子:request结构体中,dev就是表示设备号,cmd表示是读命令还是写命令,sector和nr_sectors配合找到对应的扇区;buffer缓冲区,是为了减少对块设备的访问,在数据以块的形式传入内存后,通过buffer缓冲区进行读取,不是直接和物理磁盘读取。字符设备可以直接物理磁盘读取,不经过buffer缓冲区

bh就是缓冲区头指针,waiting则表示任务等待操作执行完成的地方,next是指向下一请求项,说明request数组内部是链表。也就是说request数组其实就是块设备的请求队列,新的读写请求,进入队列排队

另外由于内存和硬盘存取数据的速度差异是非常大的,也就是说访问硬盘等块设备的操作比较耗时,影响操作系统的性能;而且硬盘的读写磁头移动到对应的磁道,这种磁盘调度也是非常耗时的。

linux0.12采用的是scan扫描算法,也叫做电梯调度算法尽可能通过减小磁头移动的距离来提高磁盘的读写效率,将新的请求项插入到磁头移动距离最小的请求队列位置处

电梯调度算法的原理:磁头在一个方向上移动,访问所有未完成的请求,直到磁头到达该方向上最后的磁道,才调换方向;

scan算法,上图来源于磁盘调度算法

除此以外还有其他常见的算法FCFS,SSF:

  1. 先来先服务算法FCFS:不管磁头初始在什么位置,都是按照服务队列的先后顺序,来依次处理进程。优点是简单,公平,但缺点就是平均寻道长度会很长,效率不高,相邻两次请求可能会造成最内到最外的柱面寻道,使磁头反复移动

  2. 最短寻道时间优先算法SSF:其实就是贪心算法,优先选择处理距离磁头位置最近的进程,处理完成后再处理距离当前磁道最近的进程,直到所有的进程被处理

不像先来先服务算法FCFS,追求极致的公平,效率较低;也不像最短寻道时间优先算法SSF,过于注重效率而忽视公平(磁头有可能只在一个小区域内来回移动,导致某些访问请求长期等待得不到服务的情况);电梯调度算法能够较好地兼顾公平与效率。我们这里就不做过多展开,感兴趣地自行去了解

blk_dev_init函数,其实是给request数组的所有项的dev设置为-1,表示无设备请求,next设置为null,表示没有下一请求项,实现对块设备请求队列的初始化。等待内核向块设备的控制器发起命令,至于后面的一系列操作,我们在后面的文章,会结合具体的源码再详细展开

chr_dev_init

chr_dev_init字符设备初始化函数,上面刚讲完块设备的初始化,字符设备的初始化不就来了嘛,我们先来看下源码:

1
2
3
4
5
//  /kernel/chr_drv/tty_io.c

void chr_dev_init(void)
{
}

咳咳,是空的,大佬linus在这个版本暂未开发,那我们就直接跳过了

先到这吧,上周末语雀挂掉,导致有部分文字没有能保存下来~


参考资料:
聊聊x86计算机启动发生的事?
https://en.wikipedia.org/wiki/Hard_disk_drive
https://en.wikipedia.org/wiki/Magnetic_storage
操作系统:磁盘调度算法_操作系统磁盘调度算法


作者:小牛呼噜噜 ,首发于公众号「小牛呼噜噜

感谢阅读,原创不易,如果有收获的话,就点个免费的[]or[转发],你的支持会激励我输出更高质量的文章,感谢!