目录
如何读写硬盘
读写操作
硬盘控制器端口及作用
硬盘中断
硬盘分区信息的获取
如何读写文件
TASK_HD
如何读写硬盘
读写操作
第一次看到linux 0.12关于读写硬盘几行代码时候,感觉很费解。
do_hd = intr_addr; outb_p(hd_info[drive].ctl,HD_CMD); port=HD_DATA; 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)
我还是不明白怎么这样就可以读写硬盘了。但是代码到此就结束了。
一直好奇程序是如何控制硬件的,这些指令就是一个个电信号在cpu中流动,怎么就能把硬盘中的数据拿到内存中呢?
正好在同一个学期开设了《计算机组成原理》和《微机原理与接口技术》这两门课程,那个时候才了解到端口的意思,了解到cpu寻址、数据传输的流程。
往端口写了数据和指令,剩下的我们只能相信硬件制造商的设计和生产能力了,然后默默等待硬件的回应。我记得当时自己疑惑了一段时间,苦于没有人来提醒这一点,可能会的人感觉这根本不是问题吧。
硬盘控制器端口及作用
Linux 0.12当时操作的硬盘是CHS寻址模式,起始扇区编号是1。对于《实现》来说,用bochs自带的工具bximage命令生成的虚拟硬盘是LBA寻址模式的,起始扇区编号是0。CHS模式和LBA模式的端口号和操作方式都一样,只是有一些端口代表的意义不一样了,来看一下LBA寻址模式的端口作用,借用书中的表9.1。
表1 LBA寻址模式的硬盘端口及其作用
I/O端口 | 读时 | 写时 | |
primary | secondary | ||
1F0H | 170H | Data | |
1F1H | 171H | Error | Features |
1F2H | 172H | Sector count | |
1F3H | 173H | LBA low | |
1F4H | 174H | LBA mid | |
1F5H | 175H | LBA high | |
1F6H | 176H | Device | |
1F7H | 177H | Status | Command |
3F6H | 376H | Alternate status | Device control |
其中Device寄存器比较特殊,它用来指明寻址模式。来看一下格式。
表2 Device寄存器各个bit为的意义
Bit位 | 值 | 意义 |
7 | 1 | |
6 | L | 0表示CHS模式,1表示LBA模式 |
5 | 1 | |
4 | DRV | 0表示主盘,1表示从盘 |
3 | HS3 |
如果是L=0,CHS模式,那么这四位的值表示磁头号
如果L=1,LBA模式,那么这四位的值表示LBA的24到27位 |
2 | HS2 | |
1 | HS1 | |
0 | HS0 |
从上面的代码可以很清楚的看到如何读写硬盘,往相应的端口写上我们要读多少个扇区,读哪个扇区,哪个柱面,哪个磁头,哪个硬盘,然后告诉硬盘我们的需求cmd,读或者写。
另外,CHS模式下,硬盘扇区编号从1开始编号。LBA模式下,从0开始编号。
硬盘中断
我们怎么知道硬盘的工作做完了没有呢?只能等待硬盘产生中断信号,通过8259A告诉cpu,这个中断信号是哪个硬件产生的。
在书中,用的是微内核,所有的进程都给TASK_HD(硬盘驱动)发送读写硬盘的命令,而不是自己调用硬盘驱动中的读写函数。所以中断产生后,仅仅需要通知TASK_HD这个进程,TASK_HD会把硬盘准备好的数据读到发出读请求进程指定的内存位置。
硬盘分区信息的获取
前面说了如何向硬盘发送命令,让它读写哪些扇区,但是这些参数都是我们提前计算好的。如何计算这些参数?我们又是如何知道该读写那个扇区呢?
之所以把分区信息的介绍放到读写文件这一小节中,是因为我觉得分区信息和文件关联很大。我们要读写文件,才需要知道分区信息,如果我们不需要按照文件形式来读写硬盘,那么知不知道分区信息就无所谓啦,凭我们的大脑记住要读取的数据在第几个分区,到时候直接汇编操作寄存器就好啦。
那为什么要分区呢?似乎不分区把所有的数据都杂糅在一起,电脑也可以正常运行啊。我百度了一下,大概是由于为了把操作系统和数据分开吧。试想,如果所有的东西和操作系统共处一个空间,那么操作系统崩溃了,这个空间的所有数据的记录索引在重新安装操作系统后都会失效,尽管数据本身依然很正常,但是由于记录索引丢失,我们却没法找到他们。如果分区了,那么最多操作系统的所在分区的数据拿不到了,其他分区数据的记录索引还在。
如何获取分区信息?
在硬盘的0号扇区(MBR扇区)偏移0x1BE处保存的有一张硬盘主分区表。只有四个表项,也就是说一个硬盘只能记录四个主分区,据说是因为当初IBM认为一个PC上装4个操作系统(只有主分区上能安装操作系统)就够用了。如果想要更多的分区,那么需要在格式化的时候指明一个(只能有一个主分区记录能用于扩展分区)表项用作扩展分区,扩展分区并不能直接使用,在这个扩展分区里面我们还要划分出逻辑分区,每一个逻辑分区的起始扇区记录的分区表只能使用两个表项。
对于操作系统而言,每个分区都被当做一个独立的设备对待。
那么书中如何记录分区信息呢?看一下保存数据的结构体:
struct part_info { u32 base; /* # of start sector (NOT byte offset, but SECTOR) */ u32 size; /* how many sectors in this partition */ }; /* main drive struct, one entry per drive */ struct hd_info { int open_cnt; struct part_info primary[NR_PRIM_PER_DRIVE];//计算后NR_PRIM_PER_DRIVE = 5 struct part_info logical[NR_SUB_PER_DRIVE];// 计算后NR_SUB_PER_DRIVE = 64 };
书中根设备编号是0x322,可以知道子设备号是0x22,一开始很困惑,这么大的子设备号,难道要分0x22个分区?或者说系统怎么就知道0x22表示的是根分区呢?
还得再看一段代码
logidx = (p->DEVICE - MINOR_hd1a) % NR_SUB_PER_DRIVE; sect_nr += p->DEVICE < MAX_PRIM ? hd_info[drive].primary[p->DEVICE].base : hd_info[drive].logical[logidx].base;
先将设备号减去第一个逻辑设备的编号得到设备号在logical数组的下标。当然,可能这个设备号不是逻辑设备,而是主分区。没关系,下一步判断p->DEVICE 是不是小于MAX_PRIM,如果小于,说明是主分区,直接用p->DEVICE在primary数组中取值就可以了。
原来是这样,你想怎么样编号就怎么样编号,只要你自己能找到映射关系就可以了。
获取信息的步骤:
- device = 0,style = P_PRIMARY
- 调用获取分区信息函数
- 如果style == P_ EXTENDED执行第10步
- 读取设备device的起始扇区,提取0x1BE处的4个表项到part_tbl
- 令i=0
- 判断第i个分区表项part_tbl[i]
- 如果是主分区,记录起始扇区sect_start和扇区数目setcs到相应的primary[i+1]。
- 如果是扩展分区,记录起始扇区sect_start和扇区数目setcs到相应的primary[i+1],令device += i+1,style = P_ EXTENDED跳到第2步
- 如果i>=4,结束;否则i++,执行第6步
- 扩展分区的起始扇区ext_start_sect = primary[device].base(这个值在第8步中已经计算出来了),求出该扩展分区的第一个逻辑分区的编号,nr_1st_sub = (device-1) * NR_SUB_PER_PART,计算该扩展分区第0个逻辑分区的起始地址s= ext_start_sect
- 令i=0(由于是递归调用,此处i的值并不影响第5步的i)
- 读取第nr_1st_sub+i个逻辑分区的起始扇区s,提取0x1BE处的2个表项(逻辑分区只使用分区表的两个表项)到part_tbl
- 记录逻辑分区的信息到logical[nr_1st_sub+i]
- s = ext_start_sect + part_tbl[1].start_sect
- 如果i>=16,本次递归结束,返回到第8步;否则i++,执行第12步
感觉文字叙述理解起来可能比较模糊,但是比代码实现起来还是省事一些,像读分区起始扇区,一句话带过,知道怎么做就可以了,如果用代码描述,可能还要牵扯到其他知识点。
如何读写文件
其实对于硬盘驱动而言,没有文件这个概念,只有扇区。硬盘驱动能接受的参数就是要读写的起始扇区,读写扇区个数。文件这个概念由上层的文件系统来处理。
这个时候,我们会想起来inode结构体中有两个记录是i_dev和i_start_sect,这两个元素把上层文件系统和硬盘关联起来了。当我们要读某某个文件的时候,文件系统告诉硬盘驱动读目录区,把文件的inode号找到,再读indoe到内存中,这个时候就有了文件在哪个分区i_dev,数据存放在第i_start_sect号扇区,及之后一共的x800个扇区中,这个i_start_sect的值是相对于分区i_dev为起始偏移的。
知道i_dev和i_start_sect之后,硬盘驱动可以做什么呢?首先将以i_dev分区为起始偏移的i_start_sect转化为相对于整个硬盘。怎么转化呢?上面获取分区信息的时候,每个分区的起始扇区都被记录,我们找到i_dev的起始扇区,加上i_start_sect就是相对于整个硬盘的了。
这样就把文件读进来了。至于读文件哪一段的内容,其实还是上层的文件系统来记录处理的,还记得file结构体中有一个元素是pos,这个值就是用来标明要读写的内容在文件中的偏移。将pos/SECT_SIZE再加上上面计算的文件相对于整个硬盘的偏移,就是要读写的某一段数据了。
所谓块设备的名称也许就是这样由来的吧,一次最少处理的数据是一个扇区(当然,不同的硬盘给出的接口肯定不一样)。
TASK_HD
这样一来,TASK_HD的任务就是很简单了啊,接收TASK_FS发送的读写请求,将针对于i_dev设备的i_start_sect转化为相对于整个硬盘的扇区号,再加上pos/SECT_SIZE,然后读写这个扇区交给TASK_FS就什么都不管了。进入下一个循环。