A boot printed about thirty lines before reaching a prompt. Every one was
useful once, and printed on every boot for ever they train the person in front
of the machine to stop reading their own console -- which is expensive on the
day one of them changes.
SchOS UEFI loader
Press F8 for boot options... booting
Welcome to SchOS! (F8 at boot for the full log, or dmesg once you are in)
root@schos / »
**Nothing was deleted, it was routed.** consputc() already wrote to the klog
ring before anywhere else, so a line can be kept in full while being held back
from the two places a person looks. kinfo() is cprintf() with that routing, and
`dmesg` still has the whole boot with timestamps afterwards. The rule for which
is which is not how interesting a line is -- everything was interesting to
somebody once -- but whether you would want to see it on a machine that is
*working*. `pci: 7 devices` is yes the first time and no every time after;
`e1000: no DMA memory for the rings` is yes for ever and stays a cprintf.
The verbose boot the F8 menu already offered now means something much bigger,
which is the other half of this: it is the only way to see any of it live.
Userspace follows through /proc/bootmode, one word. schinit, schinitd and vgpud
hold back their progress lines and never their failures -- a supervisor that
goes quiet when a job dies is worse than one that chatters.
Two things found by testing rather than by reading:
- schinit read /proc/bootmode *before* bootstrap() mounted /proc, so it got
"quiet" every time and the verbose boot was quiet in the one process that
runs first. It reads after the mount now.
- resolvconf announced "already exists; -f to replace it" on every boot after
the first, which is a line about having done nothing. It is quiet when the
file already says what it would have written, and speaks only when the two
disagree -- which means the nameserver changed and the machine is using the
old one.
Verified both ways: quiet boot is six lines, a forced-verbose build prints all
thirty plus the `verbose boot:` steps, and /proc/kmsg holds every line in both.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2
170 lines
6.8 KiB
C
170 lines
6.8 KiB
C
// Find out how much memory this machine actually has.
|
|
//
|
|
// PHYSTOP used to be a constant: 0xE000000, 224 MB, inherited from xv6 and
|
|
// never revisited. That is not a limit on how much memory the kernel *uses* --
|
|
// it is an assertion that exactly that much exists. Boot a machine with less
|
|
// and kinit1() walks off the end of RAM into nothing; boot one with more and
|
|
// every byte above 224 MB is invisible. Measured before this file existed:
|
|
// SchOS booted at 224 MB and triple faulted at 192 MB, for no reason at all.
|
|
//
|
|
// The firmware has known the answer the whole time. boot/bios/stage2.S asks it with
|
|
// INT 0x15 / E820 in real mode -- the same interface Linux's boot stub and GRUB
|
|
// use -- and leaves the result in low memory. stage2main.c already parses it to
|
|
// print detected RAM on the splash. The kernel simply never read it.
|
|
|
|
#include "types.h"
|
|
#include "defs.h"
|
|
#include "memlayout.h"
|
|
#include "mmu.h"
|
|
#include "dma.h" // DMA_ARENA_BYTES: the top of PHYSTOP must be able to hold it
|
|
|
|
// Set before kinit1() and never again. Starts at the old constant so that a
|
|
// crash between entry and e820_init() behaves the way it always did rather
|
|
// than in some new way.
|
|
uint64 phystop = PHYSTOP_FALLBACK;
|
|
|
|
#include "schboot.h" /* the hand-off block */
|
|
|
|
#define E820_USABLE 1
|
|
#define E820_MAXENTRIES SCHBOOT_E820_MAX
|
|
|
|
// Every usable region, page aligned inwards and clipped to PHYSTOP_MAX.
|
|
// kinit1() frees these; see include/memlayout.h for why a list and not just a
|
|
// top address.
|
|
struct memregion mem_usable[MEMREGION_MAX];
|
|
int nmem_usable;
|
|
|
|
// The fallback path must still hand kalloc something, or the kernel comes up
|
|
// with an empty free list and dies somewhere far less obvious than here. One
|
|
// region spanning what the old constant asserted reproduces the previous
|
|
// behaviour exactly.
|
|
static void
|
|
assume_fallback(void)
|
|
{
|
|
mem_usable[0].base = EXTMEM;
|
|
mem_usable[0].end = PHYSTOP_FALLBACK;
|
|
nmem_usable = 1;
|
|
}
|
|
|
|
struct e820entry {
|
|
uint64 base;
|
|
uint64 length;
|
|
uint32 type;
|
|
uint32 acpi_ext;
|
|
} __attribute__((packed));
|
|
|
|
void
|
|
e820_init(void)
|
|
{
|
|
uint32 count = *(uint32 *)P2V(SCHBOOT_E820_COUNT);
|
|
struct e820entry *e = (struct e820entry *)P2V(SCHBOOT_E820_MAP);
|
|
|
|
// A count of zero means the firmware declined; anything absurd means we are
|
|
// reading something that is not a memory map. Either way the constant is the
|
|
// only honest answer left.
|
|
if (count == 0 || count > E820_MAXENTRIES) {
|
|
cprintf("e820: no usable map (count %d), assuming %d MB\n",
|
|
count, (int)(PHYSTOP_FALLBACK / (1024 * 1024)));
|
|
assume_fallback();
|
|
return;
|
|
}
|
|
|
|
// Collect every usable region rather than just the one containing the
|
|
// kernel.
|
|
//
|
|
// A memory map is not one run of RAM: there are reserved holes in it -- ACPI
|
|
// tables, firmware scratch, the legacy hole below 1 MB. The old code took the
|
|
// run containing the kernel and stopped, because kalloc walked a single range
|
|
// and taking the global maximum would have stepped over a reserved region and
|
|
// put firmware tables on the free list. That was the right answer for the
|
|
// wrong shape: under OVMF the kernel's run ends at the ACPI NVS region at
|
|
// 8 MB, and every byte above it was lost. kalloc now frees region by region,
|
|
// so the holes are skipped instead of being either freed or fatal.
|
|
uint64 largest = 0;
|
|
uint64 top = 0;
|
|
nmem_usable = 0;
|
|
for (uint32 i = 0; i < count; i++) {
|
|
if (e[i].type != E820_USABLE)
|
|
continue;
|
|
|
|
// Align inwards: a region is only usable where a whole page fits.
|
|
uint64 base = (e[i].base + PGSIZE - 1) & ~(uint64)(PGSIZE - 1);
|
|
uint64 end = (e[i].base + e[i].length) & ~(uint64)(PGSIZE - 1);
|
|
|
|
// Above PHYSTOP_MAX the direct map is device memory, not RAM (see below).
|
|
if (end > PHYSTOP_MAX)
|
|
end = PHYSTOP_MAX;
|
|
if (base >= end)
|
|
continue;
|
|
|
|
mem_usable[nmem_usable].base = base;
|
|
mem_usable[nmem_usable].end = end;
|
|
nmem_usable++;
|
|
|
|
// phystop comes from the largest region, not the highest. It has to be a
|
|
// region that can hold the DMA arena, which is carved off the top of
|
|
// PHYSTOP and must be physically contiguous; the largest is the only one
|
|
// that is certain to have room. A small usable region above it is left
|
|
// unfreed rather than raising the bound past a hole -- kfree() rejects
|
|
// anything at or above PHYSTOP, and that check is worth more than the
|
|
// stray page it costs.
|
|
if (end - base > largest) {
|
|
largest = end - base;
|
|
top = end;
|
|
}
|
|
}
|
|
|
|
if (nmem_usable == 0) {
|
|
cprintf("e820: no usable regions in %d entries, assuming %d MB\n",
|
|
count, (int)(PHYSTOP_FALLBACK / (1024 * 1024)));
|
|
assume_fallback();
|
|
return;
|
|
}
|
|
|
|
// The PHYSTOP_MAX ceiling is applied per region above, when each one is
|
|
// clipped. kvmalloc() maps physical 0-4 GB with four 1 GB pages and marks the
|
|
// top one uncached because that is where PCI BARs and the framebuffer live,
|
|
// so 3 GB is the ceiling for anything the kernel will treat as RAM: above it
|
|
// the direct map either does not exist or is device memory that would be
|
|
// catastrophically slow to allocate from and is not ours to hand out.
|
|
|
|
// Below this the kernel image, its page tables and the DMA arena do not fit,
|
|
// and continuing produces a failure much harder to read than this line.
|
|
if (top < PHYSTOP_MIN) {
|
|
cprintf("e820: only %d MB usable; SchOS needs at least %d MB\n",
|
|
(int)(top / (1024 * 1024)), (int)(PHYSTOP_MIN / (1024 * 1024)));
|
|
// Left at the fallback deliberately rather than lowered: a machine this
|
|
// small is going to fail, and it should fail having said why.
|
|
assume_fallback();
|
|
return;
|
|
}
|
|
|
|
// dma_init() carves [PHYSTOP - DMA_ARENA_BYTES, PHYSTOP) and needs it
|
|
// physically contiguous, so the region that set `top` must be able to hold
|
|
// it. Picking the largest region makes this true on any machine worth
|
|
// booting; checking says so out loud if it ever is not.
|
|
//
|
|
// The kernel log is *not* up here -- it sits just above the kernel image,
|
|
// where the firmware does not allocate. See klog_init()'s caller.
|
|
if (largest < DMA_ARENA_BYTES) {
|
|
cprintf("e820: largest usable region is %d MB, too small for the %d MB "
|
|
"DMA arena\n", (int)(largest / (1024 * 1024)),
|
|
(int)(DMA_ARENA_BYTES / (1024 * 1024)));
|
|
assume_fallback();
|
|
return;
|
|
}
|
|
|
|
phystop = top;
|
|
|
|
// Report what is actually usable, not phystop: with a fragmented map those
|
|
// are different numbers, and printing the top address as though it were the
|
|
// amount of RAM is how the 8 MB under OVMF went unnoticed for so long.
|
|
uint64 usable = 0;
|
|
for (int i = 0; i < nmem_usable; i++)
|
|
usable += mem_usable[i].end - mem_usable[i].base;
|
|
|
|
kinfo("e820: %d MB usable in %d region%s, top %d MB (%d entries)\n",
|
|
(int)(usable / (1024 * 1024)), nmem_usable,
|
|
nmem_usable == 1 ? "" : "s",
|
|
(int)(phystop / (1024 * 1024)), count);
|
|
}
|