SchOS/kernel/fs/pipe.c
Nexoniarz c98bdf0019 kmem: a slab allocator, and what the fixed tables actually cost
Gate 0's last structural row. kernel/core/kmem.c: slabs are pages from
kalloc() with a header at the front, and free objects hold the free list in
their own first eight bytes.

That last detail is the whole design. **An object carries no header and
freeing performs no search**: the page base of any object *is* its slab, and
the slab names its cache. It is also exactly why an object cannot exceed a
page, which matters below.

Two interfaces. kmem_cache_create/alloc/free for a subsystem with one kind of
thing -- it gets its own line in /proc/slab, which is most of why naming it is
worth the argument -- and kmalloc/kmfree over power-of-two classes from 16 to
2048 for code with no single type to build a cache around. Each cache keeps
partial and full lists; a slab that empties goes straight back to kalloc()
rather than being kept, so /proc/slab never overstates what is held.

`make kmem-test` asserts the bookkeeping on the build machine. Same arrangement
as schfs-objtest, and for the same reason: slab lists, free-list threading, the
page-to-cache lookup and whether pages actually come back are all things that
can be wrong without a kernel to be wrong in. 26 checks, including one that a
permissive allocator cannot pass -- write a distinct pattern through 512
objects and read them all back, because two objects sharing storage shows up
there and nowhere else.

One bug the test found immediately: kmem_max_obj() returned the raw page
remainder, which kmem_cache_create() then rounded *up* past its own limit. The
largest legal size was reported as illegal.

**It does not do what the register said it would.** The claim was that every
fixed table exists to work around kalloc. Measured:

    struct proc    10656    NPROC 128     larger than a page
    struct socket   4928    NSOCK 32      larger than a page
    struct buf      4272    NBUF 256      larger than a page
    struct inode     280    NINODE 200    a cache, bounded on purpose
    struct device    120    NDEVICE 32    3.8 KB, never hit
    struct file       48    NFILE 512     24 KB, never hit

The three largest cannot go in a page-backed slab whatever it does. NINODE is
a cache and is meant to be bounded. NFILE and NDEVICE are small enough that
their tables are rounding errors and their ceilings have never been reached.
Freeing the big ones needs a contiguous multi-page allocator, which is a
different thing and is not written. That is now in the register and in
docs/architecture/kernel.md rather than left as a claim nobody checked.

What it does fix is real, and was measured rather than assumed:

  - **A pipe is 640 bytes and came from a whole 4096-byte page.** Every `|` in
    a shell command burned three and a half kilobytes it never touched. Six
    pipes per page now, and /proc/slab shows the page going back when they
    close.
  - **procread() built its answer in a 2048-byte array on the kernel stack**,
    under a comment that had warned about exactly that since it was written --
    it stayed a local because a whole page for 2 KB felt worse, and kmalloc is
    the answer that was missing. Its frame is 64 bytes now instead of ~2100.
  - procfs's other generator took a page from kalloc for the same 2 KB.

Verified on a boot: /proc/slab tracks pipe and kmalloc-2048 through real
pipelines, allocs and frees balance, and $(), ps, time, readlink, sigtest and
rename are all still green with no traps.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 09:32:06 +02:00

176 lines
4.1 KiB
C

#include "types.h"
#include "defs.h"
#include "param.h"
#include "mmu.h"
#include "proc.h"
#include "fs.h"
#include "spinlock.h"
#include "sleeplock.h"
#include "file.h"
#include "kmem.h"
#include "posix.h"
#include "errno.h"
#define PIPESIZE 512
struct pipe {
struct spinlock lock;
char data[PIPESIZE];
uint nread; // number of bytes read
uint nwrite; // number of bytes written
int readopen; // read fd is still open
int writeopen; // write fd is still open
};
// A pipe is about 560 bytes and used to come from kalloc(), which is a whole
// 4096-byte page -- so every `|` in a shell command burned three and a half
// kilobytes it never touched. This is what kmem_cache exists for, and the
// cache is created on first use rather than at boot so a kernel that never
// opens a pipe holds no page for one.
static struct kmem_cache *pipe_cache;
int
pipealloc(struct file **f0, struct file **f1)
{
struct pipe *p;
p = 0;
*f0 = *f1 = 0;
if((*f0 = filealloc()) == 0 || (*f1 = filealloc()) == 0)
goto bad;
if(!pipe_cache && !(pipe_cache = kmem_cache_create("pipe", sizeof(*p))))
goto bad;
if((p = kmem_cache_alloc(pipe_cache)) == 0)
goto bad;
p->readopen = 1;
p->writeopen = 1;
p->nwrite = 0;
p->nread = 0;
initlock(&p->lock, "pipe");
(*f0)->type = FD_PIPE;
(*f0)->readable = 1;
(*f0)->writable = 0;
(*f0)->pipe = p;
(*f1)->type = FD_PIPE;
(*f1)->readable = 0;
(*f1)->writable = 1;
(*f1)->pipe = p;
return 0;
//PAGEBREAK: 20
bad:
if(p)
kmem_cache_free(pipe_cache, p);
if(*f0)
fileclose(*f0);
if(*f1)
fileclose(*f1);
return -1;
}
void
pipeclose(struct pipe *p, int writable)
{
acquire(&p->lock);
if(writable){
p->writeopen = 0;
wakeup(&p->nread);
} else {
p->readopen = 0;
wakeup(&p->nwrite);
}
if(p->readopen == 0 && p->writeopen == 0){
release(&p->lock);
kmem_cache_free(pipe_cache, p);
} else
release(&p->lock);
}
//PAGEBREAK: 40
int
pipewrite(struct pipe *p, char *addr, int n)
{
int i;
acquire(&p->lock);
// Writing to a pipe nobody is reading is SIGPIPE and EPIPE, in that order.
//
// This returned a bare -1 with no signal and no errno, so `yes | head -2`
// came back with whatever errno happened to be lying around -- "Invalid
// argument" -- instead of quietly ending the way every shell pipeline is
// built to. Checked up front as well as in the blocking loop below: the
// common case is that head has already exited by the time the next write
// arrives, and the pipe is nowhere near full.
if(p->readopen == 0){
release(&p->lock);
sigsend(myproc()->pid, SIGPIPE);
return -EPIPE;
}
for(i = 0; i < n; i++){
while(p->nwrite == p->nread + PIPESIZE){ //DOC: pipewrite-full
if(p->readopen == 0){
release(&p->lock);
sigsend(myproc()->pid, SIGPIPE);
return i > 0 ? i : -EPIPE; // a short write is still a write
}
if(myproc()->killed){
release(&p->lock);
return -1;
}
wakeup(&p->nread);
sleep(&p->nwrite, &p->lock); //DOC: pipewrite-sleep
}
p->data[p->nwrite++ % PIPESIZE] = addr[i];
}
wakeup(&p->nread); //DOC: pipewrite-wakeup1
release(&p->lock);
return n;
}
int
piperead(struct pipe *p, char *addr, int n)
{
int i;
acquire(&p->lock);
while(p->nread == p->nwrite && p->writeopen){ //DOC: pipe-empty
if(myproc()->killed){
release(&p->lock);
return -1;
}
sleep(&p->nread, &p->lock); //DOC: piperead-sleep
}
for(i = 0; i < n; i++){ //DOC: piperead-copy
if(p->nread == p->nwrite)
break;
addr[i] = p->data[p->nread++ % PIPESIZE];
}
wakeup(&p->nwrite); //DOC: piperead-wakeup
release(&p->lock);
return i;
}
// Readiness for poll(2)/select(2). Reports state, doesn't block.
int
pipe_poll(struct pipe *p, int readable, int writable)
{
int rev = 0;
acquire(&p->lock);
if(readable){
if(p->nread != p->nwrite)
rev |= POLLIN;
if(p->writeopen == 0)
rev |= POLLHUP;
}
if(writable){
if(p->readopen == 0)
rev |= POLLERR;
else if(p->nwrite < p->nread + PIPESIZE)
rev |= POLLOUT;
}
release(&p->lock);
return rev;
}