Linux0.11源码趣读

用源码打开操作系统“黑盒子”

Linux,Linux 0.11,内核,源码,操作系统,

闪客 “低并发编程”公号作者

19|硬盘初始化hd_init:四行代码轻松解读

讲述:闪客 大小:4.55MB 时长:00:04:45
00:00
1.0×

你好,我是闪客。

上一讲我们说到,buffer_init 完成了缓冲区初始化工作,通过双向空闲链表和哈希表的方式,形成了缓冲区管理的方法。

至于缓冲区究竟是如何被使用的,等到下一期讲解如何通过文件系统来读取一个块设备的数据时,再展开讲解。

这一讲,我们看 main 函数中最后两个初始化函数!

void main(void) {
    ...
    hd_init();
    floppy_init();
    ...
}

最后两个了!兴不兴奋!不过一口气看两个会不会消化不了?

不要担心,hd_init 是硬盘初始化,我们不得不看。但 floppy_init 是软盘初始化,现在软盘几乎都被淘汰了,计算机中也没有软盘驱动器了,所以这个我们完全可以不看。

还记得小时候我特别喜欢收集软盘,里面分门别类存上我做的 Flash 动画,然后在软盘上的那个纸标签上写上文字,表示软盘存了什么,想想看还是回忆呢。

硬盘初始化 hd_init 都干了什么?

收,我们直接看 hd_init 这个硬盘初始化干了什么?

struct blk_dev_struct {
    void (*request_fn)(void);
    struct request * current_request;
};

extern struct blk_dev_struct blk_dev[NR_BLK_DEV];

// kernel/blk_drv/hd.c
void hd_init(void) {
    blk_dev[3].request_fn = do_hd_request;
    set_intr_gate(0x2E,&hd_interrupt);
    outb_p(inb_p(0x21)&0xfb,0x21);
    outb(inb_p(0xA1)&0xbf,0xA1); 
}

就这?一共就四行代码。

没错,初始化嘛,往往都比较简单,尤其是对硬件设备的初始化,大体都是:

  1. 往某些 IO 端口上读写一些数据,表示开启它。
  2. 然后再向中断向量表中添加一个中断,使得 CPU 能够响应这个硬件设备的动作。
  3. 最后再初始化一些数据结构来管理。不过像是内存管理可能结构复杂些,外设的管理,相对就简单很多了。

我们一行一行解读,反正也不多,先看第一行代码。

// kernel/blk_drv/hd.c
void hd_init(void) {
    blk_dev[3].request_fn = do_hd_request;
    ...
}

我们把 blk_dev 数组索引 3 位置处的块设备管理结构 blk_dev_structrequest_fn 赋值为了 do_hd_request,这是啥意思呢?

因为有很多块设备,所以 Linux 0.11 用了一个 blk_dev[] 这样的数组来进行管理,每一个索引表示一个块设备。

// kernel/blk_drv/ll_rw_blk.c
struct blk_dev_struct blk_dev[NR_BLK_DEV] = {
    { NULL, NULL },     /* no_dev */
    { NULL, NULL },     /* dev mem */
    { NULL, NULL },     /* dev fd */
    { NULL, NULL },     /* dev hd */
    { NULL, NULL },     /* dev ttyx */
    { NULL, NULL },     /* dev tty */
    { NULL, NULL }      /* dev lp */
};

你看,索引为 3 这个位置,就表示给硬盘 hd 这个块设备留的位置。

那么每个块设备执行读写请求都有自己的函数实现,在上层看来都是一个统一函数 request_fn 即可,具体实现各有不同,对于硬盘来说,这个实现就是 do_hd_request 函数。

是不是有点像接口?这其实就是多态思想在 C 语言的体现嘛~用 Java 程序员熟悉的话就是,父类引用 request_fn 指向子类对象 do_hd_request 的感觉。

我们再看第二行。

// kernel/blk_drv/hd.c
void hd_init(void) {
    ...
    set_intr_gate(0x2E,&hd_interrupt);
    ...
}

对于中断我们已经很熟悉了,这里就是又设置了一个新的中断,中断号是 0x2E,中断处理函数是 hd_interrupt,也就是说硬盘发生读写时,硬盘会发出中断信号给 CPU,之后 CPU 便会陷入中断处理程序,也就是执行 hd_interrupt 函数。

// kernel/system_call.s
_hd_interrupt:
    ...
    xchgl _do_hd,%edx
    ...
    
// 如果是读盘操作,这个 do_hd 是 read_intr
static void read_intr(void) {
    ...
    do_hd_request();
    ...
}

好了,又多了一个中断,那我们再次梳理下目前开启的中断都有哪些。

其中 0-0x10 这 17 个中断是 trap_init 里初始化设置的,是一些基本的中断,比如除零异常等。

之后在控制台初始化 con_init 里,我们又设置了 0x21 键盘中断,这样按下键盘就有反应了。

再之后,我们在进程调度初始化 sched_init 里又设置了 0x20 时钟中断,并且开启定时器。最后又偷偷设置了一个极为重要的 0x80 系统调用中断。

现在,我们在硬盘初始化 hd_init 里,又设置了硬盘中断,这样硬盘读写完成后将通过中断来通知 CPU。

好了,再往下看后两行。

// kernel/blk_drv/hd.c
void hd_init(void) {
    ...
    outb_p(inb_p(0x21)&0xfb,0x21);
    outb(inb_p(0xA1)&0xbf,0xA1); 
}

就是往几个 IO 端口上读写,其作用是允许硬盘控制器发送中断请求信号,仅此而已。我们向来是不深入硬件细节,知道往这个端口里写上这些数据,导致硬盘开启了中断,即可。

这样,在对硬盘发起读请求后,硬盘在读取完数据之后就可以发起中断信号,告诉 CPU 我读完啦,仅此而已。至于怎么读硬盘,读硬盘之后产生的中断处理程序要怎么处理,下一期会一一为你解惑,不要着急。

总结时刻

这一讲内容十分简单,就是将块设备统一函数 request_fn 的硬盘部分赋值为了 do_hd_request ,并且又新增加了一个 0x2E 号硬盘中断,这样 CPU 才能响应硬盘的反馈。

好了,至此,我们就把所有的初始化工作都讲完了!坚持学到现在的,该为自己鼓鼓掌!

好,我是闪客。欲知后事如何,且听下回分解。