From 1bbb0814b59676c059b886b2db5ad94e59354edb Mon Sep 17 00:00:00 2001 From: Marco Casaroli Date: Tue, 28 Jul 2026 00:25:37 +0200 Subject: [PATCH 1/2] sched/arch/libc: give fork(), vfork() and task_fork() separate semantics NuttX implemented fork() and vfork() as the same function. Both were libc wrappers around a single up_fork() syscall; vfork() differed only by a trailing waitpid(). Underneath, the child joined the parent's address environment -- the same addrenv_join() that pthread_create() uses -- and got a private copy of the stack. So the child shared .data, .bss and the heap with its parent and ran concurrently with it. That is not fork(). It is vfork()-with-a-private-stack under fork()'s name, and the history says so: today's fork() is NuttX's old vfork(), renamed in c33d1c9c97 (2023) without any change of behaviour. The failure was silent -- a program written against POSIX fork() compiled, ran, and had its child's writes land in the parent's variables. Separate them into three primitives, chosen by which function the caller called rather than by what the hardware happens to be: fork() child gets its own copy of the parent's memory at the same virtual addresses; runs concurrently. Only where an address environment can be duplicated -- elsewhere it is not declared at all, so calling it is a build error naming the function. vfork() child shares the parent's memory; parent suspended until the child _exit()s or exec()s. Implementable everywhere. task_fork() the historical behaviour under an honest name: shares memory, private stack copy, both running. Non-POSIX, in sched.h. Below libc there are now three syscalls -- up_task_fork(), up_vfork() and up_fork(). The per-arch register snapshot is common to all three; each architecture's entry points share one sequence and differ only in a FORK_TYPE_* selector (include/nuttx/fork.h) handed to nxtask_setup_fork(), which is the single place the memory semantics are decided. The vfork() parent suspension moves out of libc into nxtask_start_vfork(), released from nxsched_release_tcb(). Two things follow: the parent is resumed at exec(), since exec_swap() has already handed the child's pid to the loaded program by the time the vfork stub exits, and vfork() no longer depends on CONFIG_SCHED_WAITPID. Releasing there requires one fix in nxtask_exit(). It raises rtcb->lockcount directly rather than through sched_lock() while it tears the TCB down, so the nxsem_post() that wakes the vfork() parent queues it on g_pendingtasks -- and the matching raw lockcount-- does not merge that list the way sched_unlock() would, leaving the parent stranded with nothing left to move it off. A nxsched_merge_pending() after the decrement publishes it. The call is a no-op while pre-emption is still disabled, and up_exit() re-reads this_task() afterwards, so a change of the ready-to-run head is honoured. Without it vfork() deadlocks on any configuration where no other task happens to call sched_unlock() afterwards -- rv-virt:nsh64 and rv-virt:pnsh64, for instance, where NSH is blocked in waitpid() holding the lock. fork() is built on a new addrenv_fork(), backed by an up_addrenv_fork() hook that duplicates an address environment into freshly allocated pages mapped at the same virtual addresses -- unlike up_addrenv_clone(), which copies only the representation and leaves both pointing at the same page tables. The child then adopts the parent's stack geometry rather than being given a relocated copy: a pointer to a stack local taken before fork() must name the same object in the child that it named in the parent, and the parent's stack is already in the duplicate, with its contents, at the parent's address. No architecture implements up_addrenv_fork() yet, so this commit leaves fork() unavailable everywhere. That is the intended state. It withdraws fork() from ARCH_ARM, flat ARCH_ARM64, ARCH_RISCV, ARCH_SIM and ARCH_X86_64, where until now it named the sharing primitive; per-architecture patches restore it, with POSIX semantics, as up_addrenv_fork() lands. Nothing is lost in the meantime: task_fork() is that same sharing primitive under its own name, and CONFIG_FORK_IS_TASK_FORK (default n) aliases fork() back to it for legacy code, on exactly the configurations that had fork() before. Kconfig: ARCH_HAVE_TASK_FORK and ARCH_HAVE_VFORK inherit ARCH_HAVE_FORK's select lines, conditions included, so no configuration gains machinery; ARCH_HAVE_FORK is redefined to mean "can provide POSIX fork() semantics" and derives from the new ARCH_HAVE_ADDRENV_FORK. There is one deliberate departure from "verbatim". ARCH_ARM selected the fork family unconditionally, BUILD_KERNEL included, and that has never worked: on a kernel build the architecture's fork entry point sees the kernel's return address and stack pointer rather than the caller's, so the child resumes at a kernel address. On qemu-armv7a:knsh master faults in ostest's task_fork case with "Child did not run" and then a data abort; without the condition this change faults the same way through vfork(). ARCH_ARM64 and ARCH_X86_64 already carried "if !BUILD_KERNEL" for exactly this reason -- ARM was the outlier. Conditioning it turns a runtime fault into an honest absence, which is the whole point of the change; arch/arm takes the condition off again in the patch that adds its saved-syscall-frame path. Only the MMU-capable ARM ports are affected, since Cortex-M cannot build BUILD_KERNEL at all. ARCH_VFORK_STACK_BORROW lets a vfork() child borrow the parent's stack outright instead of relocating a copy of it, which is what an architecture whose frames hold absolute stack addresses needs; the reserve withheld for the parent's own frames is guarded by a canary. Also fixes two latent syntax errors found on the way: a missing comma in riscv_fork.c and mips_fork.c, both in *_FRAMEPOINTER && !SAVE_GP branches that are never compiled today. Signed-off-by: Marco Casaroli Assisted-by: Claude Opus 5 (1M context) --- arch/Kconfig | 114 ++++- arch/arm/src/common/arm_fork.c | 129 +++--- arch/arm/src/common/arm_fork.h | 1 + arch/arm/src/common/gnu/fork.S | 110 +++-- arch/arm/src/common/iar/fork.S | 85 ++-- arch/arm64/src/common/arm64_fork.c | 15 +- arch/arm64/src/common/arm64_fork.h | 1 + arch/arm64/src/common/arm64_fork_func.S | 81 ++-- arch/ceva/src/common/ceva_fork.c | 13 +- arch/ceva/src/xc5/fork.S | 32 +- arch/ceva/src/xm6/fork.S | 32 +- arch/mips/Kconfig | 3 +- arch/mips/src/mips32/Kconfig | 2 +- arch/mips/src/mips32/fork.S | 58 ++- arch/mips/src/mips32/mips_fork.c | 17 +- arch/mips/src/mips32/mips_fork.h | 1 + arch/risc-v/src/common/CMakeLists.txt | 4 +- arch/risc-v/src/common/Make.defs | 2 +- arch/risc-v/src/common/fork.S | 104 +++-- arch/risc-v/src/common/riscv_fork.c | 30 +- arch/risc-v/src/common/riscv_fork.h | 1 + arch/sim/src/Makefile | 2 +- arch/sim/src/sim/CMakeLists.txt | 4 +- arch/sim/src/sim/sim_fork.c | 50 ++- arch/sim/src/sim/sim_fork_arm.S | 57 ++- arch/sim/src/sim/sim_fork_arm64.S | 70 ++- arch/sim/src/sim/sim_fork_x86.S | 65 ++- arch/sim/src/sim/sim_fork_x86_64.S | 75 +++- arch/x86_64/src/common/CMakeLists.txt | 4 +- arch/x86_64/src/common/Make.defs | 2 +- arch/x86_64/src/common/fork.S | 35 +- arch/x86_64/src/common/x86_64_fork.c | 15 +- arch/x86_64/src/common/x86_64_fork.h | 1 + include/nuttx/addrenv.h | 25 ++ include/nuttx/arch.h | 77 +++- include/nuttx/fork.h | 53 +++ include/nuttx/sched.h | 68 ++- include/sched.h | 10 + include/sys/syscall_lookup.h | 8 + include/unistd.h | 9 + libs/libbuiltin/libgcc/gcov.c | 6 + libs/libc/libc.csv | 4 +- libs/libc/unistd/CMakeLists.txt | 4 +- libs/libc/unistd/Make.defs | 2 +- libs/libc/unistd/lib_fork.c | 114 +++-- sched/addrenv/addrenv.c | 65 +++ sched/sched/sched_releasetcb.c | 9 + sched/task/CMakeLists.txt | 4 +- sched/task/Make.defs | 2 +- sched/task/task_exit.c | 11 + sched/task/task_fork.c | 573 ++++++++++++++++++++---- syscall/syscall.csv | 2 + 52 files changed, 1867 insertions(+), 394 deletions(-) create mode 100644 include/nuttx/fork.h diff --git a/arch/Kconfig b/arch/Kconfig index ab0a0c066c7e5..012f95ed4ecd6 100644 --- a/arch/Kconfig +++ b/arch/Kconfig @@ -11,7 +11,8 @@ config ARCH_ARM bool "ARM" select ARCH_HAVE_BACKTRACE select ARCH_HAVE_INTERRUPTSTACK - select ARCH_HAVE_FORK + select ARCH_HAVE_TASK_FORK if !BUILD_KERNEL + select ARCH_HAVE_VFORK if !BUILD_KERNEL select ARCH_HAVE_STACKCHECK select ARCH_HAVE_CUSTOMOPT select ARCH_HAVE_STDARG_H @@ -29,7 +30,8 @@ config ARCH_ARM64 select ARCH_64BIT select ARCH_HAVE_BACKTRACE select ARCH_HAVE_INTERRUPTSTACK - select ARCH_HAVE_FORK if !BUILD_KERNEL && !BUILD_PROTECTED + select ARCH_HAVE_TASK_FORK if !BUILD_KERNEL && !BUILD_PROTECTED + select ARCH_HAVE_VFORK if !BUILD_KERNEL && !BUILD_PROTECTED select ARCH_HAVE_STACKCHECK select ARCH_HAVE_CUSTOMOPT select ARCH_HAVE_STDARG_H @@ -87,7 +89,8 @@ config ARCH_RISCV select ARCH_HAVE_CPUINFO select ARCH_HAVE_INTERRUPTSTACK select ARCH_HAVE_STACKCHECK - select ARCH_HAVE_FORK + select ARCH_HAVE_TASK_FORK + select ARCH_HAVE_VFORK select ARCH_HAVE_CUSTOMOPT select ARCH_HAVE_SETJMP select ARCH_HAVE_STDARG_H @@ -111,7 +114,8 @@ config ARCH_SIM select ARCH_HAVE_TICKLESS select ARCH_HAVE_POWEROFF select ARCH_HAVE_TESTSET - select ARCH_HAVE_FORK if !HOST_WINDOWS + select ARCH_HAVE_TASK_FORK if !HOST_WINDOWS + select ARCH_HAVE_VFORK if !HOST_WINDOWS select ARCH_HAVE_SETJMP select ARCH_HAVE_CUSTOMOPT select ARCH_HAVE_TCBINFO @@ -147,7 +151,8 @@ config ARCH_X86_64 select PCI_LATE_DRIVERS_REGISTER if PCI select ARCH_TOOLCHAIN_GNU select ARCH_HAVE_BACKTRACE - select ARCH_HAVE_FORK if !BUILD_KERNEL + select ARCH_HAVE_TASK_FORK if !BUILD_KERNEL + select ARCH_HAVE_VFORK if !BUILD_KERNEL select ARCH_HAVE_SETJMP select ARCH_HAVE_PERF_EVENTS select ARCH_HAVE_POWEROFF @@ -482,9 +487,108 @@ config ARCH_HAVE_CPUID_MAPPING default n depends on ARCH_HAVE_MULTICPU +config ARCH_HAVE_TASK_FORK + bool + default n + ---help--- + The architecture can clone the calling task: the child shares the + parent's .data/.bss/heap and runs on a private copy of the parent's + stack, concurrently with the parent. This is the non-POSIX + task_fork() primitive; it is neither fork() nor vfork(). + +config ARCH_HAVE_VFORK + bool + default n + ---help--- + The architecture can implement POSIX vfork(): the child shares the + parent's memory and the parent is suspended until the child calls + _exit() or one of the exec family of functions. + +config ARCH_VFORK_STACK_BORROW + bool + default n + depends on ARCH_HAVE_VFORK && ARCH_KERNEL_STACK + ---help--- + The vfork() child borrows the parent's stack outright rather than + running on a relocated copy of it. This is the natural thing to do + on an architecture whose stack frames contain absolute stack + addresses -- the windowed ABI of Xtensa, whose register-window save + areas hold the spilled stack pointer of each frame -- since it is the + case that needs no relocation at all, and it is a pure economy + elsewhere. + + The dependency on ARCH_KERNEL_STACK is a correctness requirement, not + a convenience. The child resumes at the parent's own stack pointer + and grows down over whatever lies below it, and what lies below it is + the parent's continuation -- the rest of nxtask_setup_fork(), + nxtask_start_vfork(), and the block it comes to rest in -- unless + that continuation runs on a stack of its own. ARCH_KERNEL_STACK is + what puts it there. + + ARCH_VFORK_STACK_RESERVE bounds where the parent may be; it cannot + bound where the child goes, so it is not an answer to this. + +config ARCH_VFORK_STACK_RESERVE + int "vfork() parent stack reserve" + default 2048 + depends on ARCH_VFORK_STACK_BORROW + ---help--- + How much of the parent's stack is withheld from a borrowing vfork() + child, in bytes. + + The child is given the parent's stack below this reserve; the reserve + is headroom for the frames the parent still has to execute before it + comes to rest. A canary is written at the boundary and checked when + the parent resumes, so a value that is too small fails loudly rather + than corrupting the parent's frames. + +config ARCH_HAVE_ADDRENV_FORK + bool + default n + depends on ARCH_ADDRENV && !ARCH_STACK_DYNAMIC + ---help--- + The architecture implements up_addrenv_fork(), which duplicates an + address environment: the copy is backed by freshly allocated pages + holding a copy of the parent's contents, mapped at the same virtual + addresses. This is what POSIX fork() is built on. + + No architecture selects this yet. Two things are needed. First, + up_addrenv_fork() itself. Second, the architecture must build the + child's register context from the *user's* saved system call frame: + in a kernel build fork() is reached through a system call, so the + return address and stack pointer the architecture's fork entry point + can see for itself are the kernel's, not the caller's, and a child + built from those resumes at a kernel address. + config ARCH_HAVE_FORK bool + default y if ARCH_ADDRENV && ARCH_HAVE_ADDRENV_FORK + ---help--- + This configuration can provide POSIX fork() semantics: the child + receives its own copy of the parent's memory at the same virtual + addresses, may modify anything, may return from the function that + called fork(), and runs concurrently with the parent. + + Where this is not selected fork() is not provided at all, and code + that calls it fails to build -- see FORK_IS_TASK_FORK. + +config FORK_IS_TASK_FORK + bool "Provide fork() as an alias for task_fork() (legacy)" default n + depends on ARCH_HAVE_TASK_FORK && !ARCH_HAVE_FORK + ---help--- + Provide fork() on configurations that cannot implement POSIX fork() + semantics, by aliasing it to task_fork(). + + This preserves the historical NuttX fork() behaviour exactly: the + child shares the parent's data and heap, runs on a private copy of + the parent's stack, and runs concurrently with the parent. It does + NOT provide POSIX fork() semantics: writes by the child are visible + to the parent, and vice versa. + + Enable this only to keep legacy applications building. New code + should call task_fork() explicitly, or use pthread_create() or + posix_spawn(). config ARCH_HAVE_CRC32 bool diff --git a/arch/arm/src/common/arm_fork.c b/arch/arm/src/common/arm_fork.c index f98f8ec896a3c..ff5b696af492d 100644 --- a/arch/arm/src/common/arm_fork.c +++ b/arch/arm/src/common/arm_fork.c @@ -49,47 +49,57 @@ * Name: arm_fork * * Description: - * The fork() function has the same effect as posix fork(), except that the - * behavior is undefined if the process created by fork() either modifies - * any data other than a variable of type pid_t used to store the return - * value from fork(), or returns from the function in which fork() was - * called, or calls any other function before successfully calling _exit() - * or one of the exec family of functions. + * The common ARM worker behind up_task_fork(), up_vfork() and up_fork(). + * All three snapshot the caller's registers identically; `type' -- one of + * the FORK_TYPE_* constants from include/nuttx/fork.h -- says which + * primitive was called, and is passed straight through to + * nxtask_setup_fork(), which is where the memory semantics are decided. + * + * What differs here is only the stack. Normally the child has a stack of + * its own, and this function fills it with a relocated copy of the + * parent's, rebasing the stack and frame pointers to match. When the + * child shares the parent's stack -- a borrowing vfork() child -- there is + * nothing to relocate and the pointers are carried over unchanged. * * The overall sequence is: * - * 1) User code calls fork(). fork() collects context information and - * transfers control up arm_fork(). - * 2) arm_fork() and calls nxtask_setup_fork(). + * 1) User code calls task_fork(), vfork() or fork(). The libc wrapper + * enters the matching architecture entry point, which collects context + * information and transfers control to arm_fork(). + * 2) arm_fork() calls nxtask_setup_fork(). * 3) nxtask_setup_fork() allocates and configures the child task's TCB. * This consists of: * - Allocation of the child task's TCB. * - Initialization of file descriptors and streams * - Configuration of environment variables - * - Allocate and initialize the stack + * - Establishing the child's address environment (for vfork() and + * task_fork(); a fork() child's is duplicated later, once its stack + * has been filled in -- see nxtask_start_fork()) + * - Allocating the stack, or borrowing the parent's * - Setup the input parameters for the task. * - Initialization of the TCB (including call to up_initial_state()) * 4) arm_fork() provides any additional operating context. arm_fork must: * - Initialize special values in any CPU registers that were not * already configured by up_initial_state() - * 5) arm_fork() then calls nxtask_start_fork() - * 6) nxtask_start_fork() then executes the child thread. + * 5) arm_fork() then calls nxtask_start_fork(), or nxtask_start_vfork() + * which additionally suspends the caller. + * 6) which executes the child thread. * * nxtask_abort_fork() may be called if an error occurs between steps 3 and * 6. * * Input Parameters: - * context - Caller context information saved by fork() + * context - Caller context information saved by the entry point + * type - One of the FORK_TYPE_* constants * * Returned Value: - * Upon successful completion, fork() returns 0 to the child process and - * returns the process ID of the child process to the parent process. - * Otherwise, -1 is returned to the parent, no child process is created, - * and errno is set to indicate the error. + * Upon successful completion, 0 is returned to the child and the process + * ID of the child is returned to the parent. Otherwise, -1 is returned to + * the parent, no child is created, and errno is set to indicate the error. * ****************************************************************************/ -pid_t arm_fork(const struct fork_s *context) +pid_t arm_fork(const struct fork_s *context, int type) { struct tcb_s *parent = this_task(); struct tcb_s *child; @@ -115,7 +125,7 @@ pid_t arm_fork(const struct fork_s *context) /* Allocate and initialize a TCB for the child task. */ - child = nxtask_setup_fork((start_t)(context->lr & ~1)); + child = nxtask_setup_fork((start_t)(context->lr & ~1), type, oldsp); if (!child) { serr("ERROR: nxtask_setup_fork failed\n"); @@ -137,43 +147,62 @@ pid_t arm_fork(const struct fork_s *context) sinfo("Parent: stackutil:%" PRIu32 "\n", stackutil); - /* Make some feeble effort to preserve the stack contents. This is - * feeble because the stack surely contains invalid pointers and other - * content that will not work in the child context. However, if the - * user follows all of the caveats of fork() usage, even this feeble - * effort is overkill. - */ + if (child->stack_base_ptr == parent->stack_base_ptr) + { + /* The child is running on the parent's own stack -- a borrowing + * vfork() child, which is safe because the parent is suspended for + * the child's whole lifetime. There is nothing to relocate: the + * register save area was already placed below the borrowed region by + * up_initial_state(), and every stack address the child inherits is + * still the address it names. + */ + + newsp = oldsp; + newfp = context->fp; + } + else + { + /* Make some feeble effort to preserve the stack contents. This is + * feeble because the stack surely contains invalid pointers and other + * content that will not work in the child context. However, if the + * user follows all of the caveats of task_fork() usage, even this + * feeble effort is overkill. + * + * For a POSIX fork() child the stack contents are not merely a feeble + * effort: the child is entitled to use them, and it does. + */ - newtop = (uint32_t)child->stack_base_ptr + - child->adj_stack_size; + newtop = (uint32_t)child->stack_base_ptr + + child->adj_stack_size; - newsp = newtop - stackutil; + newsp = newtop - stackutil; - /* Move the register context to newtop. */ + /* Move the register context to newtop. */ - memcpy((void *)(newsp - XCPTCONTEXT_SIZE), - child->xcp.regs, XCPTCONTEXT_SIZE); + memcpy((void *)(newsp - XCPTCONTEXT_SIZE), + child->xcp.regs, XCPTCONTEXT_SIZE); - child->xcp.regs = (void *)(newsp - XCPTCONTEXT_SIZE); + child->xcp.regs = (void *)(newsp - XCPTCONTEXT_SIZE); - memcpy((void *)newsp, (const void *)oldsp, stackutil); + memcpy((void *)newsp, (const void *)oldsp, stackutil); - /* Was there a frame pointer in place before? */ + /* Was there a frame pointer in place before? */ - if (context->fp >= oldsp && context->fp < stacktop) - { - uint32_t frameutil = stacktop - context->fp; - newfp = newtop - frameutil; - } - else - { - newfp = context->fp; - } + if (context->fp >= oldsp && context->fp < stacktop) + { + uint32_t frameutil = stacktop - context->fp; + newfp = newtop - frameutil; + } + else + { + newfp = context->fp; + } - sinfo("Old stack top:%08" PRIx32 " SP:%08" PRIx32 " FP:%08" PRIx32 "\n", - stacktop, oldsp, context->fp); - sinfo("New stack top:%08" PRIx32 " SP:%08" PRIx32 " FP:%08" PRIx32 "\n", - newtop, newsp, newfp); + sinfo("Old stack top:%08" PRIx32 " SP:%08" PRIx32 + " FP:%08" PRIx32 "\n", stacktop, oldsp, context->fp); + sinfo("New stack top:%08" PRIx32 " SP:%08" PRIx32 + " FP:%08" PRIx32 "\n", newtop, newsp, newfp); + } /* Update the stack pointer, frame pointer, and volatile registers. When * the child TCB was initialized, all of the values were set to zero. @@ -245,9 +274,9 @@ pid_t arm_fork(const struct fork_s *context) } #endif - /* And, finally, start the child task. On a failure, nxtask_start_fork() - * will discard the TCB by calling nxtask_abort_fork(). + /* And, finally, start the child task. A vfork() additionally suspends us + * until the child calls _exit() or exec(). */ - return nxtask_start_fork(child); + return nxtask_start_fork(child, type); } diff --git a/arch/arm/src/common/arm_fork.h b/arch/arm/src/common/arm_fork.h index 765f2043668d4..d666f4e515e58 100644 --- a/arch/arm/src/common/arm_fork.h +++ b/arch/arm/src/common/arm_fork.h @@ -28,6 +28,7 @@ ****************************************************************************/ #include +#include /**************************************************************************** * Pre-processor Definitions diff --git a/arch/arm/src/common/gnu/fork.S b/arch/arm/src/common/gnu/fork.S index 83e22326c26c8..f253ec666af1f 100644 --- a/arch/arm/src/common/gnu/fork.S +++ b/arch/arm/src/common/gnu/fork.S @@ -1,5 +1,5 @@ /**************************************************************************** - * arch/arm/src/common/fork.S + * arch/arm/src/common/gnu/fork.S * * SPDX-License-Identifier: Apache-2.0 * @@ -35,49 +35,81 @@ ****************************************************************************/ /**************************************************************************** - * Name: up_fork + * Name: up_task_fork, up_vfork, up_fork * * Description: - * The up_fork() function is the base of fork() function that provided in - * libc, and fork() is implemented as a wrapper of up_fork() function. - * The fork() function has the same effect as posix fork(), except that the - * behavior is undefined if the process created by fork() either modifies - * any data other than a variable of type pid_t used to store the return - * value from fork(), or returns from the function in which fork() was - * called, or calls any other function before successfully calling _exit() - * or one of the exec family of functions. + * These are the architecture-specific entry points of NuttX's three + * cloning primitives. All three need exactly the same thing from + * assembly -- a snapshot of the caller's callee-saved registers, stack + * pointer and return address -- and differ only in what the C code then + * does with it, so they share one snapshot sequence and are distinguished + * by a FORK_TYPE_* constant passed to arm_fork() in r1. * - * This thin layer implements fork by simply calling up_fork() with the - * fork() context as an argument. The overall sequence is: + * See include/nuttx/fork.h for what the three primitives mean. * - * 1) User code calls fork(). fork() collects context information and - * transfers control up up_fork(). - * 2) arm_fork() and calls nxtask_setup_fork(). - * 3) nxtask_setup_fork() allocates and configures the child task's TCB. - * This consists of: + * The overall sequence is: + * + * 1) User code calls task_fork(), vfork() or fork(). Each is a libc + * wrapper around the matching entry point here. + * 2) The entry point collects the context and calls arm_fork(). + * 3) arm_fork() calls nxtask_setup_fork(), which allocates and configures + * the child task's TCB. This consists of: * - Allocation of the child task's TCB. * - Initialization of file descriptors and streams * - Configuration of environment variables - * - Allocate and initialize the stack + * - Establishing the child's address environment + * - Allocating the stack (or borrowing the parent's, for vfork() on an + * architecture that selects CONFIG_ARCH_VFORK_STACK_BORROW) * - Setup the input parameters for the task. * - Initialization of the TCB (including call to up_initial_state()) - * 4) arm_fork() provides any additional operating context. arm_fork must: + * 4) arm_fork() provides any additional operating context: * - Initialize special values in any CPU registers that were not * already configured by up_initial_state() - * 5) arm_fork() then calls nxtask_start_fork() - * 6) nxtask_start_fork() then executes the child thread. + * - Relocate the copied stack, unless the child shares the parent's + * 5) arm_fork() then calls nxtask_start_fork() or nxtask_start_vfork() + * 6) which executes the child thread. * * Input Parameters: * None * * Returned Value: - * Upon successful completion, fork() returns 0 to the child process and - * returns the process ID of the child process to the parent process. - * Otherwise, -1 is returned to the parent, no child process is created, - * and errno is set to indicate the error. + * Upon successful completion, 0 is returned to the child and the process + * ID of the child is returned to the parent. Otherwise, -1 is returned to + * the parent, no child is created, and errno is set to indicate the error. * ****************************************************************************/ +#ifdef CONFIG_ARCH_HAVE_TASK_FORK + .globl up_task_fork +#ifdef __ghs__ + .type up_task_fork, $function +#else + .type up_task_fork, function +#endif + +up_task_fork: + movs r2, #FORK_TYPE_TASK + b arm_fork_common + + .size up_task_fork, .-up_task_fork +#endif + +#ifdef CONFIG_ARCH_HAVE_VFORK + .globl up_vfork +#ifdef __ghs__ + .type up_vfork, $function +#else + .type up_vfork, function +#endif + +up_vfork: + movs r2, #FORK_TYPE_VFORK + b arm_fork_common + + .size up_vfork, .-up_vfork +#endif + +#ifdef CONFIG_ARCH_HAVE_FORK .globl up_fork #ifdef __ghs__ .type up_fork, $function @@ -86,6 +118,25 @@ #endif up_fork: + movs r2, #FORK_TYPE_FORK + b arm_fork_common + + .size up_fork, .-up_fork +#endif + +/* The shared snapshot. r2 holds the FORK_TYPE_* selector on entry; it is + * call-clobbered and is not part of the snapshot, so it survives the + * sequence below untouched. lr still holds the original caller's return + * address, because the entry points above branched here rather than calling. + */ + +#ifdef __ghs__ + .type arm_fork_common, $function +#else + .type arm_fork_common, function +#endif + +arm_fork_common: /* Create a stack frame */ mov r0, sp /* Save the value of the stack on entry */ @@ -104,9 +155,12 @@ up_fork: mov r5, lr /* Copy lr to a low register */ stmia r1!, {r0,r5} /* Save sp and lr in the structure */ - /* Then, call arm_fork(), passing it a pointer to the stack structure */ + /* Then, call arm_fork(), passing it a pointer to the stack structure + * and the selector that says which primitive was called. + */ mov r0, sp + mov r1, r2 bl arm_fork /* Recover r4-r7 that were destroyed before arm_fork was called */ @@ -114,12 +168,12 @@ up_fork: mov r1, sp ldmia r1!, {r4-r7} - /* Release the stack data and return the value returned by up_fork */ + /* Release the stack data and return the value returned by arm_fork */ ldr r1, [sp, #FORK_LR_OFFSET] mov r14, r1 add sp, sp, #FORK_SIZEOF bx lr - .size up_fork, .-up_fork + .size arm_fork_common, .-arm_fork_common .end diff --git a/arch/arm/src/common/iar/fork.S b/arch/arm/src/common/iar/fork.S index 3f16484a36530..e7ee3f76f3ad8 100644 --- a/arch/arm/src/common/iar/fork.S +++ b/arch/arm/src/common/iar/fork.S @@ -28,7 +28,7 @@ #include "arm_fork.h" - MODULE up_fork + MODULE arm_fork_common SECTION .text:CODE:NOROOT(2), "ax" /**************************************************************************** @@ -39,7 +39,15 @@ * Public Symbols ****************************************************************************/ +#ifdef CONFIG_ARCH_HAVE_TASK_FORK + PUBLIC up_task_fork +#endif +#ifdef CONFIG_ARCH_HAVE_VFORK + PUBLIC up_vfork +#endif +#ifdef CONFIG_ARCH_HAVE_FORK PUBLIC up_fork +#endif EXTERN arm_fork /**************************************************************************** @@ -47,52 +55,76 @@ ****************************************************************************/ /**************************************************************************** - * Name: up_fork + * Name: up_task_fork, up_vfork, up_fork * * Description: - * The up_fork() function is the base of fork() function that provided in - * libc, and fork() is implemented as a wrapper of up_fork() function. - * The fork() function has the same effect as posix fork(), except that the - * behavior is undefined if the process created by fork() either modifies - * any data other than a variable of type pid_t used to store the return - * value from fork(), or returns from the function in which fork() was - * called, or calls any other function before successfully calling _exit() - * or one of the exec family of functions. + * These are the architecture-specific entry points of NuttX's three + * cloning primitives. All three need exactly the same thing from + * assembly -- a snapshot of the caller's callee-saved registers, stack + * pointer and return address -- and differ only in what the C code then + * does with it, so they share one snapshot sequence and are distinguished + * by a FORK_TYPE_* constant passed to arm_fork() in r1. * - * This thin layer implements fork by simply calling up_fork() with the - * fork() context as an argument. The overall sequence is: + * See include/nuttx/fork.h for what the three primitives mean. * - * 1) User code calls fork(). fork() collects context information and - * transfers control up up_fork(). - * 2) arm_fork() and calls nxtask_setup_fork(). - * 3) nxtask_setup_fork() allocates and configures the child task's TCB. - * This consists of: + * The overall sequence is: + * + * 1) User code calls task_fork(), vfork() or fork(). Each is a libc + * wrapper around the matching entry point here. + * 2) The entry point collects the context and calls arm_fork(). + * 3) arm_fork() calls nxtask_setup_fork(), which allocates and configures + * the child task's TCB. This consists of: * - Allocation of the child task's TCB. * - Initialization of file descriptors and streams * - Configuration of environment variables - * - Allocate and initialize the stack + * - Establishing the child's address environment + * - Allocating the stack, or borrowing the parent's * - Setup the input parameters for the task. * - Initialization of the TCB (including call to up_initial_state()) * 4) arm_fork() provides any additional operating context. arm_fork must: * - Initialize special values in any CPU registers that were not * already configured by up_initial_state() - * 5) arm_fork() then calls nxtask_start_fork() - * 6) nxtask_start_fork() then executes the child thread. + * - Relocate the copied stack, unless the child shares the parent's + * 5) arm_fork() then calls nxtask_start_fork() or nxtask_start_vfork() + * 6) which executes the child thread. * * Input Parameters: * None * * Returned Value: - * Upon successful completion, fork() returns 0 to the child process and - * returns the process ID of the child process to the parent process. - * Otherwise, -1 is returned to the parent, no child process is created, - * and errno is set to indicate the error. + * Upon successful completion, 0 is returned to the child and the process + * ID of the child is returned to the parent. Otherwise, -1 is returned to + * the parent, no child is created, and errno is set to indicate the error. * ****************************************************************************/ THUMB +#ifdef CONFIG_ARCH_HAVE_TASK_FORK +up_task_fork: + movs r2, #FORK_TYPE_TASK + b arm_fork_common +#endif + +#ifdef CONFIG_ARCH_HAVE_VFORK +up_vfork: + movs r2, #FORK_TYPE_VFORK + b arm_fork_common +#endif + +#ifdef CONFIG_ARCH_HAVE_FORK up_fork: + movs r2, #FORK_TYPE_FORK + b arm_fork_common +#endif + +/* The shared snapshot. r2 holds the FORK_TYPE_* selector on entry; it is + * call-clobbered and is not part of the snapshot, and lr still holds the + * original caller's return address because the entry points above branched + * here rather than calling. + */ + +arm_fork_common: /* Create a stack frame */ mov r0, sp /* Save the value of the stack on entry */ @@ -117,9 +149,12 @@ up_fork: /* Floating point registers (not yet) */ - /* Then, call arm_fork(), passing it a pointer to the stack structure */ + /* Then, call arm_fork(), passing it a pointer to the stack structure + * and the selector that says which primitive was called. + */ mov r0, sp + mov r1, r2 bl arm_fork /* Release the stack data and return the value returned by arm_fork */ diff --git a/arch/arm64/src/common/arm64_fork.c b/arch/arm64/src/common/arm64_fork.c index fbbf6a56da0e5..e6aa3341f6a51 100644 --- a/arch/arm64/src/common/arm64_fork.c +++ b/arch/arm64/src/common/arm64_fork.c @@ -52,6 +52,14 @@ * Pre-processor Definitions ****************************************************************************/ +/* This architecture gives the child a relocated copy of the parent's stack; + * a borrowed stack would need that relocation to be skipped. + */ + +#ifdef CONFIG_ARCH_VFORK_STACK_BORROW +# error "ARM64 relocates the vfork() child stack; borrowing is not supported" +#endif + /**************************************************************************** * Public Functions ****************************************************************************/ @@ -113,7 +121,7 @@ void arm64_fork_fpureg_save(struct fork_s *context) * ****************************************************************************/ -pid_t arm64_fork(const struct fork_s *context) +pid_t arm64_fork(const struct fork_s *context, int type) { struct tcb_s *parent = this_task(); struct tcb_s *child; @@ -125,7 +133,8 @@ pid_t arm64_fork(const struct fork_s *context) /* Allocate and initialize a TCB for the child task. */ - child = nxtask_setup_fork((start_t)context->lr); + child = nxtask_setup_fork((start_t)context->lr, type, + (uintptr_t)context->sp); if (!child) { serr("ERROR: nxtask_setup_fork failed\n"); @@ -235,5 +244,5 @@ pid_t arm64_fork(const struct fork_s *context) * will discard the TCB by calling nxtask_abort_fork(). */ - return nxtask_start_fork(child); + return nxtask_start_fork(child, type); } diff --git a/arch/arm64/src/common/arm64_fork.h b/arch/arm64/src/common/arm64_fork.h index a2a46e7633a01..0b77ba2f093ae 100644 --- a/arch/arm64/src/common/arm64_fork.h +++ b/arch/arm64/src/common/arm64_fork.h @@ -28,6 +28,7 @@ ****************************************************************************/ #include +#include #include /**************************************************************************** diff --git a/arch/arm64/src/common/arm64_fork_func.S b/arch/arm64/src/common/arm64_fork_func.S index 79cd9566076d5..d7943157888eb 100644 --- a/arch/arm64/src/common/arm64_fork_func.S +++ b/arch/arm64/src/common/arm64_fork_func.S @@ -41,51 +41,77 @@ ****************************************************************************/ /**************************************************************************** - * Name: fork + * Name: up_task_fork, up_vfork, up_fork * * Description: - * The up_fork() function is the base of fork() function that provided in - * libc, and fork() is implemented as a wrapper of up_fork() function. - * The fork() function has the same effect as posix fork(), except that the - * behavior is undefined if the process created by fork() either modifies - * any data other than a variable of type pid_t used to store the return - * value from fork(), or returns from the function in which fork() was - * called, or calls any other function before successfully calling _exit() - * or one of the exec family of functions. + * These are the architecture-specific entry points of NuttX's three + * cloning primitives. All three need exactly the same thing from + * assembly -- a snapshot of the caller's registers, stack pointer and + * return address -- and differ only in what the C code then does with it, + * so they share one snapshot sequence and are distinguished by a + * FORK_TYPE_* constant passed to arm64_fork() in x1. * - * This thin layer implements fork by simply calling up_fork() with the - * fork() context as an argument. The overall sequence is: + * See include/nuttx/fork.h for what the three primitives mean. * - * 1) User code calls fork(). fork() collects context information and - * transfers control up up_fork(). - * 2) arm64_fork() and calls nxtask_setup_fork(). - * 3) nxtask_setup_fork() allocates and configures the child task's TCB. - * This consists of: + * The overall sequence is: + * + * 1) User code calls task_fork(), vfork() or fork(). Each is a libc + * wrapper around the matching entry point here. + * 2) The entry point collects the context and calls arm64_fork(). + * 3) arm64_fork() calls nxtask_setup_fork(), which allocates and + * configures the child task's TCB. This consists of: * - Allocation of the child task's TCB. * - Initialization of file descriptors and streams * - Configuration of environment variables - * - Allocate and initialize the stack + * - Establishing the child's address environment + * - Allocating the stack, or borrowing the parent's * - Setup the input parameters for the task. * - Initialization of the TCB (including call to up_initial_state()) - * 4) arm64_fork() provides any additional operating context. arm64_fork must: + * 4) arm64_fork() provides any additional operating context: * - Initialize special values in any CPU registers that were not * already configured by up_initial_state() - * 5) arm64_fork() then calls nxtask_start_fork() - * 6) nxtask_start_fork() then executes the child thread. + * - Relocate the copied stack, unless the child shares the parent's + * 5) arm64_fork() then calls nxtask_start_fork() or nxtask_start_vfork() + * 6) which executes the child thread. * * Input Parameters: * None * * Returned Value: - * Upon successful completion, fork() returns 0 to the child process and - * returns the process ID of the child process to the parent process. - * Otherwise, -1 is returned to the parent, no child process is created, - * and errno is set to indicate the error. + * Upon successful completion, 0 is returned to the child and the process + * ID of the child is returned to the parent. Otherwise, -1 is returned to + * the parent, no child is created, and errno is set to indicate the error. * ****************************************************************************/ +#ifdef CONFIG_ARCH_HAVE_TASK_FORK +GTEXT(up_task_fork) +SECTION_FUNC(text, up_task_fork) + mov x1, #FORK_TYPE_TASK + b arm64_fork_common +#endif + +#ifdef CONFIG_ARCH_HAVE_VFORK +GTEXT(up_vfork) +SECTION_FUNC(text, up_vfork) + mov x1, #FORK_TYPE_VFORK + b arm64_fork_common +#endif + +#ifdef CONFIG_ARCH_HAVE_FORK GTEXT(up_fork) SECTION_FUNC(text, up_fork) + mov x1, #FORK_TYPE_FORK + b arm64_fork_common +#endif + +/* The shared snapshot. x1 holds the FORK_TYPE_* selector on entry and is + * carried through to arm64_fork(). It is saved into the snapshot along with + * the other argument registers, which is harmless: x0-x18 are caller-saved + * and arm64_fork() does not propagate them to the child. + */ + +SECTION_FUNC(text, arm64_fork_common) /* Create a stack frame */ sub sp, sp, #8 * FORK_REGS_SIZE /* Allocate the structure on the stack */ @@ -118,14 +144,17 @@ SECTION_FUNC(text, up_fork) #ifdef CONFIG_ARCH_FPU mov x0, sp stp x0, x30, [sp, #-16]! + str x1, [sp, #-16]! /* Preserve the FORK_TYPE_* selector */ bl arm64_fork_fpureg_save + ldr x1, [sp], #16 ldp x0, x30, [sp], #16 #endif - /* Then, call arm64_fork(), passing it a pointer to the stack structure */ + /* Then, call arm64_fork(), passing it a pointer to the stack structure. + * x1 already holds the FORK_TYPE_* selector. + */ mov x0, sp - mov x1, #0 bl arm64_fork /* Release the stack data and return the value returned by arm64_fork */ diff --git a/arch/ceva/src/common/ceva_fork.c b/arch/ceva/src/common/ceva_fork.c index 2eca6db1b7620..019178f581aba 100644 --- a/arch/ceva/src/common/ceva_fork.c +++ b/arch/ceva/src/common/ceva_fork.c @@ -40,7 +40,7 @@ ****************************************************************************/ /**************************************************************************** - * Name: ceva_fork + * Name: ceva_task_fork * * Description: * The fork() function has the same effect as posix fork(), except that the @@ -83,7 +83,7 @@ * ****************************************************************************/ -pid_t ceva_fork(const uint32_t *regs) +pid_t ceva_task_fork(const uint32_t *regs) { #ifdef CONFIG_SCHED_WAITPID struct tcb_s *parent = this_task(); @@ -97,9 +97,14 @@ pid_t ceva_fork(const uint32_t *regs) void *argv; int ret; + /* How large is the parent's stack argument area? */ + + argsize = (uintptr_t)parent->stack_base_ptr - + (uintptr_t)parent->stack_alloc_ptr; + /* Allocate and initialize a TCB for the child task. */ - child = nxtask_setup_fork(parent->start, &argsize); + child = nxtask_setup_fork(parent->start, FORK_TYPE_TASK, (uintptr_t)sp); if (!child) { serr("ERROR: nxtask_setup_fork failed\n"); @@ -204,7 +209,7 @@ pid_t ceva_fork(const uint32_t *regs) * will discard the TCB by calling nxtask_abort_fork(). */ - return nxtask_start_fork(child); + return nxtask_start_fork(child, FORK_TYPE_TASK); #else /* CONFIG_SCHED_WAITPID */ return (pid_t)ERROR; #endif diff --git a/arch/ceva/src/xc5/fork.S b/arch/ceva/src/xc5/fork.S index ff6dc14d22fcc..9f73fb1b5cfa1 100644 --- a/arch/ceva/src/xc5/fork.S +++ b/arch/ceva/src/xc5/fork.S @@ -34,30 +34,30 @@ ****************************************************************************/ .file "fork.S" - .extern ceva_fork + .extern ceva_task_fork /**************************************************************************** * Public Functions ****************************************************************************/ /**************************************************************************** - * Name: up_fork + * Name: up_task_fork * * Description: - * The up_fork() function is the base of fork() function that provided in - * libc, and fork() is implemented as a wrapper of up_fork() function. + * The up_task_fork() function is the base of fork() function that provided in + * libc, and fork() is implemented as a wrapper of up_task_fork() function. * The fork() function has the same effect as posix fork(), except that the behavior is * undefined if the process created by fork() either modifies any data other than * a variable of type pid_t used to store the return value from fork(), or returns * from the function in which fork() was called, or calls any other function before * successfully calling _exit() or one of the exec family of functions. * - * This thin layer implements fork by simply calling up_fork() with the fork() + * This thin layer implements fork by simply calling up_task_fork() with the fork() * context as an argument. The overall sequence is: * * 1) User code calls fork(). fork() collects context information and - * transfers control up up_fork(). - * 2) ceva_fork()and calls nxtask_forksetup(). + * transfers control up up_task_fork(). + * 2) ceva_task_fork()and calls nxtask_forksetup(). * 3) task_forksetup() allocates and configures the child task's TCB. This * consists of: * - Allocation of the child task's TCB. @@ -65,11 +65,11 @@ * - Configuration of environment variables * - Setup the input parameters for the task. * - Initialization of the TCB (including call to up_initial_state() - * 4) ceva_fork() provides any additional operating context. ceva_fork must: + * 4) ceva_task_fork() provides any additional operating context. ceva_task_fork must: * - Allocate and initialize the stack * - Initialize special values in any CPU registers that were not * already configured by up_initial_state() - * 5) ceva_fork() then calls nxtask_forkstart() + * 5) ceva_task_fork() then calls nxtask_forkstart() * 6) nxtask_forkstart() then executes the child thread. * * Input Parameters: @@ -84,10 +84,10 @@ ****************************************************************************/ .text - .public up_fork - .func_start 3 up_fork + .public up_task_fork + .func_start 3 up_task_fork -up_fork: +up_task_fork: /* Create a stack frame */ subs sp, #XCPTCONTEXT_SIZE, sp @@ -98,19 +98,19 @@ up_fork: mov sp, a1 trap - /* Then, call ceva_fork(), passing it a pointer to the stack structure */ + /* Then, call ceva_task_fork(), passing it a pointer to the stack structure */ mov sp, a0 nop push {dw} retreg - callr {t} ceva_fork + callr {t} ceva_task_fork pop {dw} retreg nop - /* Release the stack data and return the value returned by ceva_fork */ + /* Release the stack data and return the value returned by ceva_task_fork */ adds sp, #XCPTCONTEXT_SIZE, sp ret - .func_end 3 up_fork + .func_end 3 up_task_fork diff --git a/arch/ceva/src/xm6/fork.S b/arch/ceva/src/xm6/fork.S index 947be2cec5e19..8068d2788be69 100644 --- a/arch/ceva/src/xm6/fork.S +++ b/arch/ceva/src/xm6/fork.S @@ -44,30 +44,30 @@ ****************************************************************************/ .file "fork.S" - .extern up_fork + .extern up_task_fork /**************************************************************************** * Public Functions ****************************************************************************/ /**************************************************************************** - * Name: up_fork + * Name: up_task_fork * * Description: - * The up_fork() function is the base of fork() function that provided in - * libc, and fork() is implemented as a wrapper of up_fork() function. + * The up_task_fork() function is the base of fork() function that provided in + * libc, and fork() is implemented as a wrapper of up_task_fork() function. * The fork() function has the same effect as posix fork(), except that the behavior is * undefined if the process created by fork() either modifies any data other than * a variable of type pid_t used to store the return value from fork(), or returns * from the function in which fork() was called, or calls any other function before * successfully calling _exit() or one of the exec family of functions. * - * This thin layer implements fork by simply calling up_fork() with the fork() + * This thin layer implements fork by simply calling up_task_fork() with the fork() * context as an argument. The overall sequence is: * * 1) User code calls fork(). fork() collects context information and - * transfers control up up_fork(). - * 2) ceva_fork()and calls nxtask_forksetup(). + * transfers control up up_task_fork(). + * 2) ceva_task_fork()and calls nxtask_forksetup(). * 3) task_forksetup() allocates and configures the child task's TCB. This * consists of: * - Allocation of the child task's TCB. @@ -75,11 +75,11 @@ * - Configuration of environment variables * - Setup the input parameters for the task. * - Initialization of the TCB (including call to up_initial_state() - * 4) ceva_fork() provides any additional operating context. ceva_fork must: + * 4) ceva_task_fork() provides any additional operating context. ceva_task_fork must: * - Allocate and initialize the stack * - Initialize special values in any CPU registers that were not * already configured by up_initial_state() - * 5) ceva_fork() then calls nxtask_forkstart() + * 5) ceva_task_fork() then calls nxtask_forkstart() * 6) nxtask_forkstart() then executes the child thread. * * Input Parameters: @@ -94,10 +94,10 @@ ****************************************************************************/ .text - .public up_fork - .func_start 3 up_fork + .public up_task_fork + .func_start 3 up_task_fork -up_fork: +up_task_fork: /* Create a stack frame */ modr (sp.ui).ui +#-XCPTCONTEXT_SIZE /* Allocate the structure on the stack */ @@ -108,18 +108,18 @@ up_fork: mov sp.ui, r1.ui trap {t0} - /* Then, call ceva_fork(), passing it a pointer to the stack structure */ + /* Then, call ceva_task_fork(), passing it a pointer to the stack structure */ mov sp.ui, r0.ui nop push retreg.ui - callr #ceva_fork, ?prx.b + callr #ceva_task_fork, ?prx.b pop retreg.ui nop - /* Release the stack data and return the value returned by ceva_fork */ + /* Release the stack data and return the value returned by ceva_task_fork */ modr (sp.ui).ui +#XCPTCONTEXT_SIZE ret ?prx.b - .func_end 3 up_fork + .func_end 3 up_task_fork diff --git a/arch/mips/Kconfig b/arch/mips/Kconfig index 1d1f2a2965d87..bca1aa58f4b29 100644 --- a/arch/mips/Kconfig +++ b/arch/mips/Kconfig @@ -56,7 +56,8 @@ endchoice config ARCH_MIPS32 bool default n - select ARCH_HAVE_FORK + select ARCH_HAVE_TASK_FORK + select ARCH_HAVE_VFORK config ARCH_MIPS_M4K bool diff --git a/arch/mips/src/mips32/Kconfig b/arch/mips/src/mips32/Kconfig index 16fa348165463..eb854fe69d723 100644 --- a/arch/mips/src/mips32/Kconfig +++ b/arch/mips/src/mips32/Kconfig @@ -93,7 +93,7 @@ config MIPS32_TOOLCHAIN_MICROCHIP_XC32_LICENSED config MIPS32_FRAMEPOINTER bool "ABI Uses Frame Pointer" default n - depends on ARCH_HAVE_FORK + depends on ARCH_HAVE_TASK_FORK ---help--- Register r30 may be a frame pointer in some ABIs. Or may just be saved register s8. It makes a difference for fork handling. diff --git a/arch/mips/src/mips32/fork.S b/arch/mips/src/mips32/fork.S index 790aa8cd43f42..6bd5f22b21f6c 100644 --- a/arch/mips/src/mips32/fork.S +++ b/arch/mips/src/mips32/fork.S @@ -44,7 +44,7 @@ ************************************************************************************/ /************************************************************************************ - * Name: up_fork + * Name: up_task_fork, up_vfork, up_fork * * Description: * The up_fork() function is the base of fork() function that provided in @@ -88,15 +88,57 @@ .text .align 2 - .globl up_fork - .type up_fork, function .set nomips16 #ifdef CONFIG_MIPS_MICROMIPS .set micromips #endif - .ent up_fork +#ifdef CONFIG_ARCH_HAVE_TASK_FORK + .globl up_task_fork + .type up_task_fork, function + .ent up_task_fork +up_task_fork: + li $a1, FORK_TYPE_TASK + b mips_fork_common + nop + .end up_task_fork + .size up_task_fork, .-up_task_fork +#endif + +#ifdef CONFIG_ARCH_HAVE_VFORK + .globl up_vfork + .type up_vfork, function + .ent up_vfork +up_vfork: + li $a1, FORK_TYPE_VFORK + b mips_fork_common + nop + .end up_vfork + .size up_vfork, .-up_vfork +#endif + +#ifdef CONFIG_ARCH_HAVE_FORK + .globl up_fork + .type up_fork, function + .ent up_fork up_fork: + li $a1, FORK_TYPE_FORK + b mips_fork_common + nop + .end up_fork + .size up_fork, .-up_fork +#endif + + /* The shared snapshot. $a1 holds the FORK_TYPE_* selector on entry and + * is carried through to mips_fork(); it is not part of the snapshot, and + * $ra still holds the original caller's return address because the entry + * points above branched here rather than calling. + */ + + .type mips_fork_common, function + .ent mips_fork_common + +mips_fork_common: /* Create a stack frame */ move $t0, $sp /* Save the value of the stack on entry */ @@ -130,7 +172,9 @@ up_fork: /* Floating point registers (not yet) */ - /* Then, call mips_fork(), passing it a pointer to the stack structure */ + /* Then, call mips_fork(), passing it a pointer to the stack structure. + * $a1 already holds the FORK_TYPE_* selector. + */ move $a0, $sp jal mips_fork @@ -142,5 +186,5 @@ up_fork: addiu $sp, $sp, FORK_SIZEOF j $ra - .end up_fork - .size up_fork, .-up_fork + .end mips_fork_common + .size mips_fork_common, .-mips_fork_common diff --git a/arch/mips/src/mips32/mips_fork.c b/arch/mips/src/mips32/mips_fork.c index 3d931d1da1d56..dc91d24789835 100644 --- a/arch/mips/src/mips32/mips_fork.c +++ b/arch/mips/src/mips32/mips_fork.c @@ -40,6 +40,14 @@ #include "mips_fork.h" #include "sched/sched.h" +/* This architecture gives the child a relocated copy of the parent's stack; + * a borrowed stack would need that relocation to be skipped. + */ + +#ifdef CONFIG_ARCH_VFORK_STACK_BORROW +# error "MIPS relocates the vfork() child stack; borrowing is not supported" +#endif + /**************************************************************************** * Public Functions ****************************************************************************/ @@ -89,7 +97,7 @@ * ****************************************************************************/ -pid_t mips_fork(const struct fork_s *context) +pid_t mips_fork(const struct fork_s *context, int type) { struct tcb_s *parent = this_task(); struct tcb_s *child; @@ -113,7 +121,7 @@ pid_t mips_fork(const struct fork_s *context) context->fp, context->sp, context->ra, context->gp); #else sinfo("fp:%08" PRIx32 " sp:%08" PRIx32 " ra:%08" PRIx32 "\n", - context->fp context->sp, context->ra); + context->fp, context->sp, context->ra); #endif #else sinfo("s5:%08" PRIx32 " s6:%08" PRIx32 " s7:%08" PRIx32 @@ -130,7 +138,8 @@ pid_t mips_fork(const struct fork_s *context) /* Allocate and initialize a TCB for the child task. */ - child = nxtask_setup_fork((start_t)context->ra); + child = nxtask_setup_fork((start_t)context->ra, type, + (uintptr_t)context->sp); if (!child) { sinfo("nxtask_setup_fork failed\n"); @@ -217,5 +226,5 @@ pid_t mips_fork(const struct fork_s *context) * will discard the TCB by calling nxtask_abort_fork(). */ - return nxtask_start_fork(child); + return nxtask_start_fork(child, type); } diff --git a/arch/mips/src/mips32/mips_fork.h b/arch/mips/src/mips32/mips_fork.h index ab50b3eda7eff..632a2b7c57d6f 100644 --- a/arch/mips/src/mips32/mips_fork.h +++ b/arch/mips/src/mips32/mips_fork.h @@ -28,6 +28,7 @@ ****************************************************************************/ #include +#include #include /**************************************************************************** diff --git a/arch/risc-v/src/common/CMakeLists.txt b/arch/risc-v/src/common/CMakeLists.txt index 84a1d43fb47c8..2e6ccf65c0240 100644 --- a/arch/risc-v/src/common/CMakeLists.txt +++ b/arch/risc-v/src/common/CMakeLists.txt @@ -86,7 +86,9 @@ if(CONFIG_STACK_COLORATION) list(APPEND SRCS riscv_checkstack.c) endif() -if(CONFIG_ARCH_HAVE_FORK) +if(CONFIG_ARCH_HAVE_TASK_FORK + OR CONFIG_ARCH_HAVE_VFORK + OR CONFIG_ARCH_HAVE_FORK) list(APPEND SRCS fork.S riscv_fork.c) endif() diff --git a/arch/risc-v/src/common/Make.defs b/arch/risc-v/src/common/Make.defs index d98a828c508e0..aabff1ed99d3e 100644 --- a/arch/risc-v/src/common/Make.defs +++ b/arch/risc-v/src/common/Make.defs @@ -86,7 +86,7 @@ ifeq ($(CONFIG_STACK_COLORATION),y) CMN_CSRCS += riscv_checkstack.c endif -ifeq ($(CONFIG_ARCH_HAVE_FORK),y) +ifneq ($(CONFIG_ARCH_HAVE_TASK_FORK)$(CONFIG_ARCH_HAVE_VFORK)$(CONFIG_ARCH_HAVE_FORK),) CMN_ASRCS += fork.S CMN_CSRCS += riscv_fork.c endif diff --git a/arch/risc-v/src/common/fork.S b/arch/risc-v/src/common/fork.S index 108ff00f80db1..76acd88bd5399 100644 --- a/arch/risc-v/src/common/fork.S +++ b/arch/risc-v/src/common/fork.S @@ -39,59 +39,95 @@ .file "fork.S" .globl riscv_fork +#ifdef CONFIG_ARCH_HAVE_TASK_FORK + .globl up_task_fork +#endif +#ifdef CONFIG_ARCH_HAVE_VFORK + .globl up_vfork +#endif +#ifdef CONFIG_ARCH_HAVE_FORK .globl up_fork +#endif /**************************************************************************** * Public Functions ****************************************************************************/ /**************************************************************************** - * Name: fork + * Name: up_task_fork, up_vfork, up_fork * * Description: - * The up_fork() function is the base of fork() function that provided in - * libc, and fork() is implemented as a wrapper of up_fork() function. - * The fork() function has the same effect as posix fork(), except that the - * behavior is undefined if the process created by fork() either modifies - * any data other than a variable of type pid_t used to store the return - * value from fork(), or returns from the function in which fork() was - * called, or calls any other function before successfully calling _exit() - * or one of the exec family of functions. + * These are the architecture-specific entry points of NuttX's three + * cloning primitives. All three need exactly the same thing from + * assembly -- a snapshot of the caller's callee-saved registers, stack + * pointer and return address -- and differ only in what the C code then + * does with it, so they share one snapshot sequence and are distinguished + * by a FORK_TYPE_* constant passed to riscv_fork() in a1. * - * This thin layer implements fork by simply calling up_fork() with the - * fork() context as an argument. The overall sequence is: + * See include/nuttx/fork.h for what the three primitives mean. * - * 1) User code calls fork(). fork() collects context information and - * transfers control up up_fork(). - * 2) riscv_fork() and calls nxtask_setup_fork(). - * 3) nxtask_setup_fork() allocates and configures the child task's TCB. - * This consists of: - * - Allocation of the child task's TCB. - * - Initialization of file descriptors and streams - * - Configuration of environment variables - * - Allocate and initialize the stack - * - Setup the input parameters for the task. - * - Initialization of the TCB (including call to up_initial_state()) - * 4) riscv_fork() provides any additional operating context. riscv_fork must: - * - Initialize special values in any CPU registers that were not - * already configured by up_initial_state() - * 5) riscv_fork() then calls nxtask_start_fork() - * 6) nxtask_start_fork() then executes the child thread. + * The overall sequence is: + * + * 1) User code calls task_fork(), vfork() or fork(). Each is a libc + * wrapper around the matching entry point here. + * 2) The entry point collects the context and calls riscv_fork(). + * 3) riscv_fork() calls nxtask_setup_fork(), which allocates and + * configures the child task's TCB. + * 4) riscv_fork() provides any additional operating context and relocates + * the copied stack. + * 5) riscv_fork() then calls nxtask_start_fork(), or nxtask_start_vfork() + * which additionally suspends the caller. + * 6) which executes the child thread. * * Input Parameters: * None * * Returned Value: - * Upon successful completion, fork() returns 0 to the child process and - * returns the process ID of the child process to the parent process. - * Otherwise, -1 is returned to the parent, no child process is created, - * and errno is set to indicate the error. + * Upon successful completion, 0 is returned to the child and the process + * ID of the child is returned to the parent. Otherwise, -1 is returned to + * the parent, no child is created, and errno is set to indicate the error. * ****************************************************************************/ +#ifdef CONFIG_ARCH_HAVE_TASK_FORK +.type up_task_fork, function + +up_task_fork: + li a1, FORK_TYPE_TASK + j riscv_fork_common + + .size up_task_fork, .-up_task_fork +#endif + +#ifdef CONFIG_ARCH_HAVE_VFORK +.type up_vfork, function + +up_vfork: + li a1, FORK_TYPE_VFORK + j riscv_fork_common + + .size up_vfork, .-up_vfork +#endif + +#ifdef CONFIG_ARCH_HAVE_FORK .type up_fork, function up_fork: + li a1, FORK_TYPE_FORK + j riscv_fork_common + + .size up_fork, .-up_fork +#endif + +/* The shared snapshot. a1 holds the FORK_TYPE_* selector on entry and is + * carried through to riscv_fork(); it is not part of the snapshot, and ra + * still holds the original caller's return address because the entry points + * above jumped here rather than calling. + */ + +.type riscv_fork_common, function + +riscv_fork_common: #ifdef CONFIG_LIB_SYSCALL /* When coming via system call, everything is in place already */ @@ -150,7 +186,9 @@ up_fork: FSTORE fs11, FORK_FS11_OFFSET(sp) #endif - /* Then, call riscv_fork(), passing it a pointer to the stack frame */ + /* Then, call riscv_fork(), passing it a pointer to the stack frame. a1 + * already holds the FORK_TYPE_* selector. + */ mv a0, sp call riscv_fork @@ -162,5 +200,5 @@ up_fork: ret #endif - .size up_fork, .-up_fork + .size riscv_fork_common, .-riscv_fork_common .end diff --git a/arch/risc-v/src/common/riscv_fork.c b/arch/risc-v/src/common/riscv_fork.c index f25e5cf4e4062..ee9fcb1c9294c 100644 --- a/arch/risc-v/src/common/riscv_fork.c +++ b/arch/risc-v/src/common/riscv_fork.c @@ -41,7 +41,16 @@ #include "sched/sched.h" -#ifdef CONFIG_ARCH_HAVE_FORK +#if defined(CONFIG_ARCH_HAVE_TASK_FORK) || defined(CONFIG_ARCH_HAVE_VFORK) || \ + defined(CONFIG_ARCH_HAVE_FORK) + +/* This architecture gives the child a relocated copy of the parent's stack; + * a borrowed stack would need the relocation below to be skipped. + */ + +#ifdef CONFIG_ARCH_VFORK_STACK_BORROW +# error "RISC-V relocates the vfork() child stack; borrowing is not supported" +#endif /**************************************************************************** * Pre-processor Definitions @@ -102,7 +111,7 @@ #ifdef CONFIG_LIB_SYSCALL -pid_t riscv_fork(const struct fork_s *context) +pid_t riscv_fork(const struct fork_s *context, int type) { struct tcb_s *parent = this_task(); struct tcb_s *child; @@ -117,7 +126,8 @@ pid_t riscv_fork(const struct fork_s *context) /* Allocate and initialize a TCB for the child task. */ - child = nxtask_setup_fork((start_t)parent->xcp.sregs[REG_RA]); + child = nxtask_setup_fork((start_t)parent->xcp.sregs[REG_RA], type, + parent->xcp.sregs[REG_SP]); if (!child) { sinfo("nxtask_setup_fork failed\n"); @@ -184,12 +194,12 @@ pid_t riscv_fork(const struct fork_s *context) * will discard the TCB by calling nxtask_abort_fork(). */ - return nxtask_start_fork(child); + return nxtask_start_fork(child, type); } #else -pid_t riscv_fork(const struct fork_s *context) +pid_t riscv_fork(const struct fork_s *context, int type) { struct tcb_s *parent = this_task(); struct tcb_s *child; @@ -215,7 +225,7 @@ pid_t riscv_fork(const struct fork_s *context) context->fp, context->sp, context->ra, context->gp); #else sinfo("fp:%" PRIxREG " sp:%" PRIxREG " ra:%" PRIxREG "\n", - context->fp context->sp, context->ra); + context->fp, context->sp, context->ra); #endif #else sinfo("s5:%" PRIxREG " s6:%" PRIxREG " s7:%" PRIxREG " s8:%" PRIxREG "\n", @@ -231,7 +241,8 @@ pid_t riscv_fork(const struct fork_s *context) /* Allocate and initialize a TCB for the child task. */ - child = nxtask_setup_fork((start_t)(uintptr_t)context->ra); + child = nxtask_setup_fork((start_t)(uintptr_t)context->ra, type, + (uintptr_t)context->sp); if (!child) { sinfo("nxtask_setup_fork failed\n"); @@ -346,8 +357,9 @@ pid_t riscv_fork(const struct fork_s *context) * will discard the TCB by calling nxtask_abort_fork(). */ - return nxtask_start_fork(child); + return nxtask_start_fork(child, type); } #endif /* CONFIG_LIB_SYSCALL */ -#endif /* CONFIG_ARCH_HAVE_FORK */ +#endif /* CONFIG_ARCH_HAVE_TASK_FORK || CONFIG_ARCH_HAVE_VFORK || + * CONFIG_ARCH_HAVE_FORK */ diff --git a/arch/risc-v/src/common/riscv_fork.h b/arch/risc-v/src/common/riscv_fork.h index 6b3a71d581854..d807d5dc499b2 100644 --- a/arch/risc-v/src/common/riscv_fork.h +++ b/arch/risc-v/src/common/riscv_fork.h @@ -28,6 +28,7 @@ ****************************************************************************/ #include +#include #include #include "riscv_internal.h" diff --git a/arch/sim/src/Makefile b/arch/sim/src/Makefile index 280c3a8b05041..c71e986845c18 100644 --- a/arch/sim/src/Makefile +++ b/arch/sim/src/Makefile @@ -95,7 +95,7 @@ ifeq ($(CONFIG_SCHED_BACKTRACE),y) CSRCS += sim_backtrace.c endif -ifeq ($(CONFIG_ARCH_HAVE_FORK),y) +ifneq ($(CONFIG_ARCH_HAVE_TASK_FORK)$(CONFIG_ARCH_HAVE_VFORK)$(CONFIG_ARCH_HAVE_FORK),) CSRCS += sim_fork.c endif diff --git a/arch/sim/src/sim/CMakeLists.txt b/arch/sim/src/sim/CMakeLists.txt index a378448d1a324..101b94119612a 100644 --- a/arch/sim/src/sim/CMakeLists.txt +++ b/arch/sim/src/sim/CMakeLists.txt @@ -82,7 +82,9 @@ if(CONFIG_SCHED_BACKTRACE) list(APPEND SRCS sim_backtrace.c) endif() -if(CONFIG_ARCH_HAVE_FORK) +if(CONFIG_ARCH_HAVE_TASK_FORK + OR CONFIG_ARCH_HAVE_VFORK + OR CONFIG_ARCH_HAVE_FORK) list(APPEND SRCS sim_fork.c) endif() diff --git a/arch/sim/src/sim/sim_fork.c b/arch/sim/src/sim/sim_fork.c index 47ea655cbb65f..c03a2c93ced8f 100644 --- a/arch/sim/src/sim/sim_fork.c +++ b/arch/sim/src/sim/sim_fork.c @@ -34,18 +34,27 @@ #include #include +#include #include #include #include #include "sched/sched.h" +/* This architecture gives the child a relocated copy of the parent's stack; + * a borrowed stack would need that relocation to be skipped. + */ + +#ifdef CONFIG_ARCH_VFORK_STACK_BORROW +# error "The simulator relocates the vfork() child stack; borrowing is not supported" +#endif + /**************************************************************************** * Public Functions ****************************************************************************/ /**************************************************************************** - * Name: sim_fork + * Name: sim_fork_internal * * Description: * The fork() function has the same effect as posix fork(), except that the @@ -88,7 +97,7 @@ #ifdef CONFIG_SIM_ASAN nosanitize_address #endif -pid_t sim_fork(const xcpt_reg_t *context) +static pid_t sim_fork_internal(const xcpt_reg_t *context, int type) { struct tcb_s *parent = this_task(); struct tcb_s *child; @@ -106,7 +115,8 @@ pid_t sim_fork(const xcpt_reg_t *context) /* Allocate and initialize a TCB for the child task. */ - child = nxtask_setup_fork((start_t)context[JB_PC]); + child = nxtask_setup_fork((start_t)context[JB_PC], type, + (uintptr_t)context[JB_SP]); if (!child) { serr("ERROR: nxtask_setup_fork failed\n"); @@ -175,5 +185,37 @@ pid_t sim_fork(const xcpt_reg_t *context) * will discard the TCB by calling nxtask_abort_fork(). */ - return nxtask_start_fork(child); + return nxtask_start_fork(child, type); } + +/**************************************************************************** + * Name: sim_task_fork, sim_vfork, sim_fork + * + * Description: + * The three primitives, each a name for one FORK_TYPE_* value. The + * simulator's assembly entry points call these directly rather than + * carrying a selector through setjmp(), which would have to survive a call + * that is allowed to clobber every argument register. + * + ****************************************************************************/ + +#ifdef CONFIG_ARCH_HAVE_TASK_FORK +pid_t sim_task_fork(const xcpt_reg_t *context) +{ + return sim_fork_internal(context, FORK_TYPE_TASK); +} +#endif + +#ifdef CONFIG_ARCH_HAVE_VFORK +pid_t sim_vfork(const xcpt_reg_t *context) +{ + return sim_fork_internal(context, FORK_TYPE_VFORK); +} +#endif + +#ifdef CONFIG_ARCH_HAVE_FORK +pid_t sim_fork(const xcpt_reg_t *context) +{ + return sim_fork_internal(context, FORK_TYPE_FORK); +} +#endif diff --git a/arch/sim/src/sim/sim_fork_arm.S b/arch/sim/src/sim/sim_fork_arm.S index ee39f54e003b5..10032044d25c7 100644 --- a/arch/sim/src/sim/sim_fork_arm.S +++ b/arch/sim/src/sim/sim_fork_arm.S @@ -43,7 +43,19 @@ ************************************************************************************/ /************************************************************************************ - * Name: up_fork + * Name: up_task_fork, up_vfork, up_fork + * + * Description: + * These are the architecture-specific entry points of NuttX's three + * cloning primitives; see include/nuttx/fork.h for what the three mean. + * They are identical apart from the C worker each calls, which is how the + * primitives are distinguished. + * + * On the simulator the caller's context is captured with setjmp() rather + * than by hand, and the child re-enters through longjmp() -- which is why + * each entry point tests setjmp()'s return value to tell which of the two + * returns it is on. + * * * Description: * The up_fork() function is the base of fork() function that provided in @@ -86,6 +98,42 @@ ************************************************************************************/ .text + +#ifdef CONFIG_ARCH_HAVE_TASK_FORK + .globl up_task_fork + .type up_task_fork, @function +up_task_fork: + sub sp, sp, #XCPTCONTEXT_SIZE + mov r0, sp + bl setjmp + + subs r0, #1 + beq 1f + bl sim_task_fork +1: + add sp, sp, #XCPTCONTEXT_SIZE + bx lr + .size up_task_fork, . - up_task_fork +#endif /* CONFIG_ARCH_HAVE_TASK_FORK */ + +#ifdef CONFIG_ARCH_HAVE_VFORK + .globl up_vfork + .type up_vfork, @function +up_vfork: + sub sp, sp, #XCPTCONTEXT_SIZE + mov r0, sp + bl setjmp + + subs r0, #1 + beq 1f + bl sim_vfork +1: + add sp, sp, #XCPTCONTEXT_SIZE + bx lr + .size up_vfork, . - up_vfork +#endif /* CONFIG_ARCH_HAVE_VFORK */ + +#ifdef CONFIG_ARCH_HAVE_FORK .globl up_fork .type up_fork, @function up_fork: @@ -94,10 +142,11 @@ up_fork: bl setjmp subs r0, #1 - jz child + beq 1f bl sim_fork -child: +1: add sp, sp, #XCPTCONTEXT_SIZE - ret + bx lr .size up_fork, . - up_fork +#endif /* CONFIG_ARCH_HAVE_FORK */ .end diff --git a/arch/sim/src/sim/sim_fork_arm64.S b/arch/sim/src/sim/sim_fork_arm64.S index 5d813822fa620..4c8f93f449993 100644 --- a/arch/sim/src/sim/sim_fork_arm64.S +++ b/arch/sim/src/sim/sim_fork_arm64.S @@ -52,7 +52,19 @@ ***************************************************************************/ /**************************************************************************** - * Name: up_fork + * Name: up_task_fork, up_vfork, up_fork + * + * Description: + * These are the architecture-specific entry points of NuttX's three + * cloning primitives; see include/nuttx/fork.h for what the three mean. + * They are identical apart from the C worker each calls, which is how the + * primitives are distinguished. + * + * On the simulator the caller's context is captured with setjmp() rather + * than by hand, and the child re-enters through longjmp() -- which is why + * each entry point tests setjmp()'s return value to tell which of the two + * returns it is on. + * * * Description: * The up_fork() function is the base of fork() function that provided in @@ -96,9 +108,59 @@ ***************************************************************************/ .text - .globl SYMBOL(up_fork) .align 4 +#ifdef CONFIG_ARCH_HAVE_TASK_FORK + .globl SYMBOL(up_task_fork) + +SYMBOL(up_task_fork): + + stp x29, x30, [sp] /* save FP/LR register */ + sub sp, sp, #XCPTCONTEXT_SIZE /* area from stack for setjmp() */ + + mov x0, sp /* pass stack area to setjmp() */ + bl SYMBOL(setjmp) /* save register for longjmp() */ + + subs x0, x0, #1 /* 0: parent / 1: child */ + cbz x0, 1f /* child --> return */ + + mov x0, sp /* pass stack area to the worker */ + bl SYMBOL(sim_task_fork) /* further process task creation */ + +1: + add sp, sp, #XCPTCONTEXT_SIZE /* release area from stack */ + ldp x29, x30, [sp] /* restore FP/LR register */ + + ret +#endif /* CONFIG_ARCH_HAVE_TASK_FORK */ + +#ifdef CONFIG_ARCH_HAVE_VFORK + .globl SYMBOL(up_vfork) + +SYMBOL(up_vfork): + + stp x29, x30, [sp] /* save FP/LR register */ + sub sp, sp, #XCPTCONTEXT_SIZE /* area from stack for setjmp() */ + + mov x0, sp /* pass stack area to setjmp() */ + bl SYMBOL(setjmp) /* save register for longjmp() */ + + subs x0, x0, #1 /* 0: parent / 1: child */ + cbz x0, 1f /* child --> return */ + + mov x0, sp /* pass stack area to the worker */ + bl SYMBOL(sim_vfork) /* further process task creation */ + +1: + add sp, sp, #XCPTCONTEXT_SIZE /* release area from stack */ + ldp x29, x30, [sp] /* restore FP/LR register */ + + ret +#endif /* CONFIG_ARCH_HAVE_VFORK */ + +#ifdef CONFIG_ARCH_HAVE_FORK + .globl SYMBOL(up_fork) + SYMBOL(up_fork): stp x29, x30, [sp] /* save FP/LR register */ @@ -110,7 +172,7 @@ SYMBOL(up_fork): subs x0, x0, #1 /* 0: parent / 1: child */ cbz x0, 1f /* child --> return */ - mov x0, sp /* pass stack area to sim_fork() */ + mov x0, sp /* pass stack area to the worker */ bl SYMBOL(sim_fork) /* further process task creation */ 1: @@ -118,5 +180,5 @@ SYMBOL(up_fork): ldp x29, x30, [sp] /* restore FP/LR register */ ret - +#endif /* CONFIG_ARCH_HAVE_FORK */ .end diff --git a/arch/sim/src/sim/sim_fork_x86.S b/arch/sim/src/sim/sim_fork_x86.S index ec7486664c568..ab460b03c157d 100644 --- a/arch/sim/src/sim/sim_fork_x86.S +++ b/arch/sim/src/sim/sim_fork_x86.S @@ -51,7 +51,19 @@ ************************************************************************************/ /************************************************************************************ - * Name: up_fork + * Name: up_task_fork, up_vfork, up_fork + * + * Description: + * These are the architecture-specific entry points of NuttX's three + * cloning primitives; see include/nuttx/fork.h for what the three mean. + * They are identical apart from the C worker each calls, which is how the + * primitives are distinguished. + * + * On the simulator the caller's context is captured with setjmp() rather + * than by hand, and the child re-enters through longjmp() -- which is why + * each entry point tests setjmp()'s return value to tell which of the two + * returns it is on. + * * * Description: * The up_fork() function is the base of fork() function that provided in @@ -94,6 +106,52 @@ ************************************************************************************/ .text + +#ifdef CONFIG_ARCH_HAVE_TASK_FORK + .globl SYMBOL(up_task_fork) +#ifdef __ELF__ + .type SYMBOL(up_task_fork), @function +#endif + +SYMBOL(up_task_fork): + sub $XCPTCONTEXT_SIZE, %esp + push %esp + call SYMBOL(setjmp) + + sub $1, %eax + jz 1f + call SYMBOL(sim_task_fork) +1: + add $XCPTCONTEXT_SIZE+4, %esp + ret +#ifdef __ELF__ + .size SYMBOL(up_task_fork), . - SYMBOL(up_task_fork) +#endif +#endif /* CONFIG_ARCH_HAVE_TASK_FORK */ + +#ifdef CONFIG_ARCH_HAVE_VFORK + .globl SYMBOL(up_vfork) +#ifdef __ELF__ + .type SYMBOL(up_vfork), @function +#endif + +SYMBOL(up_vfork): + sub $XCPTCONTEXT_SIZE, %esp + push %esp + call SYMBOL(setjmp) + + sub $1, %eax + jz 1f + call SYMBOL(sim_vfork) +1: + add $XCPTCONTEXT_SIZE+4, %esp + ret +#ifdef __ELF__ + .size SYMBOL(up_vfork), . - SYMBOL(up_vfork) +#endif +#endif /* CONFIG_ARCH_HAVE_VFORK */ + +#ifdef CONFIG_ARCH_HAVE_FORK .globl SYMBOL(up_fork) #ifdef __ELF__ .type SYMBOL(up_fork), @function @@ -105,11 +163,12 @@ SYMBOL(up_fork): call SYMBOL(setjmp) sub $1, %eax - jz child + jz 1f call SYMBOL(sim_fork) -child: +1: add $XCPTCONTEXT_SIZE+4, %esp ret #ifdef __ELF__ .size SYMBOL(up_fork), . - SYMBOL(up_fork) #endif +#endif /* CONFIG_ARCH_HAVE_FORK */ diff --git a/arch/sim/src/sim/sim_fork_x86_64.S b/arch/sim/src/sim/sim_fork_x86_64.S index 85b072acf866e..92a2bc4630e2a 100644 --- a/arch/sim/src/sim/sim_fork_x86_64.S +++ b/arch/sim/src/sim/sim_fork_x86_64.S @@ -51,7 +51,19 @@ ************************************************************************************/ /************************************************************************************ - * Name: up_fork + * Name: up_task_fork, up_vfork, up_fork + * + * Description: + * These are the architecture-specific entry points of NuttX's three + * cloning primitives; see include/nuttx/fork.h for what the three mean. + * They are identical apart from the C worker each calls, which is how the + * primitives are distinguished. + * + * On the simulator the caller's context is captured with setjmp() rather + * than by hand, and the child re-enters through longjmp() -- which is why + * each entry point tests setjmp()'s return value to tell which of the two + * returns it is on. + * * * Description: * The up_fork() function is the base of fork() function that provided in @@ -94,6 +106,62 @@ ************************************************************************************/ .text + +#ifdef CONFIG_ARCH_HAVE_TASK_FORK + .globl SYMBOL(up_task_fork) +#ifdef __ELF__ + .type SYMBOL(up_task_fork), @function +#endif + +SYMBOL(up_task_fork): + sub $XCPTCONTEXT_SIZE, %rsp +#ifdef CONFIG_SIM_X8664_MICROSOFT + mov %rsp, %rcx +#else /* if defined(CONFIG_SIM_X8664_SYSTEMV) */ + mov %rsp, %rdi +#endif + call SYMBOL(setjmp) + + sub $1, %eax + jz 1f + + call SYMBOL(sim_task_fork) +1: + add $XCPTCONTEXT_SIZE, %rsp + ret +#ifdef __ELF__ + .size SYMBOL(up_task_fork), . - SYMBOL(up_task_fork) +#endif +#endif /* CONFIG_ARCH_HAVE_TASK_FORK */ + +#ifdef CONFIG_ARCH_HAVE_VFORK + .globl SYMBOL(up_vfork) +#ifdef __ELF__ + .type SYMBOL(up_vfork), @function +#endif + +SYMBOL(up_vfork): + sub $XCPTCONTEXT_SIZE, %rsp +#ifdef CONFIG_SIM_X8664_MICROSOFT + mov %rsp, %rcx +#else /* if defined(CONFIG_SIM_X8664_SYSTEMV) */ + mov %rsp, %rdi +#endif + call SYMBOL(setjmp) + + sub $1, %eax + jz 1f + + call SYMBOL(sim_vfork) +1: + add $XCPTCONTEXT_SIZE, %rsp + ret +#ifdef __ELF__ + .size SYMBOL(up_vfork), . - SYMBOL(up_vfork) +#endif +#endif /* CONFIG_ARCH_HAVE_VFORK */ + +#ifdef CONFIG_ARCH_HAVE_FORK .globl SYMBOL(up_fork) #ifdef __ELF__ .type SYMBOL(up_fork), @function @@ -109,12 +177,13 @@ SYMBOL(up_fork): call SYMBOL(setjmp) sub $1, %eax - jz child + jz 1f call SYMBOL(sim_fork) -child: +1: add $XCPTCONTEXT_SIZE, %rsp ret #ifdef __ELF__ .size SYMBOL(up_fork), . - SYMBOL(up_fork) #endif +#endif /* CONFIG_ARCH_HAVE_FORK */ diff --git a/arch/x86_64/src/common/CMakeLists.txt b/arch/x86_64/src/common/CMakeLists.txt index 841fa13679935..e9c320af9771c 100644 --- a/arch/x86_64/src/common/CMakeLists.txt +++ b/arch/x86_64/src/common/CMakeLists.txt @@ -34,7 +34,9 @@ set(SRCS x86_64_tcbinfo.c x86_64_tlb.c) -if(CONFIG_ARCH_HAVE_FORK) +if(CONFIG_ARCH_HAVE_TASK_FORK + OR CONFIG_ARCH_HAVE_VFORK + OR CONFIG_ARCH_HAVE_FORK) list(APPEND SRCS x86_64_fork.c fork.S) endif() diff --git a/arch/x86_64/src/common/Make.defs b/arch/x86_64/src/common/Make.defs index a5a21aff0a63a..4c5c8f2bab407 100644 --- a/arch/x86_64/src/common/Make.defs +++ b/arch/x86_64/src/common/Make.defs @@ -29,7 +29,7 @@ CMN_CSRCS += x86_64_getintstack.c x86_64_initialize.c x86_64_nputs.c CMN_CSRCS += x86_64_modifyreg8.c x86_64_modifyreg16.c x86_64_modifyreg32.c CMN_CSRCS += x86_64_switchcontext.c x86_64_tlb.c -ifeq ($(CONFIG_ARCH_HAVE_FORK),y) +ifneq ($(CONFIG_ARCH_HAVE_TASK_FORK)$(CONFIG_ARCH_HAVE_VFORK)$(CONFIG_ARCH_HAVE_FORK),) CMN_CSRCS += x86_64_fork.c CMN_ASRCS += fork.S endif diff --git a/arch/x86_64/src/common/fork.S b/arch/x86_64/src/common/fork.S index 1621b560474a1..602ded522fe24 100644 --- a/arch/x86_64/src/common/fork.S +++ b/arch/x86_64/src/common/fork.S @@ -36,7 +36,7 @@ ****************************************************************************/ /**************************************************************************** - * Name: up_fork + * Name: up_task_fork, up_vfork, up_fork * * Description: * The up_fork() function is the base of fork() function that provided in @@ -95,10 +95,43 @@ * | ......... | */ +#ifdef CONFIG_ARCH_HAVE_TASK_FORK + .globl up_task_fork + .type up_task_fork, @function + +up_task_fork: + movq $FORK_TYPE_TASK, %rsi + jmp x86_64_fork_common +#endif + +#ifdef CONFIG_ARCH_HAVE_VFORK + .globl up_vfork + .type up_vfork, @function + +up_vfork: + movq $FORK_TYPE_VFORK, %rsi + jmp x86_64_fork_common +#endif + +#ifdef CONFIG_ARCH_HAVE_FORK .globl up_fork .type up_fork, @function up_fork: + movq $FORK_TYPE_FORK, %rsi + jmp x86_64_fork_common +#endif + +/* The shared snapshot. %rsi holds the FORK_TYPE_* selector on entry and is + * carried through to x86_64_fork() as its second argument. Note that the + * return address the entry points pushed is still on the stack -- they + * jumped here rather than calling -- so the %rsp recovered below is the + * original caller's. + */ + + .type x86_64_fork_common, @function + +x86_64_fork_common: movq %rsp, %rax addq $8, %rax movq %ss, %rdi diff --git a/arch/x86_64/src/common/x86_64_fork.c b/arch/x86_64/src/common/x86_64_fork.c index ea4d90ac044b4..0c4bf9f1cd7d4 100644 --- a/arch/x86_64/src/common/x86_64_fork.c +++ b/arch/x86_64/src/common/x86_64_fork.c @@ -41,6 +41,14 @@ #include "x86_64_internal.h" #include "sched/sched.h" +/* This architecture gives the child a relocated copy of the parent's stack; + * a borrowed stack would need that relocation to be skipped. + */ + +#ifdef CONFIG_ARCH_VFORK_STACK_BORROW +# error "x86_64 relocates the vfork() child stack; borrowing is not supported" +#endif + /**************************************************************************** * Public Functions ****************************************************************************/ @@ -89,7 +97,7 @@ * ****************************************************************************/ -pid_t x86_64_fork(const struct fork_s *context) +pid_t x86_64_fork(const struct fork_s *context, int type) { struct tcb_s *parent = this_task(); struct tcb_s *child; @@ -110,7 +118,8 @@ pid_t x86_64_fork(const struct fork_s *context) /* Allocate and initialize a TCB for the child task. */ - child = nxtask_setup_fork((start_t)context->rip); + child = nxtask_setup_fork((start_t)context->rip, type, + (uintptr_t)context->rsp); if (!child) { serr("ERROR: nxtask_setup_fork failed\n"); @@ -195,5 +204,5 @@ pid_t x86_64_fork(const struct fork_s *context) * will discard the TCB by calling nxtask_abort_fork(). */ - return nxtask_start_fork(child); + return nxtask_start_fork(child, type); } diff --git a/arch/x86_64/src/common/x86_64_fork.h b/arch/x86_64/src/common/x86_64_fork.h index f6e4c31978026..acf05285be409 100644 --- a/arch/x86_64/src/common/x86_64_fork.h +++ b/arch/x86_64/src/common/x86_64_fork.h @@ -28,6 +28,7 @@ ****************************************************************************/ #include +#include /**************************************************************************** * Pre-processor Definitions diff --git a/include/nuttx/addrenv.h b/include/nuttx/addrenv.h index 8e253c88eee1b..91c2eaa28d4d2 100644 --- a/include/nuttx/addrenv.h +++ b/include/nuttx/addrenv.h @@ -394,6 +394,31 @@ int addrenv_attach(FAR struct tcb_s *tcb, FAR struct addrenv_s *addrenv); int addrenv_join(FAR struct tcb_s *ptcb, FAR struct tcb_s *tcb); +/**************************************************************************** + * Name: addrenv_fork + * + * Description: + * Duplicate the parent's address environment for a POSIX fork() child and + * attach it: the child gets its own pages holding a copy of the parent's + * contents, mapped at the same virtual addresses. Contrast + * addrenv_join(), which gives the child the parent's memory. + * + * Input Parameters: + * ptcb - The tcb of the parent process. + * tcb - The tcb of the child process. + * + * Returned Value: + * This is a NuttX internal function so it follows the convention that + * 0 (OK) is returned on success and a negated errno is returned on + * failure. -ENOMEM is returned if there is not enough free memory to + * hold a copy of the parent. + * + ****************************************************************************/ + +#ifdef CONFIG_ARCH_HAVE_FORK +int addrenv_fork(FAR struct tcb_s *ptcb, FAR struct tcb_s *tcb); +#endif + /**************************************************************************** * Name: addrenv_leave * diff --git a/include/nuttx/arch.h b/include/nuttx/arch.h index e1a598c222b2d..ce792de1a2afa 100644 --- a/include/nuttx/arch.h +++ b/include/nuttx/arch.h @@ -249,12 +249,54 @@ extern initializer_t _einit[]; * logic from architecture-specific code. ****************************************************************************/ +/**************************************************************************** + * Name: up_task_fork + * + * Description: + * Architecture-specific base of task_fork(): the child shares the + * parent's memory, runs on a private copy of the parent's stack, and runs + * concurrently. Neither fork() nor vfork(); see up_fork(), up_vfork(). + * + * Returned Value: + * Upon successful completion, up_task_fork() returns 0 to the child and + * returns the process ID of the child to the parent. Otherwise, -1 is + * returned to the parent, no child is created, and errno is set to + * indicate the error. + * + ****************************************************************************/ + +#ifdef CONFIG_ARCH_HAVE_TASK_FORK +pid_t up_task_fork(void); +#endif + +/**************************************************************************** + * Name: up_vfork + * + * Description: + * Architecture-specific base of vfork(): the child shares the parent's + * memory and the parent is suspended until the child _exit()s or exec()s. + * Under CONFIG_ARCH_VFORK_STACK_BORROW the child borrows the parent's + * stack rather than running on a relocated copy. + * + * Returned Value: + * Upon successful completion, up_vfork() returns 0 to the child and + * returns the process ID of the child to the parent. Otherwise, -1 is + * returned to the parent, no child is created, and errno is set to + * indicate the error. + * + ****************************************************************************/ + +#ifdef CONFIG_ARCH_HAVE_VFORK +pid_t up_vfork(void); +#endif + /**************************************************************************** * Name: up_fork * * Description: - * The up_fork() function is the base of fork() function that provided in - * libc, and fork() is implemented as a wrapper of up_fork() function. + * Architecture-specific base of POSIX fork(): the child receives its own + * copy of the parent's memory at the same virtual addresses and runs + * concurrently. Available only where CONFIG_ARCH_HAVE_FORK is selected. * * Returned Value: * Upon successful completion, up_fork() returns 0 to the child process @@ -264,7 +306,9 @@ extern initializer_t _einit[]; * ****************************************************************************/ +#ifdef CONFIG_ARCH_HAVE_FORK pid_t up_fork(void); +#endif /**************************************************************************** * Name: up_initialize @@ -1327,6 +1371,35 @@ int up_addrenv_clone(FAR const arch_addrenv_t *src, FAR arch_addrenv_t *dest); #endif +/**************************************************************************** + * Name: up_addrenv_fork + * + * Description: + * Duplicate an address environment for POSIX fork(): allocate fresh + * pages for the destination, copy the source's contents into them, and map + * them at the same virtual addresses. Unlike up_addrenv_clone(), which + * copies only the representation and leaves both pointing at the same page + * tables, the result is independent of the source. + * + * Implemented only where CONFIG_ARCH_HAVE_ADDRENV_FORK is selected. + * + * Input Parameters: + * src - The address environment to be duplicated. + * dest - The location to receive the duplicate. It is wiped by this + * function before anything is allocated into it. + * + * Returned Value: + * Zero (OK) on success; a negated errno value on failure. -ENOMEM is + * returned if there are not enough free pages to hold the copy, in which + * case nothing is left allocated. + * + ****************************************************************************/ + +#ifdef CONFIG_ARCH_HAVE_ADDRENV_FORK +int up_addrenv_fork(FAR const arch_addrenv_t *src, + FAR arch_addrenv_t *dest); +#endif + /**************************************************************************** * Name: up_addrenv_attach * diff --git a/include/nuttx/fork.h b/include/nuttx/fork.h new file mode 100644 index 0000000000000..da44bf3483b44 --- /dev/null +++ b/include/nuttx/fork.h @@ -0,0 +1,53 @@ +/**************************************************************************** + * include/nuttx/fork.h + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. The + * ASF licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + ****************************************************************************/ + +#ifndef __INCLUDE_NUTTX_FORK_H +#define __INCLUDE_NUTTX_FORK_H + +/**************************************************************************** + * Included Files + ****************************************************************************/ + +#include + +/**************************************************************************** + * Pre-processor Definitions + ****************************************************************************/ + +/* Which primitive a clone of the calling task implements. Passed to + * nxtask_setup_fork(), which is where the memory semantics are decided. + * Macros rather than an enumeration: the architecture entry points are + * assembly and load them into a register. + * + * FORK_TYPE_TASK task_fork(): shares memory, private stack copy, both + * run. Not POSIX. + * FORK_TYPE_VFORK vfork(): shares memory, parent suspended until the + * child _exit()s or exec()s. + * FORK_TYPE_FORK fork(): child gets its own copy of the parent's memory + * at the same virtual addresses, both run. + */ + +#define FORK_TYPE_TASK 0 +#define FORK_TYPE_VFORK 1 +#define FORK_TYPE_FORK 2 + +#endif /* __INCLUDE_NUTTX_FORK_H */ diff --git a/include/nuttx/sched.h b/include/nuttx/sched.h index 76630d3f10110..ac751fa73543f 100644 --- a/include/nuttx/sched.h +++ b/include/nuttx/sched.h @@ -420,6 +420,23 @@ struct task_join_s pthread_addr_t exit_value; /* Returned data */ }; +/* struct vfork_s ***********************************************************/ + +/* The rendezvous between a suspended vfork() parent and its child. This + * lives in the frame of nxtask_start_vfork() on the parent's stack: the + * parent is blocked in that frame for the whole lifetime of the child, so + * the storage is alive exactly as long as it is needed, and no allocation + * is required on a path that must not fail. + */ + +#ifdef CONFIG_ARCH_HAVE_VFORK +struct vfork_s +{ + sem_t sem; /* Posted when the child is done */ + bool released; /* Guards against a second post */ +}; +#endif + /* struct task_group_s ******************************************************/ /* All threads created by pthread_create belong in the same task group (along @@ -650,6 +667,14 @@ struct tcb_s /* after the frame has been */ /* removed from the stack. */ + /* vfork() Support ********************************************************/ + +#ifdef CONFIG_ARCH_HAVE_VFORK + FAR struct vfork_s *vfork_rel; /* Non-NULL in a vfork() child: */ + /* the suspended parent to release */ + /* when this task is torn down. */ +#endif + /* External Module Support ************************************************/ #ifdef CONFIG_PIC @@ -1134,34 +1159,57 @@ void nxtask_startup(main_t entrypt, int argc, FAR char *argv[]); #endif /**************************************************************************** - * Internal fork support. The overall sequence is: - * - * 1) User code calls fork(). fork() is provided in architecture-specific - * code. - * 2) fork()and calls nxtask_setup_fork(). + * Internal support for the three cloning primitives -- task_fork(), vfork() + * and fork(). See include/nuttx/fork.h for what distinguishes them; the + * sequence below is common to all three, and `type' is one of the + * FORK_TYPE_* constants defined there. + * + * 1) User code calls task_fork(), vfork() or fork(). Each is a libc wrapper + * around up_task_fork(), up_vfork() or up_fork() respectively, which are + * provided in architecture-specific code. + * 2) The architecture-specific code snapshots the caller's registers and + * calls nxtask_setup_fork(). * 3) nxtask_setup_fork() allocates and configures the child task's TCB. * This consists of: * - Allocation of the child task's TCB. * - Initialization of file descriptors and streams * - Configuration of environment variables - * - Allocate and initialize the stack + * - Establishing the child's address environment: joined to the parent's + * for task_fork() and vfork(), duplicated from it for fork() + * - Allocating the stack -- or borrowing the parent's, for vfork() on an + * architecture that selects CONFIG_ARCH_VFORK_STACK_BORROW * - Setup the input parameters for the task. * - Initialization of the TCB (including call to up_initial_state()) - * 4) fork() provides any additional operating context. fork must: + * 4) The architecture-specific code provides any additional operating + * context: * - Initialize special values in any CPU registers that were not * already configured by up_initial_state() - * 5) fork() then calls nxtask_start_fork() + * - Relocate the copied stack, unless the child shares the parent's + * 5) It then calls nxtask_start_fork(), or nxtask_start_vfork() which + * additionally suspends the caller. * 6) nxtask_start_fork() then executes the child thread. * * nxtask_abort_fork() may be called if an error occurs between * steps 3 and 6. * + * nxtask_vfork_resume() releases a suspended vfork() parent. It is called + * from nxsched_release_tcb(), the last point in the child's life -- by which + * time the child is off the ready-to-run list and has stopped touching a + * borrowed stack, and by which time an exec()ing child has already handed + * its pid to the program it loaded. + * ****************************************************************************/ -FAR struct tcb_s *nxtask_setup_fork(start_t retaddr); -pid_t nxtask_start_fork(FAR struct tcb_s *child); +FAR struct tcb_s *nxtask_setup_fork(start_t retaddr, int type, + uintptr_t parent_sp); +pid_t nxtask_start_fork(FAR struct tcb_s *child, int type); void nxtask_abort_fork(FAR struct tcb_s *child, int errcode); +#ifdef CONFIG_ARCH_HAVE_VFORK +pid_t nxtask_start_vfork(FAR struct tcb_s *child); +void nxtask_vfork_resume(FAR struct tcb_s *child); +#endif + /**************************************************************************** * Name: nxtask_argvstr * diff --git a/include/sched.h b/include/sched.h index 9fa907af2f527..a30362f337feb 100644 --- a/include/sched.h +++ b/include/sched.h @@ -233,6 +233,16 @@ int task_create_with_stack(FAR const char *name, int priority, int task_delete(pid_t pid); int task_restart(pid_t pid); +/* Clone the calling task: the child shares the parent's memory, runs on a + * private copy of the parent's stack, and runs concurrently. Returns twice, + * like fork(). Not POSIX; new code should prefer pthread_create() or + * posix_spawn(). + */ + +#ifdef CONFIG_ARCH_HAVE_TASK_FORK +pid_t task_fork(void); +#endif + int task_setcancelstate(int state, FAR int *oldstate); int task_setcanceltype(int type, FAR int *oldtype); void task_testcancel(void); diff --git a/include/sys/syscall_lookup.h b/include/sys/syscall_lookup.h index 7a1f37284c85e..5f7b338e6dabe 100644 --- a/include/sys/syscall_lookup.h +++ b/include/sys/syscall_lookup.h @@ -112,6 +112,14 @@ SYSCALL_LOOKUP(nxsem_wait_slow, 1) /* The following can be individually enabled */ +#ifdef CONFIG_ARCH_HAVE_TASK_FORK + SYSCALL_LOOKUP(up_task_fork, 0) +#endif + +#ifdef CONFIG_ARCH_HAVE_VFORK + SYSCALL_LOOKUP(up_vfork, 0) +#endif + #ifdef CONFIG_ARCH_HAVE_FORK SYSCALL_LOOKUP(up_fork, 0) #endif diff --git a/include/unistd.h b/include/unistd.h index 63ded5444ac11..a2ea8773f392e 100644 --- a/include/unistd.h +++ b/include/unistd.h @@ -347,8 +347,17 @@ extern "C" /* Task Control Interfaces */ +/* fork() is declared only where POSIX fork() semantics can be provided, so + * that calling it elsewhere is a build error rather than a silent change of + * meaning. See CONFIG_FORK_IS_TASK_FORK, and task_fork() in sched.h. + */ + +#if defined(CONFIG_ARCH_HAVE_FORK) || defined(CONFIG_FORK_IS_TASK_FORK) pid_t fork(void); +#endif +#ifdef CONFIG_ARCH_HAVE_VFORK pid_t vfork(void); +#endif pid_t getpid(void); pid_t getpgid(pid_t pid); pid_t getpgrp(void); diff --git a/libs/libbuiltin/libgcc/gcov.c b/libs/libbuiltin/libgcc/gcov.c index c73aa620b2301..390d075d456ed 100644 --- a/libs/libbuiltin/libgcc/gcov.c +++ b/libs/libbuiltin/libgcc/gcov.c @@ -468,10 +468,16 @@ void __gcov_execle(void) { } +/* GCC redirects fork() in instrumented code to __gcov_fork(), so this is + * reachable only where unistd.h declares fork() at all. + */ + +#if defined(CONFIG_ARCH_HAVE_FORK) || defined(CONFIG_FORK_IS_TASK_FORK) pid_t __gcov_fork(void) { return fork(); } +#endif void __gcov_dump(void) { diff --git a/libs/libc/libc.csv b/libs/libc/libc.csv index 4600f5329f96a..2f4ae6faa4109 100644 --- a/libs/libc/libc.csv +++ b/libs/libc/libc.csv @@ -70,7 +70,7 @@ "flockfile","stdio.h","!defined(CONFIG_FILE_STREAM)","void","FAR FILE *" "fnmatch","fnmatch.h","","int","FAR const char *","FAR const char *","int" "fopen","stdio.h","defined(CONFIG_FILE_STREAM)","FAR FILE *","FAR const char *","FAR const char *" -"fork","unistd.h","!defined(CONFIG_BUILD_KERNEL) && defined(CONFIG_ARCH_HAVE_FORK)","pid_t" +"fork","unistd.h","!defined(CONFIG_BUILD_KERNEL) && (defined(CONFIG_ARCH_HAVE_FORK) || defined(CONFIG_FORK_IS_TASK_FORK))","pid_t" "fprintf","stdio.h","defined(CONFIG_FILE_STREAM)","int","FAR FILE *","FAR const IPTR char *","..." "fputc","stdio.h","defined(CONFIG_FILE_STREAM)","int","int","FAR FILE *" "fputs","stdio.h","defined(CONFIG_FILE_STREAM)","int","FAR const IPTR char *","FAR FILE *" @@ -326,6 +326,7 @@ "swprintf","wchar.h","","int","FAR wchar_t *","size_t","FAR const wchar_t *","..." "sysconf","unistd.h","","long","int" "syslog","syslog.h","","void","int","FAR const IPTR char *","..." +"task_fork","sched.h","!defined(CONFIG_BUILD_KERNEL) && defined(CONFIG_ARCH_HAVE_TASK_FORK)","pid_t" "task_testcancel","sched.h","defined(CONFIG_CANCELLATION_POINTS)","void" "task_tls_alloc","nuttx/tls.h","!defined(CONFIG_BUILD_KERNEL) && CONFIG_TLS_TASK_NELEM > 0","int","tls_dtor_t" "task_tls_get_value","nuttx/tls.h","CONFIG_TLS_TASK_NELEM > 0","uintptr_t","int" @@ -348,6 +349,7 @@ "usleep","unistd.h","","int","useconds_t" "vasprintf","stdio.h","","int","FAR char **","FAR const IPTR char *","va_list" "versionsort","dirent.h","","int","FAR const struct dirent **","FAR const struct dirent **" +"vfork","unistd.h","!defined(CONFIG_BUILD_KERNEL) && defined(CONFIG_ARCH_HAVE_VFORK)","pid_t" "vfprintf","stdio.h","defined(CONFIG_FILE_STREAM)","int","FAR FILE *","FAR const IPTR char *","va_list" "vprintf","stdio.h","","int","FAR const IPTR char *","va_list" "vscanf","stdio.h","defined(CONFIG_FILE_STREAM)","int","FAR const IPTR char *","va_list" diff --git a/libs/libc/unistd/CMakeLists.txt b/libs/libc/unistd/CMakeLists.txt index eb4ca8e5e937d..2e074fd65d99f 100644 --- a/libs/libc/unistd/CMakeLists.txt +++ b/libs/libc/unistd/CMakeLists.txt @@ -102,7 +102,9 @@ if(NOT CONFIG_DISABLE_MOUNTPOINTS) list(APPEND SRCS lib_truncate.c lib_posix_fallocate.c) endif() -if(CONFIG_ARCH_HAVE_FORK) +if(CONFIG_ARCH_HAVE_TASK_FORK + OR CONFIG_ARCH_HAVE_VFORK + OR CONFIG_ARCH_HAVE_FORK) list(APPEND SRCS lib_fork.c) endif() diff --git a/libs/libc/unistd/Make.defs b/libs/libc/unistd/Make.defs index e3e5ed3a2148d..8cc5e26f57d5b 100644 --- a/libs/libc/unistd/Make.defs +++ b/libs/libc/unistd/Make.defs @@ -53,7 +53,7 @@ ifneq ($(CONFIG_DISABLE_MOUNTPOINTS),y) CSRCS += lib_truncate.c lib_posix_fallocate.c endif -ifeq ($(CONFIG_ARCH_HAVE_FORK),y) +ifneq ($(CONFIG_ARCH_HAVE_TASK_FORK)$(CONFIG_ARCH_HAVE_VFORK)$(CONFIG_ARCH_HAVE_FORK),) CSRCS += lib_fork.c endif diff --git a/libs/libc/unistd/lib_fork.c b/libs/libc/unistd/lib_fork.c index 53c2bfe0e9b2c..ebc9e2ea8b784 100644 --- a/libs/libc/unistd/lib_fork.c +++ b/libs/libc/unistd/lib_fork.c @@ -28,13 +28,15 @@ #include #include +#include #include #include #include #include #include -#if defined(CONFIG_ARCH_HAVE_FORK) +#if defined(CONFIG_ARCH_HAVE_TASK_FORK) || defined(CONFIG_ARCH_HAVE_VFORK) || \ + defined(CONFIG_ARCH_HAVE_FORK) /**************************************************************************** * Private Functions @@ -137,27 +139,30 @@ static void atfork_parent(void) ****************************************************************************/ /**************************************************************************** - * Name: fork + * Name: task_fork * * Description: - * The fork() function is a wrapper of up_fork() syscall + * Clone the calling task: the child shares the parent's memory, runs on + * a private copy of the parent's stack, and runs concurrently. Not POSIX. + * Wrapper of the up_task_fork() syscall. * * Returned Value: - * Upon successful completion, fork() returns 0 to the child process and - * returns the process ID of the child process to the parent process. - * Otherwise, -1 is returned to the parent, no child process is created, - * and errno is set to indicate the error. + * Upon successful completion, task_fork() returns 0 to the child and + * returns the process ID of the child to the parent. Otherwise, -1 is + * returned to the parent, no child is created, and errno is set to + * indicate the error. * ****************************************************************************/ -pid_t fork(void) +#ifdef CONFIG_ARCH_HAVE_TASK_FORK +pid_t task_fork(void) { pid_t pid; #ifdef CONFIG_PTHREAD_ATFORK atfork_prepare(); #endif - pid = up_fork(); + pid = up_task_fork(); #ifdef CONFIG_PTHREAD_ATFORK if (pid == 0) @@ -172,20 +177,23 @@ pid_t fork(void) return pid; } - -#if defined(CONFIG_SCHED_WAITPID) - -/**************************************************************************** - * Public Functions - ****************************************************************************/ +#endif /* CONFIG_ARCH_HAVE_TASK_FORK */ /**************************************************************************** * Name: vfork * * Description: - * The vfork() function is implemented based on fork() function, on - * vfork(), the parent task need to wait until the child task is performing - * exec or running finished. + * The vfork() function is equivalent to fork(), except that the behavior + * is undefined if the process created by vfork() either modifies any data + * other than a variable of type pid_t used to store the return value from + * vfork(), or returns from the function in which vfork() was called, or + * calls any other function before successfully calling _exit() or one of + * the exec family of functions. + * + * The child shares the parent's memory and the parent is suspended until + * the child _exit()s or exec()s. The suspension lives in the kernel, so + * vfork() does not depend on CONFIG_SCHED_WAITPID. Wrapper of the + * up_vfork() syscall. * * Returned Value: * Upon successful completion, vfork() returns 0 to the child process and @@ -195,16 +203,15 @@ pid_t fork(void) * ****************************************************************************/ +#ifdef CONFIG_ARCH_HAVE_VFORK pid_t vfork(void) { - int status = 0; - int ret; pid_t pid; #ifdef CONFIG_PTHREAD_ATFORK atfork_prepare(); #endif - pid = up_fork(); + pid = up_vfork(); #ifdef CONFIG_PTHREAD_ATFORK if (pid == 0) @@ -217,22 +224,61 @@ pid_t vfork(void) } #endif - if (pid != 0) - { - /* we are in parent task, and we need to wait the child task - * until running finished or performing exec - */ + return pid; +} +#endif /* CONFIG_ARCH_HAVE_VFORK */ - ret = waitpid(pid, &status, WNOWAIT); - if (ret < 0) - { - serr("ERROR: waitpid failed: %d\n", get_errno()); - } +/**************************************************************************** + * Name: fork + * + * Description: + * POSIX fork(). The child receives its own copy of the parent's memory, + * at the same virtual addresses. It may modify anything, call anything, + * return from the function that called fork(), and it runs concurrently + * with the parent. None of vfork()'s restrictions apply. + * + * Provided only where CONFIG_ARCH_HAVE_FORK is selected; elsewhere fork() + * is not declared at all, so calling it is a build error. + * CONFIG_FORK_IS_TASK_FORK aliases it to task_fork() for legacy code. + * Wrapper of the up_fork() syscall. + * + * Returned Value: + * Upon successful completion, fork() returns 0 to the child process and + * returns the process ID of the child process to the parent process. + * Otherwise, -1 is returned to the parent, no child process is created, + * and errno is set to indicate the error. + * + ****************************************************************************/ + +#if defined(CONFIG_ARCH_HAVE_FORK) +pid_t fork(void) +{ + pid_t pid; + +#ifdef CONFIG_PTHREAD_ATFORK + atfork_prepare(); +#endif + pid = up_fork(); + +#ifdef CONFIG_PTHREAD_ATFORK + if (pid == 0) + { + atfork_child(); + } + else + { + atfork_parent(); } +#endif return pid; } +#elif defined(CONFIG_FORK_IS_TASK_FORK) +pid_t fork(void) +{ + return task_fork(); +} +#endif -#endif /* CONFIG_SCHED_WAITPID */ - -#endif /* CONFIG_ARCH_HAVE_FORK */ +#endif /* CONFIG_ARCH_HAVE_TASK_FORK || CONFIG_ARCH_HAVE_VFORK || + * CONFIG_ARCH_HAVE_FORK */ diff --git a/sched/addrenv/addrenv.c b/sched/addrenv/addrenv.c index 9936a105c774c..32e6cba247c67 100644 --- a/sched/addrenv/addrenv.c +++ b/sched/addrenv/addrenv.c @@ -27,6 +27,7 @@ #include #include +#include #include #include @@ -292,6 +293,70 @@ int addrenv_join(FAR struct tcb_s *ptcb, FAR struct tcb_s *tcb) return OK; } +#ifdef CONFIG_ARCH_HAVE_FORK +/**************************************************************************** + * Name: addrenv_fork + * + * Description: + * Duplicate the parent process's address environment for a POSIX fork() + * child, and attach the duplicate to the child. + * + * This is the counterpart of addrenv_join(): where join gives the child + * the parent's memory, fork gives it a copy -- its own pages, holding a + * snapshot of the parent's contents, mapped at the same virtual addresses. + * Mapping at the same addresses is what lets the copy be exact: every + * pointer the parent held into its own memory remains valid in the child, + * including the pointers inside the copied heap's own metadata. + * + * The copy is eager -- there is no copy-on-write, because NuttX has no + * demand paging to build it on -- so forking a large process needs as much + * free memory as the process occupies, and fails with -ENOMEM if that is + * not available. That is the nature of the primitive on this class of + * system, not a defect of this implementation; spawn-heavy code should + * prefer posix_spawn() or vfork(). + * + * Input Parameters: + * ptcb - The tcb of the parent process + * tcb - The tcb of the child process + * + * Returned Value: + * This is a NuttX internal function so it follows the convention that + * 0 (OK) is returned on success and a negated errno is returned on + * failure. + * + ****************************************************************************/ + +int addrenv_fork(FAR struct tcb_s *ptcb, FAR struct tcb_s *tcb) +{ + FAR struct addrenv_s *addrenv; + int ret; + + DEBUGASSERT(ptcb->addrenv_own != NULL); + + addrenv = addrenv_allocate(); + if (addrenv == NULL) + { + return -ENOMEM; + } + + /* Duplicate the parent's regions into freshly allocated pages, mapped at + * the same virtual addresses. + */ + + ret = up_addrenv_fork(&ptcb->addrenv_own->addrenv, &addrenv->addrenv); + if (ret < 0) + { + berr("ERROR: up_addrenv_fork failed: %d\n", ret); + addrenv_drop(addrenv, false); + return ret; + } + + /* Hand the reference taken by addrenv_allocate() to the child */ + + return addrenv_attach(tcb, addrenv); +} +#endif /* CONFIG_ARCH_HAVE_FORK */ + /**************************************************************************** * Name: addrenv_leave * diff --git a/sched/sched/sched_releasetcb.c b/sched/sched/sched_releasetcb.c index 96b3e736e79cd..28cd8572c76c9 100644 --- a/sched/sched/sched_releasetcb.c +++ b/sched/sched/sched_releasetcb.c @@ -174,6 +174,15 @@ int nxsched_release_tcb(FAR struct tcb_s *tcb, uint8_t ttype) nxtask_joindestroy(tcb); #endif +#ifdef CONFIG_ARCH_HAVE_VFORK + /* Release a suspended vfork() parent here, the last point in the + * child's life: the child has stopped touching a borrowed stack, and + * exec_swap() has already handed its pid to any program it loaded. + */ + + nxtask_vfork_resume(tcb); +#endif + /* And, finally, release the TCB itself */ if (tcb->flags & TCB_FLAG_FREE_TCB) diff --git a/sched/task/CMakeLists.txt b/sched/task/CMakeLists.txt index fdc19fdc589b1..bcbb24fac7a91 100644 --- a/sched/task/CMakeLists.txt +++ b/sched/task/CMakeLists.txt @@ -46,7 +46,9 @@ if(CONFIG_SCHED_HAVE_PARENT) list(APPEND SRCS task_getppid.c task_reparent.c) endif() -if(CONFIG_ARCH_HAVE_FORK) +if(CONFIG_ARCH_HAVE_TASK_FORK + OR CONFIG_ARCH_HAVE_VFORK + OR CONFIG_ARCH_HAVE_FORK) list(APPEND SRCS task_fork.c) endif() diff --git a/sched/task/Make.defs b/sched/task/Make.defs index 1fd24403b51ae..283e30f8dfd2d 100644 --- a/sched/task/Make.defs +++ b/sched/task/Make.defs @@ -30,7 +30,7 @@ ifeq ($(CONFIG_SCHED_HAVE_PARENT),y) CSRCS += task_getppid.c task_reparent.c endif -ifeq ($(CONFIG_ARCH_HAVE_FORK),y) +ifneq ($(CONFIG_ARCH_HAVE_TASK_FORK)$(CONFIG_ARCH_HAVE_VFORK)$(CONFIG_ARCH_HAVE_FORK),) CSRCS += task_fork.c endif diff --git a/sched/task/task_exit.c b/sched/task/task_exit.c index e79c4f812504f..ab898538ad6ec 100644 --- a/sched/task/task_exit.c +++ b/sched/task/task_exit.c @@ -157,5 +157,16 @@ int nxtask_exit(void) rtcb->lockcount--; +#ifndef CONFIG_SMP + /* Publish anything woken while the TCB was being released. lockcount was + * raised directly rather than through sched_lock(), so the matching + * decrement above does not merge g_pendingtasks the way sched_unlock() + * would, and a vfork() parent released by nxsched_release_tcb() would be + * stranded there. SMP has no pending list to merge. + */ + + nxsched_merge_pending(); +#endif + return ret; } diff --git a/sched/task/task_fork.c b/sched/task/task_fork.c index 5e2b133744264..10e7382aa9567 100644 --- a/sched/task/task_fork.c +++ b/sched/task/task_fork.c @@ -34,7 +34,10 @@ #include #include +#include +#include #include +#include #include "sched/sched.h" #include "environ/environ.h" @@ -42,9 +45,177 @@ #include "task/task.h" #include "tls/tls.h" -/* fork() requires architecture-specific support as well as waipid(). */ +/* This file is the common core of task_fork(), vfork() and fork(); it is + * built if the architecture can provide any one of them. + */ -#ifdef CONFIG_ARCH_HAVE_FORK +#if defined(CONFIG_ARCH_HAVE_TASK_FORK) || defined(CONFIG_ARCH_HAVE_VFORK) || \ + defined(CONFIG_ARCH_HAVE_FORK) + +/**************************************************************************** + * Pre-processor Definitions + ****************************************************************************/ + +#ifdef CONFIG_ARCH_VFORK_STACK_BORROW +/* Written at the boundary between the part of the parent's stack lent to the + * vfork() child and the reserve kept for the parent's own remaining frames. + */ + +# define VFORK_STACK_CANARY ((uintptr_t)0xdead0f0e0f0e0f0eull) +#endif + +/**************************************************************************** + * Private Function Prototypes + ****************************************************************************/ + +#if defined(CONFIG_ARCH_ADDRENV) && defined(CONFIG_ARCH_HAVE_FORK) +static void fork_inherit_stack(FAR struct tcb_s *parent, + FAR struct tcb_s *child); +static void fork_inherit_tls(FAR struct tcb_s *child); +static void fork_restore_parent_env(void); +#endif + +/**************************************************************************** + * Private Functions + ****************************************************************************/ + +#ifdef CONFIG_ARCH_VFORK_STACK_BORROW +/**************************************************************************** + * Name: vfork_borrow_stack + * + * Description: + * Give the vfork() child the unused part of the parent's stack instead of + * a stack of its own, which is what an architecture whose frames hold + * absolute stack addresses needs. + * + * The child gets [stack_base_ptr, parent_sp - RESERVE). The reserve is + * headroom for the parent's remaining frames before it blocks; a canary at + * the boundary turns an undersized reserve into a loud failure. + * + * Input Parameters: + * parent - The parent task's TCB + * child - The child task's TCB + * parent_sp - The parent's stack pointer at the point vfork() was called + * + * Returned Value: + * Zero (OK) on success; a negated errno value on failure. + * + ****************************************************************************/ + +static int vfork_borrow_stack(FAR struct tcb_s *parent, + FAR struct tcb_s *child, + uintptr_t parent_sp) +{ + uintptr_t base = (uintptr_t)parent->stack_base_ptr; + uintptr_t top; + + DEBUGASSERT(parent_sp > base); + + if (parent_sp <= base + CONFIG_ARCH_VFORK_STACK_RESERVE) + { + serr("ERROR: Not enough parent stack left to borrow\n"); + return -ENOMEM; + } + + top = parent_sp - CONFIG_ARCH_VFORK_STACK_RESERVE; + + /* The child does not own this memory: stack_alloc_ptr is recorded so that + * stack checking and backtraces work, but TCB_FLAG_FREE_STACK is + * deliberately left clear so that up_release_stack() will not free the + * parent's allocation out from under it. + */ + + child->stack_alloc_ptr = parent->stack_alloc_ptr; + child->stack_base_ptr = parent->stack_base_ptr; + child->adj_stack_size = top - base; + child->flags &= ~TCB_FLAG_FREE_STACK; + + *(FAR uintptr_t *)top = VFORK_STACK_CANARY; + + return OK; +} +#endif /* CONFIG_ARCH_VFORK_STACK_BORROW */ + +#if defined(CONFIG_ARCH_ADDRENV) && defined(CONFIG_ARCH_HAVE_FORK) +/**************************************************************************** + * Name: fork_inherit_stack + * + * Description: + * Give the fork() child the parent's stack at the parent's virtual + * address rather than a relocated copy. The child's address environment + * is a duplicate, so the parent's stack is already there -- same contents, + * same address, its own pages -- and nothing needs allocating or copying. + * + * A relocated stack would break plain C: a pointer to a local taken + * before the fork would name the parent's copy, not the child's live + * object. + * + * TCB_FLAG_FREE_STACK is left clear: the stack belongs to the duplicated + * image and is released with it, so up_release_stack() must not free it. + * + * Input Parameters: + * parent - The parent task's TCB + * child - The child task's TCB + * + ****************************************************************************/ + +static void fork_inherit_stack(FAR struct tcb_s *parent, + FAR struct tcb_s *child) +{ + child->stack_alloc_ptr = parent->stack_alloc_ptr; + child->stack_base_ptr = parent->stack_base_ptr; + child->adj_stack_size = parent->adj_stack_size; + child->flags &= ~TCB_FLAG_FREE_STACK; +} + +/**************************************************************************** + * Name: fork_inherit_tls + * + * Description: + * Retarget the thread-local storage the fork() child inherited. + * + * tls_dup_info() cannot be used: it carves a fresh TLS block off the + * stack, which on an inherited stack would carve a second one and shift + * stack_base_ptr away from the parent's. The child's copy is already in + * place, so only the fields naming the task itself need correcting. + * + * The write lands in user memory at an address the parent also occupies, + * so the child's address environment must be current for it -- otherwise + * the parent's own TLS is what gets modified. + * + * Input Parameters: + * child - The child task's TCB + * + * Returned Value: + * Zero (OK) on success; a negated errno value on failure. + * + ****************************************************************************/ + +static void fork_inherit_tls(FAR struct tcb_s *child) +{ + FAR struct tls_info_s *info = (FAR struct tls_info_s *) + child->stack_alloc_ptr; + + info->tl_task = child->group->tg_info; + info->tl_tid = child->pid; +} + +/**************************************************************************** + * Name: fork_restore_parent_env + * + * Description: + * Undo the addrenv_select() that nxtask_setup_fork() made on the child's + * behalf, putting the caller back in its own address environment. The + * environment to go back to does not have to be remembered: the caller is + * the parent, and what was current before was the parent's own. + * + ****************************************************************************/ + +static void fork_restore_parent_env(void) +{ + addrenv_restore(this_task()->addrenv_own); +} +#endif /* CONFIG_ARCH_ADDRENV && CONFIG_ARCH_HAVE_FORK */ /**************************************************************************** * Public Functions @@ -54,37 +225,24 @@ * Name: nxtask_setup_fork * * Description: - * The fork() function has the same effect as posix fork(), except that the - * behavior is undefined if the process created by fork() either modifies - * any data other than a variable of type pid_t used to store the return - * value from fork(), or returns from the function in which fork() was - * called, or calls any other function before successfully calling _exit() - * or one of the exec family of functions. - * - * This function provides one step in the overall fork() sequence: It - * Allocates and initializes the child task's TCB. The overall sequence - * is: - * - * 1) User code calls fork(). fork() is provided in - * architecture-specific code. - * 2) fork()and calls nxtask_setup_fork(). - * 3) nxtask_setup_fork() allocates and configures the child task's TCB. - * This consists of: - * - Allocation of the child task's TCB. - * - Initialization of file descriptors and streams - * - Configuration of environment variables - * - Allocate and initialize the stack - * - Setup the input parameters for the task. - * - Initialization of the TCB (including call to up_initial_state()) - * 4) up_fork() provides any additional operating context. up_fork must: - * - Initialize special values in any CPU registers that were not - * already configured by up_initial_state() - * 5) up_fork() then calls nxtask_start_fork() - * 6) nxtask_start_fork() then executes the child thread. + * Allocate and initialize the child task's TCB. This is one step in the + * sequence common to task_fork(), vfork() and fork(); see the comment + * above the prototype in include/nuttx/sched.h for the whole sequence, and + * include/nuttx/fork.h for what the three primitives mean. + * + * Exactly two things depend on `type': + * + * - the address environment: task_fork() and vfork() join the parent's, + * fork() duplicates it. + * - the stack: a task_fork() child gets its own, which the architecture + * code fills with a relocated copy; a fork() child inherits the parent's + * address (fork_inherit_stack()); a vfork() child borrows it under + * CONFIG_ARCH_VFORK_STACK_BORROW (vfork_borrow_stack()). * * Input Parameters: - * retaddr - Return address - * argsize - Location to return the argument size + * retaddr - Address at which the child resumes + * type - One of the FORK_TYPE_* constants + * parent_sp - The caller's stack pointer, used when borrowing a stack * * Returned Value: * Upon successful completion, nxtask_setup_fork() returns a pointer to @@ -93,7 +251,8 @@ * ****************************************************************************/ -FAR struct tcb_s *nxtask_setup_fork(start_t retaddr) +FAR struct tcb_s *nxtask_setup_fork(start_t retaddr, int type, + uintptr_t parent_sp) { FAR struct tcb_s *ptcb = this_task(); FAR struct tcb_s *parent; @@ -105,6 +264,10 @@ FAR struct tcb_s *nxtask_setup_fork(start_t retaddr) int ret; DEBUGASSERT(retaddr != NULL); + DEBUGASSERT(type == FORK_TYPE_TASK || type == FORK_TYPE_VFORK || + type == FORK_TYPE_FORK); + + UNUSED(parent_sp); /* Get the type of the fork'ed task (kernel or user) */ @@ -160,16 +323,80 @@ FAR struct tcb_s *nxtask_setup_fork(start_t retaddr) } #if defined(CONFIG_ARCH_ADDRENV) - /* Join the parent address environment */ - if (ttype != TCB_FLAG_TTYPE_KERNEL) { - ret = addrenv_join(parent, child); + if (type != FORK_TYPE_FORK) + { + /* task_fork() and vfork(): join the parent address environment, + * exactly as pthread_create() does. The child shares .data, .bss + * and the heap. + */ + + ret = addrenv_join(parent, child); + } +#ifdef CONFIG_ARCH_HAVE_FORK + else + { + /* POSIX fork(): duplicate the parent's address environment now, + * before anything else is set up. The duplicate holds a copy of + * the parent's contents -- including its stack -- at the parent's + * virtual addresses, which is what lets the child go on to inherit + * the stack address rather than be given a relocated copy. See + * fork_inherit_stack(). + */ + + ret = addrenv_fork(parent, child); + if (ret >= 0) + { + /* Make the child's address environment current for the rest of + * the setup, and for the architecture code that runs after it. + * + * From here on, everything written on the child's behalf + * has to land in the child's image rather than the parent's, + * because + * the two occupy the same virtual addresses: its thread-local + * storage, and -- on architectures that keep the register save + * area on the user stack rather than on a kernel stack -- the + * register context the child is resumed from. Writing those + * under the parent's environment corrupts the parent and + * leaves the child reading whatever the snapshot happened to + * contain. + * + * Reads are unaffected: everything the setup reads from the + * parent -- environ, the argument vector -- is legible at the + * same address in the child, precisely because it is a copy. + * + * nxtask_start_fork() puts the parent's environment back. + */ + + FAR struct addrenv_s *oldenv; + + ret = addrenv_select(child->addrenv_own, &oldenv); + } + } +#else + /* An address environment without ARCH_HAVE_FORK -- a protected build + * over an MMU, for instance. There is an address environment to join, + * but no POSIX fork() to duplicate it for, so the branch above is not + * compiled and `type' can never be FORK_TYPE_FORK here. + */ + + DEBUGASSERT(type != FORK_TYPE_FORK); +#endif + if (ret < 0) { goto errout_with_tcb; } } +#else + /* Without address environments there is only one address space, so + * everything except the stack is shared no matter which primitive was + * called. POSIX fork() cannot be provided at all, and CONFIG_ARCH_HAVE_ + * FORK is not selected, so `type' can never be FORK_TYPE_FORK here. + */ + + DEBUGASSERT(type != FORK_TYPE_FORK); #endif /* Duplicate the parent tasks environment */ @@ -193,12 +420,36 @@ FAR struct tcb_s *nxtask_setup_fork(start_t retaddr) argv = nxsched_get_stackargs(parent); nxtask_setup_name(child, argv[0]); - /* Allocate the stack for the TCB */ + /* Allocate the stack for the TCB -- or inherit the parent's, or borrow + * it + */ - stack_size = (uintptr_t)ptcb->stack_base_ptr - - (uintptr_t)ptcb->stack_alloc_ptr + ptcb->adj_stack_size; +#if defined(CONFIG_ARCH_ADDRENV) && defined(CONFIG_ARCH_HAVE_FORK) + if (type == FORK_TYPE_FORK && ttype != TCB_FLAG_TTYPE_KERNEL) + { + /* The child's copy of the parent's stack is already in place, at the + * parent's address, courtesy of the duplication above. + */ + + fork_inherit_stack(parent, child); + ret = OK; + } + else +#endif +#ifdef CONFIG_ARCH_VFORK_STACK_BORROW + if (type == FORK_TYPE_VFORK) + { + ret = vfork_borrow_stack(parent, child, parent_sp); + } + else +#endif + { + stack_size = (uintptr_t)ptcb->stack_base_ptr - + (uintptr_t)ptcb->stack_alloc_ptr + ptcb->adj_stack_size; + + ret = up_create_stack(child, stack_size, ttype); + } - ret = up_create_stack(child, stack_size, ttype); if (ret < OK) { goto errout_with_tcb; @@ -235,20 +486,35 @@ FAR struct tcb_s *nxtask_setup_fork(start_t retaddr) goto errout_with_tcb; } - /* Setup thread local storage */ - - ret = tls_dup_info(child, parent); - if (ret < OK) + /* Set up thread local storage and the argument vector. + * + * A fork() child that inherited its stack already has both, byte for + * byte, at the addresses the parent has them at -- they came across with + * the rest of the image. Re-creating them would carve fresh frames off a + * stack that already contains them, moving stack_base_ptr away from the + * parent's and undoing the inheritance. Only the TLS fields that name + * the task itself need correcting. + */ + +#if defined(CONFIG_ARCH_ADDRENV) && defined(CONFIG_ARCH_HAVE_FORK) + if (type == FORK_TYPE_FORK && ttype != TCB_FLAG_TTYPE_KERNEL) { - goto errout_with_tcb; + fork_inherit_tls(child); } - - /* Setup to pass parameters to the new task */ - - ret = nxtask_setup_stackargs(child, argv[0], &argv[1]); - if (ret < OK) + else +#endif { - goto errout_with_tcb; + ret = tls_dup_info(child, parent); + if (ret < OK) + { + goto errout_with_tcb; + } + + ret = nxtask_setup_stackargs(child, argv[0], &argv[1]); + if (ret < OK) + { + goto errout_with_tcb; + } } /* Now we have enough in place that we can join the group */ @@ -258,6 +524,18 @@ FAR struct tcb_s *nxtask_setup_fork(start_t retaddr) return child; errout_with_tcb: +#if defined(CONFIG_ARCH_ADDRENV) && defined(CONFIG_ARCH_HAVE_FORK) + /* Get back into the parent's address environment before unwinding. If the + * duplication above never happened this is the environment we are already + * in, and addrenv_restore() is then a no-op. + */ + + if (type == FORK_TYPE_FORK && ttype != TCB_FLAG_TTYPE_KERNEL) + { + fork_restore_parent_env(); + } +#endif + nxsched_release_tcb((FAR struct tcb_s *)child, ttype); errout: set_errno(-ret); @@ -268,58 +546,51 @@ FAR struct tcb_s *nxtask_setup_fork(start_t retaddr) * Name: nxtask_start_fork * * Description: - * The fork() function has the same effect as fork(), except that the - * behavior is undefined if the process created by fork() either modifies - * any data other than a variable of type pid_t used to store the return - * value from fork(), or returns from the function in which fork() was - * called, or calls any other function before successfully calling _exit() - * or one of the exec family of functions. - * - * This function provides one step in the overall fork() sequence: It - * starts execution of the previously initialized TCB. The overall - * sequence is: - * - * 1) User code calls fork() - * 2) Architecture-specific code provides fork()and calls - * nxtask_setup_fork(). - * 3) nxtask_setup_fork() allocates and configures the child task's TCB. - * This consists of: - * - Allocation of the child task's TCB. - * - Initialization of file descriptors and streams - * - Configuration of environment variables - * - Allocate and initialize the stack - * - Setup the input parameters for the task. - * - Initialization of the TCB (including call to up_initial_state()) - * 4) fork() provides any additional operating context. fork must: - * - Initialize special values in any CPU registers that were not - * already configured by up_initial_state() - * 5) fork() then calls nxtask_start_fork() - * 6) nxtask_start_fork() then executes the child thread. + * The last step of all three primitives: finish the child and run it. + * The architecture-specific code calls this once it has built the child's + * register context and stack. + * + * For vfork() this additionally suspends the caller. * * Input Parameters: - * child - The tcb_s struct instance that created by - * nxtask_setup_fork() method - * wait_child - whether need to wait until the child is running finished + * child - The tcb_s struct instance created by nxtask_setup_fork() + * type - One of the FORK_TYPE_* constants * * Returned Value: - * Upon successful completion, fork() returns 0 to the child process and - * returns the process ID of the child process to the parent process. - * Otherwise, -1 is returned to the parent, no child process is created, - * and errno is set to indicate the error. + * The process ID of the child, or ERROR on failure. * ****************************************************************************/ -pid_t nxtask_start_fork(FAR struct tcb_s *child) +pid_t nxtask_start_fork(FAR struct tcb_s *child, int type) { pid_t pid; - sinfo("Starting Child TCB=%p\n", child); + sinfo("Starting Child TCB=%p type=%d\n", child, type); DEBUGASSERT(child); +#if defined(CONFIG_ARCH_ADDRENV) && defined(CONFIG_ARCH_HAVE_FORK) + /* The architecture code has finished writing the child's image, so put the + * parent back in its own address environment. See nxtask_setup_fork(). + */ + + if (type == FORK_TYPE_FORK && + (child->flags & TCB_FLAG_TTYPE_MASK) != TCB_FLAG_TTYPE_KERNEL) + { + fork_restore_parent_env(); + } +#endif + /* Get the assigned pid before we start the task */ pid = child->pid; +#ifdef CONFIG_ARCH_HAVE_VFORK + if (type == FORK_TYPE_VFORK) + { + return nxtask_start_vfork(child); + } +#endif + /* Activate the task */ nxtask_activate(child); @@ -327,6 +598,119 @@ pid_t nxtask_start_fork(FAR struct tcb_s *child) return pid; } +#ifdef CONFIG_ARCH_HAVE_VFORK +/**************************************************************************** + * Name: nxtask_start_vfork + * + * Description: + * Start execution of a vfork() child and suspend the caller until the + * child calls _exit() or one of the exec family of functions. + * + * The suspension lives here, in the kernel primitive, rather than in a + * libc waitpid() as it once did. Two things follow from that. The parent + * is released when the child's TCB is torn down (see + * nxtask_vfork_resume()), which for an exec()ing child is immediately + * after exec_swap() has handed the child's pid to the program it loaded -- + * so the parent resumes at exec(), holding a pid that names the running + * program, as POSIX requires. And vfork() no longer depends on + * CONFIG_SCHED_WAITPID. + * + * Input Parameters: + * child - The tcb_s struct instance created by nxtask_setup_fork() + * + * Returned Value: + * The process ID of the child. + * + ****************************************************************************/ + +pid_t nxtask_start_vfork(FAR struct tcb_s *child) +{ + struct vfork_s vfork; + pid_t pid; + int ret; +#ifdef CONFIG_ARCH_VFORK_STACK_BORROW + uintptr_t canary = (uintptr_t)child->stack_base_ptr + + child->adj_stack_size; +#endif + + sinfo("Starting vfork Child TCB=%p\n", child); + DEBUGASSERT(child); + + /* The rendezvous lives in this frame. We are about to block in it and + * will not leave until the child has posted, so it outlives every use. + */ + + nxsem_init(&vfork.sem, 0, 0); + vfork.released = false; + child->vfork_rel = &vfork; + + pid = child->pid; + + nxtask_activate(child); + + /* Wait for the child to _exit() or exec(). This is not a cancellation + * point and must not be interrupted by a signal: the child may be running + * on our stack, so returning early would corrupt it. + */ + + do + { + ret = nxsem_wait_uninterruptible(&vfork.sem); + } + while (ret == -EINTR); + + nxsem_destroy(&vfork.sem); + +#ifdef CONFIG_ARCH_VFORK_STACK_BORROW + /* The child ran on the unused part of our stack, below a reserve that our + * own remaining frames had to fit into. Check the canary that + * vfork_borrow_stack() left at the boundary: if the reserve was too small + * then we and the child have been using the same memory, and the only + * honest thing to do is say so rather than return into a mangled frame. + */ + + if (*(FAR uintptr_t *)canary != VFORK_STACK_CANARY) + { + serr("ERROR: vfork() child overran CONFIG_ARCH_VFORK_STACK_RESERVE\n"); + PANIC(); + } +#endif + + return pid; +} + +/**************************************************************************** + * Name: nxtask_vfork_resume + * + * Description: + * Release the vfork() parent suspended on this child, if there is one. + * + * Called from nxsched_release_tcb(), the last point in the child's life: + * any earlier and the parent could resume while the child was still on a + * borrowed stack. nxtask_abort_fork() reaches it too, so a fork that + * fails after the rendezvous also releases the parent. + * + * Input Parameters: + * child - The TCB being torn down + * + * Returned Value: + * None + * + ****************************************************************************/ + +void nxtask_vfork_resume(FAR struct tcb_s *child) +{ + FAR struct vfork_s *vfork = child->vfork_rel; + + if (vfork != NULL && !vfork->released) + { + vfork->released = true; + child->vfork_rel = NULL; + nxsem_post(&vfork->sem); + } +} +#endif /* CONFIG_ARCH_HAVE_VFORK */ + /**************************************************************************** * Name: nxtask_abort_fork * @@ -340,6 +724,20 @@ pid_t nxtask_start_fork(FAR struct tcb_s *child) void nxtask_abort_fork(FAR struct tcb_s *child, int errcode) { +#if defined(CONFIG_ARCH_ADDRENV) && defined(CONFIG_ARCH_HAVE_FORK) + /* A child holding an address environment of its own, rather than a + * reference to the caller's, is a fork() child, and nxtask_setup_fork() + * left that environment selected. Get back into the parent's before + * unwinding. See nxtask_setup_fork(). + */ + + if (child->addrenv_own != NULL && + child->addrenv_own != this_task()->addrenv_own) + { + fork_restore_parent_env(); + } +#endif + /* The TCB was added to the active task list by nxtask_setup_scheduler() */ dq_rem((FAR dq_entry_t *)child, list_inactivetasks()); @@ -350,4 +748,5 @@ void nxtask_abort_fork(FAR struct tcb_s *child, int errcode) set_errno(errcode); } -#endif /* CONFIG_ARCH_HAVE_FORK */ +#endif /* CONFIG_ARCH_HAVE_TASK_FORK || CONFIG_ARCH_HAVE_VFORK || + * CONFIG_ARCH_HAVE_FORK */ diff --git a/syscall/syscall.csv b/syscall/syscall.csv index 4622a0910b817..ad5a08d97d17f 100644 --- a/syscall/syscall.csv +++ b/syscall/syscall.csv @@ -199,6 +199,8 @@ "unlink","unistd.h","!defined(CONFIG_DISABLE_MOUNTPOINT)","int","FAR const char *" "unsetenv","stdlib.h","!defined(CONFIG_DISABLE_ENVIRON)","int","FAR const char *" "up_fork","nuttx/arch.h","defined(CONFIG_ARCH_HAVE_FORK)","pid_t" +"up_task_fork","nuttx/arch.h","defined(CONFIG_ARCH_HAVE_TASK_FORK)","pid_t" +"up_vfork","nuttx/arch.h","defined(CONFIG_ARCH_HAVE_VFORK)","pid_t" "utimens","sys/stat.h","","int","FAR const char *","const struct timespec [2]|FAR const struct timespec *" "wait","sys/wait.h","defined(CONFIG_SCHED_WAITPID) && defined(CONFIG_SCHED_HAVE_PARENT)","pid_t","FAR int *" "waitid","sys/wait.h","defined(CONFIG_SCHED_WAITPID) && defined(CONFIG_SCHED_HAVE_PARENT)","int","idtype_t","id_t"," FAR siginfo_t *","int" From ba5a7a634b8b1b5114f9adc39aa8110b81558a0e Mon Sep 17 00:00:00 2001 From: Marco Casaroli Date: Tue, 28 Jul 2026 00:30:48 +0200 Subject: [PATCH 2/2] Documentation: describe the fork()/vfork()/task_fork() split Documentation/guides/fork_vfork_migration.rst is new. It says what changed and why, gives the three primitives as a table, states plainly what breaks, and answers "which replacement do I want?" from the reader's own reason for having called fork() -- posix_spawn() or vfork() to run a program, pthread_create() or task_fork() for a second flow of control that shares memory, fork() itself for an independent copy, FORK_IS_TASK_FORK for code that cannot be changed today. It also documents the seven configuration symbols, what an architecture has to implement to gain real fork(), and the one visible consequence of moving the vfork() suspension into the kernel: a waitpid() after a child that _exit()s can only report status where CONFIG_SCHED_CHILD_STATUS is enabled. reference/user/01_task_control.rst gains entries for fork() and task_fork() and rewrites the one for vfork(), which described NuttX's limitations rather than the interface's contract. standards/posix.rst moves fork() from "No" to "Cond." and vfork() from "Yes" to "Cond.", both being conditional on the configuration now. implementation/memory_configurations.rst no longer lists fork() as unimplementable in the presence of address environments, which was the whole point of that section's wish list. Three long-standing typos in that file are corrected while touching it, since codespell checks the whole of any file a patch modifies. Signed-off-by: Marco Casaroli Assisted-by: Claude Opus 5 (1M context) --- Documentation/guides/fork_vfork_migration.rst | 223 ++++++++++++++++++ Documentation/guides/index.rst | 1 + .../implementation/memory_configurations.rst | 21 +- .../reference/user/01_task_control.rst | 109 ++++++++- Documentation/standards/posix.rst | 4 +- 5 files changed, 339 insertions(+), 19 deletions(-) create mode 100644 Documentation/guides/fork_vfork_migration.rst diff --git a/Documentation/guides/fork_vfork_migration.rst b/Documentation/guides/fork_vfork_migration.rst new file mode 100644 index 0000000000000..336d20c44a3ef --- /dev/null +++ b/Documentation/guides/fork_vfork_migration.rst @@ -0,0 +1,223 @@ +========================================= +Migrating to separate ``fork``/``vfork`` +========================================= + +What changed +============ + +NuttX used to implement ``fork()`` and ``vfork()`` as the same function. Both +were thin libc wrappers around a single ``up_fork()`` syscall; ``vfork()`` +differed only by a trailing ``waitpid()``. Underneath, the child joined the +parent's address environment -- the same ``addrenv_join()`` that +``pthread_create()`` uses -- and got a private *copy of the stack*. So the +child shared ``.data``, ``.bss`` and the heap with its parent, and ran +concurrently with it. + +That is not ``fork()``. It is ``vfork()``-with-a-private-stack published under +``fork()``'s name. The history says so plainly: today's ``fork()`` is NuttX's +old ``vfork()``, renamed in 2023 without any change of behaviour. And the +consequence was silent: a program written against POSIX ``fork()`` compiled and +ran, and its child's writes quietly landed in the parent's variables. + +There are now three distinct primitives: + +.. list-table:: + :header-rows: 1 + :widths: 14 30 26 30 + + * - API + - Memory + - Parent + - Availability + * - ``fork()`` + - child gets **its own copy** at the same virtual addresses + - runs concurrently + - ``CONFIG_ARCH_HAVE_FORK`` -- only where an address environment can be + duplicated + * - ``vfork()`` + - child **shares** the parent's memory + - **suspended** until the child ``_exit()``\ s or ``exec()``\ s + - ``CONFIG_ARCH_HAVE_VFORK`` -- everywhere, MMU or not + * - ``task_fork()`` + - child shares memory, private stack copy + - runs concurrently + - ``CONFIG_ARCH_HAVE_TASK_FORK`` -- exactly where ``fork()`` existed + before + +``task_fork()`` is the old behaviour under an honest name. Nothing was lost. + +.. note:: + + **No architecture implements** ``up_addrenv_fork()`` **yet**, so as of this + change ``CONFIG_ARCH_HAVE_FORK`` is unset everywhere and ``fork()`` is not + available on any target. It returns architecture by architecture as that + hook lands. Until then, ``vfork()`` and ``task_fork()`` are what a NuttX + configuration offers, and ``CONFIG_FORK_IS_TASK_FORK`` keeps the spelling + ``fork()`` working for code that cannot be changed. + +This is a breaking change +========================= + +Two things break, and they break loudly rather than quietly: + +**Code calling** ``fork()`` **on a target without a duplicable address +environment no longer builds.** ``fork()`` is not declared in ``unistd.h`` +there, so you get a compile error naming the function. That is the intended +outcome: a build error is strictly better than the silent wrongness it +replaces. Today that is every target. + +**Code calling** ``fork()`` **on a target that does have real** ``fork()`` +**changes behaviour** -- from sharing to copying. Code that (perhaps +unknowingly) relied on the sharing will now see the parent and child diverge. + +Which replacement do I want? +============================ + +Answer the question "why did I call ``fork()``?". + +*I want the child to run a different program.* + Use :c:func:`posix_spawn` or ``task_spawn()``. This is the single most + common reason to call ``fork()``, NuttX has always provided a better answer + for it, and that answer does not have the pid discontinuity that + ``fork()``\ +\ ``exec()`` has. If you must keep the two-step idiom, use + ``vfork()`` + ``exec*()``: that is exactly what ``vfork()`` is for, and it + is available on every target. + +*I want a second flow of control that shares my memory.* + Use ``pthread_create()``. That is the same memory relationship, spelled + clearly, with a normal entry point instead of a function that returns twice. + If you specifically need the returns-twice shape -- for example you are + porting code and do not want to restructure it -- use ``task_fork()``. It + is a rename, not a rewrite: + + .. code-block:: c + + #include + + pid = task_fork(); /* was: pid = fork(); */ + +*I want a genuinely independent copy of this process.* + Keep calling ``fork()``, and make sure your configuration selects + ``CONFIG_ARCH_HAVE_FORK``. Be aware there is no copy-on-write: the copy is + eager, so forking a large process needs as much free memory as the process + occupies and fails with ``ENOMEM`` otherwise. + +*I cannot change the code right now.* + Set ``CONFIG_FORK_IS_TASK_FORK=y``. This aliases ``fork()`` back to + ``task_fork()``, restoring the previous behaviour **exactly** -- same + sharing, same concurrency, no new suspension. It is available on precisely + the configurations that had ``fork()`` before, and it depends on + ``!ARCH_HAVE_FORK``: on a target that can provide real ``fork()``, aliasing + it back to sharing would reintroduce the very ambiguity this change removes, + so sharing-dependent callers there must be edited. + +Configuration symbols +===================== + +``CONFIG_ARCH_HAVE_TASK_FORK`` + Hidden. The architecture can clone the calling task with a copied stack. + Inherits exactly the ``select`` lines that ``ARCH_HAVE_FORK`` used to have, + so no configuration that had ``fork()`` loses the machinery. + +``CONFIG_ARCH_HAVE_VFORK`` + Hidden. The architecture can implement POSIX ``vfork()``. + +``CONFIG_ARCH_VFORK_STACK_BORROW`` / ``CONFIG_ARCH_VFORK_STACK_RESERVE`` + Hidden / tunable. The ``vfork()`` child borrows the parent's stack rather + than running on a relocated copy of it. Required on architectures whose + stack frames hold absolute stack addresses -- the windowed ABI of Xtensa, + whose register-window save areas hold each frame's spilled stack pointer, so + that a relocated copy unwinds onto the parent's stack. The reserve is the + headroom withheld from the child for the parent's own remaining frames; a + canary at the boundary turns an undersized reserve into a loud failure. + +``CONFIG_ARCH_HAVE_ADDRENV_FORK`` + Hidden. The architecture implements ``up_addrenv_fork()``, which duplicates + an address environment into freshly allocated pages mapped at the same + virtual addresses. + +``CONFIG_ARCH_HAVE_FORK`` + Hidden, derived: ``ARCH_ADDRENV && ARCH_HAVE_ADDRENV_FORK``. It no longer + means "``fork()`` exists"; it means "this configuration can provide POSIX + ``fork()`` semantics". + +``CONFIG_FORK_IS_TASK_FORK`` + Visible, default ``n``. The legacy alias described above. + +Notes for architecture maintainers +================================== + +The register/stack snapshot machinery is common to all three primitives. Each +architecture exposes three entry points -- ``up_task_fork()``, ``up_vfork()`` +and ``up_fork()`` -- which share one snapshot sequence and differ only in a +``FORK_TYPE_*`` selector (see ``include/nuttx/fork.h``) handed to +``nxtask_setup_fork()``. That is where the memory semantics are decided: +``addrenv_join()`` for ``task_fork()`` and ``vfork()``, ``addrenv_fork()`` for +``fork()``. + +Adding real ``fork()`` to an architecture +----------------------------------------- + +Two things are needed, and the second is the one that is easy to miss. + +**Implement** ``up_addrenv_fork()``. It duplicates an address environment: +allocate fresh pages, copy the parent's contents into them, and map them at the +*same* virtual addresses. ``up_addrenv_clone()`` is not this -- it copies only +the representation and leaves both processes pointing at one set of page +tables. Then give ``ARCH_HAVE_ADDRENV_FORK`` a ``default y if `` line in +``arch/Kconfig``, and ``ARCH_HAVE_FORK`` follows. + +**Build the child from the caller's saved system call frame.** In a kernel +build ``fork()`` is reached through a system call, so the return address and +stack pointer the architecture's fork entry point can observe for itself belong +to the *kernel*, not to the caller; a child built from those resumes at a +kernel address on a kernel stack. The architecture must record the caller's +exception frame when it traps -- ``xcp.sregs`` is the field that exists for +this -- and build the child from that instead, while a kernel thread that calls +the entry point directly still takes the ordinary path. + +Nothing else is required: the ``up_fork()`` entry point and the libc wrapper +are already there and become live automatically. + +Note that ``ARCH_HAVE_ADDRENV_FORK`` is about a *per-process* address +environment. A protected build has one address space carved up once at boot, +whether the boundaries are drawn by an MPU or by a fixed set of MMU mappings; +its ``up_addrenv_*()`` are stubs, and there is no mapping to duplicate at the +same virtual addresses. ``CONFIG_ARCH_ADDRENV`` being set is therefore not by +itself evidence that ``fork()`` can be provided. ``vfork()`` and +``task_fork()``, which share the parent's memory, work there as everywhere +else. + +Known gaps +========== + +**No architecture provides** ``fork()``. The generic machinery is complete -- +``addrenv_fork()``, the ``up_addrenv_fork()`` hook, the syscall, the libc +wrapper and the ``ostest`` case -- and the first ``up_addrenv_fork()`` +implementation turns it on for that architecture with no further generic work. + +**Nothing selects** ``ARCH_VFORK_STACK_BORROW``. The generic half is in place: +``vfork_borrow_stack()`` in ``sched/task/task_fork.c`` gives the child the +parent's stack instead of a relocated copy, bounded by +``CONFIG_ARCH_VFORK_STACK_RESERVE`` and checked with a canary. What is missing +is an architecture entry point that uses it. The architecture this exists for +is Xtensa, whose windowed ABI cannot run a child on a relocated stack copy at +all -- the register-window save areas embedded in the stack hold absolute stack +pointers, so the child reloads a pointer into the *parent's* stack on its very +first window underflow. That is why Xtensa has never selected the fork family +in any form. + +Note also on ``waitpid()`` after ``vfork()`` +============================================ + +The ``vfork()`` parent is now resumed when the child's TCB is torn down, so by +the time it runs the child is completely gone. Where the child called +``exec()`` this makes no difference -- ``exec_swap()`` has already given the +loaded program the child's pid, and that program is still running, so +``waitpid()`` behaves normally. Where the child called ``_exit()``, +``waitpid()`` can only return its status if ``CONFIG_SCHED_CHILD_STATUS`` is +enabled; otherwise it returns ``ECHILD``, because NuttX does not retain the +status of a task that no longer exists. That is a pre-existing property of +that configuration, not a change: the previous implementation blocked in a +libc ``waitpid(WNOWAIT)`` and an application's own ``waitpid()`` afterwards hit +the same wall. diff --git a/Documentation/guides/index.rst b/Documentation/guides/index.rst index acbb2a118b554..9b6114409aa00 100644 --- a/Documentation/guides/index.rst +++ b/Documentation/guides/index.rst @@ -37,6 +37,7 @@ Guides logging_rambuffer.rst ipv6.rst integrate_newlib.rst + fork_vfork_migration.rst protected_build.rst platform_directories.rst port_drivers_to_stm32f7.rst diff --git a/Documentation/implementation/memory_configurations.rst b/Documentation/implementation/memory_configurations.rst index 6ad536df5d6c3..eded4ff309669 100644 --- a/Documentation/implementation/memory_configurations.rst +++ b/Documentation/implementation/memory_configurations.rst @@ -30,7 +30,7 @@ On-Demand Paging NuttX also supports on-demand paging via ``CONFIG_PAGING``. On-demand paging is a method of virtual memory management and requires -the the CPU architecutre support a MMU. +the the CPU architecture support a MMU. In a system that uses on-demand paging, the OS responds to a page fault by copying data from some storage media into physical memory and setting up @@ -410,7 +410,7 @@ of functions that: 1. Have only one ``.text`` space in RAM, but 2. Separate ``.data`` and ``.bass`` space, and are -3. Separately linked into with the program in each address environmnet. +3. Separately linked into with the program in each address environment. (not implemented). @@ -484,7 +484,7 @@ at least in its current form. That full implementation of ``mmap()`` plus the minor changes to the NuttX ELF loader are all that are required to support fully share-able ``.text`` sections – as well as the memory savings -from not carrying aroung the relocation and symbol information +from not carrying around the relocation and symbol information (Not implemented). @@ -632,10 +632,13 @@ the contemplate in any real detail: and swap the state into physical memory as needed?(not implemented). * ``mmap()``. True shared memory and true file mapping could be supported. I am repeating myself (not implemented). -* ``fork()``. The ``fork()`` interface could be supported. NuttX currently - supports the "crippled" version, ``vfork()`` but with these process address - environments, the real ``fork()`` interface could be supported. - (not implemented). +* ``fork()``. The real ``fork()`` interface can be supported on configurations + with a duplicable process address environment: an architecture implements + ``up_addrenv_fork()``, selects ``CONFIG_ARCH_HAVE_ADDRENV_FORK``, and + ``CONFIG_ARCH_HAVE_FORK`` follows. What is not implemented is + copy-on-write: the duplication copies the parent's pages eagerly, which + needs as much free memory as the parent occupies. Demand paging would fix + that. * Dynamic Stack Allocation. Completely eliminate the need for constant tuning of static stack sizes.(not implemented). * Shared Libraries. Am I repeating myself again?(not implemented). @@ -688,8 +691,8 @@ There are two problems here: So how do you create new tasks/processes in such a context. There is only one way possible; by using an interface that takes a file name as an argument (rather than absolute address). -New processes started with ``vfork()`` and ``exec()`` or with -``posix_spawn()`` should not have any of these issues. +New processes started with ``fork()`` or ``vfork()`` and ``exec()``, or with +``posix_spawn()``, should not have any of these issues. ARM Memory Management diff --git a/Documentation/reference/user/01_task_control.rst b/Documentation/reference/user/01_task_control.rst index 163030776c35b..7f78f1fe50920 100644 --- a/Documentation/reference/user/01_task_control.rst +++ b/Documentation/reference/user/01_task_control.rst @@ -59,9 +59,11 @@ Standard interfaces - :c:func:`exit` - :c:func:`getpid` -Standard ``vfork`` and ``exec[v|l]`` interfaces: +Standard ``fork``/``vfork`` and ``exec[v|l]`` interfaces: + - :c:func:`fork` - :c:func:`vfork` + - :c:func:`task_fork` (non-standard) - :c:func:`exec` - :c:func:`execv` - :c:func:`execl` @@ -347,6 +349,48 @@ Functions **POSIX Compatibility:** Compatible with the POSIX interface of the same name. +.. c:function:: pid_t fork(void) + + ``fork()`` creates a new process. The child process is an exact copy of + the calling process: it receives **its own copy** of the parent's memory, + at the same virtual addresses. Writes by the child are invisible to the + parent and writes by the parent are invisible to the child. The child may + modify anything, call any function, return from the function in which + ``fork()`` was called, and run indefinitely; it runs concurrently with the + parent. None of ``vfork()``'s restrictions apply. + + NOTE: ``fork()`` requires an address environment that can be + duplicated, and so it is available only where ``CONFIG_ARCH_HAVE_FORK`` + is selected -- which in turn requires ``CONFIG_ARCH_ADDRENV`` and an + architecture that implements ``up_addrenv_fork()``. **Where it cannot + be provided, ``fork()`` is not provided at all**: the declaration is + absent from ``unistd.h`` and code that calls it fails to build. That + is deliberate. A build error naming the function is strictly better + than a ``fork()`` that silently gives the child the parent's memory. + No architecture implements ``up_addrenv_fork()`` yet, so ``fork()`` is + currently unavailable on every target; see + :doc:`/guides/fork_vfork_migration`. + + There is no copy-on-write, because NuttX has no demand paging to build + it on, so the copy is eager: forking a large process needs as much free + memory as the process occupies and fails with ``ENOMEM`` otherwise. + Spawn-heavy code should prefer :c:func:`posix_spawn` or + :c:func:`vfork`, on NuttX as anywhere. + + Applications that want the historical NuttX ``fork()`` behaviour -- + shared memory, private stack, both running -- should call + :c:func:`task_fork`. ``CONFIG_FORK_IS_TASK_FORK`` aliases ``fork()`` + back to it on configurations that cannot provide real ``fork()``, for + legacy code that cannot be changed. + + :return: Upon successful completion, ``fork()`` returns 0 to the child + process and returns the process ID of the child process to the parent + process. Otherwise, -1 is returned to the parent, no child process is + created, and ``errno`` is set to indicate the error. + + **POSIX Compatibility:** Compatible with the POSIX interface of the same + name. + .. c:function:: pid_t vfork(void) The ``vfork()`` function has the same effect as @@ -357,12 +401,25 @@ Functions function before successfully calling ``_exit()`` or one of the ``exec`` family of functions. - NOTE: ``vfork()`` is not an independent NuttX feature, but is - implemented in architecture-specific logic (using only helper - functions from the NuttX core logic). As a result, ``vfork()`` may - not be available on all architectures. The current implementation in - NuttX arm64 only guarantees that ``vfork()`` works when - CONFIG_BUILD_FLAT=y. + The child **shares** the parent's memory -- nothing is copied, which is the + entire point of ``vfork()`` and the reason a caller chooses it -- and the + parent is **suspended** until the child calls ``_exit()`` or one of the + ``exec`` family. That suspension is what makes the sharing safe, and it is + also the price: the restrictions above exist because the child is running + in the parent's address space on borrowed time. + + NOTE: ``vfork()`` is implementable with or without an MMU and is + available wherever ``CONFIG_ARCH_HAVE_VFORK`` is selected. The + suspension lives in the kernel primitive rather than in a libc + ``waitpid()``, so the parent is resumed at ``exec()`` as POSIX requires, + and ``vfork()`` does not depend on ``CONFIG_SCHED_WAITPID``. + + On architectures that select ``CONFIG_ARCH_VFORK_STACK_BORROW`` the + child borrows the parent's stack outright rather than running on a + relocated copy of it. That is required where stack frames contain + absolute stack addresses -- the windowed ABI of Xtensa, whose register + window save areas hold the spilled stack pointer of each frame -- and is + a pure economy elsewhere. :return: Upon successful completion, ``vfork()`` returns 0 to the child process and returns the process ID of the child process to the @@ -372,6 +429,34 @@ Functions **POSIX Compatibility:** Compatible with the BSD/Linux interface of the same name. POSIX marks this interface as Obsolete. +.. c:function:: pid_t task_fork(void) + + ``task_fork()`` is a non-standard NuttX interface that clones the calling + task. The child shares the parent's ``.data``, ``.bss`` and heap -- it + joins the parent's address environment, exactly as a pthread does -- but + runs on a **private copy** of the parent's stack, and it runs concurrently + with the parent. Like ``fork()`` it returns twice. + + This is neither ``fork()`` nor ``vfork()``. It is a task cloned at the + call site with the memory relationship of a thread; the nearest precedent + is Plan 9's ``rfork(RFPROC|RFMEM)``. It is the behaviour NuttX published + under the name ``fork()`` before these three interfaces were separated, and + it is preserved here under an honest name so that nothing is lost. + + NOTE: available where ``CONFIG_ARCH_HAVE_TASK_FORK`` is selected, which + is exactly the set of configurations that had ``fork()`` before the + split. New code should prefer :c:func:`pthread_create`, which is the + same memory relationship spelled clearly, or :c:func:`posix_spawn`; + ``task_fork()`` exists to give the historical behaviour a truthful name, + not to recommend it. + + :return: Upon successful completion, ``task_fork()`` returns 0 to the child + and returns the process ID of the child to the parent. Otherwise, -1 is + returned to the parent, no child is created, and ``errno`` is set to + indicate the error. + + **POSIX Compatibility:** Non-standard. + .. c:function:: int exec(FAR const char *filename, FAR char * const *argv, FAR const struct symtab_s *exports, int nexports) This non-standard, NuttX function is similar to @@ -449,7 +534,15 @@ Functions thread, then (2) call ``execv()`` or ``execl()`` to replace the new thread with a program from the file system. Since the new thread will be terminated by the ``execv()`` or ``execl()`` call, it really served no - purpose other than to support POSIX compatibility. + purpose other than to support POSIX compatibility. :c:func:`posix_spawn` + does the same job in one step and should be preferred. + + Note also that ``exec()`` does not overlay the calling process: it starts + the new program as a separate task. ``exec_swap()`` then exchanges the two + pids, so that from the parent's point of view the pid ``vfork()`` returned + does name the running program -- but the ``vfork()`` stub itself exits, and + it is that exit which releases the suspended ``vfork()`` parent. The parent + therefore resumes at ``exec()``, as POSIX requires. The non-standard binfmt function ``exec()`` needs to have (1) a symbol table that provides the list of symbols exported by the base code, and diff --git a/Documentation/standards/posix.rst b/Documentation/standards/posix.rst index cdca6eb748ea9..47622ec7921c2 100644 --- a/Documentation/standards/posix.rst +++ b/Documentation/standards/posix.rst @@ -1421,7 +1421,7 @@ Multiple Processes: +--------------------------------+---------+ | :c:func:`exit` | Yes | +--------------------------------+---------+ -| fork() | No | +| :c:func:`fork` | Cond. | +--------------------------------+---------+ | :c:func:`getpgrp` | Yes | +--------------------------------+---------+ @@ -2364,7 +2364,7 @@ XSI Multiple Process: +--------------------------------+---------+ | :c:func:`usleep` | Yes | +--------------------------------+---------+ -| :c:func:`vfork` | Yes | +| :c:func:`vfork` | Cond. | +--------------------------------+---------+ | :c:func:`waitid` | Yes | +--------------------------------+---------+