31.拿到硬盘信息

31.拿到硬盘信息

上一个大部分的名字叫一个新进程的诞生,讲述了进程 0 调用了 fork 函数创建了一个新的进程 —— 进程 1,并且使其达到了可以被调度的状态,fork 就算正式完成了自己的使命。

1
2
3
4
5
6
7
8
9
void main(void) {    
    ...    
    move_to_user_mode();    
    if (!fork()) {        
        init();    
    }    
    for(;;) 
        pause();
}

由于 fork 函数一调用,就又多出了一个进程,子进程(进程 1)会返回 0,父进程(进程 0)返回子进程的 ID,所以 init 函数只有进程 1 才会执行。

第三部分结束后,就到了现在的第四部分,shell 程序的到来。而整个第四部分的故事,就是这个 init 函数做的事情。

虽然就一行代码,但这里的事情可多了去了,我们先看一下整体结构。我已经把单纯的日志打印和错误校验逻辑去掉了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
void init(void) {    
    int pid,i;    
    setup((void *) &drive_info);    
    (void) open("/dev/tty0",O_RDWR,0);    
    (void) dup(0);    
    (void) dup(0);    
    if (!(pid=fork())) {        
        open("/etc/rc",O_RDONLY,0);        
        execve("/bin/sh",argv_rc,envp_rc);    
    }    
    if (pid>0)        
        while (pid != wait(&i))            
            /* nothing */;    
            while (1) {        
                if (!pid=fork()) {            
                    close(0);close(1);close(2);            
                    setsid();            
                    (void) open("/dev/tty0",O_RDWR,0);            
                    (void) dup(0);            
                    (void) dup(0);            
                    _exit(execve("/bin/sh",argv,envp));        
                }        
                while (1)            
                    if (pid == wait(&i))                
                        break;        
                sync();    
            }    
    _exit(0);   
    /* NOTE! _exit, not exit() */
}

是不是看着还挺复杂?

不过别急,今天我们就只讲第一行代码 setup 的一部分,硬盘信息的获取。

1
2
3
4
5
6
7
struct drive_info { char dummy[32]; } 
drive_info;
// drive_info = (*(struct drive_info *)0x90080);
void init(void) {    
    setup((void *) &drive_info);    
    ...
}

先看入参。

drive_info 是来自内存 0x90080 的数据,这部分是由之前 第5回 | 进入保护模式前的最后一次折腾内存 讲的 setup.s 程序将硬盘 1 的参数信息放在这里了,包括柱面数、磁头数、扇区数等信息。

setup 是个系统调用,会通过中断最终调用到 sys_setup 函数。关于系统调用的原理,在 第25回 | 通过 fork 看一次系统调用 中已经讲得很清楚了,此处不再赘述。

所以直接看 sys_setup 函数,我仍然是对代码做了少许的简化,去掉了日志打印和错误判断分支,并且仅当作只有一块硬盘,去掉了一层 for 循环。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
int sys_setup(void * BIOS) {    
    hd_info[0].cyl = *(unsigned short *) BIOS;    
    hd_info[0].head = *(unsigned char *) (2+BIOS);    
    hd_info[0].wpcom = *(unsigned short *) (5+BIOS);    
    hd_info[0].ctl = *(unsigned char *) (8+BIOS);    
    hd_info[0].lzone = *(unsigned short *) (12+BIOS);    
    hd_info[0].sect = *(unsigned char *) (14+BIOS);    
    BIOS += 16;    
    hd[0].start_sect = 0;    
    hd[0].nr_sects =         
    hd_info[0].head * hd_info[0].sect * hd_info[0].cyl;        
    struct buffer_head *bh = bread(0x300, 0);    
    struct partition *p = 0x1BE + (void *)bh->b_data;    
    for (int i=1;i<5;i++,p++) {        
        hd[i].start_sect = p->start_sect;        
        hd[i].nr_sects = p->nr_sects;    
    }    
    brelse(bh);        
    rd_load();    
    mount_root();    
    return (0);
}

好,我们一点点看。

先看第一部分,硬盘基本信息的赋值的操作。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
int sys_setup(void * BIOS) {    
    hd_info[0].cyl = *(unsigned short *) BIOS;    
    hd_info[0].head = *(unsigned char *) (2+BIOS);    
    hd_info[0].wpcom = *(unsigned short *) (5+BIOS);    
    hd_info[0].ctl = *(unsigned char *) (8+BIOS);    
    hd_info[0].lzone = *(unsigned short *) (12+BIOS);    
    hd_info[0].sect = *(unsigned char *) (14+BIOS);    
    BIOS += 16;    
    ...
}

刚刚说了,入参 BIOS 是来自内存 0x90080 的数据,这部分是由之前 第5回 | 进入保护模式前的最后一次折腾内存 讲的 setup.s 程序将硬盘 1 的参数信息放在这里了,包括柱面数、磁头数、扇区数等信息。

所以,一开始先往 hd_info 数组的 0 索引处存上这些信息。我们假设就只有一块硬盘,所以这个数组也只有一个元素。

这个数组里的结构就是 hd_i_struct,就表示硬盘的参数。

1
2
3
4
5
struct hd_i_struct {    
    // 磁头数、每磁道扇区数、柱面数、写前预补偿柱面号、磁头着陆区柱面号、控制字节    
    int head,sect,cyl,wpcom,lzone,ctl;
};
struct hd_i_struct hd_info[] = {}

最终效果就是这样。

图片

OK,我们继续。

看第二部分,硬盘分区表的设置。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
static struct hd_struct {    
    long start_sect;    
    long nr_sects;
} hd[5] = {}
int sys_setup(void * BIOS) {    
    ...    
    hd[0].start_sect = 0;    
    hd[0].nr_sects =         
        hd_info[0].head * hd_info[0].sect * hd_info[0].cyl;    
    struct buffer_head *bh = bread(0x300, 0);    
    struct partition *p = 0x1BE + (void *)bh->b_data;    
    for (int i=1;i<5;i++,p++) {        
        hd[i].start_sect = p->start_sect;        
        hd[i].nr_sects = p->nr_sects;    
    }    
    brelse(bh);    
    ...
}

只看最终效果,就是给 hd 数组的五项附上了值。

图片

这表示硬盘的分区信息,每个分区用 start_sect 和 nr_sects,也就是开始扇区和总扇区数来记录。

这些信息是从哪里获取的呢?就是在硬盘的第一个扇区的 0x1BE 偏移处,这里存储着该硬盘的分区信息,只要把这个地方的数据拿到就 OK 了。

所以 bread 就是干这事的,从硬盘读取数据。

1
 struct buffer_head *bh = bread(0x300, 0);

第一个参数 0x300 是第一块硬盘的主设备号,就表示要读取的块设备是硬盘一。第二个参数 0 表示读取第一个块,一个块为 1024 字节大小,也就是连续读取硬盘开始处 0 ~ 1024 字节的数据。

拿到这部分数据后,再取 0x1BE 偏移处,就得到了分区信息。

1
struct partition *p = 0x1BE + (void *)bh->b_data;

就这么点事。

图片

至于如何从硬盘中读取指定位置(块)的数据,也就是 bread 函数的内部实现,那是相当复杂的,涉及到与缓冲区配合的部分,还有读写请求队列的设置,以及中断。

当然,这个函数就是经典的问题,从硬盘中读取数据的原理,但这些都不影响主流程,因为仅仅是把硬盘某位置的数据读到内存而已,先不去深入细节,细节部分将在第五部分展开说明。

OK,目前我们已经把硬盘的基本信息存入了 hd_info[],把硬盘的分区信息存入了 hd[],我们继续往下看。

1
2
3
4
5
6
int sys_setup(void * BIOS) {    
    ...    
    rd_load();    
    mount_root();    
    return (0);
}

就剩两个函数了。

其中 rd_load 是当有 ramdisk 时,也就是虚拟内存盘,才会执行。虚拟内存盘是通过软件将一部分内存(RAM)模拟为硬盘来使用的一种技术,一种小玩法而已,我们就先当做没有,否则很影响看主流程的心情。

mount_root 直译过来就是加载根,再多说几个字是加载根文件系统,有了它之后,操作系统才能从一个根开始找到所有存储在硬盘中的文件,所以它是文件系统的基石,很重要。

为了加载根文件系统,或者说所谓的加载根文件系统,就是把硬盘中的数据加载到内存里,以文件系统的数据格式来解读这些信息。

所以第一,需要硬盘本身就有文件系统的信息,硬盘不能是裸盘,这个不归操作系统管,你为了启动我的 Linux 0.11,必须拿来一块做好了文件系统的硬盘来。

第二,需要读取硬盘的数据到内存,那就必须需要知道硬盘的参数信息,这就是我们本讲所做的事情的意义。

updatedupdated2024-05-102024-05-10