it-swarm-eu.dev

Omezte využití paměti pro jeden proces v systému Linux

Používám pdftoppm pro převedení poskytovaného uživatelem PDF na obrázek 300DPI. Funguje to skvěle, s výjimkou případů, kdy uživatel poskytne PDF) = s velmi velkou velikostí stránky. pdftoppm přidělí dostatek paměti pro uložení obrazu 300DPI této velikosti v paměti, což je pro 100 čtvereční stránku čtverce 100 * 300 * 100 * 300 * 4 bajty na pixel = 3.5GB. Uživatel se zlými úmysly mi mohl dát jen hloupé velké PDF) a způsobit všechny druhy problémů.

To, co bych chtěl udělat, je dát nějaký pevný limit využití paměti pro podřízený proces, který se chystám spustit - prostě nechte proces zemřít, pokud se pokusí přidělit více než, řekněme, 500 MB paměti. Je to možné?

Nemyslím si, že na to může být použit ulimit, ale existuje ekvivalent jednoho procesu?

172
Ben Dilts

S ulimitem jsou nějaké problémy. Zde je užitečné přečíst si toto téma: Omezení času a spotřeby paměti programu v Linux , což vede k nástroji timeout , který vám umožní proces v kleci (a jeho vidličky) ) podle času nebo spotřeby paměti.

Nástroj timeout vyžaduje Perl 5+ a /proc připojený souborový systém. Poté zkopírujete nástroj do např. /usr/local/bin jako tak:

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

Poté můžete svůj proces „klecovat“ podle spotřeby paměti jako ve vaší otázce:

timeout -m 500 pdftoppm Sample.pdf

Nebo můžete použít -t <seconds> a -x <hertz> pro omezení procesu podle času nebo omezení CPU.

Způsob, jakým tento nástroj funguje, je několikanásobná kontrola za sekundu, pokud proces tření nepřesáhl stanovené hranice. To znamená, že ve skutečnosti existuje malé okno, ve kterém by proces mohl potenciálně být nadměrně odebírat před vypršením časového limitu a proces zabije.

Správnější přístup by tedy pravděpodobně zahrnoval cgroups, ale to je mnohem více zapojeno do nastavení, i když byste použili Docker nebo runC, což mimo jiné nabízí uživatelsky příjemnější abstrakci kolem cgroups.

68
kvz

Dalším způsobem, jak to omezit, je použití řídicích skupin Linuxu. To je zvláště užitečné, pokud chcete omezit přidělování fyzické paměti procesu (nebo skupině procesů ') zřetelně od virtuální paměti. Například:

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

vytvoří kontrolní skupinu s názvem myGroup, uzavře sadu procesů spuštěných v myGroup až do 500 MB fyzické paměti a do 5000 MB odkládacího prostoru. Spuštění procesu v rámci kontrolní skupiny:

cgexec -g memory:myGroup pdftoppm

Všimněte si, že v moderní distribuci Ubuntu tento příklad vyžaduje instalaci cgroup-bin balíček a úpravy /etc/default/grub změnit GRUB_CMDLINE_LINUX_DEFAULT do:

GRUB_CMDLINE_LINUX_DEFAULT="cgroup_enable=memory swapaccount=1"

a poté běží Sudo update-grub a restartujte počítač s novými bootovacími parametry jádra.

129
user65369

Pokud váš proces nevytvoří více dětí, které spotřebovávají nejvíce paměti, můžete použít funkci setrlimit . Běžnější uživatelské rozhraní pro to používá příkaz ulimit Shell:

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

Tím se omezí pouze „virtuální“ paměť vašeho procesu, přičemž se vezme v úvahu - a omezí - paměť, kterou vyvolává proces, sdílí s jinými procesy a paměť mapovanou, ale ne rezervovanou (například velká hromada Java). Virtuální paměť je přesto nejbližší aproximací pro procesy, které rostou opravdu velké, což činí uvedené chyby nevýznamnými.

Pokud váš program plodí děti a jsou to právě oni, kdo přidělují paměť, stává se složitější a měli byste psát pomocné skripty, abyste mohli spouštět procesy pod vaší kontrolou. Ve svém blogu jsem napsal proč a jak .

81
P Shved

Na každém systému založeném na systémud můžete také použít cgroups nepřímo prostřednictvím systemd-run. Např. pro případ omezení pdftoppm na 500M RAM použijte:

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

Poznámka: požádá vás o heslo, ale aplikace se spustí jako uživatel. Nedovolte, aby vás to urazilo v domnění, že příkaz potřebuje Sudo, protože by to způsobilo, že příkaz bude spuštěn v kořenovém adresáři, což byl stěží váš záměr.

Pokud nechcete heslo zadávat (koneckonců, jako uživatel, který vlastníte paměť, proč byste potřebovali heslo k jeho omezení) , měli byste mohl použít --user možnost, ale pro tuto práci budete potřebovat povolenou podporu cgroupsv2, která právě teď vyžaduje zavedení s systemd.unified_cgroup_hierarchy parametr jádra .

15
Hi-Angel

Používám níže uvedený skript, který funguje skvěle. Používá cgroups prostřednictvím cgmanager. Aktualizace: nyní používá příkazy od cgroup-tools. Pojmenujte tento skript limitmem a vložte jej do svého $ PATH a můžete jej použít jako limitmem 100M bash. Tím se omezí využití paměti i výměny. Chcete-li omezit pouze paměť, odeberte řádek pomocí memory.memsw.limit_in_bytes.

edit: Ve výchozích instalacích v systému Linux to omezuje pouze využití paměti, nikoli využití swapů. Chcete-li povolit omezení využití swapu, musíte povolit swapové účetnictví v systému Linux. To provedete nastavením/přidáním swapaccount=1 v /etc/default/grub tak to vypadá něco jako

GRUB_CMDLINE_LINUX="swapaccount=1"

Poté spusťte Sudo update-grub a restartujte počítač.

Zřeknutí se odpovědnosti: nebyl bych překvapen, kdyby cgroup-tools se také zlomí v budoucnosti. Správným řešením by bylo použít api's systemd pro správu cgroup, ale pro a.t.m neexistují žádné nástroje příkazového řádku.

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

Kromě nástrojů od daemontools, které navrhl Mark Johnson, můžete také zvážit chpst, která se nachází v runit. Samotný runit je součástí balíčku busybox, takže jej již možná máte nainstalovaný.

manuální stránka chpst ukazuje možnost:

-m bajtů omezuje paměť. Omezte segment dat, segment zásobníku, uzamčené fyzické stránky a celkový počet všech segmentů na proces na bajty bajtů.

7
Oz123

Používám Ubuntu 18.04.2 LTS a JanKanis skript pro mě nefunguje tak, jak navrhuje. Běh limitmem 100M script omezuje 100 MB RAM s neomezeno swapem).

Běh limitmem 100M -s 100M script tiše selže jako cgget -g "memory:$cgname" nemá žádný parametr s názvem memory.memsw.limit_in_bytes.

Takže jsem zakázal swap:

# 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