操作系统(Linux Kernel 0.11&Linux Kernel 0.12)解读整理——内核初始化(main & init)之硬盘初始化

前言

硬盘软盘 块设备上数据的读写操作是通过中断处理程序 进行的。内核每次读写的数据量以一个逻辑块 (1024 字节)为单位,而块设备控制器则是以扇区 (512字节)为单位访问块设备。在处理过程中,内核使用了读写请求项等待队列来顺序地缓冲一次读写多个逻辑块的操作。

具体的,当一个程序需要读取硬盘上的一个逻辑块时,就会向缓冲区管理程序提出申请。而请求读写的程序进程则进入睡眠等待状态。缓冲区管理程序首先在缓冲区中寻找以前是否已经读取过这块数据。如果缓冲区中已经有了,就直接将对应的缓冲区块头指针返回给程序并唤醒等待的进程。若缓冲区中还不存在所要求的数据块,则缓冲管理程序就会调用低级块读写函数 ll_rw_block(),向相应的块设备驱动程序发出一个读数据块的操作请求。该函数会为此创建一个请求结构项,并插入请求队列中。为了提高读写磁盘的效率,减小磁头移动的距离,内核代码在把请求项插入请求队列时,会使用电梯算法将请求项插入到磁头移动距离最小的请求队列位置处。(关于缓冲区概念及交互过程块设备操作请求过程 将在后续另起文章介绍)

若此时对应块设备的请求项队列为空,则表明此刻该块设备不忙。于是内核就会立刻向该块设备的控制器发出读数据命令。当块设备的控制器将数据读入到指定的缓冲块后,就会发出中断请求信号,并调用相应的读命令后处理函数,处理继续读扇区操作或者结束本次请求项的过程。例如对相应块设备进行关闭操作和设置该缓冲块数据已经更新标志,最后唤醒等待该块数据的进程。

各种关于硬盘或者其他块设备的操作都离不开对关键数据结构的定义与初始化,本篇将简要地解读整理内核v0.11关于硬盘的初始化。

硬盘初始化 hd_init()

不外乎就是往某些 IO 端口上读写一些数据,表示开启它;然后再向中断向量表中添加一个中断,使得 CPU 能够响应这个硬件设备的动作;最后再初始化一些数据结构来管理。

cpp 复制代码
void main(void) {
    ...
    hd_init(); 
    ...
}

//struct blk_dev_struct {
//    void (*request_fn)(void);
//    struct request * current_request;
//};
//extern struct blk_dev_struct blk_dev[NR_BLK_DEV];

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); 
}

因为有很多块设备,所以 Linux 0.11 内核用了一个 blk_dev[] 来进行管理,每一个索引表示一个块设备

cpp 复制代码
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 语言的体现。用C++熟悉的话就是,抽象基类接口指针 request_fn 指向派生类对象具体方法 do_hd_request 的感觉。)

接着又设置了一个新的中断,中断号是 0x2E ,中断处理函数是 hd_interrupt,也即硬盘发生读写时,硬盘会发出中断信号给 CPU,之后 CPU 便会陷入中断处理程序,即执行 hd_interrupt 函数。

(tips:赵炯的内核注释书中是这样描述的:当硬盘操作完成或出错会发出此中断信号。)

cpp 复制代码
_hd_interrupt:
    ...
    xchgl _do_hd,%edx
    ...
    
// 如果是读盘操作,这个 do_hd 是 read_intr
static void read_intr(void) {
    ...
    do_hd_request();
    ...
}

(操作系统就是一个靠中断驱动的死循环而已,如果不发生任何中断,操作系统会一直在一个死循环里等待。换句话说,让操作系统工作的唯一方式,就是触发中断。)

最后就是往几个 IO 端口上读写,其作用是允许硬盘控制器发送中断请求信号

硬盘的端口表如下:

所谓读硬盘,就是往除了第一个以外的后面几个端口写数据,告诉要读硬盘的哪个扇区,读多少。然后再从 0x1F0 端口一个字节一个字节的读数据,这就完成了一次硬盘读操作。

例如:

  1. 在 0x1F2 写入要读取的扇区数

  2. 在 0x1F3 ~ 0x1F6 这四个端口写入计算好的起始 LBA 地址

  3. 在 0x1F7 处写入读命令的指令号

  4. 不断检测 0x1F7 (此时已成为状态寄存器的含义)的忙位

  5. 如果第四步骤为不忙,则开始不断从 0x1F0 处读取数据到内存指定位置,直到读完

操作系统中读写端口的核心如下:

cpp 复制代码
static void hd_out(unsigned int drive,unsigned int nsect,unsigned int sect,
        unsigned int head,unsigned int cyl,unsigned int cmd,
        void (*intr_addr)(void)) {
    ...
    do_hd = intr_addr;
    outb_p(hd_info[drive].ctl,HD_CMD);
    port = 0x1f0;
    outb_p(hd_info[drive].wpcom>>2,++port);
    outb_p(nsect,++port);
    outb_p(sect,++port);
    outb_p(cyl,++port);
    outb_p(cyl>>8,++port);
    outb_p(0xA0|(drive<<4)|head,++port);
    outb(cmd,++port);
}

outb_p 方法,转换成汇编语言,就是 out 指令,往指定的硬盘 IO 端口上写数据,达到读或者写的目的。

由用户层写的各种 read / write 函数,即便是经过系统调用、文件系统、缓冲区管理等等过程,但只要是读写硬盘,最终都要调用到这个最底层的函数,殊途同归!

hd.c 中有很多读写硬盘的方法,这个在之后文件系统会使用到,将另外整理一篇进行展开!!!

相关推荐
van叶~8 分钟前
Linux网络-------1.socket编程基础---(TCP-socket)
linux·网络·tcp/ip
风吹落叶花飘荡20 分钟前
Ubuntu系统 系统盘和数据盘扩容具体操作
linux·运维·ubuntu
aramae35 分钟前
大话数据结构之<队列>
c语言·开发语言·数据结构·算法
zoulingzhi_yjs1 小时前
haproxy配置详解
linux·云原生
bingbingyihao1 小时前
Node.js 模拟 Linux 环境
linux·node.js
大神的风范1 小时前
从0开始学linux韦东山教程Linux驱动入门实验班(5)
linux
小码过河.1 小时前
CentOS 搭建 Docker 私有镜像仓库
linux·docker·centos
cccc来财2 小时前
Java实现大根堆与小根堆详解
数据结构·算法·leetcode
程序员编程指南2 小时前
Qt 数据库连接池实现与管理
c语言·数据库·c++·qt·oracle
贾斯汀玛尔斯3 小时前
ubuntu/centos系统ping 不通域名的解决方案
linux·ubuntu·centos