docs/plan/hybrid.md's first eviction. cmd/vgpud is the virtio-gpu driver as a
process: it finds the device by vendor and device id, asks the kernel for it,
and does the whole virtio 1.0 bring-up from outside -- reset, feature
negotiation, a virtqueue in DMA memory, GET_DISPLAY_INFO, RESOURCE_CREATE_2D,
ATTACH_BACKING, SET_SCANOUT, TRANSFER and FLUSH.
vgpud: virtio-gpu at 00:03.0, currently held by virtio-gpu
virtio-gpu: released to userspace; screen is the VBE framebuffer
vgpud: claimed
vgpud: display is 1280x800
vgpud: scanout is ours, framebuffer at physical 0x3eceb000
vgpud: drew a test pattern from userspace
Verified by screendumping the virtio-gpu head: the pattern is there, with a
checkerboard that would shear if the stride were wrong. The console carries on
in VBE on the other head at the same time, which is the fallback rule from the
plan working rather than being described.
No syscall knows this is a GPU. The kernel handed over a PCI device and the
process decided what it was -- which is the property that makes this a general
mechanism, and the reason the evictions come before anything foreign.
Four things the first real driver needed that the test program had not:
- PCI config space (PCIIOC_CFG). A virtio device says where its registers
are in vendor capabilities, which live in config space, so a driver that
cannot read it cannot find its own registers.
- a way for the incumbent to let go: struct pci_driver gained a release
hook. A driver without one keeps its device, which is always safe and for
anything on the path to a shell is also correct.
- the outgoing driver freeing its DMA. Releasing the device but keeping its
4 MB framebuffer is half a handover -- the incoming driver asks for one and
is told there is no room.
- a 16 MB DMA arena. 4 MB was exactly one framebuffer at this resolution,
and either side of a handover there are two.
And one bug it reproduced on its first working run: vgpud polls the used ring,
so it registered no interrupt handler, and a virtio device interrupts on every
completion. The storm detector said so immediately --
irq 11: 77780 interrupts/second -- a device on this line is not
acknowledging it. handler 'e1000' is attached
-- naming the line, the rate and the neighbour it was drowning. One line fixes
it (VRING_AVAIL_F_NO_INTERRUPT before the queue is enabled), and that detector
was worth every minute it cost.
include/virtio.h moved out of the driver directory. Its own comment said it
should when a second reader appeared; one has.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2
138 lines
5.9 KiB
C
138 lines
5.9 KiB
C
// SPDX-License-Identifier: Apache-2.0
|
|
//
|
|
// /dev/pci -- device access from userspace.
|
|
//
|
|
// docs/plan/hybrid.md, mechanisms 2 and 4: MMIO and DMA to a process. This is
|
|
// the interface a driver that does not live in the kernel talks to, and it is
|
|
// deliberately about *PCI devices*, not about any particular driver model.
|
|
//
|
|
// That generality is the point, and it is worth being explicit about why.
|
|
// The first users of this will be SchOS's own drivers, evicted one at a time
|
|
// -- virtio-gpu, then xHCI -- and the interface has to be judged by them.
|
|
// Something that hosts foreign drivers later is then a consumer of a general
|
|
// mechanism rather than a hole shaped like one, which is both the honest
|
|
// design and the only one that stays honest under examination.
|
|
//
|
|
// What a driver needs from a kernel, and all it needs:
|
|
//
|
|
// - to find its device, and read what the bus says about it INFO
|
|
// - to be given it, exclusively CLAIM
|
|
// - to reach its registers mmap(BAR)
|
|
// - memory it can hand to the device DMA
|
|
// - to be told when the device wants attention IRQ + read()
|
|
//
|
|
// There is no IOMMU. A process holding a DMA buffer can point the device at
|
|
// any physical address, so this isolates a driver against its own *bugs* and
|
|
// not against its own *intent*. docs/plan/hybrid.md says so at length and it
|
|
// remains true here: a crashed driver that restarts instead of taking the
|
|
// machine down is the whole benefit, and it is not a security boundary.
|
|
|
|
#ifndef PCIUSER_H
|
|
#define PCIUSER_H
|
|
|
|
#define PCIUSER_MAJOR 9
|
|
|
|
#define PCIUSER_MAXBAR 6
|
|
|
|
struct pciuser_bar {
|
|
unsigned long long base; // physical address, 0 when the BAR is unused
|
|
unsigned long long size;
|
|
unsigned int flags; // PCIUSER_BAR_*
|
|
unsigned int pad;
|
|
};
|
|
|
|
#define PCIUSER_BAR_MMIO 0x01 // memory space; mappable
|
|
#define PCIUSER_BAR_IO 0x02 // I/O port space; not mappable
|
|
#define PCIUSER_BAR_PREFETCH 0x04
|
|
|
|
// One device, as the bus describes it. `index` is the only field the caller
|
|
// fills in; everything else comes back.
|
|
struct pciuser_info {
|
|
unsigned int index; // in: 0 .. PCIIOC_COUNT-1
|
|
unsigned char bus, slot, func;
|
|
unsigned char header_type;
|
|
|
|
unsigned short vendor, device;
|
|
unsigned char class, subclass, prog_if, revision;
|
|
|
|
unsigned char irq; // interrupt line, 0xFF when unrouted
|
|
unsigned char claimed; // 1 when a userspace process holds it
|
|
unsigned short pad;
|
|
|
|
// The name of whatever owns it: an in-kernel driver, "userspace" when a
|
|
// process has claimed it, or empty when nothing has.
|
|
char driver[16];
|
|
|
|
struct pciuser_bar bar[PCIUSER_MAXBAR];
|
|
};
|
|
|
|
// A DMA buffer. The kernel allocates it from the DMA arena, maps it into the
|
|
// caller, and reports the physical address the *device* must be given -- which
|
|
// is the entire reason a driver cannot do this for itself with mmap.
|
|
// A dword of PCI configuration space. A virtio device says where its registers
|
|
// live in vendor capabilities, which are here and nowhere else -- so without
|
|
// this a driver cannot find its own device.
|
|
struct pciuser_cfg {
|
|
unsigned int index; // in
|
|
unsigned int offset; // in: dword-aligned, 0..0xFC
|
|
unsigned int value; // out
|
|
unsigned int pad;
|
|
};
|
|
|
|
struct pciuser_dma {
|
|
unsigned long long size; // in: bytes, rounded up to a page
|
|
unsigned long long uaddr; // out: where it landed in this process
|
|
unsigned long long paddr; // out: what to program into the device
|
|
};
|
|
|
|
#define PCIIOC_COUNT 0x9000 // out: int, how many devices the bus has
|
|
#define PCIIOC_INFO 0x9001 // struct pciuser_info
|
|
#define PCIIOC_CLAIM 0x9002 // in: int index -- take it from the kernel
|
|
#define PCIIOC_RELEASE 0x9003 // in: int index
|
|
#define PCIIOC_DMA 0x9004 // struct pciuser_dma
|
|
#define PCIIOC_CFG 0x9006 // struct pciuser_cfg -- read config space
|
|
#define PCIIOC_IRQ 0x9005 // in: int index -- deliver this device's
|
|
// interrupts to this process
|
|
|
|
// Interrupts arrive by *reading* this device, which is how Linux's uio does it
|
|
// and for the same reason: a descriptor composes with poll, and a driver that
|
|
// stops reading falls behind rather than losing events.
|
|
//
|
|
// ioctl(fd, PCIIOC_IRQ, &index);
|
|
// for (;;) {
|
|
// unsigned long long n;
|
|
// read(fd, &n, sizeof n); // blocks until the device interrupts
|
|
// ... service the device, and ACK IT AT THE DEVICE ...
|
|
// write(fd, &n, sizeof n); // re-arm
|
|
// }
|
|
//
|
|
// **The write is not optional.** A PCI interrupt line is level-triggered: it
|
|
// stays asserted until the driver clears the cause *at the device*, and the
|
|
// kernel cannot do that because clearing it is device-specific -- it is a
|
|
// different register on every chip. So the kernel masks the line when the
|
|
// interrupt arrives and leaves it masked until the write says the device has
|
|
// been dealt with.
|
|
//
|
|
// Without that, EOI is signalled on a line that is still asserted and the
|
|
// interrupt re-fires immediately, for ever. That is not hypothetical: it
|
|
// happened in this kernel, at 72,000 interrupts a second, and what it looked
|
|
// like from userspace was a clock running seven times slow.
|
|
// See docs/history/kernel.md section 13.
|
|
//
|
|
// One device per process. A driver drives one thing; if you need two, run two.
|
|
|
|
|
|
|
|
// Registers are reached with mmap() on this device, where the *offset is the
|
|
// physical address of the BAR*:
|
|
//
|
|
// ioctl(fd, PCIIOC_INFO, &info);
|
|
// ioctl(fd, PCIIOC_CLAIM, &info.index);
|
|
// regs = mmap(0, info.bar[0].size, PROT_READ|PROT_WRITE, MAP_SHARED,
|
|
// fd, info.bar[0].base);
|
|
//
|
|
// The kernel refuses an offset that is not inside a BAR of a device this
|
|
// process has claimed, which is what stops this being /dev/mem with extra
|
|
// steps.
|
|
|
|
#endif // PCIUSER_H
|