A numeric process ID names a process only while that PID remains assigned to it. After process exit and reaping, Linux may reuse the number for another process. Code that observes a PID, performs unrelated work, then acts on that number can therefore cross a lifetime boundary that the integer itself does not encode. Linux pidfds provide a file-descriptor reference to a process so later operations can target the referenced process object rather than repeat a numeric PID lookup.
The interface is Linux-specific. pidfd_open(), pidfd_send_signal(), pidfd polling, waitid() with P_PIDFD, and related operations are kernel APIs, not properties of C or POSIX process semantics.
pidfd_open converts a PID lookup into a retained reference
pidfd_open(pid, flags) asks the kernel to resolve a numeric PID and return a file descriptor referring to that process. With flags equal to zero, the call can refer to a thread-group leader. Current kernels also define PIDFD_THREAD, which permits a pidfd for an individual thread where supported.
The important boundary is the moment of resolution. A successful call establishes the descriptor reference at that point. Subsequent operations using the pidfd do not resolve the original numeric PID again, so later PID reuse cannot redirect that descriptor to a different process.
This does not make process state immutable. The referenced process can exit after pidfd_open() succeeds. The pidfd preserves identity, not liveness. Operations performed after exit report behavior defined for their specific API rather than silently switching to a new process that inherited the same number.
PIDFD_NONBLOCK can also be supplied on kernels that support it. That flag affects operations such as waiting through the descriptor; it does not turn process lifetime into an event stream by itself.
Signaling through a pidfd closes the reuse gap
Traditional signaling with kill(pid, sig) resolves the numeric PID when kill() executes. If code obtained the PID earlier and the original process disappeared in the interval, reuse creates a race between observation and action.
pidfd_send_signal(pidfd, sig, info, flags) instead directs the operation through the retained process reference. Permission checks still apply. A pidfd is not an authority token that bypasses Linux signal access control; it changes target identity resolution.
The distinction is useful in supervisors and process managers that retain handles across asynchronous work. A descriptor can be stored beside other resource references and passed to the component that eventually performs the signal operation. The receiving component does not need to trust that an old integer still denotes the same lifetime.
Passing signal number zero retains the conventional signal-check shape: no signal is delivered, while the kernel still performs target and permission checks defined by the interface. The result remains a point-in-time operation and does not promise that the process stays alive afterward.
Poll readiness exposes process exit through descriptor machinery
A pidfd can participate in poll(), select(), and epoll. For a process pidfd, readiness is associated with termination of the referenced process, allowing process lifecycle observation to share an event loop with sockets, timers, and other descriptors.
Readiness is level-oriented state rather than a consumable byte record. There is no requirement to read() a pidfd to clear an exit event. The descriptor identifies the process and exposes poll state; exit status is obtained through a waiting interface when the caller has the required relationship to reap or inspect it.
This separation matters in event-loop design. epoll_wait() can identify which child has terminated, while waitid(P_PIDFD, ...) can retrieve the corresponding wait state without converting the event back into a numeric-PID lookup.
A pidfd becoming readable also does not imply that every caller may reap the process. Waiting rules still depend on parent-child relationships and wait semantics. Descriptor readiness and authority to consume child status are separate contracts.
P_PIDFD binds wait selection to the same identity
Linux extends waitid() with P_PIDFD. The id argument is the pidfd value, so child selection is based on the descriptor reference rather than a PID number.
For a supervisor that created a child and retained its pidfd, this joins two stages that otherwise use different naming models: readiness can arrive on the descriptor, and status collection can select that same descriptor. PID reuse between those stages cannot retarget the wait.
WNOWAIT remains relevant when status must be observed without immediately consuming the waitable state. It leaves the child in a waitable condition for a later wait. The pidfd stabilizes target identity, while wait flags control status-consumption semantics.
A nonblocking pidfd can cause waitid() to return EAGAIN when the selected child has not terminated and the operation would otherwise block. This behavior belongs to the pidfd and wait API combination, not to generic file-descriptor nonblocking semantics for arbitrary process operations.
pidfd_getfd duplicates a target descriptor under separate access rules
pidfd_getfd(pidfd, targetfd, flags) can duplicate a file descriptor from the referenced process into the caller. The resulting descriptor refers to the same open file description as the target descriptor, so file offset and status flags associated with that open file description are shared.
The pidfd again supplies stable process identity, but it does not grant unrestricted descriptor extraction. Linux applies a ptrace-style access check to this operation. Namespace, credential, capability, and security-module policy can therefore prevent access even when the caller possesses a pidfd.
There is also a concurrency boundary around targetfd. The integer is interpreted in the referenced process when pidfd_getfd() executes. If that process closes and reuses the descriptor number before the call, the operation can select the newer descriptor. A pidfd removes ambiguity about process identity; it does not freeze the target process’s descriptor table.
That distinction is central to handle-based API design. Stable identity at one layer does not automatically stabilize subordinate names inside the referenced object.
Descriptor transfer preserves the process reference
Because a pidfd is a file descriptor, it can be inherited across fork() subject to descriptor flags, and it can be transferred over a Unix domain socket with SCM_RIGHTS. The receiving process obtains a descriptor referring to the same process identity.
This permits a component that performs process creation or discovery to hand a stable reference to another component. The receiving side need not repeat PID resolution. Normal access checks for later pidfd operations still apply where the operation defines them.
Close-on-exec policy also matters. A pidfd returned by pidfd_open() has close-on-exec set, preventing accidental propagation across execve() unless the application deliberately changes descriptor flags. Explicit transfer is therefore distinct from incidental program replacement inheritance.
Descriptor lifetime is independent of the target’s ability to keep executing. Holding a pidfd does not keep a process alive. It keeps a kernel reference suitable for identity-sensitive operations and observation until the descriptor itself is closed.
Process references narrow races without creating transactions
Pidfds replace a fragile naming step with a retained kernel reference. That closes a specific class of races in which a numeric PID is resolved more than once across asynchronous boundaries. It does not serialize the target process, freeze its resources, preserve its address space, or make several pidfd operations atomic as a group.
A process can exit between two operations on the same pidfd. Its descriptor table can change while another process holds a pidfd. Permission state can also make an operation fail even though the descriptor remains valid. Correct designs therefore treat pidfds as stable identity handles with operation-specific semantics, not as snapshots of process state.
That narrower contract is also their architectural value. Signaling, readiness, waiting, and selected cross-process operations can share one process identity representation while each API retains its own lifetime, permission, and state-transition rules.