Модульная структура драйвера — КиберПедия 

История развития пистолетов-пулеметов: Предпосылкой для возникновения пистолетов-пулеметов послужила давняя тенденция тяготения винтовок...

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

Модульная структура драйвера

2020-04-01 94
Модульная структура драйвера 0.00 из 5.00 0 оценок
Заказать работу

Введение

 

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

· Файловая система /proc – позволяет прочесть различную информацию о всей системе в целом и о каждом из процессов, в том числе информацию об использовании процессом памяти и об отображениях памяти данного процесса. (пример:

# cat /proc/`pgrep test`/status

Name: test1

VmPeak: 1556 kB

VmSize: 1544 kB

VmLck: 0 kB

VmHWM: 308 kB

VmRSS: 308 kB

VmData: 148 kB

VmStk: 88 kB

VmExe: 4 kB

VmLib: 1276 kB

VmPTE: 12 kB

 

# cat /proc/`pgrep test`/maps

08048000–08049000 r-xp 00000000 08:01 17432879 /home/twee/work/mstu/coding/memmon/test/test1

08049000–0804a000 rw-p 00000000 08:01 17432879 /home/twee/work/mstu/coding/memmon/test/test1

0804a000–0806b000 rw-p 0804a000 00:00 0 [heap]

b7e4b000‑b7e4c000 rw-p b7e4b000 00:00 0

b7e4c000‑b7f75000 r-xp 00000000 03:05 1604119 /lib/tls/libc‑2.3.6.so

b7f75000‑b7f76000 r–p 00128000 03:05 1604119 /lib/tls/libc‑2.3.6.so

b7f76000‑b7f79000 rw-p 00129000 03:05 1604119 /lib/tls/libc‑2.3.6.so

b7f79000‑b7f7c000 rw-p b7f79000 00:00 0

b7f9d000‑b7fb3000 r-xp 00000000 03:05 752968 /lib/ld‑2.3.6.so

b7fb3000‑b7fb5000 rw-p 00015000 03:05 752968 /lib/ld‑2.3.6.so

bfc2a000‑bfc40000 rw-p bfc2a000 00:00 0 [stack]

ffffe000‑fffff000 r-xp 00000000 00:00 0 [vdso]

). На приведенной распечатке видно, сколько памяти использует процесс test и под какие именно нужды, а так же его карту памяти. Однако таким образом нельзя отследить динамику работы процесса с памятью.

· strace – утилита, позволяющая трассировать все системные вызовы, выполняемые данным процессом (в частности, выделение памяти вызовами brk/mmap). Она использует стандартный отладочный механизм ядра под названием ptrace – подключается к исследуемому процессу как отладчик (вызовов ptrace(), указывая при этом флаг PTRACE_SYSCALL, что заставляет систему уведомлять трассирующий процесс о всех системных вызовах трассируемого). Пример его работы:

execve (»./test3», [«test3»], [/* 61 vars */]) = 0

fsync(0) = -1 EINVAL (Invalid argument)

mmap2 (NULL, 2101248, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb7bf4000

fsync(1) = -1 EINVAL (Invalid argument)

fsync(2) = -1 EINVAL (Invalid argument)

munmap (0xb7bf4000, 2101248) = 0

exit_group(0)

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

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

 


Аналитический раздел

Постановка задачи

 

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

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

Взаимодействие с пользовательской программой осуществляется посредством файлов, создаваемых в файловой системе /proc.

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

 

Управление памятью

Память компьютера – один из главных ресурсов, и производительность системы критически зависит от политики распределения памяти. Ядро создает виртуальное адресное пространство для каждого процесса, используя при этом ограниченное количество физической памяти и, при необходимости, вторичную память, такую, как жесткий диск. По мере необходимости страницы могут быть выгружены в файл подкачки, либо файл, из которого они были отображены в память (в случае, если они не были модифицированы с момента загрузки из файла, они просто удаляются из памяти). По умолчанию ядро не позволяет выделить одному процессу больше памяти, чем суммарный объем доступной оперативной и swap‑памяти. Однако есть такая возможность, как overcommit («перевыделение»), которая позволяет выделить гораздо больше памяти, при условии, что реально использоватьcя будет лишь небольшая ее часть (допустим, при работе с разреженным массивом). Overcommit включается командой

# echo 1 > /proc/sys/vm/overcommit_memory

а отключается

# echo 0 > /proc/sys/vm/overcommit_memory

Цифра 1 означает выбранный режим управления перевыделением (0 означает его отсутствие, 1 – допустимо перевыделение неограниченных объемов памяти, 2 – некоторый эвристический алгоритм определения максимально допустимого объема перевыделения).

 

Файловая система

Система Linux основана на концепции файловой системы. Практически любой объект системы может быть рассмотрен как файл. Ядро строит единую иерархическую файловую систему (единое дерево директорий) на основе не обладающего иерархической структурой оборудования. В результате абстракция файла активно используется всей системой.

 

Подсистема ввода-вывода

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

 

Сетевая подсистема

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

 

Системные вызовы

Системные вызовы, такие как open(), fork(), read(), etc являются связующим интерфейсом между ядром и пользовательскими приложениями. В Linux 2.6 существует около 330 различных вызовов (многие из них избыточны или сохранены по причинами совместимости). Их вызов происходит через прерывание 0x80 или инструкцию sysenter (на современных процессорах). При этом в регистр EAX помещается номер системного вызова, а в остальные 6 регистров (кроме ESP) – аргументы (т.е. любой системный вызов может принимать до шести 32‑битных аргументов) в порядке EBX, ECX, EDX, ESI, EDI, EBP. Точка входа всех системных вызовов расположена в файле arch/i386/kernel/entry.S, который вызывает обработчик конкретного вызова по таблице вызовов sys_call_table, передавая ей регистры через стек.

 


Загружаемые модули

Одна из важных особенностей ядра Linux – это способность расширять собственную функциональность непосредственно в период выполнения.

Каждый фрагмент исполняемого кода, который может быть добавлен в ядро во время его работы, называется модулем ядра. Каждый модуль создается из объектного кода, не связанного в полноценный исполняемый файл. Модуль может быть загружен в ядро с помощью программы insmod (вызывающей функции create _ module () / init _ module ()), и выгружен с помощью rmmod (вызывающего delete _ module ()). В данной работе реализует именно такой динамически загружаемый модуль.

 

Типы устройств

В Linux различают три основных типа устройств. Каждый драйвер обычно соответствует одному из этих типов. Выделяют:

· Символьные драйверы

· Блочные драйверы

· Сетевые драйверы

Символьное устройство может рассматриваться как поток байт (так же как и файл); символьный драйвер должен реализовывать такое поведение. Такой драйвер имеет, как правило, функции открытия, закрытия, чтения и записи. Текстовая консоль и последовательный порт – примеры символьных устройств. Они могут легко быть представлены абстракцией потоков. Работа с символьными устройствами осуществляется через специальные файлы устройств, находящиеся в директории /dev. Единственное значимое отличие обычного файла от такого устройства – это произвольный доступ, тогда как к большинству символьных устройств можно обращаться лишь последовательно.

Как и к символьным, доступ к блочным устройствам можно получить через файлы в директории /dev. Блочное устройство – это устройство (например, диск), способное содержать в себе файловую систему. В системе Unix блочное устройство может лишь передавать один или более целых блоков данных, обычно по 512 байт. Интерфейс взаимодействия блочных драйверов с ядром значительно отличается от интерфейса символьных драйверов.

Любая сетевая транзакция выполняется через интерфейс сокетов. Сетевой интерфейс отвечает за передачу и прием пакетов под управлением сетевой подсистемы ядра вне зависимости от того, к каким именно транзакциям они относятся. Многие сетевые соединения (особенно использующие протокол TCP) ориентированы на потоки данных. Но сетевые устройства обычно работают с пакетами, а не с потоками. Таким образом, сетевые устройства содержат в себе черты как символьных, так и блочных.

 

 


Конструкторский раздел

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

 

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

Это довольно небезопасный прием, так как если 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 означает, что текущий процесс не будет вытеснен до разблокировки всех процессов).

 

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

 

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

 

 


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

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

 

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

# cd /usr/src/linux

# patch – p0 < memmon.patch

# make

# make install INSTALL_PATH=/boot modules_install

# lilo

# reboot

 

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

 

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

 

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

# insmod procmon.ko

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

# insmod procmon.ko buflen=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

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

# rmmod procmon.ko

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

 


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

 

С целью изучения особенностей выделения памяти в 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М, из которых мы обращаемся лишь к четырем страницам.

5) При попытке попытаться обратиться ко всем страницам из выделенного региона, очень скоро память исчерпывается и возникает ситуация, называемая OOM – Out Of Memory. В этом случае ядро вызывает функцию oom_kill(), которая выбирает процесс-жертву для уничтожения, основываясь на расходах памяти, времени работы, приоритете и т.п., и убивает выбранный процесс, с целью освободить память. При этом в журнал отладочных сообщений ядра выдается уведомление о том, что сработал oom_kill:

kernel: kwin invoked oom-killer: gfp_mask=0x201d2, order=0, oomkillad

6) Включим теперь файл подкачки. В случае, если объем выделяемого региона превышает размер доступной физической памяти, но меньше суммарного размера ее и файла подкачки, страницы из региона сначала выделяются по мере обращения к ним, затем старые начинают выгружаться в swap‑файл, после чего на втором цикле считывания происходит их подкачка оттуда:

19225: anon page @b7418bb0 (R)

19225: anon page @b7602893 (R)

19225: swapfile page @0ae1507c (R)

19225: swapfile page @0d8cb0e6 (R)

19225: swapfile page @0af146b0 (R)

7) Системные вызовы mlock()/mlockall() делают невыгружаемой определенную страницу виртуальной памяти процесса или все его страницы – текущие и / или выделенные в будущем, в зависимости от передаваемых в вызов mlockall() флагов.

Сделаем в начале выполнения программы невыгружемыми все страницы, выделяемые в будущем, после чего выделяем блоки по 12 кб (не освобождая):

13749: brk(00000000)

13749: brk -> 134520832 (0804a000)

13749: brk(0806e000)

13749: anon page @0804a000 (W)

13749: anon page @0806c000 (W)

13749: anon page @0806d000 (W)

13749: brk -> 134668288 (0806e000)

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

Таким образом, можно сделать следующие выводы:

1. При выделении памяти ядро не выделяет сразу все физические страницы (если в mmap() не был передан флаг MAP POPULATE или страницы процесса не были заблокированы в физической памяти вызовами mlock[all]() – в этих случаях страницы всего региона подгружаются сразу)

2. Для выделения небольших объемов памяти библиотека libc расширяет сегмент данных программы вызовом brk(), для запросов, бОльших 128К – использует mmap().

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

В рамках данной работы были исследованы вопросы, связанные с разработкой драйверов под OS Linux, работой ядра Linux с виртуальной памятью и перехватом системных вызовов.

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

Было произведено исследование механизма выделения памяти в ядре Linux и библиотеке libс, исследована технология overcommit.

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

 


Список использованной литературы

 

1. Jonathan Corbet. Linux Device Drivers, 3rd Edition.

2. Роберт Лав. Разработка ядра Linux, 2-е издание.

3. Peter Salzman. The Linux Kernel Module Programming Guide, 3rd Edition.

4. Клаудия Родригес. Азбука ядра Linux.

5. Исходники ядра и документация к ним.

 


Приложения

Код драйвера

Mmon.c

/*

* Main module and entry point of memmon.

*/

 

#include <linux/module.h>

#include <linux/moduleparam.h>

#include <linux/kernel.h>

#include <linux/proc_fs.h>

 

#include «common.h»

#include «watch-pids.h»

#include «mm-fault.h»

#include «events.h»

#include «syscalls.h»

 

/*** Internal data ***/

 

/*

* procfs directory entry

*/

struct proc_dir_entry *procdir = NULL;

 

/*

* Init entry point

*/

static int __init init(void)

{

int ret = – EBUSY;

 

procdir = proc_mkdir (PROCDIR, NULL);

if (! procdir)

goto out;

if (! init_watch_pids())

goto out_procdir;

if (! init_events())

goto out_watch_pids;

if (! capture_syscalls())

goto out_events;

capture_mmfault();

 

return 0;

 

out_events:

fini_events();

out_watch_pids:

fini_watch_pids();

out_procdir:

remove_proc_entry (PROCDIR, NULL);

out:

return ret;

}

 

module_init(init);

 

/*

* Exit point

*/

static void __exit exit(void)

{

release_mmfault();

restore_syscalls();

fini_events();

fini_watch_pids();

remove_proc_entry (PROCDIR, NULL);

}

 

module_exit(exit);

 

/*** Module info ***/

 

MODULE_LICENSE («GPL»);

MODULE_AUTHOR («Ivan Korotkov»);

MODULE_DESCRIPTION («Linux Virtual Memory Monitor»);

 

Events.h

 

/*

* Events ringbuffer.

*/

 

#ifndef MEMMON_EVENTS_H

#define MEMMON_EVENTS_H

 

/* Filename in procfs directory */

#define EVENTS_ENTRY «events»

 

/* Types of events */

enum memmon_event_type

{

NOTUSED = 0, /* to prevent treating zero-filled region as event struct */

MMAP2,

MUNMAP,

MREMAP,

MLOCK,

MUNLOCK,

MLOCKALL,

MUNLOCKALL,

BRK,

FSYNC,

ANON_PF,

SWAP_PF,

FILE_PF,

SYSCALLRET

};

 

/*

* Struct describing each event

*/

struct memmon_event

{

/* Type */

enum memmon_event_type type;

 

/* Caller PID */

pid_t pid;

 

/* Per-type data */

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;

 

struct

{

void __user *start[2];

size_t len[2];

unsigned flags;

} mremap;

 

struct

{

void __user *start;

size_t len;

} mlock, munlock;

 

struct

{

unsigned long flags;

} mlockall;

 

struct

{

void __user *addr;

} brk;

 

struct

{

int fd;

} fsync;

 

struct

{

void __user *addr;

int write;

} pagefault;

 

struct

{

char *callname;

long ret;

} callret;

};

};

 

#define NEVENTS (EVENTS_BUFLEN/sizeof (struct memmon_event))

 

/*

* Initializes event ringbuffer & creates /proc entry

*/

int init_events(void);

 

/*

* Destroys ringbuffer & removes /proc entry

*/

void fini_events(void);

 

/*

* Adds events to ringbuffer tail

*/

void put_event (const struct memmon_event *ev);

 

#endif // MEMMON_EVENTS_H

 

Events.c

/*

* Events ringbuffer.

*/

 

#include <linux/module.h>

#include <linux/moduleparam.h>

#include <linux/kernel.h>

#include <linux/seq_file.h>

#include <linux/proc_fs.h>

#include <linux/poll.h>

#include <linux/mman.h>

 

#include «common.h»

#include «events.h»

 

/*** Forward declarations ***/

 

static int events_open (struct inode *i, struct file *filp);

static unsigned events_poll (struct file *filp, struct poll_table_struct *pt);

 

static void *events_seqstart (struct seq_file *m, loff_t *pos);

static void events_seqstop (struct seq_file *m, void *p);

static void *events_seqnext (struct seq_file *m, void *p, loff_t *pos);

static int events_seqprint (struct seq_file *m, void *p);

 

/* Default ringbuffer size */

#define EVENTS_BUFLEN (32*1024)

 

/* Min ringbuffer size */

#define MIN_EVENTS_BUFLEN (8*1024)

 

/*** Module parameters ***/

 

/* Actual ringbuffer size */

static int buflen = EVENTS_BUFLEN;

module_param (buflen, int, 0444);

 

/*** File operations ***/

 

static const struct file_operations events_fops =

{

owner = THIS_MODULE,

open = events_open,

read = seq_read,

release = seq_release,

poll = events_poll

};

 

static const struct seq_operations events_seqop =

{

start = events_seqstart,

stop = events_seqstop,

next = events_seqnext,

show = events_seqprint

};

 

/*** Internal data ***/

 

/* Ringbuffer */

static struct memmon_event *events;

 

/* Last entry left in ringbuffer

* (where 1st read should begin) */

static int ev_start;

 

/* Current write position */

static int ev_end;

 

/* Whether there was ringbuffer overflow */

static int ev_ovf = 0;

 

DECLARE_WAIT_QUEUE_HEAD (ev_waitq);

spinlock_t ev_lock = SPIN_LOCK_UNLOCKED;

 

/* Damn seq_file doesn't update file pos when we return NULL iterator,

* so we first return this one and then NULL on next seqnext() call */

static void *dummy_ptr = &dummy_ptr;

 

/*** Entry points ***/

 

/*

* open() handler

*/

static int events_open (struct inode *i, struct file *filp)

{

int ret;

 

/*

* Ringbuffer is not seekable

*/

nonseekable_open (i, filp);

 

/*

* Open seq_file and set its initial pos

*/

ret = seq_open (filp, &events_seqop);

if (! ret)

{

struct seq_file *m = filp->private_data;

m->private = filp;

m->index = ev_start;

}

 

return ret;

}

 

/*

* poll/epoll() handler

*/

static unsigned events_poll (struct file *filp, struct poll_table_struct *pt)

{

struct seq_file *m = filp->private_data;

unsigned mask = 0;

 

spin_lock (&ev_lock);

 

poll_wait (filp, &ev_waitq, pt);

/*

* The only poll event we can trigger is normal read event

*/

if (m->index!= ev_end)

mask = POLLIN | POLLRDNORM;

 

spin_unlock (&ev_lock);

 

return mask;

}

 

/*

* Called by seq_file within read() request

*/

static void *events_seqstart (struct seq_file *m, loff_t *pos)

{

struct file *filp = m->private;

 

spin_lock (&ev_lock);

 

/*

* Wait for data become available

*/

while (*pos == (loff_t) ev_end)

{

void *err = NULL;

 

/* Can't schedule while atomic */

spin_unlock (&ev_lock);

 

if (filp->f_flags & O_NONBLOCK)

err = ERR_PTR(-EAGAIN);

else if (wait_event_interruptible (ev_waitq, *pos!= (loff_t) ev_end))

err = ERR_PTR(-ERESTARTSYS);

 

/*

* There IS a slim chance, that we loose waiting condition

* between awakening and acquiring spinlock – hence while() loop

*/

 

spin_lock (&ev_lock);

 

if (err)

return err;

}

 

return events + *pos;

}

 

/*

* Finish read() request

*/

static void events_seqstop (struct seq_file *m, void *p)

{

spin_unlock (&ev_lock);

}

 

/*

* Iterate to next event

*/

static void *events_seqnext (struct seq_file *m, void *p, loff_t *pos)

{

struct memmon_event *ev;

 

/* Dummy iterator – time to exit */

if (p == dummy_ptr)

return NULL;

 

++*pos;

ev = events + *pos;

 

/* Overflow */

if (ev – events > NEVENTS)

*pos = 0;

 

/*

* We reached end. Decrement file pos ('coz it will be incremented then back)

* and return dummy iterator (otherwise file pos won't be updated at all)

*/

if (*pos == (loff_t) ev_end)

{

–*pos;

return dummy_ptr;

}

 

return events + *pos;

}

 

/*

* Actually prints current iterator to read buffer

*/

static int events_seqprint (struct seq_file *m, void *p)

{

struct memmon_event *ev = p;

 

if (ev == dummy_ptr)

return 0;

 

seq_printf (m, «%d:», ev->pid);

 

switch (ev->type)

{

case MMAP2:

seq_printf (m, «mmap (%p,%u,», ev->mmap2.start, ev->mmap2.len);

if (ev->mmap2.prot & PROT_READ)

seq_puts (m, «r»);

else

seq_puts (m, «–»);

if (ev->mmap2.prot & PROT_WRITE)

seq_puts (m, «w»);

else

seq_puts (m, «–»);

if (ev->mmap2.prot & PROT_EXEC)

seq_puts (m, «x,»);

else

seq_puts (m, «-,»);

 

if (ev->mmap2.flags & MAP_SHARED)

seq_puts (m, «SHARED»);

else if (ev->mmap2.flags & MAP_PRIVATE)

seq_puts (m, «PRIVATE»);

 

if (ev->mmap2.flags & MAP_LOCKED)

seq_puts (m, «| LOCKED»);

if (ev->mmap2.flags & MAP_ANON)

seq_puts (m, «| ANON»);

if (ev->mmap2.flags & MAP_POPULATE)

seq_puts (m, «| READAHEAD»);

 

if (ev->mmap2.flags & MAP_ANON)

seq_puts (m,»)\n»);

else

seq_printf (m,», fd% ld, @%p)\n», (long) ev->mmap2.fd,

(void *) ev->mmap2.off);

break;

 

case MUNMAP:

seq_printf (m, «munmap (%p,%d)\n», ev->munmap.start, ev->munmap.len);

break;

 

case MREMAP:

seq_printf (m, «mremap (%p,%d ->%p,%d)\n», ev->mremap.start[0], ev->mremap.len[0],

ev->mremap.start[1], ev->mremap.len[1]);

break;

 

case MLOCK:

seq_printf (m, «mlock (%p,%d)\n», ev->mlock.start, ev->mlock.len);

break;

 

case MUNLOCK:

seq_printf (m, «munlock (%p,%d)\n», ev->munlock.start, ev->munlock.len);

break;

 

case MLOCKALL:

seq_puts (m, «mlockall(»);

if (ev->mlockall.flags & MCL_CURRENT)

{

seq_puts (m, «CURRENT»);

if (ev->mlockall.flags & MCL_FUTURE)

seq_puts (m, «| FUTURE»);

}

else if (ev->mlockall.flags & MCL_FUTURE)

seq_puts (m, «FUTURE»);

seq_puts (m,»)\n»);

 

break;

 

case MUNLOCKALL:

seq_puts (m, «munlockall()\n»);

break;

 

case BRK:

seq_printf (m, «brk(%p)\n», ev->brk.addr);

break;

 

case FSYNC:

seq_printf (m, «fsync(%d)\n», ev->fsync.fd);

break;

 

case ANON_PF:

seq_printf (m, «anon page @%p (%s)\n», ev->pagefault.addr,

ev->pagefault.write? «W»: «R»);

break;

 

case SWAP_PF:

seq_printf (m, «swapfile page @%p (%s)\n», ev->pagefault.addr,

ev->pagefault.write? «W»: «R»);

break;

 

case FILE_PF:

seq_printf (m, «shared file page @%p (%s)\n», ev->pagefault.addr,

ev->pagefault.write? «W»: «R»);

break;

 

case SYSCALLRET:

seq_printf (m, «%s ->%ld (%p)\n», ev->callret.callname, ev->callret.ret,

(void *) ev->callret.ret);

break;

 

default:

printk («memmon: Unexpected event% d\n», ev->type);

return 1;

}

 

return 0;

}

 

/*** Exported entries ***/

 

/*

* Initializes event ringbuffer & creates /proc entry

*/

int init_events(void)

{

struct proc_dir_entry *entry;

 

buflen = max (buflen, MIN_EVENTS_BUFLEN);

 

events = kzalloc (buflen, GFP_KERNEL);

if (! events)

{

printk («memmon: Event ringbuffer too big!\n»);

return 0;

}

 

ev_start = ev_end = 0;

 

entry = create_proc_entry (EVENTS_ENTRY, 0444, procdir);

if (entry)

entry->proc_fops = &events_fops;

else

{

kfree(events);

return 0;

}

 

return 1;

}

 

/*

* Destroys ringbuffer & removes /proc entry

*/

void fini_events(void)

{

remove_proc_entry (EVENTS_ENTRY, procdir);

kfree(events);

}

 

/*

* Adds events to ringbuffer tail

*/

void put_event (const struct memmon_event *ev)

{

spin_lock (&ev_lock);

 

events [ev_end] = *ev;

/* Overflow */

if (++ev_end > NEVENTS)

{

ev_start = ev_end = 0;

ev_ovf = 1;

}

 

/*

* If overflow happened at least once, ev_start must be next to ev_end.

* Otherwise, it remains zero.

*/

if (ev_ovf && ++ev_start > NEVENTS)

ev_start = 0;

 

spin_unlock (&ev_lock);

 

wake_up_interruptible_sync (&ev_waitq);

}

 

Watch-pids.h

/*

* Selection of PIDs to watch for.

*/

 

#ifndef MEMMON_WATCH_PIDS_H

#define MEMMON_WATCH_PIDS_H

 

/*

* Checks whether PID @pid is present in PID set

* Returns 1 if present

*/

int pid_present (pid_t pid);

 


Поделиться с друзьями:

Археология об основании Рима: Новые раскопки проясняют и такой острый дискуссионный вопрос, как дата самого возникновения Рима...

Типы оградительных сооружений в морском порту: По расположению оградительных сооружений в плане различают волноломы, обе оконечности...

Индивидуальные очистные сооружения: К классу индивидуальных очистных сооружений относят сооружения, пропускная способность которых...

Опора деревянной одностоечной и способы укрепление угловых опор: Опоры ВЛ - конструкции, предназначен­ные для поддерживания проводов на необходимой высоте над землей, водой...



© cyberpedia.su 2017-2024 - Не является автором материалов. Исключительное право сохранено за автором текста.
Если вы не хотите, чтобы данный материал был у нас на сайте, перейдите по ссылке: Нарушение авторских прав. Мы поможем в написании вашей работы!

0.798 с.