How containerd-shim Decouples Container Lifecycles

In Linux container environments, containerd-shim serves as a lightweight intermediary process between high-level runtimes like containerd and low-level OCI runtimes like runc. This article explains how the Linux operating system implements process reparenting, file descriptor preservation, and system calls to allow containerd-shim to decouple containers from daemon lifecycles. By isolating runtime dependencies, Linux ensures that containers remain running even if management daemons crash, restart, or upgrade.

The Architecture Challenge of Direct Execution

Under the Open Container Initiative (OCI) model, low-level runtimes like runc are responsible for setting up namespaces, cgroups, and root filesystems before executing the container's initial process. However, runc is ephemeral: it executes the setup, starts the container workload, and exits immediately.

If the central container manager (such as containerd) monitored the container process directly, it would create a tight coupling between the management daemon and the container. Under standard Linux process semantics, terminating or restarting containerd would drop child process handlers, sever standard I/O pipes, and terminate all active containers.

Process Reparenting and PR_SET_CHILD_SUBREAPER

Linux handles process lifecycles hierarchically. When a parent process terminates, its child processes are traditionally reparented to PID 1 (init or systemd). If runc exits, the container process becomes orphaned.

To solve this, Linux introduced the prctl system call with the PR_SET_CHILD_SUBREAPER option. When containerd creates a container, it does not launch runc directly; it first spawns containerd-shim. The shim executes:

prctl(PR_SET_CHILD_SUBREAPER, 1, 0, 0, 0);

By marking itself as a subreaper, the shim informs the Linux kernel that any orphaned processes within its descendant tree must be reparented to the shim process instead of PID 1. When containerd-shim executes runc, and runc subsequently exits, the container process's parent PID automatically becomes the shim.

Decoupling from the Daemon Hierarchy

To completely detach the container from the parent daemon, containerd-shim is executed as an orphaned or detached process relative to containerd. When containerd starts a shim, it uses a standard double-fork mechanism or launches the process in a new session (setsid()).

Because the shim is detached from the daemon’s process group:

Preserving I/O Streams and File Descriptors

A container relies on standard input, output, and error streams (stdin, stdout, stderr). If the process managing these pipes terminates, the Linux kernel generates a SIGPIPE or reaches an EOF state, causing the container process to fail.

The containerd-shim addresses this by maintaining the container's standard streams directly:

  1. The shim opens the Linux FIFO pipes or pseudo-terminals (PTYs) allocated for the container.
  2. The shim retains these open file descriptors regardless of whether containerd is actively listening.
  3. If containerd shuts down, the shim buffers output or holds the pipe open until containerd reconnects to the shim's control socket.

Because the file descriptors reside in the shim’s file descriptor table, the container’s streams remain valid and open at the kernel level.

Exit Status Retention and Zombie Prevention

In Linux, when a child process terminates, it enters a "zombie" state until its parent process invokes waitpid() or waitid() to read its exit status.

Because containerd-shim acts as the subreaper and immediate parent of the container process: