Смекни!
smekni.com

Мониторинг виртуальной памяти в ОС Linux (стр. 3 из 6)

Для добавления и удаления PID'ов отслеживаемых процессов создается файл /proc/memmon/watch-pids с обработчиками open(), release(), read(), write().

Обработчик open(), в случае, если файл открыт для чтения, выделяет буфер ядра и распечатывает туда содержимое текущей битовой карты (при помощи функции bitmap_scnlistprintf()). Попытка открыть файл одновременно на чтение и запись приводит к ошибке EINVAL.

Обработчик read() считывает запрошенный пользователем блок данных из этого буфера

Обработчик write() считывает одно число (возможно, со знаком) из пользовательского буфера. Если оно положительное, оно воспринимается как PID процесса, который необходимо добавить в битовую карту. Если отрицательное – соответствующий PID удаляется. Если 0 – битовая карта обнуляется (не отслеживается ни один процесс).

2.7 Перехват страничных ошибок и выделений страничных фреймов

Возможно осуществить перехват страничных отказов, подменив смещение обработчика исключения в IDT, однако данный метод требует повторения того же анализа сбойного адреса, который делает и стандартный системный обработчик, (файл mm/memory.c) что привело бы к падению производительности системы. В связи с этим было решено внести еще одну модификацию в ядро. При обработке страничного отказа ядро сначала определяет, относится ли сбойная страница к виртуальной памяти процесса (если нет, ему посылается сигнал SIGSEGV), после чего вызывает функцию handle_pte_fault(). Та анализирует причину отказа и либо подгружает страницу из своп-файла / файла отображения, либо выделяет процессу новую страницу, либо посылает ему SIGSEGV (например, при попытке записи в регион памяти, доступной только для чтения), либо происходит ситуация OOM (out of memory), в результате которой сбойный процесс уничтожается с целью освобождения памяти. Модификация ядра добавляет глобальную переменную-указатель на внешнюю функцию, которую каждый раз вызывает handle_pte_fault. При инициализации драйвер заносит туда адрес своей функции mm_handle_fault(), которая, в зависимости от причины страничного отказа, заносит в буфер одно из трех событий (вместе с адресом и типом доступа – чтение либо запись):

ANON_PF – сбой при обращении к новой (еще не выделенной из физической памяти) страницы;

SWAP_PF – сбой при обращении к странице, которая находится в файле подкачки;

FILE_PF – сбой при обращении к странице, которая может быть загружена из файла, отображенного в память (например, код разделяемой библиотеки).

Алгоритм анализа причины страничного отказа продемонтсрирован на рис. 3.7.

2.8 Синхронизация

Так как доступ к кольцевому буферу происходит из различных процессов, необходимо какое-то средство предохранения от «гонок» (race conditions). Ядро Linux предоставляет целый набор примитивов синхронизации. Однако, т. к. блокировка выполняется в том числе и в атомарном контексте, т.е. когда текущий процесс не может быть переведен в режим ожидания (например, при помещении события из обработчика прерывания), единственный подходящий примитив – спин-блокировка (spin-lock), т.е. простая бинарная переменная (со значением 0 либо 1) и активным ожиданием на ней. Захват спин-блокировки происходит при начале операции чтения буфера событий и перед добавлением в буфер нового события. Освобождение – по завершении чтения и добавления события, а так же перед переводом текущего процесса в режим ожидании в случае, когда новых данных в буфере нет.

3. Технологическийраздел

3.1 Язык и средства программирования

Ядро Linux написано на языке С с небольшим количеством кода на ассемблере, и его система сборки, вообще говоря, поддерживает только данные языки для использования в модулях. Выбор из них был сделан в пользу С по причине значительно большей структурированности написанного на нем кода. Однако, небольшой объем кода драйвера пришлось написать на ассемблере, в силу чего он не является платформенно-независмым и привязан к архитектуре x86.

Для сборки драйвера используется сборочная система ядра make/Kbuild и компилятор С из gcc (GNU Compiler Collection). Особенностью ядра Linux является отсутствие совместимости на бинарном уровне, совместимость присутствует лишь на уровне исходных текстов, вследствие чего все модули и само ядро должны быть собраны одной и той же версией одного и того же компилятора. Само ядро изначально написано для компиляции посредством gcc – основного компилятора С в GNU‑системах, поэтому он же должен использоваться и при компиляции модулей для него.

3.2 Компиляция драйвера

Сначала необходимо применить исправление к ядру и перекомпилировать его. Это делается командами:

# cd /usr/src/linux

# patch– p0 < memmon.patch

# make

# make install INSTALL_PATH=/boot modules_install

# lilo

# reboot

После этого возможна сама сборка драйвера. Для этого из папки с его исходниками надо выполнить команду $ make.

3.3 Работа с драйвером

Для загрузки драйвера необходимо выполнить команду

# insmodprocmon.ko

Можно указать размер буфера событий:

# insmodprocmon.kobuflen=65536

Добавить процесс с PID = 3000 к отслеживаемым:

$ echo 3000 > /proc/memmon/watch-pids

Удалить его:

$ echo -3000 > /proc/memmon/watch-pids

Добавить процесс с именем test:

$ echo `pgrep test` > /proc/memmon/watch-pids

Удалить все процессы:

$ echo 0 > /proc/memmon/watch-pids

Просмотреть буфер событий:

$ cat /proc/memmon/events

Выгрузка драйвера делается командой

# rmmodprocmon.ko

(возможно, только если файлы драйвера никем не открыты)

4. Экспериментальный раздел

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

1) Запрашиваем небольшое количество (3–4) страниц памяти, обращаемся ко всей памяти в этом диапазоне, затем освобождаем ее.

Журнал событий:

29305: anon page @b7ee1fa0 (R)

29305: fsync(0)

29305: anon page @b7f22d4e (R) – страничные отказы в сегменте кода libc.so

29305: anon page @b7ee2000 (R)

29305: anon page @b7e85180 (R)

29305: anon page @b7e86330 (R)

29305: anon page @b7f1f680 (R)

29305: anon page @b7ef6470 (R)

29305: anon page @b7e83140 (R)

29305: anon page @b7e82370 (R)

29305: anon page @b7e87de0 (R)

29305: brk(00000000)

29305: brk -> 134520832 (0804a000)

29305: brk(0806e000)

29305: brk -> 134668288 (0806e000) – malloc выделил 144 кб

29305: anon page @0804a004 (W) – здесь malloc заносит метки в начало и конец выделенного региона

29305: anon page @0804d00c (W)

29305: fsync(1)

29305: anon page @0804b000 (R) – сбои при обращении к страницам из цикла

29305: anon page @0804c000 (R)

29305: fsync(2)

29305: brk(0806b000) – free возвращает часть памяти

29305: brk -> 134656000 (0806b000)

29305: fsync(0) – `дальнейшие циклы уже не выделяют памяти

29305: fsync(1)

29305: fsync(2)

В приведенном примере видно, что при выделении 12К памяти malloc() выделяет изначально гораздо бОльший объем (144К), однако реально эти страницы из физической памяти не выделяеются, и при обращениях к ним происходят страничные отказы. В первую и последнюю страницу выделенной секции malloc заносит свои метки (сбой происходит на операции записи).

2) Выделяем подряд блоки по 4 страницы (не освобождая), обращаясь ко всем страницам:

21049: brk(00000000)

21049: brk -> (0804a000)

21049: brk(0806e000)

21049: brk -> (0806e000) – выделение первого блока, malloc выделяет 144 кб

21049: anon page @0804a004 (W)

21049: anon page @0804d00c (W) – записьметок

21049: fsync(1)

21049: anon page @0804b000 (R)

21049: anon page @0804c000 (R) – обращение к выделенной области

21049: fsync(2)

21049: fsync(0)

21049: anon page @08050014 (W) – выделение следующих 12 кб (запись метки)

21049: fsync(1)

21049: anon page @0804e000 (R)

21049: anon page @0804f000 (R) – обращения к выделенной области

21049: fsync(2)

21049: brk(0808f000) – очередной вызов malloc(), расширяем сегмент данных

21049: brk -> 134803456 (0808f000)

21049: anon page @0806e064 (W)

21049: fsync(1)

21049: fsync -> -22 (ffffffea)

21049: anon page @0806c000 (R)

21049: anon page @0806d000 (R)

Таким образом, видно, что malloc выделяет память блоками по 128 с небольшим кб при помощи вызова brk(). Рассмотрим, что происходит при увеличении размера запроса.

3) Запрашиваем последовательно 4, 8, 12, 16К, и т.д., обращаясь в цикле ко всем страницам. При этом, как только размер выделения превышает 128К, malloc выделяет память уже не из области сегмента данных программы (расширяемого вызововм brk()), а при помощи mmap(), после чего free() его освобождает последующим munmap():

789: mmap (00000000, 139264, rw-, PRIVATE | ANON, fd -1, @f019a000)

789: mmap -> -1210519552 (b7d8f000)

789: anon page @b7d8f004 (W)

789: fsync(1)

789: anon page @b7d90000 (R)

789: anon page @b7db0000 (R)

789: fsync(2)

789: munmap (b7d8f000, 139264)

789: munmap -> 0 (00000000)

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

3) Третий пример запрашивает память куда большими блоками, увеличивающимися по 100 Мб. При отключенном overcommit'e память быстро заканчивается, и очередной mmap возвращает ENOMEM:

1536: mmap (00000000, 629149696, rw-, PRIVATE | ANON)

1536: mmap -> -12 (fffffff4)

1536: anon page @b7e06de0 (R)

1536: brk(00000000)

1536: brk -> 134520832 (0804a000)

1536: brk(2d86b000)

1536: brk -> 134520832 (0804a000)

1536: mmap (00000000, 629280768, rw-, PRIVATE | ANON)

1536: mmap -> -12 (fffffff4)

1536: mmap (00000000, 2097152, rw-, PRIVATE | ANON)

1536: mmap -> -1212555264 (b7b9e000)

1536: munmap (b7b9e000, 401408)

1536: munmap -> 0 (00000000)

1536: munmap (b7d00000, 647168)

1536: munmap -> 0 (00000000)

1536: anon page @b7c00008 (W)

1536: mmap (00000000, 629149696, rw-, PRIVATE | ANON)

1536: mmap -> -12 (fffffff4)

Библиотека libc при этом пытается сначала выделить ту же память при помощи вызова brk(), затем снова при помощи вызова mmap(), но уже несколько меньший объем, однако и эти вызовы проходят неудачно.

4) Включим overcommit. Теперь, пока программа не обращается ко всем страницам из выделенного региона, а лишь к некоторым (не расходуя при этом всю физическую память), выполнение проходит нормально, и вызов mmap() успешно выделяет блоки памяти, значительно превыщающие объем свободной оперативной памяти (порядка 600 M):

2515: mmap (00000000, 996151296, rw-, PRIVATE | ANON)

2515: mmap -> 2089299968 (7c883000)

2515: anon page @7c883004 (W)

2515: fsync(1)

2515: anon page @8b603008 (R)

2515: anon page @9a383008 (R)

2515: anon page @a9103008 (R)

2515: fsync(2)

2515: munmap (7c883000, 996151296)

2515: munmap -> 0 (00000000)

В данном примере вызов mmap() выделяет 900М, из которых мы обращаемся лишь к четырем страницам.