Skip to content

Commit

Permalink
✨ 075 硬盘异步 PIO
Browse files Browse the repository at this point in the history
  • Loading branch information
StevenBaby committed Aug 26, 2022
1 parent 0836a23 commit 3b96094
Show file tree
Hide file tree
Showing 6 changed files with 137 additions and 14 deletions.
73 changes: 73 additions & 0 deletions docs/09 设备驱动/075 硬盘异步 PIO.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# 硬盘异步 PIO

由于同步状态检测,消耗了大量的 CPU 资源,所以可以使用异步的方式来等待硬盘驱动器。给驱动器发送完读写命令后,进程可以进入阻塞态,当驱动器完成一个扇区的操作 (读/写) 时,会发送中断,可以在中断中恢复进程到就绪态,继续执行。

---

## 中断

注意:当命令以错误结束时,它不会生成 IRQ。每秒检查几次备用状态寄存器是明智的,以查看是否设置了 ERR 位。否则,直到命令超时您才会知道。

---

## 处理中断

在早期,IRQ 的唯一目的是通知 IRQ 处理程序驱动器已经准备好发送或接受数据。我们的期望是 IRQ 处理程序本身将立即对下一个数据块执行基于 PIO 的数据传输。现在事情没那么简单了。总线上的一个或两个驱动器可能处于 DMA 模式,或者数据块大小不是 256 个 16 位值。此外,现在更强调以尽可能快的速度从 IRQ 处理程序例程中返回。所以问题是:IRQ处理程序需要做的最小操作集是什么?

如果您正在使用 IRQ 共享,您将需要检查 PCI 总线主状态字节,以验证 IRQ 来自磁盘。如果是这样,就需要读取常规状态寄存器一次,以使磁盘清除其中断标志。如果状态寄存器中的 ERR 位被设置(位 0,值 = 1),您可能想从错误 IO 端口(主总线上 0x1F1)读取并保存 **错误详细信息** 值。

如果传输是一个 READ DMA 操作,则必须从总线主状态寄存器读取值。因为 IRQ 处理程序可能不知道操作是否是 DMA 操作,所以在所有 IRQ 之后 (如果总线是由 PCI 控制器控制的,几乎肯定是这样),您可能会检查 Busmaster Status 字节。如果该字节设置了 ERR 位(位 1,值 = 2),您可能希望将当前值保存在磁盘的 LBA IO 端口中,它们可以告诉您驱动器上哪个扇区生成了错误。您还需要清除错误位,方法是向其写入一个 2。

您还需要向两个 PIC 发送 EOI (0x20),以清除它们的中断标志。然后,您需要设置一个标志来“解除”驱动程序,并让它知道发生了另一个 IRQ ——这样驱动程序就可以进行任何必要的数据传输。

注意:如果你仍然处于单请求模式,并且只在 PIO 模式下轮询常规状态寄存器,那么 IRQ 处理器需要做的唯一一件事就是向 PCI 发送 EOI。您甚至可能想要设置控制寄存器的 nIEN 位,以尝试完全关闭磁盘 IRQ。

---

## 轮询状态 VS 中断

当驱动发出 PIO 读写命令时,需要等待驱动准备好后才能传输数据。有两种方法可以知道驱动器何时准备好接收数据。当它就绪时,驱动器将发送一个 IRQ。或者,驱动程序可以轮询其中一个状态端口(常规或备用状态)。

轮询有两个优点:

- 轮询的响应速度比 IRQ 快
- 轮询的逻辑比等待 IRQ 简单得多

还有一个巨大的缺点:

- 在多任务环境中,轮询会耗尽所有CPU时间
- 但是,在单请求模式下,这不是问题(CPU 没有更好的事情可做),所以轮询是一件好事

如何轮询(等待驱动器准备传输数据):

- 读取常规状态端口,直到第 7 位(BSY,值= 0x80)清除,和第 3 位(DRQ,值= 8)设置
- 或直到第 0 位(ERR,值= 1) 或 第 5 位(DF,值= 0x20)设置。
- 如果两个错误位都没有设置,那么设备就已经准备好了

---

## 抢占/防止中断触发

如果驱动程序在向驱动器发送命令后读取了常规状态端口,则 **响应** IRQ 可能永远不会发生

如果您想要接收 IRQs,那么总是读取 **备用状态端口**,而不是常规状态端口。但有时 IRQ 只是浪费资源,让它们消失可能是一个好主意。

防止 ATA IRQs 发生的更完整的方法是在特定选定驱动器的控制寄存器中设置 nIEN 位。这应该会阻止总线上的驱动器发送任何 IRQs,直到你再次清除该位。

然而,它可能并不总是有效,一些程序员报告了 nIEN 运行时的问题。当 nIEN 是总线上的选定驱动器时,驱动器只响应新写入的 nIEN 值。

也就是说,如果选择了一个驱动器,并且您设置了 nIEN,然后选择带有驱动器选择寄存器的另一个驱动器,然后清除 nIEN ——然后第一个驱动器应该永远“记住”它被告知不要发送 irq ——直到您再次选择它,并在控制寄存器中写入一个 0 到 nIEN 位。

### 读写多块

在多任务 PIO 模式下,尝试减少 IRQ 数量的一种方法是使用 READ MULTIPLE (0xC4) 和 WRITE MULTIPLE (0xC5) 命令。这

些命令使驱动器缓冲区的扇区“块”,并且每个块只发送一个 IRQ,而不是每个扇区发送一个 IRQ。参考 IDENTIFY 命令的 uint16_t 47 和 59 来确定一个块中扇区的数量。您还可以尝试使用 SET MULTIPLE MODE (0xC6) 命令来更改每个块的扇区。

注意:总的来说,PIO 模式是一种慢速传输方式。在实际工作条件下,几乎任何驱动器都应该由 DMA 驱动器控制,而不应该使用 PIO。试图通过抢占 IRQ(或任何其他方法)来加快 PIO 模式的速度基本上是在浪费时间和精力。但是,400MB 或更小的 ATA 驱动器可能不支持多字 DMA 模式0。如果您希望支持这种大小的驱动器,那么在 PIO 模式驱动程序上花点功夫可能是值得的。

## 参考文献

- <https://wiki.osdev.org/PCI_IDE_Controller>
- Information Technology - AT Attachment - 8 ATA/ATAPI Command Set (ATA8-ACS)
- <https://wiki.osdev.org/ATA_PIO_Mode>
1 change: 1 addition & 0 deletions src/include/onix/ide.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ typedef struct ide_ctrl_t
u16 iobase; // IO 寄存器基址
ide_disk_t disks[IDE_DISK_NR]; // 磁盘
ide_disk_t *active; // 当前选择的磁盘
struct task_t *waiter; // 等待控制器的进程
} ide_ctrl_t;

int ide_pio_read(ide_disk_t *disk, void *buf, u8 count, idx_t lba);
Expand Down
17 changes: 16 additions & 1 deletion src/kernel/gate.c
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,24 @@ static void sys_default()
panic("syscall not implemented!!!");
}

#include <onix/string.h>
#include <onix/ide.h>

extern ide_ctrl_t controllers[2];

static u32 sys_test()
{
// LOGK("syscall test...\n");
u16 *buf = (u16 *)alloc_kpage(1);
LOGK("pio read buffer 0x%p\n", buf);
ide_disk_t *disk = &controllers[0].disks[0];
ide_pio_read(disk, buf, 4, 0);
BMB;

memset(buf, 0x5a, 512);

ide_pio_write(disk, buf, 1, 1);

free_kpage((u32)buf, 1);
return 255;
}

Expand Down
4 changes: 2 additions & 2 deletions src/kernel/handler.asm
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,8 @@ INTERRUPT_HANDLER 0x2a, 0
INTERRUPT_HANDLER 0x2b, 0
INTERRUPT_HANDLER 0x2c, 0
INTERRUPT_HANDLER 0x2d, 0
INTERRUPT_HANDLER 0x2e, 0
INTERRUPT_HANDLER 0x2f, 0
INTERRUPT_HANDLER 0x2e, 0; harddisk1 硬盘主通道
INTERRUPT_HANDLER 0x2f, 0; harddisk2 硬盘从通道

; 下面的数组记录了每个中断入口函数的指针
section .data
Expand Down
53 changes: 43 additions & 10 deletions src/kernel/ide.c
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,24 @@

ide_ctrl_t controllers[IDE_CTRL_NR];

void ide_handler(int vector)
{
send_eoi(vector); // 向中断控制器发送中断处理结束信号

// 得到中断向量对应的控制器
ide_ctrl_t *ctrl = &controllers[vector - IRQ_HARDDISK - 0x20];

// 读取常规状态寄存器,表示中断处理结束
u8 state = inb(ctrl->iobase + IDE_STATUS);
LOGK("harddisk interrupt vector %d state 0x%x\n", vector, state);
if (ctrl->waiter)
{
// 如果有进程阻塞,则取消阻塞
task_unblock(ctrl->waiter);
ctrl->waiter = NULL;
}
}

static u32 ide_error(ide_ctrl_t *ctrl)
{
u8 error = inb(ctrl->iobase + IDE_ERR);
Expand Down Expand Up @@ -157,6 +175,7 @@ static void ide_pio_write_sector(ide_disk_t *disk, u16 *buf)
int ide_pio_read(ide_disk_t *disk, void *buf, u8 count, idx_t lba)
{
assert(count > 0);
assert(!get_interrupt_state()); // 异步方式,调用该函数时不许中断

ide_ctrl_t *ctrl = disk->ctrl;

Expand All @@ -176,7 +195,16 @@ int ide_pio_read(ide_disk_t *disk, void *buf, u8 count, idx_t lba)

for (size_t i = 0; i < count; i++)
{
task_t *task = running_task();
if (task->state == TASK_RUNNING) // 系统初始化时,不能使用异步方式
{
// 阻塞自己等待中断的到来,等待磁盘准备数据
ctrl->waiter = task;
task_block(task, NULL, TASK_BLOCKED);
}

ide_busy_wait(ctrl, IDE_SR_DRQ);

u32 offset = ((u32)buf + i * SECTOR_SIZE);
ide_pio_read_sector(disk, (u16 *)offset);
}
Expand All @@ -189,6 +217,7 @@ int ide_pio_read(ide_disk_t *disk, void *buf, u8 count, idx_t lba)
int ide_pio_write(ide_disk_t *disk, void *buf, u8 count, idx_t lba)
{
assert(count > 0);
assert(!get_interrupt_state()); // 异步方式,调用该函数时不许中断

ide_ctrl_t *ctrl = disk->ctrl;

Expand All @@ -213,6 +242,14 @@ int ide_pio_write(ide_disk_t *disk, void *buf, u8 count, idx_t lba)
u32 offset = ((u32)buf + i * SECTOR_SIZE);
ide_pio_write_sector(disk, (u16 *)offset);

task_t *task = running_task();
if (task->state == TASK_RUNNING) // 系统初始化时,不能使用异步方式
{
// 阻塞自己等待磁盘写数据完成
ctrl->waiter = task;
task_block(task, NULL, TASK_BLOCKED);
}

ide_busy_wait(ctrl, IDE_SR_NULL);
}

Expand Down Expand Up @@ -262,14 +299,10 @@ void ide_init()
LOGK("ide init...\n");
ide_ctrl_init();

void *buf = (void *)alloc_kpage(1);
BMB;
LOGK("read buffer %x\n", buf);
ide_pio_read(&controllers[0].disks[0], buf, 1, 0);
BMB;
memset(buf, 0x5a, SECTOR_SIZE);
BMB;
ide_pio_write(&controllers[0].disks[0], buf, 1, 1);

free_kpage((u32)buf, 1);
// 注册硬盘中断,并打开屏蔽字
set_interrupt_handler(IRQ_HARDDISK, ide_handler);
set_interrupt_handler(IRQ_HARDDISK2, ide_handler);
set_interrupt_mask(IRQ_HARDDISK, true);
set_interrupt_mask(IRQ_HARDDISK2, true);
set_interrupt_mask(IRQ_CASCADE, true);
}
3 changes: 2 additions & 1 deletion src/kernel/thread.c
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,9 @@ static void user_init_thread()

void init_thread()
{
// set_interrupt_state(true);
char temp[100]; // 为栈顶有足够的空间
set_interrupt_state(true);
test();
task_to_user_mode(user_init_thread);
}

Expand Down

0 comments on commit 3b96094

Please sign in to comment.