Skip to main content

Modules/posixmodule.c — subprocess (part 8)

Source:

cpython 3.14 @ ab2d84fe1023/Modules/_posixsubprocess.c

This annotation covers the POSIX subprocess execution path. See modules_subprocess7_detail for Popen.__init__, pipe setup, and the Windows CreateProcess path.

Map

LinesSymbolRole
1-80subprocess_fork_execPython entry point: validate args, call C
81-200child_execPost-fork: set up fds, exec
201-320_Py_set_inheritableMark fds as non-inheritable
321-420vfork pathUse vfork for performance on Linux
421-500Error pipeReport exec failures back to parent

Reading

child_exec

// CPython: Modules/_posixsubprocess.c:420 child_exec
static void
child_exec(char *const exec_array[], char *const argv[], char *const envp[],
const char *cwd, int p2cread, int p2cwrite, int c2pread, int c2pwrite,
int errread, int errwrite, int errpipe_read, int errpipe_write,
int close_fds, int restore_signals, int call_setsid,
...)
{
/* All code here runs after fork — no Python API calls allowed */
if (cwd) {
if (chdir(cwd) == -1) goto error;
}
/* Dup2 standard fds */
if (p2cread != -1) dup2(p2cread, 0);
if (c2pwrite != -1) dup2(c2pwrite, 1);
if (errwrite != -1) dup2(errwrite, 2);
/* Close all other fds if close_fds=True */
if (close_fds) {
/* Use /proc/self/fd or iterate from 3 to MAXFD */
...
}
/* Execute */
for (int i = 0; exec_array[i] != NULL; i++) {
execve(exec_array[i], argv, envp);
}
error:
/* Report error via error pipe */
write(errpipe_write, &errno, sizeof(errno));
_exit(255);
}

child_exec runs in the child process after fork. No Python API can be called (the interpreter state is in an undefined state after fork in a multi-threaded process). Errors are communicated back to the parent by writing errno to errpipe_write.

vfork path

// CPython: Modules/_posixsubprocess.c:680 subprocess_fork_exec
/* On Linux with vfork available and no close_fds: use vfork for performance */
if (use_vfork) {
pid = vfork();
if (pid == 0) {
child_exec(...);
_exit(255); /* never reached */
}
} else {
pid = fork();
if (pid == 0) {
child_exec(...);
_exit(255);
}
}

vfork is faster than fork on Linux because it does not copy the parent's page tables — the child shares the parent's address space until exec or _exit. The parent is suspended until the child calls exec or _exit. vfork is only used when close_fds=False (no fd iteration in child) and no preexec function.

Error pipe

// CPython: Modules/_posixsubprocess.c:780 (parent side)
/* Read from errpipe_read to check for child exec failure */
char data[4];
ssize_t n = read(errpipe_read, data, sizeof(data));
if (n > 0) {
/* Child failed: decode errno and raise */
int child_errno;
memcpy(&child_errno, data, sizeof(child_errno));
errno = child_errno;
PyErr_SetFromErrnoWithFilenameObject(PyExc_OSError, executable);
}

The error pipe is a pair of file descriptors: the child writes to errpipe_write if exec fails, the parent reads from errpipe_read after waitpid. If the pipe has data, the exec failed and SubprocessError is raised. On success, the pipe is empty (EOF).

gopy notes

_posixsubprocess.fork_exec is module/subprocess.ForkExec in module/subprocess/module.go; uses syscall.ForkExec on POSIX. child_exec logic is implemented directly by Go's os/exec package via cmd.Start(). The error pipe uses a Go os.Pipe(). vfork is not used — Go's os/exec always uses fork.