- PVSM.RU - https://www.pvsm.ru -
Привет!
В предыдущей статье [1] я рассказал про vfork() и пообещал рассказать о реализации вызова fork() как с поддержкой MMU, так и без неё (последняя, само собой, со значительными ограничениями). Но прежде, чем перейти к подробностям, будет логичнее начать с устройства виртуальной памяти.
Конечно, многие слышали про MMU, страничные таблицы и TLB. К сожалению, материалы на эту тему обычно рассматривают аппаратную сторону этого механизма, упоминая механизмы ОС только в общих чертах. Я же хочу разобрать конкретную программную реализацию в проекте Embox [2]. Это лишь один из возможных подходов, и он достаточно лёгок для понимания. Кроме того, это не музейный экспонат, и при желании можно залезть “под капот” ОС и попробовать что-нибудь поменять.
Любая программная система имеет логическую модель памяти. Самая простая из них — совпадающая с физической, когда все программы имеют прямой доступ ко всему адресному пространству.
При таком подходе программы имеют доступ ко всему адресному пространству, не только могут “мешать” друг другу, но и способны привести к сбою работы всей системы — для этого достаточно, например, затереть кусок памяти, в котором располагается код ОС. Кроме того, иногда физической памяти может просто не хватить для того, чтобы все нужные процессы могли работать одновременно. Виртуальная память — один из механизмов, позволяющих решить эти проблемы. В данной статье рассматривается работа с этим механизмом со стороны операционной системы на примере ОС Embox [2]. Все функции и типы данных, упомянутые в статье, вы можете найти в исходном коде нашего проекта.
Будет приведён ряд листингов, и некоторые из них слишком громоздки для размещения в статье в оригинальном виде, поэтому по возможности они будут сокращены и адаптированы. Также в тексте будут возникать отсылки к функциям и структурам, не имеющим прямого отношения к тематике статьи. Для них будет дано краткое описание, а более полную информацию о реализации можно найти на вики проекта.
Виртуальная память — это концепция, которая позволяет уйти от использования физических адресов, используя вместо них виртуальные, и это даёт ряд преимуществ:
При этом вся виртуальная память делится на участки памяти постоянного размера, называемые страницами.
MMU — это компонент аппаратного обеспечения компьютера, через который “проходят” все запросы к памяти, совершаемые процессором. Задача этого устройства — трансляция адресов, управление кэшированием памяти и её защита.
Обращение к памяти хорошо описанно в этой хабростатье [3]. Происходит оно следующим образом:
Процессор подаёт на вход MMU виртуальный адрес
Если MMU выключено или если виртуальный адрес попал в нетранслируемую область, то физический адрес просто приравнивается к виртуальному
Если MMU включено и виртуальный адрес попал в транслируемую область, производится трансляция адреса, то есть замена номера виртуальной страницы на номер соответствующей ей физической страницы (смещение внутри страницы одинаковое):
Если запись с нужным номером виртуальной страницы есть в TLB [Translation Lookaside Buffer], то номер физической страницы берётся из нее же
Если нужной записи в TLB нет, то приходится искать ее в таблицах страниц, которые операционная система размещает в нетранслируемой области ОЗУ (чтобы не было промаха TLB при обработке предыдущего промаха). Поиск может быть реализован как аппаратно, так и программно — через обработчик исключения, называемого страничной ошибкой (page fault). Найденная запись добавляется в TLB, после чего команда, вызвавшая промах TLB, выполняется снова.
Таким образом, при обращении программы к тому или иному участку памяти трансляция адресов производится аппаратно. Программная часть работы с MMU — формирование таблиц страниц и работа с ними, распределение участков памяти, установка тех или иных флагов для страниц, а также обработка page fault, ошибки, которая происходит при отсутствии страницы в отображении.
В тексте статьи в основном будет рассматриваться трёхуровневая модель памяти, но это не является принципиальным ограничением: для получения модели с бóльшим количеством уровней можно действовать аналогичным образом, а особенности работы с меньшим количеством уровней (как, например, в архитектуре x86 — там всего два уровня) будут рассмотрены отдельно.
Для приложений работа с виртуальной памятью незаметнапрозрачна. Это прозрачность обеспечивается наличием в ядре ОС соответствующей подсистемы, осуществляющей следующие действия:. Данная подсистема содержит в себе несколько необходимых вещей, которые будут описаны ниже:
Данные механизмы будут рассмотрены подробно ниже, после введения нескольких базовых определений.
Page Global Directory (далее — PGD) — таблица (здесь и далее — то же самое, что директория) самого высокого уровня, каждая запись в ней — ссылка на Page Middle Directory (PMD), записи которой, в свою очередь, ссылаются на таблицу Page Table Entry (PTE). Записи в PTE ссылаются на реальные физические адреса, а также хранят флаги состояния страницы.
То есть, при трёхуровневой иерархии памяти виртуальный адрес будет выглядеть так:
Значения полей PGD, PMD и PTE — это индексы в соответствующих таблицах (то есть сдвиги от начала этих таблиц), а offset — это смещение адреса от начала страницы.
В зависимости от архитектуры и режима страничной адресации, количество битов, выделяемых для каждого из полей, может отличаться. Кроме того, сама страничная иерархия может иметь число уровней, отличное от трёх: например, на x86 нет PMD.
Для обеспечения переносимости мы задали границы этих полей с помощью констант: MMU_PGD_SHIFT, MMU_PMD_SHIFT, MMU_PTE_SHIFT, которые в приведённой выше схеме равны 24, 18 и 12 соответственно их определение дано в заголовочном файле src/include/hal/mmu.h [4]. В дальнейшем будет рассматриваться именно этот пример.
На основании сдвигов PGD, PMD и PTE вычисляются соответствующие маски адресов.
#define MMU_PTE_ENTRIES (1UL << (MMU_PMD_SHIFT - MMU_PTE_SHIFT))
#define MMU_PTE_MASK ((MMU_PTE_ENTRIES - 1) << MMU_PTE_SHIFT)
#define MMU_PTE_SIZE (1UL << MMU_PTE_SHIFT)
#define MMU_PAGE_SIZE (1UL << MMU_PTE_SHIFT)
#define MMU_PAGE_MASK (MMU_PAGE_SIZE - 1)
Эти макросы даны в том же заголовочном файле.
Для работы с виртуальной таблицами виртуальной памяти в некоторой области памяти хранятся указатели на все PGD. При этом каждая задача хранит в себе контекст struct mmu_context, который, по сути, является индексом в этой таблице. Таким образом, к каждой задаче относится одна таблица PGD, которую можно определить с помощью mmu_get_root(ctx).
В реальных (то есть не в учебных) системах используются страницы от 512 байт до 64 килобайт. Чаще всего размер страницы определяется архитектурой и является фиксированным для всей системы, например — 4 KiB.
С одной стороны, при меньшем размере страницы память меньше фрагментируется. Ведь наименьшая единица виртуальной памяти, которая может быть выделена процессу — это одна страница, а программам очень редко требуется целое число страниц. А значит, в последней странице, которую запросил процесс, скорее всего останется неиспользуемая память, которая, тем не менее, будет выделена, а значит — использована неэффективно.
С другой стороны, чем меньше размер страницы, тем больше размер страничных таблиц. Более того, при отгрузке на HDD и при чтении страниц с HDD быстрее получится записать несколько больших страниц, чем много маленьких такого же суммарного размера.
Отдельного внимания заслуживают так называемые большие страницы: huge pages и large pages[вики] [5].
Платформа | Размер обычной страницы | Размер страницы максимально возможного размера |
x86 | 4KB | 4MB |
x86_64 | 4KB | 1GB |
IA-64 | 4KB | 256MB |
PPC | 4KB | 16GB |
SPARC | 8KB | 2GB |
ARMv7 | 4KB | 16MB |
Действительно, при использовании таких страниц накладные расходы памяти повышаются. Тем не менее, прирост производительности программ в некоторых случаях может доходить до 10%[ссылка [6]], что объясняется меньшим размером страничных директорий и более эффективной работой TLB.
В дальнейшем речь пойдёт о страницах обычного размера.
В реализации проекта Embox тип mmu_pte_t — это указатель.
Каждая запись PTE должна ссылаться на некоторую физическую страницу, а каждая физическая страница должна быть адресована какой-то записью PTE. Таким образом, в mmu_pte_t незанятыми остаются MMU_PTE_SHIFT бит, которые можно использовать для сохранения состояния страницы. Конкретный адрес бита, отвечающего за тот или иной флаг, как и набор флагов в целом, зависит от архитектуры.
Вот некоторые из флагов:
Снимать и устанавливать эти флаги можно с помощью следующих функций:
mmu_pte_set_writable(), mmu_pte_set_usermode(), mmu_pte_set_cacheable(), mmu_pte_set_executable()
Например: mmu_pte_set_writable(pte_pointer, 0)
Можно установить сразу несколько флагов:
void vmem_set_pte_flags(mmu_pte_t *pte, vmem_page_flags_t flags)
Здесь vmem_page_flags_t — 32-битное значение, и соответствующие флаги берутся из первых MMU_PTE_SHIFT бит.
Как уже писалось выше, при обращении к памяти трансляция адресов производится аппаратно, однако, явный доступ к физическим адресам может быть полезен в ряде случаев. Принцип поиска нужного участка памяти, конечно, такой же, как и в MMU.
Для того, чтобы получить из виртуального адреса физический, необходимо пройти по цепочке таблиц PGD, PMD и PTE. Функция vmem_translate() и производит эти шаги.
Сначала проверяется, есть ли в PGD указатель на директорию PMD. Если это так, то вычисляется адрес PMD, а затем аналогичным образом находится PTE. После выделения физического адреса страницы из PTE необходимо добавить смещение, и после этого будет получен искомый физический адрес.
mmu_paddr_t vmem_translate(mmu_ctx_t ctx, mmu_vaddr_t virt_addr) {
size_t pgd_idx, pmd_idx, pte_idx;
mmu_pgd_t *pgd;
mmu_pmd_t *pmd;
mmu_pte_t *pte;
pgd = mmu_get_root(ctx);
vmem_get_idx_from_vaddr(virt_addr, &pgd_idx, &pmd_idx, &pte_idx);
if (!mmu_pgd_present(pgd + pgd_idx)) {
return 0;
}
pmd = mmu_pgd_value(pgd + pgd_idx);
if (!mmu_pmd_present(pmd + pmd_idx)) {
return 0;
}
pte = mmu_pmd_value(pmd + pmd_idx);
if (!mmu_pte_present(pte + pte_idx)) {
return 0;
}
return mmu_pte_value(pte + pte_idx) + (virt_addr & MMU_PAGE_MASK);
}
Пояснения к коду функции.
mmu_paddr_t — это физический адрес страницы, назначение mmu_ctx_t уже обсуждалось выше в разделе “Виртуальный адрес”.
С помощью функции vmem_get_idx_from_vaddr() находятся сдвиги в таблицах PGD, PMD и PTE.
void vmem_get_idx_from_vaddr(mmu_vaddr_t virt_addr, size_t *pgd_idx, size_t *pmd_idx, size_t *pte_idx) {
*pgd_idx = ((uint32_t) virt_addr & MMU_PGD_MASK) >> MMU_PGD_SHIFT;
*pmd_idx = ((uint32_t) virt_addr & MMU_PMD_MASK) >> MMU_PMD_SHIFT;
*pte_idx = ((uint32_t) virt_addr & MMU_PTE_MASK) >> MMU_PTE_SHIFT;
}
Для работы с записей в таблице страниц, а так же с самими таблицами, есть ряд функций:
Эти функции возвращают 1, если у соответствующей структуры установлен бит MMU_PAGE_PRESENT
int mmu_pgd_present(mmu_pgd_t *pgd);
int mmu_pmd_present(mmu_pmd_t *pmd);
int mmu_pte_present(mmu_pte_t *pte);
Page fault — это исключение, возникающее при обращении к странице, которая не загружена в физическую память — или потому, что она была вытеснена, или потому, что не была выделена.
В операционных системах общего назначения при обработке этого исключения происходит поиск нужной странице на внешнем носителе (жёстком диске, к примеру).
В нашей системе все страницы, к которым процесс имеет доступ, считаются присутствующими в оперативной памяти. Так, например, соответствующие сегменты .text, .data, .bss; куча; и так далее отображаются в таблицы при инициализации процесса. Данные, связанные с потоками (например, стэк), отображаются в таблицы процесса при создании потоков.
Выталкивание страниц во внешнюю память и их чтение в случае page fault не реализовано. С одной стороны, это лишает возможности использовать больше физической памяти, чем имеется на самом деле, а с другой — не является актуальной проблемой для встраиваемых систем. Нет никаких ограничений, делающих невозможной реализацию данного механизма, и при желании читатель может попробовать себя в этом деле :)
Для виртуальных страниц и для физических страниц, которые могут быть использованы при работе с виртуальной памятью, статически резервируется некоторое место в оперативной памяти. Тогда при выделении новых страниц и директорий они будут браться именно из этого места.
Исключением является набор указателей на PGD для каждого процесса (MMU-контексты процессов): этот массив хранится отдельно и используется при создании и разрушении процесса.
Выделение страниц
Итак, выделить физическую страницу можно с помощью vmem_alloc_page
void *vmem_alloc_page() {
return page_alloc(virt_page_allocator, 1);
}
Функция page_alloc() ищет участок памяти из N незанятых страниц и возвращает физический адрес начала этого участка, помечая его как занятый. В приведённом коде virt_page_allocator ссылается на участок памяти, резервированной для выделения физических страниц, а 1 — количество необходимых страниц.
Выделение таблиц
Тип таблицы (PGD, PMD, PTE) не имеет значения при аллокации. Более того, выделение таблиц производится также с помощью функции page_alloc(), только с другим аллокатором (virt_table_allocator).
После добавления страниц в соответствующие таблицы нужно уметь сопоставлять участки памяти с процессами, к которым они относятся. У нас в системе процесс представлен структурой task, содержащей всю необходимую информацию для работы ОС с процессом. Все физически доступные участки адресного пространства процесса записываются в специальный репозиторий: task_mmap. Он представляет из себя список дескрипторов этих участков (регионов), которые могут быть отображены на виртуальную память, если включена соответствующая поддержка.
struct emmap {
void *brk;
mmu_ctx_t ctx;
struct dlist_head marea_list;
};
brk — это самый большой из всех физических адресов репозитория, данное значение необходимо для ряда системных вызовов, которые не будут рассматриваться в данной статье.
ctx — это контекст задачи, использование которого обсуждалось в разделе “Виртуальный адрес”.
struct dlist_head — это указатель на начало двусвязного списка, организация которого аналогична организации Linux Linked List [7].
За каждый выделенный участок памяти отвечает структура marea
struct marea {
uintptr_t start;
uintptr_t end;
uint32_t flags;
uint32_t is_allocated;
struct dlist_head mmap_link;
};
Поля данной структуры имеют говорящие имена: адреса начала и конца данного участка памяти, флаги региона памяти. Поле mmap_link нужно для поддержания двусвязного списка, о котором говорилось выше.
void mmap_add_marea(struct emmap *mmap, struct marea *marea) {
struct marea *ma_err;
if ((ma_err = mmap_find_marea(mmap, marea->start))
|| (ma_err = mmap_find_marea(mmap, marea->end))) {
/* Обработка ошибки */
}
dlist_add_prev(&marea->mmap_link, &mmap->marea_list);
}
Ранее уже рассказывалось о том, как происходит выделение физических страниц, какие данные о виртуальной памяти относятся к задаче, и теперь всё готово для того, чтобы говорить о непосредственном отображении виртуальных участков памяти на физические.
Отображение виртуальных участков памяти на физическую память подразумевает внесение соответствующих изменений в иерархию страничных директорий.
Подразумевается, что некоторый участок физической памяти уже выделен. Для того, чтобы выделить соответствующие виртуальные страницы и привязать их к физическим, используется функция vmem_map_region()
int vmem_map_region(mmu_ctx_t ctx, mmu_paddr_t phy_addr, mmu_vaddr_t virt_addr, size_t reg_size, vmem_page_flags_t flags) {
int res = do_map_region(ctx, phy_addr, virt_addr, reg_size, flags);
if (res) {
vmem_unmap_region(ctx, virt_addr, reg_size, 0);
}
return res;
}
В качестве параметров передаётся контекст задачи, адрес начала физического участка памяти, а также адрес начала виртуального участка. Переменная flags содержит флаги, которые будут установлены у соответствующих записей в PTE.
Основную работу на себя берёт do_map_region(). Она возвращает 0 при удачном выполнении и код ошибки — в ином случае. Если во время маппирования произошла ошибка, то часть страниц, которые успели выделиться, нужно откатить сделанные изменения с помощью функции vmem_unmap_region(), которая будет рассмотрена позднее.
Рассмотрим функцию do_map_region() подробнее.
static int do_map_region(mmu_ctx_t ctx, mmu_paddr_t phy_addr, mmu_vaddr_t virt_addr, size_t reg_size, vmem_page_flags_t flags) {
mmu_pgd_t *pgd;
mmu_pmd_t *pmd;
mmu_pte_t *pte;
mmu_paddr_t p_end = phy_addr + reg_size;
size_t pgd_idx, pmd_idx, pte_idx;
/* Considering that all boundaries are already aligned */
assert(!(virt_addr & MMU_PAGE_MASK));
assert(!(phy_addr & MMU_PAGE_MASK));
assert(!(reg_size & MMU_PAGE_MASK));
pgd = mmu_get_root(ctx);
vmem_get_idx_from_vaddr(virt_addr, &pgd_idx, &pmd_idx, &pte_idx);
for ( ; pgd_idx < MMU_PGD_ENTRIES; pgd_idx++) {
GET_PMD(pmd, pgd + pgd_idx);
for ( ; pmd_idx < MMU_PMD_ENTRIES; pmd_idx++) {
GET_PTE(pte, pmd + pmd_idx);
for ( ; pte_idx < MMU_PTE_ENTRIES; pte_idx++) {
/* Considering that address has not mapped yet */
assert(!mmu_pte_present(pte + pte_idx));
mmu_pte_set(pte + pte_idx, phy_addr);
vmem_set_pte_flags(pte + pte_idx, flags);
phy_addr += MMU_PAGE_SIZE;
if (phy_addr >= p_end) {
return ENOERR;
}
}
pte_idx = 0;
}
pmd_idx = 0;
}
return -EINVAL;
}
#define GET_PMD(pmd, pgd)
if (!mmu_pgd_present(pgd)) {
pmd = vmem_alloc_pmd_table();
if (pmd == NULL) {
return -ENOMEM;
}
mmu_pgd_set(pgd, pmd);
} else {
pmd = mmu_pgd_value(pgd);
}
#define GET_PTE(pte, pmd)
if (!mmu_pmd_present(pmd)) {
pte = vmem_alloc_pte_table();
if (pte == NULL) {
return -ENOMEM;
}
mmu_pmd_set(pmd, pte);
} else {
pte = mmu_pmd_value(pmd);
}
Макросы GET_PTE и GET_PMD нужны для лучшей читаемости кода. Они делают следующее: если в таблице памяти нужный нам указатель не ссылается на существующую запись, нужно выделить её, если нет — то просто перейти по указателю к следующей записи.
В самом начале необходимо проверить, выровнены ли под размер страницы размер региона, физический и виртуальный адреса. После этого определяется PGD, соответствующая указанному контексту, и извлекаются сдвиги из виртуального адреса (более подробно это уже обсуждалось выше).
Затем последовательно перебираются виртуальные адреса, и в соответствующих записях PTE к ним привязывается нужный физический адрес. Если в таблицах отсутствуют какие-то записи, то они будут автоматически сгенерированы при вызове вышеупомянутых макросов GET_PTE и GET_PMD.
После того, как участок виртуальной памяти был отображён на физическую, рано или поздно её придётся освободить: либо в случае ошибки, либо в случае завершения работы процесса.
Изменения, которые при этом необходимо внести в структуру страничной иерархии памяти, производятся с помощью функции vmem_unmap_region().
void vmem_unmap_region(mmu_ctx_t ctx, mmu_vaddr_t virt_addr, size_t reg_size, int free_pages) {
mmu_pgd_t *pgd;
mmu_pmd_t *pmd;
mmu_pte_t *pte;
mmu_paddr_t v_end = virt_addr + reg_size;
size_t pgd_idx, pmd_idx, pte_idx;
void *addr;
/* Considering that all boundaries are already aligned */
assert(!(virt_addr & MMU_PAGE_MASK));
assert(!(reg_size & MMU_PAGE_MASK));
pgd = mmu_get_root(ctx);
vmem_get_idx_from_vaddr(virt_addr, &pgd_idx, &pmd_idx, &pte_idx);
for ( ; pgd_idx < MMU_PGD_ENTRIES; pgd_idx++) {
if (!mmu_pgd_present(pgd + pgd_idx)) {
virt_addr = binalign_bound(virt_addr, MMU_PGD_SIZE);
pte_idx = pmd_idx = 0;
continue;
}
pmd = mmu_pgd_value(pgd + pgd_idx);
for ( ; pmd_idx < MMU_PMD_ENTRIES; pmd_idx++) {
if (!mmu_pmd_present(pmd + pmd_idx)) {
virt_addr = binalign_bound(virt_addr, MMU_PMD_SIZE);
pte_idx = 0;
continue;
}
pte = mmu_pmd_value(pmd + pmd_idx);
for ( ; pte_idx < MMU_PTE_ENTRIES; pte_idx++) {
if (virt_addr >= v_end) {
// Try to free pte, pmd, pgd
if (try_free_pte(pte, pmd + pmd_idx) && try_free_pmd(pmd, pgd + pgd_idx)) {
try_free_pgd(pgd, ctx);
}
return;
}
if (mmu_pte_present(pte + pte_idx)) {
if (free_pages && mmu_pte_present(pte + pte_idx)) {
addr = (void *) mmu_pte_value(pte + pte_idx);
vmem_free_page(addr);
}
mmu_pte_unset(pte + pte_idx);
}
virt_addr += VMEM_PAGE_SIZE;
}
try_free_pte(pte, pmd + pmd_idx);
pte_idx = 0;
}
try_free_pmd(pmd, pgd + pgd_idx);
pmd_idx = 0;
}
try_free_pgd(pgd, ctx);
}
Все параметры функции, кроме последнего, должны быть уже знакомы. free_pages отвечает за то, должны ли быть удалены страничные записи из таблиц.
try_free_pte, try_free_pmd, try_free_pgd — это вспомогательные функции. При удалении очередной страницы может выясниться, что директория, её содержащая, могла стать пустой, а значит, её нужно удалить из памяти.
static inline int try_free_pte(mmu_pte_t *pte, mmu_pmd_t *pmd) {
for (int pte_idx = 0 ; pte_idx < MMU_PTE_ENTRIES; pte_idx++) {
if (mmu_pte_present(pte + pte_idx)) {
return 0;
}
}
#if MMU_PTE_SHIFT != MMU_PMD_SHIFT
mmu_pmd_unset(pmd);
vmem_free_pte_table(pte);
#endif
return 1;
}
static inline int try_free_pmd(mmu_pmd_t *pmd, mmu_pgd_t *pgd) {
for (int pmd_idx = 0 ; pmd_idx < MMU_PMD_ENTRIES; pmd_idx++) {
if (mmu_pmd_present(pmd + pmd_idx)) {
return 0;
}
}
#if MMU_PMD_SHIFT != MMU_PGD_SHIFT
mmu_pgd_unset(pgd);
vmem_free_pmd_table(pmd);
#endif
return 1;
}
static inline int try_free_pgd(mmu_pgd_t *pgd, mmu_ctx_t ctx) {
for (int pgd_idx = 0 ; pgd_idx < MMU_PGD_ENTRIES; pgd_idx++) {
if (mmu_pgd_present(pgd + pgd_idx)) {
return 0;
}
}
// Something missing
vmem_free_pgd_table(pgd);
return 1;
}
Макросы вида
#if MMU_PTE_SHIFT != MMU_PMD_SHIFT
...
#endif
нужны как раз для случая двухуровневой иерархии памяти.
Конечно, данной статьи не достаточно, чтобы с нуля организовать работу с MMU, но, я надеюсь, она хоть немного поможет погрузиться в OSDev тем, кому он кажется слишком сложным.
P.S. Всех с началом недели Мат-Меха [8] :)
Автор: 0xdde
Источник [9]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/programmirovanie/89358
Ссылки в тексте:
[1] предыдущей статье: http://habrahabr.ru/company/embox/blog/232605/
[2] Embox: https://github.com/embox/embox
[3] этой хабростатье: http://habrahabr.ru/post/211150/
[4] src/include/hal/mmu.h: https://github.com/embox/embox/blob/master/src/include/hal/mmu.h
[5] [вики]: http://en.wikipedia.org/wiki/Page_%28computer_memory%29#Huge_pages
[6] ссылка: http://www.vmware.com/files/pdf/large_pg_performance.pdf
[7] Linux Linked List: http://isis.poly.edu/kulesh/stuff/src/klist/
[8] недели Мат-Меха: https://vk.com/mmweek
[9] Источник: http://habrahabr.ru/post/256191/
Нажмите здесь для печати.