Same disease as the network stack, one subsystem over. kernel/dev/vga.c
was three things wearing one name: the owner of the framebuffer, a
bitmap drawing library, and the text console. Only the first is a
driver, and because they shared a file they shared a prefix -- so
panic.c drew the crash screen with vga_fill_rect, console.c printed with
vga_putc, /dev/fb0 handed out vga_fb_phys, and every one of those files
named a display driver it had no business knowing about.
Split three ways:
- kernel/drivers/gpu/vga/vga_fb.c owns the VBE framebuffer -- the mode
the loader programmed and handed over, the aperture, and the display
class vtable. It is a driver and now lives with the driver that
finds the card on the bus.
- kernel/dev/gfx.c draws: pixels, shapes, glyphs, the cursor, scroll,
the ANSI erase operations. It names no driver. The framebuffer
arrives through gfx_attach_scanout(), so a driver publishes what it
owns and this file draws on it without knowing what it is.
- include/gfx.h is the interface, and says why it is deliberately not
the display class: that one is about frames -- acquire, present,
vblank -- and the crash screen has to work when nothing else does.
main() no longer calls a display driver. platform_register_devices()
takes the framebuffer address and brings up whatever owns it, which is
where driver names belong; kernel/drivers/registry.c is the file whose
entire job is naming every driver in the kernel.
virtio-gpu got the e1000 treatment as well: instances in an array, the
instance hung on dev->priv by an `attached` hook, and every op reading
it rather than a file-static. It refused the second adapter outright
before, which is not hypothetical -- integrated plus discrete is an
ordinary laptop. The VGA driver deliberately has no such hook, and says
so: there is one framebuffer the loader programmed however many
VGA-class controllers the bus turns up, and a second one publishing the
same scanout is the truth rather than a bug.
Private headers moved next to their drivers: include/virtio.h and
include/xhci.h each had exactly one includer. include/ is for headers
the rest of the kernel needs.
Deleted rather than moved: consoleinit() programmed the sixteen ANSI
colours into the VGA DAC and disabled text-mode blink, by writing ports
0x3C0-0x3C9 from the console. The comment three lines above the colour
table already said this does nothing in a 32bpp linear mode -- there is
no palette to index -- so it was both dead and in the wrong file.
Verified, and this is a refactor so the evidence is that nothing moved:
- bootcheck -Fb -Net: 59 [ok], no failures. That covers the console
through every command, /dev/fb0's geometry, writing pixels through
write() and mapping the framebuffer through mmap.
- make crashshot: the crash screen renders exactly as before --
shapes, glyphs, logo and QR code -- which is the whole of gfx.c
exercised at once, in a panic, with the scanout attached by a driver.
- make schcom-test: five pixel assertions from the host oracle still
pass, so the display class and the compositor above it are unmoved.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
480 lines
15 KiB
C
480 lines
15 KiB
C
#include "types.h"
|
|
#include "defs.h"
|
|
#include "gfx.h"
|
|
#include "param.h"
|
|
#include "stat.h"
|
|
#include "memlayout.h"
|
|
#include "mmu.h"
|
|
#include "spinlock.h"
|
|
#include "sleeplock.h"
|
|
#include "proc.h"
|
|
#include "fs.h"
|
|
#include "file.h"
|
|
#include "syscall.h"
|
|
#include "errno.h"
|
|
|
|
extern int mappages(pml4e_t *pgdir, void *va, uint64 size, uint64 pa, int perm);
|
|
|
|
#define MAP_SHARED 0x01
|
|
#define MAP_PRIVATE 0x02
|
|
#define MAP_FIXED 0x10
|
|
#define MAP_ANONYMOUS 0x20
|
|
#define PROT_NONE 0
|
|
#define PROT_WRITE 0x2
|
|
|
|
// Reject user mapping requests that fall outside user space. Without this,
|
|
// MAP_FIXED at an arbitrary address could create user-accessible PTEs over
|
|
// (or vm_unmap_range/kfree pages belonging to) the kernel half of the
|
|
// address space.
|
|
static int
|
|
bad_user_range(uint64 va, uint64 length)
|
|
{
|
|
return length == 0 || va + length < va || va + length > KERNBASE;
|
|
}
|
|
|
|
addr_t
|
|
sys_sbrk(void)
|
|
{
|
|
int n;
|
|
if (argint(0, &n) < 0)
|
|
return -1;
|
|
addr_t addr = myproc()->sz;
|
|
if (growproc(n) < 0)
|
|
return -1;
|
|
return addr;
|
|
}
|
|
|
|
addr_t
|
|
sys_brk(void)
|
|
{
|
|
addr_t addr;
|
|
if (argaddr(0, &addr) < 0 || addr == 0)
|
|
return myproc()->sz;
|
|
if (addr > myproc()->sz && growproc(addr - myproc()->sz) < 0)
|
|
return myproc()->sz;
|
|
return myproc()->sz;
|
|
}
|
|
|
|
addr_t
|
|
sys_mmap(void)
|
|
{
|
|
uint64 addr, length, offset;
|
|
int prot, flags, fd;
|
|
struct file *f = 0;
|
|
|
|
if (argaddr(0, &addr) < 0 || argaddr(1, &length) < 0 ||
|
|
argint(2, &prot) < 0 || argint(3, &flags) < 0 ||
|
|
argint(4, &fd) < 0 || argaddr(5, &offset) < 0)
|
|
return -1;
|
|
|
|
if (fd >= 0 && fd < NOFILE)
|
|
f = myproc()->ofile[fd];
|
|
|
|
length = PGROUNDUP(length);
|
|
uint64 va = addr;
|
|
if (length == 0 || length > KERNBASE)
|
|
return -EINVAL;
|
|
|
|
if (flags & MAP_FIXED) {
|
|
if (bad_user_range(va, length) || (va % PGSIZE))
|
|
return -EINVAL;
|
|
vm_unmap_range(myproc()->pgdir, va, length);
|
|
if (prot == PROT_NONE)
|
|
return va;
|
|
for (uint64 a = va; a < va + length; a += PGSIZE) {
|
|
char *mem = kalloc();
|
|
if (!mem) return -1;
|
|
memset(mem, 0, PGSIZE);
|
|
if (mappages(myproc()->pgdir, (void*)a, PGSIZE, V2P((uint64)mem), PTE_W | PTE_U) < 0)
|
|
return -1;
|
|
}
|
|
return va;
|
|
}
|
|
|
|
struct vma *v = 0;
|
|
for (int i = 0; i < NVMA; i++) {
|
|
if (!myproc()->vma[i].used) { v = &myproc()->vma[i]; break; }
|
|
}
|
|
if (!v) return -1;
|
|
|
|
// Ignore non-zero hint addresses (not MAP_FIXED); always pick a fresh VA.
|
|
// Bump-allocated: each mapping gets its own disjoint, exactly-length-sized
|
|
// range, so a single mmap() can never run into a neighboring live mapping
|
|
// regardless of size (the old scheme assigned a fixed 8MB window per vma[]
|
|
// slot index, which a single allocation bigger than 8MB — e.g. a large
|
|
// realloc() growth — could map straight past, panicking mappages("remap")
|
|
// when it hit an already-present PTE in the next slot's territory).
|
|
// munmap()'d ranges are not reclaimed for reuse; that's fine at this scale.
|
|
if (myproc()->mmap_next == 0)
|
|
myproc()->mmap_next = 0x40000000;
|
|
va = myproc()->mmap_next;
|
|
if (bad_user_range(va, length))
|
|
return -ENOMEM;
|
|
myproc()->mmap_next += length;
|
|
|
|
int is_fb = 0;
|
|
int shmid = -1;
|
|
// File-backed mapping of a regular file.
|
|
//
|
|
// This used to be ignored entirely: any file mapping silently returned
|
|
// zeroed anonymous pages, so a program that mmap'd a font or a config
|
|
// file got a buffer full of nothing and no error to explain it. Reading
|
|
// the contents in is not demand paging and never will be without a page
|
|
// cache -- but it is the difference between a wrong answer and a right
|
|
// one.
|
|
//
|
|
// MAP_SHARED that is also writable is refused rather than faked. Its
|
|
// whole contract is that stores land in the file and other mappers see
|
|
// them, and there is nothing here that could make that true; quietly
|
|
// giving back a private copy would be the same lie in a new place.
|
|
if (f && f->type == FD_INODE && f->ip->type != T_DEV) {
|
|
if ((flags & MAP_SHARED) && (prot & PROT_WRITE))
|
|
return -ENODEV;
|
|
if (!f->readable)
|
|
return -EACCES;
|
|
|
|
for (uint64 a = va; a < va + length; a += PGSIZE) {
|
|
char *mem = kalloc();
|
|
if (!mem) return -ENOMEM;
|
|
memset(mem, 0, PGSIZE);
|
|
if (mappages(myproc()->pgdir, (void*)a, PGSIZE, V2P((uint64)mem), PTE_W | PTE_U) < 0) {
|
|
kfree(mem);
|
|
return -ENOMEM;
|
|
}
|
|
}
|
|
|
|
// Read through the file's own offset-independent path, so this does
|
|
// not disturb a descriptor the caller is also reading normally.
|
|
ilock(f->ip);
|
|
readi(f->ip, 0, va, offset, length);
|
|
iunlock(f->ip);
|
|
|
|
v->addr = va; v->length = length; v->used = 1;
|
|
v->no_free = 0; v->shmid = -1;
|
|
return va;
|
|
}
|
|
|
|
if (f && f->type == FD_INODE && f->ip->type == T_DEV && f->ip->major == 2) {
|
|
// Map the framebuffer where it actually is. This used to map a
|
|
// hardcoded 0xfd000000 for a hardcoded length, so a request for more
|
|
// than the screen holds handed out user-writable PTEs to whatever
|
|
// MMIO sits above the framebuffer, and any machine that put the LFB
|
|
// somewhere else got a window onto the wrong device entirely.
|
|
uint64 fb = gfx_fb_phys();
|
|
uint64 fbsz = PGROUNDUP(gfx_fb_size());
|
|
if (fb == 0 || fbsz == 0)
|
|
return -ENODEV;
|
|
if (length > fbsz)
|
|
length = fbsz;
|
|
if (mappages(myproc()->pgdir, (void*)va, length, fb, PTE_W | PTE_U) < 0)
|
|
return -1;
|
|
is_fb = 1;
|
|
// From here the drawing happens with no further syscalls, so this is
|
|
// the kernel's only chance to notice that the screen no longer belongs
|
|
// to the text console. fbclose() puts it back.
|
|
fb_mapped();
|
|
} else {
|
|
for (uint64 a = va; a < va + length; a += PGSIZE) {
|
|
char *mem = kalloc();
|
|
if (!mem) return -1;
|
|
memset(mem, 0, PGSIZE);
|
|
if (mappages(myproc()->pgdir, (void*)a, PGSIZE, V2P((uint64)mem), PTE_W | PTE_U) < 0)
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
v->addr = va; v->length = length; v->used = 1;
|
|
v->no_free = is_fb; v->shmid = shmid;
|
|
return va;
|
|
}
|
|
|
|
addr_t
|
|
sys_munmap(void)
|
|
{
|
|
uint64 addr, length;
|
|
if (argaddr(0, &addr) < 0 || argaddr(1, &length) < 0)
|
|
return -1;
|
|
length = PGROUNDUP(length);
|
|
if (bad_user_range(addr, length))
|
|
return -EINVAL;
|
|
for (int i = 0; i < NVMA; i++) {
|
|
if (myproc()->vma[i].used && myproc()->vma[i].addr == addr) {
|
|
if (myproc()->vma[i].shmid >= 0)
|
|
shm_detach(myproc()->vma[i].shmid);
|
|
if (myproc()->vma[i].no_free)
|
|
vm_unmap_norelease(myproc()->pgdir, addr, myproc()->vma[i].length);
|
|
else
|
|
vm_unmap_range(myproc()->pgdir, addr, myproc()->vma[i].length);
|
|
myproc()->vma[i].used = myproc()->vma[i].addr = myproc()->vma[i].length = 0;
|
|
myproc()->vma[i].no_free = 0;
|
|
myproc()->vma[i].shmid = -1;
|
|
return 0;
|
|
}
|
|
}
|
|
// Partial / untracked unmap — free pages anyway
|
|
vm_unmap_range(myproc()->pgdir, addr, length);
|
|
return 0;
|
|
}
|
|
|
|
addr_t sys_mprotect(void) { return 0; }
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Shared memory
|
|
//
|
|
// Two processes mapping the same physical pages. This is what a display
|
|
// server and its clients need: without it every frame is a copy through a
|
|
// pipe, and the copy is the entire cost of drawing anything.
|
|
//
|
|
// Deliberately simple. Pages are allocated up front rather than on demand,
|
|
// there is no swapping, and a segment lives until the last attachment goes
|
|
// away. What it does have is the property that matters: a store by one
|
|
// process is visible to the other, because it is the same page.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#define NSHM 16 // segments
|
|
#define SHM_MAXPAGES 512 // 2 MB each -- a 800x600x32 surface is 1.9 MB
|
|
#define IPC_PRIVATE 0
|
|
#define IPC_CREAT 01000
|
|
#define IPC_EXCL 02000
|
|
|
|
struct shmseg {
|
|
int used;
|
|
int key;
|
|
uint64 size; // rounded up to a page
|
|
int npages;
|
|
int nattach; // live attachments across all processes
|
|
char *pages[SHM_MAXPAGES]; // kernel virtual addresses
|
|
};
|
|
|
|
static struct shmseg shm[NSHM];
|
|
static struct spinlock shmlock;
|
|
static int shm_ready;
|
|
|
|
static void
|
|
shm_init_once(void)
|
|
{
|
|
if (!shm_ready) {
|
|
initlock(&shmlock, "shm");
|
|
shm_ready = 1;
|
|
}
|
|
}
|
|
|
|
static void
|
|
shm_free(struct shmseg *s)
|
|
{
|
|
for (int i = 0; i < s->npages; i++)
|
|
if (s->pages[i])
|
|
kfree(s->pages[i]);
|
|
memset(s, 0, sizeof(*s));
|
|
}
|
|
|
|
// fork() gives the child its own attachment to the same segment. Without
|
|
// this the count would say one attachment while two processes had it mapped,
|
|
// and the first to exit would free the pages under the other.
|
|
void
|
|
shm_ref(int id)
|
|
{
|
|
if (id < 0 || id >= NSHM)
|
|
return;
|
|
shm_init_once();
|
|
acquire(&shmlock);
|
|
if (shm[id].used)
|
|
shm[id].nattach++;
|
|
release(&shmlock);
|
|
}
|
|
|
|
// Called from munmap and from exit, once per detached mapping.
|
|
void
|
|
shm_detach(int id)
|
|
{
|
|
if (id < 0 || id >= NSHM)
|
|
return;
|
|
shm_init_once();
|
|
acquire(&shmlock);
|
|
if (shm[id].used && shm[id].nattach > 0) {
|
|
if (--shm[id].nattach == 0)
|
|
shm_free(&shm[id]);
|
|
}
|
|
release(&shmlock);
|
|
}
|
|
|
|
addr_t
|
|
sys_shmget(void)
|
|
{
|
|
int key, flags;
|
|
addr_t size;
|
|
|
|
if (argint(0, &key) < 0 || argaddr(1, &size) < 0 || argint(2, &flags) < 0)
|
|
return -EINVAL;
|
|
|
|
size = PGROUNDUP(size);
|
|
if (size == 0 || size > (uint64)SHM_MAXPAGES * PGSIZE)
|
|
return -EINVAL;
|
|
|
|
shm_init_once();
|
|
acquire(&shmlock);
|
|
|
|
if (key != IPC_PRIVATE) {
|
|
for (int i = 0; i < NSHM; i++) {
|
|
if (shm[i].used && shm[i].key == key) {
|
|
if ((flags & IPC_CREAT) && (flags & IPC_EXCL)) {
|
|
release(&shmlock);
|
|
return -EEXIST;
|
|
}
|
|
if (shm[i].size < size) {
|
|
release(&shmlock);
|
|
return -EINVAL;
|
|
}
|
|
release(&shmlock);
|
|
return i;
|
|
}
|
|
}
|
|
if (!(flags & IPC_CREAT)) {
|
|
release(&shmlock);
|
|
return -ENOENT;
|
|
}
|
|
}
|
|
|
|
for (int i = 0; i < NSHM; i++) {
|
|
if (shm[i].used)
|
|
continue;
|
|
struct shmseg *s = &shm[i];
|
|
s->npages = (int)(size / PGSIZE);
|
|
for (int pg = 0; pg < s->npages; pg++) {
|
|
s->pages[pg] = kalloc();
|
|
if (!s->pages[pg]) {
|
|
for (int j = 0; j < pg; j++) kfree(s->pages[j]);
|
|
memset(s, 0, sizeof(*s));
|
|
release(&shmlock);
|
|
return -ENOMEM;
|
|
}
|
|
// Zeroed, because a fresh segment handing out the last tenant's
|
|
// bytes would leak them between processes.
|
|
memset(s->pages[pg], 0, PGSIZE);
|
|
}
|
|
s->used = 1;
|
|
s->key = key;
|
|
s->size = size;
|
|
s->nattach = 0;
|
|
release(&shmlock);
|
|
return i;
|
|
}
|
|
|
|
release(&shmlock);
|
|
return -ENOSPC;
|
|
}
|
|
|
|
addr_t
|
|
sys_shmat(void)
|
|
{
|
|
int id, flags;
|
|
addr_t addr;
|
|
|
|
if (argint(0, &id) < 0 || argaddr(1, &addr) < 0 || argint(2, &flags) < 0)
|
|
return -EINVAL;
|
|
if (id < 0 || id >= NSHM)
|
|
return -EINVAL;
|
|
|
|
shm_init_once();
|
|
acquire(&shmlock);
|
|
if (!shm[id].used) {
|
|
release(&shmlock);
|
|
return -EINVAL;
|
|
}
|
|
uint64 len = shm[id].size;
|
|
int npages = shm[id].npages;
|
|
shm[id].nattach++; // claimed before the lock is dropped
|
|
release(&shmlock);
|
|
|
|
struct vma *v = 0;
|
|
for (int i = 0; i < NVMA; i++)
|
|
if (!myproc()->vma[i].used) { v = &myproc()->vma[i]; break; }
|
|
if (!v) { shm_detach(id); return -ENOMEM; }
|
|
|
|
// Ignore the hint and take a fresh range, the same way mmap does.
|
|
if (myproc()->mmap_next == 0)
|
|
myproc()->mmap_next = 0x40000000;
|
|
uint64 va = myproc()->mmap_next;
|
|
if (bad_user_range(va, len)) { shm_detach(id); return -ENOMEM; }
|
|
myproc()->mmap_next += len;
|
|
|
|
for (int pg = 0; pg < npages; pg++) {
|
|
if (mappages(myproc()->pgdir, (void*)(va + (uint64)pg * PGSIZE), PGSIZE,
|
|
V2P((uint64)shm[id].pages[pg]), PTE_W | PTE_U) < 0) {
|
|
vm_unmap_norelease(myproc()->pgdir, va, (uint64)pg * PGSIZE);
|
|
shm_detach(id);
|
|
return -ENOMEM;
|
|
}
|
|
}
|
|
|
|
v->addr = va; v->length = len; v->used = 1;
|
|
v->no_free = 1; // the segment owns these pages
|
|
v->shmid = id;
|
|
return va;
|
|
}
|
|
|
|
// Drop every mapping this process holds that it does not own the pages of,
|
|
// before its page tables are torn down.
|
|
//
|
|
// freevm() frees any mapped page below PHYSTOP, which is right for the heap
|
|
// and the stack and wrong for anything shared. The framebuffer survived only
|
|
// because it sits above PHYSTOP and was skipped by accident; shared memory is
|
|
// kalloc'd, sits below it, and would be handed to kfree while another process
|
|
// was still using it -- the worst kind of bug, silent and remote.
|
|
//
|
|
// Called from exit() and from exec() before the old address space goes away.
|
|
void
|
|
proc_release_shared(struct proc *p)
|
|
{
|
|
for (int i = 0; i < NVMA; i++) {
|
|
if (!p->vma[i].used || !p->vma[i].no_free)
|
|
continue;
|
|
vm_unmap_norelease(p->pgdir, p->vma[i].addr, p->vma[i].length);
|
|
if (p->vma[i].shmid >= 0)
|
|
shm_detach(p->vma[i].shmid);
|
|
p->vma[i].used = 0;
|
|
p->vma[i].no_free = 0;
|
|
p->vma[i].shmid = -1;
|
|
}
|
|
}
|
|
|
|
addr_t
|
|
sys_shmdt(void)
|
|
{
|
|
addr_t addr;
|
|
if (argaddr(0, &addr) < 0)
|
|
return -EINVAL;
|
|
|
|
for (int i = 0; i < NVMA; i++) {
|
|
if (myproc()->vma[i].used && myproc()->vma[i].addr == addr &&
|
|
myproc()->vma[i].shmid >= 0) {
|
|
vm_unmap_norelease(myproc()->pgdir, addr, myproc()->vma[i].length);
|
|
shm_detach(myproc()->vma[i].shmid);
|
|
myproc()->vma[i].used = myproc()->vma[i].addr = myproc()->vma[i].length = 0;
|
|
myproc()->vma[i].no_free = 0;
|
|
myproc()->vma[i].shmid = -1;
|
|
return 0;
|
|
}
|
|
}
|
|
return -EINVAL;
|
|
}
|
|
|
|
// Memory advice/hints — all no-ops; kernel decides on its own.
|
|
addr_t sys_madvise(void) { return 0; }
|
|
addr_t sys_msync(void) { return 0; }
|
|
addr_t sys_mincore(void) { return 0; }
|
|
addr_t sys_mlock(void) { return 0; }
|
|
addr_t sys_munlock(void) { return 0; }
|
|
addr_t sys_mlockall(void) { return 0; }
|
|
addr_t sys_munlockall(void){ return 0; }
|
|
addr_t sys_fadvise64(void) { return 0; }
|
|
|
|
// mremap: we don't support region relocation; return ENOMEM.
|
|
addr_t sys_mremap(void) { return -ENOMEM; }
|
|
|
|
// Resource limits: always report unlimited.
|
|
addr_t sys_setrlimit(void) { return 0; }
|
|
|
|
// Process priority: always report 0.
|
|
addr_t sys_getpriority(void) { return 0; }
|
|
addr_t sys_setpriority(void) { return 0; }
|