risposta-alla-domanda-sullo-sviluppo-web-bd.com

Limitare l'utilizzo della memoria per un singolo processo Linux

Sto eseguendo pdftoppm per 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. pdftoppm allocerà 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 PDFstupido 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?

172
Ben Dilts

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 timeout , che ti permette di mettere in gabbia un processo (e le sue forcelle ) per tempo o consumo di memoria.

Lo strumento di timeout richiede Perl 5+ e il /proc montato sul filesystem. Dopo di che copi lo strumento per es. /usr/local/bin così:

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 puoi usare -t <seconds> e -x <hertz> per 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.

68
kvz

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 chiamato 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 di cgroup-bin pacchetto e modifica /etc/default/grub cambiare GRUB_CMDLINE_LINUX_DEFAULT per:

GRUB_CMDLINE_LINUX_DEFAULT="cgroup_enable=memory swapaccount=1"

e quindi eseguendo Sudo update-grub e riavvio per l'avvio con i nuovi parametri di avvio del kernel.

129
user65369

Se il tuo processo non genera più bambini che consumano più memoria, puoi usare la funzione setrlimit . L'interfaccia utente più comune per questo sta usando il comando ulimit 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 allocano memoria, diventa più complesso e dovresti scrivere script ausiliari per eseguire processi sotto il tuo controllo. Ho scritto nel mio blog, perché e come .

81
P Shved

Su qualsiasi distribuzione basata su systemd è inoltre possibile utilizzare i cgroup indirettamente tramite systemd-run. Per esempio. per il caso di limitare pdftoppm a 500M 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 di Sudo, perché ciò causerebbe l'esecuzione del comando sotto root, che difficilmente era tua intenzione.

Se non si desidera inserire la password (dopotutto, come utente si possiede la propria memoria, perché è necessaria una password per limitarla) , si potrei usare --user opzione, tuttavia per far funzionare tutto ciò avrai bisogno del supporto cgroupsv2 abilitato, che in questo momento richiede l'avvio con systemd.unified_cgroup_hierarchy parametro del kernel .

15
Hi-Angel

Sto usando lo script seguente, che funziona alla grande. Usa cgroups attraverso cgmanager. Aggiorna: ora utilizza i comandi da cgroup-tools. Nome questo script limitmem e 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 questo limita solo l'uso 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=1 in /etc/default/grub quindi sembra qualcosa del genere

GRUB_CMDLINE_LINUX="swapaccount=1"

Quindi esegui Sudo update-grub e riavvia.

Disclaimer: non sarei sorpreso se cgroup-tools si rompe anche 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 tale 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 [email protected]" >&2
else
    memsw=1
    echo "limiting memory to $limit and total virtual memory to $swaplimit (cgroup $cgname) for command [email protected]" >&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 "[email protected]"
)

# 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

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

La pagina man di chpst 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.

7
Oz123

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

In esecuzione limitmem 100M -s 100M script fallisce silenziosamente come cgget -g "memory:$cgname" non ha alcun parametro chiamato 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`
4
d9ngle