Limiting process memory linux

Ограничение памяти, доступной программе

Решил я как-то заняться задачкой сортировки миллиона целых чисел при имеющейся памяти в 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:

Читайте также:  Signal segmentation fault linux

И в 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 в хуках и избежать рекурсивных вызовов.

Читайте также:  Гаджеты rp5 для windows 10

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.

Источник

How Limit memory usage for a single Linux process and not kill the process

How Limit memory usage for a single Linux process and not kill the process.

I know ulimit can limit memory usage, but if exceed the limit, will kill the process.

Is there any other command or shell can limit memory usage and not kill the process?

2 Answers 2

Another way besides setrlimit, which can be set using the ulimit utility:

$ ulimit -Sv 500000 # Set

is to use Linux’s control groups, because it limits a process’s (or group of processes’) allocation of physical memory distinctly from virtual memory. For example:

$ cgcreate -g memory:/myGroup
$ echo $(( 500 * 1024 * 1024 )) > /sys/fs/cgroup/memory/myGroup/memory.limit_in_bytes
$ echo $(( 5000 * 1024 * 1024 )) > /sys/fs/cgroup/memory/myGroupmemory.memsw.limit_in_bytes

will create a control group named «myGroup», cap the set of processes run under myGroup up to 500 MB of physical memory and up to 5000 MB of swap. To run a process under the control group:

Note: For what I can understand setrlimit will limit the virtual memory, although with cgroups you can limit the physical memory.

I believe you are wrong on thinking that a limit set with setrlimit(2) will always kill the process.

Indeed, if the stack space is exceeded ( RLIMIT_STACK ), the process would be killed (with SIGSEGV ).

But if it is heap memory ( RLIMIT_DATA or RLIMIT_AS ), mmap(2) would fail. If it has been called from malloc(3) or friends, that malloc would fail.

Читайте также:  Linux link time reference

Some Linux systems are configured with memory overcommit.
This is a sysadmin issue: echo 0 > /proc/sys/vm/overcommit_memory

The moral of the story is that you should always check result of malloc , at least like

Of course, sophisticated applications could handle «out-of-memory» conditions more wisely, but it is difficult to get right.

Some people incorrectly assumes that malloc does not fail (this is wrong). But that is their mistake. Then they dereference a NULL pointer, getting a SIGSEGV that they deserve.

You could consider catching SIGSEGV with some processor-specific code, see this answer. Unless you are a guru, don’t do that.

Источник

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.

Источник

Оцените статью