Whoa, this is older than I thought. First and a foremost apologies for radio silence, I got really busy with other stuff.
I did not have the time yet to go through the patchset, but I have some time to respond to this bit. On Wed, Aug 19, 2026 at 3:15 AM Li Chen <[email protected]> wrote: > > Hi Justin, > > ---- On Wed, 05 Aug 2026 04:25:25 +0800 Justin Suess > <[email protected]> wrote --- > > On Thu, Jul 16, 2026 at 10:31:26PM +0800, Li Chen wrote: > > > Hi, > > > > > > This RFC follows feedback on my earlier spawn_template RFC [1]. That > > > proposal made caching the primary interface; this one starts with general > > > process construction. Christian suggested a pidfd/pidfs exec builder > > > modeled after fsconfig(), with enough semantics for userspace to > implement > > > posix_spawn() [2], and Kees agreed [3]. > > > > > > This RFC is based on linux-next next-20260710 and depends on two pidfs > > > fixes that I sent separately: > > > > > > * pidfs: preserve thread pidfds reopened by file handle > > > https://lore.kernel.org/all/[email protected]/ > > > * pidfs: handle FS_IOC32_GETVERSION in compat ioctl > > > https://lore.kernel.org/all/[email protected]/ > > > > > > The initial implementation is source-based. The executable path can be > > > provided with the final run request: > > > > > > struct pidfd_spawn_run_args run = { > > > .path = (unsigned long)"/usr/bin/rg", > > This should probably be an FD for the path. > > > > This way it prevents race conditions over multiple configuration steps. > > Thanks, that makes sense. Using an fd avoids the pathname race and pins > the executable we actually want to run. > I don't understand what race is supposed to be addressed either as none of the setup depends on the content binary. Regardless I don't think this is going to fly as it directly shafts NUMA setups. What's needed is a path to the binary + the PATH envifornment variable or a collection of paths to try out. Here is how it harms NUMA: In the most extreme case suppose there is a container bound to a specific node and you want to exec something into it. Further suppose none of the relevant data is in RAM yet. And finally the process doing all the work is running a different node. Say you are going to spawn ${container}/usr/bin/coolprog. At minimum this will allocate a dentry and an inode for usr, bin and coolprog itself. Afterwards any path lookup going through usr in that that container will have to fetch memory from a non-local node to traverse it, same thing for the bin subdirectory. This is a self-induced problem when using a fd. In the real world userspace notoriously performs failed execs as it makes its way through dirs in provided in PATH, and while you can patch select programs all you want, the mechanism will have to account for it. This is where the next NUMA issue lies its ugly head. Suppose you have a box with multiple nodes, but don't use anything to maintain affinity (as in, processes run where the scheduler sees fit). A well-known exec-heavy workload where this is fine is compilation. Any big project will do, to give you one trivial example, make -j $BIGNUM modules in the Linux kernel directory. Say you have a box with 4 domains and 32 threads per each. make -j 128 will end up spilling on the entirety of the machine and there will be some loss of locality when spawning gcc and whatnot. Even then, a sensibly behaving scheduler will not migrate processes between nodes without needing to do it. Meaning after the process gets to run, you can assume it stays on the given node. The stock kernel with the fork + exec machinery is doing a shitty job here, but the new mechanism can assure process-specific allocation to come form the target node as long as the scheduler does not mess up. To that end, the final form of the machinery needs to be in position to do the following: 1. copy in whatever data it needs from userspace 2. if no affinity is specified, ask the scheduler: assuming something forks now, where is it going to run? the answer would be speculative ofc 3. to the minimum allocations needed (task_struct etc.) while passing the node number from above 4. have the newly spawned task handle everything else This in particular means that a failed exec would be very expensive, as it would go through task creation and teardown. Meaning the child needs to be able to figure out where to binary to execute is. And user-visible part has to account for it from the get-go. I'm going to write more after I go through the patchset, should be next week. > > > .argv = (unsigned long)argv, > > > .envp = (unsigned long)envp, > > > }; > > > > > > fd = pidfd_open(0, PIDFD_EMPTY); > > > pidfd_spawn_run(fd, &run, sizeof(run)); > > > > > > Alternatively, the path can be staged before the final run step: > > > > > > struct pidfd_spawn_run_args run = { > > > .argv = (unsigned long)argv, > > > .envp = (unsigned long)envp, > > > }; > > > > > > fd = pidfd_open(0, PIDFD_EMPTY); > > > pidfd_config(fd, PIDFD_CONFIG_SET_STRING, > > > PIDFD_CONFIG_KEY_PATH, "/usr/bin/rg", 0); > > Same here. Should probably be an FD to the binary instead. > > > > > pidfd_spawn_run(fd, &run, sizeof(run)); > > I'm worried this pidfd_spawn_run just adds another varient to the existing > > myriad of exec* syscalls we already have. Would it be better to just have > this > > work through execveat(fd, "", argv, envp, AT_EMPTY_PATH) instead? > > > > (i.e have execveat take a pidfd directly). > > > > Then you can get rid of pidfd_spawn_run which looks almost structurally > > identical to execveat (with the argv and envp collapsed). > > Thanks, but I'm less sure about using execveat() as the run operation, since > it > normally replaces the caller while the builder creates a new child and > returns. A separate run operation still seems clearer to me. > > Regards, > Li > >

