Async IO, IOAPIC and improvements

This commit is contained in:
Aaron committed 2026-03-03 18:35:10 +00:00
1 parent 81d1b97cd0
commit 64a83a6c83
15 files changed
+782 -348

No files matched your search

File diff suppressed because it is too large. Load diff
+26 -8
View File
@@ -248,24 +248,42 @@ type
PM
);
{ Callback signature fired from IRQ context when an async I/O command completes.
Must be very short — set a flag or post to a queue.
Never call kalloc/kfree/VFS from within this callback. }
TIOCompletion = procedure(success : boolean; userdata : puint32);
{ Per-slot pending operation — one entry per HBA command slot }
TPendingOp = record
inUse : boolean;
completion : TIOCompletion;
userdata : puint32;
end;
{
AHCI device reference
AHCI device reference.
Declared as a plain record (not bitpacked) so that procedure-pointer fields
in TPendingOp are safe to store and call.
}
TAHCI_Device = bitpacked record
port : PHBA_Port;
device_type : TDeviceType;
ata_info : TIdentResponse;
TAHCI_Device = record
port : PHBA_Port;
device_type : TDeviceType;
ata_info : TIdentResponse;
command_list : PHBA_CMD_HEADER;
fis : PHBA_FIS;
command_table : PHBA_CMD_TABLE;
fis : PHBA_FIS;
command_table: PHBA_CMD_TABLE;
{ One pending-operation slot per HBA command slot (AHCI supports up to 32) }
pending : array[0..31] of TPendingOp;
end;
PAHCI_Device = ^TAHCI_Device;
{
controller reference
Declared as a plain record (not bitpacked) because it embeds TAHCI_Device
which contains procedure-pointer fields.
}
TAHCI_Controller = bitpacked record
TAHCI_Controller = record
pci_device : PPCI_Device;
mio: PHBA_Memory;
ata_info : TIdentResponse;
+58
View File
@@ -0,0 +1,58 @@
# AHCI Driver Notes
## Pre-scheduling / Process Kill Hazards
### 1. Dangling DMA Buffer (critical)
When a process is killed, its `kalloc`'d buffers get freed. But the AHCI hardware
is still DMA'ing into that physical memory. The DMA write lands on whatever now
occupies that memory, silently corrupting the kernel heap or another process.
**Fix:** Before freeing a process's memory, either:
- Wait for all its pending AHCI slots to drain (`cmd_issue` bits clear), or
- Issue a port reset to abort in-flight commands, *then* free memory.
### 2. Dangling Completion Callback / Userdata Pointer (critical)
In `storage_read`/`storage_write`, the completion callback writes to `@done` —
a **stack-local variable**. If the scheduler preempts or kills the process while
it's in the `hlt` loop, that stack frame is gone. The ISR fires and writes to a
dangling stack address, corrupting whatever now uses that stack page.
**Fix:** The `done` flag and completion context must be heap-allocated (or in a
kernel-owned structure that outlives the process). A per-request struct like:
```pascal
type TPendingIO = record
done : uint32;
ownerPID : uint32; { skip delivery if process is dead }
end;
```
### 3. Command Slot Leak
If a process is killed with pending I/O, `pending[slot].inUse` stays `true`
forever. Eventually all 32 slots fill up and the device becomes unusable.
**Fix:** When killing a process, scan all AHCI devices for `pending[]` entries
belonging to that process, and either:
- Let the hardware finish but null out the completion callback (ISR just frees slot), or
- Abort the command (port reset) and clear the slot.
### 4. Interrupt-Disabled Preemption
The `asm sti` before the `hlt` loop is fine, but if the timer fires during setup
(between `readCallbackAsync(...)` and `asm sti`), the process could be switched
out with interrupts in an unexpected state. `hlt` is also not preemption-safe.
**Fix:** Move to a proper wait-queue/sleep mechanism instead of `hlt`.
## Recommended Path Forward
Implement a small **I/O request queue** owned by the kernel (not the process stack):
1. Allocate a request struct
2. Put it on a per-device queue
3. ISR completes it
4. Process kill walks the queue and cancels entries for that PID
## Other Bugs Fixed (2026-03-03)
- **vtop mask bug:** `$FFFFFF` (24-bit) → `$3FFFFF` (22-bit) for 4MB page offset
- **find_cmd_slot:** Now checks `pending[].inUse` in addition to `cmd_issue` bit
- **Missing lba5:** Both `send_read_dma_async` and `send_write_dma_async` now write all 48 LBA bits
- **IOAPIC routing:** PCI interrupts routed through IOAPIC so AHCI interrupts actually reach the CPU
- **LAPIC EOI:** `ISR_N` now sends LAPIC EOI for IOAPIC-delivered interrupts
- **PxIS pre-clear removed:** No longer blanket-clears port interrupt status before issuing commands (was eating other slots' completions)
+3 -2
View File
@@ -28,6 +28,7 @@ uses
rtc,
serial,
storagemanagement,
storagemanager,
strings,
terminal,
tracer,
@@ -100,7 +101,7 @@ begin
filesystemRecord^.endOfData := filesystemRecord.sectorsPerTable;
//filesystemRecord.volumeLabel := config^
disk^.writecallback(disk, start, 1, buffer);
storagemanager.storage_write(disk, start, 1, buffer);
memset(uint32(buffer), 0, disk^.sectorSize);
fileEntry := PFileEntry(buffer)[0];
@@ -113,7 +114,7 @@ begin
fileEntry^.attributes := 0;
fileEntry^.location := 1;
disk^.writecallback(disk, start + 1, 1, buffer);
storagemanager.storage_write(disk, start + 1, 1, buffer);
end;
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+4 -3
View File
@@ -16,6 +16,7 @@ uses
filesystemmanager,
lists,
lmemorymanager,
storagemanager,
storagetypes,
strings,
tracer,
@@ -88,7 +89,7 @@ var
begin
buf := puint32(kalloc(count * device^.sectorSize));
memset(uint32(buf), 0, count * device^.sectorSize);
device^.readCallback(device, lba, count, buf);
storagemanager.storage_read(device, lba, count, buf);
readSectors := buf;
end;
@@ -324,7 +325,7 @@ var
begin
identify_volume := false;
if volume^.device = nil then exit;
if volume^.device^.readCallback = nil then exit;
if (volume^.device^.readCallback = nil) and (volume^.device^.readCallbackAsync = nil) then exit;
buf := readSectors(volume^.device, volume^.sectorStart + PVD_SECTOR, 1);
identify_volume := isPVD(buf);
@@ -339,7 +340,7 @@ var
volume : PStorage_Volume;
begin
if disk = nil then exit;
if disk^.readCallback = nil then exit;
if (disk^.readCallback = nil) and (disk^.readCallbackAsync = nil) then exit;
buf := readSectors(disk, PVD_SECTOR, 1);
if not isPVD(buf) then begin
+51 -2
View File
@@ -31,6 +31,13 @@ function controller_type_2_string(controllerType : TControllerType) : pchar;
function read_mbr(device : PStorage_Device) : PMaster_Boot_Record;
procedure write_mbr(device : PStorage_Device; mbr : PMaster_Boot_Record);
{ Block I/O helpers — prefer the async path (interrupt-driven wait) when
available, falling back to the legacy sync callback for non-AHCI devices.
These are meant as a transitional bridge; callers should migrate to the
async hooks with their own completion callbacks over time. }
procedure storage_read(device : PStorage_Device; addr : uint32; sectors : uint32; buffer : puint32);
procedure storage_write(device : PStorage_Device; addr : uint32; sectors : uint32; buffer : puint32);
implementation
procedure init();
@@ -83,14 +90,56 @@ function read_mbr(device : PStorage_Device) : PMaster_Boot_Record;
begin
push_trace('StorageManager.read_mbr');
read_mbr := PMaster_Boot_Record(kalloc(sizeof(TMaster_Boot_Record)));
device^.readCallback(device, 0, 1, puint32(read_mbr));
storage_read(device, 0, 1, puint32(read_mbr));
end;
procedure write_mbr(device : PStorage_Device; mbr : PMaster_Boot_Record);
begin
push_trace('StorageManager.write_mbr');
if not device^.writable then exit;
device^.writeCallback(device, 0, 1, puint32(mbr));
storage_write(device, 0, 1, puint32(mbr));
end;
{ Completion callback for storage_read/storage_write — sets flag so hlt loop exits }
procedure storage_sync_done(success : boolean; userdata : puint32);
begin
userdata^ := 1;
end;
procedure storage_read(device : PStorage_Device; addr : uint32; sectors : uint32; buffer : puint32);
var
done : uint32;
begin
if device^.readCallbackAsync <> nil then begin
done := 0;
device^.readCallbackAsync(device, addr, sectors, buffer, @storage_sync_done, @done);
asm sti end;
while done = 0 do begin
asm hlt end; { sleep until next interrupt }
{ Fallback: if the hardware IRQ was not delivered through the PIC,
poll the driver inline so the completion fires. }
if (done = 0) and (device^.pollCallback <> nil) then
device^.pollCallback();
end;
end else if device^.readCallback <> nil then
device^.readCallback(device, addr, sectors, buffer);
end;
procedure storage_write(device : PStorage_Device; addr : uint32; sectors : uint32; buffer : puint32);
var
done : uint32;
begin
if device^.writeCallbackAsync <> nil then begin
done := 0;
device^.writeCallbackAsync(device, addr, sectors, buffer, @storage_sync_done, @done);
asm sti end;
while done = 0 do begin
asm hlt end; { sleep until next interrupt }
if (done = 0) and (device^.pollCallback <> nil) then
device^.pollCallback();
end;
end else if device^.writeCallback <> nil then
device^.writeCallback(device, addr, sectors, buffer);
end;
end.
+27
View File
@@ -70,6 +70,19 @@ type
PPHIOHook = procedure(drive : PStorage_device; addr : uint32; sectors : uint32; buffer : puint32);
PPHIOHook_ = procedure;
{ Completion callback fired from IRQ context after async block I/O.
Must be short — set a flag or post to a queue.
Never call kalloc/kfree/VFS from within this callback. }
TStorageCompletion = procedure(success : boolean; userdata : puint32);
{ Async block I/O callback — issues the command and returns immediately;
calls completion from IRQ context when done. }
PPHIOHookAsync = procedure(drive : PStorage_Device; addr : uint32; sectors : uint32; buffer : puint32; completion : TStorageCompletion; userdata : puint32);
{ Poll callback — called by sync bridge after each hlt wake-up to process
completions when the hardware interrupt isn't delivered through the PIC. }
PPPollHook = procedure;
{ Filesystem callback types }
PPWriteHook = procedure(volume : PStorage_volume; directory : pchar; entry : PDirectory_Entry; byteCount : uint32; buffer : puint32; statusOut : puint32);
PPReadHook = function(volume : PStorage_Volume; directory : pchar; fileName : pchar; buffer : puint32; bytecount : puint32) : uint32;
@@ -80,6 +93,11 @@ type
PPDeleteFileHook = procedure(volume : PStorage_Volume; filePath : pchar; status : puint32);
PPDeleteDirHook = procedure(volume : PStorage_Volume; path : pchar; status : puint32);
PPIdentifyHook = function(volume : PStorage_Volume) : boolean;
{ Offset-based read: reads byteCount bytes starting at byte offset into the file.
Returns the number of bytes actually read.
Filesystems that do not implement this leave the field nil and VFS falls back
to the load-all readCallback behaviour. }
PPReadOffsetHook = function(volume : PStorage_Volume; directory : pchar; fileName : pchar; offset : uint32; buffer : puint32; byteCount : uint32) : uint32;
byteArray8 = array[0..7] of char;
PByteArray8 = ^byteArray8;
@@ -93,6 +111,13 @@ type
volumes : PDList;
writeCallback : PPHIOHook;
readCallback : PPHIOHook;
{ Async block I/O callbacks — nil if the driver does not support async }
readCallbackAsync : PPHIOHookAsync;
writeCallbackAsync : PPHIOHookAsync;
{ Poll callback — processes pending completions inline when the
hardware interrupt is not delivered. Called from storage_read/write
sync bridge after each hlt wake-up. nil if not needed. }
pollCallback : PPPollHook;
maxSectorCount : uint32;
sectorSize : uint32; //in bytes
start : uint32; //start of device in sectors
@@ -111,6 +136,8 @@ type
deleteFileCallback : PPDeleteFileHook;
deleteDirCallback : PPDeleteDirHook;
identifyCallback : PPIdentifyHook;
{ Offset-based read for streaming mode — nil if not implemented }
readOffsetCallback : PPReadOffsetHook;
end;
{ Generic storage volume }
+21 -2
View File
@@ -32,7 +32,7 @@ uses
tracer;
type
TOpenMode = (omReadOnly, omWriteOnly, omReadWrite);
TOpenMode = (omReadOnly, omWriteOnly, omReadWrite, omStream);
TWriteMode = (wmRewrite, wmAppend, wmNew);
TIsPathValid = (pvInvalid, pvFile, pvDirectory);
TRegError = (pvUnknown, pvNotRegistered, pvRegistered, pvUnregistered);
@@ -486,9 +486,11 @@ type
fileName : pchar; { Full filename including extension, e.g. 'README.TXT' }
openMode : TOpenMode;
writeMode : TWriteMode;
dataBuffer : puint32; { Pointer to loaded data }
dataBuffer : puint32; { Pointer to loaded data (nil for omStream) }
dataSize : uint32; { Size of loaded data in bytes }
loaded : boolean; { True if data has been read from disk }
{ Current byte position for omStream mode; 0 on open, advanced by each ReadFile call }
streamOffset : uint32;
end;
POpenFileEntry = ^TOpenFileEntry;
@@ -656,6 +658,7 @@ begin
OpenFiles[slot].dataBuffer := nil;
OpenFiles[slot].dataSize := 0;
OpenFiles[slot].loaded := false;
OpenFiles[slot].streamOffset := 0;
{ If reading, load the file data now }
if (OpenMode = omReadOnly) or (OpenMode = omReadWrite) then begin
@@ -691,6 +694,7 @@ begin
kfree(puint32(dataSize));
end;
end else begin
{ Write-only or streaming mode — no pre-load }
if Error <> nil then Error^ := eNone;
end;
@@ -767,6 +771,21 @@ begin
entry := @OpenFiles[idx];
if not entry^.inUse then exit;
{ Streaming mode: call readOffsetCallback on demand — no pre-loaded buffer needed }
if entry^.openMode = omStream then begin
if Buffer = nil then exit;
if entry^.volume = nil then exit;
if entry^.volume^.filesystem = nil then exit;
if entry^.volume^.filesystem^.readOffsetCallback = nil then exit;
ReadFile := entry^.volume^.filesystem^.readOffsetCallback(
entry^.volume, entry^.directory, entry^.fileName,
entry^.streamOffset, puint32(Buffer), Length);
entry^.streamOffset := entry^.streamOffset + ReadFile;
tracer.push_trace('vfs.ReadFile.exit');
exit;
end;
if not entry^.loaded then exit;
if entry^.dataBuffer = nil then exit;
if Buffer = nil then exit;
+144
View File
@@ -0,0 +1,144 @@
{
ISR->IOAPIC - I/O APIC and Local APIC minimal support.
On systems where VirtualBox (or real hardware) routes PCI interrupts
through the I/O APIC instead of the legacy 8259 PIC, we must program
the IOAPIC redirection entry for each PCI IRQ we want to receive.
ISA interrupts (timer, keyboard) continue to work through the 8259
PIC via Virtual Wire mode (PIC -> LINT0 -> Local APIC -> CPU).
@author(Aaron Hance <[email protected]>)
}
unit ioapic;
interface
uses
util,
lmemorymanager,
console;
{ Initialise IOAPIC/LAPIC support — maps MMIO regions, ensures LAPIC
is enabled. Safe to call even if the hardware doesn't have an IOAPIC
(will detect and bail out). }
procedure init();
{ Route a PCI interrupt through the IOAPIC.
irq — PCI IRQ number (the value from PCI config interrupt_line)
vector — IDT vector to deliver (typically irq + 32) }
procedure route_pci_irq(irq : uint8; vector : uint8);
{ Send End-of-Interrupt to the Local APIC.
Must be called for every IOAPIC-delivered (Fixed mode) interrupt.
Safe no-op if IOAPIC support was not initialised. }
procedure lapic_eoi();
{ True after init() succeeds — ISR_N checks this to know whether LAPIC
EOI is needed. }
var
ioapic_present : boolean;
implementation
const
IOAPIC_PHYS = $FEC00000;
LAPIC_PHYS = $FEE00000;
{ IOAPIC register offsets (indirect via IOREGSEL / IOWIN) }
IOREGSEL = $00;
IOWIN = $10;
{ IOAPIC registers }
IOAPIC_ID = $00;
IOAPIC_VER = $01;
IOAPIC_REDTBL = $10; { base — each entry is 2 dwords }
{ LAPIC register offsets }
LAPIC_SVR = $F0; { Spurious Interrupt Vector Register }
LAPIC_EOI_REG = $B0; { End-of-Interrupt register }
{ ---- low-level IOAPIC register access ---- }
procedure ioapic_write(reg : uint32; val : uint32);
begin
puint32(IOAPIC_PHYS + IOREGSEL)^ := reg;
puint32(IOAPIC_PHYS + IOWIN)^ := val;
end;
function ioapic_read(reg : uint32) : uint32;
begin
puint32(IOAPIC_PHYS + IOREGSEL)^ := reg;
ioapic_read := puint32(IOAPIC_PHYS + IOWIN)^;
end;
{ ---- public API ---- }
procedure init();
var
ver : uint32;
svr : uint32;
begin
ioapic_present := false;
{ Map the IOAPIC and LAPIC MMIO pages so we can access them }
kpalloc(IOAPIC_PHYS);
kpalloc(LAPIC_PHYS);
{ Probe: read IOAPIC version register — should return non-zero/non-$FF }
ver := ioapic_read(IOAPIC_VER);
if (ver = 0) or (ver = $FFFFFFFF) then begin
console.outputln('IOAPIC', 'Not detected — PCI interrupts rely on 8259 PIC.');
exit;
end;
console.outputln('IOAPIC', 'Detected.');
{ Ensure the Local APIC is software-enabled (bit 8 of SVR).
We set the spurious vector to $FF which is unused. }
svr := puint32(LAPIC_PHYS + LAPIC_SVR)^;
if (svr and $100) = 0 then begin
svr := svr or $100; { Enable APIC }
svr := (svr and $FFFFFF00) or $FF; { Spurious vector = 0xFF }
puint32(LAPIC_PHYS + LAPIC_SVR)^ := svr;
console.outputln('IOAPIC', 'LAPIC software-enabled.');
end;
ioapic_present := true;
end;
procedure route_pci_irq(irq : uint8; vector : uint8);
var
lo : uint32;
begin
if not ioapic_present then exit;
{ Build low dword of redirection entry:
bits 7:0 — delivery vector
bits 10:8 — delivery mode (000 = Fixed)
bit 11 — destination mode (0 = Physical)
bit 13 — pin polarity (1 = active-low for PCI)
bit 15 — trigger mode (1 = level for PCI)
bit 16 — mask (0 = not masked) }
lo := uint32(vector);
lo := lo or (1 shl 13); { active-low }
lo := lo or (1 shl 15); { level-triggered }
{ Write low dword first, then high dword (dest APIC = 0, the BSP) }
ioapic_write(IOAPIC_REDTBL + uint32(irq) * 2, lo);
ioapic_write(IOAPIC_REDTBL + uint32(irq) * 2 + 1, 0);
console.output('IOAPIC', 'Routed IRQ ');
console.writeint(irq);
console.writestring(' -> vector ');
console.writeint(vector);
console.writestringln('');
end;
procedure lapic_eoi();
begin
if ioapic_present then
puint32(LAPIC_PHYS + LAPIC_EOI_REG)^ := 0;
end;
end.
+5 -1
View File
@@ -34,7 +34,7 @@ procedure registerISR(INT_N : uint8; callback : TISRHook);
implementation
uses
console;
console, ioapic;
var
Hooks : TISRHookArray;
@@ -68,6 +68,10 @@ begin
for i:=0 to MAX_HOOKS do begin
if Hooks[INT_N][i] <> nil then Hooks[INT_N][i]();
end;
{ Send EOI to Local APIC (needed for IOAPIC-delivered interrupts).
Safe no-op when IOAPIC is not present. }
ioapic.lapic_eoi();
{ Send EOI to 8259 PIC (slave then master) }
if (INT_N >= 40) then outb($A0, $20);
outb($20, $20);
end;
+2 -2
View File
@@ -230,7 +230,7 @@ begin
exit;
end;
if device^.writeCallback = nil then begin
if (device^.writeCallback = nil) and (device^.writeCallbackAsync = nil) then begin
println('Error: device has no write support.');
exit;
end;
@@ -257,7 +257,7 @@ begin
else
batch := remain;
device^.writeCallback(device, sector, batch, buf);
storagemanager.storage_write(device, sector, batch, buf);
sector := sector + batch;
end;
+3 -3
View File
@@ -186,7 +186,7 @@ begin
exit;
end;
if device^.readCallback = nil then begin
if (device^.readCallback = nil) and (device^.readCallbackAsync = nil) then begin
println('Error: device has no read support.');
exit;
end;
@@ -276,7 +276,7 @@ begin
exit;
end;
if device^.writeCallback = nil then begin
if (device^.writeCallback = nil) and (device^.writeCallbackAsync = nil) then begin
println('Error: device has no write support.');
exit;
end;
@@ -392,7 +392,7 @@ begin
exit;
end;
if device^.writeCallback = nil then begin
if (device^.writeCallback = nil) and (device^.writeCallbackAsync = nil) then begin
println('Error: device has no write support.');
exit;
end;
+1 -1
View File
@@ -190,7 +190,7 @@ begin
push_trace('vmemorymanager.vtop');
idx:= address SHR 22;
paddress:= uint32(KERNEL_PAGE_DIRECTORY^[idx].address) SHL 12;
loadd:= address AND $FFFFFF;
loadd:= address AND $3FFFFF;
vtop:= paddress + loadd;
pop_trace;
end;