Everything created after boot was stamped 1970. know() in SchFS and now_sec() in the v1 filesystem both returned uptime(), under comments saying the kernel had no wall clock -- which stopped being true when time_init() started reading the RTC. The comments outlived the condition they described. Two errors, and the second is the less obvious: uptime() counts 10 ms *ticks* and they were being used as *seconds*, so a node created 0.6 s into boot read back as 00:01:00 and the clocks ran a hundred times too fast. Both now go through wallclock(), which exists precisely so nothing can disagree with gettimeofday about the time. Confirmed against the RTC line from the same boot. The occupancy counters are the instrumentation docs/plan/roadmap.md asked for while hunting the ls -lR hang: inode, file and pinned-buffer counts with a high-water mark each, maintained at the 0->1 and 1->0 transitions rather than by scanning, so they cost one increment on a path that already holds the table's lock. The peaks are the diagnostic. A live count only describes the moment you looked, and a reference leak is invisible in it right up until the table is full -- at which point iget() panics and filealloc() returns 0, which is where the trail used to go cold. What they measured: across ten iterations of ls -lR with writes interleaved, 33 execs and 13378 tree descents, inode_peak stayed at 9/200, file_peak at 9/512 and buf_pinned_peak at 1/256, with the SchFS bracket unlocked throughout. That eliminates the roadmap's own hypothesis -- "a lock or reference-count leak somewhere in the *at() paths" -- and the hang did not reproduce. It is very likely fixed in passing by the *at() dirfd rewrite, the getdents cookie rewrite or SchFS becoming the root, all of which landed after it was reported. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2
292 lines
8.1 KiB
C
292 lines
8.1 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"
|
|
|
|
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;
|
|
}
|
|
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();
|
|
return ideflush(dev);
|
|
}
|
|
|
|
// Look through buffer cache for block on device dev.
|
|
// If not found, allocate a buffer.
|
|
// In either case, return locked buffer.
|
|
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!
|