当前位置: 首页 > news >正文

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_sectnr_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 >> 13i & 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)是计算机硬盘驱动器上一个重要的数据结构

http://www.xdnf.cn/news/367039.html

相关文章:

  • 【技术突破】CAN转Profinet:破解堆垛起重机智能互联的“密钥”
  • Python爬虫抓取Bilibili弹幕并生成词云
  • Qt 系统相关
  • 元强化学习
  • Django项目中不同app使用不同数据库的实现
  • MySQL主从同步(主从复制)
  • PPL困惑度的计算
  • 使用 NSSM 安装 Tomcat 11.0.6 为 Windows 服务
  • loop对象
  • 根据文件路径获取base64照片
  • 具身智能数据集解析
  • LVGL的核心:lv_timer_handler
  • 【AI入门】CherryStudio入门7:引入魔搭中的MCP服务
  • WDG看门狗(独立看门狗和窗口看门狗)
  • Babylon.js学习之路《二、开发环境搭建与第一个Hello World》
  • Windows11开机后黑屏,且任务管理器点击无反应
  • JGL051厌氧反应加膜生物反应实验装置
  • 数据结构5.0
  • YOLO数据集标注工具LabelImg(打包Exe版本及使用)
  • 请求从发送到页面渲染的全过程
  • 体育数据库:搭建体育应用的核心「数据引擎」
  • PHP:互联网时代的经典编程语言魅力与未来展望
  • 关于大数据的基础知识(一)——定义特征结构要素
  • 人工智能顶会ICLR 2025论文分享│PointOBB-v2:更简单、更快、更强的单点监督有向目标检测
  • 红黑树算法笔记(一)
  • 聚焦边缘 AI 推理,Akamai 发布最新云与 AI 战略
  • 火山引擎火山云主推产品
  • 两根485线支持多少通信协议
  • C++Primerplus编程练习 第六章
  • 操作系统 == 内存管理