diff --git a/kernel/kmain/kmain.go b/kernel/kmain/kmain.go index 9ee1f8f..cd9f81d 100644 --- a/kernel/kmain/kmain.go +++ b/kernel/kmain/kmain.go @@ -3,7 +3,8 @@ package kmain import ( "github.com/achilleasa/gopher-os/kernel/hal" "github.com/achilleasa/gopher-os/kernel/hal/multiboot" - "github.com/achilleasa/gopher-os/kernel/mem/pmm" + "github.com/achilleasa/gopher-os/kernel/kfmt/early" + "github.com/achilleasa/gopher-os/kernel/mem/pmm/allocator" ) // Kmain is the only Go symbol that is visible (exported) from the rt0 initialization @@ -23,5 +24,7 @@ func Kmain(multibootInfoPtr uintptr) { hal.InitTerminal() hal.ActiveTerminal.Clear() - pmm.EarlyAllocator.Init() + if err := allocator.Init(); err != nil { + early.Printf("[%s] error: %s\n", err.Module, err.Message) + } } diff --git a/kernel/mem/pmm/allocator/bootmem.go b/kernel/mem/pmm/allocator/bootmem.go index 8121c64..23b1c5d 100644 --- a/kernel/mem/pmm/allocator/bootmem.go +++ b/kernel/mem/pmm/allocator/bootmem.go @@ -9,38 +9,87 @@ import ( ) var ( + // earlyAllocator is a boot mem allocator instance used for page + // allocations before switching to a more advanced allocator. + earlyAllocator bootMemAllocator + errBootAllocOutOfMemory = &kernel.Error{Module: "boot_mem_alloc", Message: "out of memory"} ) -// BootMemAllocator implements a rudimentary physical memory allocator which is used -// to bootstrap the kernel. +// bootMemAllocator implements a rudimentary physical memory allocator which is +// used to bootstrap the kernel. // // The allocator implementation uses the memory region information provided by // the bootloader to detect free memory blocks and return the next available -// free frame. -// -// Allocations are tracked via an internal counter that contains the last -// allocated frame index. The system memory regions are mapped into a linear -// page index by aligning the region start address to the system's page size -// and then dividing by the page size. +// free frame. Allocations are tracked via an internal counter that contains +// the last allocated frame. // // Due to the way that the allocator works, it is not possible to free // allocated pages. Once the kernel is properly initialized, the allocated // blocks will be handed over to a more advanced memory allocator that does // support freeing. -type BootMemAllocator struct { +type bootMemAllocator struct { // allocCount tracks the total number of allocated frames. allocCount uint64 - // lastAllocIndex tracks the last allocated frame index. - lastAllocIndex int64 + // lastAllocFrame tracks the last allocated frame number. + lastAllocFrame pmm.Frame } -// Init sets up the boot memory allocator internal state and prints out the -// system memory map. -func (alloc *BootMemAllocator) Init() { - alloc.lastAllocIndex = -1 +// init sets up the boot memory allocator internal state. +func (alloc *bootMemAllocator) init() { + // TODO +} +// AllocFrame scans the system memory regions reported by the bootloader and +// reserves the next available free frame. +// +// AllocFrame returns an error if no more memory can be allocated. +func (alloc *bootMemAllocator) AllocFrame() (pmm.Frame, *kernel.Error) { + var err = errBootAllocOutOfMemory + + multiboot.VisitMemRegions(func(region *multiboot.MemoryMapEntry) bool { + // Ignore reserved regions and regions smaller than a single page + if region.Type != multiboot.MemAvailable || region.Length < uint64(mem.PageSize) { + return true + } + + // Reported addresses may not be page-aligned; round up to get + // the start frame and round down to get the end frame + pageSizeMinus1 := uint64(mem.PageSize - 1) + regionStartFrame := pmm.Frame(((region.PhysAddress + pageSizeMinus1) & ^pageSizeMinus1) >> mem.PageShift) + regionEndFrame := pmm.Frame(((region.PhysAddress+region.Length) & ^pageSizeMinus1)>>mem.PageShift) - 1 + + // Ignore already allocated regions + if alloc.allocCount != 0 && alloc.lastAllocFrame >= regionEndFrame { + return true + } + + // The last allocated frame will be either pointing to a + // previous region or will point inside this region. In the + // first case (or if this is the first allocation) we select + // the start frame for this region. In the latter case we + // select the next available frame. + if alloc.allocCount == 0 || alloc.lastAllocFrame < regionStartFrame { + alloc.lastAllocFrame = regionStartFrame + } else { + alloc.lastAllocFrame++ + } + err = nil + return false + }) + + if err != nil { + return pmm.InvalidFrame, errBootAllocOutOfMemory + } + + alloc.allocCount++ + return alloc.lastAllocFrame, nil +} + +// printMemoryMap scans the memory region information provided by the +// bootloader and prints out the system's memory map. +func (alloc *bootMemAllocator) printMemoryMap() { early.Printf("[boot_mem_alloc] system memory map:\n") var totalFree mem.Size multiboot.VisitMemRegions(func(region *multiboot.MemoryMapEntry) bool { @@ -54,49 +103,9 @@ func (alloc *BootMemAllocator) Init() { early.Printf("[boot_mem_alloc] free memory: %dKb\n", uint64(totalFree/mem.Kb)) } -// AllocFrame scans the system memory regions reported by the bootloader and -// reserves the next available free frame. -// -// AllocFrame returns an error if no more memory can be allocated. -func (alloc *BootMemAllocator) AllocFrame() (pmm.Frame, *kernel.Error) { - var ( - foundPageIndex int64 = -1 - regionStartPageIndex, regionEndPageIndex int64 - ) - multiboot.VisitMemRegions(func(region *multiboot.MemoryMapEntry) bool { - if region.Type != multiboot.MemAvailable { - return true - } - - // Align region start address to a page boundary and find the start - // and end page indices for the region - regionStartPageIndex = int64(((mem.Size(region.PhysAddress) + (mem.PageSize - 1)) & ^(mem.PageSize - 1)) >> mem.PageShift) - regionEndPageIndex = int64(((mem.Size(region.PhysAddress+region.Length) - (mem.PageSize - 1)) & ^(mem.PageSize - 1)) >> mem.PageShift) - - // Ignore already allocated regions - if alloc.lastAllocIndex >= regionEndPageIndex { - return true - } - - // We found a block that can be allocated. The last allocated - // index will be either pointing to a previous region or will - // point inside this region. In the first case we just need to - // select the regionStartPageIndex. In the latter case we can - // simply select the next available page in the current region. - if alloc.lastAllocIndex < regionStartPageIndex { - foundPageIndex = regionStartPageIndex - } else { - foundPageIndex = alloc.lastAllocIndex + 1 - } - return false - }) - - if foundPageIndex == -1 { - return pmm.InvalidFrame, errBootAllocOutOfMemory - } - - alloc.allocCount++ - alloc.lastAllocIndex = foundPageIndex - - return pmm.Frame(foundPageIndex), nil +// Init sets up the kernel physical memory allocation sub-system. +func Init() *kernel.Error { + earlyAllocator.init() + earlyAllocator.printMemoryMap() + return nil } diff --git a/kernel/mem/pmm/allocator/bootmem_test.go b/kernel/mem/pmm/allocator/bootmem_test.go index e6bbdbb..713dd3c 100644 --- a/kernel/mem/pmm/allocator/bootmem_test.go +++ b/kernel/mem/pmm/allocator/bootmem_test.go @@ -1,41 +1,27 @@ package allocator import ( + "bytes" "testing" "unsafe" "github.com/achilleasa/gopher-os/kernel/driver/video/console" "github.com/achilleasa/gopher-os/kernel/hal" "github.com/achilleasa/gopher-os/kernel/hal/multiboot" - "github.com/achilleasa/gopher-os/kernel/mem" ) func TestBootMemoryAllocator(t *testing.T) { - // Mock a tty to handle early.Printf output - mockConsoleFb := make([]byte, 160*25) - mockConsole := &console.Ega{} - mockConsole.Init(80, 25, uintptr(unsafe.Pointer(&mockConsoleFb[0]))) - hal.ActiveTerminal.AttachTo(mockConsole) - multiboot.SetInfoPtr(uintptr(unsafe.Pointer(&multibootMemoryMap[0]))) - var totalFreeFrames uint64 - multiboot.VisitMemRegions(func(region *multiboot.MemoryMapEntry) bool { - if region.Type == multiboot.MemAvailable { - regionStartFrameIndex := uint64(((mem.Size(region.PhysAddress) + (mem.PageSize - 1)) & ^(mem.PageSize - 1)) >> mem.PageShift) - regionEndFrameIndex := uint64(((mem.Size(region.PhysAddress+region.Length) - (mem.PageSize - 1)) & ^(mem.PageSize - 1)) >> mem.PageShift) - - totalFreeFrames += regionEndFrameIndex - regionStartFrameIndex + 1 - } - - return true - }) + // region 1 extents get rounded to [0, 9f000] and provides 159 frames [0 to 158] + // region 1 uses the original extents [100000 - 7fe0000] and provides 32480 frames [256-32735] + var totalFreeFrames uint64 = 159 + 32480 var ( - alloc BootMemAllocator + alloc bootMemAllocator allocFrameCount uint64 ) - for alloc.Init(); ; allocFrameCount++ { + for { frame, err := alloc.AllocFrame() if err != nil { if err == errBootAllocOutOfMemory { @@ -43,10 +29,9 @@ func TestBootMemoryAllocator(t *testing.T) { } t.Fatalf("[frame %d] unexpected allocator error: %v", allocFrameCount, err) } - - expAddress := uintptr(uint64(alloc.lastAllocIndex) * uint64(mem.PageSize)) - if got := frame.Address(); got != expAddress { - t.Errorf("[frame %d] expected frame address to be 0x%x; got 0x%x", allocFrameCount, expAddress, got) + allocFrameCount++ + if frame != alloc.lastAllocFrame { + t.Errorf("[frame %d] expected allocated frame to be %d; got %d", allocFrameCount, alloc.lastAllocFrame, frame) } if !frame.Valid() { @@ -59,8 +44,32 @@ func TestBootMemoryAllocator(t *testing.T) { } } +func TestAllocatorPackageInit(t *testing.T) { + fb := mockTTY() + multiboot.SetInfoPtr(uintptr(unsafe.Pointer(&multibootMemoryMap[0]))) + + Init() + + var buf bytes.Buffer + for i := 0; i < len(fb); i += 2 { + if fb[i] == 0x0 { + continue + } + buf.WriteByte(fb[i]) + } + + exp := "[boot_mem_alloc] system memory map: [0x0000000000 - 0x000009fc00], size: 654336, type: available [0x000009fc00 - 0x00000a0000], size: 1024, type: reserved [0x00000f0000 - 0x0000100000], size: 65536, type: reserved [0x0000100000 - 0x0007fe0000], size: 133038080, type: available [0x0007fe0000 - 0x0008000000], size: 131072, type: reserved [0x00fffc0000 - 0x0100000000], size: 262144, type: reserved[boot_mem_alloc] free memory: 130559Kb" + if got := buf.String(); got != exp { + t.Fatalf("expected printMemoryMap to generate the following output:\n%q\ngot:\n%q", exp, got) + } +} + var ( - // A dump of multiboot data when running under qemu containing only the memory region tag. + // A dump of multiboot data when running under qemu containing only the + // memory region tag. The dump encodes the following available memory + // regions: + // [ 0 - 9fc00] length: 654336 + // [100000 - 7fe0000] length: 133038080 multibootMemoryMap = []byte{ 72, 5, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 160, 0, 0, 0, 24, 0, 0, 0, 0, 0, 0, 0, @@ -81,3 +90,13 @@ var ( 24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, } ) + +func mockTTY() []byte { + // Mock a tty to handle early.Printf output + mockConsoleFb := make([]byte, 160*25) + mockConsole := &console.Ega{} + mockConsole.Init(80, 25, uintptr(unsafe.Pointer(&mockConsoleFb[0]))) + hal.ActiveTerminal.AttachTo(mockConsole) + + return mockConsoleFb +}