Introduction
In previous articles, we systematically analyzed container namespaces, filesystems, resource control (Cgroups and Intel RDT), and security defenses. Once all static configurations and isolated environments are prepared, the container startup enters the actual execution phase.
This article parses the Runtime and Init Process section in SPEC.md. This chapter specifies the inter-process synchronization mechanism during container creation, standard I/O handling, key files injected by the runtime, and the default parameter settings for the container process environment.
Synchronization Between Parent and Init Process
When creating a container in a Linux system, the parent process on the host (i.e., the runtime environment) needs to synchronize its state with the main process inside the container (the Init process). The container process cannot execute user business logic immediately after creation because the parent process requires time to complete subsequent configurations that depend on the assigned PID.
SPEC.md describes this synchronization process as follows:
During container creation the parent process needs to talk to the container’s init process and have a form of synchronization. This is accomplished by creating a pipe that is passed to the container’s init. When the init process first spawns it will block on its side of the pipe until the parent closes its side.
The specification mandates the use of an anonymous pipe to achieve this synchronization. The specific workflow is as follows:
- The parent process creates a pipe and passes its file descriptor (specified as
FD 3in the specification) to the newly spawned container Init process. - Upon startup, the Init process blocks and waits on the reading end of the pipe.
- The parent process utilizes this time window to perform operations such as placing the new process PID into a cgroup hierarchy or writing UID/GID mappings required for user namespaces.
- After completing these configurations, the parent process closes its writing end of the pipe.
- The Init process detects the pipe closure, unblocks, and proceeds with the subsequent initialization tasks.
This design strictly avoids race conditions, ensuring that before the process inside the container formally executes, it has been correctly incorporated into all resource and permission restriction frameworks.
Init Process Execution Model
Regarding the container’s Init process, the specification establishes two clear requirements:
The application consuming libcontainer should be compiled statically. libcontainer does not define any init process and the arguments provided are used to
execthe process inside the application. There should be no long running init within the container spec.
- No Long-Running Init Daemon: Unlike traditional Linux operating systems, there should not be a long-running Init process (similar to
systemdorsysvinit) inside the container.libcontainerdoes not provide a default daemon; instead, it directly utilizes the provided arguments to start the target application via theexecsystem call. - Static Compilation: The document suggests that applications using
libcontainershould be compiled statically to reduce dependencies on host dynamic link libraries, thereby enhancing portability and the purity of environmental isolation.
Pseudo TTY and Standard Stream Redirection
For container tasks that require interaction, terminal configuration is indispensable.
If a pseudo tty is provided to a container it will open and
dup2the console as the container’s STDIN, STDOUT, STDERR as well as mounting the console as/dev/console.
If a pseudo terminal (Pseudo TTY) is specified for the container in the configuration, the runtime performs the following processing:
- Opens the terminal device.
- Uses the
dup2system call to redirect the container’s standard input (STDIN), standard output (STDOUT), and standard error (STDERR) entirely to the console. - Mounts the console device to the
/dev/consolepath within the container’s filesystem.
Files Injected by the Runtime
The container’s rootfs typically originates from a static image. However, certain files related to network resolution and host identification exhibit strong environmental dependency. Directly using the files packaged in the image may cause unexpected side effects during process execution.
To address this issue, the specification points out that the runtime intervenes in the creation and population of these specific files:
An extra set of mounts are provided to a container and setup for use. A container’s rootfs can contain some non portable files inside that can cause side effects during execution of a process. These files are usually created and populated with the container specific information via the runtime.
SPEC.md provides a complete list of 4 extra files created and managed by the runtime (Extra runtime files):
/etc/hosts/etc/resolv.conf/etc/hostname/etc/localtime
In actual implementations, the runtime typically generates these files on the host and then overwrites the corresponding locations in the container’s root filesystem using bind mounts.
Default Parameter Configuration (Defaults)
SPEC.md stipulates a set of default environmental parameters. In cases where users do not explicitly override these configuration items, the processes within the container will be subjected to the following default values. All default items in the document are listed completely here without omission:
| Type | Value |
|---|---|
| Parent Death Signal | SIGKILL |
| UID | 0 |
| GID | 0 |
| GROUPS | 0, NULL |
| CWD | "/" |
| $HOME | Current user’s home dir or "/" |
| Readonly rootfs | false |
| Pseudo TTY | false |
Standard Container Actions API (Public API)
The specification explicitly states in the Actions section:
After a container is created there is a standard set of actions that can be done to the container. These actions are part of the public API for a container.
Once a container is created, a standard set of actions can be applied to it. To ensure the integrity of the specification, the 5 core actions defined in SPEC.md are listed below exactly as they appear in the original document:
| Action | Description |
|---|---|
| Get processes | Return all the pids for processes running inside a container |
| Get Stats | Return resource statistics for the container as a whole |
| Wait | Waits on the container’s init process ( pid 1 ) |
| Wait Process | Wait on any of the container’s processes returning the exit status |
| Destroy | Kill the container’s init process and remove any filesystem state |
These actions appear simple, but they encapsulate highly complex underlying system calls. Below, we analyze the implementation principles of these 5 APIs at the libcontainer level.
1. Get processes
A container is essentially a group of isolated processes, but from the host’s perspective, they remain standard Linux processes. How can we accurately locate which processes belong to a specific container?
Underlying Mechanism: libcontainer does not traverse the entire /proc directory. Since each container is placed into an independent Cgroup, the most rigorous and lowest-overhead method to retrieve the container’s process list is to read the cgroup.procs file under the corresponding Cgroup controller directory (usually the pids or cpu controller). Maintained by the Linux kernel, this file naturally contains the PIDs of all processes belonging to that Cgroup (from the host’s namespace perspective).
2. Get Stats
High-level commands like docker stats or kubectl top can display the container’s CPU, memory, and network usage in real-time. The data source for these metrics is precisely this Get Stats action.
Underlying Mechanism: Similar to Get processes, this relies heavily on the filesystem interface exposed by Cgroups. libcontainer accesses the container-specific subdirectories under /sys/fs/cgroup/:
- Reads
memory.statandmemory.usage_in_bytesto acquire memory and Swap usage, as well as Page Cache statistics. - Reads
cpuacct.usageandcpuacct.statto acquire CPU time slice consumption (distinguishing between User and System modes). - Reads
blkio.throttle.io_service_bytesto acquire disk I/O statistics.
This API parses these files, which are updated by the kernel in real-time, and aggregates them into structured data objects returned to the caller.
3. Wait and Wait Process
A container’s lifecycle is typically tied to its primary internal process (the Init process). When synchronous waiting for container exit is required, two levels of Wait methods are provided.
Underlying Mechanism:
- For Wait (Waiting on the Init process): The parent process utilizes the Linux
waitpid()system call to suspend itself, monitoring state changes of the target PID (usually passing the Init process’s mapped PID on the host). Upon receiving theSIGCHLDsignal, which indicates the Init process has exited, the system can proceed to reclaim resources. - For Wait Process (Waiting on any process): This is typically used for additional processes executed inside the container via
runc exec(equivalent todocker exec). The parent process similarly useswaitpid()or lower-level file descriptor event-driven mechanisms (likeepoll) to wait for the specific child process to terminate and accurately capture its Exit Code to feed back to the terminal user.
4. Destroy
When a container finishes its task or is forcibly stopped, the Destroy action is responsible for completely reclaiming all system resources, preventing Zombie Processes or mount point leaks.
Underlying Mechanism: This is a composite operation with a strict execution sequence:
- Process Cleanup: Sends a
SIGKILLsignal to the container’s Init process. Based on a critical feature of the Linux PID Namespace—when the PID 1 process within a PID Namespace terminates, the kernel automatically sends a SIGKILL signal to all other processes in that Namespace. This ensures that all descendant processes inside the container are instantaneously and thoroughly wiped out. - Filesystem Unmounting: After process cleanup,
libcontainerexecutesumountoperations to detach the rootfs,/dev,/proc,/sys, and other virtual filesystem mount points prepared for the container. - Control Group Destruction: Finally, it deletes the dedicated subdirectories for this container under
/sys/fs/cgroup/. Once the processes in these directories are completely cleared, these Cgroup directories can be removed viarmdir.
Conclusion
As the concluding section of SPEC.md, the Actions chapter defines the minimum essential set of operations for higher-level container engines to interact with the underlying runtime environment. Through these 5 straightforward API interfaces, the underlying Cgroup states, system calls (waitpid, kill), and PID Namespace cleanup characteristics are encapsulated into user-friendly lifecycle management logic.
With this, we have completed our section-by-section interpretation of runc’s core specification repository, libcontainer/SPEC.md. From isolation mechanisms to lifecycle control, this document serves as an excellent textbook for understanding the foundation of modern Linux container technology.