mm-fault.c – обработчик страничных ошибок
syscalls.c – высокоуровневая часть перехвата системных вызовов
syscalls-entry.S – низкоуровневая часть перехвата системных вызовов
watch-pids.c – список процессов, за которыми осуществляется мониторинг, добавление и удаление из него
events.c – кольцевой буфер событий
Инициализацию драйвера выполняет функция int __init init(void). Она вызывается при загрузке драйвера в контексте процесса, вызвашего init_module() (системный вызов загрузки драйвера) и выполняет следующие действия:
1. Инициализирует битовую карту отслеживаемых процессов и кольцевой буфер событий
2. Устанавливает обработчики системных вызовов и страничной ошибки
3. Создает директорию /proc/memmon и файлы в ней
Создание файлов происходит в последнюю очередь для того, чтобы пользовательские приложения не могли обратиться к драйверу до завершения инициализации.
Выгрузку выполняет функция void __exit exit(void), вызываемая в контексте процесса, сделавшего delete_module(). Она выполняет действия, обратные к init():
1. Удаляет директорию /proc/memmon
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 драйвера перехватят один и тот же вызов, и будут выгружены не в том порядке, в каком загрузились, последний восстановит неверный адрес в таблице вызовов, вследствие чего произойдет сбой при следующей попытке сделать данный вызов. В связи с этим, начиная с версии 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 вызвавшего процесса и список (с расшифровкой, там, где это имеет смысл) аргументов.
Для хранения журнала событий, таких как выделение блоков виртуальной памяти и страниц, использует кольцевой буфер, защищенный спин-блокировкой. Размер данного буфера может задаваться при загрузке драйвера в качестве его параметра, значение по умолчанию – 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 означает, что текущий процесс не будет вытеснен до разблокировки всех процессов).
Набор отслеживаемых процессов представляется в виде битовой карты на 65536 записей. При запуске нового процесса ядро дает ему PID, на 1 больший, чем PID предыдущего процесса. При достижении очередным процессом PID, равного 65536, новым процессам PID выделяется с единицы (т.е. с первого положительного незанятого). Лишь когда количество процессов в системе превышает эту цифру, ядро начинает выделять бОльшие номера. Данная ситуация крайне маловероятна и драйвером не обрабатывается.