Limite el uso de memoria para un solo proceso de Linux

152

Estoy corriendo pdftoppmpara convertir un PDF proporcionado por el usuario en una imagen de 300 DPI. Esto funciona muy bien, excepto si el usuario proporciona un PDF con un tamaño de página muy grande. pdftoppmasignará suficiente memoria para contener una imagen de 300 DPI de ese tamaño en la memoria, que para una página cuadrada de 100 pulgadas es 100 * 300 * 100 * 300 * 4 bytes por píxel = 3.5GB. Un usuario malintencionado podría darme un PDF tonto y grande y causar todo tipo de problemas.

Entonces, lo que me gustaría hacer es poner algún tipo de límite estricto en el uso de memoria para un proceso secundario que estoy a punto de ejecutar; solo haga que el proceso muera si intenta asignar más de, digamos, 500 MB de memoria. ¿Es eso posible?

No creo que se pueda usar ulimit para esto, pero ¿hay un equivalente de un proceso?

Ben Dilts
fuente
Tal vez docker?
Sridhar Sarnobat

Respuestas:

58

Hay algunos problemas con ulimit. Aquí hay una lectura útil sobre el tema: Limitar el tiempo y el consumo de memoria de un programa en Linux , lo que lleva a la herramienta de tiempo de espera , que le permite enjaular un proceso (y sus tenedores) por tiempo o consumo de memoria.

La herramienta de tiempo de espera requiere Perl 5+ y el /procsistema de archivos montado. Después de eso, copie la herramienta para, p. Ej /usr/local/bin.

curl https://raw.githubusercontent.com/pshved/timeout/master/timeout | \
  sudo tee /usr/local/bin/timeout && sudo chmod 755 /usr/local/bin/timeout

Después de eso, puede 'enjaular' su proceso por consumo de memoria como en su pregunta de esta manera:

timeout -m 500 pdftoppm Sample.pdf

Alternativamente, podría usar -t <seconds>y -x <hertz>para limitar el proceso respectivamente por tiempo o restricciones de CPU.

La forma en que funciona esta herramienta es verificando varias veces por segundo si el proceso generado no ha suscrito en exceso sus límites establecidos. Esto significa que en realidad hay una pequeña ventana en la que un proceso podría estar suscribiéndose en exceso antes de que el tiempo de espera se dé cuenta y mate el proceso.

Por lo tanto, un enfoque más correcto probablemente involucraría a cgroups, pero eso es mucho más complicado de configurar, incluso si usa Docker o runC, que entre otras cosas, ofrece una abstracción más fácil de usar en torno a cgroups.

kvz
fuente
Parece estar funcionando para mí ahora (¿otra vez?) Pero aquí está la versión de caché de google: webcache.googleusercontent.com/…
kvz
¿Podemos usar el tiempo de espera junto con el conjunto de tareas (necesitamos limitar tanto la memoria como los núcleos)?
ransh
77
Cabe señalar que esta respuesta no se refiere a la coreutilsutilidad estándar de Linux del mismo nombre. Por lo tanto, la respuesta es potencialmente peligrosa si en algún lugar de su sistema, ¡algún paquete tiene un script que espera timeoutser el coreutilspaquete estándar de Linux ! No tengo conocimiento de que esta herramienta esté empaquetada para distribuciones como debian.
user1404316
¿La -t <seconds>restricción mata el proceso después de tantos segundos?
xxx374562
116

Otra forma de limitar esto es usar los grupos de control de Linux. Esto es especialmente útil si desea limitar la asignación de memoria física de un proceso (o grupo de procesos) de forma distinta a la memoria virtual. Por ejemplo:

cgcreate -g memory:myGroup
echo 500M > /sys/fs/cgroup/memory/myGroup/memory.limit_in_bytes
echo 5G > /sys/fs/cgroup/memory/myGroup/memory.memsw.limit_in_bytes

creará un grupo de control llamado myGroup, limitará el conjunto de procesos ejecutados bajo myGroup hasta 500 MB de memoria física y hasta 5000 MB de intercambio. Para ejecutar un proceso bajo el grupo de control:

cgexec -g memory:myGroup pdftoppm

Tenga en cuenta que en una distribución moderna de Ubuntu, este ejemplo requiere instalar el cgroup-binpaquete y editarlo /etc/default/grubpara cambiarlo GRUB_CMDLINE_LINUX_DEFAULTa:

GRUB_CMDLINE_LINUX_DEFAULT="cgroup_enable=memory swapaccount=1"

y luego ejecutar sudo update-gruby reiniciar para arrancar con los nuevos parámetros de arranque del kernel.

usuario65369
fuente
3
El firejailprograma también le permitirá iniciar un proceso con límites de memoria (utilizando cgroups y espacios de nombres para limitar más que solo la memoria). ¡En mis sistemas no tuve que cambiar la línea de comando del núcleo para que esto funcione!
Ned64
1
¿Necesita la GRUB_CMDLINE_LINUX_DEFAULTmodificación para que la configuración sea persistente? Encontré otra forma de hacerlo persistente aquí .
stason
Sería útil tener en cuenta en esta respuesta que en algunas distribuciones (por ejemplo, Ubuntu) se requiere sudo para cgcreate, y también los comandos posteriores, a menos que se otorgue permiso al usuario actual. Esto evitaría que el lector tenga que buscar esta información en otro lugar (por ejemplo, askubuntu.com/questions/345055 ). Sugerí una edición en este sentido, pero fue rechazado.
stewbasic
77

Si su proceso no genera más hijos que consumen la mayor cantidad de memoria, puede usar la setrlimitfunción. La interfaz de usuario más común para eso es usar el ulimitcomando del shell:

$ ulimit -Sv 500000     # Set ~500 mb limit
$ pdftoppm ...

Esto solo limitará la memoria "virtual" de su proceso, teniendo en cuenta, y limitando, la memoria que el proceso que se invoca comparte con otros procesos, y la memoria asignada pero no reservada (por ejemplo, el gran montón de Java). Aún así, la memoria virtual es la aproximación más cercana para procesos que crecen realmente grandes, haciendo que dichos errores sean insignificantes.

Si su programa genera hijos, y son ellos los que asignan memoria, se vuelve más complejo, y debe escribir scripts auxiliares para ejecutar procesos bajo su control. Escribí en mi blog, por qué y cómo .

P Shved
fuente
2
¿Por qué es setrlimitmás complejo para más niños? man setrlimitme dice que "Un proceso hijo creado mediante fork (2) hereda los límites de recursos de sus padres. Los límites de recursos se conservan en execve (2)"
akira
66
Porque el núcleo no suma el tamaño de vm para todos los procesos secundarios; si lo hiciera, obtendría la respuesta incorrecta de todos modos. El límite es por proceso, y es el espacio de direcciones virtuales, no el uso de memoria. El uso de la memoria es más difícil de medir.
MarkR
1
si entiendo la pregunta correctamente, entonces OP cuál es el límite por subproceso (hijo) ... no en total.
akira
@MarkR, de todos modos, el espacio de direcciones virtuales es una buena aproximación para la memoria utilizada, especialmente si ejecuta un programa que no está controlado por una máquina virtual (por ejemplo, Java). Al menos no conozco mejor métrica.
2
Solo quería decir gracias: este ulimitenfoque me ayudó con firefoxel error 622816: cargar una imagen grande puede "congelar" Firefox o bloquear el sistema ; que en un arranque USB (desde RAM) tiende a congelar el sistema operativo, lo que requiere un reinicio completo; ahora al menos se firefoxbloquea, dejando vivo el sistema operativo ... ¡Salud!
sdaau
8

Estoy usando el siguiente script, que funciona muy bien. Utiliza cgroups a través de cgmanager. Actualización: ahora utiliza los comandos de cgroup-tools. Nombra este script limitmemy ponlo en tu $ PATH y puedes usarlo como limitmem 100M bash. Esto limitará el uso de memoria y de intercambio. Para limitar solo la memoria, elimine la línea con memory.memsw.limit_in_bytes.

editar: en las instalaciones de Linux predeterminadas, esto solo limita el uso de memoria, no el uso de intercambio. Para habilitar la limitación de uso de intercambio, debe habilitar la contabilidad de intercambio en su sistema Linux. Hacer que al establecer / adición swapaccount=1en /etc/default/grublo que se ve algo como

GRUB_CMDLINE_LINUX="swapaccount=1"

Luego corre sudo update-gruby reinicia.

Descargo de responsabilidad: no me sorprendería si cgroup-toolstambién se rompe en el futuro. La solución correcta sería usar la API de systemd para la administración de cgroup, pero no hay herramientas de línea de comandos para ese cajero automático

#!/bin/sh

# This script uses commands from the cgroup-tools package. The cgroup-tools commands access the cgroup filesystem directly which is against the (new-ish) kernel's requirement that cgroups are managed by a single entity (which usually will be systemd). Additionally there is a v2 cgroup api in development which will probably replace the existing api at some point. So expect this script to break in the future. The correct way forward would be to use systemd's apis to create the cgroups, but afaik systemd currently (feb 2018) only exposes dbus apis for which there are no command line tools yet, and I didn't feel like writing those.

# strict mode: error if commands fail or if unset variables are used
set -eu

if [ "$#" -lt 2 ]
then
    echo Usage: `basename $0` "<limit> <command>..."
    echo or: `basename $0` "<memlimit> -s <swaplimit> <command>..."
    exit 1
fi

cgname="limitmem_$$"

# parse command line args and find limits

limit="$1"
swaplimit="$limit"
shift

if [ "$1" = "-s" ]
then
    shift
    swaplimit="$1"
    shift
fi

if [ "$1" = -- ]
then
    shift
fi

if [ "$limit" = "$swaplimit" ]
then
    memsw=0
    echo "limiting memory to $limit (cgroup $cgname) for command $@" >&2
else
    memsw=1
    echo "limiting memory to $limit and total virtual memory to $swaplimit (cgroup $cgname) for command $@" >&2
fi

# create cgroup
sudo cgcreate -g "memory:$cgname"
sudo cgset -r memory.limit_in_bytes="$limit" "$cgname"
bytes_limit=`cgget -g "memory:$cgname" | grep memory.limit_in_bytes | cut -d\  -f2`

# try also limiting swap usage, but this fails if the system has no swap
if sudo cgset -r memory.memsw.limit_in_bytes="$swaplimit" "$cgname"
then
    bytes_swap_limit=`cgget -g "memory:$cgname" | grep memory.memsw.limit_in_bytes | cut -d\  -f2`
else
    echo "failed to limit swap"
    memsw=0
fi

# create a waiting sudo'd process that will delete the cgroup once we're done. This prevents the user needing to enter their password to sudo again after the main command exists, which may take longer than sudo's timeout.
tmpdir=${XDG_RUNTIME_DIR:-$TMPDIR}
tmpdir=${tmpdir:-/tmp}
fifo="$tmpdir/limitmem_$$_cgroup_closer"
mkfifo --mode=u=rw,go= "$fifo"
sudo -b sh -c "head -c1 '$fifo' >/dev/null ; cgdelete -g 'memory:$cgname'"

# spawn subshell to run in the cgroup. If the command fails we still want to remove the cgroup so unset '-e'.
set +e
(
set -e
# move subshell into cgroup
sudo cgclassify -g "memory:$cgname" --sticky `sh -c 'echo $PPID'`  # $$ returns the main shell's pid, not this subshell's.
exec "$@"
)

# grab exit code 
exitcode=$?

set -e

# show memory usage summary

peak_mem=`cgget -g "memory:$cgname" | grep memory.max_usage_in_bytes | cut -d\  -f2`
failcount=`cgget -g "memory:$cgname" | grep memory.failcnt | cut -d\  -f2`
percent=`expr "$peak_mem" / \( "$bytes_limit" / 100 \)`

echo "peak memory used: $peak_mem ($percent%); exceeded limit $failcount times" >&2

if [ "$memsw" = 1 ]
then
    peak_swap=`cgget -g "memory:$cgname" | grep memory.memsw.max_usage_in_bytes | cut -d\  -f2`
    swap_failcount=`cgget -g "memory:$cgname" |grep memory.memsw.failcnt | cut -d\  -f2`
    swap_percent=`expr "$peak_swap" / \( "$bytes_swap_limit" / 100 \)`

    echo "peak virtual memory used: $peak_swap ($swap_percent%); exceeded limit $swap_failcount times" >&2
fi

# remove cgroup by sending a byte through the pipe
echo 1 > "$fifo"
rm "$fifo"

exit $exitcode
JanKanis
fuente
1
call to cgmanager_create_sync failed: invalid requestpara cada proceso que intento ejecutar limitmem 100M processname. Estoy en Xubuntu 16.04 LTS y ese paquete está instalado.
Aaron Franke
Ups, recibo este mensaje de error: ¿ $ limitmem 400M rstudio limiting memory to 400M (cgroup limitmem_24575) for command rstudio Error org.freedesktop.DBus.Error.InvalidArgs: invalid request alguna idea?
R Kiselev
@RKiselev cgmanager está en desuso ahora, y ni siquiera está disponible en Ubuntu 17.10. La API del sistema que utiliza se cambió en algún momento, por lo que probablemente esa sea la razón. He actualizado el script para usar los comandos de cgroup-tools.
JanKanis
Si el cálculo de los percentresultados es cero, el exprcódigo de estado es 1 y este script se cierra prematuramente. recomiendo cambiar la línea a: percent=$(( "$peak_mem" / $(( "$bytes_limit" / 100 )) ))(ref: unix.stackexchange.com/questions/63166/… )
Willi Ballenthin
¿Cómo puedo configurar cgroup para matar mi proceso si supero el límite?
d9ngle
7

Además de las herramientas de daemontools, sugeridas por Mark Johnson, también puede considerar chpstcuál se encuentra en runit. Runit en sí está incluido busybox, por lo que es posible que ya lo tenga instalado.

La página de manual dechpst muestra la opción:

-m bytes limitan la memoria. Limite el segmento de datos, el segmento de pila, las páginas físicas bloqueadas y el total de todos los segmentos por proceso a bytes bytes cada uno.

Oz123
fuente
3

Estoy ejecutando Ubuntu 18.04.2 LTS y el script JanKanis no funciona para mí como él sugiere. Correr limitmem 100M scriptes limitar 100 MB de RAM con intercambio ilimitado .

La ejecución limitmem 100M -s 100M scriptfalla en silencio ya que cgget -g "memory:$cgname"no tiene ningún parámetro nombrado memory.memsw.limit_in_bytes.

Entonces deshabilité el intercambio:

# create cgroup
sudo cgcreate -g "memory:$cgname"
sudo cgset -r memory.limit_in_bytes="$limit" "$cgname"
sudo cgset -r memory.swappiness=0 "$cgname"
bytes_limit=`cgget -g "memory:$cgname" | grep memory.limit_in_bytes | cut -d\  -f2`
d9ngle
fuente
@sourcejedi lo agregó :)
d9ngle
2
Correcto, edité mi respuesta. Para habilitar los límites de intercambio, debe habilitar la contabilidad de intercambio en su sistema. Hay una pequeña sobrecarga de tiempo de ejecución para que no esté habilitado de forma predeterminada en Ubuntu. Mira mi edición.
JanKanis
3

En cualquier distribución basada en systemd, también puede usar cgroups indirectamente a través de systemd-run. Por ejemplo, para su caso de limitación pdftoppma 500M de RAM, use:

systemd-run --scope -p MemoryLimit=500M pdftoppm

Nota: esto te pedirá una contraseña, pero la aplicación se iniciará como tu usuario. No permita que esto lo engañe y piense que el comando lo necesita sudo, porque eso haría que el comando se ejecute bajo la raíz, lo que difícilmente era su intención.

Si no desea ingresar la contraseña (después de todo, como usuario posee su memoria, ¿por qué necesitaría una contraseña para limitarla?) , --userPuede usar la opción, sin embargo, para que esto funcione, necesitará la compatibilidad con cgroupsv2 habilitada, ahora requiere arrancar con el systemd.unified_cgroup_hierarchyparámetro del kernel .

Hola angel
fuente
Gracias, me
alegraron el