Compare commits
4 Commits
0c5aa72fd3
...
6910deae7c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6910deae7c | ||
|
|
42328ead0b | ||
|
|
f1de5b6da6 | ||
|
|
a6e6e3d8ca |
@@ -19,13 +19,20 @@ add_subdirectory(src)
|
||||
file(MAKE_DIRECTORY ${CMAKE_SOURCE_DIR}/release)
|
||||
file(MAKE_DIRECTORY ${CMAKE_BINARY_DIR}/isodir/boot/grub)
|
||||
|
||||
# Generate CPIO initial ramdisk from apps directory.
|
||||
# All files in apps/ are packed into a newc-format CPIO archive.
|
||||
# Build user-mode applications as flat binaries.
|
||||
set(APPS_BIN_DIR ${CMAKE_BINARY_DIR}/apps_bin)
|
||||
file(MAKE_DIRECTORY ${APPS_BIN_DIR})
|
||||
add_custom_target(apps
|
||||
COMMAND ${CMAKE_SOURCE_DIR}/scripts/build_apps.sh ${CMAKE_SOURCE_DIR}/apps ${APPS_BIN_DIR}
|
||||
COMMENT "Building user-mode applications"
|
||||
)
|
||||
|
||||
# Generate CPIO initial ramdisk from built app binaries.
|
||||
set(INITRD_FILE ${CMAKE_BINARY_DIR}/isodir/boot/initrd.cpio)
|
||||
add_custom_command(
|
||||
OUTPUT ${INITRD_FILE}
|
||||
COMMAND ${CMAKE_SOURCE_DIR}/scripts/gen_initrd.sh ${CMAKE_SOURCE_DIR}/apps ${INITRD_FILE}
|
||||
DEPENDS ${CMAKE_SOURCE_DIR}/apps
|
||||
COMMAND ${CMAKE_SOURCE_DIR}/scripts/gen_initrd.sh ${APPS_BIN_DIR} ${INITRD_FILE}
|
||||
DEPENDS apps
|
||||
COMMENT "Generating CPIO initial ramdisk"
|
||||
)
|
||||
add_custom_target(initrd DEPENDS ${INITRD_FILE})
|
||||
|
||||
@@ -53,8 +53,8 @@ Once a task is completed, it should be checked off.
|
||||
- [x] Update the build script to generate a ramdisk containing any applications to run. This initial ramdisk is in CPIO format.
|
||||
- [x] Write a VFS subsystem.
|
||||
- [x] Write a VFS driver that provides the contents of the CPIO initial ramdisk to the VFS layer.
|
||||
- [ ] Create a `hello-world` app. It should print `Hello, World` to its own stdout. The kernel should route this to Qemu and to the VGA dispaly. Ensure this work.
|
||||
- [ ] Implement the fork system call.
|
||||
- [x] Create a `hello-world` app. It should print `Hello, World` to its own stdout. The kernel should route this to Qemu and to the VGA dispaly. Ensure this work.
|
||||
- [x] Implement the fork system call.
|
||||
- [ ] Implement environment variables. Apps should be able to modify this, and it should be copied to new forks of an app.
|
||||
- [ ] Create a basic shell program `sh`. This shell must be able to start the hello-world app. It must include `cd` as a built-in to change the current working directory.
|
||||
- [ ] Create an `ls` app. It should list the contents of the current working directory, via the environment variable.
|
||||
|
||||
79
apps/fork-test/fork-test.S
Normal file
79
apps/fork-test/fork-test.S
Normal file
@@ -0,0 +1,79 @@
|
||||
#
|
||||
# fork-test: Tests the fork system call.
|
||||
#
|
||||
# 1. Calls SYS_FORK
|
||||
# 2. Parent prints "Parent: pid=<pid>\n" and waits for child
|
||||
# 3. Child prints "Child: pid=0\n" and exits with code 7
|
||||
# 4. Parent exits with code 0
|
||||
#
|
||||
|
||||
.section .text
|
||||
.global _start
|
||||
|
||||
# System call numbers
|
||||
.equ SYS_EXIT, 0
|
||||
.equ SYS_WRITE, 1
|
||||
.equ SYS_FORK, 3
|
||||
.equ SYS_GETPID, 4
|
||||
.equ SYS_WAITPID, 6
|
||||
|
||||
_start:
|
||||
# Fork
|
||||
movl $SYS_FORK, %eax
|
||||
int $0x80
|
||||
|
||||
# EAX = 0 in child, child PID in parent
|
||||
testl %eax, %eax
|
||||
jz .child
|
||||
|
||||
.parent:
|
||||
# Save child PID on the stack
|
||||
pushl %eax
|
||||
|
||||
# Print "Parent\n"
|
||||
movl $SYS_WRITE, %eax
|
||||
movl $1, %ebx # fd = stdout
|
||||
movl $parent_msg, %ecx
|
||||
movl $parent_msg_len, %edx
|
||||
int $0x80
|
||||
|
||||
# Waitpid for child
|
||||
popl %ebx # child PID
|
||||
movl $SYS_WAITPID, %eax
|
||||
int $0x80
|
||||
# EAX now has child's exit code (should be 7)
|
||||
|
||||
# Print "Reaped\n"
|
||||
pushl %eax # save exit code
|
||||
movl $SYS_WRITE, %eax
|
||||
movl $1, %ebx
|
||||
movl $reaped_msg, %ecx
|
||||
movl $reaped_msg_len, %edx
|
||||
int $0x80
|
||||
popl %ebx # exit code (unused, exit with 0)
|
||||
|
||||
# Exit with code 0
|
||||
movl $SYS_EXIT, %eax
|
||||
movl $0, %ebx
|
||||
int $0x80
|
||||
|
||||
.child:
|
||||
# Print "Child\n"
|
||||
movl $SYS_WRITE, %eax
|
||||
movl $1, %ebx # fd = stdout
|
||||
movl $child_msg, %ecx
|
||||
movl $child_msg_len, %edx
|
||||
int $0x80
|
||||
|
||||
# Exit with code 7
|
||||
movl $SYS_EXIT, %eax
|
||||
movl $7, %ebx
|
||||
int $0x80
|
||||
|
||||
.section .rodata
|
||||
parent_msg: .ascii "Parent\n"
|
||||
.equ parent_msg_len, . - parent_msg
|
||||
child_msg: .ascii "Child\n"
|
||||
.equ child_msg_len, . - child_msg
|
||||
reaped_msg: .ascii "Reaped\n"
|
||||
.equ reaped_msg_len, . - reaped_msg
|
||||
28
apps/hello-world/hello-world.S
Normal file
28
apps/hello-world/hello-world.S
Normal file
@@ -0,0 +1,28 @@
|
||||
# hello-world.S - ClaudeOS user-mode hello world application
|
||||
# Assembled as flat binary, loaded at USER_CODE_START (0x08048000)
|
||||
#
|
||||
# Uses INT 0x80 system calls:
|
||||
# SYS_WRITE(1): EAX=1, EBX=fd, ECX=buf, EDX=len
|
||||
# SYS_EXIT(0): EAX=0, EBX=code
|
||||
|
||||
.code32
|
||||
.section .text
|
||||
.globl _start
|
||||
_start:
|
||||
/* SYS_WRITE(stdout, "Hello, World\n", 13) */
|
||||
movl $1, %eax /* SYS_WRITE */
|
||||
movl $1, %ebx /* fd = stdout */
|
||||
movl $msg, %ecx /* buf = absolute address of message */
|
||||
movl $13, %edx /* len = 13 */
|
||||
int $0x80
|
||||
|
||||
/* SYS_EXIT(0) */
|
||||
movl $0, %eax /* SYS_EXIT */
|
||||
movl $0, %ebx /* exit code = 0 */
|
||||
int $0x80
|
||||
|
||||
/* Safety: infinite loop (should never reach here) */
|
||||
jmp .
|
||||
|
||||
msg:
|
||||
.ascii "Hello, World\n"
|
||||
25
apps/user.ld
Normal file
25
apps/user.ld
Normal file
@@ -0,0 +1,25 @@
|
||||
/* Linker script for ClaudeOS user-mode flat binary.
|
||||
* Applications are loaded at 0x08048000 (USER_CODE_START).
|
||||
* This produces a flat binary (no ELF headers). */
|
||||
|
||||
ENTRY(_start)
|
||||
|
||||
SECTIONS {
|
||||
. = 0x08048000;
|
||||
|
||||
.text : {
|
||||
*(.text)
|
||||
}
|
||||
|
||||
.rodata : {
|
||||
*(.rodata*)
|
||||
}
|
||||
|
||||
.data : {
|
||||
*(.data)
|
||||
}
|
||||
|
||||
.bss : {
|
||||
*(.bss)
|
||||
}
|
||||
}
|
||||
83
docs/fork.md
Normal file
83
docs/fork.md
Normal file
@@ -0,0 +1,83 @@
|
||||
# Fork System Call
|
||||
|
||||
## Overview
|
||||
|
||||
The `fork()` system call duplicates the calling process, creating a new child
|
||||
process with an independent copy of the parent's address space.
|
||||
|
||||
## System Call Interface
|
||||
|
||||
- **Number**: `SYS_FORK` (3)
|
||||
- **Arguments**: None
|
||||
- **Returns**: Child PID in the parent, 0 in the child, -1 on error
|
||||
|
||||
## Implementation
|
||||
|
||||
### Address Space Cloning
|
||||
|
||||
`paging_clone_directory_from(src_pd_phys)` performs a deep copy of a process's
|
||||
page directory:
|
||||
|
||||
1. **Kernel-space entries** (no `PAGE_USER` flag): shared directly between
|
||||
parent and child. Both processes see the same kernel mappings.
|
||||
|
||||
2. **User-space entries** (`PAGE_USER` flag set): fully deep-copied. For each
|
||||
user-space page directory entry:
|
||||
- A new page table is allocated
|
||||
- Each present user page has a new physical page allocated and the content
|
||||
copied byte-for-byte
|
||||
- This ensures parent and child have completely independent memory
|
||||
|
||||
### Register State
|
||||
|
||||
The child receives a copy of the parent's register state at the time of the
|
||||
`INT 0x80` syscall, with `EAX` set to 0. This means the child resumes execution
|
||||
at the instruction immediately following the `INT 0x80` that triggered fork.
|
||||
|
||||
### Process Exit and Waitpid
|
||||
|
||||
`process_exit()` was refactored to support multi-process scenarios:
|
||||
|
||||
- When a process exits, it scans for any process blocked on `waitpid()` for
|
||||
its PID and unblocks it, setting the waiter's saved `EAX` to the exit code.
|
||||
- If another process is ready, `process_switch_to_user()` is called to
|
||||
directly context-switch via an assembly stub that loads the full register
|
||||
set and performs `iret`.
|
||||
- If no processes remain, the system halts.
|
||||
|
||||
`sys_waitpid()` supports blocking:
|
||||
|
||||
- If the child is already a zombie, it reaps immediately
|
||||
- Otherwise, the caller is marked `PROCESS_BLOCKED` and the scheduler is
|
||||
invoked to switch to another process
|
||||
- When the child exits, the parent is unblocked with the exit code
|
||||
|
||||
### Assembly Support
|
||||
|
||||
`process_switch_to_user` in `interrupts.S` loads a full `registers_t` struct
|
||||
and performs `iret` to enter user mode. This is used when `process_exit()`
|
||||
needs to context-switch outside the normal ISR return path.
|
||||
|
||||
## Syscall Flow
|
||||
|
||||
```
|
||||
User: INT 0x80 (EAX=SYS_FORK)
|
||||
→ ISR stub pushes registers
|
||||
→ isr_handler → syscall_handler → sys_fork(regs)
|
||||
→ process_fork(regs)
|
||||
→ Clone page directory with deep user-page copy
|
||||
→ Copy current interrupt frame to child (EAX=0)
|
||||
→ Return child PID to parent (via EAX)
|
||||
→ ISR stub pops registers, iret
|
||||
→ Parent continues with EAX=child_pid
|
||||
→ [Timer interrupt] → scheduler picks child
|
||||
→ Child starts with EAX=0
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
The `fork-test` application validates fork by:
|
||||
1. Calling `SYS_FORK`
|
||||
2. Parent prints "Parent" and calls `SYS_WAITPID`
|
||||
3. Child prints "Child" and exits with code 7
|
||||
4. Parent reaps child, prints "Reaped", exits with code 0
|
||||
49
scripts/build_apps.sh
Executable file
49
scripts/build_apps.sh
Executable file
@@ -0,0 +1,49 @@
|
||||
#!/bin/sh
|
||||
# Build all user-mode applications as flat binaries.
|
||||
# Usage: build_apps.sh <apps_dir> <output_dir>
|
||||
# Each app directory in <apps_dir>/ gets compiled and its flat binary
|
||||
# is placed in <output_dir>/.
|
||||
set -e
|
||||
|
||||
APPS_DIR="$1"
|
||||
OUTPUT_DIR="$2"
|
||||
LINKER_SCRIPT="$APPS_DIR/user.ld"
|
||||
|
||||
CC="${CC:-clang}"
|
||||
OBJCOPY="${OBJCOPY:-objcopy}"
|
||||
CFLAGS="-ffreestanding -m32 -fno-pie -fno-pic -fno-builtin -fno-stack-protector -mno-sse -mno-mmx -O2 -Wall"
|
||||
LDFLAGS="-m32 -nostdlib -no-pie -Wl,--no-dynamic-linker"
|
||||
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
|
||||
for app_dir in "$APPS_DIR"/*/; do
|
||||
[ -d "$app_dir" ] || continue
|
||||
app_name=$(basename "$app_dir")
|
||||
|
||||
echo "Building app: $app_name"
|
||||
|
||||
# Collect source files
|
||||
OBJ_FILES=""
|
||||
for src in "$app_dir"*.S "$app_dir"*.c; do
|
||||
[ -f "$src" ] || continue
|
||||
obj="$OUTPUT_DIR/${app_name}_$(basename "${src%.*}").o"
|
||||
$CC $CFLAGS -c "$src" -o "$obj"
|
||||
OBJ_FILES="$OBJ_FILES $obj"
|
||||
done
|
||||
|
||||
if [ -z "$OBJ_FILES" ]; then
|
||||
echo " No sources found, skipping"
|
||||
continue
|
||||
fi
|
||||
|
||||
# Link into ELF
|
||||
elf="$OUTPUT_DIR/$app_name.elf"
|
||||
$CC $LDFLAGS -T "$LINKER_SCRIPT" $OBJ_FILES -o "$elf"
|
||||
|
||||
# Convert to flat binary (strip non-code sections)
|
||||
bin="$OUTPUT_DIR/$app_name"
|
||||
$OBJCOPY -O binary --only-section=.text --only-section=.rodata --only-section=.data "$elf" "$bin"
|
||||
|
||||
size=$(wc -c < "$bin")
|
||||
echo " Built: $bin ($size bytes)"
|
||||
done
|
||||
@@ -1,14 +1,16 @@
|
||||
#!/bin/sh
|
||||
# Generate CPIO initial ramdisk from apps directory
|
||||
# Usage: gen_initrd.sh <apps_dir> <output_file>
|
||||
# Generate CPIO initial ramdisk from built application binaries.
|
||||
# Usage: gen_initrd.sh <binaries_dir> <output_file>
|
||||
# Packs all files in <binaries_dir> into a newc-format CPIO archive.
|
||||
set -e
|
||||
|
||||
APPS_DIR="$1"
|
||||
BIN_DIR="$1"
|
||||
OUTPUT="$2"
|
||||
|
||||
# Ensure output directory exists
|
||||
mkdir -p "$(dirname "$OUTPUT")"
|
||||
|
||||
cd "$APPS_DIR"
|
||||
find . -not -name '.' | cpio -o -H newc > "$OUTPUT" 2>/dev/null
|
||||
cd "$BIN_DIR"
|
||||
# Only pack actual binary files (no .o, .elf intermediates)
|
||||
find . -maxdepth 1 -type f ! -name '*.o' ! -name '*.elf' | cpio -o -H newc > "$OUTPUT" 2>/dev/null
|
||||
echo "Generated initrd: $(wc -c < "$OUTPUT") bytes"
|
||||
|
||||
@@ -174,3 +174,28 @@ enter_usermode:
|
||||
push $0x1B /* CS (user code) */
|
||||
push %ecx /* EIP (entry point) */
|
||||
iret
|
||||
|
||||
/*
|
||||
* process_switch_to_user - Restore full register state and iret to user mode.
|
||||
* void process_switch_to_user(registers_t *regs);
|
||||
*
|
||||
* Used by process_exit to context-switch to the next process when the normal
|
||||
* interrupt-return path isn't available (because we're not returning through
|
||||
* an ISR stub). Loads all registers from the registers_t struct and performs
|
||||
* iret to enter user mode.
|
||||
*/
|
||||
.global process_switch_to_user
|
||||
.type process_switch_to_user, @function
|
||||
process_switch_to_user:
|
||||
movl 4(%esp), %esp /* Point ESP to the registers_t struct */
|
||||
|
||||
/* Restore segment register (ds → all data segments) */
|
||||
pop %eax
|
||||
mov %ax, %ds
|
||||
mov %ax, %es
|
||||
mov %ax, %fs
|
||||
mov %ax, %gs
|
||||
|
||||
popa /* Restore EAX-EDI */
|
||||
addl $8, %esp /* Skip int_no and err_code */
|
||||
iret /* Pops EIP, CS, EFLAGS, UserESP, SS */
|
||||
|
||||
75
src/kernel.c
75
src/kernel.c
@@ -152,64 +152,31 @@ void kernel_main(uint32_t magic, uint32_t addr) {
|
||||
offset_print("FAILED to kmalloc\n");
|
||||
}
|
||||
|
||||
/*
|
||||
* Create a minimal test user-mode program.
|
||||
* This is flat binary machine code that calls SYS_WRITE then SYS_EXIT.
|
||||
*
|
||||
* The program writes "Hello from Ring 3!\n" to stdout (fd=1) via INT 0x80,
|
||||
* then exits with code 42.
|
||||
*
|
||||
* Assembly (i386):
|
||||
* ; SYS_WRITE(1, msg, 19)
|
||||
* mov eax, 1 ; SYS_WRITE
|
||||
* mov ebx, 1 ; fd = stdout
|
||||
* call next ; get EIP for position-independent addressing
|
||||
* next:
|
||||
* pop ecx ; ECX = address of 'next'
|
||||
* add ecx, 25 ; ECX = address of message string (offset to msg)
|
||||
* mov edx, 19 ; len = 19
|
||||
* int 0x80
|
||||
* ; SYS_EXIT(42)
|
||||
* mov eax, 0 ; SYS_EXIT
|
||||
* mov ebx, 42 ; code = 42
|
||||
* int 0x80
|
||||
* ; loop forever (shouldn't reach here)
|
||||
* jmp $
|
||||
* msg:
|
||||
* db "Hello from Ring 3!", 10
|
||||
*/
|
||||
static const uint8_t user_program[] = {
|
||||
0xB8, 0x01, 0x00, 0x00, 0x00, /* mov eax, 1 (SYS_WRITE) */
|
||||
0xBB, 0x01, 0x00, 0x00, 0x00, /* mov ebx, 1 (stdout) */
|
||||
0xE8, 0x00, 0x00, 0x00, 0x00, /* call next (push EIP) */
|
||||
/* next: offset 15 */
|
||||
0x59, /* pop ecx */
|
||||
0x83, 0xC1, 0x19, /* add ecx, 25 (offset from 'next' to msg) */
|
||||
0xBA, 0x13, 0x00, 0x00, 0x00, /* mov edx, 19 (length) */
|
||||
0xCD, 0x80, /* int 0x80 */
|
||||
/* SYS_EXIT(42): offset 26 */
|
||||
0xB8, 0x00, 0x00, 0x00, 0x00, /* mov eax, 0 (SYS_EXIT) */
|
||||
0xBB, 0x2A, 0x00, 0x00, 0x00, /* mov ebx, 42 (exit code) */
|
||||
0xCD, 0x80, /* int 0x80 */
|
||||
0xEB, 0xFE, /* jmp $ (infinite loop safety) */
|
||||
/* msg: offset 40 */
|
||||
'H','e','l','l','o',' ','f','r','o','m',' ',
|
||||
'R','i','n','g',' ','3','!','\n'
|
||||
};
|
||||
/* Load hello-world from the initrd and run it as a user process */
|
||||
cpio_entry_t app_entry;
|
||||
if (cpio_find("hello-world", &app_entry) == 0) {
|
||||
offset_print("Found hello-world in initrd (");
|
||||
print_hex(app_entry.datasize);
|
||||
offset_print(" bytes)\n");
|
||||
|
||||
int32_t pid = process_create("init", user_program, sizeof(user_program));
|
||||
if (pid > 0) {
|
||||
offset_print("Created init process, pid=");
|
||||
print_hex((uint32_t)pid);
|
||||
int32_t pid = process_create("hello-world",
|
||||
app_entry.data,
|
||||
app_entry.datasize);
|
||||
if (pid > 0) {
|
||||
offset_print("Created hello-world process, pid=");
|
||||
print_hex((uint32_t)pid);
|
||||
|
||||
/* Enable interrupts before entering user mode */
|
||||
asm volatile("sti");
|
||||
offset_print("Interrupts enabled\n");
|
||||
/* Enable interrupts before entering user mode */
|
||||
asm volatile("sti");
|
||||
offset_print("Interrupts enabled\n");
|
||||
|
||||
/* Enter user mode - does not return */
|
||||
process_run_first();
|
||||
/* Enter user mode - does not return */
|
||||
process_run_first();
|
||||
} else {
|
||||
offset_print("FAILED to create hello-world process\n");
|
||||
}
|
||||
} else {
|
||||
offset_print("FAILED to create init process\n");
|
||||
offset_print("hello-world not found in initrd\n");
|
||||
}
|
||||
|
||||
/* Enable interrupts */
|
||||
|
||||
58
src/paging.c
58
src/paging.c
@@ -299,6 +299,64 @@ uint32_t paging_clone_directory(void) {
|
||||
return new_dir_phys;
|
||||
}
|
||||
|
||||
uint32_t paging_clone_directory_from(uint32_t src_pd_phys) {
|
||||
uint32_t *src_pd = (uint32_t *)src_pd_phys;
|
||||
|
||||
/* Allocate a new page directory */
|
||||
phys_addr_t new_pd_phys = pmm_alloc_page(PMM_ZONE_NORMAL);
|
||||
if (new_pd_phys == 0) {
|
||||
offset_print(" PAGING: cannot allocate page directory for fork\n");
|
||||
return 0;
|
||||
}
|
||||
uint32_t *new_pd = (uint32_t *)new_pd_phys;
|
||||
|
||||
/* Copy all page directory entries (shares kernel mappings) */
|
||||
memcpy(new_pd, src_pd, 4096);
|
||||
|
||||
/* Deep-copy user-space page tables (those with PAGE_USER set) */
|
||||
for (uint32_t i = 0; i < PAGE_ENTRIES; i++) {
|
||||
if (!(src_pd[i] & PAGE_PRESENT)) continue;
|
||||
if (!(src_pd[i] & PAGE_USER)) continue; /* kernel entry, shared */
|
||||
|
||||
uint32_t *src_pt = (uint32_t *)(src_pd[i] & 0xFFFFF000);
|
||||
|
||||
/* Allocate a new page table */
|
||||
phys_addr_t new_pt_phys = pmm_alloc_page(PMM_ZONE_NORMAL);
|
||||
if (new_pt_phys == 0) {
|
||||
offset_print(" PAGING: fork: cannot allocate page table\n");
|
||||
return 0; /* TODO: free partially allocated pages */
|
||||
}
|
||||
uint32_t *new_pt = (uint32_t *)new_pt_phys;
|
||||
|
||||
/* Deep-copy each page in the page table */
|
||||
for (uint32_t j = 0; j < PAGE_ENTRIES; j++) {
|
||||
if (!(src_pt[j] & PAGE_PRESENT)) {
|
||||
new_pt[j] = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (src_pt[j] & PAGE_USER) {
|
||||
/* User page: allocate new physical page and copy content */
|
||||
phys_addr_t old_phys = src_pt[j] & 0xFFFFF000;
|
||||
phys_addr_t new_phys = pmm_alloc_page(PMM_ZONE_NORMAL);
|
||||
if (new_phys == 0) {
|
||||
offset_print(" PAGING: fork: cannot allocate page\n");
|
||||
return 0;
|
||||
}
|
||||
memcpy((void *)new_phys, (void *)old_phys, 4096);
|
||||
new_pt[j] = new_phys | (src_pt[j] & 0xFFF);
|
||||
} else {
|
||||
/* Kernel page within a user page table: share directly */
|
||||
new_pt[j] = src_pt[j];
|
||||
}
|
||||
}
|
||||
|
||||
new_pd[i] = new_pt_phys | (src_pd[i] & 0xFFF);
|
||||
}
|
||||
|
||||
return new_pd_phys;
|
||||
}
|
||||
|
||||
void paging_map_page_in(uint32_t *pd, uint32_t vaddr, uint32_t paddr, uint32_t flags) {
|
||||
uint32_t pd_idx = PD_INDEX(vaddr);
|
||||
uint32_t pt_idx = PT_INDEX(vaddr);
|
||||
|
||||
11
src/paging.h
11
src/paging.h
@@ -98,6 +98,17 @@ uint32_t paging_get_directory_phys(void);
|
||||
*/
|
||||
uint32_t paging_clone_directory(void);
|
||||
|
||||
/**
|
||||
* Clone a page directory, deep-copying all user-space pages.
|
||||
* Kernel-space entries are shared (same page tables). User-space page
|
||||
* tables and their physical pages are duplicated so the clone is fully
|
||||
* independent.
|
||||
*
|
||||
* @param src_pd_phys Physical address of the source page directory.
|
||||
* @return Physical address of the new page directory, or 0 on failure.
|
||||
*/
|
||||
uint32_t paging_clone_directory_from(uint32_t src_pd_phys);
|
||||
|
||||
/**
|
||||
* Map a page in a specific page directory (not necessarily the active one).
|
||||
*
|
||||
|
||||
@@ -234,18 +234,43 @@ void process_exit(int32_t code) {
|
||||
current_process->state = PROCESS_ZOMBIE;
|
||||
current_process->exit_code = code;
|
||||
|
||||
/* Find another process to run.
|
||||
* We construct a minimal register frame to pass to schedule_tick.
|
||||
* Since the process is zombie, schedule_tick won't save its state. */
|
||||
registers_t dummy;
|
||||
memset(&dummy, 0, sizeof(dummy));
|
||||
schedule_tick(&dummy);
|
||||
|
||||
/* If we get here, no other process was ready. Halt. */
|
||||
offset_print(" PROCESS: no processes remaining, halting\n");
|
||||
for (;;) {
|
||||
__asm__ volatile("hlt");
|
||||
/* Wake any process blocked on waitpid for this PID */
|
||||
for (int i = 0; i < MAX_PROCESSES; i++) {
|
||||
if (process_table[i].state == PROCESS_BLOCKED &&
|
||||
process_table[i].waiting_for_pid == current_process->pid) {
|
||||
process_table[i].state = PROCESS_READY;
|
||||
process_table[i].saved_regs.eax = (uint32_t)code;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/* Find next ready process to switch to */
|
||||
process_t *next = NULL;
|
||||
for (int i = 0; i < MAX_PROCESSES; i++) {
|
||||
if (process_table[i].state == PROCESS_READY) {
|
||||
next = &process_table[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!next) {
|
||||
offset_print(" PROCESS: no processes remaining, halting\n");
|
||||
for (;;) {
|
||||
__asm__ volatile("cli; hlt");
|
||||
}
|
||||
}
|
||||
|
||||
/* Context switch to the next process via assembly stub */
|
||||
current_process = next;
|
||||
next->state = PROCESS_RUNNING;
|
||||
tss_set_kernel_stack(next->kernel_stack_top);
|
||||
paging_switch_directory(next->page_directory);
|
||||
|
||||
extern void process_switch_to_user(registers_t *regs);
|
||||
process_switch_to_user(&next->saved_regs);
|
||||
|
||||
/* Should never reach here */
|
||||
__builtin_unreachable();
|
||||
}
|
||||
|
||||
process_t *process_current(void) {
|
||||
@@ -262,7 +287,7 @@ process_t *process_get(uint32_t pid) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int32_t process_fork(void) {
|
||||
int32_t process_fork(registers_t *regs) {
|
||||
if (!current_process) {
|
||||
return -1;
|
||||
}
|
||||
@@ -278,6 +303,7 @@ int32_t process_fork(void) {
|
||||
child->pid = next_pid++;
|
||||
child->state = PROCESS_READY;
|
||||
child->parent_pid = current_process->pid;
|
||||
child->waiting_for_pid = 0;
|
||||
|
||||
/* Allocate a separate kernel stack for the child */
|
||||
void *child_kstack = paging_alloc_page();
|
||||
@@ -288,17 +314,27 @@ int32_t process_fork(void) {
|
||||
child->kernel_stack = (uint32_t)child_kstack;
|
||||
child->kernel_stack_top = child->kernel_stack + 4096;
|
||||
|
||||
/* Clone the page directory */
|
||||
child->page_directory = paging_clone_directory();
|
||||
/* Deep-clone the parent's page directory (copies all user-space pages) */
|
||||
child->page_directory = paging_clone_directory_from(current_process->page_directory);
|
||||
if (!child->page_directory) {
|
||||
kfree((void *)child->kernel_stack);
|
||||
paging_free_page((void *)child->kernel_stack);
|
||||
child->state = PROCESS_UNUSED;
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Copy the current syscall registers to the child.
|
||||
* This ensures the child resumes at the same point as the parent
|
||||
* (right after the INT 0x80 instruction). */
|
||||
child->saved_regs = *regs;
|
||||
|
||||
/* Child's return value is 0 (in EAX) */
|
||||
child->saved_regs.eax = 0;
|
||||
|
||||
offset_print(" PROCESS: forked pid ");
|
||||
print_hex(current_process->pid);
|
||||
offset_print(" PROCESS: -> child pid ");
|
||||
print_hex(child->pid);
|
||||
|
||||
/* Parent's return value is child's PID */
|
||||
return (int32_t)child->pid;
|
||||
}
|
||||
|
||||
@@ -56,6 +56,7 @@ typedef struct process {
|
||||
uint32_t entry_point; /**< User-mode entry point. */
|
||||
int32_t exit_code; /**< Exit code (if ZOMBIE). */
|
||||
uint32_t parent_pid; /**< Parent process ID. */
|
||||
uint32_t waiting_for_pid; /**< PID we are blocked waiting for (if BLOCKED). */
|
||||
char name[32]; /**< Process name (for debugging). */
|
||||
} process_t;
|
||||
|
||||
@@ -113,10 +114,12 @@ process_t *process_get(uint32_t pid);
|
||||
|
||||
/**
|
||||
* Fork the current process.
|
||||
* Clones the current process's address space and register state.
|
||||
*
|
||||
* @param regs Pointer to the current interrupt frame (syscall registers).
|
||||
* @return PID of the child in the parent, 0 in the child, -1 on error.
|
||||
*/
|
||||
int32_t process_fork(void);
|
||||
int32_t process_fork(registers_t *regs);
|
||||
|
||||
/**
|
||||
* Start the first user-mode process. Does not return if a process is ready.
|
||||
|
||||
@@ -13,6 +13,10 @@
|
||||
#include "vga.h"
|
||||
#include <stddef.h>
|
||||
|
||||
/** Magic return value indicating the syscall blocked and switched processes.
|
||||
* syscall_handler must NOT overwrite regs->eax in this case. */
|
||||
#define SYSCALL_SWITCHED 0x7FFFFFFF
|
||||
|
||||
/* Debug print helpers defined in kernel.c */
|
||||
extern void offset_print(const char *str);
|
||||
extern void print_hex(uint32_t val);
|
||||
@@ -66,8 +70,7 @@ static int32_t sys_read(registers_t *regs) {
|
||||
* Handle SYS_FORK: fork the current process.
|
||||
*/
|
||||
static int32_t sys_fork(registers_t *regs) {
|
||||
(void)regs;
|
||||
return process_fork();
|
||||
return process_fork(regs);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -90,6 +93,11 @@ static int32_t sys_yield(registers_t *regs) {
|
||||
|
||||
/**
|
||||
* Handle SYS_WAITPID: wait for a child to exit.
|
||||
*
|
||||
* If the child is already a zombie, reaps immediately and returns the code.
|
||||
* Otherwise, blocks the current process and switches to the next one.
|
||||
* When the child exits, process_exit() will unblock the waiting parent
|
||||
* and set its saved_regs.eax to the exit code.
|
||||
*/
|
||||
static int32_t sys_waitpid(registers_t *regs) {
|
||||
uint32_t pid = regs->ebx;
|
||||
@@ -98,14 +106,29 @@ static int32_t sys_waitpid(registers_t *regs) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Busy-wait until child is zombie */
|
||||
while (child->state != PROCESS_ZOMBIE) {
|
||||
schedule();
|
||||
/* If child already exited, reap immediately */
|
||||
if (child->state == PROCESS_ZOMBIE) {
|
||||
int32_t code = child->exit_code;
|
||||
child->state = PROCESS_UNUSED;
|
||||
return code;
|
||||
}
|
||||
|
||||
int32_t code = child->exit_code;
|
||||
child->state = PROCESS_UNUSED;
|
||||
return code;
|
||||
/* Block the current process until the child exits */
|
||||
process_t *cur = process_current();
|
||||
cur->state = PROCESS_BLOCKED;
|
||||
cur->waiting_for_pid = pid;
|
||||
|
||||
/* Save the current syscall registers so we resume here when unblocked.
|
||||
* The return value (eax) will be set by process_exit when the child dies. */
|
||||
cur->saved_regs = *regs;
|
||||
|
||||
/* Schedule the next process. This modifies *regs to the next process's
|
||||
* saved state, so when the ISR stub does iret, it enters the next process. */
|
||||
schedule_tick(regs);
|
||||
|
||||
/* Tell syscall_handler not to overwrite regs->eax, since regs now
|
||||
* points to the next process's registers on the kernel stack. */
|
||||
return SYSCALL_SWITCHED;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -140,7 +163,9 @@ void syscall_handler(registers_t *regs) {
|
||||
}
|
||||
|
||||
int32_t ret = syscall_table[num](regs);
|
||||
regs->eax = (uint32_t)ret;
|
||||
if (ret != SYSCALL_SWITCHED) {
|
||||
regs->eax = (uint32_t)ret;
|
||||
}
|
||||
}
|
||||
|
||||
void init_syscalls(void) {
|
||||
|
||||
Reference in New Issue
Block a user