Representative interview topic

Coding interview: How would you build a Linux process supervisor with pidfds?

CodingHard
Offer.cc Editorial TeamPublished Updated

Question

A worker supervisor currently stores numeric PIDs, polls `/proc`, and occasionally kills a newly created process after PID reuse. How would you redesign it with Linux pidfds and prove the lifecycle is race-free?

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 pidfd_open() for a file descriptor referring to a process and pidfd_send_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 pidfd_send_signal() 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 pidfd_send_signal() 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 pidfd_send_signal() 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 /proc for 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.

Public sources

Related questions

Related interview tool

Use Screenshot for a coding prompt

Capture the problem, then work through the constraints, solution, code, edge cases, and complexity in order.

View the tool