web-dev-qa-db-fra.com

Limitez l'utilisation de la mémoire pour un seul processus Linux

J'exécute pdftoppm pour convertir un PDF PDF fourni par l'utilisateur en image 300 DPI). Cela fonctionne très bien, sauf si l'utilisateur fournit un PDF = avec une très grande taille de page. pdftoppm allouera suffisamment de mémoire pour contenir une image 300 DPI de cette taille en mémoire, ce qui pour une page carrée de 100 pouces est de 100 * 300 * 100 * 300 * 4 octets par pixel = 3,5 Go. Un utilisateur malveillant pourrait simplement me donner un idiot-large PDF et causer toutes sortes de problèmes.

Donc, ce que j'aimerais faire, c'est mettre une sorte de limite stricte sur l'utilisation de la mémoire pour un processus enfant que je suis sur le point d'exécuter - il suffit que le processus meure s'il essaie d'allouer plus de, disons, 500 Mo de mémoire. Est-ce possible?

Je ne pense pas que ulimit puisse être utilisé pour cela, mais y a-t-il un équivalent à un processus?

172
Ben Dilts

Il y a quelques problèmes avec ulimit. Voici une lecture utile sur le sujet: Limiter la consommation de temps et de mémoire d'un programme sous Linux , ce qui conduit à l'outil timeout , qui vous permet de mettre en cage un processus (et ses fourches) ) par la consommation de temps ou de mémoire.

L'outil de temporisation nécessite Perl 5+ et le /proc système de fichiers monté. Après cela, vous copiez l'outil dans par exemple /usr/local/bin ainsi:

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

Après cela, vous pouvez `` mettre en cage '' votre processus par la consommation de mémoire comme dans votre question comme ceci:

timeout -m 500 pdftoppm Sample.pdf

Vous pouvez également utiliser -t <seconds> et -x <hertz> pour limiter respectivement le processus par des contraintes de temps ou de CPU.

Cet outil fonctionne en vérifiant plusieurs fois par seconde si le processus généré n'a pas sursouscrit ses limites définies. Cela signifie qu'il existe en fait une petite fenêtre où un processus pourrait potentiellement être sursouscrit avant les délais d'expiration et tue le processus.

Une approche plus correcte impliquerait donc probablement des cgroups, mais cela est beaucoup plus compliqué à configurer, même si vous utiliseriez Docker ou runC, qui, entre autres, offrent une abstraction plus conviviale autour des cgroups.

68
kvz

Une autre façon de limiter cela est d'utiliser les groupes de contrôle de Linux. Cela est particulièrement utile si vous souhaitez limiter l'allocation de mémoire physique d'un processus (ou d'un groupe de processus) distinctement de la mémoire virtuelle. Par exemple:

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

va créer un groupe de contrôle nommé myGroup, limiter l'ensemble des processus exécutés sous myGroup jusqu'à 500 Mo de mémoire physique et jusqu'à 5000 Mo de swap. Pour exécuter un processus sous le groupe de contrôle:

cgexec -g memory:myGroup pdftoppm

Notez que sur une distribution Ubuntu moderne, cet exemple nécessite l'installation de cgroup-bin package et modification /etc/default/grub changer GRUB_CMDLINE_LINUX_DEFAULT à:

GRUB_CMDLINE_LINUX_DEFAULT="cgroup_enable=memory swapaccount=1"

puis en exécutant Sudo update-grub et redémarrage pour démarrer avec les nouveaux paramètres de démarrage du noyau.

129
user65369

Si votre processus ne génère pas plus d'enfants qui consomment le plus de mémoire, vous pouvez utiliser la fonction setrlimit . Une interface utilisateur plus courante pour cela utilise la commande ulimit du Shell:

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

Cela limitera uniquement la mémoire "virtuelle" de votre processus, en tenant compte - et en limitant - la mémoire que le processus invoqué partage avec d'autres processus, et la mémoire mappée mais non réservée (par exemple, le grand tas de Java). Pourtant, la mémoire virtuelle est l'approximation la plus proche pour les processus qui deviennent vraiment grands, ce qui rend lesdites erreurs insignifiantes.

Si votre programme génère des enfants et que ce sont eux qui allouent la mémoire, cela devient plus complexe et vous devez écrire des scripts auxiliaires pour exécuter les processus sous votre contrôle. J'ai écrit dans mon blog, pourquoi et comment .

81
P Shved

Sur n'importe quelle distribution basée sur systemd, vous pouvez également utiliser des groupes de contrôle indirectement via systemd-run. Par exemple. pour votre cas de limitation de pdftoppm à 500 Mo de RAM, utilisez:

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

Remarque: cela vous demandera un mot de passe, mais l'application sera lancée en tant qu'utilisateur. Ne laissez pas cela vous faire croire que la commande a besoin de Sudo, car cela entraînerait l'exécution de la commande sous root, ce qui n'était pas vraiment votre intention.

Si vous ne voulez pas entrer le mot de passe (après tout, en tant qu'utilisateur vous possédez votre mémoire, pourquoi auriez-vous besoin d'un mot de passe pour le limiter) , vous pourrait utiliser --user option, mais pour que cela fonctionne, vous aurez besoin de la prise en charge de cgroupsv2, qui pour le moment nécessite de démarrer avec systemd.unified_cgroup_hierarchy paramètre du noya .

15
Hi-Angel

J'utilise le script ci-dessous, qui fonctionne très bien. Il utilise des groupes de contrôle via cgmanager. Mise à jour: il utilise désormais les commandes de cgroup-tools. Nommez ce script limitmem et mettez-le dans votre $ PATH et vous pouvez l'utiliser comme limitmem 100M bash. Cela limitera à la fois l'utilisation de la mémoire et du swap. Pour limiter uniquement la mémoire, supprimez la ligne avec memory.memsw.limit_in_bytes.

edit: Sur les installations Linux par défaut, cela limite uniquement l'utilisation de la mémoire, pas l'échange. Pour activer la limitation d'utilisation du swap, vous devez activer la comptabilité du swap sur votre système Linux. Pour ce faire, définissez/ajoutez swapaccount=1 dans /etc/default/grub donc ça ressemble à quelque chose

GRUB_CMDLINE_LINUX="swapaccount=1"

Exécutez ensuite Sudo update-grub et redémarrez.

Avertissement: je ne serais pas surpris si cgroup-tools aussi des ruptures à l'avenir. La bonne solution serait d'utiliser les api systemd pour la gestion de cgroup mais il n'y a pas d'outils de ligne de commande pour cela a.t.m.

#!/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
10
JanKanis

En plus des outils de daemontools, suggérés par Mark Johnson, vous pouvez également considérer chpst qui se trouve dans runit. Runit lui-même est fourni dans busybox, vous pouvez donc déjà l'avoir installé.

La page de manuel de chpst montre l'option:

-m octets limite la mémoire. Limitez le segment de données, le segment de pile, les pages physiques verrouillées et le total de tous les segments par processus à octets octets chacun.

7
Oz123

J'utilise Ubuntu 18.04.2 LTS et le script JanKanis ne fonctionne pas pour moi tout à fait comme il le suggère. Fonctionnement limitmem 100M script limite 100 Mo de RAM avec illimité swap.

Fonctionnement limitmem 100M -s 100M script échoue silencieusement car cgget -g "memory:$cgname" n'a pas de paramètre nommé memory.memsw.limit_in_bytes.

J'ai donc désactivé l'échange:

# 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`
4
d9ngle