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

153

Je suis en cours pdftoppmd' exécution pour convertir un fichier PDF fourni par l'utilisateur en une image 300DPI. Cela fonctionne très bien, sauf si l'utilisateur fournit un fichier PDF avec un très grand format de page. pdftoppmallouera suffisamment de mémoire pour contenir en mémoire une image de 300 ppp de cette taille, ce qui, pour une page carrée de 100 pouces correspond à 100 * 300 * 100 * 300 * 4 octets par pixel = 3,5 Go. Un utilisateur malveillant pourrait simplement me donner un fichier PDF de taille stupide et causer toutes sortes de problèmes.

Donc, ce que je voudrais faire, c’est imposer une limite stricte à l’utilisation de la mémoire pour un processus enfant que je suis sur le point d’exécuter - faites en sorte que le processus disparaisse si vous essayez d’allouer plus de 500 Mo de mémoire, par exemple. Est-ce possible?

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

Ben Dilts
la source
Peut docker- être ?
Sridhar Sarnobat

Réponses:

59

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

L'outil de délai d'attente requiert Perl 5+ et le /procsystème de fichiers monté. Après cela, vous copiez l'outil /usr/local/bincomme par exemple :

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:

timeout -m 500 pdftoppm Sample.pdf

Vous pouvez également utiliser -t <seconds>et -x <hertz>pour limiter respectivement le processus en fonction des contraintes de temps ou d'UC.

Cet outil fonctionne en vérifiant plusieurs fois par seconde si le processus généré n'a pas dépassé ses limites. Cela signifie qu’il existe en fait une petite fenêtre dans laquelle un processus pourrait être potentiellement sursouscrit avant que les avis de dépassement de délai ne se produisent et que le processus ne soit plus mis à mort.

Une approche plus correcte impliquerait donc probablement des groupes de contrôle, mais elle est beaucoup plus complexe à configurer, même si vous utilisiez Docker ou runC, qui, entre autres choses, offre une abstraction plus conviviale autour des groupes de contrôle.

kvz
la source
Cela semble fonctionner pour moi maintenant (encore?), Mais voici la version de cache de Google: webcache.googleusercontent.com/…
kvz
Pouvons-nous utiliser le délai d’attente avec un ensemble de tâches (nous devons limiter la mémoire et les cœurs)?
Ransh
7
Il convient de noter que cette réponse ne fait pas référence à l' coreutilsutilitaire standard Linux du même nom! Ainsi, la réponse est potentiellement dangereuse si, quelque part sur votre système, un paquet contient un script qui s’attend timeoutà être le coreutilspaquet standard Linux ! Je ne suis pas au courant que cet outil soit empaqueté pour des distributions telles que debian.
user1404316
La -t <seconds>contrainte tue- t-elle le processus après tant de secondes?
xxx374562
117

Une autre façon de limiter cela consiste à utiliser les groupes de contrôle de Linux. Ceci est particulièrement utile si vous souhaitez limiter l'allocation de mémoire physique à un processus (ou à 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

créera un groupe de contrôle nommé myGroup, coiffera l’ensemble des processus exécutés sous myGroup jusqu’à 500 Mo de mémoire physique et jusqu’à 5 000 Mo d’échange. 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 du cgroup-binpackage et la modification /etc/default/grubpour passer GRUB_CMDLINE_LINUX_DEFAULTà:

GRUB_CMDLINE_LINUX_DEFAULT="cgroup_enable=memory swapaccount=1"

puis en cours d'exécution sudo update-grubet en redémarrant pour démarrer avec les nouveaux paramètres de démarrage du noyau.

utilisateur65369
la source
3
Le firejailprogramme vous permettra également de démarrer un processus avec des limites de mémoire (en utilisant des groupes de contrôle et des espaces de noms pour limiter davantage que la mémoire). Sur mes systèmes, je n'ai pas eu à changer la ligne de commande du noyau pour que cela fonctionne!
Ned64
1
Avez-vous besoin de la GRUB_CMDLINE_LINUX_DEFAULTmodification pour rendre le paramètre persistant? J'ai trouvé un autre moyen de le rendre persistant ici .
Stason
Il serait utile de noter dans cette réponse que sur certaines distributions (par exemple Ubuntu), sudo est requis pour cgcreate, ainsi que pour les commandes ultérieures, sauf autorisation de l'utilisateur actuel. Cela éviterait au lecteur de trouver cette information ailleurs (par exemple, askubuntu.com/questions/345055 ). J'ai suggéré une modification à cet effet mais elle a été rejetée.
stewbasic
77

Si votre processus ne génère pas plus d'enfants qui utilisent le plus de mémoire, vous pouvez utiliser la setrlimitfonction. L’interface utilisateur la plus courante utilise la ulimitcommande du shell:

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

Cela ne fera que limiter la mémoire "virtuelle" de votre processus, en prenant en compte et en limitant la mémoire du processus appelé partage avec d'autres processus, ainsi que la mémoire mappée mais non réservée (par exemple, le grand segment de mémoire de Java). Néanmoins, la mémoire virtuelle est l’approximation la plus proche pour les processus qui deviennent vraiment volumineux, rendant lesdites erreurs insignifiantes.

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

P Shved
la source
2
pourquoi est setrlimitplus complexe pour plus d'enfants? man setrlimit«Un processus enfant créé avec fork (2) hérite des limites de ressources de ses parents. Les limites de ressources sont préservées entre execve (2)»
Akira le
6
Parce que le noyau ne totalise pas la taille de la machine virtuelle pour tous les processus enfants; si c'était le cas, la réponse serait fausse de toute façon. La limite est par processus et constitue un espace d'adressage virtuel et non une utilisation de la mémoire. L'utilisation de la mémoire est plus difficile à mesurer.
MarkR
1
si je comprends bien la question, alors OP est la limite par sous-processus (enfant) .. pas au total.
Akira
De toute façon, @ MarkR, l'espace d'adressage virtuel est une bonne approximation de la mémoire utilisée, en particulier si vous exécutez un programme qui n'est pas contrôlé par une machine virtuelle (par exemple, Java). Au moins, je ne connais pas de meilleure métrique.
2
Je voulais juste dire merci - cette ulimitapproche m'a aidé avec firefoxle bogue 622816 - Le chargement d'une grande image peut "geler" Firefox ou faire planter le système ; qui sur un démarrage USB (à partir de la RAM) a tendance à geler le système d'exploitation, nécessitant un redémarrage forcé; maintenant au moins se firefoxbloque, laissant le système d'exploitation en vie ... A la vôtre!
Sdaau
8

J'utilise le script ci-dessous, qui fonctionne très bien. Il utilise cgroups à travers cgmanager. Mise à jour: il utilise maintenant les commandes de cgroup-tools. Nommez ce script limitmemet mettez-le dans votre $ PATH et vous pourrez l'utiliser comme limitmem 100M bash. Cela limitera l'utilisation de la mémoire et de l'échange. Pour limiter 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'utilisation de l'échange. Pour activer la limitation d'utilisation d'échange, vous devez activer la comptabilité d'échange sur votre système Linux. Faites cela en paramétrant / ajoutant swapaccount=1de /etc/default/grubsorte qu'il ressemble à quelque chose comme

GRUB_CMDLINE_LINUX="swapaccount=1"

Puis lancez sudo update-grubet redémarrez.

Disclaimer: Je ne serais pas surpris si cgroup-toolségalement des pauses dans le futur. La solution correcte consisterait à utiliser les API systemd pour la gestion des groupes de contrôle, mais il n'existe aucun outil de ligne de commande pour cet environnement.

#!/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
la source
1
call to cgmanager_create_sync failed: invalid requestpour chaque processus, j'essaie de courir avec limitmem 100M processname. Je suis sur Xubuntu 16.04 LTS et ce paquet est installé.
Aaron Franke
Ups, je reçois ce message d'erreur: $ limitmem 400M rstudio limiting memory to 400M (cgroup limitmem_24575) for command rstudio Error org.freedesktop.DBus.Error.InvalidArgs: invalid request une idée?
R Kiselev
@RKiselev cgmanager est obsolète et n'est même pas disponible dans Ubuntu 17.10. L'API systemd qu'elle utilise a été modifiée à un moment donné, c'est probablement la raison. J'ai mis à jour le script pour utiliser les commandes cgroup-tools.
JanKanis
si le calcul des percentrésultats est nul, le exprcode d'état est 1 et ce script se ferme prématurément. recommande de changer la ligne comme suit : percent=$(( "$peak_mem" / $(( "$bytes_limit" / 100 )) ))(ref: unix.stackexchange.com/questions/63166/… )
Willi Ballenthin
comment puis-je configurer cgroup pour tuer mon processus si je dépasse la limite?
D9ngle
7

Outre les outils de daemontools, suggérés par Mark Johnson, vous pouvez également considérer chpstlesquels se trouvent dans runit. Runit lui-même est intégré busybox, vous l'avez donc peut-être déjà installé.

La page de manuel dechpst montre l'option:

-m octets limitent 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 à des octets octets chacun.

Oz123
la source
3

J'utilise Ubuntu 18.04.2 LTS et le script JanKanis ne fonctionne pas pour moi tout à fait comme il le suggère. Courir, limitmem 100M scriptc’est limiter à 100 Mo de RAM avec un échange illimité .

L'exécution limitmem 100M -s 100M scriptéchoue silencieusement car cgget -g "memory:$cgname"aucun paramètre n'est 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`
déni
la source
@sourcejedi l'a ajouté :)
d9ngle
2
Oui, j'ai édité ma réponse. Pour activer les limites de swap, vous devez activer la comptabilité de swap sur votre système. Il y a une petite surcharge d'exécution à cela, donc il n'est pas activé par défaut sur Ubuntu. Voir mon édition.
JanKanis
3

Sur toute distribution basée sur systemd, vous pouvez également utiliser les groupes de contrôle indirectement via systemd-run. Par exemple, dans votre cas de limitation pdftoppmà 500 M de RAM, utilisez:

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

Remarque: ceci vous demandera un mot de passe mais l'application sera lancée en tant qu'utilisateur. Ne laissez pas ceci vous induire en erreur en pensant que la commande a besoin sudo, car cela entraînerait son exécution sous la racine, ce qui était à peine 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 pouvez utiliser l' --useroption, mais pour que cela fonctionne, vous aurez besoin de la prise en charge de cgroupsv2, nécessitesystemd.unified_cgroup_hierarchy maintenant de démarrer avec le paramètre du noyau .

Salut ange
la source
Merci, faites ma journée
Geradlus_RU Il y a