Дослідження моделі пам'яті Linux

Розуміння моделей пам'яті, використовуваних в Linux, – це перший крок до освоєння на більш високому рівні структури операційної системи Linux та її реалізації. У цій статті моделі пам'яті Linux і керування пам'яттю розглядаються на ознайомлювальному рівні.


Операційна система Linux використовує монолітний підхід, при якому визначається набір примітивів або системних викликів для реалізації служб операційної системи, таких як управління процесами, паралельна робота і керування пам'яттю, в кількох модулях, що працюють в режимі супервізора. І хоча Linux з метою сумісності підтримує модель модуля управління сегментами (segment control unit) як символічне уявлення, вона використовує цю модель на мінімальному рівні.


Основними завданнями управління пам'яттю є:



Дана стаття покликана допомогти вам в освоєнні внутрішнього устрою Linux з точки зору управління пам'яттю операційної системи. Розглядаються наступні теми:



У даній статті не розглядаються детально питання управління пам'яттю в ядрі Linux, але інформація по загальній моделі пам'яті і по роботі з нею повинна дати вам основу для подальшого вивчення. Увага в статті приділяється архітектурі x86, але ви можете використовувати ці матеріали і з іншими апаратними реалізаціями.


Архітектура пам'яті x86


У x86-архітектурі пам'ять поділяється на три типи адрес:



CPU використовує два модулі для перетворення логічного адреси в його фізичний еквівалент. Перший називається модулем сегментації (segmented unit), а другий – модулем поділу на сторінки (paging unit).


Малюнок 1. Два модулі перетворять адресний простір

Давайте досліджуємо модель модуля управління сегментами.


 


Загальна модель модуля управління сегментами


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



Сегментований адресу теж складається з двох компонентів – селектора сегмента і зміщення в сегменті. Селектор сегмента вказує на використовуваний сегмент (тобто, значення базової адреси і довжини), а компонент зсуву вказує зміщення від базової адреси для реального доступу до пам'яті. Фізична адреса реального місця розташування пам'яті є сумою значень зміщення і базового адреси. Якщо зсув перевищує довжину сегмента, система генерує порушення захисту.


Підіб'ємо підсумки:






 Модуль сегментації представляється як -> модель Сегмент: Зміщення
також може бути представлений як -> Ідентифікатор сегмента: Зміщення

Кожен сегмент – це 16-бітове поле, зване ідентифікатором сегмента або селектором сегмента. Процесори сімейства x86 містять кілька програмованих регістрів, званих сегментними, які зберігають ці селектори сегментів. Такими регістрами є cs (Сегмент коду), ds (Сегмент даних) і ss (Сегмент стека). Кожен ідентифікатор сегмента вказує сегмент, який представлений 64-бітним (8-байтним) дескриптором сегмента. Ці дескриптори сегментів зберігаються в GDT (глобальна таблиця дескрипторів) і може бути також збережена в LDT (локальна таблиця дескрипторів).


Малюнок 2. Взаємодія дескрипторів сегментів і регістрів сегментів

Кожного разу, коли селектор сегмента завантажується в сегментний регістр, відповідний дескриптор сегменту завантажується з пам'яті у відповідний непрограмувальний регістр CPU. Кожен дескриптор сегменту має довжину 8 байт і представляє один сегмент в пам'яті. Вони зберігаються в таблицях LDT чи GDT. Запис елемента дескриптора сегмента містить і покажчик на перший байт у зв'язаному сегменті, представленому полем Base, і 20-бітове значення (поле Limit), яке представляє розмір сегменту в пам'яті.


Кілька інших полів містять спеціальні атрибути, такі як рівень привілеїв і тип сегмента (cs або ds). Тип сегмента представляється у четирехбітном поле Type.


Оскільки використовуються непрограмувальний регістр, до GDT чи LDT не проводиться звернень до тих пір, поки не буде виконана трансляція логічного адреси в фізичний. Це прискорює роботу з пам'яттю.


Селектор сегмента містить:



Оскільки дескриптор сегменту має довжину 8 байт, його відносний адреса в GDT чи LDT обчислюється шляхом множення самих значущих 13 бітів селектора сегмента на 8. Наприклад, якщо GDT зберігається за адресою 0x00020000, і поле Index, вказане селектором сегмента, дорівнює 2, тоді адресу відповідного дескриптора сегмента дорівнює (2 * 8) + 0x00020000. Максимальна кількість дескрипторів сегмента, яке може бути збережене в GDT, одно (2 ^ 13 – 1) чи 8191.


На малюнку 3 показано графічне представлення обчислення лінійного адреси з логічного.


Малюнок 3. Отримання лінійного адреси з логічного

А тепер подивимося, які відмінності в Linux?


 


Модуль управління сегментами в Linux


У Linux ця модель має невеликі відмінності. Я вже говорив про те, що Linux використовує модель сегментування обмежено (в основному для сумісності).


У Linux усі сегментні регістри вказують на один і той же діапазон адрес сегментів – іншими словами, кожен використовує один і той же набір лінійних адрес. Це дозволяє Linux використовувати обмежену число дескрипторів сегментів, тобто, всі дескриптори можуть зберігатися в GDT. Двома перевагами цієї моделі є:



На малюнку 4 показані ці відмінності.


Малюнок 4. У Linux сегментні регістри вказують на один і той же набір адрес

Дескриптори сегментів


Linux використовує наступні дескриптори сегментів:



Розглянемо детально кожен з них.


Дескриптор сегмента коду ядра в GDT має наступні значення:



Лінійна адреса для цього сегмента дорівнює 4 GB. S = 1 і type = 0xa позначають сегмент коду. Селектор знаходиться в регістрі cs. Доступ до відповідного селектору сегмента в Linux здійснюється через макрос _KERNEL_CS.


Дескриптор сегмента даних ядра має аналогічні значення за винятком поля Type, значення якого встановлено в 2. Це вказує на те, що сегмент є сегментом даних і селектор зберігається в регістрі ds. Доступ до відповідного селектору сегмента в Linux здійснюється через макрос _KERNEL_DS.


Сегмент коду користувача використовується спільно всіма процесами в режимі користувача. Відповідний дескриптор сегменту, що зберігається в GDT, має такі значення:



Доступ до відповідного селектору сегмента в Linux здійснюється через макрос _USER_CS.


У дескрипторі сегмента даних користувача є тільки одна зміна в порівнянні з попереднім дескриптором – поле Type встановлено в 2 і визначає сегмент даних, які можна прочитати і записати. Доступ до відповідного селектору сегмента в Linux здійснюється через макрос _USER_DS.


Крім цих дескрипторів сегментів GDT містить два додаткових дескриптора сегментів для кожного створеного процесу – для сегментів TSS і LDT.


Кожен дескриптор сегмента TSS вказує на окремий процес. TSS зберігає інформацію про апаратне контексті для кожного CPU, який бере участь у перемиканні контексту. Наприклад, при перемиканні режимів U->K x86 CPU отримує адресу стека режиму ядра з TSS.


Кожен процес має свій власний TSS-дескриптор, що зберігається в GDT. Значення цього дескриптора такі:



Всі процеси спільно використовують сегмент LDT за замовчуванням . За замовчуванням він містить нульовий дескриптор сегменту. Цей дескриптор сегменту LDT за умовчанням зберігається у GDT. Згенерований ядром Linux LDT займає 24 байти. За умовчанням завжди присутні три запису:






LDT[0] = null
LDT [1] = сегмент коду користувача
LDT [2] = дескриптор сегменту даних / стека користувача

Обчислення TASKS


Знання NR_TASKS (Змінної, що визначає кількість одночасних процесів, які підтримуються в Linux. Її значення за замовчуванням у вихідному коді ядра одно 512, що дозволяє мати 256 одночасних підключень до одного примірнику) необхідно для обчислення максимальної кількості дозволених записів в GDT.


Максимальна кількість дозволених в GDT записів може бути визначено за такою формулою:






 Загальна кількість записів в GDT = 12 + 2 * NR_TASKS.
Можлива кількість записів в GDT = 2 ^ 13 -1 = 8192.

З 8192 дескрипторів сегментів Linux використовує 6 дескрипторів сегментів, ще 4 додаткових для APM-функцій (функції розширеного керування живленням), а 4 записи в GDT залишаються не використаними. Таким чином, кінцеве число можливих записів у GDT одно 8192 – 14, або 8180.


У будь-який момент часу ми не можемо мати більш ніж 8180 записів у GDT, отже:


2 * NR_TASKS = 8180
і NR_TASKS = 8180/2 = 4090


Чому 2 * NR_TASKS? Тому що для кожного створеного процесу завантажується не тільки TSS-дескриптор, використовуваний для обслуговування перемикань контексту, а й LDT-дескриптор.


Це обмеження кількості процесів в x86-архітектурі відносилося до Linux 2.2, але після ядра 2.4 ця проблема була вирішена частково шляхом відмови від перемикання апаратного контексту (яке робило необхідним використання TSS), а також шляхом заміни його перемиканням процесу.


Тепер, давайте розглянемо сторінкову модель.


 


Огляд моделі сторінкової організації


Модуль управління сторінками перетворює лінійні адреси у фізичні (див. малюнок 1). Набір лінійних адрес групується разом, утворюючи сторінки. Ці лінійні адреси безупинні за своєю природою – модуль управління сторінками відображає ці набори безперервної пам'яті у відповідний набір безперервних фізичних адрес, званих сторінковими фреймами. Зверніть увагу на те, що модуль управління сторінками представляє RAM розділеним на сторінкові фрейми фіксованого розміру.


З цієї причини розбиття на сторінки має такі переваги:



  • Права доступу, визначені для сторінки, будуть дійсні для групи лінійних адрес, формують сторінку

  • Довжина сторінки дорівнює довжині сторінкового фрейму

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


Малюнок 5. Таблиця сторінок визначає відповідність сторінок сторінковим фреймах

Зверніть увагу на те, що набір адрес, що містяться в Page1, відповідає певному набору адрес, що містяться в Page Frame1.


Linux використовує модуль поділу на сторінки більш інтенсивно, ніж модуль сегментації. Як ми бачили раніше в розділі "Linux і сегментація", кожен дескриптор сегменту використовує один і той же набір адрес для лінійної адресації, мінімізуючи необхідність використання модуля сегментації для перетворення логічних адрес у лінійні. Використовуючи модуль розбиття на сторінки більш часто, ніж модуль сегментації, Linux значно полегшує управління пам'яттю і забезпечує переносимість між різними апаратними платформами.


Поля, використовувані при розбитті сторінок


Наведемо опис полів, використовуваних для визначення сторінок в x86-архітектурі і допомагають керувати сторінками в Linux. Модуль розбиття на сторінки отримує лінійний адресу, який видає модуль сегментації. Цей лінійний адресу потім розбивається на наступні поля:



  • Directory (Каталог) представлений десятьма MSB (Most Significant Bit – біт двійкового числа, що має найбільше значення; MSB іноді називається "самий лівий біт").

  • Table (Таблиця) представлена десятьма середніми бітами.

  • Offset (Зміщення) представлено дванадцятьма LSB (Least Significant Bit – біт двійкового числа, що представляє значення одиниці, тобто визначає, є число парних, або непарних; LSB іноді називають "найбільшим правим бітом "; він аналогічний найменш значущої цифрі десяткового числа, що позначає одиниці і розташованої на самій правій позиції).

Перетворення лінійних адрес у їх відповідну фізичну місце розташування є процесом, що складається з двох кроків. На першому кроці використовується таблиця перетворення, звана Page Directory (Здійснює перехід від Page Directory до Page Table), а на другому кроці використовується таблиця, звана Page Table (Page Table плюс Offset для запитуваної фрейму сторінки). Цей процес зображено на рисунку 6.


Малюнок 6. Поля адреси при розбитті на сторінки

На початку фізичну адресу Page Directory завантажується в регістр cr3. Поле directory в лінійному адресі визначає запис в Page Directory, що вказує на потрібну Page Table. Адреса в полі table визначає запис в Page Table, яка містить фізична адреса сторінкового фрейму, що містить сторінку. Поле offset визначає відносну позицію в сторінковому фреймі. Оскільки довжина цього поля дорівнює 12 бітам, кожна сторінка містить 4 KB даних.


Отже, фізичну адресу обчислюється таким чином:



  1. cr3 + Page Directory (10 MSBs) = вказує table_base

  2. table_base + Page Table (10 середніх біт) = вказує page_base

  3. page_base + Offset = фізичну адресу (сторінковий фрейм)

Оскільки Page Directory і Page Table займають 10 біт, вони можуть адресувати максимум 1024 * 1024 KB, а Offset може адресувати до 2 ^ 12 (4096 байт). Таким чином, сумарне адресуються простір в Page Directory одно 1024 * 1024 * 4096 (2 ^ 32 елементів пам'яті, що дорівнює 4 GB). Тобто, на x86-архітектурах адресується простір обмежений 4 GB.


Розширене розбиття на сторінки


Розширене розбиття на сторінки виходить при видаленні таблиці перетворення Page Table; тоді поділ лінійного адреси здійснюється між Page Directory (10 MSB) і Offset (22 LSB).


22 біт LSB формують кордон в 4 MB для сторінкового фрейму (2 ^ 22). Розширене розбиття на сторінки існує разом із звичайним розбивкою і включається для відображення великої кількості безперервних лінійних адрес у відповідні фізичні адреси. Операційна система видаляє Page Table і забезпечує, таким чином, розширене розбиття на сторінки. Це вирішується шляхом установки прапора PSE (page size extension).


36-бітний PSE розширює підтримку 36-бітного фізичної адреси зі сторінками розміром 4 MB і підтримує також 4-байтних запис сторінкових каталогів, надаючи, таким чином, простий механізм адресації фізичної пам'яті вище 4 GB і не вимагаючи великих змін дизайну операційних систем. Такий підхід має практичні обмеження, що стосуються потреби розбиття на сторінки.


 


Модель розбиття на сторінки в Linux


Розбиття на сторінки в Linux аналогічно звичайному розбиття, але x86-архітектура має трирівневий табличний механізм, що складається з:



  • Page Global Directory (Pgd – глобальний каталог сторінок), абстрактний верхній рівень багаторівневих таблиць сторінок. Кожен рівень таблиці сторінок має справу з різними розмірами пам'яті – цей глобальний каталог може працювати з областями розміром 4 MB. Кожен запис буде дороговказом на більш низкоуровневую таблицю каталогів меншого розміру, тобто pgd – це каталог таблиць сторінок. Коли код проходить цю структуру (Це роблять деякі драйвери), говориться, що виконується "прохід" за таблицями сторінок.

  • Page Middle Directory (Pmd – проміжний каталог сторінок), проміжний рівень таблиць сторінок. На x86-архітектурі pmd не представлений апаратно, але входить в pgd в коді ядра.

  • Page Table Entry (Pte – запис таблиці сторінок), нижній рівень, що працює зі сторінками безпосередньо (шукайте PAGE_SIZE), Є значенням, яке містить фізична адреса сторінки разом з пов'язаними бітами, що вказують, наприклад, що запис коректна, і відповідна сторінка присутня в реальному пам'яті.

Ця трирівнева схема розбиття на сторінки також реалізована в Linux для підтримки областей пам'яті великих розмірів. Якщо підтримка великих областей пам'яті не потрібно, ви можете повернутися до дворівневої схемою, встановивши pmd в "1".


Ці рівні оптимізуються під час компіляції, дозволяючи другий і третій рівні (з використанням того ж самого набору коду) простим дозволом або забороною проміжного каталогу. 32-бітові процесори використовують pmd-розбиття, а 64-биті використовують pgd-розбиття.


У 64-бітних процесорах:


  • 21 біт MSB не використовуються

  • 13 біт LSB представлені зміщенням сторінки

  • Решта 30 біт розділені на

    • 10 біт для Page Table

    • 10 біт для Page Global Directory

    • 10 біт для Page Middle Directory

Як видно з архітектури, для адресації фактично використовуються 43 біта. Тобто, на 64-бітних процесорах реально доступно 2 в ступені 43 осередків віртуальної пам'яті.


Кожен процес має свій власний набір каталогів сторінок і таблиць сторінок. Для звернення до сторінковому фрейму з реальними для користувача даними операційна система починає з завантаження (на x86-архітектурах) pgd в регістр cr3. Linux зберігає в TSS-сегменті вміст регістру cr3 і потім завантажує інше значення з TSS-сегменту в регістр cr3 кожен раз під час виконання нового процесу в CPU. У результаті модуль розбиття на сторінки посилається на коректний набір таблиць сторінок.


Кожен запис в таблиці pgd вказує на сторінковий фрейм, що містить масив записів pmd, які, у свою чергу, вказують на сторінковий фрейм, що містить pte, який, нарешті, вказує на сторінковий фрейм, що містить дані користувачів. Якщо шукана сторінка знаходиться у файлі підкачки, в таблиці pte зберігається запис файлу підкачки, яка використовується (при помилці відсутності сторінки) при пошуку сторінкового фрейму для завантаження в пам'ять.


На рисунку 8 показано, що ми додаємо зсув на кожному рівні таблиць сторінок для відображення на відповідний запис сторінкового фрейму. Ці зміщення ми отримуємо, розбивши лінійний адресу, прийнятий з модуля сегментації. Для розбиття лінійного адреси, відповідного кожному компоненту таблиці сторінок, в ядрі використовуються різні макроси. Не розглядаючи детально ці макроси, давайте подивимося на схему розбивки лінійного адреси.


Малюнок 7. Лінійні адреси мають різні розміри

Резервні сторінкові фрейми


Linux резервує кілька станичних фреймів для коду ядра і структур даних. Ці сторінки ніколи не скидаються у файл підкачки на диск. Лінійні адреси з 0x0 до 0xc0000000 (PAGE_OFFSET) Використовуються кодом користувача та кодом ядра. Адреси з PAGE_OFFSET до 0xffffffff використовуються кодом ядра.


Це означає, що з 4 GB тільки 3 GB доступні для користувальницького додатка.


Як дозволяється розбиття на сторінки


Механізм розбиття на сторінки, що використовується процесами Linux, має дві фази:



  • Під час початкового завантаження, система встановлює таблицю сторінок для 8 MB фізичної пам'яті.

  • Потім друга фаза завершує відображення для залишилася фізичної пам'яті.

У фазі первісного завантаження за ініціалізацію розбиття на сторінки відповідає виклик startup_32(). Ця функція реалізована у файлі arch/i386/kernel/head.S. Відображення цих 8 MB відбувається з адреси вище PAGE_OFFSET. Ініціалізація починається із статично визначеної під час компіляції масиву, званого swapper_pg_dir. Під час компіляції він розміщується за конкретною адресою (0x00101000).


Визначаються запису для двох таблиць, визначених у коді статично – pg0 і pg1. Розмір цих сторінкових фреймів за замовчуванням дорівнює 4 KB, якщо не встановлено біт розширення розміру сторінок (page size extension) (додаткова інформація за PSE знаходиться в розділі "Розширене розбиття на сторінки "). Тоді розмір кожної дорівнює 4 MB. Адреса даних, що вказується глобальним масивом, зберігається в регістрі cr3, Що, я вважаю, є першим етапом установки модуля розбиття на сторінки для процесів Linux. Решта сторінкові запису встановлюються у другій фазі.


Друга фаза реалізована в методі paging_init().


Відображення RAM виконується між PAGE_OFFSET і адресою, що становлять кордон 4 GB (0xFFFFFFFF) в 32-бітної x86-архітектурі. Це означає, що RAM розміром приблизно в 1 GB може бути відображена при завантаженні Linux, і це відбувається за умовчанням. Однак, якщо встановити HIGHMEM_CONFIG, То фізична пам'ять розміром більше 1 GB може бути відображена на ядра – майте на увазі, що це тимчасова міра. Це робиться викликом kmap().


 


Зона фізичної пам'яті


Я вже показав вам, що ядро Linux (на 32-бітної архітектури) ділить віртуальну пам'ять у відношенні 3:1 – 3 GB віртуальної пам'яті для простору користувача та 1 GB для простору ядра. Код ядра і його структури даних повинні розміщуватися в цьому 1 GB адресного простору, але навіть ще більшим споживачем цього адресного простору є віртуальне відображення фізичної пам'яті.


Це відбувається тому, що ядро не може маніпулювати пам'яттю, якщо вона не відображена в його адресний простір. Таким чином, максимальною кількістю фізичної пам'яті, яке може обробити ядро, є об'єм пам'яті, який міг би з'явитися у віртуальний адресний простір ядра мінус обсяг, необхідний для відображення самого коду ядра. У результаті Linux на основі x86-системи змогла б працювати з об'ємом менше 1 GB фізичної пам'яті. І це максимум.


Тому, для обслуговування великої кількості користувачів, для підтримки більшого обсягу пам'яті, для поліпшення продуктивності і для установки архітектурно-незалежного способу опису пам'яті модель пам'яті Linux повинна була вдосконалюватися. Для досягнення цих цілей в більш нової моделі кожному CPU був призначений свій банк пам'яті. Кожен банк називається вузлом; кожен вузол поділяється на зони. Зони (Що представляють діапазони пам'яті) далі розбивалися на наступні типи:



  • ZONE_DMA (0-16 MB): Діапазон пам'яті, розташований в нижній області пам'яті, чого вимагають деякі пристрої ISA / PCI.

  • ZONE_NORMAL (16-896 MB): Діапазон пам'яті, який безпосередньо відображається ядром у верхні ділянки фізичної пам'яті. Всі операції ядра можуть виконуватися тільки з використанням цієї зони пам'яті; таким чином, це сама критична для продуктивності зона.

  • ZONE_HIGHMEM (896 MB і вище): Залишилося доступна пам'ять системи не відображається ядром.

Концепція вузла реалізована в ядрі за допомогою структури struct pglist_data. Зона описується структурою struct zone_struct. Фізичний сторінковий фрейм представлений структурою struct Page, І всі ці структури зберігаються в глобальному масиві структур struct mem_map, Який розміщується на початку NORMAL_ZONE. Ці основні взаємовідносини між вузлом, зоною і сторінковим фреймом показані на рисунку 9.


Малюнок 8. Взаємовідносини між вузлом, зоною і сторінковим фреймом

Зона верхній області пам'яті з'явилася в системі управління пам'яттю ядра тоді, коли були реалізовані розширення віртуальної пам'яті Pentium II (для доступу до 64 GB пам'яті засобами PAE – Physical Address Extension – на 32-бітних системах) і підтримка 4 GB фізичної пам'яті (знову ж таки, на 32-бітних системах). Ця концепція застосовна до платформ x86 і SPARC. Зазвичай ці 4 GB пам'яті роблять доступними відображення ZONE_HIGHMEM в ZONE_NORMAL за допомогою kmap(). Зверніть увагу, будь ласка, на те, що не бажано мати більше 16 GB RAM на 32-бітної архітектури навіть при дозволеному PAE.


(PAE – розроблене Intel розширення адрес пам'яті, що дозволяє процесорам збільшити кількість бітів, які можуть бути використані для адресації фізичної пам'яті, з 32 до 36 через підтримку в операційній системі додатків, що використовують Address Windowing Extensions API.)


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


Наприклад:



  • Запит на користувальницьку сторінку повинен бути спочатку заповнений з "нормальною" зони (ZONE_NORMAL);

  • якщо завершення невдало – з ZONE_HIGHMEM;

  • якщо знову невдало – з ZONE_DMA.

Список зон для такого розподілу складається із зон ZONE_NORMAL, ZONE_HIGHMEM і ZONE_DMA у зазначеному порядку. З іншого боку, запит DMA-сторінки може бути виконаний із зони DMA, тому список зон для таких запитів містить тільки зону DMA.


 


Висновок


Управління пам'яттю – це великий, комплексний і трудомісткий набір завдань, один з тих, в яких розібратися непросто, тому що створення моделі поведінки системи в реальних умовах в багатозадачному середовищі є важкою роботою. Такі компоненти як планування, розбиття на сторінки та взаємодія процесів пред'являють серйозні вимоги. Я сподіваюся, що ця стаття допоможе вам розібратися в основних поняттях, необхідних для оволодіння завданнями управління пам'яттю в Linux, і забезпечить вас початковою інформацією для подальших досліджень.

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


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

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

Ваш отзыв

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

*

*