SchOS/kernel/dev/ide.c
Nexoniarz 03cc316001 schfs: the commit barrier, reflink, and attributes with an index over them
The last three phases of the filesystem, which were written together and
touch the same files throughout -- kmount.c, objw.c and vfs.c are each
rewritten by all three -- so they land as one commit rather than three
that could not be built separately.

Durability. A transaction that reaches the platter out of order is worse
than one that never reached it: the uberblock can name a tree whose
blocks are still in the drive's write cache. bdirty/bflush give the
buffer layer an explicit barrier, ideflush issues ATA FLUSH CACHE and
usb_storage_flush the SCSI SYNCHRONIZE CACHE that means the same thing,
and a commit now waits for the data before it writes the root that
points at it. The uberblock groups are read newest-first with fallback,
so a volume whose newest roots are torn mounts at an older one and says
which generations were lost.

Reflink. Extents carry a refcount, so two objects can name the same
blocks and neither frees them while the other holds them. Exposed as
FICLONE -- Linux's ioctl value, so `cp --reflink` works unmodified --
and as cmd/reflink for the case where there is no cp to hand.

Attributes, and the one genuinely new idea here. attr.c stores named
values on an object, reachable through setxattr/getxattr/removexattr at
Linux's syscall numbers. On top of that, schfs_index declares an index
over an attribute name and schfs_query answers "which objects carry this
value" from the B-tree rather than by walking. A query against an
unindexed attribute is refused with an error, never silently answered by
a full scan: a query that quietly becomes a scan is worse than one that
admits it cannot help. cmd/schattr drives both.

Three bugs fixed on the way, each found by the numbers being wrong
rather than by inspection:

  - statfs's f_fsid was declared int64[2]. Linux's fsid_t is two *ints*,
    so every field after it landed one slot late: userland read f_frsize
    out of f_namelen, got 255, and df reported a 32 MB root filesystem
    as 1992 KB. It hid for as long as it did because the fields used to
    be invented constants, so wrong numbers looked like the usual ones.
  - struct inode's nlink was a short against SchFS's uint32, so a file
    past 32767 names came back negative -- and iput frees an object
    whose count reaches zero. It is an int now, signed on purpose, so an
    underflow lands somewhere a check can see rather than wrapping.
  - filewrite split every write to fit v1's log. That limit belongs to
    the journal, not to writing, and SchFS makes one transaction of
    whatever it is handed: a 1 MB write was becoming 170 commits and
    170 flushes for nothing. fs_ops carries max_write now, and SchFS
    sets no limit.

Also here: a real mount table, so `mount -t schfs /dev/hdb /mnt` shows
up in /proc/mounts -- df looks a path up there before it calls statfs at
all, so a mount missing from that list was invisible to it -- several
volumes mounted at once, snapshots mounted by name, and /proc/fsstat
with an rdtsc-based counter, which is the only instrument fine enough to
see inside a single syscall.

Verified: `make schfs-check` passes in full, including the sweep that
crashes the volume at each of 119 points mid-transaction and requires
every one to mount with exactly its committed data, and the case where
both newest uberblocks are rubbish and the volume still comes back. The
headless boot check passes with 27 [ok] and no failures, on a SchFS root.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 16:02:02 +02:00

355 lines
11 KiB
C

// Simple PIO-based (non-DMA) IDE driver code.
#include "types.h"
#include "defs.h"
#include "irq.h"
#include "param.h"
#include "memlayout.h"
#include "mmu.h"
#include "proc.h"
#include "x86.h"
#include "traps.h"
#include "spinlock.h"
#include "sleeplock.h"
#include "fs.h"
#include "buf.h"
#define SECTOR_SIZE 512
#define IDE_BSY 0x80
#define IDE_DRDY 0x40
#define IDE_DF 0x20
#define IDE_ERR 0x01
#define IDE_CMD_READ 0x20
#define IDE_CMD_WRITE 0x30
#define IDE_CMD_RDMUL 0xc4
#define IDE_CMD_SETMULT 0xc6
#define IDE_CMD_WRMUL 0xc5
// The 48-bit forms. Same operations, an address that is not capped at 128 GB.
#define IDE_CMD_READ_EXT 0x24
#define IDE_CMD_WRITE_EXT 0x34
#define IDE_CMD_RDMUL_EXT 0x29
#define IDE_CMD_WRMUL_EXT 0x39
// FLUSH CACHE, and the form a drive using 48-bit addressing wants. Neither
// takes an address; the difference is only which one a given drive admits to
// supporting, so ideflush tries the plain one and falls back.
#define IDE_CMD_FLUSH 0xe7
#define IDE_CMD_FLUSH_EXT 0xea
// idequeue points to the buf now being read/written to the disk.
// idequeue->qnext points to the next buf to be processed.
// You must hold idelock while manipulating queue.
static struct spinlock idelock;
static struct buf *idequeue;
static int havedisk0;
static int havedisk1;
static void idestart(struct buf*);
// Wait for the drive to become ready, or give up.
//
// This used to be an unbounded `while`. On a machine with nothing attached to
// the primary channel the status register reads 0xFF forever, so the very
// first call from ideinit() hung the kernel -- no panic, no message, just a
// dead machine right after the VGA banner. That is not an exotic
// configuration: it is every CD boot, and most hardware newer than IDE.
//
// The bound is deliberately enormous. A real drive answers in microseconds;
// this only expires when there is nothing there at all.
static int
idewait(int checkerr)
{
int r = 0;
for(int i = 0; i < 20000000; i++){
r = inb(0x1f7);
if((r & (IDE_BSY|IDE_DRDY)) == IDE_DRDY){
if(checkerr && (r & (IDE_DF|IDE_ERR)) != 0)
return -1;
return 0;
}
}
return -1;
}
static void
ide_irq(void *arg)
{
(void)arg;
ideintr();
}
// SET MULTIPLE MODE: how many sectors the drive moves per interrupt during
// READ/WRITE MULTIPLE. Must equal BSIZE/SECTOR_SIZE or idestart()'s single
// insl/outsl of a whole block does not line up with the drive's bursts.
//
// A drive may refuse a count it cannot do (IDENTIFY word 47 carries its
// maximum; QEMU allows 16). If it does, say so rather than continue: the
// transfers would silently desynchronise, which reads as filesystem
// corruption rather than as a driver problem.
static void
ide_set_multiple(int dev)
{
int spb = BSIZE / SECTOR_SIZE;
outb(0x1f6, 0xe0 | ((dev & 1) << 4));
if(idewait(0) < 0)
return;
outb(0x1f2, spb);
outb(0x1f7, IDE_CMD_SETMULT);
if(idewait(1) < 0)
cprintf("ide%d: SET MULTIPLE MODE %d rejected; expect trouble\n", dev, spb);
}
void
ideinit(void)
{
initlock(&idelock, "ide");
// ISA signalling: edge-triggered, active high. Everything at a fixed
// legacy port is wired this way; PCI devices are not.
irq_register(IRQ_IDE, ide_irq, 0, "ide", 0);
// A timeout here means there is no drive on this channel. Say so and leave
// havedisk0/1 clear: with a ramdisk root there is nothing else the system
// needs from IDE, and booting on is strictly better than stopping.
if(idewait(0) < 0){
cprintf("ide: no drive on the primary channel\n");
return;
}
havedisk0 = 1;
// Check if disk 1 is present
outb(0x1f6, 0xe0 | (1<<4));
for(int i=0; i<1000; i++){
if(inb(0x1f7) != 0){
havedisk1 = 1;
break;
}
}
// Tell each drive how many sectors to move per interrupt.
//
// READ/WRITE MULTIPLE transfer the drive's *multiple count* of sectors
// between interrupts, and that count is whatever SET MULTIPLE MODE last
// established -- it is not taken from the sector-count register. Nothing
// ever issued it here, so the count was undefined, which is the real reason
// idestart() carried a `sector_per_block > 7` panic: BSIZE was 1024, two
// sectors, and nobody had tested further.
//
// Establishing it explicitly is what lets BSIZE be a whole SchFS block. At
// 1024 every 4096-byte SchFS block cost four IDE requests, each with its own
// interrupt and sleep/wakeup, and exec() of the 800 KB toybox binary meant
// ~800 round trips -- about two seconds, on every command.
ide_set_multiple(0);
if(havedisk1)
ide_set_multiple(1);
// Switch back to disk 0.
outb(0x1f6, 0xe0 | (0<<4));
}
// Is there actually a drive there? Callers that probe -- rather than callers
// that know what they are looking for -- have to ask, because reading an
// absent drive is a bug and iderw() treats it as one.
int
ide_disk_present(int dev)
{
return dev == 0 ? havedisk0 : havedisk1;
}
// Start the request for b. Caller must hold idelock.
static void
idestart(struct buf *b)
{
if(b == 0)
panic("idestart");
// No FSSIZE check. It used to panic on any block past 150000 -- 614 MB at
// BSIZE -- which is a constant describing the *v1 filesystem image*, not the
// disk. It had no business bounding a device, and it was the lowest ceiling
// in the stack by three orders of magnitude: the format addresses 64 ZB and
// the drive was refused past half a gigabyte.
int sector_per_block = BSIZE/SECTOR_SIZE;
uint64 sector = b->blockno * (uint64)sector_per_block;
// No `sector_per_block > 7` panic here any more. That limit existed because
// nothing had set the drive's multiple count, so anything past the tested
// two sectors was a guess; ideinit() issues SET MULTIPLE MODE now, and the
// count is whatever BSIZE says.
// LBA28 addresses 2^28 sectors, which is 128 GB and no more. Past that the
// 48-bit commands are the only way to name the sector at all, so the mode is
// chosen per request: LBA28 stays the path for everything that fits, because
// it is the one every existing image has been read through.
int lba48 = sector >= (1ULL << 28);
int read_cmd, write_cmd;
if(lba48){
read_cmd = (sector_per_block == 1) ? IDE_CMD_READ_EXT : IDE_CMD_RDMUL_EXT;
write_cmd = (sector_per_block == 1) ? IDE_CMD_WRITE_EXT : IDE_CMD_WRMUL_EXT;
} else {
read_cmd = (sector_per_block == 1) ? IDE_CMD_READ : IDE_CMD_RDMUL;
write_cmd = (sector_per_block == 1) ? IDE_CMD_WRITE : IDE_CMD_WRMUL;
}
idewait(0);
outb(0x3f6, 0); // generate interrupt
if(lba48){
// The high half goes first, then the low half into the same registers:
// each is a two-deep FIFO, and writing them in the other order leaves the
// drive addressing whichever sector the low bytes alone describe.
outb(0x1f2, 0); // sector count, high byte
outb(0x1f3, (sector >> 24) & 0xff);
outb(0x1f4, (sector >> 32) & 0xff);
outb(0x1f5, (sector >> 40) & 0xff);
outb(0x1f2, sector_per_block); // sector count, low byte
outb(0x1f3, sector & 0xff);
outb(0x1f4, (sector >> 8) & 0xff);
outb(0x1f5, (sector >> 16) & 0xff);
// LBA48 carries no address bits in the drive/head register.
outb(0x1f6, 0x40 | ((b->dev&1)<<4));
} else {
outb(0x1f2, sector_per_block);
outb(0x1f3, sector & 0xff);
outb(0x1f4, (sector >> 8) & 0xff);
outb(0x1f5, (sector >> 16) & 0xff);
outb(0x1f6, 0xe0 | ((b->dev&1)<<4) | ((sector>>24)&0x0f));
}
if(b->flags & B_DIRTY){
outb(0x1f7, write_cmd);
outsl(0x1f0, b->data, BSIZE/4);
} else {
outb(0x1f7, read_cmd);
}
}
// Interrupt handler.
void
ideintr(void)
{
struct buf *b;
// First queued buffer is the active request.
acquire(&idelock);
if((b = idequeue) == 0){
release(&idelock);
// cprintf("spurious IDE interrupt\n");
return;
}
idequeue = b->qnext;
// Read data if needed.
if(!(b->flags & B_DIRTY) && idewait(1) >= 0)
insl(0x1f0, b->data, BSIZE/4);
// Wake process waiting for this buf.
b->flags |= B_VALID;
b->flags &= ~B_DIRTY;
wakeup(b);
// Start disk on next buf in queue.
if(idequeue != 0)
idestart(idequeue);
else
wakeup(&idequeue); // ideflush waits for the channel to go quiet
release(&idelock);
}
// FLUSH CACHE: do not return until the drive has committed what it has taken
// to the medium.
//
// Every layer above this driver assumes a write that has returned is on the
// disk. On a drive with a volatile write cache that is simply not true, and it
// is the assumption SchFS's whole crash-safety argument rests on: copy-on-write
// promises that a crash lands on either the old tree or the new one, but only
// if the tree blocks reach the platter before the uberblock that names them.
// The drive is otherwise free to write them in whatever order suits its head,
// and a power cut then leaves a newest uberblock -- correctly checksummed --
// pointing at a tree that was never written. Nothing can recover from that,
// because nothing about it looks wrong.
//
// Polled rather than queued: it moves no data, so it does not fit idestart's
// shape, and it is rare enough that the simplest correct thing wins.
int
ideflush(int dev)
{
int r = 0;
if(dev == 0 ? !havedisk0 : !havedisk1)
return 0; // nothing there is nothing to flush
acquire(&idelock);
// The channel has to be idle. This talks straight to the registers, and
// issuing it underneath a queued transfer would corrupt that transfer's
// handshake rather than merely delay it.
while(idequeue != 0)
sleep(&idequeue, &idelock);
outb(0x3f6, 2); // nIEN: this command answers by status, not IRQ
outb(0x1f6, 0xe0 | ((dev & 1) << 4));
if(idewait(0) < 0){
r = -1;
goto out;
}
outb(0x1f7, IDE_CMD_FLUSH);
// A drive with a full write cache can take seconds over this, which is the
// other reason idewait's bound is as large as it is.
if(idewait(1) < 0){
// Drives that came up in 48-bit mode may abort the 28-bit spelling. Same
// operation, and there is no third option, so a failure here is real.
outb(0x1f7, IDE_CMD_FLUSH_EXT);
if(idewait(1) < 0){
cprintf("ide%d: FLUSH CACHE failed; writes are not durable\n", dev);
r = -1;
}
}
out:
outb(0x3f6, 0); // interrupts back on for ordinary transfers
release(&idelock);
return r;
}
//PAGEBREAK!
// Sync buf with disk.
// If B_DIRTY is set, write buf to disk, clear B_DIRTY, set B_VALID.
// Else if B_VALID is not set, read buf from disk, set B_VALID.
void
iderw(struct buf *b)
{
struct buf **pp;
if(!holdingsleep(&b->lock))
panic("iderw: buf not locked");
if((b->flags & (B_VALID|B_DIRTY)) == B_VALID)
panic("iderw: nothing to do");
if(b->dev != 0 && !havedisk1)
panic("iderw: ide disk 1 not present");
if(b->dev == 0 && !havedisk0)
panic("iderw: ide disk 0 not present");
acquire(&idelock); //DOC:acquire-lock
// Append b to idequeue.
b->qnext = 0;
for(pp=&idequeue; *pp; pp=&(*pp)->qnext) //DOC:insert-queue
;
*pp = b;
// Start disk if necessary.
if(idequeue == b)
idestart(b);
// Wait for request to finish.
while((b->flags & (B_VALID|B_DIRTY)) != B_VALID){
sleep(b, &idelock);
}
release(&idelock);
}