summaryrefslogtreecommitdiff
path: root/na/kernel/task.cc
diff options
context:
space:
mode:
authorRichard Qin <richard.qin.x@gmail.com>2026-07-10 11:24:56 +0800
committerRichard Qin <richard.qin.x@gmail.com>2026-07-10 11:24:56 +0800
commit1767d390fbf3d0c65389de46caf0b6d43ef3b6ad (patch)
tree0f5e01cd7c7a05abb6b1c7472d7f00f49caf02cc /na/kernel/task.cc
parent63dc025705f6533b91fb1a7d26e6ed864efd9900 (diff)
downloadlume-1767d390fbf3d0c65389de46caf0b6d43ef3b6ad.tar.gz
lume-1767d390fbf3d0c65389de46caf0b6d43ef3b6ad.tar.xz
feat: establish bootable new architecture checkpoint
Replace the compressed boot path with a relocatable higher-half RISC-V entry, expand memory and SMP support, and add the driver, TTY, VFS, exec, signal, syscall, initramfs, and self-test infrastructure needed for a bootable kernel checkpoint.
Diffstat (limited to 'na/kernel/task.cc')
-rw-r--r--na/kernel/task.cc481
1 files changed, 431 insertions, 50 deletions
diff --git a/na/kernel/task.cc b/na/kernel/task.cc
index 9f09d62..9f9b043 100644
--- a/na/kernel/task.cc
+++ b/na/kernel/task.cc
@@ -20,8 +20,18 @@
#include <lume/vmm.h>
#include <lume/errno.h>
#include <lume/pid.h>
+#include <lume/wait.h>
+#include <lume/clone.h>
+#include <lume/signal_nr.h>
#include <arch/cpu.h>
#include <arch/context.h>
+#include <lume/uaccess.h>
+#include <lume/fs_struct.h>
+#include <lume/fdtable.h>
+#include <lume/signal.h>
+#include <lume/signal_types.h>
+#include <lume/pgrp.h>
+#include <arch/pmap.h>
#include <kernel/sync/spinlock.h>
/* ======================================================================
@@ -57,11 +67,16 @@ void task_init()
}
/* ======================================================================
- * Internal: allocate TCB + kernel stack
+ * alloc_task — allocate TCB + kernel stack
+ *
+ * This is the SINGLE canonical TCB initializer. Both kthread_create
+ * (task.cc) and sched_init (sched_core.cc) must use this function.
+ * NEVER duplicate this logic — adding fields to TaskControlBlock
+ * requires updating only this function.
* ====================================================================== */
-static TaskControlBlock* alloc_task(const char* name, uint8 priority,
- uint8 flags)
+TaskControlBlock* alloc_task(const char* name, uint8 priority,
+ uint8 flags, uint8 cpu)
{
auto* task = new TaskControlBlock;
if (!task) return nullptr;
@@ -80,23 +95,45 @@ static TaskControlBlock* alloc_task(const char* name, uint8 priority,
/* Write stack canary at bottom */
*reinterpret_cast<uint64*>(task->kstack_va) = kStackCanary;
+ /* Place TrapFrame at the top of the kernel stack (like Linux pt_regs).
+ * ALL tasks — kernel threads and user processes — have a TrapFrame
+ * here. kthread_create sets the initial SP below the TrapFrame so
+ * the kthread function never touches this reserved area.
+ * kernel_execve / do_execve can later fill it in for user-mode return. */
+ uint64 tf_addr = task->kstack_va + kKernelStackSize
+ - sizeof(arch::TrapFrame);
+ task->trapframe = reinterpret_cast<arch::TrapFrame*>(tf_addr);
+ __builtin_memset(task->trapframe, 0, sizeof(arch::TrapFrame));
+
/* Basic fields */
task->name = name;
task->priority = priority;
task->time_slice = priority_to_timeslice(priority);
task->state = TaskState::Runnable;
- task->cpu_id = static_cast<uint8>(arch::cpu::id());
+ task->cpu_id = cpu;
task->flags = flags;
task->exit_code = 0;
+ task->exit_signal = 0;
+ task->preempt_count = 0;
+ task->clear_child_tid = 0;
+ task->stopped_code = 0;
+ task->notify_continued = false;
task->parent = nullptr;
task->mm = nullptr;
- task->trapframe = nullptr;
+ task->fpstate = {};
+ task->files = nullptr;
+ task->fs = nullptr;
+ task->pending.init();
+ task->blocked.clear();
+ task->sighand = nullptr;
+ task->signal = nullptr;
/* Init all list nodes */
task->run_link.init();
task->all_link.init();
task->children.init();
task->sibling.init();
+ task->lock.init("task");
task->wait_chld.init("wait_chld");
/* Default sched class */
@@ -143,7 +180,8 @@ TaskControlBlock* kthread_create(const char* name,
void* arg,
uint8 priority)
{
- TaskControlBlock* task = alloc_task(name, priority, TF_KTHREAD);
+ TaskControlBlock* task = alloc_task(name, priority, TF_KTHREAD,
+ static_cast<uint8>(arch::cpu::id()));
if (!task) return nullptr;
/* Allocate PID */
@@ -156,19 +194,30 @@ TaskControlBlock* kthread_create(const char* name,
}
task->tgid = task->pid;
- /* Set up context for kthread_entry_trampoline */
- uint64 stack_top = task->kstack_va + kKernelStackSize;
+ /* Kernel threads: exit_signal = -1 → auto-reap on exit.
+ * Linux: kernel_thread() passes (flags & ~CSIGNAL) → exit_signal = 0,
+ * but kthreads' parent (kthreadd) reaps them. We have no kthreadd,
+ * so we use -1 which makes exit_notify() call release_task() directly,
+ * identical to Linux's CLONE_THREAD autoreap path. */
+ task->exit_signal = -1;
+
+ /* Set up context for kthread_entry_trampoline.
+ * SP starts BELOW the TrapFrame reserved at the stack top.
+ * This ensures the kthread function never clobbers the TrapFrame area. */
+ uint64 stack_top = task->kstack_va + kKernelStackSize
+ - sizeof(arch::TrapFrame);
task->context = {};
task->context.ra = reinterpret_cast<uint64>(kthread_entry_trampoline);
task->context.sp = stack_top;
task->context.s[0] = reinterpret_cast<uint64>(entry);
task->context.s[1] = reinterpret_cast<uint64>(arg);
- /* Parent is kernel_task (PID 0) */
- task->parent = g_kernel_task;
- if (g_kernel_task) {
- LockGuard guard(g_task_list_lock);
- list_node* head = &g_kernel_task->children;
+ /* Parent is kernel_task (PID 0) or current caller */
+ TaskControlBlock* parent = g_kernel_task ? g_kernel_task : current_task();
+ task->parent = parent;
+ if (parent) {
+ LockGuard guard(parent->lock);
+ list_node* head = &parent->children;
task->sibling.prev = head->prev;
task->sibling.next = head;
head->prev->next = &task->sibling;
@@ -191,18 +240,32 @@ void task_exit(int code)
TaskControlBlock* curr = current_task();
if (!curr) kernel_panic("task_exit: no current task");
- curr->exit_code = code;
+ /* Linux ABI: normal exit encodes status as (code & 0xFF) << 8 */
+ curr->exit_code = (code & 0xFF) << 8;
- /* Release user address space if we're the last reference */
- if (curr->mm) {
- curr->mm->destroy();
+ /* CLONE_CHILD_CLEARTID / set_tid_address: write 0 to user tidptr.
+ * This is how pthread_join detects thread termination.
+ * TODO: futex_wake(clear_child_tid, 1) after writing 0. */
+ if (curr->clear_child_tid != 0 && curr->mm != nullptr) {
+ int32 zero = 0;
+ copy_to_user(curr->clear_child_tid, &zero, sizeof(zero));
+ curr->clear_child_tid = 0;
+ }
+
+ /* Release user address space (refcount-safe).
+ * MUST switch to kernel page table FIRST — otherwise we're running
+ * on a freed page table after put() deallocates it. */
+ if (curr->mm != nullptr) {
+ extern uint64 g_kernel_pgtbl;
+ pmap::activate(g_kernel_pgtbl);
+ curr->mm->put();
curr->mm = nullptr;
}
/* Reparent children to init (or kernel_task if no init yet) */
TaskControlBlock* foster = g_init_task ? g_init_task : g_kernel_task;
if (foster && !curr->children.is_empty()) {
- LockGuard guard(g_task_list_lock);
+ LockGuard guard(curr->lock);
list_node* child_node = curr->children.next;
while (child_node != &curr->children) {
@@ -226,7 +289,46 @@ void task_exit(int code)
curr->children.init();
}
- /* Transition to Zombie — TCB + kstack preserved for parent wait() */
+
+ /* ---- exit_notify: Linux kernel/exit.c:exit_notify() ----
+ * Determine whether this task should be auto-reaped or become zombie.
+ *
+ * Linux logic (simplified):
+ * - exit_signal == -1 (kthreads, sub-threads): autoreap = true
+ * - exit_signal >= 1 (user processes): notify parent, become zombie
+ *
+ * If autoreap, we call release_task() equivalent inline:
+ * free PID, remove from task list, then deschedule to Dead. */
+ bool autoreap = (curr->exit_signal <= 0);
+
+ if (autoreap) {
+ /* Auto-reap: release resources immediately (no zombie state).
+ * Linux: exit_notify() → tsk->exit_state = EXIT_DEAD → release_task() */
+ task_list_remove(curr);
+ g_pid_alloc.free(curr->pid);
+
+ /* Remove from parent's children list */
+ curr->sibling.prev->next = curr->sibling.next;
+ curr->sibling.next->prev = curr->sibling.prev;
+ curr->sibling.init();
+
+ /* Drop signal_struct ref: last task in the thread group also
+ * detaches us from the pgrp hash. */
+ if (curr->signal) {
+ put_signal_struct(curr->signal);
+ curr->signal = nullptr;
+ }
+ if (curr->sighand) {
+ put_sighand(curr->sighand);
+ curr->sighand = nullptr;
+ }
+
+ curr->state = TaskState::Dead;
+ schedule();
+ __builtin_unreachable();
+ }
+
+ /* User process path: become Zombie for parent wait() */
curr->state = TaskState::Zombie;
/* Wake parent->wait_chld queue */
@@ -242,8 +344,55 @@ void task_exit(int code)
void task_kill_current(int sig)
{
- int code = (sig & 0x7F) | kExitCodeSignalFlag;
- task_exit(code);
+ TaskControlBlock* curr = current_task();
+ if (!curr) kernel_panic("task_kill_current: no current task");
+
+ /* Linux ABI: signal-killed encodes status as (signo & 0x7F) */
+ curr->exit_code = sig & 0x7F;
+
+ /* Release user address space (refcount-safe).
+ * Switch to kernel page table first (same as task_exit). */
+ if (curr->mm) {
+ extern uint64 g_kernel_pgtbl;
+ pmap::activate(g_kernel_pgtbl);
+ curr->mm->put();
+ curr->mm = nullptr;
+ }
+
+ /* Reparent children (same as task_exit) */
+ TaskControlBlock* foster = g_init_task ? g_init_task : g_kernel_task;
+ if (foster && !curr->children.is_empty()) {
+ LockGuard guard(curr->lock);
+
+ list_node* child_node = curr->children.next;
+ while (child_node != &curr->children) {
+ list_node* next_sib = child_node->next;
+ auto* child = list_entry<TaskControlBlock,
+ &TaskControlBlock::sibling>(child_node);
+ child->parent = foster;
+
+ child_node->prev->next = child_node->next;
+ child_node->next->prev = child_node->prev;
+
+ list_node* fhead = &foster->children;
+ child_node->prev = fhead->prev;
+ child_node->next = fhead;
+ fhead->prev->next = child_node;
+ fhead->prev = child_node;
+
+ child_node = next_sib;
+ }
+ curr->children.init();
+ }
+
+ curr->state = TaskState::Zombie;
+
+ if (curr->parent) {
+ curr->parent->wait_chld.wake_all();
+ }
+
+ schedule();
+ __builtin_unreachable();
}
/* ======================================================================
@@ -263,6 +412,19 @@ void task_destroy(TaskControlBlock* task)
/* Free PID */
g_pid_alloc.free(task->pid);
+ /* Drop signal_struct + sighand refs. For zombies these are delayed
+ * until reap (here) so a waitpid() walker can still inspect
+ * pgid/sid; Linux defers __exit_signal/__cleanup_sighand to
+ * release_task() for the same reason. */
+ if (task->signal) {
+ put_signal_struct(task->signal);
+ task->signal = nullptr;
+ }
+ if (task->sighand) {
+ put_sighand(task->sighand);
+ task->sighand = nullptr;
+ }
+
/* Free kernel stack */
if (task->kstack_pa != 0) {
pmm_free_pages(pa_to_page(phys_addr(task->kstack_pa)), 1);
@@ -273,61 +435,155 @@ void task_destroy(TaskControlBlock* task)
}
/* ======================================================================
- * task_wait — sleep until a child exits, then reclaim it
- * Returns PID of exited child, or -ECHILD if no children exist.
+ * do_wait4 — wait for a child to exit (Linux-compatible)
+ *
+ * @wait_pid: >0 = specific child PID
+ * -1 = any child
+ * 0 = any child in same process group (TODO)
+ * <-1 = any child in specific process group (TODO)
+ * @options: WNOHANG = return immediately if no zombie
+ * @exit_code: output parameter (already in Linux wstatus format)
+ *
+ * Returns: PID of exited child, -ECHILD, or -EAGAIN (WNOHANG).
* ====================================================================== */
-int32 task_wait(int32* exit_code)
+int32 do_wait4(int32 wait_pid, int options, int32* exit_code)
{
TaskControlBlock* curr = current_task();
if (!curr) return -EINVAL;
while (true) {
TaskControlBlock* zombie = nullptr;
+ TaskControlBlock* report = nullptr; /* Stopped/Continued report */
+ int32 report_status = 0;
+ bool has_children = false;
+
+ /*
+ * Two-phase wait: put ourselves on the wait queue BEFORE
+ * checking the condition. This closes the race window where
+ * a child exits between our check and schedule().
+ *
+ * If a child exits and calls wake_all() while we're checking,
+ * our state will be flipped back to Runnable, and schedule()
+ * will simply return immediately.
+ */
+ curr->wait_chld.prepare_to_wait(curr, TaskState::Sleeping);
+
+ /* Scan children */
+ curr->lock.acquire();
- /* Look for a Zombie child */
- g_task_list_lock.acquire();
if (curr->children.is_empty()) {
- g_task_list_lock.release();
+ curr->lock.release();
+ curr->wait_chld.finish_wait(curr);
return -ECHILD;
}
+ has_children = true;
list_node* node = curr->children.next;
while (node != &curr->children) {
auto* child = list_entry<TaskControlBlock, &TaskControlBlock::sibling>(node);
+ bool matches = (wait_pid == -1) || (wait_pid == child->pid);
+ if (!matches) {
+ node = node->next;
+ continue;
+ }
+
+ /* Zombie reaping has priority — it frees resources. */
if (child->state == TaskState::Zombie) {
zombie = child;
break;
}
+
+ /* WUNTRACED: report Stopped child once per stop event. */
+ if ((options & WUNTRACED) && child->stopped_code != 0) {
+ report = child;
+ report_status = child->stopped_code;
+ break;
+ }
+
+ /* WCONTINUED: report a one-shot SIGCONT resume. */
+ if ((options & WCONTINUED) && child->notify_continued) {
+ report = child;
+ report_status = W_CONTINUED();
+ break;
+ }
+
node = node->next;
}
- g_task_list_lock.release();
+ curr->lock.release();
if (zombie) {
+ curr->wait_chld.finish_wait(curr);
int32 pid = zombie->pid;
- if (exit_code) {
- /* In a real kernel, we copy_to_user */
- *exit_code = zombie->exit_code;
- }
+ if (exit_code) *exit_code = zombie->exit_code;
task_destroy(zombie);
return pid;
}
- /* Sleep if no child has exited yet */
- curr->wait_chld.sleep(&g_task_list_lock);
+ if (report) {
+ /* Status-only report: clear the one-shot flag, leave the
+ * child alive. WNOWAIT (peek) would skip the clear, but
+ * we don't yet honour it. */
+ curr->wait_chld.finish_wait(curr);
+ int32 pid = report->pid;
+ if (exit_code) *exit_code = report_status;
+ if (W_CONTINUED() == report_status) {
+ report->notify_continued = false;
+ } else {
+ report->stopped_code = 0;
+ }
+ return pid;
+ }
+
+ /* No event to report */
+ if (!has_children) {
+ curr->wait_chld.finish_wait(curr);
+ return -ECHILD;
+ }
+ if (options & WNOHANG) {
+ curr->wait_chld.finish_wait(curr);
+ return 0;
+ }
+
+ /* Condition still false — deschedule.
+ * We are already on the wait queue and marked Sleeping. */
+ schedule();
+
+ /* Woken up — finish_wait removes us from queue, restores Runnable */
+ curr->wait_chld.finish_wait(curr);
+
+ /* Loop back to re-check the condition */
}
}
+/* Legacy wrapper for internal callers */
+int32 task_wait(int32* exit_code)
+{
+ return do_wait4(-1, 0, exit_code);
+}
+
/* ======================================================================
- * task_fork — clone current task
+ * task_clone — clone current task with Linux-compatible flags
+ *
+ * @tf: trap frame to copy to child
+ * @clone_flags: CLONE_VM | CLONE_THREAD | CSIGNAL etc.
+ * @child_stack: if non-zero, set as child's user SP (for pthread)
+ * @parent_tidptr: write child tid here in parent (CLONE_PARENT_SETTID)
+ * @child_tidptr: write child tid here in child (CLONE_CHILD_SETTID)
+ * @tls: TLS pointer (CLONE_SETTLS → tp register)
+ *
+ * Returns: child PID in parent, 0 in child, or negative errno.
* ====================================================================== */
-int32 task_fork(arch::TrapFrame* tf)
+int32 task_clone(arch::TrapFrame* tf, uint64 clone_flags,
+ uint64 child_stack, uint64 parent_tidptr,
+ uint64 child_tidptr, uint64 tls)
{
TaskControlBlock* curr = current_task();
if (!curr) return -EINVAL;
- TaskControlBlock* child = alloc_task("forked", curr->priority, 0);
+ TaskControlBlock* child = alloc_task("cloned", curr->priority, 0,
+ static_cast<uint8>(arch::cpu::id()));
if (!child) return -ENOMEM;
/* Allocate PID */
@@ -337,16 +593,35 @@ int32 task_fork(arch::TrapFrame* tf)
delete child;
return -ENOMEM;
}
- child->tgid = child->pid;
- /* Copy VmSpace */
- if (curr->mm) {
- child->mm = vmm_clone(curr->mm);
- if (!child->mm) {
- g_pid_alloc.free(child->pid);
- pmm_free_pages(pa_to_page(phys_addr(child->kstack_pa)), 1);
- delete child;
- return -ENOMEM;
+ /* exit_signal + tgid: from clone_flags.
+ * Linux: fork.c line 2418/2424 */
+ if (clone_flags & CLONE_THREAD) {
+ child->tgid = curr->tgid;
+ child->exit_signal = -1; /* Linux: sub-threads auto-reap */
+ } else {
+ child->tgid = child->pid;
+ child->exit_signal = static_cast<int32>(clone_flags & 0xFF); /* CSIGNAL */
+ if (child->exit_signal == 0) {
+ child->exit_signal = SIGCHLD; /* default for fork() */
+ }
+ }
+
+ /* Address space: CLONE_VM shares, otherwise deep copy */
+ if (clone_flags & CLONE_VM) {
+ if (curr->mm) {
+ curr->mm->incref();
+ child->mm = curr->mm;
+ }
+ } else {
+ if (curr->mm) {
+ child->mm = vmm_clone(curr->mm);
+ if (!child->mm) {
+ g_pid_alloc.free(child->pid);
+ pmm_free_pages(pa_to_page(phys_addr(child->kstack_pa)), 1);
+ delete child;
+ return -ENOMEM;
+ }
}
}
@@ -355,20 +630,94 @@ int32 task_fork(arch::TrapFrame* tf)
child->trapframe = reinterpret_cast<arch::TrapFrame*>(tf_addr);
*child->trapframe = *tf;
- /* Child returns 0 from fork */
+ /* Set Kernel SP */
+ child->trapframe->kernel_sp = reinterpret_cast<uint64>(child->trapframe);
+ child->trapframe->kernel_cpu_id = child->cpu_id;
+
+ /* Child returns 0 from fork/clone */
child->trapframe->a[0] = 0;
+ /* If caller specified a child stack, use it (pthread_create) */
+ if (child_stack != 0) {
+ child->trapframe->sp = child_stack;
+ }
+
+ /* CLONE_SETTLS: set thread pointer register */
+ if (clone_flags & CLONE_SETTLS) {
+ child->trapframe->tp = tls;
+ }
+
/* Set up context for ret_from_fork */
- uint64 stack_top = tf_addr; // Context goes below TrapFrame conceptually, but we just set SP
+ uint64 stack_top = tf_addr;
child->context = {};
child->context.ra = reinterpret_cast<uint64>(ret_from_fork);
child->context.sp = stack_top;
- child->context.s[0] = tf_addr; /* Passed to a0 in ret_from_fork */
+ child->context.s[0] = tf_addr;
+
+ /* ---- Copy fs_struct (Linux: copy_fs in fork.c) ----
+ * CLONE_FS: share the same fs_struct (increment refcount).
+ * Otherwise: allocate a new fs_struct copying root/pwd. */
+ if (curr->fs != nullptr) {
+ if (clone_flags & CLONE_FS) {
+ curr->fs->count.fetch_add(1, __ATOMIC_RELAXED);
+ child->fs = curr->fs;
+ } else {
+ child->fs = copy_fs_struct(curr->fs);
+ }
+ }
+
+ /* ---- Copy files_struct (Linux: copy_files in fork.c) ----
+ * CLONE_FILES: share the same fd table.
+ * Otherwise: duplicate the fd table. */
+ if (curr->files != nullptr) {
+ if (clone_flags & CLONE_FILES) {
+ curr->files->count.fetch_add(1, __ATOMIC_RELAXED);
+ child->files = curr->files;
+ } else {
+ child->files = dup_fd(curr->files);
+ }
+ }
+
+ /* ---- Copy sighand (Linux kernel/fork.c:copy_sighand) ----
+ * CLONE_SIGHAND: share signal handlers (incref parent's).
+ * Otherwise: deep copy of the action table; child gets its own
+ * sighand_struct (and its own siglock). Both branches MUST yield
+ * a non-null sighand — every user task needs one so signal paths
+ * can take the siglock without lazy-allocation races. */
+ if (clone_flags & CLONE_SIGHAND) {
+ if (!curr->sighand) {
+ /* CLONE_SIGHAND requires the parent to have a sighand. */
+ return -EINVAL;
+ }
+ get_sighand(curr->sighand);
+ child->sighand = curr->sighand;
+ } else {
+ child->sighand = copy_sighand(curr->sighand);
+ if (!child->sighand) {
+ /* OOM — same best-effort policy as fs/files/signal above.
+ * A separate cleanup-on-OOM pass is on the to-do list. */
+ }
+ }
+
+ /* ---- Copy signal_struct (Linux: copy_signal in fork.c) ----
+ * CLONE_THREAD shares the parent's signal_struct (incref).
+ * Otherwise allocate a new one that inherits pgid/sid/tty from the
+ * parent so the child stays in the same process group and session —
+ * this is what makes fork() participate in the existing job-control
+ * identity without explicit setpgid() calls. */
+ if (curr->signal != nullptr) {
+ if (clone_flags & CLONE_THREAD) {
+ get_signal_struct(curr->signal);
+ child->signal = curr->signal;
+ } else {
+ child->signal = fork_signal_struct(curr->signal, child);
+ }
+ }
/* Add to task tree */
child->parent = curr;
{
- LockGuard guard(g_task_list_lock);
+ LockGuard guard(curr->lock);
list_node* head = &curr->children;
child->sibling.prev = head->prev;
child->sibling.next = head;
@@ -380,9 +729,41 @@ int32 task_fork(arch::TrapFrame* tf)
task_list_add(child);
sched_enqueue(child);
+ /* CLONE_PARENT_SETTID: write child tid to parent's user address */
+ if ((clone_flags & CLONE_PARENT_SETTID) && parent_tidptr != 0) {
+ copy_to_user(parent_tidptr, &child->pid, sizeof(child->pid));
+ }
+
+ /* CLONE_CHILD_SETTID: write child tid into child's user address.
+ * Note: this runs in parent context, but the address is in the
+ * child's mm. For CLONE_VM (threads), parent and child share mm
+ * so this works directly. For fork, the page was just CoW-copied. */
+ if ((clone_flags & CLONE_CHILD_SETTID) && child_tidptr != 0) {
+ if (clone_flags & CLONE_VM) {
+ /* Same address space — write directly */
+ copy_to_user(child_tidptr, &child->pid, sizeof(child->pid));
+ }
+ /* For non-CLONE_VM fork, the child's mm is separate.
+ * The write would need to go into the child's pagetable.
+ * Linux handles this in the child's first schedule via
+ * a special flag. For now, threads (CLONE_VM) are the
+ * primary consumer. */
+ }
+
+ /* CLONE_CHILD_CLEARTID: kernel writes 0 to child_tidptr on exit */
+ if (clone_flags & CLONE_CHILD_CLEARTID) {
+ child->clear_child_tid = child_tidptr;
+ }
+
return child->pid;
}
+/* Legacy wrapper — plain fork() semantics */
+int32 task_fork(arch::TrapFrame* tf)
+{
+ return task_clone(tf, SIGCHLD, 0, 0, 0, 0);
+}
+
/* ======================================================================
* task_check_stack_canary