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.
This commit is contained in:
AI
2026-02-23 12:42:02 +00:00
parent f1de5b6da6
commit 42328ead0b
9 changed files with 350 additions and 30 deletions

View File

@@ -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) {