Процесс, в простейших терминах, — это выполняющаяся программа. Процессы в операционной системе изолированы друг от друга. В частности, каждый из них имеет своё виртуальное адресное пространство. Оно разбито на маленькие блоки, называемые страницами. Физически страница может находиться не только в памяти, но и на диске. Это позволяет исполнять программы, которым нужно больше памяти, чем есть в компьютере. Кроме того, одну и ту же физическую страницу можно использовать в нескольких процессах (например ту, в которую загружен фрагмент стандартной библиотеки). Трансляцией виртуальных адресов в физические занимается процессор, а конкретно Memory Management Unit в нём.
В рамках виртуального адресного пространства память разделена на несколько секций.
.text
хранит код программы.data
хранит глобальные инииализрованные переменные.bss
хранит неинициализированные глобальные переменные.
Посмотреть на содежимое этих секций можно с помощью флага -t
команды objdump
. Предлагается погрепать её вывод на примере sections.c
.
С кучей и стеком вы уже знакомы из прошлых курсов. Важно про них запомнить, что стек растёт вниз, а куча наверх (по адресам). При этом через objdump
вы не увидите переменные созданные на стеке и куче, потому что память под них выделяется динамически.
Адреса внутри массива всё ещё растут вверх. Почему? (
a[i] = *(a + i)
)
#include <sys/mman.h>
void *mmap(
void *addr, /* рекомендуемый адрес отображения */
size_t length, /* размер отображения, дополняется до кратного размеру страницы */
int prot, /* аттрибуты доступа PROT_READ/PROT_WRITE/PROT_EXE/PROT_NONE */
int flags, /* флаги совместного отображения MAP_PRIVATE/MAP_SHARED/MAP_FIXED/MAP_ANONYMOUS */
int fd, /* файловый декскриптор фала */
off_t offset /* смещение относительно начала файла, должен быть кратен размеру страницы */
);
int munmap(void *addr, size_t length) /* освободить отображение */
Системный вызов mmap
предназначен для создания в виртуальном адресном пространстве процесса доступной области по определенному адресу. Эта область может быть связана как с открытым файлом, так и только с участком в оперативной памяти (флаг MAP_ANONYMOUS
). Память выделяется постранично.
Атрибуты доступа аналогичны файловым. Флаг MAP_SHARED
позволяет делать память доступной другим процессам, MAP_PRIVATE
наоборот (в случае файла, изменения в него не сохраняются).
proc
это виртуальная файловая система, которая позволяет просматривать структуры ядра. Про все её применения можно почитать, например, в man'е. Мы здесь поговорим только о том, как можно с её помощью залезть в память процесса.
Чтобы получить доступ к маппингам, можно воспользоваться командой
cat /proc/pid/maps
.
Вывод получится примерно следующего вида:
7f6d4e51e000-7f6d4e51f000 r--p 00000000 08:09 948631 /home/arslan/teaching/caos-seminars/sem10-mmap/small_file.txt
Где
7f6d4e51e000-7f6d4e51f000
это диапазон адресов в виртуально памяти процессаr--p
это флаги доступа00000000
это отступ от начала файла08:09
id устройства948631
inode файла/home/arslan/teaching/caos-seminars/sem10-mmap/small_file.txt
путь к файлу
Через /proc/pid/mem
можно получить доступ к памяти процесса. Для этого нужно сначала подцепиться к процессу через ptrace(PTRACE_ATTACH, pid, NULL, NULL)
(чтобы сработало, нужно либо дать конкретному исполняемому файлу разрешение на ptrace sudo setcap cap_sys_ptrace+eip proc_mem_demo.out
, либо запустить его под sudo). Так делают, например, дебаггеры. После этого можно открыть /proc/pid/mem
просто как файл на чтение, сдвинуться с помощью lseek
на смещение, равное адресу в виртуальной памяти процесса и что-нибудь прочитать. proc_mem_demo.c
, например, считывает первые несколько байт заmmap'ленного в память файла.
Page fault это аппаратное исключение, возникающее при обращении к странице памяти, которая не загружена в адресное пространство процесса. Они бывают трёх видов:
- minor -- если страница есть в оперативной памяти, но не в отмечена как загруженная в виртуальном адресном пространстве процесса. Обычно это означает, что кто-то из других процессов её уже загрузил.
- major -- если страницы нет в оперативной памяти. Это значит, что нужно выделить место в памяти и загрузить туда страницу с диска.
- invalid -- если обращение идёт по адресу не из адрессного пространство процесса. Обычно приводит к Segmenation fault.
Считать количество page fault можно с помощью системного вызова getrusage
.
Будем проводить следующий эксперимент. Возьмём файл big_file.txt
, представлющий собой строку цифр, предваритально сгенерированный скриптом gen_big_file.py
. Будем считать количество вхождений каждой цифры в него с помощью mmap
(см. mmap_demo.c
).
Перед каждым запуском эксперимента будем чистить кэш страниц с помощью команды
sudo sh -c 'echo 1 >/proc/sys/vm/drop_caches'
Запустим программу в первый раз ./mmap_demo.out big_file.txt
. Сколько вы ожидаете увидеть Major page fault
. Почему?
На самом деле, получились единчиные Major page faults
. Почему?
Доступ к диску довольно трудозатратная операция. Поэтому, когда вы вычитываете заmmapленные данные, операционная система пытается предугадать ваши действия и считывает несколько следующих блоков.
Запустим программу ещё раз, увидим с большой вероятностью Major page faults: 0
, потому что страницы с содержимым файла ещё не успело вытеснить из кэша.
Почистим кэш и раскомментируем 17 строку. В ней мы говорим операционной системе, что к данному участку памяти собираемся обращаться случайным образом, поэтому смысла вычитывать несколько следующих блоков нет. Почистим кэши и перезапустим программу (не забыв её перекомпилировать). Увидим, что количество page fault примерно стало равно file_size_in_bytes/4096
, что мы означально и ожидали.