Як Linux працює з пам'яттю

Stanislav Ievlev

Трапилося мені одного разу поцікавитися, як же ядро працює з
найдорожчим, що у нього є, з оперативною пам'яттю. Перші спроби
розібратися з нальоту, що і як ні до чого не привели. Не все так просто як
хотілося б. Звідусіль стирчать кінці, начебто всі ясно, але як зв'язати їх воєдино …

Виникла думка звернутися до минулого, щоб, принаймні,
розібратися, як все це розвивалося (з версії 0.1). Витівка вдалася … це
допомогло зрозуміти і сучасне ядро. Надалі мова піде про ядра серії 2.2,
про зміни в 2.4 буде повідомлено окремо.

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

Отже, в основі всього лежать сторінки пам'яті. У ядрі вони описуються структурою
mem_map_t.

typedef struct page {
/* these must be first (free area handling) */
struct page *next;
struct page *prev;
struct inode *inode;
unsigned long offset;
struct page *next_hash;
atomic_t count;
unsigned long flags; / * atomic flags, some possibly updated asynchronously * /
struct wait_queue *wait;
struct page **pprev_hash;
struct buffer_head * buffers;
} mem_map_t;

Вже тут спостерігається певна навороченность. Безліч
жодних посилань. Ви не повірите, але всі вони використовуються. Одна сторінка може
знаходитися в різних списках, наприклад і в списку сторінок в сторінковому кеші і в
списку сторінок відносяться до відображеного в пам'ять файлу (inode). У структурі,
описує останній, можна знайти і зворотне посилання, що дуже зручно.

Всі сторінки адресуються глобальним покажчиком mem_map

mem_map_t * mem_map

Адресація відбувається дуже хитро. Якщо раніше (у ранніх версіях
ядра) в структурі page було окреме поле вказує на фізичну адресу
(Map_nr), то тепер він обчислюється. Алгоритм обчислення можна виявити в
наступної функції ядра.

static inline unsigned long page_address(struct page * page)
{
return PAGE_OFFSET + PAGE_SIZE * (page – mem_map);
}
Вільні сторінки зберігаються в особливій структурі free_area
static struct free_area_struct free_area [NR_MEM_TYPES] [NR_MEM_LISTS];
, Де перше поле відповідає за тип області: Ядра, Користувача, DMA і т.д.
І обробляються по дуже цікавому алгоритму.

Сторінки діляться на вільні безперервні області розміру 2 у ступені x
помноженої на розмір сторінки ((2 ^ x) * PAGE_SIZE). Області одного розміру лежать в
однієї області масиву.

….
|——
| Вільні Сторінки розміру PAGE_SIZE * 4 —> список вільних областей
|——
| Вільні Сторінки розміру PAGE_SIZE * 2 —> список вільних областей
|——
| Вільні Сторінки розміру PAGE_SIZE —> список вільних областей
|——

Виділяє сторінку функція get_free_pages (order). Вона виділяє
сторінки складові область розміру PAGE_SIZE * (2 ^ order). Робиться це так.
Шукається область відповідного розміру або більше. Якщо є тільки область
більшого розміру, то вона ділиться на декілька маленьких і береться потрібний шматок.
Якщо вільних сторінок недостатньо, то деякі будуть скинуті в область
підкачки і процес виділення почнеться знову.

Повертає сторінку функція free_pages (struct page, order).
Вивільняє сторінки, що починаються з page розміру PAGE_SIZE * (2 ^ order). Область
повертається в масив вільних областей у відповідну позицію і після цього
відбувається спроба об'єднати кілька областей для створення однієї більшого
розміру.

Відсутність сторінки в пам'яті обробляються ядром особливо. Сторінка може або
взагалі відсутнім або знаходитися в області підкачки.

Ось власне і вся базова робота з реальними сторінками.
Саме час згадати, що процес працює все-таки з віртуальними адресами, а
не з фізичними. Перетворення відбувається за допомогою обчислень, використовуючи
таблиці дескрипторів, і каталоги таблиць. Linux підтримує 3 рівня таблиць:
каталог таблиць першого рівня (PGD – Page Table Directory), каталог таблиць
другого рівня (PMD – Medium Page Table Diractory), і, нарешті, таблиця
дескрипторів (PTE – Page Table Entry). Реально конкретним процесором можуть
підтримуватися не всі рівні, але запас дозволяє підтримувати більше можливих
архітектур (Intel має 2 рівні таблиць, а Alpha – цілих 3). Перетворення
ВА у фізичний відбувається відповідно в 3 етапи. Береться
покажчик PGD, наявний у структурі описує кожен процес, перетворюється на
покажчик запису PMD, а останній перетвориться в покажчик у таблиці
дескрипторів PTE. І, нарешті, до реального адресою, яка вказує на початок
сторінки додають зсув від її початку. Хороший приклад подібної процедури
можна подивитися у функції ядра partial_clear:

page_dir = pgd_offset(vma->vm_mm, address);
if (pgd_none(*page_dir))
return;
if (pgd_bad(*page_dir)) {
printk ("bad page table directory entry% p: [% lx] \ n", page_dir, pgd_val (* page_dir));
pgd_clear(page_dir);
return;
}
page_middle = pmd_offset(page_dir, address);
if (pmd_none(*page_middle))
return;
if (pmd_bad(*page_middle)) {
printk ("bad page table directory entry% p: [% lx] \ n", page_dir, pgd_val (* page_dir));
pmd_clear(page_middle);
return;
}
page_table = pte_offset(page_middle, address);
Взагалі-то всі дані про використовувану процесом пам'яті поміщаються в
структурі mm_struct
struct mm_struct {
struct vm_area_struct * mmap; / * Список відображених областей * /
struct vm_area_struct * mmap_avl; / * Ті ж області але вже у вигляді дерева
для більш швидкого пошуку * /
struct vm_area_struct * mmap_cache; / * Остання знайдена область * /
pgd_t * pgd; / * Каталог таблиць * /
atomic_t count;
int map_count; / * Кількість областей * /
struct semaphore mmap_sem;
unsigned long context;
unsigned long start_code, end_code, start_data, end_data;
unsigned long start_brk, brk, start_stack;
unsigned long arg_start, arg_end, env_start, env_end;
unsigned long rss, total_vm, locked_vm;
unsigned long def_flags;
unsigned long cpu_vm_mask;
unsigned long swap_cnt; / * кількість сторінок для свопінгу при наступному проході * /
unsigned long swap_address;
/*
* Це архітектурно-залежний покажчик. Стерпна частина Linux
нічого не знає про сегменти. * /
void * segments;
};

Відразу помічаємо, що крім цілком зрозумілих покажчиків на початок
даних (start_code, end_code …) коду і стека є покажчики на дані
відображених файлів (mmap). Це, треба сказати, особливість Linux – тягти в себе
все, що тільки можна. Може бути це і добре, але з іншого боку так
розбазарюватися пам'яттю … (згадаємо ще буфера введення / виводу при файлової
системі, які теж будуть їсти всі нову пам'ять поки вона є). Даний
підхід може негативно позначитися на стабільності системи, адже для запуску
якогось життєво необхідного процесу може знадобитися час на
звільнення зайвих Кешей. Простенька перевірка на втрату вільної пам'яті:
введіть команду "cat / dev / mem> / image" і подивіться скільки вільної пам'яті
після цього залишилося. Якщо вам це не подобається, то звернете погляд на функцію
invalidate_inode_pages (* struct_inode), що звільняє сторінковий кеш для даного
файлу.

При будь-якому відкритті файлу, він відразу ж відображається на згадку
(Точніше його частина, дочитала до розміру сторінки. Наприклад, для Intel при
читанні 10 байт будуть прочитані 4096) і додається в сторінковий кеш. Реальний же
запит на відображення файлу тільки повертає адресу на вже кешовані
сторінки.

На рівні процесу робота може вестися як із сторінками безпосередньо, так і
через абстрактну структуру vm_area_struct

struct vm_area_struct {
struct mm_struct * vm_mm; / * параметри області віртуальної пам'яті * /
unsigned long vm_start;
unsigned long vm_end;
/ * Пов'язується список областей завдання відсортований за адресами * /
struct vm_area_struct *vm_next;
pgprot_t vm_page_prot;
unsigned short vm_flags;
/ * AVL-дерево областей, для прискореного пошуку, сортування за адресами * /
short vm_avl_height;
struct vm_area_struct * vm_avl_left;
struct vm_area_struct * vm_avl_right;
/ * Для областей використовуваних при відображенні файлів або при роботі
з пам'яті, що розділяється, інакше ця частина структури не використовується * /
struct vm_area_struct *vm_next_share;
struct vm_area_struct **vm_pprev_share;
struct vm_operations_struct * vm_ops; / * операції над областю * /
unsigned long vm_offset;
struct file * vm_file;
unsigned long vm_pte; / * колективна пам'ять * /
};
struct vm_operations_struct {
void (*open)(struct vm_area_struct * area);
void (*close)(struct vm_area_struct * area);
void (* unmap) (struct vm_area_struct * area, unsigned long, size_t);
void (* protect) (struct vm_area_struct * area, unsigned long, size_t, unsigned int newprot);
int (* sync) (struct vm_area_struct * area, unsigned long, size_t, unsigned int flags);
void (* advise) (struct vm_area_struct * area, unsigned long, size_t, unsigned int advise);
unsigned long (* nopage) (struct vm_area_struct * area, unsigned long address, int write_access);
unsigned long (* wppage) (struct vm_area_struct * area, unsigned long address,
unsigned long page);
int (*swapout)(struct vm_area_struct *, struct page *);
pte_t (* swapin) (struct vm_area_struct *, unsigned long, unsigned long);
};

Ідея даної структури виникла з ідеї віртуальної файлової
системи, тому всі операції над віртуальними областями абстрактні і можуть бути
специфічними для різних типів пам'яті, наприклад при відображенні файлів операції
читання одні, а при відображенні пам'яті (через файл / dev / mem) зовсім інші.
Спочатку vm_area_struct з'явилася для забезпечення потреб відображення, але
поступово поширюється і для інших цілей.

Що робити, коли потрібно отримати нову область пам'яті. Є цілих 3
способу.


  1. Вже знайомий get_free_page ()
  2. kmalloc – Простенька (по можливостях, але аж ніяк не коду) процедура з
    великими обмеженнями по виділенню нових областей і за їх розміром.
  3. vmalloc – Потужна процедура, яка працює з віртуальною пам'яттю, може
    виділяти великі обсяги пам'яті.

З кожною з двох процедур у ядрі пов'язані ще за списком вільних / зайнятих
областей, що ще більше ускладнює розуміння роботи з пам'яттю. (Vmlist для
vmalloc, kmem_cash для kmalloc)

Що ж в 2.4?

Додана підтримка нової архітектури пам'яті NUMA. На противагу
класичної UMA пам'ять ділиться на зони з різним часом доступу до кожної з
них. Це дуже корисно і для кластерних рішень. У зв'язку з цим з'явилися нові
обгортки на функції, нові структури і знайти суть стало ще складніше. З'явилася
також підтримка пам'яті до 64Гб.

Раніше для всіх файлових систем був один generic_file_read і
generic_file_mmap у зв'язку з тотальним засмоктуванням усього підряд у пам'ять при
читанні (розходження робилися вже тільки на рівні inode-> readpage). Тепер
з'явився і generic_file_write. Загалом, ще пара таких generic і прощай
віртуальна файлова система.

Але подивимося – побачимо. Адже Linux розвивається дуже швидко і не завжди
передбачувано.

Ось і зроблена спроба оглянути один з найскладніших моментів
роботи операційної системи – робота з оперативною пам'яттю. Можливо огляд не
самий вичерпний, але, принаймні, напрямок правильний. А далі …
далі рекомендується дивитися вихідні тексти. Там ви знайдете вже точно всі
відповіді на виниклі питання.

Удачи.

Схожі статті:


Сподобалася стаття? Ви можете залишити відгук або підписатися на RSS , щоб автоматично отримувати інформацію про нові статтях.

Коментарів поки що немає.

Ваш отзыв

Поділ на параграфи відбувається автоматично, адреса електронної пошти ніколи не буде опублікований, допустимий HTML: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*

*