到目前为止,我们一直都是在使用软盘,还从来没有接触过硬盘,我们的操作系统也是在软盘上的。那么这篇文章开始,就记录如何实现一个简单的文件系统。这个文件系统将建立在真正的硬盘之上,它通过硬盘驱动程序直接操作硬盘。在编写这个文件系统的过程中,我们在上一篇文章中所实现的IPC机制将发挥作用,你会发现,通过几个消息,用户进程、文件系统和驱动程序之间可以方便地协同工作。顺带着,还将建立一个驱动程序框架的雏形。
既然我们对硬盘进行操作,我们需要先来认识一下硬盘。虽然大家想必都已经比较熟悉硬盘,但对于一些术语,我们还是我必要来说明一下的。书上这里对硬盘进行了一些说明,把它记录下来,以供阅读和扩展知识。
PC发展到今天,每一个部分细讲起来都是个很长的故事,无论其发展历史还是技术细节,都一言难尽,硬盘也是如此。更加令人迷惑的是,硬盘标准中有太多的术语和名词,让人眼花缭乱,甚至不知所云。在这里,书上将一些重要的术语进行了描述,当然,如果想要了解所有细节,可能需要翻阅一些标准文档,最重要的文档目前由T13技术委员会维护,他们的官方网站位于?http://www.t13.org。
很久以前,硬盘控制器和硬盘本身是分离的,直到有一天,西数(Western Digital)、康柏、CDC的一个事业部Imprimis(后来被希捷收购)等合作开发了一种新的接口标准,将硬盘控制器和硬盘合在一起,这一标准被称为IDE(Intergrated Drive Electronics)或者ATA(At Attachment,AT指的是IBM的PC/AT个人计算机)。事实上ATA这一名称更加“正确”,它是接口的“真正的”名字,而IDE这一名称仅仅是在当时区分开了那些控制器和硬盘分离的设计,它不是个标准的名称。不过IDE这个名称被使用得非常广泛,或许比ATA流传还广,有些人干脆称这一接口为IDE/ATA或者ATA/IDE。后来ATA被标准化,今天我们称之为ATA-1。
再后来ATA标准不断发展,陆续出现了ATA-2、ATA-3、ATA/ATAPI-4、ATA/ATAPI-5、ATA/ATAPI-6,以及ATA/ATAPI-7。所谓ATAPI,其实是CD-ROM的接口的真正名称,它是AT Attachment Packet Interface的缩写。ATAPI使得硬盘和CD-ROM的接口统一起来。
你可能还听过其它一些名词,诸如EIDE、FASTATA、FASTATA-2、ULTRAATA等,不要被迷惑,所有这些都是为了让产品听上去比较有新意而起的名字,也就是说,它们都是营销手段,或者是遵循某个标准的产品别名。它们并不是另外的标准。
你或许还听过“温盘”这个名称,它所指代的温彻斯特硬盘(Winchester Disk)其实是1973年IBM一款硬盘驱动器的代号。我们现在的硬盘都是直接或间接建立在温彻斯特技术(Winchester Technology)之上。了解这一事实很重要,因为在一些操作系统的代码中,仍使用Winchester这一名称指代硬盘驱动器。
近几年,一种新的标准出现了,它就是SATA(Serial ATA),或被称为“串口硬盘”,而且它正在逐渐取代原先的ATA。为了区分开来,之前的硬盘则被称为“并口硬盘”,原先ATA的叫法也相应地改为PATA(Parallel ATA)。ATA/ATAPI-7的文档包含了SATA的内容。从我们想要编写相当低级的驱动程序的角度来看,由于SATA通常兼容PATA的操作方式,所以暂时地,我们姑且认为所有的硬盘都是无差别的ATA硬盘。
如果你下载了ATA/ATAPI的技术文档,会看到有很多标准内容,但是我们能用到的却很少。
跟我们之前接触过的键盘控制器、VGA控制器等硬件类似,对硬盘控制器的操作仍是通过I/O端口来进行,这些端口分为两组,它们对应命令块寄存器(Command Block Registers)和控制块寄存器(Control Block Register),如下表所示。
组别 | I/O 端口 | 读时 | 写时 | |
Primary | Secondary | |||
Command Block Registers | 1F0h | 170h | Data | Data |
1F1h | 171h | Error | Features | |
1F2h | 172h | Sector Count | Sector Count | |
1F3h | 173h | LBA Low | LBA Low | |
1F4h | 174h | LBA Mid | LBA Mid | |
1F5h | 175h | LBA High | LBA High | |
1F6h | 176h | Device | Device | |
1F7h | 177h | Status | Command | |
Control Block Register | 3F6h | 376h | AlternateStatus | DeviceControl |
上表中的Primary和Secondary指的是ATA接口通道(Channel),通俗地说就是主板上的IDE口。一个普通的PC主板上通常有两个IDE口,分别对应两个IDE通道:Primary和Secondary,它们有时也被标注为IDE0和IDE1。每个IDE通道又能连接两个设备,设为主设备(Master)和从设备(Slave)。对不同的IDE通道的访问是通过I/O端口来区分的,对同一IDE通道上的主从设备的访问是通过Device寄存器上的第4位的值来区分的——第4位为0是操作主设备,为1时操作从设备。事实上一个机器不只允许有两个IDE通道,但超过两个的情况非常罕见,在此不做介绍。在书中,只考虑硬盘接在Primary通道的情况。
对硬盘的操作并不复杂,只需先往命令块寄存器(Command Block Registers)写入正确的值,再通过控制块寄存器(Control Block Register)发送命令就可以了。当然或许每个命令会有不同的编程细节,我们在遇见具体情况时再做介绍。
驱动程序的作用在于隐藏硬件细节,向上层进程提供统一的接口。由于我们的进程通过收发消息来相互通信,那么驱动程序的接口自然也是消息了。所以只要我们定义了驱动程序可以接收什么消息,也就定义了驱动程序的接口。为简单起见,我们先只定义一种消息:DEV_OPEN。我们稍后通过FS任务向硬盘驱动程序发送一个DEV_OPEN消息。可是硬盘驱动程序收到这个消息之后干点什么呢?我们还是先干点简单工作:向硬盘驱动器发送一个IDENTIFY命令,这个命令可以用来获取硬盘参数。
向硬盘发送IDENTIFY命令很简单,只需要通过Device寄存器的第4位指定驱动器——0表示Master,1表示Slave——然后往Command寄存器写入十六进制ECh就可以。硬盘准备好参数后,会产生一个中断,这时我们就可以通过Data寄存器读取数据了。参数有很多,总共是256个字(WORD),在下面的程序中,我们仅仅取出其中的几个值来显示。我们来看代码。
代码 kernel/hd.c,硬盘驱动,这是一个新建的文件。
PRIVATE void init_hd();
PRIVATE void hd_cmd_out(struct hd_cmd* cmd);
PRIVATE int waitfor(int mask, int val, int timeout);
PRIVATE void interrupt_wait();
PRIVATE void hd_identify(int drive);
PRIVATE void print_identify_info(u16* hdinfo);
PRIVATE u8 hd_status;
PRIVATE u8 hdbuf[SECTOR_SIZE * 2];
/* task_hd */
/* Main loop of HD driver */
PUBLIC void task_hd()
{
MESSAGE msg;
init_hd();
while (1) {
send_recv(RECEIVE, ANY, &msg);
int src = msg.source;
switch (msg.type) {
case DEV_OPEN:
hd_identify(0);
break;
default:
dump_msg("HD driver::unknown msg", &msg);
spin("FS::main_loop (invalid msg.type)");
break;
}
send_recv(SEND, src, &msg);
}
}
/**
* <Ring 1> Check hard drive, set IRQ handler, enable IRQ and initialize data structures.
*/
PRIVATE void init_hd()
{
/* Get the number of drives from the BIOS data area */
u8 * pNrDrives = (u8*)(0x475);
printl("NrDrives:%d.\n", *pNrDrives);
assert(*pNrDrives);
put_irq_handler(AT_WINI_IRQ, hd_handler);
enable_irq(CASCADE_IRQ);
enable_irq(AT_WINI_IRQ);
}
/**
* <Ring 1> Get the disk information.
*
* @param drive Drive Nr.
*/
PRIVATE void hd_identify(int drive)
{
struct hd_cmd cmd;
cmd.device = MAKE_DEVICE_REG(0, drive, 0);
cmd.command = ATA_IDENTIFY;
hd_cmd_out(&cmd);
interrupt_wait();
port_read(REG_DATA, hdbuf, SECTOR_SIZE);
print_identify_info((u16*)hdbuf);
}
/**
* <Ring 1> Print the hdinfo retrieved via ATA_IDENTIFY command.
*
* @param hdinfo The buffer read from the disk i/o port.
*/
PRIVATE void print_identify_info(u16* hdinfo)
{
int i, k;
char s[64];
struct iden_info_ascii {
int idx;
int len;
char * desc;
} iinfo[] = {{10, 20, "HD SN"}, /* Serial Number in ASCII */
{27, 40, "HD Model"} /* Model number in ASCII */
};
for (k = 0; k < sizeof(iinfo)/sizeof(iinfo[0]); k++) {
char * p = (char*)&hdinfo[iinfo[k].idx];
for (i = 0; i < iinfo[k].len/2; i++) {
s[i * 2 + 1] = *p++;
s[i * 2] = *p++;
}
s[i * 2] = 0;
printl("%s: %s\n", iinfo[k].desc, s);
}
int capabilities = hdinfo[49];
printl("LBA supported: %s\n", (capabilities & 0x0200 ? "Yes" : "No"));
int cmd_set_supported = hdinfo[83];
printl("LBA48 supported: %s\n", (cmd_set_supported & 0x0400) ? "Yes" : "No");
int sectors = ((int)hdinfo[61] << 16) + hdinfo[60];
printl("HD size: %dMB\n", sectors * 512 / 1000000);
}
/**
* <Ring 1> Output a command to HD controller.
*
* @param cmd The command struct ptr.
*/
PRIVATE void hd_cmd_out(struct hd_cmd * cmd)
{
/**
* For all commands, the host first check if BSY=1,
* and should proceed no further unless and unitl BSY=0
*/
if (!waitfor(STATUS_BSY, 0, HD_TIMEOUT)) {
panic("hd error");
}
/* Active the Interrupt Enable (nIEN) bit */
out_byte(REG_DEV_CTRL, 0);
/* Load required parameters in the Command Block Registers */
out_byte(REG_FEATURES, cmd->features);
out_byte(REG_NSECTOR, cmd->count);
out_byte(REG_LBA_LOW, cmd->lba_low);
out_byte(REG_LBA_MID, cmd->lba_mid);
out_byte(REG_LBA_HIGH, cmd->lba_high);
out_byte(REG_DEVICE, cmd->device);
/* Write the command code to the Command Register */
out_byte(REG_CMD, cmd->command);
}
/**
* <Ring 1> Wait until a disk interrupt occurs.
*/
PRIVATE void interrupt_wait()
{
MESSAGE msg;
send_recv(RECEIVE, INTERRUPT, &msg);
}
/**
* <Ring 1> Wait for a certain status.
*
* @param mask Status mark.
* @param val Required status.
* @param timeout Timeout in milliseconds.
*
* @return One if success, zero if timeout.
*/
PRIVATE int waitfor(int mask, int val, int timeout)
{
int t = get_ticks();
while (((get_ticks() - t) * 1000 / HZ) < timeout) {
if ((in_byte(REG_STATUS) & mask) == val) {
return 1;
}
}
return 0;
}
/**
* <Ring 0> Interrupt handler.
*
* @param irq IRQ nr of the disk interrupt
*/
PUBLIC void hd_handler(int irq)
{
/**
* Interrupts are cleared when the host
* - reads the Status Register,
* - issues a reset, or
* - writes to the Command Register.
*/
hd_status = in_byte(REG_STATUS);
inform_int(TASK_HD);
}
在代码中总共有这么几个函数:
task_hd()是硬盘驱动的主循环,它跟我们之前的任务没什么区别。开头调用init_hd()做一些初始化工作,然后开始不停地处理消息。我们目前只接收DEV_OPEN消息,收到它之后将会调用hd_identify()来获取并打印部分硬盘参数。
init_hd()中所做的工作分两部分。它显示从物理地址0x475处获取系统内硬盘数量——这个地址是由BIOS指定的。然后它指定hd_handler()为硬盘中断处理程序,并且打开硬盘中断。注意,这里不仅仅需要打开硬盘对应的中断信号线,还要打开主8259A用于级联从片的信号线。
hd_identify()便是获取硬盘参数的函数了。我们先来看看看Device寄存器的格式,如下图所示。
可以看到,寄存器主要有三部分:LBA模式位、DRV位和低四位。我们之前已经提到两次。DRV位(第4位)用于指定主盘或从盘,0表示主盘,1表示从盘。LBA模式位用于指定操作模式,当此位为0时,对磁盘的操作使用CHS模式,也即用“柱面/磁头/扇区号”来定位扇区;当此位为1时,对磁盘的操作使用LBA(Logical Block Address)模式。由于现代硬盘都支持LBA模式,在书中使用的也是LBA模式。寄存器的低四位,在CHS模式中表示磁头号,在LBA模式中表示LBA的24到27位。也就是说,LBA的地址被拆分成了四部分,第0到7为由LBA Low寄存器指定,第8到15位由LBA Mid寄存器指定,第16到23位由LBA High寄存器指定。这个LBA是28位的,经过计算可得知,2的28次方是268435456,这么多个扇区意味着128GB的容量,这也是28位LBA的最大寻址能力。
可是目前市面上的硬盘容量早就超过了128GB了,怎么突破限制呢?原理也很简单,每次读写的时候,三个寄存器——LBA Low、LBA Mid、LBA High——每个使用两次,这样就相当于有了6个寄存器来存储地址,这就是LBA48(ATA-6开始支持)。48位LBA的寻址上限为128PB,我们觉得并希望它是足够了,但是谁知道呢,或许不久的以后每个寄存器将不得不使用三次——我们已经见到太多例子,人们不停在想办法突破自己之前设下的限制,但当下一次做决定时,还是会低估技术的发展速度。
好了言归正传,由于Device这个寄存器有这么一点复杂,我们于是用一个宏来合成它,见下面代码。
代码 include/hd.h,MAKE_DEVICE_REG。
/* for DEVICE register. */
#define MAKE_DEVICE_REG(lba, drv, lba_highest) (((lba) << 6) | ((drv) << 4) | (lba_highest & 0xF) | 0xA0)
我们在代码61行用到了这个宏。
在接下来的63行,我们用了一个函数hd_cmd_out()来进行实际的向硬盘驱动器发送命令的工作,因为今后我们还会用到它。当发送命令之后,我们用interrupt_wait()这一函数来等待中断的发生,而等待的方法是调用了一个接收消息的函数。说到这里,在上一篇文章的代码里,我们在进行消息处理时,有一种单独的消息来源叫做INTERRUPT,在这里就用到了。之所以发生中断后还要通过消息来通知驱动程序,是因为驱动程序是运行在非内核态的独立进程,而发生中断这一事件是独立于进程的。驱动程序获知中断发生的情形可以参考下图,图中列出了两种情形。
第一种情况中各时间点/段的状况是这样的:
ω | 硬盘驱动调用hd_cmd_out()。 |
ω~α | 硬盘已处于工作状态,同时硬盘驱动也在运行——执行hd_cmd_out()到send_recv()之间的少量代码。 |
α | 硬盘驱动调用send_recv(),由于此时硬盘尚未完成工作,所以被阻塞,控制权于是交给Proc X。 |
α~β | Proc X运行中,同时硬盘也在工作。 |
β | 硬盘完成工作,并触发中断,执行中断处理程序hd_handler()(它应被视为内核的一部分,因为它工作在Ring0),它最终调用inform_int(),这会解除硬盘驱动的阻塞。 |
γ | hd_handler()结束,控制权回到Proc X手中。 |
δ | Proc X的时间片用完,调度程序将控制权交给硬盘驱动(它早在β时已被解除阻塞)。 |
第二种情况是这样的:
ω‘ | 硬盘驱动执行完hd_cmd_out()之后便由调度程序将控制权交给另一个进程Proc X。 |
ω‘~β’ | Proc X运行的同时,硬盘在工作。 |
β’ | 硬盘完成工作,并触发中断,执行中断处理程序hd_handler(),它最终调用inform_int()。 |
γ’ | hd_handler()结束,控制权还给Proc X。 |
δ’ | Proc X的时间片用完,调用程序将控制权交给硬盘驱动。 |
α’ | 硬盘驱动执行send_recv()得到消息,并继续执行。 |
这两种情况的区别在于,第一种情况调用send_recv()时无法立刻返回,因为那时硬盘还没有完成任务,而第二种情况调用send_recv()时直接返回,因为硬盘已经把数据准备好了。不管是哪种情况,从驱动程序的角度来看,都是等待一个中断的发生,而且它应该能够等到。
当中断发生以后,接下来的第65行从DATA端口读取数据,这样硬盘参数就得到了。
接下来的print_identify_info()选取了其中几个参数打印了出来。它的偏移和解释见下表所示,如果大家有兴趣,可以从T13网站上下载AT Attachment with Packet Interface文档以获得更详细的参数列表。
通过IDENTIFY命令得到的硬盘参数(节选)。
偏移 | 描述 |
10~19 | 序列号(20个ASCII字符) |
27~46 | 型号(40个ASCII字符) |
60~61 | 用户可用的最大扇区数 |
49 | 功能(Capabilities)(bit 9 为 1 表示支持 LBA) |
83 | 支持的命令集(bit 10 为 1 表示支持 48 位寻址) |
到这里我们获取硬盘参数并打印的整体流程已经清楚了。下面我们就来看看用来向硬盘发送命令的函数hd_cmd_out()。它的一开头先判断硬盘Status寄存器(参考下图9.3)的BSY位,因为只有这一位为0时我们才能行动。若BSY为零则先通过Device Control寄存器(参考下图9.4)打开中断,再依次写Features、Sector Count、LBA Low、LBA Mid、LBA High、Device和Command等寄存器。一旦写入Command寄存器,命令便被执行了。函数hd_cmd_out()接收的参数类型为一结构体,它里面保存的是我们要写入的寄存器应有的值。
代码第172行处是一个只有两行代码的硬盘中断处理程序,它显示通过读Status寄存器来恢复中断响应,然后调用inform_int()来通知驱程序。
再次说明一下,硬盘中断处理程序虽然也放在hd.c中,但它跟其它代码有着本质的不同。中断处理程序运行在RIng0,是内核的一部分,而其它代码都是驱动程序进程运行时调用,都运行在RIng1。
好了,现在我们来看一下代码中用到的其它几个函数的内容,首先是在函数task_hd()中调用的dump_msg()函数,该函数的作用是用来打印消息结构体MESSAGE的内容,具体代码如下所示。
代码 kernel/proc.c,dump_msg。
PUBLIC void dump_msg(const char * title, MESSAGE* m)
{
int packed = 0;
printl("{%s}<0x%x>{%ssrc:%s(%d),%stype:%d,%s(0x%x, 0x%x, 0x%x, 0x%x, 0x%x, 0x%x)%s}%s",
title,
(int)m,
packed ? "" : "\n ",
proc_table[m->source].p_name,
m->source,
packed ? " " : "\n ",
m->type,
packed ? " " : "\n ",
m->u.m3.m3i1,
m->u.m3.m3i2,
m->u.m3.m3i3,
m->u.m3.m3i4,
(int)m->u.m3.m3p1,
(int)m->u.m3.m3p2,
packed ? "" : "\n",
packed ? "" : "\n"
);
}
第二个是函数inform_int(),作用是用来通知驱动程序,代码如下所示。
/**
* <Ring 0> Inform a proc that an interrupt has occured.
*
* @param task_nr The task which will be informed.
*****************************************************************************/
PUBLIC void inform_int(int task_nr)
{
PROCESS* p = proc_table + task_nr;
if ((p->p_flags & RECEIVING) && /* dest is waiting for the msg */
((p->p_recvfrom == INTERRUPT) || (p->p_recvfrom == ANY))) {
p->p_msg->source = INTERRUPT;
p->p_msg->type = HARD_INT;
p->p_msg = 0;
p->has_int_msg = 0;
p->p_flags &= ~RECEIVING; /* dest has received the msg */
p->p_recvfrom = NO_TASK;
assert(p->p_flags == 0);
unblock(p);
assert(p->p_flags == 0);
assert(p->p_msg == 0);
assert(p->p_recvfrom == NO_TASK);
assert(p->p_sendto == NO_TASK);
}
else {
p->has_int_msg = 1;
}
}
第三个函数是port_read(),用于从指定的端口中读取数据,代码如下所示。
代码 lib/kliba.asm,port_read。
...
global port_read
...
; void port_read(u16 port, void* buf, int n);
port_read:
mov edx, [esp + 4] ; port
mov edi, [esp + 4 + 4] ; buf
mov ecx, [esp + 4 + 4 + 4] ; n
shr ecx, 1
cld
rep insw
ret
最后我们来看一下结构体hd_cmd的定义和代码中用到的和硬盘驱动相关宏的定义。
代码 include/hd.h,hd_cmd和宏定义。
struct hd_cmd {
u8 features;
u8 count;
u8 lba_low;
u8 lba_mid;
u8 lba_high;
u8 device;
u8 command;
};
/* Command Block Registers */
#define REG_DATA 0x1F0
#define REG_FEATURES 0x1F1
#define REG_ERROR REG_FEATURES
#define REG_NSECTOR 0x1F2 /* Sector Count I/O */
#define REG_LBA_LOW 0x1F3 /* Sector Number / LBA Bits 0-7 I/O */
#define REG_LBA_MID 0x1F4 /* Cylinder Low / LBA Bits 8-15 I/O */
#define REG_LBA_HIGH 0x1F5 /* Cylinder High / LBA Bits 16-23 I/O */
#define REG_DEVICE 0x1F6
#define REG_STATUS 0x1F7
#define REG_CMD REG_STATUS
#define REG_DEV_CTRL 0x3F6
#define REG_ALT_STATUS REG_DEV_CTRL
#define REG_DRV_ADDR 0x3F7
#define STATUS_BSY 0x80
/* DEFINITIONS */
#define HD_TIMEOUT 10000 /* in millisec */
#define PARTITION_TABLE_OFFSET 0x1BE
#define ATA_IDENTIFY 0xEC
#define ATA_READ 0x20
#define ATA_WRITE 0x30
硬盘中断的信号线是连在8259A从片上的,这是我们第一次用到从片,之前的文章中记录了处理主片的宏,现在我们就按照处理主片宏的方式写一个处理从片的宏,代码如下所示。
代码 kernel/kernel.asm,hwint_slave。
%macro hwint_slave 1
call save
in al, INT_S_CTLMASK ; `.
or al, (1 << (%1 - 8)) ; | 屏蔽当前中断
out INT_S_CTLMASK, al ; /
mov al, EOI ; `. 置EOI位(master)
out INT_M_CTL, al ; /
nop ; `. 置EOI位(slave)
out INT_S_CTL, al ; / 一定注意:salve和master都要置EOI
sti ; CPU在响应中断的过程中会自动关闭中断,这句之后就允许响应新的中断
push %1 ; `.
call [irq_table + 4 * %1] ; | 中断处理程序
pop ecx ; /
cli
in al, INT_S_CTLMASK ; `.
and al, ~(1 << (%1 - 8)) ; | 恢复接受当前中断
out INT_S_CTLMASK, al ; /
ret
%endmacro
一定要注意,从片连接的设备发生中断之后,从片和主片都要置EOI,忘掉主片的话,硬盘就再也响应不了中断了。
好了,硬盘驱动程序暂时就这些内容了,我们来看看使用它的FS进程,见下面代码。
代码 fs/main.c,新建立的文件系统进程,该.c文件是一个新文件。
/**
* <Ring 1> The main loop of TASK FS.
*/
PUBLIC void task_fs()
{
printl("Task FS begins.\n");
/* open the device: hard disk */
MESSAGE dirver_msg;
dirver_msg.type = DEV_OPEN;
send_recv(BOTH, TASK_HD, &dirver_msg);
spin("FS");
}
可以看到,我们新建立的文件系统进程其实就是个空壳,它什么都没有做,只是向硬盘驱动程序发送一个DEV_OPEN消息。跟之前的所有进程一样,FS进程会在系统启动时开始运行,所以我们在启动操作系统之后很快硬盘驱动就会接收到DEV_OPEN消息,并且获取硬盘参数并进行一些打印工作。
现在我们把写好的硬盘驱动程序和FS进程配置到global.c的task_table[]数组中:
PUBLIC TASK task_table[NR_TASKS] = {{task_tty, STACK_SIZE_TTY, "task_tty"},
{task_sys, STACK_SIZE_SYS, "task_sys"},
{task_hd, STACK_SIZE_HD, "task_hd"},
{task_fs, STACK_SIZE_FS, "task_fs"}};
不要忘记在相应的文件中定义有关的宏和声明方法。
下面我们就该运行查看效果了,不过我们的机器目前还没有硬盘,这不要紧,我们使用 bximage 命令造一个硬盘出来:
硬盘的大小和名称可以根据你的想法进行设置。好了,简单的操作就创建出了一块硬盘。更好的是,bximage把应该把什么放进Bochs配置文件都给列出来了,我们马上来修改bochsrc,相当于把新造好的硬盘插入机器。
ata0: enabled=true, ioaddr1=0x1f0, ioaddr2=0x3f0, irq=14
ata0-master: type=disk, path="hd.img", mode=flat
下面我们就可以运行查看效果了,但还是要提醒一下,我们新添加了两个C文件,不要忘记更改Makefile。运行效果如下图所示。
真是太棒了,我们的操作系统成功获取了硬盘参数。
如果你有兴趣,完全可以对照ATA文档,打印出更多硬盘参数。
欢迎关注我的公众号