Initial commit: Schunix as it stands before the SchOS rename

An x86-64 operating system built from xv6, now largely rewritten: SMP,
a copy-on-write root filesystem (SchFS), PCI/USB/virtio drivers, a TTF
rasteriser, and a Linux-numbered syscall ABI.

No history before this point -- the tree was never under version control.
This commit exists so the rename that follows has something to undo to.
This commit is contained in:
Nexoniarz 2026-08-09 13:47:01 +02:00
commit d0f7acf9eb
275 changed files with 51417 additions and 0 deletions

264
kernel/arch/x86_64/acpi.c Normal file
View file

@ -0,0 +1,264 @@
// Find the processors, the way firmware from this century describes them.
//
// mpinit() walks the Intel MP table, which is from 1994. Modern firmware fills
// it in minimally or omits it: measured 2026-08-08, QEMU/SeaBIOS with -smp 4
// publishes a 208-byte MP table containing exactly *one* processor entry, then
// buses and interrupt assignments. Everything since has expected the operating
// system to read the ACPI MADT instead.
//
// The consequence was not that SMP was slow, it was that ncpu was 1 and SMP was
// silently never exercised. Machines whose firmware does publish a full MP
// table -- VMware, older hardware -- took the other path, started their APs,
// and triple faulted. So this file is a prerequisite for debugging that: until
// the CPUs are visible under QEMU there is no way to reproduce the crash except
// on the hardware it happens on.
//
// Written from the ACPI specification. No table layout here is copied from
// another kernel; they are all in chapter 5 of the published standard.
#include "types.h"
#include "defs.h"
#include "param.h"
#include "memlayout.h"
#include "mmu.h"
#include "proc.h"
#include "x86.h"
extern struct cpu cpus[NCPU];
extern int ncpu;
extern uchar ioapicid;
extern volatile uint *lapic;
// The Root System Description Pointer. Found by signature, not by being told
// where it is -- it sits either in the Extended BIOS Data Area or in the BIOS
// read-only region, on a 16-byte boundary.
struct acpi_rsdp {
char sig[8]; // "RSD PTR "
uint8 checksum; // over the first 20 bytes
char oemid[6];
uint8 revision; // 0 = ACPI 1.0 (RSDT), 2+ = 2.0 (XSDT)
uint32 rsdt_addr;
// ACPI 2.0 and later only, and only valid when revision >= 2.
uint32 length;
uint64 xsdt_addr;
uint8 ext_checksum; // over `length` bytes
uint8 reserved[3];
} __attribute__((packed));
// Every ACPI table starts with this.
struct acpi_sdt {
char sig[4];
uint32 length;
uint8 revision;
uint8 checksum;
char oemid[6];
char oem_table_id[8];
uint32 oem_revision;
uint32 creator_id;
uint32 creator_revision;
} __attribute__((packed));
// The Multiple APIC Description Table: signature "APIC".
struct acpi_madt {
struct acpi_sdt h;
uint32 lapic_addr;
uint32 flags; // bit 0: legacy 8259 PICs are present
} __attribute__((packed));
#define MADT_PCAT_COMPAT 1
// MADT entry types. Only these three are read; the rest are interrupt
// routing overrides that ioapic.c does not consult yet.
#define MADT_LAPIC 0
#define MADT_IOAPIC 1
#define MADT_LAPIC_OVERRIDE 5
struct madt_entry {
uint8 type;
uint8 length;
} __attribute__((packed));
struct madt_lapic {
struct madt_entry e;
uint8 acpi_id;
uint8 apic_id;
uint32 flags; // bit 0: enabled, bit 1: online-capable
} __attribute__((packed));
#define LAPIC_ENABLED 1
#define LAPIC_ONLINE_CAPABLE 2
struct madt_ioapic {
struct madt_entry e;
uint8 id;
uint8 reserved;
uint32 addr;
uint32 gsi_base;
} __attribute__((packed));
struct madt_lapic_override {
struct madt_entry e;
uint16 reserved;
uint64 addr;
} __attribute__((packed));
static int
checksum_ok(const void *p, uint32 len)
{
const uint8 *b = p;
uint8 sum = 0;
for (uint32 i = 0; i < len; i++)
sum += b[i];
return sum == 0;
}
// Scan a physical range for the RSDP signature. It is 16-byte aligned by
// specification, which makes this cheap enough to do twice.
static struct acpi_rsdp *
scan_rsdp(uint64 base, uint64 len)
{
for (uint64 off = 0; off + sizeof(struct acpi_rsdp) <= len; off += 16) {
struct acpi_rsdp *r = (struct acpi_rsdp *)P2V(base + off);
if (memcmp(r->sig, "RSD PTR ", 8) != 0)
continue;
// Signature alone is not enough: the string can appear in unrelated data,
// and following a bad pointer here means faulting before there is anything
// to report the fault with.
if (!checksum_ok(r, 20))
continue;
return r;
}
return 0;
}
static struct acpi_rsdp *
find_rsdp(void)
{
// The EBDA, whose segment address the BIOS leaves at 0x40E.
uint16 ebda_seg = *(uint16 *)P2V(0x40E);
if (ebda_seg) {
struct acpi_rsdp *r = scan_rsdp((uint64)ebda_seg << 4, 1024);
if (r)
return r;
}
return scan_rsdp(0xE0000, 0x20000);
}
// Walk the RSDT or XSDT looking for a table by signature.
//
// Two nearly identical loops rather than one clever one: the only difference is
// the pointer width -- 32-bit entries in the RSDT, 64-bit in the XSDT -- and
// reading a 64-bit pointer out of a 32-bit array is exactly the sort of bug
// that produces a plausible address pointing at nothing.
static struct acpi_sdt *
find_table(struct acpi_rsdp *rsdp, const char *sig)
{
struct acpi_sdt *root;
int wide;
if (rsdp->revision >= 2 && rsdp->xsdt_addr) {
root = (struct acpi_sdt *)P2V(rsdp->xsdt_addr);
wide = 1;
} else {
root = (struct acpi_sdt *)P2V((uint64)rsdp->rsdt_addr);
wide = 0;
}
if (!checksum_ok(root, root->length))
return 0;
uint32 n = (root->length - sizeof(struct acpi_sdt)) / (wide ? 8 : 4);
for (uint32 i = 0; i < n; i++) {
uint64 pa;
if (wide) {
// Byte-wise, because the array is packed and 8-byte entries in it are
// not guaranteed to be 8-byte aligned.
uint8 *p = (uint8 *)(root + 1) + i * 8;
pa = 0;
for (int b = 7; b >= 0; b--)
pa = (pa << 8) | p[b];
} else {
pa = ((uint32 *)(root + 1))[i];
}
struct acpi_sdt *t = (struct acpi_sdt *)P2V(pa);
if (memcmp(t->sig, sig, 4) == 0)
return t;
}
return 0;
}
// Returns 1 if the processors were enumerated, 0 to fall back to mpinit().
int
acpi_init(void)
{
struct acpi_rsdp *rsdp = find_rsdp();
if (!rsdp)
return 0;
struct acpi_madt *madt = (struct acpi_madt *)find_table(rsdp, "APIC");
if (!madt || !checksum_ok(madt, madt->h.length))
return 0;
lapic = (uint *)P2V((uint64)madt->lapic_addr);
uint8 *p = (uint8 *)madt + sizeof(struct acpi_madt);
uint8 *e = (uint8 *)madt + madt->h.length;
int found_ioapic = 0;
while (p + sizeof(struct madt_entry) <= e) {
struct madt_entry *ent = (struct madt_entry *)p;
// A zero length would spin here forever. Firmware tables are not always
// well formed, and an infinite loop this early is indistinguishable from a
// hang with no output at all.
if (ent->length < sizeof(struct madt_entry))
break;
switch (ent->type) {
case MADT_LAPIC: {
struct madt_lapic *l = (struct madt_lapic *)p;
// Skip processors that are neither enabled nor online-capable: those are
// sockets the firmware is describing but not offering.
if (!(l->flags & (LAPIC_ENABLED | LAPIC_ONLINE_CAPABLE)))
break;
if (ncpu < NCPU) {
cpus[ncpu].apicid = l->apic_id;
ncpu++;
}
break;
}
case MADT_IOAPIC: {
struct madt_ioapic *io = (struct madt_ioapic *)p;
// The first one only. Multi-I/O-APIC routing needs the interrupt source
// overrides too, which nothing here reads yet, so claiming to support it
// would be worse than not.
if (!found_ioapic) {
ioapicid = io->id;
found_ioapic = 1;
}
break;
}
case MADT_LAPIC_OVERRIDE: {
struct madt_lapic_override *o = (struct madt_lapic_override *)p;
lapic = (uint *)P2V(o->addr);
break;
}
}
p += ent->length;
}
if (ncpu == 0)
return 0;
// The 8259s are still there and still wired to the CPU. On a system with an
// IMCR, this switches interrupt delivery from the PICs to the APIC; on one
// without, the ports are unclaimed and the writes go nowhere. mpinit() does
// the same thing guarded by the MP table's imcrp flag.
if (madt->flags & MADT_PCAT_COMPAT) {
outb(0x22, 0x70);
outb(0x23, inb(0x23) | 1);
}
cprintf("acpi: %d cpu%s, lapic at %p, ioapic id %d\n",
ncpu, ncpu == 1 ? "" : "s", lapic, ioapicid);
return 1;
}

105
kernel/arch/x86_64/e820.c Normal file
View file

@ -0,0 +1,105 @@
// 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:
// Schunix 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/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"
// 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;
// Where stage2 left the map. Must match E820_COUNT_ADDR / E820_MAP_ADDR in
// boot/stage2main.c, which are themselves relative to VBE_INFO_ADDR.
#define E820_COUNT_ADDR (0x30000 + 0x200)
#define E820_MAP_ADDR (0x30000 + 0x208)
#define E820_USABLE 1
#define E820_MAXENTRIES 32
struct e820entry {
uint64 base;
uint64 length;
uint32 type;
uint32 acpi_ext;
} __attribute__((packed));
void
e820_init(void)
{
uint32 count = *(uint32 *)P2V(E820_COUNT_ADDR);
struct e820entry *e = (struct e820entry *)P2V(E820_MAP_ADDR);
// 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)));
return;
}
// The end of the usable region that *contains the kernel*, not the highest
// usable address anywhere.
//
// This distinction matters and is easy to get wrong. 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 -- and kalloc's freerange() walks
// straight from the end of the kernel image to phystop handing out every page
// between. Taking the global maximum would step over a reserved region and
// put firmware tables on the free list, which fails later, elsewhere, and
// looks like anything but a memory map problem.
uint64 top = 0;
for (uint32 i = 0; i < count; i++) {
if (e[i].type != E820_USABLE)
continue;
uint64 base = e[i].base, end = base + e[i].length;
if (base <= EXTMEM && EXTMEM < end) {
top = end;
break;
}
}
if (top == 0) {
cprintf("e820: no usable region covers 0x%x, assuming %d MB\n",
EXTMEM, (int)(PHYSTOP_FALLBACK / (1024 * 1024)));
return;
}
// 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.
if (top > PHYSTOP_MAX)
top = PHYSTOP_MAX;
top &= ~(uint64)(PGSIZE - 1);
// 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; Schunix 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.
return;
}
phystop = top;
cprintf("e820: %d MB usable (%d entries)\n",
(int)(phystop / (1024 * 1024)), count);
}

174
kernel/arch/x86_64/entry.S Normal file
View file

@ -0,0 +1,174 @@
/* entry.S
*
* Copyright (c) 2013 Brian Swetland
* Modified by Nexoniarz
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*
*/
#define mboot_magic 0x1badb002
#define mboot_flags 0x00010000
#include "mmu.h"
.code64
.global mboot_header
.global mboot_entry
mboot_header:
.long mboot_magic
.long mboot_flags
.long (-mboot_magic -mboot_flags)
.long mboot_load_addr
.long mboot_load_addr
.long mboot_load_end
.long mboot_bss_end
.long mboot_entry_addr
.code32
mboot_entry:
mov %eax, %ebp
# bootstrap page tables (0x1000 - 0x3000)
xor %eax, %eax
mov $0x1000, %edi
mov $0x2000, %ecx
rep stosb
# Bootstrap page tables (Identity mapping 4GB)
# PML4T[0] i PML4T[256] -> PDPT pod 0x2000
mov $(0x2000 | PTE_P | PTE_W), %edx
mov %edx, 0x1000
mov %edx, 0x1800
# PDPT mapping 4GB using 1GB huge pages
mov $(0x00000000 | PTE_P | PTE_PS | PTE_W), %edx
mov %edx, 0x2000
mov $(0x40000000 | PTE_P | PTE_PS | PTE_W), %edx
mov %edx, 0x2008
mov $(0x80000000 | PTE_P | PTE_PS | PTE_W), %edx
mov %edx, 0x2010
mov $(0xC0000000 | PTE_P | PTE_PS | PTE_W), %edx
mov %edx, 0x2018
mov %ebp, %ebx
.global entry32mp
entry32mp:
mov $0x1000, %eax
mov %eax, %cr3
lgdt (gdtr64 - mboot_header + mboot_load_addr)
mov %cr4, %eax
bts $5, %eax
mov %eax, %cr4
mov $MSR_EFER, %ecx
rdmsr
bts $0, %eax
bts $8, %eax
wrmsr
mov %cr0, %eax
orl $(CR0_PG | CR0_WP | CR0_MP), %eax
mov %eax, %cr0
ljmp $8, $(entry64low - mboot_header + mboot_load_addr)
.align 16
gdtr64:
.word gdt64_end - gdt64_begin - 1;
.quad gdt64_begin - mboot_header + mboot_load_addr
.align 16
gdt64_begin:
.long 0x00000000
.long 0x00000000
.long 0x00000000 # 64-bit code
.long 0x00209800
.long 0x00000000 # 64-bit data
.long 0x00009000
gdt64_end:
.align 16
.code64
entry64low:
movabs $entry64high, %rax
jmp *%rax
.global _start
_start:
entry64high:
xor %rax, %rax
mov %ax, %ss
mov %ax, %ds
mov %ax, %es
mov %ax, %fs
mov %ax, %gs
# Is this an application processor, or the boot processor?
#
# This check used to be a comment saying it had not been written yet, and
# every AP fell straight through into the BSP path below: it set RSP to the
# *boot processor's* stack, passed its own flag to kmain() as a framebuffer
# address, and re-ran the entire bootstrap -- e820_init, kinit1, kvmalloc --
# on a second core sharing one stack with the first. That is the SMP triple
# fault. QEMU hid it for years by never reporting more than one CPU (see
# kernel/arch/x86_64/acpi.c); VMware did not.
#
# EBX is the discriminator, and entryother.S sets it to 1 immediately before
# entering long mode for exactly this purpose. It is safe: on the boot
# processor EBX carries the VESA framebuffer address from the loader, which is
# either zero or a physical MMIO address, and can never be 1.
cmp $1, %ebx
je entry64mp
# Setup initial stack
movabs $0xFFFF800000010000, %rax
mov %rax, %rsp
# Pass VESA address to main (1st argument in RDI)
mov %rbx, %rdi
call kmain
.global __deadloop
__deadloop:
jmp .
entry64mp:
mov $0x7000, %rax
mov -16(%rax), %rsp
jmp mpenter
.global wrmsr
wrmsr:
mov %rdi, %rcx
mov %rsi, %rax
shr $32, %rsi
mov %rsi, %rdx
wrmsr
retq
.global ignore_sysret
ignore_sysret:
mov $-38, %rax
sysretq

View file

@ -0,0 +1,78 @@
#include "asm.h"
#include "memlayout.h"
#include "mmu.h"
# Each non-boot CPU ("AP") is started up in response to a STARTUP
# IPI from the boot CPU. Section B.4.2 of the Multi-Processor
# Specification says that the AP will start in real mode with CS:IP
# set to XY00:0000, where XY is an 8-bit value sent with the
# STARTUP. Thus this code must start at a 4096-byte boundary.
#
# Because this code sets DS to zero, it must sit
# at an address in the low 2^16 bytes.
#
# Startothers (in main.c) sends the STARTUPs one at a time.
# It copies this code (start) at 0x7000. It puts the address of
# a newly allocated per-core stack in start-4,the address of the
# place to jump to (mpenter) in start-8, and the physical address
# of entrypgdir in start-12.
#
# This code is identical to bootasm.S except:
# - it does not need to enable A20
# - it uses the address at start-4, start-8, and start-12
.code16
.global start
start:
cli
xorw %ax,%ax
movw %ax,%ds
movw %ax,%es
movw %ax,%ss
lgdt gdtdesc
movl %cr0, %eax
orl $CR0_PE, %eax
movl %eax, %cr0
ljmpl $(SEG_KCODE<<3), $(start32)
//PAGEBREAK!
.code32
start32:
movw $(SEG_KDATA<<3), %ax
movw %ax, %ds
movw %ax, %es
movw %ax, %ss
movw $0, %ax
movw %ax, %fs
movw %ax, %gs
# defer paging until we switch to 64bit mode
# set ebx=1 so shared boot code knows we're booting a secondary core
mov $1, %ebx
# Switch to the stack allocated by startothers()
movl (start-4), %esp
# Call mpenter()
call *(start-8)
movw $0x8a00, %ax
movw %ax, %dx
outw %ax, %dx
movw $0x8ae0, %ax
outw %ax, %dx
spin:
jmp spin
.p2align 2
gdt:
SEG_NULLASM
SEG_ASM(STA_X|STA_R, 0, 0xffffffff)
SEG_ASM(STA_W, 0, 0xffffffff)
gdtdesc:
.word (gdtdesc - gdt - 1)
.long gdt

102
kernel/arch/x86_64/ioapic.c Normal file
View file

@ -0,0 +1,102 @@
// The I/O APIC manages hardware interrupts for an SMP system.
// http://www.intel.com/design/chipsets/datashts/29056601.pdf
// See also picirq.c.
#include "types.h"
#include "defs.h"
#include "traps.h"
#include "memlayout.h"
#define IOAPIC 0xFEC00000 // Default physical address of IO APIC
#define REG_ID 0x00 // Register index: ID
#define REG_VER 0x01 // Register index: version
#define REG_TABLE 0x10 // Redirection table base
// The redirection table starts at REG_TABLE and uses
// two registers to configure each interrupt.
// The first (low) register in a pair contains configuration bits.
// The second (high) register contains a bitmask telling which
// CPUs can serve that interrupt.
#define INT_DISABLED 0x00010000 // Interrupt disabled
#define INT_LEVEL 0x00008000 // Level-triggered (vs edge-)
#define INT_ACTIVELOW 0x00002000 // Active low (vs high)
#define INT_LOGICAL 0x00000800 // Destination is CPU id (vs APIC ID)
volatile struct ioapic *ioapic;
// IO APIC MMIO structure: write reg, then read or write data.
struct ioapic {
uint reg;
uint pad[3];
uint data;
};
static uint
ioapicread(int reg)
{
ioapic->reg = reg;
return ioapic->data;
}
static void
ioapicwrite(int reg, uint data)
{
ioapic->reg = reg;
ioapic->data = data;
}
void
ioapicinit(void)
{
int i, id, maxintr;
ioapic = P2V((volatile struct ioapic*)IOAPIC);
maxintr = (ioapicread(REG_VER) >> 16) & 0xFF;
id = ioapicread(REG_ID) >> 24;
if(id != ioapicid)
cprintf("ioapicinit: id isn't equal to ioapicid; not a MP\n");
// Mark all interrupts edge-triggered, active high, disabled,
// and not routed to any CPUs.
for(i = 0; i <= maxintr; i++){
ioapicwrite(REG_TABLE+2*i, INT_DISABLED | (T_IRQ0 + i));
ioapicwrite(REG_TABLE+2*i+1, 0);
}
}
// Route an interrupt to a CPU and unmask it.
//
// `level_low` picks PCI's signalling instead of ISA's. This distinction did
// not matter while every interrupt in the kernel came from an ISA-era device
// at a fixed pin, all of which are edge-triggered and active high -- which is
// why the original ioapicenable() hardcoded exactly that. A PCI device wired
// through an INTx pin is the opposite on both counts, and configuring it as
// edge/high does not fail cleanly: the device raises the line, we take one
// interrupt, and because the level never goes away there is no second edge
// and the device is never heard from again.
void
ioapicroute(int irq, int cpunum, int level_low)
{
uint lo = T_IRQ0 + irq;
if(level_low)
lo |= INT_LEVEL | INT_ACTIVELOW;
// Program the destination before unmasking, so the entry is never briefly
// live pointing at CPU 0 when it was meant for another.
ioapicwrite(REG_TABLE+2*irq+1, cpunum << 24);
ioapicwrite(REG_TABLE+2*irq, lo);
}
void
ioapicmask(int irq)
{
ioapicwrite(REG_TABLE+2*irq, INT_DISABLED | (T_IRQ0 + irq));
}
void
ioapicenable(int irq, int cpunum)
{
ioapicroute(irq, cpunum, 0);
}

245
kernel/arch/x86_64/lapic.c Normal file
View file

@ -0,0 +1,245 @@
// The local APIC manages internal (non-I/O) interrupts.
// See Chapter 8 & Appendix C of Intel processor manual volume 3.
#include "param.h"
#include "types.h"
#include "defs.h"
#include "date.h"
#include "memlayout.h"
#include "traps.h"
#include "mmu.h"
#include "x86.h"
#include "proc.h" // ncpu
// Local APIC registers, divided by 4 for use as uint[] indices.
#define ID (0x0020/4) // ID
#define VER (0x0030/4) // Version
#define TPR (0x0080/4) // Task Priority
#define EOI (0x00B0/4) // EOI
#define SVR (0x00F0/4) // Spurious Interrupt Vector
#define ENABLE 0x00000100 // Unit Enable
#define ESR (0x0280/4) // Error Status
#define ICRLO (0x0300/4) // Interrupt Command
#define SINIT 0x00000500 // INIT/RESET
#define STARTUP 0x00000600 // Startup IPI
#define DELIVS 0x00001000 // Delivery status
#define ASSERT 0x00004000 // Assert interrupt (vs deassert)
#define LEVEL 0x00008000 // Level triggered
#define BCAST 0x00080000 // Send to all APICs, including self.
#define BUSY 0x00001000
#define ICRHI (0x0310/4) // Interrupt Command [63:32]
#define TIMER (0x0320/4) // Local Vector Table 0 (TIMER)
#define X1 0x0000000B // divide counts by 1
#define PERIODIC 0x00020000 // Periodic
#define PCINT (0x0340/4) // Performance Counter LVT
#define LINT0 (0x0350/4) // Local Vector Table 1 (LINT0)
#define LINT1 (0x0360/4) // Local Vector Table 2 (LINT1)
#define ERROR (0x0370/4) // Local Vector Table 3 (ERROR)
#define MASKED 0x00010000 // Interrupt masked
#define TICR (0x0380/4) // Timer Initial Count
#define TCCR (0x0390/4) // Timer Current Count
#define TDCR (0x03E0/4) // Timer Divide Configuration
volatile uint *lapic; // Initialized in mp.c
static void
lapicw(int index, int value)
{
lapic[index] = value;
lapic[ID]; // wait for write to finish, by reading
}
void
lapicinit(void)
{
if(!lapic)
return;
// Enable local APIC; set spurious interrupt vector.
lapicw(SVR, ENABLE | (T_IRQ0 + IRQ_SPURIOUS));
// The timer repeatedly counts down at bus frequency
// from lapic[TICR] and then issues an interrupt.
// If xv6 cared more about precise timekeeping,
// TICR would be calibrated using an external time source.
lapicw(TDCR, X1);
lapicw(TIMER, PERIODIC | (T_IRQ0 + IRQ_TIMER));
lapicw(TICR, 10000000);
// Disable logical interrupt lines.
lapicw(LINT0, MASKED);
lapicw(LINT1, MASKED);
// Disable performance counter overflow interrupts
// on machines that provide that interrupt entry.
if(((lapic[VER]>>16) & 0xFF) >= 4)
lapicw(PCINT, MASKED);
// Map error interrupt to IRQ_ERROR.
lapicw(ERROR, T_IRQ0 + IRQ_ERROR);
// Clear error status register (requires back-to-back writes).
lapicw(ESR, 0);
lapicw(ESR, 0);
// Ack any outstanding interrupts.
lapicw(EOI, 0);
// Send an Init Level De-Assert to synchronise arbitration ID's.
lapicw(ICRHI, 0);
lapicw(ICRLO, BCAST | SINIT | LEVEL);
while(lapic[ICRLO] & DELIVS)
;
// Enable interrupts on the APIC (but not on the processor).
lapicw(TPR, 0);
}
int
cpunum(void)
{
int apicid, i;
// Cannot call cpu when interrupts are enabled:
// result not guaranteed to last long enough to be used!
// Would prefer to panic but even printing is chancy here:
// almost everything, including cprintf and panic, calls cpu,
// often indirectly through acquire and release.
if(readeflags()&FL_IF){
static int n;
if(n++ == 0)
cprintf("cpu called from %x with interrupts enabled\n",
__builtin_return_address(0));
}
if (!lapic)
return 0;
apicid = lapic[ID] >> 24;
for (i = 0; i < ncpu; ++i) {
if (cpus[i].apicid == apicid)
return i;
}
panic("unknown apicid\n");
}
// Acknowledge interrupt.
void
lapiceoi(void)
{
if(lapic)
lapicw(EOI, 0);
}
// Spin for a given number of microseconds.
// On real hardware would want to tune this dynamically.
void
microdelay(int us)
{
}
#define CMOS_PORT 0x70
#define CMOS_RETURN 0x71
// Start additional processor running entry code at addr.
// See Appendix B of MultiProcessor Specification.
void
lapicstartap(uchar apicid, uint addr)
{
int i;
ushort *wrv;
// "The BSP must initialize CMOS shutdown code to 0AH
// and the warm reset vector (DWORD based at 40:67) to point at
// the AP startup code prior to the [universal startup algorithm]."
outb(CMOS_PORT, 0xF); // offset 0xF is shutdown code
outb(CMOS_PORT+1, 0x0A);
wrv = (ushort*)P2V((0x40<<4 | 0x67)); // Warm reset vector
wrv[0] = 0;
wrv[1] = addr >> 4;
// "Universal startup algorithm."
// Send INIT (level-triggered) interrupt to reset other CPU.
lapicw(ICRHI, apicid<<24);
lapicw(ICRLO, SINIT | LEVEL | ASSERT);
microdelay(200);
lapicw(ICRLO, SINIT | LEVEL);
microdelay(100); // should be 10ms, but too slow in Bochs!
// Send startup IPI (twice!) to enter code.
// Regular hardware is supposed to only accept a STARTUP
// when it is in the halted state due to an INIT. So the second
// should be ignored, but it is part of the official Intel algorithm.
// Bochs complains about the second one. Too bad for Bochs.
for(i = 0; i < 2; i++){
lapicw(ICRHI, apicid<<24);
lapicw(ICRLO, STARTUP | (addr>>12));
microdelay(200);
}
}
#define CMOS_STATA 0x0a
#define CMOS_STATB 0x0b
#define CMOS_UIP (1 << 7) // RTC update in progress
#define SECS 0x00
#define MINS 0x02
#define HOURS 0x04
#define DAY 0x07
#define MONTH 0x08
#define YEAR 0x09
static uint cmos_read(uint reg)
{
outb(CMOS_PORT, reg);
microdelay(200);
return inb(CMOS_RETURN);
}
static void fill_rtcdate(struct rtcdate *r)
{
r->second = cmos_read(SECS);
r->minute = cmos_read(MINS);
r->hour = cmos_read(HOURS);
r->day = cmos_read(DAY);
r->month = cmos_read(MONTH);
r->year = cmos_read(YEAR);
}
//PAGEBREAK!
// qemu seems to use 24-hour GWT and the values are BCD encoded
void cmostime(struct rtcdate *r)
{
struct rtcdate t1, t2;
int sb, bcd;
sb = cmos_read(CMOS_STATB);
bcd = (sb & (1 << 2)) == 0;
// make sure CMOS doesn't modify time while we read it
for(;;) {
fill_rtcdate(&t1);
if(cmos_read(CMOS_STATA) & CMOS_UIP)
continue;
fill_rtcdate(&t2);
if(memcmp(&t1, &t2, sizeof(t1)) == 0)
break;
}
// convert
if(bcd) {
#define CONV(x) (t1.x = ((t1.x >> 4) * 10) + (t1.x & 0xf))
CONV(second);
CONV(minute);
CONV(hour );
CONV(day );
CONV(month );
CONV(year );
#undef CONV
}
*r = t1;
r->year += 2000;
}

159
kernel/arch/x86_64/mp.c Normal file
View file

@ -0,0 +1,159 @@
// Multiprocessor support
// Search memory for MP description structures.
// http://developer.intel.com/design/pentium/datashts/24201606.pdf
#include "types.h"
#include "defs.h"
#include "param.h"
#include "memlayout.h"
#include "mp.h"
#include "x86.h"
#include "mmu.h"
#include "proc.h"
struct cpu cpus[NCPU];
// Processors the firmware says exist.
int ncpu;
// Processors actually running, which is not the same number and must not be
// confused with it. Enumeration is cheap and startup is where the difficulty
// is: right now the APs are found and deliberately not started, so this stays
// at 1 while ncpu is 2, 4 or 16.
//
// /proc/cpuinfo reports *this* one, matching Linux, where cpuinfo lists online
// CPUs. Reporting ncpu there told a VM with four processors that it had four
// working ones, which was a comfortable thing to read and false.
int ncpu_online = 1;
uchar ioapicid;
static uchar
sum(uchar *addr, int len)
{
int i, sum;
sum = 0;
for(i=0; i<len; i++)
sum += addr[i];
return sum;
}
// Look for an MP structure in the len bytes at addr.
static struct mp*
mpsearch1(addr_t a, int len)
{
uchar *e, *p, *addr;
addr = P2V(a);
e = addr+len;
for(p = addr; p < e; p += sizeof(struct mp))
if(memcmp(p, "_MP_", 4) == 0 && sum(p, sizeof(struct mp)) == 0)
return (struct mp*)p;
return 0;
}
// Search for the MP Floating Pointer Structure, which according to the
// spec is in one of the following three locations:
// 1) in the first KB of the EBDA;
// 2) in the last KB of system base memory;
// 3) in the BIOS ROM between 0xE0000 and 0xFFFFF.
static struct mp*
mpsearch(void)
{
uchar *bda;
uint p;
struct mp *mp;
bda = (uchar *) P2V(0x400);
if((p = ((bda[0x0F]<<8)| bda[0x0E]) << 4)){
if((mp = mpsearch1(p, 1024)))
return mp;
} else {
p = ((bda[0x14]<<8)|bda[0x13])*1024;
if((mp = mpsearch1(p-1024, 1024)))
return mp;
}
return mpsearch1(0xF0000, 0x10000);
}
// Search for an MP configuration table. For now,
// don't accept the default configurations (physaddr == 0).
// Check for correct signature, calculate the checksum and,
// if correct, check the version.
// To do: check extended table checksum.
static struct mpconf*
mpconfig(struct mp **pmp)
{
struct mpconf *conf;
struct mp *mp;
if((mp = mpsearch()) == 0 || mp->physaddr == 0)
return 0;
conf = (struct mpconf*) P2V((addr_t) mp->physaddr);
if(memcmp(conf, "PCMP", 4) != 0)
return 0;
if(conf->version != 1 && conf->version != 4)
return 0;
if(sum((uchar*)conf, conf->length) != 0)
return 0;
*pmp = mp;
return conf;
}
void
mpinit(void)
{
uchar *p, *e;
struct mp *mp;
struct mpconf *conf;
struct mpproc *mproc;
struct mpioapic *ioapic;
if((conf = mpconfig(&mp)) == 0) {
cprintf("No other CPUs found.\n");
return;
}
lapic = (uint*)P2V(conf->lapicaddr_p);
for(p=(uchar*)(conf+1), e=(uchar*)conf+conf->length; p<e; ){
switch(*p){
case MPPROC:
mproc = (struct mpproc*)p;
if(ncpu < NCPU) {
cpus[ncpu].apicid = mproc->apicid; // apicid may differ from ncpu
ncpu++;
}
p += sizeof(struct mpproc);
continue;
case MPIOAPIC:
ioapic = (struct mpioapic*)p;
ioapicid = ioapic->apicno;
p += sizeof(struct mpioapic);
continue;
case MPBUS:
case MPIOINTR:
case MPLINTR:
p += 8;
continue;
default:
panic("Major problem parsing mp config.");
break;
}
}
// "Seems we are" is doing a lot of work here, and the number is usually
// wrong. This walks the *legacy Intel MP table* (1994), which modern firmware
// fills in minimally or not at all -- QEMU/SeaBIOS emits exactly one
// processor entry regardless of -smp, because it expects the OS to enumerate
// CPUs from the ACPI MADT instead. Verified 2026-08-08: with -smp 4 the table
// is 208 bytes and contains one type-0 entry, then buses, an I/O APIC and
// interrupt assignments. So ncpu is 1 on QEMU no matter what, and SMP is
// silently never exercised there.
//
// See docs/roadmap.md "Known broken" -- this is half of the SMP bug, and the
// reason the other half (AP startup triple-faulting) cannot be reproduced
// under QEMU until the MADT is parsed.
cprintf("Seems we are SMP, ncpu = %d\n",ncpu);
if(mp->imcrp){
// Bochs doesn't support IMCR, so this doesn't run on Bochs.
// But it would on real hardware.
outb(0x22, 0x70); // Select IMCR
outb(0x23, inb(0x23) | 1); // Mask external interrupts.
}
}

View file

@ -0,0 +1,30 @@
# Context switch
#
# void swtch(struct context **old, struct context *new);
#
# Save current register context in old
# and then load register context from new.
.global swtch
swtch:
# Save old callee-save registers
pushq %rbp
pushq %rbx
pushq %r12
pushq %r13
pushq %r14
pushq %r15
# Switch stacks
movq %rsp, (%rdi)
movq %rsi, %rsp
# Load new callee-save registers
popq %r15
popq %r14
popq %r13
popq %r12
popq %rbx
popq %rbp
retq #??

159
kernel/arch/x86_64/trap.c Normal file
View file

@ -0,0 +1,159 @@
#include "types.h"
#include "defs.h"
#include "irq.h"
#include "param.h"
#include "memlayout.h"
#include "mmu.h"
#include "proc.h"
#include "x86.h"
#include "traps.h"
#include "spinlock.h"
#include "posix.h"
uint *idt;
extern addr_t vectors[];
struct spinlock tickslock;
uint ticks;
static void
mkgate(uint *idt, uint n, addr_t kva, uint pl)
{
uint64 addr = (uint64) kva;
n *= 4;
idt[n+0] = (addr & 0xFFFF) | (KERNEL_CS << 16);
idt[n+1] = (addr & 0xFFFF0000) | 0x8E00 | ((pl & 3) << 13);
idt[n+2] = addr >> 32;
idt[n+3] = 0;
}
void idtinit(void)
{
struct {
ushort limit;
uint64 base;
} __attribute__((packed)) idtdesc;
idtdesc.limit = PGSIZE - 1;
idtdesc.base = (uint64)idt;
asm volatile("lidt %0" : : "m" (idtdesc));
}
void tvinit(void)
{
int n;
idt = (uint*) kalloc();
memset(idt, 0, PGSIZE);
for (n = 0; n < 256; n++)
mkgate(idt, n, vectors[n], 0);
mkgate(idt, T_SYSCALL, vectors[T_SYSCALL], 3);
}
uint
uptime(void)
{
uint xticks;
acquire(&tickslock);
xticks = ticks;
release(&tickslock);
return xticks;
}
// Which signal a CPU fault corresponds to, so a process killed by a fault is
// reported to wait() as died-by-signal with a sensible cause rather than a
// blanket SIGKILL.
static int
trapsig(uint64 trapno)
{
switch(trapno){
case T_DIVIDE: return SIGFPE;
case T_ILLOP: return SIGILL;
case T_GPFLT: return SIGSEGV;
case T_PGFLT: return SIGSEGV;
case T_BRKPT: return SIGTRAP;
case T_DEBUG: return SIGTRAP;
default: return SIGSEGV;
}
}
void
trap(struct trapframe *tf)
{
kstack_check_canary("trap entry");
switch(tf->trapno){
case T_IRQ0 + IRQ_TIMER:
if(cpunum() == 0){
acquire(&tickslock);
ticks++;
wakeup(&ticks);
release(&tickslock);
}
lapiceoi();
break;
case T_IRQ0 + IRQ_SPURIOUS:
cprintf("cpu%d: spurious interrupt at %p:%p\n",
cpunum(), tf->cs, tf->rip);
lapiceoi();
break;
default:
// Every other IOAPIC pin goes through the registration table. The device
// interrupts used to be listed here one case at a time, which cannot work
// for anything found on the PCI bus: a driver does not know its line
// until it reads the device's config space at probe time, so there is no
// constant to write a case label for. See include/irq.h.
if(tf->trapno >= T_IRQ0 && tf->trapno < T_IRQ0 + NIRQ){
irq_dispatch(tf->trapno - T_IRQ0);
lapiceoi();
break;
}
if(myproc() == 0 || (tf->cs&3) == 0){
cprintf("unexpected trap %d from cpu %d rip %p (cr2=0x%p) err=0x%p\n",
tf->trapno, cpunum(), tf->rip, rcr2(), tf->err);
if (myproc())
cprintf("proc id: %d\n", myproc()->pid);
if (tf->trapno == T_GPFLT) {
uint64 *iretframe = (uint64*)tf->rsp;
cprintf("iretq frame @ %p: rip=%p cs=%p rflags=%p rsp=%p ss=%p\n",
iretframe, iretframe[0], iretframe[1], iretframe[2],
iretframe[3], iretframe[4]);
if (myproc())
cprintf("this tf=%p myproc()->tf=%p myproc()->kstack=%p (tf-kstack=%p)\n",
tf, myproc()->tf, myproc()->kstack, (uint64)tf - (uint64)myproc()->kstack);
}
panic("trap");
}
cprintf("pid %d %s: trap %d err %d on cpu %d "
"rip 0x%p addr 0x%p--kill proc\n",
myproc()->pid, myproc()->name, tf->trapno, tf->err, cpunum(), tf->rip,
rcr2());
// General-purpose register dump. This used to be followed by ~25 lines
// that walked musl's mallocng group/meta structures out of rax/rdi and
// dumped the user stack -- scaffolding for the boot-crash investigation
// recorded in docs/architecture/kernel-history.md section 1. That
// investigation is closed and the underlying kernel bugs are fixed, so
// the allocator-specific part is gone; the registers stay, since they are
// useful for any fault.
if (tf->trapno == T_GPFLT) {
cprintf(" rax=%p rbx=%p rcx=%p rdx=%p rsi=%p rdi=%p rbp=%p rsp=%p\n",
tf->rax, tf->rbx, tf->rcx, tf->rdx, tf->rsi, tf->rdi, tf->rbp, tf->rsp);
}
myproc()->killed = 1;
if(myproc()->killsig == 0)
myproc()->killsig = trapsig(tf->trapno);
}
if(myproc() && myproc()->killed && (tf->cs&3) == DPL_USER)
exit(W_EXITCODE(0, myproc()->killsig ? myproc()->killsig : SIGKILL));
if(myproc() && myproc()->state == RUNNING && tf->trapno == T_IRQ0+IRQ_TIMER)
yield();
if(myproc() && myproc()->killed && (tf->cs&3) == DPL_USER)
exit(W_EXITCODE(0, myproc()->killsig ? myproc()->killsig : SIGKILL));
kstack_check_canary("trap exit");
}

View file

@ -0,0 +1,257 @@
# trapasm.S — kernel/user entry/exit with FS_BASE swapping for TLS
#
# The kernel uses __thread struct cpu *cpu and __thread struct proc *proc,
# accessed via %fs (IA32_FS_BASE MSR, 0xC0000100). seginit() sets this to
# kernel_fs_base. User-space TLS (musl) uses the same MSR via arch_prctl
# ARCH_SET_FS (saved in proc->fs_base).
#
# Rule: on ENTRY from user (syscall or interrupt), restore kernel FS_BASE.
# on EXIT to user (sysretq or iretq), write proc->fs_base to MSR.
#
# proc->fs_base is at byte offset 0x18 (24) inside struct proc.
# proc pointer is at %gs:24 (PERCPU_PROC in include/proc.h); cpu is at %gs:16.
# -----------------------------------------------------------------------
# Helper macro: write kernel_fs_base to IA32_FS_BASE.
# Clobbers %rax, %rcx, %rdx. Caller must save them if needed.
# -----------------------------------------------------------------------
# Read from %gs, not from a global.
#
# This used to be `movq kernel_fs_base(%rip), %rax`, and kernel_fs_base was a
# single global that every CPU wrote during seginit(). With one processor that
# is merely untidy. With two it is fatal: whichever CPU ran seginit() last wins,
# and from the first trap onward *every* CPU restores that one CPU's per-CPU
# block -- so `cpu` and `proc` are shared, pushcli/popcli nesting counts collide,
# and the kernel dies in panic("sched locks") shortly after the APs start.
#
# GS base is set per CPU by seginit() to the same value as FS base, and %gs:0
# holds the block's self-pointer. So this reads *this* CPU's kernel FS base with
# no global involved.
#
# No swapgs, deliberately. The usual reason for it is that userspace owns GS;
# here it does not -- sys_arch_prctl() implements only ARCH_SET_FS/ARCH_GET_FS
# and rejects everything else, musl uses FS for TLS on x86-64, and the only
# other writes to %gs are selector loads in entry.S and entryother.S that happen
# before seginit(). GS is the kernel's alone, so it can simply stay pointing at
# the per-CPU block. If ARCH_SET_GS is ever implemented, this must become a
# proper swapgs pair.
.macro RESTORE_KERNEL_FS
movq %gs:0, %rax
movq %rax, %rdx
shrq $32, %rdx
movl $0xC0000100, %ecx
wrmsr
.endm
# -----------------------------------------------------------------------
# Helper macro: write proc->fs_base to IA32_FS_BASE.
# Requires %fs to still hold the kernel base.
# Clobbers %rax, %rcx, %rdx. Caller must save them if needed.
# -----------------------------------------------------------------------
.macro WRITE_USER_FS
# proc, from the per-CPU block at %gs:24 (PERCPU_PROC in include/proc.h).
# This used to be %fs:(-8), the __thread slot, which no longer exists --
# see the comment in include/proc.h for why those stopped being __thread.
movq %gs:24, %rax # proc pointer
movq 0x18(%rax), %rax # proc->fs_base (offset 24 = 0x18)
movq %rax, %rdx
shrq $32, %rdx
movl $0xC0000100, %ecx
wrmsr
.endm
# -----------------------------------------------------------------------
# alltraps — IDT interrupt/fault entry
# Stack on entry (from user mode, CPU has done privilege switch):
# [rsp+0] trapno (pushed by vectors.S)
# [rsp+8] errorcode
# [rsp+16] rip
# [rsp+24] cs ← bit 1:0 == 3 means user mode
# [rsp+32] rflags
# [rsp+40] user_rsp
# [rsp+48] user_ss
# -----------------------------------------------------------------------
.global alltraps
alltraps:
# Always restore kernel FS_BASE before touching %fs:, even when the
# saved cs says we were already in kernel mode. A fault/interrupt can
# land in that narrow kernel-mode window after syscall_trapret/trapret
# have already done WRITE_USER_FS (cs is still 0x8 there, but FS_BASE
# is the *user* value) — the old cs-based skip trusted that combination
# never happens and read %fs:(-8) as garbage (proc/cpu == NULL) in that
# case. RESTORE_KERNEL_FS is just a wrmsr of an already-known value when
# we truly were in the kernel, so doing it unconditionally is cheap and
# always correct.
pushq %rax
pushq %rcx
pushq %rdx
RESTORE_KERNEL_FS
popq %rdx
popq %rcx
popq %rax
# Build trap frame.
pushq %r15
pushq %r14
pushq %r13
pushq %r12
pushq %r11
pushq %r10
pushq %r9
pushq %r8
pushq %rdi
pushq %rsi
pushq %rbp
pushq %rdx
pushq %rcx
pushq %rbx
pushq %rax
movq %rsp, %rdi # frame in arg1
callq trap
# Return falls through to trapret...
.global trapret
trapret:
popq %rax
popq %rbx
popq %rcx
popq %rdx
popq %rbp
popq %rsi
popq %rdi
popq %r8
popq %r9
popq %r10
popq %r11
popq %r12
popq %r13
popq %r14
popq %r15
addq $16, %rsp # discard trapno and errorcode
# If returning to user mode, write proc->fs_base to FS_BASE MSR.
# Stack now: [rsp+0]=rip, [rsp+8]=cs, [rsp+16]=rflags, [rsp+24]=rsp, [rsp+32]=ss
testb $3, 8(%rsp)
jz .Ltrapret_kret
# cli before WRITE_USER_FS: close the same race as in syscall_trapret.
# iretq will atomically restore RFLAGS (IF=1) when switching to user mode.
pushq %rax
pushq %rcx
pushq %rdx
cli
WRITE_USER_FS
popq %rdx
popq %rcx
popq %rax
.Ltrapret_kret:
iretq
# -----------------------------------------------------------------------
# syscall_entry — SYSCALL instruction entry point
# On entry: rcx=user rip, r11=user rflags, rsp=user stack, IF cleared.
# FS_BASE may be the user's TLS value (set at last sysretq).
# -----------------------------------------------------------------------
.global syscall_entry
syscall_entry:
# Swap FS_BASE to kernel value BEFORE any %fs: access.
# Save rax/rcx/rdx to single-CPU scratch globals (IF is off, no race).
movq %rax, _sc_rax(%rip)
movq %rcx, _sc_rcx(%rip) # user rip (SYSCALL stores it here)
movq %rdx, _sc_rdx(%rip)
RESTORE_KERNEL_FS # clobbers rax, rcx, rdx — that's OK
movq _sc_rax(%rip), %rax
movq _sc_rcx(%rip), %rcx
movq _sc_rdx(%rip), %rdx
# Switch to kernel stack: store user rsp at top of proc->kstack.
#
# The scratch slot is %fs:(8), NOT %fs:(0). %fs:(0) is the TCB
# self-pointer -- the x86-64 TLS ABI reserves the word at the thread
# pointer to hold the thread pointer itself, and a compiler is entitled to
# read it to materialise the TP before applying a variable's offset.
#
# It used to be %fs:(0), and that survived for years only because GCC
# compiles `proc`/`cpu` into direct `%fs:-8` segment references and never
# touches the word. clang at -O0 does `movq %fs:0, %rax; addq $-8, %rax;
# (%rax)` instead -- so it read the syscall number this line had just
# stored (7, SYS_exec, from initcode), computed 7-8 = -1, and page-faulted
# on the very first syscall of the very first process.
#
# Positive offsets are safe: the TLS block occupies negative offsets
# (-16 = cpu, -8 = proc) and seginit puts the thread pointer 2048 bytes
# into a 4096-byte page, so %fs:(8) onward is 2040 bytes we own outright.
movq %rax, %fs:(8) # save rax in per-CPU scratch slot
movq %gs:24, %rax # proc pointer (PERCPU_PROC)
movq 0x10(%rax), %rax # proc->kstack (offset 16)
addq $(4096-16), %rax # point to tf->rsp slot
movq %rsp, (%rax) # save user rsp
movq %rax, %rsp # switch to kernel stack
movq %fs:(8), %rax # restore rax
pushq %r11 # rflags
pushq $0 # cs (ignored)
pushq %rcx # rip (next user instruction)
pushq $0 # err
pushq $0 # trapno (ignored)
pushq %r15
pushq %r14
pushq %r13
pushq %r12
pushq %r11
pushq %r10
pushq %r9
pushq %r8
pushq %rdi
pushq %rsi
pushq %rbp
pushq %rdx
pushq %rcx
pushq %rbx
pushq %rax
movq %rsp, %rdi # frame in arg1
callq syscall
# Return falls through to syscall_trapret...
# -----------------------------------------------------------------------
# syscall_trapret — return path for SYSCALL-based syscalls
# -----------------------------------------------------------------------
.global syscall_trapret
syscall_trapret:
# Disable interrupts BEFORE writing user FS_BASE to the MSR.
# Without cli here, a timer IRQ between WRITE_USER_FS and the cli below
# would arrive in ring-0 with user FS_BASE set, causing %fs:(-8) to return
# garbage (proc=NULL) → panic in the interrupt handler.
# rax/rcx/rdx are clobbered by WRITE_USER_FS then restored by the pops.
cli
WRITE_USER_FS
popq %rax
popq %rbx
popq %rcx
popq %rdx
popq %rbp
popq %rsi
popq %rdi
popq %r8
popq %r9
popq %r10
popq %r11
popq %r12
popq %r13
popq %r14
popq %r15
addq $40, %rsp # discard trapno, errorcode, rip, cs, rflags
# Mask interrupts: sysretq is unsafe with IF set on non-canonical RSP.
# See https://www.felixcloutier.com/x86/sysret
cli
movq (%rsp), %rsp # restore the user stack
sysretq

1281
kernel/arch/x86_64/vectors.S Normal file

File diff suppressed because it is too large Load diff

631
kernel/arch/x86_64/vm.c Normal file
View file

@ -0,0 +1,631 @@
#include "param.h"
#include "types.h"
#include "defs.h"
#include "x86.h"
#include "memlayout.h"
#include "mmu.h"
#include "proc.h"
int mappages(pml4e_t *pgdir, void *va, addr_t size, addr_t pa, int perm);
static pte_t * walkpgdir(pml4e_t *pml4, const void *va, int alloc);
// `cpu` and `proc` are no longer __thread variables; they live at fixed offsets
// in the per-CPU block and are read through %gs at each use. See the long
// comment in include/proc.h for why -- the short version is that __thread means
// per *thread*, the compiler caches the thread pointer across calls on that
// basis, and a kernel thread changes CPU at every sched().
// Kernel FS_BASE set by seginit(); read by trapasm.S to restore after user TLS
uint64 kernel_fs_base;
// Single-CPU scratch for syscall_entry FS swap (safe while IF cleared by SYSCALL/SFMASK)
uint64 _sc_rax, _sc_rcx, _sc_rdx;
static pml4e_t *kpml4;
static pdpe_t *kpdpt;
struct tassdesc {
uint lim_15_0 : 16;
uint base_15_0 : 16;
uint base_23_16 : 8;
uint type : 4;
uint s : 1;
uint dpl : 2;
uint p : 1;
uint lim_19_16 : 4;
uint avl : 1;
uint l : 1;
uint d : 1;
uint g : 1;
uint base_31_24 : 8;
uint32 base_63_32;
uint32 res;
};
void
syscallinit(void)
{
// SYSRETQ does NOT use USER_CS/USER_DS directly. Per the AMD64/Intel
// convention, it computes CS = STAR[63:48]+16 and SS = STAR[63:48]+8
// (RPL forced to 3), so STAR[63:48] must hold the *compat* 32-bit user
// code selector (SEG_UCODE32), relying on the GDT's consecutive
// [UCODE32, UDATA, UCODE] layout — NOT USER_CS itself. Using USER_CS
// here made every sysretq load CS=USER_CS+16 (an invalid selector,
// pointing past UCODE into the TSS descriptor) and SS=USER_CS+8. QEMU's
// TCG apparently doesn't validate segment descriptors on SYSRET, so this
// went unnoticed until an interrupt forced a strictly-validated iretq
// with that same bogus CS still loaded, which correctly #GP'd.
wrmsr(MSR_STAR,
((((uint64)((SEG_UCODE32 << 3) | DPL_USER) << 16) | (uint64)KERNEL_CS) << 32));
wrmsr(MSR_LSTAR, (addr_t)syscall_entry);
wrmsr(MSR_CSTAR, (addr_t)ignore_sysret);
wrmsr(MSR_SFMASK, FL_TF|FL_DF|FL_IF|FL_IOPL_MASK|FL_AC|FL_NT);
}
void
seginit(void)
{
struct segdesc *gdt;
uint *tss;
uint64 addr;
void *local;
struct cpu *c;
// The per-CPU page, preallocated by the boot processor for every AP.
//
// It must not be kalloc()'d here on an application processor. kalloc takes a
// spinlock; acquire() calls holding(); holding() reads `cpu`, which is a
// __thread variable reached through %fs -- and %fs is set up four lines
// below. So an AP allocating its own page reads the per-CPU pointer before
// the per-CPU pointer exists, and the kernel dies in panic("acquire") with
// every lock appearing to be held.
//
// startothers() fills c->local in before starting each AP, on the boot
// processor, where locking works.
c = &cpus[cpunum()];
local = c->local;
if (local == 0) {
local = kalloc(); // the boot processor's own, taken normally
memset(local, 0, PGSIZE);
}
gdt = (struct segdesc*) local;
tss = (uint*) (((char*) local) + 1024);
tss[16] = 0x00680000;
kernel_fs_base = ((uint64) local) + 2048;
wrmsr(0xC0000100, kernel_fs_base);
// The TCB self-pointer: the word at %fs:0 must hold the thread pointer.
//
// This is part of the x86-64 TLS ABI and it was missing, which worked only
// by accident. GCC with -mtls-direct-seg-refs compiles `cpu = c` into a
// single `movq %rcx, %fs:-0x10` and never needs the thread pointer as a
// value, so nothing ever read this word. clang materialises it instead --
// `movq %fs:0x0, %rax; addq $-0x10, %rax; movq %rcx, (%rax)` -- which read
// the zero left by the memset above and stored through address
// 0xfffffffffffffff0, three instructions later.
//
// Writing it costs one store per CPU and removes the dependency on a
// particular compiler's TLS optimisation.
*(uint64 *) kernel_fs_base = kernel_fs_base;
// Clear the two per-CPU slots before anything can read them. Their offsets
// are PERCPU_CPU and PERCPU_PROC in include/proc.h and are shared with
// trapasm.S, which reads %gs:16 for proc on the way out to user mode.
((uint64 *) kernel_fs_base)[2] = 0; // struct cpu * (PERCPU_CPU)
((uint64 *) kernel_fs_base)[3] = 0; // struct proc * (PERCPU_PROC)
// The same address in GS base, per CPU.
//
// trapasm.S needs to find *this* CPU's kernel FS base on every entry from
// user mode, and it has no register to work from at that instant -- which is
// why it used to read the kernel_fs_base global. That global is written by
// every CPU during its own seginit(), so with more than one processor the
// last writer wins and every CPU restores one CPU's per-CPU block. GS base is
// per-CPU in hardware, so %gs:0 answers the question with no global at all.
//
// Safe because GS belongs entirely to the kernel here: sys_arch_prctl()
// implements ARCH_SET_FS and ARCH_GET_FS only, and musl uses FS for TLS on
// x86-64. See the comment on RESTORE_KERNEL_FS.
wrmsr(0xC0000101, kernel_fs_base); // IA32_GS_BASE
c->local = local;
cpu_set(c);
proc_set(0);
// Two of the entries below are deliberately null descriptors, and neither is
// dead code even though both look it:
//
// SEG_UCODE32 is never loaded, but SYSRETQ computes CS from STAR[63:48]+16
// and SS from +8, so the *slot* must exist to keep the
// [UCODE32, UDATA, UCODE] triple consecutive. See syscallinit.
// SEG_KCPU is a leftover slot number from 32-bit xv6, where %gs held the
// per-CPU pointer. Schunix uses FS_BASE/TLS instead, but the
// index has to stay occupied or SEG_TSS stops landing at 7.
//
// Deleting either moves every descriptor after it and breaks sysret.
addr = (uint64) tss;
gdt[0] = (struct segdesc) {0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
gdt[SEG_KCODE] = SEG(STA_X|STA_R, 0, 0, 0);
gdt[SEG_KDATA] = SEG(STA_W, 0, 0, 0);
gdt[SEG_UCODE32] = (struct segdesc) {0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
gdt[SEG_UDATA] = SEG(STA_W, 0, 0, DPL_USER);
gdt[SEG_UCODE] = SEG(STA_X|STA_R, 0, 0, DPL_USER);
gdt[SEG_KCPU] = (struct segdesc) {0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
struct tassdesc *td = (struct tassdesc*)(gdt + SEG_TSS);
td->lim_15_0 = 0x67;
td->base_15_0 = addr & 0xffff;
td->base_23_16 = (addr >> 16) & 0xff;
td->type = STS_T64A;
td->s = 0;
td->dpl = 0;
td->p = 1;
td->lim_19_16 = 0;
td->avl = 0;
td->l = 0;
td->d = 0;
td->g = 0;
td->base_31_24 = (addr >> 24) & 0xff;
td->base_63_32 = (addr >> 32) & 0xffffffff;
td->res = 0;
lgdt((void*) gdt, (NSEGS+1) * sizeof(struct segdesc));
ltr(SEG_TSS << 3);
}
pml4e_t*
setupkvm(void)
{
pml4e_t *pml4 = (pml4e_t*) kalloc();
if(pml4 == 0) return 0;
memset(pml4, 0, PGSIZE);
pml4[PMX(KERNBASE)] = v2p(kpdpt) | PTE_P | PTE_W;
// The framebuffer is deliberately NOT mapped here.
//
// This used to identity-map 0xfd000000 for 800*600*4 bytes, user-writable,
// into every process that ever existed. Three things were wrong with that.
// The address and the size were both guesses that only hold on QEMU's
// stdvga at one resolution; setupkvm runs before vga_init, so on the first
// call nothing had even read the VBE block yet; and every process could
// scribble on the screen without asking anyone.
//
// Nothing used it -- userspace reaches the framebuffer through
// mmap("/dev/fb0"), which maps the real base at the real size. So it is
// gone, and a process gets the framebuffer exactly when it asks for it.
return pml4;
}
void
kvmalloc(void)
{
kpml4 = (pml4e_t*) kalloc();
memset(kpml4, 0, PGSIZE);
kpdpt = (pdpe_t*) kalloc();
memset(kpdpt, 0, PGSIZE);
kpml4[0] = v2p(kpdpt) | PTE_P | PTE_W;
kpml4[PMX(KERNBASE)] = v2p(kpdpt) | PTE_P | PTE_W;
for(uint64 i = 0; i < 4; i++) {
uint64 flags = PTE_PS | PTE_P | PTE_W;
if(i >= 3) {
flags |= PTE_PCD | PTE_PWT;
}
kpdpt[i] = (i << 30) | flags;
}
switchkvm();
}
void
switchuvm(struct proc *p)
{
pushcli();
if(p->pgdir == 0)
panic("switchuvm: no pgdir");
uint *tss = (uint*) (((char*) mycpu()->local) + 1024);
const addr_t stktop = (addr_t)p->kstack + KSTACKSIZE;
tss[1] = (uint)stktop;
tss[2] = (uint)(stktop >> 32);
lcr3(v2p(p->pgdir));
popcli();
}
static pte_t *
walkpgdir(pml4e_t *pml4, const void *va, int alloc)
{
pml4e_t *pml4e;
pdpe_t *pdp, *pdpe;
pde_t *pd, *pde;
pte_t *pgtab;
pml4e = &pml4[PMX(va)];
if(*pml4e & PTE_P)
pdp = (pdpe_t*)P2V(PTE_ADDR(*pml4e));
else {
if(!alloc || (pdp = (pdpe_t*)kalloc()) == 0)
return 0;
memset(pdp, 0, PGSIZE);
*pml4e = V2P(pdp) | PTE_P | PTE_W | PTE_U;
}
pdpe = &pdp[PDPX(va)];
if(*pdpe & PTE_P)
pd = (pde_t*)P2V(PTE_ADDR(*pdpe));
else {
if(!alloc || (pd = (pde_t*)kalloc()) == 0)
return 0;
memset(pd, 0, PGSIZE);
*pdpe = V2P(pd) | PTE_P | PTE_W | PTE_U;
}
pde = &pd[PDX(va)];
if(*pde & PTE_P)
pgtab = (pte_t*)P2V(PTE_ADDR(*pde));
else {
if(!alloc || (pgtab = (pte_t*)kalloc()) == 0)
return 0;
memset(pgtab, 0, PGSIZE);
*pde = V2P(pgtab) | PTE_P | PTE_W | PTE_U;
}
return (pte_t*)&pgtab[PTX(va)];
}
void
switchkvm(void)
{
lcr3(v2p(kpml4));
}
int
mappages(pml4e_t *pgdir, void *va, addr_t size, addr_t pa, int perm)
{
char *a, *last;
pte_t *pte;
a = (char*)PGROUNDDOWN((addr_t)va);
last = (char*)PGROUNDDOWN(((addr_t)va) + size - 1);
for(;;){
if((pte = walkpgdir(pgdir, a, 1)) == 0)
return -1;
if(*pte & PTE_P)
panic("remap");
*pte = pa | perm | PTE_P | PTE_U;
invlpg(a);
if(a == last)
break;
a += PGSIZE;
pa += PGSIZE;
}
return 0;
}
void
vm_unmap_range(pml4e_t *pgdir, uint64 va, uint64 length)
{
uint64 a = PGROUNDDOWN(va);
uint64 end = PGROUNDUP(va + length);
for (; a < end; a += PGSIZE) {
pte_t *pte = walkpgdir(pgdir, (void*)a, 0);
if (pte && (*pte & PTE_P)) {
uint64 pa = PTE_ADDR(*pte);
kfree((char*)P2V(pa));
*pte = 0;
invlpg((void*)a);
}
}
}
// Tear down a mapping without freeing what is underneath it.
//
// vm_unmap_range() kfree()s every page it unmaps, which is right for an
// anonymous mapping and catastrophic for anything shared: the framebuffer is
// device memory that was never allocated, and a shared memory segment's pages
// belong to the segment, not to whichever process happens to be detaching.
// Handing either of those to kfree puts them on the free list while another
// process is still reading and writing them.
void
vm_unmap_norelease(pml4e_t *pgdir, uint64 va, uint64 length)
{
uint64 a = PGROUNDDOWN(va);
uint64 end = PGROUNDUP(va + length);
for (; a < end; a += PGSIZE) {
pte_t *pte = walkpgdir(pgdir, (void*)a, 0);
if (pte && (*pte & PTE_P)) {
*pte = 0;
invlpg((void*)a);
}
}
}
// Duplicate an mmap()'d VMA region from src into dst for fork().
// Framebuffer mappings are re-pointed at the same physical MMIO range
// (shared, never copied/freed); anonymous mappings get fresh pages with
// the parent's contents copied in — mirrors what copyuvm() does for the
// [0, sz) heap range, which never covers mmap()'d addresses.
int
copy_vma_range(pml4e_t *dstpgdir, pml4e_t *srcpgdir, uint64 va, uint64 length, int share)
{
uint64 a = PGROUNDDOWN(va);
uint64 end = PGROUNDUP(va + length);
for (; a < end; a += PGSIZE) {
if (share) {
// Point the child at the very same physical pages, whatever they are.
//
// This used to special-case the framebuffer and map vga_fb_phys()
// explicitly. Reading the parent's own PTE instead covers device memory
// and shared memory segments with one rule, and cannot disagree with
// what the parent actually has mapped -- which the explicit version
// could, and did, when the address was hardcoded.
pte_t *spte = walkpgdir(srcpgdir, (void*)a, 0);
if (!spte || !(*spte & PTE_P))
continue;
if (mappages(dstpgdir, (void*)a, PGSIZE, PTE_ADDR(*spte), PTE_FLAGS(*spte)) < 0)
return -1;
continue;
}
pte_t *spte = walkpgdir(srcpgdir, (void*)a, 0);
if (!spte || !(*spte & PTE_P))
continue;
char *mem = kalloc();
if (!mem)
return -1;
memmove(mem, (char*)P2V(PTE_ADDR(*spte)), PGSIZE);
if (mappages(dstpgdir, (void*)a, PGSIZE, V2P(mem), PTE_FLAGS(*spte)) < 0) {
kfree(mem);
return -1;
}
}
return 0;
}
void
inituvm(pde_t *pgdir, char *sinit, uint64 sz)
{
char *mem;
if(sz >= PGSIZE)
panic("inituvm: more than a page");
mem = kalloc();
memset(mem, 0, PGSIZE);
mappages(pgdir, 0, PGSIZE, v2p(mem), PTE_W|PTE_U);
memmove(mem, sinit, sz);
}
int
loaduvm(pml4e_t *pgdir, char *addr, struct inode *ip, uint64 offset, uint64 sz)
{
uint64 done, n, off_in_page;
addr_t pa;
pte_t *pte;
char *cur_page;
for(done = 0; done < sz; done += n){
cur_page = (char*)PGROUNDDOWN((uint64)(addr + done));
off_in_page = (uint64)(addr + done) - (uint64)cur_page;
if((pte = walkpgdir(pgdir, cur_page, 0)) == 0)
panic("loaduvm: address should exist");
pa = PTE_ADDR(*pte);
n = PGSIZE - off_in_page;
if(n > sz - done)
n = sz - done;
if(readi(ip, 0, (uint64)(P2V(pa) + off_in_page), offset + done, n) != (int)n)
return -1;
}
return 0;
}
uint64
allocuvm(pml4e_t *pgdir, uint64 oldsz, uint64 newsz)
{
char *mem;
addr_t a;
if(newsz >= KERNBASE)
return 0;
if(newsz < oldsz)
return oldsz;
a = PGROUNDUP(oldsz);
for(; a < newsz; a += PGSIZE){
mem = kalloc();
if(mem == 0){
deallocuvm(pgdir, newsz, oldsz);
return 0;
}
memset(mem, 0, PGSIZE);
if(mappages(pgdir, (char*)a, PGSIZE, V2P(mem), PTE_W|PTE_U) < 0){
deallocuvm(pgdir, newsz, oldsz);
kfree(mem);
return 0;
}
}
return newsz;
}
uint64
deallocuvm(pml4e_t *pgdir, uint64 oldsz, uint64 newsz)
{
pte_t *pte;
addr_t a, pa;
if(newsz >= oldsz)
return oldsz;
a = PGROUNDUP(newsz);
for(; a < oldsz; a += PGSIZE){
pte = walkpgdir(pgdir, (char*)a, 0);
if(pte && (*pte & PTE_P) != 0){
pa = PTE_ADDR(*pte);
if(pa == 0)
panic("kfree");
if(pa < PHYSTOP) {
char *v = P2V(pa);
kfree(v);
}
*pte = 0;
}
}
return newsz;
}
void
freevm(pml4e_t *pml4)
{
uint i, j, k, l;
pdpe_t *pdp;
pde_t *pd;
pte_t *pt;
if(pml4 == 0)
panic("freevm: no pgdir");
for(i = 0; i < (512/2); i++){
if(pml4[i] & PTE_P){
pdp = (pdpe_t*)P2V(PTE_ADDR(pml4[i]));
for(j = 0; j < 512; j++){
if(pdp[j] & PTE_P){
pd = (pde_t*)P2V(PTE_ADDR(pdp[j]));
for(k = 0; k < 512; k++){
if(pd[k] & PTE_P) {
pt = (pte_t*)P2V(PTE_ADDR(pd[k]));
for(l = 0; l < 512; l++){
if(pt[l] & PTE_P) {
addr_t pa = PTE_ADDR(pt[l]);
if(pa < PHYSTOP && pa != 0) {
kfree(P2V(pa));
}
}
}
kfree((char*)pt);
}
}
kfree((char*)pd);
}
}
kfree((char*)pdp);
}
}
kfree((char*)pml4);
}
void
clearpteu(pml4e_t *pgdir, char *uva)
{
pte_t *pte;
pte = walkpgdir(pgdir, uva, 0);
if(pte == 0)
panic("clearpteu");
*pte &= ~PTE_U;
}
pml4e_t*
copyuvm(pml4e_t *pgdir, uint64 sz)
{
pml4e_t *d;
pte_t *pte;
addr_t pa, i, flags;
char *mem;
if((d = setupkvm()) == 0)
return 0;
for(i = 0; i < sz; i += PGSIZE){
if((pte = walkpgdir(pgdir, (void*)i, 0)) == 0)
continue;
if(!(*pte & PTE_P))
continue;
pa = PTE_ADDR(*pte);
flags = PTE_FLAGS(*pte);
if(pa < PHYSTOP) {
if((mem = kalloc()) == 0)
goto bad;
memmove(mem, (char*)P2V(pa), PGSIZE);
if(mappages(d, (void*)i, PGSIZE, V2P(mem), flags) < 0) {
kfree(mem);
goto bad;
}
}
}
return d;
bad:
freevm(d);
return 0;
}
char*
uva2ka(pml4e_t *pgdir, char *uva)
{
if((addr_t)uva >= KERNBASE)
return 0;
pte_t *pte;
pte = walkpgdir(pgdir, uva, 0);
if(pte == 0 || (*pte & PTE_P) == 0)
return 0;
if((*pte & PTE_U) == 0)
return 0;
addr_t pa = (*pte) & 0x000FFFFFFFFFF000ull;
// Include uva's in-page offset — callers dereference the returned pointer
// directly at uva's address, not just somewhere within its page.
return (char*)P2V(pa) + ((addr_t)uva & 0xFFF);
}
int
copyout(pml4e_t *pgdir, addr_t va, void *p, uint64 len)
{
char *buf, *pa0;
addr_t n, va0;
buf = (char*)p;
while(len > 0){
va0 = PGROUNDDOWN(va);
pa0 = uva2ka(pgdir, (char*)va0);
if(pa0 == 0) {
return -1;
}
n = PGSIZE - (va - va0);
if(n > len)
n = len;
memmove(pa0 + (va - va0), buf, n);
len -= n;
buf += n;
va += n;
}
return 0;
}
int
copyin(pml4e_t *pgdir, addr_t srcva, void *dst, uint64 len)
{
uint64 n, va0;
char *pa0;
char *dst_buf = (char*)dst;
while(len > 0){
va0 = PGROUNDDOWN(srcva);
pa0 = uva2ka(pgdir, (char*)va0);
if(pa0 == 0)
return -1;
n = PGSIZE - (srcva - va0);
if(n > len)
n = len;
memmove(dst_buf, pa0 + (srcva - va0), n);
len -= n;
dst_buf += n;
srcva += n;
}
return 0;
}

810
kernel/core/console.c Normal file
View file

@ -0,0 +1,810 @@
#include <stdarg.h>
#include "types.h"
#include "defs.h"
#include "theme.h"
#include "irq.h"
#include "param.h"
#include "traps.h"
#include "spinlock.h"
#include "sleeplock.h"
#include "fs.h"
#include "file.h"
#include "memlayout.h"
#include "mmu.h"
#include "proc.h"
#include "posix.h"
#include "x86.h"
void consputc(int);
// Set by panic() once the crash screen has been painted: after that, consputc
// spins instead of writing, so nothing else can scribble over the report.
int panicked = 0;
// When set, console output goes to the serial line only and the framebuffer
// is left alone.
//
// **Starts set, on purpose.** The display is the product; the serial line is
// the diagnostics. A machine that boots by printing "pci: 6 devices" and
// "vga: display controller 1234:1111, aperture fd000000" to the screen the
// user is looking at is showing them a developer's log, and that single fact
// is most of what makes an OS look like a hobby project.
//
// Nothing is lost: every one of those lines still goes out the UART, which is
// where tools/bootcheck.ps1 reads them from and where you want them when
// something goes wrong. console_takeover() clears this once userspace is up
// and the display genuinely belongs to the console. The F8 verbose boot never
// sets it, so "show me everything" still works.
int cons_serial_only = 1;
extern void vga_screen_reset(void);
// Text colour used when drawing to the framebuffer. Changed by SGR escapes
// (see sgr() below); reset to the default by ESC[0m and ESC c.
//
// This was initialised to 7, a leftover from when it was a VGA text-mode
// attribute byte. vga_putc takes a 32-bit RGB value, so 7 meant "very dark
// blue" -- it looked black against the cream background purely by accident.
static uint32 cons_fg = SCH_FG;
static struct {
struct spinlock lock;
int locking;
} cons;
static char digits[] = "0123456789abcdef";
static void
print_int(int64 xx, int base, int sign)
{
char buf[64];
int i;
uint64 x;
if(sign && (sign = (xx < 0)))
x = -xx;
else
x = xx;
i = 0;
do {
buf[i++] = digits[x % base];
} while((x /= base) != 0);
if(sign)
buf[i++] = '-';
while(--i >= 0)
consputc(buf[i]);
}
void
cprintf(char *fmt, ...)
{
va_list ap;
int i, c, locking;
char *s;
va_start(ap, fmt);
locking = cons.locking;
if (locking)
acquire(&cons.lock);
if (fmt == 0)
panic("null fmt");
for (i = 0; (c = fmt[i] & 0xff) != 0; i++) {
if (c != '%') {
consputc(c);
continue;
}
c = fmt[++i] & 0xff;
if (c == 0)
break;
switch(c) {
case 'd':
print_int(va_arg(ap, int), 10, 1);
break;
case 'u':
print_int(va_arg(ap, uint32), 10, 0);
break;
case 'l':
print_int(va_arg(ap, int64), 10, 1);
break;
case 'x':
print_int(va_arg(ap, uint32), 16, 0);
break;
case 'p':
print_int(va_arg(ap, uint64), 16, 0);
break;
case 'b':
print_int(va_arg(ap, uint32), 2, 0);
break;
case 's':
if ((s = va_arg(ap, char*)) == 0)
s = "(null)";
while (*s)
consputc(*(s++));
break;
case '%':
consputc('%');
break;
default:
consputc('%');
consputc(c);
break;
}
}
if (locking)
release(&cons.lock);
}
// Hand the console over to panic().
//
// Two things change. The lock stops being taken, so a panic can report itself
// from anywhere — including from inside a routine that already holds it. And
// output stops going to the framebuffer, because from here on the framebuffer
// belongs to the crash screen: without this the report would scroll straight
// over the panel that is trying to display it.
void
cons_panic_mode(void)
{
cons.locking = 0;
cons_serial_only = 1;
}
// Hand the display over to the text console.
//
// Called once, when userspace is far enough along that the screen should stop
// being a boot splash and start being a terminal. Until this runs the kernel
// has been talking to the serial line only (see cons_serial_only), so this is
// the moment the framebuffer changes owner -- which is why it clears first
// rather than scrolling the shell prompt in underneath the splash.
void
console_takeover(void)
{
if (!cons_serial_only)
return; // verbose boot: the console never left
cons_serial_only = 0;
vga_screen_reset();
}
// Foreground process group of the console, i.e. the controlling terminal.
// tcgetpgrp()/tcsetpgrp() read and write this through TIOCGPGRP/TIOCSPGRP in
// sys_fs.c. Initialised to 1 -- init's process group -- so the first shell,
// which inherits that group, already sees itself as the foreground job and
// does not immediately stop itself with SIGTTIN.
int tty_fg_pgrp = 1;
#define BACKSPACE 0x100
// ---------------------------------------------------------------------------
// Output: an ANSI escape parser, and UTF-8
//
// The console used to print every byte it was given, which meant every escape
// sequence any program emitted appeared on screen as literal text -- `clear`
// drew "^[[2J^[[H" and left the screen exactly as it found it. Colour, cursor
// addressing, and anything full-screen were all equally impossible.
//
// The serial line does NOT go through any of this. On the other end of it is
// a real terminal emulator that already understands ANSI far better than this
// ever will, so escapes are passed through to it verbatim and interpreted
// only for the framebuffer. Two consumers, two right answers.
// ---------------------------------------------------------------------------
// The sixteen ANSI colours as RGB, for a linear framebuffer.
//
// consoleinit() used to program these into the VGA DAC with set_vga_palette(),
// which does nothing at all in a 32bpp linear mode -- there is no palette to
// index. The colours had to live somewhere real, and this is it.
static const uint32 ansi_rgb[16] = {
0x000000, 0xAA0000, 0x00AA00, 0xAA5500, // black red green yellow(brown)
0x0000AA, 0xAA00AA, 0x00AAAA, 0xAAAAAA, // blue magenta cyan white
0x555555, 0xFF5555, 0x55FF55, 0xFFFF55, // bright versions
0x5555FF, 0xFF55FF, 0x55FFFF, 0xFFFFFF,
};
#define CONS_DEFAULT_FG SCH_FG // see include/theme.h
#define CONS_DEFAULT_BG SCH_BG
static uint32 cons_bg = CONS_DEFAULT_BG;
static int cons_bold = 0;
// Escape parser state.
enum { E_NORMAL, E_ESC, E_CSI };
static int esc_state = E_NORMAL;
static int esc_parm[8];
static int esc_nparm;
static int esc_private; // saw '?' after CSI
static int saved_col, saved_row;
// UTF-8 decoder state. A codepoint arrives as up to four bytes and has to be
// reassembled before it can be turned into a glyph.
static uint32 u8_cp;
static int u8_need;
// ---------------------------------------------------------------------------
// Unicode -> glyph
//
// The font's upper half is CP437, not Latin-1, so a codepoint cannot be used
// as an index. It looks like it can -- font.h's own comments say so, labelling
// each glyph with the Unicode character of the same number -- but the bitmaps
// disagree: index 0xE9 draws a capital theta, and rendering U+00E9 there turns
// "cafe" into "caf" followed by a theta. Every accented character came out as
// an unrelated symbol, consistently enough to look almost deliberate.
//
// So: a table. Entry i covers codepoint 0x80 + i, and 0 means "this font has
// no glyph for that". The C1 range (0x80-0x9F) has no printable characters at
// all, hence the leading run of zeros.
// ---------------------------------------------------------------------------
static const uchar latin1_to_cp437[128] = {
/* 0x80-0x9F: C1 controls, nothing to draw */
0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,
/* 0xA0 */ 0x20, 0xAD, 0x9B, 0x9C, 0, 0x9D, 0, 0x15,
/* 0xA8 */ 0, 0, 0xA6, 0xAE, 0xAA, 0x2D, 0, 0,
/* 0xB0 */ 0xF8, 0xF1, 0xFD, 0, 0, 0xE6, 0x14, 0xFA,
/* 0xB8 */ 0, 0, 0xA7, 0xAF, 0xAC, 0xAB, 0, 0xA8,
/* 0xC0 */ 0, 0, 0, 0, 0x8E, 0x8F, 0x92, 0x80,
/* 0xC8 */ 0, 0x90, 0, 0, 0, 0, 0, 0,
/* 0xD0 */ 0, 0xA5, 0, 0, 0, 0, 0x99, 0,
/* 0xD8 */ 0, 0, 0, 0, 0x9A, 0, 0, 0xE1,
/* 0xE0 */ 0x85, 0xA0, 0x83, 0, 0x84, 0x86, 0x91, 0x87,
/* 0xE8 */ 0x8A, 0x82, 0x88, 0x89, 0x8D, 0xA1, 0x8C, 0x8B,
/* 0xF0 */ 0, 0xA4, 0x95, 0xA2, 0x93, 0, 0x94, 0xF6,
/* 0xF8 */ 0, 0x97, 0xA3, 0x96, 0x81, 0, 0, 0x98,
};
// The handful of characters above U+00FF that CP437 does have. Box drawing
// and block elements matter because they are what every text-mode program
// draws frames and progress bars out of.
struct cp_pair { uint32 cp; uchar glyph; };
static const struct cp_pair cp437_extra[] = {
{ 0x2022, 0x07 }, /* bullet */
{ 0x2500, 0xC4 }, { 0x2502, 0xB3 }, /* light horizontal/vertical */
{ 0x250C, 0xDA }, { 0x2510, 0xBF },
{ 0x2514, 0xC0 }, { 0x2518, 0xD9 },
{ 0x251C, 0xC3 }, { 0x2524, 0xB4 },
{ 0x252C, 0xC2 }, { 0x2534, 0xC1 }, { 0x253C, 0xC5 },
{ 0x2550, 0xCD }, { 0x2551, 0xBA }, /* double */
{ 0x2554, 0xC9 }, { 0x2557, 0xBB },
{ 0x255A, 0xC8 }, { 0x255D, 0xBC },
{ 0x2580, 0xDF }, { 0x2584, 0xDC }, { 0x2588, 0xDB },
{ 0x258C, 0xDD }, { 0x2590, 0xDE },
{ 0x2591, 0xB0 }, { 0x2592, 0xB1 }, { 0x2593, 0xB2 },
{ 0x25A0, 0xFE }, /* black square */
{ 0x221A, 0xFB }, { 0x221E, 0xEC }, /* root, infinity */
{ 0x2260, 0, }, { 0x2264, 0xF3 }, { 0x2265, 0xF2 },
{ 0x2248, 0xF7 }, { 0x2261, 0xF0 },
};
// Returns the glyph to draw for a codepoint, or 0 if there is none.
static int
glyph_for(uint32 cp)
{
if (cp < 0x80)
return (int)cp; // ASCII is CP437's lower half
if (cp < 0x100)
return latin1_to_cp437[cp - 0x80];
for (unsigned i = 0; i < NELEM(cp437_extra); i++)
if (cp437_extra[i].cp == cp)
return cp437_extra[i].glyph;
return 0;
}
static void
sgr(void)
{
for (int i = 0; i < esc_nparm; i++) {
int p = esc_parm[i];
if (p == 0) {
cons_fg = CONS_DEFAULT_FG;
cons_bg = CONS_DEFAULT_BG;
cons_bold = 0;
} else if (p == 1) {
cons_bold = 1;
// Re-apply brightness to a colour already chosen.
for (int j = 0; j < 8; j++)
if (cons_fg == ansi_rgb[j]) { cons_fg = ansi_rgb[j + 8]; break; }
} else if (p == 22) {
cons_bold = 0;
} else if (p >= 30 && p <= 37) {
cons_fg = ansi_rgb[(p - 30) + (cons_bold ? 8 : 0)];
} else if (p == 39) {
cons_fg = CONS_DEFAULT_FG;
} else if (p >= 40 && p <= 47) {
cons_bg = ansi_rgb[p - 40];
} else if (p == 49) {
cons_bg = CONS_DEFAULT_BG;
} else if (p >= 90 && p <= 97) {
cons_fg = ansi_rgb[(p - 90) + 8];
} else if (p >= 100 && p <= 107) {
cons_bg = ansi_rgb[(p - 100) + 8];
}
}
vga_set_bg(cons_bg);
}
// One final byte of a CSI sequence, with esc_parm[] already filled in.
static void
csi_dispatch(int final)
{
int col, row;
int n = esc_nparm > 0 ? esc_parm[0] : 0;
int n1 = n ? n : 1; // most sequences default to 1
vga_cursor_get(&col, &row);
switch (final) {
case 'A': vga_cursor_set(col, row - n1); break;
case 'B': vga_cursor_set(col, row + n1); break;
case 'C': vga_cursor_set(col + n1, row); break;
case 'D': vga_cursor_set(col - n1, row); break;
case 'E': vga_cursor_set(0, row + n1); break;
case 'F': vga_cursor_set(0, row - n1); break;
case 'G': vga_cursor_set(n1 - 1, row); break;
case 'd': vga_cursor_set(col, n1 - 1); break;
case 'H': case 'f': {
int r = esc_nparm > 0 && esc_parm[0] ? esc_parm[0] : 1;
int c = esc_nparm > 1 && esc_parm[1] ? esc_parm[1] : 1;
vga_cursor_set(c - 1, r - 1); // ANSI counts from 1
break;
}
case 'J': vga_erase_display(n); break;
case 'K': vga_erase_line(n); break;
case 'm': sgr(); break;
case 's': saved_col = col; saved_row = row; break;
case 'u': vga_cursor_set(saved_col, saved_row); break;
// Cursor visibility (DECTCEM) and the other private modes. There is no
// hardware cursor being drawn, so there is nothing to show or hide --
// swallowing them silently is the correct behaviour, and far better than
// printing "?25l" into the middle of someone's output.
case 'h': case 'l': break;
default: break; // unknown: drop it, do not print it
}
}
// Feed one output byte to the framebuffer side. Returns nothing; the serial
// side has already had the raw byte.
static void
vga_feed(int c)
{
switch (esc_state) {
case E_ESC:
if (c == '[') {
esc_state = E_CSI;
esc_nparm = 0;
esc_private = 0;
for (int i = 0; i < 8; i++) esc_parm[i] = 0;
return;
}
esc_state = E_NORMAL;
if (c == 'c') { // RIS -- full reset
cons_fg = CONS_DEFAULT_FG;
cons_bg = CONS_DEFAULT_BG;
vga_screen_reset();
}
return; // other two-byte escapes: ignored
case E_CSI:
if (c == '?') { esc_private = 1; return; }
if (c >= '0' && c <= '9') {
if (esc_nparm == 0) esc_nparm = 1;
if (esc_nparm <= 8)
esc_parm[esc_nparm - 1] = esc_parm[esc_nparm - 1] * 10 + (c - '0');
return;
}
if (c == ';') {
if (esc_nparm < 8) esc_nparm++;
return;
}
esc_state = E_NORMAL;
if (!esc_private)
csi_dispatch(c);
return;
}
// E_NORMAL
if (c == '\x1b') { esc_state = E_ESC; return; }
// UTF-8 continuation. A codepoint the font cannot draw becomes '?' -- the
// one thing it must not become is a *different* character, which is exactly
// what indexing the font by codepoint did.
if (u8_need > 0) {
if ((c & 0xC0) != 0x80) { // malformed: abandon the sequence
u8_need = 0;
vga_putc('?', cons_fg);
return;
}
u8_cp = (u8_cp << 6) | (c & 0x3F);
if (--u8_need > 0)
return;
int g = glyph_for(u8_cp);
vga_putc(g ? g : '?', cons_fg);
return;
}
if (c >= 0xF0) { u8_cp = c & 0x07; u8_need = 3; return; }
else if (c >= 0xE0) { u8_cp = c & 0x0F; u8_need = 2; return; }
else if (c >= 0xC0) { u8_cp = c & 0x1F; u8_need = 1; return; }
else if (c >= 0x80) { vga_putc('?', cons_fg); return; } // stray continuation
vga_putc(c, cons_fg);
}
void
consputc(int c)
{
if (panicked) {
cli();
for(;;) hlt();
}
if (c == BACKSPACE) {
uartputc('\b'); uartputc(' '); uartputc('\b');
if (!cons_serial_only)
vga_putc(c, cons_fg);
return;
}
uartputc(c);
if (!cons_serial_only)
vga_feed(c & 0xff);
}
#define INPUT_BUF 256
struct {
struct spinlock lock;
char buf[INPUT_BUF];
uint r;
uint w;
uint e;
uint abort_read;
// Set by EOF (Ctrl-D) on an empty line. Canonical-mode end of input is not
// a character -- it is the *absence* of one -- so it cannot be signalled by
// putting something in the buffer. The old code pushed a literal ^D in and
// had consoleread() pick it back out, which meant a ^D typed mid-line was
// indistinguishable from one typed alone.
uint eof;
} input;
#define C(x) ((x)-'@')
// ---------------------------------------------------------------------------
// The line discipline
//
// TCGETS used to return a hardcoded set of flags and TCSETS threw its argument
// away, so the terminal was permanently in one mode: line-at-a-time, always
// echoing. Anything wanting a keypress at a time -- a pager, an editor, a
// shell doing its own line editing, which is most of what zsh is -- had no way
// to ask for one. This is the real thing: a struct programs can write, that
// changes what the kernel actually does.
// ---------------------------------------------------------------------------
static struct termios tio = {
.c_iflag = ICRNL,
.c_oflag = OPOST | ONLCR,
.c_cflag = CS8 | CREAD | CLOCAL,
.c_lflag = ISIG | ICANON | ECHO | ECHOE,
.c_cc = {
[VINTR] = C('C'), [VQUIT] = C('\\'), [VERASE] = '\x7f', [VKILL] = C('U'),
[VEOF] = C('D'), [VSUSP] = C('Z'), [VWERASE] = C('W'), [VMIN] = 1,
},
.c_ispeed = 15, .c_ospeed = 15,
};
void
cons_get_termios(struct termios *out)
{
acquire(&input.lock);
*out = tio;
release(&input.lock);
}
void
cons_set_termios(const struct termios *in)
{
acquire(&input.lock);
tio = *in;
// Switching out of canonical mode has to hand over whatever has been typed
// but not yet submitted, or a shell that turns off ICANON mid-line silently
// eats the characters already in the edit buffer.
if (!(tio.c_lflag & ICANON))
input.w = input.e;
release(&input.lock);
wakeup(&input.r);
}
static void
echo(int c)
{
if (c == '\n' || c == '\r') {
if ((tio.c_lflag & ECHO) || (tio.c_lflag & ECHONL))
consputc('\n');
return;
}
if (!(tio.c_lflag & ECHO))
return;
// Control characters echo as ^X, the way every other terminal shows them.
if (c < 32 && c != '\t') {
consputc('^');
consputc(c + '@');
} else {
consputc(c);
}
}
// Rub out the last character of the current line.
static void
erase_one(void)
{
if (input.e == input.w)
return;
input.e--;
if (tio.c_lflag & ECHOE)
consputc(BACKSPACE);
}
void
consoleintr(int (*getc)(void))
{
int c;
acquire(&input.lock);
while((c = getc()) >= 0){
if (c == 0)
continue;
// Input mapping, before anything looks at the character.
if (tio.c_iflag & ISTRIP) c &= 0x7f;
if (c == '\r') {
if (tio.c_iflag & IGNCR) continue;
if (tio.c_iflag & ICRNL) c = '\n';
} else if (c == '\n' && (tio.c_iflag & INLCR)) {
c = '\r';
}
// Signals. These are the characters that must not reach the reader.
//
// Ctrl-C used to do `myproc()->killed = 1` -- and `proc` in an interrupt
// handler is whatever happened to be running on this CPU when the key was
// pressed, which is almost never the program the user is looking at. It
// also set a kill flag rather than raising a signal, so nothing could
// catch it and the shell never learned the job had died. Both wrong;
// both fixed by sending a real signal to the foreground process group,
// which the kernel has been tracking in tty_fg_pgrp all along.
if (tio.c_lflag & ISIG) {
int sig = 0;
if (c == tio.c_cc[VINTR]) sig = SIGINT;
else if (c == tio.c_cc[VQUIT]) sig = SIGQUIT;
else if (c == tio.c_cc[VSUSP]) sig = SIGTSTP;
if (sig) {
echo(c);
input.e = input.w = input.r; // discard the partial line
input.abort_read = 1;
release(&input.lock);
sigsend_pgrp(tty_fg_pgrp, sig);
acquire(&input.lock);
wakeup(&input.r);
continue;
}
}
// Ctrl-P: dump the process table. Not POSIX, but it is the one debugging
// aid that works when everything else has stopped responding.
if (c == C('P')) {
procdump();
continue;
}
if (tio.c_lflag & ICANON) {
if (c == tio.c_cc[VERASE] || c == C('H')) {
erase_one();
continue;
}
if (c == tio.c_cc[VKILL]) {
while (input.e != input.w &&
input.buf[(input.e-1) % INPUT_BUF] != '\n')
erase_one();
continue;
}
if (c == tio.c_cc[VWERASE]) {
while (input.e != input.w &&
input.buf[(input.e-1) % INPUT_BUF] == ' ')
erase_one();
while (input.e != input.w &&
input.buf[(input.e-1) % INPUT_BUF] != ' ')
erase_one();
continue;
}
if (c == tio.c_cc[VEOF]) {
if (input.e == input.w)
input.eof = 1; // empty line: end of input
input.w = input.e; // otherwise just submit what is there
wakeup(&input.r);
continue;
}
}
if (input.e - input.r >= INPUT_BUF)
continue; // buffer full: drop it
// Everything else goes in, including bytes above 127 -- those are UTF-8
// continuation bytes, and the old `c >= 32 && c <= 126` test silently
// refused every non-ASCII character anyone tried to type.
input.buf[input.e++ % INPUT_BUF] = c;
echo(c);
// Canonical mode hands over a line at a time; raw mode hands over
// everything as it arrives.
if (!(tio.c_lflag & ICANON) ||
c == '\n' || input.e == input.r + INPUT_BUF) {
input.w = input.e;
wakeup(&input.r);
}
}
release(&input.lock);
}
int
consoleread(struct inode *ip, uint off, char *dst, int n)
{
uint target;
int c;
static uint32 prng_seed = 0x9A7B3C1D;
if (ip->minor == 2) {
return 0;
}
if (ip->minor == 4) {
for (int i = 0; i < n; i++) {
prng_seed ^= prng_seed << 13;
prng_seed ^= prng_seed >> 17;
prng_seed ^= prng_seed << 5;
dst[i] = prng_seed & 0xFF;
}
return n;
}
iunlock(ip);
target = n;
acquire(&input.lock);
while(n > 0){
while(input.r == input.w){
if (input.eof) {
input.eof = 0;
release(&input.lock);
ilock(ip);
return 0; // end of input
}
if (myproc()->killed || input.abort_read) {
input.abort_read = 0;
release(&input.lock);
ilock(ip);
return -1;
}
sleep(&input.r, &input.lock);
}
c = input.buf[input.r++ % INPUT_BUF];
*dst++ = c;
--n;
// Canonical mode returns a line at a time. Raw mode returns whatever has
// arrived: a program that asked for a keypress must not be made to wait
// for a newline that is never coming.
if (tio.c_lflag & ICANON) {
if (c == '\n')
break;
} else {
if (target - n >= (uint)(tio.c_cc[VMIN] ? tio.c_cc[VMIN] : 1))
break;
}
}
release(&input.lock);
ilock(ip);
return target - n;
}
// Nonzero if a completed input line is waiting — poll(2)'s POLLIN answer
// for the console tty. Mirrors the r/w cursors consoleread() blocks on.
int
console_input_ready(void)
{
int ready;
acquire(&input.lock);
ready = input.r != input.w;
release(&input.lock);
return ready;
}
int
consolewrite(struct inode *ip, uint off, char *buf, int n)
{
int i;
if (ip->minor == 2 || ip->minor == 3 || ip->minor == 4) {
return n;
}
iunlock(ip);
acquire(&cons.lock);
for(i = 0; i < n; i++)
consputc(buf[i] & 0xff);
release(&cons.lock);
ilock(ip);
return n;
}
void
disable_blink(void)
{
inb(0x3DA);
outb(0x3C0, 0x10 | 0x20);
uchar dummy = inb(0x3C1);
outb(0x3C0, dummy & ~0x08);
}
static void
set_vga_palette(uchar index, uchar r, uchar g, uchar b)
{
outb(0x3C6, 0xFF);
outb(0x3C8, index);
outb(0x3C9, r & 0x3F);
outb(0x3C9, g & 0x3F);
outb(0x3C9, b & 0x3F);
}
// The keyboard handler lives in kbd.c, but the console is what owns the
// keyboard as an input source, so this is where the line gets claimed.
static void
kbd_irq(void *arg)
{
(void)arg;
kbdintr();
}
void
consoleinit(void)
{
initlock(&cons.lock, "console");
initlock(&input.lock, "input");
memset(&input, 0, sizeof(input));
devsw[CONSOLE].write = consolewrite;
devsw[CONSOLE].read = consoleread;
cons.locking = 1;
disable_blink();
set_vga_palette(0, 0, 0, 0); // Black
set_vga_palette(1, 0, 0, 42); // Blue
set_vga_palette(2, 0, 42, 0); // Green
set_vga_palette(3, 0, 42, 42); // Cyan
set_vga_palette(4, 42, 0, 0); // Red
set_vga_palette(5, 42, 0, 42); // Magenta
set_vga_palette(6, 42, 21, 0); // Brown
set_vga_palette(7, 42, 42, 42); // Light Gray
set_vga_palette(8, 21, 21, 21); // Dark Gray
set_vga_palette(9, 21, 21, 63); // Light Blue
set_vga_palette(10, 21, 63, 21); // Light Green
set_vga_palette(11, 21, 63, 63); // Light Cyan
set_vga_palette(12, 63, 21, 21); // Light Red
set_vga_palette(13, 21, 21, 63); // Light Magenta
set_vga_palette(14, 63, 63, 21); // Yellow
set_vga_palette(15, 63, 63, 63); // White
irq_register(IRQ_KBD, kbd_irq, 0, "ps2-kbd", 0);
}

191
kernel/core/dma.c Normal file
View file

@ -0,0 +1,191 @@
// Physically contiguous memory for devices. See include/dma.h.
#include "types.h"
#include "defs.h"
#include "param.h"
#include "memlayout.h"
#include "mmu.h"
#include "spinlock.h"
#include "dma.h"
#define DMA_GRAIN 64 // one cache line; also xHCI's ring alignment
#define NEXTENT 64 // free extents we can track
struct extent {
addr_t base; // kernel virtual address
uint64 len;
};
static struct {
struct spinlock lock;
struct extent free[NEXTENT];
int n; // free extents, kept sorted by base
addr_t lo, hi;
uint64 total;
} arena;
static uint64
roundup(uint64 v, uint64 to)
{
return (v + to - 1) & ~(to - 1);
}
void
dma_init(void *vstart, void *vend)
{
initlock(&arena.lock, "dma");
arena.lo = roundup((addr_t)vstart, PGSIZE);
arena.hi = (addr_t)vend & ~(addr_t)(PGSIZE - 1);
if(arena.hi <= arena.lo)
panic("dma_init: empty arena");
arena.free[0].base = arena.lo;
arena.free[0].len = arena.hi - arena.lo;
arena.n = 1;
arena.total = arena.free[0].len;
}
// Remove free[i], shifting the tail down.
static void
drop(int i)
{
int j;
for(j = i; j + 1 < arena.n; j++)
arena.free[j] = arena.free[j+1];
arena.n--;
}
// Insert a free extent at index i, shifting the tail up. Returns 0 if the
// table is full, in which case the memory is leaked rather than corrupting
// the list -- a leak is recoverable by rebooting, a corrupt free list hands
// two drivers the same ring.
static int
insert(int i, addr_t base, uint64 len)
{
int j;
if(arena.n >= NEXTENT)
return 0;
for(j = arena.n; j > i; j--)
arena.free[j] = arena.free[j-1];
arena.free[i].base = base;
arena.free[i].len = len;
arena.n++;
return 1;
}
void *
dma_alloc(uint64 size, uint64 align)
{
int i;
addr_t start;
if(size == 0)
return 0;
if(align < DMA_GRAIN)
align = DMA_GRAIN;
if(align & (align - 1))
panic("dma_alloc: alignment not a power of two");
size = roundup(size, DMA_GRAIN);
acquire(&arena.lock);
for(i = 0; i < arena.n; i++){
addr_t base = arena.free[i].base;
uint64 len = arena.free[i].len;
uint64 lead;
start = roundup(base, align);
lead = start - base;
if(lead + size > len)
continue;
// Three cases: the request consumes the extent, sits at its head, sits
// at its tail, or sits in the middle and splits it in two.
if(lead == 0 && size == len){
drop(i);
} else if(lead == 0){
arena.free[i].base += size;
arena.free[i].len -= size;
} else if(lead + size == len){
arena.free[i].len = lead;
} else {
uint64 tail_len = len - lead - size;
arena.free[i].len = lead;
if(!insert(i + 1, start + size, tail_len)){
// No room to record the tail. Give the whole extent to the caller
// rather than lose track of where the tail was.
arena.free[i].len = len;
continue;
}
}
release(&arena.lock);
memset((void*)start, 0, size);
return (void*)start;
}
release(&arena.lock);
cprintf("dma_alloc: out of DMA memory (wanted %d bytes, %d free)\n",
(int)size, (int)dma_free_bytes());
return 0;
}
void
dma_free(void *p, uint64 size)
{
addr_t base = (addr_t)p;
int i;
if(p == 0)
return;
size = roundup(size, DMA_GRAIN);
if(base < arena.lo || base + size > arena.hi)
panic("dma_free: not DMA memory");
acquire(&arena.lock);
// Find the first extent that starts after this block, so the list stays
// sorted and neighbours end up adjacent for coalescing.
for(i = 0; i < arena.n && arena.free[i].base < base; i++)
;
if(i > 0 && arena.free[i-1].base + arena.free[i-1].len == base){
arena.free[i-1].len += size; // merge with the block before
i--;
} else if(!insert(i, base, size)){
release(&arena.lock);
cprintf("dma_free: extent table full, leaking %d bytes\n", (int)size);
return;
}
if(i + 1 < arena.n &&
arena.free[i].base + arena.free[i].len == arena.free[i+1].base){
arena.free[i].len += arena.free[i+1].len; // and with the block after
drop(i + 1);
}
release(&arena.lock);
}
addr_t
dma_phys(const void *p)
{
return V2P((addr_t)p);
}
uint64
dma_free_bytes(void)
{
uint64 n = 0;
int i;
for(i = 0; i < arena.n; i++)
n += arena.free[i].len;
return n;
}
uint64
dma_total_bytes(void)
{
return arena.total;
}

124
kernel/core/kalloc.c Normal file
View file

@ -0,0 +1,124 @@
// Physical memory allocator, intended to allocate
// memory for user processes, kernel stacks, page table pages,
// and pipe buffers. Allocates 4096-byte pages.
#include "types.h"
#include "defs.h"
#include "param.h"
#include "memlayout.h"
#include "mmu.h"
#include "spinlock.h"
void freerange(void *vstart, void *vend);
extern char end[]; // first address after kernel loaded from ELF file
struct run {
struct run *next;
};
struct {
struct spinlock lock;
int use_lock;
struct run *freelist;
} kmem;
// Catch kmem.freelist corruption at the point it's touched, rather than
// letting a bad pointer be silently returned by kalloc() and crash whatever
// code dereferences it minutes later on an unrelated call stack.
static void
check_run(struct run *r, char *where)
{
if(r == 0)
return;
if((addr_t)r % PGSIZE || (char*)r < end || V2P(r) >= PHYSTOP){
cprintf("kalloc: corrupt freelist entry %p at %s\n", r, where);
panic("kalloc: corrupt freelist");
}
}
void
kinit1(void *vstart, void *vend)
{
initlock(&kmem.lock, "kmem");
kmem.use_lock = 0;
kmem.freelist = 0; // empty
freerange(vstart, vend);
}
void
kinit2()
{
kmem.use_lock = 1;
}
void
freerange(void *vstart, void *vend)
{
char *p;
p = (char*)PGROUNDUP((addr_t)vstart);
for(; p + PGSIZE <= (char*)vend; p += PGSIZE)
kfree(p);
}
// Free the page of physical memory pointed at by v,
// which normally should have been returned by a
// call to kalloc(). (The exception is when
// initializing the allocator; see kinit above.)
void
kfree(char *v)
{
struct run *r;
if((addr_t)v % PGSIZE || v < end || V2P(v) >= PHYSTOP)
panic("kfree");
// Fill with junk to catch dangling refs.
memset(v, 1, PGSIZE);
if(kmem.use_lock)
acquire(&kmem.lock);
check_run(kmem.freelist, "kfree entry");
r = (struct run*)v;
r->next = kmem.freelist;
kmem.freelist = r;
if(kmem.use_lock)
release(&kmem.lock);
}
// Allocate one 4096-byte page of physical memory.
// Returns a pointer that the kernel can use.
// Returns 0 if the memory cannot be allocated.
char*
kalloc(void)
{
struct run *r;
if(kmem.use_lock)
acquire(&kmem.lock);
check_run(kmem.freelist, "kalloc entry");
r = kmem.freelist;
if(r)
kmem.freelist = r->next;
if(kmem.use_lock)
release(&kmem.lock);
return (char*)r;
}
// Total free physical memory, in bytes. Walks the free list; only /proc
// readers call it, so the O(n) scan is not on any hot path.
uint64
kalloc_free_bytes(void)
{
struct run *r;
uint64 n = 0;
if(kmem.use_lock)
acquire(&kmem.lock);
for(r = kmem.freelist; r; r = r->next)
n += PGSIZE;
if(kmem.use_lock)
release(&kmem.lock);
return n;
}

231
kernel/core/main.c Normal file
View file

@ -0,0 +1,231 @@
#include "types.h"
#include "defs.h"
#include "param.h"
#include "memlayout.h"
#include "mmu.h"
#include "proc.h"
#include "x86.h"
#include "irq.h"
#include "dma.h"
static void startothers(void);
static void mpmain(void) __attribute__((noreturn));
extern char end[]; // first address after kernel loaded from ELF file
// Set by stage2main.c (boot/stage2main.c, BOOT_VERBOSE_FLAG_ADDR) when the
// user picks "Boot Schunix (verbose)" from the F8 boot menu; read once here,
// before anything else, so subsystem init can log its own steps.
int boot_verbose;
// Must match BOOT_VERBOSE_FLAG_ADDR in boot/stage2main.c exactly: just past
// the VBE info block, not 0x7000 (startothers() below uses that address for
// the AP boot trampoline).
#define BOOT_VERBOSE_FLAG_ADDR (0x30000 + 0x100)
// Bootstrap processor starts running C code here.
// Allocate a real stack and switch to it, first
// doing some setup required for memory allocator to work.
int
kmain(uint32 fb_addr)
{
boot_verbose = *(uchar*)P2V(BOOT_VERBOSE_FLAG_ADDR);
uartearlyinit();
if (boot_verbose) cprintf("verbose boot: uartearlyinit done\n");
// The DMA arena is carved off the top of physical memory *before* kalloc is
// given the rest, because a device needs physically contiguous memory and
// kalloc's free list makes no such promise -- two pages from it are adjacent
// only by chance. Taking it first is the only way to be sure the region
// exists; asking for it later would mean asking kalloc for something it
// fundamentally cannot provide. See include/dma.h.
// Before anything touches PHYSTOP, which is now a variable read from the
// firmware's memory map rather than a constant asserting 224 MB exists.
e820_init();
kinit1(end, P2V(PHYSTOP - DMA_ARENA_BYTES));
if (boot_verbose) cprintf("verbose boot: kinit1 done\n");
dma_init(P2V(PHYSTOP - DMA_ARENA_BYTES), P2V(PHYSTOP));
if (boot_verbose) cprintf("verbose boot: dma_init done\n");
kvmalloc();
if (boot_verbose) cprintf("verbose boot: kvmalloc done\n");
// ACPI first, the legacy MP table only if it finds nothing. Modern firmware
// publishes a complete MADT and an MP table with one processor in it, so
// asking the older interface first means never seeing more than one CPU.
// See kernel/arch/x86_64/acpi.c.
if (!acpi_init())
mpinit();
if (boot_verbose) cprintf("verbose boot: cpu enumeration done\n");
lapicinit();
if (boot_verbose) cprintf("verbose boot: lapicinit done\n");
tvinit();
if (boot_verbose) cprintf("verbose boot: tvinit done\n");
seginit();
if (boot_verbose) cprintf("verbose boot: seginit done\n");
ioapicinit();
if (boot_verbose) cprintf("verbose boot: ioapicinit done\n");
irq_init(); // must precede any driver init that registers a handler
if (boot_verbose) cprintf("verbose boot: irq_init done\n");
// Before the keyboard and mouse, which start producing events the moment
// their handlers are registered.
inputinit();
if (boot_verbose) cprintf("verbose boot: inputinit done\n");
consoleinit();
if (boot_verbose) cprintf("verbose boot: consoleinit done\n");
uartinit();
if (boot_verbose) cprintf("verbose boot: uartinit done\n");
mouseinit();
if (boot_verbose) cprintf("verbose boot: mouseinit done\n");
vga_init(fb_addr);
if (boot_verbose) cprintf("verbose boot: vga_init done (fb=0x%x)\n", fb_addr);
// No splash drawing here on purpose. The loader owns the splash, logo and
// all, and vga_init() deliberately does not clear -- so it is still on the
// display right through to console_takeover(). Painting it again from here
// would land in the last few hundred milliseconds of boot and change nothing.
// After vga_init, because the display driver cross-checks the framebuffer
// address it finds on the bus against the one VBE reported -- and it can
// only do that once vga.c has read the VBE block.
// The devices that are simply known to be on a PC rather than enumerated by
// any bus. Registered before pci_init() so that /proc/devices lists them
// first, which puts the machine's fixed furniture above whatever happened to
// be plugged into it.
platform_register_devices();
pci_init();
if (boot_verbose) cprintf("verbose boot: pci_init done\n");
pinit();
if (boot_verbose) cprintf("verbose boot: pinit done\n");
binit();
if (boot_verbose) cprintf("verbose boot: binit done\n");
fileinit();
if (boot_verbose) cprintf("verbose boot: fileinit done\n");
sockinit();
if (boot_verbose) cprintf("verbose boot: sockinit done\n");
// After the timer is running, so boot_epoch can subtract the uptime so far,
// and before userspace, so the very first timestamp any program takes is
// already real. Reading the RTC is slow, which is exactly why it happens once
// here rather than on every gettimeofday().
time_init();
ideinit();
if (boot_verbose) cprintf("verbose boot: ideinit done\n");
ramdiskinit();
if (boot_verbose) cprintf("verbose boot: ramdiskinit done\n");
// Start the application processors.
//
// This was broken from the beginning of the project and is fixed as of
// 2026-08-08. Five distinct bugs, each hidden behind the one before it, and
// all of them invisible under QEMU because it publishes only one processor in
// the legacy MP table -- see kernel/arch/x86_64/acpi.c. VMware published the
// real count, which is why the triple fault only ever appeared there.
//
// 1. entry64high had no AP branch at all; the check was a comment saying it
// had never been written. Every AP fell into the boot processor's path,
// took the BSP's stack, and ran kmain() a second time.
// 2. seginit() called kalloc() for its per-CPU page. kalloc locks, locking
// reads `cpu`, and `cpu` is what seginit() was about to set up.
// 3. kernel_fs_base was one global for all CPUs, and trapasm.S read it on
// every entry from user mode, so both CPUs shared one per-CPU block.
// 4. `cpu` and `proc` were __thread. That means per *thread*, so the compiler
// cached the thread pointer across calls -- but %fs here is per *CPU*, and
// a kernel thread changes CPU at every sched(). See include/proc.h.
// 5. The new per-CPU slots collided with syscall_entry's scratch word at
// %fs:(8), which is the same memory as %gs:8.
//
int smp_start_aps = 1;
if (smp_start_aps) {
startothers();
if (boot_verbose) cprintf("verbose boot: startothers done\n");
} else if (ncpu > 1) {
cprintf("smp: %d cpus found, running on 1 (AP startup is broken)\n", ncpu);
}
kinit2();
if (boot_verbose) cprintf("verbose boot: kinit2 done\n");
sinit();
if (boot_verbose) cprintf("verbose boot: sinit done\n");
mpmain();
}
// Other CPUs jump here from entryother.S.
void
mpenter(void)
{
switchkvm();
seginit();
lapicinit();
mpmain();
}
// Enable SSE/FXSAVE so user binaries can use XMM registers.
static void
enable_sse(void)
{
uint64 cr0, cr4;
asm volatile("mov %%cr0, %0" : "=r"(cr0));
cr0 &= ~(1UL << 2); // clear CR0.EM (no FP emulation)
cr0 |= (1UL << 1); // set CR0.MP (monitor coprocessor)
asm volatile("mov %0, %%cr0" :: "r"(cr0));
asm volatile("mov %%cr4, %0" : "=r"(cr4));
cr4 |= (1UL << 9); // CR4.OSFXSR — OS handles FXSAVE/FXRSTOR
cr4 |= (1UL << 10); // CR4.OSXMMEXCPT — OS handles SSE exceptions
asm volatile("mov %0, %%cr4" :: "r"(cr4));
}
// Common CPU setup code.
static void
mpmain(void)
{
cprintf("cpu%d: starting\n", cpunum());
enable_sse(); // allow user XMM/SSE instructions
idtinit(); // load idt register
syscallinit(); // syscall set up
xchg(&mycpu()->started, 1); // tell startothers() we're up
scheduler(); // start running processes
}
void entry32mp(void);
// Start the non-boot (AP) processors.
static void
startothers(void)
{
extern uchar _binary_entryother_start[], _binary_entryother_size[];
uchar *code;
struct cpu *c;
char *stack;
// Write entry code to unused memory at 0x7000.
// The linker has placed the image of entryother.S in
// _binary_entryother_start.
code = P2V(0x7000);
memmove(code, _binary_entryother_start,
(addr_t)_binary_entryother_size);
for(c = cpus; c < cpus+ncpu; c++){
if(c == cpus+cpunum()) // We've started already.
continue;
// Tell entryother.S what stack to use, where to enter, and what
// pgdir to use. We cannot use kpgdir yet, because the AP processor
// is running in low memory, so we use entrypgdir for the APs too.
// The AP's per-CPU page, allocated here rather than by the AP itself.
// seginit() explains why: an AP cannot call kalloc() before %fs is set up,
// because kalloc locks and locking reads the per-CPU pointer.
if (c->local == 0) {
c->local = kalloc();
memset(c->local, 0, PGSIZE);
}
stack = kalloc();
*(uint32*)(code-4) = 0x8000; // enough stack to get us to entry64mp
*(uint32*)(code-8) = v2p(entry32mp);
*(uint64*)(code-16) = (uint64) (stack + KSTACKSIZE);
lapicstartap(c->apicid, V2P(code));
// wait for cpu to finish mpmain()
while(c->started == 0)
;
// Counted here, on the boot processor, after the AP has actually reported
// in -- not by the AP itself. This is serialised by the loop, so it needs
// no atomic, and it cannot overcount a processor that started and died.
ncpu_online++;
}
}

543
kernel/core/panic.c Normal file
View file

@ -0,0 +1,543 @@
// SPDX-License-Identifier: Apache-2.0
//
// PSOGM — the Schunix crash screen.
//
// The name is a nod to the Amiga's "Guru Meditation", but nothing else about
// this is a nod to 1985. Three things it is built around:
//
// *It does not clear the screen.* Whatever was being displayed stays there,
// dimmed and pushed back, with the report floating over it. A crash screen that
// wipes the display tells you the machine is gone; one that leaves your work
// visible underneath tells you it stopped, which is both truer and much less
// alarming.
//
// *It is not frightening.* No red, no capitals, no "FATAL". A calm sentence, a
// face that is obviously a cartoon, and then the facts. Whoever is reading this
// is already having a bad time.
//
// *It says considerably more than it needs to.* The screen is the only artefact
// of a kernel panic — there is no core dump and no log to go back to — so
// everything cheap to collect is collected: which CPU, which process, how far
// into the boot, where the kernel is loaded, and a call chain. Anything left
// out here is gone.
//
// Everything is also written to the serial console, in the same order. That is
// what tools/bootcheck.ps1 reads, and it is what you get when the crash happens
// before there is a framebuffer at all.
#include "types.h"
#include "defs.h"
#include "param.h"
#include "memlayout.h"
#include "mmu.h"
#include "x86.h"
#include "proc.h"
#include "spinlock.h"
// Purple, because the name says so, and white on it because nothing else is
// legible at every brightness a monitor might be set to. The palette lives in
// theme.h with everything else; this file used to keep its own copy under the
// same names but with different values, which meant theme.h described a crash
// screen nobody had ever seen.
#include "theme.h"
#include "qr.h"
extern uint ticks;
extern int panicked;
/* ------------------------------------------------------------------ */
/* Small formatting helpers */
/* */
/* cprintf goes to the console, which during a panic is also the screen */
/* being painted over -- so the panel builds its strings itself and */
/* places them with vga_text_at. */
/* ------------------------------------------------------------------ */
static char *
put_str(char *p, char *end, const char *s)
{
while (*s && p < end - 1)
*p++ = *s++;
*p = '\0';
return p;
}
static char *
put_dec(char *p, char *end, uint64 v)
{
char tmp[24];
int i = 0;
if (v == 0)
tmp[i++] = '0';
while (v && i < (int)sizeof(tmp)) { tmp[i++] = '0' + (v % 10); v /= 10; }
while (i-- && p < end - 1)
*p++ = tmp[i];
*p = '\0';
return p;
}
static char *
put_hex(char *p, char *end, uint64 v, int digits)
{
const char *d = "0123456789abcdef";
if (p < end - 2) { *p++ = '0'; *p++ = 'x'; }
for (int i = digits - 1; i >= 0 && p < end - 1; i--)
*p++ = d[(v >> (i * 4)) & 0xF];
*p = '\0';
return p;
}
/* ------------------------------------------------------------------ */
/* The face */
/* */
/* ":(" — two dots and a parenthesis, drawn large. It is a sad face */
/* only if you tilt your head, which is exactly why it works: it is */
/* legible as a mood without being a picture of anything, and it does */
/* not pretend the machine has feelings about what just happened. */
/* */
/* Drawn from primitives rather than stored as a bitmap, so it scales */
/* with the display and costs no space in a kernel image that stage2 */
/* loads one sector at a time by PIO. */
/* ------------------------------------------------------------------ */
static int
isqrt(int v)
{
if (v <= 0)
return 0;
// Newton's method, iterated only while the estimate is still *decreasing*.
//
// The obvious "stop when it stops changing" version does not terminate: on a
// non-square it settles into a two-cycle rather than a fixed point, and
// isqrt(3) alternates between 1 and 2 forever. In a panic handler that is a
// hang with the screen half drawn and no way to find out why.
int x = v;
int y = (x + 1) / 2;
while (y < x) {
x = y;
y = (x + v / x) / 2;
}
return x;
}
// The "(" of ":(" — the left half of a squashed circle, so the tips point right
// and the middle bulges left, the way the glyph does.
static void
draw_paren(int cx, int cy, int h, int thick, uint32 color)
{
int r = h / 2;
for (int dy = -r; dy <= r; dy++) {
int dx = isqrt(r * r - dy * dy);
// Narrow it: a true half-circle reads as a bracket, not a parenthesis.
int x = cx - (dx * 45) / 100;
vga_fill_circle(x, cy + dy, thick / 2, color);
}
}
static void
draw_sad_face(int x, int y, int h, uint32 color)
{
int dot = h / 12;
int gap = h / 4;
// The colon.
vga_fill_circle(x + dot, y + h / 2 - gap, dot, color);
vga_fill_circle(x + dot, y + h / 2 + gap, dot, color);
// The parenthesis, a little to the right of it.
draw_paren(x + dot * 2 + h / 2, y + h / 2, h, dot * 2, color);
}
/* ------------------------------------------------------------------ */
/* The report */
/* ------------------------------------------------------------------ */
struct field {
const char *label;
char value[72];
};
#define NFIELD 10
static struct field fields[NFIELD];
static int nfields;
static struct field *
field(const char *label)
{
if (nfields >= NFIELD)
return &fields[NFIELD - 1];
struct field *f = &fields[nfields++];
f->label = label;
f->value[0] = '\0';
return f;
}
// Collect everything worth knowing while the machine is still in the state
// that produced the fault. Interrupts are already off and no lock is taken:
// a panic that deadlocks trying to report itself tells you nothing at all.
// A short, shoutable name for the failure, derived from the panic message
// rather than declared at every call site.
//
// Windows stop codes are genuinely useful: they are the thing you search for,
// and they stay the same across builds while the prose around them changes.
// Taking the message up to its first colon (or its first few words) gets that
// for free -- "sysrq: crash requested" becomes SYSRQ, "short filewrite" becomes
// SHORT_FILEWRITE -- with no risk of a call site forgetting to supply one.
static void
stop_code(char *out, int cap, const char *reason)
{
int n = 0;
for (int i = 0; reason[i] && n < cap - 1; i++) {
char c = reason[i];
if (c == ':')
break;
if (c == ' ' || c == '\t') {
if (n == 0 || out[n-1] == '_')
continue;
out[n++] = '_';
continue;
}
if (c >= 'a' && c <= 'z') c = c - 'a' + 'A';
if ((c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_')
out[n++] = c;
}
while (n > 0 && out[n-1] == '_')
n--;
if (n == 0)
{ out[n++] = 'P'; out[n++] = 'A'; out[n++] = 'N'; out[n++] = 'I'; out[n++] = 'C'; }
out[n] = '\0';
}
static void
gather(char *reason, addr_t *pcs, int npcs)
{
char *e;
struct field *f;
nfields = 0;
f = field("Stop code");
stop_code(f->value, sizeof(f->value), reason);
f = field("Reason");
put_str(f->value, f->value + sizeof(f->value), reason);
f = field("CPU");
e = f->value + sizeof(f->value);
put_dec(f->value, e, mycpu() ? (uint64)mycpu()->id : 0);
if (!mycpu())
put_str(f->value, e, "unknown (very early boot)");
f = field("Process");
e = f->value + sizeof(f->value);
if (myproc()) {
char *p = put_str(f->value, e, myproc()->name);
p = put_str(p, e, " pid ");
put_dec(p, e, (uint64)myproc()->pid);
} else {
put_str(f->value, e, "none -- in the kernel, not on anybody's behalf");
}
f = field("Uptime");
e = f->value + sizeof(f->value);
{
char *p = put_dec(f->value, e, (uint64)ticks);
p = put_str(p, e, " ticks (about ");
p = put_dec(p, e, (uint64)ticks / 100);
put_str(p, e, " s)");
}
// Every address below is a link-time address. Printing where the kernel
// actually sits is what lets addr2line resolve them afterwards, and it is
// the single most useful line here when the trace is all you have.
f = field("Kernel base");
e = f->value + sizeof(f->value);
put_hex(f->value, e, (uint64)KERNBASE, 16);
f = field("Fault PC");
e = f->value + sizeof(f->value);
if (npcs > 0 && pcs[0])
put_hex(f->value, e, (uint64)pcs[0], 16);
else
put_str(f->value, e, "unavailable");
}
/* ------------------------------------------------------------------ */
/* The QR code */
/* */
/* A panic report is only worth what can be carried away from it, and */
/* at this point there is no network, no log to write to, and often no */
/* second machine on the desk. Photographing the screen works but */
/* turns a call chain into something that has to be retyped by hand, */
/* with sixteen hex digits per frame and no way to tell 8 from B. */
/* */
/* A phone camera is a working out-of-band channel that needs no */
/* driver on our side at all. See kernel/core/qr.c. */
/* ------------------------------------------------------------------ */
// Deliberately not a URL. This encodes the report itself, so it goes wherever
// the person holding the phone decides -- and reaches nothing on its own.
//
// Compact because the budget is 134 bytes for the largest symbol this encoder
// builds, and a crash with a deep call chain is exactly when the budget is
// tightest. Two decisions buy most of the room: frames are stored as offsets
// from the kernel base rather than absolute addresses, which halves 16 hex
// digits to 8, and the fields are single letters. The base is included, so
// nothing is lost -- addr2line wants the link-time address and can be given it.
//
// Fields are appended in priority order and the loop simply stops when the
// buffer is full, so a long reason string costs call-chain depth rather than
// producing a report that will not encode.
static int
build_qr_payload(char *buf, int cap, char *reason, addr_t *pcs, int npcs)
{
char *p = buf, *end = buf + cap;
// Magic and format version, so whatever reads this later can tell what it is
// looking at and can change the layout without ambiguity.
p = put_str(p, end, "SXPSOGM1\n");
// The reason, not the stop code: the stop code is derived from the reason by
// stop_code() and carries strictly less, so encoding both would spend bytes
// on something the reader can recompute.
//
// Held back from the end of the buffer so that a long message costs some of
// the call chain but never all of it. A report with a sentence and no
// addresses is much harder to act on than one with addresses and a truncated
// sentence.
{
char *reason_end = end - 34; // three frames and their separators
if (reason_end > p)
p = put_str(p, reason_end, reason);
}
p = put_str(p, end, "\nc");
p = put_dec(p, end, mycpu() ? (uint64)mycpu()->id : 0);
p = put_str(p, end, " p");
p = put_dec(p, end, myproc() ? (uint64)myproc()->pid : 0);
p = put_str(p, end, " t");
p = put_dec(p, end, (uint64)ticks);
if (myproc()) {
p = put_str(p, end, " ");
p = put_str(p, end, myproc()->name);
}
p = put_str(p, end, "\nb");
p = put_hex(p, end, (uint64)KERNBASE, 16);
p = put_str(p, end, "\n");
for (int i = 0; i < npcs && pcs[i]; i++) {
// Nine bytes: eight hex digits and the separator. Stopping here rather than
// letting put_hex truncate matters -- half an address in a crash report is
// worse than one fewer address, because it looks like a real one.
if (end - p < 10)
break;
if (i)
p = put_str(p, end, " ");
// put_hex prefixes "0x", which is 14 wasted bytes across seven frames.
{
const char *d = "0123456789abcdef";
uint64 off = (uint64)pcs[i] - (uint64)KERNBASE;
for (int k = 7; k >= 0; k--)
*p++ = d[(off >> (k * 4)) & 0xF];
*p = '\0';
}
}
return (int)(p - buf);
}
// One static symbol rather than a local: struct qr is about 3.4 KB and the
// panic path runs on whatever stack the fault left behind, which may not have
// that much room. It is only ever touched here, after everything else has
// stopped.
static struct qr panic_qr;
// Returns the width used, so the text column knows what it may not overlap.
static int
draw_qr_panel(int sw, int sh, int ch, char *reason, addr_t *pcs, int npcs)
{
char payload[QR_MAX_BYTES];
int n = build_qr_payload(payload, sizeof(payload), reason, pcs, npcs);
if (n <= 0 || qr_encode(&panic_qr, (unsigned char *)payload, n) != 0)
return 0;
// Four modules of quiet zone, which the standard requires and which decoders
// genuinely need -- without it many will not find the symbol at all.
const int quiet = 4;
int cells = panic_qr.size + 2 * quiet;
// Scale to about a quarter of the display, then clamp. The lower bound is
// what a phone can still resolve from a photograph of a monitor; the upper
// bound stops the thing dominating a screen whose actual job is the text.
int scale = (sw / 4) / cells;
if (scale < 3) scale = 3;
if (scale > 8) scale = 8;
int card = cells * scale;
int x0 = sw - card - sw / 24;
int y0 = sh / 8;
vga_fill_rect(x0, y0, card, card, PSOGM_CARD);
for (int y = 0; y < panic_qr.size; y++)
for (int x = 0; x < panic_qr.size; x++)
if (panic_qr.mod[y][x])
vga_fill_rect(x0 + (x + quiet) * scale, y0 + (y + quiet) * scale,
scale, scale, PSOGM_INK);
vga_text_at(x0, y0 + card + ch, "Scan for a text copy", PSOGM_DIM);
vga_text_at(x0, y0 + card + ch * 2 + 2, "of this report.", PSOGM_DIM);
return card + sw / 24;
}
// The frame pointers, one per line, as one string per row.
static void
trace_line(char *buf, int cap, int i, addr_t pc)
{
char *e = buf + cap;
char *p = put_str(buf, e, " #");
p = put_dec(p, e, (uint64)i);
p = put_str(p, e, i < 10 ? " " : " ");
put_hex(p, e, (uint64)pc, 16);
}
/* ------------------------------------------------------------------ */
static void
paint(char *reason, addr_t *pcs, int npcs)
{
int sw, sh;
vga_dims(&sw, &sh);
int ch = vga_char_h();
int cw = vga_char_w();
// The whole display, one colour. An earlier version dimmed the previous
// screen and floated a panel over it, which looked good and was wrong: the
// thing is called a Purple Screen, and half a screen of somebody's abandoned
// work behind the report is a distraction at the worst possible moment.
vga_clear(PSOGM_BG);
// Glyphs are drawn with their background filled, so the console's cream has
// to become the background or every line of text arrives in its own box.
vga_set_bg(PSOGM_BG);
// The symbol goes first, because it decides how much width the text has. It
// is drawn before anything can fail: if the encoder refuses the payload the
// screen simply has no card and the report is unchanged.
draw_qr_panel(sw, sh, ch, reason, pcs, npcs);
int left = sw / 8;
if (left > 110) left = 110;
int y = sh / 8;
int face_h = 96;
draw_sad_face(left, y, face_h, PSOGM_FG);
y += face_h + ch * 2;
// Left-aligned, not centred. Centred text reads as a poster; this is a
// report, and a report is read down the left edge.
vga_text_at(left, y, "Schunix ran into a problem and needs to restart.",
PSOGM_FG);
y += ch + ch / 2;
vga_text_at(left, y, "Nothing on your disks was touched by this.",
PSOGM_DIM);
// A rule in the boot splash's accent, the same width as the one under the
// wordmark there, tying this screen to the one the machine started with.
//
// Equal space above and below on purpose: at any less it sits close enough to
// the line above to read as an underline of that sentence rather than as a
// divider before the facts.
y += ch * 2;
vga_fill_rect(left, y, 120, 2, SCH_ACCENT);
y += ch;
// Facts: quiet labels down the left, bright values in a second column.
int vx = left + 14 * cw;
for (int i = 0; i < nfields; i++) {
vga_text_at(left, y, fields[i].label, PSOGM_DIM);
vga_text_at(vx, y, fields[i].value, PSOGM_FG);
y += ch + 2;
}
y += ch;
vga_text_at(left, y, "Call chain", PSOGM_DIM);
y += ch + 2;
for (int i = 0; i < npcs && pcs[i]; i++) {
char line[48];
trace_line(line, sizeof(line), i, pcs[i]);
vga_text_at(vx - 3 * cw, y, line, PSOGM_FG);
y += ch;
if (y > sh - ch * 5)
break;
}
// Two closing lines, bottom left: what to do with the numbers above, and
// what this screen is called, so it can be searched for by name. Shortened
// from the full command line, which at 62 characters ran under the QR card.
vga_text_at(left, sh - ch * 3,
"Resolve: addr2line -e kernel <address>", PSOGM_DIM);
vga_text_at(left, sh - ch * 3 + ch + 4,
"PSOGM -- Purple Screen of Guru Meditation", PSOGM_DIM);
}
// Same report, down the serial line. This is the copy that survives: it is what
// bootcheck reads, and the only one that exists when the framebuffer does not.
static void
report_serial(char *reason, addr_t *pcs, int npcs)
{
cprintf("\n");
cprintf(" :(\n\n");
cprintf(" Schunix ran into a problem and needs to restart.\n");
cprintf(" PSOGM -- Purple Screen of Guru Meditation\n\n");
for (int i = 0; i < nfields; i++) {
// Pad the label out so the values line up, without a printf width
// specifier -- cprintf has no such thing.
int n = 0;
while (fields[i].label[n]) n++;
cprintf(" %s", fields[i].label);
for (int pad = n; pad < 14; pad++)
cprintf(" ");
cprintf("%s\n", fields[i].value);
}
cprintf("\n Call chain\n");
for (int i = 0; i < npcs && pcs[i]; i++)
cprintf(" #%d %p\n", i, pcs[i]);
cprintf("\n Resolve with: addr2line -e build/kernel/kernel <address>\n");
cprintf(" Reason: %s\n", reason);
cprintf(" System halted.\n");
}
__attribute__((noreturn))
void
panic(char *s)
{
addr_t pcs[10];
cli();
// Stop every other path from writing to the console. consputc checks this
// and spins, which is what keeps another CPU from painting over the report
// while it is being drawn.
cons_panic_mode();
for (int i = 0; i < 10; i++)
pcs[i] = 0;
getcallerpcs(&s, pcs);
gather(s, pcs, 10);
report_serial(s, pcs, 10);
if (vga_ready())
paint(s, pcs, 10);
panicked = 1;
for (;;)
hlt();
}

456
kernel/core/qr.c Normal file
View file

@ -0,0 +1,456 @@
// QR encoder. See include/qr.h for why it exists and why it is this small.
//
// Freestanding on purpose: no allocation, no floating point, no libc beyond
// memset/memmove. It has to run in panic context, where the heap may be exactly
// what is broken. Every buffer here is either on the caller's struct or a
// bounded local.
//
// The structure follows ISO/IEC 18004 in the order the standard describes it,
// which is also the only order in which any of it can be checked:
//
// 1. encode the payload into data codewords
// 2. append Reed-Solomon error correction, per block
// 3. interleave the blocks
// 4. draw the function patterns (finders, timing, alignment, format area)
// 5. lay the codeword bits into the remaining modules in a zigzag
// 6. try all 8 masks, score each, keep the best
//
// Verified by scanning the output with a phone, which is the only test that
// matters and is also literally the feature.
#ifdef QR_HOST
#include <string.h>
#else
#include "types.h"
#include "defs.h"
#endif
#include "qr.h"
// ---------------------------------------------------------------------------
// Version table
// ---------------------------------------------------------------------------
//
// Per version, at error correction level L: total codewords, error correction
// codewords per block, and number of blocks. These are from the standard's
// tables and cannot be derived, so they are the one part of this file that has
// to be right by transcription rather than by reasoning.
//
// A useful check on them: total - ecc*blocks must be divisible by blocks for
// every row below, because levels 1..6 at L happen to have equally sized
// blocks. qr_encode() asserts exactly that at runtime, which is what turns a
// mistyped number here into a visible failure instead of an unscannable symbol.
static const struct {
short total; // total codewords in the symbol
unsigned char ecc; // EC codewords per block
unsigned char blocks; // number of RS blocks
} vinfo[QR_MAX_VERSION + 1] = {
{ 0, 0, 0 }, // version 0 does not exist; keeps indexing natural
{ 26, 7, 1 },
{ 44, 10, 1 },
{ 70, 15, 1 },
{ 100, 20, 1 },
{ 134, 26, 1 },
{ 172, 18, 2 },
};
int
qr_capacity(int version)
{
if (version < 1 || version > QR_MAX_VERSION)
return 0;
// Data codewords, less the two bytes of framing: a 4-bit mode indicator, an
// 8-bit character count (versions 1-9), and a 4-bit terminator come to
// exactly two codewords' worth of overhead.
int data = vinfo[version].total - vinfo[version].ecc * vinfo[version].blocks;
return data - 2;
}
// ---------------------------------------------------------------------------
// GF(256) and Reed-Solomon
// ---------------------------------------------------------------------------
// Multiply in GF(256) modulo x^8 + x^4 + x^3 + x^2 + 1 (0x11D).
//
// Done by shift-and-add rather than log/exp tables. Two 256-byte tables plus
// their initialisation is more code than this loop, and this needs no init step
// at all -- which matters when the caller is a panic handler that cannot assume
// any particular earlier function ran.
static unsigned char
gmul(unsigned char x, unsigned char y)
{
unsigned char z = 0;
for (int i = 7; i >= 0; i--) {
z = (unsigned char)((z << 1) ^ ((z >> 7) * 0x1D));
z ^= (unsigned char)(((y >> i) & 1) * x);
}
return z;
}
// Coefficients of the degree-`degree` generator polynomial, highest power
// first, with the leading 1 left implicit.
static void
rs_divisor(int degree, unsigned char *result)
{
memset(result, 0, degree);
result[degree - 1] = 1; // start at the monomial x^0
// Multiply by (x - r) for r = 1, a, a^2, ... Each pass shifts the polynomial
// up one degree and folds in the new root.
unsigned char root = 1;
for (int i = 0; i < degree; i++) {
for (int j = 0; j < degree; j++) {
result[j] = gmul(result[j], root);
if (j + 1 < degree)
result[j] ^= result[j + 1];
}
root = gmul(root, 0x02);
}
}
// The remainder of data[] divided by the generator: the EC codewords.
static void
rs_remainder(const unsigned char *data, int len,
const unsigned char *gen, int degree, unsigned char *result)
{
memset(result, 0, degree);
for (int i = 0; i < len; i++) {
unsigned char factor = data[i] ^ result[0];
memmove(&result[0], &result[1], degree - 1);
result[degree - 1] = 0;
for (int j = 0; j < degree; j++)
result[j] ^= gmul(gen[j], factor);
}
}
// ---------------------------------------------------------------------------
// Module access
// ---------------------------------------------------------------------------
static void
set_fn(struct qr *q, int x, int y, int dark)
{
if (x < 0 || y < 0 || x >= q->size || y >= q->size)
return;
q->mod[y][x] = (unsigned char)(dark != 0);
q->fn[y][x] = 1;
}
static int
absi(int v)
{
return v < 0 ? -v : v;
}
static int
cheb(int dx, int dy)
{
int ax = absi(dx), ay = absi(dy);
return ax > ay ? ax : ay;
}
// ---------------------------------------------------------------------------
// Function patterns
// ---------------------------------------------------------------------------
// A finder plus its separator, as one 9x9 stamp centred on (cx, cy).
//
// Writing it by Chebyshev distance rather than as a literal bitmap is not
// cleverness for its own sake: the concentric rings *are* the definition -- dark
// 3x3 core, light ring, dark ring, light separator -- and the edge stamps run
// off the symbol, which set_fn() clips.
static void
draw_finder(struct qr *q, int cx, int cy)
{
for (int dy = -4; dy <= 4; dy++)
for (int dx = -4; dx <= 4; dx++) {
int d = cheb(dx, dy);
set_fn(q, cx + dx, cy + dy, d != 2 && d != 4);
}
}
// The 5x5 alignment pattern: dark 3x3 outline with a light ring inside.
static void
draw_alignment(struct qr *q, int cx, int cy)
{
for (int dy = -2; dy <= 2; dy++)
for (int dx = -2; dx <= 2; dx++)
set_fn(q, cx + dx, cy + dy, cheb(dx, dy) != 1);
}
// The 15-bit format information, twice, plus the always-dark module.
//
// Called once with the real mask at the end, and once early with mask 0 purely
// to *reserve* these modules -- the zigzag below skips anything marked as a
// function pattern, so the reservation has to exist before data is placed.
static void
draw_format(struct qr *q, int mask)
{
// 0b01 is error correction level L. Five data bits, then a BCH(15,5) check
// computed with generator 0x537, then XOR 0x5412 so an all-zero format never
// produces an all-zero pattern.
int data = (1 << 3) | mask;
int rem = data;
for (int i = 0; i < 10; i++)
rem = (rem << 1) ^ ((rem >> 9) * 0x537);
int bits = ((data << 10) | rem) ^ 0x5412;
int n = q->size;
// First copy: down the left of the top-right finder, then left along the top
// of the bottom-left one. The 6/7 and 7/8 jogs skip the timing pattern.
for (int i = 0; i <= 5; i++)
set_fn(q, 8, i, (bits >> i) & 1);
set_fn(q, 8, 7, (bits >> 6) & 1);
set_fn(q, 8, 8, (bits >> 7) & 1);
set_fn(q, 7, 8, (bits >> 8) & 1);
for (int i = 9; i < 15; i++)
set_fn(q, 14 - i, 8, (bits >> i) & 1);
// Second copy, so the format survives damage to either corner.
for (int i = 0; i < 8; i++)
set_fn(q, n - 1 - i, 8, (bits >> i) & 1);
for (int i = 8; i < 15; i++)
set_fn(q, 8, n - 15 + i, (bits >> i) & 1);
set_fn(q, 8, n - 8, 1); // the dark module, always set, always here
}
static void
draw_function_patterns(struct qr *q)
{
int n = q->size;
// Timing patterns first: the finders overwrite their ends, which is correct.
for (int i = 0; i < n; i++) {
set_fn(q, 6, i, i % 2 == 0);
set_fn(q, i, 6, i % 2 == 0);
}
draw_finder(q, 3, 3);
draw_finder(q, n - 4, 3);
draw_finder(q, 3, n - 4);
// For versions 2-6 the alignment centres are {6, n-7}, and three of those
// four combinations collide with a finder. Only the bottom-right survives,
// which is why there is no position table here. Version 7 would need one.
if (q->size > 21)
draw_alignment(q, n - 7, n - 7);
draw_format(q, 0); // reserve; overwritten once a mask is chosen
}
// ---------------------------------------------------------------------------
// Masking
// ---------------------------------------------------------------------------
static int
mask_bit(int mask, int x, int y)
{
switch (mask) {
case 0: return (x + y) % 2 == 0;
case 1: return y % 2 == 0;
case 2: return x % 3 == 0;
case 3: return (x + y) % 3 == 0;
case 4: return (x / 3 + y / 2) % 2 == 0;
case 5: return x * y % 2 + x * y % 3 == 0;
case 6: return (x * y % 2 + x * y % 3) % 2 == 0;
case 7: return ((x + y) % 2 + x * y % 3) % 2 == 0;
}
return 0;
}
static void
apply_mask(struct qr *q, int mask)
{
for (int y = 0; y < q->size; y++)
for (int x = 0; x < q->size; x++)
if (!q->fn[y][x])
q->mod[y][x] ^= (unsigned char)mask_bit(mask, x, y);
}
// The standard's four penalty rules. Lower is better.
//
// A decoder will read any legal mask, so this is not correctness -- it is scan
// reliability, which is the whole point when the reader is a handheld camera
// pointed at a screen. Rule 3 in particular exists to stop the data accidentally
// containing something that looks like a finder pattern.
static int
penalty(const struct qr *q)
{
int n = q->size, score = 0, dark = 0;
for (int y = 0; y < n; y++) {
for (int x = 0; x < n; x++)
dark += q->mod[y][x];
// Rule 1, both directions: runs of five or more identical modules.
for (int dir = 0; dir < 2; dir++) {
int run = 1;
int prev = dir ? q->mod[0][y] : q->mod[y][0];
for (int i = 1; i < n; i++) {
int cur = dir ? q->mod[i][y] : q->mod[y][i];
if (cur == prev) {
run++;
} else {
if (run >= 5) score += 3 + (run - 5);
run = 1;
prev = cur;
}
}
if (run >= 5) score += 3 + (run - 5);
}
}
// Rule 2: every 2x2 block of one colour.
for (int y = 0; y + 1 < n; y++)
for (int x = 0; x + 1 < n; x++) {
int c = q->mod[y][x];
if (c == q->mod[y][x + 1] && c == q->mod[y + 1][x] && c == q->mod[y + 1][x + 1])
score += 3;
}
// Rule 3: the finder-lookalike 1011101 with four light modules on either
// side, in either direction.
for (int dir = 0; dir < 2; dir++)
for (int a = 0; a < n; a++) {
unsigned bits = 0;
for (int b = 0; b < n; b++) {
int m = dir ? q->mod[b][a] : q->mod[a][b];
bits = ((bits << 1) | (unsigned)m) & 0x7FF;
if (b >= 10 && (bits == 0x05D || bits == 0x5D0))
score += 40;
}
}
// Rule 4: deviation from an even split of dark and light, in 5% steps.
int total = n * n;
int k = (absi(dark * 20 - total * 10) + total - 1) / total;
score += k * 10;
return score;
}
// ---------------------------------------------------------------------------
// Encode
// ---------------------------------------------------------------------------
static void
put_bits(unsigned char *buf, int *pos, unsigned val, int n)
{
for (int i = n - 1; i >= 0; i--) {
buf[*pos >> 3] |= (unsigned char)(((val >> i) & 1) << (7 - (*pos & 7)));
(*pos)++;
}
}
int
qr_encode(struct qr *q, const unsigned char *data, int len)
{
int v;
if (len < 0)
return -1;
for (v = 1; v <= QR_MAX_VERSION; v++)
if (len <= qr_capacity(v))
break;
if (v > QR_MAX_VERSION)
return -1;
int total = vinfo[v].total;
int ecclen = vinfo[v].ecc;
int blocks = vinfo[v].blocks;
int datacw = total - ecclen * blocks;
// The table claims equal-sized blocks for every version this file supports.
// If that is ever untrue the interleave below is silently wrong, so check it
// rather than trust it.
if (datacw % blocks != 0)
return -1;
int blocklen = datacw / blocks;
// --- 1. payload -> data codewords ---------------------------------------
unsigned char cw[172]; // >= max total codewords
memset(cw, 0, sizeof(cw));
int pos = 0;
put_bits(cw, &pos, 0x4, 4); // byte mode
put_bits(cw, &pos, (unsigned)len, 8);
for (int i = 0; i < len; i++)
put_bits(cw, &pos, data[i], 8);
// Terminator, up to four zero bits, then round up to a whole codeword.
int spare = datacw * 8 - pos;
put_bits(cw, &pos, 0, spare < 4 ? spare : 4);
if (pos & 7)
put_bits(cw, &pos, 0, 8 - (pos & 7));
// Fill the rest with the standard alternating pad bytes. They are specified
// values, not arbitrary filler: a decoder uses the character count to know
// where the data ends, but the pad pattern keeps the symbol from having large
// uniform regions that mask selection would then have to fight.
for (int i = pos / 8; i < datacw; i++)
cw[i] = (i - pos / 8) % 2 == 0 ? 0xEC : 0x11;
// --- 2 and 3. error correction, then interleave -------------------------
unsigned char gen[30];
unsigned char ec[4][30];
rs_divisor(ecclen, gen);
for (int b = 0; b < blocks; b++)
rs_remainder(&cw[b * blocklen], blocklen, gen, ecclen, ec[b]);
unsigned char out[172];
int o = 0;
for (int i = 0; i < blocklen; i++)
for (int b = 0; b < blocks; b++)
out[o++] = cw[b * blocklen + i];
for (int i = 0; i < ecclen; i++)
for (int b = 0; b < blocks; b++)
out[o++] = ec[b][i];
// --- 4. function patterns ------------------------------------------------
memset(q, 0, sizeof(*q));
q->size = 17 + 4 * v;
draw_function_patterns(q);
// --- 5. zigzag placement -------------------------------------------------
//
// Two-module-wide columns walked from the right edge leftwards, alternating
// upward and downward. Column 6 is skipped entirely because it is the vertical
// timing pattern -- without that step every subsequent column lands one place
// out and the symbol is quietly unreadable.
int bit = 0;
int nbits = o * 8;
for (int right = q->size - 1; right >= 1; right -= 2) {
if (right == 6)
right = 5;
for (int vert = 0; vert < q->size; vert++) {
for (int j = 0; j < 2; j++) {
int x = right - j;
int upward = ((right + 1) & 2) == 0;
int y = upward ? q->size - 1 - vert : vert;
if (!q->fn[y][x] && bit < nbits) {
q->mod[y][x] = (unsigned char)((out[bit >> 3] >> (7 - (bit & 7))) & 1);
bit++;
}
}
}
}
// --- 6. pick a mask ------------------------------------------------------
int best = 0, bestscore = 0;
for (int m = 0; m < 8; m++) {
apply_mask(q, m);
draw_format(q, m);
int s = penalty(q);
if (m == 0 || s < bestscore) {
bestscore = s;
best = m;
}
apply_mask(q, m); // XOR again to undo; masking is involutive
}
apply_mask(q, best);
draw_format(q, best);
return 0;
}

55
kernel/core/sleeplock.c Normal file
View file

@ -0,0 +1,55 @@
// Sleeping locks
#include "types.h"
#include "defs.h"
#include "param.h"
#include "x86.h"
#include "memlayout.h"
#include "mmu.h"
#include "proc.h"
#include "spinlock.h"
#include "sleeplock.h"
void
initsleeplock(struct sleeplock *lk, char *name)
{
initlock(&lk->lk, "sleep lock");
lk->name = name;
lk->locked = 0;
lk->pid = 0;
}
void
acquiresleep(struct sleeplock *lk)
{
acquire(&lk->lk);
while (lk->locked) {
sleep(lk, &lk->lk);
}
lk->locked = 1;
if (myproc()) {
lk->pid = myproc()->pid;
} else {
lk->pid = 0;
}
release(&lk->lk);
}
void
releasesleep(struct sleeplock *lk)
{
acquire(&lk->lk);
lk->locked = 0;
lk->pid = 0;
wakeup(lk);
release(&lk->lk);
}
int
holdingsleep(struct sleeplock *lk)
{
acquire(&lk->lk);
int r = lk->locked;
release(&lk->lk);
return r;
}

115
kernel/core/spinlock.c Normal file
View file

@ -0,0 +1,115 @@
#include "types.h"
#include "defs.h"
#include "param.h"
#include "x86.h"
#include "memlayout.h"
#include "mmu.h"
#include "proc.h"
#include "spinlock.h"
void
initlock(struct spinlock *lk, char *name)
{
lk->name = name;
lk->locked = 0;
lk->cpu = 0;
}
void
acquire(struct spinlock *lk)
{
pushcli();
if(holding(lk))
panic("acquire");
{
uint64 spins = 0;
while(xchg(&lk->locked, 1) != 0){
if(++spins == 200000000ULL){
cprintf("acquire: stuck on \"%s\" held by cpu=%p pcs0=%p pcs1=%p pcs2=%p, acquirer cpu=%p\n",
lk->name, lk->cpu, lk->pcs[0], lk->pcs[1], lk->pcs[2], mycpu());
panic("acquire: deadlock");
}
}
}
__sync_synchronize();
lk->cpu = mycpu();
getcallerpcs(&lk, lk->pcs);
}
void
release(struct spinlock *lk)
{
if(!holding(lk))
panic("release");
lk->pcs[0] = 0;
lk->cpu = 0;
__sync_synchronize();
asm volatile("movq $0, %0" : "+m" (lk->locked) : );
popcli();
}
void
getcallerpcs(void *v, addr_t pcs[])
{
addr_t *rbp;
asm volatile("mov %%rbp, %0" : "=r" (rbp));
getstackpcs(rbp, pcs);
}
void
getstackpcs(addr_t *rbp, addr_t pcs[])
{
int i;
for(i = 0; i < 10; i++){
if(rbp == 0 || rbp < (addr_t*)KERNBASE || rbp == (addr_t*)0xffffffff)
break;
pcs[i] = rbp[1];
rbp = (addr_t*)rbp[0];
}
for(; i < 10; i++)
pcs[i] = 0;
}
int
holding(struct spinlock *lock)
{
return lock->locked && lock->cpu == mycpu();
}
void
pushcli(void)
{
int eflags;
eflags = readeflags();
cli();
if(mycpu() == 0){
extern uint64 kernel_fs_base;
cprintf("pushcli: cpu==NULL cs=0x%p kernel_fs_base=0x%p actual_fs_base=0x%p\n",
readcs(), kernel_fs_base, rdmsr(0xC0000100));
panic("pushcli: cpu is NULL");
}
if(mycpu()->ncli == 0)
mycpu()->intena = eflags & FL_IF;
mycpu()->ncli += 1;
}
void
popcli(void)
{
if(readeflags()&FL_IF)
panic("popcli - interruptible");
if(--mycpu()->ncli < 0)
panic("popcli");
if(mycpu()->ncli == 0 && mycpu()->intena)
sti();
}

113
kernel/core/string.c Normal file
View file

@ -0,0 +1,113 @@
#include "types.h"
#include "x86.h"
void*
memset(void *dst, int c, uint64 n)
{
uint64 c64;
if ((addr_t)dst % 8 == 0 && n % 8 == 0) {
c &= 0xFF;
c64 = (uint64)c;
c64 = (c64 << 56) | (c64 << 48) | (c64 << 40) | (c64 << 32) |
(c64 << 24) | (c64 << 16) | (c64 << 8) | c64;
stosq(dst, c64, n / 8);
} else {
stosb(dst, c, n);
}
return dst;
}
int
memcmp(const void *v1, const void *v2, uint n)
{
const uchar *s1, *s2;
s1 = v1;
s2 = v2;
while(n-- > 0){
if(*s1 != *s2)
return *s1 - *s2;
s1++, s2++;
}
return 0;
}
void*
memmove(void *dst, const void *src, uint n)
{
const char *s;
char *d;
s = src;
d = dst;
if(s < d && s + n > d){
s += n;
d += n;
while(n-- > 0)
*--d = *--s;
} else
while(n-- > 0)
*d++ = *s++;
return dst;
}
void*
memcpy(void *dst, const void *src, uint n)
{
return memmove(dst, src, n);
}
int
strncmp(const char *p, const char *q, uint n)
{
while(n > 0 && *p && *p == *q)
n--, p++, q++;
if(n == 0)
return 0;
return (uchar)*p - (uchar)*q;
}
char*
strncpy(char *s, const char *t, int n)
{
char *os = s;
while(n-- > 0 && (*s++ = *t++) != 0)
;
while(n-- > 0)
*s++ = 0;
return os;
}
char*
safestrcpy(char *s, const char *t, int n)
{
char *os = s;
if(n <= 0)
return os;
while(--n > 0 && (*s++ = *t++) != 0)
;
*s = 0;
return os;
}
int
strlen(const char *s)
{
int n;
for(n = 0; s[n]; n++)
;
return n;
}
int
strnlen(const char *s, int max)
{
int n;
for(n = 0; n < max && s[n]; n++)
;
return n;
}

116
kernel/core/time.c Normal file
View file

@ -0,0 +1,116 @@
// Wall-clock time.
//
// The kernel has always known how long it had been running -- `ticks`, bumped
// 100 times a second by the timer interrupt -- and has never known what time it
// was. sys_gettimeofday() returned `ticks / 100` as though it were seconds
// since the epoch, so every Schunix machine believed it was the 1st of January
// 1970 and that the date advanced at the rate of its own uptime. `date` printed
// a number that barely moved, which is what it looks like when a clock is
// counting from the wrong place rather than not counting at all.
//
// cmostime() in kernel/arch/x86_64/lapic.c has been able to read the hardware
// clock the whole time. Nothing called it.
//
// The arrangement here is the usual one: read the RTC once at boot, convert it
// to a Unix timestamp, and add uptime to it thereafter. The RTC is slow to read
// (it is polled over two I/O ports and has to be sampled twice to catch it
// mid-update) so reading it on every gettimeofday would be absurd; and `ticks`
// is a perfectly good interval clock once it has something to count from.
#include "types.h"
#include "defs.h"
#include "date.h"
#include "param.h"
#include "spinlock.h"
extern uint ticks;
// Seconds between the Unix epoch and the moment this machine booted. Zero until
// time_init() runs, which makes the behaviour before that point exactly what it
// used to be -- uptime presented as absolute time -- rather than something new
// and differently wrong.
uint64 boot_epoch;
// Days from 1970-01-01 to the given civil date, proleptic Gregorian.
//
// Howard Hinnant's days_from_civil. It is worth using rather than a loop over
// years because it is branchless, exact for any year, and has no table to get
// wrong; the alternative -- accumulating 365 or 366 days per year since 1970 --
// gets leap centuries wrong unless you remember that 1900 was not a leap year
// and 2000 was, which is precisely the sort of thing that is discovered on a
// February morning years later.
//
// The trick is shifting the year so that March is month 1, which puts the leap
// day at the *end* of the year and makes the leap rule depend only on the year
// number rather than on where in the year you are.
static int64
days_from_civil(int64 y, unsigned m, unsigned d)
{
y -= m <= 2;
const int64 era = (y >= 0 ? y : y - 399) / 400;
const unsigned yoe = (unsigned)(y - era * 400); // [0, 399]
const unsigned doy = (153 * (m + (m > 2 ? -3 : 9)) + 2) / 5 + d - 1;
const unsigned doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; // [0, 146096]
return era * 146097 + (int64)doe - 719468;
}
// Read the hardware clock and work out when we booted.
//
// The RTC is read in UTC. That is not universally true on a PC -- a machine
// that has had Windows installed on it usually keeps the RTC in local time --
// but there is nowhere to store a timezone yet and no way to ask, and UTC is
// the honest default for a system with no configuration. When there is a
// settings store, this is where the offset goes.
void
time_init(void)
{
struct rtcdate r;
cmostime(&r);
// cmostime() returns a two-digit year on hardware that only stores one.
// 2000-2099 is the right guess now and stays right for a lifetime; a machine
// reporting 70 means the battery is flat, and the epoch is as good an answer
// as any for that.
int64 year = r.year;
if (year < 100)
year += 2000;
// A wildly implausible reading means the CMOS is not telling us anything.
// Leaving boot_epoch at zero keeps the old behaviour, which is wrong but
// predictable, rather than putting the machine somewhere in the Bronze Age.
if (r.month < 1 || r.month > 12 || r.day < 1 || r.day > 31 ||
r.hour > 23 || r.minute > 59 || r.second > 60 || year < 1970) {
cprintf("time: CMOS clock is not plausible, staying at the epoch\n");
return;
}
int64 days = days_from_civil(year, r.month, r.day);
uint64 now = (uint64)(days * 86400) + r.hour * 3600 + r.minute * 60 + r.second;
// Subtract the uptime so far, so boot_epoch is the moment of boot rather than
// the moment this function ran. It is only a fraction of a second, but it
// keeps boot_epoch meaning exactly what its name says.
uint64 up = ticks / 100;
boot_epoch = now > up ? now - up : now;
cprintf("time: %d-%d-%d %d:%d:%d UTC from the hardware clock\n",
(int)year, r.month, r.day, r.hour, r.minute, r.second);
}
// Seconds and microseconds since the epoch. One place, so gettimeofday,
// clock_gettime and anything added later cannot disagree about what time it is.
void
wallclock(uint64 *sec, uint64 *usec)
{
uint xticks;
acquire(&tickslock);
xticks = ticks;
release(&tickslock);
if (sec)
*sec = boot_epoch + xticks / 100;
if (usec)
*usec = (uint64)(xticks % 100) * 10000;
}

79
kernel/core/uaccess.c Normal file
View file

@ -0,0 +1,79 @@
// User-memory access helpers — see include/uaccess.h for the rules.
//
// Everything here validates against the *current* process's page table
// (myproc()->pgdir), which is also the live CR3 while a syscall runs, so a
// range that passes access_ok() can be dereferenced directly by kernel
// code for the remainder of the syscall (single-threaded processes can't
// unmap it out from under us mid-call).
#include "types.h"
#include "defs.h"
#include "param.h"
#include "memlayout.h"
#include "mmu.h"
#include "proc.h"
#include "errno.h"
#include "uaccess.h"
int
access_ok(addr_t uaddr, uint64 size)
{
addr_t a, end;
if(size == 0)
return 1;
end = uaddr + size;
if(end < uaddr || end > KERNBASE)
return 0;
for(a = PGROUNDDOWN(uaddr); a < end; a += PGSIZE)
if(uva2ka(myproc()->pgdir, (char*)a) == 0)
return 0;
return 1;
}
void*
user_ptr(addr_t uaddr, uint64 size)
{
if(uaddr == 0 || !access_ok(uaddr, size))
return 0;
return (void*)uaddr;
}
int
copy_from_user(void *dst, addr_t uaddr, uint64 n)
{
if(uaddr + n < uaddr || uaddr + n > KERNBASE)
return -EFAULT;
if(copyin(myproc()->pgdir, uaddr, dst, n) < 0)
return -EFAULT;
return 0;
}
int
copy_to_user(addr_t uaddr, const void *src, uint64 n)
{
if(uaddr + n < uaddr || uaddr + n > KERNBASE)
return -EFAULT;
if(copyout(myproc()->pgdir, uaddr, (void*)src, n) < 0)
return -EFAULT;
return 0;
}
long
strlen_user(addr_t uaddr)
{
addr_t a = uaddr;
while(a - uaddr < USER_STR_MAX){
// Scan through the per-page kernel alias so an unmapped page is a
// clean failure instead of a fault.
char *ka = uva2ka(myproc()->pgdir, (char*)a);
if(ka == 0)
return -EFAULT;
addr_t pgend = PGROUNDDOWN(a) + PGSIZE;
for(; a < pgend; a++, ka++)
if(*ka == 0)
return a - uaddr;
}
return -EFAULT;
}

160
kernel/dev/bio.c Normal file
View file

@ -0,0 +1,160 @@
// Buffer cache.
//
// The buffer cache is a linked list of buf structures holding
// cached copies of disk block contents. Caching disk blocks
// in memory reduces the number of disk reads and also provides
// a synchronization point for disk blocks used by multiple processes.
//
// Interface:
// * To get a buffer for a particular disk block, call bread.
// * After changing buffer data, call bwrite to write it to disk.
// * When done with the buffer, call brelse.
// * Do not use the buffer after calling brelse.
// * Only one process at a time can use a buffer,
// so do not keep them longer than necessary.
//
// The implementation uses two state flags internally:
// * B_VALID: the buffer data has been read from the disk.
// * B_DIRTY: the buffer data has been modified
// and needs to be written to disk.
#include "types.h"
#include "defs.h"
#include "param.h"
#include "spinlock.h"
#include "sleeplock.h"
#include "fs.h"
#include "buf.h"
struct {
struct spinlock lock;
struct buf buf[NBUF];
// Linked list of all buffers, through prev/next.
// head.next is most recently used.
struct buf head;
} bcache;
void
binit(void)
{
struct buf *b;
initlock(&bcache.lock, "bcache");
//PAGEBREAK!
// Create linked list of buffers
bcache.head.prev = &bcache.head;
bcache.head.next = &bcache.head;
for(b = bcache.buf; b < bcache.buf+NBUF; b++){
b->next = bcache.head.next;
b->prev = &bcache.head;
initsleeplock(&b->lock, "buffer");
bcache.head.next->prev = b;
bcache.head.next = b;
}
}
// Route a request to the driver that owns b->dev. Everything above this line
// is device-agnostic; everything below picks a backend. RAMDISK_DEV is served
// out of memory (kernel/dev/ramdisk.c), USBDISK_DEV by bulk transfers to a
// USB stick, everything else by the IDE driver.
static void
bdevrw(struct buf *b)
{
if(b->dev == RAMDISK_DEV){
ramdiskrw(b);
return;
}
if(b->dev == USBDISK_DEV){
usb_storage_rw(b);
return;
}
iderw(b);
}
// Look through buffer cache for block on device dev.
// If not found, allocate a buffer.
// In either case, return locked buffer.
static struct buf*
bget(uint dev, uint blockno)
{
struct buf *b;
acquire(&bcache.lock);
// Is the block already cached?
for(b = bcache.head.next; b != &bcache.head; b = b->next){
if(b->dev == dev && b->blockno == blockno){
b->refcnt++;
release(&bcache.lock);
acquiresleep(&b->lock);
return b;
}
}
// Not cached; recycle some unused buffer and clean buffer
// "clean" because B_DIRTY and not locked means log.c
// hasn't yet committed the changes to the buffer.
for(b = bcache.head.prev; b != &bcache.head; b = b->prev){
if(b->refcnt == 0 && (b->flags & B_DIRTY) == 0) {
b->dev = dev;
b->blockno = blockno;
b->flags = 0;
b->refcnt = 1;
release(&bcache.lock);
acquiresleep(&b->lock);
return b;
}
}
panic("bget: no buffers");
}
// Return a locked buf with the contents of the indicated block.
struct buf*
bread(uint dev, uint blockno)
{
struct buf *b;
b = bget(dev, blockno);
if(!(b->flags & B_VALID)) {
bdevrw(b);
}
return b;
}
// Write b's contents to disk. Must be locked.
void
bwrite(struct buf *b)
{
if(!holdingsleep(&b->lock))
panic("bwrite");
b->flags |= B_DIRTY;
bdevrw(b);
}
// Release a locked buffer.
// Move to the head of the MRU list.
void
brelse(struct buf *b)
{
if(!holdingsleep(&b->lock))
panic("brelse");
releasesleep(&b->lock);
acquire(&bcache.lock);
b->refcnt--;
if (b->refcnt == 0) {
// no one is waiting for it.
b->next->prev = b->prev;
b->prev->next = b->next;
b->next = bcache.head.next;
b->prev = &bcache.head;
bcache.head.next->prev = b;
bcache.head.next = b;
}
release(&bcache.lock);
}
//PAGEBREAK!

107
kernel/dev/cdev.c Normal file
View file

@ -0,0 +1,107 @@
#include "types.h"
#include "defs.h"
#include "param.h"
#include "spinlock.h"
#include "sleeplock.h"
#include "fs.h"
#include "file.h"
#include "x86.h"
#include "errno.h"
extern int consoleread(struct inode*, uint, char*, int);
extern int consolewrite(struct inode*, uint, char*, int);
extern int mouseread(struct inode*, uint, char*, int);
static uint64 rseed = 0x2A99554433221100ULL;
int
nullwrite(struct inode *ip, uint off, char *dst, int n)
{
return n;
}
int
nullread(struct inode *ip, uint off, char *dst, int n)
{
return 0;
}
int
zeroread(struct inode *ip, uint off, char *dst, int n)
{
memset(dst, 0, n);
return n;
}
int
randomread(struct inode *ip, uint off, char *dst, int n)
{
for(int i = 0; i < n; i++){
rseed = rseed * 6364136223846793005ULL + 1442695040888963407ULL;
dst[i] = (rseed >> 32) & 0xFF;
}
return n;
}
// /dev/full — reads as an endless run of zero bytes, every write fails with
// ENOSPC. Programs use it to exercise their out-of-disk-space paths.
int
fullwrite(struct inode *ip, uint off, char *dst, int n)
{
return -ENOSPC;
}
int
mousewrite(struct inode *ip, uint off, char *dst, int n)
{
return n;
}
int
memread(struct inode *ip, uint off, char *dst, int n)
{
switch(ip->minor) {
case 1: return consoleread(ip, off, dst, n);
case 2: return nullread(ip, off, dst, n);
case 3: return consoleread(ip, off, dst, n);
case 5: return zeroread(ip, off, dst, n);
case 7: return zeroread(ip, off, dst, n); // /dev/full reads as zeros
case 8:
case 9: return randomread(ip, off, dst, n);
default: return -1;
}
}
int
memwrite(struct inode *ip, uint off, char *dst, int n)
{
switch(ip->minor) {
case 1: return consolewrite(ip, off, dst, n);
case 7: return fullwrite(ip, off, dst, n); // /dev/full: always ENOSPC
case 2:
case 5:
case 8:
case 9: return nullwrite(ip, off, dst, n);
default: return -1;
}
}
struct devsw devsw[] = {
[0] = { 0, 0 },
[1] = { memread, memwrite },
// /dev/fb0 (major 2) — the framebuffer, kernel/dev/fb.c. This used to be
// { nullread, nullwrite }, so writing pixels to it succeeded and did
// nothing.
// The close hook gives the text console its screen back when whatever was
// drawing on the framebuffer exits — see fbclose().
[2] = { fbread, fbwrite, 0, fbclose },
[3] = { mouseread, mousewrite },
[4] = { 0, 0 },
// /dev/tty (major 5) — alias for the console
[5] = { memread, memwrite },
// /proc status files (major 6) — see kernel/dev/procfs.c
[6] = { procread, procwrite },
// /dev/input/eventN (major 7) — raw evdev events, kernel/dev/input.c. The
// minor is the device index, so event0 is the keyboard and event1 the mouse.
[7] = { inputread, inputwrite, inputready, inputclose, inputopen },
};

308
kernel/dev/device.c Normal file
View file

@ -0,0 +1,308 @@
// The device model. See include/device.h for why it exists.
//
// A fixed table rather than a linked list: this is populated once during boot
// by whichever bus found each device, and read afterwards. Nothing is removed
// yet -- hot-unplug would need it, and when that arrives the in_use flag is
// already the hook for it.
//
// No lock. Every device_add() happens during single-threaded boot, before
// userspace exists, and every reader afterwards only walks. If a bus ever
// enumerates at runtime -- USB hotplug is the obvious one -- this needs a
// spinlock, and the reader in procfs needs to take it.
#include "types.h"
#include "defs.h"
#include "param.h"
#include "device.h"
#include "display.h"
#include "theme.h"
#include "text.h"
static struct device devices[NDEVICE];
static int ndevices;
static const char *bus_names[DEVBUS_NBUS] = {
[DEVBUS_PLATFORM] = "platform",
[DEVBUS_PCI] = "pci",
[DEVBUS_USB] = "usb",
};
static const char *class_names[DEVCLASS_NCLASS] = {
[DEVCLASS_OTHER] = "other",
[DEVCLASS_DISPLAY] = "display",
[DEVCLASS_INPUT] = "input",
[DEVCLASS_STORAGE] = "storage",
[DEVCLASS_NET] = "net",
[DEVCLASS_BUS] = "bus",
};
const char *
dev_bus_name(enum dev_bus b)
{
if (b < 0 || b >= DEVBUS_NBUS || !bus_names[b])
return "?";
return bus_names[b];
}
const char *
dev_class_name(enum dev_class c)
{
if (c < 0 || c >= DEVCLASS_NCLASS || !class_names[c])
return "?";
return class_names[c];
}
// How many devices already share this base name, so "vga" becomes vga0, vga1.
static int
count_base(const char *base)
{
int n = 0;
for (int i = 0; i < ndevices; i++) {
const char *a = devices[i].name, *b = base;
while (*b && *a == *b) { a++; b++; }
// Matched the whole base and the rest is the index we appended.
if (*b == '\0' && *a >= '0' && *a <= '9')
n++;
}
return n;
}
struct device *
device_add(const char *base, enum dev_bus bus, enum dev_class class,
const char *driver)
{
if (ndevices >= NDEVICE)
return 0;
struct device *d = &devices[ndevices++];
d->bus = bus;
d->class = class;
d->driver = driver;
d->in_use = 1;
int idx = count_base(base);
int n = 0;
while (base[n] && n < DEV_NAMELEN - 3) {
d->name[n] = base[n];
n++;
}
// Single digit is enough for NDEVICE of 32 per base name, and a two-digit
// path that is never taken is a two-digit path that is never tested.
if (idx > 9)
idx = 9;
d->name[n++] = (char)('0' + idx);
d->name[n] = '\0';
return d;
}
void
device_set_desc(struct device *d, const char *s)
{
if (!d)
return;
int n = 0;
while (s[n] && n < (int)sizeof(d->desc) - 1) {
d->desc[n] = s[n];
n++;
}
d->desc[n] = '\0';
}
struct device *
device_at(int i)
{
if (i < 0 || i >= ndevices)
return 0;
return &devices[i];
}
int
device_count(void)
{
return ndevices;
}
struct device *
device_find_class(enum dev_class class)
{
for (int i = 0; i < ndevices; i++)
// Unclaimed devices are deliberately skipped. A display nobody drives is
// not a display you can draw on, and returning it would push the "is there
// a driver" check out to every caller.
if (devices[i].class == class && devices[i].driver)
return &devices[i];
return 0;
}
// --- the display class -----------------------------------------------------
//
// Thin wrappers over device_find_class() plus a null check on each op. They
// exist so that consumers are not each written around a vtable that may be
// absent -- a compositor asking to present on a machine with no display driver
// should be a no-op, not a null dereference in the middle of a frame.
struct device *
display_get(void)
{
return device_find_class(DEVCLASS_DISPLAY);
}
int
display_mode(struct display_mode *out)
{
struct device *d = display_get();
if (!d || !d->ops || !((struct display_ops *)d->ops)->mode)
return -1;
((struct display_ops *)d->ops)->mode(d, out);
return 0;
}
void *
display_acquire(uint32 *stride_out)
{
struct device *d = display_get();
if (!d || !d->ops || !((struct display_ops *)d->ops)->acquire)
return 0;
return ((struct display_ops *)d->ops)->acquire(d, stride_out);
}
void
display_present(int x, int y, int w, int h)
{
struct device *d = display_get();
if (!d || !d->ops || !((struct display_ops *)d->ops)->present)
return;
((struct display_ops *)d->ops)->present(d, x, y, w, h);
}
void
display_wait_vblank(void)
{
struct device *d = display_get();
if (!d || !d->ops || !((struct display_ops *)d->ops)->wait_vblank)
return;
((struct display_ops *)d->ops)->wait_vblank(d);
}
// Drive the whole display path once and report what happened: find the class,
// read the mode, acquire a back buffer, draw into RAM, present it.
//
// A self-test rather than a unit test because none of this can be checked
// without a display: the assertion is "did the right pixels reach the screen",
// and the only instrument for that is a screendump. Triggered with
// `echo d > /proc/sysrq` and photographed by tools/dispshot.ps1.
//
// The pattern is chosen to be wrong in visible ways. Four solid quadrants in
// theme colours catch a stride error (the bands would slant) and a bpp error
// (the colours would be wrong); the white frame one pixel inside the edge
// catches an off-by-one in the present rectangle; and the fact that it is
// drawn entirely into RAM before a single byte reaches the framebuffer is what
// double buffering means.
void
display_selftest(void)
{
struct display_mode m;
uint32 stride;
if (display_mode(&m) != 0) {
cprintf("display: no display bound\n");
return;
}
cprintf("display: %dx%d %dbpp stride %d\n", m.width, m.height, m.bpp, m.stride);
unsigned char *b = display_acquire(&stride);
if (!b) {
cprintf("display: no back buffer available\n");
return;
}
static const uint32 quad[4] = {
SCH_ACCENT, SCH_ACCENT_DIM, SCH_DIM, SCH_BG
};
for (int y = 0; y < m.height; y++) {
uint32 *row = (uint32 *)(b + (uint64)y * stride);
int top = y < m.height / 2;
for (int x = 0; x < m.width; x++) {
int left = x < m.width / 2;
row[x] = quad[(top ? 0 : 2) + (left ? 0 : 1)];
}
}
for (int x = 1; x < m.width - 1; x++) {
*(uint32 *)(b + 1 * (uint64)stride + x * 4) = SCH_FG;
*(uint32 *)(b + (uint64)(m.height - 2) * stride + x * 4) = SCH_FG;
}
for (int y = 1; y < m.height - 1; y++) {
*(uint32 *)(b + (uint64)y * stride + 1 * 4) = SCH_FG;
*(uint32 *)(b + (uint64)y * stride + (m.width - 2) * 4) = SCH_FG;
}
// Between frames, so the blit does not land halfway down a scan-out.
display_wait_vblank();
display_present(0, 0, m.width, m.height);
cprintf("display: presented a full frame\n");
}
// Draw a text specimen through the display class. `echo t > /proc/sysrq`.
//
// It exercises the three things a character-cell console cannot do, so a
// screenshot says whether the layer works rather than whether it compiled:
// glyphs on a common baseline at arbitrary x (not multiples of eight), text
// blended over an existing background rather than drawn into cleared cells, and
// codepoints above 255.
void
text_selftest(void)
{
struct display_mode m;
uint32 stride;
if (display_mode(&m) != 0) {
cprintf("text: no display\n");
return;
}
unsigned char *b = display_acquire(&stride);
if (!b) {
cprintf("text: no back buffer\n");
return;
}
struct surface s = { b, stride, m.width, m.height };
struct font *f = font_builtin();
// A background with structure in it, so blending is visible as blending
// rather than as drawing onto a flat colour.
for (int y = 0; y < m.height; y++) {
uint32 *row = (uint32 *)(b + (uint64)y * stride);
for (int x = 0; x < m.width; x++)
row[x] = ((x / 32 + y / 32) & 1) ? SCH_BG : SCH_ACCENT_DIM;
}
int base = f->ops->baseline(f);
int line = f->ops->height(f) + 6;
int y = 80;
text_draw(&s, f, 60, y + base, "Schunix text rendering", SCH_FG);
y += line * 2;
// Deliberately not on a multiple of eight. On the old console this string
// could only start at x = 56 or x = 64.
text_draw(&s, f, 61, y + base, "positioned at x=61, off the cell grid", SCH_DIM);
y += line;
text_draw(&s, f, 63, y + base, "and here at x=63", SCH_DIM);
y += line * 2;
// Above U+00FF, which a 256-entry table cannot index at all.
text_draw(&s, f, 60, y + base,
"utf-8: \xe2\x94\x8c\xe2\x94\x80\xe2\x94\x90 "
"\xe2\x96\x88\xe2\x96\x93\xe2\x96\x92\xe2\x96\x91 "
"\xc2\xb0 \xc2\xb1 \xe2\x86\x92 \xe2\x80\xa2", SCH_ACCENT);
y += line * 2;
// Measure, then centre on it -- the thing you cannot do without metrics.
const char *msg = "measured and centred";
int w = text_measure(f, msg);
text_draw(&s, f, (m.width - w) / 2, y + base, msg, SCH_FG);
display_present(0, 0, m.width, m.height);
cprintf("text: drew a specimen, \"%s\" measures %d px\n", msg, w);
}

202
kernel/dev/fb.c Normal file
View file

@ -0,0 +1,202 @@
// /dev/fb0 -- the linear framebuffer as a file.
//
// Until now this node existed but was wired to major 2, whose devsw entry was
// { nullread, nullwrite }: reads returned end-of-file and writes were thrown
// away, silently. The only thing that actually worked was mmap, and only
// because sys_mmap special-cased the major number.
//
// Reads and writes here address the framebuffer by byte offset, so lseek
// positions the "cursor" and a whole frame can be written with one write().
// That is slower than mmap and always will be -- it is a copy -- but it is
// what makes the device inspectable from a shell, which is worth a great deal
// while there is no GUI yet to inspect it with.
#include "types.h"
#include "defs.h"
#include "param.h"
#include "mmu.h"
#include "proc.h"
#include "spinlock.h"
#include "sleeplock.h"
#include "fs.h"
#include "file.h"
#include "fb.h"
#include "uaccess.h"
#include "errno.h"
// Describe one colour channel. A channel that does not exist gets length 0,
// which is how callers are supposed to detect "no alpha" rather than by
// assuming from bits_per_pixel.
static void
chan(struct fb_bitfield *f, unsigned int offset, unsigned int length)
{
f->offset = offset;
f->length = length;
f->msb_right = 0;
}
// Pixel layout for the depths vga_draw_pixel knows how to write. Keeping the
// two in step matters: this is the kernel telling userspace how to build a
// pixel, and if it disagrees with how the kernel builds one, every colour
// drawn by a program is wrong in a way that looks like a hardware fault.
static void
pixel_format(struct fb_var_screeninfo *v, unsigned int bpp)
{
switch (bpp) {
case 32:
chan(&v->red, 16, 8); chan(&v->green, 8, 8); chan(&v->blue, 0, 8);
chan(&v->transp, 24, 0); // the byte is there; nothing honours it
break;
case 24:
chan(&v->red, 16, 8); chan(&v->green, 8, 8); chan(&v->blue, 0, 8);
chan(&v->transp, 0, 0);
break;
case 16:
// RGB565, matching the packing vga_draw_pixel does for this depth.
chan(&v->red, 11, 5); chan(&v->green, 5, 6); chan(&v->blue, 0, 5);
chan(&v->transp, 0, 0);
break;
default:
chan(&v->red, 0, 0); chan(&v->green, 0, 0); chan(&v->blue, 0, 0);
chan(&v->transp, 0, 0);
break;
}
}
int
fbread(struct inode *ip, uint off, char *dst, int n)
{
volatile uchar *fb = vga_fb_ptr();
uint64 size = vga_fb_size();
if (!fb)
return -ENODEV;
if (n < 0)
return -EINVAL;
if ((uint64)off >= size)
return 0; // past the end of the screen: EOF
if ((uint64)off + (uint64)n > size)
n = (int)(size - (uint64)off);
memmove(dst, (void*)(fb + off), n);
return n;
}
// Set once something has drawn through this device, so that fbclose knows
// whether the screen still belongs to the text console. Reading the
// framebuffer, or asking it its geometry, does not count -- `fbinfo` with no
// arguments must not wipe its own output off the screen.
static int fb_dirty;
// Called from sys_mmap when /dev/fb0 is mapped. A mapping is a licence to
// draw, and the drawing happens with no further syscalls, so this is the last
// point at which the kernel hears about it at all.
void
fb_mapped(void)
{
fb_dirty = 1;
}
int
fbwrite(struct inode *ip, uint off, char *src, int n)
{
volatile uchar *fb = vga_fb_ptr();
uint64 size = vga_fb_size();
if (!fb)
return -ENODEV;
if (n < 0)
return -EINVAL;
if ((uint64)off >= size)
return -ENOSPC; // a screen cannot be extended
if ((uint64)off + (uint64)n > size)
n = (int)(size - (uint64)off);
memmove((void*)(fb + off), src, n);
fb_dirty = 1;
return n;
}
// The last descriptor on /dev/fb0 is going away.
//
// Whatever was drawing has finished, and the screen still holds its final
// frame -- so the shell prompt underneath is invisible and the machine looks
// hung. It is not: the process exited cleanly and the console is writing
// happily into a framebuffer nobody can read any more. There is no character
// backing store to repaint from (the console draws glyphs straight into the
// framebuffer and scrolls with a memmove), so the honest recovery is to clear
// and go home rather than to pretend the old text can be restored.
void
fbclose(struct inode *ip)
{
if (!fb_dirty)
return;
fb_dirty = 0;
// Said out loud, because a screen clearing on its own is otherwise
// indistinguishable from a bug -- and this is the only place in the system
// that wipes the display without being asked to.
cprintf("fb0: released by pid %d, console restored\n", myproc() ? myproc()->pid : -1);
vga_screen_reset();
}
// FBIO* handling, called from sys_ioctl once it has established that the fd
// refers to this device. `arg` is the raw user address; each request
// validates it for the size it is actually going to touch, rather than for
// the largest struct here -- fb_fix_screeninfo is half the size of
// fb_var_screeninfo, and a buffer sitting near the end of a mapping is a
// legitimate place to put the smaller one.
int
fb_ioctl(int request, addr_t arg)
{
int w, h;
if (!vga_fb_ptr())
return -ENODEV;
vga_dims(&w, &h);
switch (request) {
case FBIOGET_VSCREENINFO: {
struct fb_var_screeninfo *v =
(struct fb_var_screeninfo *)user_ptr(arg, sizeof(*v));
if (!v)
return -EFAULT;
memset(v, 0, sizeof(*v));
v->xres = v->xres_virtual = (unsigned int)w;
v->yres = v->yres_virtual = (unsigned int)h;
v->bits_per_pixel = vga_fb_bpp();
v->activate = FB_ACTIVATE_NOW;
pixel_format(v, v->bits_per_pixel);
// height/width (physical millimetres) and every timing field stay zero:
// VBE does not report them and a guess would be worse than an admission.
return 0;
}
case FBIOGET_FSCREENINFO: {
struct fb_fix_screeninfo *f =
(struct fb_fix_screeninfo *)user_ptr(arg, sizeof(*f));
if (!f)
return -EFAULT;
memset(f, 0, sizeof(*f));
safestrcpy(f->id, "schunixfb", sizeof(f->id));
f->smem_start = (unsigned long)vga_fb_phys();
f->smem_len = (unsigned int)vga_fb_size();
f->type = FB_TYPE_PACKED_PIXELS;
f->visual = FB_VISUAL_TRUECOLOR;
f->line_length = vga_fb_stride();
return 0;
}
// The mode came from the bootloader's VBE call and we have no way to
// change it. Saying so is more useful than accepting the request and
// leaving the caller to discover, by drawing, that nothing happened.
case FBIOPUT_VSCREENINFO:
return -EINVAL;
case FBIOBLANK:
return -EINVAL;
}
return -ENOTTY;
}

208
kernel/dev/ide.c Normal file
View file

@ -0,0 +1,208 @@
// Simple PIO-based (non-DMA) IDE driver code.
#include "types.h"
#include "defs.h"
#include "irq.h"
#include "param.h"
#include "memlayout.h"
#include "mmu.h"
#include "proc.h"
#include "x86.h"
#include "traps.h"
#include "spinlock.h"
#include "sleeplock.h"
#include "fs.h"
#include "buf.h"
#define SECTOR_SIZE 512
#define IDE_BSY 0x80
#define IDE_DRDY 0x40
#define IDE_DF 0x20
#define IDE_ERR 0x01
#define IDE_CMD_READ 0x20
#define IDE_CMD_WRITE 0x30
#define IDE_CMD_RDMUL 0xc4
#define IDE_CMD_WRMUL 0xc5
// idequeue points to the buf now being read/written to the disk.
// idequeue->qnext points to the next buf to be processed.
// You must hold idelock while manipulating queue.
static struct spinlock idelock;
static struct buf *idequeue;
static int havedisk0;
static int havedisk1;
static void idestart(struct buf*);
// Wait for the drive to become ready, or give up.
//
// This used to be an unbounded `while`. On a machine with nothing attached to
// the primary channel the status register reads 0xFF forever, so the very
// first call from ideinit() hung the kernel -- no panic, no message, just a
// dead machine right after the VGA banner. That is not an exotic
// configuration: it is every CD boot, and most hardware newer than IDE.
//
// The bound is deliberately enormous. A real drive answers in microseconds;
// this only expires when there is nothing there at all.
static int
idewait(int checkerr)
{
int r = 0;
for(int i = 0; i < 20000000; i++){
r = inb(0x1f7);
if((r & (IDE_BSY|IDE_DRDY)) == IDE_DRDY){
if(checkerr && (r & (IDE_DF|IDE_ERR)) != 0)
return -1;
return 0;
}
}
return -1;
}
static void
ide_irq(void *arg)
{
(void)arg;
ideintr();
}
void
ideinit(void)
{
initlock(&idelock, "ide");
// ISA signalling: edge-triggered, active high. Everything at a fixed
// legacy port is wired this way; PCI devices are not.
irq_register(IRQ_IDE, ide_irq, 0, "ide", 0);
// A timeout here means there is no drive on this channel. Say so and leave
// havedisk0/1 clear: with a ramdisk root there is nothing else the system
// needs from IDE, and booting on is strictly better than stopping.
if(idewait(0) < 0){
cprintf("ide: no drive on the primary channel\n");
return;
}
havedisk0 = 1;
// Check if disk 1 is present
outb(0x1f6, 0xe0 | (1<<4));
for(int i=0; i<1000; i++){
if(inb(0x1f7) != 0){
havedisk1 = 1;
break;
}
}
// Switch back to disk 0.
outb(0x1f6, 0xe0 | (0<<4));
}
// Is there actually a drive there? Callers that probe -- rather than callers
// that know what they are looking for -- have to ask, because reading an
// absent drive is a bug and iderw() treats it as one.
int
ide_disk_present(int dev)
{
return dev == 0 ? havedisk0 : havedisk1;
}
// Start the request for b. Caller must hold idelock.
static void
idestart(struct buf *b)
{
if(b == 0)
panic("idestart");
if(b->blockno >= FSSIZE)
panic("incorrect blockno");
int sector_per_block = BSIZE/SECTOR_SIZE;
int sector = b->blockno * sector_per_block;
int read_cmd = (sector_per_block == 1) ? IDE_CMD_READ : IDE_CMD_RDMUL;
int write_cmd = (sector_per_block == 1) ? IDE_CMD_WRITE : IDE_CMD_WRMUL;
if (sector_per_block > 7) panic("idestart");
idewait(0);
outb(0x3f6, 0); // generate interrupt
outb(0x1f2, sector_per_block); // number of sectors
outb(0x1f3, sector & 0xff);
outb(0x1f4, (sector >> 8) & 0xff);
outb(0x1f5, (sector >> 16) & 0xff);
outb(0x1f6, 0xe0 | ((b->dev&1)<<4) | ((sector>>24)&0x0f));
if(b->flags & B_DIRTY){
outb(0x1f7, write_cmd);
outsl(0x1f0, b->data, BSIZE/4);
} else {
outb(0x1f7, read_cmd);
}
}
// Interrupt handler.
void
ideintr(void)
{
struct buf *b;
// First queued buffer is the active request.
acquire(&idelock);
if((b = idequeue) == 0){
release(&idelock);
// cprintf("spurious IDE interrupt\n");
return;
}
idequeue = b->qnext;
// Read data if needed.
if(!(b->flags & B_DIRTY) && idewait(1) >= 0)
insl(0x1f0, b->data, BSIZE/4);
// Wake process waiting for this buf.
b->flags |= B_VALID;
b->flags &= ~B_DIRTY;
wakeup(b);
// Start disk on next buf in queue.
if(idequeue != 0)
idestart(idequeue);
release(&idelock);
}
//PAGEBREAK!
// Sync buf with disk.
// If B_DIRTY is set, write buf to disk, clear B_DIRTY, set B_VALID.
// Else if B_VALID is not set, read buf from disk, set B_VALID.
void
iderw(struct buf *b)
{
struct buf **pp;
if(!holdingsleep(&b->lock))
panic("iderw: buf not locked");
if((b->flags & (B_VALID|B_DIRTY)) == B_VALID)
panic("iderw: nothing to do");
if(b->dev != 0 && !havedisk1)
panic("iderw: ide disk 1 not present");
if(b->dev == 0 && !havedisk0)
panic("iderw: ide disk 0 not present");
acquire(&idelock); //DOC:acquire-lock
// Append b to idequeue.
b->qnext = 0;
for(pp=&idequeue; *pp; pp=&(*pp)->qnext) //DOC:insert-queue
;
*pp = b;
// Start disk if necessary.
if(idequeue == b)
idestart(b);
// Wait for request to finish.
while((b->flags & (B_VALID|B_DIRTY)) != B_VALID){
sleep(b, &idelock);
}
release(&idelock);
}

394
kernel/dev/input.c Normal file
View file

@ -0,0 +1,394 @@
// Raw input event devices -- /dev/input/event0 and /dev/input/event1.
//
// Everything that produces input feeds this: the PS/2 keyboard and mouse, and
// the USB HID driver. The console keeps its own path and is unaffected; these
// nodes are a *second* consumer of the same interrupts, so a program can read
// keys while the shell still gets characters.
//
// Two devices rather than one because they are two devices. A compositor
// grabs the pointer and leaves the keyboard alone, or the other way round, and
// merging them into a single stream makes that impossible to express.
#include "types.h"
#include "defs.h"
#include "param.h"
#include "mmu.h"
#include "proc.h"
#include "spinlock.h"
#include "sleeplock.h"
#include "fs.h"
#include "file.h"
#include "input.h"
#include "errno.h"
#define NEVENT 256 // per device; ~10 seconds of frantic typing
struct input_dev {
struct spinlock lock;
struct input_event ev[NEVENT];
uint r, w; // free-running; the difference is the count
// Exclusive grab (EVIOCGRAB). While set, keys still arrive here but stop
// being handed to the console's line discipline.
//
// Without this a full-screen program does not really own the keyboard: it
// reads /dev/input/event0 *and* the shell behind it reads the same keystrokes
// off the tty. The observed failure was a full-screen program quietly typing
// its entire session into the shell underneath, so the first Enter after
// exiting ran whatever had accumulated. The grab is also exactly what a
// compositor needs, which is why it lives here rather than in any one client.
int grabbed;
// How many descriptors are open on this device.
//
// Events are only queued while this is non-zero, and the backlog is dropped
// on the first open. Both halves matter: without them the ring is a log of
// every key pressed since boot, and the next program to open the device is
// handed the lot as though the user had just typed it.
int opens;
};
static struct input_dev idev[NINPUTDEV];
static int input_up;
extern uint ticks;
void
inputinit(void)
{
int i;
for(i = 0; i < NINPUTDEV; i++)
initlock(&idev[i].lock, "input");
// Producers run from interrupt handlers and can fire before this point in
// boot. Until the locks exist, an event has nowhere safe to go.
__sync_synchronize();
input_up = 1;
}
// Append one event. Called from interrupt context, so it never sleeps and
// never blocks on a full ring.
void
input_report(int dev, int type, int code, int value)
{
struct input_dev *d;
struct input_event *e;
if(!input_up || dev < 0 || dev >= NINPUTDEV)
return;
d = &idev[dev];
// Nobody is listening, so there is nowhere for this to go. Linux does the
// same: an evdev device with no clients does not accumulate.
if(d->opens == 0)
return;
acquire(&d->lock);
// A full ring drops the *oldest* event, not the newest. Dropping the newest
// is the obvious choice and it is wrong: the event most likely to be lost is
// then a key release, and a lost release leaves the key held down forever
// as far as every reader is concerned. Losing the oldest costs history, not
// state.
if(d->w - d->r >= NEVENT)
d->r++;
e = &d->ev[d->w % NEVENT];
e->time.tv_sec = ticks / 100;
e->time.tv_usec = (ticks % 100) * 10000;
e->type = (uint16)type;
e->code = (uint16)code;
e->value = (int32)value;
d->w++;
release(&d->lock);
}
// End a report. A reader that acts on each event as it arrives will see a
// diagonal mouse move as two separate moves; the SYN is how it knows the two
// axes belong to the same physical motion.
void
input_sync(int dev)
{
if(!input_up || dev < 0 || dev >= NINPUTDEV)
return;
input_report(dev, EV_SYN, SYN_REPORT, 0);
wakeup(&idev[dev]);
}
int
input_ready(int dev)
{
int n;
if(!input_up || dev < 0 || dev >= NINPUTDEV)
return 0;
acquire(&idev[dev].lock);
n = idev[dev].w != idev[dev].r;
release(&idev[dev].lock);
return n;
}
// A descriptor has been opened on this device.
//
// The backlog is discarded, which is the whole point. Events queued before a
// program opened the device were meant for whoever had it *then* -- almost
// always the shell, which already consumed them as a command line. Replaying
// them into the new owner means a program is handed the keystrokes that
// launched it, as input, the instant it starts. That was a real crash and not a
// cosmetic one: the replayed characters were interpreted as commands, and one of
// them asked for a mode the program had not initialised yet.
//
// Linux gets this for free by giving every open its own client queue. One
// shared ring plus "start from now" is the same answer for a single reader,
// which is the case that exists.
void
inputopen(struct inode *ip)
{
int dev = ip->minor;
if(dev < 0 || dev >= NINPUTDEV)
return;
acquire(&idev[dev].lock);
idev[dev].opens++;
idev[dev].r = idev[dev].w; // start from now, not from boot
release(&idev[dev].lock);
}
// The devsw ready hook: would a read block?
int
inputready(struct inode *ip)
{
return input_ready(ip->minor);
}
// Is this device grabbed? Asked by the keyboard drivers before they hand a
// character to the console.
int
input_is_grabbed(int dev)
{
if(!input_up || dev < 0 || dev >= NINPUTDEV)
return 0;
return idev[dev].grabbed;
}
// EVIOCGRAB. Linux's semantics: one holder at a time, and it is not an error
// to grab what you already hold.
int
input_grab(int dev, int on)
{
if(!input_up || dev < 0 || dev >= NINPUTDEV)
return -ENODEV;
on = on ? 1 : 0;
if(idev[dev].grabbed != on){
idev[dev].grabbed = on;
// Said out loud. A grabbed keyboard is a console that has gone deaf on
// purpose, and that is indistinguishable from one that has gone deaf by
// accident unless the kernel says which it was.
cprintf("input: event%d %s by pid %d\n", dev,
on ? "grabbed" : "released", myproc() ? myproc()->pid : -1);
}
return 0;
}
// The last descriptor on this device is going away.
//
// Releasing the grab here is not tidiness, it is the difference between a
// crashed game and a machine you have to reboot: a program that grabs the
// keyboard and dies without ungrabbing would leave the console permanently
// deaf. The close hook makes the release unconditional.
void
inputclose(struct inode *ip)
{
int dev = ip->minor;
if(dev < 0 || dev >= NINPUTDEV)
return;
if(idev[dev].opens > 0)
idev[dev].opens--;
if(idev[dev].grabbed){
idev[dev].grabbed = 0;
cprintf("input: event%d grab released\n", dev);
}
}
// ioctl on /dev/input/eventN. Only EVIOCGRAB so far; the EVIOCG* query
// requests (name, id, capability bitmaps) can arrive when something needs
// them.
int
input_ioctl(struct inode *ip, int request, addr_t arg)
{
int on;
if(request == EVIOCGRAB){
// Linux passes the flag *by value* in the argument, not by pointer.
on = (int)arg;
return input_grab(ip->minor, on);
}
return -ENOTTY;
}
// A read returns whole events only. Handing back a partial struct would
// desynchronise the reader permanently -- every later event would be split
// across two reads and reassembled wrong.
int
inputread(struct inode *ip, uint off, char *dst, int n)
{
struct input_dev *d;
int got = 0;
int dev = ip->minor;
if(dev < 0 || dev >= NINPUTDEV)
return -ENODEV;
if(n < (int)sizeof(struct input_event))
return -EINVAL;
d = &idev[dev];
acquire(&d->lock);
while(d->r == d->w){
if(myproc()->killed){
release(&d->lock);
return -EINTR;
}
sleep(d, &d->lock);
}
while(d->r != d->w && got + (int)sizeof(struct input_event) <= n){
memmove(dst + got, &d->ev[d->r % NEVENT], sizeof(struct input_event));
d->r++;
got += sizeof(struct input_event);
}
release(&d->lock);
return got;
}
// evdev accepts writes to set keyboard LEDs. There are no LEDs to set here,
// and a program that writes one should not see an error for it.
int
inputwrite(struct inode *ip, uint off, char *src, int n)
{
return n;
}
// ---------------------------------------------------------------------------
// Producers
// ---------------------------------------------------------------------------
// AT scancode set 1, E0-prefixed. The unprefixed bytes need no table at all:
// Linux's keycodes were numbered from this set and still match it below 0x54.
// These are the ones that do not.
static const struct { uint8 sc; uint8 key; } e0map[] = {
{0x1C, 96}, // KP_ENTER
{0x1D, KEY_RIGHTCTRL},
{0x35, 98}, // KP_SLASH
{0x38, KEY_RIGHTALT},
{0x47, KEY_HOME},
{0x48, KEY_UP},
{0x49, KEY_PAGEUP},
{0x4B, KEY_LEFT},
{0x4D, KEY_RIGHT},
{0x4F, KEY_END},
{0x50, KEY_DOWN},
{0x51, KEY_PAGEDOWN},
{0x52, KEY_INSERT},
{0x53, KEY_DELETE},
{0x5B, KEY_LEFTMETA},
{0x5C, KEY_RIGHTMETA},
};
// One byte from the PS/2 port, before it has been turned into a character.
void
input_ps2_scancode(int raw, int e0)
{
int code = raw & 0x7F;
int down = (raw & 0x80) ? 0 : 1;
int key = 0;
unsigned i;
if(e0){
for(i = 0; i < sizeof(e0map)/sizeof(e0map[0]); i++)
if(e0map[i].sc == code){
key = e0map[i].key;
break;
}
if(key == 0)
return; // an E0 key nothing has a name for
} else {
if(code == 0 || code > 0x58)
return;
key = code;
}
input_report(INPUT_KBD, EV_KEY, key, down);
input_sync(INPUT_KBD);
}
// USB HID usage -> Linux keycode. The mapping is not derivable: HID numbers
// keys by position on a US layout starting at 'a', and the AT set numbers them
// by the order IBM wired the matrix. The table is the same one Linux carries
// for the same reason.
static const uint8 hid_keycode[] = {
0, 0, 0, 0, 30, 48, 46, 32, 18, 33, 34, 35, 23, 36, 37, 38,
50, 49, 24, 25, 16, 19, 31, 20, 22, 47, 17, 45, 21, 44, 2, 3,
4, 5, 6, 7, 8, 9, 10, 11, 28, 1, 14, 15, 57, 12, 13, 26,
27, 43, 43, 39, 40, 41, 51, 52, 53, 58, 59, 60, 61, 62, 63, 64,
65, 66, 67, 68, 87, 88, 99, 70,119,110,102,104,111,107,109,106,
105,108,103, 69, 98, 55, 74, 78, 96, 79, 80, 81, 75, 76, 77, 71,
72, 73, 82, 83, 86,127,116,117,
};
// The eight modifier bits in a boot report, in bit order.
static const uint8 hid_modkey[8] = {
KEY_LEFTCTRL, KEY_LEFTSHIFT, KEY_LEFTALT, KEY_LEFTMETA,
KEY_RIGHTCTRL, KEY_RIGHTSHIFT, KEY_RIGHTALT, KEY_RIGHTMETA,
};
int
input_hid_keycode(int usage)
{
if(usage >= 0xE0 && usage <= 0xE7)
return hid_modkey[usage - 0xE0];
if(usage < 0 || usage >= (int)sizeof(hid_keycode))
return 0;
return hid_keycode[usage];
}
void
input_key(int usage, int down)
{
int key = input_hid_keycode(usage);
if(key == 0)
return;
input_report(INPUT_KBD, EV_KEY, key, down);
input_sync(INPUT_KBD);
}
// One mouse report: relative motion plus the current button state. `buttons`
// is a bitmask in the order both PS/2 and HID boot protocol use -- left,
// right, middle -- which is why neither caller has to reorder it.
void
input_mouse(int dx, int dy, int buttons)
{
static int last;
static const int btncode[3] = { BTN_LEFT, BTN_RIGHT, BTN_MIDDLE };
int i;
if(dx)
input_report(INPUT_MOUSE, EV_REL, REL_X, dx);
if(dy)
input_report(INPUT_MOUSE, EV_REL, REL_Y, dy);
// Only transitions. A button held down for a second is one press event, not
// a hundred -- a reader that has to filter repeats itself cannot tell a held
// button from a very fast double-click.
for(i = 0; i < 3; i++){
int now = (buttons >> i) & 1;
if(now != ((last >> i) & 1))
input_report(INPUT_MOUSE, EV_KEY, btncode[i], now);
}
last = buttons;
input_sync(INPUT_MOUSE);
}

214
kernel/dev/irq.c Normal file
View file

@ -0,0 +1,214 @@
// Hardware interrupt registration. See include/irq.h for why this exists.
#include "types.h"
#include "defs.h"
#include "param.h"
#include "traps.h"
#include "spinlock.h"
#include "irq.h"
struct irq_action {
irq_handler_t fn;
void *arg;
const char *name;
uint64 count;
};
static struct {
struct irq_action act[NCHAIN];
int n; // handlers installed; only ever grows during boot
int enabled;
int level_low;
uint64 unhandled;
} line[NIRQ];
// Guards registration against itself. The *dispatch* path deliberately does
// not take it: an interrupt handler that blocked on a lock held by whoever
// was registering would deadlock the CPU it fired on. Instead registration
// publishes an entry by writing all of its fields, issuing a barrier, and
// only then incrementing n -- so a concurrent dispatcher either does not see
// the entry at all or sees it complete.
//
// Removal is the harder direction, because compacting the array *does* move
// entries a dispatcher may be walking. irq_unregister therefore masks the
// line first and only re-routes it once the chain has been rewritten: with
// the line masked no new interrupt can arrive, and any handler already
// running holds no reference past its own return.
static struct spinlock irqlock;
void
irq_init(void)
{
initlock(&irqlock, "irq");
}
// IRQ_ERROR and IRQ_SPURIOUS are not IOAPIC pins -- they are local APIC
// vectors that xv6 numbered as though they were, so T_IRQ0+19 and T_IRQ0+31
// are already spoken for. A PCI device routed to pin 19 would deliver on the
// same vector as the LAPIC error interrupt. Refuse it here rather than let it
// be debugged later.
static int
reserved(int irq)
{
return irq == IRQ_ERROR || irq == IRQ_SPURIOUS || irq == IRQ_TIMER;
}
int
irq_register(int irq, irq_handler_t fn, void *arg, const char *name,
int level_low)
{
struct irq_action *a;
if(irq < 0 || irq >= NIRQ || fn == 0)
return -1;
if(reserved(irq)){
cprintf("irq_register: %s wants irq %d, which is reserved\n", name, irq);
return -1;
}
acquire(&irqlock);
if(line[irq].n >= NCHAIN){
release(&irqlock);
cprintf("irq_register: irq %d chain full, %s not attached\n", irq, name);
return -1;
}
// Two devices sharing a line must agree on how it is signalled; they
// physically cannot be wired both ways.
if(line[irq].n > 0 && line[irq].level_low != level_low){
release(&irqlock);
cprintf("irq_register: irq %d shared with conflicting trigger mode "
"(%s)\n", irq, name);
return -1;
}
a = &line[irq].act[line[irq].n];
a->fn = fn;
a->arg = arg;
a->name = name;
a->count = 0;
__sync_synchronize(); // publish the entry before it is reachable
line[irq].n++;
if(!line[irq].enabled){
line[irq].level_low = level_low;
ioapicroute(irq, 0, level_low);
line[irq].enabled = 1;
}
release(&irqlock);
return 0;
}
void
irq_unregister(int irq, irq_handler_t fn, void *arg)
{
int i, j;
if(irq < 0 || irq >= NIRQ)
return;
acquire(&irqlock);
for(i = 0; i < line[irq].n; i++){
if(line[irq].act[i].fn == fn && line[irq].act[i].arg == arg){
// Mask before touching the chain, always -- not only when this is the
// last handler. Compacting the array moves entries that a dispatcher on
// another CPU may be walking, so the line has to be quiet for the
// duration, and then brought back only if anything is left on it.
ioapicmask(irq);
line[irq].enabled = 0;
for(j = i; j + 1 < line[irq].n; j++)
line[irq].act[j] = line[irq].act[j+1];
line[irq].n--;
__sync_synchronize();
if(line[irq].n > 0){
ioapicroute(irq, 0, line[irq].level_low);
line[irq].enabled = 1;
}
break;
}
}
release(&irqlock);
}
void
irq_dispatch(int irq)
{
int i, n;
if(irq < 0 || irq >= NIRQ)
return;
n = line[irq].n;
if(n == 0){
line[irq].unhandled++;
return;
}
// Every handler on a shared line is offered the interrupt; each one is
// responsible for recognising that its device is idle and returning.
for(i = 0; i < n; i++){
line[irq].act[i].count++;
line[irq].act[i].fn(line[irq].act[i].arg);
}
}
// ---------------------------------------------------------------------------
// /proc/interrupts
// ---------------------------------------------------------------------------
static int
emit(char *buf, int cap, int off, const char *s)
{
while(*s && off < cap)
buf[off++] = *s++;
return off;
}
static int
emit_dec(char *buf, int cap, int off, uint64 v, int width)
{
char tmp[24];
int n = 0, i;
if(v == 0)
tmp[n++] = '0';
while(v > 0){
tmp[n++] = '0' + (v % 10);
v /= 10;
}
for(i = n; i < width && off < cap; i++)
buf[off++] = ' ';
while(n > 0 && off < cap)
buf[off++] = tmp[--n];
return off;
}
int
irq_info(char *buf, int cap)
{
int irq, i, off = 0;
for(irq = 0; irq < NIRQ; irq++){
if(line[irq].n == 0 && line[irq].unhandled == 0)
continue;
off = emit_dec(buf, cap, off, irq, 4);
off = emit(buf, cap, off, ": ");
for(i = 0; i < line[irq].n; i++){
off = emit_dec(buf, cap, off, line[irq].act[i].count, 10);
off = emit(buf, cap, off, " ");
off = emit(buf, cap, off,
line[irq].act[i].name ? line[irq].act[i].name : "?");
off = emit(buf, cap, off, i + 1 < line[irq].n ? ", " : "");
}
if(line[irq].n == 0)
off = emit(buf, cap, off, "(unhandled)");
if(line[irq].unhandled)
off = emit(buf, cap, off, " [spurious]");
off = emit(buf, cap, off, line[irq].level_low ? " level\n" : " edge\n");
}
return off;
}

75
kernel/dev/kbd.c Normal file
View file

@ -0,0 +1,75 @@
#include "types.h"
#include "x86.h"
#include "defs.h"
#include "kbd.h"
#include "input.h"
int
kbdgetc(void)
{
static uint shift;
static uchar *charcode[4] = {
normalmap, shiftmap, ctlmap, ctlmap
};
uint st, data, c;
st = inb(KBSTATP);
if((st & KBS_DIB) == 0)
return -1;
if(st & 0x20)
return -1;
data = inb(KBDATAP);
if(data == 0xE0){
shift |= E0ESC;
return 0;
}
// Feed /dev/input/event0 before the byte is turned into a character. This
// has to happen here rather than in a second handler because the byte can
// only be read from the port once -- whoever reads it owes it to everyone
// else. The console path below is untouched: both consumers see every key.
input_ps2_scancode(data, (shift & E0ESC) != 0);
// While something holds an exclusive grab on the keyboard, the console gets
// nothing. The shift/caps state below is still tracked, so releasing the
// grab does not leave the line discipline believing a modifier is held.
if(input_is_grabbed(INPUT_KBD)){
if(data & 0x80){
data = (shift & E0ESC ? data : data & 0x7F);
shift &= ~(shiftcode[data] | E0ESC);
} else {
shift |= shiftcode[data];
shift ^= togglecode[data];
}
return 0;
}
if(data & 0x80){
data = (shift & E0ESC ? data : data & 0x7F);
shift &= ~(shiftcode[data] | E0ESC);
return 0;
} else if(shift & E0ESC){
data |= 0x80;
shift &= ~E0ESC;
}
shift |= shiftcode[data];
shift ^= togglecode[data];
c = charcode[shift & (CTL | SHIFT)][data];
if(shift & CAPSLOCK){
if('a' <= c && c <= 'z')
c += 'A' - 'a';
else if('A' <= c && c <= 'Z')
c += 'a' - 'A';
}
return c;
}
void
kbdintr(void)
{
consoleintr(kbdgetc);
}

214
kernel/dev/mouse.c Normal file
View file

@ -0,0 +1,214 @@
#include "types.h"
#include "defs.h"
#include "irq.h"
#include "x86.h"
#include "traps.h"
#include "spinlock.h"
#include "sleeplock.h"
#include "fs.h"
#include "file.h"
#define KBD_S_STAT 0x64
#define KBD_S_DATA 0x60
#define KBD_C_WRITE 0xD4
#define MOUSE_ACK 0xFA
struct {
struct spinlock lock;
int x;
int y;
uint8 buttons;
} mouse_state;
static void
mouse_wait(int type)
{
int timeout = 100000;
if(type == 0){
while(timeout-- && !(inb(KBD_S_STAT) & 0x01));
} else {
while(timeout-- && (inb(KBD_S_STAT) & 0x02));
}
}
static void
mouse_write(uchar data)
{
mouse_wait(1);
outb(KBD_S_STAT, KBD_C_WRITE);
mouse_wait(1);
outb(KBD_S_DATA, data);
}
static uchar
mouse_read(void)
{
mouse_wait(0);
return inb(KBD_S_DATA);
}
static void
mouse_irq(void *arg)
{
(void)arg;
mouseintr();
}
// Move the pointer. Caller holds mouse_state.lock.
//
// Positive dy is *down*, matching both the screen and USB HID. The PS/2
// packet reports the opposite, which is why its caller negates first --
// there is one pointer and two protocols disagreeing about which way is up,
// and the conversion belongs at the edge rather than in here.
static void
mouse_move_locked(int dx, int dy)
{
mouse_state.x += dx;
mouse_state.y += dy;
if(mouse_state.x < 0) mouse_state.x = 0;
if(mouse_state.y < 0) mouse_state.y = 0;
if(mouse_state.x >= 800) mouse_state.x = 799;
if(mouse_state.y >= 600) mouse_state.y = 599;
}
// The same pointer, moved by something that is not the PS/2 port. USB HID
// uses this; anything else that produces motion events should too.
void
mouse_inject(int dx, int dy, int buttons)
{
acquire(&mouse_state.lock);
mouse_state.buttons = buttons;
mouse_move_locked(dx, dy);
release(&mouse_state.lock);
input_mouse(dx, dy, buttons);
}
void
mouseintr(void)
{
static uchar buf[3];
static int phase = 0;
uchar status;
status = inb(KBD_S_STAT);
if(!(status & 0x01) || !(status & 0x20)){
return;
}
uchar data = inb(KBD_S_DATA);
acquire(&mouse_state.lock);
if(phase == 0 && !(data & 0x08)){
release(&mouse_state.lock);
return;
}
buf[phase++] = data;
if(phase == 3){
phase = 0;
mouse_state.buttons = buf[0] & 0x07;
int dx = (buf[0] & 0x10) ? (int)buf[1] - 256 : buf[1];
int dy = (buf[0] & 0x20) ? (int)buf[2] - 256 : buf[2];
// The overflow bits mean the delta saturated and is meaningless; moving
// by it would throw the pointer across the screen.
if(!(buf[0] & 0x40) && !(buf[0] & 0x80))
mouse_move_locked(dx, -dy);
else
dx = dy = 0;
release(&mouse_state.lock);
// Outside the lock: input_report takes its own, and holding two at once
// is how a lock ordering rule gets invented by accident.
input_mouse(dx, -dy, buf[0] & 0x07);
return;
}
release(&mouse_state.lock);
}
int get_mouse_x(void) {
int x;
acquire(&mouse_state.lock);
x = mouse_state.x;
release(&mouse_state.lock);
return x;
}
int get_mouse_y(void) {
int y;
acquire(&mouse_state.lock);
y = mouse_state.y;
release(&mouse_state.lock);
return y;
}
uint8 get_mouse_buttons(void) {
uint8 b;
acquire(&mouse_state.lock);
b = mouse_state.buttons;
release(&mouse_state.lock);
return b;
}
int
mouseread(struct inode *ip, uint off, char *dst, int n)
{
struct mouse_data m;
int size = sizeof(struct mouse_data);
if(n < size) {
return -1;
}
m.x = get_mouse_x();
m.y = get_mouse_y();
m.buttons = get_mouse_buttons();
memmove(dst, &m, size);
return size;
}
void
mouseinit(void)
{
initlock(&mouse_state.lock, "mouse");
mouse_state.x = 400;
mouse_state.y = 300;
while(inb(KBD_S_STAT) & 0x01)
inb(KBD_S_DATA);
mouse_wait(1);
outb(KBD_S_STAT, 0xA8);
mouse_wait(1);
outb(KBD_S_STAT, 0x20);
mouse_wait(0);
uchar cb = inb(KBD_S_DATA);
cb |= 0x02;
cb |= 0x01;
cb &= ~0x20;
mouse_wait(1);
outb(KBD_S_STAT, 0x60);
mouse_wait(1);
outb(KBD_S_DATA, cb);
mouse_write(0xF6);
mouse_read();
mouse_write(0xF4);
mouse_read();
irq_register(IRQ_MOUSE, mouse_irq, 0, "ps2-mouse", 0);
}

492
kernel/dev/pci.c Normal file
View file

@ -0,0 +1,492 @@
// PCI bus enumeration.
//
// Everything here is done through the two-port configuration mechanism at
// 0xCF8/0xCFC: write a (bus, slot, function, register) address to one port,
// read or write the value at the other. It is the oldest and least clever way
// to reach config space, and it is the one that works everywhere without
// first having to find an ACPI table to tell you where the memory-mapped
// version lives.
//
// The scan is recursive rather than a flat sweep of all 256 buses. A flat
// sweep does find everything on a PC, but it also probes 8192 slots that are
// not there, and it cannot tell you which bridge a device sits behind -- and
// that matters as soon as interrupt routing does.
#include "types.h"
#include "defs.h"
#include "param.h"
#include "x86.h"
#include "spinlock.h"
#include "sleeplock.h"
#include "fs.h"
#include "file.h"
#include "pci.h"
#include "device.h"
static struct pci_dev devs[NPCI];
static int ndevs;
static void scan_bus(uint8 bus);
// ---------------------------------------------------------------------------
// Configuration space access
// ---------------------------------------------------------------------------
static uint32
cfg_addr(uint8 bus, uint8 slot, uint8 func, uint8 off)
{
return 0x80000000u
| ((uint32)bus << 16)
| ((uint32)slot << 11)
| ((uint32)func << 8)
| (off & 0xFC); // the low two bits are not part of the address
}
uint32
pci_read32(uint8 bus, uint8 slot, uint8 func, uint8 off)
{
outl(PCI_CONFIG_ADDR, cfg_addr(bus, slot, func, off));
return inl(PCI_CONFIG_DATA);
}
void
pci_write32(uint8 bus, uint8 slot, uint8 func, uint8 off, uint32 val)
{
outl(PCI_CONFIG_ADDR, cfg_addr(bus, slot, func, off));
outl(PCI_CONFIG_DATA, val);
}
// Config space is only addressable in dwords, so narrower reads are a shift
// and a mask of the containing dword rather than a narrower bus cycle.
uint16
pci_read16(uint8 bus, uint8 slot, uint8 func, uint8 off)
{
return (uint16)(pci_read32(bus, slot, func, off) >> ((off & 2) * 8));
}
uint8
pci_read8(uint8 bus, uint8 slot, uint8 func, uint8 off)
{
return (uint8)(pci_read32(bus, slot, func, off) >> ((off & 3) * 8));
}
void
pci_enable(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 | bits);
}
// ---------------------------------------------------------------------------
// Capabilities
//
// Everything a device can do beyond the 64 bytes of standard config header --
// MSI, MSI-X, power management, and the whole of virtio's configuration -- is
// described by a singly linked list of capability structures living in the
// device's config space. A driver that needs any of it has to walk that list.
// ---------------------------------------------------------------------------
// Returns the config-space offset of the first capability with this ID, or 0.
uint8
pci_find_cap(struct pci_dev *d, uint8 id)
{
uint8 off;
int guard;
if(!(pci_read16(d->bus, d->slot, d->func, PCI_STATUS) & PCI_STATUS_CAPLIST))
return 0;
off = pci_read8(d->bus, d->slot, d->func, PCI_CAP_PTR) & 0xFC;
// The list is built by the device, so it can be circular if the device is
// broken or if we misread it. 48 is more capabilities than the 192 usable
// bytes can hold; past that we are certainly looping.
for(guard = 0; off >= 0x40 && guard < 48; guard++){
if(pci_read8(d->bus, d->slot, d->func, off) == id)
return off;
off = pci_read8(d->bus, d->slot, d->func, off + 1) & 0xFC;
}
return 0;
}
// Point a device's MSI at a CPU and a vector.
//
// MSI is not a wire: it is a memory write the device performs to a magic
// address that the local APIC decodes as an interrupt. That makes it strictly
// better than INTx for anything modern -- no sharing, no level/edge question,
// and no chance of the interrupt arriving before the DMA it announces, since
// both are posted writes travelling the same path in order.
//
// Returns 0 on success, -1 if the device has no MSI capability.
int
pci_msi_enable(struct pci_dev *d, int vector, int cpu)
{
uint8 cap = pci_find_cap(d, PCI_CAP_MSI);
uint16 ctl;
if(cap == 0)
return -1;
ctl = pci_read16(d->bus, d->slot, d->func, cap + 2);
// 0xFEE00000 | (apic id << 12) is the LAPIC's message address; the data
// word is the vector, with delivery mode 000 (fixed) and edge trigger.
pci_write32(d->bus, d->slot, d->func, cap + 4,
0xFEE00000u | ((uint32)cpu << 12));
if(ctl & (1 << 7)){ // 64-bit address capable: data moved to +12
pci_write32(d->bus, d->slot, d->func, cap + 8, 0);
pci_write32(d->bus, d->slot, d->func, cap + 12, (uint32)vector);
} else {
pci_write32(d->bus, d->slot, d->func, cap + 8, (uint32)vector);
}
// Enable, and request exactly one vector (multiple-message-enable = 0).
// Message control shares its dword with the capability ID and next pointer,
// and config space is only writable a dword at a time, so read-modify-write
// the whole thing rather than clobbering the list this capability is on.
{
uint32 dw = pci_read32(d->bus, d->slot, d->func, cap);
ctl = (ctl & ~0x0070) | 1;
dw = (dw & 0x0000FFFF) | ((uint32)ctl << 16);
pci_write32(d->bus, d->slot, d->func, cap, dw);
}
// INTx must be masked or the device will signal both.
pci_write32(d->bus, d->slot, d->func, PCI_COMMAND,
pci_read32(d->bus, d->slot, d->func, PCI_COMMAND) | (1 << 10));
return 0;
}
// ---------------------------------------------------------------------------
// Base address registers
// ---------------------------------------------------------------------------
// A BAR does not report its own size. You find it by writing all ones and
// reading back: the device leaves the bits it does not decode as zero, so the
// size is one more than the inverse of what comes back. The original value
// has to be put back afterwards -- this is a live device whose decoder is
// briefly pointed at the top of the address space while we look.
static void
read_bars(struct pci_dev *d)
{
for (int i = 0; i < 6; i++) {
uint8 off = PCI_BAR0 + i * 4;
uint32 lo = pci_read32(d->bus, d->slot, d->func, off);
if (lo == 0)
continue;
pci_write32(d->bus, d->slot, d->func, off, 0xFFFFFFFF);
uint32 mask = pci_read32(d->bus, d->slot, d->func, off);
pci_write32(d->bus, d->slot, d->func, off, lo);
if (mask == 0)
continue;
if (lo & 1) { // I/O space
d->bar[i].is_io = 1;
d->bar[i].base = lo & ~0x3u;
d->bar[i].size = (~(mask & ~0x3u) + 1) & 0xFFFF;
continue;
}
d->bar[i].prefetch = (lo >> 3) & 1;
if (((lo >> 1) & 3) == 2) { // 64-bit BAR: eats the next slot
uint32 hi = pci_read32(d->bus, d->slot, d->func, off + 4);
pci_write32(d->bus, d->slot, d->func, off + 4, 0xFFFFFFFF);
uint32 hmask = pci_read32(d->bus, d->slot, d->func, off + 4);
pci_write32(d->bus, d->slot, d->func, off + 4, hi);
d->bar[i].is_64 = 1;
d->bar[i].base = ((uint64)hi << 32) | (lo & ~0xFu);
d->bar[i].size = ~(((uint64)hmask << 32) | (mask & ~0xFu)) + 1;
i++; // the upper half is not its own BAR
continue;
}
d->bar[i].base = lo & ~0xFu;
d->bar[i].size = (~(mask & ~0xFu) + 1) & 0xFFFFFFFF;
}
}
// ---------------------------------------------------------------------------
// Enumeration
// ---------------------------------------------------------------------------
static void
add_func(uint8 bus, uint8 slot, uint8 func)
{
uint16 vendor = pci_read16(bus, slot, func, PCI_VENDOR_ID);
if (vendor == 0xFFFF) // nothing plugged in
return;
if (ndevs >= NPCI) {
cprintf("pci: more than %d devices, ignoring the rest\n", NPCI);
return;
}
struct pci_dev *d = &devs[ndevs++];
d->bus = bus;
d->slot = slot;
d->func = func;
d->vendor = vendor;
d->device = pci_read16(bus, slot, func, PCI_DEVICE_ID);
d->revision = pci_read8(bus, slot, func, PCI_REVISION);
d->prog_if = pci_read8(bus, slot, func, PCI_PROG_IF);
d->subclass = pci_read8(bus, slot, func, PCI_SUBCLASS);
d->class = pci_read8(bus, slot, func, PCI_CLASS);
d->header_type = pci_read8(bus, slot, func, PCI_HEADER_TYPE);
d->irq = pci_read8(bus, slot, func, PCI_INTERRUPT_LINE);
// Only header type 0 has six BARs. A bridge (type 1) has two and a very
// different layout, and reading it as though it had six would hand out
// window registers as if they were device apertures.
if ((d->header_type & 0x7F) == 0)
read_bars(d);
// Walk down through a PCI-to-PCI bridge to whatever is behind it.
if ((d->header_type & 0x7F) == 1) {
uint8 secondary = pci_read8(bus, slot, func, PCI_SECONDARY_BUS);
if (secondary != 0 && secondary != bus)
scan_bus(secondary);
}
}
static void
scan_bus(uint8 bus)
{
for (uint8 slot = 0; slot < 32; slot++) {
if (pci_read16(bus, slot, 0, PCI_VENDOR_ID) == 0xFFFF)
continue;
add_func(bus, slot, 0);
// Bit 7 of the header type says the device has more than one function.
// Probing all eight unconditionally is harmless on real hardware but
// produces phantom entries on some emulated devices, which answer every
// function with a copy of function 0.
if (pci_read8(bus, slot, 0, PCI_HEADER_TYPE) & 0x80)
for (uint8 func = 1; func < 8; func++)
add_func(bus, slot, func);
}
}
// ---------------------------------------------------------------------------
// Lookup
// ---------------------------------------------------------------------------
int
pci_count(void)
{
return ndevs;
}
struct pci_dev *
pci_get(int i)
{
return (i >= 0 && i < ndevs) ? &devs[i] : 0;
}
struct pci_dev *
pci_find_class(uint8 class, uint8 subclass)
{
for (int i = 0; i < ndevs; i++)
if (devs[i].class == class && devs[i].subclass == subclass)
return &devs[i];
return 0;
}
struct pci_dev *
pci_find_id(uint16 vendor, uint16 device)
{
for (int i = 0; i < ndevs; i++)
if (devs[i].vendor == vendor && devs[i].device == device)
return &devs[i];
return 0;
}
// ---------------------------------------------------------------------------
// Driver binding
// ---------------------------------------------------------------------------
static int
matches(const struct pci_match *m, struct pci_dev *d)
{
if (m->vendor != PCI_ANY)
return m->vendor == d->vendor &&
(m->device == PCI_ANY || m->device == d->device);
// A class match, for drivers that handle a kind of device rather than a
// particular model.
return m->class == d->class &&
(m->subclass == 0xFF || m->subclass == d->subclass);
}
// Offer every unclaimed device to a driver. Called by pci_init() for each
// entry in the registry; see kernel/drivers/registry.c.
// Defined further down with the /proc/pci formatting it was written for.
static const char *class_name(uint8 class, uint8 subclass);
// Translate a PCI class code into the bus-agnostic one the device model uses.
//
// The two vocabularies are deliberately separate. PCI's class byte describes
// how the card presents itself on that bus; DEVCLASS describes what the rest of
// the kernel can *do* with it, and a USB disk has to land in the same bucket as
// a SATA one despite sharing no numbering with it at all.
enum dev_class
pci_class_of(const struct pci_dev *d)
{
switch (d->class) {
case PCI_CLASS_DISPLAY: return DEVCLASS_DISPLAY;
case PCI_CLASS_STORAGE: return DEVCLASS_STORAGE;
case PCI_CLASS_NETWORK: return DEVCLASS_NET;
case PCI_CLASS_SERIAL_BUS: return DEVCLASS_BUS;
default: return DEVCLASS_OTHER;
}
}
// Human-readable description, the same text /proc/pci prints for the device.
void
pci_describe(const struct pci_dev *d, char *out, int cap)
{
const char *s = class_name(d->class, d->subclass);
int n = 0;
while (s[n] && n < cap - 1) {
out[n] = s[n];
n++;
}
out[n] = '\0';
}
void
pci_bind(const struct pci_driver *drv)
{
for (int i = 0; i < ndevs; i++) {
struct pci_dev *d = &devs[i];
if (d->driver)
continue;
for (const struct pci_match *m = drv->match; m->vendor || m->class; m++) {
if (!matches(m, d))
continue;
if (drv->probe(d) == 0) {
d->driver = drv->name;
// Publish it in the bus-agnostic device list. Done here rather than in
// each driver's probe() so that a driver cannot forget to, and so the
// list is populated in bind order -- which is the order a device
// manager should show.
struct device *dev = device_add(drv->devname ? drv->devname : drv->name,
DEVBUS_PCI, pci_class_of(d), drv->name);
if (dev) {
dev->ops = (void *)drv->ops;
dev->vendor = d->vendor;
dev->product = d->device;
dev->b = d->bus; dev->s = d->slot; dev->f = d->func;
pci_describe(d, dev->desc, sizeof(dev->desc));
}
}
break;
}
}
}
// ---------------------------------------------------------------------------
// Fixed-width lowercase hex, the way lspci prints addresses.
static int
put_hex(char *out, uint64 v, int digits)
{
static const char d[] = "0123456789abcdef";
for (int i = digits - 1; i >= 0; i--)
*out++ = d[(v >> (i * 4)) & 0xF];
return digits;
}
static int
put_str(char *out, const char *s)
{
int n = 0;
while (s[n]) { out[n] = s[n]; n++; }
return n;
}
static const char *
class_name(uint8 class, uint8 subclass)
{
switch (class) {
case PCI_CLASS_STORAGE:
return subclass == 0x01 ? "IDE controller"
: subclass == 0x06 ? "SATA controller"
: subclass == 0x08 ? "NVMe controller"
: "storage controller";
case PCI_CLASS_NETWORK: return "network controller";
case PCI_CLASS_DISPLAY: return "display controller";
case PCI_CLASS_MULTIMEDIA: return "multimedia device";
case PCI_CLASS_BRIDGE:
return subclass == 0x00 ? "host bridge"
: subclass == 0x01 ? "ISA bridge"
: subclass == 0x04 ? "PCI bridge"
: "bridge";
case PCI_CLASS_SERIAL_BUS:
return subclass == 0x03 ? "USB controller" : "serial bus controller";
}
return "device";
}
// Fills a buffer for /proc/pci. Returns bytes written.
//
// There is no snprintf in the kernel, so this appends by hand in the same
// style as kernel/dev/procfs.c. The output is deliberately lspci-shaped --
// "bus:slot.func vendor:device description" -- because that is the format
// anyone reading it will already know how to scan.
int
pci_info(char *buf, int cap)
{
int len = 0;
for (int i = 0; i < ndevs; i++) {
struct pci_dev *d = &devs[i];
if (len > cap - 96) // room for one more line
break;
len += put_hex(buf + len, d->bus, 2);
buf[len++] = ':';
len += put_hex(buf + len, d->slot, 2);
buf[len++] = '.';
len += put_hex(buf + len, d->func, 1);
buf[len++] = ' ';
len += put_hex(buf + len, d->vendor, 4);
buf[len++] = ':';
len += put_hex(buf + len, d->device, 4);
buf[len++] = ' ';
len += put_str(buf + len, class_name(d->class, d->subclass));
len += put_str(buf + len, " [");
len += put_str(buf + len, d->driver ? d->driver : "-");
buf[len++] = ']';
buf[len++] = '\n';
}
return len;
}
void
pci_init(void)
{
ndevs = 0;
// If there is no host bridge at 0:0.0 there is no PCI, and every config
// read will come back 0xFFFFFFFF. Saying so once beats 8192 silent probes.
if (pci_read16(0, 0, 0, PCI_VENDOR_ID) == 0xFFFF) {
cprintf("pci: no host bridge; bus not present\n");
return;
}
scan_bus(0);
cprintf("pci: %d device%s\n", ndevs, ndevs == 1 ? "" : "s");
pci_bind_all();
}

431
kernel/dev/procfs.c Normal file
View file

@ -0,0 +1,431 @@
// /proc — synthesised kernel status files.
//
// Each file is a character device node under major PROCFS_MAJOR, distinguished
// by its minor number, created by init at boot. Reading one renders the whole
// text into a scratch buffer and returns the slice starting at the caller's
// file offset, so a normal read-until-zero loop terminates.
//
// SCOPE: these are the *global* files. Per-process directories (/proc/<pid>/…)
// are not here, because they need something this kernel does not have yet:
// synthetic directories, i.e. a directory whose entries are generated at
// lookup and getdents time rather than read from disk. That is a real VFS
// feature touching iget/ilock/dirlookup/getdents, and it is the natural
// companion to the SchFS work rather than a bolt-on to the current fs.c.
// The namespace (which files exist, including one directory per process) is
// kernel/fs/procfs.c; this file only generates the *content* of the global
// files. `ps` and `top` work through the former.
#include "types.h"
#include "defs.h"
#include "irq.h"
#include "x86.h"
#include "device.h"
#include "display.h"
#include "param.h"
#include "memlayout.h"
#include "mmu.h"
#include "spinlock.h"
#include "sleeplock.h"
#include "fs.h"
#include "file.h"
#include "proc.h"
#include "uaccess.h"
#include "errno.h"
#define PROC_BUFSZ 2048
// --- tiny formatting helpers (no snprintf in the kernel) -------------------
struct sbuf {
char *p;
int len;
int cap;
};
static void
sb_putc(struct sbuf *sb, char c)
{
if (sb->len < sb->cap - 1)
sb->p[sb->len++] = c;
}
static void
sb_puts(struct sbuf *sb, const char *s)
{
while (*s)
sb_putc(sb, *s++);
}
// A string in a fixed-width column, space-padded. cprintf has no width
// specifier and neither does this buffer, so tables build their own columns.
// Over-long values are allowed to run past their column rather than be
// truncated: a misaligned row is easier to read than a mangled name.
static void
sb_pad(struct sbuf *sb, const char *s, int width)
{
int n = 0;
while (s[n]) {
sb_putc(sb, s[n]);
n++;
}
while (n++ < width)
sb_putc(sb, ' ');
}
// Four lowercase hex digits, zero-padded. PCI and USB identifiers are always
// written this way, and a lookup table keyed on "8086:100e" does not match
// "8086:100E" or "8086:10e".
static void
sb_puthex4(struct sbuf *sb, uint32 v)
{
static const char hex[] = "0123456789abcdef";
for (int i = 3; i >= 0; i--)
sb_putc(sb, hex[(v >> (i * 4)) & 0xF]);
}
static void
sb_putu(struct sbuf *sb, uint64 v)
{
char tmp[24];
int i = 0;
if (v == 0) { sb_putc(sb, '0'); return; }
while (v && i < (int)sizeof(tmp)) { tmp[i++] = '0' + (v % 10); v /= 10; }
while (i--) sb_putc(sb, tmp[i]);
}
// "<label><value> <unit>\n" — the shape almost every /proc line takes.
static void
sb_kv(struct sbuf *sb, const char *label, uint64 v, const char *unit)
{
sb_puts(sb, label);
sb_putu(sb, v);
if (unit && *unit) { sb_putc(sb, ' '); sb_puts(sb, unit); }
sb_putc(sb, '\n');
}
// --- the files -------------------------------------------------------------
// PHYSTOP is the top of usable physical memory; kalloc hands out everything
// above the kernel image. Reporting the total honestly and leaving "free" as a
// share of it is better than the invented constants sysinfo() used to return.
#define TOTAL_KB (PHYSTOP / 1024)
static void
gen_meminfo(struct sbuf *sb)
{
uint64 total = TOTAL_KB;
uint64 freek = kalloc_free_bytes() / 1024;
sb_kv(sb, "MemTotal: ", total, "kB");
sb_kv(sb, "MemFree: ", freek, "kB");
sb_kv(sb, "MemAvailable: ", freek, "kB");
sb_kv(sb, "Buffers: ", 0, "kB");
sb_kv(sb, "Cached: ", 0, "kB");
sb_kv(sb, "SwapTotal: ", 0, "kB");
sb_kv(sb, "SwapFree: ", 0, "kB");
}
static void
gen_uptime(struct sbuf *sb)
{
uint t = uptime(); // timer ticks, 100 Hz
sb_putu(sb, t / 100);
sb_putc(sb, '.');
sb_putu(sb, (t % 100) / 10);
sb_putu(sb, (t % 100) % 10);
sb_puts(sb, " 0.00\n"); // idle time: not tracked
}
static void
gen_cpuinfo(struct sbuf *sb)
{
// Online CPUs, not present ones -- the same rule Linux follows, and the
// reason matters: something reading this to size a thread pool would
// otherwise be told a VM with four processors has four to schedule on, when
// three of them are enumerated and idle. "cpu cores" stays the present count,
// which is what that field means.
// Real values, from CPUID. These used to read "Schunix" and "Schunix virtual
// CPU" -- a thing to print when you have not asked the processor, and
// actively unhelpful, because identifying the machine is the one question
// /proc/cpuinfo exists to answer.
//
// Leaf 0 returns the vendor string in EBX:EDX:ECX, in that order, which is
// not the order anyone expects. Leaves 0x80000002..4 hold the 48-character
// brand string. Leaf 0x80000000 reports the highest extended leaf, and asking
// for one the CPU does not implement returns the highest supported leaf's
// value rather than an error -- so checking first is not optional.
char vendor[13], brand[49];
uint32 a, b, c, d;
cpuid_leaf(0, &a, &b, &c, &d);
*(uint32 *)&vendor[0] = b;
*(uint32 *)&vendor[4] = d;
*(uint32 *)&vendor[8] = c;
vendor[12] = '\0';
brand[0] = '\0';
cpuid_leaf(0x80000000, &a, 0, 0, 0);
if (a >= 0x80000004) {
for (int leaf = 0; leaf < 3; leaf++) {
cpuid_leaf(0x80000002 + leaf, &a, &b, &c, &d);
*(uint32 *)&brand[leaf * 16 + 0] = a;
*(uint32 *)&brand[leaf * 16 + 4] = b;
*(uint32 *)&brand[leaf * 16 + 8] = c;
*(uint32 *)&brand[leaf * 16 + 12] = d;
}
brand[48] = '\0';
}
// Intel pads the brand string with leading spaces, which read as a broken
// field rather than a name.
const char *model = brand;
while (*model == ' ')
model++;
if (*model == '\0')
model = "unknown";
for (int i = 0; i < ncpu_online; i++) {
sb_kv(sb, "processor\t: ", i, 0);
sb_puts(sb, "vendor_id\t: ");
sb_puts(sb, vendor);
sb_putc(sb, '\n');
sb_puts(sb, "model name\t: ");
sb_puts(sb, model);
sb_putc(sb, '\n');
sb_kv(sb, "cpu cores\t: ", ncpu, 0);
sb_putc(sb, '\n');
}
if (ncpu_online < ncpu) {
sb_kv(sb, "cpus present\t: ", ncpu, 0);
sb_puts(sb, "note\t\t: additional processors are enumerated but not "
"started; see docs/roadmap.md\n");
}
}
static void
gen_version(struct sbuf *sb)
{
sb_puts(sb, "Schunix version 0.1.0 (schunix) #1 SMP x86_64\n");
}
static void
gen_stat(struct sbuf *sb)
{
uint t = uptime();
sb_puts(sb, "cpu ");
sb_putu(sb, t); sb_puts(sb, " 0 0 0 0 0 0 0 0 0\n");
// Online, for the same reason as cpuinfo: top and friends count the cpuN
// lines here to decide how many cores to draw.
for (int i = 0; i < ncpu_online; i++) {
sb_puts(sb, "cpu");
sb_putu(sb, i);
sb_putc(sb, ' ');
sb_putu(sb, t); sb_puts(sb, " 0 0 0 0 0 0 0 0 0\n");
}
sb_kv(sb, "processes ", (uint64)proc_num(), 0);
sb_kv(sb, "procs_running ", 1, 0);
sb_kv(sb, "btime ", 0, 0);
}
// Everything attached, whatever bus found it. The one place that answers "what
// is in this machine" across PCI, USB and platform devices at once -- which is
// the question a device manager is, so this is where one would read from.
//
// Columns rather than key/value: this is a table of like things, and a reader
// scanning for an unclaimed device wants the driver column to line up.
static void
gen_devices(struct sbuf *sb)
{
sb_puts(sb, "name bus class driver id device\n");
for (int i = 0; i < device_count(); i++) {
struct device *d = device_at(i);
if (!d)
break;
// 14 for the name and driver columns: "virtio-gpu0" is eleven characters
// and overran a ten-wide column, which sb_pad allows on purpose but which
// made the whole table unreadable.
sb_pad(sb, d->name, 14);
sb_pad(sb, dev_bus_name(d->bus), 10);
sb_pad(sb, dev_class_name(d->class), 10);
// An unclaimed device is the interesting row, so it gets a visible marker
// rather than an empty column.
sb_pad(sb, d->driver ? d->driver : "(none)", 14);
// vendor:product, the way lspci and lsusb print it. The kernel has no
// business resolving these to names -- that is what /usr/share/usb.ids is
// for, and it belongs to a userspace tool. What the kernel owes is the
// number, in the form a lookup expects.
if (d->vendor || d->product) {
sb_puthex4(sb, d->vendor);
sb_putc(sb, ':');
sb_puthex4(sb, d->product);
sb_putc(sb, ' ');
} else {
sb_puts(sb, " ");
}
sb_puts(sb, d->desc[0] ? d->desc : "-");
sb_putc(sb, '\n');
}
}
static void
gen_loadavg(struct sbuf *sb)
{
// No load tracking; report zeros but a truthful process count.
sb_puts(sb, "0.00 0.00 0.00 1/");
sb_putu(sb, (uint64)proc_num());
sb_puts(sb, " 1\n");
}
// df and mountpoint parse this. One root filesystem, no mount table yet.
static void
gen_mounts(struct sbuf *sb)
{
// Root must be last: df keeps the last mount point that prefixes the path
// it was asked about, so listing / first made `df /` report /proc instead.
sb_puts(sb, "devtmpfs /dev devtmpfs rw 0 0\n");
sb_puts(sb, "proc /proc proc rw 0 0\n");
sb_puts(sb, "/dev/root / schfs rw 0 0\n");
}
static void
gen_filesystems(struct sbuf *sb)
{
sb_puts(sb, "\tschfs\n");
sb_puts(sb, "nodev\tproc\n");
sb_puts(sb, "nodev\tdevtmpfs\n");
}
// --- dispatch --------------------------------------------------------------
#define PROC_MEMINFO 1
#define PROC_UPTIME 2
#define PROC_CPUINFO 3
#define PROC_VERSION 4
#define PROC_STAT 5
#define PROC_LOADAVG 6
#define PROC_MOUNTS 7
#define PROC_FILESYSTEMS 8
#define PROC_SCHFS 9
#define PROC_SYSRQ 10
#define PROC_PCI 11
#define PROC_INTERRUPTS 12
#define PROC_DEVICES 13
// Render global file `id` (a PROC_* constant) into `buf`. Returns its length.
//
// Split out of procread() so the /proc *filesystem* (kernel/fs/procfs.c) can
// use the same generators: that file owns the namespace, this one owns the
// content, and they have no other business with each other.
int
proc_gen_global(int id, char *buf, int cap)
{
struct sbuf sb = { buf, 0, cap };
switch (id) {
case PROC_MEMINFO: gen_meminfo(&sb); break;
case PROC_UPTIME: gen_uptime(&sb); break;
case PROC_CPUINFO: gen_cpuinfo(&sb); break;
case PROC_VERSION: gen_version(&sb); break;
case PROC_STAT: gen_stat(&sb); break;
case PROC_LOADAVG: gen_loadavg(&sb); break;
case PROC_MOUNTS: gen_mounts(&sb); break;
case PROC_FILESYSTEMS: gen_filesystems(&sb); break;
// SchFS renders straight into the buffer rather than through sbuf, because
// it lives in kernel/fs/schfs and must not depend on this file's helpers.
case PROC_SCHFS: sb.len = schfs_kinfo(buf, PROC_BUFSZ); break;
// Same arrangement as SchFS above: pci.c renders its own lines so that it
// does not depend on this file's helpers.
case PROC_PCI: sb.len = pci_info(buf, PROC_BUFSZ); break;
// Likewise irq.c: it owns the table, so it renders it.
case PROC_INTERRUPTS: sb.len = irq_info(buf, PROC_BUFSZ); break;
case PROC_DEVICES: gen_devices(&sb); break;
default: return 0;
}
return sb.len;
}
int
procread(struct inode *ip, uint off, char *dst, int n)
{
char buf[PROC_BUFSZ];
int len = proc_gen_global(ip->minor, buf, PROC_BUFSZ);
if (len <= 0 || (int)off >= len)
return 0; // EOF
int avail = len - (int)off;
if (n > avail)
n = avail;
memmove(dst, buf + off, n);
return n;
}
// /proc is read-only, with one deliberate exception.
//
// /proc/sysrq is the same idea as Linux's sysrq-trigger: a way to make the
// kernel do something drastic on purpose. Right now the only command is 'c',
// crash — which exists because the crash screen is otherwise the one part of
// the system that can only be tested by having a bug. A screen you cannot look
// at until something goes wrong is a screen nobody has ever actually read.
// One sysrq command byte. Shared by the device path (procwrite, below) and
// the filesystem path (kernel/fs/procfs.c), so the two cannot drift.
static void
proc_sysrq_cmd(char cmd)
{
switch (cmd) {
case 'c':
panic("sysrq: crash requested from /proc/sysrq");
case 'u':
xhci_dump();
break;
case 's':
usb_storage_dump();
break;
case 'd':
display_selftest();
break;
case 'n':
e1000_selftest();
break;
case 't':
text_selftest();
break;
default:
cprintf("sysrq: unknown command '%c' "
"(known: c = crash, u = dump USB, s = read the USB disk, "
"d = display self-test)\n",
cmd);
break;
}
}
// `user_src` says whether `src` is a user address that still needs copying in;
// the filesystem hands one over, the device path has already copied.
int
proc_write_sysrq(int user_src, uint64 src, uint n)
{
char c;
if (n < 1)
return -1;
if (user_src) {
if (copy_from_user(&c, src, 1) < 0)
return -EFAULT;
} else {
c = *(char *)src;
}
proc_sysrq_cmd(c);
return (int)n;
}
// The character-device path: /proc/sysrq reached through the old mknod'd node
// rather than through the /proc filesystem. `src` is already a kernel buffer
// here, which is why it passes user_src = 0.
int
procwrite(struct inode *ip, uint off, char *src, int n)
{
(void)off;
if (ip->minor != PROC_SYSRQ)
return -1;
return proc_write_sysrq(0, (uint64)src, (uint)n);
}

92
kernel/dev/ramdisk.c Normal file
View file

@ -0,0 +1,92 @@
// Memory-backed block device.
//
// Serves an entire SCHFS image out of RAM, so the system can boot and run a
// shell with the physical disk untouched. That is what makes destructive work
// on the real disk survivable: reformatting, running mkfs, or bringing up a new
// on-disk format cannot take away the root you are running from. It is also the
// first piece of the installer -- an install environment is exactly "a root in
// RAM plus a disk you are allowed to destroy".
//
// The image is linked into the kernel as a binary blob (see the ramdisk_obj
// rule in the Makefile) and is served in place: the blob lives in writable
// kernel memory, so writes to the ramdisk simply modify it. They are lost on
// reboot, which is the expected behaviour for this kind of root.
//
// Enabled by building with RAMROOT=1. Without it this file still compiles, the
// device reports zero size, and the kernel boots from the IDE disk as before.
#include "types.h"
#include "defs.h"
#include "param.h"
#include "memlayout.h"
#include "mmu.h"
#include "spinlock.h"
#include "sleeplock.h"
#include "fs.h"
#include "buf.h"
#ifdef RAMDISK_ROOT
// Supplied by `ld -r -b binary` over build/fs-ram.img, renamed by objcopy.
extern uchar _binary_ramdisk_start[];
extern uchar _binary_ramdisk_size[];
#endif
static uchar *rd_base;
static uint64 rd_size; // bytes
static uint rd_blocks; // rd_size / BSIZE
void
ramdiskinit(void)
{
#ifdef RAMDISK_ROOT
rd_base = _binary_ramdisk_start;
rd_size = (uint64)_binary_ramdisk_size;
rd_blocks = (uint)(rd_size / BSIZE);
if(rd_blocks == 0)
panic("ramdiskinit: embedded image is empty");
// A truncated image is worth catching here rather than as a confusing
// "invalid file system" panic later: the superblock lives in block 1.
if(rd_blocks < 2)
panic("ramdiskinit: embedded image smaller than a superblock");
cprintf("ramdisk: %u blocks (%u KB) at %p\n",
rd_blocks, (uint)(rd_size / 1024), rd_base);
#else
rd_base = 0;
rd_size = 0;
rd_blocks = 0;
#endif
}
int
ramdisk_present(void)
{
return rd_blocks != 0;
}
// Sync buf with the in-memory image. Mirrors iderw()'s contract: on a read,
// fill b->data and set B_VALID; on a write, copy out and clear B_DIRTY.
void
ramdiskrw(struct buf *b)
{
if(!holdingsleep(&b->lock))
panic("ramdiskrw: buf not locked");
if((b->flags & (B_VALID|B_DIRTY)) == B_VALID)
panic("ramdiskrw: nothing to do");
if(rd_base == 0)
panic("ramdiskrw: no ramdisk");
if(b->blockno >= rd_blocks)
panic("ramdiskrw: block out of range");
uchar *p = rd_base + (uint64)b->blockno * BSIZE;
if(b->flags & B_DIRTY){
memmove(p, b->data, BSIZE);
b->flags &= ~B_DIRTY;
} else {
memmove(b->data, p, BSIZE);
}
b->flags |= B_VALID;
}

242
kernel/dev/text.c Normal file
View file

@ -0,0 +1,242 @@
// Text rendering. See include/text.h for what this replaces and why.
#include "types.h"
#include "defs.h"
#include "param.h"
#include "text.h"
// Declared, not included. include/font.h *defines* font_data rather than
// declaring it, so including it here as well as in vga.c gives two definitions
// of the same array and the link fails. The header is generated data; leaving
// it alone and referring to the symbol is less fragile than editing it.
extern unsigned char font_data[256][16];
// --- UTF-8 -----------------------------------------------------------------
uint32
utf8_next(const char **p)
{
const unsigned char *s = (const unsigned char *)*p;
uint32 c = s[0];
if (c < 0x80) {
*p += 1;
return c;
}
int extra;
uint32 cp;
if ((c & 0xE0) == 0xC0) { extra = 1; cp = c & 0x1F; }
else if ((c & 0xF0) == 0xE0) { extra = 2; cp = c & 0x0F; }
else if ((c & 0xF8) == 0xF0) { extra = 3; cp = c & 0x07; }
else {
// A continuation byte or 0xF8..0xFF where a lead byte should be. Consume
// exactly one so a malformed string cannot stall the loop -- a decoder that
// does not advance on bad input is a hang, not a rendering artefact.
*p += 1;
return 0xFFFD;
}
for (int i = 1; i <= extra; i++) {
if ((s[i] & 0xC0) != 0x80) { // truncated sequence
*p += 1;
return 0xFFFD;
}
cp = (cp << 6) | (s[i] & 0x3F);
}
*p += extra + 1;
// Surrogates are not characters, and an overlong encoding is the classic way
// to smuggle a byte past a filter that checks the decoded form.
if (cp >= 0xD800 && cp <= 0xDFFF)
return 0xFFFD;
if ((extra == 1 && cp < 0x80) || (extra == 2 && cp < 0x800) ||
(extra == 3 && cp < 0x10000))
return 0xFFFD;
return cp;
}
// --- the built-in bitmap font as a provider --------------------------------
//
// font_data is 256 CP437 glyphs of 8x16, one bit per pixel. Expanded here into
// 8-bit coverage so that every caller works in one representation and an
// outline rasteriser can be added without touching any of them.
//
// The expansion buffer is static and reused per glyph. That is fine because
// glyphs are consumed immediately by the blitter and nothing holds one; it
// would not be fine the moment a caller kept a struct glyph across calls, which
// is why the header says nothing about the lifetime being longer than that.
#define BUILTIN_W 8
#define BUILTIN_H 16
static unsigned char builtin_cov[BUILTIN_W * BUILTIN_H];
// Unicode to CP437, for the range where they differ and it matters.
//
// The built-in font is CP437, not Latin-1 -- a fact that has bitten this
// codebase before. Rather than pretend otherwise, map the handful of
// codepoints worth having and fall back for the rest. This table exists to be
// *deleted* when a real font arrives; it is a bridge, not a design.
static uint32
to_cp437(uint32 cp)
{
if (cp < 0x80)
return cp;
switch (cp) {
case 0x00A0: return 0x20; // no-break space
case 0x00B0: return 0xF8; // degree
case 0x00B1: return 0xF1; // plus-minus
case 0x00B7: return 0xFA; // middle dot
case 0x2022: return 0x07; // bullet
case 0x2190: return 0x1B; // arrows
case 0x2191: return 0x18;
case 0x2192: return 0x1A;
case 0x2193: return 0x19;
case 0x2500: return 0xC4; // box drawing, the part worth keeping
case 0x2502: return 0xB3;
case 0x250C: return 0xDA;
case 0x2510: return 0xBF;
case 0x2514: return 0xC0;
case 0x2518: return 0xD9;
case 0x251C: return 0xC3;
case 0x2524: return 0xB4;
case 0x252C: return 0xC2;
case 0x2534: return 0xC1;
case 0x253C: return 0xC5;
case 0x2588: return 0xDB; // full block
case 0x2591: return 0xB0; // shades
case 0x2592: return 0xB1;
case 0x2593: return 0xB2;
default: return 0; // "no glyph"
}
}
static int
builtin_glyph(struct font *f, uint32 cp, struct glyph *out)
{
uint32 idx = to_cp437(cp);
if (idx == 0 && cp != 0)
return -1; // caller substitutes
for (int y = 0; y < BUILTIN_H; y++) {
unsigned char bits = font_data[idx & 0xFF][y];
for (int x = 0; x < BUILTIN_W; x++)
builtin_cov[y * BUILTIN_W + x] = (bits & (0x80 >> x)) ? 0xFF : 0x00;
}
out->cov = builtin_cov;
out->w = BUILTIN_W;
out->h = BUILTIN_H;
out->bearing_x = 0;
// The whole cell sits above the baseline, less two rows of descender space.
// The value matters only for consistency with whatever font comes next.
out->bearing_y = BUILTIN_H - 3;
out->advance = BUILTIN_W;
return 0;
}
static int builtin_height(struct font *f) { return BUILTIN_H; }
static int builtin_baseline(struct font *f) { return BUILTIN_H - 3; }
static const struct font_ops builtin_ops = {
.glyph = builtin_glyph,
.height = builtin_height,
.baseline = builtin_baseline,
};
static struct font builtin = { &builtin_ops, 0 };
struct font *
font_builtin(void)
{
return &builtin;
}
// --- blitting --------------------------------------------------------------
// Blend `color` over the pixel at (x, y) by coverage a.
//
// Reads the destination, which a bitmap font never needs -- but an
// anti-aliased edge is defined as a blend with what is already there, and a
// renderer that cannot do it is a renderer that cannot draw a real typeface.
static void
blend_px(struct surface *s, int x, int y, uint32 color, unsigned a)
{
if (x < 0 || y < 0 || x >= s->width || y >= s->height)
return;
uint32 *px = (uint32 *)((unsigned char *)s->pixels + (uint64)y * s->stride) + x;
if (a >= 255) {
*px = color;
return;
}
uint32 dst = *px;
uint32 r = (((color >> 16) & 0xFF) * a + ((dst >> 16) & 0xFF) * (255 - a)) / 255;
uint32 g = (((color >> 8) & 0xFF) * a + ((dst >> 8) & 0xFF) * (255 - a)) / 255;
uint32 b = (((color ) & 0xFF) * a + ((dst ) & 0xFF) * (255 - a)) / 255;
*px = (r << 16) | (g << 8) | b;
}
int
text_draw_glyph(struct surface *s, struct font *f, int x, int y,
uint32 cp, uint32 color)
{
struct glyph g;
if (f->ops->glyph(f, cp, &g) != 0) {
// Substitute rather than skip. A missing glyph that draws nothing makes
// text silently shorter and the fault impossible to see; a visible box
// says exactly what happened and where.
if (f->ops->glyph(f, 0x25A1, &g) != 0 && // white square
f->ops->glyph(f, '?', &g) != 0)
return 0;
}
int ox = x + g.bearing_x;
int oy = y - g.bearing_y; // bearing is measured up from the baseline
for (int gy = 0; gy < g.h; gy++)
for (int gx = 0; gx < g.w; gx++) {
unsigned a = g.cov[gy * g.w + gx];
if (a)
blend_px(s, ox + gx, oy + gy, color, a);
}
return g.advance;
}
int
text_draw(struct surface *s, struct font *f, int x, int y,
const char *utf8, uint32 color)
{
int pen = x;
const char *p = utf8;
while (*p) {
uint32 cp = utf8_next(&p);
if (cp == '\n')
break; // layout is the caller's business, not ours
pen += text_draw_glyph(s, f, pen, y, cp, color);
}
return pen - x;
}
int
text_measure(struct font *f, const char *utf8)
{
int w = 0;
const char *p = utf8;
struct glyph g;
while (*p) {
uint32 cp = utf8_next(&p);
if (cp == '\n')
break;
if (f->ops->glyph(f, cp, &g) == 0)
w += g.advance;
else if (f->ops->glyph(f, '?', &g) == 0)
w += g.advance;
}
return w;
}

606
kernel/dev/ttf.c Normal file
View file

@ -0,0 +1,606 @@
// TrueType outlines. See include/ttf.h for scope.
//
// Written from Apple's TrueType Reference Manual and the OpenType
// specification. Nothing here is derived from FreeType or stb_truetype.
#ifdef TTF_HOST
#include <string.h>
#include <stdlib.h>
typedef unsigned char uint8;
typedef unsigned short uint16;
typedef unsigned int uint32;
typedef signed short int16;
#else
#include "types.h"
#include "defs.h"
#include "param.h"
#endif
#include "text.h"
#include "ttf.h"
// Every multi-byte field in an sfnt file is big-endian, whatever the machine
// is. Read them byte by byte rather than casting: the file is not guaranteed
// to be aligned, and on x86 a cast would silently byte-swap everything.
static uint16 rd16(const uint8 *p) { return (uint16)((p[0] << 8) | p[1]); }
static int16 rs16(const uint8 *p) { return (int16)rd16(p); }
static uint32 rd32(const uint8 *p)
{
return ((uint32)p[0] << 24) | ((uint32)p[1] << 16) |
((uint32)p[2] << 8) | p[3];
}
static uint32
find_table(const uint8 *d, uint32 len, const char *tag, uint32 *out_len)
{
if (len < 12)
return 0;
int n = rd16(d + 4);
for (int i = 0; i < n; i++) {
const uint8 *e = d + 12 + 16 * i;
if ((uint32)(12 + 16 * i + 16) > len)
return 0;
if (e[0] == (uint8)tag[0] && e[1] == (uint8)tag[1] &&
e[2] == (uint8)tag[2] && e[3] == (uint8)tag[3]) {
uint32 off = rd32(e + 8), sz = rd32(e + 12);
if (off >= len)
return 0;
if (out_len)
*out_len = sz;
return off;
}
}
return 0;
}
int
ttf_open(struct ttf *f, const void *data, uint32 len, int px)
{
const uint8 *d = data;
memset(f, 0, sizeof(*f));
f->data = d;
f->len = len;
f->px = px;
// 0x00010000 is TrueType; 'true' is Apple's older tag. 'OTTO' is CFF
// outlines, which this cannot read -- rejecting it explicitly beats failing
// later with an empty glyph and no explanation.
if (len < 12)
return -1;
uint32 ver = rd32(d);
if (ver == 0x4F54544FU) { // 'OTTO'
return -1;
}
if (ver != 0x00010000U && ver != 0x74727565U)
return -1;
uint32 head = find_table(d, len, "head", 0);
uint32 maxp = find_table(d, len, "maxp", 0);
uint32 hhea = find_table(d, len, "hhea", 0);
f->hmtx = find_table(d, len, "hmtx", 0);
f->glyf = find_table(d, len, "glyf", &f->glyf_len);
f->loca = find_table(d, len, "loca", &f->loca_len);
if (!head || !maxp || !hhea || !f->hmtx || !f->glyf || !f->loca)
return -1;
f->units_per_em = rd16(d + head + 18);
f->long_loca = rs16(d + head + 50);
f->num_glyphs = rd16(d + maxp + 4);
f->ascent = rs16(d + hhea + 4);
f->descent = rs16(d + hhea + 6);
f->num_hmetrics = rd16(d + hhea + 34);
if (f->units_per_em == 0)
return -1;
// cmap: prefer a Windows BMP subtable (platform 3, encoding 1), fall back to
// symbol (3,0). Format 4 only -- it covers the basic multilingual plane,
// which is every codepoint this system will draw for a long time.
uint32 cmap = find_table(d, len, "cmap", 0);
if (cmap) {
int n = rd16(d + cmap + 2);
for (int i = 0; i < n; i++) {
const uint8 *rec = d + cmap + 4 + 8 * i;
int plat = rd16(rec), enc = rd16(rec + 2);
uint32 sub = cmap + rd32(rec + 4);
if (sub + 4 > len)
continue;
if (rd16(d + sub) != 4)
continue;
if (plat == 3 && (enc == 1 || enc == 0)) {
f->cmap4 = sub;
if (enc == 1)
break; // exact match; keep looking otherwise
}
}
}
return f->cmap4 ? 0 : -1;
}
// Codepoint to glyph index, cmap format 4.
//
// The format is four parallel arrays of segments plus an idDelta/idRangeOffset
// pair whose second form is the awkward one: idRangeOffset is a byte offset
// *from its own address* into glyphIdArray, which is why the arithmetic below
// looks like it does rather than being an index.
static int
glyph_index(struct ttf *f, uint32 cp)
{
if (cp > 0xFFFF || !f->cmap4)
return 0;
const uint8 *t = f->data + f->cmap4;
int segx2 = rd16(t + 6);
const uint8 *ends = t + 14;
const uint8 *starts = ends + segx2 + 2;
const uint8 *deltas = starts + segx2;
const uint8 *ranges = deltas + segx2;
for (int i = 0; i < segx2; i += 2) {
if (rd16(ends + i) < cp)
continue;
if (rd16(starts + i) > cp)
return 0; // in a gap between segments
int ro = rd16(ranges + i);
if (ro == 0)
return (int)((cp + rd16(deltas + i)) & 0xFFFF);
const uint8 *p = ranges + i + ro + (cp - rd16(starts + i)) * 2;
if (p + 1 >= f->data + f->len)
return 0;
int g = rd16(p);
return g ? (int)((g + rd16(deltas + i)) & 0xFFFF) : 0;
}
return 0;
}
static int
advance_for(struct ttf *f, int gid)
{
// hmtx holds numberOfHMetrics full entries, then only left side bearings --
// monospaced tails are stored once. Clamping is the whole trick.
int i = gid < f->num_hmetrics ? gid : f->num_hmetrics - 1;
if (i < 0)
return 0;
return rd16(f->data + f->hmtx + 4 * i);
}
// --- outline extraction ----------------------------------------------------
#define MAXPTS 512
#define MAXCONT 32
struct outline {
int x[MAXPTS], y[MAXPTS]; // font units
uint8 on[MAXPTS]; // on-curve flag
int ends[MAXCONT];
int npts, ncont;
};
static int
load_outline(struct ttf *f, int gid, struct outline *o)
{
uint32 g0, g1;
if (gid < 0 || gid >= f->num_glyphs)
return -1;
if (f->long_loca) {
g0 = rd32(f->data + f->loca + 4 * gid);
g1 = rd32(f->data + f->loca + 4 * gid + 4);
} else {
g0 = (uint32)rd16(f->data + f->loca + 2 * gid) * 2;
g1 = (uint32)rd16(f->data + f->loca + 2 * gid + 2) * 2;
}
o->npts = o->ncont = 0;
if (g0 >= g1)
return 0; // empty glyph, e.g. space -- not an error
const uint8 *g = f->data + f->glyf + g0;
int nc = rs16(g);
// A negative contour count means a composite: the glyph is other glyphs
// placed at offsets. This is not an exotic case to skip -- `i` is built from
// a dotless i plus the dot, so skipping composites loses a letter from
// "Schunix", which is exactly how the gap was found.
//
// Only the translated case is handled: components with a scale or a 2x2
// matrix are rare outside of synthesised obliques, and applying a transform
// wrongly is worse than not applying one.
if (nc < 0) {
const uint8 *p = g + 10;
int flags;
do {
flags = rd16(p);
int cgid = rd16(p + 2);
p += 4;
int dx, dy;
if (flags & 0x0001) { // ARG_1_AND_2_ARE_WORDS
dx = rs16(p); dy = rs16(p + 2); p += 4;
} else {
dx = (signed char)p[0]; dy = (signed char)p[1]; p += 2;
}
if (!(flags & 0x0002)) // ARGS_ARE_XY_VALUES
dx = dy = 0; // point matching: not supported
if (flags & 0x0008) p += 2; // WE_HAVE_A_SCALE
else if (flags & 0x0040) p += 4; // X_AND_Y_SCALE
else if (flags & 0x0080) p += 8; // TWO_BY_TWO
// Recurse, then merge the component's contours into ours with the offset
// applied. Depth is bounded by the recursion into load_outline, which
// rejects anything that would overflow the point arrays.
struct outline sub;
if (load_outline(f, cgid, &sub) != 0)
continue;
if (o->npts + sub.npts > MAXPTS || o->ncont + sub.ncont > MAXCONT)
break;
for (int i = 0; i < sub.npts; i++) {
o->x[o->npts + i] = sub.x[i] + dx;
o->y[o->npts + i] = sub.y[i] + dy;
o->on[o->npts + i] = sub.on[i];
}
for (int i = 0; i < sub.ncont; i++)
o->ends[o->ncont + i] = sub.ends[i] + o->npts;
o->npts += sub.npts;
o->ncont += sub.ncont;
} while (flags & 0x0020); // MORE_COMPONENTS
return 0;
}
if (nc > MAXCONT)
return -1;
o->ncont = nc;
const uint8 *p = g + 10;
for (int i = 0; i < nc; i++) {
o->ends[i] = rd16(p);
p += 2;
}
int npts = nc ? o->ends[nc - 1] + 1 : 0;
if (npts > MAXPTS)
return -1;
o->npts = npts;
p += 2 + rd16(p); // skip the hinting instructions
// Flags are run-length encoded: bit 3 means "the next byte is a repeat
// count". Missing that turns every glyph with a repeated flag into garbage.
uint8 flags[MAXPTS];
for (int i = 0; i < npts; ) {
uint8 fl = *p++;
flags[i++] = fl;
if (fl & 8) {
int rep = *p++;
while (rep-- > 0 && i < npts)
flags[i++] = fl;
}
}
// Coordinates are deltas, and each axis is stored in one of three ways
// depending on two flag bits: 8-bit with a separate sign bit, or 16-bit
// signed, or "same as the previous point" which occupies no bytes at all.
int v = 0;
for (int i = 0; i < npts; i++) {
if (flags[i] & 2) {
int dv = *p++;
v += (flags[i] & 16) ? dv : -dv;
} else if (!(flags[i] & 16)) {
v += rs16(p);
p += 2;
}
o->x[i] = v;
o->on[i] = (flags[i] & 1) != 0;
}
v = 0;
for (int i = 0; i < npts; i++) {
if (flags[i] & 4) {
int dv = *p++;
v += (flags[i] & 32) ? dv : -dv;
} else if (!(flags[i] & 32)) {
v += rs16(p);
p += 2;
}
o->y[i] = v;
}
return 0;
}
// --- rasterising -----------------------------------------------------------
//
// Scanline fill with the non-zero winding rule, four sub-scanlines per output
// row. Coverage is accumulated in fixed point per pixel, so a horizontal span
// ending part-way through a pixel contributes a fraction rather than being
// rounded -- which is where anti-aliasing actually comes from. Vertical
// smoothing comes from the four samples.
#define SUBS 4
struct edge { int x0, y0, x1, y1; }; // in 1/64 pixel
#define MAXEDGE 1024
struct raster {
struct edge e[MAXEDGE];
int n;
};
static void
add_edge(struct raster *r, int x0, int y0, int x1, int y1)
{
if (y0 == y1 || r->n >= MAXEDGE)
return; // horizontal edges contribute no crossings
r->e[r->n].x0 = x0; r->e[r->n].y0 = y0;
r->e[r->n].x1 = x1; r->e[r->n].y1 = y1;
r->n++;
}
// Flatten a quadratic bézier into line segments.
//
// Fixed subdivision rather than adaptive. At text sizes the control polygon is
// a few pixels across, so eight segments is already below the point where the
// difference is visible, and an adaptive version would spend more time deciding
// than drawing.
static void
quad_to(struct raster *r, int x0, int y0, int cx, int cy, int x1, int y1)
{
const int N = 8;
int px = x0, py = y0;
for (int i = 1; i <= N; i++) {
int t = (i * 256) / N, u = 256 - t;
// (u*u*p0 + 2*u*t*c + t*t*p1) / 65536
int nx = (u * u * x0 + 2 * u * t * cx + t * t * x1) >> 16;
int ny = (u * u * y0 + 2 * u * t * cy + t * t * y1) >> 16;
add_edge(r, px, py, nx, ny);
px = nx; py = ny;
}
}
int
ttf_render(struct ttf *f, uint32 cp, struct glyph *out, unsigned char *cov,
int covcap, int *ox, int *oy)
{
struct outline o;
struct raster r;
int gid = glyph_index(f, cp);
if (gid == 0 && cp != 0)
return -1;
if (load_outline(f, gid, &o) != 0)
return -1;
// Font units to 1/64 pixel.
int upem = f->units_per_em;
#define SX(v) (((v) * f->px * 64) / upem)
int minx = 0, miny = 0, maxx = 0, maxy = 0;
if (o.npts) {
minx = maxx = SX(o.x[0]);
miny = maxy = SX(o.y[0]);
for (int i = 1; i < o.npts; i++) {
int X = SX(o.x[i]), Y = SX(o.y[i]);
if (X < minx) minx = X;
if (X > maxx) maxx = X;
if (Y < miny) miny = Y;
if (Y > maxy) maxy = Y;
}
}
int x0 = minx >> 6, y0 = miny >> 6;
int w = ((maxx + 63) >> 6) - x0 + 1;
int h = ((maxy + 63) >> 6) - y0 + 1;
if (w < 1) w = 1;
if (h < 1) h = 1;
if (w * h > covcap)
return -1;
memset(cov, 0, (uint32)(w * h));
// Build the edge list, contour by contour. Off-curve points imply an
// on-curve point midway between consecutive off-curve neighbours -- TrueType
// omits those, and a renderer that does not synthesise them draws corners
// where the font has curves.
r.n = 0;
int start = 0;
for (int c = 0; c < o.ncont; c++) {
int end = o.ends[c];
int n = end - start + 1;
if (n < 2) { start = end + 1; continue; }
int px = 0, py = 0, have = 0, fx = 0, fy = 0;
int cx = 0, cy = 0, havec = 0;
for (int k = 0; k <= n; k++) {
int i = start + (k % n);
int X = SX(o.x[i]) - (x0 << 6), Y = SX(o.y[i]) - (y0 << 6);
if (o.on[i]) {
if (!have) { fx = px = X; fy = py = Y; have = 1; }
else if (havec) { quad_to(&r, px, py, cx, cy, X, Y); px = X; py = Y; havec = 0; }
else { add_edge(&r, px, py, X, Y); px = X; py = Y; }
} else {
if (havec) {
int mx = (cx + X) / 2, my = (cy + Y) / 2; // implied on-curve point
if (!have) { fx = px = mx; fy = py = my; have = 1; }
else { quad_to(&r, px, py, cx, cy, mx, my); px = mx; py = my; }
}
cx = X; cy = Y; havec = 1;
}
}
if (have) {
if (havec) quad_to(&r, px, py, cx, cy, fx, fy);
else add_edge(&r, px, py, fx, fy);
}
start = end + 1;
}
// Fill. Y grows upward in font space and downward in the bitmap, so the
// sample line is mirrored as it is computed.
for (int row = 0; row < h; row++) {
for (int s = 0; s < SUBS; s++) {
int sy = ((h - 1 - row) << 6) + (s * 64) / SUBS + 32 / SUBS;
int xs[64], nx = 0, dir[64];
for (int i = 0; i < r.n && nx < 64; i++) {
struct edge *e = &r.e[i];
int ya = e->y0, yb = e->y1, d = 1;
if (ya > yb) { int t = ya; ya = yb; yb = t; d = -1; }
if (sy < ya || sy >= yb)
continue;
int t = ((sy - e->y0) * 1024) / (e->y1 - e->y0);
xs[nx] = e->x0 + ((e->x1 - e->x0) * t) / 1024;
dir[nx] = d;
nx++;
}
// Insertion sort: nx is small and this keeps the direction paired.
for (int i = 1; i < nx; i++) {
int vx = xs[i], vd = dir[i], j = i - 1;
while (j >= 0 && xs[j] > vx) { xs[j+1] = xs[j]; dir[j+1] = dir[j]; j--; }
xs[j+1] = vx; dir[j+1] = vd;
}
int wind = 0;
for (int i = 0; i + 1 < nx; i++) {
wind += dir[i];
if (wind == 0)
continue; // non-zero winding
int xa = xs[i], xb = xs[i + 1];
for (int col = xa >> 6; col <= (xb >> 6) && col < w; col++) {
if (col < 0)
continue;
// Fraction of this pixel the span covers, in 1/64ths.
int l = xa > (col << 6) ? xa : (col << 6);
int rr = xb < ((col + 1) << 6) ? xb : ((col + 1) << 6);
if (rr <= l)
continue;
int add = ((rr - l) * 255) / (64 * SUBS);
int v = cov[row * w + col] + add;
cov[row * w + col] = (unsigned char)(v > 255 ? 255 : v);
}
}
}
}
out->cov = cov;
out->w = w;
out->h = h;
out->bearing_x = x0;
out->bearing_y = (maxy + 63) >> 6;
out->advance = (advance_for(f, gid) * f->px + upem / 2) / upem;
if (ox) *ox = x0;
if (oy) *oy = y0;
#undef SX
return 0;
}
// --- font provider ---------------------------------------------------------
// --- glyph cache -----------------------------------------------------------
//
// Rasterising is not cheap -- it walks the outline, flattens every curve and
// fills four sub-scanlines per row -- and a terminal draws the same hundred
// glyphs over and over. Without a cache every character costs that again, and
// worse: the single scratch buffer meant a glyph was only valid until the next
// call, so a caller could not hold two at once.
//
// Direct-mapped over printable ASCII, which is the case that matters and needs
// no eviction policy, no hashing and no allocation. Anything outside that range
// still renders, just uncached, through the scratch buffer.
//
// The pool is filled linearly and never freed. A cache that can only fill up is
// the right shape here because the contents are pure functions of (codepoint,
// size) -- nothing ever becomes stale -- and the only event that invalidates it
// is a size change, which drops the whole thing.
#define CACHE_LO 32 // space
#define CACHE_HI 127 // exclusive
#define CACHE_N (CACHE_HI - CACHE_LO)
#define CACHE_POOL (48 * 1024)
struct gcache {
struct glyph g;
uint32 off; // into pool, valid only when `have`
int have;
};
static struct gcache cache[CACHE_N];
static unsigned char cache_pool[CACHE_POOL];
static uint32 cache_used;
static int cache_px; // size the cache was built at
// Scratch for anything the cache cannot hold. Same contract as before -- valid
// until the next call -- and now only used off the common path.
static unsigned char ttf_cov[128 * 128];
static int
ttf_glyph_op(struct font *fo, uint32 cp, struct glyph *out)
{
struct ttf *f = fo->priv;
// A size change makes every cached bitmap wrong. Dropping the lot is correct
// and costs nothing: it happens when a font is resized, not per glyph.
if (cache_px != f->px) {
for (int i = 0; i < CACHE_N; i++)
cache[i].have = 0;
cache_used = 0;
cache_px = f->px;
}
if (cp < CACHE_LO || cp >= CACHE_HI)
return ttf_render(f, cp, out, ttf_cov, (int)sizeof(ttf_cov), 0, 0);
struct gcache *c = &cache[cp - CACHE_LO];
if (c->have) {
*out = c->g;
out->cov = cache_pool + c->off; // rebind: the pool may have moved in
return 0; // a future revision, the offset has not
}
struct glyph g;
if (ttf_render(f, cp, &g, ttf_cov, (int)sizeof(ttf_cov), 0, 0) != 0)
return -1;
uint32 need = (uint32)(g.w * g.h);
if (cache_used + need <= CACHE_POOL) {
memmove(cache_pool + cache_used, g.cov, need);
c->off = cache_used;
c->g = g;
c->g.cov = cache_pool + cache_used; // not the scratch buffer
c->have = 1;
cache_used += need;
*out = c->g;
return 0;
}
// Pool exhausted. Still correct, just uncached -- and the glyph is in the
// scratch buffer, which is exactly the old contract.
*out = g;
return 0;
}
static int ttf_height_op(struct font *fo)
{
struct ttf *f = fo->priv;
return ((f->ascent - f->descent) * f->px + f->units_per_em / 2) / f->units_per_em;
}
static int ttf_baseline_op(struct font *fo)
{
struct ttf *f = fo->priv;
return (f->ascent * f->px + f->units_per_em / 2) / f->units_per_em;
}
static const struct font_ops ttf_ops = {
.glyph = ttf_glyph_op,
.height = ttf_height_op,
.baseline = ttf_baseline_op,
};
static struct font ttf_font_obj;
struct font *
ttf_font(struct ttf *f)
{
ttf_font_obj.ops = &ttf_ops;
ttf_font_obj.priv = f;
return &ttf_font_obj;
}

95
kernel/dev/uart.c Normal file
View file

@ -0,0 +1,95 @@
// Intel 8250 serial port (UART).
#include "types.h"
#include "defs.h"
#include "irq.h"
#include "param.h"
#include "traps.h"
#include "spinlock.h"
#include "sleeplock.h"
#include "fs.h"
#include "file.h"
#include "mmu.h"
#include "proc.h"
#include "x86.h"
#define COM1 0x3f8
static int uart; // is there a uart?
void
uartearlyinit(void)
{
char *p;
// Turn off the FIFO
outb(COM1+2, 0);
// 9600 baud, 8 data bits, 1 stop bit, parity off.
outb(COM1+3, 0x80); // Unlock divisor
outb(COM1+0, 115200/9600);
outb(COM1+1, 0);
outb(COM1+3, 0x03); // Lock divisor, 8 data bits.
outb(COM1+4, 0);
outb(COM1+1, 0x01); // Enable receive interrupts.
// If status is 0xFF, no serial port.
if(inb(COM1+5) == 0xFF)
return;
uart = 1;
// Announce that we're here.
for(p="Schunix UART...\n"; *p; p++)
uartputc(*p);
}
// irq.c hands every handler an opaque argument, because a driver with two
// controllers needs to know which one interrupted. There is only one COM1.
static void
uart_irq(void *arg)
{
(void)arg;
uartintr();
}
void
uartinit(void)
{
if(!uart)
return;
// Acknowledge pre-existing interrupt conditions;
// enable interrupts.
inb(COM1+2);
inb(COM1+0);
irq_register(IRQ_COM1, uart_irq, 0, "serial", 0);
}
void
uartputc(int c)
{
int i;
if(!uart)
return;
for(i = 0; i < 128 && !(inb(COM1+5) & 0x20); i++)
microdelay(10);
outb(COM1+0, c);
}
static int
uartgetc(void)
{
if(!uart)
return -1;
if(!(inb(COM1+5) & 0x01))
return -1;
return inb(COM1+0);
}
void
uartintr(void)
{
consoleintr(uartgetc);
}

555
kernel/dev/vga.c Normal file
View file

@ -0,0 +1,555 @@
#include "types.h"
#include "x86.h"
#include "memlayout.h"
#include "defs.h"
#include "theme.h"
#include "font.h"
#include "logo.h"
#include "device.h"
#include "display.h"
#include "dma.h"
#define BACKSPACE 0x100
#define CHAR_HEIGHT 16
#define CHAR_WIDTH 8
#define COLOR_BG SCH_BG
volatile uchar *vga_mem = 0;
// Physical base of the linear framebuffer, as reported by VBE. Every other
// part of the kernel that needs to map the framebuffer -- fork's VMA copy,
// mmap of /dev/fb0 -- asks for this rather than assuming a value. Three
// places used to hardcode 0xfd000000, which is only where QEMU's stdvga
// happens to put it.
static uint64 vga_phys = 0;
static uint32 vga_stride = 0;
static uint32 vga_bpp = 0;
static uint32 vga_width = 800;
static uint32 vga_height = 600;
static int cursor_x = 0;
static int cursor_y = 0;
// Current fill color for glyph backgrounds, backspace erase, and scroll —
// normally COLOR_BG, but panic() switches this to paint the whole console
// a solid color (e.g. PSOGM's purple) instead of leaving cream-colored boxes
// around each character.
static uint32 vga_bg = COLOR_BG;
void
vga_set_bg(uint32 color)
{
vga_bg = color;
}
// Written by boot/stage2.S's VBE call and read by boot/stage2main.c under
// the same name (VBE_INFO_ADDR) — not 0x9000, since stage2's own code now
// runs up to nearly 0x9000 once the boot-menu/progress-bar logic is
// included, which would otherwise clobber a mode info block placed there.
#define VBE_INFO_ADDR 0x30000
void
vga_init(uint64 phys_addr) {
if (phys_addr == 0) {
phys_addr = *(uint32*)(P2V(VBE_INFO_ADDR) + 40);
}
vga_mem = (volatile uchar*)P2V(phys_addr);
vga_phys = phys_addr;
vga_stride = *(ushort*)(P2V(VBE_INFO_ADDR) + 16);
vga_width = *(ushort*)(P2V(VBE_INFO_ADDR) + 18);
vga_height = *(ushort*)(P2V(VBE_INFO_ADDR) + 20);
vga_bpp = *(uchar*)(P2V(VBE_INFO_ADDR) + 25);
if (vga_width == 0) vga_width = 800;
if (vga_height == 0) vga_height = 600;
if (vga_stride == 0) vga_stride = vga_width * (vga_bpp / 8);
cprintf("VGA: %dx%d, %d bpp, stride %d, LFB: 0x%x\n", vga_width, vga_height, vga_bpp, vga_stride, (uint32)phys_addr);
// Deliberately does NOT clear the screen.
//
// The bootloader has already drawn the splash, and wiping it here is what
// produced the old two-act boot: a purple splash for an instant, then an
// abrupt cream screen filling with kernel log lines. The splash now stays up
// for the whole of boot, and console_takeover() clears it exactly once --
// when userspace starts and the display changes owner.
}
void
vga_draw_pixel(int x, int y, uint32 color)
{
if (!vga_mem || x < 0 || x >= vga_width || y < 0 || y >= vga_height)
return;
uint32 offset = (y * vga_stride) + (x * (vga_bpp / 8));
if (vga_bpp == 32) {
*(volatile uint32*)(vga_mem + offset) = color;
} else if (vga_bpp == 24) {
vga_mem[offset] = color & 0xFF;
vga_mem[offset + 1] = (color >> 8) & 0xFF;
vga_mem[offset + 2] = (color >> 16) & 0xFF;
} else if (vga_bpp == 16) {
uint16 c16 = (((color >> 19) & 0x1F) << 11) | (((color >> 10) & 0x3F) << 5) | ((color >> 3) & 0x1F);
*(volatile uint16*)(vga_mem + offset) = c16;
}
}
void
vga_clear(uint32 color)
{
for (int y = 0; y < vga_height; y++) {
for (int x = 0; x < vga_width; x++) {
vga_draw_pixel(x, y, color);
}
}
}
void
vga_draw_char(int x, int y, uchar c, uint32 color)
{
// No `c > 127` rejection any more: font_data holds 256 Latin-1 glyphs, and
// a uchar cannot index past them.
if (!vga_mem) return;
for (int row = 0; row < CHAR_HEIGHT; row++) {
uchar row_data = font_data[c][row];
for (int col = 0; col < CHAR_WIDTH; col++) {
if (row_data & (0x80 >> col)) {
vga_draw_pixel(x + col, y + row, color);
} else {
vga_draw_pixel(x + col, y + row, vga_bg);
}
}
}
}
void
vga_puts(int x, int y, char *s, uint32 color)
{
while (*s) {
vga_putc((uchar)*s, color);
s++;
}
}
void
vga_putc(int c, uint32 color)
{
if (!vga_mem) return;
if (c == BACKSPACE || c == 0x08 || c == '\b' || c == 127) {
if (cursor_x >= CHAR_WIDTH) {
cursor_x -= CHAR_WIDTH;
for (int i = 0; i < CHAR_HEIGHT; i++) {
for (int j = 0; j < CHAR_WIDTH; j++) {
vga_draw_pixel(cursor_x + j, cursor_y + i, vga_bg);
}
}
}
return;
}
if (c == '\n') {
cursor_x = 0;
cursor_y += CHAR_HEIGHT;
} else if (c == '\r') {
cursor_x = 0;
} else if (c == '\t') {
// Advance to the next multiple of eight columns, erasing as it goes so a
// tab overwrites rather than skipping over stale characters.
//
// There was no tab case at all, so '\t' fell through to vga_draw_char and
// was drawn as glyph 9 -- which in CP437 is a hollow circle. Pressing Tab
// printed a circle. Same for every other control character: the font has
// a picture for each of them, and none of those pictures is what the
// character means.
int col = cursor_x / CHAR_WIDTH;
int stop = (col + 8) & ~7;
while (col < stop && cursor_x < (int)vga_width) {
for (int i = 0; i < CHAR_HEIGHT; i++)
for (int j = 0; j < CHAR_WIDTH; j++)
vga_draw_pixel(cursor_x + j, cursor_y + i, vga_bg);
cursor_x += CHAR_WIDTH;
col++;
}
} else if (c < 32) {
return; // any other control character: not a glyph
} else {
vga_draw_char(cursor_x, cursor_y, (uchar)c, color);
cursor_x += CHAR_WIDTH;
}
if (cursor_x >= vga_width) {
cursor_x = 0;
cursor_y += CHAR_HEIGHT;
}
if (cursor_y > (vga_height - CHAR_HEIGHT)) {
uint32 line_size = vga_stride * CHAR_HEIGHT;
uint32 total_size = vga_stride * vga_height;
uint32 copy_size = total_size - line_size;
memmove((void*)vga_mem, (void*)(vga_mem + line_size), copy_size);
for (int y = vga_height - CHAR_HEIGHT; y < vga_height; y++) {
for (int x = 0; x < vga_width; x++) {
vga_draw_pixel(x, y, vga_bg);
}
}
cursor_y = vga_height - CHAR_HEIGHT;
}
}
void
vga_screen_reset(void)
{
vga_bg = COLOR_BG;
vga_clear(COLOR_BG);
cursor_x = 0;
cursor_y = 0;
}
/* ------------------------------------------------------------------ */
/* Drawing primitives for the crash screen */
/* */
/* The crash screen lays itself out rather than printing, which needs */
/* two things the console never did: filled shapes, and text at an */
/* absolute position that leaves the cursor alone. */
/* ------------------------------------------------------------------ */
int
vga_ready(void)
{
return vga_mem != 0;
}
void
vga_dims(int *w, int *h)
{
if (w) *w = (int)vga_width;
if (h) *h = (int)vga_height;
}
int vga_char_w(void) { return CHAR_WIDTH; }
int vga_char_h(void) { return CHAR_HEIGHT; }
/* ------------------------------------------------------------------ */
/* Framebuffer geometry */
/* */
/* vga.c is the only place that reads the VBE mode info, so it is the */
/* only place that knows where the framebuffer actually is. Everything */
/* else asks. See kernel/dev/fb.c for the device these back. */
/* ------------------------------------------------------------------ */
uint64 vga_fb_phys(void) { return vga_phys; }
// Bytes of framebuffer, measured in scanlines rather than as
// width * height * bpp/8 -- the two differ whenever the card pads its rows,
// and it is the stride that says how far apart two rows really are.
uint64 vga_fb_size(void)
{
if (!vga_mem)
return 0;
return (uint64)vga_stride * (uint64)vga_height;
}
uint32 vga_fb_stride(void) { return vga_stride; }
uint32 vga_fb_bpp(void) { return vga_bpp; }
volatile uchar *vga_fb_ptr(void) { return vga_mem; }
/* ------------------------------------------------------------------ */
/* Cursor addressing and erasing */
/* */
/* What an ANSI escape parser needs and vga_putc alone cannot give it: */
/* the ability to put the cursor somewhere specific and to erase a */
/* region without writing spaces over it. See kernel/core/console.c. */
/* ------------------------------------------------------------------ */
int vga_cols(void) { return vga_width ? (int)(vga_width / CHAR_WIDTH) : 0; }
int vga_rows(void) { return vga_height ? (int)(vga_height / CHAR_HEIGHT) : 0; }
void
vga_cursor_get(int *col, int *row)
{
if (col) *col = cursor_x / CHAR_WIDTH;
if (row) *row = cursor_y / CHAR_HEIGHT;
}
// Clamped rather than validated: an escape sequence is untrusted input that
// arrives from any program that can write to the terminal, and "CUP to row
// 9999" should put the cursor on the last row, not off the end of the
// framebuffer.
void
vga_cursor_set(int col, int row)
{
int maxc = vga_cols(), maxr = vga_rows();
if (maxc <= 0 || maxr <= 0)
return;
if (col < 0) col = 0;
if (row < 0) row = 0;
if (col >= maxc) col = maxc - 1;
if (row >= maxr) row = maxr - 1;
cursor_x = col * CHAR_WIDTH;
cursor_y = row * CHAR_HEIGHT;
}
// Fill a run of character cells with the background colour.
static void
erase_cells(int col, int row, int count)
{
int maxc = vga_cols();
while (count-- > 0) {
int x = col * CHAR_WIDTH, y = row * CHAR_HEIGHT;
for (int i = 0; i < CHAR_HEIGHT; i++)
for (int j = 0; j < CHAR_WIDTH; j++)
vga_draw_pixel(x + j, y + i, vga_bg);
if (++col >= maxc) { col = 0; row++; }
}
}
// ED -- erase in display. 0: cursor to end, 1: start to cursor, 2: all.
void
vga_erase_display(int mode)
{
int col, row, maxc = vga_cols(), maxr = vga_rows();
vga_cursor_get(&col, &row);
switch (mode) {
case 0: erase_cells(col, row, (maxr - row) * maxc - col); break;
case 1: erase_cells(0, 0, row * maxc + col + 1); break;
default: vga_clear(vga_bg); break;
}
}
// EL -- erase in line. Same three modes, bounded to the current row.
void
vga_erase_line(int mode)
{
int col, row, maxc = vga_cols();
vga_cursor_get(&col, &row);
switch (mode) {
case 0: erase_cells(col, row, maxc - col); break;
case 1: erase_cells(0, row, col + 1); break;
default: erase_cells(0, row, maxc); break;
}
}
// Row pointer computed once per row rather than per pixel. The crash screen
// paints a solid card behind the QR symbol, and at 8 pixels per module that is
// tens of thousands of writes going through vga_draw_pixel's bounds check and
// multiply.
void
vga_fill_rect(int x0, int y0, int w, int h, uint32 color)
{
if (!vga_mem)
return;
if (x0 < 0) { w += x0; x0 = 0; }
if (y0 < 0) { h += y0; y0 = 0; }
if (x0 + w > (int)vga_width) w = (int)vga_width - x0;
if (y0 + h > (int)vga_height) h = (int)vga_height - y0;
if (w <= 0 || h <= 0)
return;
// Only the 32bpp case gets the fast path; anything else is rare enough that
// correctness through vga_draw_pixel is the better trade.
if (vga_bpp != 32) {
for (int y = y0; y < y0 + h; y++)
for (int x = x0; x < x0 + w; x++)
vga_draw_pixel(x, y, color);
return;
}
for (int y = y0; y < y0 + h; y++) {
volatile uint32 *row = (volatile uint32 *)(vga_mem + y * vga_stride) + x0;
for (int x = 0; x < w; x++)
row[x] = color;
}
}
void
vga_fill_circle(int cx, int cy, int r, uint32 color)
{
for (int dy = -r; dy <= r; dy++)
for (int dx = -r; dx <= r; dx++)
if (dx * dx + dy * dy <= r * r)
vga_draw_pixel(cx + dx, cy + dy, color);
}
// Text at an absolute position, leaving the console cursor alone. The crash
// screen lays itself out, so vga_putc's cursor tracking and scrolling are
// exactly what it does not want.
void
vga_text_at(int x, int y, const char *s, uint32 color)
{
for (int i = 0; s[i]; i++)
vga_draw_char(x + i * CHAR_WIDTH, y, (uchar)s[i], color);
}
// --- Logo ------------------------------------------------------------------
//
// The boot splash's logo is drawn by the *loader*, not here -- see splash() in
// boot/stage2main.c. It has to be, because the kernel does not reach vga_init()
// until roughly the last 300ms of a 2.8s boot: drawing it here put the logo on
// screen for one frame before console_takeover() wiped it, which is worse than
// not having one.
//
// What lives here is the same drawing code against the same mask, for the
// things that want the mark *after* boot -- the crash screen, and eventually
// whatever schinit shows. Keeping one copy of the compositing logic is why this
// is not simply deleted.
// Blend `fg` over `bg` by coverage a (0..255). Per-channel, integer only: this
// runs before any floating point is safe to touch.
static uint32
blend(uint32 bg, uint32 fg, uint a)
{
uint32 r = (((fg >> 16) & 0xFF) * a + ((bg >> 16) & 0xFF) * (255 - a)) / 255;
uint32 g = (((fg >> 8) & 0xFF) * a + ((bg >> 8) & 0xFF) * (255 - a)) / 255;
uint32 b = (((fg ) & 0xFF) * a + ((bg ) & 0xFF) * (255 - a)) / 255;
return (r << 16) | (g << 8) | b;
}
void
vga_draw_logo(int x, int y, uint32 color, uint32 bg)
{
if (!vga_mem)
return;
for (int dy = 0; dy < LOGO_H; dy++) {
for (int dx = 0; dx < LOGO_W; dx++) {
uint a = LOGO_INK(dy * LOGO_W + dx);
// Skip the background rather than painting it. The mask is a square but
// the mark is a circle, so most of it is empty -- and drawing that empty
// area in bg would stamp a visible box over whatever is underneath.
if (a == 0)
continue;
vga_draw_pixel(x + dx, y + dy, a == 255 ? color : blend(bg, color, a));
}
}
}
// Centred, for callers that own the whole screen.
void
vga_logo_centered(int cy, uint32 color, uint32 bg)
{
if (!vga_mem || vga_width < LOGO_W)
return;
vga_draw_logo(((int)vga_width - LOGO_W) / 2, cy - LOGO_H / 2, color, bg);
}
// --- display class implementation ------------------------------------------
//
// See include/display.h. This is the VBE linear framebuffer presenting itself
// as a generic display, so a compositor can be written against the class and
// keep working when virtio-gpu arrives.
static uchar *back; // shadow buffer in RAM, or 0 if never acquired
static uint64 back_bytes;
static void
vga_disp_mode(struct device *d, struct display_mode *m)
{
m->width = (int)vga_width;
m->height = (int)vga_height;
m->bpp = (int)vga_bpp;
m->stride = vga_stride;
}
static void *
vga_disp_acquire(struct device *d, uint32 *stride_out)
{
if (stride_out)
*stride_out = vga_stride;
if (back)
return back;
if (!vga_mem || vga_height == 0 || vga_stride == 0)
return 0;
back_bytes = (uint64)vga_stride * vga_height;
// From the DMA arena, which is the wrong pool and is used here because it is
// the only *physically contiguous* allocator in the kernel -- kalloc hands
// out single pages whose neighbours are adjacent only by chance, and a
// framebuffer wants one flat span.
//
// At 800x600x32 this is 1.9 MB of a 4 MB arena meant for device rings, which
// is tolerable only because nothing else large lives there yet. The real fix
// is a contiguous page allocator, or reserving this at boot from the memory
// map -- which is work that belongs with the E820 change in docs/roadmap.md
// §3b, since that is what stops the kernel pretending 224 MB exists.
//
// Allocated on first use rather than at boot on purpose: a machine that never
// starts a compositor should not pay two megabytes for one.
back = dma_alloc(back_bytes, 4096);
if (!back) {
cprintf("display: no room for a %d KB back buffer; drawing will tear\n",
(int)(back_bytes / 1024));
return 0;
}
memset(back, 0, back_bytes);
cprintf("display: %d KB back buffer, double buffered\n",
(int)(back_bytes / 1024));
return back;
}
// Blit a rectangle from the shadow buffer to the framebuffer.
//
// Row at a time rather than one memmove for the whole rectangle: only a
// full-width rectangle is contiguous in either buffer, and a compositor
// updating one window is the case worth being right about.
static void
vga_disp_present(struct device *d, int x, int y, int w, int h)
{
if (!back || !vga_mem)
return;
if (x < 0) { w += x; x = 0; }
if (y < 0) { h += y; y = 0; }
if (x + w > (int)vga_width) w = (int)vga_width - x;
if (y + h > (int)vga_height) h = (int)vga_height - y;
if (w <= 0 || h <= 0)
return;
int bytes = w * (int)(vga_bpp / 8);
uint32 off = (uint32)x * (vga_bpp / 8);
for (int row = y; row < y + h; row++)
memmove((void *)(vga_mem + row * vga_stride + off),
back + row * vga_stride + off, bytes);
}
// Wait for vertical retrace, by polling the VGA input status register.
//
// Bit 3 of 0x3DA is set during vertical retrace. This is the original 1987 VGA
// interface and it still works on everything that emulates a VGA, QEMU's
// stdvga included -- VBE's own protected-mode interface for this is optional
// and frequently absent, so the old port is the portable answer.
//
// Two loops, not one: waiting only for the flag to *be* set would return
// immediately if a retrace is already in progress, having just missed the
// start of it. Waiting for the current one to end and the next to begin is
// what actually lands the blit between frames.
//
// The counters are a deadlock guard. On hardware that never reports retrace
// this returns after a bounded spin instead of hanging the caller forever,
// which matters because this can be called with the console locked.
static void
vga_disp_wait_vblank(struct device *d)
{
int guard;
for (guard = 0; guard < 1000000; guard++)
if (!(inb(0x3DA) & 0x08))
break;
for (guard = 0; guard < 1000000; guard++)
if (inb(0x3DA) & 0x08)
break;
}
const struct display_ops vga_display_ops = {
.mode = vga_disp_mode,
.acquire = vga_disp_acquire,
.present = vga_disp_present,
.wait_vblank = vga_disp_wait_vblank,
};

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,95 @@
// Display controller -- the first driver to find its hardware by asking.
//
// It does not draw anything: kernel/dev/vga.c already owns the framebuffer,
// and it learned where that is from the bootloader's VBE call. What this adds
// is a second, independent answer to the same question, from the bus rather
// than from the BIOS -- and a complaint when the two disagree.
//
// That is worth more than it sounds. Three places in the kernel used to
// hardcode 0xfd000000 as the framebuffer address, and the only reason nobody
// noticed was that QEMU's stdvga happens to put it there. A disagreement
// between VBE and the card's own BAR is exactly the symptom that would have
// shown up first on hardware where the guess was wrong, and until now there
// was nothing in the kernel capable of noticing it.
#include "types.h"
#include "defs.h"
#include "param.h"
#include "x86.h"
#include "spinlock.h"
#include "sleeplock.h"
#include "fs.h"
#include "file.h"
#include "pci.h"
#include "display.h"
// The framebuffer lives in the first memory BAR big enough to hold a screen.
// Display controllers commonly expose two apertures -- the framebuffer and a
// much smaller register block -- and which index they land on varies, so the
// choice is made by size rather than by position.
static struct pci_bar *
framebuffer_bar(struct pci_dev *d)
{
uint64 need = vga_fb_size();
for (int i = 0; i < 6; i++) {
struct pci_bar *b = &d->bar[i];
if (b->base == 0 || b->is_io)
continue;
if (b->size >= need)
return b;
}
return 0;
}
static int
vga_pci_probe(struct pci_dev *d)
{
struct pci_bar *bar = framebuffer_bar(d);
uint64 vbe = vga_fb_phys();
// Memory decoding has to be on for the aperture to respond at all. The BIOS
// will have done this already to put up a boot splash, but a device we only
// reached through enumeration cannot be assumed to have been touched.
pci_enable(d, PCI_CMD_MEMORY);
if (bar == 0) {
cprintf("vga: display controller at %x:%x.%x has no framebuffer aperture\n",
d->bus, d->slot, d->func);
return 0; // still ours; nothing more to do
}
if (vbe != 0 && bar->base != vbe) {
// Trust VBE: it describes the mode that is actually programmed and being
// displayed, which is the thing vga.c is writing into. The BAR is the
// card's whole aperture and the mode may sit at an offset within it. Say
// so rather than silently preferring one.
cprintf("vga: framebuffer disagreement -- VBE says %x, BAR says %x\n",
(uint32)vbe, (uint32)bar->base);
}
cprintf("vga: display controller %x:%x, aperture %x (%d MB)\n",
d->vendor, d->device, (uint32)bar->base,
(uint32)(bar->size / (1024 * 1024)));
return 0;
}
// Matched by class, not by model: every VGA-compatible controller answers
// class 0x03, and this driver cares about none of the differences between
// them.
static const struct pci_match vga_pci_match[] = {
{ PCI_ANY, PCI_ANY, PCI_CLASS_DISPLAY, 0xFF },
{ 0, 0, 0, 0 },
};
// The display vtable lives in kernel/dev/vga.c, which owns the framebuffer;
// this file only finds the card on the bus. Publishing it here is what makes
// device_find_class(DEVCLASS_DISPLAY) return something a compositor can use.
extern const struct display_ops vga_display_ops;
const struct pci_driver vga_pci_driver = {
.name = "vga",
.match = vga_pci_match,
.ops = &vga_display_ops,
.probe = vga_pci_probe,
};

View file

@ -0,0 +1,435 @@
// virtio-gpu, 2D. Written from the OASIS virtio 1.1 specification.
//
// This is the second implementation of the display class (include/display.h),
// and the first one that is a real GPU driver rather than a flat framebuffer.
// The difference that matters for a compositor: presenting is an explicit
// operation. With VBE you write pixels and hope the scanout is not looking;
// here you transfer a rectangle to the host and then flush it, and the host
// does not show a half-drawn frame.
//
// Bring-up is the standard virtio 1.0 sequence and the order is not negotiable:
// reset, ACKNOWLEDGE, DRIVER, read features, write features, FEATURES_OK, check
// FEATURES_OK stuck, set up queues, DRIVER_OK. A device that finds the sequence
// wrong is entitled to simply refuse, and it will not say why.
#include "types.h"
#include "defs.h"
#include "param.h"
#include "x86.h"
#include "spinlock.h"
#include "sleeplock.h"
#include "fs.h"
#include "file.h"
#include "mmu.h"
#include "memlayout.h"
#include "pci.h"
#include "device.h"
#include "display.h"
#include "dma.h"
#include "virtio.h"
// One control queue is enough for 2D: there is also a cursor queue, which is
// only worth having once there is a hardware cursor to move.
#define CTRL_QUEUE 0
struct vgpu {
volatile uint8 *common; // common configuration structure
volatile uint8 *notify; // notification area
uint32 notify_mult; // queue_notify_off multiplier
// The queue. Allocated as one contiguous block from the DMA arena, because
// the device reads it by physical address and kalloc's pages are adjacent
// only by chance.
struct vring_desc *desc;
struct vring_avail *avail;
struct vring_used *used;
uint16 qsize;
uint16 notify_off;
uint16 last_used;
uint16 next_desc;
// A command buffer and a response buffer, both in DMA memory, reused for
// every request. Only one command is ever in flight -- everything here is
// called from process context during setup or from present(), and a GPU
// command queue with one entry is not the bottleneck in a 2D compositor.
void *cmd;
void *resp;
int width, height;
void *fb; // the framebuffer we scan out from
uint32 fb_bytes;
int ready;
};
static struct vgpu gpu;
// --- MMIO accessors --------------------------------------------------------
//
// Explicit widths. The common configuration structure has 8-, 16- and 32-bit
// fields side by side, and a device is entitled to ignore an access made at the
// wrong width -- silently, with no error anywhere.
static inline uint8 r8 (volatile uint8 *p, int o) { return *(volatile uint8 *)(p + o); }
static inline uint16 r16(volatile uint8 *p, int o) { return *(volatile uint16 *)(p + o); }
static inline uint32 r32(volatile uint8 *p, int o) { return *(volatile uint32 *)(p + o); }
static inline void w8 (volatile uint8 *p, int o, uint8 v) { *(volatile uint8 *)(p + o) = v; }
static inline void w16(volatile uint8 *p, int o, uint16 v) { *(volatile uint16 *)(p + o) = v; }
static inline void w32(volatile uint8 *p, int o, uint32 v) { *(volatile uint32 *)(p + o) = v; }
static void
w64(volatile uint8 *p, int o, uint64 v)
{
// Two 32-bit halves, low first. The specification defines these fields as a
// pair of 32-bit registers precisely so that a 32-bit driver can write them,
// and some implementations latch on the high write.
w32(p, o, (uint32)v);
w32(p, o + 4, (uint32)(v >> 32));
}
// --- capability walk -------------------------------------------------------
// Find a virtio configuration structure and map it.
//
// Each lives at an offset inside one of the device's BARs, and which BAR is not
// fixed -- the device says so in a vendor-specific PCI capability. Assuming BAR
// 0 works on QEMU today and is exactly the kind of assumption that stops
// working on the next device.
static volatile uint8 *
find_cfg(struct pci_dev *d, int want, uint32 *out_mult)
{
uint8 off = pci_read8(d->bus, d->slot, d->func, 0x34) & 0xFC;
while (off) {
uint8 id = pci_read8(d->bus, d->slot, d->func, off);
uint8 next = pci_read8(d->bus, d->slot, d->func, off + 1);
if (id == PCI_CAP_ID_VNDR) {
uint8 type = pci_read8(d->bus, d->slot, d->func, off + 3);
uint8 bar = pci_read8(d->bus, d->slot, d->func, off + 4);
uint32 o = pci_read32(d->bus, d->slot, d->func, off + 8);
if (type == want && bar < 6 && d->bar[bar].base) {
if (out_mult) // only the notify capability has this trailing field
*out_mult = pci_read32(d->bus, d->slot, d->func, off + 16);
return (volatile uint8 *)P2V(d->bar[bar].base + o);
}
}
off = next;
}
return 0;
}
// --- virtqueue -------------------------------------------------------------
static int
queue_setup(struct vgpu *g)
{
w16(g->common, VCOMMON_Q_SELECT, CTRL_QUEUE);
uint16 qsize = r16(g->common, VCOMMON_Q_SIZE);
if (qsize == 0)
return -1;
if (qsize > 64)
qsize = 64; // more than we will ever have outstanding
g->qsize = qsize;
g->notify_off = r16(g->common, VCOMMON_Q_NOTIFY_OFF);
// The three rings, each aligned as the specification requires: 16 for the
// descriptor table, 2 for available, 4 for used. Allocated as one block and
// carved up, so there is one physical address to reason about.
uint32 desc_sz = qsize * sizeof(struct vring_desc);
uint32 avail_sz = 6 + 2 * qsize;
uint32 used_sz = 6 + 8 * qsize;
uint8 *mem = dma_alloc(4096 * 3, 4096);
if (!mem)
return -1;
memset(mem, 0, 4096 * 3);
g->desc = (struct vring_desc *) mem;
g->avail = (struct vring_avail *)(mem + ((desc_sz + 1) & ~1U));
g->used = (struct vring_used *)(mem + 4096); // its own page, 4-aligned
(void)avail_sz; (void)used_sz;
w64(g->common, VCOMMON_Q_DESC, dma_phys(g->desc));
w64(g->common, VCOMMON_Q_DRIVER, dma_phys(g->avail));
w64(g->common, VCOMMON_Q_DEVICE, dma_phys(g->used));
w16(g->common, VCOMMON_Q_ENABLE, 1);
return 0;
}
// Submit one command and wait for the reply.
//
// Two descriptors: the request, which the device reads, and the response, which
// it writes. Polled rather than interrupt-driven -- every caller is either boot
// setup or present(), both of which want the answer before continuing, and an
// interrupt would only add a sleep and a wakeup to a wait of a few microseconds.
static int
cmd(struct vgpu *g, uint32 cmdlen, uint32 resplen)
{
uint16 head = g->next_desc;
uint16 second = (uint16)((head + 1) % g->qsize);
g->desc[head].addr = dma_phys(g->cmd);
g->desc[head].len = cmdlen;
g->desc[head].flags = VRING_DESC_F_NEXT;
g->desc[head].next = second;
g->desc[second].addr = dma_phys(g->resp);
g->desc[second].len = resplen;
g->desc[second].flags = VRING_DESC_F_WRITE;
g->desc[second].next = 0;
g->next_desc = (uint16)((second + 1) % g->qsize);
g->avail->ring[g->avail->idx % g->qsize] = head;
__sync_synchronize(); // ring entry visible before idx
g->avail->idx++;
__sync_synchronize();
// Notify. The address is the notify region plus this queue's offset scaled by
// the multiplier the device advertised; the value written is the queue index.
*(volatile uint16 *)(g->notify + g->notify_off * g->notify_mult) = CTRL_QUEUE;
// Bounded, because a device that never answers must not hang the boot. At
// QEMU's speed the reply is there almost immediately; on a wedged device this
// returns an error that a caller can report.
for (int i = 0; i < 10000000; i++) {
if (g->used->idx != g->last_used) {
g->last_used = g->used->idx;
__sync_synchronize();
struct virtio_gpu_ctrl_hdr *h = g->resp;
return (h->type == VIRTIO_GPU_RESP_OK_NODATA ||
h->type == VIRTIO_GPU_RESP_OK_DISPLAY_INFO) ? 0 : -1;
}
}
cprintf("virtio-gpu: device did not answer\n");
return -1;
}
static void
hdr_init(void *p, uint32 type)
{
struct virtio_gpu_ctrl_hdr *h = p;
memset(h, 0, sizeof(*h));
h->type = type;
}
// --- the 2D sequence -------------------------------------------------------
static int
setup_scanout(struct vgpu *g)
{
// What resolution is the display actually at? Asking is the point of
// GET_DISPLAY_INFO; guessing 1024x768 is how a driver ends up working on
// exactly one host configuration.
hdr_init(g->cmd, VIRTIO_GPU_CMD_GET_DISPLAY_INFO);
if (cmd(g, sizeof(struct virtio_gpu_ctrl_hdr),
sizeof(struct virtio_gpu_resp_display_info)) != 0)
return -1;
struct virtio_gpu_resp_display_info *info = g->resp;
if (!info->pmodes[0].enabled || info->pmodes[0].r.width == 0) {
cprintf("virtio-gpu: no enabled scanout\n");
return -1;
}
g->width = (int)info->pmodes[0].r.width;
g->height = (int)info->pmodes[0].r.height;
// A host resource to scan out from.
{
struct virtio_gpu_resource_create_2d *c = g->cmd;
hdr_init(c, VIRTIO_GPU_CMD_RESOURCE_CREATE_2D);
c->resource_id = 1;
c->format = VIRTIO_GPU_FORMAT_B8G8R8X8_UNORM;
c->width = g->width;
c->height = g->height;
if (cmd(g, sizeof(*c), sizeof(struct virtio_gpu_ctrl_hdr)) != 0)
return -1;
}
// Guest memory to back it. This is the buffer the rest of the kernel draws
// into; the device never reads it directly -- TRANSFER_TO_HOST_2D copies from
// here into the host resource, which is what makes presenting explicit.
g->fb_bytes = (uint32)g->width * g->height * 4;
g->fb = dma_alloc(g->fb_bytes, 4096);
if (!g->fb) {
cprintf("virtio-gpu: no room for a %d KB framebuffer\n", g->fb_bytes / 1024);
return -1;
}
memset(g->fb, 0, g->fb_bytes);
{
struct virtio_gpu_resource_attach_backing *a = g->cmd;
hdr_init(a, VIRTIO_GPU_CMD_RESOURCE_ATTACH_BACKING);
a->resource_id = 1;
a->nr_entries = 1;
struct virtio_gpu_mem_entry *e = (struct virtio_gpu_mem_entry *)(a + 1);
e->addr = dma_phys(g->fb);
e->length = g->fb_bytes;
e->padding = 0;
if (cmd(g, sizeof(*a) + sizeof(*e), sizeof(struct virtio_gpu_ctrl_hdr)) != 0)
return -1;
}
// Point scanout 0 at it.
{
struct virtio_gpu_set_scanout *s = g->cmd;
hdr_init(s, VIRTIO_GPU_CMD_SET_SCANOUT);
s->r.x = s->r.y = 0;
s->r.width = g->width;
s->r.height = g->height;
s->scanout_id = 0;
s->resource_id = 1;
if (cmd(g, sizeof(*s), sizeof(struct virtio_gpu_ctrl_hdr)) != 0)
return -1;
}
return 0;
}
// --- display class ---------------------------------------------------------
static void
vgpu_mode(struct device *d, struct display_mode *m)
{
m->width = gpu.width;
m->height = gpu.height;
m->bpp = 32;
m->stride = (uint32)gpu.width * 4;
}
static void *
vgpu_acquire(struct device *d, uint32 *stride_out)
{
if (stride_out)
*stride_out = (uint32)gpu.width * 4;
// No separate back buffer: the guest-side framebuffer already *is* one. The
// host cannot see it until TRANSFER_TO_HOST_2D, so drawing into it is
// inherently off-screen -- which is the whole reason this device is better to
// composite against than a raw VBE aperture.
return gpu.ready ? gpu.fb : 0;
}
static void
vgpu_present(struct device *d, int x, int y, int w, int h)
{
if (!gpu.ready)
return;
if (x < 0) { w += x; x = 0; }
if (y < 0) { h += y; y = 0; }
if (x + w > gpu.width) w = gpu.width - x;
if (y + h > gpu.height) h = gpu.height - y;
if (w <= 0 || h <= 0)
return;
{
struct virtio_gpu_transfer_to_host_2d *t = gpu.cmd;
hdr_init(t, VIRTIO_GPU_CMD_TRANSFER_TO_HOST_2D);
t->r.x = x; t->r.y = y; t->r.width = w; t->r.height = h;
t->offset = (uint64)y * gpu.width * 4 + (uint64)x * 4;
t->resource_id = 1;
if (cmd(&gpu, sizeof(*t), sizeof(struct virtio_gpu_ctrl_hdr)) != 0)
return;
}
{
struct virtio_gpu_resource_flush *f = gpu.cmd;
hdr_init(f, VIRTIO_GPU_CMD_RESOURCE_FLUSH);
f->r.x = x; f->r.y = y; f->r.width = w; f->r.height = h;
f->resource_id = 1;
cmd(&gpu, sizeof(*f), sizeof(struct virtio_gpu_ctrl_hdr));
}
}
// No wait_vblank. The device presents whole rectangles atomically, so there is
// nothing to synchronise against -- and a null op is more honest than one that
// spins and achieves nothing. display_wait_vblank() handles the absence.
const struct display_ops virtio_gpu_display_ops = {
.mode = vgpu_mode,
.acquire = vgpu_acquire,
.present = vgpu_present,
.wait_vblank = 0,
};
// --- probe -----------------------------------------------------------------
static int
vgpu_probe(struct pci_dev *d)
{
struct vgpu *g = &gpu;
if (g->ready)
return -1; // one display is enough for now
pci_enable(d, 0x0006); // memory space + bus master
g->common = find_cfg(d, VIRTIO_PCI_CAP_COMMON_CFG, 0);
g->notify = find_cfg(d, VIRTIO_PCI_CAP_NOTIFY_CFG, &g->notify_mult);
if (!g->common || !g->notify) {
cprintf("virtio-gpu: no modern virtio capabilities; not a 1.0 device\n");
return -1;
}
// Reset, then the status handshake in the specified order.
w8(g->common, VCOMMON_STATUS, 0);
while (r8(g->common, VCOMMON_STATUS) != 0)
;
w8(g->common, VCOMMON_STATUS, VSTATUS_ACK);
w8(g->common, VCOMMON_STATUS, VSTATUS_ACK | VSTATUS_DRIVER);
// Accept VIRTIO_F_VERSION_1 and nothing else. Feature bit 32 lives in the
// second 32-bit window, which is what the select registers are for.
w32(g->common, VCOMMON_DFEAT_SEL, 1);
uint32 hi = r32(g->common, VCOMMON_DFEAT);
if (!(hi & (1u << (VIRTIO_F_VERSION_1 - 32)))) {
cprintf("virtio-gpu: device is not virtio 1.0\n");
w8(g->common, VCOMMON_STATUS, VSTATUS_FAILED);
return -1;
}
w32(g->common, VCOMMON_GFEAT_SEL, 0);
w32(g->common, VCOMMON_GFEAT, 0);
w32(g->common, VCOMMON_GFEAT_SEL, 1);
w32(g->common, VCOMMON_GFEAT, 1u << (VIRTIO_F_VERSION_1 - 32));
w8(g->common, VCOMMON_STATUS, VSTATUS_ACK | VSTATUS_DRIVER | VSTATUS_FEATURES_OK);
if (!(r8(g->common, VCOMMON_STATUS) & VSTATUS_FEATURES_OK)) {
// The device is allowed to clear this bit to reject the feature set, and
// this readback is the only way it ever tells you.
cprintf("virtio-gpu: device refused our features\n");
w8(g->common, VCOMMON_STATUS, VSTATUS_FAILED);
return -1;
}
g->cmd = dma_alloc(4096, 16);
g->resp = dma_alloc(4096, 16);
if (!g->cmd || !g->resp || queue_setup(g) != 0) {
cprintf("virtio-gpu: queue setup failed\n");
w8(g->common, VCOMMON_STATUS, VSTATUS_FAILED);
return -1;
}
w8(g->common, VCOMMON_STATUS,
VSTATUS_ACK | VSTATUS_DRIVER | VSTATUS_FEATURES_OK | VSTATUS_DRIVER_OK);
if (setup_scanout(g) != 0) {
w8(g->common, VCOMMON_STATUS, VSTATUS_FAILED);
return -1;
}
g->ready = 1;
cprintf("virtio-gpu: %dx%d, %d KB framebuffer, queue %d\n",
g->width, g->height, g->fb_bytes / 1024, g->qsize);
return 0;
}
static const struct pci_match virtio_gpu_match[] = {
{ VIRTIO_VENDOR, VIRTIO_DEV_GPU, 0, 0 },
{ 0, 0, 0, 0 },
};
const struct pci_driver virtio_gpu_driver = {
.name = "virtio-gpu",
.match = virtio_gpu_match,
.ops = &virtio_gpu_display_ops,
.probe = vgpu_probe,
};

View file

@ -0,0 +1,456 @@
// Intel 8254x gigabit Ethernet, written from Intel's public datasheet (317453).
//
// The first network device. There is no IP stack yet, so what this provides is
// the layer under one: a MAC address, a transmit path, and a receive ring that
// fills with frames. That is deliberately the right order -- an IP stack
// written before a driver has nothing to be tested against, and every bug in it
// looks like a driver bug.
//
// Bring-up follows the manual's initialisation sequence (section 14.3-14.5):
// reset, read the MAC out of EEPROM, clear the multicast table, set up the
// receive and transmit rings, then enable each. The order matters -- enabling a
// receiver whose ring base is still zero makes the device DMA into physical
// address 0, which on this kernel is the boot page table.
#include "types.h"
#include "defs.h"
#include "param.h"
#include "x86.h"
#include "spinlock.h"
#include "sleeplock.h"
#include "fs.h"
#include "file.h"
#include "mmu.h"
#include "memlayout.h"
#include "pci.h"
#include "device.h"
#include "dma.h"
#include "irq.h"
#include "e1000.h"
struct e1000 {
volatile uint8 *regs;
struct e1000_tx_desc *tx;
struct e1000_rx_desc *rx;
uint8 *txbuf[E1000_NTX];
uint8 *rxbuf[E1000_NRX];
uint16 tx_next;
uint16 rx_next;
uint8 mac[6];
int ready;
uint64 rx_packets, tx_packets, rx_dropped;
struct spinlock lock;
};
static struct e1000 nic;
static inline uint32 rd(struct e1000 *e, int off) { return *(volatile uint32 *)(e->regs + off); }
static inline void wr(struct e1000 *e, int off, uint32 v) { *(volatile uint32 *)(e->regs + off) = v; }
// The EEPROM is read one 16-bit word at a time through a single register: write
// the address with the start bit, poll for done, take the data out of the top
// half.
//
// Bounded, because a device whose EEPROM never reports done would otherwise
// hang the boot -- and QEMU's e1000 does answer, but a real card with a blank
// EEPROM is a thing that exists.
static int
eeprom_read(struct e1000 *e, int addr, uint16 *out)
{
wr(e, E1000_EERD, (addr << E1000_EERD_ADDR_SHIFT) | E1000_EERD_START);
for (int i = 0; i < 100000; i++) {
uint32 v = rd(e, E1000_EERD);
if (v & E1000_EERD_DONE) {
*out = (uint16)(v >> E1000_EERD_DATA_SHIFT);
return 0;
}
}
return -1;
}
// The MAC address, from EEPROM words 0..2, little-endian within each word.
//
// Falls back to the receive-address registers, which the firmware may already
// have programmed. QEMU fills both; some cards leave the EEPROM path
// unimplemented, and a driver that only knows one of the two ends up with a MAC
// of all zeroes and no idea why nothing answers it.
static void
read_mac(struct e1000 *e)
{
uint16 w;
int ok = 1;
for (int i = 0; i < 3; i++) {
if (eeprom_read(e, i, &w) != 0) { ok = 0; break; }
e->mac[i * 2] = (uint8)(w & 0xFF);
e->mac[i * 2 + 1] = (uint8)(w >> 8);
}
if (ok)
return;
uint32 lo = rd(e, E1000_RAL), hi = rd(e, E1000_RAH);
e->mac[0] = (uint8)lo; e->mac[1] = (uint8)(lo >> 8);
e->mac[2] = (uint8)(lo >> 16); e->mac[3] = (uint8)(lo >> 24);
e->mac[4] = (uint8)hi; e->mac[5] = (uint8)(hi >> 8);
}
static int
rings_setup(struct e1000 *e)
{
// One block for both rings and every buffer, from the DMA arena: the device
// reads all of it by physical address, and kalloc's pages are contiguous only
// by accident.
uint32 txring = E1000_NTX * sizeof(struct e1000_tx_desc);
uint32 rxring = E1000_NRX * sizeof(struct e1000_rx_desc);
e->tx = dma_alloc(txring, 16);
e->rx = dma_alloc(rxring, 16);
if (!e->tx || !e->rx)
return -1;
memset(e->tx, 0, txring);
memset(e->rx, 0, rxring);
for (int i = 0; i < E1000_NTX; i++) {
e->txbuf[i] = dma_alloc(E1000_BUFSZ, 16);
if (!e->txbuf[i])
return -1;
e->tx[i].addr = dma_phys(e->txbuf[i]);
// DD set on every transmit descriptor at start-up, so the first pass round
// the ring sees them as free. The hardware only sets it after it has used
// one, and a driver that waits for a bit the device has never had reason to
// write waits forever.
e->tx[i].status = E1000_TXD_STAT_DD;
}
for (int i = 0; i < E1000_NRX; i++) {
e->rxbuf[i] = dma_alloc(E1000_BUFSZ, 16);
if (!e->rxbuf[i])
return -1;
e->rx[i].addr = dma_phys(e->rxbuf[i]);
e->rx[i].status = 0;
}
wr(e, E1000_TDBAL, (uint32)dma_phys(e->tx));
wr(e, E1000_TDBAH, (uint32)(dma_phys(e->tx) >> 32));
wr(e, E1000_TDLEN, txring);
wr(e, E1000_TDH, 0);
wr(e, E1000_TDT, 0);
wr(e, E1000_RDBAL, (uint32)dma_phys(e->rx));
wr(e, E1000_RDBAH, (uint32)(dma_phys(e->rx) >> 32));
wr(e, E1000_RDLEN, rxring);
wr(e, E1000_RDH, 0);
// Tail points at the last descriptor the hardware may fill. Setting it to
// NRX-1 hands it every descriptor but one; the ring is full when head meets
// tail, so one must always be held back or "full" and "empty" look identical.
wr(e, E1000_RDT, E1000_NRX - 1);
return 0;
}
// Queue one frame. Returns 0, or -1 if the ring is full.
//
// Copies into the descriptor's own buffer rather than mapping the caller's.
// A zero-copy path wants the caller to guarantee the memory stays put and is
// physically contiguous, which nothing above this can promise yet.
int
e1000_transmit(const void *frame, int len)
{
struct e1000 *e = &nic;
if (!e->ready || len <= 0 || len > E1000_BUFSZ)
return -1;
acquire(&e->lock);
struct e1000_tx_desc *d = &e->tx[e->tx_next];
if (!(d->status & E1000_TXD_STAT_DD)) {
release(&e->lock);
return -1; // the device has not finished with it
}
memmove(e->txbuf[e->tx_next], frame, len);
d->length = (uint16)len;
d->cmd = E1000_TXD_CMD_EOP | E1000_TXD_CMD_IFCS | E1000_TXD_CMD_RS;
d->status = 0;
e->tx_next = (uint16)((e->tx_next + 1) % E1000_NTX);
__sync_synchronize(); // descriptor visible before the tail moves
wr(e, E1000_TDT, e->tx_next);
e->tx_packets++;
release(&e->lock);
return 0;
}
// Take one received frame, if there is one. Returns its length, or 0.
//
// Polled. There is nothing above this to wake yet, and a receive path that
// wakes a nonexistent protocol stack is harder to reason about than one that is
// asked. The interrupt handler below exists to keep the cause register clear
// and to count; when an IP stack arrives, this is where the hand-off changes.
int
e1000_receive(void *out, int cap)
{
struct e1000 *e = &nic;
if (!e->ready)
return 0;
acquire(&e->lock);
struct e1000_rx_desc *d = &e->rx[e->rx_next];
if (!(d->status & E1000_RXD_STAT_DD)) {
release(&e->lock);
return 0;
}
int len = d->length;
if (len > cap)
len = cap;
memmove(out, e->rxbuf[e->rx_next], len);
d->status = 0;
__sync_synchronize();
// Hand the descriptor back by moving the tail to it. The tail always trails
// the descriptor just released, which is why it is the *previous* index.
wr(e, E1000_RDT, e->rx_next);
e->rx_next = (uint16)((e->rx_next + 1) % E1000_NRX);
e->rx_packets++;
release(&e->lock);
return len;
}
static void
e1000_intr(void *arg)
{
struct e1000 *e = arg;
// Reading ICR clears it. Not reading it leaves the cause asserted, and with a
// level-triggered PCI interrupt that means the handler is re-entered forever.
uint32 cause = rd(e, E1000_ICR);
(void)cause;
}
int
e1000_info(char *buf, int cap)
{
struct e1000 *e = &nic;
int n = 0;
if (!e->ready)
return 0;
static const char hex[] = "0123456789abcdef";
n += 0;
for (int i = 0; i < 6 && n + 3 < cap; i++) {
buf[n++] = hex[e->mac[i] >> 4];
buf[n++] = hex[e->mac[i] & 0xF];
if (i < 5)
buf[n++] = ':';
}
if (n < cap) buf[n++] = '\n';
return n;
}
static int
e1000_probe(struct pci_dev *d)
{
struct e1000 *e = &nic;
if (e->ready)
return -1;
if (!d->bar[0].base)
return -1;
initlock(&e->lock, "e1000");
pci_enable(d, 0x0006); // memory space + bus master
e->regs = (volatile uint8 *)P2V(d->bar[0].base);
// Reset, then wait for it to clear. The manual asks for a delay after reset
// before any other register is touched; polling CTRL is that delay.
wr(e, E1000_CTRL, rd(e, E1000_CTRL) | E1000_CTRL_RST);
for (int i = 0; i < 1000000; i++)
if (!(rd(e, E1000_CTRL) & E1000_CTRL_RST))
break;
wr(e, E1000_IMC, 0xFFFFFFFF); // mask everything while we set up
rd(e, E1000_ICR); // and clear whatever was pending
wr(e, E1000_CTRL, rd(e, E1000_CTRL) | E1000_CTRL_SLU | E1000_CTRL_ASDE);
read_mac(e);
// The multicast table is undefined after reset. Left as it comes up, the
// device accepts multicast traffic for addresses nobody asked for.
for (int i = 0; i < 128; i++)
wr(e, E1000_MTA + i * 4, 0);
// Program our own MAC into the receive-address filter, with the valid bit.
{
uint32 lo = (uint32)e->mac[0] | ((uint32)e->mac[1] << 8) |
((uint32)e->mac[2] << 16) | ((uint32)e->mac[3] << 24);
uint32 hi = (uint32)e->mac[4] | ((uint32)e->mac[5] << 8) | (1u << 31);
wr(e, E1000_RAL, lo);
wr(e, E1000_RAH, hi);
}
if (rings_setup(e) != 0) {
cprintf("e1000: no DMA memory for the rings\n");
return -1;
}
wr(e, E1000_TIPG, 10 | (8 << 10) | (6 << 20)); // the manual's IEEE defaults
wr(e, E1000_TCTL, E1000_TCTL_EN | E1000_TCTL_PSP |
(0x10 << E1000_TCTL_CT_SHIFT) |
(0x40 << E1000_TCTL_COLD_SHIFT));
wr(e, E1000_RCTL, E1000_RCTL_EN | E1000_RCTL_BAM | E1000_RCTL_SECRC |
E1000_RCTL_SZ_2048 | E1000_RCTL_LBM_NONE);
if (d->irq != 0xFF)
// level_low = 1: this is a PCI interrupt, which is level-triggered and
// active low. Registering it as edge-triggered makes it fire once and then
// wedge, because the device holds the line asserted and no further edge
// ever arrives. See the comment on irq_register in include/irq.h.
irq_register(d->irq, e1000_intr, e, "e1000", 1);
wr(e, E1000_IMS, E1000_ICR_RXT0 | E1000_ICR_RXDMT0 | E1000_ICR_LSC);
e->ready = 1;
cprintf("e1000: %x:%x:%x:%x:%x:%x, link %s, irq %d\n",
e->mac[0], e->mac[1], e->mac[2], e->mac[3], e->mac[4], e->mac[5],
(rd(e, E1000_STATUS) & E1000_STATUS_LU) ? "up" : "down", d->irq);
return 0;
}
// Every 8254x that shares the register map above. See include/e1000.h for
// where the IDs come from and why the PCIe parts are excluded.
#define M(id) { E1000_VENDOR, (id), 0, 0 }
static const struct pci_match e1000_match[] = {
M(E1000_DEV_82540EM), M(E1000_DEV_82540EM_LOM),
M(E1000_DEV_82540EP_LOM), M(E1000_DEV_82540EP),
M(E1000_DEV_82540EP_LP),
M(E1000_DEV_82541EI), M(E1000_DEV_82541EI_MOBILE),
M(E1000_DEV_82541ER_LOM), M(E1000_DEV_82541ER),
M(E1000_DEV_82541GI), M(E1000_DEV_82541GI_MOBILE),
M(E1000_DEV_82541GI_LF),
M(E1000_DEV_82544EI_FIBER), M(E1000_DEV_82544GC_COPPER),
M(E1000_DEV_82544GC_LOM),
M(E1000_DEV_82545EM_COPPER), M(E1000_DEV_82545EM_FIBER),
M(E1000_DEV_82545GM_COPPER), M(E1000_DEV_82545GM_FIBER),
M(E1000_DEV_82545GM_SERDES),
M(E1000_DEV_82546EB_COPPER), M(E1000_DEV_82546EB_FIBER),
M(E1000_DEV_82546EB_QUAD),
M(E1000_DEV_82546GB_COPPER), M(E1000_DEV_82546GB_FIBER),
M(E1000_DEV_82546GB_SERDES),
M(E1000_DEV_82547EI),
{ 0, 0, 0, 0 },
};
#undef M
const struct pci_driver e1000_driver = {
.name = "e1000",
.devname = "eth",
.match = e1000_match,
.ops = 0, // no class vtable yet; there is no net class
.probe = e1000_probe,
};
// --- self-test: does this thing actually move packets? ---------------------
//
// The transmit and receive paths were written together and neither had ever
// carried a frame. That is exactly the state in which a driver looks finished
// and is not: the rings are set up, the registers read back plausibly, and the
// first thing to discover otherwise would have been a protocol stack failing
// for reasons indistinguishable from its own bugs.
//
// So: build an ARP request by hand, send it, and wait for the reply. It proves
// the whole path at once -- descriptor format, DD handshake, tail bookkeeping,
// DMA addresses, the receive ring handback, and the MAC filter -- because an
// ARP reply is addressed to *us* and will not arrive if the filter is wrong.
//
// Under QEMU's user-mode networking the guest is 10.0.2.15 and the gateway is
// 10.0.2.2. SLIRP is silent until spoken to, which is why this has to transmit
// before it can expect to receive.
//
// `echo n > /proc/sysrq`.
static void
put_be16(uint8 *p, uint16 v)
{
p[0] = (uint8)(v >> 8);
p[1] = (uint8)v;
}
const unsigned char *
e1000_mac(void)
{
return nic.mac;
}
void
e1000_selftest(void)
{
struct e1000 *e = &nic;
uint8 frame[42];
uint8 reply[E1000_BUFSZ];
if (!e->ready) {
cprintf("e1000: no device\n");
return;
}
static const uint8 me[4] = { 10, 0, 2, 15 }; // QEMU SLIRP guest address
static const uint8 them[4] = { 10, 0, 2, 2 }; // and its gateway
memset(frame, 0, sizeof(frame));
for (int i = 0; i < 6; i++)
frame[i] = 0xFF; // broadcast
for (int i = 0; i < 6; i++)
frame[6 + i] = e->mac[i];
put_be16(&frame[12], 0x0806); // ARP
put_be16(&frame[14], 1); // Ethernet
put_be16(&frame[16], 0x0800); // IPv4
frame[18] = 6; // hardware address length
frame[19] = 4; // protocol address length
put_be16(&frame[20], 1); // request
for (int i = 0; i < 6; i++)
frame[22 + i] = e->mac[i]; // sender hardware address
for (int i = 0; i < 4; i++)
frame[28 + i] = me[i]; // sender protocol address
// target hardware address stays zero -- that is what is being asked
for (int i = 0; i < 4; i++)
frame[38 + i] = them[i];
cprintf("e1000: asking who has %d.%d.%d.%d\n", them[0], them[1], them[2], them[3]);
if (e1000_transmit(frame, sizeof(frame)) != 0) {
cprintf("e1000: transmit ring full\n");
return;
}
// Bounded poll. There is no interrupt path to sleep on yet and this runs from
// a sysrq write, so spinning briefly is the honest way to wait.
for (int spin = 0; spin < 20000000; spin++) {
int n = e1000_receive(reply, sizeof(reply));
if (n <= 0)
continue;
cprintf("e1000: %d bytes from %x:%x:%x:%x:%x:%x type %x\n",
n, reply[6], reply[7], reply[8], reply[9], reply[10], reply[11],
((uint32)reply[12] << 8) | reply[13]);
if (n >= 42 && reply[12] == 0x08 && reply[13] == 0x06 &&
reply[20] == 0 && reply[21] == 2) {
cprintf("e1000: ARP reply -- %d.%d.%d.%d is at %x:%x:%x:%x:%x:%x\n",
reply[28], reply[29], reply[30], reply[31],
reply[22], reply[23], reply[24], reply[25],
reply[26], reply[27]);
cprintf("e1000: self-test passed, %d tx %d rx\n",
(int)e->tx_packets, (int)e->rx_packets);
return;
}
}
cprintf("e1000: no reply -- %d tx, %d rx\n",
(int)e->tx_packets, (int)e->rx_packets);
}

114
kernel/drivers/registry.c Normal file
View file

@ -0,0 +1,114 @@
// The driver registry.
//
// One table, one line per driver. This is the same shape as
// kernel/sys/table.inc and kernel/dev/cdev.c's devsw: a single place that
// lists what exists, so a driver cannot be half-registered.
//
// It is a plain array rather than the linker-section trick a bigger kernel
// would use (a __attribute__((section(".drivers"))) entry per driver, walked
// between __start_ and __stop_ symbols). That approach lets a driver register
// itself by existing, which is genuinely nicer -- but it also means the list
// of what is in your kernel is not written down anywhere you can read, and
// a driver silently dropped by the linker looks exactly like one that was
// never written. At this size, being able to read the list wins.
//
// To add a driver: write kernel/drivers/<name>.c exporting a struct
// pci_driver, declare it here, and add it to the array and to the Makefile's
// KERN_DRIVERS.
#include "types.h"
#include "defs.h"
#include "param.h"
#include "spinlock.h"
#include "sleeplock.h"
#include "fs.h"
#include "file.h"
#include "pci.h"
#include "usb.h"
#include "device.h"
extern const struct pci_driver vga_pci_driver;
extern const struct pci_driver xhci_driver;
extern const struct pci_driver virtio_gpu_driver;
extern const struct pci_driver e1000_driver;
// Order matters here in one respect: virtio-gpu is offered the bus before the
// VBE framebuffer driver, so that on a machine with both, the device with real
// mode setting and an explicit flush wins the display class. vga_pci still
// claims the stdvga card underneath, which is what the boot splash and the
// text console are drawn on.
static const struct pci_driver *drivers[] = {
&virtio_gpu_driver,
&vga_pci_driver,
&e1000_driver,
&xhci_driver,
};
void
pci_bind_all(void)
{
for (unsigned i = 0; i < sizeof(drivers) / sizeof(drivers[0]); i++)
pci_bind(drivers[i]);
}
// USB class drivers, same arrangement and for the same reason. These are not
// found on the PCI bus -- they are offered a device by the USB core once its
// interface descriptor has been read -- but a driver you cannot find in a
// list is a driver nobody knows is there, whichever bus it sits on.
//
// To add one: write kernel/drivers/usb_<name>.c exporting a struct
// usb_driver, declare it here, add it to the array and to KERN_DRIVERS.
extern const struct usb_driver usb_hid_driver;
extern const struct usb_driver usb_storage_driver;
extern const struct usb_driver usb_hub_driver;
static const struct usb_driver *usb_drivers[] = {
&usb_hid_driver,
&usb_storage_driver,
&usb_hub_driver,
};
int usb_offer(const struct usb_driver *, struct usb_dev *);
void
usb_bind_all(struct usb_dev *dev)
{
for (unsigned i = 0; i < sizeof(usb_drivers) / sizeof(usb_drivers[0]); i++)
if (usb_offer(usb_drivers[i], dev)) {
// Same as the PCI path: publish it in the bus-agnostic list so that
// "what is attached" has one answer covering every bus. Class comes from
// the driver that claimed it rather than the USB class code, because a
// driver knows what it made of the device and the descriptor only says
// what the device claims to be.
const char *n = usb_drivers[i]->name;
struct device *d = device_add(n, DEVBUS_USB, usb_drivers[i]->devclass, n);
if (d)
device_set_desc(d, usb_drivers[i]->name);
return;
}
}
// Devices that are not enumerated by any bus -- they are simply known to be
// there on a PC, and the driver pokes fixed I/O ports to find out whether it
// is right. Listing them matters because a device manager that shows the
// graphics card but not the keyboard is not showing the machine.
//
// Registered here rather than inside each driver's init so that the whole set
// is visible in one place, which is the same reasoning as the driver arrays
// above.
void
platform_register_devices(void)
{
struct device *d;
if ((d = device_add("kbd", DEVBUS_PLATFORM, DEVCLASS_INPUT, "ps2kbd")))
device_set_desc(d, "PS/2 keyboard");
if ((d = device_add("mouse", DEVBUS_PLATFORM, DEVCLASS_INPUT, "ps2mouse")))
device_set_desc(d, "PS/2 mouse");
if ((d = device_add("uart", DEVBUS_PLATFORM, DEVCLASS_OTHER, "uart8250")))
device_set_desc(d, "16550 serial port");
if ((d = device_add("ide", DEVBUS_PLATFORM, DEVCLASS_STORAGE, "ide")))
device_set_desc(d, "ATA disk controller");
}

View file

@ -0,0 +1,209 @@
// The USB core: everything between "the controller can talk to a device" and
// "a driver knows what the device is".
//
// The split matters. xhci.c deals in TRBs, slot IDs and doorbells; nothing
// here knows any of those words. A class driver deals in reports and
// commands; nothing here knows those either. What lives in the middle is the
// part of USB that is the same for every device ever made: read the
// configuration, choose one, look at the interface, find someone who wants it.
#include "types.h"
#include "defs.h"
#include "param.h"
#include "memlayout.h"
#include "mmu.h"
#include "spinlock.h"
#include "dma.h"
#include "usb.h"
// A staging buffer for control transfers.
//
// It exists because a descriptor has to be read into memory the controller
// can reach and that does not straddle a 64 KB boundary -- a rule that holds
// for xHCI data stages and that a static buffer inside some other structure
// satisfies only by luck. One page, allocated once, copied out of afterwards.
static void *scratch;
static void *
get_scratch(void)
{
if(scratch == 0)
scratch = dma_alloc(4096, 4096);
return scratch;
}
static int
get_descriptor(struct usb_dev *dev, int type, int index, void *buf, int len)
{
struct usb_setup s;
// Clear first. The scratch buffer is shared by every device on the bus, and
// a control transfer reports success without saying how many bytes it
// actually moved -- so a device that answers short leaves the previous
// device's descriptor visible past the end of its own, and the parser
// happily walks into it. Zeroed bytes stop the walk instead, because a
// descriptor whose length is 0 is rejected.
memset(buf, 0, len);
s.bmRequestType = USB_DIR_IN | USB_TYPE_STANDARD | USB_RECIP_DEVICE;
s.bRequest = USB_REQ_GET_DESCRIPTOR;
s.wValue = (type << 8) | index;
s.wIndex = 0;
s.wLength = len;
return dev->ops->control(dev, &s, buf, len);
}
static int
set_configuration(struct usb_dev *dev, int value)
{
struct usb_setup s;
s.bmRequestType = USB_DIR_OUT | USB_TYPE_STANDARD | USB_RECIP_DEVICE;
s.bRequest = USB_REQ_SET_CONFIG;
s.wValue = value;
s.wIndex = 0;
s.wLength = 0;
return dev->ops->control(dev, &s, 0, 0);
}
// Walk the configuration descriptor.
//
// What comes back from GET_DESCRIPTOR(CONFIG) is not one descriptor: it is
// the configuration descriptor followed by every interface and endpoint
// descriptor belonging to it, packed end to end, each one starting with its
// own length. That length is the only thing that can be relied on to find the
// next -- descriptor types this kernel has never heard of appear in the
// middle (HID's own, class-specific ones) and skipping them by size is the
// only way through. A parser that assumed a fixed order would work on one
// device and fail on the next.
static void
parse_config(struct usb_dev *dev)
{
int off = 0;
int seen_iface = 0;
dev->has_intr_in = dev->has_bulk_in = dev->has_bulk_out = 0;
while(off + 2 <= dev->conflen){
uint8 len = dev->conf[off];
uint8 type = dev->conf[off + 1];
if(len < 2 || off + len > dev->conflen)
break; // malformed: stop rather than guess
if(type == USB_DT_INTERFACE && len >= (int)sizeof(dev->iface)){
if(seen_iface)
break; // first interface only, for now
memmove(&dev->iface, &dev->conf[off], sizeof(dev->iface));
seen_iface = 1;
} else if(type == USB_DT_ENDPOINT && seen_iface &&
len >= (int)sizeof(struct usb_endpoint_desc)){
struct usb_endpoint_desc ep;
int in;
memmove(&ep, &dev->conf[off], sizeof(ep));
in = USB_EP_IS_IN(ep.bEndpointAddress);
switch(USB_EP_XFERTYPE(ep.bmAttributes)){
case USB_EP_INTERRUPT:
if(in && !dev->has_intr_in){ dev->intr_in = ep; dev->has_intr_in = 1; }
break;
case USB_EP_BULK:
if(in && !dev->has_bulk_in){ dev->bulk_in = ep; dev->has_bulk_in = 1; }
if(!in && !dev->has_bulk_out){ dev->bulk_out = ep; dev->has_bulk_out = 1; }
break;
}
}
off += len;
}
}
// ---------------------------------------------------------------------------
// Class drivers
// ---------------------------------------------------------------------------
extern const struct usb_driver usb_hid_driver;
void usb_bind_all(struct usb_dev *dev);
static int
matches(const struct usb_driver *drv, struct usb_dev *dev)
{
if(drv->class != 0xFF && drv->class != dev->iface.bInterfaceClass)
return 0;
if(drv->subclass != 0xFF && drv->subclass != dev->iface.bInterfaceSubClass)
return 0;
if(drv->protocol != 0xFF && drv->protocol != dev->iface.bInterfaceProtocol)
return 0;
return 1;
}
// Offer a device to one driver. Returns 1 if it was claimed.
int
usb_offer(const struct usb_driver *drv, struct usb_dev *dev)
{
if(dev->driver || !matches(drv, dev))
return 0;
if(drv->probe(dev) != 0)
return 0;
dev->driver = drv->name;
return 1;
}
// ---------------------------------------------------------------------------
void
usb_attach(struct usb_dev *dev)
{
struct usb_config_desc *cd;
void *buf = get_scratch();
int total;
if(buf == 0)
return;
// Nine bytes first, because wTotalLength is in them and there is no other
// way to know how much to ask for.
if(get_descriptor(dev, USB_DT_CONFIG, 0, buf, 9) < 0){
cprintf("usb: slot %d would not give up its configuration\n", dev->slot);
return;
}
cd = buf;
total = cd->wTotalLength;
// A configuration that does not contain its own descriptor is not one we
// can do anything with, and reading it as a length would walk off the end.
if(cd->bDescriptorType != USB_DT_CONFIG || total < (int)sizeof(*cd)){
cprintf("usb: slot %d returned a malformed configuration descriptor\n",
dev->slot);
return;
}
if(total > USB_MAXCONF)
total = USB_MAXCONF;
if(get_descriptor(dev, USB_DT_CONFIG, 0, buf, total) < 0)
return;
memmove(dev->conf, buf, total);
dev->conflen = total;
parse_config(dev);
// A device is not obliged to do anything at all until it has been
// configured -- before this, endpoints other than 0 do not exist.
if(set_configuration(dev, cd->bConfigurationValue) < 0){
cprintf("usb: slot %d refused SET_CONFIGURATION\n", dev->slot);
return;
}
cprintf("usb: interface class %d.%d protocol %d, %d endpoint%s%s%s%s\n",
dev->iface.bInterfaceClass, dev->iface.bInterfaceSubClass,
dev->iface.bInterfaceProtocol, dev->iface.bNumEndpoints,
dev->iface.bNumEndpoints == 1 ? "" : "s",
dev->has_intr_in ? ", interrupt IN" : "",
dev->has_bulk_in ? ", bulk IN" : "",
dev->has_bulk_out ? ", bulk OUT" : "");
usb_bind_all(dev);
if(dev->driver == 0)
cprintf("usb: no driver for this device\n");
}

View file

@ -0,0 +1,305 @@
// USB HID, boot protocol: keyboards and mice.
//
// Boot protocol is the reason a USB keyboard works in a BIOS setup screen
// before any operating system exists. A HID device normally describes its
// reports with a report descriptor -- a small stack language that has to be
// parsed to know which bit means which key -- but a device claiming the boot
// subclass also promises to support one fixed layout on request. Eight bytes
// for a keyboard, three for a mouse, defined in the HID specification and
// identical on every device.
//
// That is worth taking. A full report-descriptor parser is a real piece of
// work and buys nothing for a keyboard; it is what a game controller or a
// touchscreen will need, and it can arrive with them.
#include "types.h"
#include "defs.h"
#include "param.h"
#include "spinlock.h"
#include "dma.h"
#include "usb.h"
#include "input.h"
#define NHID 4
struct hid {
int in_use;
int proto; // USB_HID_PROTO_KEYBOARD or _MOUSE
uchar prev[8]; // the previous report, for edge detection
};
static struct hid hids[NHID];
// ---------------------------------------------------------------------------
// Keyboard
//
// The report is: modifiers, a reserved byte, then up to six usage IDs of keys
// currently held. It is a *state*, not an event -- a key that stays down
// appears in every report until it is released, so a character is emitted
// only for usages that were not in the previous report. Sending one per
// report instead would repeat every held key a few hundred times a second.
// ---------------------------------------------------------------------------
#define HID_MOD_CTRL 0x11 // left | right
#define HID_MOD_SHIFT 0x22
#define HID_MOD_ALT 0x44
// Usage IDs 0x00-0x38 of the Keyboard/Keypad page. 0 means "nothing to type".
static const uchar hid_unshifted[0x39] = {
0, 0, 0, 0,
'a','b','c','d','e','f','g','h','i','j','k','l','m',
'n','o','p','q','r','s','t','u','v','w','x','y','z',
'1','2','3','4','5','6','7','8','9','0',
'\n', 0x1B, '\b', '\t', ' ',
'-','=','[',']','\\','#',';','\'','`',',','.','/',
};
static const uchar hid_shifted[0x39] = {
0, 0, 0, 0,
'A','B','C','D','E','F','G','H','I','J','K','L','M',
'N','O','P','Q','R','S','T','U','V','W','X','Y','Z',
'!','@','#','$','%','^','&','*','(',')',
'\n', 0x1B, '\b', '\t', ' ',
'_','+','{','}','|','~',':','"','~','<','>','?',
};
// Characters decoded from a report, waiting to be handed to the console.
//
// The console pulls input through a getc function rather than being pushed
// to (consoleintr takes one), which is exactly the shape the PS/2 driver
// needs and exactly the wrong shape here -- a USB report arrives complete,
// with up to six keys in it. So the report is decoded into this queue and
// the console then drains it through the same path as any other keyboard,
// which means the entire line discipline is reused rather than reimplemented.
#define KQSIZE 64
static struct {
uchar buf[KQSIZE];
uint r, w;
} kq;
static void
kq_put(int c)
{
if(kq.w - kq.r < KQSIZE)
kq.buf[kq.w++ % KQSIZE] = c;
}
static int
usb_kbd_getc(void)
{
if(kq.r == kq.w)
return -1;
return kq.buf[kq.r++ % KQSIZE];
}
// Was this usage in that report?
static int
was_down(const uchar *prev, uchar usage)
{
int i;
for(i = 2; i < 8; i++)
if(prev[i] == usage)
return 1;
return 0;
}
// Turn the difference between two reports into press and release events for
// /dev/input/event0.
//
// The console path below cannot do this job: it emits a character per new key
// and nothing at all for a release, because a character stream has no way to
// say "released". A program that wants to know a key is being *held* -- which
// is every game and every compositor -- needs exactly the events this makes.
static void
keyboard_events(const uchar *prev, const uchar *rep)
{
int i, bit, n = 0;
for(bit = 0; bit < 8; bit++){
int now = (rep[0] >> bit) & 1;
int then = (prev[0] >> bit) & 1;
if(now != then){
input_report(INPUT_KBD, EV_KEY, input_hid_keycode(0xE0 + bit), now);
n++;
}
}
for(i = 2; i < 8; i++){
uchar u = rep[i];
if(u && !was_down(prev, u)){
int k = input_hid_keycode(u);
if(k){ input_report(INPUT_KBD, EV_KEY, k, 1); n++; }
}
}
for(i = 2; i < 8; i++){
uchar u = prev[i];
if(u && !was_down(rep, u)){
int k = input_hid_keycode(u);
if(k){ input_report(INPUT_KBD, EV_KEY, k, 0); n++; }
}
}
if(n)
input_sync(INPUT_KBD);
}
static void
keyboard_report(struct hid *h, const uchar *rep, int len)
{
uchar mods = rep[0];
int i, c;
if(len < 8)
return;
keyboard_events(h->prev, rep);
// Grabbed: the events above still went to /dev/input/event0, but nothing
// reaches the console. Note prev[] is updated before returning, or the next
// report would look like every held key had just been pressed again.
if(input_is_grabbed(INPUT_KBD)){
memmove(h->prev, rep, 8);
return;
}
for(i = 2; i < 8; i++){
uchar usage = rep[i];
if(usage == 0 || usage >= 0x39)
continue;
if(was_down(h->prev, usage))
continue; // still held from last time, not a new press
c = (mods & HID_MOD_SHIFT) ? hid_shifted[usage] : hid_unshifted[usage];
if(c == 0)
continue;
// Ctrl folds a letter down into its control character, which is how ^C
// and ^D reach the line discipline at all.
if(mods & HID_MOD_CTRL){
if(c >= 'a' && c <= 'z')
c = c - 'a' + 1;
else if(c >= 'A' && c <= 'Z')
c = c - 'A' + 1;
else
continue;
}
kq_put(c);
}
memmove(h->prev, rep, 8);
consoleintr(usb_kbd_getc);
}
// ---------------------------------------------------------------------------
// Mouse
// ---------------------------------------------------------------------------
static void
mouse_report(struct hid *h, const uchar *rep, int len)
{
int dx, dy;
(void)h;
if(len < 3)
return;
// The deltas are signed bytes. Reading them as unsigned is the classic way
// to get a pointer that only travels right and down.
dx = (signed char)rep[1];
dy = (signed char)rep[2];
// HID's Y axis points down, and so does the screen's. The PS/2 protocol's
// points up, which is why mouseintr subtracts where this adds.
mouse_inject(dx, dy, rep[0] & 0x07);
}
// ---------------------------------------------------------------------------
static void
hid_complete(struct usb_dev *dev, void *buf, int len)
{
struct hid *h = dev->drvdata;
if(h == 0)
return;
if(h->proto == USB_HID_PROTO_KEYBOARD)
keyboard_report(h, buf, len);
else
mouse_report(h, buf, len);
}
static int
hid_request(struct usb_dev *dev, int request, int value)
{
struct usb_setup s;
s.bmRequestType = USB_DIR_OUT | USB_TYPE_CLASS | USB_RECIP_INTERFACE;
s.bRequest = request;
s.wValue = value;
s.wIndex = dev->iface.bInterfaceNumber;
s.wLength = 0;
return dev->ops->control(dev, &s, 0, 0);
}
static int
hid_probe(struct usb_dev *dev)
{
struct hid *h = 0;
int i, proto = dev->iface.bInterfaceProtocol;
if(!dev->has_intr_in){
cprintf("usb-hid: no interrupt IN endpoint; nothing to read\n");
return -1;
}
for(i = 0; i < NHID; i++)
if(!hids[i].in_use){ h = &hids[i]; break; }
if(h == 0)
return -1;
// Ask for the fixed layout explicitly. A device that supports the boot
// subclass still starts in report protocol, so without this the reports
// arrive in whatever shape its report descriptor declares -- which we have
// not read and cannot interpret.
if(hid_request(dev, USB_HID_SET_PROTOCOL, USB_HID_PROTOCOL_BOOT) < 0){
cprintf("usb-hid: SET_PROTOCOL(boot) refused\n");
return -1;
}
// Idle rate 0 = "report only when something changes". The default is to
// resend the current state every few milliseconds forever, which for a
// keyboard means a constant stream of interrupts saying nothing.
hid_request(dev, USB_HID_SET_IDLE, 0);
if(dev->ops->open_ep(dev, &dev->intr_in) < 0)
return -1;
h->in_use = 1;
h->proto = proto;
dev->drvdata = h;
dev->complete = hid_complete;
// Arm the first transfer. Until a TRB is waiting on the endpoint the device
// has nowhere to put a report, so nothing would ever arrive.
if(dev->ops->queue_in(dev, dev->intr_in.bEndpointAddress, 0, 0) < 0)
return -1;
cprintf("usb-hid: %s on slot %d, endpoint 0x%x every %d ms\n",
proto == USB_HID_PROTO_KEYBOARD ? "keyboard" :
proto == USB_HID_PROTO_MOUSE ? "mouse" : "device",
dev->slot, dev->intr_in.bEndpointAddress, dev->intr_in.bInterval);
return 0;
}
// Boot subclass only. A HID device that does not claim it needs its report
// descriptor parsed, and we would have nothing useful to do with it.
const struct usb_driver usb_hid_driver = {
.name = "usb-hid",
.class = USB_CLASS_HID,
.subclass = USB_HID_SUBCLASS_BOOT,
.protocol = 0xFF,
.devclass = DEVCLASS_INPUT,
.probe = hid_probe,
};

View file

@ -0,0 +1,185 @@
// USB hub class driver.
//
// A hub is what makes USB a tree rather than a handful of sockets, and
// nothing about driving one is special: every operation is an ordinary
// control request sent to the hub as a device. "Reset port 3" is a
// SET_FEATURE(PORT_RESET, wIndex=3). The hub has an interrupt endpoint too,
// which reports which ports have changed, but that is only needed for
// hotplug -- at boot it is enough to walk every port and look.
//
// What *is* special is what the host controller has to be told. xHCI does not
// discover topology; it is informed of it. A device three hubs deep is
// addressed by a route string naming the port taken at each tier, and a
// slow device behind a fast hub is scheduled against that hub's transaction
// translator rather than against the device. So this driver's real job is
// less "operate the hub" than "tell the controller what shape the tree is".
#include "types.h"
#include "defs.h"
#include "param.h"
#include "spinlock.h"
#include "dma.h"
#include "usb.h"
#define MAX_PORTS 15 // per hub; more than any real bus hub has
static int
hub_request(struct usb_dev *dev, int type, int req, int value, int index,
void *buf, int len)
{
struct usb_setup s;
s.bmRequestType = type;
s.bRequest = req;
s.wValue = value;
s.wIndex = index;
s.wLength = len;
return dev->ops->control(dev, &s, buf, len);
}
static int
port_set_feature(struct usb_dev *dev, int port, int feature)
{
return hub_request(dev, USB_DIR_OUT | USB_TYPE_CLASS | USB_RECIP_OTHER,
USB_REQ_SET_FEATURE, feature, port, 0, 0);
}
static int
port_clear_feature(struct usb_dev *dev, int port, int feature)
{
return hub_request(dev, USB_DIR_OUT | USB_TYPE_CLASS | USB_RECIP_OTHER,
USB_REQ_CLEAR_FEATURE, feature, port, 0, 0);
}
// Returns wPortStatus, or -1. The change bits are in the upper half and are
// deliberately dropped here -- they are acknowledged separately.
static int
port_status(struct usb_dev *dev, int port, void *scratch)
{
uint16 *st = scratch;
if(hub_request(dev, USB_DIR_IN | USB_TYPE_CLASS | USB_RECIP_OTHER,
USB_REQ_GET_STATUS, 0, port, scratch, 4) < 0)
return -1;
return st[0];
}
// Bring up whatever is plugged into one downstream port.
static void
hub_port(struct usb_dev *hub, int port, void *scratch)
{
int st, speed, i;
st = port_status(hub, port, scratch);
if(st < 0 || !(st & PORTSTAT_CONNECTION))
return;
// Acknowledge the connect-change bit before resetting. Leaving it set is
// harmless here but confuses any later attempt to use these bits for
// hotplug, which is what they are actually for.
port_clear_feature(hub, port, PORT_FEAT_C_CONNECTION);
if(port_set_feature(hub, port, PORT_FEAT_RESET) < 0){
cprintf("usb-hub: port %d would not reset\n", port);
return;
}
// A hub does not report reset completion by clearing the request; it sets
// C_PORT_RESET and enables the port. Poll for the enable rather than
// guessing at a delay, because the time depends on the device.
for(i = 0; i < 200; i++){
microdelay(1000);
st = port_status(hub, port, scratch);
if(st >= 0 && !(st & PORTSTAT_RESET) && (st & PORTSTAT_ENABLE))
break;
}
port_clear_feature(hub, port, PORT_FEAT_C_RESET);
st = port_status(hub, port, scratch);
if(st < 0 || !(st & PORTSTAT_ENABLE)){
cprintf("usb-hub: port %d did not enable after reset\n", port);
return;
}
// Speed comes out of two status bits rather than a field, and the absence
// of both means full speed.
if(st & PORTSTAT_LOWSPEED)
speed = USB_SPEED_LOW;
else if(st & PORTSTAT_HIGHSPEED)
speed = USB_SPEED_HIGH;
else
speed = USB_SPEED_FULL;
// A device needs time to settle after reset before it will answer on
// endpoint 0. The specification calls this TRSTRCY and allows 10 ms.
microdelay(20000);
hub->ops->attach(hub, port, speed);
}
static int
hub_probe(struct usb_dev *dev)
{
struct usb_hub_desc *hd;
void *scratch;
int nports, mtt, i;
scratch = dma_alloc(64, 64);
if(scratch == 0)
return -1;
// The hub descriptor is class-specific, so it comes from a class GET
// request rather than the standard one -- same request number, different
// recipient and type.
if(hub_request(dev, USB_DIR_IN | USB_TYPE_CLASS | USB_RECIP_DEVICE,
USB_REQ_GET_DESCRIPTOR, USB_DT_HUB << 8, 0,
scratch, sizeof(struct usb_hub_desc)) < 0){
cprintf("usb-hub: no hub descriptor\n");
dma_free(scratch, 64);
return -1;
}
hd = scratch;
nports = hd->bNbrPorts;
if(nports < 1 || nports > MAX_PORTS){
cprintf("usb-hub: %d ports is not a number of ports\n", nports);
dma_free(scratch, 64);
return -1;
}
mtt = (hd->wHubCharacteristics & HUB_CHAR_TT_MASK) != 0;
cprintf("usb-hub: %d ports on slot %d\n", nports, dev->slot);
// Tell the controller *before* powering anything on. Addressing a device
// behind a slot the controller does not know is a hub fails, and it fails
// at Address Device with a completion code rather than anywhere useful.
if(dev->ops->set_hub(dev, nports, mtt) < 0){
dma_free(scratch, 64);
return -1;
}
// Ports come up unpowered. bPwrOn2PwrGood is how long the hub says it needs
// after switching power on before a device behind it is worth talking to,
// in 2 ms units -- a number the hub knows and we do not.
for(i = 1; i <= nports; i++)
port_set_feature(dev, i, PORT_FEAT_POWER);
microdelay((hd->bPwrOn2PwrGood * 2 + 10) * 1000);
for(i = 1; i <= nports; i++)
hub_port(dev, i, scratch);
dma_free(scratch, 64);
return 0;
}
// Any hub, whatever its protocol: 0 is a full-speed hub, 1 single-TT, 2
// multi-TT, and they differ only in how transactions are translated, which
// is the controller's problem rather than ours.
const struct usb_driver usb_hub_driver = {
.name = "usb-hub",
.class = USB_CLASS_HUB,
.subclass = 0xFF,
.protocol = 0xFF,
.devclass = DEVCLASS_BUS,
.probe = hub_probe,
};

View file

@ -0,0 +1,395 @@
// USB mass storage: bulk-only transport, SCSI transparent command set.
//
// Almost every USB stick, external drive and card reader is this, and the
// name is a fair description of the whole protocol: there are exactly two
// bulk endpoints and everything happens by sending three things over them.
//
// command a 31-byte wrapper (CBW) carrying a SCSI command block, OUT
// data the payload, IN or OUT, if the command has one
// status a 13-byte wrapper (CSW) saying whether it worked, IN
//
// That is the entire transport. The interesting content is SCSI, which is
// what the wrapper carries and what the device actually speaks -- so a USB
// stick is a SCSI disk that happens to be reached over USB, and the commands
// below (INQUIRY, READ CAPACITY, READ(10), WRITE(10)) are the same ones a
// SCSI drive answered in 1990.
#include "types.h"
#include "defs.h"
#include "param.h"
#include "memlayout.h"
#include "mmu.h"
#include "spinlock.h"
#include "sleeplock.h"
#include "fs.h"
#include "buf.h"
#include "dma.h"
#include "usb.h"
#define CBW_SIGNATURE 0x43425355 // "USBC"
#define CSW_SIGNATURE 0x53425355 // "USBS"
struct cbw {
uint32 signature;
uint32 tag;
uint32 data_len;
uint8 flags; // 0x80 = device-to-host
uint8 lun;
uint8 cb_len; // 1..16
uint8 cb[16];
} __attribute__((packed));
struct csw {
uint32 signature;
uint32 tag;
uint32 residue;
uint8 status; // 0 = passed, 1 = failed, 2 = phase error
} __attribute__((packed));
// SCSI opcodes
#define SCSI_TEST_UNIT_READY 0x00
#define SCSI_REQUEST_SENSE 0x03
#define SCSI_INQUIRY 0x12
#define SCSI_READ_CAPACITY10 0x25
#define SCSI_READ10 0x28
#define SCSI_WRITE10 0x2A
#define SECTOR_SIZE 512
static struct {
struct usb_dev *dev;
int present;
uint32 nsectors;
uint32 sector_size;
uint32 tag;
void *xfer; // DMA bounce buffer, one block
struct cbw *cbw; // allocated once, not per command
struct csw *csw;
struct sleeplock lock; // one command at a time
} ums;
// SCSI is big-endian, everywhere, for everything. x86 is not.
static void
be32(uint8 *p, uint32 v)
{
p[0] = v >> 24; p[1] = v >> 16; p[2] = v >> 8; p[3] = v;
}
static uint32
rd_be32(const uint8 *p)
{
return ((uint32)p[0] << 24) | ((uint32)p[1] << 16) |
((uint32)p[2] << 8) | p[3];
}
// One complete command-data-status exchange.
//
// `buf`, when there is data, must be DMA memory -- it is handed to the
// controller as a physical address.
static int
bot_command(const uint8 *cdb, int cdb_len, void *buf, int len, int in)
{
struct usb_dev *dev = ums.dev;
struct cbw *cbw = ums.cbw;
struct csw *csw = ums.csw;
uint32 tag = ++ums.tag;
int i, data_ok = 1;
memset(cbw, 0, sizeof(*cbw));
cbw->signature = CBW_SIGNATURE;
cbw->tag = tag;
cbw->data_len = len;
cbw->flags = in ? 0x80 : 0x00;
cbw->lun = 0;
cbw->cb_len = cdb_len;
for(i = 0; i < cdb_len && i < 16; i++)
cbw->cb[i] = cdb[i];
if(dev->ops->bulk(dev, dev->bulk_out.bEndpointAddress, cbw,
sizeof(*cbw)) != (int)sizeof(*cbw)){
cprintf("usb-storage: the device would not take the command\n");
return -1;
}
if(len > 0){
int addr = in ? dev->bulk_in.bEndpointAddress
: dev->bulk_out.bEndpointAddress;
if(dev->ops->bulk(dev, addr, buf, len) < 0)
data_ok = 0;
}
// The status wrapper is not optional and not advisory: a device that has
// sent its data is still waiting to be asked how it went.
//
// It is read even when the data phase failed, which is the whole point --
// returning early there would leave the device still holding a status it
// has not been asked for, so the *next* command's data phase would receive
// this command's status instead. One failed read would turn into every
// subsequent read failing, which is a far worse fault than the one that
// started it, and one that looks nothing like its cause.
memset(csw, 0, sizeof(*csw));
if(dev->ops->bulk(dev, dev->bulk_in.bEndpointAddress, csw,
sizeof(*csw)) != (int)sizeof(*csw)){
cprintf("usb-storage: no status returned for command 0x%x\n", cdb[0]);
return -1;
}
if(csw->signature != CSW_SIGNATURE || csw->tag != tag){
cprintf("usb-storage: status wrapper does not match the command\n");
return -1;
}
if(csw->status != 0){
cprintf("usb-storage: command 0x%x failed (status %d)\n",
cdb[0], csw->status);
return -1;
}
if(!data_ok)
return -1;
// Residue is what was *not* transferred, and a device is allowed to report
// more than was asked for only if it is broken -- clamp rather than return
// a negative length that a caller would read as an error code.
if(csw->residue > (uint32)len)
return -1;
return len - (int)csw->residue;
}
static int
scsi_inquiry(char *vendor, char *product)
{
uint8 cdb[6] = { SCSI_INQUIRY, 0, 0, 0, 36, 0 };
uint8 *r = ums.xfer;
int i;
if(bot_command(cdb, sizeof(cdb), r, 36, 1) < 0)
return -1;
// Bytes 8-15 are the vendor, 16-31 the product, space padded and not
// NUL-terminated -- the only fixed-width strings left in computing.
for(i = 0; i < 8; i++)
vendor[i] = r[8 + i];
vendor[8] = 0;
for(i = 0; i < 16; i++)
product[i] = r[16 + i];
product[16] = 0;
return 0;
}
static int
scsi_read_capacity(void)
{
uint8 cdb[10] = { SCSI_READ_CAPACITY10, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
uint8 *r = ums.xfer;
if(bot_command(cdb, sizeof(cdb), r, 8, 1) < 0)
return -1;
// The first word is the address of the *last* sector, not the count.
ums.nsectors = rd_be32(r) + 1;
ums.sector_size = rd_be32(r + 4);
return 0;
}
static int
scsi_rw(uint32 lba, int count, void *buf, int write)
{
uint8 cdb[10];
memset(cdb, 0, sizeof(cdb));
cdb[0] = write ? SCSI_WRITE10 : SCSI_READ10;
be32(&cdb[2], lba);
cdb[7] = (count >> 8) & 0xFF;
cdb[8] = count & 0xFF;
return bot_command(cdb, sizeof(cdb), buf, count * SECTOR_SIZE, !write);
}
// ---------------------------------------------------------------------------
// Block device
// ---------------------------------------------------------------------------
int
usb_storage_present(void)
{
return ums.present;
}
// Sync one buffer-cache block with the stick. Same contract as iderw() and
// ramdiskrw(): on a read fill b->data and set B_VALID, on a write push it out
// and clear B_DIRTY.
void
usb_storage_rw(struct buf *b)
{
int per_block = BSIZE / SECTOR_SIZE;
uint32 lba = b->blockno * per_block;
int write = (b->flags & B_DIRTY) != 0;
int i, n = -1;
if(!holdingsleep(&b->lock))
panic("usb_storage_rw: buf not locked");
if(!ums.present)
panic("usb_storage_rw: no USB disk");
if(lba + per_block > ums.nsectors)
panic("usb_storage_rw: block out of range");
// One command at a time. Bulk-only transport has no tagging beyond the CBW
// tag and no queueing: a second command sent while the first is mid-phase
// desynchronises the endpoint and every command after it fails.
acquiresleep(&ums.lock);
// Removable media fails transiently in ways a fixed disk does not, so a
// couple of retries are worth having before giving up.
for(i = 0; i < 3; i++){
if(write)
memmove(ums.xfer, b->data, BSIZE);
n = scsi_rw(lba, per_block, ums.xfer, write);
if(n == BSIZE)
break;
cprintf("usb-storage: %s of block %d failed, retrying\n",
write ? "write" : "read", b->blockno);
}
releasesleep(&ums.lock);
// A read that failed must not return quietly. bread() hands the buffer
// straight back to its caller, which has no way to ask whether it worked --
// so leaving B_VALID clear would deliver whatever the buffer happened to
// contain as though it were the block on disk. Corrupting a filesystem
// silently is worse than stopping.
if(n != BSIZE)
panic("usb_storage_rw: transfer failed");
if(!write)
memmove(b->data, ums.xfer, BSIZE);
b->flags |= B_VALID;
b->flags &= ~B_DIRTY;
}
// Read block 1 through the ordinary buffer cache and report what is there.
//
// This is the only test that proves the whole path rather than a piece of it:
// bread() -> bdevrw() -> usb_storage_rw() -> bulk-only transport -> SCSI
// READ(10) -> an interrupt -> a sleeping process woken with data. Everything
// before it -- INQUIRY answering, a plausible capacity -- can be true of a
// device that cannot actually read a sector.
//
// Reached with `echo s > /proc/sysrq`.
void
usb_storage_dump(void)
{
struct buf *b;
struct superblock *sb;
if(!ums.present){
cprintf("usb-storage: no disk\n");
return;
}
cprintf("usb-storage: %d sectors of %d bytes\n", ums.nsectors,
ums.sector_size);
b = bread(USBDISK_DEV, 1);
sb = (struct superblock*)b->data;
cprintf("usb-storage: block 1 magic %x size %d nblocks %d ninodes %d\n",
sb->magic, sb->size, sb->nblocks, sb->ninodes);
brelse(b);
}
// ---------------------------------------------------------------------------
static int
storage_probe(struct usb_dev *dev)
{
char vendor[9], product[17];
int i;
if(ums.present)
return -1; // one disk is enough for now
if(!dev->has_bulk_in || !dev->has_bulk_out){
cprintf("usb-storage: needs a bulk endpoint in each direction\n");
return -1;
}
initsleeplock(&ums.lock, "usbms");
ums.dev = dev;
// One block, used for every transfer. bread() hands us b->data, which lives
// in the buffer cache rather than in DMA memory, so everything bounces
// through here. The wrappers are allocated once too: they are used on every
// single I/O, and churning the DMA extent list twice per block read would
// fragment an arena that has no business fragmenting.
ums.xfer = dma_alloc(BSIZE, 4096);
ums.cbw = dma_alloc(sizeof(struct cbw), 64);
ums.csw = dma_alloc(sizeof(struct csw), 64);
if(ums.xfer == 0 || ums.cbw == 0 || ums.csw == 0)
goto fail;
// Deliberately OUT first, which is the order that used to break.
//
// The slot context's Context Entries field must cover the highest DCI in
// use, and xhci_open_ep originally wrote whichever endpoint it was adding.
// Bulk OUT here has the higher DCI, so opening it first and then IN would
// *lower* the field and silently invalidate the endpoint that was already
// configured. Opening IN first hid that. Now that xhci_open_ep tracks the
// maximum itself the order genuinely does not matter -- and doing it in the
// formerly-broken order is what keeps that true.
if(dev->ops->open_ep(dev, &dev->bulk_out) < 0 ||
dev->ops->open_ep(dev, &dev->bulk_in) < 0)
goto fail;
// Deliberately no GET MAX LUN. It is answered with a STALL by
// single-LUN devices, and recovering from a STALL on endpoint 0 needs a
// Reset Endpoint command this driver does not implement yet. LUN 0 is the
// only one used below, so asking would gain nothing and could wedge us.
// A device that has just been configured is usually "not ready" for the
// first command or two while it spins up or reads its own media.
for(i = 0; i < 5; i++){
uint8 cdb[6] = { SCSI_TEST_UNIT_READY, 0, 0, 0, 0, 0 };
if(bot_command(cdb, sizeof(cdb), 0, 0, 0) >= 0)
break;
microdelay(100000);
}
if(scsi_inquiry(vendor, product) < 0)
goto fail;
if(scsi_read_capacity() < 0)
goto fail;
if(ums.sector_size != SECTOR_SIZE){
cprintf("usb-storage: %d-byte sectors are not supported\n",
ums.sector_size);
goto fail;
}
if(ums.nsectors == 0){
cprintf("usb-storage: device reports no capacity\n");
goto fail;
}
ums.present = 1;
cprintf("usb-storage: %s %s, %d sectors (%d MB)\n",
vendor, product, ums.nsectors,
(int)((uint64)ums.nsectors * SECTOR_SIZE / (1024 * 1024)));
return 0;
fail:
// Give the memory back and forget the device. Leaving ums.dev set would let
// a later block read reach a device we decided we could not drive.
if(ums.xfer) dma_free(ums.xfer, BSIZE);
if(ums.cbw) dma_free(ums.cbw, sizeof(struct cbw));
if(ums.csw) dma_free(ums.csw, sizeof(struct csw));
ums.xfer = 0;
ums.cbw = 0;
ums.csw = 0;
ums.dev = 0;
return -1;
}
const struct usb_driver usb_storage_driver = {
.name = "usb-storage",
.class = USB_CLASS_MASS_STORAGE,
.subclass = USB_MSC_SUBCLASS_SCSI,
.protocol = USB_MSC_PROTO_BOT,
.devclass = DEVCLASS_STORAGE,
.probe = storage_probe,
};

241
kernel/fs/exec.c Normal file
View file

@ -0,0 +1,241 @@
#include "types.h"
#include "traps.h"
#include "param.h"
#include "memlayout.h"
#include "mmu.h"
#include "proc.h"
#include "defs.h"
#include "x86.h"
#include "elf.h"
#include "fcntl.h"
#include "errno.h"
// Binaries with entry points at 0x100000+ are musl/Linux-ABI.
// Schunix-native binaries (init) are linked at 0x4000.
#define LINUX_ABI_ENTRY_THRESHOLD 0x100000UL
extern int linux_syscall_map[];
extern int linux_syscall_map_size;
int
exec(char *path, char **argv)
{
char *s, *last;
int i, off;
uint64 argc, sz, sp, ustack[MAXARG+1];
struct elfhdr elf;
struct inode *ip;
struct proghdr ph;
pml4e_t *pgdir, *oldpgdir;
uint64 stack_slots[MAXARG + 32];
int slot;
uint64 max_sz = 0;
uint64 rand_va = 0;
begin_op();
if((ip = namei(path)) == 0){
end_op();
// Must be ENOENT, not a bare -1: execvp() relies on this exact errno to
// know it should try the next $PATH component rather than give up.
return -ENOENT;
}
ilock(ip);
pgdir = 0;
if(readi(ip, 0, (uint64)&elf, 0, sizeof(elf)) != sizeof(elf))
goto bad;
if(elf.magic != ELF_MAGIC)
goto bad;
if((pgdir = setupkvm()) == 0)
goto bad;
for(i=0, off=elf.phoff; i<elf.phnum; i++, off+=elf.phentsize){
if(readi(ip, 0, (uint64)&ph, off, sizeof(ph)) != sizeof(ph))
goto bad;
if(ph.type != ELF_PROG_LOAD)
continue;
if(ph.memsz < ph.filesz)
goto bad;
if(ph.vaddr + ph.memsz < ph.vaddr)
goto bad;
uint64 start = ph.vaddr & ~0xFFF;
uint64 end = ph.vaddr + ph.memsz;
if(allocuvm(pgdir, start, end) == 0)
goto bad;
if(loaduvm(pgdir, (char*)ph.vaddr, ip, ph.off, ph.filesz) < 0)
goto bad;
if(end > max_sz)
max_sz = end;
}
iunlockput(ip);
end_op();
ip = 0;
sz = PGROUNDUP(max_sz);
if((sz = allocuvm(pgdir, sz, sz + 64*PGSIZE)) == 0)
goto bad;
clearpteu(pgdir, (char*)(sz - 64*PGSIZE));
sp = sz;
for(argc = 0; argv[argc]; argc++) {
if(argc >= MAXARG)
goto bad;
sp -= strlen(argv[argc]) + 1;
sp &= ~7;
if(copyout(pgdir, sp, argv[argc], strlen(argv[argc]) + 1) < 0)
goto bad;
ustack[argc] = sp;
}
// Push 16 pseudo-random bytes for AT_RANDOM.
{
uint8 randbuf[16];
uint rseed = (uint)(uintptr_t)myproc() ^ (uint)myproc()->pid;
for(int ri = 0; ri < 16; ri++){
rseed = rseed * 1664525u + 1013904223u;
randbuf[ri] = (uint8)(rseed >> 16);
}
sp -= 16; sp &= ~0xFUL;
rand_va = sp;
if(copyout(pgdir, rand_va, (char*)randbuf, 16) < 0) goto bad;
}
slot = 0;
stack_slots[slot++] = argc;
for(i = 0; i < argc; i++) {
stack_slots[slot++] = ustack[i];
}
stack_slots[slot++] = 0; // end of argv
stack_slots[slot++] = 0; // end of envp (no environment)
// Auxiliary vector — musl reads these for page size, clock, etc.
stack_slots[slot++] = 6; // AT_PAGESZ
stack_slots[slot++] = 4096;
stack_slots[slot++] = 17; // AT_CLKTCK
stack_slots[slot++] = 100;
stack_slots[slot++] = 23; // AT_SECURE = 0 (not setuid)
stack_slots[slot++] = 0;
stack_slots[slot++] = 11; // AT_UID
stack_slots[slot++] = 0;
stack_slots[slot++] = 12; // AT_EUID
stack_slots[slot++] = 0;
stack_slots[slot++] = 13; // AT_GID
stack_slots[slot++] = 0;
stack_slots[slot++] = 14; // AT_EGID
stack_slots[slot++] = 0;
stack_slots[slot++] = 16; // AT_HWCAP
stack_slots[slot++] = 0;
stack_slots[slot++] = 25; // AT_RANDOM
stack_slots[slot++] = rand_va;
stack_slots[slot++] = 0; // AT_NULL
stack_slots[slot++] = 0;
sp -= slot * sizeof(uint64);
sp &= ~0xF;
if(copyout(pgdir, sp, (char*)stack_slots, slot * sizeof(uint64)) < 0)
goto bad;
for(last=s=path; *s; s++)
if(*s == '/')
last = s+1;
safestrcpy(myproc()->name, last, sizeof(myproc()->name));
safestrcpy(myproc()->exe, path, sizeof(myproc()->exe));
for(int i = 0; i < NOFILE; i++){
if(myproc()->ofile[i] && (myproc()->ofile_flags[i] & FD_CLOEXEC)){
fileclose(myproc()->ofile[i]);
myproc()->ofile[i] = 0;
myproc()->ofile_flags[i] = 0;
}
}
// Auto-select syscall ABI: musl binaries have entry >= 0x100000;
// Schunix-native binaries (init) are linked at 0x4000.
if (elf.entry >= LINUX_ABI_ENTRY_THRESHOLD) {
myproc()->syscall_map = linux_syscall_map;
myproc()->map_size = linux_syscall_map_size;
} else {
myproc()->syscall_map = 0;
myproc()->map_size = 0;
}
// Detach shared segments before anything forgets which mappings they were.
//
// This has to happen while myproc()->pgdir and myproc()->vma[] still describe the
// *old* image: the memset below erases the bookkeeping, and freevm() at the
// end would then free a shared memory segment's pages as though they were
// ordinary heap. Doing it here rather than there is the difference between
// detaching a segment and corrupting whoever else has it mapped.
proc_release_shared(myproc());
oldpgdir = myproc()->pgdir;
myproc()->pgdir = pgdir;
myproc()->sz = sz;
// New address space: reset all per-process mmap/signal/TLS state.
// sig_handler/sig_restorer point into the *old* image — POSIX resets
// handled signals to default on exec, and keeping them would make the
// first delivered signal jump to a stale address in the new image.
// mmap_next likewise belongs to the old address space; left alone it
// could hand out mmap ranges overlapping the new image's layout.
memset(myproc()->vma, 0, sizeof(myproc()->vma));
memset(myproc()->sig_handler, 0, sizeof(myproc()->sig_handler));
memset(myproc()->sig_restorer, 0, sizeof(myproc()->sig_restorer));
myproc()->fs_base = 0;
myproc()->signal_mask = 0;
myproc()->pending_signals = 0;
myproc()->in_signal = 0;
myproc()->sigsusp_active = 0;
myproc()->mmap_next = 0;
myproc()->tf->rax = 0;
myproc()->tf->rbx = 0;
myproc()->tf->rdx = 0;
myproc()->tf->rbp = 0;
myproc()->tf->r8 = 0;
myproc()->tf->r9 = 0;
myproc()->tf->r10 = 0;
myproc()->tf->r11 = 0x200; // RFLAGS for sysretq: IF=1 (bit 9) so user gets timer IRQs
myproc()->tf->r12 = 0;
myproc()->tf->r13 = 0;
myproc()->tf->r14 = 0;
myproc()->tf->r15 = 0;
myproc()->tf->rdi = argc;
myproc()->tf->rsi = sp + 8;
myproc()->tf->rip = elf.entry;
myproc()->tf->rcx = elf.entry;
myproc()->tf->rsp = sp;
// syscall_entry saves user RSP at kstack+PGSIZE-16 (not in the trapframe).
// sysretq reads RSP from that slot, so update it to the new stack pointer.
// Without this, sysretq returns with the old parent's RSP, causing lksh to
// start with an invalid/wrong stack pointer.
*(uint64*)((char*)myproc()->kstack + PGSIZE - 16) = sp;
// Userspace is running now, so the splash has done its job and the display
// becomes a terminal. Only the first exec does anything; every later one
// finds the console already in charge.
console_takeover();
switchuvm(myproc());
freevm(oldpgdir);
return 0;
bad:
if(pgdir)
freevm(pgdir);
if(ip){
iunlockput(ip);
end_op();
}
return -1;
}

294
kernel/fs/file.c Normal file
View file

@ -0,0 +1,294 @@
#include "types.h"
#include "defs.h"
#include "stat.h"
#include "param.h"
#include "fs.h"
#include "spinlock.h"
#include "sleeplock.h"
#include "file.h"
#include "posix.h"
#include "errno.h"
extern struct devsw devsw[];
struct {
struct spinlock lock;
struct file file[NFILE];
} ftable;
void
fileinit(void)
{
initlock(&ftable.lock, "ftable");
}
struct file*
filealloc(void)
{
struct file *f;
acquire(&ftable.lock);
for(f = ftable.file; f < ftable.file + NFILE; f++){
if(f->ref == 0){
f->ref = 1;
// Cleared here rather than left to the caller: every creator sets
// readable/writable/off, but only open() sets nonblock, so a struct file
// recycled from a non-blocking device would hand its flag to the next
// pipe or socket that happened to land in this slot.
f->nonblock = 0;
release(&ftable.lock);
return f;
}
}
release(&ftable.lock);
return 0;
}
struct file*
filedup(struct file *f)
{
acquire(&ftable.lock);
if(f->ref < 1)
panic("filedup");
f->ref++;
release(&ftable.lock);
return f;
}
void
fileclose(struct file *f)
{
struct file ff;
acquire(&ftable.lock);
if(f->ref < 1)
panic("fileclose");
if(--f->ref > 0){
release(&ftable.lock);
return;
}
ff = *f;
f->ref = 0;
f->type = FD_NONE;
release(&ftable.lock);
if(ff.type == FD_PIPE)
pipeclose(ff.pipe, ff.writable);
else if(ff.type == FD_SOCKET)
sockclose(ff.sock);
else if(ff.type == FD_INODE){
// Give a device a chance to put the hardware back before the inode goes.
// Done here rather than in exit() because a program that closes the
// framebuffer without exiting has finished with it just as thoroughly.
if(ff.ip && ff.ip->type == T_DEV && ff.ip->major >= 0 &&
ff.ip->major < NDEV && devsw[ff.ip->major].close)
devsw[ff.ip->major].close(ff.ip);
begin_op();
iput(ff.ip);
end_op();
}
}
int
filestat(struct file *f, struct stat *st)
{
if(f->type == FD_INODE){
ilock(f->ip);
stati(f->ip, st);
iunlock(f->ip);
return 0;
}
return -1;
}
int
fileread(struct file *f, char *addr, int n)
{
int r;
if(f->readable == 0)
return -1;
// O_NONBLOCK on a pipe or a socket. Reported here rather than inside
// piperead/sockread so that the blocking implementations stay unchanged:
// this is a check of the same condition their sleep loops wait on, made one
// level up. Writes still block -- fcntl documents that.
if(f->type == FD_PIPE){
if(f->nonblock && !(pipe_poll(f->pipe, f->readable, f->writable) & POLLIN))
return -EAGAIN;
return piperead(f->pipe, addr, n);
}
if(f->type == FD_SOCKET){
if(f->nonblock && !sockreadable(f->sock))
return -EAGAIN;
return sockread(f->sock, addr, n);
}
if(f->type == FD_INODE){
ilock(f->ip);
if(f->ip->type == 3){
if(f->ip->major < 0 || f->ip->major >= NDEV || !devsw[f->ip->major].read){
iunlock(f->ip);
return -1;
}
// O_NONBLOCK. Without this a game that opens an input device
// non-blocking and drains it once per frame stops dead on the first
// frame -- it blocks in read() waiting for a key, and the result looks
// exactly like a hung renderer rather than a blocking read.
if(f->nonblock && devsw[f->ip->major].ready &&
!devsw[f->ip->major].ready(f->ip)){
iunlock(f->ip);
return -EAGAIN;
}
r = devsw[f->ip->major].read(f->ip, f->off, addr, n);
if(r > 0)
f->off += r;
iunlock(f->ip);
return r;
}
if((f->ip->rwmode & 2) == 0){
cprintf("Schunix: permission denied (read)\n");
iunlock(f->ip);
return -1;
}
// The "recycle bin" check that used to sit here is gone.
//
// It made a file that still existed, still had a name and still had blocks
// refuse every read with a printf -- a state no POSIX program is prepared
// for. It was reachable only through `delete`, a NO_LINUX syscall from the
// third-party filesystem this file derives from, which the decision to keep
// only Linux numbering had already orphaned.
//
// The on-disk `showmode` field stays for now: removing it changes the inode
// layout, and this whole file is being replaced by SchFS. It is simply
// never consulted.
if((r = readi(f->ip, 0, (uint64)addr, f->off, n)) > 0)
f->off += r;
iunlock(f->ip);
return r;
}
panic("fileread");
}
int
filewrite(struct file *f, char *addr, int n)
{
int r;
if(f->writable == 0)
return -1;
if(f->type == FD_PIPE)
return pipewrite(f->pipe, addr, n);
if(f->type == FD_SOCKET)
return sockwrite(f->sock, addr, n);
if(f->type == FD_INODE){
ilock(f->ip);
if(f->ip->type == 3){
if(f->ip->major < 0 || f->ip->major >= NDEV || !devsw[f->ip->major].write){
iunlock(f->ip);
return -1;
}
r = devsw[f->ip->major].write(f->ip, f->off, addr, n);
if(r > 0)
f->off += r;
iunlock(f->ip);
return r;
}
if((f->ip->rwmode & 1) == 0){
cprintf("Schunix: permission denied (write)\n");
iunlock(f->ip);
return -1;
}
// Recycle-bin check removed here too; see the note in fileread().
iunlock(f->ip);
int max = ((LOGSIZE-1-1-2) / 2) * 512;
int i = 0;
while(i < n){
int n1 = n - i;
if(n1 > max)
n1 = max;
begin_op();
ilock(f->ip);
if ((r = writei(f->ip, 1, (uint64)(addr + i), f->off, n1)) > 0)
f->off += r;
iunlock(f->ip);
end_op();
if(r < 0)
break;
if(r != n1)
panic("short filewrite");
i += r;
}
return i == n ? n : -1;
}
panic("filewrite");
}
// poll(2) readiness for one open file. Returns a POLL* bitmask restricted
// to the requested events (plus error/hangup conditions, which POSIX says
// are always reported). Pipes and the console tty have real state; other
// inodes (regular files, block-ish devices) are always ready, per POSIX.
int
file_poll(struct file *f, int events)
{
if(f->type == FD_PIPE)
return pipe_poll(f->pipe, f->readable, f->writable) &
(events | POLLERR | POLLHUP);
if(f->type == FD_SOCKET){
int rev = 0;
if(sockreadable(f->sock))
rev |= POLLIN;
// A stream socket is writable whenever it has a peer. The buffer can
// still be full, in which case the write blocks -- same latitude poll()
// has always had, and the same answer a pipe gives.
if(sock_hup(f->sock))
rev |= POLLHUP;
else
rev |= POLLOUT;
return rev & (events | POLLERR | POLLHUP);
}
if(f->type == FD_INODE){
// An input device is only readable when an event is waiting. Reporting it
// always-ready -- which is what the generic answer below does -- turns a
// poll loop into a busy loop that blocks in read().
if(f->ip && f->ip->type == T_DEV && f->ip->major == INPUT_MAJOR){
int rev = 0;
if((events & POLLIN) && f->readable && input_ready(f->ip->minor))
rev |= POLLIN;
if(events & POLLOUT)
rev |= POLLOUT;
return rev;
}
if(f->ip && f->ip->type == T_DEV && f->ip->major == CONSOLE){
int rev = 0;
if((events & POLLIN) && f->readable && console_input_ready())
rev |= POLLIN;
if((events & POLLOUT) && f->writable)
rev |= POLLOUT;
return rev;
}
return events & (POLLIN | POLLOUT);
}
return POLLNVAL;
}

1839
kernel/fs/fs.c Normal file

File diff suppressed because it is too large Load diff

255
kernel/fs/log.c Normal file
View file

@ -0,0 +1,255 @@
#include "types.h"
#include "defs.h"
#include "param.h"
#include "spinlock.h"
#include "sleeplock.h"
#include "fs.h"
#include "buf.h"
// Simple logging that allows concurrent FS system calls.
//
// A log transaction contains the updates of multiple FS system
// calls. The logging system only commits when there are
// no FS system calls active. Thus there is never
// any reasoning required about whether a commit might
// write an uncommitted system call's updates to disk.
//
// A system call should call begin_op()/end_op() to mark
// its start and end. Usually begin_op() just increments
// the count of in-progress FS system calls and returns.
// But if it thinks the log is close to running out, it
// sleeps until the last outstanding end_op() commits.
//
// The log is a physical re-do log containing disk blocks.
// The on-disk log format:
// header block, containing block #s for block A, B, C, ...
// block A
// block B
// block C
// ...
// Log appends are synchronous.
// Contents of the header block, used for both the on-disk header block
// and to keep track in memory of logged block# before commit.
struct logheader {
int n;
int block[LOGSIZE];
};
struct log {
struct spinlock lock;
int start;
int size;
int outstanding; // how many FS sys calls are executing.
int committing; // in commit(), please wait.
int dev;
struct logheader lh;
};
struct log log;
static void recover_from_log(void);
static void commit();
void
initlog(int dev)
{
if (sizeof(struct logheader) >= BSIZE)
panic("initlog: too big logheader");
struct superblock sb;
initlock(&log.lock, "log");
readsb(dev, &sb);
log.start = sb.logstart;
log.size = sb.nlog;
log.dev = dev;
recover_from_log();
}
// Copy committed blocks from log to their home location
static void
install_trans(void)
{
int tail;
for (tail = 0; tail < log.lh.n; tail++) {
struct buf *lbuf = bread(log.dev, log.start+tail+1); // read log block
struct buf *dbuf = bread(log.dev, log.lh.block[tail]); // read dst
memmove(dbuf->data, lbuf->data, BSIZE); // copy block to dst
bwrite(dbuf); // write dst to disk
brelse(lbuf);
brelse(dbuf);
}
}
// Read the log header from disk into the in-memory log header
static void
read_head(void)
{
struct buf *buf = bread(log.dev, log.start);
struct logheader *lh = (struct logheader *) (buf->data);
int i;
log.lh.n = lh->n;
for (i = 0; i < log.lh.n; i++) {
log.lh.block[i] = lh->block[i];
}
brelse(buf);
}
// Write in-memory log header to disk.
// This is the true point at which the
// current transaction commits.
static void
write_head(void)
{
struct buf *buf = bread(log.dev, log.start);
struct logheader *hb = (struct logheader *) (buf->data);
int i;
hb->n = log.lh.n;
for (i = 0; i < log.lh.n; i++) {
hb->block[i] = log.lh.block[i];
}
bwrite(buf);
brelse(buf);
}
static void
recover_from_log(void)
{
read_head();
install_trans(); // if committed, copy from log to disk
log.lh.n = 0;
write_head(); // clear the log
}
// called at the start of each FS system call.
void
begin_op(void)
{
// Nothing to do: the root filesystem is SchFS, which does not use this log.
//
// This is not a shortcut. The log exists to make a multi-block v1 update
// atomic; SchFS gets that from copy-on-write instead -- every schfs_k* call
// that changes anything is one transaction and is durable by the time it
// returns. So there is nothing here for begin_op to protect.
//
// It also cannot do anything: initlog() ran inside fsinit(), which no longer
// runs at all, so the lock below and everything it guards are uninitialised.
// Taking them is where the first SchFS-root boot stopped -- after the mount,
// before init ever ran, looking for all the world like a hang.
//
// The rest of this file is dead with it and goes when the v1 format does.
// begin_op/end_op stay as no-ops meanwhile because sys_fs.c calls them in
// dozens of places, and emptying those out is a separate change.
return;
#if 0
acquire(&log.lock);
while(1){
if(log.committing){
sleep(&log, &log.lock);
} else if(log.lh.n + (log.outstanding+1)*MAXOPBLOCKS > LOGSIZE){
// this op might exhaust log space; wait for commit.
sleep(&log, &log.lock);
} else {
log.outstanding += 1;
release(&log.lock);
break;
}
}
#endif
}
// called at the end of each FS system call.
// commits if this was the last outstanding operation.
void
end_op(void)
{
return; // see begin_op()
#if 0
int do_commit = 0;
acquire(&log.lock);
log.outstanding -= 1;
if(log.committing)
panic("log.committing");
if(log.outstanding == 0){
do_commit = 1;
log.committing = 1;
} else {
// begin_op() may be waiting for log space.
wakeup(&log);
}
release(&log.lock);
if(do_commit){
// call commit w/o holding locks, since not allowed
// to sleep with locks.
commit();
acquire(&log.lock);
log.committing = 0;
wakeup(&log);
release(&log.lock);
}
#endif
}
// Copy modified blocks from cache to log.
static void
write_log(void)
{
int tail;
for (tail = 0; tail < log.lh.n; tail++) {
struct buf *to = bread(log.dev, log.start+tail+1); // log block
struct buf *from = bread(log.dev, log.lh.block[tail]); // cache block
memmove(to->data, from->data, BSIZE);
bwrite(to); // write the log
brelse(from);
brelse(to);
}
}
static void
commit()
{
if (log.lh.n > 0) {
write_log(); // Write modified blocks from cache to log
write_head(); // Write header to disk -- the real commit
install_trans(); // Now install writes to home locations
log.lh.n = 0;
write_head(); // Erase the transaction from the log
}
}
// Caller has modified b->data and is done with the buffer.
// Record the block number and pin in the cache with B_DIRTY.
// commit()/write_log() will do the disk write.
//
// log_write() replaces bwrite(); a typical use is:
// bp = bread(...)
// modify bp->data[]
// log_write(bp)
// brelse(bp)
void
log_write(struct buf *b)
{
int i;
if (log.lh.n >= LOGSIZE || log.lh.n >= log.size - 1)
panic("too big a transaction");
if (log.outstanding < 1)
panic("log_write outside of trans");
acquire(&log.lock);
for (i = 0; i < log.lh.n; i++) {
if (log.lh.block[i] == b->blockno) // log absorbtion
break;
}
log.lh.block[i] = b->blockno;
if (i == log.lh.n)
log.lh.n++;
b->flags |= B_DIRTY; // prevent eviction
release(&log.lock);
}

166
kernel/fs/pipe.c Normal file
View file

@ -0,0 +1,166 @@
#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 "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
};
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((p = (struct pipe*)kalloc()) == 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)
kfree((char*)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);
kfree((char*)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;
}

524
kernel/fs/procfs.c Normal file
View file

@ -0,0 +1,524 @@
// /proc as a real filesystem.
//
// It used to be a handful of character devices: init ran `mknod("/proc/uptime",
// 6, 2)` and friends, so every file was a real inode on the root filesystem
// with a major/minor pair behind it. That works for a fixed set of files and
// cannot work at all for the thing /proc is mostly *for* -- one directory per
// process. You cannot mknod a directory for a process that does not exist yet
// and will not exist in a minute, which is why `ps` and `top` have never
// worked: they call dirtree_flagread("/proc") and get a list with no pids in
// it.
//
// So this is a synthetic filesystem behind the same `struct fs_ops` vtable
// SchFS uses. Nothing is stored: every directory listing is generated from the
// process table at the moment it is asked for, and every file's contents are
// generated when it is read.
//
// The global files (meminfo, uptime, pci, ...) keep their existing generators
// in kernel/dev/procfs.c; this layer only decides what exists and hands the
// reads over. That split is deliberate -- the generators are about content and
// this file is about namespace, and they change for different reasons.
#include "types.h"
#include "defs.h"
#include "param.h"
#include "memlayout.h"
#include "mmu.h"
#include "proc.h"
#include "spinlock.h"
#include "sleeplock.h"
#include "fs.h"
#include "file.h"
#include "stat.h"
#include "uaccess.h"
#include "errno.h"
// ---------------------------------------------------------------------------
// Inode numbering
//
// An inode number has to survive being handed out, stored in a struct inode,
// and given back later with no other context -- so it encodes the whole
// identity of the object.
//
// 1 the root, /proc
// PROCFS_GLOBAL | id a global file, id being the PROC_* minor that
// kernel/dev/procfs.c already uses
// pid << 8 the directory /proc/<pid>
// (pid << 8) | fileid a file inside it
//
// pid is bounded by NPROC and file ids by PF_MAX, so the ranges cannot
// collide. Shifting by 8 rather than packing tightly keeps the numbers
// readable in a debugger, which matters more than density here.
// ---------------------------------------------------------------------------
// Size of the scratch buffer every generator renders into. Must stay equal to
// PROC_BUFSZ in kernel/dev/procfs.c: the SchFS and PCI generators there write
// up to PROC_BUFSZ regardless of the cap they are handed, so a smaller value
// here would be a buffer overrun rather than a truncation.
#define PROCFS_GENBUF 2048
#define PROCFS_ROOT 1ULL
#define PROCFS_GLOBAL 0x10000ULL
#define PF_DIR 0 // the directory itself
#define PF_STAT 1
#define PF_STATUS 2
#define PF_CMDLINE 3
#define PF_STATM 4
#define PF_EXE 5
#define PF_MAX 6
#define INO_PID(ino) ((int)((ino) >> 8))
#define INO_FILE(ino) ((int)((ino) & 0xFF))
#define INO_IS_GLOBAL(ino) (((ino) & PROCFS_GLOBAL) != 0)
// The global files, by the name they appear under and the PROC_* id that
// kernel/dev/procfs.c generates them from.
struct global_file { const char *name; int id; };
static const struct global_file globals[] = {
{ "meminfo", 1 },
{ "uptime", 2 },
{ "cpuinfo", 3 },
{ "version", 4 },
{ "stat", 5 },
{ "loadavg", 6 },
{ "mounts", 7 },
{ "filesystems", 8 },
{ "schfs", 9 },
{ "sysrq", 10 },
{ "pci", 11 },
{ "interrupts", 12 },
{ "devices", 13 },
};
#define NGLOBALS ((int)(sizeof(globals) / sizeof(globals[0])))
// Per-process files. `statm` and `status` are here because ps reads them; a
// missing file is not an error to it, but a present one is cheaper than the
// fallback paths it takes without them.
static const struct global_file pidfiles[] = {
{ "stat", PF_STAT },
{ "status", PF_STATUS },
{ "cmdline", PF_CMDLINE },
{ "statm", PF_STATM },
{ "exe", PF_EXE },
};
#define NPIDFILES ((int)(sizeof(pidfiles) / sizeof(pidfiles[0])))
// ---------------------------------------------------------------------------
// Content
// ---------------------------------------------------------------------------
// A tiny append-only formatter. procfs files are small and generated whole,
// so there is no reason for anything cleverer.
struct sb { char *p; int n, cap; };
static void sbputc(struct sb *s, char c)
{ if (s->n < s->cap - 1) s->p[s->n++] = c; }
static void sbputs(struct sb *s, const char *t)
{ while (*t) sbputc(s, *t++); }
static void sbputi(struct sb *s, int64 v)
{
char tmp[24];
int i = 0, neg = 0;
uint64 u;
if (v < 0) { neg = 1; u = (uint64)(-v); } else u = (uint64)v;
do { tmp[i++] = '0' + (int)(u % 10); u /= 10; } while (u);
if (neg) sbputc(s, '-');
while (i--) sbputc(s, tmp[i]);
}
// Linux's /proc/<pid>/stat, as far as anything here reads it.
//
// The field order is not negotiable: ps counts fields by position, so a
// missing one silently shifts everything after it. Fields Schunix does not
// track are zero, which is what Linux itself reports for things a given
// process has never done.
static char
state_char(int st)
{
switch (st) {
case RUNNING: return 'R';
case RUNNABLE: return 'R';
case SLEEPING: return 'S';
case ZOMBIE: return 'Z';
case EMBRYO: return 'R';
default: return '?';
}
}
static void
gen_pid_stat(struct sb *s, const struct procinfo *pi)
{
int i;
sbputi(s, pi->pid);
sbputs(s, " (");
sbputs(s, pi->name);
sbputs(s, ") ");
sbputc(s, state_char(pi->state));
sbputc(s, ' ');
sbputi(s, pi->ppid); // 4 ppid
sbputc(s, ' ');
sbputi(s, pi->pgid); // 5 pgrp
sbputc(s, ' ');
sbputi(s, pi->sid); // 6 session
sbputs(s, " 0 -1 0"); // 7 tty_nr, 8 tpgid, 9 flags
// 10..17: minflt cminflt majflt cmajflt utime stime cutime cstime.
// No per-process accounting yet, so these are honestly zero rather than
// invented -- a fabricated CPU time is worse than none, because top would
// draw a plausible graph of a number nobody measured.
for (i = 0; i < 8; i++)
sbputs(s, " 0");
sbputs(s, " 20 0 1 0 0 "); // priority nice num_threads itrealvalue starttime
sbputi(s, (int64)pi->sz); // 23 vsize, in bytes
sbputc(s, ' ');
sbputi(s, (int64)(pi->sz / PGSIZE)); // 24 rss, in pages
sbputs(s, " 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n");
}
static void
gen_pid_status(struct sb *s, const struct procinfo *pi)
{
sbputs(s, "Name:\t"); sbputs(s, pi->name); sbputc(s, '\n');
sbputs(s, "State:\t"); sbputc(s, state_char(pi->state));
sbputs(s, "\nTgid:\t"); sbputi(s, pi->pid);
sbputs(s, "\nPid:\t"); sbputi(s, pi->pid);
sbputs(s, "\nPPid:\t"); sbputi(s, pi->ppid);
sbputs(s, "\nUid:\t"); sbputi(s, pi->uid);
sbputc(s, '\t'); sbputi(s, pi->uid);
sbputc(s, '\t'); sbputi(s, pi->uid);
sbputc(s, '\t'); sbputi(s, pi->uid);
sbputs(s, "\nVmSize:\t"); sbputi(s, (int64)(pi->sz / 1024));
sbputs(s, " kB\nThreads:\t1\n");
}
static void
gen_pid_statm(struct sb *s, const struct procinfo *pi)
{
int64 pages = (int64)(pi->sz / PGSIZE);
sbputi(s, pages); sbputc(s, ' ');
sbputi(s, pages); sbputs(s, " 0 0 0 0 0\n");
}
// argv is not kept after exec, so the best honest answer is the process name.
// Linux separates arguments with NUL and ps turns those into spaces; one
// NUL-terminated string is a valid degenerate case of that.
static void
gen_pid_cmdline(struct sb *s, const struct procinfo *pi)
{
sbputs(s, pi->name);
sbputc(s, '\0');
}
// Generate the whole of one file into `buf`. Returns its length.
static int
gen_file(uint64 ino, char *buf, int cap)
{
struct sb s = { buf, 0, cap };
struct procinfo pi;
if (INO_IS_GLOBAL(ino))
return proc_gen_global((int)(ino & 0xFF), buf, cap);
if (proc_snapshot_pid(INO_PID(ino), &pi) < 0)
return 0; // it exited between lookup and read
switch (INO_FILE(ino)) {
case PF_STAT: gen_pid_stat(&s, &pi); break;
case PF_STATUS: gen_pid_status(&s, &pi); break;
case PF_CMDLINE: gen_pid_cmdline(&s, &pi); break;
case PF_STATM: gen_pid_statm(&s, &pi); break;
case PF_EXE: sbputs(&s, pi.exe[0] ? pi.exe : pi.name); break;
default: return 0;
}
return s.n;
}
// ---------------------------------------------------------------------------
// fs_ops
// ---------------------------------------------------------------------------
static void
procfs_iload(struct inode *ip)
{
uint64 ino = ip->inum;
ip->valid = 1;
ip->nlink = 1;
ip->major = ip->minor = 0;
// Read-only, with one exception: /proc/sysrq is the deliberate write target
// (`echo c > /proc/sysrq` to crash on purpose). Marking everything 2 made
// filewrite() refuse it with "permission denied", which the USB suite
// caught immediately -- three of its checks drive the disk through sysrq.
ip->rwmode = (INO_IS_GLOBAL(ino) && (ino & 0xFF) == 10) ? 3 : 2;
ip->showmode = 1;
ip->atime = ip->mtime = ip->ctime = 0;
if (ino == PROCFS_ROOT || (!INO_IS_GLOBAL(ino) && INO_FILE(ino) == PF_DIR)) {
ip->type = T_DIR;
ip->size = 0;
ip->nlink = 2;
return;
}
ip->type = T_FILE;
// Size is reported as zero.
//
// A synthetic file has no length until it is generated, and generating it
// here -- on every stat(), for every process -- to fill in a number almost
// nobody reads would be absurd. Linux does exactly the same thing, which is
// why `ls -l /proc` shows a column of zeroes, and why every tool that reads
// /proc reads until EOF instead of trusting st_size.
ip->size = 0;
}
static void procfs_istore(struct inode *ip) { (void)ip; }
static void procfs_ifree(struct inode *ip) { (void)ip; }
static void procfs_truncate(struct inode *ip, uint64 n) { (void)ip; (void)n; }
static int
procfs_read(struct inode *ip, int user_dst, uint64 dst, uint64 off, uint n)
{
char *buf;
int len, r;
if (ip->type == T_DIR)
return -EISDIR;
// The 2048-byte scratch used to be a local. KSTACKSIZE is 4096, so that was
// half the kernel stack in one array, on a path already several frames deep
// -- a generator with any locals of its own, or one more frame below it,
// runs off the end. /proc/schfs is what finally did it: the canary at the
// bottom of the stack came back zeroed. Nothing about SchFS caused this; it
// only pushed an existing overrun over the edge.
//
// A whole page for a 2 KB buffer is not wasteful here -- kalloc's unit is a
// page either way, and this is not a hot path.
if ((buf = kalloc()) == 0)
return -ENOMEM;
len = gen_file(ip->inum, buf, PROCFS_GENBUF);
if (off >= (uint64)len) {
kfree(buf);
return 0;
}
if (off + n > (uint64)len)
n = (uint)((uint64)len - off);
r = (int)n;
if (user_dst) {
if (copy_to_user(dst, buf + off, n) < 0)
r = -EFAULT;
} else {
memmove((char *)dst, buf + off, n);
}
kfree(buf);
return r;
}
static int
procfs_write(struct inode *ip, int user_src, uint64 src, uint64 off, uint n)
{
// Only /proc/sysrq accepts writes, and its handler lives with the other
// generators. Everything else is read-only, and says so.
if (INO_IS_GLOBAL(ip->inum) && (ip->inum & 0xFF) == 10)
return proc_write_sysrq(user_src, src, n);
(void)off;
return -EROFS;
}
// Decimal string -> pid, or -1 if it is not all digits. This is also what
// makes a name like "meminfo" fall through to the global table.
static int
name_to_pid(const char *name)
{
int v = 0;
if (!name || !*name)
return -1;
while (*name) {
if (*name < '0' || *name > '9')
return -1;
v = v * 10 + (*name++ - '0');
if (v > 1 << 20)
return -1;
}
return v;
}
static struct inode *
procfs_dirlookup(struct inode *dp, char *name, uint *poff)
{
struct procinfo pi;
int i, pid;
if (poff)
*poff = 0;
if (namecmp(name, ".") == 0)
return iget(dp->dev, dp->inum);
if (namecmp(name, "..") == 0)
return iget(dp->dev, PROCFS_ROOT); // /proc/<pid>/.. is /proc
// Inside /proc/<pid>/
if (dp->inum != PROCFS_ROOT) {
for (i = 0; i < NPIDFILES; i++)
if (namecmp(name, (char *)pidfiles[i].name) == 0)
return iget(dp->dev, (dp->inum & ~0xFFULL) | pidfiles[i].id);
return 0;
}
// In /proc itself: a number is a process, a word is a global file.
pid = name_to_pid(name);
if (pid > 0) {
if (proc_snapshot_pid(pid, &pi) < 0)
return 0;
return iget(dp->dev, ((uint64)pid << 8) | PF_DIR);
}
for (i = 0; i < NGLOBALS; i++)
if (namecmp(name, (char *)globals[i].name) == 0)
return iget(dp->dev, PROCFS_GLOBAL | globals[i].id);
return 0;
}
// Nothing in /proc can be created, removed or linked. Saying so with an error
// is better than a silent no-op: a program that thinks it wrote a file and
// finds it missing later has a much harder problem to debug.
static int procfs_dirlink(struct inode *dp, char *name, uint64 inum)
{ (void)dp; (void)name; (void)inum; return -EROFS; }
static int procfs_dirunlink(struct inode *dp, char *name, uint off)
{ (void)dp; (void)name; (void)off; return -EROFS; }
static int procfs_dirempty(struct inode *dp) { (void)dp; return 0; }
static struct inode *procfs_ialloc(uint dev, short type)
{ (void)dev; (void)type; return 0; }
// One getdents64 record.
struct linux_dirent64 {
uint64 d_ino;
int64 d_off;
ushort d_reclen;
uchar d_type;
char d_name[];
};
#define DT_DIR_ 4
#define DT_REG_ 8
static int
emit(char *out, int cap, int used, uint64 ino, int type, const char *name)
{
struct linux_dirent64 *de;
int nlen = 0, reclen;
while (name[nlen]) nlen++;
reclen = (int)sizeof(*de) + nlen + 1;
reclen = (reclen + 7) & ~7; // 8-byte aligned, as getdents64 requires
if (used + reclen > cap)
return -1;
de = (struct linux_dirent64 *)(out + used);
de->d_ino = ino;
de->d_off = used + reclen;
de->d_reclen = (ushort)reclen;
de->d_type = (uchar)type;
memmove(de->d_name, name, nlen);
de->d_name[nlen] = 0;
return reclen;
}
static void
utoa(uint v, char *buf)
{
char t[12];
int i = 0, j = 0;
do { t[i++] = '0' + (v % 10); v /= 10; } while (v);
while (i) buf[j++] = t[--i];
buf[j] = 0;
}
// The listing is generated fresh each time, and *off is an index into it.
//
// A process that exits between two getdents calls therefore shifts everything
// after it up by one, and one entry can be missed. That is a real race and it
// is the same one Linux has -- /proc has no stable ordering to offer, and
// anything that wants a consistent snapshot has to re-read.
static int
procfs_getdents(struct inode *dp, char *out, int cap, uint64 *off)
{
struct procinfo pi;
char name[16];
int used = 0, idx = 0, i, r;
uint64 want = *off;
#define EMIT(ino, type, nm) do { \
if (idx++ >= (int)want) { \
r = emit(out, cap, used, (ino), (type), (nm)); \
if (r < 0) goto full; \
used += r; (*off)++; \
} \
} while (0)
if (dp->inum == PROCFS_ROOT) {
EMIT(PROCFS_ROOT, DT_DIR_, ".");
EMIT(PROCFS_ROOT, DT_DIR_, "..");
for (i = 0; i < NGLOBALS; i++)
EMIT(PROCFS_GLOBAL | globals[i].id, DT_REG_, globals[i].name);
for (i = 0; proc_snapshot(i, &pi) == 0; i++) {
utoa((uint)pi.pid, name);
EMIT(((uint64)pi.pid << 8) | PF_DIR, DT_DIR_, name);
}
} else {
EMIT(dp->inum, DT_DIR_, ".");
EMIT(PROCFS_ROOT, DT_DIR_, "..");
for (i = 0; i < NPIDFILES; i++)
EMIT((dp->inum & ~0xFFULL) | pidfiles[i].id, DT_REG_, pidfiles[i].name);
}
full:
#undef EMIT
return used;
}
const struct fs_ops procfs_ops = {
.name = "procfs",
.iload = procfs_iload,
.istore = procfs_istore,
.ifree = procfs_ifree,
.read = procfs_read,
.write = procfs_write,
.truncate = procfs_truncate,
.dirlookup = procfs_dirlookup,
.dirlink = procfs_dirlink,
.dirunlink = procfs_dirunlink,
.dirempty = procfs_dirempty,
.ialloc = procfs_ialloc,
.getdents = procfs_getdents,
};
// Graft this filesystem over /proc.
//
// Called by init once it has made the directory. The mknod'd files that used
// to live there are simply covered: path resolution stops at the mount point,
// so they are unreachable and harmless. Removing them from init is a separate,
// later cleanup -- doing both at once would make a boot failure ambiguous
// between "the mount is wrong" and "the nodes are missing".
void
procfs_mount(void)
{
struct inode *dp = namei("/proc");
if (dp == 0) {
cprintf("procfs: /proc does not exist; not mounted\n");
return;
}
if (mount_add(dp->dev, dp->inum, PROCFS_DEV, PROCFS_ROOT) < 0)
cprintf("procfs: /proc already has something mounted on it\n");
iput(dp);
}

1181
kernel/fs/schfs/btree.c Normal file

File diff suppressed because it is too large Load diff

215
kernel/fs/schfs/btree.h Normal file
View file

@ -0,0 +1,215 @@
// SchFS copy-on-write B-tree.
//
// This is the load-bearing piece of the whole filesystem: inodes, directory
// entries, extent maps and indexes are all just records in this one tree, so a
// subtle bug here is a bug in everything. It is therefore written as portable C
// against an abstract block device, and exercised on the host by
// tools/schfs_test.c before it is ever run inside the kernel.
//
// Copy-on-write means no block that is part of the *committed* tree is ever
// modified. An update copies the path from the leaf back up to the root, and
// the caller then publishes the new root. A crash therefore lands on either the
// old tree or the new one, never a mixture — which is why there is no journal
// and no fsck.
//
// "Committed" is the operative word: a block first written during the
// transaction group now being built is not reachable from any published root,
// so it may be rewritten in place. See schfs_btree.generation.
#ifndef SCHFS_BTREE_H
#define SCHFS_BTREE_H
#include "schfs.h"
// Block device the tree lives on. The kernel supplies one backed by the buffer
// cache; the test harness supplies one backed by a plain file or by memory.
struct schfs_bdev {
void *ctx;
int (*bread)(void *ctx, uint64_t blk, void *buf);
int (*bwrite)(void *ctx, uint64_t blk, const void *buf);
// Allocate a block for copy-on-write. Must never return a block that is
// reachable from the last committed root.
uint64_t (*balloc)(void *ctx);
// Release a block that the new tree no longer references. Freeing may only
// take effect at commit, since the old tree must stay readable until then.
void (*bfree)(void *ctx, uint64_t blk);
// Optional: allocate n physically contiguous blocks, returning the first,
// or 0 if that cannot be satisfied. This is what lets a file become one
// extent rather than one per block, and it has to be a separate entry point
// because an allocator with a free list will happily return scattered
// blocks from balloc. NULL means unsupported; callers then fall back to
// allocating singly and accept the extra extent records.
uint64_t (*balloc_run)(void *ctx, uint64_t n);
};
struct schfs_btree {
struct schfs_bdev *dev;
uint64_t root; // block number of the current root
uint32_t root_crc;
uint64_t root_gen; // generation stamped in the root block itself
// The transaction group being built. Every block written by a mutation is
// stamped with this, and a block already carrying it is rewritten in place
// rather than copied — it cannot be part of a committed tree. The volume
// layer advances it on every commit; nothing else may touch it.
uint64_t generation;
};
// ---------------------------------------------------------------------------
// Scratch buffers
//
// Nothing in this file may put a block on the stack. A SchFS block is 4096
// bytes and so is the entire kernel stack (KSTACKSIZE), so even one such local
// overflows it — the kstack canary caught exactly that on the first boot of the
// read path. Mutations need three blocks live at once (the node being edited,
// the sibling a split produces, and a repacking buffer), so callers hand in
// three and the tree borrows them.
//
// The three must not alias, and their contents do not survive across calls.
// ---------------------------------------------------------------------------
struct schfs_ctx {
void *a; // node currently being edited
void *b; // sibling produced by a split
void *c; // repacking scratch for leaves
};
// ---------------------------------------------------------------------------
// Cursor
//
// The read path is driven through a cursor for the same reason: a cursor holds
// exactly one block buffer, supplied by the caller, and remembers the path as
// block numbers, re-reading a parent on the way back up. Walking a tree of any
// depth therefore costs one page rather than one page per level.
// ---------------------------------------------------------------------------
struct schfs_path_ent {
uint64_t blocknr;
uint32_t crc;
int index; // internal: child descended into; leaf: item
};
struct schfs_cursor {
struct schfs_btree *t;
void *blk; // caller-owned, SCHFS_BLKSIZE bytes
int depth; // path[depth] is the leaf
int valid; // 0 once the walk has run off either end
struct schfs_path_ent path[SCHFS_MAX_LEVEL + 1];
};
// `blk` must point at SCHFS_BLKSIZE bytes that outlive the cursor.
void schfs_cursor_init(struct schfs_cursor *c, struct schfs_btree *t, void *blk);
// Position at the first key >= start, or at the very first key if start is
// NULL. Returns 0 if positioned, -1 if the tree holds no such key, -2 on
// corruption.
int schfs_cursor_seek(struct schfs_cursor *c, const struct schfs_key *start);
// Step. Return 0, -1 at the end of the tree, -2 on corruption.
int schfs_cursor_next(struct schfs_cursor *c);
int schfs_cursor_prev(struct schfs_cursor *c);
// Current record. Both pointers are invalidated by the next seek/next/prev.
// Return NULL when the cursor is not positioned.
const struct schfs_key *schfs_cursor_key(struct schfs_cursor *c);
const void *schfs_cursor_val(struct schfs_cursor *c, unsigned *len);
// Ordering over keys: (objectid, type, offset). Returns <0, 0, >0.
int schfs_key_cmp(const struct schfs_key *a, const struct schfs_key *b);
uint32_t schfs_crc32c(uint32_t crc, const void *buf, unsigned len);
// ---------------------------------------------------------------------------
// Node accessors
//
// Plain views over the on-disk layout in schfs.h. Exposed because the space
// manager walks the tree by block number rather than by key, and a second,
// slightly different copy of these three lines is exactly the sort of thing
// that ends up disagreeing with the first.
// ---------------------------------------------------------------------------
#define SCHFS_HDR(b) ((struct schfs_node_hdr *)(b))
#define SCHFS_KPTRS(b) ((struct schfs_kptr *) \
((char *)(b) + sizeof(struct schfs_node_hdr)))
#define SCHFS_ITEMS(b) ((struct schfs_item *) \
((char *)(b) + sizeof(struct schfs_node_hdr)))
// Read one tree block into `out` and validate it completely: magic, the block's
// self-pointer, its checksum, and — when want_crc is non-zero — that the
// checksum is the one its parent recorded. Returns 0 or -2.
int schfs_node_read(struct schfs_bdev *dev, uint64_t blk, uint32_t want_crc,
void *out);
// Create an empty tree (a single empty leaf) and set t->root.
int schfs_btree_create(struct schfs_btree *t, struct schfs_bdev *dev,
struct schfs_ctx *x);
// Look a key up. On success copies at most *len bytes of the value into buf and
// stores the true size in *len. Returns 0 on success, -1 if absent, -2 on a
// corrupt or unreadable block. Uses x->a as its block buffer.
int schfs_btree_lookup(struct schfs_btree *t, struct schfs_ctx *x,
const struct schfs_key *k, void *buf, unsigned *len);
// Same, through a cursor the caller already has positioned somewhere else.
int schfs_btree_lookup_cur(struct schfs_cursor *c, const struct schfs_key *k,
void *buf, unsigned *len);
// Greatest key <= k, which is how an extent covering a file offset is found:
// extents are keyed by the file offset they start at, so the record governing
// byte N is the last one at or before (ino, EXTENT, N). Leaves the cursor
// positioned on it. Returns 0, -1 if every key in the tree is greater, -2 on
// corruption.
int schfs_btree_lookup_le(struct schfs_cursor *c, const struct schfs_key *k);
// Insert or replace. Copies the path, so t->root changes on success.
// Returns 0, -1 if the value is too large to store inline, -2 on I/O or
// corruption, -3 if the device is out of blocks.
int schfs_btree_insert(struct schfs_btree *t, struct schfs_ctx *x,
const struct schfs_key *k, const void *val, unsigned len);
// Remove a key. Emptied nodes are freed and the root is collapsed when it drops
// to a single child, so a tree that is emptied returns to one block.
// Returns 0, -1 if absent, -2 on corruption, -3 out of space.
int schfs_btree_delete(struct schfs_btree *t, struct schfs_ctx *x,
const struct schfs_key *k);
// Walk records from `start` (inclusive) onward. This is what a directory read
// uses: every entry of directory D is a key in [(D,DIRENT,0), (D,DIRENT,~0)],
// so listing a directory costs the size of that directory rather than the size
// of the filesystem. fn returns non-zero to stop.
//
// WARNING: the `val` pointer handed to fn points *into the cursor's block
// buffer*. Any other tree operation the callback performs may overwrite it —
// including one on a different cursor, if the two were given the same buffer.
// A callback that looks anything up must copy what it needs first. The key is
// already copied out for you; the value is not, because it can be a kilobyte
// and most callbacks consume it immediately.
int schfs_btree_range(struct schfs_cursor *c, const struct schfs_key *start,
int (*fn)(const struct schfs_key *k,
const void *val, unsigned len, void *arg),
void *arg);
// ---------------------------------------------------------------------------
// Host-only facilities
//
// These recurse and keep a block per level, which is fine in a tool and fatal
// in the kernel. Compiling them out of the kernel makes that a link error
// rather than a comment nobody reads.
// ---------------------------------------------------------------------------
#ifdef SCHFS_HOSTED
// Walk every record in key order, calling fn for each. A non-zero return from
// fn stops the walk and is passed back.
int schfs_btree_iterate(struct schfs_btree *t,
int (*fn)(const struct schfs_key *k,
const void *val, unsigned len, void *arg),
void *arg);
// Full structural check: magic, self-pointer, checksum, level agreement, key
// ordering within and across nodes, that values lie inside their block, and
// that every child's first key is exactly the key its parent files it under.
// Returns 0 if the tree is sound, non-zero on the first problem found.
int schfs_btree_check(struct schfs_btree *t);
#endif /* SCHFS_HOSTED */
#endif /* SCHFS_BTREE_H */

623
kernel/fs/schfs/kmount.c Normal file
View file

@ -0,0 +1,623 @@
// SPDX-License-Identifier: Apache-2.0
//
// SchFS in the kernel: block device, mount, and the transaction bracket.
//
// The filesystem code itself (btree.c, space.c, vol.c, obj.c, objw.c) is the
// same portable C the host tools compile, so nothing here re-implements the
// format. This file is only the three things that cannot be shared: a block
// device backed by the buffer cache, the boot-time mount, and the rule about
// when a group of changes becomes durable.
//
// The running root filesystem is still the v1 code in kernel/fs/fs.c, so a
// SchFS volume that fails to mount costs nothing — the system boots exactly as
// before and says so on the console.
//
// ---------------------------------------------------------------------------
// Transactions
//
// Every entry point below that changes anything is one transaction group: it
// takes the volume lock, does its work, and commits before returning. That
// makes an operation durable the moment it returns, and — because a commit is a
// single uberblock write that either lands or does not — indivisible against a
// crash. There is no journal to replay and nothing to undo, because none of the
// work is reachable until that one block is written.
//
// The cost is one uberblock write per operation. Batching several operations
// into a group would amortise it, and the structure here is deliberately shaped
// so that becomes a change to where commit is called rather than a redesign;
// but a filesystem that loses the last few operations on a crash is a much
// worse default than a slow one.
#include "types.h"
#include "defs.h"
#include "param.h"
#include "spinlock.h"
#include "sleeplock.h"
#include "fs.h"
#include "buf.h"
#include "schfs_int.h"
// A SchFS block is 4096 bytes and a buffer-cache block is BSIZE (1024), so each
// filesystem block spans SCHFS_BPB consecutive buffers. Keeping the buffer
// cache at its own block size avoids disturbing the v1 filesystem, the log and
// the IDE driver, all of which assume BSIZE.
#define SCHFS_BPB (SCHFS_BLKSIZE / BSIZE)
struct schfs_kdev {
uint dev;
};
static struct schfs_kdev kdev;
static struct schfs_vol kvol;
static int kmounted;
// Block buffers, allocated once at mount.
//
// Nothing in SchFS may put a block on the stack: a cursor needs a whole
// SCHFS_BLKSIZE buffer and the kernel stack is one 4096-byte page (KSTACKSIZE),
// so a single such local overflows it. kalloc returns exactly one page, which
// is exactly one SchFS block, so each of these fits with nothing to spare.
//
// kcurblk and kdatablk serve the read paths; kctx* are the three the volume
// keeps for mutations. They are separate pages because a write path runs a
// cursor of its own inside the volume's buffers while the caller's cursor is
// still positioned in kcurblk.
static char *kcurblk;
static char *kdatablk;
static char *kctxa, *kctxb, *kctxc;
// Those buffers are shared by every caller, so every entry point below holds
// this lock for the whole operation. A sleeplock rather than a spinlock because
// bread() sleeps waiting on the disk.
//
// It also serialises transactions, which is not incidental: a group being built
// rewrites its own blocks in place, so two operations interleaving inside one
// group would each see the other's half-finished tree.
static struct sleeplock schfs_lock;
// Seconds, for inode timestamps. The kernel has no wall clock wired up, so this
// matches what fs.c does for the v1 filesystem rather than inventing a second,
// differently-wrong answer.
static uint64_t
know(void)
{
return (uint64_t)uptime();
}
// Start and finish one transaction. `changed` says whether anything was
// written, so a read does not pay for an uberblock write.
static void
ktx_begin(void)
{
acquiresleep(&schfs_lock);
}
static int
ktx_end(int changed, int r)
{
if (changed && r >= 0) {
int c = schfs_vol_commit(&kvol);
if (c != 0)
r = c;
}
releasesleep(&schfs_lock);
return r;
}
static int
kd_read(void *ctx, uint64_t blk, void *out)
{
struct schfs_kdev *d = ctx;
if (blk == 0)
return -1;
// The buffer cache addresses blocks with a uint, which caps the volume long
// before the on-disk format does. Refusing out here beats truncating and
// reading some unrelated sector, which would surface as corruption.
if (blk > 0xffffffffULL / SCHFS_BPB)
return -1;
for (int i = 0; i < SCHFS_BPB; i++) {
struct buf *b = bread(d->dev, (uint)(blk * SCHFS_BPB + i));
memmove((char *)out + i * BSIZE, b->data, BSIZE);
brelse(b);
}
return 0;
}
static int
kd_write(void *ctx, uint64_t blk, const void *in)
{
struct schfs_kdev *d = ctx;
if (blk == 0)
return -1;
if (blk > 0xffffffffULL / SCHFS_BPB)
return -1;
// Straight through the buffer cache, with no log. The xv6 log exists to make
// multi-block updates atomic; copy-on-write makes them atomic by never
// modifying a block the committed tree can reach, so logging every write
// would double the I/O to re-solve a problem the format has already solved.
//
// bread before the overwrite because the cache has no "give me this block
// without reading it" entry point. It costs a read the first time a block is
// touched and nothing afterwards.
for (int i = 0; i < SCHFS_BPB; i++) {
struct buf *b = bread(d->dev, (uint)(blk * SCHFS_BPB + i));
memmove(b->data, (const char *)in + i * BSIZE, BSIZE);
bwrite(b);
brelse(b);
}
return 0;
}
// Allocation goes through the volume's space manager. These take no ctx of
// their own because a schfs_bdev has one ctx shared by every hook and the block
// I/O above needs it for the device number.
static uint64_t
kd_alloc(void *ctx)
{
(void)ctx;
return schfs_vol_alloc(&kvol);
}
static uint64_t
kd_alloc_run(void *ctx, uint64_t n)
{
(void)ctx;
return schfs_vol_alloc_run(&kvol, n);
}
static void
kd_free(void *ctx, uint64_t b)
{
(void)ctx;
schfs_vol_free(&kvol, b);
}
// Pages for the allocation bitmap: one per 32768 blocks of volume, taken once
// at mount and never given back.
static void *
kd_palloc(void)
{
return kalloc();
}
// Try to mount a SchFS volume from `dev`. Never panics: a disk that is not
// SchFS, or is an unreadable SchFS, simply leaves the volume unmounted.
void
schfs_kmount(uint dev)
{
if (kmounted)
return;
initsleeplock(&schfs_lock, "schfs");
if (kcurblk == 0) kcurblk = kalloc();
if (kdatablk == 0) kdatablk = kalloc();
if (kctxa == 0) kctxa = kalloc();
if (kctxb == 0) kctxb = kalloc();
if (kctxc == 0) kctxc = kalloc();
if (kcurblk == 0 || kdatablk == 0 ||
kctxa == 0 || kctxb == 0 || kctxc == 0) {
cprintf("schfs: no memory for block buffers\n");
return;
}
kdev.dev = dev;
struct schfs_bdev bd;
bd.ctx = &kdev;
bd.bread = kd_read;
bd.bwrite = kd_write;
bd.balloc = kd_alloc;
bd.bfree = kd_free;
bd.balloc_run = kd_alloc_run;
struct schfs_ctx cx;
cx.a = kctxa;
cx.b = kctxb;
cx.c = kctxc;
// The uberblock scan doubles as the probe: no valid uberblock means this is
// simply not a SchFS volume, which is not an error worth reporting loudly.
// Opening with a page allocator also builds the allocation bitmap, by walking
// the tree — the only mount-time cost SchFS has, and what buys it a mount
// that cannot disagree with the filesystem it just adopted.
int r = schfs_vol_open(&kvol, &bd, &cx, kd_palloc);
if (r == -1)
return;
if (r == -4) {
cprintf("schfs: device %d is larger than %d GB and cannot be mapped\n",
dev, (int)(SCHFS_MAX_BLOCKS * SCHFS_BLKSIZE / (1024 * 1024 * 1024)));
return;
}
if (r == -5) {
cprintf("schfs: no memory for the allocation map of device %d\n", dev);
return;
}
if (r != 0) {
cprintf("schfs: device %d has a SchFS uberblock but an unreadable tree\n",
dev);
return;
}
kmounted = 1;
cprintf("schfs: mounted '%s' on device %d, %d MB, generation %d\n",
kvol.label, dev,
(int)(kvol.total_blocks * SCHFS_BLKSIZE / (1024 * 1024)),
(int)kvol.generation);
}
int
schfs_kmounted(void)
{
return kmounted;
}
// Resolve a path on the mounted volume. Returns the object id, or 0.
uint64_t
schfs_klookup(const char *path)
{
if (!kmounted)
return 0;
acquiresleep(&schfs_lock);
struct schfs_cursor c;
schfs_cursor_init(&c, &kvol.tree, kcurblk);
uint64_t r = schfs_path_lookup(&kvol, &c, path);
releasesleep(&schfs_lock);
return r;
}
int
schfs_kstat(uint64_t ino, struct schfs_inode *out)
{
if (!kmounted)
return -1;
acquiresleep(&schfs_lock);
struct schfs_cursor c;
schfs_cursor_init(&c, &kvol.tree, kcurblk);
int r = schfs_inode_get(&kvol, &c, ino, out);
releasesleep(&schfs_lock);
return r;
}
long
schfs_kread(uint64_t ino, uint64_t off, void *buf, unsigned n)
{
if (!kmounted)
return -1;
acquiresleep(&schfs_lock);
struct schfs_cursor c;
schfs_cursor_init(&c, &kvol.tree, kcurblk);
long r = schfs_file_read(&kvol, &c, ino, off, buf, n, kdatablk);
releasesleep(&schfs_lock);
return r;
}
int
schfs_kreaddir(uint64_t dir,
int (*fn)(uint64_t, uint16_t, const char *, unsigned, void *),
void *arg)
{
if (!kmounted)
return -1;
// NOTE: fn must not call back into schfs_k* -- the lock is not recursive and
// the cursor buffer is shared. Callers that need per-entry detail collect the
// entries first, then look them up after the walk.
acquiresleep(&schfs_lock);
struct schfs_cursor c;
schfs_cursor_init(&c, &kvol.tree, kcurblk);
int r = schfs_dir_iterate(&kvol, &c, dir, fn, arg);
releasesleep(&schfs_lock);
return r;
}
struct schfs_vol *
schfs_kvol(void)
{
return kmounted ? &kvol : 0;
}
/* ------------------------------------------------------------------ */
/* Write side */
/* */
/* Each of these is one transaction: everything it does is invisible */
/* until it commits, and a crash part way through leaves the volume at */
/* the state before the call. That is the entire failure story — there */
/* is no partial state to detect and nothing to roll back. */
/* ------------------------------------------------------------------ */
int
schfs_kcreate(uint64_t dir, const char *name, unsigned namelen, uint16_t type,
uint32_t mode, uint16_t major, uint16_t minor, uint64_t *out)
{
if (!kmounted || kvol.readonly)
return -1;
ktx_begin();
int r = schfs_create(&kvol, dir, name, namelen, type, mode, major, minor,
know(), out);
return ktx_end(1, r);
}
long
schfs_kwrite(uint64_t ino, uint64_t off, const void *buf, unsigned n)
{
if (!kmounted || kvol.readonly)
return -1;
ktx_begin();
long r = schfs_file_write(&kvol, ino, off, buf, n, kdatablk);
// ktx_end speaks in error codes, and a write speaks in byte counts. Passing
// the count straight through told it the transaction had failed with status
// 17, which it duly reported -- and a short write is a panic one layer up.
int e = ktx_end(1, r < 0 ? (int)r : 0);
return e == 0 ? r : (long)e;
}
int
schfs_ktruncate(uint64_t ino, uint64_t size)
{
if (!kmounted || kvol.readonly)
return -1;
ktx_begin();
int r = schfs_file_truncate(&kvol, ino, size, kdatablk);
return ktx_end(1, r);
}
int
schfs_kunlink(uint64_t dir, const char *name, unsigned namelen)
{
if (!kmounted || kvol.readonly)
return -1;
ktx_begin();
int r = schfs_unlink(&kvol, dir, name, namelen);
return ktx_end(1, r);
}
int
schfs_klink(uint64_t dir, const char *name, unsigned namelen, uint64_t ino)
{
if (!kmounted || kvol.readonly)
return -1;
ktx_begin();
int r = schfs_link(&kvol, dir, name, namelen, ino);
return ktx_end(1, r);
}
int
schfs_krename(uint64_t olddir, const char *oldname, unsigned oldlen,
uint64_t newdir, const char *newname, unsigned newlen)
{
if (!kmounted || kvol.readonly)
return -1;
ktx_begin();
int r = schfs_rename(&kvol, olddir, oldname, oldlen,
newdir, newname, newlen);
return ktx_end(1, r);
}
// Write back an inode record the caller has modified — the one way to change
// mode, ownership or timestamps. The caller reads it with schfs_kstat, edits
// the struct, and hands it back.
int
schfs_ksetattr(uint64_t ino, const struct schfs_inode *in)
{
if (!kmounted || kvol.readonly)
return -1;
ktx_begin();
int r = schfs_inode_put(&kvol, ino, in);
return ktx_end(1, r);
}
// Flush without changing anything. Nothing needs this yet — every operation
// above commits already — but sync(2) has to mean something.
int
schfs_ksync(void)
{
if (!kmounted || kvol.readonly)
return 0;
ktx_begin();
return ktx_end(1, 0);
}
/* ------------------------------------------------------------------ */
/* Pieces the VFS needs */
/* */
/* The VFS keeps its own link count and decides when an object dies, */
/* so it needs the directory entry and the object as separate */
/* operations rather than the combined create/unlink above. Two layers */
/* counting links independently is exactly how a filesystem loses a */
/* file that something still has open. */
/* ------------------------------------------------------------------ */
// Resolve one name inside a directory. 0 if there is no such entry.
uint64_t
schfs_klookup_at(uint64_t dir, const char *name, unsigned namelen)
{
if (!kmounted)
return 0;
acquiresleep(&schfs_lock);
struct schfs_cursor c;
schfs_cursor_init(&c, &kvol.tree, kcurblk);
uint64_t ino = 0;
if (schfs_dir_lookup(&kvol, &c, dir, name, namelen, &ino, 0) != 0)
ino = 0;
releasesleep(&schfs_lock);
return ino;
}
// 1 if the directory holds no entries, 0 if it does, -1 on error.
int
schfs_kdirempty(uint64_t dir)
{
if (!kmounted)
return -1;
acquiresleep(&schfs_lock);
int r = schfs_dir_empty(&kvol, dir);
releasesleep(&schfs_lock);
return r == -2 ? -1 : r;
}
// An object with no name and no links yet. Nothing can reach it until the
// caller links it, and nothing reaches the disk until the transaction commits,
// so a caller that gives up in between leaves nothing behind.
int
schfs_kalloc_object(uint16_t type, uint32_t mode, uint64_t *out)
{
if (!kmounted || kvol.readonly)
return -1;
ktx_begin();
uint64_t ino = schfs_alloc_objectid(&kvol);
struct schfs_inode in;
memset(&in, 0, sizeof(in));
in.type = type;
in.mode = mode;
in.nlink = 0;
in.atime = in.mtime = in.ctime = know();
in.gen = kvol.tree.generation;
int r = schfs_inode_put(&kvol, ino, &in);
if (r == 0 && out)
*out = ino;
return ktx_end(1, r);
}
int
schfs_kdiradd(uint64_t dir, const char *name, unsigned namelen, uint64_t ino,
uint16_t type)
{
if (!kmounted || kvol.readonly)
return -1;
ktx_begin();
int r = schfs_dir_add(&kvol, dir, name, namelen, ino, type);
return ktx_end(1, r);
}
// Remove one directory entry, leaving the object alone. The VFS decrements the
// link count itself and calls schfs_kfree when it reaches zero.
int
schfs_kdirdel(uint64_t dir, const char *name, unsigned namelen)
{
if (!kmounted || kvol.readonly)
return -1;
ktx_begin();
int r = schfs_dir_del(&kvol, dir, name, namelen);
return ktx_end(1, r);
}
// Release an object's data, checksums and inode record. Called from iput once
// the last name is gone and nothing has it open.
int
schfs_kfree(uint64_t ino)
{
if (!kmounted || kvol.readonly)
return -1;
ktx_begin();
int r = schfs_object_free(&kvol, ino);
return ktx_end(1, r);
}
uint64_t
schfs_kroot(void)
{
return kmounted ? kvol.root_objectid : 0;
}
/* ------------------------------------------------------------------ */
/* /proc/schfs */
/* */
/* Phase 2 has no VFS integration -- SchFS files are not reachable */
/* through open(2) yet -- so this is how the mount is observable from */
/* userland at all. It reports the volume and lists the root directory, */
/* which is enough to confirm from inside a running Schunix that the */
/* kernel really did parse an image mkschfs wrote. */
/* ------------------------------------------------------------------ */
static void
kp_puts(char *buf, int cap, int *len, const char *s)
{
while (*s && *len < cap - 1)
buf[(*len)++] = *s++;
}
static void
kp_putu(char *buf, int cap, int *len, uint64 v)
{
char tmp[24];
int i = 0;
if (v == 0) { if (*len < cap - 1) buf[(*len)++] = '0'; return; }
while (v && i < (int)sizeof(tmp)) { tmp[i++] = '0' + (v % 10); v /= 10; }
while (i--) { if (*len < cap - 1) buf[(*len)++] = tmp[i]; }
}
struct kp_ctx {
char *buf;
int cap;
int *len;
int count;
};
static int
kp_entry(uint64_t ino, uint16_t type, const char *name, unsigned namelen,
void *arg)
{
struct kp_ctx *c = arg;
c->count++;
// Only the first handful, and no per-entry stat: schfs_kreaddir holds the
// lock across the walk, so calling schfs_kstat from here would deadlock.
if (c->count > 16)
return 0;
kp_puts(c->buf, c->cap, c->len, " ");
kp_puts(c->buf, c->cap, c->len,
type == SCHFS_T_DIR ? "dir " :
type == SCHFS_T_SYMLINK ? "symlink " :
type == SCHFS_T_DEV ? "dev " : "file ");
for (unsigned i = 0; i < namelen && *c->len < c->cap - 1; i++)
c->buf[(*c->len)++] = name[i];
kp_puts(c->buf, c->cap, c->len, " #");
kp_putu(c->buf, c->cap, c->len, ino);
kp_puts(c->buf, c->cap, c->len, "\n");
return 0;
}
int
schfs_kinfo(char *buf, int cap)
{
int len = 0;
if (!kmounted) {
kp_puts(buf, cap, &len, "no SchFS volume mounted\n");
return len;
}
kp_puts(buf, cap, &len, "label: ");
kp_puts(buf, cap, &len, kvol.label);
kp_puts(buf, cap, &len, "\nblocksize: ");
kp_putu(buf, cap, &len, SCHFS_BLKSIZE);
kp_puts(buf, cap, &len, "\ntotal_blocks: ");
kp_putu(buf, cap, &len, kvol.total_blocks);
kp_puts(buf, cap, &len, "\nfree_blocks: ");
kp_putu(buf, cap, &len, kvol.free_blocks);
kp_puts(buf, cap, &len, "\ngeneration: ");
kp_putu(buf, cap, &len, kvol.generation);
kp_puts(buf, cap, &len, "\ntree_root: ");
kp_putu(buf, cap, &len, kvol.tree.root);
kp_puts(buf, cap, &len, "\nroot_objectid: ");
kp_putu(buf, cap, &len, kvol.root_objectid);
kp_puts(buf, cap, &len, "\n\nroot directory:\n");
struct kp_ctx c = { buf, cap, &len, 0 };
if (schfs_kreaddir(kvol.root_objectid, kp_entry, &c) != 0) {
kp_puts(buf, cap, &len, " (directory read failed)\n");
return len;
}
if (c.count > 16) {
kp_puts(buf, cap, &len, " ... ");
kp_putu(buf, cap, &len, (uint64)(c.count - 16));
kp_puts(buf, cap, &len, " more\n");
}
return len;
}

441
kernel/fs/schfs/obj.c Normal file
View file

@ -0,0 +1,441 @@
// SPDX-License-Identifier: Apache-2.0
//
// SchFS object layer, read side: inodes, directories and file data.
//
// Everything here is a record in the one B-tree, which is the point of the
// btrfs-style design — there is no inode table, no directory block format and
// no indirect-block scheme to get wrong, only key types:
//
// (ino, INODE, 0) struct schfs_inode
// (dir, DIRENT, hash) packed list of schfs_dirent_rec + name
// (ino, EXTENT, fileblk) struct schfs_extent
// (CSUM, CSUM, diskblk) uint32 CRC32c of that data block
//
// Kernel code reaches all of this through a caller-supplied cursor, so no
// function here puts a 4096-byte block on the stack.
#include "schfs_int.h"
#ifdef SCHFS_HOSTED
#include <string.h>
#else
#include "types.h"
#include "defs.h"
#endif
// Point a cursor at the volume's current tree, keeping the caller's block
// buffer. Called before every operation rather than once at open, because the
// tree root moves on every commit under copy-on-write: a cursor that cached the
// old root would keep reading a stale — though perfectly valid — filesystem.
static void
cur_bind(struct schfs_cursor *c, struct schfs_vol *v)
{
c->t = &v->tree;
c->depth = 0;
c->valid = 0;
}
void
schfs_mkkey(struct schfs_key *k, uint64_t objectid, uint8_t type,
uint64_t offset)
{
memset(k, 0, sizeof(*k));
k->objectid = objectid;
k->type = type;
k->offset = offset;
}
// FNV-1a, 64-bit. Directory lookup is a tree descent on this value, so it wants
// to spread short, highly similar names ("ls", "ln", "cp") across the keyspace.
// FNV does that, is a dozen lines, and needs no table — worth more here than
// the marginally better distribution of something larger.
//
// Collisions are handled by the packed dirent list rather than assumed away;
// see struct schfs_dirent_rec.
// Test-only: narrows the hash so collisions become common.
//
// With a full 64-bit hash, two names in one directory colliding is so unlikely
// that the packed-list code which handles it would never run — and untested
// code that only executes on rare inputs is exactly the code that loses a file
// years later. Setting this to a small mask makes every directory a pile of
// collisions, so the round-trip test exercises the path properly. Always ~0
// in the kernel and in real images.
uint64_t schfs_hash_mask = ~0ULL;
uint64_t
schfs_namehash(const char *name, unsigned len)
{
uint64_t h = 1469598103934665603ULL; /* FNV offset basis */
for (unsigned i = 0; i < len; i++) {
h ^= (uint8_t)name[i];
h *= 1099511628211ULL; /* FNV prime */
}
// Key ordering is by this value, so a directory's entries come back in an
// order that has nothing to do with their names. That is fine for readdir
// (POSIX promises no order) and is what makes lookup one descent.
return h & schfs_hash_mask;
}
/* ------------------------------------------------------------------ */
/* Inodes */
/* ------------------------------------------------------------------ */
int
schfs_inode_get(struct schfs_vol *v, struct schfs_cursor *c,
uint64_t ino, struct schfs_inode *out)
{
struct schfs_key k;
schfs_mkkey(&k, ino, SCHFS_KEY_INODE, 0);
cur_bind(c, v);
unsigned len = sizeof(*out);
int r = schfs_btree_lookup_cur(c, &k, out, &len);
if (r != 0)
return r;
// A short record means an image written by an older or broken tool. Reject
// it rather than acting on a half-filled struct.
if (len != sizeof(*out))
return -2;
return 0;
}
/* ------------------------------------------------------------------ */
/* Directories */
/* ------------------------------------------------------------------ */
// Walk a packed dirent list, calling fn per entry. Stops early if fn returns
// non-zero. Returns fn's value, or -2 if the list is malformed.
static int
dirent_walk(const void *val, unsigned len,
int (*fn)(const struct schfs_dirent_rec *, const char *, void *),
void *arg)
{
unsigned off = 0;
while (off < len) {
if (len - off < sizeof(struct schfs_dirent_rec))
return -2;
const struct schfs_dirent_rec *d =
(const struct schfs_dirent_rec *)((const char *)val + off);
unsigned step = SCHFS_DIRENT_LEN(d->namelen);
// Bounds-check before touching the name: a corrupt namelen would
// otherwise read past the end of the block.
if (d->namelen == 0 || d->namelen > SCHFS_NAME_MAX || off + step > len)
return -2;
int r = fn(d, (const char *)d + sizeof(*d), arg);
if (r)
return r;
off += step;
}
return 0;
}
struct find_ctx {
const char *name;
unsigned namelen;
uint64_t ino;
uint16_t type;
};
static int
find_one(const struct schfs_dirent_rec *d, const char *nm, void *arg)
{
struct find_ctx *f = (struct find_ctx *)arg;
if (d->namelen != f->namelen)
return 0;
if (memcmp(nm, f->name, f->namelen) != 0)
return 0;
f->ino = d->objectid;
f->type = d->type;
return 1; /* found; stop */
}
int
schfs_dir_lookup(struct schfs_vol *v, struct schfs_cursor *c,
uint64_t dir, const char *name, unsigned namelen,
uint64_t *out_ino, uint16_t *out_type)
{
if (namelen == 0 || namelen > SCHFS_NAME_MAX)
return -1;
struct schfs_key k;
schfs_mkkey(&k, dir, SCHFS_KEY_DIRENT, schfs_namehash(name, namelen));
cur_bind(c, v);
int r = schfs_cursor_seek(c, &k);
if (r == -2)
return -2;
if (r != 0)
return -1;
const struct schfs_key *got = schfs_cursor_key(c);
if (!got || schfs_key_cmp(got, &k) != 0)
return -1;
unsigned len;
const void *val = schfs_cursor_val(c, &len);
if (!val)
return -2;
struct find_ctx f;
f.name = name; f.namelen = namelen; f.ino = 0; f.type = 0;
r = dirent_walk(val, len, find_one, &f);
if (r == -2)
return -2;
if (r != 1)
return -1; /* hash matched, name did not */
if (out_ino) *out_ino = f.ino;
if (out_type) *out_type = f.type;
return 0;
}
struct iter_ctx {
int (*fn)(uint64_t, uint16_t, const char *, unsigned, void *);
void *arg;
struct schfs_cursor *cur;
uint64_t dir;
int stop;
};
// Hand each entry of one packed dirent list to the caller.
//
// The list lives in the cursor's block buffer, and almost every readdir caller
// immediately looks each entry up — to stat it, to recurse into it — through a
// cursor that may well share that buffer. Walking the list in place meant the
// name and every record after it turned to garbage the moment the callback
// touched the filesystem, which is how an extracted tree once ended up with
// directories reparented under their own children.
//
// The obvious fix, copying the whole item aside, costs SCHFS_MAX_INLINE (just
// over 1 KB) of stack — a quarter of the kernel's entire 4096-byte stack, on a
// path that also runs under procread's 2 KB buffer. So only the current entry
// is copied, and the item is re-fetched from the cursor each time round:
// schfs_cursor_val re-reads the block, which restores whatever the callback
// disturbed.
static int
iter_item(const struct schfs_key *k, const void *val, unsigned len, void *arg)
{
struct iter_ctx *ic = (struct iter_ctx *)arg;
(void)val;
// The range scan runs until something stops it, so stopping at the first
// key that is no longer this directory's is what bounds the walk.
if (k->objectid != ic->dir || k->type != SCHFS_KEY_DIRENT)
return 1;
if (len > SCHFS_MAX_INLINE)
return 1;
unsigned off = 0;
while (off < len) {
unsigned now;
const char *v = (const char *)schfs_cursor_val(ic->cur, &now);
if (!v || now != len) {
ic->stop = -2;
return 1;
}
if (len - off < sizeof(struct schfs_dirent_rec)) {
ic->stop = -2;
return 1;
}
const struct schfs_dirent_rec *d =
(const struct schfs_dirent_rec *)(v + off);
unsigned namelen = d->namelen;
unsigned step = SCHFS_DIRENT_LEN(namelen);
// Bounds-check before touching the name: a corrupt namelen would
// otherwise read past the end of the block.
if (namelen == 0 || namelen > SCHFS_NAME_MAX || off + step > len) {
ic->stop = -2;
return 1;
}
char name[SCHFS_NAME_MAX];
uint64_t oid = d->objectid;
uint16_t type = d->type;
memmove(name, (const char *)d + sizeof(*d), namelen);
int r = ic->fn(oid, type, name, namelen, ic->arg);
if (r) {
ic->stop = r;
return 1;
}
off += step;
}
return 0;
}
int
schfs_dir_iterate(struct schfs_vol *v, struct schfs_cursor *c, uint64_t dir,
int (*fn)(uint64_t, uint16_t, const char *, unsigned, void *),
void *arg)
{
struct schfs_key start;
schfs_mkkey(&start, dir, SCHFS_KEY_DIRENT, 0);
struct iter_ctx ic;
ic.fn = fn; ic.arg = arg; ic.cur = c; ic.dir = dir; ic.stop = 0;
cur_bind(c, v);
int r = schfs_btree_range(c, &start, iter_item, &ic);
if (r == -2)
return -2;
return ic.stop == 1 ? 0 : ic.stop;
}
/* ------------------------------------------------------------------ */
/* File data */
/* ------------------------------------------------------------------ */
// Map a file block to a disk block. Returns 1 if mapped, 0 for a hole, -2 on
// corruption. Extents are keyed by the file block they start at, so the record
// governing block B is the last one at or before (ino, EXTENT, B).
static int
extent_map(struct schfs_vol *v, struct schfs_cursor *c, uint64_t ino,
uint64_t fblk, uint64_t *out)
{
struct schfs_key k;
schfs_mkkey(&k, ino, SCHFS_KEY_EXTENT, fblk);
cur_bind(c, v);
int r = schfs_btree_lookup_le(c, &k);
if (r == -2)
return -2;
if (r != 0)
return 0;
const struct schfs_key *got = schfs_cursor_key(c);
if (!got)
return 0;
// lookup_le can land on some other object's records entirely; a sparse file
// whose first extent starts past this block looks the same way.
if (got->objectid != ino || got->type != SCHFS_KEY_EXTENT)
return 0;
unsigned len;
const struct schfs_extent *e =
(const struct schfs_extent *)schfs_cursor_val(c, &len);
if (!e || len != sizeof(*e))
return -2;
if (fblk >= got->offset + e->nblocks)
return 0; /* hole past the end of this extent */
*out = e->disk_block + (fblk - got->offset);
return 1;
}
// Verify a data block against its checksum record. Tree nodes carry their
// checksum in the parent pointer; data blocks have no parent, so the check must
// be an explicit lookup. Returns 0 if good, -2 if it fails, and 0 as well when
// no record exists — an image may legitimately predate checksums for a block,
// and refusing to read data over a *missing* checksum would turn an upgrade
// into a total loss.
static int
csum_check(struct schfs_vol *v, struct schfs_cursor *c, uint64_t dblk,
const void *data)
{
struct schfs_key k;
schfs_mkkey(&k, SCHFS_CSUM_OBJECTID, SCHFS_KEY_CSUM, dblk);
uint32_t want;
unsigned len = sizeof(want);
cur_bind(c, v);
int r = schfs_btree_lookup_cur(c, &k, &want, &len);
if (r == -1)
return 0;
if (r != 0 || len != sizeof(want))
return -2;
if (schfs_crc32c(0, data, SCHFS_BLKSIZE) != want)
return -2;
return 0;
}
long
schfs_file_read(struct schfs_vol *v, struct schfs_cursor *c, uint64_t ino,
uint64_t off, void *buf, unsigned n, void *dblk)
{
struct schfs_inode in;
int r = schfs_inode_get(v, c, ino, &in);
if (r != 0)
return r;
if (off >= in.size)
return 0;
if (off + n > in.size)
n = (unsigned)(in.size - off);
char *dst = (char *)buf;
unsigned done = 0;
while (done < n) {
uint64_t fblk = (off + done) / SCHFS_BLKSIZE;
unsigned inblk = (unsigned)((off + done) % SCHFS_BLKSIZE);
unsigned want = SCHFS_BLKSIZE - inblk;
if (want > n - done)
want = n - done;
uint64_t disk = 0;
int m = extent_map(v, c, ino, fblk, &disk);
if (m == -2)
return -2;
if (m == 0) {
memset(dst + done, 0, want); /* hole reads as zeros */
} else {
if (v->dev.bread(v->dev.ctx, disk, dblk) != 0)
return -2;
if (csum_check(v, c, disk, dblk) != 0)
return -2;
memmove(dst + done, (char *)dblk + inblk, want);
}
done += want;
}
return (long)done;
}
/* ------------------------------------------------------------------ */
/* Path resolution */
/* ------------------------------------------------------------------ */
uint64_t
schfs_path_lookup(struct schfs_vol *v, struct schfs_cursor *c, const char *path)
{
uint64_t cur = v->root_objectid;
while (*path == '/')
path++;
while (*path) {
const char *s = path;
while (*path && *path != '/')
path++;
unsigned len = (unsigned)(path - s);
while (*path == '/')
path++;
if (len == 1 && s[0] == '.')
continue;
if (len == 2 && s[0] == '.' && s[1] == '.') {
// ".." is synthetic: directories store a parent id rather than an
// entry, so walking up is an inode read, not a lookup.
struct schfs_inode in;
if (schfs_inode_get(v, c, cur, &in) != 0)
return 0;
cur = in.parent ? in.parent : v->root_objectid;
continue;
}
// Every component but the last must be a directory. Checking here is
// what makes "cat /etc/passwd/x" fail rather than silently succeed.
struct schfs_inode in;
if (schfs_inode_get(v, c, cur, &in) != 0)
return 0;
if (in.type != SCHFS_T_DIR)
return 0;
uint64_t next;
if (schfs_dir_lookup(v, c, cur, s, len, &next, 0) != 0)
return 0;
cur = next;
}
return cur;
}

932
kernel/fs/schfs/objw.c Normal file
View file

@ -0,0 +1,932 @@
// SPDX-License-Identifier: Apache-2.0
//
// SchFS object layer, write side: create, link, unlink, rename, write,
// truncate.
//
// The same portable C as the read side, compiled both hosted (mkschfs) and
// freestanding (the kernel mount), so an image the tools produce and one the
// kernel produces come from a single implementation rather than two that agree
// by luck. Everything below is expressed as records in the one B-tree; there is
// no inode table, no directory block format and no bitmap to keep in step.
//
// Two rules run through the whole file.
//
// Nothing here may put a block on the stack. A SchFS block is 4096 bytes and so
// is the entire kernel stack, so every function borrows the volume's three
// scratch buffers (v->ctx) instead. Where a cursor is needed, ctx.a doubles as
// its buffer — which is safe only because no path below holds a cursor across a
// tree mutation, and every one of them copies what it needs out first.
//
// Nothing reachable from a committed root is ever overwritten. For tree blocks
// the B-tree enforces that; for *data* blocks it is enforced here, by
// schfs_extent.gen. A run allocated during the group being built may be written
// in place, because no published root can see it. Anything older is copied to a
// fresh block and the extent record split around it.
#include "schfs_int.h"
#ifdef SCHFS_HOSTED
#include <string.h>
#else
#include "types.h"
#include "defs.h"
#endif
/* ------------------------------------------------------------------ */
/* Inodes */
/* ------------------------------------------------------------------ */
uint64_t
schfs_alloc_objectid(struct schfs_vol *v)
{
return v->next_objectid++;
}
int
schfs_inode_put(struct schfs_vol *v, uint64_t ino, const struct schfs_inode *in)
{
struct schfs_key k;
schfs_mkkey(&k, ino, SCHFS_KEY_INODE, 0);
return schfs_btree_insert(&v->tree, &v->ctx, &k, in, sizeof(*in));
}
// Read an inode using the volume's own scratch block. Every mutation below is a
// read-modify-write of one of these, and each one that took a cursor buffer
// from its caller was one more place to get the aliasing rules wrong.
static int
inode_load(struct schfs_vol *v, uint64_t ino, struct schfs_inode *out)
{
struct schfs_cursor c;
schfs_cursor_init(&c, &v->tree, v->ctx.a);
return schfs_inode_get(v, &c, ino, out);
}
/* ------------------------------------------------------------------ */
/* Directory entries */
/* */
/* One key per name hash, holding a packed list of records, because two */
/* names in a directory can hash the same and the second insert would */
/* otherwise silently replace the first. */
/* ------------------------------------------------------------------ */
// Load the packed list for `name`'s hash into v->dirbuf. Returns its length, or
// 0 if there is no such key, or a negative error.
static int
dirlist_load(struct schfs_vol *v, uint64_t dir, const char *name,
unsigned namelen, struct schfs_key *k)
{
schfs_mkkey(k, dir, SCHFS_KEY_DIRENT, schfs_namehash(name, namelen));
unsigned len = sizeof(v->dirbuf);
int r = schfs_btree_lookup(&v->tree, &v->ctx, k, v->dirbuf, &len);
if (r == -2)
return -2;
if (r != 0)
return 0;
if (len > sizeof(v->dirbuf))
return -2; /* stored value larger than can exist */
return (int)len;
}
// Offset of `name` within the packed list, or -1 if absent, -2 if malformed.
static int
dirlist_find(const char *list, unsigned len, const char *name, unsigned namelen)
{
for (unsigned off = 0; off + sizeof(struct schfs_dirent_rec) <= len; ) {
const struct schfs_dirent_rec *d =
(const struct schfs_dirent_rec *)(list + off);
unsigned step = SCHFS_DIRENT_LEN(d->namelen);
if (step == 0 || off + step > len)
return -2;
if (d->namelen == namelen &&
memcmp(list + off + sizeof(*d), name, namelen) == 0)
return (int)off;
off += step;
}
return -1;
}
int
schfs_dir_add(struct schfs_vol *v, uint64_t dir, const char *name,
unsigned namelen, uint64_t child, uint16_t type)
{
if (namelen == 0 || namelen > SCHFS_NAME_MAX)
return -1;
struct schfs_key k;
int got = dirlist_load(v, dir, name, namelen, &k);
if (got < 0)
return got;
unsigned len = (unsigned)got;
// Reject a duplicate outright. Without this, adding a name twice produces a
// directory with two entries that lookup can never tell apart.
int at = dirlist_find(v->dirbuf, len, name, namelen);
if (at == -2)
return -2;
if (at >= 0)
return -1;
unsigned add = SCHFS_DIRENT_LEN(namelen);
if (len + add > sizeof(v->dirbuf))
return -4; /* collision list will not fit in one item */
struct schfs_dirent_rec *d = (struct schfs_dirent_rec *)(v->dirbuf + len);
memset(d, 0, add);
d->objectid = child;
d->type = type;
d->namelen = (uint16_t)namelen;
memmove(v->dirbuf + len + sizeof(*d), name, namelen);
return schfs_btree_insert(&v->tree, &v->ctx, &k, v->dirbuf, len + add);
}
int
schfs_dir_del(struct schfs_vol *v, uint64_t dir, const char *name,
unsigned namelen)
{
if (namelen == 0 || namelen > SCHFS_NAME_MAX)
return -1;
struct schfs_key k;
int got = dirlist_load(v, dir, name, namelen, &k);
if (got < 0)
return got;
unsigned len = (unsigned)got;
if (len == 0)
return -1;
int at = dirlist_find(v->dirbuf, len, name, namelen);
if (at == -2)
return -2;
if (at < 0)
return -1;
const struct schfs_dirent_rec *d =
(const struct schfs_dirent_rec *)(v->dirbuf + at);
unsigned step = SCHFS_DIRENT_LEN(d->namelen);
memmove(v->dirbuf + at, v->dirbuf + at + step, len - (unsigned)at - step);
len -= step;
// An emptied list means the key itself goes, not a zero-length value: a key
// that exists with nothing in it would make the directory scan stop on a
// record readdir cannot describe.
if (len == 0)
return schfs_btree_delete(&v->tree, &v->ctx, &k);
return schfs_btree_insert(&v->tree, &v->ctx, &k, v->dirbuf, len);
}
int
schfs_dir_empty(struct schfs_vol *v, uint64_t dir)
{
struct schfs_key start;
schfs_mkkey(&start, dir, SCHFS_KEY_DIRENT, 0);
struct schfs_cursor c;
schfs_cursor_init(&c, &v->tree, v->ctx.a);
int r = schfs_cursor_seek(&c, &start);
if (r == -2)
return -2;
if (r != 0)
return 1; /* nothing at or after: empty */
const struct schfs_key *got = schfs_cursor_key(&c);
if (!got)
return 1;
return (got->objectid == dir && got->type == SCHFS_KEY_DIRENT) ? 0 : 1;
}
/* ------------------------------------------------------------------ */
/* Data-block checksums */
/* */
/* Tree nodes carry their checksum in their parent's pointer. Data */
/* blocks have no parent, so theirs live under one reserved object id, */
/* exactly as btrfs keeps a checksum tree. */
/* ------------------------------------------------------------------ */
static int
csum_put(struct schfs_vol *v, uint64_t dblk, const void *data)
{
struct schfs_key k;
schfs_mkkey(&k, SCHFS_CSUM_OBJECTID, SCHFS_KEY_CSUM, dblk);
uint32_t c = schfs_crc32c(0, data, SCHFS_BLKSIZE);
return schfs_btree_insert(&v->tree, &v->ctx, &k, &c, sizeof(c));
}
static int
csum_del(struct schfs_vol *v, uint64_t dblk)
{
struct schfs_key k;
schfs_mkkey(&k, SCHFS_CSUM_OBJECTID, SCHFS_KEY_CSUM, dblk);
int r = schfs_btree_delete(&v->tree, &v->ctx, &k);
return r == -1 ? 0 : r; /* absent is not a failure */
}
/* ------------------------------------------------------------------ */
/* Extents */
/* ------------------------------------------------------------------ */
static int
extent_put(struct schfs_vol *v, uint64_t ino, uint64_t fblk,
const struct schfs_extent *e)
{
struct schfs_key k;
schfs_mkkey(&k, ino, SCHFS_KEY_EXTENT, fblk);
return schfs_btree_insert(&v->tree, &v->ctx, &k, e, sizeof(*e));
}
static int
extent_del(struct schfs_vol *v, uint64_t ino, uint64_t fblk)
{
struct schfs_key k;
schfs_mkkey(&k, ino, SCHFS_KEY_EXTENT, fblk);
int r = schfs_btree_delete(&v->tree, &v->ctx, &k);
return r == -1 ? 0 : r;
}
// The extent covering file block `fblk`, if any. Extents are keyed by the file
// block they start at, so the record governing block B is the last one at or
// before (ino, EXTENT, B). Returns 1 and fills *start and *e, 0 for a hole, -2
// on corruption.
static int
extent_find(struct schfs_vol *v, uint64_t ino, uint64_t fblk,
uint64_t *start, struct schfs_extent *e)
{
struct schfs_cursor c;
schfs_cursor_init(&c, &v->tree, v->ctx.a);
struct schfs_key probe;
schfs_mkkey(&probe, ino, SCHFS_KEY_EXTENT, fblk);
int r = schfs_btree_lookup_le(&c, &probe);
if (r == -2)
return -2;
if (r != 0)
return 0;
const struct schfs_key *got = schfs_cursor_key(&c);
unsigned vlen;
const struct schfs_extent *found =
(const struct schfs_extent *)schfs_cursor_val(&c, &vlen);
if (!got || !found)
return 0;
// lookup_le happily lands on some other object's records; so does a sparse
// file whose first extent starts past this block.
if (got->objectid != ino || got->type != SCHFS_KEY_EXTENT)
return 0;
if (vlen != sizeof(*e))
return -2;
if (fblk >= got->offset + found->nblocks)
return 0;
*start = got->offset;
*e = *found;
return 1;
}
// The last extent record belonging to `ino`, or 0 if it has none.
static int
extent_last(struct schfs_vol *v, uint64_t ino, uint64_t *start,
struct schfs_extent *e)
{
struct schfs_cursor c;
schfs_cursor_init(&c, &v->tree, v->ctx.a);
struct schfs_key probe;
schfs_mkkey(&probe, ino, SCHFS_KEY_EXTENT, ~0ULL);
int r = schfs_btree_lookup_le(&c, &probe);
if (r == -2)
return -2;
if (r != 0)
return 0;
const struct schfs_key *got = schfs_cursor_key(&c);
unsigned vlen;
const struct schfs_extent *found =
(const struct schfs_extent *)schfs_cursor_val(&c, &vlen);
if (!got || !found)
return 0;
if (got->objectid != ino || got->type != SCHFS_KEY_EXTENT)
return 0;
if (vlen != sizeof(*e))
return -2;
*start = got->offset;
*e = *found;
return 1;
}
// Record a newly allocated single block, merging it into the preceding extent
// when the two are adjacent both in the file and on the disk.
//
// This is what keeps extents worth having. Sequential writes get consecutive
// blocks from the allocator, so without merging a one-megabyte file carries 256
// extent records instead of one, and the tree ends up mostly describing where
// the data is rather than holding it.
static int
extent_grow(struct schfs_vol *v, uint64_t ino, uint64_t fblk,
const struct schfs_extent *e)
{
if (fblk > 0) {
uint64_t pstart;
struct schfs_extent p;
int m = extent_find(v, ino, fblk - 1, &pstart, &p);
if (m == -2)
return -2;
if (m == 1 &&
pstart + p.nblocks == fblk &&
p.disk_block + p.nblocks == e->disk_block &&
p.refs == e->refs && p.gen == e->gen) {
p.nblocks++;
return extent_put(v, ino, pstart, &p);
}
}
return extent_put(v, ino, fblk, e);
}
// Release a run of data blocks and the checksum records that describe them.
// Shared runs are left alone: another file still points at them, and proper
// refcounting needs an extent tree that nothing creates yet.
static int
extent_release(struct schfs_vol *v, const struct schfs_extent *e,
uint64_t first, uint64_t n)
{
if (e->refs != 1)
return 0;
for (uint64_t i = 0; i < n; i++) {
int r = csum_del(v, first + i);
if (r != 0)
return r;
v->dev.bfree(v->dev.ctx, first + i);
}
return 0;
}
// Drop everything from file block `from` onward. Used by truncate and by the
// deletion of an object.
static int
extents_drop_from(struct schfs_vol *v, uint64_t ino, struct schfs_inode *in,
uint64_t from)
{
for (;;) {
uint64_t start;
struct schfs_extent e;
int m = extent_last(v, ino, &start, &e);
if (m == -2)
return -2;
if (m == 0)
return 0; /* no extents left */
if (start + e.nblocks <= from)
return 0; /* nothing beyond the cut */
if (start >= from) {
int r = extent_release(v, &e, e.disk_block, e.nblocks);
if (r != 0)
return r;
r = extent_del(v, ino, start);
if (r != 0)
return r;
if (in->blocks >= e.nblocks)
in->blocks -= e.nblocks;
continue; /* there may be earlier ones too */
}
// The cut falls inside this run: keep its head, release its tail.
uint64_t keep = from - start;
uint64_t tail = e.nblocks - keep;
int r = extent_release(v, &e, e.disk_block + keep, tail);
if (r != 0)
return r;
e.nblocks = keep;
r = extent_put(v, ino, start, &e);
if (r != 0)
return r;
if (in->blocks >= tail)
in->blocks -= tail;
return 0;
}
}
/* ------------------------------------------------------------------ */
/* File data */
/* ------------------------------------------------------------------ */
int
schfs_file_reserve(struct schfs_vol *v, uint64_t ino, uint64_t nblocks)
{
if (nblocks == 0)
return 0;
// Take the whole run before touching the tree.
//
// This is what makes extents worth having when the final size is already
// known. Allocating a data block, then inserting its extent record -- which
// itself allocates blocks for the copy-on-write path -- then allocating the
// next data block interleaves metadata with data, so consecutive file
// blocks are never physically adjacent. Grabbing the run up front keeps it
// contiguous and a whole file becomes one record.
uint64_t first;
if (v->dev.balloc_run) {
first = v->dev.balloc_run(v->dev.ctx, nblocks);
if (first == 0)
return -3;
} else {
first = v->dev.balloc(v->dev.ctx);
if (first == 0)
return -3;
uint64_t prev = first;
for (uint64_t i = 1; i < nblocks; i++) {
uint64_t b = v->dev.balloc(v->dev.ctx);
if (b == 0)
return -3;
if (b != prev + 1)
return -2; /* allocator is not handing out a run */
prev = b;
}
}
struct schfs_extent e;
memset(&e, 0, sizeof(e));
e.disk_block = first;
e.nblocks = nblocks;
e.refs = 1;
e.gen = v->tree.generation;
// Deliberately does not touch the inode. Re-reading and rewriting it here
// costs two extra tree inserts per file, and every insert copies its whole
// path. The caller knows the block count before it writes the inode the
// first time, so it sets in.blocks there instead.
return extent_put(v, ino, 0, &e);
}
// Where the next write to file block `fblk` should land.
struct wblk {
uint64_t disk; // block to write
uint64_t src; // block holding the current contents, 0 for a hole
int fresh; // a block that did not exist before
};
static int
block_for_write(struct schfs_vol *v, uint64_t ino, uint64_t fblk,
struct wblk *w)
{
uint64_t start;
struct schfs_extent e;
int m = extent_find(v, ino, fblk, &start, &e);
if (m == -2)
return -2;
if (m == 0) {
uint64_t d = v->dev.balloc(v->dev.ctx);
if (d == 0)
return -3;
struct schfs_extent ne;
memset(&ne, 0, sizeof(ne));
ne.disk_block = d;
ne.nblocks = 1;
ne.refs = 1;
ne.gen = v->tree.generation;
int r = extent_grow(v, ino, fblk, &ne);
if (r != 0)
return r;
w->disk = d;
w->src = 0;
w->fresh = 1;
return 0;
}
uint64_t old = e.disk_block + (fblk - start);
// Allocated by the group being built, and owned outright: no published root
// can reach it, so it may simply be overwritten.
if (e.gen == v->tree.generation && e.refs == 1) {
w->disk = old;
w->src = old;
w->fresh = 0;
return 0;
}
// Otherwise copy this one block out of the run, and split the record around
// it: a head covering what came before, the new block, and a tail covering
// what came after. No deletion is needed — when the block being replaced is
// the run's first, the middle record simply overwrites the original key.
uint64_t nd = v->dev.balloc(v->dev.ctx);
if (nd == 0)
return -3;
if (fblk > start) {
struct schfs_extent head = e;
head.nblocks = fblk - start;
int r = extent_put(v, ino, start, &head);
if (r != 0)
return r;
}
{
struct schfs_extent mid;
memset(&mid, 0, sizeof(mid));
mid.disk_block = nd;
mid.nblocks = 1;
mid.refs = 1;
mid.gen = v->tree.generation;
int r = extent_put(v, ino, fblk, &mid);
if (r != 0)
return r;
}
if (fblk + 1 < start + e.nblocks) {
struct schfs_extent tail = e;
tail.disk_block = e.disk_block + (fblk + 1 - start);
tail.nblocks = start + e.nblocks - (fblk + 1);
int r = extent_put(v, ino, fblk + 1, &tail);
if (r != 0)
return r;
}
// The block just replaced rejoins the free map at the next commit, not now:
// until then the committed tree still points at it and a crash lands there.
int r = extent_release(v, &e, old, 1);
if (r != 0)
return r;
w->disk = nd;
w->src = old;
w->fresh = 0;
return 0;
}
long
schfs_file_write(struct schfs_vol *v, uint64_t ino, uint64_t off,
const void *buf, unsigned n, void *dblk)
{
struct schfs_inode in;
int r = inode_load(v, ino, &in);
if (r != 0)
return r;
const char *src = (const char *)buf;
unsigned done = 0;
while (done < n) {
uint64_t at = off + done;
uint64_t fblk = at / SCHFS_BLKSIZE;
unsigned inblk = (unsigned)(at % SCHFS_BLKSIZE);
unsigned want = SCHFS_BLKSIZE - inblk;
if (want > n - done)
want = n - done;
struct wblk w;
r = block_for_write(v, ino, fblk, &w);
if (r != 0)
return r;
if (w.fresh)
in.blocks++;
// A whole-block write needs no read: every byte is about to be
// replaced. A partial one has to start from whatever is there, or from
// zeros where the file had a hole.
if (inblk != 0 || want != SCHFS_BLKSIZE) {
if (w.src) {
if (v->dev.bread(v->dev.ctx, w.src, dblk) != 0)
return -2;
} else {
memset(dblk, 0, SCHFS_BLKSIZE);
}
}
memmove((char *)dblk + inblk, src + done, want);
if (v->dev.bwrite(v->dev.ctx, w.disk, dblk) != 0)
return -2;
// The checksum is rewritten every time the block changes, not just when
// it is allocated, or a topped-up tail block fails its own check on the
// very next read.
r = csum_put(v, w.disk, dblk);
if (r != 0)
return r;
done += want;
}
if (off + n > in.size)
in.size = off + n;
in.gen = v->tree.generation;
r = schfs_inode_put(v, ino, &in);
if (r != 0)
return r;
return (long)done;
}
int
schfs_file_append(struct schfs_vol *v, uint64_t ino, const void *buf,
unsigned n, void *dblk)
{
struct schfs_inode in;
int r = inode_load(v, ino, &in);
if (r != 0)
return r == -1 ? -2 : r;
long got = schfs_file_write(v, ino, in.size, buf, n, dblk);
if (got < 0)
return (int)got;
return got == (long)n ? 0 : -2;
}
int
schfs_file_truncate(struct schfs_vol *v, uint64_t ino, uint64_t size,
void *dblk)
{
struct schfs_inode in;
int r = inode_load(v, ino, &in);
if (r != 0)
return r;
if (size < in.size) {
uint64_t keep = (size + SCHFS_BLKSIZE - 1) / SCHFS_BLKSIZE;
r = extents_drop_from(v, ino, &in, keep);
if (r != 0)
return r;
// Whole blocks past the new end are gone; the block the new size falls
// *inside* has to have its tail zeroed. It is tempting not to bother,
// since a read stops at in.size and can never see those bytes — but
// truncating down and then growing again is required to read back
// zeros, and without this it reads back whatever the file used to hold.
unsigned tail = (unsigned)(size % SCHFS_BLKSIZE);
if (tail != 0) {
uint64_t fblk = size / SCHFS_BLKSIZE;
uint64_t start;
struct schfs_extent e;
int m = extent_find(v, ino, fblk, &start, &e);
if (m == -2)
return -2;
if (m == 1) { /* a hole is already zeros */
struct wblk w;
r = block_for_write(v, ino, fblk, &w);
if (r != 0)
return r;
if (w.src) {
if (v->dev.bread(v->dev.ctx, w.src, dblk) != 0)
return -2;
} else {
memset(dblk, 0, SCHFS_BLKSIZE);
}
memset((char *)dblk + tail, 0, SCHFS_BLKSIZE - tail);
if (v->dev.bwrite(v->dev.ctx, w.disk, dblk) != 0)
return -2;
r = csum_put(v, w.disk, dblk);
if (r != 0)
return r;
if (w.fresh)
in.blocks++;
}
}
}
// Growing needs nothing at all: the range with no extent records reads back
// as zeros, which is a sparse file for free.
in.size = size;
in.gen = v->tree.generation;
return schfs_inode_put(v, ino, &in);
}
// Release an object entirely: its data, its checksums and its inode record.
int
schfs_object_free(struct schfs_vol *v, uint64_t ino)
{
struct schfs_inode in;
int r = inode_load(v, ino, &in);
if (r == -1)
return 0; /* already gone */
if (r != 0)
return r;
r = extents_drop_from(v, ino, &in, 0);
if (r != 0)
return r;
struct schfs_key k;
schfs_mkkey(&k, ino, SCHFS_KEY_INODE, 0);
r = schfs_btree_delete(&v->tree, &v->ctx, &k);
return r == -1 ? 0 : r;
}
/* ------------------------------------------------------------------ */
/* Namespace */
/* ------------------------------------------------------------------ */
// A directory's link count is 2 (itself and its own ".") plus one per
// subdirectory, because each subdirectory's ".." points back at it. Neither
// entry is stored, but the count still has to match what POSIX tools expect or
// "find -type d" prunes real directories and du walks the tree wrongly.
static int
parent_nlink_adjust(struct schfs_vol *v, uint64_t dir, int delta)
{
struct schfs_inode in;
int r = inode_load(v, dir, &in);
if (r != 0)
return r;
if (delta < 0 && in.nlink < (uint32_t)(-delta))
return -2;
in.nlink = (uint32_t)((int64_t)in.nlink + delta);
return schfs_inode_put(v, dir, &in);
}
int
schfs_create(struct schfs_vol *v, uint64_t dir, const char *name,
unsigned namelen, uint16_t type, uint32_t mode,
uint16_t rdev_major, uint16_t rdev_minor, uint64_t now,
uint64_t *out_ino)
{
if (namelen == 0 || namelen > SCHFS_NAME_MAX)
return -1;
struct schfs_inode parent;
int r = inode_load(v, dir, &parent);
if (r != 0)
return r;
if (parent.type != SCHFS_T_DIR)
return -1;
uint64_t ino = schfs_alloc_objectid(v);
struct schfs_inode in;
memset(&in, 0, sizeof(in));
in.type = type;
in.parent = dir;
in.mode = mode;
in.nlink = type == SCHFS_T_DIR ? 2 : 1;
in.rdev_major = rdev_major;
in.rdev_minor = rdev_minor;
in.atime = in.mtime = in.ctime = now;
in.gen = v->tree.generation;
r = schfs_inode_put(v, ino, &in);
if (r != 0)
return r;
// The entry goes in last, so a failure leaves an unreferenced object rather
// than a name pointing at nothing. Neither survives a crash — none of this
// is visible until the transaction commits — but an unreachable inode is
// the harmless one of the two if a later step in the same group fails.
r = schfs_dir_add(v, dir, name, namelen, ino, type);
if (r != 0)
return r;
if (type == SCHFS_T_DIR) {
r = parent_nlink_adjust(v, dir, +1);
if (r != 0)
return r;
}
if (out_ino)
*out_ino = ino;
return 0;
}
int
schfs_link(struct schfs_vol *v, uint64_t dir, const char *name,
unsigned namelen, uint64_t ino)
{
struct schfs_inode in;
int r = inode_load(v, ino, &in);
if (r != 0)
return r;
// Directories are not hard-linkable. Allowing it makes cycles possible, and
// a cycle turns every recursive walk into an infinite one.
if (in.type == SCHFS_T_DIR)
return -1;
r = schfs_dir_add(v, dir, name, namelen, ino, in.type);
if (r != 0)
return r;
in.nlink++;
in.ctime = in.mtime;
return schfs_inode_put(v, ino, &in);
}
int
schfs_unlink(struct schfs_vol *v, uint64_t dir, const char *name,
unsigned namelen)
{
uint64_t ino;
uint16_t type;
{
struct schfs_cursor c;
schfs_cursor_init(&c, &v->tree, v->ctx.a);
int r = schfs_dir_lookup(v, &c, dir, name, namelen, &ino, &type);
if (r != 0)
return r;
}
struct schfs_inode in;
int r = inode_load(v, ino, &in);
if (r != 0)
return r;
if (in.type == SCHFS_T_DIR) {
int e = schfs_dir_empty(v, ino);
if (e == -2)
return -2;
if (e == 0)
return -5; /* directory not empty */
}
r = schfs_dir_del(v, dir, name, namelen);
if (r != 0)
return r;
if (in.type == SCHFS_T_DIR) {
r = parent_nlink_adjust(v, dir, -1);
if (r != 0)
return r;
return schfs_object_free(v, ino);
}
if (in.nlink > 1) {
in.nlink--;
return schfs_inode_put(v, ino, &in);
}
return schfs_object_free(v, ino);
}
int
schfs_rename(struct schfs_vol *v, uint64_t olddir, const char *oldname,
unsigned oldlen, uint64_t newdir, const char *newname,
unsigned newlen)
{
uint64_t ino;
uint16_t type;
{
struct schfs_cursor c;
schfs_cursor_init(&c, &v->tree, v->ctx.a);
int r = schfs_dir_lookup(v, &c, olddir, oldname, oldlen, &ino, &type);
if (r != 0)
return r;
}
// Renaming a directory into its own subtree would detach that subtree from
// the root and leave it reachable only from itself. Walking up from the new
// parent is the cheap way to catch it, since every directory records its
// own parent.
if (type == SCHFS_T_DIR && newdir != olddir) {
uint64_t up = newdir;
for (int i = 0; i <= SCHFS_MAX_LEVEL * 8 && up != 0; i++) {
if (up == ino)
return -1;
struct schfs_inode t;
if (inode_load(v, up, &t) != 0)
break;
if (up == v->root_objectid)
break;
up = t.parent;
}
}
// A target that already exists is replaced, which means removing it first —
// and refusing when that would silently discard a non-empty directory.
{
struct schfs_cursor c;
schfs_cursor_init(&c, &v->tree, v->ctx.a);
uint64_t victim;
if (schfs_dir_lookup(v, &c, newdir, newname, newlen, &victim, 0) == 0) {
if (victim == ino)
return 0; /* renaming a name onto itself */
int r = schfs_unlink(v, newdir, newname, newlen);
if (r != 0)
return r;
}
}
int r = schfs_dir_add(v, newdir, newname, newlen, ino, type);
if (r != 0)
return r;
r = schfs_dir_del(v, olddir, oldname, oldlen);
if (r != 0)
return r;
if (newdir != olddir) {
if (type == SCHFS_T_DIR) {
// ".." is synthesised from the inode's parent field, so moving a
// directory means rewriting that field and moving the link its ".."
// contributes to each parent's count.
struct schfs_inode in;
r = inode_load(v, ino, &in);
if (r != 0)
return r;
in.parent = newdir;
r = schfs_inode_put(v, ino, &in);
if (r != 0)
return r;
r = parent_nlink_adjust(v, olddir, -1);
if (r != 0)
return r;
r = parent_nlink_adjust(v, newdir, +1);
if (r != 0)
return r;
}
}
return 0;
}

277
kernel/fs/schfs/schfs_int.h Normal file
View file

@ -0,0 +1,277 @@
// SPDX-License-Identifier: Apache-2.0
//
// SchFS internals: the volume (uberblock ring, block allocation) and the object
// layer (inodes, directories, file data) that sit on top of the B-tree.
//
// Split three ways so the kernel links only what it needs:
//
// vol.c open / format / commit — the uberblock ring
// obj.c read paths — kernel needs these in phase 2
// objw.c write paths — host tools only, until phase 3
//
// Everything here is portable C compiled both hosted (mkfs, fsck) and freestanding
// (kernel), so a bug is found on the host, where a test takes a second rather
// than a boot.
#ifndef SCHFS_INT_H
#define SCHFS_INT_H
#include "btree.h"
// ---------------------------------------------------------------------------
// space.c — the allocation bitmap
//
// One bit per block, held in RAM and derived from the committed tree at mount
// rather than stored on disk. See space.c for why.
//
// The bitmap is a vector of pages because the kernel allocates a page at a time
// and gives no promise that two are adjacent. That vector is fixed-size, and
// its length is therefore the hard ceiling on how large a volume this build can
// mount: SCHFS_SPACE_PAGES pages x SCHFS_SPACE_BITS_PER_PAGE blocks each.
// ---------------------------------------------------------------------------
#define SCHFS_SPACE_BITS_PER_PAGE ((uint64_t)SCHFS_BLKSIZE * 8) /* 32768 */
#define SCHFS_SPACE_PAGES 512
#define SCHFS_MAX_BLOCKS \
(SCHFS_SPACE_BITS_PER_PAGE * (uint64_t)SCHFS_SPACE_PAGES) /* 64 GiB */
struct schfs_space {
uint64_t *inuse[SCHFS_SPACE_PAGES]; // 1 = allocated
uint64_t *pend [SCHFS_SPACE_PAGES]; // 1 = freed, but not yet reusable
int npages;
uint64_t nblocks;
uint64_t free_blocks;
uint64_t pending; // bits set in pend[], counted
uint64_t hint; // rotating search start
};
// A mounted (or under-construction) volume.
//
// NOT copyable: tree.dev points at this struct's own dev field, so a copy
// leaves the tree talking to whichever struct it was built in. Open into the
// storage it will live in.
struct schfs_vol {
struct schfs_bdev dev;
struct schfs_btree tree;
// The three block buffers every mutation borrows, handed in at open or
// format and owned by the caller for the life of the volume. Nothing in
// SchFS may put a SCHFS_BLKSIZE buffer on the stack: a block is 4096 bytes
// and so is the whole kernel stack.
//
// The object layer also uses ctx.a as a cursor buffer, which is safe on the
// one condition that no cursor is still live when a tree mutation runs.
// Every write path here reads what it needs out of the cursor first.
struct schfs_ctx ctx;
struct schfs_space space;
uint64_t total_blocks;
uint64_t free_blocks;
uint64_t next_objectid;
uint64_t root_objectid;
uint64_t generation; // last *committed* transaction group
int uber_slot; // ring slot the last commit used
int readonly;
char label[SCHFS_LABEL_MAX];
uint8_t uuid[16];
// Staging for a directory's packed dirent list. Deliberately not one of the
// ctx buffers: a split reuses ctx.b and ctx.c while the value being
// inserted is still being read from, so the value needs storage of its own.
char dirbuf[SCHFS_MAX_INLINE];
};
// ---------------------------------------------------------------------------
// vol.c
// ---------------------------------------------------------------------------
// Scan the whole uberblock ring and adopt the highest generation that both
// checksums and points at a readable root. This *is* the crash recovery story:
// there is no journal to replay, because a torn update simply leaves an older
// slot as the newest valid one. Returns 0, -1 if no valid uberblock exists
// (not a SchFS volume), -2 on I/O error.
//
// `x` supplies the volume's three scratch blocks; they are kept for its whole
// lifetime, so they must outlive it and must not alias one another. `palloc`
// yields pages for the allocation bitmap and may be NULL for a read-only mount,
// which then cannot allocate at all.
int schfs_vol_open(struct schfs_vol *v, struct schfs_bdev *dev,
struct schfs_ctx *x, void *(*palloc)(void));
// Lay down an empty volume: one empty tree and one uberblock.
int schfs_vol_format(struct schfs_vol *v, struct schfs_bdev *dev,
uint64_t total_blocks, const char *label,
struct schfs_ctx *x, void *(*palloc)(void));
// Publish the current tree by writing the next slot in the ring. Until this
// returns, everything written since the last commit is invisible and a crash
// discards it — which is exactly what makes the format crash-safe.
int schfs_vol_commit(struct schfs_vol *v);
// Block allocation. Returns 0 when the volume is full.
//
// Not themselves schfs_bdev callbacks: a bdev has one ctx shared by all of its
// hooks, and the caller's block I/O needs that slot for its own state (a file
// handle, a device number). Callers wrap these in a one-line adapter instead.
uint64_t schfs_vol_alloc(struct schfs_vol *v);
uint64_t schfs_vol_alloc_run(struct schfs_vol *v, uint64_t n);
void schfs_vol_free(struct schfs_vol *v, uint64_t blk);
// ---------------------------------------------------------------------------
// space.c
// ---------------------------------------------------------------------------
// Build an empty map covering `nblocks`, with everything below
// SCHFS_FIRST_BLOCK already reserved. `palloc` yields SCHFS_BLKSIZE-sized
// pages; the map keeps them for the volume's lifetime. Returns 0, -3 if the
// volume is too small to hold anything, -4 if it is larger than this build can
// map (see SCHFS_MAX_BLOCKS), -5 if palloc ran out.
int schfs_space_init(struct schfs_space *s, uint64_t nblocks,
void *(*palloc)(void));
// Record a run as allocated. Idempotent, because an extent range may be shared
// by several files.
void schfs_space_mark(struct schfs_space *s, uint64_t blk, uint64_t n);
uint64_t schfs_space_alloc(struct schfs_space *s);
uint64_t schfs_space_alloc_run(struct schfs_space *s, uint64_t n);
// Release a block — but only for the *next* transaction group. Until the
// commit that supersedes it, the last committed tree still points here and a
// crash would land on it.
void schfs_space_free(struct schfs_space *s, uint64_t blk);
// Fold this group's deferred frees back into the free map. Called by
// schfs_vol_commit once the new uberblock is on the disk, never before.
void schfs_space_commit(struct schfs_space *s);
// Walk the committed tree and mark every block it reaches. This is what makes
// the map correct without storing it. Returns 0 or -2 on an unreadable tree.
int schfs_space_scan(struct schfs_vol *v);
// ---------------------------------------------------------------------------
// obj.c — read side
// ---------------------------------------------------------------------------
uint64_t schfs_namehash(const char *name, unsigned len);
// Test-only knob; see obj.c. Narrowing this forces directory-hash collisions
// so the packed-dirent path actually runs. Never changed outside tools/.
extern uint64_t schfs_hash_mask;
void schfs_mkkey(struct schfs_key *k, uint64_t objectid, uint8_t type,
uint64_t offset);
// Fetch an inode record. Returns 0, -1 if there is no such object, -2 corrupt.
int schfs_inode_get(struct schfs_vol *v, struct schfs_cursor *c,
uint64_t ino, struct schfs_inode *out);
// Resolve one name inside a directory. Returns 0 and fills *out_ino/*out_type,
// -1 if absent, -2 corrupt.
int schfs_dir_lookup(struct schfs_vol *v, struct schfs_cursor *c,
uint64_t dir, const char *name, unsigned namelen,
uint64_t *out_ino, uint16_t *out_type);
// Iterate a directory's real entries in hash order. "." and ".." are not
// stored and are not reported; callers that need them synthesise them.
// A non-zero return from fn stops the walk and is passed back.
int schfs_dir_iterate(struct schfs_vol *v, struct schfs_cursor *c, uint64_t dir,
int (*fn)(uint64_t ino, uint16_t type, const char *name,
unsigned namelen, void *arg),
void *arg);
// Read from a file. Short reads at end-of-file are normal; returns the byte
// count, or -1 on a missing object, -2 on corruption or a failed data checksum.
// `dblk` is a caller-owned SCHFS_BLKSIZE scratch buffer for data blocks.
long schfs_file_read(struct schfs_vol *v, struct schfs_cursor *c, uint64_t ino,
uint64_t off, void *buf, unsigned n, void *dblk);
// Walk an absolute path. Returns the object id, or 0 if any component is
// missing or is not a directory.
uint64_t schfs_path_lookup(struct schfs_vol *v, struct schfs_cursor *c,
const char *path);
// ---------------------------------------------------------------------------
// objw.c — write side
//
// Shared error codes throughout: 0 success, -1 absent or not permitted,
// -2 corruption or I/O failure, -3 out of space, -4 a directory hash-collision
// list that will not fit in one item, -5 directory not empty.
//
// None of it is visible to a reader until schfs_vol_commit publishes the tree,
// which is what lets a failure part way through a multi-record operation be
// abandoned rather than unwound.
// ---------------------------------------------------------------------------
uint64_t schfs_alloc_objectid(struct schfs_vol *v);
int schfs_inode_put(struct schfs_vol *v, uint64_t ino,
const struct schfs_inode *in);
// Add one entry, appending to the packed list if another name already hashes
// the same. -1 if the name is already present.
int schfs_dir_add(struct schfs_vol *v, uint64_t dir, const char *name,
unsigned namelen, uint64_t child, uint16_t type);
// Remove one entry. -1 if there is no such name.
int schfs_dir_del(struct schfs_vol *v, uint64_t dir, const char *name,
unsigned namelen);
// 1 if the directory holds no entries, 0 if it holds some, -2 on corruption.
int schfs_dir_empty(struct schfs_vol *v, uint64_t dir);
// Claim a contiguous run of data blocks for a file whose final size is already
// known, and record the whole run as ONE extent. Worth calling before a bulk
// write: without it, data and tree blocks interleave, and only the merging in
// schfs_file_write keeps the extent count down.
int schfs_file_reserve(struct schfs_vol *v, uint64_t ino, uint64_t nblocks);
// Write at an arbitrary offset, allocating blocks, extent records and data
// checksums, copying any block that a committed tree can still reach, and
// splitting the extent record around it. Writing past the end extends the file;
// a gap left behind stays a hole and reads back as zeros. Returns the byte
// count written, or a negative error.
long schfs_file_write(struct schfs_vol *v, uint64_t ino, uint64_t off,
const void *buf, unsigned n, void *dblk);
// Append at the current end of file. A convenience over schfs_file_write, kept
// because image builders write nothing else.
int schfs_file_append(struct schfs_vol *v, uint64_t ino, const void *buf,
unsigned n, void *dblk);
// Set a file's length, releasing whole blocks past the new end and zeroing what
// is left of the block the new end falls inside — truncating down and growing
// again has to read back zeros. Growing alone costs nothing: a range with no
// extent records is a hole. `dblk` is a caller-owned SCHFS_BLKSIZE buffer.
int schfs_file_truncate(struct schfs_vol *v, uint64_t ino, uint64_t size,
void *dblk);
// Release an object's data, checksums and inode record. The caller must already
// have removed every name pointing at it.
int schfs_object_free(struct schfs_vol *v, uint64_t ino);
// Create an object and the entry naming it. `now` is the timestamp to stamp all
// three times with — SchFS has no clock of its own.
int schfs_create(struct schfs_vol *v, uint64_t dir, const char *name,
unsigned namelen, uint16_t type, uint32_t mode,
uint16_t rdev_major, uint16_t rdev_minor, uint64_t now,
uint64_t *out_ino);
// A second name for an existing object. -1 for a directory: hard-linking one
// makes cycles possible, and a cycle turns every recursive walk infinite.
int schfs_link(struct schfs_vol *v, uint64_t dir, const char *name,
unsigned namelen, uint64_t ino);
// Remove a name, and the object with it once the last name is gone. Handles
// directories too, refusing with -5 if the directory still has entries.
int schfs_unlink(struct schfs_vol *v, uint64_t dir, const char *name,
unsigned namelen);
// Move a name, replacing the target if one exists. Refuses to move a directory
// into its own subtree, which would detach that subtree from the root.
int schfs_rename(struct schfs_vol *v, uint64_t olddir, const char *oldname,
unsigned oldlen, uint64_t newdir, const char *newname,
unsigned newlen);
#endif /* SCHFS_INT_H */

314
kernel/fs/schfs/space.c Normal file
View file

@ -0,0 +1,314 @@
// SPDX-License-Identifier: Apache-2.0
//
// SchFS space manager: which blocks are in use.
//
// There is no free-space map on disk, and that is the interesting decision
// here. The committed tree already records exactly which blocks are live —
// every tree node is named by its parent's pointer, every data block by an
// extent record — so the map is *derived* at mount by walking the tree once,
// held in RAM, and never written back.
//
// The alternative, persisting it, buys a fast mount and costs the one problem
// copy-on-write is otherwise free of: the map has to be updated atomically with
// the tree it describes, and writing the map needs blocks, which changes the
// map. ZFS spends real complexity on that recursion and btrfs resolves it with
// a fixpoint loop. Deriving the map instead cannot disagree with the filesystem
// no matter what a crash interrupts, because it is computed from it — there is
// no second source of truth to get out of step.
//
// What it costs is a metadata scan at mount, proportional to the number of tree
// blocks rather than to the size of the volume. Nothing is written, so it is
// also safe on a read-only mount. If mount time ever becomes the thing that
// hurts, the map can be cached on disk later purely as a hint, validated
// against the uberblock's generation and discarded when it does not match — an
// addition, not a format change.
//
// ---------------------------------------------------------------------------
// Deferred frees
//
// A block released during the transaction group being built is still reachable
// from the last *committed* root, which is where a crash lands. Handing it
// straight back would let the filesystem overwrite the very copy it is supposed
// to fall back to. So frees go into a second bitmap and only rejoin the free
// map when the commit that supersedes them lands. One transaction of delay, no
// more: once group N is published, group N-1's tree is no longer anybody's
// fallback.
#include "schfs_int.h"
#ifdef SCHFS_HOSTED
#include <string.h>
#else
#include "types.h"
#include "defs.h"
#endif
/* ------------------------------------------------------------------ */
/* Bit vectors, paged */
/* */
/* The bitmap has to be one flat array to be indexable, and the kernel */
/* hands out memory a page at a time with no promise that two pages */
/* are adjacent. So it is a vector of page pointers instead: one page */
/* covers SCHFS_SPACE_BITS_PER_PAGE blocks, and the page vector itself */
/* is a fixed-size array, which is what puts a hard ceiling on volume */
/* size. See docs/architecture/schfs.md for the resulting limits. */
/* ------------------------------------------------------------------ */
static int
bit_test(uint64_t *const *pages, uint64_t b)
{
uint64_t pg = b / SCHFS_SPACE_BITS_PER_PAGE;
uint64_t off = b % SCHFS_SPACE_BITS_PER_PAGE;
return (int)((pages[pg][off / 64] >> (off % 64)) & 1);
}
static void
bit_set(uint64_t *const *pages, uint64_t b)
{
uint64_t pg = b / SCHFS_SPACE_BITS_PER_PAGE;
uint64_t off = b % SCHFS_SPACE_BITS_PER_PAGE;
pages[pg][off / 64] |= 1ULL << (off % 64);
}
// The whole 64-block word containing b, so a scan can skip a fully used region
// sixty-four blocks at a time instead of one. On a nearly full volume that is
// the difference between an allocation costing microseconds and milliseconds.
static uint64_t
bit_word(uint64_t *const *pages, uint64_t b)
{
uint64_t pg = b / SCHFS_SPACE_BITS_PER_PAGE;
uint64_t off = b % SCHFS_SPACE_BITS_PER_PAGE;
return pages[pg][off / 64];
}
static int
popcount64(uint64_t v)
{
int n = 0;
while (v) { v &= v - 1; n++; }
return n;
}
/* ------------------------------------------------------------------ */
/* Setup */
/* ------------------------------------------------------------------ */
int
schfs_space_init(struct schfs_space *s, uint64_t nblocks,
void *(*palloc)(void))
{
memset(s, 0, sizeof(*s));
if (nblocks <= SCHFS_FIRST_BLOCK)
return -3;
if (nblocks > SCHFS_MAX_BLOCKS)
return -4; /* larger than this build can map */
uint64_t npages = (nblocks + SCHFS_SPACE_BITS_PER_PAGE - 1)
/ SCHFS_SPACE_BITS_PER_PAGE;
for (uint64_t i = 0; i < npages; i++) {
void *p = palloc();
if (p == 0)
return -5; /* caller frees what was taken */
memset(p, 0, SCHFS_BLKSIZE);
s->inuse[i] = (uint64_t *)p;
p = palloc();
if (p == 0)
return -5;
memset(p, 0, SCHFS_BLKSIZE);
s->pend[i] = (uint64_t *)p;
s->npages = (int)(i + 1);
}
s->nblocks = nblocks;
s->free_blocks = nblocks - SCHFS_FIRST_BLOCK;
s->hint = SCHFS_FIRST_BLOCK;
// The boot block and the uberblock ring are never allocatable, and neither
// is anything past the end of the volume: the last bitmap page covers more
// blocks than exist, and a stray allocation out there would be written to a
// sector the device does not have.
for (uint64_t b = 0; b < SCHFS_FIRST_BLOCK; b++)
bit_set(s->inuse, b);
for (uint64_t b = nblocks; b < (uint64_t)npages * SCHFS_SPACE_BITS_PER_PAGE; b++)
bit_set(s->inuse, b);
return 0;
}
void
schfs_space_mark(struct schfs_space *s, uint64_t blk, uint64_t n)
{
for (uint64_t i = 0; i < n; i++) {
uint64_t b = blk + i;
if (b < SCHFS_FIRST_BLOCK || b >= s->nblocks)
continue;
if (bit_test(s->inuse, b))
continue; /* already counted; extents may be shared */
bit_set(s->inuse, b);
s->free_blocks--;
}
}
/* ------------------------------------------------------------------ */
/* Allocation */
/* ------------------------------------------------------------------ */
uint64_t
schfs_space_alloc_run(struct schfs_space *s, uint64_t n)
{
if (n == 0 || n > s->free_blocks)
return 0;
// Two sweeps: from the rotating hint to the end of the volume, then from
// the start back to it. Starting every search at block 0 would make the
// front of the volume the only place anything is ever written and turn
// allocation into a linear scan of everything already used.
for (int pass = 0; pass < 2; pass++) {
uint64_t lo = pass == 0 ? s->hint : SCHFS_FIRST_BLOCK;
uint64_t hi = pass == 0 ? s->nblocks : s->hint;
uint64_t run = 0, first = 0;
for (uint64_t b = lo; b < hi; ) {
if (b % 64 == 0 && bit_word(s->inuse, b) == ~0ULL) {
run = 0;
b += 64;
continue;
}
if (bit_test(s->inuse, b)) {
run = 0;
b++;
continue;
}
if (run == 0)
first = b;
run++;
b++;
if (run == n) {
for (uint64_t i = 0; i < n; i++)
bit_set(s->inuse, first + i);
s->free_blocks -= n;
s->hint = first + n < s->nblocks ? first + n : SCHFS_FIRST_BLOCK;
return first;
}
}
}
return 0;
}
uint64_t
schfs_space_alloc(struct schfs_space *s)
{
return schfs_space_alloc_run(s, 1);
}
void
schfs_space_free(struct schfs_space *s, uint64_t blk)
{
if (blk < SCHFS_FIRST_BLOCK || blk >= s->nblocks)
return;
if (!bit_test(s->inuse, blk))
return; /* never allocated, or freed twice */
if (bit_test(s->pend, blk))
return; /* already queued this group */
bit_set(s->pend, blk);
s->pending++;
}
void
schfs_space_commit(struct schfs_space *s)
{
uint64_t words = SCHFS_SPACE_BITS_PER_PAGE / 64;
for (int p = 0; p < s->npages; p++) {
uint64_t *pend = s->pend[p];
uint64_t *inuse = s->inuse[p];
for (uint64_t w = 0; w < words; w++) {
if (pend[w] == 0)
continue;
s->free_blocks += popcount64(pend[w] & inuse[w]);
inuse[w] &= ~pend[w];
pend[w] = 0;
}
}
s->pending = 0;
}
/* ------------------------------------------------------------------ */
/* Deriving the map from the committed tree */
/* ------------------------------------------------------------------ */
// Every extent record in a leaf names a run of data blocks. A range shared by
// several files (a reflink, a snapshot) appears more than once, which is why
// schfs_space_mark is idempotent rather than counting.
static void
mark_leaf_extents(struct schfs_space *s, const void *blk)
{
const struct schfs_item *it = SCHFS_ITEMS(blk);
int n = SCHFS_HDR(blk)->nritems;
for (int i = 0; i < n; i++) {
if (it[i].key.type != SCHFS_KEY_EXTENT)
continue;
if (it[i].size != sizeof(struct schfs_extent))
continue;
if ((int)it[i].offset + (int)it[i].size > SCHFS_BLKSIZE)
continue;
const struct schfs_extent *e =
(const struct schfs_extent *)((const char *)blk + it[i].offset);
schfs_space_mark(s, e->disk_block, e->nblocks);
}
}
// Walk the whole tree, marking every block it reaches.
//
// Iterative, with one block buffer and a path of block numbers, for the same
// reason everything else here is: a recursive walk keeps a block per level and
// the kernel stack is one block in total. Climbing re-reads the parent, so an
// internal node is read once per child — mount-time cost that the buffer cache
// largely absorbs, in exchange for bounded memory.
int
schfs_space_scan(struct schfs_vol *v)
{
struct schfs_space *s = &v->space;
void *blk = v->ctx.a;
struct {
uint64_t nr;
uint32_t crc;
int idx; /* next child to descend into */
} path[SCHFS_MAX_LEVEL + 1];
int d = 0;
path[0].nr = v->tree.root;
path[0].crc = v->tree.root_crc;
path[0].idx = 0;
schfs_space_mark(s, v->tree.root, 1);
for (;;) {
if (schfs_node_read(&v->dev, path[d].nr, path[d].crc, blk) != 0)
return -2;
if (SCHFS_HDR(blk)->level == 0) {
mark_leaf_extents(s, blk);
d--;
} else if (path[d].idx < SCHFS_HDR(blk)->nritems) {
struct schfs_kptr *kp = &SCHFS_KPTRS(blk)[path[d].idx];
path[d].idx++;
if (d + 1 > SCHFS_MAX_LEVEL)
return -2;
d++;
path[d].nr = kp->blocknr;
path[d].crc = kp->crc;
path[d].idx = 0;
schfs_space_mark(s, path[d].nr, 1);
continue;
} else {
d--; /* every child visited */
}
if (d < 0)
return 0;
}
}

414
kernel/fs/schfs/vfs.c Normal file
View file

@ -0,0 +1,414 @@
// SPDX-License-Identifier: Apache-2.0
//
// SchFS behind the VFS: the fs_ops table that makes SchFS files reachable
// through open(2), read(2), write(2) and the rest.
//
// Everything below is translation, not filesystem logic. The filesystem lives
// in btree.c/space.c/vol.c/obj.c/objw.c and is driven through the schfs_k*
// entry points in kmount.c, each of which is one transaction. This file's whole
// job is turning a `struct inode` into an object id and back.
//
// ---------------------------------------------------------------------------
// The two models, and where they disagree
//
// The VFS inherited xv6's shape: allocate an inode, fill it in, then link a
// name to it. SchFS has no free-standing inode — an object exists because a
// record with its id exists — so schfs_ialloc writes an inode record with
// nlink 0 and no name at all. Nothing can reach it until dirlink adds an entry.
// If the caller dies in between, the record is unreferenced but harmless, and
// it never reaches the disk anyway: nothing is durable until the transaction
// that created it commits.
//
// The other disagreement is `nlink`. The VFS adjusts it itself (create sets it
// to 1, link bumps it, unlink drops it) and then calls iupdate. So the object
// layer's own link accounting is deliberately bypassed here: schfs_dir_add and
// schfs_dir_del are used rather than schfs_link and schfs_unlink, and iput's
// ifree hook is what releases an object once the VFS has decided it has no
// names left. Two pieces of code counting links independently is precisely how
// a filesystem loses a file that is still referenced.
#include "types.h"
#include "x86.h"
#include "defs.h"
#include "param.h"
#include "stat.h"
#include "spinlock.h"
#include "proc.h" /* `proc` — copyin/copyout need the current pgdir */
#include "sleeplock.h"
#include "fs.h"
#include "buf.h"
#include "file.h"
#include "schfs_int.h"
// The VFS talks in T_*; the disk format uses SCHFS_T_*. Neither is going to
// change to suit the other, so the mapping lives here, once, in both
// directions rather than as an assumption that the numbers happen to line up.
static short
type_to_vfs(uint16_t t)
{
switch (t) {
case SCHFS_T_DIR: return T_DIR;
case SCHFS_T_DEV: return T_DEV;
case SCHFS_T_SYMLINK: return T_SYMLINK;
default: return T_FILE;
}
}
static uint16_t
type_from_vfs(short t)
{
switch (t) {
case T_DIR: return SCHFS_T_DIR;
case T_DEV: return SCHFS_T_DEV;
case T_SYMLINK: return SCHFS_T_SYMLINK;
default: return SCHFS_T_FILE;
}
}
/* ------------------------------------------------------------------ */
/* Inode load / store / free */
/* ------------------------------------------------------------------ */
static void
schfs_vfs_iload(struct inode *ip)
{
struct schfs_inode in;
if (schfs_kstat(ip->inum, &in) != 0) {
// An unreadable inode on the root filesystem is a panic in v1 because the
// system cannot continue without it. Here it is a mounted volume that may
// simply be damaged, so report an empty file and let the caller fail
// normally rather than taking the machine down with it.
ip->type = T_FILE;
ip->nlink = 0;
ip->size = 0;
ip->valid = 1;
return;
}
ip->type = type_to_vfs(in.type);
ip->major = (short)in.rdev_major;
ip->minor = (short)in.rdev_minor;
ip->nlink = (short)in.nlink;
ip->size = in.size;
ip->rwmode = RW;
ip->supermode = 0;
ip->showmode = 1;
ip->atime = (uint)in.atime;
ip->mtime = (uint)in.mtime;
ip->ctime = (uint)in.ctime;
memset(ip->addrs, 0, sizeof(ip->addrs));
ip->valid = 1;
}
static void
schfs_vfs_istore(struct inode *ip)
{
struct schfs_inode in;
// Read-modify-write rather than build-from-scratch: the on-disk record holds
// fields the VFS has no slot for (uid, gid, the real mode bits, the
// generation), and rebuilding it here would quietly zero every one of them.
if (schfs_kstat(ip->inum, &in) != 0) {
memset(&in, 0, sizeof(in));
in.mode = ip->type == T_DIR ? 0755 : 0644;
}
in.type = type_from_vfs(ip->type);
in.rdev_major = (uint16_t)ip->major;
in.rdev_minor = (uint16_t)ip->minor;
in.nlink = (uint32_t)ip->nlink;
in.size = ip->size;
in.atime = ip->atime;
in.mtime = ip->mtime;
in.ctime = ip->ctime;
schfs_ksetattr(ip->inum, &in);
}
static void
schfs_vfs_ifree(struct inode *ip)
{
schfs_kfree(ip->inum);
}
/* ------------------------------------------------------------------ */
/* Data */
/* ------------------------------------------------------------------ */
// Reads and writes are staged through a bounce buffer rather than going
// straight to user memory, because the filesystem takes a sleeplock and walks
// the tree while a copyout could fault. One page at a time keeps that buffer
// off the 4096-byte kernel stack -- it is kalloc'd -- and bounds how long the
// volume lock is held.
#define BOUNCE 2048
static int
schfs_vfs_read(struct inode *ip, int user_dst, uint64 dst, uint64 off, uint n)
{
char *tmp = kalloc();
if (tmp == 0)
return -1;
uint done = 0;
while (done < n) {
uint want = n - done;
if (want > BOUNCE)
want = BOUNCE;
long got = schfs_kread(ip->inum, off + done, tmp, want);
if (got < 0) { kfree(tmp); return done ? (int)done : -1; }
if (got == 0) break; /* end of file */
if (user_dst) {
if (copyout(myproc()->pgdir, dst + done, tmp, (uint)got) < 0)
break;
} else {
memmove((char *)(dst + done), tmp, (uint)got);
}
done += (uint)got;
if ((uint)got < want)
break; /* short read: end of file */
}
kfree(tmp);
return (int)done;
}
static int
schfs_vfs_write(struct inode *ip, int user_src, uint64 src, uint64 off, uint n)
{
char *tmp = kalloc();
if (tmp == 0)
return -1;
uint done = 0;
while (done < n) {
uint want = n - done;
if (want > BOUNCE)
want = BOUNCE;
if (user_src) {
if (copyin(myproc()->pgdir, (addr_t)(src + done), tmp, (uint64)want) < 0)
break;
} else {
memmove(tmp, (char *)(src + done), want);
}
long put = schfs_kwrite(ip->inum, off + done, tmp, want);
if (put <= 0) break;
done += (uint)put;
if ((uint)put < want) break;
}
kfree(tmp);
if (off + done > ip->size)
ip->size = off + done;
return (int)done;
}
static void
schfs_vfs_truncate(struct inode *ip, uint64 newsize)
{
if (schfs_ktruncate(ip->inum, newsize) == 0)
ip->size = newsize;
}
/* ------------------------------------------------------------------ */
/* Directories */
/* ------------------------------------------------------------------ */
static struct inode *
schfs_vfs_dirlookup(struct inode *dp, char *name, uint *poff)
{
// "." and ".." are not stored: a directory costs nothing until it has real
// entries. Both are answered from what the VFS already knows, except ".."
// at the mount root, which the mount layer redirects before we get here.
if (name[0] == '.' && name[1] == '\0') {
if (poff) *poff = 0;
return iget(dp->dev, dp->inum);
}
unsigned namelen = 0;
while (name[namelen]) namelen++;
if (namelen == 2 && name[0] == '.' && name[1] == '.') {
struct schfs_inode in;
if (schfs_kstat(dp->inum, &in) != 0)
return 0;
if (poff) *poff = 0;
return iget(dp->dev, in.parent ? in.parent : dp->inum);
}
uint64 ino = schfs_klookup_at(dp->inum, name, namelen);
if (ino == 0)
return 0;
// poff is v1's byte offset into the directory file. SchFS finds an entry by
// name, so there is nothing meaningful to hand back; dirunlink below takes
// the name and ignores it.
if (poff) *poff = 0;
return iget(dp->dev, ino);
}
static int
schfs_vfs_dirlink(struct inode *dp, char *name, uint64 inum)
{
unsigned namelen = 0;
while (name[namelen]) namelen++;
if (namelen == 0 || namelen > SCHFS_NAME_MAX)
return -1;
struct schfs_inode in;
if (schfs_kstat(inum, &in) != 0)
return -1;
int r = schfs_kdiradd(dp->inum, name, namelen, inum, in.type);
if (r != 0)
return -1;
// A subdirectory's ".." is synthesised from its parent field, so moving or
// creating one has to set that field or the new directory reports the wrong
// parent -- and every ".." walk out of it lands somewhere else entirely.
if (in.type == SCHFS_T_DIR && in.parent != dp->inum) {
in.parent = dp->inum;
schfs_ksetattr(inum, &in);
}
return 0;
}
static int
schfs_vfs_dirunlink(struct inode *dp, char *name, uint off)
{
(void)off;
unsigned namelen = 0;
while (name[namelen]) namelen++;
return schfs_kunlink(dp->inum, name, namelen) == 0 ? 0 : -1;
}
static int
schfs_vfs_dirempty(struct inode *dp)
{
return schfs_kdirempty(dp->inum);
}
static struct inode *
schfs_vfs_ialloc(uint dev, short type)
{
uint64 ino = 0;
if (schfs_kalloc_object(type_from_vfs(type), type == T_DIR ? 0755 : 0644,
&ino) != 0)
return 0;
return iget(dev, ino);
}
/* ------------------------------------------------------------------ */
/* getdents64 */
/* ------------------------------------------------------------------ */
// One record's worth of state for the callback below.
//
// Entries come back in name-hash order, which has nothing to do with their
// names -- POSIX promises no order, and hash order is what makes a lookup one
// tree descent instead of a scan. The position handed back to userland is
// therefore an *index*: entry N of the walk. Resuming means walking to N again,
// which readdir already does linearly, so a full listing stays linear overall
// only because the common case is resuming exactly where the last call stopped.
struct gd_ctx {
char *out;
int cap;
int written;
uint64 skip; // entries to step over before emitting
uint64 index; // entries seen so far
int full;
};
static int
gd_entry(uint64 ino, unsigned short type, const char *name, unsigned namelen,
void *arg)
{
struct gd_ctx *g = arg;
if (g->index++ < g->skip)
return 0;
if (g->full)
return 1;
if (namelen > 255)
namelen = 255;
int reclen = (8 + 8 + 2 + 1 + (int)namelen + 1 + 7) & ~7;
if (g->written + reclen > g->cap) {
g->full = 1;
return 1; /* stop; the caller will come back */
}
char *k = g->out + g->written;
uint64 *p64 = (uint64 *)k;
p64[0] = ino;
p64[1] = g->index; /* resume position: entries consumed */
*(uint16 *)(k + 16) = (uint16)reclen;
*(uint8 *)(k + 18) = type == SCHFS_T_DIR ? 4 /* DT_DIR */
: type == SCHFS_T_DEV ? 2 /* DT_CHR */
: type == SCHFS_T_SYMLINK ? 10 /* DT_LNK */
: 8; /* DT_REG */
memmove(k + 19, name, namelen);
memset(k + 19 + namelen, 0, reclen - 19 - namelen);
g->written += reclen;
return 0;
}
static int
schfs_vfs_getdents(struct inode *dp, char *out, int cap, uint64 *poff)
{
struct gd_ctx g;
g.out = out;
g.cap = cap;
g.written = 0;
g.skip = *poff;
g.index = 0;
g.full = 0;
// "." and ".." are synthesised: they are not records, so a walk would never
// produce them, and every readdir caller expects both.
static const char *dots[2] = { ".", ".." };
for (int i = 0; i < 2; i++) {
if (g.index++ < g.skip)
continue;
int reclen = (8 + 8 + 2 + 1 + (i + 1) + 1 + 7) & ~7;
if (g.written + reclen > cap)
return g.written;
char *k = out + g.written;
uint64 *p64 = (uint64 *)k;
p64[0] = dp->inum; /* close enough for ".."; nothing reads it */
p64[1] = g.index;
*(uint16 *)(k + 16) = (uint16)reclen;
*(uint8 *)(k + 18) = 4; /* DT_DIR */
memmove(k + 19, dots[i], i + 1);
memset(k + 19 + i + 1, 0, reclen - 19 - i - 1);
g.written += reclen;
}
if (schfs_kreaddir(dp->inum, gd_entry, &g) != 0 && g.written == 0)
return -1;
*poff = g.index;
return g.written;
}
/* ------------------------------------------------------------------ */
const struct fs_ops schfs_ops = {
.name = "schfs",
.iload = schfs_vfs_iload,
.istore = schfs_vfs_istore,
.ifree = schfs_vfs_ifree,
.read = schfs_vfs_read,
.write = schfs_vfs_write,
.truncate = schfs_vfs_truncate,
.dirlookup = schfs_vfs_dirlookup,
.dirlink = schfs_vfs_dirlink,
.dirunlink = schfs_vfs_dirunlink,
.dirempty = schfs_vfs_dirempty,
.ialloc = schfs_vfs_ialloc,
.getdents = schfs_vfs_getdents,
};

299
kernel/fs/schfs/vol.c Normal file
View file

@ -0,0 +1,299 @@
// SPDX-License-Identifier: Apache-2.0
//
// SchFS volume layer: the uberblock ring.
//
// A ring of SCHFS_UBER_COUNT slots is written round-robin, each stamped with a
// transaction number and a checksum. Mounting reads every slot and adopts the
// highest-numbered one that validates.
//
// That single rule replaces journalling entirely. A commit writes one block; it
// either lands whole or it does not. If it does not, its checksum fails, the
// previous slot is still the highest valid one, and the volume comes up at the
// previous transaction with no replay and no fsck. Rotating through many slots
// rather than overwriting one means a run of bad sectors under the current slot
// cannot take the volume with it.
#include "schfs_int.h"
#ifdef SCHFS_HOSTED
#include <string.h>
#else
#include "types.h"
#include "defs.h"
#endif
// Checksum an uberblock with its own crc field zeroed, the same convention the
// tree nodes use.
static uint32_t
uber_crc(struct schfs_uberblock *u)
{
uint32_t saved = u->crc;
u->crc = 0;
uint32_t c = schfs_crc32c(0, u, sizeof(*u));
u->crc = saved;
return c;
}
static int
uber_valid(struct schfs_uberblock *u)
{
if (u->magic != SCHFS_MAGIC)
return 0;
if (u->version != SCHFS_VERSION)
return 0;
// A volume made with a different block size would otherwise be read with
// every offset wrong, which looks exactly like corruption.
if (u->blocksize != SCHFS_BLKSIZE)
return 0;
if (uber_crc(u) != u->crc)
return 0;
// Self-consistency: a root inside the ring or past the end of the device is
// a corrupt uberblock that happens to checksum, not a usable one.
if (u->root < SCHFS_FIRST_BLOCK || u->root >= u->total_blocks)
return 0;
return 1;
}
static void
vol_from_uber(struct schfs_vol *v, struct schfs_uberblock *u, int slot)
{
v->tree.dev = &v->dev;
v->tree.root = u->root;
v->tree.root_crc = u->root_crc;
v->tree.root_gen = u->root_generation;
// The tree's `generation` is the transaction group being *built*, one past
// the newest committed one. Setting it to the committed root's generation
// instead would tell the tree that every block of the mounted filesystem
// belongs to the open transaction, and it would then rewrite the live tree
// in place — losing the very thing copy-on-write exists to preserve.
v->tree.generation = u->generation + 1;
v->total_blocks = u->total_blocks;
v->free_blocks = u->free_blocks;
v->next_objectid = u->next_objectid;
v->root_objectid = u->root_objectid;
v->generation = u->generation;
v->uber_slot = slot;
memmove(v->uuid, u->uuid, sizeof(v->uuid));
memmove(v->label, u->label, sizeof(v->label));
v->label[SCHFS_LABEL_MAX - 1] = '\0';
}
int
schfs_vol_open(struct schfs_vol *v, struct schfs_bdev *dev,
struct schfs_ctx *x, void *(*palloc)(void))
{
struct schfs_uberblock *u = (struct schfs_uberblock *)x->a;
int best_slot = -1;
uint64_t best_gen = 0;
int io_errors = 0;
v->dev = *dev;
v->ctx = *x;
v->tree.dev = &v->dev;
// Remember which slot won, not a copy of it.
//
// An uberblock is a whole SCHFS_BLKSIZE block, and keeping one as a local
// put 4096 bytes on a kernel stack that is 4096 bytes in total — the canary
// in kalloc'd stacks caught it as "kstack overflow" on the very first boot.
// Re-reading the winner costs one block read, once, at mount.
for (int i = 0; i < SCHFS_UBER_COUNT; i++) {
if (v->dev.bread(v->dev.ctx, SCHFS_UBER_START + i, u) != 0) {
io_errors++;
continue; // an unreadable slot is not fatal: try the rest
}
if (!uber_valid(u))
continue;
if (best_slot < 0 || u->generation > best_gen) {
best_gen = u->generation;
best_slot = i;
}
}
if (best_slot < 0)
return io_errors == SCHFS_UBER_COUNT ? -2 : -1;
if (v->dev.bread(v->dev.ctx, SCHFS_UBER_START + best_slot, u) != 0)
return -2;
if (!uber_valid(u))
return -2; /* changed under us, or a flaky device */
vol_from_uber(v, u, best_slot);
v->readonly = 1; // until an allocation map exists, below
// The newest uberblock passing its own checksum does not prove the tree it
// names is readable — a misdirected write could have landed on the root.
// Check now, while there is still the option of falling back, rather than
// discovering it on the first lookup.
if (v->dev.bread(v->dev.ctx, v->tree.root, x->a) != 0)
return -2;
{
struct schfs_node_hdr *h = (struct schfs_node_hdr *)x->a;
if (h->magic != SCHFS_NODE_MAGIC || h->blocknr != v->tree.root)
return -2;
}
// Rebuild the allocation map from the tree just adopted. Without a palloc
// the volume stays read-only, which is exactly right for a tool that only
// inspects an image: it can then never allocate, rather than allocating
// from a map that says everything is free.
if (palloc) {
int sr = schfs_space_init(&v->space, v->total_blocks, palloc);
if (sr != 0)
return sr;
if (schfs_space_scan(v) != 0)
return -2;
v->free_blocks = v->space.free_blocks;
v->readonly = 0;
}
return 0;
}
int
schfs_vol_commit(struct schfs_vol *v)
{
struct schfs_uberblock *u = (struct schfs_uberblock *)v->ctx.a;
v->generation++;
v->uber_slot = (v->uber_slot + 1) % SCHFS_UBER_COUNT;
memset(u, 0, sizeof(*u));
u->magic = SCHFS_MAGIC;
u->version = SCHFS_VERSION;
u->blocksize = SCHFS_BLKSIZE;
u->generation = v->generation;
u->root = v->tree.root;
u->root_crc = v->tree.root_crc;
u->root_generation = v->tree.root_gen;
u->total_blocks = v->total_blocks;
// Blocks this group released count as free *as of this uberblock*: nothing
// the root below reaches touches them. They are only still marked in use in
// memory because the group being superseded is what a crash would land on
// until this very block is written.
u->free_blocks = v->space.nblocks
? v->space.free_blocks + v->space.pending
: v->free_blocks;
u->next_objectid = v->next_objectid;
u->root_objectid = v->root_objectid;
memmove(u->uuid, v->uuid, sizeof(u->uuid));
memmove(u->label, v->label, sizeof(u->label));
u->crc = uber_crc(u);
if (v->dev.bwrite(v->dev.ctx, SCHFS_UBER_START + v->uber_slot, u) != 0)
return -2;
// Order matters here, and it is the whole crash-safety argument in three
// lines. Only now that the new uberblock is on the disk is the group this
// freed blocks in superseded, so only now may those blocks be handed out
// again; releasing them before the write would let a crash come back to a
// tree whose blocks had already been reused.
if (v->space.nblocks) {
schfs_space_commit(&v->space);
v->free_blocks = v->space.free_blocks;
}
// Everything written from here on belongs to the next transaction group, so
// the blocks just published become read-only to the tree again and the
// copy-on-write rule resumes. Advancing this is the whole reason a commit
// is a barrier rather than just another block write.
v->tree.generation = v->generation + 1;
return 0;
}
int
schfs_vol_format(struct schfs_vol *v, struct schfs_bdev *dev,
uint64_t total_blocks, const char *label,
struct schfs_ctx *x, void *(*palloc)(void))
{
if (total_blocks <= SCHFS_FIRST_BLOCK)
return -3;
memset(v, 0, sizeof(*v));
v->dev = *dev;
v->ctx = *x;
v->tree.dev = &v->dev;
v->total_blocks = total_blocks;
v->root_objectid = SCHFS_ROOT_OBJECTID;
// Nothing is allocated on a fresh volume beyond the boot block and the
// ring, so the map starts empty rather than being scanned into existence.
{
int sr = schfs_space_init(&v->space, total_blocks, palloc);
if (sr != 0)
return sr;
}
v->free_blocks = v->space.free_blocks;
// The root's object id is consumed by the root itself, so allocation must
// start *past* it. Starting at SCHFS_FIRST_OBJECTID instead handed the
// first directory created the same id as the root: that directory then
// silently became the root, so it contained itself and any tree walk
// recursed until the stack ran out.
v->next_objectid = SCHFS_ROOT_OBJECTID + 1;
v->generation = 0; /* nothing committed yet */
v->tree.generation = 1; /* ...so this is the group being built */
v->uber_slot = -1;
v->readonly = 0;
if (label) {
unsigned i = 0;
for (; i < SCHFS_LABEL_MAX - 1 && label[i]; i++)
v->label[i] = label[i];
v->label[i] = '\0';
}
// A UUID from the block count and a fixed salt. Not random — mkfs is
// deliberately reproducible, so two runs over the same tree produce
// byte-identical images and a diff means a real change.
{
uint32_t a = schfs_crc32c(0, &total_blocks, sizeof(total_blocks));
uint32_t b = schfs_crc32c(a, v->label, SCHFS_LABEL_MAX);
for (int i = 0; i < 16; i++)
v->uuid[i] = (uint8_t)((i & 1 ? a : b) >> ((i % 4) * 8));
}
// Zero the whole ring so a stale uberblock from a previous filesystem on
// this device cannot outrank ours and be mounted instead.
memset(x->a, 0, SCHFS_BLKSIZE);
for (int i = 0; i < SCHFS_UBER_COUNT; i++)
if (v->dev.bwrite(v->dev.ctx, SCHFS_UBER_START + i, x->a) != 0)
return -2;
if (schfs_btree_create(&v->tree, &v->dev, x) != 0)
return -2;
return schfs_vol_commit(v);
}
/* ------------------------------------------------------------------ */
/* Allocation */
/* */
/* Thin adapters over the space manager, so callers wiring up a */
/* schfs_bdev have something with the right shape to point at. The */
/* readonly check lives here rather than in space.c because "this */
/* volume may not be written" is a property of the mount, not of the */
/* bitmap. */
/* ------------------------------------------------------------------ */
uint64_t
schfs_vol_alloc(struct schfs_vol *v)
{
if (v->readonly)
return 0;
return schfs_space_alloc(&v->space);
}
uint64_t
schfs_vol_alloc_run(struct schfs_vol *v, uint64_t n)
{
if (v->readonly)
return 0;
return schfs_space_alloc_run(&v->space, n);
}
void
schfs_vol_free(struct schfs_vol *v, uint64_t blk)
{
schfs_space_free(&v->space, blk);
}

104
kernel/kernel.ld Normal file
View file

@ -0,0 +1,104 @@
/* Simple linker script for the JOS kernel.
See the GNU ld 'info' manual ("info ld") to learn the syntax. */
/* OUTPUT_FORMAT("elf32-i386", "elf32-i386", "elf32-i386") */
/* OUTPUT_FORMAT("elf64-x86-64", "elf64-x86-64", "elf64-x86-64") */
OUTPUT_FORMAT("elf64-x86-64")
OUTPUT_ARCH(i386:x86-64)
ENTRY(_start)
mboot_load_addr = 0x00100000;
SECTIONS
{
/* Link the kernel at this address: "." means the current address */
/* Must be equal to KERNLINK */
. = 0xFFFF800000100000;
PROVIDE(begin = .);
/* The .l* section names are the x86-64 psABI's "large" sections, and they
* are not optional decoration: with -mcmodel=large both LLVM and GCC 13+
* put nearly all code and data into .ltext/.lrodata/.ldata/.lbss rather
* than the classic names.
*
* A script that does not name them still links. The large sections become
* orphans placed after everything the script did name, which puts them past
* PROVIDE(end = .) -- so kinit1(end, ...) hands the page allocator a megabyte
* of live .lbss and the kernel destroys itself within a few instructions of
* its first printf. That is exactly how this was found: the boot printed one
* line and triple-faulted in a loop.
*
* Matched alongside their small counterparts so the layout is unchanged for
* a toolchain that does not use them. */
.text : AT(mboot_load_addr) {
*(.text .rela.text .stub .text.* .gnu.linkonce.t.*)
*(.ltext .ltext.*)
}
PROVIDE(etext = .); /* Define the 'etext' symbol to this value */
.rodata : {
*(.rodata .rodata.* .gnu.linkonce.r.*)
*(.lrodata .lrodata.*)
}
/* Thread-local storage.
*
* The kernel really does use TLS: `__thread struct cpu *cpu` and
* `__thread struct proc *proc` (kernel/arch/x86_64/vm.c), reached through
* FS_BASE, which seginit() points at kernel_fs_base.
*
* These output sections are what make the linker emit a PT_TLS segment, and
* without a PT_TLS the local-exec relocations have nothing to resolve
* against: lld left every access as a bare `movq %fs:0x0`, so `cpu` and
* `proc` both aliased the thread pointer itself and the kernel died a few
* instructions into seginit. GNU ld happened to place an orphan .tbss well
* enough to work; that was luck, not correctness.
*
* Order matters and is checked by more than C: trapasm.S hardcodes `proc` at
* %fs:(-8). With cpu at TLS offset 0 and proc at 8 in a 16-byte block, the
* local-exec offsets come out as -16 and -8, which is the layout that file
* assumes. */
.tdata : { *(.tdata .tdata.* .gnu.linkonce.td.*) }
.tbss : { *(.tbss .tbss.* .gnu.linkonce.tb.*) *(.tcommon) }
/* Adjust the address for the data segment to the next page */
. = ALIGN(0x1000);
/* Conventionally, Unix linkers provide pseudo-symbols
* etext, edata, and end, at the end of the text, data, and bss.
* For the kernel mapping, we need the address at the beginning
* of the data section, but that's not one of the conventional
* symbols, because the convention started before there was a
* read-only rodata section between text and data. */
PROVIDE(data = .);
/* The data segment */
.data : {
*(.data .data.*)
*(.ldata .ldata.*)
}
. = ALIGN(0x1000);
PROVIDE(edata = .);
.bss : {
*(.bss .bss.*)
*(.lbss .lbss.*)
*(COMMON)
}
. = ALIGN(0x1000);
PROVIDE(end = .);
/DISCARD/ : {
*(.eh_frame .rela.eh_frame .note.GNU-stack)
}
}
mboot_load_end = mboot_load_addr + (edata - begin);
mboot_bss_end = mboot_load_addr + (end - begin);
mboot_entry_addr = mboot_load_addr + (mboot_entry - begin);

441
kernel/net/unix.c Normal file
View file

@ -0,0 +1,441 @@
// Unix domain sockets.
//
// A connected pair is two buffers, not one. Writing to a socket appends to
// its *peer's* buffer and reading drains its own, which is what makes a
// socket bidirectional where a pipe is not, without needing two of anything.
//
// **One lock covers the whole table**, rather than a lock per socket. A pipe
// gets away with a lock per pipe because every operation touches exactly one
// object; a socket operation touches two, and two-object locking needs an
// ordering rule that every path obeys or it deadlocks. Worse, the wakeup
// races are subtle: a reader that tests `peer_closed` under its own lock while
// the closer sets it under the peer's has no interlock at all, and sleeps
// through the wakeup that was meant for it. With one lock the condition and
// the sleep are atomic with respect to every writer of that condition, which
// is the property sleep/wakeup actually requires. Thirty-two sockets carrying
// GUI messages will not notice the contention.
//
// **Names are held in a table here, not in the filesystem.** A real Unix
// socket appears in a directory listing as a special file, and binding one
// creates that node. Doing that properly means a new inode type threaded
// through both filesystems, iget, dirlookup and stat -- a VFS change, not a
// socket change. The table gives every behaviour that matters: a client
// finds a server by a name it agreed in advance, which is the whole job.
// What is lost is `ls -l /tmp/schcom` showing an `s`, and `unlink()` of a
// socket path failing with ENOENT. Both are worth fixing when the VFS next
// gets opened up; neither blocks anything.
#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 "posix.h"
#include "errno.h"
#define _KERNEL_SOCKET_INTERNALS
#include "socket.h"
static struct {
struct spinlock lock;
struct socket sock[NSOCK];
} stab;
void
sockinit(void)
{
initlock(&stab.lock, "socktab");
}
// Caller holds stab.lock.
static struct socket *
alloc_locked(int type)
{
struct socket *s;
for(s = stab.sock; s < stab.sock + NSOCK; s++){
if(s->state == SS_FREE && s->ref == 0){
memset(s, 0, sizeof(*s));
s->ref = 1;
s->type = type;
s->state = SS_UNBOUND;
return s;
}
}
return 0;
}
// Find a socket listening on this name. Caller holds stab.lock.
static struct socket *
find_listener(const char *name)
{
struct socket *s;
for(s = stab.sock; s < stab.sock + NSOCK; s++)
if(s->state == SS_LISTENING && strncmp(s->name, name, UNIX_PATH_MAX) == 0)
return s;
return 0;
}
// Detach s from its peer and tell the peer about it. Caller holds stab.lock.
static void
orphan_peer(struct socket *s)
{
struct socket *peer = s->peer;
if(peer == 0)
return;
peer->peer_closed = 1;
peer->peer = 0;
s->peer = 0;
// Both channels, because the peer may be blocked reading (waiting for data
// that will now never come) or writing (waiting for room in a buffer nobody
// will ever drain).
wakeup(&peer->nread);
wakeup(&peer->nwrite);
}
void
sockclose(struct socket *s)
{
int i;
if(s == 0)
return;
acquire(&stab.lock);
if(--s->ref > 0){
release(&stab.lock);
return;
}
orphan_peer(s);
// A listener that goes away takes its unaccepted connections with it. Each
// queued entry is the *server* side of a connection whose client is already
// live and may be writing into it; that client has to be told, or it hangs
// on a socket that will never be read.
for(i = 0; i < s->nqueue; i++){
struct socket *q = s->queue[i];
if(q == 0)
continue;
orphan_peer(q);
q->state = SS_FREE;
q->ref = 0;
}
s->nqueue = 0;
s->state = SS_FREE;
s->ref = 0;
wakeup(&s->nqueue); // an accept() blocked on this listener
release(&stab.lock);
}
void
sockdup(struct socket *s)
{
acquire(&stab.lock);
s->ref++;
release(&stab.lock);
}
const char *
sockname(struct socket *s)
{
return s->name;
}
// ---------------------------------------------------------------------------
// Data
// ---------------------------------------------------------------------------
int
sockread(struct socket *s, char *addr, int n)
{
int i;
if(n < 0)
return -EINVAL;
acquire(&stab.lock);
while(s->nread == s->nwrite && !s->peer_closed){
if(myproc()->killed || s->state == SS_FREE){
release(&stab.lock);
return -EINTR;
}
sleep(&s->nread, &stab.lock);
}
for(i = 0; i < n; i++){
if(s->nread == s->nwrite)
break;
addr[i] = s->data[s->nread++ % SOCKBUF];
}
// Waking the writer matters even when nothing was read: it may be blocked
// on a full buffer that this call has just made room in.
wakeup(&s->nwrite);
release(&stab.lock);
return i;
}
int
sockwrite(struct socket *s, char *addr, int n)
{
struct socket *dst;
int i;
if(n < 0)
return -EINVAL;
acquire(&stab.lock);
// Data goes into the peer's buffer, because that is where the peer will
// look for it. Same contract as a pipe with no reader: a signal, then an
// error. A process that writes to a dead connection and is not told about
// it spins forever believing it is making progress.
if(s->peer == 0){
release(&stab.lock);
sigsend(myproc()->pid, SIGPIPE);
return -EPIPE;
}
for(i = 0; i < n; i++){
// Re-read every iteration: the peer can go away while we sleep below.
dst = s->peer;
while(dst && dst->nwrite == dst->nread + SOCKBUF){
if(myproc()->killed){
release(&stab.lock);
return i > 0 ? i : -EINTR;
}
wakeup(&dst->nread);
sleep(&dst->nwrite, &stab.lock);
dst = s->peer;
}
if(dst == 0){
release(&stab.lock);
sigsend(myproc()->pid, SIGPIPE);
return i > 0 ? i : -EPIPE; // a short write is still a write
}
dst->data[dst->nwrite++ % SOCKBUF] = addr[i];
}
wakeup(&s->peer->nread);
release(&stab.lock);
return i;
}
// ---------------------------------------------------------------------------
// Connection setup
// ---------------------------------------------------------------------------
struct socket *
socknew(int domain, int type)
{
struct socket *s;
if(domain != AF_UNIX)
return 0;
if(type != SOCK_STREAM)
return 0;
acquire(&stab.lock);
s = alloc_locked(type);
release(&stab.lock);
return s;
}
// A connected pair with no name and no listener, for socketpair(2).
int
sockpair(struct socket **pa, struct socket **pb)
{
struct socket *a, *b;
acquire(&stab.lock);
if((a = alloc_locked(SOCK_STREAM)) == 0){
release(&stab.lock);
return -ENFILE;
}
if((b = alloc_locked(SOCK_STREAM)) == 0){
a->state = SS_FREE;
a->ref = 0;
release(&stab.lock);
return -ENFILE;
}
a->peer = b; a->state = SS_CONNECTED;
b->peer = a; b->state = SS_CONNECTED;
release(&stab.lock);
*pa = a;
*pb = b;
return 0;
}
int
sockbind(struct socket *s, const char *path)
{
struct socket *other;
if(path[0] == '\0')
return -EINVAL;
acquire(&stab.lock);
if(s->state != SS_UNBOUND){
release(&stab.lock);
return -EINVAL;
}
for(other = stab.sock; other < stab.sock + NSOCK; other++){
if(other != s && other->state >= SS_BOUND &&
strncmp(other->name, path, UNIX_PATH_MAX) == 0){
release(&stab.lock);
return -EADDRINUSE;
}
}
safestrcpy(s->name, path, UNIX_PATH_MAX);
s->state = SS_BOUND;
release(&stab.lock);
return 0;
}
int
socklisten(struct socket *s, int backlog)
{
(void)backlog; // the queue is a fixed NBACKLOG either way
acquire(&stab.lock);
if(s->state != SS_BOUND && s->state != SS_LISTENING){
release(&stab.lock);
return -EINVAL;
}
s->state = SS_LISTENING;
release(&stab.lock);
return 0;
}
// Returns the server-side socket of a new connection with a reference already
// held, or 0 on error.
struct socket *
sockaccept(struct socket *s)
{
struct socket *c;
int i;
acquire(&stab.lock);
if(s->state != SS_LISTENING){
release(&stab.lock);
return 0;
}
while(s->nqueue == 0){
if(myproc()->killed || s->state != SS_LISTENING){
release(&stab.lock);
return 0;
}
sleep(&s->nqueue, &stab.lock);
}
c = s->queue[0];
for(i = 0; i + 1 < s->nqueue; i++)
s->queue[i] = s->queue[i+1];
s->nqueue--;
release(&stab.lock);
return c;
}
int
sockconnect(struct socket *s, const char *path)
{
struct socket *srv, *peer;
acquire(&stab.lock);
if(s->state != SS_UNBOUND && s->state != SS_BOUND){
release(&stab.lock);
return -EINVAL;
}
srv = find_listener(path);
if(srv == 0){
release(&stab.lock);
return -ECONNREFUSED;
}
if(srv->nqueue >= NBACKLOG){
release(&stab.lock);
return -EAGAIN;
}
// Every connection needs a socket at each end. The listener is not one of
// them -- it stays listening, and accept() hands back this new one.
peer = alloc_locked(s->type);
if(peer == 0){
release(&stab.lock);
return -ENFILE;
}
peer->peer = s;
peer->state = SS_CONNECTED;
s->peer = peer;
s->state = SS_CONNECTED;
srv->queue[srv->nqueue++] = peer;
wakeup(&srv->nqueue); // an accept() may be blocked on this
release(&stab.lock);
// Deliberately does *not* wait for accept(). On Unix a queued connection is
// already established: the client may write, and the bytes sit in the
// server-side buffer until somebody accepts and reads them. Blocking here
// instead would deadlock any server that connects to itself, and would make
// a busy server's backlog useless.
return 0;
}
// shutdown(2). There is one buffer per direction and no per-direction flag,
// so a half-close is approximated: SHUT_RD stops this end reading anything
// further, and SHUT_WR/SHUT_RDWR give the peer end-of-file. What programs
// actually use shutdown for -- "I have finished sending, read until EOF and
// then reply" -- works; what is missing is the ability to keep reading after
// SHUT_WR, since the connection is torn down rather than half-closed.
int
sock_shutdown(struct socket *s, int how)
{
acquire(&stab.lock);
if(s->state != SS_CONNECTED){
release(&stab.lock);
return -ENOTCONN;
}
if(how == SHUT_RD || how == SHUT_RDWR){
s->peer_closed = 1;
wakeup(&s->nread);
}
if(how == SHUT_WR || how == SHUT_RDWR)
orphan_peer(s);
release(&stab.lock);
return 0;
}
// ---------------------------------------------------------------------------
// Readiness, for poll/select
// ---------------------------------------------------------------------------
int
sockreadable(struct socket *s)
{
if(s->state == SS_LISTENING)
return s->nqueue > 0;
return s->nread != s->nwrite || s->peer_closed;
}
// The peer is gone and everything it sent has been read.
int
sock_hup(struct socket *s)
{
if(s->state == SS_LISTENING)
return 0;
return s->peer == 0 && s->nread == s->nwrite;
}

838
kernel/proc/proc.c Normal file
View file

@ -0,0 +1,838 @@
#include "types.h"
#include "defs.h"
#include "param.h"
#include "memlayout.h"
#include "mmu.h"
#include "x86.h"
#include "traps.h"
#include "proc.h"
#include "spinlock.h"
#include "sleeplock.h"
#include "fs.h"
#include "file.h"
#include "posix.h"
#include "errno.h"
// Verify myproc()->fs_base offset matches the hardcoded 0x688 in trapasm.S.
_Static_assert(__builtin_offsetof(struct proc, fs_base) == 0x18,
"myproc()->fs_base offset changed — update trapasm.S WRITE_USER_FS macro (must be 0x18)");
struct {
struct spinlock lock;
struct proc proc[NPROC];
} ptable;
static struct proc *initproc;
int nextpid = 1;
extern void forkret(void);
extern void syscall_trapret(void);
static void wakeup1(void *chan);
void
kstack_check_canary(const char *where)
{
if(myproc() == 0 || myproc()->kstack == 0)
return;
if(*(uint64*)(myproc()->kstack) != KSTACK_CANARY){
cprintf("kstack canary corrupted for pid %d (%s) at %s: got 0x%p\n",
myproc()->pid, myproc()->name, where, *(uint64*)(myproc()->kstack));
panic("kstack overflow");
}
}
void
pinit(void)
{
initlock(&ptable.lock, "ptable");
}
static struct proc*
allocproc(void)
{
struct proc *p;
char *sp;
acquire(&ptable.lock);
for(p = ptable.proc; p < &ptable.proc[NPROC]; p++)
if(p->state == UNUSED)
goto found;
release(&ptable.lock);
return 0;
found:
p->state = EMBRYO;
p->pid = nextpid++;
// ptable slots get reused once a previous occupant is reaped, but
// nothing previously cleared the signal-delivery fields added for
// SIGCHLD (sig_handler/sig_restorer/pending_signals/signal_mask/
// in_signal/saved_sigtf) — a freshly allocated process would silently
// inherit whatever a totally unrelated earlier process left behind,
// including stale handler addresses. deliver_signals() (syscall.c)
// would then jump to garbage on this new process's very first syscall
// return, faulting at whatever bogus rip happened to be sitting there.
memset(p->sig_handler, 0, sizeof(p->sig_handler));
memset(p->sig_restorer, 0, sizeof(p->sig_restorer));
p->pending_signals = 0;
p->signal_mask = 0;
p->in_signal = 0;
p->sigsusp_oldmask = 0;
p->sigsusp_active = 0;
// Same reasoning as the signal fields above: a recycled slot must not
// inherit a dead process's identity or exit status.
p->xstate = 0;
p->killsig = 0;
p->pgid = 0;
p->sid = 0;
p->uid = p->euid = p->suid = 0;
p->gid = p->egid = p->sgid = 0;
release(&ptable.lock);
if((p->kstack = kalloc()) == 0){
p->state = UNUSED;
return 0;
}
// Canary at the very bottom of the kstack page: if a deep/nested trap
// ever overflows this 4096-byte stack, this is the first thing clobbered.
// Checked on every trap()/syscall() entry (kstack_check_canary()).
*(uint64*)(p->kstack) = KSTACK_CANARY;
sp = p->kstack + KSTACKSIZE;
sp -= sizeof(struct trapframe);
p->tf = (struct trapframe*)sp;
sp -= sizeof(addr_t);
*(addr_t*)sp = (addr_t)syscall_trapret;
sp -= sizeof(struct context);
p->context = (struct context*)sp;
memset(p->context, 0, sizeof *p->context);
p->context->rip = (addr_t)forkret;
p->syscall_map = 0;
p->map_size = 0;
p->mmap_next = 0;
memset(p->ofile_flags, 0, sizeof(p->ofile_flags));
return p;
}
void
sinit(void)
{
struct proc *p;
extern char _binary_initcode_start[], _binary_initcode_size[];
p = allocproc();
initproc = p;
if((p->pgdir = setupkvm()) == 0)
panic("sinit: setupkvm failed");
inituvm(p->pgdir, _binary_initcode_start, (uint64)_binary_initcode_size);
if(allocuvm(p->pgdir, PGSIZE, 32 * PGSIZE) == 0)
panic("sinit: allocuvm failed");
p->sz = 32 * PGSIZE;
memset(p->tf, 0, sizeof(*p->tf));
p->tf->cs = (SEG_UCODE << 3) | DPL_USER;
p->tf->ss = (SEG_UDATA << 3) | DPL_USER;
p->tf->rflags = FL_IF;
p->tf->rsp = p->sz;
p->tf->rip = 0;
// PID 1 leads its own session and process group, and runs as root.
p->pgid = p->pid;
p->sid = p->pid;
p->uid = p->euid = p->suid = 0;
p->gid = p->egid = p->sgid = 0;
safestrcpy(p->name, "sinit", sizeof(p->name));
p->cwd = namei("/");
p->cwd_path[0] = '/'; p->cwd_path[1] = 0;
p->fs_base = 0;
p->state = RUNNABLE;
}
int
growproc(int64 n)
{
addr_t sz;
sz = myproc()->sz;
if(n > 0){
if((sz = allocuvm(myproc()->pgdir, sz, sz + n)) == 0)
return -1;
} else if(n < 0){
if((sz = deallocuvm(myproc()->pgdir, sz, sz + n)) == 0)
return -1;
}
myproc()->sz = sz;
switchuvm(myproc());
return 0;
}
int
fork(void)
{
int i, pid;
struct proc *np;
if((np = allocproc()) == 0)
return -1;
if((np->pgdir = copyuvm(myproc()->pgdir, myproc()->sz)) == 0){
kfree(np->kstack);
np->kstack = 0;
np->state = UNUSED;
return -1;
}
np->sz = myproc()->sz;
np->parent = myproc();
// copyuvm() only duplicates [0, sz) — mmap()'d regions live above that
// and were never copied, leaving the child's vma[] bookkeeping (stale
// from whichever process last occupied this ptable slot) pointing at
// memory that isn't actually mapped in np->pgdir. Reset and rebuild it
// from the parent's real, current mappings.
memset(np->vma, 0, sizeof(np->vma));
for(i = 0; i < NVMA; i++){
if(!myproc()->vma[i].used)
continue;
if(copy_vma_range(np->pgdir, myproc()->pgdir, myproc()->vma[i].addr,
myproc()->vma[i].length, myproc()->vma[i].no_free) < 0){
freevm(np->pgdir);
kfree(np->kstack);
np->kstack = 0;
np->state = UNUSED;
return -1;
}
np->vma[i] = myproc()->vma[i];
if(np->vma[i].shmid >= 0)
shm_ref(np->vma[i].shmid);
}
np->mmap_next = myproc()->mmap_next;
memmove(np->tf, myproc()->tf, sizeof(struct trapframe));
np->syscall_map = myproc()->syscall_map;
np->map_size = myproc()->map_size;
// fork() preserves the parent's signal handlers and mask (POSIX); the
// child starts with no pending signals of its own, though — those
// aren't inherited. allocproc() already zeroed pending_signals/
// in_signal/saved_sigtf for this fresh slot.
memmove(np->sig_handler, myproc()->sig_handler, sizeof(np->sig_handler));
memmove(np->sig_restorer, myproc()->sig_restorer, sizeof(np->sig_restorer));
np->signal_mask = myproc()->signal_mask;
np->tf->rax = 0;
for(i = 0; i < NOFILE; i++)
if(myproc()->ofile[i]) {
np->ofile[i] = filedup(myproc()->ofile[i]);
np->ofile_flags[i] = myproc()->ofile_flags[i];
}
// POSIX: the child inherits process group, session and all four ids.
np->pgid = myproc()->pgid;
np->sid = myproc()->sid;
np->uid = myproc()->uid; np->euid = myproc()->euid; np->suid = myproc()->suid;
np->gid = myproc()->gid; np->egid = myproc()->egid; np->sgid = myproc()->sgid;
np->cwd = idup(myproc()->cwd);
memmove(np->cwd_path, myproc()->cwd_path, sizeof(myproc()->cwd_path));
np->fs_base = myproc()->fs_base;
safestrcpy(np->name, myproc()->name, sizeof(myproc()->name));
pid = np->pid;
__sync_synchronize();
np->state = RUNNABLE;
return pid;
}
// status is already wait(2)-encoded -- see W_EXITCODE() in proc.h. Callers
// that die from a fault or signal pass W_EXITCODE(0, sig); a normal _exit(n)
// passes W_EXITCODE(n, 0).
void
exit(int status)
{
struct proc *p;
int fd;
if(myproc() == initproc)
panic("init exiting");
myproc()->xstate = status;
for(fd = 0; fd < NOFILE; fd++){
if(myproc()->ofile[fd]){
fileclose(myproc()->ofile[fd]);
myproc()->ofile[fd] = 0;
myproc()->ofile_flags[fd] = 0;
}
}
begin_op();
iput(myproc()->cwd);
end_op();
myproc()->cwd = 0;
acquire(&ptable.lock);
// Tell the parent a child exited — needed for mksh's sigsuspend()-based
// job-control wait loop to have any chance of making progress (without
// this, sigsuspend() can never return for the right reason and the
// parent hangs unconditionally). Signal delivery itself is verified
// working (handler invocation + sigreturn round-trip is clean); mksh's
// own job-control bookkeeping has further gaps (see docs/architecture/
// kernel-history.md) that this alone doesn't fully resolve, but
// disabling it guarantees no progress, whereas leaving it enabled at
// least allows the cases that don't hit those gaps to work.
if (myproc()->parent)
myproc()->parent->pending_signals |= (1ULL << SIGCHLD);
wakeup1(myproc()->parent);
for(p = ptable.proc; p < &ptable.proc[NPROC]; p++){
if(p->parent == myproc()){
p->parent = initproc;
if(p->state == ZOMBIE)
wakeup1(initproc);
}
}
myproc()->state = ZOMBIE;
sched();
panic("zombie exit");
}
int
wait(void)
{
return waitpid(-1, 0, 0);
}
// waitpid(2)/wait4(2) proper: a specific pid (-1 = any child, matching
// wait()'s old behavior), an optional status-out pointer (NULL is valid —
// POSIX callers commonly do waitpid(pid, NULL, WNOHANG)), and WNOHANG.
//
// This used to be missing: sys_waitpid()/sys_wait4() both ignored pid and
// options and called the any-child, always-blocking wait() above. A NULL
// status pointer additionally got rejected by argptr() (page 0 is
// unmapped) before ever reaching here. Together, a caller doing
// waitpid(child, NULL, WNOHANG) right after a child exits — exactly what
// mksh's job control does — always failed instead of immediately reaping
// the already-zombie child. mksh then fell back to sigsuspend(), which
// this kernel can't wake for a real SIGCHLD (no signal delivery), so it
// silently retried forever: a real, low-CPU hang with no error printed.
int
waitpid(int pid, int *status, int options)
{
struct proc *p;
int havekids;
acquire(&ptable.lock);
for(;;){
havekids = 0;
for(p = ptable.proc; p < &ptable.proc[NPROC]; p++){
if(p->parent != myproc())
continue;
// POSIX pid selection: >0 one child, 0 = caller's group,
// -1 = any child, < -1 = the process group with id -pid.
if(pid > 0 && p->pid != pid)
continue;
if(pid == 0 && p->pgid != myproc()->pgid)
continue;
if(pid < -1 && p->pgid != -pid)
continue;
havekids = 1;
if(p->state == ZOMBIE){
int rpid = p->pid;
int rstatus = p->xstate; // copy out before the slot is recycled
kfree(p->kstack);
p->kstack = 0;
// Give back anything shared before freevm() decides it owns the
// pages. See proc_release_shared() in kernel/sys/sys_mem.c.
proc_release_shared(p);
freevm(p->pgdir);
p->pid = 0;
p->parent = 0;
p->name[0] = 0;
p->killed = 0;
p->xstate = 0;
p->killsig = 0;
p->pgid = 0;
p->sid = 0;
p->state = UNUSED;
release(&ptable.lock);
if (status) *status = rstatus;
return rpid;
}
}
if(!havekids || myproc()->killed){
release(&ptable.lock);
return -1;
}
if (options & 1) { // WNOHANG
release(&ptable.lock);
return 0;
}
sleep(myproc(), &ptable.lock);
}
}
void
scheduler(void)
{
struct proc *p;
int skipped = 0;
for(;;){
sti();
acquire(&ptable.lock);
for(p = ptable.proc; p < &ptable.proc[NPROC]; p++){
if(p->state != RUNNABLE) {
skipped++;
continue;
}
skipped = 0;
proc_set(p);
switchuvm(p);
p->state = RUNNING;
swtch(&mycpu()->scheduler, p->context);
switchkvm();
proc_set(0);
}
release(&ptable.lock);
if (skipped > NPROC) {
hlt();
skipped = 0;
}
}
}
void
sched(void)
{
int intena;
if(!holding(&ptable.lock))
panic("sched ptable.lock");
if(mycpu()->ncli != 1)
panic("sched locks");
if(myproc()->state == RUNNING)
panic("sched running");
if(readeflags()&FL_IF)
panic("sched interruptible");
intena = mycpu()->intena;
swtch(&myproc()->context, mycpu()->scheduler);
mycpu()->intena = intena;
}
void
yield(void)
{
acquire(&ptable.lock);
myproc()->state = RUNNABLE;
sched();
release(&ptable.lock);
}
void
forkret(void) {
static int first = 1;
release(&ptable.lock);
if (first) {
first = 0;
iinit();
mountinit();
// SchFS is the root filesystem.
//
// Mounted here rather than in main() because mounting reads blocks,
// bread() sleeps, and sleeping needs a process context.
//
// schfs_kmount() reports failure by leaving the volume unmounted rather
// than by returning -- it is a probe elsewhere, where "not SchFS" is an
// ordinary answer. Here it is fatal, so ask afterwards.
schfs_kmount(ROOTDEV);
if (!schfs_kmounted())
panic("schfs root: no SchFS volume on the root device");
// Re-point pid 1's cwd at the real root, now that there is one.
//
// sinit() sets cwd = namei("/") back in main(), which is before this runs.
// The old v1 root survived that because iget(ROOTDEV, ROOTINO) is two
// compile-time constants and touches no disk -- the inode is filled in
// lazily at the first ilock(). SchFS has no constant to offer: the root
// object id comes off the uberblock, so namei("/") that early returns an
// inode built from whatever schfs_kroot() reads out of an unmounted
// volume.
//
// Nothing complains, because absolute paths never consult cwd -- exec
// "/bin/init" works fine. It is the first *relative* lookup that fails,
// and in init that is open("console"): no console means no fd 0/1/2, so
// every shell it spawns exits immediately and init respawns it forever.
// The symptom is a silent boot with no init: ready, which reads like a
// hang and is not one.
{
struct inode *stale = myproc()->cwd;
myproc()->cwd = iget(SCHFS_DEV, schfs_kroot());
if (stale)
iput(stale);
}
}
}
void
sleep(void *chan, struct spinlock *lk)
{
if(myproc() == 0)
panic("sleep");
if(lk == 0)
panic("sleep without lk");
if(lk != &ptable.lock){
acquire(&ptable.lock);
release(lk);
}
myproc()->chan = chan;
myproc()->state = SLEEPING;
sched();
myproc()->chan = 0;
if(lk != &ptable.lock){
release(&ptable.lock);
acquire(lk);
}
}
static void
wakeup1(void *chan)
{
struct proc *p;
for(p = ptable.proc; p < &ptable.proc[NPROC]; p++)
if(p->state == SLEEPING && p->chan == chan)
p->state = RUNNABLE;
}
void
wakeup(void *chan)
{
acquire(&ptable.lock);
wakeup1(chan);
release(&ptable.lock);
}
// Raise a signal on a process: set the pending bit and wake it if it's
// sleeping so blocking syscalls (sigsuspend, pause, read) re-check their
// conditions promptly. Signals with no handler installed fall back to the
// default action — termination for the fatal set, dropped otherwise.
// mksh depends on kill(getpid(), SIGCHLD) genuinely re-raising SIGCHLD
// (its held_sigchld recovery in j_startjob); the previous sys_kill
// silently discarded every non-fatal signal.
int
sigsend(int pid, int sig)
{
struct proc *p;
if(sig <= 0 || sig >= 32)
return -1;
acquire(&ptable.lock);
for(p = ptable.proc; p < &ptable.proc[NPROC]; p++){
if(p->pid == pid && p->state != UNUSED && p->state != ZOMBIE){
p->pending_signals |= 1ULL << sig;
if(p->sig_handler[sig] == 0 &&
(sig == SIGKILL || sig == SIGTERM || sig == SIGABRT ||
sig == SIGSEGV || sig == SIGFPE || sig == SIGBUS ||
sig == SIGILL || sig == SIGINT || sig == SIGQUIT ||
sig == SIGHUP || sig == SIGPIPE || sig == SIGUSR1 ||
sig == SIGUSR2 || sig == SIGALRM))
p->killed = 1;
if(sig == SIGKILL)
p->killed = 1;
if(p->killed && p->killsig == 0)
p->killsig = sig; // so exit() can report the real cause
if(p->state == SLEEPING)
p->state = RUNNABLE;
release(&ptable.lock);
return 0;
}
}
release(&ptable.lock);
return -1;
}
// Deliver a signal to every member of a process group. This is what job
// control actually needs: a shell sends SIGINT/SIGTSTP/SIGHUP to a *group*,
// never to individual pids. sys_kill() with pid <= 0 routes here.
//
// Returns 0 if at least one process was signalled, -1 if the group is empty.
int
sigsend_pgrp(int pgid, int sig)
{
struct proc *p;
int found = 0;
if(sig < 0 || sig >= 32)
return -1;
acquire(&ptable.lock);
for(p = ptable.proc; p < &ptable.proc[NPROC]; p++){
if(p->state == UNUSED || p->state == ZOMBIE)
continue;
if(p->pgid != pgid)
continue;
found = 1;
if(sig == 0)
continue; // signal 0 is an existence probe only
p->pending_signals |= 1ULL << sig;
if(p->sig_handler[sig] == 0 &&
(sig == SIGKILL || sig == SIGTERM || sig == SIGABRT ||
sig == SIGSEGV || sig == SIGFPE || sig == SIGBUS ||
sig == SIGILL || sig == SIGINT || sig == SIGQUIT ||
sig == SIGHUP || sig == SIGPIPE || sig == SIGUSR1 ||
sig == SIGUSR2 || sig == SIGALRM)){
p->killed = 1;
if(p->killsig == 0)
p->killsig = sig;
}
if(sig == SIGKILL){
p->killed = 1;
if(p->killsig == 0)
p->killsig = sig;
}
if(p->state == SLEEPING)
p->state = RUNNABLE;
}
release(&ptable.lock);
return found ? 0 : -1;
}
// Look up a process's process-group id. Returns -1 if no such live process.
int
proc_pgid(int pid)
{
struct proc *p;
int r = -1;
acquire(&ptable.lock);
for(p = ptable.proc; p < &ptable.proc[NPROC]; p++){
if(p->pid == pid && p->state != UNUSED && p->state != ZOMBIE){
r = p->pgid;
break;
}
}
release(&ptable.lock);
return r;
}
// setpgid(pid, pgid) with POSIX's argument shorthands: pid 0 means the caller,
// pgid 0 means "use pid as the group id" (i.e. become a group leader).
int
proc_setpgid(int pid, int pgid)
{
struct proc *p;
int r = -ESRCH;
if(pid < 0 || pgid < 0)
return -EINVAL;
if(pid == 0)
pid = myproc()->pid;
if(pgid == 0)
pgid = pid;
acquire(&ptable.lock);
for(p = ptable.proc; p < &ptable.proc[NPROC]; p++){
if(p->pid != pid || p->state == UNUSED || p->state == ZOMBIE)
continue;
// A caller may only move itself or one of its own children, and never
// across a session boundary.
if(p != myproc() && p->parent != myproc())
r = -ESRCH;
else if(p->sid != myproc()->sid)
r = -EPERM;
else {
p->pgid = pgid;
r = 0;
}
break;
}
release(&ptable.lock);
return r;
}
// setsid(): become leader of a brand-new session and process group. Fails if
// the caller already leads a process group, since that group would otherwise
// end up split across two sessions.
int
proc_setsid(void)
{
struct proc *p;
int r;
acquire(&ptable.lock);
for(p = ptable.proc; p < &ptable.proc[NPROC]; p++){
if(p->state != UNUSED && p->state != ZOMBIE && p->pgid == myproc()->pid){
release(&ptable.lock);
return -EPERM;
}
}
myproc()->sid = myproc()->pid;
myproc()->pgid = myproc()->pid;
r = myproc()->sid;
release(&ptable.lock);
return r;
}
int
kill(int pid)
{
struct proc *p;
acquire(&ptable.lock);
for(p = ptable.proc; p < &ptable.proc[NPROC]; p++){
if(p->pid == pid){
p->killed = 1;
if(p->state == SLEEPING)
p->state = RUNNABLE;
release(&ptable.lock);
return 0;
}
}
release(&ptable.lock);
return -1;
}
void
procdump(void)
{
static char *states[] = {
[UNUSED] "unused",
[EMBRYO] "embryo",
[SLEEPING] "sleep ",
[RUNNABLE] "runble",
[RUNNING] "run ",
[ZOMBIE] "zombie"
};
int i;
struct proc *p;
char *state;
addr_t pc[10];
for(p = ptable.proc; p < &ptable.proc[NPROC]; p++){
if(p->state == UNUSED)
continue;
if(p->state >= 0 && p->state < NELEM(states) && states[p->state])
state = states[p->state];
else
state = "???";
cprintf("%d %s %s", p->pid, state, p->name);
if(p->state == SLEEPING){
getstackpcs((addr_t*)p->context->rbp, pc);
for(i=0; i<10 && pc[i] != 0; i++)
cprintf(" %p", pc[i]);
}
cprintf("\n");
}
}
// ---------------------------------------------------------------------------
// Snapshots, for /proc
//
// procfs must not walk ptable itself: it would need the lock, the struct
// layout, and a rule about what happens when a process exits between the
// readdir that listed it and the open that reads it. These two copy what is
// needed out from under the lock and hand back a value, so a process that dies
// mid-listing produces a stale line rather than a use-after-free.
// ---------------------------------------------------------------------------
// Fill `out` with the `idx`-th live process (0-based, in table order).
// Returns 0 on success, -1 when idx is past the end.
int
proc_snapshot(int idx, struct procinfo *out)
{
struct proc *p;
int n = 0;
acquire(&ptable.lock);
for (p = ptable.proc; p < &ptable.proc[NPROC]; p++) {
if (p->state == UNUSED)
continue;
if (n++ != idx)
continue;
out->pid = p->pid;
out->ppid = p->parent ? p->parent->pid : 0;
out->pgid = p->pgid;
out->sid = p->sid;
out->uid = p->uid;
out->state = p->state;
out->sz = p->sz;
out->killed = p->killed;
safestrcpy(out->name, p->name, sizeof(out->name));
safestrcpy(out->exe, p->exe, sizeof(out->exe));
release(&ptable.lock);
return 0;
}
release(&ptable.lock);
return -1;
}
// The same, by pid rather than index.
int
proc_snapshot_pid(int pid, struct procinfo *out)
{
int i;
struct procinfo pi;
for (i = 0; proc_snapshot(i, &pi) == 0; i++)
if (pi.pid == pid) { *out = pi; return 0; }
return -1;
}
int proc_num(void)
{
struct proc *p;
int num = 0;
acquire(&ptable.lock);
for (p = ptable.proc; p < &ptable.proc[NPROC]; p++)
if (p->state != UNUSED) num++;
release(&ptable.lock);
return num;
}
int proc_exists(int pid)
{
struct proc *p;
acquire(&ptable.lock);
for (p = ptable.proc; p < &ptable.proc[NPROC]; p++) {
if (p->pid == pid && p->state != UNUSED && p->state != ZOMBIE) {
release(&ptable.lock);
return 1;
}
}
release(&ptable.lock);
return 0;
}

33
kernel/sys/linux_abi.c Normal file
View file

@ -0,0 +1,33 @@
// SPDX-License-Identifier: Apache-2.0
// Linux x86_64 syscall number -> Schunix syscall number translation.
// Enabled per process by sys_enable_linux_abi (see kernel/sys/sys_compat.c).
//
// This file used to be the second half of a two-place registration scheme: a
// hand-written `#define LINUX_foo N` for every Linux number, plus a hand-written
// map entry pointing it at SYS_foo. Adding a syscall meant three edits across
// two files, and forgetting this one left the syscall unreachable from every
// musl-linked program while looking perfectly registered in table.inc.
//
// The map is now generated from kernel/sys/table.inc, which carries the Linux
// number alongside the native one. There is nothing to hand-edit here.
#include "types.h"
#include "syscall.h"
#define LINUX_NSYSCALL NSYSCALLS
// Index: Linux x86_64 syscall number. Value: Schunix SYS_* number, or 0 for
// "not implemented" (syscall() then returns -ENOSYS).
//
// Syscalls declared NO_LINUX in table.inc all land on the single scratch index
// NO_LINUX. They overwrite each other there, which is harmless: no real Linux
// syscall uses that number, so the slot is never dispatched.
int linux_syscall_map[LINUX_NSYSCALL] = {
#define SYSCALL(nr, lnr, name) [lnr] = SYS_##name,
#define SYSCALL_ALIAS(lnr, name) [lnr] = SYS_##name,
#include "table.inc"
#undef SYSCALL
#undef SYSCALL_ALIAS
};
int linux_syscall_map_size = LINUX_NSYSCALL;

162
kernel/sys/sys_compat.c Normal file
View file

@ -0,0 +1,162 @@
// POSIX / Linux-ABI compatibility syscalls.
// These are stubs or thin wrappers that make musl and ksh happy
// without implementing full semantics.
#include "types.h"
#include "defs.h"
#include "param.h"
#include "memlayout.h"
#include "mmu.h"
#include "traps.h"
#include "proc.h"
#include "x86.h"
#include "syscall.h"
#include "posix.h"
#include "uaccess.h"
#include "errno.h"
extern int linux_syscall_map[];
extern int linux_syscall_map_size;
// enable_linux_abi: switch this process to Linux syscall numbering.
addr_t
sys_enable_linux_abi(void)
{
myproc()->syscall_map = linux_syscall_map;
myproc()->map_size = linux_syscall_map_size;
return 0;
}
// set_tid_address: musl calls this at startup to register its TID location.
addr_t
sys_set_tid_address(void)
{
addr_t tidp;
argaddr(0, &tidp);
if (tidp)
put_user(myproc()->pid, tidp);
return myproc()->pid;
}
// set_robust_list: robust mutex cleanup on exit — stub.
addr_t sys_set_robust_list(void) { return 0; }
// futex: used by musl for single-threaded lock primitives.
addr_t
sys_futex(void)
{
int op;
argint(1, &op);
op &= ~(FUTEX_WAIT_PRIVATE - FUTEX_WAIT);
if (op == FUTEX_WAKE || op == FUTEX_WAKE_PRIVATE)
return 0;
return -EAGAIN; // no blocking, musl retries
}
// arch_prctl: save FS_BASE for TLS.
addr_t
sys_arch_prctl(void)
{
int code;
addr_t addr;
argint(0, &code);
argaddr(1, &addr);
if (code == ARCH_SET_FS) {
myproc()->fs_base = addr;
return 0;
}
if (code == ARCH_GET_FS) {
if (put_user(myproc()->fs_base, addr) < 0)
return -EFAULT;
return 0;
}
return -EINVAL;
}
addr_t
sys_getrlimit(void)
{
int resource;
struct rlimit *rlp;
argint(0, &resource);
if (argptr(1, (char**)&rlp, sizeof(*rlp)) < 0)
return -1;
rlp->rlim_cur = (uint64)-1;
rlp->rlim_max = (uint64)-1;
return 0;
}
addr_t
sys_prlimit(void)
{
struct rlimit *oldrlp;
if (argptr(3, (char**)&oldrlp, sizeof(*oldrlp)) == 0 && oldrlp) {
oldrlp->rlim_cur = (uint64)-1;
oldrlp->rlim_max = (uint64)-1;
}
return 0;
}
// uname: return fake but plausible kernel/host info
addr_t
sys_uname(void)
{
struct utsname {
char sysname[65];
char nodename[65];
char release[65];
char version[65];
char machine[65];
char domainname[65];
};
addr_t ubuf;
argaddr(0, &ubuf);
struct utsname *u = (struct utsname*)user_ptr(ubuf, sizeof(*u));
if (!u) return -EFAULT;
memset(u, 0, sizeof(*u));
memmove(u->sysname, "Schunix", 8);
memmove(u->nodename, "schunix", 8);
memmove(u->release, "0.1.0", 6);
memmove(u->version, "#1", 3);
memmove(u->machine, "x86_64", 7);
memmove(u->domainname, "(none)", 7);
return 0;
}
// socket/bind/listen/accept/connect live in sys_net.c now that they do
// something.
// syslog: kernel logging facility — no-op for user processes.
addr_t sys_syslog(void) { return 0; }
// getrandom: fill buffer with pseudo-random bytes.
addr_t sys_getrandom(void)
{
addr_t bufaddr;
int len, flags;
argaddr(0, &bufaddr); argint(1, &len); argint(2, &flags);
if (len <= 0) return 0;
uint8 *buf = (uint8*)user_ptr(bufaddr, (uint64)len);
if (!buf) return -EFAULT;
// LCG seeded from proc pointer + pid; no lock needed (read-only fields).
uint rseed = (uint)(uintptr_t)myproc() ^ (uint)myproc()->pid;
for (int i = 0; i < len; i++) {
rseed = rseed * 1664525u + 1013904223u;
buf[i] = (uint8)(rseed >> 16);
}
return len;
}
// pselect6 / ppoll live in sys_poll.c alongside select/poll.
// epoll: not implemented.
addr_t sys_epoll_create1(void) { return -ENOSYS; }
addr_t sys_epoll_ctl(void) { return -ENOSYS; }
addr_t sys_epoll_wait(void) { return -ENOSYS; }
// signalfd / eventfd / timerfd: not implemented.
addr_t sys_signalfd4(void) { return -ENOSYS; }
addr_t sys_eventfd2(void) { return -ENOSYS; }
addr_t sys_timerfd_create(void) { return -ENOSYS; }
addr_t sys_timerfd_settime(void){ return -ENOSYS; }
addr_t sys_timerfd_gettime(void){ return -ENOSYS; }

1413
kernel/sys/sys_fs.c Normal file

File diff suppressed because it is too large Load diff

479
kernel/sys/sys_mem.c Normal file
View file

@ -0,0 +1,479 @@
#include "types.h"
#include "defs.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 = vga_fb_phys();
uint64 fbsz = PGROUNDUP(vga_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; }

346
kernel/sys/sys_net.c Normal file
View file

@ -0,0 +1,346 @@
// Socket syscalls.
//
// The kernel side is in kernel/net/unix.c; this file is only the translation
// between a syscall's user-supplied `struct sockaddr` and the path string the
// socket layer actually wants, plus the file-descriptor bookkeeping.
//
// Only AF_UNIX is answered. AF_INET is recognised and refused with
// EAFNOSUPPORT rather than EINVAL, because a program that probes for IPv6 and
// falls back to IPv4 needs to be able to tell "this address family does not
// exist here" from "you passed nonsense".
#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 "posix.h"
#include "syscall.h"
#include "uaccess.h"
#include "errno.h"
#include "socket.h"
// Pull the sun_path out of a user `struct sockaddr_un`, NUL-terminated.
//
// The length the caller gives is checked rather than trusted: musl passes
// `offsetof(sockaddr_un, sun_path) + strlen(path) + 1`, which is shorter than
// the full struct, so reading sizeof(struct sockaddr_un) from user memory
// would fault on a path that sits near the end of a page.
static int
fetch_sun_path(addr_t uaddr, int ulen, char *out)
{
struct sockaddr_un sa;
int plen;
if (ulen < (int)sizeof(sa.sun_family) || ulen > (int)sizeof(sa))
return -EINVAL;
memset(&sa, 0, sizeof(sa));
if (copy_from_user(&sa, uaddr, (uint64)ulen) < 0)
return -EFAULT;
if (sa.sun_family == AF_INET)
return -EAFNOSUPPORT;
if (sa.sun_family != AF_UNIX)
return -EAFNOSUPPORT;
plen = ulen - (int)sizeof(sa.sun_family);
if (plen <= 0)
return -EINVAL;
if (plen > UNIX_PATH_MAX - 1)
plen = UNIX_PATH_MAX - 1;
memmove(out, sa.sun_path, (uint)plen);
out[plen] = '\0';
// A NUL inside the copied region is fine -- it just ends the path early,
// which is what a caller passing the full struct length does.
out[UNIX_PATH_MAX - 1] = '\0';
return 0;
}
// Wrap an established socket in a file and a descriptor. Consumes the
// socket's reference on success; on failure the caller still owns it.
static int
sock_to_fd(struct socket *s)
{
struct file *f;
int fd;
if ((f = filealloc()) == 0)
return -ENFILE;
if ((fd = fdalloc(f)) < 0) {
f->ref = 0;
return -EMFILE;
}
f->type = FD_SOCKET;
f->sock = s;
f->readable = 1;
f->writable = 1;
f->off = 0;
return fd;
}
// Resolve a descriptor to the socket behind it.
static int
argsock(int n, struct socket **ps)
{
int fd;
struct file *f;
if (argint(n, &fd) < 0)
return -EBADF;
if (fd < 0 || fd >= NOFILE || !(f = myproc()->ofile[fd]))
return -EBADF;
if (f->type != FD_SOCKET)
return -ENOTSOCK;
*ps = f->sock;
return 0;
}
SYSCALL_DEFINE(socket)
{
int domain, type, protocol;
struct socket *s;
int fd;
argint(0, &domain);
argint(1, &type);
argint(2, &protocol);
if (domain != AF_UNIX)
return -EAFNOSUPPORT;
// SOCK_CLOEXEC / SOCK_NONBLOCK ride in the high bits of `type`. Neither
// is honoured yet, but masking them off means a musl program that always
// sets SOCK_CLOEXEC still gets a socket instead of EINVAL.
type &= 0xFF;
if (type != SOCK_STREAM)
return -EPROTOTYPE;
if (protocol != 0)
return -EPROTONOSUPPORT;
if ((s = socknew(domain, type)) == 0)
return -ENFILE;
if ((fd = sock_to_fd(s)) < 0) {
sockclose(s);
return fd;
}
return fd;
}
SYSCALL_DEFINE(bind)
{
struct socket *s;
addr_t uaddr;
int ulen, r;
char path[UNIX_PATH_MAX];
if ((r = argsock(0, &s)) < 0)
return r;
argaddr(1, &uaddr);
argint(2, &ulen);
if ((r = fetch_sun_path(uaddr, ulen, path)) < 0)
return r;
return sockbind(s, path);
}
SYSCALL_DEFINE(listen)
{
struct socket *s;
int backlog, r;
if ((r = argsock(0, &s)) < 0)
return r;
argint(1, &backlog);
return socklisten(s, backlog);
}
SYSCALL_DEFINE(connect)
{
struct socket *s;
addr_t uaddr;
int ulen, r;
char path[UNIX_PATH_MAX];
if ((r = argsock(0, &s)) < 0)
return r;
argaddr(1, &uaddr);
argint(2, &ulen);
if ((r = fetch_sun_path(uaddr, ulen, path)) < 0)
return r;
return sockconnect(s, path);
}
SYSCALL_DEFINE(accept)
{
struct socket *s, *c;
addr_t uaddr, ulenp;
int r, fd;
if ((r = argsock(0, &s)) < 0)
return r;
argaddr(1, &uaddr);
argaddr(2, &ulenp);
if ((c = sockaccept(s)) == 0)
return -EINVAL;
if ((fd = sock_to_fd(c)) < 0) {
sockclose(c);
return fd;
}
// The peer address of a Unix socket that never bound a name is the empty
// one: family AF_UNIX and a zero-length path. Reporting it is optional
// (both pointers may be null) but a caller that asks must not be left
// with an uninitialised struct it then prints.
if (uaddr && ulenp) {
struct sockaddr_un sa;
int len = (int)sizeof(sa.sun_family);
int want = 0;
memset(&sa, 0, sizeof(sa));
sa.sun_family = AF_UNIX;
safestrcpy(sa.sun_path, sockname(c), UNIX_PATH_MAX);
len += (int)strlen(sa.sun_path) + 1;
if (get_user(want, ulenp) < 0)
return fd; // the fd is already live; do not fail it
if (want > len)
want = len;
if (want > 0)
copy_to_user(uaddr, &sa, (uint64)want);
put_user(len, ulenp);
}
return fd;
}
SYSCALL_DEFINE(socketpair)
{
int domain, type, protocol;
addr_t usv;
struct socket *a, *b;
int fd0, fd1, r;
argint(0, &domain);
argint(1, &type);
argint(2, &protocol);
argaddr(3, &usv);
if (domain != AF_UNIX)
return -EAFNOSUPPORT;
type &= 0xFF;
if (type != SOCK_STREAM)
return -EPROTOTYPE;
if ((r = sockpair(&a, &b)) < 0)
return r;
if ((fd0 = sock_to_fd(a)) < 0) {
sockclose(a);
sockclose(b);
return fd0;
}
if ((fd1 = sock_to_fd(b)) < 0) {
myproc()->ofile[fd0] = 0;
sockclose(a);
sockclose(b);
return fd1;
}
int sv[2] = { fd0, fd1 };
if (copy_to_user(usv, sv, sizeof(sv)) < 0) {
myproc()->ofile[fd0] = 0;
myproc()->ofile[fd1] = 0;
sockclose(a);
sockclose(b);
return -EFAULT;
}
return 0;
}
// send/recv with no flags are read/write. Implementing them here rather than
// leaving them ENOSYS costs four lines and saves every ported program from
// having to be rewritten to use write().
SYSCALL_DEFINE(sendto)
{
struct socket *s;
addr_t ubuf;
int len, flags, r;
if ((r = argsock(0, &s)) < 0)
return r;
argaddr(1, &ubuf);
argint(2, &len);
argint(3, &flags);
if (len < 0)
return -EINVAL;
char *p = (char *)user_ptr(ubuf, (uint64)len);
if (len && !p)
return -EFAULT;
return sockwrite(s, p, len);
}
SYSCALL_DEFINE(recvfrom)
{
struct socket *s;
addr_t ubuf;
int len, flags, r;
if ((r = argsock(0, &s)) < 0)
return r;
argaddr(1, &ubuf);
argint(2, &len);
argint(3, &flags);
if (len < 0)
return -EINVAL;
char *p = (char *)user_ptr(ubuf, (uint64)len);
if (len && !p)
return -EFAULT;
return sockread(s, p, len);
}
// shutdown(2). Half-close is not modelled -- there is one buffer per
// direction and no per-direction flag -- so SHUT_WR and SHUT_RDWR both mean
// "the peer sees end-of-file", which is the behaviour programs depend on.
SYSCALL_DEFINE(sockshutdown)
{
struct socket *s;
int how, r;
if ((r = argsock(0, &s)) < 0)
return r;
argint(1, &how);
return sock_shutdown(s, how);
}
// getsockopt/setsockopt: accepted and ignored. A server that sets SO_REUSEADDR
// and treats failure as fatal is common enough to be worth not failing.
SYSCALL_DEFINE(setsockopt) { return 0; }
SYSCALL_DEFINE(getsockopt)
{
addr_t uval, ulen;
int zero = 0, len = 0;
argaddr(3, &uval);
argaddr(4, &ulen);
if (uval && ulen) {
if (get_user(len, ulen) == 0 && len >= (int)sizeof(int)) {
put_user(zero, uval);
len = sizeof(int);
put_user(len, ulen);
}
}
return 0;
}

292
kernel/sys/sys_poll.c Normal file
View file

@ -0,0 +1,292 @@
// poll(2) / select(2) — real readiness-based I/O multiplexing over pipes,
// the console tty, and regular files (via file_poll()). This is the
// foundation every event-loop program (dash, toybox netcat, eventually a
// display server) sits on; the previous stubs returned -ENOSYS, which made
// anything poll-driven bail out instantly.
//
// Blocking strategy: scan all fds; if nothing is ready and the timeout
// allows, sleep until the next timer tick (10ms granularity at HZ=100) and
// rescan. A pending unblocked signal or kill interrupts the wait with
// -EINTR so handlers get a chance to run, matching POSIX.
#include "types.h"
#include "defs.h"
#include "param.h"
#include "spinlock.h"
#include "sleeplock.h"
#include "fs.h"
#include "file.h"
#include "proc.h"
#include "syscall.h"
#include "posix.h"
#include "uaccess.h"
#include "errno.h"
#define POLL_MAX_FDS 64
// Sleep one timer tick, or until killed/signaled. Returns 0 to keep
// waiting, -EINTR if the wait must be abandoned.
static int
poll_block_one_tick(void)
{
if (myproc()->killed)
return -EINTR;
if (myproc()->pending_signals & ~myproc()->signal_mask)
return -EINTR;
acquire(&tickslock);
uint t0 = ticks;
while (ticks == t0 && !myproc()->killed &&
!(myproc()->pending_signals & ~myproc()->signal_mask))
sleep(&ticks, &tickslock);
release(&tickslock);
return 0;
}
// Shared core: fds[] already in kernel memory. timeout_ticks < 0 means
// wait forever. Returns number of fds with nonzero revents, 0 on timeout,
// -EINTR if interrupted.
static int
do_poll(struct pollfd *fds, int nfds, int timeout_ticks)
{
for (;;) {
int nready = 0;
for (int i = 0; i < nfds; i++) {
fds[i].revents = 0;
if (fds[i].fd < 0)
continue; // negative fd: ignore, per poll(2)
if (fds[i].fd >= NOFILE || !myproc()->ofile[fds[i].fd]) {
fds[i].revents = POLLNVAL;
nready++;
continue;
}
int rev = file_poll(myproc()->ofile[fds[i].fd], fds[i].events);
if (rev) {
fds[i].revents = (short)rev;
nready++;
}
}
if (nready > 0 || timeout_ticks == 0)
return nready;
if (poll_block_one_tick() < 0)
return -EINTR;
if (timeout_ticks > 0)
timeout_ticks--;
}
}
// poll(fds, nfds, timeout_ms)
addr_t
sys_poll(void)
{
addr_t ufds;
int nfds, timeout_ms;
struct pollfd fds[POLL_MAX_FDS];
argaddr(0, &ufds);
argint(1, &nfds);
argint(2, &timeout_ms);
if (nfds < 0 || nfds > POLL_MAX_FDS)
return -EINVAL;
if (nfds > 0 && copy_from_user(fds, ufds, nfds * sizeof(struct pollfd)) < 0)
return -EFAULT;
int timeout_ticks = timeout_ms < 0 ? -1 : (timeout_ms + 9) / 10;
int r = do_poll(fds, nfds, timeout_ticks);
if (r >= 0 && nfds > 0 &&
copy_to_user(ufds, fds, nfds * sizeof(struct pollfd)) < 0)
return -EFAULT;
return r;
}
// ppoll(fds, nfds, timespec*, sigmask) — sigmask is ignored (poll already
// breaks out for any deliverable signal); NULL timespec = wait forever.
addr_t
sys_ppoll(void)
{
addr_t ufds, utsp;
int nfds;
struct pollfd fds[POLL_MAX_FDS];
argaddr(0, &ufds);
argint(1, &nfds);
argaddr(2, &utsp);
if (nfds < 0 || nfds > POLL_MAX_FDS)
return -EINVAL;
if (nfds > 0 && copy_from_user(fds, ufds, nfds * sizeof(struct pollfd)) < 0)
return -EFAULT;
int timeout_ticks = -1;
if (utsp) {
int64 ts[2]; // {tv_sec, tv_nsec}
if (copy_from_user(ts, utsp, sizeof(ts)) < 0)
return -EFAULT;
timeout_ticks = (int)(ts[0] * 100 + ts[1] / 10000000);
}
int r = do_poll(fds, nfds, timeout_ticks);
if (r >= 0 && nfds > 0 &&
copy_to_user(ufds, fds, nfds * sizeof(struct pollfd)) < 0)
return -EFAULT;
return r;
}
// select(nfds, readfds, writefds, exceptfds, timeval*). fd_set is a bit
// array; with NOFILE=128 the interesting part fits in two 64-bit words.
#define FDSET_WORDS (NOFILE / 64)
static int
fdset_fetch(addr_t uaddr, uint64 *set)
{
if (!uaddr) {
set[0] = set[1] = 0;
return 0;
}
return copy_from_user(set, uaddr, FDSET_WORDS * sizeof(uint64));
}
static int
fdset_isset(uint64 *set, int fd) { return (set[fd / 64] >> (fd % 64)) & 1; }
static void
fdset_set(uint64 *set, int fd) { set[fd / 64] |= 1ULL << (fd % 64); }
addr_t
sys_select(void)
{
int nfds;
addr_t urd, uwr, uex, utv;
uint64 rd[FDSET_WORDS], wr[FDSET_WORDS], ex[FDSET_WORDS];
uint64 ord[FDSET_WORDS] = {0}, owr[FDSET_WORDS] = {0};
argint(0, &nfds);
argaddr(1, &urd);
argaddr(2, &uwr);
argaddr(3, &uex);
argaddr(4, &utv);
if (nfds < 0 || nfds > NOFILE)
return -EINVAL;
if (fdset_fetch(urd, rd) < 0 || fdset_fetch(uwr, wr) < 0 ||
fdset_fetch(uex, ex) < 0)
return -EFAULT;
int timeout_ticks = -1;
if (utv) {
int64 tv[2]; // {tv_sec, tv_usec}
if (copy_from_user(tv, utv, sizeof(tv)) < 0)
return -EFAULT;
timeout_ticks = (int)(tv[0] * 100 + tv[1] / 10000);
}
for (;;) {
int nready = 0;
for (int fd = 0; fd < nfds; fd++) {
int want_r = fdset_isset(rd, fd);
int want_w = fdset_isset(wr, fd);
if (!want_r && !want_w)
continue;
struct file *f = myproc()->ofile[fd];
if (!f)
return -EBADF;
int rev = file_poll(f, (want_r ? POLLIN : 0) | (want_w ? POLLOUT : 0));
// Hangup/error count as readable: a read() will not block
// (it returns EOF/error immediately) — same rule Linux uses.
if (want_r && (rev & (POLLIN | POLLHUP | POLLERR))) {
fdset_set(ord, fd);
nready++;
}
if (want_w && (rev & (POLLOUT | POLLERR))) {
fdset_set(owr, fd);
nready++;
}
}
if (nready > 0 || timeout_ticks == 0) {
if (urd && copy_to_user(urd, ord, sizeof(ord)) < 0) return -EFAULT;
if (uwr && copy_to_user(uwr, owr, sizeof(owr)) < 0) return -EFAULT;
if (uex) { // no exceptional conditions exist here; report none
uint64 zero[FDSET_WORDS] = {0};
if (copy_to_user(uex, zero, sizeof(zero)) < 0) return -EFAULT;
}
return nready;
}
if (poll_block_one_tick() < 0)
return -EINTR;
if (timeout_ticks > 0)
timeout_ticks--;
}
}
// pselect6: select with a timespec and (ignored) sigmask.
addr_t
sys_pselect6(void)
{
// Delegate: args 0-3 line up with select's; arg 4 is a timespec instead
// of a timeval and arg 5 the sigmask. Reuse sys_select's field reads but
// treat the timeout as nanoseconds.
int nfds;
addr_t urd, uwr, uex, utsp;
argint(0, &nfds);
argaddr(1, &urd);
argaddr(2, &uwr);
argaddr(3, &uex);
argaddr(4, &utsp);
// Convert the timespec (if any) into a timeval in place on the user
// stack? No — simplest correct route: temporarily run the same logic.
// sys_select re-reads the same registers, so just fix up the timeout
// interpretation here by patching r8 semantics is not possible; instead
// duplicate the small tail:
uint64 rd[FDSET_WORDS], wr[FDSET_WORDS], ex[FDSET_WORDS];
uint64 ord[FDSET_WORDS] = {0}, owr[FDSET_WORDS] = {0};
if (nfds < 0 || nfds > NOFILE)
return -EINVAL;
if (fdset_fetch(urd, rd) < 0 || fdset_fetch(uwr, wr) < 0 ||
fdset_fetch(uex, ex) < 0)
return -EFAULT;
int timeout_ticks = -1;
if (utsp) {
int64 ts[2];
if (copy_from_user(ts, utsp, sizeof(ts)) < 0)
return -EFAULT;
timeout_ticks = (int)(ts[0] * 100 + ts[1] / 10000000);
}
for (;;) {
int nready = 0;
for (int fd = 0; fd < nfds; fd++) {
int want_r = fdset_isset(rd, fd);
int want_w = fdset_isset(wr, fd);
if (!want_r && !want_w)
continue;
struct file *f = myproc()->ofile[fd];
if (!f)
return -EBADF;
int rev = file_poll(f, (want_r ? POLLIN : 0) | (want_w ? POLLOUT : 0));
if (want_r && (rev & (POLLIN | POLLHUP | POLLERR))) {
fdset_set(ord, fd);
nready++;
}
if (want_w && (rev & (POLLOUT | POLLERR))) {
fdset_set(owr, fd);
nready++;
}
}
if (nready > 0 || timeout_ticks == 0) {
if (urd && copy_to_user(urd, ord, sizeof(ord)) < 0) return -EFAULT;
if (uwr && copy_to_user(uwr, owr, sizeof(owr)) < 0) return -EFAULT;
if (uex) {
uint64 zero[FDSET_WORDS] = {0};
if (copy_to_user(uex, zero, sizeof(zero)) < 0) return -EFAULT;
}
return nready;
}
if (poll_block_one_tick() < 0)
return -EINTR;
if (timeout_ticks > 0)
timeout_ticks--;
}
}

451
kernel/sys/sys_proc.c Normal file
View file

@ -0,0 +1,451 @@
#include "types.h"
#include "defs.h"
#include "param.h"
#include "memlayout.h"
#include "mmu.h"
#include "proc.h"
#include "x86.h"
#include "syscall.h"
#include "posix.h"
#include "uaccess.h"
#include "errno.h"
addr_t sys_fork(void) { return fork(); }
addr_t sys_wait(void) { return wait(); }
// _exit(status)/exit_group(status): the low 8 bits of the argument are what
// wait() reports back through WEXITSTATUS.
addr_t
sys_exit(void)
{
int code = 0;
argint(0, &code);
exit(W_EXITCODE(code, 0));
return 0;
}
addr_t sys_exit_group(void) { return sys_exit(); }
addr_t
sys_kill(void)
{
int pid, sig;
argint(0, &pid);
argint(1, &sig);
// POSIX target selection:
// pid > 0 -> that process
// pid == 0 -> every process in the caller's own group
// pid == -1 -> every process we may signal (approximated as our group)
// pid < -1 -> the process group with id -pid
if (pid == 0 || pid == -1)
return sigsend_pgrp(myproc()->pgid, sig) < 0 ? -ESRCH : 0;
if (pid < -1)
return sigsend_pgrp(-pid, sig) < 0 ? -ESRCH : 0;
// sig == 0: existence check only
if (sig == 0)
return proc_exists(pid) ? 0 : -ESRCH;
// Raise the signal for real: pending bit + wakeup, handler delivery on
// the target's next syscall boundary, default action if unhandled.
return sigsend(pid, sig) < 0 ? -ESRCH : 0;
}
addr_t sys_getpid(void) { return myproc()->pid; }
addr_t sys_getppid(void) { return myproc()->parent ? myproc()->parent->pid : 1; }
addr_t sys_gettid(void) { return myproc()->pid; } // single-threaded: tid == pid
addr_t
sys_waitpid(void)
{
int pid, options;
addr_t statusp;
// status is optional (NULL is standard, valid usage — waitpid(pid, NULL,
// WNOHANG) — so this must NOT go through argptr(), which rejects a NULL/
// unmapped pointer outright).
if (argint(0, &pid) < 0 || argaddr(1, &statusp) < 0 || argint(2, &options) < 0)
return -1;
int status;
int wpid = waitpid(pid, &status, options);
if (wpid > 0 && statusp)
put_user(status, statusp);
return wpid;
}
addr_t
sys_wait4(void)
{
int pid, options;
addr_t statusp;
argint(0, &pid);
argaddr(1, &statusp);
argint(2, &options);
int status;
int wpid = waitpid(pid, &status, options);
if (wpid <= 0)
return wpid;
if (statusp)
put_user(status, statusp);
return wpid;
}
addr_t
sys_clone(void)
{
int flags;
argint(0, &flags);
if (flags & (CLONE_VM | CLONE_THREAD | CLONE_SIGHAND))
return -ENOSYS;
return fork();
}
addr_t
sys_sleep(void)
{
int n;
uint ticks0;
if (argint(0, &n) < 0)
return -1;
acquire(&tickslock);
ticks0 = ticks;
while (ticks - ticks0 < (uint)n) {
if (myproc()->killed) { release(&tickslock); return -1; }
sleep(&ticks, &tickslock);
}
release(&tickslock);
return 0;
}
addr_t
sys_uptime(void)
{
uint xticks;
acquire(&tickslock);
xticks = ticks;
release(&tickslock);
return xticks;
}
addr_t sys_reboot(void)
{
cprintf("System is rebooting...\n");
outb(0xCF9, 0x06);
outb(0x64, 0xFE);
return 0;
}
addr_t sys_shutdown(void)
{
cprintf("System is shutting down...\n");
outw(0x604, 0x2000);
outw(0xB004, 0x2000);
outw(0x4004, 0x3400);
return 0;
}
// --- Sessions and process groups ---------------------------------------
// Job control is defined entirely in terms of these. They used to be stubs
// returning the caller's own pid, which made every process look like its own
// group leader and left a shell's wait loop unable to make progress.
addr_t sys_setsid(void) { return proc_setsid(); }
addr_t
sys_getsid(void)
{
int pid;
argint(0, &pid);
if (pid == 0 || pid == myproc()->pid)
return myproc()->sid;
return proc_exists(pid) ? (addr_t)myproc()->sid : (addr_t)-ESRCH;
}
addr_t
sys_getpgid(void)
{
int pid;
argint(0, &pid);
if (pid == 0 || pid == myproc()->pid)
return myproc()->pgid;
int g = proc_pgid(pid);
return g < 0 ? (addr_t)-ESRCH : (addr_t)g;
}
addr_t
sys_setpgid(void)
{
int pid, pgid;
argint(0, &pid);
argint(1, &pgid);
return proc_setpgid(pid, pgid);
}
addr_t sys_getpgrp(void) { return myproc()->pgid; }
// --- User and group identity -------------------------------------------
// Tracked for real now. Nothing *enforces* permissions with these yet -- the
// on-disk format has no owner field until SchFS -- but they are no longer
// invented, so getuid() after setuid() reports what actually happened.
addr_t sys_getuid(void) { return myproc()->uid; }
addr_t sys_geteuid(void) { return myproc()->euid; }
addr_t sys_getgid(void) { return myproc()->gid; }
addr_t sys_getegid(void) { return myproc()->egid; }
// Privileged (euid 0) callers may set any id; an unprivileged caller may only
// switch between its real and saved ids. Standard POSIX rule.
addr_t
sys_setuid(void)
{
int uid;
argint(0, &uid);
if (uid < 0)
return -EINVAL;
if (myproc()->euid == 0) {
myproc()->uid = myproc()->euid = myproc()->suid = (uint)uid;
return 0;
}
if ((uint)uid != myproc()->uid && (uint)uid != myproc()->suid)
return -EPERM;
myproc()->euid = (uint)uid;
return 0;
}
addr_t
sys_setgid(void)
{
int gid;
argint(0, &gid);
if (gid < 0)
return -EINVAL;
if (myproc()->euid == 0) {
myproc()->gid = myproc()->egid = myproc()->sgid = (uint)gid;
return 0;
}
if ((uint)gid != myproc()->gid && (uint)gid != myproc()->sgid)
return -EPERM;
myproc()->egid = (uint)gid;
return 0;
}
addr_t sys_umask(void) { return 022; }
addr_t
sys_setreuid(void)
{
int ruid, euid;
argint(0, &ruid); argint(1, &euid);
if (myproc()->euid != 0)
return -EPERM;
if (ruid >= 0) myproc()->uid = (uint)ruid;
if (euid >= 0) myproc()->euid = (uint)euid;
myproc()->suid = myproc()->euid;
return 0;
}
addr_t
sys_setregid(void)
{
int rgid, egid;
argint(0, &rgid); argint(1, &egid);
if (myproc()->euid != 0)
return -EPERM;
if (rgid >= 0) myproc()->gid = (uint)rgid;
if (egid >= 0) myproc()->egid = (uint)egid;
myproc()->sgid = myproc()->egid;
return 0;
}
addr_t
sys_setresuid(void)
{
int r, e, sv;
argint(0, &r); argint(1, &e); argint(2, &sv);
if (myproc()->euid != 0)
return -EPERM;
if (r >= 0) myproc()->uid = (uint)r;
if (e >= 0) myproc()->euid = (uint)e;
if (sv >= 0) myproc()->suid = (uint)sv;
return 0;
}
addr_t
sys_setresgid(void)
{
int r, e, sv;
argint(0, &r); argint(1, &e); argint(2, &sv);
if (myproc()->euid != 0)
return -EPERM;
if (r >= 0) myproc()->gid = (uint)r;
if (e >= 0) myproc()->egid = (uint)e;
if (sv >= 0) myproc()->sgid = (uint)sv;
return 0;
}
// Linux-specific; the fs id follows the effective id here.
addr_t sys_setfsuid(void) { return myproc()->euid; }
addr_t sys_setfsgid(void) { return myproc()->egid; }
addr_t
sys_getresuid(void)
{
addr_t rp, ep, sp;
argaddr(0, &rp); argaddr(1, &ep); argaddr(2, &sp);
if (rp) put_user(myproc()->uid, rp);
if (ep) put_user(myproc()->euid, ep);
if (sp) put_user(myproc()->suid, sp);
return 0;
}
addr_t
sys_getresgid(void)
{
addr_t rp, ep, sp;
argaddr(0, &rp); argaddr(1, &ep); argaddr(2, &sp);
if (rp) put_user(myproc()->gid, rp);
if (ep) put_user(myproc()->egid, ep);
if (sp) put_user(myproc()->sgid, sp);
return 0;
}
addr_t sys_getgroups(void)
{
int size;
argint(0, &size);
return 0; // no supplementary groups
}
addr_t sys_setgroups(void) { return 0; }
// --- Process/scheduling extensions ---
addr_t sys_sched_yield(void) { return 0; }
addr_t sys_sched_getaffinity(void)
{
int pid, cpusetsize;
addr_t maskp;
argint(0, &pid); argint(1, &cpusetsize); argaddr(2, &maskp);
if (maskp && cpusetsize > 0) {
char *m = (char*)user_ptr(maskp, (uint64)cpusetsize);
if (m) { memset(m, 0, cpusetsize); m[0] = 1; } // CPU 0 only
}
return 0;
}
addr_t sys_sched_setaffinity(void) { return 0; }
addr_t sys_prctl(void)
{
int op;
addr_t arg2;
argint(0, &op);
argaddr(1, &arg2);
if (op == PR_SET_NAME) {
char *name;
if (argstr(1, &name) < 0) return -1;
safestrcpy(myproc()->name, name, sizeof(myproc()->name));
return 0;
}
if (op == PR_GET_NAME) {
char *buf = (char*)user_ptr(arg2, 16);
if (!buf) return -EFAULT;
safestrcpy(buf, myproc()->name, 16);
return 0;
}
return 0; // all other prctl ops: silently succeed
}
addr_t sys_tgkill(void)
{
int tgid, tid, sig;
argint(0, &tgid); argint(1, &tid); argint(2, &sig);
// In our single-threaded model tgid == tid == pid.
if (tgid <= 0) return 0;
if (sig == 0) return proc_exists(tgid) ? 0 : -ESRCH;
return sigsend(tgid, sig) < 0 ? -ESRCH : 0;
}
addr_t sys_tkill(void)
{
int tid, sig;
argint(0, &tid); argint(1, &sig);
if (tid <= 0) return 0;
if (sig == 0) return proc_exists(tid) ? 0 : -ESRCH;
return sigsend(tid, sig) < 0 ? -ESRCH : 0;
}
addr_t sys_pause(void)
{
// Sleep until next timer tick and return -EINTR (no real signal delivery).
acquire(&tickslock);
uint t0 = ticks;
while (ticks == t0 && !myproc()->killed)
sleep(&ticks, &tickslock);
release(&tickslock);
return -EINTR;
}
addr_t sys_getrusage(void)
{
int who;
struct rusage *ru;
argint(0, &who);
if (argptr(1, (char**)&ru, sizeof(*ru)) < 0) return -1;
memset(ru, 0, sizeof(*ru));
acquire(&tickslock);
uint xt = ticks;
release(&tickslock);
ru->ru_utime.tv_sec = (long)(xt / 100);
ru->ru_utime.tv_usec = (long)((xt % 100) * 10000);
return 0;
}
addr_t sys_times(void)
{
addr_t bufaddr;
argaddr(0, &bufaddr);
if (bufaddr) {
struct tms *t = (struct tms*)user_ptr(bufaddr, sizeof(*t));
if (t) memset(t, 0, sizeof(*t));
}
acquire(&tickslock);
uint xt = ticks;
release(&tickslock);
return (addr_t)xt; // return clock ticks since boot
}
addr_t sys_sysinfo(void)
{
addr_t bufaddr;
argaddr(0, &bufaddr);
struct sysinfo *si = (struct sysinfo*)user_ptr(bufaddr, sizeof(*si));
if (!si) return -EFAULT;
memset(si, 0, sizeof(*si));
acquire(&tickslock);
uint xt = ticks;
release(&tickslock);
si->uptime = (long)(xt / 100);
// Real values: PHYSTOP is the top of usable physical memory and the page
// allocator knows exactly how much of it is still on the free list. These
// used to be the constants 256MB/128MB, invented and always wrong.
si->totalram = (uint64)PHYSTOP;
si->freeram = kalloc_free_bytes();
si->mem_unit = 1;
si->procs = (uint16)proc_num();
return 0;
}
addr_t sys_waitid(void)
{
// Simplified: wait for any child, ignore idtype/id/options.
int wpid = wait();
if (wpid < 0) return -ECHILD;
// siginfo_t pointer (arg 2) — zero-fill if provided.
addr_t infop;
argaddr(2, &infop);
if (infop) {
char *p = (char*)user_ptr(infop, 128); // sizeof(siginfo_t)
if (p) memset(p, 0, 128);
}
return 0;
}

296
kernel/sys/sys_sig.c Normal file
View file

@ -0,0 +1,296 @@
#include "types.h"
#include "defs.h"
#include "param.h"
#include "proc.h"
#include "syscall.h"
#include "uaccess.h"
#include "errno.h"
// Linux sigset_t puts signal N at bit N-1; kernel-internal masks
// (pending_signals / signal_mask / deliver_signals) put signal N at bit N.
// Convert at the user boundary — storing user sets raw silently shifted
// every mask by one signal (blocking SIGCHLD actually blocked SIGCONT),
// which let mksh's SIGCHLD handler run inside the very critical sections
// sigprocmask() was guarding, wrecking its job table.
static uint64 sigset_from_user(uint64 uset) { return uset << 1; }
static uint64 sigset_to_user(uint64 kset) { return kset >> 1; }
addr_t
sys_sigfillset(void)
{
uint64 *set;
if (argptr(0, (char**)&set, sizeof(uint64)) < 0)
return -1;
if (set) *set = ~0ULL;
return 0;
}
addr_t
sys_sigprocmask(void)
{
int how;
addr_t setp, oldsetp;
argint(0, &how);
argaddr(1, &setp);
argaddr(2, &oldsetp);
// oldset is optional (NULL = don't return previous mask)
if (oldsetp) {
uint64 old = sigset_to_user(myproc()->signal_mask);
put_user(old, oldsetp);
}
// set is optional (NULL = just query)
if (setp) {
uint64 uset;
if (copy_from_user(&uset, setp, sizeof(uset)) == 0) {
uint64 set = sigset_from_user(uset);
if (how == 0) myproc()->signal_mask |= set; // SIG_BLOCK
else if (how == 1) myproc()->signal_mask &= ~set; // SIG_UNBLOCK
else myproc()->signal_mask = set; // SIG_SETMASK
}
}
return 0;
}
addr_t
sys_sigwait(void)
{
uint64 *set_ptr;
int *sig_ptr;
if (argptr(0, (char**)&set_ptr, sizeof(uint64)) < 0 ||
argptr(1, (char**)&sig_ptr, sizeof(int)) < 0)
return -1;
uint64 set = sigset_from_user(*set_ptr);
acquire(&tickslock);
while (!(myproc()->pending_signals & set)) {
if (myproc()->killed) { release(&tickslock); return -1; }
sleep(myproc(), &tickslock);
}
for (int i = 0; i < 64; i++) {
if ((myproc()->pending_signals & set) & (1ULL << i)) {
myproc()->pending_signals &= ~(1ULL << i);
*sig_ptr = i;
break;
}
}
release(&tickslock);
return 0;
}
// musl's struct sigaction (musl 1.2.5 signal.h) for this target:
// sa_handler (8 bytes) at offset 0, then a 128-byte sigset_t (musl uses
// 128/sizeof(long) = 16 longs regardless of NSIG), sa_flags (4 bytes) +
// padding, then sa_restorer (8 bytes) at offset 144. We only care about
// those two 8-byte fields.
#define SA_HANDLER_OFF 0
#define SA_RESTORER_OFF 144
// rt_sigaction: store the handler + restorer so deliver_signals() (called
// from syscall()'s tail, kernel/sys/syscall.c) can actually jump to user
// code instead of silently doing nothing. sig 0 is invalid (matches
// sigaction(2)); we don't validate against NSIG beyond our own array size.
//
// User accesses go through copy_from_user/put_user (uaccess.h), which are
// page-safe. An earlier version bulk-copied the ~152-byte struct through a
// single uva2ka() translation of its start address; when the user's
// act/oldact pointer landed close enough to a page boundary, the write ran
// past the mapped page into an unrelated physical page, silently crashing
// every freshly exec'd /bin/sh (mksh calls sigaction() during startup).
addr_t
sys_rt_sigaction(void)
{
int sig;
addr_t actp, oldactp;
argint(0, &sig);
argaddr(1, &actp);
argaddr(2, &oldactp);
if (sig <= 0 || sig >= 32) return -1;
if (oldactp) {
put_user(myproc()->sig_handler[sig], oldactp + SA_HANDLER_OFF);
put_user(myproc()->sig_restorer[sig], oldactp + SA_RESTORER_OFF);
}
if (actp) {
copy_from_user(&myproc()->sig_handler[sig], actp + SA_HANDLER_OFF,
sizeof(myproc()->sig_handler[sig]));
copy_from_user(&myproc()->sig_restorer[sig], actp + SA_RESTORER_OFF,
sizeof(myproc()->sig_restorer[sig]));
}
return 0;
}
// rt_sigreturn: entered via `syscall` (rax=15) from musl's __restore_rt
// trampoline, which is exactly what we set as the manufactured return
// address in deliver_signals() below — the handler function's own `ret`
// lands here once it's done. Restore the trapframe deliver_signals()
// stashed before redirecting to the handler, so syscall()'s normal
// sysretq path resumes the interrupted code exactly where it left off.
addr_t
sys_rt_sigreturn(void)
{
addr_t saved_rax = myproc()->saved_sigtf.rax;
*myproc()->tf = myproc()->saved_sigtf;
myproc()->in_signal = 0;
// If this handler interrupted a sigsuspend(), put its pre-suspend
// signal mask back now that the handler has finished.
if (myproc()->sigsusp_active) {
myproc()->signal_mask = myproc()->sigsusp_oldmask;
myproc()->sigsusp_active = 0;
}
// syscall() does `tf->rax = <our return value>` right after this
// returns — hand back the already-restored rax so that assignment is
// a harmless no-op instead of clobbering what we just restored.
return saved_rax;
}
// Called from syscall()'s tail (kernel/sys/syscall.c), right before
// returning to user mode, for every syscall a process makes (this is the
// only "check point" we have without also hooking the interrupt-return
// path in trap.c — sufficient for what actually needed this: a process
// blocked in a syscall, like sigsuspend(), waiting for its own next
// syscall return to notice a signal arrived).
//
// If there's a pending, unblocked signal with a registered handler,
// redirect tf to it: rdi = signal number (SysV ABI first arg), rip =
// handler.
//
// The return address (what the handler's own `ret` lands on) can't be
// sig_restorer[sig] — this musl build has no SA_RESTORER defined at all
// (checked: not defined anywhere in musl 1.2.5), so sigaction() never
// fills it in; it's always 0. Pushing 0 as a return address is exactly
// what crashed every mksh instance the first time a signal was ever
// actually delivered (handler ran, `ret` jumped to address 0). Instead,
// inject a tiny trampoline directly onto the user's own stack — 7 bytes
// of machine code for `mov eax, 15; syscall` (rt_sigreturn) — below the
// current stack pointer (clear of the SysV red zone) and point the
// return address at that, the standard way a kernel supplies sigreturn
// when the C library doesn't hand it one itself.
void
deliver_signals(struct trapframe *tf)
{
if (myproc()->in_signal) return; // no nested delivery
uint64 deliverable = myproc()->pending_signals & ~myproc()->signal_mask;
if (!deliverable) return;
for (int sig = 1; sig < 32; sig++) {
if (!(deliverable & (1ULL << sig))) continue;
// SIG_DFL is 0 and SIG_IGN is 1 -- neither is an address to jump to.
//
// Only the 0 case was checked here. A process that had asked to
// ignore a signal got its handler "called" at address 1, which is a
// page fault at rip=0x1 and a dead process. Nothing sent SIGPIPE
// until pipes learned to, at which point every shell that ignores
// SIGPIPE -- which is every shell -- died the first time a pipeline
// ended early. `cat big | head -c 5` was enough.
//
// The default action for a signal with no handler has already been
// applied by sigsend(); all that is left here is to stop it being
// pending, which the old `continue` never did, so an ignored signal
// was re-examined on the way out of every syscall forever after.
if (myproc()->sig_handler[sig] <= 1) {
myproc()->pending_signals &= ~(1ULL << sig);
continue;
}
uint64 tramp_addr = (tf->rsp - 256) & ~0xFULL;
uint8 *tramp = (uint8*)uva2ka(myproc()->pgdir, (char*)tramp_addr);
if (!tramp) continue; // can't deliver safely; try another pending signal
uint64 sp = tramp_addr - 8;
uint64 *retaddr = (uint64*)uva2ka(myproc()->pgdir, (char*)sp);
if (!retaddr) continue;
// mov eax, 15 ; syscall
tramp[0] = 0xb8; tramp[1] = 0x0f; tramp[2] = 0x00;
tramp[3] = 0x00; tramp[4] = 0x00;
tramp[5] = 0x0f; tramp[6] = 0x05;
*retaddr = tramp_addr;
myproc()->pending_signals &= ~(1ULL << sig);
myproc()->saved_sigtf = *tf;
myproc()->in_signal = 1;
tf->rsp = sp;
tf->rdi = (uint64)sig;
// Both return paths must point at the handler: iretq-based returns
// use tf->rip, but syscall_trapret exits via sysretq, which takes
// the user RIP from RCX and *discards* tf->rip (trapasm.S pops rcx
// from the frame, then `addq $40` throws rip/cs/rflags away). With
// only tf->rip set, the "delivered" handler never actually ran on
// the syscall path: user code resumed at the old rcx with the
// shifted rsp, and its ret fell into the sigreturn trampoline,
// silently undoing everything. exec() handles this same quirk by
// setting tf->rcx = elf.entry.
tf->rip = myproc()->sig_handler[sig];
tf->rcx = myproc()->sig_handler[sig];
return;
}
}
// sigaltstack: set/get alternate signal stack.
// We don't deliver signals, so just store/return zeros.
addr_t sys_sigaltstack(void) { return 0; }
// rt_sigpending: write the set of pending signals to user buffer.
addr_t sys_rt_sigpending(void)
{
addr_t setp;
argaddr(0, &setp);
if (setp) {
uint64 pend = sigset_to_user(myproc()->pending_signals);
put_user(pend, setp);
}
return 0;
}
// rt_sigtimedwait: wait for a signal from the set; returns -EAGAIN immediately.
addr_t sys_rt_sigtimedwait(void) { return -EAGAIN; }
// rt_sigsuspend: atomically replace the signal mask and suspend until a
// signal not blocked by the *temporary* mask is pending, then return -EINTR.
//
// The atomic mask swap is the entire point of sigsuspend(2): mksh blocks
// SIGCHLD while it inspects its job table, then calls sigsuspend() with the
// old (SIGCHLD-unblocked) mask to wait for a child. The previous stub
// ignored the mask argument and just slept one tick — SIGCHLD stayed
// blocked, deliver_signals() could never fire the handler, and the shell
// hung forever after running any external command (unreaped zombie child,
// shell parked in sigsuspend: the "can't type after a command" hang).
//
// The temporary mask must still be in effect when deliver_signals() runs at
// syscall()'s tail (that's what allows the delivery), so the old mask can't
// be restored here — sys_rt_sigreturn() restores it once the handler
// finishes, via sigsusp_oldmask/sigsusp_active.
addr_t sys_rt_sigsuspend(void)
{
addr_t maskp;
uint64 newmask;
argaddr(0, &maskp);
uint64 oldmask = myproc()->signal_mask;
if (maskp && copy_from_user(&newmask, maskp, sizeof(newmask)) == 0)
myproc()->signal_mask = sigset_from_user(newmask);
acquire(&tickslock);
while (!(myproc()->pending_signals & ~myproc()->signal_mask) && !myproc()->killed)
sleep(&ticks, &tickslock);
release(&tickslock);
// Will any pending signal actually reach a handler? If so, the mask is
// restored by sigreturn after the handler runs; otherwise (no handler
// installed, or we were killed) restore it right now.
uint64 deliverable = myproc()->pending_signals & ~myproc()->signal_mask;
int will_deliver = 0;
for (int sig = 1; sig < 32; sig++)
if ((deliverable & (1ULL << sig)) && myproc()->sig_handler[sig])
{ will_deliver = 1; break; }
if (will_deliver && !myproc()->in_signal) {
myproc()->sigsusp_oldmask = oldmask;
myproc()->sigsusp_active = 1;
} else {
myproc()->signal_mask = oldmask;
}
return -EINTR;
}

139
kernel/sys/sys_time.c Normal file
View file

@ -0,0 +1,139 @@
#include "types.h"
#include "defs.h"
#include "param.h"
#include "date.h"
#include "spinlock.h"
#include "proc.h"
#include "x86.h"
#include "syscall.h"
#include "posix.h"
#include "uaccess.h"
#include "errno.h"
addr_t
sys_alarm(void)
{
int seconds;
if (argint(0, &seconds) < 0)
return -1;
myproc()->alarm_ticks = seconds * 100;
return 0;
}
addr_t
sys_gettimeofday(void)
{
struct timeval *tv;
if (argptr(0, (char**)&tv, sizeof(*tv)) < 0)
return -1;
if (tv) {
// Real time, from the hardware clock read at boot plus uptime -- not
// uptime alone, which is what this used to return and why every machine
// thought it was 1970. See kernel/core/time.c.
uint64 sec, usec;
wallclock(&sec, &usec);
tv->tv_sec = (long)sec;
tv->tv_usec = (long)usec;
}
return 0;
}
addr_t
sys_nanosleep(void)
{
addr_t tsaddr, raw;
if (argaddr(0, &tsaddr) < 0 || fetchaddr(tsaddr, &raw) < 0)
return -1;
int64 sec = (int64)raw;
if (sec > 0) {
acquire(&tickslock);
uint t0 = ticks;
while (ticks - t0 < (uint)(sec * 100)) {
if (myproc()->killed) { release(&tickslock); return -EINTR; }
sleep(&ticks, &tickslock);
}
release(&tickslock);
}
return 0;
}
addr_t
sys_clock_gettime(void)
{
int clkid;
struct timespec *tp;
argint(0, &clkid);
if (argptr(1, (char**)&tp, sizeof(*tp)) < 0)
return -1;
// CLOCK_MONOTONIC (1) counts from boot; everything else is treated as a
// realtime clock and counts from the epoch. Returning boot-relative values
// for CLOCK_REALTIME is what made timestamps land in 1970.
uint64 sec, usec;
wallclock(&sec, &usec);
if (clkid == 1)
sec -= boot_epoch;
tp->tv_sec = (long)sec;
tp->tv_nsec = (long)(usec * 1000);
return 0;
}
addr_t sys_getitimer(void) { return 0; } // stub
addr_t
sys_setitimer(void)
{
int which;
addr_t newp;
argint(0, &which); argaddr(1, &newp);
// We don't implement interval timers; just track the alarm if it's ITIMER_REAL.
if (which == 0 && newp) {
struct itimerval iv;
if (copy_from_user(&iv, newp, sizeof(iv)) == 0)
myproc()->alarm_ticks = (int)(iv.it_value.tv_sec * 100 +
iv.it_value.tv_usec / 10000);
}
return 0;
}
addr_t
sys_time(void)
{
addr_t tloc;
argaddr(0, &tloc);
uint64 sec;
wallclock(&sec, 0);
long t = (long)sec;
if (tloc)
put_user(t, tloc);
return (addr_t)t;
}
addr_t
sys_clock_getres(void)
{
struct timespec *tp;
if (argptr(1, (char**)&tp, sizeof(*tp)) < 0) return 0;
if (tp) { tp->tv_sec = 0; tp->tv_nsec = 10000000L; } // 10ms resolution
return 0;
}
addr_t
sys_clock_nanosleep(void)
{
// Treat the same as nanosleep: arg2 is the timespec, arg1 is flags.
// Linux: clock_nanosleep(clockid, flags, rqtp, rmtp)
addr_t tsaddr, raw;
if (argaddr(2, &tsaddr) < 0 || fetchaddr(tsaddr, &raw) < 0)
return -1;
int64 sec = (int64)raw;
if (sec > 0) {
acquire(&tickslock);
uint t0 = ticks;
while (ticks - t0 < (uint)(sec * 100)) {
if (myproc()->killed) { release(&tickslock); return -EINTR; }
sleep(&ticks, &tickslock);
}
release(&tickslock);
}
return 0;
}

141
kernel/sys/syscall.c Normal file
View file

@ -0,0 +1,141 @@
#include "types.h"
#include "defs.h"
#include "param.h"
#include "memlayout.h"
#include "mmu.h"
#include "traps.h"
#include "proc.h"
#include "x86.h"
#include "syscall.h"
#include "uaccess.h"
#include "errno.h"
#include "posix.h"
// -----------------------------------------------------------------------
// Argument helpers — read syscall args from the saved register frame.
// x86-64 SysV ABI: rdi, rsi, rdx, r10, r8, r9
// -----------------------------------------------------------------------
// Note: address validation goes through uaccess.h (page-table presence +
// user bit on every page of the range). There is deliberately no
// `addr < myproc()->sz` check: that would assume every valid user pointer
// lives in the heap — false for stack addresses and mmap()'d buffers
// (both live above myproc()->sz), which made syscalls fail whenever a
// Linux-ABI program passed a string/pointer from either region.
//
// Validated pointers are returned as raw user VAs — the process page
// table is live in kernel mode, so they dereference correctly across
// page boundaries (unlike a uva2ka() alias, which is only good within
// the one page it translated).
int
fetchint(addr_t addr, int *ip)
{
return copy_from_user(ip, addr, sizeof(*ip)) ? -1 : 0;
}
int
fetchaddr(addr_t addr, addr_t *ip)
{
return copy_from_user(ip, addr, sizeof(*ip)) ? -1 : 0;
}
int
fetchstr(addr_t addr, char **pp)
{
long len = strlen_user(addr);
if (len < 0)
return -1;
*pp = (char*)addr;
return len;
}
static addr_t
fetcharg(int n)
{
switch (n) {
case 0: return myproc()->tf->rdi;
case 1: return myproc()->tf->rsi;
case 2: return myproc()->tf->rdx;
case 3: return myproc()->tf->r10;
case 4: return myproc()->tf->r8;
case 5: return myproc()->tf->r9;
}
panic("fetcharg: bad n");
return 0;
}
int argint(int n, int *ip) { *ip = (int)fetcharg(n); return 0; }
int argaddr(int n, addr_t *ip) { *ip = fetcharg(n); return 0; }
int
argptr(int n, char **pp, int size)
{
addr_t i;
if (argaddr(n, &i) < 0)
return -1;
if (size < 0)
return -1;
char *p = user_ptr(i, (uint64)size);
if (!p)
return -1;
*pp = p;
return 0;
}
int
argstr(int n, char **pp)
{
addr_t addr;
if (argaddr(n, &addr) < 0)
return -1;
return fetchstr(addr, pp);
}
// -----------------------------------------------------------------------
// Dispatch table — auto-generated from table.inc
// -----------------------------------------------------------------------
#define SYSCALL(nr, lnr, name) extern addr_t sys_##name(void);
#define SYSCALL_ALIAS(lnr, name)
#include "table.inc"
#undef SYSCALL
#undef SYSCALL_ALIAS
static addr_t (*syscalls[NSYSCALLS])(void) = {
#define SYSCALL(nr, lnr, name) [SYS_##name] = sys_##name,
#define SYSCALL_ALIAS(lnr, name)
#include "table.inc"
#undef SYSCALL
#undef SYSCALL_ALIAS
};
// -----------------------------------------------------------------------
// Main entry point called from trap.c
// -----------------------------------------------------------------------
void
syscall(struct trapframe *tf)
{
kstack_check_canary("syscall entry");
myproc()->tf = tf;
uint64 orig = myproc()->tf->rax;
uint64 num = orig;
if (myproc()->syscall_map) {
num = (num < (uint64)myproc()->map_size) ? myproc()->syscall_map[num] : 0;
}
if (num > 0 && num < NSYSCALLS && syscalls[num]) {
tf->rax = syscalls[num]();
} else {
tf->rax = -ENOSYS;
}
if (myproc()->killed)
exit(W_EXITCODE(0, myproc()->killsig ? myproc()->killsig : SIGKILL));
deliver_signals(tf);
kstack_check_canary("syscall exit");
}

282
kernel/sys/table.inc Normal file
View file

@ -0,0 +1,282 @@
// Segregated Kernel -- central syscall table. ONE line per syscall.
//
// Format: SYSCALL(nr, linux_nr, name)
// nr Schunix native syscall number
// linux_nr Linux x86_64 syscall number, or NO_LINUX if none
// name implemented as sys_<name>() in kernel/sys/sys_*.c
//
// To add a syscall:
// 1. Add ONE line here.
// 2. Write the body with SYSCALL_DEFINE(<name>) in a kernel/sys/sys_*.c.
//
// Everything else is generated from this file: the SYS_* constants
// (include/syscall.h), the dispatch table (kernel/sys/syscall.c), and the
// Linux ABI translation map (kernel/sys/linux_abi.c). The Linux number used
// to live in a second hand-maintained table in linux_abi.c; it does not any
// more, so a syscall can no longer be half-registered.
//
// A syscall reachable under more than one Linux number (e.g. stat/newstat)
// gets extra SYSCALL_ALIAS(linux_nr, name) lines.
// --- process ---
SYSCALL( 1, 57, fork)
SYSCALL( 2, 60, exit)
SYSCALL( 3, NO_LINUX, wait)
SYSCALL( 6, 62, kill)
SYSCALL( 7, 59, exec)
SYSCALL( 11, 39, getpid)
SYSCALL( 13, NO_LINUX, sleep)
SYSCALL( 14, NO_LINUX, uptime)
SYSCALL( 29, NO_LINUX, waitpid)
SYSCALL( 30, 112, setsid)
SYSCALL( 31, NO_LINUX, reboot)
SYSCALL( 32, NO_LINUX, shutdown)
SYSCALL( 66, 56, clone)
SYSCALL( 67, 231, exit_group)
SYSCALL( 68, 61, wait4)
SYSCALL( 83, 105, setuid)
SYSCALL( 84, 106, setgid)
SYSCALL( 85, 124, getsid)
SYSCALL( 89, 110, getppid)
SYSCALL( 91, 121, getpgid)
SYSCALL( 94, 186, gettid)
SYSCALL( 92, 109, setpgid)
SYSCALL( 93, 111, getpgrp)
// --- identity ---
SYSCALL( 47, 102, getuid)
SYSCALL( 48, 107, geteuid)
SYSCALL( 69, 104, getgid)
SYSCALL( 70, 108, getegid)
SYSCALL( 71, 95, umask)
// --- memory ---
SYSCALL( 12, NO_LINUX, sbrk)
SYSCALL( 35, 9, mmap)
SYSCALL( 36, 11, munmap)
// Shared memory. These carried NO_LINUX while they were ENOSYS stubs, which
// meant a musl-linked program calling shmget() never reached them at all --
// it got "bad syscall" from the ABI translation rather than an answer. Now
// that they do something, they need the numbers programs actually use.
SYSCALL( 37, 29, shmget)
SYSCALL( 38, 30, shmat)
SYSCALL( 39, 67, shmdt)
SYSCALL( 59, 10, mprotect)
SYSCALL( 74, 12, brk)
// --- filesystem ---
SYSCALL( 4, 22, pipe)
SYSCALL( 5, 0, read)
SYSCALL( 8, 5, fstat)
SYSCALL( 9, 80, chdir)
SYSCALL( 10, 32, dup)
SYSCALL( 15, 2, open)
SYSCALL( 16, 1, write)
SYSCALL( 17, 133, mknod)
SYSCALL( 18, 87, unlink)
SYSCALL( 19, 86, link)
SYSCALL( 20, 83, mkdir)
SYSCALL( 21, 3, close)
SYSCALL( 24, 8, lseek)
SYSCALL( 33, 33, dup2)
SYSCALL( 34, 72, fcntl)
// `delete` -- the recycle bin. It hides a file instead of removing it, so the
// blocks are never freed and a program that deletes a temp file gets no space
// back. Inherited from the third-party filesystem in kernel/fs/fs.c and
// orphaned by the decision to keep only Linux syscall numbering, so nothing
// reaches it. Its effect on read and write has been removed; the entry stays
// only because sys_delete() is still compiled, and both die with fs.c.
SYSCALL( 51, NO_LINUX, delete)
SYSCALL( 52, NO_LINUX, show)
SYSCALL( 53, NO_LINUX, showstat)
SYSCALL( 54, 77, ftruncate)
SYSCALL( 55, 76, truncate)
SYSCALL( 56, 82, rename)
SYSCALL( 57, 88, symlink)
SYSCALL( 58, 89, readlink)
SYSCALL( 61, 79, getcwd)
SYSCALL( 73, 19, readv)
SYSCALL( 72, 20, writev)
SYSCALL( 76, 4, stat)
SYSCALL( 77, 6, lstat)
SYSCALL( 78, 21, access)
SYSCALL( 79, 257, openat)
SYSCALL( 80, 81, fchdir)
SYSCALL( 86, 292, dup3)
SYSCALL( 87, 293, pipe2)
// --- device / I/O ---
SYSCALL( 22, NO_LINUX, vga_sync)
SYSCALL( 23, NO_LINUX, getmouse)
SYSCALL( 45, 23, select)
SYSCALL( 46, 7, poll)
SYSCALL( 60, 16, ioctl)
// --- sockets ---
//
// These carried NO_LINUX while they were ENOSYS stubs, which meant a
// musl-linked program calling socket() never reached them at all -- the ABI
// translation answered "bad syscall" before the stub could answer ENOSYS.
// Now that they do something they need the numbers programs actually use.
//
// `shutdown` is deliberately not the name here: Schunix already has a native
// shutdown() that powers the machine off (syscall 32), and two syscalls
// cannot share a sys_ function name. Linux 48 maps to sockshutdown instead.
SYSCALL( 40, 41, socket)
SYSCALL( 41, 49, bind)
SYSCALL( 42, 50, listen)
SYSCALL( 43, 43, accept)
SYSCALL( 44, 42, connect)
SYSCALL(187, 44, sendto)
SYSCALL(188, 45, recvfrom)
SYSCALL(189, 48, sockshutdown)
SYSCALL(190, 53, socketpair)
SYSCALL(191, 54, setsockopt)
SYSCALL(192, 55, getsockopt)
// --- time ---
SYSCALL( 28, 37, alarm)
SYSCALL( 49, 96, gettimeofday)
SYSCALL( 50, 35, nanosleep)
SYSCALL( 88, 228, clock_gettime)
// --- signals ---
SYSCALL( 25, NO_LINUX, sigfillset)
SYSCALL( 26, 14, sigprocmask)
SYSCALL( 27, NO_LINUX, sigwait)
SYSCALL( 90, 13, rt_sigaction)
// --- POSIX / Linux-ABI compat ---
SYSCALL( 62, 158, arch_prctl)
SYSCALL( 95, 63, uname)
SYSCALL( 96, 217, getdents64)
SYSCALL( 63, 218, set_tid_address)
SYSCALL( 64, 273, set_robust_list)
SYSCALL( 65, 202, futex)
SYSCALL( 75, 302, prlimit)
SYSCALL( 81, NO_LINUX, enable_linux_abi)
SYSCALL( 82, 97, getrlimit)
// --- signal extensions ---
SYSCALL( 97, 15, rt_sigreturn)
SYSCALL( 98, 131, sigaltstack)
SYSCALL( 99, 127, rt_sigpending)
SYSCALL(100, 128, rt_sigtimedwait)
SYSCALL(101, 130, rt_sigsuspend)
// --- process extensions ---
SYSCALL(102, 157, prctl)
SYSCALL(103, 234, tgkill)
SYSCALL(104, 200, tkill)
SYSCALL(105, 34, pause)
SYSCALL(106, NO_LINUX, getitimer)
SYSCALL(107, NO_LINUX, setitimer)
SYSCALL(108, 98, getrusage)
SYSCALL(109, 100, times)
SYSCALL(110, 99, sysinfo)
SYSCALL(111, 24, sched_yield)
SYSCALL(112, 204, sched_getaffinity)
SYSCALL(113, 203, sched_setaffinity)
SYSCALL(114, 247, waitid)
// --- identity extensions ---
SYSCALL(115, 113, setreuid)
SYSCALL(116, 114, setregid)
SYSCALL(117, 118, getresuid)
SYSCALL(118, 120, getresgid)
SYSCALL(119, 117, setresuid)
SYSCALL(120, 119, setresgid)
SYSCALL(121, 115, getgroups)
SYSCALL(122, 116, setgroups)
SYSCALL(123, 122, setfsuid)
SYSCALL(124, 123, setfsgid)
// --- filesystem extensions ---
SYSCALL(125, 90, chmod)
SYSCALL(126, 91, fchmod)
SYSCALL(127, 92, chown)
SYSCALL(128, 93, fchown)
SYSCALL(129, 94, lchown)
SYSCALL(130, 84, rmdir)
SYSCALL(131, NO_LINUX, fsync)
SYSCALL(132, NO_LINUX, fdatasync)
SYSCALL(133, 162, sync)
SYSCALL(134, 17, pread)
SYSCALL(135, 18, pwrite)
SYSCALL(148, 132, utime)
SYSCALL(149, 235, utimes)
SYSCALL(150, 261, utimensat)
SYSCALL(151, 187, sendfile)
SYSCALL(152, NO_LINUX, preadv2)
SYSCALL(153, NO_LINUX, pwritev2)
SYSCALL(154, 285, fallocate)
SYSCALL(155, 137, statfs)
SYSCALL(156, 138, fstatfs)
SYSCALL(157, 161, chroot)
// --- memory extensions ---
SYSCALL(136, 28, madvise)
SYSCALL(137, NO_LINUX, mremap)
SYSCALL(138, NO_LINUX, msync)
SYSCALL(139, NO_LINUX, mincore)
SYSCALL(140, 149, mlock)
SYSCALL(141, 150, munlock)
SYSCALL(142, 151, mlockall)
SYSCALL(143, 152, munlockall)
SYSCALL(144, 221, fadvise64)
SYSCALL(145, 160, setrlimit)
SYSCALL(146, NO_LINUX, getpriority)
SYSCALL(147, NO_LINUX, setpriority)
// --- at-syscalls ---
SYSCALL(158, 258, mkdirat)
SYSCALL(159, 259, mknodat)
SYSCALL(160, 263, unlinkat)
SYSCALL(161, 264, renameat)
SYSCALL(162, 265, linkat)
SYSCALL(163, 266, symlinkat)
SYSCALL(164, 267, readlinkat)
SYSCALL(165, 268, fchmodat)
SYSCALL(166, 269, faccessat)
SYSCALL(167, 262, fstatat)
SYSCALL(168, 260, fchownat)
SYSCALL(169, 316, renameat2)
// --- time extensions ---
SYSCALL(170, 201, time)
SYSCALL(171, 229, clock_getres)
SYSCALL(172, 230, clock_nanosleep)
// --- misc ---
SYSCALL(173, 103, syslog)
SYSCALL(174, 318, getrandom)
SYSCALL(175, 270, pselect6)
SYSCALL(176, 271, ppoll)
// --- epoll / eventfd / signalfd / timerfd ---
SYSCALL(177, 291, epoll_create1)
SYSCALL(178, 233, epoll_ctl)
SYSCALL(179, 232, epoll_wait)
SYSCALL(180, 282, signalfd4)
SYSCALL(181, 284, eventfd2)
SYSCALL(182, 283, timerfd_create)
SYSCALL(183, 286, timerfd_settime)
SYSCALL(184, 287, timerfd_gettime)
// --- mounts ---
SYSCALL(185, 165, mount)
SYSCALL(186, 166, umount2)
// --- additional Linux numbers routed to an existing handler ---
SYSCALL_ALIAS( 58, fork)
SYSCALL_ALIAS( 280, utimensat)
SYSCALL_ALIAS( 281, epoll_wait)
SYSCALL_ALIAS( 289, signalfd4)
SYSCALL_ALIAS( 290, eventfd2)
SYSCALL_ALIAS( 295, readv)
SYSCALL_ALIAS( 296, writev)
// accept4() differs from accept() only in a flags argument naming
// SOCK_CLOEXEC/SOCK_NONBLOCK, neither of which is honoured yet.
SYSCALL_ALIAS( 288, accept)