SchOS/kernel/dev/bio.c
Nexoniarz fa7669254e usb: plug and play -- hot-plug, unplug, every interface, real mice
Devices can now arrive and leave after boot. A "usbhotplug" kernel thread
handles root-port changes, which it is told about by Port Status Change
events and also checks for every quarter second. It also handles changes
behind hubs, which each hub reports on its status-change endpoint. An
unplugged device has its endpoints closed and any transfer in flight
failed. Then its drivers' disconnect hooks run, the slot is disabled and
its memory is freed. Everything behind a hub that goes goes with it. This
needed the interrupt handler to file command and endpoint-0 completions
for their waiters, so that enumeration can run while keyboards interrupt.

On real hardware: the controller is taken from the firmware through USB
Legacy Support before reset, with SMIs off. On Intel 7/8/9 series parts
the ports are switched over from EHCI, which is where firmware leaves
them.

Every interface of a device is offered to the class drivers, not just
the first, so a receiver's mouse on interface 1 gets a driver. Configure
Endpoint now copies the slot context from the controller. Before, it
rebuilt it and lost the route string and TT fields of devices behind
hubs.

HID: pointers are read in report protocol from their report descriptor.
That covers buttons, X/Y and wheel, relative or absolute, and report IDs.
Boot protocol is kept as the fallback for mice and for keyboards.
Non-pointer HID interfaces are declined rather than treated as tablets.
USB keyboards get software key repeat. Mass storage handles up to four
sticks, each unregistered from the block layer when it is pulled out,
along with its cached blocks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HH7xukt4aaiGj1nQEAoQf1
2026-10-22 11:38:13 +02:00

316 lines
8.8 KiB
C

// Buffer cache.
//
// The buffer cache is a linked list of buf structures holding
// cached copies of disk block contents. Caching disk blocks
// in memory reduces the number of disk reads and also provides
// a synchronization point for disk blocks used by multiple processes.
//
// Interface:
// * To get a buffer for a particular disk block, call bread.
// * After changing buffer data, call bwrite to write it to disk.
// * When done with the buffer, call brelse.
// * Do not use the buffer after calling brelse.
// * Only one process at a time can use a buffer,
// so do not keep them longer than necessary.
//
// The implementation uses two state flags internally:
// * B_VALID: the buffer data has been read from the disk.
// * B_DIRTY: the buffer data has been modified
// and needs to be written to disk.
#include "types.h"
#include "defs.h"
#include "param.h"
#include "spinlock.h"
#include "sleeplock.h"
#include "fs.h"
#include "buf.h"
#include "x86.h"
#include "fsstat.h"
#include "blkdev.h"
struct fsstat fsstat;
struct {
struct spinlock lock;
struct buf buf[NBUF];
// Linked list of all buffers, through prev/next.
// head.next is most recently used.
struct buf head;
} bcache;
void
binit(void)
{
struct buf *b;
initlock(&bcache.lock, "bcache");
//PAGEBREAK!
// Create linked list of buffers
bcache.head.prev = &bcache.head;
bcache.head.next = &bcache.head;
for(b = bcache.buf; b < bcache.buf+NBUF; b++){
b->next = bcache.head.next;
b->prev = &bcache.head;
initsleeplock(&b->lock, "buffer");
bcache.head.next->prev = b;
bcache.head.next = b;
}
}
// Route a request to the driver that owns b->dev. Everything above this line
// is device-agnostic; everything below picks a backend. RAMDISK_DEV is served
// out of memory (kernel/dev/ramdisk.c), USBDISK_DEV by bulk transfers to a
// USB stick, everything else by the IDE driver.
static void bdevrw_inner(struct buf *b);
static void
bdevrw(struct buf *b)
{
uint64 t0 = rdtsc();
if(b->flags & B_DIRTY) fsstat.disk_writes++; else fsstat.disk_reads++;
bdevrw_inner(b);
fsstat.disk_cycles += rdtsc() - t0;
}
static void
bdevrw_inner(struct buf *b)
{
if(b->dev == RAMDISK_DEV){
ramdiskrw(b);
return;
}
if(b->dev == USBDISK_DEV){
usb_storage_rw(b);
return;
}
// SATA, NVMe and every partition: registered at probe time rather than
// known at build time. See include/blkdev.h.
if(b->dev >= BLKDEV_FIRST){
blkdev_rw(b);
return;
}
iderw(b);
}
// Tell the device to commit whatever it has accepted but not yet made durable.
// Same dispatch as bdevrw, and the same rule: everything above this line is
// device-agnostic.
static int
bdevflush(uint dev)
{
if(dev == RAMDISK_DEV)
return 0; // memory has no volatile stage below it
if(dev == USBDISK_DEV)
return usb_storage_flush();
if(dev >= BLKDEV_FIRST)
return blkdev_flush(dev);
return ideflush(dev);
}
// Look through buffer cache for block on device dev.
// If not found, allocate a buffer.
// In either case, return locked buffer.
// Forget every cached block of a device that is no longer there: clean and
// invalid, so it is neither served to a reader nor written back on eviction.
// A buffer somebody is holding is left to them -- they are about to find out.
void
binval(uint dev)
{
struct buf *b;
acquire(&bcache.lock);
for(b = bcache.head.next; b != &bcache.head; b = b->next)
if(b->dev == dev && b->refcnt == 0)
b->flags = 0;
release(&bcache.lock);
}
static struct buf*
bget(uint dev, uint64 blockno)
{
struct buf *b;
retry:
acquire(&bcache.lock);
// Is the block already cached?
for(b = bcache.head.next; b != &bcache.head; b = b->next){
if(b->dev == dev && b->blockno == blockno){
if(b->refcnt++ == 0)
FSSTAT_TAKE(fsstat.buf_pinned, fsstat.buf_pinned_peak);
release(&bcache.lock);
acquiresleep(&b->lock);
return b;
}
}
// Not cached; recycle some unused buffer and clean buffer
// "clean" because B_DIRTY and not locked means log.c
// hasn't yet committed the changes to the buffer.
for(b = bcache.head.prev; b != &bcache.head; b = b->prev){
if(b->refcnt == 0 && (b->flags & B_DIRTY) == 0) {
b->dev = dev;
b->blockno = blockno;
b->flags = 0;
b->refcnt = 1;
FSSTAT_TAKE(fsstat.buf_pinned, fsstat.buf_pinned_peak);
release(&bcache.lock);
acquiresleep(&b->lock);
return b;
}
}
// Every buffer is dirty and none is clean enough to take.
//
// This used to be a straight panic, which was right while every writer went
// through bwrite and a dirty buffer therefore meant "the log has not
// committed yet". SchFS marks tree blocks dirty and leaves them there (see
// bdirty), so an all-dirty cache is now an ordinary state: a transaction that
// touches more than NBUF blocks reaches it long before it commits.
//
// Writing one out early is always safe. The only ordering the format needs is
// that every block of the new tree precede the uberblock that names it, and
// the uberblock goes down after an explicit bflush -- so nothing here can
// land on the wrong side of that barrier.
for(b = bcache.head.prev; b != &bcache.head; b = b->prev){
if(b->refcnt == 0){
fsstat.bget_evict_dirty++;
b->refcnt++; // claim it, so nobody else recycles it either
FSSTAT_TAKE(fsstat.buf_pinned, fsstat.buf_pinned_peak);
release(&bcache.lock);
acquiresleep(&b->lock);
if(b->flags & B_DIRTY)
bdevrw(b);
// Between dropping bcache.lock and taking b's, another process may have
// found this buffer under its *old* identity and be waiting on it. It is
// clean now either way, so hand it back rather than renaming a block out
// from under them, and look for another victim.
acquire(&bcache.lock);
if(b->refcnt != 1){
b->refcnt--;
FSSTAT_DROP(fsstat.buf_pinned);
release(&bcache.lock);
releasesleep(&b->lock);
goto retry;
}
b->dev = dev;
b->blockno = blockno;
b->flags = 0;
release(&bcache.lock);
return b; // b->lock still held, refcnt 1
}
}
panic("bget: no buffers");
}
// Return a locked buf with the contents of the indicated block.
struct buf*
bread(uint dev, uint64 blockno)
{
struct buf *b;
fsstat.bread_calls++;
b = bget(dev, blockno);
if(!(b->flags & B_VALID)) {
fsstat.bread_miss++;
bdevrw(b);
}
return b;
}
// Write b's contents to disk. Must be locked.
void
bwrite(struct buf *b)
{
if(!holdingsleep(&b->lock))
panic("bwrite");
b->flags |= B_DIRTY;
bdevrw(b);
}
// Mark b modified but do not write it. The buffer stays dirty in the cache
// until something evicts it or bflush() comes past.
//
// This is what makes a copy-on-write filesystem affordable. SchFS's B-tree
// seals and writes a node on every single record insert, so a hot leaf -- the
// one collecting a file's data checksums, say -- was written to the disk once
// per 4 KB of file. Measured over 1 MB: 1288 block writes for 256 blocks of
// data. Deferring collapses every one of those repeats into the last one.
//
// It is only safe because the filesystem asks for the ordering it needs
// explicitly, at commit, instead of assuming that write order is medium order.
// A filesystem that did assume that would be broken here -- and was already
// broken on any drive with a write cache, which is all of them.
void
bdirty(struct buf *b)
{
if(!holdingsleep(&b->lock))
panic("bdirty");
b->flags |= B_DIRTY;
b->flags |= B_VALID;
}
// Push every dirty buffer for `dev` out, then make the device itself commit
// what it has taken. Returns 0, or -1 if the device could not confirm it.
//
// Walks the array rather than the MRU list: brelse relinks buffers as it goes,
// and the array is the one order that cannot change underneath the walk.
int
bflush(uint dev)
{
struct buf *b;
for(b = bcache.buf; b < bcache.buf+NBUF; b++){
acquire(&bcache.lock);
if(b->dev != dev || !(b->flags & B_DIRTY)){
release(&bcache.lock);
continue;
}
b->refcnt++;
release(&bcache.lock);
acquiresleep(&b->lock);
if(b->flags & B_DIRTY) // recheck: an eviction may have beaten us to it
bdevrw(b);
releasesleep(&b->lock);
acquire(&bcache.lock);
b->refcnt--;
release(&bcache.lock);
}
return bdevflush(dev);
}
// Release a locked buffer.
// Move to the head of the MRU list.
void
brelse(struct buf *b)
{
if(!holdingsleep(&b->lock))
panic("brelse");
releasesleep(&b->lock);
acquire(&bcache.lock);
b->refcnt--;
if (b->refcnt == 0) {
FSSTAT_DROP(fsstat.buf_pinned);
// no one is waiting for it.
b->next->prev = b->prev;
b->prev->next = b->next;
b->next = bcache.head.next;
b->prev = &bcache.head;
bcache.head.next->prev = b;
bcache.head.next = b;
}
release(&bcache.lock);
}
//PAGEBREAK!