Blog OS: Writing an Operating System in Rust

repository·main·Indexed 30 days ago

https://github.com/phil-opp/blog_os

A tutorial-based repository for learning low-level systems programming by building an operating system in Rust. Covers bare bones kernels, VGA text mode, interrupts, CPU exceptions, memory management (paging and heap allocation), and multitasking using Async/Await. Includes instructions for Docker environments and cross-compiling GNU Binutils for x86_64-elf.

Tokens
508.6K
Snippets
1.4K
Records
1.8K
Agent score
90%

What's inside phil-opp/blog_os

  1. Overview of Blog OS tutorial series

    main

    Blog OS provides step-by-step tutorials for building an operating system in Rust. The series is divided into several technical modules:

    • Bare Bones: Freestanding Rust binaries, minimal kernels, VGA text mode, and testing.
    • Interrupts: CPU exceptions, double faults, and hardware interrupts.
    • Memory Management: Paging implementation and heap allocation.
    • Multitasking: Async/Await implementation.

    Note: The current repository contains the second edition of the series. A first edition exists but is outdated and unmaintained.

  2. Understand the role of Executors and Wakers in async/await

    main

    In Rust's asynchronous model, futures do not progress unless they are polled. This necessitates two key components:

    Executors

    An Executor is a global component responsible for polling all active futures in the system until they complete. Instead of manually polling futures in a loop, you can use an executor to spawn futures as independent tasks. When a future returns Poll::Pending, the executor can switch to another future, allowing multiple asynchronous operations to progress concurrently.

    Wakers

    To avoid the inefficiency of constantly re-polling futures that are not ready, executors use the Waker API.

    • During each poll call, a Waker is passed to the future inside a Context object.
    • The Waker is created by the executor.
    • An asynchronous task (like a disk driver) can use this Waker to signal the executor when the task is ready to make progress again.
    • The executor will not call poll on a future again until it receives a notification via the Waker.
  3. Understand CPU Exceptions on x86-64

    main

    CPU exceptions occur when the processor encounters an error during instruction execution, such as division by zero or invalid memory access. When an exception occurs, the processor stops current execution and immediately calls a specific exception handler function defined in the Interrupt Descriptor Table (IDT).

    Common x86 exceptions include:

    • Page Fault: Occurs during illegal memory access (e.g., reading from an unmapped page or writing to a read-only page).
    • Invalid Opcode: Occurs when the current instruction is invalid (e.g., using unsupported SSE instructions on an older CPU).
    • General Protection Fault: A broad category of access violations, such as attempting to execute privileged instructions in user mode.
    • Double Fault: Occurs if a second exception happens while the processor is attempting to call the handler for the first exception, or if no handler is registered.
    • Triple Fault: Occurs if an exception happens while attempting to call the Double Fault handler. Most processors respond to a triple fault by resetting the system.
  4. Understand the limitations of a Bump Allocator

    main

    A Bump Allocator is extremely fast (often just a few assembly instructions) but has a significant limitation: it cannot reuse memory from individual deallocations unless all allocations are freed simultaneously.

    If a single long-lived allocation exists, the allocator cannot reset its pointer, causing subsequent allocations to eventually exhaust the heap. This makes it unsuitable as a general-purpose global allocator but useful for specific patterns like Arena Allocation (grouping allocations together to free them all at once).

  5. Understand CPU Exceptions in x86

    main

    CPU exceptions occur when an instruction encounters an error, such as accessing invalid memory or dividing by zero. When an exception occurs, the CPU interrupts its current task and immediately calls a specific handler function based on the exception type.

    Key exception types include:

    • Page Fault: Occurs during illegal memory accesses (e.g., reading unmapped memory or writing to read-only memory).
    • Invalid Opcode: Occurs when the current instruction is invalid or unsupported by the CPU.
    • General Protection Fault: A broad category for access violations, such as executing privileged instructions in user mode.
    • Double Fault: Occurs if a second exception happens while the CPU is attempting to call the handler for the first exception.
    • Triple Fault: A fatal error occurring if an exception happens while handling a Double Fault. This typically causes the processor to reset the system.
  6. Compare Slab and Buddy Allocator designs

    main

    When designing or choosing an allocator, consider these two common variations of the fixed-size block approach:

    Slab Allocator

    • Concept: Uses block sizes that correspond directly to specific kernel types.
    • Benefit: Eliminates memory waste for those types as they fit perfectly into blocks. It can also be used to implement an object pool pattern.
    • Usage: Often combined with other allocators to reduce waste.

    Buddy Allocator

    • Concept: Uses a binary tree data structure with block sizes that are powers of two.
    • Mechanism: When a block is needed, a larger block is split into two 'buddies'. When a block is freed, the allocator checks if its buddy is also free; if so, they are merged back into a larger block.
    • Benefit: Reduces external fragmentation by allowing small freed blocks to be recombined for larger allocations.
    • Trade-off: Can lead to significant internal fragmentation because it only supports power-of-two sizes.
  7. Understand Segmentation and Virtual Memory

    main

    Segmentation was originally designed to increase addressable memory. In x86, it uses segment registers to provide a displacement that is added to a memory address.

    Key Terms

    • Segment Registers: Specific registers used for different types of access: CS (code), SS (stack), DS (data), ES (extra), and FS/GS (general purpose).
    • Protected Mode: A mode where segment descriptors contain an index to a Descriptor Table (Global or Local), which includes the segment's base address, size, and access permissions.
    • Virtual Memory: An abstraction that translates virtual addresses (before translation) into physical addresses (after translation).

    Limitations: External Fragmentation

    Segmentation suffers from external fragmentation. Because it requires large, contiguous blocks of physical memory, the system may have enough total free memory to run a program, but if that memory is split into small, non-contiguous gaps, the program cannot be loaded without a costly defragmentation process (moving memory contents to create contiguous space).

  8. Compare allocator design trade-offs

    main

    When choosing a memory allocator for your kernel, consider the following design trade-offs:

    • Fixed-Size Block Allocator: Uses multiple free lists for predefined block sizes. It is very fast because allocation and deallocation only involve pushing/popping from the head of a list. However, it suffers from internal fragmentation because every allocation is rounded up to the next available block size.
    • Slab Allocation: Optimized for allocating common fixed-size structures, but may not be suitable for all general-purpose scenarios.
    • Buddy Allocation: Uses a binary tree to implement merging of free blocks. It is efficient for merging but can waste significant memory because it typically only supports block sizes that are powers of two.

    There is no single 'best' allocator; the choice depends on the specific workload of your kernel implementation.

  9. Understand the Linked List Allocator (Pool Allocator) concept

    main

    A Linked List Allocator (or pool allocator) manages an unbounded number of non-continuous, unused memory regions by using the freed memory regions themselves as backing storage.

    Instead of using an external collection (which is impossible for a heap allocator as it cannot depend on itself), each freed region stores a ListNode containing:

    • size: The size of the memory region.
    • next: A pointer to the next unused memory region.

    A single head pointer tracks the start of this list, allowing the allocator to traverse all available free regions regardless of their number.

  10. Understand Paging and Internal Fragmentation

    main

    Paging divides both virtual and physical memory into fixed-size blocks:

    • Pages: Blocks in virtual memory.
    • Frames: Blocks in physical memory.

    Advantages of Paging

    Unlike segmentation, paging allows a contiguous virtual memory region to be mapped to non-contiguous physical frames. This eliminates external fragmentation because any available frame can be used to satisfy a page request.

    Internal Fragmentation

    Paging introduces internal fragmentation. This occurs because memory regions are rarely an exact multiple of the page size. If a program requires 101 bytes and the page size is 50 bytes, the system must allocate 3 pages (150 bytes), wasting 49 bytes. While this wastes memory, it is predictable and does not require the expensive defragmentation processes used in segmentation.

  11. Understand CPU Exceptions

    main

    An exception is a signal from the CPU that the current instruction encountered an error. When an exception occurs, the CPU interrupts its current work and jumps to a specific handler function defined in the Interrupt Descriptor Table (IDT).

    Common exception types include:

    • Invalid Opcode: Occurs when the CPU encounters an instruction it doesn't recognize or isn't enabled (e.g., using SSE instructions without SSE enabled).
    • Page Fault: Occurs during illegal memory accesses, such as reading from an unmapped page or writing to a read-only page.
    • Double Fault: Occurs if a second exception happens while the CPU is attempting to call the handler for the first exception. This also happens if no handler is registered for an exception.
    • Triple Fault: A fatal error occurring if an exception happens while trying to call the double fault handler. This typically causes the CPU to reset and reboot the system.
  12. Understand Cooperative vs. Non-cooperative Multitasking

    main

    Multitasking allows multiple tasks to appear as if they are running simultaneously by switching execution between them. There are two primary models:

    Non-cooperative Multitasking

    • Mechanism: The OS controls task switching by using hardware interrupts (e.g., timer interrupts or I/O interrupts). When an interrupt occurs, the CPU jumps to an interrupt handler, allowing the OS to regain control and switch tasks.
    • State Management: Requires a context switch. The OS must save the entire state of the task (CPU registers, program counter, stack pointer) to a dedicated stack for that task (often called a thread).
    • Pros/Cons: Provides guaranteed CPU fairness and control over untrusted user code, but has higher memory overhead because each task requires its own stack.

    Cooperative Multitasking

    • Mechanism: Tasks voluntarily relinquish control of the CPU, typically via a yield operation. This is common in language-level implementations like coroutines and async/await.
    • State Management: Instead of saving the entire CPU state, tasks only save the specific state needed to resume (e.g., local variables). In Rust, async/await transforms tasks into state machines where necessary variables are stored in an automatically generated struct.
    • Pros/Cons: Highly efficient and allows many tasks to share a single call stack, significantly reducing memory consumption compared to threads.