Ограничить использование памяти для одного процесса Linux

153

Я бегу, pdftoppmчтобы преобразовать предоставленный пользователем PDF в изображение с разрешением 300 точек на дюйм. Это прекрасно работает, за исключением случаев, когда пользователь предоставляет PDF с очень большим размером страницы. pdftoppmвыделит достаточно памяти для хранения изображения с разрешением 300 точек на дюйм такого размера, которое для 100-дюймовой квадратной страницы составляет 100 * 300 * 100 * 300 * 4 байта на пиксель = 3,5 ГБ. Злонамеренный пользователь может просто дать мне большой глупый PDF и вызвать все виды проблем.

Поэтому я хотел бы установить какое-то жесткое ограничение на использование памяти для дочернего процесса, который я собираюсь запустить - просто заставьте процесс умереть, если он попытается выделить больше, скажем, 500 МБ памяти. Это возможно?

Я не думаю, что ulimit может быть использован для этого, но есть ли однопроцессный эквивалент?

Бен Дилтс
источник
Может быть docker?
Шридхар Сарнобат

Ответы:

59

Есть некоторые проблемы с 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>соответственно ограничить процесс по времени или ограничениям процессора.

Этот инструмент работает путем проверки нескольких раз в секунду, если порожденный процесс не превысил свои установленные границы. Это означает, что на самом деле есть небольшое окно, в котором процесс потенциально может быть переподписан до того, как тайм-аут уведомит и убьет процесс.

Следовательно, более правильный подход, вероятно, будет включать cgroups, но это гораздо более сложная настройка, даже если вы используете Docker или runC, которые, среди прочего, предлагают более удобную для пользователя абстракцию вокруг cgroups.

КВЗ
источник
Кажется, работает для меня сейчас (снова?), Но вот версия кеша Google: webcache.googleusercontent.com/…
kvz
Можем ли мы использовать тайм-аут вместе с набором задач (нам нужно ограничить как память, так и ядра)?
17
7
Следует отметить, что этот ответ не относится к одноименной стандартной coreutilsутилите linux ! Таким образом, ответ потенциально опасен, если где-нибудь в вашей системе какой-то пакет содержит скрипт, ожидающий, timeoutчто он станет стандартным coreutilsпакетом linux ! Я не знаю, упакован ли этот инструмент для таких дистрибутивов, как Debian.
user1404316
Уничтожает ли -t <seconds>ограничение процесс через столько секунд?
xxx374562
117

Другой способ ограничить это - использовать контрольные группы 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, ограничив набор процессов, выполняемых под 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и перезагрузка для загрузки с новыми параметрами загрузки ядра.

user65369
источник
3
firejailПрограмма также позволит вам начать процесс с ограничениями памяти ( с использованием контрольных групп и пространств имен , чтобы ограничить больше , чем просто память). В моих системах мне не нужно было менять командную строку ядра, чтобы это работало!
Нед64
1
Вам нужна GRUB_CMDLINE_LINUX_DEFAULTмодификация, чтобы сделать настройку постоянной? Я нашел другой способ сделать это постоянным здесь .
Стасон
1
Смотрите также: Что делает swapaccount=1в GRUB_CMDLINE_LINUX_DEFAULT?
Мартин Тома
В этом ответе было бы полезно отметить, что в некоторых дистрибутивах (например, Ubuntu) sudo требуется для cgcreate, а также для более поздних команд, если разрешение не предоставлено текущему пользователю. Это избавит читателя от необходимости находить эту информацию где-либо еще (например, 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 для всех дочерних процессов; если это так, то все равно получит неправильный ответ. Ограничение на процесс и виртуальное адресное пространство, а не использование памяти. Использование памяти сложнее измерить.
MarkR
1
если я правильно понимаю вопрос, то ОП какой предел для подпроцесса (ребенка) .. не в сумме.
Акира
@MarkR, в любом случае, виртуальное адресное пространство является хорошим приближением к используемой памяти, особенно если вы запускаете программу, которая не управляется виртуальной машиной (скажем, Java). По крайней мере, я не знаю лучшего показателя.
2
Просто хотел сказать спасибо - этот ulimitподход помог мне с firefox«ы ошибкой 622816 - Загрузка большого изображения может„заморозить“браузер Firefox, или сбой системы ; который при загрузке с USB (из ОЗУ) имеет тенденцию к зависанию ОС, требуя жесткого перезапуска; теперь хотя бы firefoxвылетает сам, оставляя ОС в живых ... ура!
sdaau
8

Я использую приведенный ниже скрипт, который прекрасно работает. Он использует cgroups через cgmanager. Обновление: теперь он использует команды из cgroup-tools. Назовите этот скрипт limitmemи поместите его в $ PATH, и вы можете использовать его как limitmem 100M bash. Это ограничит использование памяти и подкачки. Чтобы ограничить только память, удалите строку с memory.memsw.limit_in_bytes.

edit: при установке Linux по умолчанию это ограничивает использование памяти, а не использование подкачки. Чтобы включить ограничение использования свопа, вам нужно включить учет свопа в вашей системе Linux. Сделайте это путем установки / добавления swapaccount=1в /etc/default/grubтак выглядит что - то вроде

GRUB_CMDLINE_LINUX="swapaccount=1"

Затем запустите sudo update-grubи перезагрузите компьютер.

Отказ от ответственности: я не удивлюсь, если cgroup-toolsтакже перерывы в будущем. Правильным решением было бы использовать API-интерфейсы systemd для управления cgroup, но для этой системы нет инструментов командной строки.

#!/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
JanKanis
источник
1
call to cgmanager_create_sync failed: invalid requestдля каждого процесса, с которым я пытаюсь работать limitmem 100M processname. Я на Xubuntu 16.04 LTS и этот пакет установлен.
Аарон Франке
Ups, я получаю это сообщение об ошибке: $ limitmem 400M rstudio limiting memory to 400M (cgroup limitmem_24575) for command rstudio Error org.freedesktop.DBus.Error.InvalidArgs: invalid request есть идеи?
Киселев
@RKiselev cgmanager устарел и даже недоступен в Ubuntu 17.10. API-интерфейс systemd, который он использует, был изменен в какой-то момент, так что это, вероятно, причина. Я обновил скрипт для использования команд 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, так что, возможно, он уже установлен.

Страница manchpst показывает опцию:

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

Oz123
источник
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`
d9ngle
источник
@sourcejedi добавил это :)
d9ngle
2
Хорошо, я отредактировал свой ответ. Чтобы включить лимиты подкачки, вам нужно включить учет подкачки в вашей системе. В этом есть небольшая накладная нагрузка, поэтому по умолчанию она не включена в Ubuntu. Смотрите мое редактирование.
JanKanis
3

В любом системном дистрибутиве вы также можете использовать cgroups косвенно через systemd-run. Например, для вашего случая ограничения pdftoppmдо 500M RAM, используйте:

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

Примечание: это попросит вас ввести пароль, но приложение будет запущено как ваш пользователь. Не позволяйте этому вводить вас в заблуждение, заставляя вас думать, что команда нуждается в этом sudo, потому что это заставит команду работать от имени пользователя root, что вряд ли было вашим намерением.

Если вы не хотите вводить пароль (в конце концов, как пользователь, которому вы владеете своей памятью, зачем вам нужен пароль для его ограничения) , вы можете использовать --userопцию, однако для этого вам понадобится включить поддержку cgroupsv2, что правильно теперь требует загрузки с systemd.unified_cgroup_hierarchyпараметром ядра .

Привет, ангел
источник
Спасибо, сделал мой день
Geradlus_RU