How to Limit Time and Memory Usage of Processes in Linux
The timeout script is a useful resource monitoring program for limiting time and memory consumption of processes in Linux. It allows you to run programs under control, and enforce time and memory limits, terminating the program upon violation of these parameters.
No installation needed, simply execute a command together with its arguments using timeout program and it will monitor the command’s memory and time consumption, interrupting the process if it goes out of the limits, and notifies you with the predefined message.
To run this script, you must have Perl 5 installed on your Linux system and the /proc filesystem mounted.
To check the installed version of Perl on your Linux system, run the following command.
Check Perl Version in Linux
Next, clone the timeout repository to your system using git command, then move into the local repository using cd command and invoke it as a usual Linux command.
Let’s now look at how timeout script works.
Basic Memory Limiting (100M of Virtual Memory):
This first example shows how to limit the memory usage of a process to 100M of virtual memory, using the -m flag. The default unit for memory is in kilobytes.
Here, the stress-ng command runs 4 virtual memory stressors (VMS) that combine to use 40% of the available memory for 10 minutes. Thus each stressor uses 10% of the available memory.
Limiting Memory of Process
Considering the output of the timeout command above, the stress-ng worker processes were terminated after just 1.16 seconds. This is because the combined memory consumption of the VMS (438660 kilobytes) is greater than the permitted virtual memory usage for stress-ng and its child processes.
Basic Time Limiting of Process:
To enable time limiting of process, use the -t flag as shown.
Time Limiting of Process
In the above example, when the stress-ng CPU+SYS time exceeds the defined value of 4, the worker processes are killed.
Limiting both Time and Memory of Process
You can also limit both memory and time at once as follows.
Timeout also supports some advanced options such as —detect-hangups , which enables hangup detection.
You can monitor RSS (resident set size) memory limit using the —memlimit-rss or -s switch.
In addition, to return the exit code or signal+128 of a process, use the —confess or -c option as shown.
For more information and usage example, see the timeout Github repository: https://github.com/pshved/timeout.
You might also find these following related articles equally useful:
The timeout script is a simple resource monitoring program that essentially restricts the time and memory consumption of processes in Linux. You can give us feedback about the timeout script via the comment form below.
If You Appreciate What We Do Here On TecMint, You Should Consider:
TecMint is the fastest growing and most trusted community site for any kind of Linux Articles, Guides and Books on the web. Millions of people visit TecMint! to search or browse the thousands of published articles available FREELY to all.
If you like what you are reading, please consider buying us a coffee ( or 2 ) as a token of appreciation.
We are thankful for your never ending support.
Источник
Using Linux Control Groups to Constrain Process Memory
Linux Control Groups (cgroups) are a nifty way to limit the amount of resource, such as CPU, memory, or IO throughput, that a process or group of processes may use. Frits Hoogland wrote a great blog demonstrating how to use it to constrain the I/O a particular process could use, and was the inspiration for this one. I have been doing some digging into the performance characteristics of OBIEE in certain conditions, including how it behaves under memory pressure. I’ll write more about that in a future blog, but wanted to write this short blog to demonstrate how cgroups can be used to constrain the memory that a given Linux process can be allocated.
This was done on Amazon EC2 running an image imported originally from Oracle’s OBIEE SampleApp, built on Oracle Linux 6.5.
First off, install the necessary package in order to use them, and start the service. Throughout this blog where I quote shell commands those prefixed with # are run as root and $ as non-root:
Create a cgroup (I’m shamelessly ripping off Frits’ code here, hence the same cgroup name 😉 ):
You can use cgget to view the current limits, usage, & high watermarks of the cgroup:
For more information about the field meaning see the doc here.
To test out the cgroup ability to limit memory used by a process we’re going to use the tool stress, which can be used to generate CPU, memory, or IO load on a server. It’s great for testing what happens to a server under resource pressure, and also for testing memory allocation capabilities of a process which is what we’re using it for here.
We’re going to configure cgroups to add stress to the myGroup group whenever it runs
[Re-]start the cg rules engine service:
Now we’ll use the watch command to re-issue the cgget command every second enabling us to watch cgroup’s metrics in realtime:
In a separate terminal (or even better, use screen!) run stress, telling it to grab 150MB of memory:
Review the cgroup, and note that the usage fields have increased:
Both memory.memsw.usage_in_bytes and memory.usage_in_bytes are 157548544 = 150.25MB
Having a look at the process stats for stress shows us:
The man page for proc gives us more information about these fields, but of particular note are:
- VmSize: Virtual memory size.
- VmRSS: Resident set size.
- VmSwap: Swapped-out virtual memory size by anonymous private pages
Our stress process has a VmSize of 156MB, VmRSS of 150MB, and zero swap.
Kill the stress process, and set a memory limit of 100MB for any process in this cgroup:
Run cgset and you should see the see new limit. Note that at this stage we’re just setting memory.limit_in_bytes and leaving memory.memsw.limit_in_bytes unchanged.
Let’s see what happens when we try to allocate the memory, observing the cgroup and process Virtual Memory process information at each point:
Note that VmSwap has now gone above zero, despite the machine having plenty of usable memory:
So it looks like the memory cap has kicked in and the stress process is being forced to get the additional memory that it needs from swap.
Let’s tighten the screw a bit further:
The process is now using 100MB of swap (since we’ve asked it to grab 200MB but cgroup is constraining it to 100MB real):
The cgget command confirms that we’re using swap, as the memsw value shows:
So now what happens if we curtail the use of all memory, including swap? To do this we’ll set the memory.memsw.limit_in_bytes parameter. Note that running cgset whilst a task under the cgroup is executing seems to get ignored if it is below that currently in use (per the usage_in_bytes field). If it is above this then the change is instantaneous:
- Current state
- Set the limit below what is currently in use (150m limit vs 200m in use)
- Check the limit – it remains unchanged
- Set the limit above what is currently in use (250m limit vs 200m in use)
- Check the limit — it’s taken effect
So now we’ve got limits in place of 100MB real memory and 250MB total (real + swap). What happens when we test that out?
The process is using 245MB total ( VmData ), of which 95MB is resident ( VmRSS ) and 150MB is swapped out ( VmSwap )
The cgroup stats reflect this:
If we try to go above this absolute limit (memory.memsw.max_usage_in_bytes) then the cgroup kicks in a stops the process getting the memory, which in turn causes stress to fail:
This gives you an indication of how careful you need to be using this type of low-level process control. Most tools will not be happy if they are starved of resource, including memory, and may well behave in unstable ways.
Thanks to Frits Hoogland for reading a draft of this post and providing valuable feedback.
Источник
Ограничение памяти, доступной программе
Решил я как-то заняться задачкой сортировки миллиона целых чисел при имеющейся памяти в 1 Мб. Но перед этим мне пришлось подумать над тем, как можно ограничить объём доступной памяти для программы. И вот, что я придумал.
Виртуальная память процесса
Перед тем, как окунуться в разные методы ограничения памяти, необходимо знать, как устроена виртуальная память процесса. Лучшая статья на эту тему — «Анатомия программы в памяти».
Прочитав статью, я могу предложить две возможности для ограничения памяти: уменьшить виртуальное адресное пространство или объём кучи.
Первое: уменьшение объёма адресного пространства. Это довольно просто, но не совсем корректно. Мы не можем уменьшить всё пространство до 1 Мб — не хватит места для ядра и библиотек.
Второе: уменьшение объёма кучи. Это не так-то просто сделать, и обычно так никто не делает, поскольку это доступно только через возню с компоновщиком. Но для нашей задачи это был бы более корректный вариант.
Также я рассмотрю другие методы, такие, как отслеживание использования памяти через перехват вызовов библиотек и системы, и изменение окружения программы через эмуляцию и введение «песочницы».
Для тестирования будем использовать небольшую программу по имени big_alloc, размещающую, и затем освобождающую 100 MiB.
Все исходники есть на github.
ulimit
То, о чём сразу вспоминает старый unix-хакер, когда ему нужно ограничить память. Это утилита из bash, которая позволяет ограничивать ресурсы программы. На деле это интерфейс к setrlimit.
Мы можем установить ограничение на объём памяти для программы.
Мы задали ограничение в 1024 кб — 1 MiB. Но если мы попытаемся запустить программу, она отработает без ошибок. Несмотря на лимит в 1024 кб, в top видно, что программа занимает аж 4872 кб.
Причина в том, что Linux не устанавливает жёстких ограничений, и в man об этом написано:
Есть также опция ulimit -d, которая должна работать, но всё равно не работает из-за mmap (см. раздел про компоновщик).
Для манипуляции программным окружением QEMU прекрасно подходит. У неё есть опция –R для ограничения виртуального адресного пространства. Но до слишком малых значений его ограничивать нельзя – не поместятся libc и kernel.
Тут -R 1048576 оставляет 1 MiB на виртуальное адресное пространство.
Для этого надо отвести что-то порядка 20 MB. Вот:
Останавливается после 100 итераций (10 MB).
В общем, QEMU пока лидирует среди методов для ограничения, надо только поиграться с величиной –R.
Контейнер
Ещё вариант – запустить программу в контейнере и ограничить ресурсы. Для этого можно:
- использовать какой-нибудь docker
- использовать инструменты usermode из пакета lxc
- написать свой скрипт с libvirt.
- что-то ещё…
Но ресурсы будут ограничены при помощи подсистемы Linux под названием cgroups. Можно играться с ними напрямую, но я рекомендую через lxc. Я бы хотел использовать docker, но он работает только на 64-битных машинах.
LXC — это LinuX Containers. Это набор инструментов и библиотек из userspace для управления функциями ядра и создания контейнеров – изолированных безопасных окружений для приложений, или для всей системы.
Функции ядра следующие:
- Control groups (cgroups)
- Kernel namespaces
- chroot
- Kernel capabilities
- SELinux, AppArmor
- Seccomp policies
Документацию можно найти на офсайте или в блоге автора.
Для запуска приложения в контейнере необходимо предоставить lxc-execute конфиг, где указать все настройки контейнера. Начать можно с примеров в /usr/share/doc/lxc/examples. Man рекомендует начать с lxc-macvlan.conf. Начнём:
Теперь давайте ограничим память при помощи cgroup. LXC позволяет настроить подсистему памяти для cgroup контейнера, задавая ограничения памяти. Параметры можно найти в документации по RedHat. Я нашёл 2:
- memory.limit_in_bytes — задаёт максимальное количество пользовательской памяти, включая файловый кэш
- memory.memsw.limit_in_bytes — задаёт максимальное количество в сумме памяти и свопа
Что я добавил в lxc-my.conf:
Тишина — видимо, памяти слишком мало. Попробуем запустить из шелла
bash не запустился. Попробуем /bin/sh:
И в dmesg можно отследить славную смерть процесса:
Хотя мы не получили сообщение об ошибке от big_alloc насчёт malloc failure и количества доступной памяти, мне кажется, мы удачно ограничили память при помощи контейнеров. Пока остановимся на этом
Компоновщик
Попробуем изменить бинарный образ, ограничив доступное куче место. Компоновка – последний этап построения программы. Для этого используется компоновщик и его скрипт. Скрипт – описание разделов программы в памяти вместе со всякими атрибутами и прочим.
Пример компоновочного скрипта:
Точка означает текущее положение. Например, раздел .text начинается с адреса 0×10000, а затем, начиная с 0×8000000 у нас есть два следующих раздела: .data и .bss. Точка входа — main.
Всё круто, но в реальных программах работать не будет. Функция main, которую вы пишете в программах на С, реально не является первой вызываемой. Сперва совершается очень много инициализаций и подчисток. Этот код содержится в библиотеке времени исполнения С (crt) и распределено по библиотекам crt#.o в /usr/lib.
Подробности можно увидеть, запустив gcc –v. Сначала она вызывает ccl, создаёт ассемблерный код, транслирует в объектный файл через as и в конце собирает всё вместе с ELF при помощи collect2. collect2 — обёртка ld. Она принимает объектный файл и 5 дополнительных библиотек, чтобы создать конечный бинарный образ:
Всё это очень сложно, поэтому вместо написания собственного скрипта я отредактирую скрипт компоновщика по умолчанию. Получим его, передав -Wl,-verbose в gcc:
Теперь подумаем, как его изменить. Посмотрим, как бинарник строится по умолчанию. Скомпилируем и поищем адрес раздела .data. Вот выдача objdump -h big_alloc
Разделы .text, .data и .bss расположены около 128 MiB.
Посмотрим, где стек, при помощи gdb:
esp указывает на 0xbffff0a0, что около 3 GiB. Значит, у нас есть куча в
В реальном мире верхний адрес стека случайный, его можно увидеть, например, в выдаче:
Как мы знаем, куча растёт от конца .data по направлению к стеку. Что, если мы подвинем раздел .data как можно выше?
Давайте разместим сегмент данных в 2 MiB перед стеком. Берём верх стека, вычитаем 2 MiB:
0xbffff0a0 — 0x200000 = 0xbfdff0a0
Смещаем все разделы, начинающиеся с .data на этот адрес:
Опции -Wl и -T hack.lst говорят компоновщику, чтобы он использовал hack.lst в качестве сценария работы.
Посмотрим на заголовок:
И всё равно данные размещаются в памяти. Как? Когда я попытался посмотреть значения указателей, возвращаемых malloc, я увидел, что размещение начинается где-то после окончания раздела.data по адресам вроде 0xbf8b7000, постепенно продолжается с увеличением указателей, а затем опять возвращается к нижним адресам вроде 0xb5e76000. Выглядит так, будто куча растёт вниз.
Если подумать, ничего странного в этом нет. Я проверил исходники glibc и выяснил, что когда brk не справляется, то используется mmap. Значит, glibc просит ядро разместить страницы, ядро видит, что у процесса куча дыр в виртуальной памяти, и размещает в одном из пустых мест страницу, после чего glibc возвращает указатель с неё.
Запуск big_alloc под strace подтвердил теорию. Посмотрите на нормальный бинарник:
А теперь на модифицированный:
Сдвиг раздела .data к стеку с целью уменьшить место для кучи смысла не имеет, поскольку ядро разместит страницу в пустом пространстве.
Песочница
Ещё один способ ограничения памяти программы — sandboxing. Отличие от эмуляции в том, что мы ничего не эмулируем, а просто отслеживаем и контролируем некоторые вещи в поведении программы. Обычно используется в исследованиях в области безопасности, когда вы изолируете зловреда и анализируете его так, чтобы он не нанёс вреда вашей системе.
Трюк с LD_PRELOAD
LD_PRELOAD — специальная переменная окружения, заставляющая динамический компоновщик использовать в приоритете предзагруженные библиотеки, в т.ч. libc. Этот трюк, кстати, также используют и некоторые зловреды.
Я написал простую песочницу, перехватывающую вызовы malloc/free, работающую с памятью и возвращающую ENOMEM по исчерпанию лимита.
Для этого я сделал библиотеку общего пользования (shared library) c моими реализациями вокруг malloc/free, увеличивающими счётчик на объём malloc, и уменьшающими, когда вызывается free. Она предзагружается через LD_PRELOAD.
Моя реализация malloc:
libc_malloc — указатель на оригинальный malloc из libc. no_hook локальный флаг в потоке. Используется для того, чтобы можно было использовать malloc в хуках и избежать рекурсивных вызовов.
malloc используется неявно в функции account библиотекой uthash. Зачем использовать таблицу хешей? Потому, что при вызове free вы передаёте в неё только указатель, а внутри free неизвестно, сколько памяти было выделено. Поэтому у вас есть таблица с указателями-ключами и объёмом размещённой памяти в виде значений. Вот что я делаю в malloc:
mem_allocated это та статическая переменная, которую сравнивают с ограничением в malloc.
Теперь при вызове free происходит следующее:
Да, просто уменьшаем mem_allocated.
И что самое крутое — это работает.
Полный код библиотеки на github
Получается, что LD_PRELOAD – отличный способ ограничить память
ptrace
ptrace — ещё одна возможность для построения песочницы. Это системный вызов, позволяющий управлять выполнением другого процесса. Встроен в различные POSIX ОС.
Это основа таких трассировщиков, как strace, ltrace, и почти всех программ для создания песочниц — systrace, sydbox, mbox и дебаггеров, включая gdb.
Я сделал свой инструмент при помощи ptrace. Он отслеживает вызовы brk и меряет расстояние между изначальным значением break и новым, которое задаётся следующим вызовом brk.
Программа форкается и запускает 2 процесса. Родительский – трассировщик, а дочерний – трассируемый. В дочернем процессе я вызываю ptrace(PTRACE_TRACEME) и затем execv. В родительском использую ptrace(PTRACE_SYSCALL) чтобы остановиться на syscall и отфильтровать вызовы brk из дочернего, а затем ещё один ptrace(PTRACE_SYSCALL) для получения значения, возвращаемого brk.
Когда brk выходит за заданное значение, я выставляю -ENOMEM в качестве возвращаемого значения brk. Это задаётся в регистре eax, поэтому я просто перезаписываю его с ptrace(PTRACE_SETREGS). Вот самая вкусная часть:
Также я перехватываю вызовы mmap/mmap2, так как у libc хватает мозгов вызывать их при проблемах с brk. Так что когда заданное значение превышено и я вижу вызов mmap, я обламываю его с ENOMEM.
Но мне это не нравится. Это завязано на ABI, т.е. тут приходится использовать rax вместо eax на 64-битной машине, поэтому надо либо делать отдельную версию, или использовать #ifdef, или принудительно использовать опцию -m32 option. И скорее всего не будет работать на других POSIX-подобных системах, у которых может быть другой ABI.
Иные способы
Что ещё можно попробовать (эти варианты были отвергнуты по разным причинам):
- хуки malloc. В man написано, что уже не поддерживаются
- Seccomp и prctl при помощи PR_SET_MM_START_BRK. Может сработать – но, как сказано в документации, это не песочница, а способ минимизации доступной поверхности ядра. То есть, это будет ещё более криво, чем использовать ручной ptrace
- libvirt-sandbox. Всего лишь обёртка для lxc и qemu.
- SELinux sandbox. Не работает, ибо использует cgroup.
Источник