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?
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.
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.
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 .
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 .
Používám níže uvedený skript, který funguje skvěle. Používá cgroups prostřednictvím Aktualizace: nyní používá příkazy od cgmanager
.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
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ů.
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`