Смекни!
smekni.com

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

mm-fault.c – обработчик страничных ошибок

syscalls.c – высокоуровневая часть перехвата системных вызовов

syscalls-entry.S – низкоуровневая часть перехвата системных вызовов

watch-pids.c – список процессов, за которыми осуществляется мониторинг, добавление и удаление из него

events.c – кольцевой буфер событий

2.2 Инициализация и выгрузка драйвера

Инициализацию драйвера выполняет функция int __init init(void). Она вызывается при загрузке драйвера в контексте процесса, вызвашего init_module() (системный вызов загрузки драйвера) и выполняет следующие действия:

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

2. Устанавливает обработчики системных вызовов и страничной ошибки

3. Создает директорию /proc/memmon и файлы в ней

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

Выгрузку выполняет функция void __exit exit(void), вызываемая в контексте процесса, сделавшего delete_module(). Она выполняет действия, обратные к init():

1. Удаляет директорию /proc/memmon

2. Снимает перехват системных вызовов и страничного отказа

3. Освобождает память

2.3 Взаимодействие с пользовательскими приложениями

Для взаимодействия с пользовательскими приложениями драйвер использует файловую систему procfs – псевдо-файловую систему, предоставляющую различную информацию о системе. Любой драйвер может добавлять в ее иерархию свои файлы и папки для передачи пользовательским программам различной информации. Ядро предоставляет ряд функций для работы с procfs, из который данный драйвер использует следующие:

proc_mkdir() – создает папку в /proc

create_proc_entry() – создает файл в указанной папке /proc или самой /proc

remove_proc_entry() – удаляет папку или файл в /proc

На уровне ядра Linux любой открытый файл представлен структурой file, хранящей указатель f_fops на структуру file_operations, содержащую адреса обработчиков различных запросов к файлу. Допустим, когда приложение открывает файл (делая системный вызов open()), он вызывает обобщенный уровень файловых систем VFS (функцию vfs_read()), которая в свою очередь вызывает обработчик f_ops->open. Для обычных файловых систем обработчики запросов к файлу находятся в драйвере файловой системы, которой принадлежит данный файл. Однако /proc не представляет из себя какой-либо реальной ФС на реальном хранилище данных, и каждый драйвер, добавляющий туда файлы, должен для них предоставлять свои обработчики (точки входа), которые и будут вызываться при работе с этими файлами.

Файлы, создаваемые в /proc, представляются структурой proc_entry, содержащую поле proc_fops, куда и заносится указатель на структуру file_operations для данного файла.

Данный драйвер создает в папке /proc/memmon 2 файла – watch-pids – для добавления / удаления процессов в список отслеживаемых и events – файл, содержащий собственно лог событий.

2.4 Перехват системных вызовов

Одно из основных действий, выполняемых драйвером – перехват системных вызовов.

Это довольно небезопасный прием, так как если 2 драйвера перехватят один и тот же вызов, и будут выгружены не в том порядке, в каком загрузились, последний восстановит неверный адрес в таблице вызовов, вследствие чего произойдет сбой при следующей попытке сделать данный вызов. В связи с этим, начиная с версии 2.5, ядро более не экспортирует таблицу системных вызовов. Тем не менее, эта проблема устраняется небольшим исправлением ядра (patch), который добавляет в произвольный файл ядра следующие строки, экспортирующие из ядра данную таблицу.

extern void *sys_call_table[];

EXPORT_SYMBOL_GPL (sys_call_table);

Для перехвата системных вызовов имеются 3 таблицы указателей – оригинальных (системных) обработчиков, наших пре-обработчиков (вызываемых ДО оригинального, и принимающих аргументы) и пост-обработчиков (вызываемых ПОСЛЕ и принимающих возвращаемое значение):

void *old_sys_call [NR_syscalls];

void *sys_call_trap [NR_syscalls];

void *sys_call_exit [NR_syscalls];

Кроме того, есть общая таблица структур, каждый элемент которой описывает один из перехватываемых системных вызовов:

struct syscall_handler

{

/* Syscall nr */

int nr;

/* Pre-call & post-call handler */

void *hand1, *hand2;

};

Функцияcapture_syscalls(), вызываемаяприинициализациидрайвера, копируетэтиадресавпредыдущие 2 таблицыизаписываетвsys_call_tableпонужнымномерамадресуниверсальногоперехватчика – syscalls_entry, находящегосявфайлеsyscalls-entry.

Необходимость в ассемблерном коде обусловлена механизмом обработки системных вызовов в Linux. На рис. 3 показан стек на момент вызова обработчика системного вызова (замененного или стандартного). Проблема заключается в том, что некоторые стандартные обработчики требуют, чтобы стек имел именно такой вид, и если вызывать их из нового обработчика, они правильно работать не будут. В связи с этим syscalls_entry сначала вызывает пре-обработчик системного вызова, затем заменяет в стеке адрес возврата на адрес следующей инструкции и делает переход на стандартный обработчик, который получает кадр стека в изначальном виде. Затем, при возврате, мы попадаем на следующую инструкцию, которая вызывает пост-обработчик и делает перход на изначальный адрес возврата, на код в arch/i386/kernel/entry.S (точки входа всех системных вызовов в Linux). Этот адрес сохраняется внизу стека ядра, там же где хранится указатель на текущую задачу и некоторая другая служебная информация ядра. Данные действия продемонстрированы на рис. 3.3–3.5.

Данный драйвер перехватывает следующие системные вызовы:

m[un] map() – выделение / освобождение региона памяти

mremap() – перемещение региона памяти

brk() – расширение / сужение сегмента данных программы

m[un] lock[all]() – блокировка набора страниц в рабочем множестве процесса

fsync() – используется в качестве маркера в журнале событий

Для каждого из вызовов в журнал печатается имя вызова, PID вызвавшего процесса и список (с расшифровкой, там, где это имеет смысл) аргументов.

2.5 Кольцевой буфер событий

Для хранения журнала событий, таких как выделение блоков виртуальной памяти и страниц, использует кольцевой буфер, защищенный спин-блокировкой. Размер данного буфера может задаваться при загрузке драйвера в качестве его параметра, значение по умолчанию – 32 кб, минимальное – 8 кб. Память для буфера выделяется функцией kzalloc() – аналогом фукнций malloc/calloc() из стандартной библиотеки С. Передаваемый ей параметр GFP_KERNEL означает, что память выделяется для ядра (т.е. не может быть позже выгружена с диска), но не в атомарном контексте (т.е. текущий процесс может быть отложен до освобождения необходимой памяти).

Каждая запись в буфере представляет из себя следующую структуру:

enum memmon_event_type – типсобытия

{

NOTUSED = 0

MMAP2,

MUNMAP,

MREMAP,

MLOCK,

MUNLOCK,

MLOCKALL,

MUNLOCKALL,

BRK,

FSYNC,

ANON_PF, – страничная ошибка на анонимной странице

SWAP_PF, – на странице из файла подкачки

FILE_PF, – из разделяемого файла

SYSCALLRET – возврат из системного вызова

};

struct memmon_event

{

enum memmon_event_type type; – типсобытия

pid_t pid; – PID вызвавшего процесса

union – специфичны для события данные

{

struct

{

void __user *start;

size_t len;

} munmap;

struct

{

void __user *start;

size_t len;

unsigned long prot, flags;

unsigned long fd, off;

} mmap2;

……

};

}

С буфером событий связаны следующие переменные:

static struct memmon_event *events; – собственнобуфер

static int ev_start; – индекс самой старой записи в буфере

static int ev_end; – индекс последней записи

static int ev_ovf = 0; – было ли уже переполнение буфера

DECLARE_WAIT_QUEUE_HEAD (ev_waitq); – очередь ожидания (для блокирующего чтения)

spinlock_t ev_lock = SPIN_LOCK_UNLOCKED; – спин-блокировка для защиты от гонок при обращении к буферу

Пользовательские приложения запрашивают содержимое буфера событий, читая файл /proc/memmon/events. Если при открытии файла не был установлен флаг O_NONBLOCK, операция чтения по нему блокирующая – т.е., если новых данных в буфере нет, read() переводит процесс в состояние ожидания (interruptible sleep) вызовом функции wait_event_interruptible() до получения сигнала либо появления новых данных в буфере.

Помимо open() и release(), вызываемых при открытии (создании новой структуры file) и ее уничтожении, в file_operations данного файла определены всего 2 точки входа – read() и poll(). Обработчик poll() вызываемается, когда какой-то процесс делает вызов select() по данному файлу – ожидает, пока на нем будут доступные для чтения данные. Кроме того, в флагах структуры file вызовом nonseekable_open() сбрасывается бит, позволяющий делать вызов llseek() по файлу (т. к. данная операция лишена смысла для кольцевого буфера).

Для реализации функции read() используется абстракция под названием seq_file, предназначенная для буферизации считываемых данных. Она требует задания 4 функций – seq_start(), вызываемой при начале чтения из файла, seq_next(), вызываемой перед копированием в буфер пользователя записи об очередном событии, seq_show(), собственно осуществляющющей это копирование, и seq_stop(), вызываемой при завершении передачи данных в пользовательский буфер (когда скопировано затребованное количество данных или не осталось событий в буфере).

Рис. показывает связь между этими функциями и структурами.

При добавлении нового события в буфер происходит пробуждение все ожидающих на очереди процессов вызовом wake_up_interruptible_sync() (sync означает, что текущий процесс не будет вытеснен до разблокировки всех процессов).

2.6 Набор отслеживаемых процессов

Набор отслеживаемых процессов представляется в виде битовой карты на 65536 записей. При запуске нового процесса ядро дает ему PID, на 1 больший, чем PID предыдущего процесса. При достижении очередным процессом PID, равного 65536, новым процессам PID выделяется с единицы (т.е. с первого положительного незанятого). Лишь когда количество процессов в системе превышает эту цифру, ядро начинает выделять бОльшие номера. Данная ситуация крайне маловероятна и драйвером не обрабатывается.