Running Emacs in systemd's session.slice
November 12, 2023 – Leon Schuermann – #emacs #nix
I use Emacs and EXWM as my window manager. For this setup, running an
Emacs daemon as a systemd-user unit allows me to attach multiple
clients to this process (for instance, to work in the same session
through an SSH connection on my iPad), and have the daemon survive
restarts of my graphical session. However, all subprocesses started
from within Emacs—which are virtually all applications & shells on my
system—are then tracked within the same systemd scope. This means
that a single application consuming excessive amounts of memory can
bring down my entire user session (looking at you, Firefox). This post
documents how you can move this Emacs daemon from the systemd
app.slice
into the more appropriate session.slice
, and run
applications & shells from within Emacs in their own scopes in
app.slice
.
Table of Contents
1. Emacs As A Systemd User Unit
To run an Emacs daemon as a systemd user-unit on a NixOS system, it's as easy setting the following configuration option:
services.emacs.enable = true;
This will generate a systemd user-unit globally, for all users:
> systemctl --user status emacs.service ● emacs.service - Emacs: the extensible, self-documenting text editor Loaded: loaded (/etc/systemd/user/emacs.service; enabled; preset: enabled) Active: active (running) since Sun 2023-11-12 12:43:15 EST; 14min ago
On non-NixOS systems you can manually create a systemd user-unit that
simply starts emacs --daemon
with Type=forking
to achieve the same
result.
2. Systemd slices, scopes And services
However, when we use this daemon for a while and spawn some subprocesses, we can observe a systemd supervision tree such as the following:
> systemctl --user status ● caesium State: running Units: 356 loaded (incl. loaded aliases) Jobs: 0 queued Failed: 0 units Since: Fri 2023-11-03 13:09:42 EDT; 1 week 2 days ago systemd: 253.6 CGroup: /user.slice/user-1000.slice/user@1000.service ├─app.slice │ ├─emacs.service │ │ ├─ 527741 /run/current-system/sw/bin/fish │ │ ├─ 599026 bash /tmp/nix-shell-599026-0/rc │ │ ├─ 601605 fish │ │ ├─ 719460 /run/current-system/sw/bin/fish │ │ ├─1313249 bash /tmp/nix-shell-1313249-0/rc │ │ ├─1480430 /nix/store/85vasp6dpm5jldv45sqggxijcp910px6-erlang-25.3.2/lib/erlang/erts-13.2.2/bin/beam.smp> │ │ ├─1480452 erl_child_setup 1024 │ │ ├─2116444 /nix/store/5vx99s8cjzv8hcirly8g06alhjz0zaba-emacs-28.2/bin/emacs --daemon │ │ ├─2116678 /nix/store/sbr06rvajdmqdxhdf5rg9z3r87fifral-emacs-packages-deps/share/emacs/site-lisp/elpa/pd> │ │ ├─3378466 /run/current-system/sw/bin/fish │ │ ├─3408377 ssh root@10.237.4.2 │ │ ├─3408502 "ssh: /home/leons/.ssh/S.root@10.237.4.2:22 [mux]" │ │ └─3466757 /run/current-system/sw/bin/fish
We can see that the emacs.service
is managed as part of the
app.slice
systemd slice. Also, the emacs.service
unit tracks a
bunch of subprocesses, such as shells or programs I've started from
within Emacs. Let's check the systemd manual on slice units:
A slice unit is a concept for hierarchically managing resources of a group of processes. This management is performed by creating a node in the Linux Control Group (cgroup) tree. Units that manage processes (primarily scope and service units) may be assigned to a specific slice. For each slice, certain resource limits may be set that apply to all processes of all units contained in that slice.
So essentially, a slice is a collection of scope and service managed under a given cgroup. Systemd user sessions create a few such slices for us, namely:
session.slice
: Contains only processes essential to run the user’s graphical sessionapp.slice
: Contains all normal applications that the user is runningbackground.slice
: Useful for low-priority background tasks
And the manpage of systemd-oomd
tells us exactly why the above
situation on my system is not great:
Be aware that if you intend to enable monitoring and actions on
user.slice
,user-$UID.slice
, or their ancestor cgroups, it is highly recommended that your programs be managed by the systemd user manager to prevent running too many processes under the same session scope (and thus avoid a situation where memory intensive tasks trigger systemd-oomd to kill everything under the cgroup).
Thus, to conform with systemd's expectations, and to ultimately avoid having a single memory-hungry app kill our entire user-session, we should
- move the
emacs.service
into thesession.slice
, as it is very much essential to running our user session, and - run subprocesses started from within Emacs in
app.slice
, ideally in their own scope, such that they are assigned their own cgroup and thatsystemd-oomd
can apply more fine-grained out-of-memory policies.
3. Moving emacs.service
Into session.slice
To run emacs.service
as part of the session.slice
supervision
tree, we simply need to set the following option in the service's
[Unit]
section:
Slice=session.slice
In NixOS, we can achieve this by setting this option in the generated systemd user-unit configuration:
systemd.user.services.emacs.serviceConfig = { Slice = "session.slice"; };
Reloading the systemd user daemon (systemctl --user daemon-reload
)
and restarting the emacs.service
should now spawn it under the
session.slice
, as intended:
> systemctl --user status ● silicon CGroup: /user.slice/user-1000.slice/user@1000.service ├─app.slice ├─session.slice │ ├─emacs.service │ │ ├─1122918 /nix/store/p7pp0ix0wr7gaxjdz7r8bpcbx2cdfms5-emacs-28.2/bin/emacs --daemon [...]
However, all subprocesses launched from within Emacs are still
attached to emacs.service
, and thus now managed in the
session.slice
.
4. Running Emacs Subprocesses Under app.slice
Scopes
In general, we can instruct systemd to run a process under a new scope
using the systemd-run
command:
> systemd-run --user --scope -- sleep 60 Running scope as unit: run-r243444bafda04cc08f72aa350fa7175c.scope
This will create a new anonymous scope and launch the supplied command within it:
> systemctl --user status ● silicon CGroup: /user.slice/user-1000.slice/user@1000.service ├─app.slice │ ├─run-r243444bafda04cc08f72aa350fa7175c.scope │ │ └─1350382 /run/current-system/sw/bin/sleep 60 [...]
While systemd-run --user --scope
appears to default to creating
scopes in app.slice
, we can make that explicit by passing
--slice=app.slice
. Furthermore, to suppress the Running scope as
unit message, we can pass -q
(quiet). Now, starting subprocesses
in such scopes should be as easy as prefixing their commands with the
above systemd-run
incantation.
Unfortunately, I am not aware of a method to apply this to all subprocesses launched by Emacs. Given that I almost exclusively work in EXWM and vterm-mode, spawning GUI applications and vterm shells in their own scopes is sufficient for me. However, Emacs may spawn a plethora of other (potentially memory-intensive) applications such as through Magit, etc. If you know of a more general approach to run subprocesses in their own scopes, please let me know!
4.1. Running EXWM Applications In Their Own Scopes
EXWM defines the s-&
key-binding by default to spawn an application
without creating a buffer for its output. We can simply override the
definition of this key-binding to start the passed commands through
systemd-run
instead:
(exwm-input-set-key (kbd "s-&") (lambda (command) (interactive (list (read-shell-command "$ "))) (start-process-shell-command command nil (format "systemd-run -q --user --slice=app.slice --scope -- %s" command))))
4.2. Running Vterm Shell Processes In Systemd Scopes
vterm-mode supports the vterm-shell
customization, defaulting to the
contents of the shell-file-name
variable. We can simply customize
this variable to prefix the shell command with systemd-run
:
(use-package vterm :ensure t) (custom-set-variables '(vterm-shell (format "systemd-run -q --user --slice=app.slice --scope -- %s" shell-file-name)))