Перехват системных вызовов
Одно из основных действий, выполняемых драйвером – перехват системных вызовов. Это довольно небезопасный прием, так как если 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 означает, что текущий процесс не будет вытеснен до разблокировки всех процессов).
Воспользуйтесь поиском по сайту: ©2015 - 2024 megalektsii.ru Все авторские права принадлежат авторам лекционных материалов. Обратная связь с нами...
|