Начнем издалека. В спецификации любого компьютера и в частности сервера непременно числится надпись "N гигабайт оперативной памяти" - именно столько в его распоряжении находится физической памяти.

Задача распределения доступных ресурсов между исполняемым программным обеспечением, в том числе и физической памяти, лежит на плечах операционной системы, в нашем случае Linux. Для обеспечения иллюзии полной независимости, она предоставляет каждой из программ свое независимое виртуальное адресное пространство и низкоуровневый интерфейс работы с ним. Это избавляет их от необходимости знать друг о друге, размере доступной физической памяти и текущей её занятости. Адреса в виртуальном пространстве процессов называют логическими.

Для отслеживания соответствия между физической и виртуальной памятью ядро Linux использует иерархический набор структур данных в своей служебной области физической памяти (только оно работает с ней напрямую), а также специализированные аппаратные контуры, которые в совокупности называют MMU.

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

Также стоит упомянуть, что на аппаратном уровне как правило есть поддержка дополнительного уровня абстракции в виде "сегментов" оперативной памяти, с помощью которых можно разделять программы на части. В отличии от других операционных систем, в Linux она практически не используется - логический адрес всегда совпадает с линейным (адресом внутри сегмента, которые сконфигурированы фиксированным образом).

Как многие знают, существует виртуальная память - это то, что создаёт операционная система для работы программ с ней. Это сама ОЗУ и все SWAP разделы. Выделяемая память процессу может быть либо резидентная, либо виртуальная. В листинге ниже видно у процессов резидентную (rss) и виртуальную память (vsz). Эта память отображается в KB.

$ ps -C apache2 -o pid,user,rss,vsz,comm
    PID USER       RSS    VSZ COMMAND
    403 root      7316  11188 apache2
    405 www-data  7032 1216200 apache2
    406 www-data 11128 1216200 apache2

Виртуальная память (VSZ) — это память которую выделили процессу, но не факт что он успел в эту память что-то записать.

Резидентная память (RSS) — это память которую процесс занял, то есть что-то сохранил в виртуальную память. Именно резидентная память показывает сколько процесс потребляет физической памяти.

Приложение может запросить много памяти, а использовать малую её часть. Поэтому почти всегда rss меньше чем vsz.

Раздел подкачки (SWAP) — это раздел на жестком диске, куда помещаются:

  • редко используемые данные из резидентной памяти;

  • любые данные при нехватки физической памяти.

Если какие-то данные из rss сбрасываются в swap то rss освобождается, а vsz нет. От сюда следует что данные процесса, которые лежат в swap, входят в виртуальную память этого процесса.

Linux умеет работать не только с разделом подкачки, но и с файлом подкачки. То есть данные из резидентной памяти могут сбрасываться в специальный файл, который лежит на жёстком диске.

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

Посмотреть более подробно на используемую память процесса поможет файл /proc/<pid>/status.

Кластеры - это блоки на уровне файловой системы, с которыми происходит работа. Запись и чтение происходит блоками. Размер по умолчанию - 4KB.

Вся виртуальная память состоит из страниц. Страницы - это набор ячеек памяти в виртуальном пространстве, которому сопоставлена реальная память на диске. Страницы бывают обычные - 4 килобайта и huge page - блоки по 2 мегабайтам, либо гигабайту. Используется, когда происходит работа с большими данными, например базами данных (также структура таблицы страниц становится оптимальнее).

Страницы бывают грязные и чистые. Чистые - не подвергались изменению, грязные - подвергались. Например, вы загрузили библиотеку - это чистая страница, т.к. ты её не изменял и она может быть спокойно забыта. А если загружен файл, который был изменен после загрузки - то он становится грязным. Его нужно записать на диск, прежде чем забывать.

Больше всего в системе память занимает страничный кеш (Page Cache). Вся работа с файлами на диске (запись или чтение) идет через Page Cache. Запись в linux всегда быстрее чтения (Не всегда, например, если использовать O_SYNC), так как запись вначале идет в Page Cache, а затем сбрасывается на диск. А при чтении ядро ищет файл в Page Cache, и если не находит читает файл с диска. Узнать сколько сейчас система тратит памяти на Page Cache можно выполнив команду free:

$ free -h

               total        used        free      shared  buff/cache   available

Mem:           976Mi        74Mi       764Mi       0,0Ki       137Mi       765Mi

Swap:          974Mi          0B       974Mi

Страничный кеш показан в колонке buff/cache. Как мы видим у нас занято 137MB страничным кешем. Хотя тут не только Page Cache, тут также находится Buffer, который тоже связан с файлами на диске.

Посмотреть информацию по Page Cache и Buffer отдельно можно в файле /proc/meminfo:

egrep "^Cach|^Buff" /proc/meminfo

Buffers:           16012 kB

Cached:           101220 kB

При создании нового файла, запись идет в cache, а страницы памяти для этого файла помечаются как грязные (dirty). Раз в какой-то промежуток времени грязные страницы сбрасываются на диск, и если таких страниц будет слишком много, то они тоже сбросятся на диск. Управлять этим можно через параметры sysctl (sudo nano /etc/sysctl.conf):

  • vm.dirty_expire_centisecs — интервал сброса грязных страниц на диск в сотых долях секунд (100 = 1с);

  • vm.dirty_ratio — объем оперативной памяти в процентах который может быть выделен под Page Cache.

$ sudo sysctl vm.dirty_expire_centisecs

vm.dirty_expire_centisecs = 3000

$ sudo sysctl vm.dirty_ratio

vm.dirty_ratio = 20

Существует утилита — vmtouch, она может показать какой процент указанного файла находится в страничном кеше. Она есть в репозиториях Debian и в Arch User Repository.

$ sudo apt update

$ sudo apt install vmtouch

$ vmtouch /etc/passwd

           Files: 1

     Directories: 0

  Resident Pages: 1/1  4K/4K  100%

         Elapsed: 6.3e-05 seconds

Видно что весь файл /etc/passwd сейчас находится в Page Cache (Resident Pages).

Узнать объем грязных страниц можно из файла /proc/meminfo. А команда sync записывает грязные страницы на диск:

$ grep Dirty /proc/meminfo

Dirty:                24 kB

# sync

$ grep Dirty /proc/meminfo

Dirty:                 0 kB

HugePages

Поговорим немного про большие страницы HugePages. Особенности таких страниц:

  • размер таких страниц равен 2MB;

  • приложение должно уметь работать с такими страницами;

  • эти страницы никогда не сбрасываются в swap.

Выделить под HugePages страницы можно параметром sysctl:

  • vm.nr_hugepages = <число страниц> (так если указать 1024 то выделится 1024*2МБ=2048MB).

  • vm.hugetlb_shm_group = <gid> — только члены этой группы могут использовать HugePages.

После исправления /etc/sysctl.conf нужно перезагрузиться и посмотреть на результат в файле /proc/meminfo:

$ egrep "HugePages_T|HugePages_F" /proc/meminfo

HugePages_Total:    1024

HugePages_Free:     1024

Выделено 1024 страниц и все они свободны. При этом у нас 2GB памяти не сможет использоваться обычными приложениями, которые не умеют работать с HugePages. Поэтому не всегда нужно выделять HugePages.

Методы управления подсистемой памяти

swap

С файловой памятью всё просто: если данные в ней не менялись, то для её вытеснения делать особо ничего не нужно - просто перетираешь, а затем всегда можно восстановить из файловой системы.

С анонимной памятью такой трюк не работает: ей не соответствует никакой файл, по-этому чтобы данные не пропали безвозвратно, их нужно положить куда-то ещё. Для этого можно использовать так называемый "swap" раздел или файл. Можно, но на практике не нужно. Если swap выключен, то анонимная память становится невытесняемой, что делает время обращения к ней предсказуемым.

Может показаться минусом выключенного swap, что, например, если у приложения утекает память, то оно будет гарантированно зря держать физическую память (утекшая не сможет быть вытеснена). Но на подобные вещи скорее стоит смотреть с той точки зрения, что это наоборот поможет раньше обнаружить и устранить ошибку.

mlock

По-умолчанию вся файловая память является вытесняемой, но ядро Linux предоставляет возможность запрещать её вытеснение с точностью не только до файлов, но и до страниц внутри файла.

Для этого используется системный вызов mlock на области виртуальной памяти, полученной с помощью mmap. Если спускаться до уровня системных вызовов не хочется, рекомендую посмотреть в сторону консольной утилиты vmtouch, которая делает ровно то же самое, но снаружи относительно приложения.

Несколько примеров, когда это может быть целесообразно:

  • У приложения большой исполняемый файл с большим количеством ветвлений, некоторые из которых срабатывают редко, но регулярно. Такого стоит избегать и по другим причинам, но если иначе никак, то чтобы не ждать лишнего на этих редких ветках кода - можно запретить им вытесняться.

  • Индексы в базах данных часто физически представляют собой именно файл, с которым работают через mmap, а mlock нужен чтобы минимизировать задержки и число операций ввода-вывода на и без того нагруженном диске(-ах).

  • Приложение использует какой-то статический словарь, например с соответствием подсетей IP-адресов и стран, к которым они относятся. Вдвойне актуально, если на одном сервере запущено несколько процессов, работающих с этим словарем.

OOM killer

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

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

cgroups

По-умолчанию все пользовательские процессы наравне претендуют на почти всю физически доступную память в рамках одного сервера. Это поведение редко является приемлемым. Даже если сервер условно-однозадачный, например только отдает статические файлы по HTTP с помощью nginx, всегда есть какие-то служебные процессы вроде syslog или какой-то временной команды, запущенной человеком. Если же на сервере одновременно работает несколько production процессов, например, популярный вариант - подсадить к веб-серверу memcached, крайне желательно, чтобы они не могли начать "воевать" друг с другом за память в случае её дефицита.

Для изоляции важных процессов в современных ядрах существует механизм cgroups, c его помощью можно разделить процессы на логические группы и статически сконфигурировать для каждой из групп сколько физической памяти может быть ей выделено. После чего для каждой группы создается своя почти независимая подсистема памяти, со своим отслеживанием вытеснения, OOM killer и прочими радостями.

Механизм cgroups намного обширнее, чем просто контроль за потреблением памяти, с его помощью можно распределять вычислительные ресурсы, "прибивать" группы к ядрам процессора, ограничивать ввод-вывод и многое другое. Сами группы могут быть организованы в иерархию и вообще на основе cgroups работают многие системы "легкой" виртуализации и нынче модные Docker-контейнеры.

Но на мой взгляд именно контроль за потреблением памяти - самый необходимый минимум, который определенно стоит настроить, остальное уже по желанию/необходимости.

NUMA

В многопроцессорных системах не вся память одинакова. Если на материнской плате предусмотрено N процессоров (например, 2 или 4), то как правило все слоты для оперативной памяти физически разделены на N групп так, что каждая из них располагается ближе к соответствующему ей процессору - такую схему называют NUMA.

Таким образом, каждый процессор может обращаться к определенной 1/N части физической памяти быстрее (примерно раза в полтора), чем к оставшимся (N-1)/N.

Ядро Linux самостоятельно умеет это всё определять и по-умолчанию достаточно разумным образом учитывать при планировании выполнения процессоров и выделении им памяти. Посмотреть как это все выглядит и подкорректировать можно с помощью утилиты numactl и ряда доступных системных вызовов, в частности get_mempolicy/set_mempolicy.

Работа с памятью в Linux

Подсистема управления памятью одна из самых важных. От её быстродействия и от того насколько эффективно она распоряжается оперативной памятью зависят все остальные подсистемы.

При рассмотрении подсистемы памяти важно знать и понимать, какие типы памяти есть и про какие говорят. Далее будут рассматриваться два типа памяти:

  • Физическая - оперативная память машины.

  • Линейная - виртуальная память, она может быть больше, чем реально физической памяти у вас есть.

Вся физическая память разбита на страничные кадры. Размер страничного кадра - платформозависимая величина, для x86 она обычно равна 4 Кб, хотя может быть и 4 Мб. Каждый физический кадр описывается фундаментальной структурой данных - struct page (include/linux/mm_types.h). Структура используется, чтобы отслеживать состояние страничного кадра: свободен или выделен, кому он принадлежит, что на нём хранится: данные, код и т.д. Struct page организована в блоки двойных слов для выполнения над ними атомарных операций, работающих с двойными словами. Опишем некоторые важные поля struct page:

  • atomic_t _refcount - количество ссылок на структуру page. Из функции init_free_pfn_range() (mm/init.c) следует, что если _refcount равен 0, то страничный кадр свободен, если >0, то кем-то или чем-то занят.

  • unsigned long flags - содержит флаги, описывающие состояние страничного кадра. Все флаги описаны в файле (include/linux/page-flags.h).

Физическая память 32-битной машины в Linux разделяется на 3 части - зоны:

  • ZONE_DMA - первые 16 Мб физической памяти,

  • ZONE_NORMAL - занимает адреса с 16 Мб по 896 Мб,

  • ZONE_HIGHMEM - содержит страничные кадры выше 896 Мб

Такое разбиение физической памяти в 32 битных системах связано с тем, что в них можно адресовать только лишь 4 Гб линейной памяти, при этом процессу необходимо работать, как в пользовательском режиме, так и в режиме ядра, например, для выполнения системных вызовов. Потому линейное пространство адресов процесса разбивается на несколько частей: 3 Гб под пользователя и 1 Гб под ядро. В первых 3 Гб в адресах до 0xС0000000 процесс работает в режиме обычного пользователя, а адреса выше 0xС0000000 используются в режиме суперюзера. Зоны NORMAL и DMA напрямую отображаются в 4-ый Гб линейного адресного пространства. К объектам, расположенным в этих областях, всегда можно получить доступ, так как для них существуют линейные адреса. А вот HIGHMEM зона содержит кадры, к которым ядро так просто обратится не может. Из-за того, что HIGHMEM содержит кадры, линейные адреса которых просто напросто не существуют в 32-битной системе. Потому функция для выделения страничных кадров - alloc_page() возвращает указатель(линейный адрес) не на первый страничный кадр, а на первый страничный дескриптор, описывающий этот кадр. При этом все дескрипторы страничных кадров находятся в NORMAL зоне, потому для них всегда существует линейный адрес. Для отображения верхних адресов в линейное адресное пространство используются верхние 128 МБ NORMAL адресов. Вообще для отображения HIGHMEM есть несколько техник:

  • постоянное отображение,

  • временное отображения,

  • работа с несмежными областями памяти.

Linux - современная кроссплатформенная операционная система, а такая система обязана уметь эффективно работать с многопроцессорными системами. В таких системах существует несколько подходов к реализации компьютерной памяти. Первая - Uniform memory access (UMA). В этой схеме доступ ко всей физической памяти примерно равноценен по времени, потому нет абсолютно никакой разницы для производительности операционной системы к каким адресам обращаться. Надо заметить, что не в каждой вычислительной системе поддерживается одинаковый доступ к памяти, потому в Linux в качестве базовой модели поддерживается - Non-Uniform memory access (NUMA). В этой модели физическая память системы разделяется на несколько узлов. Каждый узел описывается структурой pg_data_t (include/linux/mmzone.h). Каждый узел потенциально может содержать любую из зон памяти, потому структура pg_data_t содержит их описатели. Все дескрипторы страничных кадров узлов хранятся в глобальном массиве zone_mem_map, который располагается в описателе соотвествующей зоны:

                     pg_data_t
                         |
     ________________node_zones_______________
    /                    |                    \
ZONE_DMA            ZONE_NORMAL          ZONE_HIGHMEM
    |                    |                     |
zone_mem_map        zone_mem_map           zone_mem_map

Красота такого подхода при работе с памятью заключается в том, что UMA представляется просто, как NUMA с одним узлом, что так же позволяет использовать везде одинаковые методы - универсальность во всём, так сказать.

На 64-битных машинах, физическая память так же разделяется на 3 части, но в силу объективных причин, реальные 64-битные машины не могут сейчас содержать все 2^64 степени байт памяти. В x86, например, поддерживается память только до 2^48 байт = 256 Тб, что, согласитесь, достаточно много. Так как реальной физической памяти много меньше линейной, то у 64-битных систем надобность в HIGHMEM зоне пока отсутствует, она нулевая, а вся помять делиться между DMA и NORMAL.

Теперь мы знаем, как Linux описывает доступную ему физическую память, пришло время разобрать, как ядро работает с памятью. Для этого важно разобрать, как Linux её выделяет, или, иными словами, как работают аллокаторы.

Bootmem

Самый первый доступный ядру алокатор памяти - bootmem(mm/bootmem.c). bootmem алокатор используется только при загрузке ядра для начального выделения физической памяти до того, как подсистема управления памятью станет доступной. bootmem работает очень прямолинейно по алгоритму первый подходящий - ищет первый свободный кусок(страницу) физической памяти и выдаёт. Для представления физической памяти использует bitmap, если 1, то страница занята, если 0, то свободна. Для выделения памяти меньше страницы он записывает PFN последней такой алокации, и следующая маленькая локация будет, если возможно, располагаться на той же физической странице. Алокатор с алгоритмом первый наиболее подходящий, не сильно страдает от фрагментации, но из-за использования bitmap крайне медленный.

/include/linux/bootmem.h
/*
 * node_bootmem_map is a map pointer - the bits represent all physical
 * memory pages (including holes) on the node.
 */
typedef struct bootmem_data {
    unsigned long       node_min_pfn;
    unsigned long       node_low_pfn;
    void                *node_bootmem_map;
    unsigned long       last_end_off;
    unsigned long       hint_idx;
    struct list_head    list;
} bootmem_data_t;

После начальной загрузки и инициализации памяти ядру становятся доступны другие аллокаторы:

---------------
|   kmalloc   |
------------------------
|   kmemcache | vmalloc|
------------------------
|         buddy        |
------------------------

Buddy

Buddy - аллокатор смежных страничных кадров, а не линейных страниц, так как для некоторых задач, таких как DMA нужны именно смежные физические страницы, потому что DMA-устройства работают с памятью напрямую. Ещё одной причиной такого подхода является то, что это позволяет не трогать таблицы страниц ядра, что ускоряет работу с памятью. Проблема аллокаторов смежных страниц - внешняя фрагментация, потому в buddy аллокаторе в Linux применяется стандартный подход - разбиение всех доступных страничных кадров на списки по степени двойки: 1, 2, 4, 8, 16, …, 1024. 1024*4096 = 4МВ. Физический адрес первого страничного кадра в блоке кратен размеру группы. Алгоритм работы: хотим выделить 256 кадров. Аллокатор проверит в списке 256, если нет, заглянет в 512, если есть возьмёт 256 кадров, а оставшиеся поместит в список 256. Если и в 512 нет, то проверяет в 1024, если есть, то возвращает 256 кадров запросившему, а оставшиеся 768 разобьёт по двум спискам 512 и 256, если и в 1024 нет, то сигналит об ошибке. У системы buddy есть глобальный объект, хранящий дескрипторы всех доступных кадров, а на каждом отдельном процессоре есть свои локальные списки доступных кадров, если в локальных списках закончилась память, то он подтягивает из глобального и наоборот возвращает если в локальных они свободны. У каждой зоны свой собственный buddy аллокатор. Для работы с buddy аллокатором необходимо использовать функции alloc_page/__rmqueue()(mm/page_alloc.c) - выделение, __free_pages()- освобождение. При работе с этими функциями необходимо отключать прерывания и брать спин блокировку zone->lock.

Плюсы buddy:

  • Быстрее bootmem(не использует bitmap).

  • Можно выделять несколько страничных кадров подряд.

Минусы buddy:

  • Нельзя выделить меньше страничного кадра, всегда выделяет >= PAGESIZE.

  • Выделяет только идущие по очереди в физической памяти, что всё равно приводит к фрагментации.

Vmalloc

У работы со смежными физическими областями есть свои плюсы в виде быстрой работы с памятью, однако и минусы в виде внешней фрагментации. В Linux есть возможность работать с несмежными областями физической памяти, к которым можно обращаться через смежные области линейного пространства. Начало области линейного пространства, где отображаются несмежные области физического, можно получить из макроса VMALLOC_START, конец - VMALLOC_END. Каждая несмежная область памяти описывается структурой(include/linux/vmalloc.h)

struct vm_struct {
    struct vm_struct    *next;      // <- список
    void                *addr;      // линейный адрес первой ячейки
    unsigned long       size;       // size + 4096(окно безопасности между несмежными областями)
    unsigned long       flags;      // тип памяти, отображаемой несметной области
    struct page         **pages;
    unsigned int        nr_pages;
    phys_addr_t         phys_addr;
    const void          *caller;
};

Выделение страниц производится функцией void *vmalloc(unsigned long size) (mm/vmalloc.c). size - размер запрашиваемой области. Выделяет память кратно странице, потому первым делом округляет size до кратного странице размера. Он выдаёт последовательные страницы, но уже в виртуальном адресном пространстве. vmalloc берёт физические страницы у buddy по страничному кадру. Освобождать память можно с помощью vfree(). Минус заключается в том, что наступает фрагментация, но уже в виртуальном памяти, плюс появляется необходимость обращаться в таблицы страниц, что долго. Потому vmalloc редко вызывают. Его применяют для модулей, буферы ввода /вывода, сетевого экрана,отображение верхней памяти.

Kmemcache

Очевидно, что для работы с маленькими областями памяти произвольной длины не buddy, не vmalloc не подходят, из-за их расточительности. Потому в Linux есть ещё одна система памяти - kmemcache, которая позволяет выделять память под небольшие объекты в пределах страничного кадра. Однако тут надо быть осторожнее, так как может возникнуть проблема внутренней фрагментации. Вообще говоря под kmemcache скрывается аде целых 3 системы: SLAB/SLUB/SLOB. Суть этих систем достаточно похожа, но имеются и существенные отличия:

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

  • SLAB - был введён в солярисе и изначально был только он, но системы становились большими и SLAB стал себя плохо показывать в системах с большим количеством процессоров.

  • SLUB - эволюция SLAB - быстрее, выше, сильнее.

Сначала опишем интерфейс SLAB. Slab базируется на нескольких наблюдениях. Во-первых, ядро часто запрашивает и возвращает области памяти одного и того же размера для различных структур, потому для ускорения можно не освобождать, а оставлять их в кеше для себя, а потом переиспользовать, что сэкономит время. Лучше как можно реже обращаться к buddy, так как каждое обращение к нему загрязняет аппаратный кэш. Так же можно создать объекты размером не кратным двойки, если к ним происходит частое обращение, что ещё может улучшить работу аппаратного кэша. Slab группирует объекты в кэш. Каждый кэш - хранилище объектов одного типа( размера). Кеш имеет несколько slab-списков: с полностью свободными объектами, частично свободными и полностью занятыми. Кэш работает с гранулярностью 1-2-4-8 страниц.

kmem_cache       slab - список
________
|      |——————> | | -  | | - | |  - полностью свободны
|      |
|      |
|      |——————> | | -  | | - | |  - частично свободны
|      |
|      |——————> | | -  | | - | |  - полностью заняты
|      |

(include/linux/slub_def.h)

Для того чтобы пользоваться struct kmem_cache надо получить хэндл через функцию:

struct kmem_cache *kmem_cache_create(size);

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

void kmem_cache_alloc(kc, flags);

И освобождать:

void kmem_cache_free(kc);

Уничтожить кэш можно с помощью:

kmem_cache_destroy()

Всю информацию по SLAB можно получить в /proc/slabinfo.

Под SLAB тоже нужно было выделять память, дескриптор описывающий SLAB мог лежать: У другого kmem_cache - off-slab. Дескриптор slab может лежать в голове страницы, которую выдаёт buddy - on-slab. Но buddy выдавал нам страницу и struct page, который по размеру совпадал со slab -> struct page можно забрать у системы и использовать его под slab. Потому появился slub. Минус SLAB allocator - выделяет объекты константного размера, хотя нам не всегда известен размер объекта под который нужно выделить память.

Более высокого уровня аллокатор kmalloc/kfree(include/linux/slab.h). Он обращается к необходимому kmem_cache, получая его через статическую функцию kmalloc_index(size). В статической функции, если размер будет известен на этапе компиляции, то вызов функции будет компилятором заменён на итоговый индекс.:

static __always_inline int kmalloc_index(size_t size)
{
...
if (KMALLOC_MIN_SIZE <= 32 && size > 64 && size <= 96)
        return 1;
if (KMALLOC_MIN_SIZE <= 64 && size > 128 && size <= 192)
	return 2;
if (size <= 8)
	return 3;
...
}
  • 0 = zero alloc

  • 1 = 65 .. 96 bytes

  • 2 = 129 .. 192 bytes

  • n = (2$^{n-1}$+1) .. 2$^n$ //todo

Кэши размером 0/ 8/ 16/ 32/ 64/ 96/ 128/ 192 /256 …/2$^{26}$. 96 и 192 - эвристически вычисленные часто запрашиваемые значения.

Все аллокаторы работаю с группой флагов gfp_flags(include/linux/gfp.h) - get free page flags. Изначально они появились в buddy потом просочись на уровни повыше.

Типы флагов:

  1. Откуда выделять: __GFP_DMA (Get Free Page), __GFP_HIGHMEM, __GFP_DMA32. По умолчанию система старается выделять память в ZONE_NORMAL.

  2. Поведение при нехватке памяти - контекст, в котором мы работаем по сути. Если памяти нет, то её нужно найти, например:

    • в дисковом кэше - требуется брать мютекс;

    • ядерном кэше - требуется брать мютекс;

    • освободить грязный дисковый кэш - требуется брать мютекс и обращаться к файловой системе и блокам;

    • swap требуется брать мютекс и обращаться к блокам;

    • kill кого-нибудь; Пример, __GFP_ATOMIC - ничего нельзя делать и buddy вернёт NULL. __GFP_NOFS - используются кэшами и буферами, чтобы быть уверенными, что их рекурсивно не позовут. __GFP_NOIO.

  3. Всё остальное - __GFP_ZERO - память которую выдаст аллокатор должен быть забит нулями. __GFP_TEMPORARY - мне нужно выделить страницу подержу её недолго и верну. (пути) GFP_NORETRY GFP_NOFAIL

User memory management

Запросы ядра на выделение памяти: alloc_pages() и kmalloc(), приводят к немедленному выделению памяти, если могут быть удовлетворены. Это оправдано, потому что:

  • Ядро - самый приоритетный компонент системы, его запросы критические.

  • Ядро себе доверяет, предполагается, что в ядре нет ошибок.

Для процессов, работающих в режиме пользователя, всё иначе:

  • Запросы процесса на память можно отложить.

  • В коде пользователя могут быть ошибки, потому нужно быть готовым к обработке ошибок. Когда процесс запрашивает память, он получает не новые страничные кадры, а право обращаться к новым линейным адресам.

Адресное пространство процесса

Адресное пространство процесса - линейные адреса, к которым процесс может обращаться. Ядро может динамически изменять адресное пространство процесса с помощью добавления или удаления областей памяти(vm_area_struct).

Процесс может получить новые области памяти, например, с помощью вызывов: malloc(), calloc(), mmap(), brk(), shmget() + shmat(), posix_memalign(), mmap() и т.д. В основе всех этих вызовов лежит void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);:

  • addr - адрес, где выделять память.

  • flags:

    • NULL - нет никакой разницы, где выделять память. Параметр addr используется, как рекомендация.

    • MAP_FIXED - именно там, где указано в addr.

    • MAP_ANON(MAP_ANONYMOUS) - изменения не будут видны ни в каком файле.

    • MAP_FILE - мапим из файла или устройства.

  • prot:

    • PROT_EXEC

    • PROT_READ

    • PROT_WRITE

    • PROT_NONE

Дескриптор памяти

Вся информация относительно адресного пространства процесса хранится в mm_struct (дескриптор памяти), на который указывает поле mm в task_struct.

task_struct
_________
|   …   |     mm_struct
---------    _________
|   mm  | -> |   …   |
---------    ---------
|   …   |    |  mmap |  ->   vm_area_struct * (VMA) – список двунап.
---------    ---------
             |  pgd  |  - указатель на глобальный каталог страниц
             ---------

Описание структур mm_struct и vm_area_struct монжо найти в /include/linux/mm_types.h.

Область памяти

struct vm_area_struct {
    /* The first cache line has the info for VMA tree walking. */

    unsigned long vm_start;     /* Our start address within vm_mm. */
    unsigned long vm_end;       /* The first byte after our end address
                       within vm_mm. */

    /* linked list of VM areas per task, sorted by address */
    struct vm_area_struct *vm_next, *vm_prev;
             ...................
             struct rb_node               vm_rb;
             ………..
    /* Function pointers to deal with this struct. */
    const struct vm_operations_struct *vm_ops;
}

У области памяти есть два поля vm_start и vm_end, обозначающие соответственно адрес начала и первого бита после конца выделенной области. Если применить mmap() с одинаковыми аргументами, то ядро не будет создавать новый VMA, а просто изменит vm_end уже существующего.

Все области памяти объеденены в двунаправленный список, где они упорядочены по возрастанию адресов. Для того чтобы не приходилось пробегаться по всему списку при выделении, возвращении памяти или поиску VMA, которому принадлежит адрес, все VMA так же объединены в красно-чёрное дерево.

struct rb_node {
    unsigned long  __rb_parent_color;
    struct rb_node *rb_right;
    struct rb_node *rb_left;
} __attribute__((aligned(sizeof(long))));
    /* The alignment might seem pointless, but allegedly CRIS needs it */

struct rb_root {
    struct rb_node *rb_node;
};

mm_struct -> pgd - указатель на глобальный каталог страниц каждого процесса. На x86 при переключении процесса mm_struct -> pgd помещается в cr3. Изменение cr3 в свою очередь приводит к сбросу TLB. Однако, у двух task_struct может быть один и тот же mm, например, у двух потоков, тогда изменения cr3 не будет, что существенно ускоряет работу с памятью.

Помимо потоков переключение cr3 так же не происходит для kernel_thread. Для них просто нет необходимости в областях памяти, так как они всегда обращаются к фиксированным линейным адресам выше TASK_SIZE = PAGE_OFFSET = 0xffff880000000000 (x86_64). Потому собственный mm kernel_thread в task_struct просто не нужен, он равен NULL. Зато в task_struct есть active_mm, равный active_mm вытесненного процесса.

Ещё одним интересным полем в VMA является vm_ops, оно определяет операции, которые можно выполнять для конкретной области памяти.

Работа с областями памяти

Описание функций:

  • do_mmap() (/mm/mmap.c) – выделение новой области памяти

  • do_munmap() (/mm/mmap.c) – возвращение области памяти

  • find_vma()(/mm/mmap.c) – поиск области ближайшей к данному адресу

  • find_vma_intersection() (/include/linux/mm.h) – поиск области, содержащей адрес.

  • get_unmapped_area() (/mm/mmap.c) - поиск свободного интервала

  • insert_vm_struct() (/mm/mmap.c) – внесение области в список дескрипторов

Выделение интервала линейных адресов

Линейные адреса, которые выделяются, могут быть связаны с файлом (FILE) или нет (ANON). При этом, процесс, который запрашивает память, может владеть ими совместно с кем-то (MAP_SHARED) или уникально (MAP_PRIVATE). 

do_mmap
do_mmap

FILE

ANON

MAP_SHARED

vma->file(get_page)

файл на tmpfs(shmat)

MAP_PRIVATE

library(COW)

HIGHMEM, ZERO(buddy)

Отложенное выделение

Как было сказано выше, запросы пользовательского процесса на память можно отложить до момента, когда память действительно понадобиться. Для этого используется механизм обработки исключения page fault, сигнализирующего об отсутствие страницы.

В x86 каждая запись в таблице страниц выровнена по 4096(2^12), потому первые 12 бит несут служебную информацию относительно страницы, например:

  • 0 бит - P (Present) Flag

  • 1 бит - R/W (Read/Write) Flag

  • 2 бит - U/S (User/Supervisor) Flag

Таким образом, если выставить P бит в ноль, то при обращении к данной области памяти будет генерироваться исключение. При генерировании исключения адрес, который его вызвал, сохранится в регистре cr2. Итоговый алгоритм можно представить в виде диаграммы.

                             page fault
                                 \/
              Принадлежит ли адрес пространству процесса?
                  Да /                       Нет  \
                    \/                            \/
   Соответствуют ли права доступа?         Исключение возникло в режиме пользователя?
    Да /                     Нет\           Да /                             Нет \
      \/                        \/            \/                                 \/
Выделить новый                  Послать SIGSEGV                      Ошибка ядра: уничтожить процесс
страничный кадр

Если обращение происходит рядом со stack VMA – область созданная с флагом MAP_GROWDOWN, то происходит расширение области.

Заключение

Вся информация взята из открытых источников.

Документация физической памяти в ядре Linux

Документация на русском "Управлению памятью в Linux"

Если вам понравилась статья, то ставьте плюсы! Следующая статья будет об монтировании и обнаружении дисков в Linux.

Комментарии (44)


  1. E32_735i
    29.11.2023 13:38
    +1

    Не могу плюсануть, поэтому выражаю лайк комментом :)


    1. DrArgentum Автор
      29.11.2023 13:38

      Спасибо!


    1. select26
      29.11.2023 13:38

      Присоединяюсь!
      Отличная статья! Высокий технический уровень.
      Спасибо большое!


  1. mixermsk
    29.11.2023 13:38
    +1

    Привет. Спасибо за пост. Хотел бы добавить несколько комментариев:

    1. Кажется, перед "Из предыдущего листинга видно что процесс..." отвалился сам листинг )

    2. "Запись в linux всегда быстрее чтения". Не всегда, например, если использовать O_SYNC.

    3. vmtouch не нужно собирать, он отлично ставится из apt'a :)


    1. DrArgentum Автор
      29.11.2023 13:38

      Спасибо за примечания!


  1. SIISII
    29.11.2023 13:38
    +8

    Графоманство продолжается. Навскидку:

    Следить за каждым байтом памяти в отдельности было бы накладно, по-этому ядро оперирует достаточно большими блоками памяти - страницами, типовой размер которых составляет 4 килобайта

    Ядро оперирует страницами не потому, что следить за каждым байтом накладно, а потому что аппаратура управления памятью (MMU) отображает виртуальные адреса на реальные, в конечном счёте, именно страницами, а не отдельными байтами. И, кстати говоря, размер страницы вовсе не обязан быть равен 4 Кбайтам -- это, как минимум, зависит от возможностей аппаратуры.

    Также стоит упомянуть, что на аппаратном уровне как правило есть поддержка дополнительного уровня абстракции в виде "сегментов" оперативной памяти, с помощью которых можно разделять программы на части. В отличии от других операционных систем, в Linux она практически не используется - логический адрес всегда совпадает с линейным (адресом внутри сегмента, которые сконфигурированы фиксированным образом).

    Во-первых, сегментация -- отличительная фишка архитектуры IA-32 aka x86, доступная в 16- и 32-разрядных режимах, но выпиленная из 64-разрядного режима. У других архитектур такого бреда мне не встречалось (хотя, возможно, альтернативно одарённые архитекторы подвизаются не только в Интел).

    Во-вторых, сегментацией практически никто никогда не пользовался с момента появления 80386. В частности, ей никогда не пользовались в 32-разрядных версиях Винды.

    Ну и т.д. и т.п. В общем, как обычно: списано у других без особого понимания материала.


    1. robert_ayrapetyan
      29.11.2023 13:38
      +4

      Это не бред, а обратная совместимость. То, что, не в последнюю очередь, позволило данной архитектуре завоевать рынок в 80х.


      1. SIISII
        29.11.2023 13:38
        +2

        Само появление сегментации, как и целая куча других особенностей интеловской архитектуры, является бредом. Они ухитрились собрать практически все возможные грабли в плане архитектуры -- причём сделали это уже в 1970-80-х годах, а не в 1950-60-х, когда опыт только нарабатывался (да и само понятие "архитектура" и её отделение от конкретной реализации появилось лишь в IBMовской Системе 360, а это 1964-й). Ну а что дальше приходится тянуть совместимость -- оно понятно...


        1. robert_ayrapetyan
          29.11.2023 13:38
          +1

          В 8080, разработанной в 70-х, у них не было сегментной модели памяти, никаких граблей они не добавляли. Добавление сегментных регистров в 8086 и 80286 обусловлено, в первую очередь, необходимостью запускать доминирующую тогда в корпоративной среде CP/M на 8086 (фактически эмулируя 8080, для которой эта ОС разрабатывалась) с минимальными тратами на уровне аппаратных ресурсов, это был, по-сути, вынужденный шаг.


          1. SIISII
            29.11.2023 13:38
            +4

            Вообще-то, CP/M делалась для 8-разрядных процов и благополучно работала на том же 8080. Сегментные регистры им понадобились, чтобы расширить физическое адресное пространство до 1 Мбайта с 64 Кбайт. И если в 8086 это ещё можно понять (хотя Zilog в Z8000 сделала куда разумнее, ну а раньше это было сделано DECом в PDP-11 -- тоже 16-разрядных, но с физическим адресом в зависимости от модели в 16, 18 или 22 разряда, и с довольно специфическим, но удобным в использовании MMU), то в 80286... Если уж вводите защиту памяти, то вводите по-человечески, а не через задницу. Собственно, по-человечески и пришлось сделать -- но в 80386.

            Но претензии далеко не только в сегментации. Сама система команд ужасна, а её кодировка -- это вообще нечто... В частности, нет никакого простого способа определить длину кода команды -- приходится байт за байтом его анализировать. Ну и сравните это и с Системой 360, и с той же PDP-11, где длина команды однозначно устанавливается из её первого полуслова (Система 360) / слова (PDP-11; в обоих случаях это 2 байта), а сама команда всегда состоит из 2, 4 или 6 байтов. И всё это дерьмо приходится тащить ради совместимости...


            1. robert_ayrapetyan
              29.11.2023 13:38

              Расширение адресного пространства это само собой, но для этого не было нужды использовать сегментную модель, можно было просто усовершенствовать существующую flat модель из 8080. Почему этого не сделали я написал в предыдущем ответе. По поводу декодирования команд - так оно не для человека, а для железа, у железа несколько другой взгляд на красивость структуры, там важнее производительность и экономия ресурсов.


              1. SIISII
                29.11.2023 13:38
                +3

                Вот производительность как раз и получается кошмарная, и с экономией ресурсов тоже огромные напряги: чем сложнее кодирование, тем труднее декодировать. Ну а про CP/M я тоже написал: она была создана для 8-разрядных компьютеров, и сегментация ей даром была не нужна.


                1. robert_ayrapetyan
                  29.11.2023 13:38

                  Может есть какие пруфы по производительности? у меня вот есть такой: https://web.archive.org/web/20000304083834/http://www.amd-embedded.com/Benchmarks/whyx86.htm. Из них можно понять, почему AMD похерило RISC линейку в пользу x86 в 90-х.


                  1. SIISII
                    29.11.2023 13:38
                    +1

                    Во-первых, я не сравниваю RISC с CISC, как делают в той статье. Я сравниваю отвратительный CISC (IA-32) с хорошими CISCами (PDP-11, VAX-11, System/360,370...z/Architecture).

                    Во-вторых, производительность может сильно зависеть от задачи. Скажем, если тебе нужно массовое шифрование или там преобразование из UTF-8 в UTF-32 и другие весьма распространённые, но не совсем уж тривиальные стандартные операции, то в z/Architecture это выполняется одной командой -- что при прочих равных прилично быстрей чисто программной реализации. (Может, кстати, поддержку подобных вещей и в IA-32 добавили: я особо за ним не слежу.) Ну а на каких-нибудь "графических" задачах даже посредственный GPU уделает любой центральный процессор "обычной" архитектуры, хотя безнадёжно ему сольёт на "задаче общего назначения", где нет возможности массово распараллеливать потоки данных.

                    Ну а в-третьих, есть ещё вопрос энергоэффективности. Интеловский Атом как основа для мобильных платформ, как известно, составить реальную конкуренцию ARMу не смог -- не в последнюю очередь из-за дикой сложности системы команд и её кодирования, для реализации чего требуется куча железа (которое жрёт кучу энергии). При этом собственно вычислительные блоки у процессоров одинаковы: математика -- она и в Африке математика, и её реализация не зависит от системы команд как таковой.

                    Кстати говоря, у современных ARMов от RISC мало что осталось -- по сути, лишь выполнение операций обязательно в регистрах (нет команд "регистр-память" и "память-память"). Это косвенно доказывает порочность самой идеи RISC (максимально простая система команд), если речь заходит от необходимости получить сколько-нибудь приличную производительность. Конкретно в 1980-е и в начале 1990-х RISCи имели более высокую производительность по той причине, что их можно было реализовать в рамках одной микросхемы, а CISCи туда не очень-то лезли (можно вспомнить математические сопроцессоры 8087...80487, хотя у части 80486 эти команды были таки уже реализованы в одном кристалле с основным процессором). Но по мере увеличения доступного количества транзисторов на одном кристалле это преимущество было утрачено, что, собсно, Интел и АМД и демонстрируют, реализуя высокопроизводительные процессоры даже для отвратительной системы команд, хотя и ценой большой сложности и большого потребления энергии.


                    1. cdriper
                      29.11.2023 13:38

                      и, собственно говоря, те феноменальные результаты, которые показывают процессоры Apple в сравнении с Intel и AMD как раз и демонстрируют, где больше нехорошего легаси гавна накопилось -- в x86 или ARM.


                      1. SIISII
                        29.11.2023 13:38
                        +1

                        Только надо не забывать, что все эти бенчмарки нередко "оптимизируют", чтобы показать преимущество над конкурентом, в то время как в реальных приложениях результаты могут быть... иными.


                      1. cdriper
                        29.11.2023 13:38

                        та, ладно, речь идет о серьезных сбалансированных пакетах типа SPEC 2017, в них есть все
                        https://www.anandtech.com/show/17024/apple-m1-max-performance-review/5


                      1. nervoushammer
                        29.11.2023 13:38
                        +2

                        те феноменальные результаты, которые показывают процессоры Apple в сравнении с Intel и AMD как раз и демонстрируют,

                        Феноменальными у семейства M1 были прежде всего размер кэша и пропускная способность памяти. Как только конкурирующие организации тоже завели себе 100+ MB кэша и удвоили толщину канала с памятью, то результаты Apple тут же закончились.

                        где больше нехорошего легаси гавна накопилось -- в x86 или ARM.

                        И таким образом, архитектурное legacy вообще не имеет отношения к вопросу.


                      1. cdriper
                        29.11.2023 13:38

                        а вы TDP при этом сравнивали, а?


                      1. nervoushammer
                        29.11.2023 13:38
                        +1

                        а вы TDP при этом сравнивали, а?

                        Я много каких характеристик не сравнивал. Ни техпроцесс, ни TDP, ни возможность сделать восьмисокетный сервер...


                    1. robert_ayrapetyan
                      29.11.2023 13:38
                      -2

                      Я сравниваю отвратительный CISC (IA-32) с хорошими CISCами (PDP-11, VAX-11, System/360,370...z/Architecture).
                      Реальные результаты сравнения процессоров из разных эпох оставляю на вашей совести.


                      1. SIISII
                        29.11.2023 13:38
                        +1

                        Я сравниваю архитектуры, а не конкретные реализации. Разницу видите? Да и с разными эпохами как-то не очень. Скажем, и PDP-11, и VAX-11, и 8086 -- это 1970-е годы, а z/Architecture -- точно такая же современность, как современные процессоры Интел и АМД.


                      1. robert_ayrapetyan
                        29.11.2023 13:38
                        -2

                        Вот производительность как раз и получается кошмарная
                        Я про это, что было сказано вами в контексте "кошмарной x86 архитектуры" - будут хоть какие-то пруфы? Я вам привел бенчи где АМД отказалась от "красивой" архитектуры в пользу "ужасной" в 90х. Спустя 30 лет мы все там же, в задачах, требующих производительность, все те же "бредовые и ужасные" х86, вот только недавно Амазон стал пропихивать свои гравитоны, но о их внутренней "красоте" стоит только догадываться.


  1. mikegordan
    29.11.2023 13:38
    +1

    главное чтобы не как в Windows когда свободно ФИЗИЧЕСКОЙ памяти 30гб, но даже еще одна вкладка хрома вылетает с out of memory :DDD


    1. kekoz
      29.11.2023 13:38
      +6

      В любой системе когда процесс помирает с“Out of Memory”, речь не о RAM. Для иллюстрации и понимания этого — пара примеров:

      1. В Windows процесс не будет создан (CreateProcess обвалится), если под всю статически необходимую процессу память нет места — внимание — в pagefile.sys. Ещё раз для закрепления в уме — не в RAM, а в файле подкачки.

        Да, там есть нюансы, например, пейджинг сегментов кода и RO-данных может осуществляться из файла самой программы, при условии, что он не на тормозном носителе типа CD :) Именно это, кстати, и есть причина, почему в Windows нельзя удалить файл исполняющейся сейчас программы — он используется, чтобы не забивать pagefile.sys.

      2. Механизмами ограничений (свирепым админом, ulimit, setrlimit, etc...) процессу установлено ограничение на использование памяти. Вот пусть прям по жесткачу — RLIMIT_AS. И пусть у ОС будут хоть терабайты свободной RAM, не в меру прожорливый процесс будет помирать с диагностикой “Out of Memory”


      1. lorc
        29.11.2023 13:38
        +2

        Именно это, кстати, и есть причина, почему в Windows нельзя удалить файл исполняющейся сейчас программы — он используется, чтобы не забивать pagefile.sys.

        Можно подумать что в линуксе не так. Там точно так же секции кода и ro_data мапятся из исполняемого файла в память. Но исполняющийся файл удалить тем не менее можно.

        Фокус в том, что удаляется только запись в директории, само содержимое файла остается на диске до того момента пока файл не отпустят - потом и содержимое будет удалено. Почему этого не может сделать windows - непонятно.


        1. kekoz
          29.11.2023 13:38

          Почему этого не может сделать windows - непонятно.

          Может. Но простому пользователю (Administrator — это тоже простой пользователь в модели безопасности NT) не даёт.


          1. svpcom
            29.11.2023 13:38

            Не может. В файловых системах windows нет inodes. То есть там имя файла в каталоге и само описание файла это одна и та же структура. В linux (и других юниксах) все не так. Там можно отлично держать открытым удаленный файл в то время как запись в каталоге уже указывает на новый или вообще не существует.

            И это фундаментальное ограничение архитектуры NT. Дейв Катлер видно хотел все сделать не так как в unix


        1. strvv
          29.11.2023 13:38

          С юникс-системами немного по-другому, работают не с именем, т.е. ссылкой на тот или иной узел, а с самим узлом, i-node, да и в виндовс, можно также работать (требуются административные права), во всяком случае, когда я кодил в 90-х (nt3.51, nt4.0), когда все ссылки на i-node, включая отображение в память закрываются - тогда она освобождается. хотя пока процесс работает, эту иноду можно подвязать под другую файловую структуру и "восстановить".

          возможно не правильно именую i-node, более 15 лет не программирую, могу лажать (память не становится лучше ;) ).

          Автору - Спасибо, многое вспомнилось и можно просто ткнуть в статью, чем пытаться на пальцах коряво объяснять ;)


      1. mikegordan
        29.11.2023 13:38

        какая мне разница как пользователю ваша тех информация? Мне показывают что ФИЗИЧЕСКОЙ памяти свободно ДОФИГА, которую я за свои $ купил!

        Почему они тогда не изменят интерфейс Диспетчера который будет говорить правду относительно вашей тех информации? в формате (ЗАНЯТО\СВОБОДНО) мне больше ничего не нужно.

        У меня просто как у пользователя случается батхэд когда СВОБОДНО означает НЕ СВОБОДНА


        1. lorc
          29.11.2023 13:38
          +2

          Во-первых "свободно" != "доступно конкретному приложению", что мы видим на вашем примере с хромом. Память то свободна. То что Хром не может ее использовать - это отдельная история. Во-вторых, память которая не используется - куплена зря. Поэтому любая адекватная ОС использует всю свободную память для дискового кеша.

          У вас, кстати, таск менеджер показывает доступную, а не свободную память. Это разные вещи. Свободной у вас должно быть сильно меньше - мегабайт 400, судя по тому что пишет таскменеджер.

          Второе "кстати" - у вас выделено 61 из 64 гб.Такое ощущение что какое-то приложение зарезервировало себе дофига памяти но не использует ее. Человеки, кто винду лучше знает - винда позволяет оверкоммит по памяти? Беглый гуглеж говорит что нет, не позволяет. Так что ищите что у вас там за приложение не умеет обращаться с памятью.


          1. strvv
            29.11.2023 13:38

            Будешь смеяться, но скорее всего сам хром и не умеет. С моделями http страниц так наворочано, что парсить их становится всё сложнее и сложнее (это вылилось в то что сейчас не только лишь все могут распарсить, а осталось два относительно валидных движка - gecko(FFox, Mozilla) и chromium(Chrome, Google) ).

            А если принять всю динамику и подгрузку и подмену, то код ядра парсера должен быть очень аккуратным, а это дорого и долго, а раз в полгода надо выкатывать новую версию (привет Гуглу), вот и вместо того чтобы старый код вымыть и причесать, они новый рождают в дополнение к старому.


    1. nervoushammer
      29.11.2023 13:38

      главное чтобы не как в Windows когда свободно

      Не "свободно", а "доступно". Пункт "Кэшировано" показывает, что эти 30 GB вполне себе используются прямо сейчас в качестве прозрачного кэша файловой системы.

      ФИЗИЧЕСКОЙ памяти 30гб

      В современных ОС, то что вы называете "физической памятью" это на самом деле всего лишь страничный кэш для настоящей памяти, которой являются pagefile.sys и прочие file mappings и section objects.

      И на скриншоте видно, что настоящая память (пункт "Выделено") как раз почти полностью выжрана.

      *Кстати, тот самый Chrome именно так и "работает". Хапает память (настоящую) как не в себя, но при этом единовременно реально использует меньше половины захапанного.

      Так что со сценарием нагрузки со скриншота имеет смысл увеличить размер pagefile.sys раза в полтора-два.


      1. Siemargl
        29.11.2023 13:38
        +1

        Вероятно, данный юзер из секты неиспользующих pagefile.

        Ибо выделено у него столько же, сколько физического ОЗУ


        1. nervoushammer
          29.11.2023 13:38

          Вероятно, данный юзер из секты неиспользующих pagefile.

          Отличное наблюдение. Я уже забыл, что такое вообще бывает и даже не думал на тему.

          Ибо выделено у него столько же, сколько физического ОЗУ

          В собственно отключении pagefile вроде ничего особо ужасного нет. Всё зависит от поведения нагрузки. И вот Chrome как раз очень плох в этом плане.


  1. beremour
    29.11.2023 13:38
    +1

    Почему программы типа Oracle DB требуют отключения transparent hugepages ?


    1. onegreyonewhite
      29.11.2023 13:38

      Просто потому что не умеют с ними работать ещё. Ну и профит можно получить только на ооочень больших объёмах памяти (там на уровне терабайтов).

      Я не претендую на полноту ответа, но это на уровне того, что я знаю об Oracle и Postgres.


  1. zzzzzzerg
    29.11.2023 13:38
    +3

    Советую ознакомиться с классикой - What Every Programmer Should Know About Memory (akkadia.org)


  1. ganzmavag
    29.11.2023 13:38

    Если swap скорее вреден при достаточном количестве памяти, зачем он тогда используется? Почему даже если половина оперативки постоянно свободна, туда что-то, да скидывается? Очень интересно узнать ответ на этот вечный вопрос.

    Сам текст интересно и последовательно изложен, спасибо.


    1. Siemargl
      29.11.2023 13:38
      +2

      Если swap скорее вреден при достаточном количестве памяти

      Кто сказал? Опять секта нестраничников?


      1. ganzmavag
        29.11.2023 13:38

        Там в тексте написано, я поэтому и спрашиваю


        1. Siemargl
          29.11.2023 13:38
          +1

          Не видел, где это в теме написано, но в целом:

          - в Линухе если полезло в своп, это полный писец

          • в Винде, если свопа нет, полный писец

          За форматирование спс новому движку хабра, чтобы их черти жарили


          1. ganzmavag
            29.11.2023 13:38
            +1

            Для этого можно использовать так называемый "swap" раздел или файл. Можно, но на практике не нужно. Если swap выключен, то анонимная память становится невытесняемой, что делает время обращения к ней предсказуемым.

            Вот здесь это написано. И дальше там про то, что минусов у выключения swap толком нет.

            в Линухе если полезло в своп, это полный писец

            Но я как раз там и встречал ситуации, когда из 8 гигов бывает занято максимум 4, но при этом в swap что-то со временем сбрасывается. Зачем и почему? Причем там мелочь какая-то на считанные мегабайты, от них оперативке было бы ни холодно ни жарко. На Windows такое еще больше выражено, но там другая система работы с памятью и это к этой теме относится.


            1. Siemargl
              29.11.2023 13:38
              +1

              Вот здесь это написано. И дальше там про то, что минусов у выключения swap толком нет

              Для Линуха да(собственно тема статьи). Для Винды нет.

              Если глубже копнуть, то там вылезет разница между динзагрузкой elf и pe