Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 29 additions & 4 deletions book/src/kernel/memory/memory_layout.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ This is the structure of the memory for any process running in the system.
## Layout
```txt
0000_0000_0000_0000 .. FFFF_FF7F_FFFF_FFFF - User (15.99~ EB)
FFFF_FF80_0000_0000 .. FFFF_FFFF_7FFF_FFFF - Process specific kernel (510 GB)
FFFF_FF80_0000_0000 .. FFFF_FF82_0000_0000 - Processes kernel stacks (2 GB, 32768, each 256 KB)
FFFF_FF82_0000_0000 .. FFFF_FF82_0000_1000 - Processes kernel stacks bitmap (4 KB)
FFFF_FFFF_8000_0000 .. FFFF_FFFF_FFFF_FFFF - Kernel (2 GB)
```

Expand Down Expand Up @@ -40,10 +41,34 @@ This is useful for reading structures that are in specific location in physical

Its very simple, it will take memory from the `kernel extra` space, and map it to the physical address.

### Process specific kernel layout
### Processes kernel stacks layout
```txt
FFFF_FF80_0000_0000 .. FFFF_FF80_0000_1000 - process kernel stack 0 guard page (4KB) *not mapped by purpose*
FFFF_FF80_0000_1000 .. FFFF_FF80_0004_0000 - process kernel stack 0 (63 * 4KB = 252KB)
FFFF_FF80_0000_1000 .. FFFF_FF80_0004_1000 - process kernel stack 1 guard page (4KB) *not mapped by purpose*
FFFF_FF80_0004_1000 .. FFFF_FF80_0008_0000 - process kernel stack 1 (63 * 4KB = 252KB)
...
```

We have capacity to have `32768` kernel stacks, each of size `256KB`, which is `8GB` in total.

This space is mapped for each process, but each process have its own segment, but it can still
access all the rest of the kernel stacks.

This allows us to switch to a process from another process (while in kernel mode), without the need
to switch completely to the kernel stack (used by the kernel).

See below for the previous design, where each process had its own mapped space that other processes can't access.

The issue that this new design solves is that, we have to be very careful about when and how to change the context (going to user mode or switching to another process), for example, we can't switch to another process from a syscall, we have to switch
first to kernel mode, and then let the scheduler schedule another process. i.e. scheduler only work on kernel-only stack.

#### [Outdated] Process specific kernel layout
```txt
FFFF_FF80_0000_0000..FFFF_FF80_0000_1000 process kernel stack guard page (4KB) *not mapped by purpose*
FFFF_FF80_0000_1000..FFFF_FF80_0004_1000 process kernel stack (64 * 4KB = 256KB)
FFFF_FF80_0000_0000 .. FFFF_FFFF_7FFF_FFFF - Process specific kernel (510 GB)
---
FFFF_FF80_0000_0000 .. FFFF_FF80_0000_1000 - process kernel stack guard page (4KB) *not mapped by purpose*
FFFF_FF80_0000_1000 .. FFFF_FF80_0004_1000 - process kernel stack (64 * 4KB = 256KB)
```

This is a space specific to each process, but reside in kernel space.
Expand Down
6 changes: 5 additions & 1 deletion book/src/kernel/processes/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,20 @@ The process structure [`Process`][process_structure] contain all the information
- `priority`: The priority of the process, this is used by the scheduler. see [`PriorityLevel`](https://docs.rs/emerald_kernel_user_link/latest/emerald_kernel_user_link/process/enum.PriorityLevel.html).)
- `exit_code`: The exit code of the process, if the process is exited, this will be set to the exit code.
- `children_exits`: A list of the children processes that have exited, with their exit code (see #process-exit later for more information).
- `process_kernel_stack`: The kernel stack identifier of the process, this is a 252KB stack that is used when the process is in kernel mode (i.e. when an interrupt happens while the process is in user mode). This stack is shared between all processes, but each process has its own segment to use.

## Process Creation

Process creation (structure creation) is as follows:
- Creates a new `VirtualMemoryMapper` instance, which is a clone of the current kernel's virtual memory.
- Allocates new `ProcessKernelStack` which is a 252KB stack for the process to use when
its in kernel mode (interrupts, syscalls, etc...), this memory is mapped and accessible by all
processes, but each process has its own stack segment to use.
- Load the `ELF` file, this doesn't load the whole thing, just the header to make sure its valid.
- Maps the stack region.
- Loads argv into the stack (check [argv structure](#argv-structure) for more information).
- Load `ELF` regions into memory.
- Load the `Process Metadata` structure (check [Process Metadata](#process-metadata-structure) for more information).
- Add process-specific kernel memory regions, like the kernel stack (**this must be done after loading the ELF, and last modification to the VM manually, because we can't switch to this VM after this point unless its by the scheduler, see the comments `process/mod.rs::allocate_process` for more details**)
- Add data about the heap, with size `0` and max size `1GB`, i.e. no memory allocated yet.
- Default `context` is created, everything is `0`, except for:
- `rip`: The entry point of the `ELF` file.
Expand Down
1 change: 1 addition & 0 deletions book/src/kernel/processes/scheduler.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ here, since we can't do it while the process is running (still handling the `exi
Running the process is simple:
- copy the `context` of the `process` to the saved `context` of the `CPU`, see [processor saved state](../processor/index.md#saved-cpu-state), which will be used by the [scheduler interrupt](#scheduler-interrupt) to jump to it.
- Set the `pid` of the `process` to the `process_id` of the `CPU`.
- Load the `process kernel stack` into the `RSP[KERNEL_RING]` of the `TSS`, this is the stack that will be used when the process is in kernel mode, i.e. when an interrupt happens while the process is in user mode. (see [TSS](../processor/gdt.md#task-state-segment-tss)).
- Mark the `process` as `ProcessState::Running`, and move it to the `running_and_waiting` list as mentioned.

## Yielding
Expand Down
4 changes: 2 additions & 2 deletions book/src/kernel/processor/gdt.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,8 @@ A value of `None` means to use the default stack.
The default stack will be the current stack if the privilege level is the same as the current privilege level,
otherwise it will change to the stack specified in the [TSS] based on the target privilege level.

Currently, we only have 1 stack for `KERNEL_RING`, which is at `Process kernel stack` in the [memory layout](../memory/memory_layout.md).
I.e. this is a stack specific to each process, as this will only be used when transitioning from user to kernel mode, and inside user mode, we will always be inside a process.
For the `RSP` values, we only use the `KERNEL_RING`, and its being set to the `ProcessKernelStack` of a process before switching to it, i.e. it will change for each process.
This stack is one of the `Processes kernel stacks` (see [memory layout](../memory/memory_layout.md)).


[IDT]: https://wiki.osdev.org/Interrupt_Descriptor_Table
Expand Down
2 changes: 1 addition & 1 deletion kernel/src/acpi/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ impl Acpi {
apic::assign_io_irq(
acpi_handler as BasicInterruptHandler,
facp.sci_interrupt(),
cpu::cpu(),
&cpu::cpu(),
);

if !facp.is_acpi_enabled() {
Expand Down
229 changes: 120 additions & 109 deletions kernel/src/cpu/gdt.rs
Original file line number Diff line number Diff line change
@@ -1,20 +1,16 @@
use core::{mem, ptr::addr_of};
use core::{mem, pin::Pin, ptr::addr_of};

use crate::{
cpu,
memory_management::{
memory_layout::{
is_aligned, INTR_STACK_BASE, INTR_STACK_EMPTY_SIZE, INTR_STACK_ENTRY_SIZE,
INTR_STACK_SIZE, INTR_STACK_TOTAL_SIZE, PAGE_4K, PROCESS_KERNEL_STACK_END,
INTR_STACK_SIZE, INTR_STACK_TOTAL_SIZE, PAGE_4K,
},
virtual_memory_mapper::{self, VirtualMemoryMapEntry},
virtual_memory_mapper::{self, ProcessKernelStack, VirtualMemoryMapEntry},
},
sync::spin::mutex::Mutex,
};

static GDT: Mutex<GlobalDescriptorManager> = Mutex::new(GlobalDescriptorManager::empty());
/// SAFETY: TSS is only used when `GDT` is locked, so its safe to use as `static mut`
static mut TSS: TaskStateSegment = TaskStateSegment::empty();

pub const KERNEL_RING: u8 = 0;
pub const USER_RING: u8 = 3;

Expand All @@ -29,108 +25,18 @@ impl SegmentSelector {
}
}

/// This should be called only once, otherwise, it will crash
pub fn init_kernel_gdt() {
let mut manager = GDT.lock();
if manager.gdt.index != 1 {
panic!("GDT already initialized");
}

manager.kernel_code_seg = SegmentSelector::from_index(unsafe {
manager.gdt.push_user(UserDescriptorEntry {
access: flags::PRESENT | flags::CODE | flags::USER | flags::dpl(KERNEL_RING),
flags_and_limit: flags::LONG_MODE,
..UserDescriptorEntry::empty()
})
});
manager.user_code_seg = SegmentSelector::from_index(unsafe {
manager.gdt.push_user(UserDescriptorEntry {
access: flags::PRESENT | flags::CODE | flags::USER | flags::dpl(USER_RING),
flags_and_limit: flags::LONG_MODE,
..UserDescriptorEntry::empty()
})
});
manager.kernel_data_seg = SegmentSelector::from_index(unsafe {
manager.gdt.push_user(UserDescriptorEntry {
access: flags::PRESENT | flags::USER | flags::WRITE | flags::dpl(KERNEL_RING),
..UserDescriptorEntry::empty()
})
});
manager.user_data_seg = SegmentSelector::from_index(unsafe {
manager.gdt.push_user(UserDescriptorEntry {
access: flags::PRESENT | flags::USER | flags::WRITE | flags::dpl(USER_RING),
..UserDescriptorEntry::empty()
})
});

// setup TSS

// setup stacks, for each use `INTR_STACK_SIZE` bytes, but also allocate another one of these
// and use as padding between the stacks, so that we can detect stack overflows
for i in 0..7 {
unsafe {
// allocate after an empty offset, so that we can detect stack overflows
let stack_start_virtual =
INTR_STACK_BASE + (i * INTR_STACK_ENTRY_SIZE) + INTR_STACK_EMPTY_SIZE;
let stack_end_virtual = stack_start_virtual + INTR_STACK_SIZE;
assert!(stack_end_virtual <= INTR_STACK_BASE + INTR_STACK_TOTAL_SIZE);
if i == 6 {
// make sure we have allocated everything
assert_eq!(stack_end_virtual, INTR_STACK_BASE + INTR_STACK_TOTAL_SIZE);
}
// make sure that the stack is aligned, so we can easily allocate pages
assert!(
is_aligned(INTR_STACK_SIZE, PAGE_4K) && is_aligned(stack_start_virtual, PAGE_4K)
);

// map the stack
virtual_memory_mapper::map_kernel(&VirtualMemoryMapEntry {
virtual_address: stack_start_virtual,
physical_address: None,
size: INTR_STACK_SIZE,
flags: virtual_memory_mapper::flags::PTE_WRITABLE,
});

// set the stack pointer
// subtract 8, since the boundary is not mapped
TSS.ist[i] = stack_end_virtual as u64 - 8;
}

// A kernel stack for this process
// this will be used on transitions from user to kernel
unsafe { TSS.rsp[KERNEL_RING as usize] = PROCESS_KERNEL_STACK_END as u64 - 8 };
}

let tss_ptr = addr_of!(TSS) as u64;

manager.tss_seg = SegmentSelector::from_index(unsafe {
manager.gdt.push_system(SystemDescriptorEntry {
limit: (mem::size_of::<TaskStateSegment>() - 1) as u16,
access: flags::PRESENT | flags::TSS_TYPE,
base_low: (tss_ptr & 0xFFFF) as u16,
base_middle: ((tss_ptr >> 16) & 0xFF) as u8,
base_high: ((tss_ptr >> 24) & 0xFF) as u8,
base_upper: ((tss_ptr >> 32) & 0xFFFFFFFF) as u32,
..SystemDescriptorEntry::empty()
})
});
drop(manager);
// call the special `run_with` so that we get the `static` lifetime
GDT.run_with(|manager| {
manager.gdt.apply_lgdt();

manager.load_kernel_segments();
manager.load_tss();
});
}

pub fn get_user_code_seg_index() -> SegmentSelector {
GDT.run_with(|manager| manager.user_code_seg)
// TODO: for now, we use the segment from the current CPU
// technically, its all would be the same
cpu::cpu().gdt.user_code_seg
}

pub fn get_user_data_seg_index() -> SegmentSelector {
GDT.run_with(|manager| manager.user_data_seg)
// TODO: for now, we use the segment from the current CPU
// technically, its all would be the same
cpu::cpu().gdt.user_data_seg
}

mod flags {
// this is in the flags byte
pub const LONG_MODE: u8 = 1 << 5;
Expand Down Expand Up @@ -208,7 +114,8 @@ impl SystemDescriptorEntry {
///
/// This is the structure that is pointed to by the `TSS` descriptor
#[repr(C, packed(4))]
struct TaskStateSegment {
#[derive(Debug, Clone, Copy)]
pub struct TaskStateSegment {
reserved: u32,
rsp: [u64; 3],
reserved2: u64,
Expand Down Expand Up @@ -238,8 +145,10 @@ pub(super) struct GlobalDescriptorTablePointer {
base: *const GlobalDescriptorTable,
}

struct GlobalDescriptorManager {
#[derive(Debug, Clone, Copy)]
pub struct GlobalDescriptorManager {
gdt: GlobalDescriptorTable,
tss: TaskStateSegment,
kernel_code_seg: SegmentSelector,
user_code_seg: SegmentSelector,
// there is only one data segment and its not even used, as we are using
Expand All @@ -253,6 +162,7 @@ impl GlobalDescriptorManager {
pub const fn empty() -> Self {
Self {
gdt: GlobalDescriptorTable::empty(),
tss: TaskStateSegment::empty(),
kernel_code_seg: SegmentSelector::from_index(0),
kernel_data_seg: SegmentSelector::from_index(0),
user_code_seg: SegmentSelector::from_index(0),
Expand All @@ -261,6 +171,105 @@ impl GlobalDescriptorManager {
}
}

fn gdt(self: Pin<&Self>) -> Pin<&GlobalDescriptorTable> {
// SAFETY: This is safe because we are using `Pin<&Self>` which guarantees that
// the GDT is not moved, and we are accessing it in a read-only manner.
unsafe { self.map_unchecked(|s| &s.gdt) }
}

pub fn init_segments(mut self: Pin<&'static mut Self>) {
if self.gdt.index != 1 {
panic!("GDT already initialized");
}

self.kernel_code_seg = SegmentSelector::from_index(unsafe {
self.gdt.push_user(UserDescriptorEntry {
access: flags::PRESENT | flags::CODE | flags::USER | flags::dpl(KERNEL_RING),
flags_and_limit: flags::LONG_MODE,
..UserDescriptorEntry::empty()
})
});
self.user_code_seg = SegmentSelector::from_index(unsafe {
self.gdt.push_user(UserDescriptorEntry {
access: flags::PRESENT | flags::CODE | flags::USER | flags::dpl(USER_RING),
flags_and_limit: flags::LONG_MODE,
..UserDescriptorEntry::empty()
})
});
self.kernel_data_seg = SegmentSelector::from_index(unsafe {
self.gdt.push_user(UserDescriptorEntry {
access: flags::PRESENT | flags::USER | flags::WRITE | flags::dpl(KERNEL_RING),
..UserDescriptorEntry::empty()
})
});
self.user_data_seg = SegmentSelector::from_index(unsafe {
self.gdt.push_user(UserDescriptorEntry {
access: flags::PRESENT | flags::USER | flags::WRITE | flags::dpl(USER_RING),
..UserDescriptorEntry::empty()
})
});

// setup TSS

// setup stacks, for each use `INTR_STACK_SIZE` bytes, but also allocate another one of these
// and use as padding between the stacks, so that we can detect stack overflows
for i in 0..7 {
// allocate after an empty offset, so that we can detect stack overflows
let stack_start_virtual =
INTR_STACK_BASE + (i * INTR_STACK_ENTRY_SIZE) + INTR_STACK_EMPTY_SIZE;
let stack_end_virtual = stack_start_virtual + INTR_STACK_SIZE;
assert!(stack_end_virtual <= INTR_STACK_BASE + INTR_STACK_TOTAL_SIZE);
if i == 6 {
// make sure we have allocated everything
assert_eq!(stack_end_virtual, INTR_STACK_BASE + INTR_STACK_TOTAL_SIZE);
}
// make sure that the stack is aligned, so we can easily allocate pages
assert!(
is_aligned(INTR_STACK_SIZE, PAGE_4K) && is_aligned(stack_start_virtual, PAGE_4K)
);

// map the stack
virtual_memory_mapper::map_kernel(&VirtualMemoryMapEntry {
virtual_address: stack_start_virtual,
physical_address: None,
size: INTR_STACK_SIZE,
flags: virtual_memory_mapper::flags::PTE_WRITABLE,
});

// set the stack pointer
// subtract 8, since the boundary is not mapped
self.tss.ist[i] = stack_end_virtual as u64 - 8;

// // A kernel stack for this process
// // this will be used on transitions from user to kernel
// self.tss.rsp[KERNEL_RING as usize] = PROCESS_KERNEL_STACK_END as u64 - 8;
}

let tss_ptr = addr_of!(self.tss) as u64;

self.tss_seg = SegmentSelector::from_index(unsafe {
self.gdt.push_system(SystemDescriptorEntry {
limit: (mem::size_of::<TaskStateSegment>() - 1) as u16,
access: flags::PRESENT | flags::TSS_TYPE,
base_low: (tss_ptr & 0xFFFF) as u16,
base_middle: ((tss_ptr >> 16) & 0xFF) as u8,
base_high: ((tss_ptr >> 24) & 0xFF) as u8,
base_upper: ((tss_ptr >> 32) & 0xFFFFFFFF) as u32,
..SystemDescriptorEntry::empty()
})
});
// convert to ref at this point and do the actual loading
let s = self.into_ref();
s.gdt().apply_lgdt(); // apply the GDT
s.load_kernel_segments();
s.load_tss();
}

pub fn load_process_kernel_stack(&mut self, stack: &ProcessKernelStack) {
// set the process kernel stack in the TSS
self.tss.rsp[KERNEL_RING as usize] = stack.end_address() as u64 - 8;
}

pub fn load_kernel_segments(&self) {
assert_ne!(self.kernel_code_seg.0, 0);
unsafe {
Expand All @@ -281,6 +290,7 @@ impl GlobalDescriptorManager {
}

#[repr(C, packed(16))]
#[derive(Debug, Clone, Copy)]
struct GlobalDescriptorTable {
data: [u64; 8],
index: usize,
Expand Down Expand Up @@ -316,11 +326,12 @@ impl GlobalDescriptorTable {
index
}

pub fn apply_lgdt(&'static self) {
pub fn apply_lgdt(self: Pin<&'static Self>) {
let size_used = self.index * mem::size_of::<u64>() - 1;
let base: &'static Self = self.get_ref();
let gdt_ptr = GlobalDescriptorTablePointer {
limit: size_used as u16,
base: self,
base,
};

unsafe {
Expand Down
Loading
Loading