A numeric process ID is a name in a PID namespace, not a durable handle to one process lifetime. After a process exits and its PID becomes available for reuse, a later process can receive the same number. Linux PID file descriptors add a different interface boundary: a pidfd is a file descriptor referring to a particular task, so later operations can target that kernel reference rather than resolving the numeric PID again.
This distinction matters in supervisors, service managers, launchers, and other code that acts on processes after an interval. The issue is not that PID reuse makes every PID-based operation unsafe. The issue is that a numeric identifier and a retained object reference carry different lifetime semantics.
A PID names a task through a namespace
Traditional process APIs commonly accept a pid_t. Calls such as kill(pid, sig) interpret that integer when the call executes. A positive PID selects the process currently associated with that number in the relevant namespace, subject to permission checks.
The number itself does not preserve the identity of a process that was observed earlier. Consider a control path with separate observation and action steps:
observe PID 4812
|
time passes
|
original task exits
|
PID 4812 is reused
|
act on PID 4812If the action performs a fresh lookup by number, the target can differ from the process associated with the earlier observation. Avoiding that gap requires an interface that carries object identity across the interval.
A PID file descriptor provides such a reference on Linux. pidfd_open(pid, flags) obtains a descriptor referring to the selected task if it exists when the call succeeds. The descriptor is also created with close-on-exec set, which constrains accidental inheritance across execve().
Descriptor identity survives numeric PID reuse
Once acquired, the pidfd continues to refer to the task selected at acquisition. It does not become a reference to a later task merely because the numeric PID is recycled.
That property changes the shape of process-control code. Instead of storing a number and asking the kernel to resolve it again for each operation, code can retain the descriptor and pass it to interfaces that accept PID file descriptors.
For signal delivery, pidfd_send_signal() targets the process referenced by the descriptor. The kernel still applies signal permission rules and can report that the target no longer exists. Stable identity does not grant authority, and it does not keep a terminated process running.
The important semantic boundary is narrower: reuse of the original numeric PID cannot retarget an existing pidfd to an unrelated process.
Readiness represents process termination
PID file descriptors also participate in Linux readiness APIs. A process-oriented descriptor can be monitored with poll(), select(), or epoll.
For a process pidfd created without PIDFD_THREAD, readiness is reported when the last thread in the thread group exits and the process becomes a zombie. After the task is reaped, polling can additionally report a hangup condition. A pidfd is therefore useful as an event source in a loop that already multiplexes sockets, timers, signals, or other descriptors.
epoll set
|-- socket fd
|-- timerfd
|-- signalfd
`-- pidfdThis does not turn the pidfd into a byte stream. Current Linux behavior rejects read() on a pidfd with EINVAL. Readiness communicates task state through the polling contract; process status is obtained through process-specific interfaces.
That distinction prevents an incorrect abstraction in which every pollable descriptor is assumed to contain readable payload bytes. File-descriptor readiness is a common notification surface, not a universal data-transfer model.
Waiting has a parent-child constraint
waitid() can select a child through P_PIDFD, allowing the descriptor to identify the child whose state change is requested. This preserves stable identity at the wait boundary.
The capability is still constrained by process relationships. A pidfd referring to an arbitrary process does not make that process waitable by the caller. The normal child-waiting rules continue to apply. The descriptor changes target identification; it does not erase the kernel’s ownership rules for reaping child state.
This separation is significant for designs that combine observation and reaping. A supervisor that created a child can use a pidfd both as a readiness source and as the identifier supplied to waitid(). A process that merely obtained a pidfd for an unrelated task can monitor termination, but that fact alone does not grant child-reaping semantics.
Acquisition timing still creates a boundary
pidfd_open() solves repeated numeric lookup after successful acquisition, but acquisition itself occurs at a point in time. Code that first receives an untrusted or stale PID and only later calls pidfd_open() must account for the possibility that the number was already reused before the descriptor was obtained.
For newly created children, Linux provides creation paths that can return a pidfd as part of process creation, including clone() or clone3() with CLONE_PIDFD. That arrangement can remove a separate post-creation lookup from the design.
For an already existing process, pidfd_open() is the direct interface for acquiring the descriptor. The guarantee begins with the object actually selected by that successful call, not with an earlier application-level belief about the PID.
This is a general handle-design principle: stable references remove races after binding, but they cannot retroactively validate identity before binding.
Thread references alter readiness and signal scope
Modern Linux can create a pidfd for a specific thread by using PIDFD_THREAD with pidfd_open(). That changes the object represented by the descriptor.
A thread pidfd becomes readable when that specific task exits and becomes a zombie, even if other threads remain in the thread group. Signal delivery can also distinguish thread, thread-group, and process-group scope through the pidfd signal interface and its supported flags.
Code therefore cannot treat every pidfd as semantically identical. The acquisition flags form part of the handle’s contract. A descriptor for a thread-group leader without PIDFD_THREAD and a descriptor for a specific thread can have different readiness boundaries even when both originate from related numeric IDs.
The file-descriptor type provides stable reference semantics, while the creation mode determines the referenced task scope.
Descriptor lifetime is not target lifetime
Holding a pidfd keeps the reference object available to the caller, but it does not extend the execution lifetime of the target. The process can exit while the descriptor remains open.
This is similar to other kernel handles whose referent can transition into a terminal state. The handle preserves identity sufficiently for supported operations and state observation; it is not a lease that prevents termination.
Closing the pidfd releases the caller’s descriptor reference. As with other descriptors, resource accounting applies: pidfd_open() can fail when per-process or system-wide file-descriptor limits are reached. A design that retains pidfds for many processes must therefore include them in descriptor-lifetime management.
The practical ownership model has two independent events:
target task lifetime: running -------- exit
pidfd lifetime: open ---------------- closeTheir overlap enables stable process operations. Neither lifetime is defined as identical to the other.
Stable references narrow a class of process-control races
PID file descriptors do not replace every numeric PID API, nor do they remove permission checks, namespace rules, parent-child constraints, or termination races. Their contribution is more precise: they let process-management code bind to a task once and carry that binding through a file-descriptor interface.
That shifts several operations from repeated name resolution toward handle-based identity. Signal delivery can use the retained reference, termination can enter an existing readiness loop, and eligible child waits can select the same referenced process.
The resulting boundary is valuable precisely because it is limited. A pidfd establishes stable target identity for supported Linux interfaces after successful acquisition. Process authority, execution lifetime, wait ownership, and task scope remain separate contracts that callers must preserve.