Limitare l'utilizzo della memoria per un singolo processo Linux


152

Sto correndo pdftoppmper convertire un PDF fornito dall'utente in un'immagine da 300 DPI. Funziona alla grande, tranne se l'utente fornisce un PDF con dimensioni di pagina molto grandi. pdftoppmallocherà memoria sufficiente per contenere un'immagine di 300 DPI di quella dimensione in memoria, che per una pagina quadrata da 100 pollici è 100 * 300 * 100 * 300 * 4 byte per pixel = 3,5 GB. Un utente malintenzionato potrebbe semplicemente darmi un PDF sciocco e causare tutti i tipi di problemi.

Quindi quello che vorrei fare è mettere un certo limite all'utilizzo della memoria per un processo figlio che sto per eseguire: basta che il processo muoia se tenta di allocare più di, diciamo, 500 MB di memoria. È possibile?

Non credo che ulimit possa essere usato per questo, ma esiste un equivalente a un processo?


Forse docker?
Sridhar Sarnobat,

Risposte:


58

Ci sono alcuni problemi con ulimit. Ecco una lettura utile sull'argomento: Limitare il tempo e il consumo di memoria di un programma in Linux , che porta allo strumento di timeout , che consente di mettere in gabbia un processo (e i suoi fork) in base al tempo o al consumo di memoria.

Lo strumento di timeout richiede Perl 5+ e il /procfilesystem montato. Dopo di ciò copi lo strumento, ad esempio in questo /usr/local/binmodo:

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

Dopodiché, puoi 'mettere in gabbia' il tuo processo consumando memoria come nella tua domanda in questo modo:

timeout -m 500 pdftoppm Sample.pdf

In alternativa, è possibile utilizzare -t <seconds>e -x <hertz>limitare rispettivamente il processo in base al tempo o ai vincoli della CPU.

Il modo in cui funziona questo strumento è controllando più volte al secondo se il processo generato non ha sovrascritto i suoi limiti impostati. Ciò significa che in realtà esiste una piccola finestra in cui un processo potrebbe potenzialmente essere sovrascritto prima che il timeout si accorga e uccida il processo.

Un approccio più corretto dovrebbe quindi coinvolgere probabilmente i cgroups, ma è molto più complicato da configurare, anche se si utilizzasse Docker o runC, che tra l'altro offrono un'astrazione più user-friendly intorno ai cgroups.


Sembra funzionare per me ora (di nuovo?) Ma ecco la versione cache di google: webcache.googleusercontent.com/…
kvz

Possiamo usare il timeout insieme al tasket (dobbiamo limitare sia la memoria che i core)?
riscatto

7
Va notato che questa risposta non si riferisce coreutilsall'utilità standard linux con lo stesso nome! Pertanto, la risposta è potenzialmente pericolosa se in qualsiasi parte del tuo sistema, alcuni pacchetti hanno uno script che si aspetta timeoutsia il coreutilspacchetto standard di Linux ! Non sono a conoscenza del fatto che questo strumento sia impacchettato per distribuzioni come Debian.
user1404316

Il -t <seconds>vincolo uccide il processo dopo tanti secondi?
xxx374562,

116

Un altro modo per limitare questo è usare i gruppi di controllo di Linux. Ciò è particolarmente utile se si desidera limitare l'allocazione della memoria fisica di un processo (o gruppo di processi) in modo distinto dalla memoria virtuale. Per esempio:

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

creerà un gruppo di controllo denominato myGroup, limiterà l'insieme di processi eseguiti in myGroup fino a 500 MB di memoria fisica e fino a 5000 MB di scambio. Per eseguire un processo nel gruppo di controllo:

cgexec -g memory:myGroup pdftoppm

Nota che su una moderna distribuzione Ubuntu questo esempio richiede l'installazione del cgroup-binpacchetto e la modifica /etc/default/grubper passare GRUB_CMDLINE_LINUX_DEFAULTa:

GRUB_CMDLINE_LINUX_DEFAULT="cgroup_enable=memory swapaccount=1"

e quindi eseguire sudo update-grube riavviare per avviare con i nuovi parametri di avvio del kernel.


3
Il firejailprogramma ti consentirà anche di avviare un processo con limiti di memoria (utilizzando cgroups e namespace per limitare più della semplice memoria). Sui miei sistemi non ho dovuto cambiare la riga di comando del kernel per farlo funzionare!
Ned64,

1
È necessaria la GRUB_CMDLINE_LINUX_DEFAULTmodifica per rendere persistente l'impostazione? Ho trovato un altro modo per renderlo persistente qui .
Stason,


Sarebbe utile notare in questa risposta che su alcune distribuzioni (ad es. Ubuntu) su cgcreate è richiesto sudo, e anche i comandi successivi a meno che non venga data l'autorizzazione all'utente corrente. Ciò eviterebbe al lettore di trovare queste informazioni altrove (ad es. Askubuntu.com/questions/345055 ). Ho suggerito una modifica in tal senso ma è stata respinta.
stewbasic,

77

Se il processo non genera più bambini che consumano più memoria, è possibile utilizzare la setrlimitfunzione. L'interfaccia utente più comune per questo sta usando il ulimitcomando della shell:

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

Ciò limiterà solo la memoria "virtuale" del processo, tenendo conto e limitando la memoria che il processo invocato condivide con altri processi e la memoria mappata ma non riservata (ad esempio, il grande heap di Java). Tuttavia, la memoria virtuale è l'approssimazione più vicina per i processi che diventano davvero grandi, rendendo insignificanti questi errori.

Se il tuo programma genera figli, ed è loro che alloca memoria, diventa più complesso e dovresti scrivere script ausiliari per eseguire processi sotto il tuo controllo. Ho scritto nel mio blog, perché e come .


2
perché è setrlimitpiù complesso per più bambini? man setrlimitmi dice che "Un processo figlio creato tramite fork (2) eredita i limiti delle risorse dei suoi genitori. I limiti delle risorse sono preservati attraverso execve (2)"
Akira,

6
Perché il kernel non somma la dimensione VM per tutti i processi figlio; se lo facesse, la risposta sarebbe comunque sbagliata. Il limite è per processo ed è lo spazio degli indirizzi virtuali, non l'utilizzo della memoria. L'uso della memoria è più difficile da misurare.
Mark R

1
se capisco correttamente la domanda allora OP qual è il limite per sottoprocesso (figlio) .. non in totale.
Akira,

@MarkR, comunque, lo spazio di indirizzi virtuali è una buona approssimazione per la memoria utilizzata, specialmente se si esegue un programma non controllato da una macchina virtuale (ad esempio Java). Almeno non conosco metriche migliori.

2
Volevo solo dire grazie - questo ulimitapproccio mi ha aiutato con firefoxil bug 622816 - Il caricamento di un'immagine di grandi dimensioni può "bloccare" firefox o bloccare il sistema ; che su un avvio USB (dalla RAM) tende a congelare il sistema operativo, richiedendo un riavvio forzato; ora almeno si firefoxschianta da solo, lasciando vivo il sistema operativo ... Saluti!
sdaau,

8

Sto usando lo script seguente, che funziona alla grande. Usa cgroups attraverso cgmanager. Aggiornamento: ora utilizza i comandi di cgroup-tools. Assegna un nome a questo script limitmeme inseriscilo nel tuo $ PATH e puoi usarlo come limitmem 100M bash. Ciò limiterà l'utilizzo della memoria e dello scambio. Per limitare solo la memoria, rimuovere la riga con memory.memsw.limit_in_bytes.

modifica: nelle installazioni Linux predefinite ciò limita solo l'utilizzo della memoria, non l'utilizzo dello scambio. Per abilitare la limitazione dell'utilizzo degli swap, è necessario abilitare l'account swap sul proprio sistema Linux. Fallo impostando / aggiungendo swapaccount=1in /etc/default/grubmodo che assomigli a qualcosa del genere

GRUB_CMDLINE_LINUX="swapaccount=1"

Quindi eseguire sudo update-grube riavviare.

Disclaimer: non sarei sorpreso se cgroup-toolsanche si rompe in futuro. La soluzione corretta sarebbe quella di utilizzare le API di systemd per la gestione dei cgroup, ma non ci sono strumenti da riga di comando per quell'atm

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

1
call to cgmanager_create_sync failed: invalid requestper ogni processo con cui cerco di eseguire limitmem 100M processname. Sono su Xubuntu 16.04 LTS e quel pacchetto è installato.
Aaron Franke,

Ups, ricevo questo messaggio di errore: $ limitmem 400M rstudio limiting memory to 400M (cgroup limitmem_24575) for command rstudio Error org.freedesktop.DBus.Error.InvalidArgs: invalid request qualche idea?
R Kiselev,

@RKiselev cgmanager è obsoleto e non è nemmeno disponibile in Ubuntu 17.10. L'API systemd che utilizza è stata modificata ad un certo punto, quindi è probabilmente questo il motivo. Ho aggiornato lo script per utilizzare i comandi di cgroup-tools.
JanKanis,

se il calcolo per i percentrisultati è zero, il exprcodice di stato è 1 e questo script esce prematuramente. consiglio di cambiare la riga in: percent=$(( "$peak_mem" / $(( "$bytes_limit" / 100 )) ))(ref: unix.stackexchange.com/questions/63166/… )
Willi Ballenthin,

come posso configurare cgroup per terminare il mio processo se vado oltre il limite?
d9ngle

7

Oltre agli strumenti di daemontools, suggeriti da Mark Johnson, puoi anche considerare chpstquale si trova in runit. Runit stesso è integrato busybox, quindi potresti già averlo installato.

La pagina man dichpst mostra l'opzione:

-m byte limitano la memoria. Limitare il segmento di dati, il segmento di stack, le pagine fisiche bloccate e il totale di tutti i segmenti per processo a byte ciascuno.


3

Sto eseguendo Ubuntu 18.04.2 LTS e lo script JanKanis non funziona per me come suggerisce. L'esecuzione limitmem 100M scriptsta limitando 100 MB di RAM con swap illimitato .

L'esecuzione limitmem 100M -s 100M scriptnon riesce in modo silenzioso poiché cgget -g "memory:$cgname"non ha alcun parametro denominato memory.memsw.limit_in_bytes.

Quindi ho disabilitato lo scambio:

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

@sourcejedi lo ha aggiunto :)
d9ngle

2
Bene, ho modificato la mia risposta. Per abilitare i limiti di swap è necessario abilitare la contabilità di swap sul proprio sistema. C'è un piccolo sovraccarico di runtime in questo, quindi non è abilitato di default su Ubuntu. Vedi la mia modifica.
JanKanis,

3

Su qualsiasi distribuzione basata su systemd è inoltre possibile utilizzare i cgroup indirettamente tramite systemd-run. Ad esempio, nel caso in cui si limiti pdftoppma 500 M di RAM, utilizzare:

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

Nota: questo ti chiederà una password ma l'app viene avviata come utente. Non permettere a questo di illuderti nel pensare che il comando abbia bisogno sudo, perché ciò causerebbe l'esecuzione del comando sotto root, che difficilmente era tua intenzione.

Se non vuoi inserire la password (dopotutto, come utente sei il proprietario della tua memoria, perché dovresti aver bisogno di una password per limitarla) , potresti usare l' --useropzione, tuttavia per farlo per funzionare avrai bisogno del supporto cgroupsv2 abilitato, che diritto ora richiede l'avvio con il systemd.unified_cgroup_hierarchyparametro kernel .


Grazie, ha reso la mia giornata
Geradlus_RU
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.