Prompt and scope
Numeric PIDs can be reused after a process exits, so a delayed signal or status check can target the wrong process. Linux provides pidfdopen() for a file descriptor referring to a process and pidfdsend_signal() for signaling that process. The core skill is systems programming and lifecycle correctness, so this is a coding question.
What interviewers assess
Strong answers explain descriptor identity, poll or epoll readiness, waitid integration, close-on-exec, permissions, and cleanup. They distinguish a pidfd from a process handle that grants unlimited access, handle fork/exec races, and define behavior for exit, restart, timeout, and supervisor crash. They also mention kernel-version feature detection and a tested fallback.
Questions to clarify first
- Which kernel versions and namespaces must the supervisor support?
- Does it launch children itself, or attach to existing processes?
- Must it observe exit status, send signals, or both?
- Can the worker fork descendants, and who owns their cleanup?
- What are timeout, restart, and supervisor-crash guarantees?
- Is a fallback required on kernels without pidfd support?
30-second answer framework
“I would obtain a pidfd for each managed process and store the descriptor, not only its numeric PID. Add the descriptor to poll or epoll, use the documented wait operation to collect status, and call pidfdsendsignal() for timeout or shutdown. Set close-on-exec, close descriptors on every terminal path, and treat readiness plus status collection as one lifecycle state machine. I would test PID reuse, rapid exit, permission errors, forked children, kernel feature detection, and supervisor restart.”
Step-by-step answer
Step 1: Acquire and own the descriptor
After launching or locating a process, call pidfd_open() where supported and record the descriptor in an owner table. Mark it close-on-exec and keep the numeric PID only for logs. Do not pass the descriptor to unrelated workers without an explicit ownership transfer.
Step 2: Observe lifecycle events
Register pidfds with poll or epoll. Readiness indicates the referenced process has exited; collect its status with the appropriate wait operation and then close the descriptor. Do not infer liveness from a stale /proc path or a PID integer.
Step 3: Signal the right process
Use pidfdsendsignal() for graceful termination and escalation. Handle permission and namespace errors explicitly. A pidfd identifies the referenced process even if its numeric PID is later reused, but it does not replace authorization checks.
Step 4: Model restart and descendants
Represent each worker as starting, running, stopping, exited, or failed. On restart, create a fresh pidfd and generation record. Decide whether descendants are in the same process group, cgroup, or separate ownership domain; never assume signaling a parent cleans up every child.
Step 5: Test race and portability boundaries
Stress rapid exit and PID reuse, signals concurrent with exit, descriptor exhaustion, supervisor crash, namespace changes, and unsupported kernels. Compare against a carefully constrained fallback such as waitpid for direct children, and record which guarantees the fallback cannot provide.
Model answer
“The defect comes from using a reusable integer as process identity. I would store a pidfd per worker, register it with an event loop, collect exit status after readiness, and signal through pidfdsendsignal() for graceful stop and escalation. Descriptor ownership, close-on-exec, permissions, and descendant cleanup become explicit state-machine rules. Every restart gets a new generation and pidfd. Tests must force rapid exit and PID reuse, cover namespace and kernel support, and document the weaker guarantees of a direct-child fallback.”
Common mistakes
- Keeping only the numeric PID → delayed signals race with PID reuse → retain the pidfd.
- Polling
/procfor liveness → observations become stale → use pidfd readiness and status collection. - Assuming a pidfd bypasses permissions → signals still require authorization → handle permission errors.
- Leaking descriptors across exec → unrelated programs inherit lifecycle handles → set close-on-exec.
- Killing only the parent → descendants remain orphaned → define group or cgroup ownership.
- Treating readiness as a full status → exit code handling is incomplete → collect and persist wait status.
Follow-up questions
Follow-up 1: Does a pidfd prevent PID reuse?
It provides a stable descriptor reference to the process for operations that accept pidfds. The numeric PID may be reused, but operations through the descriptor still refer to the original process.
Follow-up 2: Can you use pidfds with epoll?
Yes, a pidfd is pollable for process exit, so it can participate in the supervisor’s event loop alongside pipes, timers, and control sockets.
Follow-up 3: What if the kernel lacks pidfd support?
Detect capability at startup and use a documented fallback for direct children, while exposing weaker race and observability guarantees rather than pretending equivalence.
Follow-up 4: What happens after supervisor restart?
Persist enough ownership metadata to rediscover or deliberately abandon workers, then recreate monitoring state and pidfds. Never trust a persisted numeric PID alone.