Compare commits

4 Commits

Author SHA1 Message Date
AI
6910deae7c docs: check off fork system call task in README 2026-02-23 12:42:14 +00:00
AI
42328ead0b feat: implement fork system call with deep address space cloning (AI)
- Added paging_clone_directory_from(): deep-copies user-space pages so
  parent and child have independent memory. Kernel pages are shared.
- Fixed process_fork() to accept registers_t* for accurate child state,
  and to clone from the parent's page directory (not the kernel's).
- Refactored process_exit() to properly context-switch to next process
  using new process_switch_to_user assembly stub (loads full registers_t
  and performs iret), instead of halting unconditionally.
- Fixed sys_waitpid() to use proper blocking: marks process BLOCKED,
  invokes scheduler, and resumes with exit code when child dies.
- Added SYSCALL_SWITCHED mechanism to prevent syscall_handler from
  clobbering the next process's EAX after a context switch.
- Created fork-test user app that validates fork + waitpid.
- Added docs/fork.md with architecture documentation.

Tested: fork-test creates child, both print messages, parent waits for
child exit (code 7), parent reaps and exits (code 0). hello-world also
verified to still work correctly after the process_exit refactor.
2026-02-23 12:42:02 +00:00
AI
f1de5b6da6 docs: check off hello-world app task in README 2026-02-23 12:31:00 +00:00
AI
a6e6e3d8ca feat: hello-world user-mode app loaded from initrd via VFS
- Created apps/hello-world/hello-world.S: AT&T assembly user program that
  calls SYS_WRITE to print 'Hello, World' then SYS_EXIT(0)
- Created apps/user.ld: linker script for user apps at 0x08048000
- Created scripts/build_apps.sh: builds each app dir into flat binary
- Updated CMakeLists.txt: added apps build target in ISO pipeline
- Updated gen_initrd.sh: packs built binaries from build/apps_bin/
- Updated kernel.c: replaced inline machine code with VFS-based loading
  of hello-world from /initrd/hello-world via cpio_find()

Verified: hello-world binary (49 bytes) loads from CPIO initrd,
prints 'Hello, World', and exits with code 0.
2026-02-23 12:30:36 +00:00
15 changed files with 488 additions and 90 deletions

View File

@@ -19,13 +19,20 @@ add_subdirectory(src)
file(MAKE_DIRECTORY ${CMAKE_SOURCE_DIR}/release) file(MAKE_DIRECTORY ${CMAKE_SOURCE_DIR}/release)
file(MAKE_DIRECTORY ${CMAKE_BINARY_DIR}/isodir/boot/grub) file(MAKE_DIRECTORY ${CMAKE_BINARY_DIR}/isodir/boot/grub)
# Generate CPIO initial ramdisk from apps directory. # Build user-mode applications as flat binaries.
# All files in apps/ are packed into a newc-format CPIO archive. 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) set(INITRD_FILE ${CMAKE_BINARY_DIR}/isodir/boot/initrd.cpio)
add_custom_command( add_custom_command(
OUTPUT ${INITRD_FILE} OUTPUT ${INITRD_FILE}
COMMAND ${CMAKE_SOURCE_DIR}/scripts/gen_initrd.sh ${CMAKE_SOURCE_DIR}/apps ${INITRD_FILE} COMMAND ${CMAKE_SOURCE_DIR}/scripts/gen_initrd.sh ${APPS_BIN_DIR} ${INITRD_FILE}
DEPENDS ${CMAKE_SOURCE_DIR}/apps DEPENDS apps
COMMENT "Generating CPIO initial ramdisk" COMMENT "Generating CPIO initial ramdisk"
) )
add_custom_target(initrd DEPENDS ${INITRD_FILE}) add_custom_target(initrd DEPENDS ${INITRD_FILE})

View 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] 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 subsystem.
- [x] Write a VFS driver that provides the contents of the CPIO initial ramdisk to the VFS layer. - [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. - [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.
- [ ] Implement the fork system call. - [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. - [ ] 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 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. - [ ] Create an `ls` app. It should list the contents of the current working directory, via the environment variable.

View 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

View 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
View 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
View 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
View 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

View File

@@ -1,14 +1,16 @@
#!/bin/sh #!/bin/sh
# Generate CPIO initial ramdisk from apps directory # Generate CPIO initial ramdisk from built application binaries.
# Usage: gen_initrd.sh <apps_dir> <output_file> # Usage: gen_initrd.sh <binaries_dir> <output_file>
# Packs all files in <binaries_dir> into a newc-format CPIO archive.
set -e set -e
APPS_DIR="$1" BIN_DIR="$1"
OUTPUT="$2" OUTPUT="$2"
# Ensure output directory exists # Ensure output directory exists
mkdir -p "$(dirname "$OUTPUT")" mkdir -p "$(dirname "$OUTPUT")"
cd "$APPS_DIR" cd "$BIN_DIR"
find . -not -name '.' | cpio -o -H newc > "$OUTPUT" 2>/dev/null # 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" echo "Generated initrd: $(wc -c < "$OUTPUT") bytes"

View File

@@ -174,3 +174,28 @@ enter_usermode:
push $0x1B /* CS (user code) */ push $0x1B /* CS (user code) */
push %ecx /* EIP (entry point) */ push %ecx /* EIP (entry point) */
iret 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 */

View File

@@ -152,64 +152,31 @@ void kernel_main(uint32_t magic, uint32_t addr) {
offset_print("FAILED to kmalloc\n"); offset_print("FAILED to kmalloc\n");
} }
/* /* Load hello-world from the initrd and run it as a user process */
* Create a minimal test user-mode program. cpio_entry_t app_entry;
* This is flat binary machine code that calls SYS_WRITE then SYS_EXIT. if (cpio_find("hello-world", &app_entry) == 0) {
* offset_print("Found hello-world in initrd (");
* The program writes "Hello from Ring 3!\n" to stdout (fd=1) via INT 0x80, print_hex(app_entry.datasize);
* then exits with code 42. offset_print(" bytes)\n");
*
* 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'
};
int32_t pid = process_create("init", user_program, sizeof(user_program)); int32_t pid = process_create("hello-world",
if (pid > 0) { app_entry.data,
offset_print("Created init process, pid="); app_entry.datasize);
print_hex((uint32_t)pid); if (pid > 0) {
offset_print("Created hello-world process, pid=");
print_hex((uint32_t)pid);
/* Enable interrupts before entering user mode */ /* Enable interrupts before entering user mode */
asm volatile("sti"); asm volatile("sti");
offset_print("Interrupts enabled\n"); offset_print("Interrupts enabled\n");
/* Enter user mode - does not return */ /* Enter user mode - does not return */
process_run_first(); process_run_first();
} else {
offset_print("FAILED to create hello-world process\n");
}
} else { } else {
offset_print("FAILED to create init process\n"); offset_print("hello-world not found in initrd\n");
} }
/* Enable interrupts */ /* Enable interrupts */

View File

@@ -299,6 +299,64 @@ uint32_t paging_clone_directory(void) {
return new_dir_phys; 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) { 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 pd_idx = PD_INDEX(vaddr);
uint32_t pt_idx = PT_INDEX(vaddr); uint32_t pt_idx = PT_INDEX(vaddr);

View File

@@ -98,6 +98,17 @@ uint32_t paging_get_directory_phys(void);
*/ */
uint32_t paging_clone_directory(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). * Map a page in a specific page directory (not necessarily the active one).
* *

View File

@@ -234,18 +234,43 @@ void process_exit(int32_t code) {
current_process->state = PROCESS_ZOMBIE; current_process->state = PROCESS_ZOMBIE;
current_process->exit_code = code; current_process->exit_code = code;
/* Find another process to run. /* Wake any process blocked on waitpid for this PID */
* We construct a minimal register frame to pass to schedule_tick. for (int i = 0; i < MAX_PROCESSES; i++) {
* Since the process is zombie, schedule_tick won't save its state. */ if (process_table[i].state == PROCESS_BLOCKED &&
registers_t dummy; process_table[i].waiting_for_pid == current_process->pid) {
memset(&dummy, 0, sizeof(dummy)); process_table[i].state = PROCESS_READY;
schedule_tick(&dummy); process_table[i].saved_regs.eax = (uint32_t)code;
break;
/* If we get here, no other process was ready. Halt. */ }
offset_print(" PROCESS: no processes remaining, halting\n");
for (;;) {
__asm__ volatile("hlt");
} }
/* 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) { process_t *process_current(void) {
@@ -262,7 +287,7 @@ process_t *process_get(uint32_t pid) {
return NULL; return NULL;
} }
int32_t process_fork(void) { int32_t process_fork(registers_t *regs) {
if (!current_process) { if (!current_process) {
return -1; return -1;
} }
@@ -278,6 +303,7 @@ int32_t process_fork(void) {
child->pid = next_pid++; child->pid = next_pid++;
child->state = PROCESS_READY; child->state = PROCESS_READY;
child->parent_pid = current_process->pid; child->parent_pid = current_process->pid;
child->waiting_for_pid = 0;
/* Allocate a separate kernel stack for the child */ /* Allocate a separate kernel stack for the child */
void *child_kstack = paging_alloc_page(); 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 = (uint32_t)child_kstack;
child->kernel_stack_top = child->kernel_stack + 4096; child->kernel_stack_top = child->kernel_stack + 4096;
/* Clone the page directory */ /* Deep-clone the parent's page directory (copies all user-space pages) */
child->page_directory = paging_clone_directory(); child->page_directory = paging_clone_directory_from(current_process->page_directory);
if (!child->page_directory) { if (!child->page_directory) {
kfree((void *)child->kernel_stack); paging_free_page((void *)child->kernel_stack);
child->state = PROCESS_UNUSED; child->state = PROCESS_UNUSED;
return -1; 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's return value is 0 (in EAX) */
child->saved_regs.eax = 0; 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 */ /* Parent's return value is child's PID */
return (int32_t)child->pid; return (int32_t)child->pid;
} }

View File

@@ -56,6 +56,7 @@ typedef struct process {
uint32_t entry_point; /**< User-mode entry point. */ uint32_t entry_point; /**< User-mode entry point. */
int32_t exit_code; /**< Exit code (if ZOMBIE). */ int32_t exit_code; /**< Exit code (if ZOMBIE). */
uint32_t parent_pid; /**< Parent process ID. */ 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). */ char name[32]; /**< Process name (for debugging). */
} process_t; } process_t;
@@ -113,10 +114,12 @@ process_t *process_get(uint32_t pid);
/** /**
* Fork the current process. * 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. * @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. * Start the first user-mode process. Does not return if a process is ready.

View File

@@ -13,6 +13,10 @@
#include "vga.h" #include "vga.h"
#include <stddef.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 */ /* Debug print helpers defined in kernel.c */
extern void offset_print(const char *str); extern void offset_print(const char *str);
extern void print_hex(uint32_t val); 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. * Handle SYS_FORK: fork the current process.
*/ */
static int32_t sys_fork(registers_t *regs) { static int32_t sys_fork(registers_t *regs) {
(void)regs; return process_fork(regs);
return process_fork();
} }
/** /**
@@ -90,6 +93,11 @@ static int32_t sys_yield(registers_t *regs) {
/** /**
* Handle SYS_WAITPID: wait for a child to exit. * 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) { static int32_t sys_waitpid(registers_t *regs) {
uint32_t pid = regs->ebx; uint32_t pid = regs->ebx;
@@ -98,14 +106,29 @@ static int32_t sys_waitpid(registers_t *regs) {
return -1; return -1;
} }
/* Busy-wait until child is zombie */ /* If child already exited, reap immediately */
while (child->state != PROCESS_ZOMBIE) { if (child->state == PROCESS_ZOMBIE) {
schedule(); int32_t code = child->exit_code;
child->state = PROCESS_UNUSED;
return code;
} }
int32_t code = child->exit_code; /* Block the current process until the child exits */
child->state = PROCESS_UNUSED; process_t *cur = process_current();
return code; 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); 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) { void init_syscalls(void) {