Introducción
systemd-run permite lanzar cualquier comando dentro de una unidad transitoria de systemd, es decir, una unidad que se crea al vuelo y desaparece cuando el proceso termina, sin necesidad de escribir un archivo .service. Como cada unidad tiene su propio cgroup, se le pueden aplicar las mismas directrices de control de recursos que a un servicio normal mediante la opción -p (--property).
Requisitos:
- cgroups v2, que es lo predeterminado en cualquier distribución moderna. Se comprueba con
stat -fc %T /sys/fs/cgroup, que debe devolvercgroup2fs. - Con
--userel comando corre bajo el gestor de usuario, sin sudo, pero solo se pueden usar los controladores que systemd delega al usuario. Por defecto soncpu,memoryypids:
cat /sys/fs/cgroup/user.slice/user-$(id -u).slice/user@$(id -u).service/cgroup.controllers
Para delegar más controladores (por ejemplo cpuset para limitar núcleos o io para disco):
sudo systemctl edit user@.service
[Service]
Delegate=cpu cpuset io memory pids
Después hay que cerrar sesión y volver a entrar, o reiniciar.
Creando procesos como scope
systemd-run puede crear dos tipos de unidades transitorias:
| Tipo | Opción | Comportamiento |
|---|---|---|
.service |
Por defecto | Lo lanza systemd en segundo plano, con un entorno limpio, y la salida va al journal |
.scope |
--scope |
Lo lanza la propia terminal en primer plano, hereda el entorno y la salida se ve en la terminal |
Para programas interactivos o gráficos lo más cómodo es --scope, ya que el programa se comporta exactamente igual que si se lanzara directamente, solo que dentro de su propio cgroup:
systemd-run --user --scope -p MemoryMax=2G firefox
Opciones útiles:
-u nombre/--unit=nombre: nombre de la unidad. Si no se indica, se genera uno tiporun-r3f2a....scope.--slice=nombre.slice: agrupa varias unidades bajo un mismo slice, de modo que el límite se reparte entre todas.-pse puede repetir tantas veces como propiedades se quieran aplicar.-q/--quiet: no muestra el mensaje con el nombre de la unidad.- Solo para
.service:--ptyconecta la terminal al proceso,--waitespera a que termine y--collectdescarga la unidad aunque falle.
Consultar el consumo y modificar los límites en caliente:
systemctl --user status firefox.scope # Estado, consumo y límites
systemd-cgtop # Consumo por cgroup en tiempo real
systemctl --user set-property --runtime firefox.scope MemoryMax=1G
Para no escribir todo cada vez, se puede añadir una función al .zshrc / .bashrc:
limitar() { systemd-run --user --scope -q "$@"; }
limitar -p MemoryMax=1G -p CPUQuota=50% programa
Limitar CPU
Limitar uso de CPU
CPUQuota=: Límite duro de tiempo de CPU, en porcentaje de un núcleo:50%es medio núcleo y200%son dos núcleos completos. Aunque el sistema esté libre, el proceso nunca pasa de ese límite.CPUWeight=: Prioridad relativa, de 1 a 10000 (por defecto 100). Solo tiene efecto cuando hay competencia por la CPU; si el sistema está libre, el proceso usa toda la que necesite.CPUWeight=idlele da la mínima prioridad posible.--nice=: Ajusta el nice clásico (de -20 a 19). Los valores negativos requieren root.
# Compilar sin pasar de 2 núcleos
systemd-run --user --scope -p CPUQuota=200% make -j8
# Tarea de fondo que solo usa la CPU sobrante
systemd-run --user --scope -p CPUWeight=idle ffmpeg -i entrada.mkv salida.mp4
Limitar núcleos
A diferencia de CPUQuota, aquí no se limita cuánto tiempo de CPU usa el proceso sino en qué núcleos puede ejecutarse. Los núcleos disponibles se ven con lscpu -e. Hay tres formas:
AllowedCPUs=: Usa el controladorcpusetdel cgroup. Acepta rangos y listas:0-3,0,2,4. Funciona con--scope, pero en modo--userrequiere haber delegadocpuset(ver Introducción); si no, la propiedad simplemente se ignora.CPUAffinity=: Usasched_setaffinity(), por lo que no necesita cgroups ni delegación, pero solo funciona en unidades.service(sin--scope), ya que es systemd quien lanza el proceso.taskset: Dentro de un scope, sin delegación ni servicios.
# Con cpuset delegado
systemd-run --user --scope -p AllowedCPUs=0-3 blender
# Sin delegación, como servicio
systemd-run --user --pty -p CPUAffinity=0-3 ./script.sh
# Sin delegación, como scope
systemd-run --user --scope taskset -c 0-3 blender
Para comprobar en qué núcleos corre un proceso: taskset -cp <PID>.
Limitar Ram
| Directriz | Descripción |
|---|---|
MemoryMax= |
Límite duro. Si se supera, el OOM killer mata el proceso |
MemoryHigh= |
Límite blando. Al superarlo el kernel ralentiza el proceso y lo obliga a liberar memoria (moviéndola a swap), pero no lo mata |
MemorySwapMax= |
Swap máxima que puede usar. 0 le desactiva la swap |
MemoryLow=, MemoryMin= |
Lo contrario: memoria protegida que el kernel intenta no reclamarle |
Los valores aceptan sufijos K, M, G y T (base 1024), un porcentaje de la RAM física (MemoryMax=25%) o infinity.
Lo recomendable es combinar MemoryHigh un poco por debajo de MemoryMax, de manera que el proceso se ralentice antes de llegar al límite duro y que el OOM killer sea solo el último recurso:
systemd-run --user --scope -p MemoryHigh=3G -p MemoryMax=4G -p MemorySwapMax=1G chromium
Si se usa MemorySwapMax=0 junto a MemoryMax, al llegar al límite el proceso muere directamente sin pasar por la swap.
Limitar red
cgroups v2 no tiene controlador de red, así que systemd no puede limitar el ancho de banda, pero sí controlar a dónde puede conectarse un proceso. Estas directrices funcionan mediante BPF, por lo que requieren el gestor del sistema (sudo, sin --user); en el gestor de usuario no están disponibles. Para no ejecutar el programa como root se usa --uid:
| Directriz | Descripción |
|---|---|
IPAddressDeny=any |
Bloquea todo el tráfico IP |
IPAddressAllow= |
Excepciones a lo anterior: localhost, link-local, multicast, 192.168.1.0/24... |
RestrictNetworkInterfaces= |
Solo permite ciertas interfaces, o las prohíbe si se antepone ~ |
PrivateNetwork=yes |
El proceso solo ve su propia interfaz lo. Solo en .service |
IPAccounting=yes |
No limita, pero contabiliza los bytes enviados y recibidos (se ven en systemctl status) |
# Sin acceso a internet, solo red local
sudo systemd-run --scope --uid=$USER \
-p IPAddressDeny=any \
-p IPAddressAllow=localhost \
-p IPAddressAllow=192.168.1.0/24 \
programa
# Que solo pueda salir por la VPN
sudo systemd-run --scope --uid=$USER -p "RestrictNetworkInterfaces=lo wg0" programa
RestrictNetworkInterfaces también afecta a lo, por eso hay que incluirlo; si no, deja de funcionar la resolución DNS a través de systemd-resolved (127.0.0.53).
Al pasar por sudo el entorno se resetea, por lo que las aplicaciones gráficas pueden perder DISPLAY / WAYLAND_DISPLAY. En ese caso hay que usar sudo -E o pasarlas con -E DISPLAY=$DISPLAY.
Ancho de banda
Para limitar la velocidad hay que recurrir a herramientas externas:
- trickle: funciona en espacio de usuario mediante
LD_PRELOAD, así que no sirve con binarios estáticos (por ejemplo, los compilados en Go). Las velocidades van en KB/s:
systemd-run --user --scope trickle -s -d 500 -u 100 programa
- nftables + tc: la opción robusta. Se marcan los paquetes del cgroup con la expresión
socket cgroupv2de nftables y se limitan contc. Es bastante más complejo de configurar.
Resumen
| Recurso | Propiedad | --user |
--scope |
|---|---|---|---|
| Uso de CPU | CPUQuota, CPUWeight |
Sí | Sí |
| Núcleos | AllowedCPUs |
Delegando cpuset |
Sí |
| Núcleos | CPUAffinity |
Sí | No |
| RAM | MemoryMax, MemoryHigh, MemorySwapMax |
Sí | Sí |
| Red | IPAddressDeny, IPAddressAllow, RestrictNetworkInterfaces |
No, requiere sudo | Sí |
| Red | PrivateNetwork |
No, requiere sudo | No |
https://www.freedesktop.org/software/systemd/man/latest/systemd-run.html
https://www.freedesktop.org/software/systemd/man/latest/systemd.resource-control.html