hybrid: the driver is killed, and the driver comes back
H2 of docs/plan/hybrid.md -- the milestone the plan itself calls the one
that proves the whole idea, and the one that cannot be skipped.
# cat /proc/schinit
1 4 11 os.schos.vgpud
# kill 4
schinitd: restarting os.schos.vgpud
vgpud: claimed
vgpud: pid 8 drew a test pattern from userspace
The test pattern ends in a twelve-block tally of the drawing process's
pid, because a second run that redraws an identical picture is
indistinguishable from a first run that never stopped. Read off the
virtio-gpu scanout across four generations it says 4, 8, 10, 12, and the
DMA arena hands back the same physical address every time.
What it took:
* A halt the kernel can apply to a device it does not understand.
Clearing PCI_CMD_BUS_MASTER is the only one there is, and it has to
happen before the DMA goes back to the arena -- a dead driver's device
keeps reading a framebuffer the kernel is about to give to somebody
else. Claim sets the bit, release clears it; pcitest shows it in a bm
column, with a third state so a failed read is never printed as off.
* A refusal. vgpu_release() asks vga_fb_restore() whether there is a
framebuffer to put the console back on, and declines to hand over the
device if there is not. The rule that the kernel keeps a minimal
driver for everything on the path to a shell is worth writing down
only if something enforces it.
* The DMA arena not being owned by two allocators at once. This is the
bug that stopped H2: freevm() frees every mapped page below PHYSTOP
and the arena is the top 16 MB of exactly that range, so a DMA block
mapped into a driver went onto the page allocator's free list when the
driver died, while pciuser_release_all() handed the same pages back to
the arena. The restarted vgpud asked for its framebuffer, got pages
kalloc() had already given out -- including its own page tables -- and
dma_alloc()'s zeroing wiped them, so it died inside the ioctl on the
return from a syscall whose stack had stopped existing. do_dma() now
records a VMA with no_free set, the way shared memory does.
* One exit status. KeepAlive must restart a driver that crashed and must
not restart one that has looked at the machine and found its device
absent: on hardware with no virtio-gpu that was fifteen spawns in
forty-five seconds, each printing a line to the console, for ever.
SCHINIT_EXIT_NORESTART is 78, sysexits.h's EX_CONFIG. It is not a new
manifest key -- the wait status has always travelled from PID 1 to the
supervisor, and note_exit() simply stopped discarding it.
The milestone's third clause, "the compositor reattaches", is not done
and is not claimed: there is no compositor running. That is recorded in
the plan as a Schcom milestone rather than counted here.
Closes the register row "A released PCI device" in docs/plan/1.0.md.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2
This commit is contained in:
parent
c0ed23cc56
commit
d1f50c4da3
15 changed files with 442 additions and 40 deletions
|
|
@ -93,12 +93,31 @@ main(int argc, char *argv[])
|
||||||
}
|
}
|
||||||
|
|
||||||
if (argc == 1) {
|
if (argc == 1) {
|
||||||
puts(" # bdf vendor:device irq class driver\n");
|
puts(" # bdf vendor:device irq class bm driver\n");
|
||||||
for (int i = 0; i < count; i++) {
|
for (int i = 0; i < count; i++) {
|
||||||
struct pciuser_info in;
|
struct pciuser_info in;
|
||||||
in.index = (unsigned int)i;
|
in.index = (unsigned int)i;
|
||||||
if (ioctl(fd, PCIIOC_INFO, &in) < 0)
|
if (ioctl(fd, PCIIOC_INFO, &in) < 0)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
// Bus mastering, straight out of the command register.
|
||||||
|
//
|
||||||
|
// Worth a column of its own because it is the one bit that says
|
||||||
|
// whether a device can still touch memory, and the whole lifecycle
|
||||||
|
// turns on it: claiming a device sets it, releasing one clears it,
|
||||||
|
// and a driver that died leaves a device that must not be left
|
||||||
|
// reading buffers the kernel is about to hand to somebody else.
|
||||||
|
struct pciuser_cfg cfg;
|
||||||
|
cfg.index = (unsigned int)i;
|
||||||
|
cfg.offset = 0x04; // PCI_COMMAND
|
||||||
|
cfg.value = 0;
|
||||||
|
// Three states, not two. A read that failed is not the same as a
|
||||||
|
// bit that is clear, and printing them the same way is how a
|
||||||
|
// column stops being evidence -- which is exactly what happened
|
||||||
|
// the first time this was run.
|
||||||
|
const char *bm = "? ";
|
||||||
|
if (ioctl(fd, PCIIOC_CFG, &cfg) == 0)
|
||||||
|
bm = (cfg.value & 0x0004) ? "on " : "-- ";
|
||||||
puts(" "); putn(i); puts(" ");
|
puts(" "); putn(i); puts(" ");
|
||||||
puthex(in.bus, 2); puts(":"); puthex(in.slot, 2);
|
puthex(in.bus, 2); puts(":"); puthex(in.slot, 2);
|
||||||
puts("."); puthex(in.func, 1); puts(" ");
|
puts("."); puthex(in.func, 1); puts(" ");
|
||||||
|
|
@ -106,6 +125,7 @@ main(int argc, char *argv[])
|
||||||
if (in.irq == 0xFF) puts(" - "); else { putn(in.irq); puts(" "); }
|
if (in.irq == 0xFF) puts(" - "); else { putn(in.irq); puts(" "); }
|
||||||
puts(class_name(in.class));
|
puts(class_name(in.class));
|
||||||
puts(" ");
|
puts(" ");
|
||||||
|
puts(bm);
|
||||||
puts(in.driver[0] ? in.driver : "-");
|
puts(in.driver[0] ? in.driver : "-");
|
||||||
puts("\n");
|
puts("\n");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -455,7 +455,6 @@ job_by_cookie(unsigned int cookie)
|
||||||
static void
|
static void
|
||||||
note_exit(unsigned int cookie, int pid, int status)
|
note_exit(unsigned int cookie, int pid, int status)
|
||||||
{
|
{
|
||||||
(void)status;
|
|
||||||
struct job *j = job_by_cookie(cookie);
|
struct job *j = job_by_cookie(cookie);
|
||||||
if (!j)
|
if (!j)
|
||||||
return; // not ours, or already noticed
|
return; // not ours, or already noticed
|
||||||
|
|
@ -465,6 +464,16 @@ note_exit(unsigned int cookie, int pid, int status)
|
||||||
if (!j->keep_alive)
|
if (!j->keep_alive)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
// A job that says it cannot run on this machine is believed. The status is
|
||||||
|
// wait(2)-encoded: the low seven bits are the signal that killed it, so a
|
||||||
|
// job that was killed or faulted never matches this and is restarted the
|
||||||
|
// way KeepAlive promises. See SCHINIT_EXIT_NORESTART.
|
||||||
|
if ((status & 0x7f) == 0 && ((status >> 8) & 0xff) == SCHINIT_EXIT_NORESTART) {
|
||||||
|
out("schinitd: "); out(j->label);
|
||||||
|
out(" has nothing to do on this machine; not restarting\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Throttle. A job that exits immediately and forever would otherwise spin
|
// Throttle. A job that exits immediately and forever would otherwise spin
|
||||||
// the machine; launchd's ThrottleInterval exists for exactly this.
|
// the machine; launchd's ThrottleInterval exists for exactly this.
|
||||||
int ticks = j->throttle * 100;
|
int ticks = j->throttle * 100;
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@
|
||||||
#include "posix.h"
|
#include "posix.h"
|
||||||
#include "pciuser.h"
|
#include "pciuser.h"
|
||||||
#include "virtio.h"
|
#include "virtio.h"
|
||||||
|
#include "schinit.h"
|
||||||
|
|
||||||
// --- small output, since lib/ has no printf ---------------------------------
|
// --- small output, since lib/ has no printf ---------------------------------
|
||||||
|
|
||||||
|
|
@ -64,6 +65,17 @@ die(const char *why)
|
||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Not a failure: a report. There is no virtio-gpu here, or the kernel will not
|
||||||
|
// give it up, and neither of those changes by trying again -- so this tells the
|
||||||
|
// supervisor to stop, rather than being restarted every three seconds for the
|
||||||
|
// rest of the uptime. See SCHINIT_EXIT_NORESTART in include/schinit.h.
|
||||||
|
static void
|
||||||
|
nothing_to_do(const char *why)
|
||||||
|
{
|
||||||
|
puts("vgpud: "); puts(why); puts("\n");
|
||||||
|
exit_group(SCHINIT_EXIT_NORESTART);
|
||||||
|
}
|
||||||
|
|
||||||
// --- register access --------------------------------------------------------
|
// --- register access --------------------------------------------------------
|
||||||
|
|
||||||
static inline unsigned char r8 (volatile unsigned char *p, int o) { return p[o]; }
|
static inline unsigned char r8 (volatile unsigned char *p, int o) { return p[o]; }
|
||||||
|
|
@ -453,6 +465,12 @@ draw_test_pattern(void)
|
||||||
{
|
{
|
||||||
volatile unsigned int *fb = (volatile unsigned int *)fb_uaddr;
|
volatile unsigned int *fb = (volatile unsigned int *)fb_uaddr;
|
||||||
|
|
||||||
|
// A tally of this instance's pid along the bottom edge, because the thing
|
||||||
|
// H2 has to show is that the driver *came back* -- and a second run that
|
||||||
|
// redraws an identical picture is indistinguishable from a first run that
|
||||||
|
// never stopped. One block per bit, low bit at the left.
|
||||||
|
int mypid = getpid();
|
||||||
|
|
||||||
for (int y = 0; y < height; y++) {
|
for (int y = 0; y < height; y++) {
|
||||||
for (int x = 0; x < width; x++) {
|
for (int x = 0; x < width; x++) {
|
||||||
unsigned int c;
|
unsigned int c;
|
||||||
|
|
@ -474,6 +492,14 @@ draw_test_pattern(void)
|
||||||
fb[y * width + x] = c;
|
fb[y * width + x] = c;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (int b = 0; b < 12; b++) {
|
||||||
|
unsigned int c = (mypid >> b) & 1 ? 0x4ADE80 : 0x12151C; /* SCH_OK */
|
||||||
|
for (int y = height - 24; y < height - 8; y++)
|
||||||
|
for (int x = 16 + b * 24; x < 16 + b * 24 + 16; x++)
|
||||||
|
if (y >= 0 && x < width)
|
||||||
|
fb[y * width + x] = c;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- main -------------------------------------------------------------------
|
// --- main -------------------------------------------------------------------
|
||||||
|
|
@ -502,7 +528,7 @@ main(int argc, char *argv[])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (index_ < 0)
|
if (index_ < 0)
|
||||||
die("no virtio-gpu on this machine");
|
nothing_to_do("no virtio-gpu on this machine");
|
||||||
|
|
||||||
puts("vgpud: virtio-gpu at "); puthex(info.bus, 2); puts(":");
|
puts("vgpud: virtio-gpu at "); puthex(info.bus, 2); puts(":");
|
||||||
puthex(info.slot, 2); puts("."); puthex(info.func, 1);
|
puthex(info.slot, 2); puts("."); puthex(info.func, 1);
|
||||||
|
|
@ -513,7 +539,7 @@ main(int argc, char *argv[])
|
||||||
// back on screen before returning, so there is never a moment with no
|
// back on screen before returning, so there is never a moment with no
|
||||||
// display -- see vgpu_release() and vga_fb_restore().
|
// display -- see vgpu_release() and vga_fb_restore().
|
||||||
if (ioctl(pcifd, PCIIOC_CLAIM, &index_) < 0)
|
if (ioctl(pcifd, PCIIOC_CLAIM, &index_) < 0)
|
||||||
die("claim refused -- the kernel driver would not release it");
|
nothing_to_do("claim refused -- the kernel driver would not release it");
|
||||||
puts("vgpud: claimed\n");
|
puts("vgpud: claimed\n");
|
||||||
|
|
||||||
// The info is re-read because claiming is what enables memory space and
|
// The info is re-read because claiming is what enables memory space and
|
||||||
|
|
@ -532,7 +558,9 @@ main(int argc, char *argv[])
|
||||||
draw_test_pattern();
|
draw_test_pattern();
|
||||||
present();
|
present();
|
||||||
|
|
||||||
puts("vgpud: drew a test pattern from userspace\n");
|
puts("vgpud: pid ");
|
||||||
|
putn(getpid());
|
||||||
|
puts(" drew a test pattern from userspace\n");
|
||||||
|
|
||||||
if (argc > 1 && argv[1][0] == '-' && argv[1][1] == 'x') {
|
if (argc > 1 && argv[1][0] == '-' && argv[1][1] == 'x') {
|
||||||
// Exit without releasing, to show what a crashed driver leaves behind:
|
// Exit without releasing, to show what a crashed driver leaves behind:
|
||||||
|
|
@ -542,10 +570,17 @@ main(int argc, char *argv[])
|
||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Nothing to serve yet -- the compositor is what will connect here, over
|
// Hold the device and wait.
|
||||||
// mechanism 1, which does not exist. Holding the device is the honest
|
//
|
||||||
// thing to do until then: releasing it would put the console back and make
|
// Mechanism 1 exists now, so what belongs here eventually is an AF_UNIX
|
||||||
// it look like this never worked.
|
// socket a compositor connects to. It is not built, and building it before
|
||||||
|
// there is a compositor to judge it is the mistake this plan spends a
|
||||||
|
// section warning about -- so this sleeps instead, and holding the device
|
||||||
|
// is the honest state: releasing it would put the console back and make it
|
||||||
|
// look like none of this ever worked.
|
||||||
|
//
|
||||||
|
// Being killed here is not an error path, it is the H2 path: schinit
|
||||||
|
// restarts this process, and the device has to come back with it.
|
||||||
for (;;)
|
for (;;)
|
||||||
sleep(100);
|
sleep(100);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -113,6 +113,44 @@ console falls back to the VBE framebuffer.
|
||||||
**No syscall on that path knows it is talking about a GPU.** The kernel hands
|
**No syscall on that path knows it is talking about a GPU.** The kernel hands
|
||||||
over a PCI device; the process decides what it is.
|
over a PCI device; the process decides what it is.
|
||||||
|
|
||||||
|
### Taking it back
|
||||||
|
|
||||||
|
A driver that dies is the case this whole arrangement exists for, so release is a
|
||||||
|
symmetric operation rather than a courtesy. Three things happen that are not
|
||||||
|
obvious from "the process exited":
|
||||||
|
|
||||||
|
**Bus mastering is cleared before the DMA is freed.** A device whose driver has
|
||||||
|
just died is still writing into memory the kernel is about to hand to somebody
|
||||||
|
else. The kernel cannot quiesce a device it does not understand — that is a
|
||||||
|
different register on every chip — but it can clear `PCI_CMD_BUS_MASTER`, which
|
||||||
|
stops the writes whatever the device is, and it is the only halt available to a
|
||||||
|
kernel that knows nothing but the header. So `PCIIOC_CLAIM` sets the bit and
|
||||||
|
release clears it, in that order relative to freeing the memory. `pcitest` prints
|
||||||
|
the bit in a `bm` column, with a separate mark for "could not read it", because a
|
||||||
|
failed read rendered as "off" is worse than no column at all.
|
||||||
|
|
||||||
|
**DMA memory a driver mapped is not `freevm()`'s to release.** `freevm()` frees
|
||||||
|
every mapped page below `PHYSTOP` and the DMA arena is the top 16 MB of exactly
|
||||||
|
that range, so a DMA block left for it went onto the page allocator's free list
|
||||||
|
while `pciuser_release_all()` handed the same pages back to the arena — two
|
||||||
|
allocators, one set of pages. `PCIIOC_DMA` records a VMA with `no_free` set, the
|
||||||
|
same way shared memory does, and `proc_release_shared()` unmaps the range before
|
||||||
|
the page tables are torn down. The same rule covers every future case: **anything
|
||||||
|
mapped into a process that the process does not own the pages of needs a VMA, or
|
||||||
|
exit will free it twice.**
|
||||||
|
|
||||||
|
**A device that is the only display will not be released.** `vgpu_release()` asks
|
||||||
|
`vga_fb_restore()` whether there is a framebuffer to put the console back on, and
|
||||||
|
declines if the answer is no. The rule that the kernel keeps a minimal driver for
|
||||||
|
everything on the path to a shell is only worth writing down if something
|
||||||
|
enforces it.
|
||||||
|
|
||||||
|
Coming back is a job, not a kernel feature. `etc/schinit/vgpud.job` carries
|
||||||
|
`KeepAlive = yes`, so killing the driver is the entire test: `schinitd` starts
|
||||||
|
another one, it claims the device, and the pid of whichever process drew the
|
||||||
|
frame is legible in the frame itself. See
|
||||||
|
[../plan/hybrid.md](../plan/hybrid.md), "The driver came back".
|
||||||
|
|
||||||
### What this is not
|
### What this is not
|
||||||
|
|
||||||
**It is not a security boundary.** There is no IOMMU, so a process holding a DMA
|
**It is not a security boundary.** There is no IOMMU, so a process holding a DMA
|
||||||
|
|
@ -170,9 +208,11 @@ existing mechanisms for keeping that true mechanically rather than by intention.
|
||||||
kernel is an ordinary program and can be started whenever — but it reaches its
|
kernel is an ordinary program and can be started whenever — but it reaches its
|
||||||
device through `/dev/pci` and publishes nothing to the class vtables, so the
|
device through `/dev/pci` and publishes nothing to the class vtables, so the
|
||||||
console cannot be handed to one yet.
|
console cannot be handed to one yet.
|
||||||
- **A released device does not come back by itself.** `PCIIOC_RELEASE`, or the
|
- **The kernel does not rebind its own driver.** `PCIIOC_RELEASE`, or the process
|
||||||
process exiting, leaves the interrupt line masked and the in-kernel driver
|
exiting, leaves the interrupt line masked and the in-kernel driver unbound —
|
||||||
unbound. Something has to start a replacement; nothing watches for one.
|
the console stays wherever the release put it. A *supervisor* can start a
|
||||||
|
replacement driver, and `schinitd` does; the kernel itself watches for
|
||||||
|
nothing.
|
||||||
- **Only two class vtables are defined**: `display_ops` and `netdev_ops`.
|
- **Only two class vtables are defined**: `display_ops` and `netdev_ops`.
|
||||||
Storage and input devices appear in the list with `ops = 0` and are reached
|
Storage and input devices appear in the list with `ops = 0` and are reached
|
||||||
through their own subsystems instead.
|
through their own subsystems instead.
|
||||||
|
|
|
||||||
|
|
@ -229,7 +229,7 @@ and this is not a language).
|
||||||
| `Program` | The binary to execute |
|
| `Program` | The binary to execute |
|
||||||
| `Args` | argv, space separated. argv[0] is conventionally the basename, so `Args = sh` for `/bin/sh` |
|
| `Args` | argv, space separated. argv[0] is conventionally the basename, so `Args = sh` for `/bin/sh` |
|
||||||
| `RunAtLoad` | Start it when schinit loads |
|
| `RunAtLoad` | Start it when schinit loads |
|
||||||
| `KeepAlive` | Restart it when it exits |
|
| `KeepAlive` | Restart it when it exits — unless it exits `SCHINIT_EXIT_NORESTART`, below |
|
||||||
| `Disabled` | Load it but do not start it |
|
| `Disabled` | Load it but do not start it |
|
||||||
| `ThrottleInterval` | Seconds between respawns (default 1) |
|
| `ThrottleInterval` | Seconds between respawns (default 1) |
|
||||||
| `ControllingTerminal` | Claim the console |
|
| `ControllingTerminal` | Claim the console |
|
||||||
|
|
@ -272,6 +272,31 @@ subject to `ThrottleInterval` — a job that exits immediately and forever would
|
||||||
otherwise spin the machine. launchd defaults to 10 seconds; 1 is enough here,
|
otherwise spin the machine. launchd defaults to 10 seconds; 1 is enough here,
|
||||||
where the common case is a shell someone just exited on purpose and expects back.
|
where the common case is a shell someone just exited on purpose and expects back.
|
||||||
|
|
||||||
|
### One exit status the supervisor reads
|
||||||
|
|
||||||
|
Throttling stops a restart loop from costing CPU. It does not stop it being a
|
||||||
|
loop, and a job that can never succeed on this machine will print its reason
|
||||||
|
every `ThrottleInterval` for the rest of the uptime — measured at fifteen spawns
|
||||||
|
in forty-five seconds for `vgpud` on a machine with no virtio-gpu.
|
||||||
|
|
||||||
|
So a `KeepAlive` job that exits **`SCHINIT_EXIT_NORESTART`** (78, sysexits.h's
|
||||||
|
`EX_CONFIG`) is not restarted, and the supervisor says which job stopped and
|
||||||
|
why:
|
||||||
|
|
||||||
|
```
|
||||||
|
vgpud: no virtio-gpu on this machine
|
||||||
|
schinitd: os.schos.vgpud has nothing to do on this machine; not restarting
|
||||||
|
```
|
||||||
|
|
||||||
|
The distinction it draws is the right one: the status is wait(2)-encoded, so a
|
||||||
|
job that was **killed or faulted** has a signal in the low bits, never matches,
|
||||||
|
and comes back exactly as `KeepAlive` promises. Only a job that exited
|
||||||
|
deliberately, saying this machine is not one it can run on, is believed.
|
||||||
|
|
||||||
|
This is not a new manifest key and there is no policy knob. The wait status has
|
||||||
|
always travelled from PID 1 to the supervisor inside `struct schinit_reply`;
|
||||||
|
`note_exit()` simply stopped discarding it.
|
||||||
|
|
||||||
## The fallback
|
## The fallback
|
||||||
|
|
||||||
With no manifests found, the supervisor says so and starts `/bin/sh` on the
|
With no manifests found, the supervisor says so and starts `/bin/sh` on the
|
||||||
|
|
|
||||||
|
|
@ -94,9 +94,14 @@ The galling ones. The work exists, is tested, and nothing uses it.
|
||||||
| | Where it stopped |
|
| | Where it stopped |
|
||||||
|---|---|
|
|---|---|
|
||||||
| **`foundations.md`** | Item 1 built. Items 2 (a shareable memory object), 3 (`AF_UNIX` in the filesystem namespace), 4 and 5 open. **Wayland stops at item 2.** |
|
| **`foundations.md`** | Item 1 built. Items 2 (a shareable memory object), 3 (`AF_UNIX` in the filesystem namespace), 4 and 5 open. **Wayland stops at item 2.** |
|
||||||
| **`hybrid.md`** | All four mechanisms built, H1 done. **H2 — kill the driver on purpose and watch it come back — is the milestone the plan itself calls the one that proves the whole idea**, and it is not done. |
|
| **`hybrid.md`** | All four mechanisms built. H1 and H2 done — the driver can be killed and comes back, which is the milestone the plan calls the one that proves the whole idea. H3 (a modeset API) and H4 (USB) open. |
|
||||||
| **Schcom v0** | 715 lines that composite, over a wire format that is superseded. Frozen: it runs, nothing more is added, and it is kept only because it is the one thing in the tree that has ever put a client's pixels on screen. |
|
| **Schcom v0** | 715 lines that composite, over a wire format that is superseded. Frozen: it runs, nothing more is added, and it is kept only because it is the one thing in the tree that has ever put a client's pixels on screen. |
|
||||||
| **A released PCI device** | `PCIIOC_RELEASE` leaves the line masked and the in-kernel driver unbound. Nothing starts a replacement. Half a handover. |
|
|
||||||
|
**Closed since this register was written.** *A released PCI device* — release is
|
||||||
|
now symmetric with claim (bus mastering cleared before the DMA is freed, a
|
||||||
|
refusal when the device is the only display), a supervised job starts the
|
||||||
|
replacement, and it is described in
|
||||||
|
[../architecture/devices.md](../architecture/devices.md) under "Taking it back".
|
||||||
|
|
||||||
### Named in a "what it does not do" and still true
|
### Named in a "what it does not do" and still true
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -172,6 +172,79 @@ version that survives a licensing conversation.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## The driver came back
|
||||||
|
|
||||||
|
H2 is the milestone this plan calls the one that cannot be skipped, and the
|
||||||
|
reason is that H1 proves only half of the idea. A driver that runs in a process
|
||||||
|
is interesting; a driver that runs in a process, *dies*, and is replaced by
|
||||||
|
another process that picks the device up is the thing the whole hybrid argument
|
||||||
|
rests on.
|
||||||
|
|
||||||
|
`etc/schinit/vgpud.job` makes it a supervised job -- `KeepAlive = yes` -- so
|
||||||
|
killing it is the whole test:
|
||||||
|
|
||||||
|
```
|
||||||
|
# cat /proc/schinit
|
||||||
|
1 4 11 os.schos.vgpud
|
||||||
|
# kill 4
|
||||||
|
schinitd: restarting os.schos.vgpud
|
||||||
|
vgpud: virtio-gpu at 00:03.0
|
||||||
|
vgpud: claimed
|
||||||
|
vgpud: display is 1280x800
|
||||||
|
vgpud: scanout is ours, framebuffer at physical 0x3eceb000
|
||||||
|
vgpud: pid 8 drew a test pattern from userspace
|
||||||
|
```
|
||||||
|
|
||||||
|
The test pattern ends in a twelve-block tally of the drawing process's pid,
|
||||||
|
because a second run that redraws an identical picture is indistinguishable
|
||||||
|
from a first run that never stopped. Read off the virtio-gpu scanout across
|
||||||
|
four generations of the driver it says 4, 8, 10, 12. The same physical address
|
||||||
|
comes back every time, which is the DMA arena reclaiming cleanly rather than
|
||||||
|
drifting.
|
||||||
|
|
||||||
|
### What it took that H1 had not needed
|
||||||
|
|
||||||
|
- **A halt the kernel can apply without knowing what the device is.** A driver
|
||||||
|
that dies leaves its device bus-mastering into memory the kernel is about to
|
||||||
|
hand to somebody else. The kernel cannot quiesce a device it does not
|
||||||
|
understand, but it can clear `PCI_CMD_BUS_MASTER`, and that is enough to stop
|
||||||
|
the writes. `pciuser_stop()` does it before the DMA is freed, not after, and
|
||||||
|
claim/release became a symmetric pair: claiming sets the bit, releasing
|
||||||
|
clears it. `pcitest` grew a `bm` column so the state is visible rather than
|
||||||
|
assumed.
|
||||||
|
- **A refusal, when there is nothing to fall back to.** `vgpu_release()` now
|
||||||
|
asks `vga_fb_restore()` whether there is a framebuffer to put the console on,
|
||||||
|
and declines to hand the device over if there is not. The rule that the
|
||||||
|
kernel keeps a minimal driver for everything on the path to a shell is only
|
||||||
|
worth writing down if something enforces it.
|
||||||
|
- **The DMA arena not being handed to two allocators at once.** This is the bug
|
||||||
|
that stopped H2 for a day, and it is worth writing out: `freevm()` frees
|
||||||
|
every mapped page below `PHYSTOP`, and the DMA arena is the top 16 MB of
|
||||||
|
exactly that range. So a DMA block mapped into a driver and left for
|
||||||
|
`freevm()` went onto the page allocator's free list when the driver died,
|
||||||
|
while `pciuser_release_all()` handed the same pages back to the arena. Both
|
||||||
|
allocators then believed they owned it. The restarted `vgpud` asked for its
|
||||||
|
4 MB framebuffer, got pages `kalloc()` had already given out -- including its
|
||||||
|
own page tables -- and `dma_alloc()`'s zeroing wiped them, so the driver died
|
||||||
|
inside the ioctl, on the return from a syscall whose stack had stopped
|
||||||
|
existing. `do_dma()` now records a VMA with `no_free` set, the same way
|
||||||
|
shared memory does, so `proc_release_shared()` unmaps the range before the
|
||||||
|
page tables are torn down.
|
||||||
|
|
||||||
|
### The clause that is not true yet
|
||||||
|
|
||||||
|
The milestone says *"the compositor reattaches"*. It does not, because there is
|
||||||
|
no compositor running: Schcom is v0 and is not started at boot. What has been
|
||||||
|
shown is the two parts that do not depend on it -- the screen falls back to
|
||||||
|
VBE, and the driver restarts and takes the device back. During the gap the VBE
|
||||||
|
head carries a live console and the virtio-gpu head still shows the dead
|
||||||
|
driver's last frame, which is exactly what a device nobody is driving does.
|
||||||
|
|
||||||
|
Reattachment is a Schcom milestone, not a hybrid one, and it is recorded that
|
||||||
|
way rather than counted here.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## The mechanisms
|
## The mechanisms
|
||||||
|
|
||||||
Four, and only the fourth is large.
|
Four, and only the fourth is large.
|
||||||
|
|
@ -279,13 +352,13 @@ start because the display driver is a process that needs a compositor.
|
||||||
|
|
||||||
## Milestones
|
## Milestones
|
||||||
|
|
||||||
| | |
|
| | | |
|
||||||
|---|---|
|
|---|---|---|
|
||||||
| **H0** | Mechanisms 2, 3 and 4, each with a test that exercises it from a userspace program against a device nothing else is using. |
|
| **H0** | Mechanisms 2, 3 and 4, each with a test that exercises it from a userspace program against a device nothing else is using. | done |
|
||||||
| **H1** | A userspace virtio-gpu driver, publishing `display_ops` across the channel. Schcom talks to it and does not know the difference. |
|
| **H1** | A userspace virtio-gpu driver, publishing `display_ops` across the channel. Schcom talks to it and does not know the difference. | done, except that Schcom does not talk to it -- `cmd/vgpud` holds the device and draws its own frame |
|
||||||
| **H2** | Kill the driver process on purpose. The screen falls back to VBE, the driver restarts, the compositor reattaches. **This is the milestone that proves the whole idea**, and it is the one that cannot be skipped. |
|
| **H2** | Kill the driver process on purpose. The screen falls back to VBE, the driver restarts, the compositor reattaches. **This is the milestone that proves the whole idea**, and it is the one that cannot be skipped. | done, less the compositor -- see "The driver came back" |
|
||||||
| **H3** | A modeset API, which is the thing an out-of-kernel GPU driver makes affordable and an in-kernel one never got. |
|
| **H3** | A modeset API, which is the thing an out-of-kernel GPU driver makes affordable and an in-kernel one never got. | |
|
||||||
| **H4** | USB, same shape. |
|
| **H4** | USB, same shape. | |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,14 @@ key = value; # starts a comment, keys are case-insensitive.
|
||||||
Path a unix socket schinit will hold on the job's behalf and
|
Path a unix socket schinit will hold on the job's behalf and
|
||||||
start the job on first connect
|
start the job on first connect
|
||||||
|
|
||||||
|
A KeepAlive job that exits with status 78 (sysexits.h's EX_CONFIG) is not
|
||||||
|
restarted, and schinit says which job stopped and that it stopped on purpose.
|
||||||
|
This is for a program that has looked at the machine and found nothing here it
|
||||||
|
can run against -- a driver whose device is absent, say -- which would otherwise
|
||||||
|
report the same thing every ThrottleInterval for the rest of the uptime. A job
|
||||||
|
that was killed or crashed carries a signal in its status, never matches, and
|
||||||
|
comes back exactly as KeepAlive promises.
|
||||||
|
|
||||||
Socket sections are parsed but not yet acted on -- that is milestone 2, and
|
Socket sections are parsed but not yet acted on -- that is milestone 2, and
|
||||||
demand launching is the whole reason this is shaped after launchd rather than
|
demand launching is the whole reason this is shaped after launchd rather than
|
||||||
sysvinit. The key exists now so manifests do not have to change later.
|
sysvinit. The key exists now so manifests do not have to change later.
|
||||||
|
|
|
||||||
31
etc/schinit/vgpud.job
Normal file
31
etc/schinit/vgpud.job
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
# The virtio-gpu driver, as an ordinary process.
|
||||||
|
#
|
||||||
|
# docs/plan/hybrid.md milestone H2. This manifest is the "the driver restarts"
|
||||||
|
# half of it: KeepAlive means schinit brings vgpud back when it dies, and
|
||||||
|
# because PID 1 forks every job, a driver that crashes is reaped and replaced by
|
||||||
|
# the same machinery that looks after a shell. Nothing about driver recovery is
|
||||||
|
# special -- that is the point.
|
||||||
|
#
|
||||||
|
# Enabled unconditionally, and safe to be, on two machines it is not written
|
||||||
|
# for: one with no virtio-gpu at all, and one where the GPU is the only display
|
||||||
|
# and the kernel therefore refuses to hand it over (virtio_gpu.c's release
|
||||||
|
# hook). In both cases vgpud says so and exits SCHINIT_EXIT_NORESTART, which
|
||||||
|
# stops KeepAlive from starting it again -- the alternative, measured, is
|
||||||
|
# fifteen spawns in forty-five seconds, each printing a line to the console for
|
||||||
|
# the rest of the uptime. No per-machine configuration either way.
|
||||||
|
|
||||||
|
Label = os.schos.vgpud
|
||||||
|
Program = /bin/vgpud
|
||||||
|
Args = vgpud
|
||||||
|
RunAtLoad = yes
|
||||||
|
KeepAlive = yes
|
||||||
|
|
||||||
|
# Higher than a shell's second, because a driver that cannot claim its device
|
||||||
|
# will fail identically every time and there is no user waiting on it. Low
|
||||||
|
# enough that a kill is answered while you are still looking at the screen.
|
||||||
|
ThrottleInterval = 3
|
||||||
|
|
||||||
|
# It has no terminal and wants none: a driver that seized the console would be
|
||||||
|
# doing exactly what moving it out of the kernel was meant to stop.
|
||||||
|
StandardOut = /dev/tty1
|
||||||
|
StandardError = /dev/tty1
|
||||||
|
|
@ -172,6 +172,7 @@ uint16 pci_read16(uint8, uint8, uint8, uint8);
|
||||||
uint8 pci_read8(uint8, uint8, uint8, uint8);
|
uint8 pci_read8(uint8, uint8, uint8, uint8);
|
||||||
void pci_write32(uint8, uint8, uint8, uint8, uint32);
|
void pci_write32(uint8, uint8, uint8, uint8, uint32);
|
||||||
void pci_enable(struct pci_dev *, uint16 bits);
|
void pci_enable(struct pci_dev *, uint16 bits);
|
||||||
|
void pci_disable(struct pci_dev *, uint16 bits);
|
||||||
uint8 pci_find_cap(struct pci_dev *, uint8 id);
|
uint8 pci_find_cap(struct pci_dev *, uint8 id);
|
||||||
int pci_msi_enable(struct pci_dev *, int vector, int cpu);
|
int pci_msi_enable(struct pci_dev *, int vector, int cpu);
|
||||||
int pci_count(void);
|
int pci_count(void);
|
||||||
|
|
@ -611,7 +612,7 @@ void platform_register_devices(uint64 fb_addr);
|
||||||
// from registry.c; everything else draws through include/gfx.h.
|
// from registry.c; everything else draws through include/gfx.h.
|
||||||
void vga_fb_init(uint64 phys_addr);
|
void vga_fb_init(uint64 phys_addr);
|
||||||
uint64 vga_fb_phys(void);
|
uint64 vga_fb_phys(void);
|
||||||
void vga_fb_restore(void);
|
int vga_fb_restore(void); // 0 = there is no fallback
|
||||||
uint64 vga_fb_size(void);
|
uint64 vga_fb_size(void);
|
||||||
|
|
||||||
void e820_init(void);
|
void e820_init(void);
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,26 @@
|
||||||
#define SCHINIT_EXITED 130 // struct schinit_reply -- a child has been reaped
|
#define SCHINIT_EXITED 130 // struct schinit_reply -- a child has been reaped
|
||||||
#define SCHINIT_PING 131 // struct schinit_hello -- answer within the deadline
|
#define SCHINIT_PING 131 // struct schinit_hello -- answer within the deadline
|
||||||
|
|
||||||
|
// Exit statuses a job can use to tell the supervisor something.
|
||||||
|
//
|
||||||
|
// KeepAlive restarts a job that crashed or was killed, which is the whole
|
||||||
|
// point of it for a driver. It must not restart one that has looked at the
|
||||||
|
// machine and reported there is nothing here for it to do: a driver whose
|
||||||
|
// device is absent fails identically every time, and a supervisor that tries
|
||||||
|
// anyway spends the rest of the uptime doing so -- 15 spawns in 45 seconds,
|
||||||
|
// on hardware with no virtio-gpu, each one printing a line to the console.
|
||||||
|
//
|
||||||
|
// 78 is sysexits.h's EX_CONFIG, "something was found in an unconfigured or
|
||||||
|
// misconfigured state", which is exactly what a driver with no device has
|
||||||
|
// found. Borrowed rather than invented, so a program written against any
|
||||||
|
// other Unix already means the right thing by it.
|
||||||
|
//
|
||||||
|
// Note what this is *not*: it is not a new manifest key and not a new policy
|
||||||
|
// knob. The wait status has always travelled from PID 1 to the supervisor in
|
||||||
|
// struct schinit_reply, and note_exit() has always thrown it away. This is
|
||||||
|
// the other half of a thing that was already built.
|
||||||
|
#define SCHINIT_EXIT_NORESTART 78
|
||||||
|
|
||||||
// The heartbeat.
|
// The heartbeat.
|
||||||
//
|
//
|
||||||
// docs/architecture/init.md, "The heartbeat" -- the failure the two-init scheme
|
// docs/architecture/init.md, "The heartbeat" -- the failure the two-init scheme
|
||||||
|
|
|
||||||
|
|
@ -77,6 +77,24 @@ pci_enable(struct pci_dev *d, uint16 bits)
|
||||||
pci_write32(d->bus, d->slot, d->func, PCI_COMMAND, cmd | bits);
|
pci_write32(d->bus, d->slot, d->func, PCI_COMMAND, cmd | bits);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The other direction, and PCI_CMD_BUS_MASTER is the reason it exists.
|
||||||
|
//
|
||||||
|
// Clearing that bit is the only way this kernel can stop a device it does not
|
||||||
|
// understand from touching memory. Everything else about halting a device is
|
||||||
|
// device-specific -- a different register on every chip -- but bus mastering is
|
||||||
|
// in the standard config header of every PCI device ever made, and clearing it
|
||||||
|
// means the next DMA cycle does not happen.
|
||||||
|
//
|
||||||
|
// That makes it the correct thing to do when a userspace driver dies: the
|
||||||
|
// kernel is about to take its DMA buffers back, and a device still scanning out
|
||||||
|
// of them would read whatever is put there next. See kernel/dev/pciuser.c.
|
||||||
|
void
|
||||||
|
pci_disable(struct pci_dev *d, uint16 bits)
|
||||||
|
{
|
||||||
|
uint32 cmd = pci_read32(d->bus, d->slot, d->func, PCI_COMMAND);
|
||||||
|
pci_write32(d->bus, d->slot, d->func, PCI_COMMAND, cmd & ~(uint32)bits);
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Capabilities
|
// Capabilities
|
||||||
//
|
//
|
||||||
|
|
|
||||||
|
|
@ -131,16 +131,49 @@ pciuser_mmio_ok(uint64 phys, uint64 len)
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Stop a device dead, without knowing anything about it.
|
||||||
|
//
|
||||||
|
// Clearing PCI_CMD_BUS_MASTER is the one halt available to a kernel that does
|
||||||
|
// not know what the device is: bus mastering lives in the standard config
|
||||||
|
// header of every PCI device, and with it clear the device cannot start another
|
||||||
|
// DMA cycle. Everything else -- quiescing a queue, parking a scanout, waiting
|
||||||
|
// for in-flight work -- is device-specific, which is exactly the knowledge that
|
||||||
|
// was moved out of the kernel on purpose.
|
||||||
|
//
|
||||||
|
// **This has to happen before the DMA buffers go back to the arena.** A driver
|
||||||
|
// that died left its device configured and running: virtio-gpu with a scanout
|
||||||
|
// set keeps reading its framebuffer for ever, and that framebuffer is about to
|
||||||
|
// be handed to whoever asks next. The failure is not a crash, which is why it
|
||||||
|
// is worth writing down -- it is a display showing another program's memory, or
|
||||||
|
// a NIC writing packets into a buffer a different driver is using.
|
||||||
|
static void
|
||||||
|
pciuser_stop(int idx)
|
||||||
|
{
|
||||||
|
const struct pci_dev *d = pci_get(idx);
|
||||||
|
if (d)
|
||||||
|
pci_disable((struct pci_dev *)d, PCI_CMD_BUS_MASTER);
|
||||||
|
}
|
||||||
|
|
||||||
// Release everything a process held. Called when it exits, because a driver
|
// Release everything a process held. Called when it exits, because a driver
|
||||||
// that crashed must not keep its device locked away from its replacement --
|
// that crashed must not keep its device locked away from its replacement --
|
||||||
// that is the entire point of running drivers in processes.
|
// that is the entire point of running drivers in processes.
|
||||||
void
|
void
|
||||||
pciuser_release_all(struct proc *p)
|
pciuser_release_all(struct proc *p)
|
||||||
{
|
{
|
||||||
acquire(&pciuser_lock);
|
// Devices first, and outside the lock, because stopping one is a config-space
|
||||||
for (int i = 0; i < NPCI; i++)
|
// write and the ordering against the DMA free below is the whole point.
|
||||||
if (owner[i] == p)
|
for (int i = 0; i < NPCI; i++) {
|
||||||
|
int mine;
|
||||||
|
acquire(&pciuser_lock);
|
||||||
|
mine = (owner[i] == p);
|
||||||
|
if (mine)
|
||||||
owner[i] = 0;
|
owner[i] = 0;
|
||||||
|
release(&pciuser_lock);
|
||||||
|
if (mine)
|
||||||
|
pciuser_stop(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
acquire(&pciuser_lock);
|
||||||
|
|
||||||
// And its interrupt line -- unregistered, and left MASKED.
|
// And its interrupt line -- unregistered, and left MASKED.
|
||||||
//
|
//
|
||||||
|
|
@ -277,7 +310,12 @@ do_claim(uint64 argp)
|
||||||
// Bus mastering and memory space, because a driver that has to ask for this
|
// Bus mastering and memory space, because a driver that has to ask for this
|
||||||
// separately will forget, and the symptom -- DMA that silently does nothing
|
// separately will forget, and the symptom -- DMA that silently does nothing
|
||||||
// -- costs a day to find.
|
// -- costs a day to find.
|
||||||
pci_enable((struct pci_dev *)d, 0x0006);
|
//
|
||||||
|
// This is also what makes a device usable again after the driver holding it
|
||||||
|
// died: release clears bus mastering to stop the device, so claim has to put
|
||||||
|
// it back. The two are a pair, and the second driver to run is the one that
|
||||||
|
// finds out if they are not.
|
||||||
|
pci_enable((struct pci_dev *)d, PCI_CMD_MEMORY | PCI_CMD_BUS_MASTER);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -291,9 +329,17 @@ do_release(uint64 argp)
|
||||||
return -ENODEV;
|
return -ENODEV;
|
||||||
|
|
||||||
acquire(&pciuser_lock);
|
acquire(&pciuser_lock);
|
||||||
if (owner[idx] == myproc())
|
int mine = (owner[idx] == myproc());
|
||||||
|
if (mine)
|
||||||
owner[idx] = 0;
|
owner[idx] = 0;
|
||||||
release(&pciuser_lock);
|
release(&pciuser_lock);
|
||||||
|
|
||||||
|
// Same halt as the death path, for the same reason. A driver that releases
|
||||||
|
// politely has usually quiesced its device already, but "usually" is not a
|
||||||
|
// property the kernel can check, and the cost of being sure is one config
|
||||||
|
// write.
|
||||||
|
if (mine)
|
||||||
|
pciuser_stop(idx);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -314,33 +360,63 @@ do_dma(uint64 argp)
|
||||||
if (size == 0 || size > 4 * 1024 * 1024)
|
if (size == 0 || size > 4 * 1024 * 1024)
|
||||||
return -EINVAL;
|
return -EINVAL;
|
||||||
|
|
||||||
// A slot to remember it by, found before allocating: handing out memory that
|
struct proc *p = myproc();
|
||||||
// cannot be recorded is how the leak this fixes got written in the first
|
|
||||||
// place.
|
// Two slots, both found before anything is allocated: one to remember the
|
||||||
|
// block by, one to remember the mapping by. Handing out memory that cannot
|
||||||
|
// be recorded is how the leak this fixes got written in the first place.
|
||||||
|
//
|
||||||
|
// The VMA is not bookkeeping. It is the thing that keeps this memory out of
|
||||||
|
// kfree()'s hands. freevm() frees every mapped page below PHYSTOP, and the
|
||||||
|
// DMA arena is the top 16 MB of exactly that range -- so a DMA block left
|
||||||
|
// for freevm() to find goes onto the page allocator's free list when the
|
||||||
|
// process dies, while pciuser_release_all() hands the same pages back to
|
||||||
|
// the arena. Both allocators then believe they own it.
|
||||||
|
//
|
||||||
|
// That is what broke H2. The restarted vgpud asked for a 4 MB framebuffer,
|
||||||
|
// dma_alloc() returned pages kalloc() had already given to somebody --
|
||||||
|
// including the page tables of the process doing the asking -- and zeroed
|
||||||
|
// them, so the driver died inside this ioctl, on the return from a syscall
|
||||||
|
// whose stack had just stopped existing.
|
||||||
|
//
|
||||||
|
// no_free says these pages are not freevm()'s to release;
|
||||||
|
// proc_release_shared() unmaps the range before the page tables go away.
|
||||||
struct dmablk *blk = 0;
|
struct dmablk *blk = 0;
|
||||||
acquire(&pciuser_lock);
|
acquire(&pciuser_lock);
|
||||||
for (int i = 0; i < PCIUSER_MAXDMA; i++)
|
for (int i = 0; i < PCIUSER_MAXDMA; i++)
|
||||||
if (!dmablk[i].proc) { blk = &dmablk[i]; blk->proc = myproc(); break; }
|
if (!dmablk[i].proc) { blk = &dmablk[i]; blk->proc = p; break; }
|
||||||
release(&pciuser_lock);
|
release(&pciuser_lock);
|
||||||
if (!blk)
|
if (!blk)
|
||||||
return -ENOMEM;
|
return -ENOMEM;
|
||||||
|
|
||||||
void *kva = dma_alloc(size, PGSIZE);
|
struct vma *v = 0;
|
||||||
if (!kva) {
|
for (int i = 0; i < NVMA; i++)
|
||||||
|
if (!p->vma[i].used) { v = &p->vma[i]; break; }
|
||||||
|
if (!v) {
|
||||||
blk->proc = 0;
|
blk->proc = 0;
|
||||||
return -ENOMEM;
|
return -ENOMEM;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Its own mapping in the caller, taken from the same bump allocator mmap
|
// Its own mapping in the caller, taken from the same bump allocator mmap
|
||||||
// uses so the two cannot collide.
|
// uses so the two cannot collide.
|
||||||
struct proc *p = myproc();
|
|
||||||
if (p->mmap_next == 0)
|
if (p->mmap_next == 0)
|
||||||
p->mmap_next = 0x40000000;
|
p->mmap_next = 0x40000000;
|
||||||
uint64 va = p->mmap_next;
|
uint64 va = p->mmap_next;
|
||||||
|
if (va + size < va || va + size > KERNBASE) {
|
||||||
|
blk->proc = 0;
|
||||||
|
return -ENOMEM;
|
||||||
|
}
|
||||||
|
|
||||||
|
void *kva = dma_alloc(size, PGSIZE);
|
||||||
|
if (!kva) {
|
||||||
|
blk->proc = 0;
|
||||||
|
return -ENOMEM;
|
||||||
|
}
|
||||||
p->mmap_next += size;
|
p->mmap_next += size;
|
||||||
|
|
||||||
uint64 phys = dma_phys(kva);
|
uint64 phys = dma_phys(kva);
|
||||||
if (mappages(p->pgdir, (void *)va, size, phys, PTE_W | PTE_U) < 0) {
|
if (mappages(p->pgdir, (void *)va, size, phys, PTE_W | PTE_U) < 0) {
|
||||||
|
vm_unmap_norelease(p->pgdir, va, size); // whatever part of it took
|
||||||
dma_free(kva, size);
|
dma_free(kva, size);
|
||||||
blk->proc = 0;
|
blk->proc = 0;
|
||||||
return -ENOMEM;
|
return -ENOMEM;
|
||||||
|
|
@ -348,6 +424,12 @@ do_dma(uint64 argp)
|
||||||
blk->kva = kva;
|
blk->kva = kva;
|
||||||
blk->size = size;
|
blk->size = size;
|
||||||
|
|
||||||
|
v->addr = va;
|
||||||
|
v->length = size;
|
||||||
|
v->used = 1;
|
||||||
|
v->no_free = 1;
|
||||||
|
v->shmid = -1;
|
||||||
|
|
||||||
req->uaddr = va;
|
req->uaddr = va;
|
||||||
req->paddr = phys;
|
req->paddr = phys;
|
||||||
return 0;
|
return 0;
|
||||||
|
|
@ -422,11 +504,25 @@ do_cfg(uint64 argp)
|
||||||
unsigned int idx = c->index;
|
unsigned int idx = c->index;
|
||||||
if (idx >= (unsigned int)pci_count() || idx >= NPCI)
|
if (idx >= (unsigned int)pci_count() || idx >= NPCI)
|
||||||
return -ENODEV;
|
return -ENODEV;
|
||||||
if (owner[idx] != myproc())
|
|
||||||
return -EACCES;
|
|
||||||
if (c->offset > 0xFC)
|
if (c->offset > 0xFC)
|
||||||
return -EINVAL;
|
return -EINVAL;
|
||||||
|
|
||||||
|
// The standard header -- the first 64 bytes -- is readable without holding
|
||||||
|
// the device. Refusing it was inconsistent rather than careful: PCIIOC_INFO
|
||||||
|
// already hands back the vendor, device, class, interrupt line and every BAR,
|
||||||
|
// all of which are read out of exactly these bytes. Denying the same bytes in
|
||||||
|
// a different shape protects nothing and makes the bus unenumerable by
|
||||||
|
// anything that is not already driving something.
|
||||||
|
//
|
||||||
|
// Past 0x40 is the capability list and whatever the device puts after it.
|
||||||
|
// That is device-specific, it is what a driver reads to find its own
|
||||||
|
// registers, and it belongs to whoever holds the device.
|
||||||
|
//
|
||||||
|
// Reads only. There is no config *write* here at all, and that is the line
|
||||||
|
// that matters -- a write is how you move a BAR out from under the kernel.
|
||||||
|
if (c->offset >= 0x40 && owner[idx] != myproc())
|
||||||
|
return -EACCES;
|
||||||
|
|
||||||
const struct pci_dev *d = pci_get((int)idx);
|
const struct pci_dev *d = pci_get((int)idx);
|
||||||
if (!d)
|
if (!d)
|
||||||
return -ENODEV;
|
return -ENODEV;
|
||||||
|
|
|
||||||
|
|
@ -225,13 +225,19 @@ const struct display_ops vga_display_ops = {
|
||||||
// This is the fallback rule from docs/plan/hybrid.md made real: the kernel
|
// This is the fallback rule from docs/plan/hybrid.md made real: the kernel
|
||||||
// keeps a minimal driver for everything on the path to a shell, so a fancy
|
// keeps a minimal driver for everything on the path to a shell, so a fancy
|
||||||
// driver leaving degrades to a working screen instead of to no screen.
|
// driver leaving degrades to a working screen instead of to no screen.
|
||||||
void
|
int
|
||||||
vga_fb_restore(void)
|
vga_fb_restore(void)
|
||||||
{
|
{
|
||||||
struct fb_info info;
|
struct fb_info info;
|
||||||
|
|
||||||
|
// Zero means there is no framebuffer to go back to, and the caller has to
|
||||||
|
// care. It used to return void and say nothing here, which reads as "done"
|
||||||
|
// -- so on a machine whose only display is the one being handed over, the
|
||||||
|
// eviction went ahead and left the console drawing into a driver that had
|
||||||
|
// just freed its memory. The answer is not to try harder; it is to say no,
|
||||||
|
// which is what the caller now does.
|
||||||
if (!fb || !fb_width || !fb_height)
|
if (!fb || !fb_width || !fb_height)
|
||||||
return; // nothing was ever set up; nothing to go back to
|
return 0;
|
||||||
|
|
||||||
info.ptr = fb;
|
info.ptr = fb;
|
||||||
info.phys = fb_phys;
|
info.phys = fb_phys;
|
||||||
|
|
@ -241,4 +247,5 @@ vga_fb_restore(void)
|
||||||
info.width = fb_width;
|
info.width = fb_width;
|
||||||
info.height = fb_height;
|
info.height = fb_height;
|
||||||
gfx_attach_scanout(&info);
|
gfx_attach_scanout(&info);
|
||||||
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -537,7 +537,21 @@ vgpu_release(struct pci_dev *d)
|
||||||
if (!g)
|
if (!g)
|
||||||
return -1;
|
return -1;
|
||||||
|
|
||||||
vga_fb_restore();
|
// The rule from docs/plan/hybrid.md, enforced rather than described: **the
|
||||||
|
// kernel keeps a minimal driver for everything on the path to a shell.** If
|
||||||
|
// there is no framebuffer to fall back to, this device is the only screen the
|
||||||
|
// machine has, and handing it to a process that might crash would make a
|
||||||
|
// failed driver indistinguishable from a dead machine.
|
||||||
|
//
|
||||||
|
// Refusing here is what lets the userspace driver be started unconditionally
|
||||||
|
// at boot: on hardware where it is safe it takes the GPU, and on hardware
|
||||||
|
// where it is not it is told no and says so, rather than each machine needing
|
||||||
|
// to be configured for its own display topology.
|
||||||
|
if (!vga_fb_restore()) {
|
||||||
|
cprintf("virtio-gpu: refusing to release -- it is the only display, and "
|
||||||
|
"there is no framebuffer to fall back to\n");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
// Status 0 is the virtio reset. The spec requires a driver to wait for the
|
// Status 0 is the virtio reset. The spec requires a driver to wait for the
|
||||||
// device to acknowledge by reading the register back as zero.
|
// device to acknowledge by reading the register back as zero.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue