Note: the Python script used to run all the tests mentioned in this post can be found on my GitHub Gist: https://gist.github.com/piyueh/54b85e13800d42b9eccb47c7e0d6db7f
I’ve been struggling for a while to understand the difference between PartOf and BindsTo. The man page for systemd.unit says this about PartOf=:
Configures dependencies similar to
Requires=, but limited to stopping and restarting of units. When systemd stops or restarts the units listed here, the action is propagated to this unit. Note that this is a one-way dependency. Changes to this unit do not affect the listed units.
And for BindsTo=, it says:
Configures requirement dependencies, very similar in style to
Requires=. However, this dependency type is stronger: in addition to the effect ofRequires=it declares that if the unit bound to is stopped, this unit will be stopped too. This means a unit bound to another unit that suddenly enters inactive state will be stopped too. Units can suddenly, unexpectedly enter inactive state for different reasons: the main process of a service unit might terminate on its own choice, the backing device of a device unit might be unplugged or the mount point of a mount unit might be unmounted without involvement of the system and service manager.
For reference, here’s what the man page says about Requires=:
If this unit gets activated, the units listed will be activated as well. If one of the other units fails to activate, and an ordering dependency
After=on the failing unit is set, this unit will not be started. Besides, with or without specifyingAfter=, this unit will be stopped if one of the other units is explicitly stopped.
Maybe because I’m not a native English speaker, the difference was not clear to me. So I decided to do some experiments.
Experiments
I carried out three groups of experiments. Each group corresponds to one of Requires=, PartOf=, and BindsTo=.
In each group, a systemd unit a.service was kept the same. It served as the dependency of the second systemd unit, b.service. b.service depended on a.service using either Requires=, PartOf=, or BindsTo=.
Eight tests were done for each group. In other words, I ran a total of 24 tests. To keep the experiments in user space, a.service and b.service were saved in ~/.config/systemd/user/.
Here’s the content of a.service:
a.service
[Unit]Description=Test service A
[Service]Type=simpleExecStart=sh -c 'while true; do echo A is alive; sleep 3; done'The following are the contents of b.service in the three experiment groups:
-
Experiment group A: with
Requires=b.servicewithRequires=a.service
[Unit]Description=Test service B.Requires=a.service
[Service]Type=simpleExecStart=sh -c 'while true; do echo B is alive; sleep 3; done'-
Experiment group B: with
PartOf=b.servicewithPartOf=a.service
[Unit]Description=Test service B.PartOf=a.service
[Service]Type=simpleExecStart=sh -c 'while true; do echo B is alive; sleep 3; done'-
Experiment group C: with
BindsTo=b.servicewithBindsTo=a.service
[Unit]Description=Test service B.BindsTo=a.service
[Service]Type=simpleExecStart=sh -c 'while true; do echo B is alive; sleep 3; done'The following are the commands I ran in every experiment group, along with what I wanted to observe from the results:
- Test 1 ``` $ systemctl —user start a.service
*What I want to see*: if `b.service` also starts when `a.service` starts.2. Test 2 ``` $ systemctl --user start b.service *What I want to see*: if `a.service` also starts when `b.service` starts.3. Test 3 ``` systemctl —user stop a.service
*What I want to see*: if `b.service` also stops when `a.service` stops normally.4. Test 4 ``` $ systemctl --user start a.service b.service $ systemctl --user stop b.service *What I want to see*: if `a.service` also stops when `b.service` stops normally.5. Test 5 ``` kill -9 $(systemctl —user show a.service | grep -oP ’(?<=ExecMainPID=)\d*’)
*What I want to see*: if `b.service` stops when `a.service` stops abnormally (i.e., `a.service` is not stopped by `systemd`).6. Test 6 ``` $ systemctl --user start a.service b.service $ kill -9 $(systemctl --user show b.service | grep -oP '(?<=ExecMainPID=)\d*') *What I want to see*: if `a.service` stops when `b.service` stops abnormally (i.e., `b.service` is not stopped by `systemd`).7. Test 7 ``` systemctl —user restart a.service
*What I want to see*: if `b.service` also restarts when `a.service` restarts.8. Test 8 ``` $ systemctl --user start a.service b.service $ systemctl --user restart b.service *What I want to see*: if `a.service` also restarts when `b.service` restarts.Results
The following table shows the results. Each column represents an experiment group, and each row represents a test. Each result is either V/V, V/X, X/V, or X/X. They indicate whether a.service and b.service are running. For example, V/X means a.service is running while b.service has stopped.
| Requires | PartOf | BindsTo | |
|---|---|---|---|
| Test 1 | V/X | V/X | V/X |
| Test 2 | V/V | X/V | V/V |
| Test 3 | X/X | X/X | X/X |
| Test 4 | V/X | V/X | V/X |
| Test 5 | X/V | X/V | X/X |
| Test 6 | V/X | V/X | V/X |
| Test 7 | V/V | V/V | V/V |
| Test 8 | V/V | V/V | V/V |
From the results, we can ignore the tests in which we applied changes to the status of b.service (i.e., tests 4, 6, and 8), because changes to b.service were not propagated to a.service. This is expected behavior: b.service depended on a.service, but a.service did not depend on b.service.
One exception is when we started b.service (test 2). When we started b.service, a.service also started when using Requires and BindsTo. But when using PartOf, starting b.service had no effect on a.service.
Now let’s look at what happened to b.service when we changed the status of a.service (i.e., tests 1, 3, 5, and 7). In test 1, starting a.service did not trigger b.service to start. This is expected, because a.service did not know anything about b.service before b.service was loaded into systemd. So when a.service started, nothing happened to b.service.
Once both a.service and b.service were running, systemd knew about the existence of b.service and the dependency relationship. In test 3, when both services were running and then we stopped a.service, b.service also stopped no matter whether the dependency was Requires, PartOf, or BindsTo.
Now comes the interesting part, at least to me. In test 5, we simulated a situation in which a.service stopped unexpectedly. That is, a.service was not stopped by systemd or by any normal mechanism. It was killed with a SIGKILL signal to mimic a crash. In that case, b.service stopped only when using BindsTo. In other words, when using Requires and PartOf, b.service did not stop when a.service crashed unexpectedly.
Test 7 shows that when we restarted a.service, b.service also restarted regardless of whether the dependency was Requires, PartOf, or BindsTo.
Conclusion
Requires v.s. PartOf
The difference is that when b.service starts, Requires= also starts a.service. On the other hand, with PartOf=, starting b.service does not start a.service. This is why the man page says ”… similar to Requires=, but limited to stopping and restarting ….” Otherwise, PartOf behaves the same as Requires.
Requires v.s. BindsTo
BindsTo differs from Requires in test 5. When a.service stopped accidentally or abnormally, b.service stopped only when using BindsTo. The man page does say that with Requires, “explicitly” stopping a.service also stops b.service. But it is less clear about the same situation when the stop is unexpected. The BindsTo section explicitly says that unexpectedly stopping a.service also stops b.service.
PartOf v.s. BindsTo
My main interest is the difference between PartOf and BindsTo. They differ in tests 2 and 5. So basically, the difference is:
- Starting
b.servicedid not starta.servicewhen usingPartOf, but it did starta.servicewhen usingBindsTo. - When
a.servicestopped unexpectedly, usingBindsTocausedb.serviceto stop as well. But usingPartOfdid not.
The last
Finally, one thing to remember is that these tests do not consider the situation where we enable either service. If we use enable, an extra [Install] section is needed in the unit files. And in [Install], we have more options for configuring dependencies. Once we combine the dependency settings in [Unit] and [Install], things may become more complicated.