Глава 11. Управление памятью ядра

Содержание

Глава 11. Управление памятью ядра
Схема памяти системы - пространство ядра и пространство пользователя
Адресация ядра - понятие нижних и верхних адресов памяти
Нижние адреса памяти
Верхние адреса памяти
Адреса пространства пользователя
Область виртуальной памяти (VMA)
Трансляция адресов и MMU
Поиск страниц и TLB
Как работает TLB
Механизм выделения памяти
Блок выделения страниц
API блока выделения страниц
Функции преобразования
Распределение slab
Алгоритм распределения buddy
Прогулка по распределению slab
Семейство выделения памяти kmalloc
Распределение vmalloc
Под капотом процесса выделения памяти
Вариант копирования записью (CoW)
Работа с вводом/ выводом памяти для взаимодействия с оборудованием
Доступ к устройствам PIO
Доступ к устройствам MMIO
Куки __iomem
Назначение (переназначение) памяти
kmap
Установка соответствия памяти ядра пространству пользователя
Применение remap_pfn_range
Применение io_remap_pfn_range
Файловые операции mmap
Реализация mmap в самом ядре
Система кэширования Linux
Что такое кэш?
Кэш ЦПУ - кэширование памяти
Кэширование страниц Linux - кэш диска
Специализированное кэширование (кэширование пространства пользователя)
Зачем откладывать запись данных на диск?
Стратегии кэширования записи
Потоки сброса
Управление ресурсами устройств - Devres
Выводы

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

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

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


(номер панели переключения + номер комнаты) <=> частный (реальный) номер телефона
 	   

Всякий раз, когда кто-то в этом городе (или по всему миру) желает соединиться с занимающим комнату, он должен пройти имеющуюся линию оперативной поддержки. Ему необходимо знать верный номер оперативной связи с самим отелем, а также необходимый номер комнаты. Таким образом, номер панели переключения + номер комнаты = виртуальному адресу, в то время как private phone number соответствует искомому физическому адресу. Имеется ряд правил, связанных с моделью отеля, которые также относятся и к Linux:

Таблица 11-1. Соответствие правил отеля для Linux
Отель Linux

Вы не имеете возможности контактировать с частным телефоном того, кто занимает комнату. Даже нет варианта осуществить такую покупку. Ваш вызов внезапно будет оборван.

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

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

Если вы осуществляете доступ к не поставленной в соответствие памяти, имеющийся ЦПУ возбуждает отказ страницы и сама ОС отрабатывает его.

Вы не можете осуществить соединение с тем, чьё пребывание в отеле завершено.

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

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

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

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

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

Именно так можно себе представлять то, как работают все виртуальные адреса в системе Linux.

В этой главе мы будем иметь дело со всей системой управления памяти Linux, охватывая следующие разделы:

  • Схемы памяти, применяемые при трансляции адресов и MMU

  • Механизмы выделения памяти (блок распределения страниц, блок распределения листов - slab, блок распределения kmalloc и так далее)

  • Доступ ввода/ вывода памяти

  • Установление соответствия памяти ядра пространству пользователя и реализации функции обратного вызова mmap()

  • Введение в систему кэширования Linux

  • Введение в инфраструктуру ресурсов управления имеющимися устройствами (devres)

Схема памяти системы - пространство ядра и пространство пользователя

На протяжении данной главы такие термины как пространство ядра (kernel space) и пространство пользователя (user space) будут относиться к их виртуальному адресному пространству. В системах Linux всякий процесс обладает собственным виртуальным адресным пространством. Это некий вид песочница памяти на время жизни данного процесса. В системах с 32 битами это адресное пространство составляет 4ГБ в размере (даже для систем с физическим размером менее 4ГБ). Для каждого процесса это адресное пространство в 4ГБ расщепляется на две части:

  • Виртуальные адреса пространства пользователя

  • Виртуальные адреса пространства ядра

Тот способ, которым осуществляется данное расщепление зависит от специальной опции настройки ядра, CONFIG_PAGE_OFFSET, которая определяет где начинаются адреса самого ядра в адресном пространстве некоего процесса. Обычным значением является значение по умолчанию 0xC0000000 в 32- битных системах, однако его можно изменять, как это имеет место в случае процессоров семейства i.MX6 от NXP, которые используют 0x80000000. Во всей данной главе мы будем считать значением по умолчанию адрес 0xC0000000. Такое расщепление именуется как расщепление 3G/1G, при котором пространству пользователя предоставляются нижние 3ГБ виртуального адресного пространства, а само ядро применяет оставшийся 1ГБ верхних адресов. Некая типичная схема виртуального адресного пространства процесса выглядит следующим образом:


.------------------------. 0xFFFFFFFF
|                        | (4 GB)
|    Kernel addresses    |
|                        |
|                        |
.------------------------.CONFIG_PAGE_OFFSET
|                        |(x86: 0xC0000000, ARM: 0x80000000)
|                        |
|                        |
|  User space addresses  |
|                        |
|                        |
|                        |
|                        |
'------------------------' 00000000
 	   

Оба адреса, применяемые и пространством ядра, и пространством пользователя, являются виртуальными адресами. Единственная разница состоит в том, что доступ к некоторому адресу ядра требует какого- то привилегированного режима. Привилегированные режим имеет расширенные полномочия. Когда ЦПУ осуществляет исполнение кода на стороне пространства пользователя, такой активный процесс именуется как исполняемый в определённом режиме пользователя; когда этот ЦПУ исполняет код на стороне своего ядра, такой активный процесс именуется исполняемым в режиме ядра.

[Замечание]Замечание

Определяя некий адрес (естественно, виртуальный), можно различить будет ли это адрес пространства ядра или адрес пространства пользователя, применяя приведённую выше схему процесса. Всякий адрес, попадающий в 0-3ГБ поступает из пространства пользователя, в противном случае он из своего ядра.

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

Управление элементами всей памяти организовано в виде единиц фиксированного размера, именуемых страницами. Страница содержит 4 096 байт (4кБ). Даже хотя данный размер может отличаться в прочих системах, он фиксирован в ARM и x86, архитектурами которых мы интересуемся:

  • некая памяти страница, виртуальная страница или просто страница являются терминами, применяемыми для ссылки на непрерывный блок виртуальной памяти фиксированной длины. Аналогичное наименование, page, применяется как некая структура данных ядра для представления какой- то страницы памяти.

  • С другой стороны, кадр (frame, или страничный блок) указывает на некий непрерывный блок фиксированного размера физической памяти в верхних адресах, в которых данная операционная система ставит в соответствие некую страницу памяти. Каждый страничный блок определяется неким номером, PFN (page frame number, номером страничного блока). Задавая некую страницу можно легко получить её PFN и наоборот, применяя макросы page_to_pfn и pfn_to_page, что подробно будет обсуждено в следующих разделах.

  • Некая таблица страниц является определяющей структурой данных ядра и архитектуры, применяемой для имеющегося соответствия между виртуальными адресами и физическими адресами. Определённый ключ пары страница/ кадр задаёт некую единичную запись в данной таблице страниц. Это и представляет из себя соответствие.

Так как некая страница памяти ставится в соответствие страничному блоку, само собой разумеется, что страницы и страничные блоки имеют один и тот же размер, в нашем случае 4кБ. Данный размер страницы определяется в самом ядре посредством макроса PAGE_SIZE.

[Замечание]Замечание

Существуют ситуации, при которых необходимо выравнивать память на границы страниц. Говорят, что некая память является выравненной на границу страниц, если её адреса начинаются в точности в самом начале какой- то страницы. Например, в системе с размером страницы 4K, 4 096, 20 480, 409 600 представляют собой адреса памяти выравненные на границу страниц. Другими словами, всякая память, адрес которой является произведением имеющегося размера страницы именуется выравненным на границу страницы.

  Адресация ядра - понятие нижних и верхних адресов памяти

Ядро Linux имеет своё собственное виртуальное адресное пространство, как это имеется в случае режима каждого процесса пользователя. Такое виртуальное адресное пространство самого ядра (с размером в 1ГБ при расщеплении 3G/1G) подразделяется на две части:

  • Нижние адреса, или LOWMEM, которыми являются самые первые 896МБ

  • Верхние адреса, или HIGHMEM, представленные вверху 1286МБ


                                        Physical mem
   Process address space     +------> +------------+
                             |        |  3200 M    |
                             |        |            |
4 GB +---------------+ <-----+        |  HIGH MEM  |
     |    128 MB     |                |            |
     +---------------+ <---------+    |            |
     +---------------+ <------+  |    |            |
     |    896 MB     |        |  +--> +------------+
3 GB +---------------+ <--+   +-----> +------------+
     |               |    |           |   896 MB   | LOW MEM
     |    /////      |    +---------> +------------+
     |               |
0 GB +---------------+
 	   
 

Нижние адреса памяти

 

Верхние адреса памяти

  Адреса пространства пользователя


struct task_struct{
    [...]
    struct mm_struct *mm, *active_mm;
    [...]
}
 	   


struct mm_struct {
        struct vm_area_struct *mmap;
        struct rb_root mm_rb;
        unsigned long mmap_base;
        unsigned long task_size;
        unsigned long highest_vm_end;
        pgd_t * pgd;
        atomic_t mm_users;
        atomic_t mm_count;
        atomic_long_t nr_ptes;
#if CONFIG_PGTABLE_LEVELS > 2
        atomic_long_t nr_pmds;
#endif
        int map_count;
        spinlock_t page_table_lock;
        struct rw_semaphore mmap_sem;
        unsigned long hiwater_rss;
        unsigned long hiwater_vm;
        unsigned long total_vm;
        unsigned long locked_vm;
        unsigned long pinned_vm;
        unsigned long data_vm;
        unsigned long exec_vm;
        unsigned long stack_vm;
        unsigned long def_flags;
        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;

        /* Architecture-specific MM context */
        mm_context_t context;

        unsigned long flags;
        struct core_state *core_state;
#ifdef CONFIG_MEMCG
        /*
         * "owner" points to a task that is regarded as the canonical
         * user/owner of this mm. All of the following must be true in
         * order for it to be changed:
         *
         * current == mm->owner
         * current->mm != mm
         * new_owner->mm == mm
         * new_owner->alloc_lock is held
         */
        struct task_struct __rcu *owner;
#endif
        struct user_namespace *user_ns;
        /* store ref to file /proc/<pid>/exe symlink points to */
        struct file __rcu *exe_file;
};
 	   

 

Рисунок 11-1



  Область виртуальной памяти (VMA)

 

Рисунок 11-2




# cat /proc/1073/maps
00400000-00403000 r-xp 00000000 b3:04 6438 /usr/sbin/net-listener
00602000-00603000 rw-p 00002000 b3:04 6438 /usr/sbin/net-listener
00603000-00624000 rw-p 00000000 00:00 0 [heap]
7f0eebe4d000-7f0eebe54000 r-xp 00000000 b3:04 11717 /usr/lib/libffi.so.6.0.4
7f0eebe54000-7f0eec054000 ---p 00007000 b3:04 11717 /usr/lib/libffi.so.6.0.4
7f0eec054000-7f0eec055000 rw-p 00007000 b3:04 11717 /usr/lib/libffi.so.6.0.4
7f0eec055000-7f0eec069000 r-xp 00000000 b3:04 21629 /lib/libresolv-2.22.so
7f0eec069000-7f0eec268000 ---p 00014000 b3:04 21629 /lib/libresolv-2.22.so
[...]
7f0eee1e7000-7f0eee1e8000 rw-s 00000000 00:12 12532 /dev/shm/sem.thk-mcp-231016-sema
[...]
 	   


/* Look up the first VMA which satisfies addr < vm_end, NULL if none. */
extern struct vm_area_struct * find_vma(struct mm_struct * mm, unsigned long addr);
 	   


struct vm_area_struct *vma = find_vma(task->mm, 0x13000);
if (vma == NULL) /* Not found ? */
    return -EFAULT;
if (0x13000 >= vma->vm_end) /* Beyond the end of returned VMA ? */
    return -EFAULT;
 	   

Трансляция адресов и MMU

 

Рисунок 11-3



 

Рисунок 11-4



 

Рисунок 11-5




#define PAGE_SHIFT     12
#ifdef __ASSEMBLY__
#define PAGE_SIZE      (1 << PAGE_SHIFT)
#else
#define PAGE_SIZE      (1UL << PAGE_SHIFT)
#endif
 	   

 

Рисунок 11-6



  Поиск страниц и TLB

 

Как работает TLB

 

Рисунок 11-7



Механизм выделения памяти

 

Рисунок 11-8



  Блок выделения страниц

 

API блока выделения страниц

 

Функции преобразования

  Распределение slab

 

Алгоритм распределения buddy

 

Рисунок 11-9



 

Рисунок 11-10



 

Прогулка по распределению slab

 

Рисунок 11-11



  Семейство выделения памяти kmalloc

 

Рисунок 11-12



  Распределение vmalloc

 

Рисунок 11-13



  Под капотом процесса выделения памяти

 

Вариант копирования записью (CoW)

Работа с вводом/ выводом памяти для взаимодействия с оборудованием

  Доступ к устройствам PIO

  Доступ к устройствам MMIO

Отображаемый в памяти ввод/ вывод (MMIO, Memory-mapped I/O) находится в том же самом пространстве адресов, что и память. Имеющееся ядро применяет часть существующего адресного пространства обычно применяется оперативной памятью (RAM, фактически, HIGH_MEM) для установления соответствия регистрам устройств с тем, чтобы вместо наличия по этим адресам действительной памяти (то есть оперативной памяти, RAM), там находятся устройства ввода/ вывода. Тем самым взаимодействие с неким устройством ввода/ вывода становится подобным чтению и записи по адресам памяти приданых этому устройству ввода/ вывода.

Как и в случае PIO, имеются функции MMIO для информирования ядра о нашем намерении применять некую область памяти. Помните, что это всего лишь резервирование. Существуют request_mem_region() и release_mem_region():


struct resource* request_mem_region(unsigned long start,
                                    unsigned long len, char *name)
void release_mem_region(unsigned long start, unsigned long len)
 	   

Это к тому же некая учтивость.

[Замечание]Замечание

Можно отобразить области памяти на самом деле находящиеся в использовании путём просмотра содержимого файла /proc/iomem.

Прежде чем осуществить доступ к некоторой области памяти (и после того как вы успешно запросили её), эта область должна быть поставлена в соответствие в адресном пространстве ядра путём вызова особых зависящих от архитектуры функций (которые применяют MMU для построения необходимой таблицы страниц и, таким образом, не могут вызываться из имеющихся обработчиков прерывания). Имеются также ioremap() и iounmap():


void __iomem *ioremap(unsigned long phys_add, unsigned long size)
void iounmap(void __iomem *addr)
 	   

ioremap() возвращает некий указатель __iomem void на самое начало такой области соответствия. Не подвергайте себя искушению применять (получать/ устанавливать необходимое значение путём чтения/ записи по этому указателю) такие указатели. Имеющееся ядро предоставляет функции для доступа к памяти, поставленной в соответствие вводу/ выводу. Вот они:


unsigned int ioread8(void __iomem *addr);
unsigned int ioread16(void __iomem *addr);
unsigned int ioread32(void __iomem *addr);
void iowrite8(u8 value, void __iomem *addr);
void iowrite16(u16 value, void __iomem *addr);
void iowrite32(u32 value, void __iomem *addr);
 	   
[Замечание]Замечание

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

В 32- битной системе факт состоит в том, что MMIO похищает адресное пространство физической памяти для создания соответствия устройствам с отображением в памяти (MMIO), что является недостатком, так как это не даёт возможности такой системе применять украденную память для целей обыкновенной оперативной памяти.

 

Куки __iomem

__iomem является объектом куки (cookie) ядра, применяемого Sparse, блока синтаксического разбора, используемого имеющимся ядром для поиска возможных отказов в коде. Для получения преимуществ от предлагаемой Sparse функциональности она должна быть включена во время компиляции ядра; если это не так, __iomem будет в любом случее проигнорирован.

Присвоение c=1 в соответствующей командной строке включит для вас Sparse, однако вначале необходимо установить в вашей системе синтаксический разбор:


sudo apt-get install sparse
		

Например, при построении некоего модуля воспользуйтесь:


make -C $KPATH M=$PWD C=1 modules
		

В качестве альтернативы, в случае когда написан надлежащий makefile, просто наберите:


make C=1
		

Приводимое ниже определение показывает как __iomem задаётся в самом ядре:


#define __iomem    __attribute__((noderef, address_space(2)))
		

Оно предостерегает нас от ошибочного применения драйверами доступа к памяти ввода/ вывода. Добавление __iomem для всего доступа ввода/ вывода является неким способом, которому необходимо строго следовать. Так как даже доступ к вводу/ выводу осуществляется через виртуальную память (в системах с MMU), эти объекты куки оберегают нас от применения абсолютных физических адресов, а также требует от нас применения ioremap(), которая возвратит некий виртуальный адрес, помеченный объектом куки __iomem:


void __iomem *ioremap(phys_addr_t offset, unsigned long size);
		

Поэтому мы можем применять специально предназначенные функции, такие как ioread32() и iowrite32(). Вы можете удивиться почему бы не применить обычные функции readl()/ writel(). Они отклоняются по той причине, поскольку они не выполняют проверку на вменяемость и менее безопасны (не требуют никаких __iomem) в отличии от семейства функций ioreadX()/ iowriteX(), которые применяют только адреса __iomem.

Кроме того, noderef является неким атрибутом, который применяется Sparse чтобы гарантировать программистам и не выполнять разименование некоего указателя __iomem. Даже несмотря на то, что это может работать в какой- то архитектуре, настоятельно не рекомендуется поступать так. Применяйте вместо этого специальные функции ioreadX()/ iowriteX(). Именно они являются переносимыми и работают в любых архитектурах. Теперь позвольте нам показать как Sparse будет предостерегать нас при разименовании какого- то указателя __iomem:


#define BASE_ADDR 0x20E01F8
void * _addrTX = ioremap(BASE_ADDR, 8);
		

Во первых, Sparse не удовлетворится по причине начальной инициализации:


warning: incorrect type in initializer (different address spaces)expected void *_addrTXgot void [noderef] <asn:2>*
		

Либо:


u32 __iomem* _addrTX = ioremap(BASE_ADDR, 8);
*_addrTX = 0xAABBCCDD; /* плохо. Никакого разименования */
pr_info("%x\n", *_addrTX); /* плохо. Никакого разименования */
		

Sparse всё ещё не будет удовлетворён:


New-VM -Name VM01 -Generation 2
		

Наш последний пример удовлетворяет Sparse:


void __iomem* _addrTX = ioremap(BASE_ADDR, 8);
iowrite32(0xAABBCCDD, _addrTX);
pr_info("%x\n", ioread32(_addrTX));
		

Двумя правилами, которые вам следует запомнить являются:

  • Всегда применяйте __iomem там, где они необходимы, будь это возвращаемый тип, либо это часть типа параметра, а также применяйте Sparse для проверки того что вы это сделали.

  • Не разименовывайте какой бы то ни было указатель __iomem; вместо этого применяйте специально для этого предназначенные функции.

Назначение (переназначение) памяти

  kmap

  Установка соответствия памяти ядра пространству пользователя

 

Применение remap_pfn_range

 

Применение io_remap_pfn_range

 

Файловые операции mmap

 

Реализация mmap в самом ядре

Система кэширования Linux

  Что такое кэш?

 

Кэш ЦПУ - кэширование памяти

 

Кэширование страниц Linux - кэш диска

 

Специализированное кэширование (кэширование пространства пользователя)

  Зачем откладывать запись данных на диск?

 

Стратегии кэширования записи

 

Потоки сброса

Управление ресурсами устройств Devres

Выводы