首页 > 代码库 > 对于硬盘驱动的理解

对于硬盘驱动的理解

目录

如何读写硬盘

  读写操作

  硬盘控制器端口及作用

  硬盘中断

  硬盘分区信息的获取

  如何读写文件

  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数组中取值就可以了。

  原来是这样,你想怎么样编号就怎么样编号,只要你自己能找到映射关系就可以了。

  获取信息的步骤:

  1. device = 0,style = P_PRIMARY
  2. 调用获取分区信息函数
  3. 如果style == P_ EXTENDED执行第10步
  4. 读取设备device的起始扇区,提取0x1BE处的4个表项到part_tbl
  5. 令i=0
  6. 判断第i个分区表项part_tbl[i]
  7. 如果是主分区,记录起始扇区sect_start和扇区数目setcs到相应的primary[i+1]。
  8. 如果是扩展分区,记录起始扇区sect_start和扇区数目setcs到相应的primary[i+1],令device += i+1,style = P_ EXTENDED跳到第2步
  9. 如果i>=4,结束;否则i++,执行第6步
  10. 扩展分区的起始扇区ext_start_sect = primary[device].base(这个值在第8步中已经计算出来了),求出该扩展分区的第一个逻辑分区的编号,nr_1st_sub = (device-1) * NR_SUB_PER_PART,计算该扩展分区第0个逻辑分区的起始地址s= ext_start_sect
  11. 令i=0(由于是递归调用,此处i的值并不影响第5步的i)
  12. 读取第nr_1st_sub+i个逻辑分区的起始扇区s,提取0x1BE处的2个表项(逻辑分区只使用分区表的两个表项)到part_tbl
  13. 记录逻辑分区的信息到logical[nr_1st_sub+i]
  14. s = ext_start_sect + part_tbl[1].start_sect
  15. 如果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就什么都不管了。进入下一个循环。

 

对于硬盘驱动的理解