§ 11. Память

01 Второй важной и наиболее часто используемой группой системных вызовов являются функции работы с памятью. Ядро выделяет память постранично (фактически, ядро выделяет страницы, а не произвольные блоки памяти), размер страницы равен 4 Кб. Если за один раз выделяется очень много страниц, то ядро может объединить их в одну большую размером 2 Мб или 2 Гб в зависимости от настроек. Функция mmap используется для выделения памяти, а также для отображения содержимого файлов на страницы памяти. Первый аргумент обозначает адрес из адресного пространства процесса, где должна отобразиться страница. Выбор адреса полезен при создании страниц, с которым одновременно работает сразу несколько процессов. Третий аргумент задает права доступа. Как правило используют чтение/запись, но не исполнение кода, чтобы избежать возможных уязвимостей. Следующий аргумент — это флаги. Их в Linux больше десяти. Наиболее часто используются либо анонимные страницы, то есть память доступная только текущему процессу, либо общедоступные процессы, то есть память доступная сразу нескольким процессам и использующаяся для межпроцессного взаимодействия. Также общедоступная память используется для отображения файлов, чтобы все процессы, отображающие его в память, видели изменения его содержимого. Предпоследний аргумент — это файловый дескриптор, используемые для отображения содержимого файла, а последний аргумент — это отступ в байтах внутри файла.

size_t size = 4096;
void* ptr = mmap(              // выделить страницы памяти
    nullptr,                   // адрес
    size,                      // размер в байтах
    PROT_READ|PROT_WRITE,      // права доступа
    MAP_PRIVATE|MAP_ANONYMOUS, // опции
    -1,                        // файловый дескриптор
    0                          // отступ внутри файла
);
munmap(ptr, size);             // освободить страницы памяти
Выделение и освобождение памяти в Linux.

02 Функция munmap освобождает отображенную страницу, записывая ее содержимое в связанный с ней файл, если таковой имеется.

03 Эти функции используются для выделения и освобождения памяти в соответствующих функциях из стандартной библиотеки C (malloc и др.) и операторах new и delete языка C++.

04 Функция mremap аналогичная функции realloc языка C. Она используется для изменения размера выделенного участка памяти.

05 Наконец, функция madvise используется для управления выделенным участком памяти. В таблице показаны четыре флага в качестве примера. Первые два флага являются указанием ядру, как будут использоваться страницы памяти: имеет ли смысл делать упреждающее чтение или нет? Вторые два флага позволяют пометить страницы как нужные или ненужные, что используется для для эффективного чтения файлов большого размера.

ptr = mremap(
    ptr,                       // текущий адрес
    size,                      // текущий размер
    size*2,                    // новый размер
    MREMAP_MAYMOVE             // опции
);
size *= 2;
madvise(ptr, size, MADV_SEQUENTIAL); // управление страницами
Изменение размера выделенного блока памяти.
ФлагОписание
MADV_SEQUENTIALпоследовательный
MADV_RANDOMпроизвольный
MADV_WILLNEEDскоро понадобится
MADV_DONTNEEDбольше не нужен
Флаги системного вызова madvise.

06 В различных источниках можно найти упоминание того, что раз ядро использует страницы памяти для чтения и записи файлов, то использование этих страниц напрямую позволит наиболее эффективно читать и писать файлы. Чтобы проверить это утверждения я написал небольшую программу, которая копировала файлы разных размеров с помощью стандартных средств C++, с помощью системного вызова read и с помощью системного вызова mmap. Как видно из графиков для больших файлов использование страниц напрямую действительно наиболее эффективно при чтении, а для маленьких файлов (размером не более трех страниц) использование обычных операций чтения и записи позволяет получить небольшое преимущество.

0510152025300246810121416Время, мсРазмер, Мбstreambufreadmmap681012141618202224260246810121416Время, мксРазмер, Кбstreambufreadmmap
Скорость работы mmap по сравнению со стандартными спообами чтения файлов.

07 Куда же попадают страницы, на которые отображаются файлы? Все очень просто: они так и остаются в отдельной области памяти ядра, называемой кэшем. Делается это из соображений эффективности, Linux настраивается путем изменения текстовых файлов. Любое повторное чтение файла происходит быстрее, чем первое чтение, что оптимизирует скорость работы системы. Как только свободной памяти начинает не хватать приложениям, вместо нее используются страницы из кэша.

$ free -m
       total  used  free  shared  buff/cache  available
Mem:    7973  1669  3860      64        2442       6140
Swap:   7999    55  7944
Разные типы памяти.

08 Подведем итог.

Задания

Буферы1 балл

09 Напишите класс ByteBuffer, который представляет собой массив байт, размер которого можно изменять динамически. Для этого в конструкторе выделите память с помощью системного вызова mmap, в деструкторе освободите память с помощью munmap. Не забудьте про проверку возвращаемого значения каждого системного вызова!

Перемещение1 балл

10 Добавьте в класс ByteBuffer конструкторы и операторы перемещения. Проверьте, что память корректно освобождается с помощью санитайзеров.

Изменение размера1 балл

11 Добавьте в класс ByteBuffer метод для изменения размера выделенной области памяти resize, который использует вызов mremap.

File + ByteBuffer2 балла

12 Объедините класс File из заданий к предыдущем занятию с классом ByteBuffer. Для этого сделайте File полем класса ByteBuffer и добавьте конструктор ByteBuffer, который позволяет создавать буфер из файла: вам понадобится вызов stat, чтобы узнать размер файла. Также модифицируйте деструктор, так чтобы он записывал буфер в файл с помощью вызова msync.

Видео

Запись лекции 13.11.2021.