Обмежте використання пам'яті для одного процесу Linux


151

Я працюю, pdftoppmщоб перетворити наданий користувачем PDF в зображення 300DPI. Це чудово працює, за винятком випадків, коли користувач надає PDF з дуже великим розміром сторінки. pdftoppmвиділить достатньо пам’яті, щоб вмістити в пам’яті зображення 300DPI такого розміру, яке на 100-дюймовій квадратній сторінці становить 100 * 300 * 100 * 300 * 4 байти на піксель = 3,5 Гб. Зловмисний користувач міг просто надати мені дурно-великий PDF-файл і викликати всілякі проблеми.

Тож, що я хотів би зробити, це поставити якийсь жорсткий ліміт на використання пам’яті для дочірнього процесу, який я збираюся запустити - просто майте процес загинути, якщо він намагатиметься виділити більше, ніж, скажімо, 500 Мб пам'яті. Це можливо?

Я не думаю, що для цього можна використовувати ulimit, але чи існує однопроцесовий еквівалент?


Можливо docker?
Шрідхар Сарнобат

Відповіді:


58

Є деякі проблеми з ulimit. Ось корисне прочитання на тему: Обмеження часу та споживання пам’яті програми в Linux , що призводить до інструменту тайм-аут , який дозволяє кегнути процес (і його вилки) за часом та витратою пам’яті.

Інструмент тайм-ауту вимагає Perl 5+ та встановлена /procфайлова система. Після цього ви скопіюєте інструмент, наприклад, /usr/local/binтак:

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

Після цього ви можете «кегерувати» процес шляхом споживання пам’яті, як у вашому запитанні так:

timeout -m 500 pdftoppm Sample.pdf

Крім того, ви можете використовувати -t <seconds>та -x <hertz>відповідно обмежувати процес за часом або обмеженням процесора.

Спосіб роботи цього інструмента полягає у перевірці декількох разів на секунду, якщо породжений процес не підписав встановлені межі. Це означає, що насправді є невелике вікно, де процес може потенційно переплачувати перед сповіщенням про час очікування та вбивати процес.

Більш правильний підхід, отже, може включати групи, але це набагато більше стосується створення, навіть якщо ви використовуєте Docker або runC, які, крім усього іншого, пропонують більш зручну абстракцію навколо груп.


Здається, зараз для мене працює (знову?), Але ось версія кеша google: webcache.googleusercontent.com/…
kvz

Чи можемо ми використовувати тайм-аут разом із набором завдань (нам потрібно обмежити і пам'ять, і ядра)?
ransh

7
Слід зазначити, що ця відповідь не стосується однойменної coreutilsутиліти linux ! Таким чином, відповідь потенційно небезпечна, якщо десь у вашій системі, якийсь пакет має сценарій, який очікує timeoutстандартний coreutilsпакет Linux ! Мені не відомо, що цей інструмент упаковується для дистрибутивів, таких як debian.
користувач1404316

Чи -t <seconds>обмеження вбиває процес через багато секунд?
xxx374562

116

Ще один спосіб обмежити це - використовувати контрольні групи Linux. Це особливо корисно, якщо ви хочете обмежити розподіл фізичної пам'яті (або групи процесів) фізичної пам'яті від віртуальної пам'яті. Наприклад:

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

створить контрольну групу з назвою myGroup, обмежить набір процесів, що виконуються в моїй групі до 500 Мб фізичної пам'яті та до 5000 МБ свопу. Для запуску процесу в контрольній групі:

cgexec -g memory:myGroup pdftoppm

Зауважте, що в сучасному дистрибутиві Ubuntu цей приклад вимагає встановлення cgroup-binпакета та редагування /etc/default/grubдля зміни GRUB_CMDLINE_LINUX_DEFAULTна:

GRUB_CMDLINE_LINUX_DEFAULT="cgroup_enable=memory swapaccount=1"

а потім запустити sudo update-grubта перезавантажити для завантаження нові параметри завантаження ядра.


3
firejailПрограма також дозволить вам почати процес з обмеженнями пам'яті ( з використанням контрольних груп і просторів імен , щоб обмежити більше , ніж просто пам'ять). У моїх системах мені не потрібно було змінювати командний рядок ядра, щоб це працювало!
Ned64

1
Вам потрібна GRUB_CMDLINE_LINUX_DEFAULTмодифікація, щоб зробити налаштування стійким? Я знайшов ще один спосіб зробити його стійким тут .
стасон


Було б корисно відзначити у цій відповіді, що для деяких дистрибутивів (наприклад, Ubuntu) для cgcreate потрібен sudo, а також пізніші команди, якщо дозволу не надано поточному користувачеві. Це врятувало б читача від необхідності пошуку цієї інформації в іншому місці (наприклад, askubuntu.com/questions/345055 ). Я запропонував змінити в цьому сенсі, але він був відхилений.
тушканчик

77

Якщо ваш процес не породить більше дітей, які споживають найбільше пам'яті, ви можете використовувати setrlimitфункцію. Більш поширений інтерфейс користувача для використання ulimitкоманди оболонки:

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

Це обмежить лише "віртуальну" пам'ять вашого процесу, враховуючи та обмежуючи - пам'ять, на яку посилається процес, ділиться з іншими процесами, а пам'ять, відображена на карті, але не зарезервована (наприклад, велика купа Java). Проте віртуальна пам'ять є найближчим наближенням до процесів, які зростають дійсно великими, що робить ці помилки незначними.

Якщо ваша програма нереститься дітям, і саме вони виділяють пам'ять, вона стає складнішою, і вам слід написати допоміжні сценарії для запуску процесів під вашим контролем. Я написав у своєму блозі, чому і як .


2
чому setrlimitскладніше для більшої кількості дітей? man setrlimitговорить мені, що "Дочірній процес, створений за допомогою fork (2), успадковує обмеження ресурсів своїх батьків. Обмеження ресурсів зберігається через execve (2)"
akira

6
Оскільки ядро ​​не підсумовує розмір vm для всіх дочірніх процесів; якби це зробило, то відповідь все одно отримає неправильну відповідь. Обмеження - це процес, і це віртуальний адресний простір, а не використання пам'яті. Використання пам'яті важче виміряти.
МаркР

1
якщо я правильно розумію питання, то ОП, яка межа на підпроцес (дочірня) .. не в цілому.
акіра

@MarkR, у будь-якому випадку, віртуальний адресний простір є хорошим наближенням до використовуваної пам'яті, особливо якщо ви запускаєте програму, яка не контролюється віртуальною машиною (скажімо, Java). Принаймні, я не знаю кращої метрики.

2
Просто хотів сказати спасибі - цей ulimitпідхід допоміг мені з firefox«и помилкою 622816 - Завантаження великого зображення може" заморозити "браузер Firefox, або збій системи ; який на завантажувальному пристрої USB (з оперативної пам’яті) прагне заморозити ОС, вимагаючи жорсткого перезавантаження; тепер принаймні firefoxвиходить з ладу, залишаючи ОС живою ... Будьте здорові!
sdaau

8

Я використовую нижченаведений сценарій, який чудово працює. Він використовує групи через cgmanager. Оновлення: тепер він використовує команди з cgroup-tools. Назвіть цей скрипт limitmemі помістіть його у свій $ PATH, і ви можете ним користуватися, як limitmem 100M bash. Це обмежить використання пам'яті та обміну. Щоб обмежити лише пам'ять, видаліть рядок за допомогою memory.memsw.limit_in_bytes.

редагувати: У стандартних установках Linux це обмежує лише використання пам'яті, а не обмін. Щоб увімкнути обмеження користування свопом, вам потрібно включити обмін облік у вашій системі Linux. Зробіть це шляхом установки / додавання swapaccount=1в /etc/default/grubтак виглядає що - щось на зразок

GRUB_CMDLINE_LINUX="swapaccount=1"

Потім запустіть sudo update-grubі перезавантажте.

Відмова: Я не здивуюсь, якщо cgroup-toolsв майбутньому теж перерва. Правильним рішенням буде використання системного api для управління cgroup, але для цього 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 requestдля кожного процесу, з яким я намагаюся запустити limitmem 100M processname. Я на Xubuntu 16.04 LTS і цей пакет встановлений.
Аарон Франке

Джу, я отримую це повідомлення про помилку: $ limitmem 400M rstudio limiting memory to 400M (cgroup limitmem_24575) for command rstudio Error org.freedesktop.DBus.Error.InvalidArgs: invalid request будь-яка ідея?
R Кисельов

@RKiselev cgmanager зараз застарілий і навіть недоступний в Ubuntu 17.10. Системний api, який він використовує, в якийсь момент був змінений, тому, ймовірно, це причина. Я оновив сценарій для використання команд cgroup-tools.
JanKanis

якщо обчислення percentрезультатів дорівнює нулю, exprкод статусу дорівнює 1, і цей сценарій закінчується передчасно. рекомендую змінити лінію на: percent=$(( "$peak_mem" / $(( "$bytes_limit" / 100 )) ))(ref: unix.stackexchange.com/questions/63166/… )
Віллі Баллентін

як я можу налаштувати cgroup, щоб вбити процес, якщо я вийшов за межі?
d9ngle

7

Окрім інструментів daemontools, запропонованих Марком Джонсоном, ви також можете врахувати, chpstякий вміщено в runit. Сам Runit в комплекті busybox, тому ви, можливо, вже встановили його.

Сторінка людиниchpst показує опцію:

-м байт обмежувати пам'ять. Обмежте сегмент даних, сегмент стека, заблоковані фізичні сторінки та загальний обсяг усіх сегментів на один процес байтами на кожній байті.


3

Я запускаю Ubuntu 18.04.2 LTS, і сценарій JanKanis працює для мене не так, як він пропонує. Біг limitmem 100M scriptобмежує 100 Мб оперативної пам’яті з необмеженим свопом.

Запуск limitmem 100M -s 100M scriptпрацює беззвучно, оскільки cgget -g "memory:$cgname"не має жодного параметра memory.memsw.limit_in_bytes.

Тому я відключив своп:

# 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 додав його :)
d9ngle

2
Правильно, я відредагував свою відповідь. Щоб увімкнути обмеження свопів, вам потрібно включити своп-облік у своїй системі. Тут є невеликі накладні витрати, тому він не включений за замовчуванням на Ubuntu. Дивіться мою редакцію.
ЯнКаніс

2

У будь-якому дистрибутиві, що базується на системі, ви також можете використовувати групи непрямим шляхом через системний запуск. Наприклад, у випадку обмеження pdftoppmдо 500 млн оперативної пам’яті, використовуйте:

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

Примітка: це запитає вас про пароль, але додаток запускається як ваш користувач. Не дозволяйте цьому обманювати вас на думку про те, що команда потребує sudo, бо це призведе до запуску команди під корінь, що навряд чи було вашим наміром.

Якщо ви не хочете вводити пароль (зрештою, як власник вашої пам’яті, навіщо вам потрібен пароль, щоб обмежити його) , ви можете скористатися --userопцією, однак для цього вам потрібна підтримка cgroupsv2, яке право Тепер потрібно завантажити з systemd.unified_cgroup_hierarchyпараметром ядра .

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.