linux0.11内核源码修仙传第十六章——获取硬盘信息
🚀 前言
书接第十四章:linux0.11内核源码修仙传第十四章——进程调度之fork函数,在这一节博客中已经通过fork进程创建了一个新的进程1,并且可以被调度,接下来接着主线继续走下去。希望各位给个三连,拜托啦,这对我真的很重要!!!
目录
- 🚀 前言
- 🏆硬盘基本信息的赋值
- 🏆硬盘分区表的设置
- 📃硬盘分区表(Disk Partition Table,DPT)
- 📃代码实现
- 🏆加载根文件系统
- 📃mount_root 整体解读
- 📃内存中用于文件系统的数据结构
- 文件信息初始化
- 超级块初始化
- inode信息读取
- 记录位图信息
- 记录inode位图信息
- 🎯总结
- 📖参考资料
🏆硬盘基本信息的赋值
好久没回顾 main
函数了,来回顾一下:
void main(void)
{···mem_init(main_memory_start,memory_end);trap_init();blk_dev_init();chr_dev_init();tty_init();time_init();sched_init();buffer_init(buffer_memory_end);hd_init();floppy_init();sti();move_to_user_mode();if (!fork()) { /* we count on this going ok */init();}for(;;) pause();
}
这里面前面的一堆初始化已经看完了,fork函数也运行了,成功创建了进程1。进程1会返回0,。现在压力来到了init
函数:
void init(void)
{int pid,i;setup((void *) &drive_info);(void) open("/dev/tty0",O_RDWR,0);(void) dup(0);(void) dup(0);printf("%d buffers = %d bytes buffer space\n\r",NR_BUFFERS,NR_BUFFERS*BLOCK_SIZE);printf("Free mem: %d bytes\n\r",memory_end-main_memory_start);if (!(pid=fork())) {close(0);if (open("/etc/rc",O_RDONLY,0))_exit(1);execve("/bin/sh",argv_rc,envp_rc);_exit(2);}if (pid>0)while (pid != wait(&i))/* nothing */;while (1) {if ((pid=fork())<0) {printf("Fork failed in init\r\n");continue;}if (!pid) {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;printf("\n\rchild %d died with code %04x\n\r",pid,i);sync();}_exit(0); /* NOTE! _exit, not exit() */
}
ok,fine,里面内容很多,没关系,一点点来,这一篇博文来看第一行 setup
函数的内容。
struct drive_info { char dummy[32]; } drive_info;void init(void)
{setup((void *) &drive_info);···
}
先来看看传入的参数:drive_info
。这个变量是来自内存 0x90080 的数据,设置是在main函数的最开始。详情可以看博客:linux0.11内核源码修仙传第二章——setup.s,内存里的存放位置如下所示:
接下来来看setup
函数:
static inline _syscall1(int,setup,void *,BIOS)
好的,这又是一个系统调用,返回类型是int
,有一个参数是 void *
类型的BIOS。有关于系统调用可以参考这篇博客:linux0.11内核源码修仙传第十四章——进程调度之fork函数。其实直白一点,可以直接去系统调用的sys_call_table
里面找对应的函数,这里讲结论,它会直接调用 sys_setup
函数:
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 处的数据,包括柱面数、磁头数、扇区数等信息。不了解这些信息的可以看参考资料[3]。其实这里本来是个循环的,循环赋值所有硬盘信息进hd_info这个结构体数组,但是这里只考虑一个盘的情况,因此去掉了循环。最后BIOS加了16可以看上面内存的图,硬盘1和硬盘2参数之间隔了16字节。这里是准备到下一个硬盘了,但是这里没有了,所以不作考虑。
最终结果如下所示:
🏆硬盘分区表的设置
📃硬盘分区表(Disk Partition Table,DPT)
硬盘分区表用于定义硬盘的分区信息。分区表存储在硬盘的某个特定区域,通常是硬盘的第一个扇区,称为主引导记录(Master Boot Record, MBR),现代的分区表格式则主要是GPT(GUID Partition Table)。其作用主要是告诉操作系统硬盘上有多少个分区,每个分区的大小和位置。
📃代码实现
在linux0.11中的做法如下所示:
int sys_setup(void * BIOS)
{···hd[0].start_sect = 0;hd[0].nr_sects = hd_info[i].head*hd_info[i].sect*hd_info[i].cyl;struct buffer_head *bh = bread(0x300 + drive*5,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
,也就是开始扇区和总扇区数来记录。循环里面一共4次,加上最开始初始化的,因此一共是五个分区,最后结果如下所示:
这些信息从哪里获取呢,就是在硬盘的第一个扇区的 0x1BE 偏移处,这里存储着该硬盘的分区信息,只要把这个地方的数据拿到就 OK 了。
所以 bread 就是干这事的,从硬盘读取数据:
struct buffer_head *bh = bread(0x300 + drive*5,0);
第一个参数 0x300 是第一块硬盘的主设备号,就表示要读取的块设备是硬盘一。第二个参数 0 表示读取第一个块,一个块为 1024 字节大小,也就是连续读取硬盘开始处 0 ~ 1024 字节的数据。拿到这部分数据后,再取 0x1BE 偏移处,就得到了分区信息:
struct partition *p = 0x1BE + (void *)bh->b_data;
从硬盘的视角来看分区。0号块本来是一个超级块,可以参考这篇博客:linux0.11内核源码修仙传第十五章——文件系统,现在在里面多放一个分区信息。下面是示意图:
至于如何从硬盘中读取指定位置(块)的数据,也就是 bread 函数的内部实现,这部分略微复杂,先埋个坑日后再细聊。
🏆加载根文件系统
最后setup
函数还有一部分:
int sys_setup(void * BIOS)
{···rd_load(); // 不用管mount_root();···
}
其中 rd_load
是当有 ramdisk 时,也就是虚拟内存盘,才会执行。虚拟内存盘是通过软件将一部分内存(RAM)模拟为硬盘来使用的一种技术,此处当不存在,因此这行代码无用。
mount_root
是加载根文件系统,有了它之后,操作系统才能从一个根开始找到所有存储在硬盘中的文件,所以它是文件系统的基石,很重要:
void mount_root(void)
{int i,free;struct super_block * p;struct m_inode * mi;if (32 != sizeof (struct d_inode))panic("bad i-node size");for(i=0;i<NR_FILE;i++)file_table[i].f_count=0;if (MAJOR(ROOT_DEV) == 2) {printk("Insert root floppy and press ENTER");wait_for_keypress();}for(p = &super_block[0] ; p < &super_block[NR_SUPER] ; p++) {p->s_dev = 0;p->s_lock = 0;p->s_wait = NULL;}if (!(p=read_super(ROOT_DEV)))panic("Unable to mount root");if (!(mi=iget(ROOT_DEV,ROOT_INO)))panic("Unable to read root i-node");mi->i_count += 3 ; /* NOTE! it is logically used 4 times, not 1 */p->s_isup = p->s_imount = mi;current->pwd = mi;current->root = mi;free=0;i=p->s_nzones;while (-- i >= 0)if (!set_bit(i&8191,p->s_zmap[i>>13]->b_data))free++;printk("%d/%d free blocks\n\r",free,p->s_nzones);free=0;i=p->s_ninodes+1;while (-- i >= 0)if (!set_bit(i&8191,p->s_imap[i>>13]->b_data))free++;printk("%d/%d free inodes\n\r",free,p->s_ninodes);
}
📃mount_root 整体解读
从整体上来说,mount_root
这个函数就是要把硬盘中的数据以文件系统的格式进行解读,加载到内存中设计好的数据结构,这样操作系统就可以通过内存中的数据,以文件系统的方式访问硬盘中的一个个文件。首先回顾一下硬盘中的文件系统格式,区别于之前我们的博客里介绍的文件系统,哪个是ext2的格式,但是linux-0.11是MINIX
文件系统,但是都是大同小异的,这里贴出来这个文件系统的格式:
简单看看这个文件系统:
引导块:启动区,当然不一定所有的硬盘都有启动区,但我们还是得预留出这个位置,以保持格式的统一。
超级块:用于描述整个文件系统的整体信息,我们看它的字段就知道了,有后面的 inode 数量,块数量,第一个块在哪里等信息。
inode位图:inode的使用情况。
块位图:块的使用情况。
i 结点:inode存放每个文件或目录的元信息和索引信息。
块:存放文件数据。
可是硬盘中凭什么就有了这些信息呢?这就是个鸡生蛋蛋生鸡的问题了。你可以先写一个操作系统,然后给一个硬盘做某种文件系统类型的格式化,这样你就得到一个有文件系统的硬盘了,有了这个硬盘,你的操作系统就可以成功启动了。
📃内存中用于文件系统的数据结构
文件信息初始化
下面来逐步看,就只看第一个循环目前:
void mount_root(void)
{for(i=0;i<64;i++)file_table[i].f_count=0;···
}
这个循环就干了一件事,把 64 个 file_table
里的 f_count
清零。来看看具体这个 file_table
,其表示进程所使用的文件,进程每使用一个文件都需要记录在这里面,包括文件类型,inode索引信息,引用次数f_count
,现在没有被引用,所以先将其都置为0。来看看代码里的具体实现:
struct file file_table[NR_FILE];struct file {unsigned short f_mode;unsigned short f_flags;unsigned short f_count;struct m_inode * f_inode;off_t f_pos;
};
来看一个file_table
的使用案例。比如现在有如下的命令:echo "hello" > 0
。这个命令表示将字符串“hello”写入到0号文件描述符。这个0号文件就是file_table[0]对应的文件。这个文件在硬盘哪里呢?注意到其中有个f_inode
成员,通过这个即可找到indoe信息,inode里面包含了一个文件所需要的全部信息,包括文件大小,文件类型,文件所在硬盘号等。此事已在前面有所记载。
超级块初始化
接着看这个函数后面的内容:
void mount_root(void)
{···for(p = &super_block[0] ; p < &super_block[8] ; p++) {p->s_dev = 0;p->s_lock = 0;p->s_wait = NULL;}···
}
这又是一个初始化的操作。super_block
就是我们之前讲的超级块,其作用也和之前的超级块一样,里面存的这个把设备的信息,通过这个超级块就可以掌握整个设备的文件系统全局了。
s_dev
:超级块对应的设备号,置为 0 表示未使用。
s_lock
:超级块锁,置为 0(未锁定)
s_wait
:等待队列指针,置为NULL
来看接下来的操作:
void mount_root(void)
{···if (!(p=read_super(0)))panic("Unable to mount root");···
}
接下来就是读取硬盘的超级块信息到内存中,read_super
函数就是读取硬盘中的超级块。
inode信息读取
接下来读取根目录的inode信息。
int ROOT_DEV = 0;
#define ROOT_INO 1void mount_root(void)
{···if (!(mi=iget(ROOT_DEV, ROOT_INO)))panic("Unable to read root i-node");mi->i_count += 3 ; // 逻辑上使用4次,初始为1···
}
iget
函数会获取根inode,同时会增加inode引用次数,表示inode被使用。
接下来就是将 inode 设置当前进程(新建的进程1)的根目录和工作目录:
void mount_root(void)
{···p->s_isup = p->s_imount = mi;current->pwd = mi; // 当前工作目录(m_inode指针)current->root = mi; // 根目录(m_inode指针)···
}
记录位图信息
超级块下面是块位图:
void mount_root(void)
{···free=0;i=p->s_nzones; // 文件系统的总磁盘块数while (-- i >= 0)if (!set_bit(i&8191,p->s_zmap[i>>13]->b_data))free++;···
}
首先来看看结构体p的内容:s_zmap 是位图数组,每个元素指向一个页,b_data是页的内存地址。用于管理块的占用状态。i>>13
是因为一页是 2^13 个块,超出了就是其他页,就是其他索引了。
其次来搞清楚set_bit
函数的含义:
#define set_bit(bitnr,addr) ({ \
register int __res __asm__("ax"); \
__asm__("bt %2,%3;setb %%al":"=a" (__res):"a" (0),"r" (bitnr),"m" (*(addr))); \
__res; })
这个不是设置位!!!是检查位的值。若 addr
的第 bitnr
位为 1,返回 1;否则返回 0。
8191换算成二进制是这个:1 1111 1111 1111
,共13个1,这是取出低13位的值。为什么是13呢?那是因为在早期文件系统,如 MINIX。每个位图页管理8192(2^13)个块。i&8191
表示位图页内偏移。合起来就是检查每个块对应的位是否为0,0就是空闲,就递增free
变量。通过 i >> 13
和 i & 8191
分页定位位图中的位。
free变量的含义就是统计文件系统中未被占用的磁盘块(空闲块)数量。
记录inode位图信息
最后就是inode位图:
void mount_root(void)
{···free=0;i=p->s_ninodes+1;while (-- i >= 0)if (!set_bit(i&8191,p->s_imap[i>>13]->b_data))free++;···
}
做法和目标同上面的块位图,只是i的值不一样。
🎯总结
这篇博客就主要讲了setup函数的内容,获硬盘信息以及加载根文件。
📖参考资料
[1] linux源码趣读
[2] 一个64位操作系统的设计与实现
[3] 硬盘基本知识(磁道、扇区、柱面、磁头数、簇、MBR、DBR)
[4] 分区表(Partition Table)是计算机硬盘驱动器上一个重要的数据结构