Systemd units cannot share memory directly because each ExecStart command runs in an isolated process. To share data, you must use external storage mechanisms like files, sockets, or environment variables passed via EnvironmentFile. Initializing this data before the main service starts ensures availability and prevents race conditions during boot.
The Isolation Model of Systemd Units
When you define a systemd unit file, the daemon creates a new execution context for every command specified in the[Service] section. This design is intentional for security and stability, ensuring that one service crashing does not corrupt the memory space of another. However, this isolation creates a significant hurdle when developers need to pass configuration, state, or credentials from one step of the startup process to the next.
It is crucial to understand that there is no shared heap or global variable space between different ExecStart lines. Even if you have a "fork" scenario where a parent script spawns a child, the child inherits the environment but not the runtime memory state of the parent beyond what was explicitly exported. Therefore, any data that needs to be accessible by subsequent steps must be persisted to the filesystem or exposed through inter-process communication (IPC) channels managed by systemd itself.
Using Environment Files for Static Data
The most common and straightforward method for sharing simple key-value data across unit dependencies is theEnvironmentFile directive. This allows you to store configuration in a plain text file that multiple services can read. When a service starts, systemd loads these variables into its environment before executing the ExecStart command.
To implement this, create a file such as /etc/myapp/config.env with content like:
DB_HOST=localhost
DB_PORT=5432
API_KEY=secret123
Then, reference it in your unit file:
[Service]
EnvironmentFile=/etc/myapp/config.env
ExecStart=/usr/bin/myapp --host $DB_HOST --port $DB_PORT
This approach ensures that all processes spawned by the unit see the same variables. It is ideal for static configuration that does not change during runtime. For more complex scenarios involving dynamic data generation, you might look into Understanding Variable Usage in systemd Unit Files to explore advanced expansion techniques.
Sharing State via Files and Sockets
For data that changes dynamically or requires synchronization, files and sockets are the primary tools. If Service A generates a token that Service B needs, Service A should write that token to a specific file path, such as/run/myapp/token, and Service B should read from it. Systemd’s RuntimeDirectory directive helps manage temporary files that persist only for the lifetime of the service.
Alternatively, Unix domain sockets allow for direct IPC. You can configure a service to listen on a socket using ListenStream or ListenDatagram. Other services can then connect to this socket to request data or send commands. This is particularly useful for microservice architectures running on a single host, where avoiding disk I/O for small messages improves performance.
Ensuring Initialization Order
Data availability depends heavily on the order in which services start. If Service B tries to read a file created by Service A, but Service B starts first, it will fail. You must enforce dependencies usingAfter= and BindsTo= directives.
[Unit]
Description=My App Worker
After=myapp-generator.service
BindsTo=myapp-generator.service
[Service]
ExecStart=/usr/bin/worker
The After= directive ensures that systemd waits for the generator service to reach its active state before starting the worker. The BindsTo= directive adds a stronger dependency, ensuring that if the generator stops, the worker also stops. This prevents the worker from operating on stale or missing data. For more details on managing shutdown sequences to prevent data loss, see Preserve Order of Closing Services While Shutting Down Linux.
Handling Errors and Race Conditions
Even with proper ordering, race conditions can occur if a service writes data asynchronously. To mitigate this, use systemd’s watchdog features or explicit readiness checks. For example, you can useExecStartPre to verify that required files exist before launching the main process.
ExecStartPre=/usr/bin/test -f /run/myapp/token || exit 1
This pre-start check fails the service initialization if the token is missing, forcing systemd to retry or report an error immediately rather than letting the application run in a broken state.
Frequently Asked Questions
Can two systemd services share a global C++ object?
No, systemd units are completely isolated processes. They do not share memory spaces. You must use external mechanisms like files, databases, or network sockets to exchange complex objects.How do I pass sensitive credentials securely?
UseEnvironmentFile with restricted permissions (e.g., root-only readable) or use systemd’s LoadCredential feature available in newer versions, which provides secure, encrypted credential passing without exposing them in process listings.
What happens if the data file is deleted while a service is running?
If a service reads from a file, deleting it may cause errors depending on how the application handles file descriptors. UsingRuntimeDirectory ensures the directory exists, but you should still implement file existence checks in ExecStartPre to handle unexpected deletions gracefully.
Is there a way to share data between a systemd timer and its triggered service?
Yes, you can use a shared file inRuntimeDirectory or StateDirectory. The timer unit can trigger a service that writes data, and another service can read it. Ensure proper After= dependencies to guarantee the writer has executed before the reader attempts access.