В данной заметке речь пойдёт об отображении кадров страниц памяти из ZONE_HIGHMEM
на линейное адресное пространство ядра. Как было упомянуто в Управление памятью#Зоны памяти, ядро не может непосредственно адресовать память из ZONE_HIGHMEM
. Чтобы ядро могло получить доступ до этой памяти и нужно её отображение в адресное пространство ядра.
На современных ПК, в том числе смартфонах, вряд ли уже можно встретить ZONE_HIGHMEM
, т.к. 64-битные платформы не имеют ограничение по адресному пространству. Однако, представленный механизм имеет место в Linux v5.17, т.к. 32-битные платформы из нашей жизни не исчезли: кто знает, сколько Aarch32-/MIPS32-роутеров, работающих на Linux, пересекла эта страница, прежде чем попасть в браузер.
Так что же всё-таки такое, эта "верхняя память"? Дело в том, что исполняемый код может оперировать лишь с виртуальными адресами, которые ограничены 4 ГиБ виртуального адресного пространства для 32-битных систем. Для потоков пользовательского пространства это может означать, что одному такому потоку может быть доступно лишь 4 ГиБ. Забегаю вперёд, но скажу, что для каждого процесса есть своя таблица трансляции виртуальных адресов в физические. Это также называют виртуальным адресным пространством процесса. И с пользовательскими процессами всё понятно: если у каждого процесса своё пространство памяти, то можно каждое такое виртуальное пространство отображать на разные участки физической памяти.
Однако, для пространства ядра Linux есть свои особенности. Часть рутин, вроде обработки прерываний, вовсе не имеет своего контекста: оно заимствует часть у вытесненного процесса, часть хранится в глобальных переменных. Например, init_mm
, которым пользуется ядро всю свою жизнь от загрузки ядра, до отключения питания у машины. В этой переменной — грубо говоря — и хранится виртуальное адресное пространство ядра.
В виду вышесказанного, программист, который разрабатывает, например, драйвер периферийного устройства, не может позволить себе сделать __get_free_page(__GFP_HIGHMEM)
: виртуальный адрес, как минимум, не влезет в тип данных указателя, да и фактически адресовать такую память было бы невозможно: её адрес выходит за пределы аппаратных возможностей (от размера регистра и невозможности адресовать память двумя регистрами, с точки зрения набора инструкций, до аппаратных блоков, занимающихся декодированием виртуального адреса и его трансляцией).
ZONE_HIGHMEM
В качестве примера я возьму исходный код arch/arm
. Суть не изменится и для arch/x86
, собранного под 32-битные ПК.
Начальный виртуальный адрес ZONE_HIGHMEM
лежит в переменной void *high_memory
. При инициализации ядра, это значение инициализируется следующим образом:
high_memory = __va(arm_lowmem_limit - 1) + 1;
То есть, берётся виртуальный адрес от ограничения arm_lowmem_limit
, которое является физическим адресом первого байта, идущего за — доступной ядру для непосредственного доступа — памятью. Откуда берётся arm_lowmem_limit
? Фактически, это конец последнего блока памяти, который помещается в vmalloc_limit
(подробнее см. adjust_lowmem_bounds()
в arch/arm/mm/mmu.c
). А vmalloc_limit
в свою очередь является нижней границей физических адресов для vmalloc
(я пока ничего не писал, но тут можно почитать подробнее). По умолчанию, arm
выделяет под это дело 240 МиБ памяти, но это можно переопределить параметром загрузки ядра vmalloc
(подробнее про установку значения см. early_vmalloc()
в arch/arm/mm/mmu.c
).
Сколько в итоге будет верхней памяти зависит от конкретного устройства: базовый физический адрес памяти, объём памяти — всё это влияет на исчисления при инициализации ядра Linux. На имеющемся в моём распоряжении устройстве на Aarch32 SoC на базе ядер Cortex-A9 с 1008 МиБ оперативной памяти (на самом деле их там больше, но в параметры загрузки Linux была передана строка mem=1008M
) ZONE_HIGHMEM
не суждено было появиться, lowmem
занял её полностью:
[ 0.000000] Virtual kernel memory layout:
[ 0.000000] vector : 0xffff0000 - 0xffff1000 ( 4 kB)
[ 0.000000] fixmap : 0xffc00000 - 0xfff00000 (3072 kB)
[ 0.000000] vmalloc : 0xbf800000 - 0xff800000 (1024 MB)
[ 0.000000] lowmem : 0x80000000 - 0xbf000000 (1008 MB)
[ 0.000000] modules : 0x7f000000 - 0x80000000 ( 16 MB)
[ 0.000000] .text : 0x80008000 - 0x80bae09c (11929 kB)
[ 0.000000] .init : 0x80baf000 - 0x80c34000 ( 532 kB)
[ 0.000000] .data : 0x80c34000 - 0x80cbfdbc ( 560 kB)
[ 0.000000] .bss : 0x80cbfdbc - 0x80d97e00 ( 865 kB)
А вот на устройстве на базе 32-битного Intel Atom ZONE_HIGHMEM
присутствует:
[ 0.000000] 5388MB HIGHMEM available.
[ 0.000000] 755MB LOWMEM available.
[ 0.000000] mapped low ram: 0 - 2f3fe000
[ 0.000000] low ram: 0 - 2f3fe000
[ 0.000000] Zone ranges:
[ 0.000000] DMA [mem 0x0000000000001000-0x0000000000ffffff]
[ 0.000000] Normal [mem 0x0000000001000000-0x000000002f3fdfff]
[ 0.000000] HighMem [mem 0x000000002f3fe000-0x000000017fffffff]
...
[ 0.000000] virtual kernel memory layout:
[ 0.000000] fixmap : 0xfff16000 - 0xfffff000 ( 932 kB)
[ 0.000000] pkmap : 0xffc00000 - 0xffe00000 (2048 kB)
[ 0.000000] vmalloc : 0xefbfe000 - 0xffbfe000 ( 256 MB)
[ 0.000000] lowmem : 0xc0000000 - 0xef3fe000 ( 755 MB)
[ 0.000000] .init : 0xc1bc1000 - 0xc86f6000 (109780 kB)
[ 0.000000] .data : 0xc17deaa5 - 0xc1bbf680 (3970 kB)
[ 0.000000] .text : 0xc1000000 - 0xc17deaa5 (8058 kB)
В общем, не надо верить в константные 896 МиБ, о которых обычно пишут в книжках: не всё, не везде и не всегда.
Разработчики ядра Linux реализовали возможность использования верхней памяти следующим образом:
ZONE_HIGHMEM
происходит исключительно через функцию alloc_pages()
или макрос alloc_page()
. Этими рутинами можно спокойно пользоваться, т.к. они возвращают не линейный виртуальный адрес начала кадра памяти, а указатель на дескриптор страницы памяти. Дескрипторы страниц памяти всегда лежат в нижней памяти, поэтому доступ до них есть в любом случае.arm
выделяет под него 240 МиБ, как я писал выше, а x86
выделит только 128 МиБ (см. переменную __VMALLOC_RESERVE
в arch/x86/mm/pgtable_32.c
).В рамках этих двух условий есть три механизма, которые позволяют отобразить верхнюю память в линейное виртуальное адресное пространство ядра: постоянное отображение памяти, временное отображение памяти, несмежное выделение памяти(и есть vmalloc
). В рамках этой заметки я постараюсь раскрыть первые два механизма, а для третьего выделим отдельное время.
Попытка произвести постоянное отображение памяти может заблокировать процесс в случаях, когда количество элементов таблицы страниц недостаточное, чтобы использовать их как "окно" в верхнюю память. То есть, постоянное отображение памяти не может быть произведено, например, в обработчиках прерываний и отложенных функциях. Как противовес, временное отображение памяти гарантирует, что процесс не будет заблокирован, однако единовременно может быть отображено лишь небольшое количество памяти.
Ограничением временного отображения памяти является тот факт, что поток исполнения кода ядра обязан быть не вытесняемым. Делается это для того, чтобы быть уверенным, что другой поток исполнения кода ядра не будет использовать те же окна для других кадров верхней памяти.
Отдельно подчеркну то, что уже не раз мелькало между строк: ни один из этих механизмов не позволит ядру Linux использовать всю имеющуюся память — в основном ограничение будет равным ограничению vmalloc
.
Постоянное отображение памяти — как намекает название — позволяет отображать верхнюю память в адресное пространство ядра на длительный период. Этот механизм использует отдельную таблицу страниц, которая объявлена переменной pte_t *pkmap_page_table
в файле mm/highmem.c
. По сути, она является указателем на первую запись таблицы, которая относится к отображению верхней памяти. Количество таких записей можно получить из макроса LAST_PKMAP
, который объявлен в arch/
для каждой архитектуры. Для arch/arm
это будет 512 записей. Таким образом, отображено может быть до 512 * размер_страницы
верхней памяти. При страницах в 4 КиБ это будет 2 МиБ — единовременно, разумеется.
Все записи таблицы страниц, которые могут быть использованы под отображение верхней памяти, начинаются с адреса (линейного виртуального адреса, указывающего на фрагмент таблицы страниц), который хранится в макросе PKMAP_BASE
. Этот макрос также определён для каждой, использующей его, архитектуры. Для arch/arm
это будет следующее значение:
#define PKMAP_BASE (PAGE_OFFSET - PMD_SIZE)
— то есть, под отображение верхней памяти ARM использует последнюю Page Middle Directory.
Для учёта отображённых страниц используется массив static int pkmap_count[LAST_PKMAP]
, определённый в файле mm/highmem.c
. Каждый элемент в нём отвечает за отдельную запись в таблице страниц. Значение каждого элемента может иметь три смысловых значения:
0
: страница не используется для отображения верхней памяти и может быть использована для этого.1
: страница не используется для доступа к верхней памяти, но она всё ещё её отображает. Код mm
обязан очистить такую запись и очистить кэш (см. flush_all_zero_pkmaps
в mm/highmem.c
).1 + n
, при n > 0
: страница используется для отображения верхней памяти n
пользователями.Также в учёте отображённых страниц используется хэш-таблица page_addreess_htable
, внутри которой хранится по одному элементу типа struct page_address_map
на каждый кадр верхней памяти. Сама struct page_address_map
содержит указатель на дескриптор страницы и линейный виртуальный адрес памяти, по которому будет отображён кадр верхней памяти. Обе структуры определены в файле mm/highmem.c
:
struct page_address_map {
struct page *page;
void *virtual;
struct list_head list;
};
static struct page_address_map page_address_maps[LAST_PKMAP];
static struct page_address_slot {
struct list_head lh; /* List of page_address_maps */
spinlock_t lock; /* Protect this bucket's list */
} ____cacheline_aligned_in_smp page_address_htable[1<<PA_HASH_ORDER];
Не знаю, как у тебя читатель, а у меня возник вопрос: нам "дано", что под дескрипторы страниц для отображения верхней памяти задействовано некоторое количество дескрипторов, которые и так имеют линейный виртуальный адрес. Зачем же нам тогда хранить его как поле struct page_address_map
? Нам для отображения памяти куда-либо надо передать уже заполненный дескриптор страницы (см. kmap_high()
). И именно его мы присваиваем полю page
, одновременно с полем virtual
(см. set_page_address()
). А в поле virtual
будет лежать виртуальный адрес начала кадра той страницы, дескриптор которой мы зарезервировали под vmalloc
. Таким образом, чтобы получить линейный адрес кадра памяти по дескриптору страницы, можно вызвать функцию void *page_address(const struct page *page)
, которая:
ZONE_HIGHMEM
, вернёт виртуальный адрес начала кадра стандартным арифметическим методом (см. page_to_virt()
):__va((unsigned long)(page - mem_map) << PAGE_OFFSET)
ZONE_HIGHMEM
, то поищет в хэш-таблице запись, чьё поле page
совпадёт с переданным, и вернёт поле virtual
.Тут хочу оговориться, что до коммита cbe37d093707 ("mm: remove PG_highmem")
существовал отдельный флаг в дескрипторе страниц, но его убрали ещё в Linux v2.6.13. И ведь действительно, всегда можно арифметически посчитать принадлежность страницы к той или иной зоне по номеру этой страницы.
kmap
Функция static inline void *kmap(struct page *page)
, определённая в файле include/linux/highmem-internal.h
, выполняет постоянное отображение памяти, при условии, что переданная страница принадлежит ZONE_HIGHMEM
, а в противном случае — возвращает page_address()
от переданной страницы (то есть, возвращает виртуальный адрес начала кадра страницы). Но в любом случае, прежде чем вернуть адрес, эта функция очистит записи TLB, связанные с этим адресом, ведь он мог измениться после вызова kmap_high()
, которая и выполняет отображение верхней память в адресное пространство ядра.
Сама функция void *kmap_high(struct page *page)
определена в файле mm/highmem.c
. Она тоже простая, её суть заключается в следующем:
page_address()
проверить наличие страницы в page_address_htable
. Строго говоря, если мы передадим страницу, кадр которой лежит в нижней памяти, то функция page_addreess()
вернёт нам правильный виртуальный адрес начала кадра этой страницы с помощью lowmem_page_address()
. Но мы не должны использовать kmap_high
напрямую: есть очень узкое число использований этой функции, а передача туда страницы, адрес кадра которой не принадлежит верхней памяти чревато выходом за границы массива в строке pkmap_count[PKMAP_NR(vaddr)]++;
.page_address_htable
такой страницы ещё нет, то нужно отобразить запрошенную память в одну из свободных страниц. По сути, map_new_virtual()
буквально ищет страницу через массив pkmap_count
, устанавливает в ней запись об указанном виртуальном адресе верхней памяти, заполняет соответствующий struct page_address_map
и кладёт в хэш-таблицу page_address_htable
. Для ускорения поиска, индекс последней использованной страницы восстанавливается при каждом поиске, чтобы не начинать со страниц, которые скорее всего ещё заняты (см. переменную last_pkmap_nr
в map_new_virtual()
). Если поиск дошёл до конца и не нашёл свободных страниц, то функция может продолжить с нулевого индекса, предварительно проведя очистку освободившихся страниц (те, у которых значение элемента массива pkmap_count
строго равное единице). Если и после этого поиск не увенчался успехом, то процесс может заснуть, чтобы подождать, пока другие пользователи освободят свои страницы.pkmap_count
. То есть, после этого шага он должен быть равен двум или более.Я уже упомянул о lock_kmap()
. Да, функция kmap_high()
обладает критической секцией кода, который защищён блокировкой. Это spin_lock
, он определён рядом с pkmap_count
в том же файле: mm/highmem.c
:
static __cacheline_aligned_in_smp DEFINE_SPINLOCK(kmap_lock);
А сами методы работы с этой блокировкой — макросы, которые вызывают spin_lock(&kmap_lock)
или spin_unlock(&kmap_lock)
. Да, kmap_high()
захватывает блокировку, но не выключает прерывания: как я писал выше — в начале пункта Работа с верхней памятью — постоянное отображение памяти может заблокировать процесс, поэтому его нельзя использовать в обработчиках прерывания. Заблокировать процесс — точнее, встать в waitqueue
— можно внутри функции map_new_virtual()
. Если, пройдясь по всем элементам pkmap_count
, свободной страницы из резерва так и не нашлось, поток может заснуть в ожидании других потоков, которые вызовут kunmap_high()
, освободя этим страницу для отображения и будя, спящих в ожидании свободных страниц, потоки.
kunmap
Функция static inline void kunmap(struct page *page)
служит для освобождения отображённых страниц. Если переданная страница не принадлежит ZONE_HIGHMEM
, то она ничего не сделает. В противном случае, будет вызвана функция kunmap_high()
— всё по аналогии с kmap()
.
Освободить отображённую страницу намного проще: достаточно декрементировать счётчик страницы в pkmap_count
. Если счётчик после этого стал равен единице, то процессы, которые заснули в ожидании свободных страниц, должны быть разбужены. По сути, вся полезная нагрузка этой функции выглядит следующим образом:
switch (--pkmap_count[PKMAP_NR(page_addreess(page))]) {
case 0:
BUG();
case 1:
pkmap_map_wait = get_pkmap_wait_queue_head(color);
need_wakeup = waitqueue_active(pkmap_map_wait);
}
...
if (need_wakeup)
wake_up(pkmap_map_wait);
Дальнейшая работа по очистке отображённых страниц будет происходить в функции static void flush_all_zero_pkmaps(void)
. Именно в ней всем зарезервированным страницам, чей счётчик опустился до единицы, будет присвоен ноль в значение счётчика, их записям в таблице трансляции будет присвоен адрес NULL
, а вызов функции set_page_address()
обработает virtual
со значением NULL
как отдельный случай: удалит элемент из хэш-таблицы page_address_htable
и очистит элемент отображения из массива page_address_maps
.
Временное отображение памяти устроено гораздо проще постоянного. Кроме того, им можно пользоваться в контексте прерываний, т.к. оно либо вернёт виртуальный адрес отображённое памяти, либо вернёт ошибку, но не будет спать.
kmap_atomic
Начнём с функции static inline void *kmap_atomic(struct page *page)
из файла include/linux/highmem-internal.h
. Там два варианта этой функции, по итогу скомпилируется лишь один. Нам интересен та, что скомпилируется, если KMAP_LOCAL
определён — именно она действительно отобразит запрошенную память в адресное пространство ядра.
По сути, эта функция является обёрткой над следующим вызовом:
return kmap_atomic_prot(page, kmap_prot);
— где page
это переданный аргумент, а kmap_prot
— флаги доступа к памяти ядра, определённые самой архитектурой.
В свою очередь kmap_atomic_prot()
запрещает вытеснение задач (если CONFIG_PREEMPT_RT
включен, то только миграцию между CPU) и отключает обработку исключений Page fault
. Последнее делается с той целью, что мы не хотим быть вытеснены обработкой этого исключения (вытеснение мы уже запретили). Затем вызывается __kmap_local_page_prot()
, который по аналогии с kmap()
проверяет, что запрошенная страница принадлежит ZONE_HIGHMEM
— если да, то управление передаётся функции arch_kmap_local_high_get()
. Сама функция arch_kmap_local_high_get()
опционально определяется для каждой архитектуры. Для arch/arm
она определена в файле arch/arm/include/asm/highmem.h
. Таким образом, arch/arm
может вызвать kmap_high_get()
из mm/highmem.c
для отображения страницы. Однако, kmap_high_get()
на самом деле ничего не отображает. Она возвратит ненулевой указатель при условии, что переданная страница уже отображена в памяти. В таком случае она инкрементирует её счётчик в pkmap_count
. В случае, если arch_kmap_local_high_get()
вернёт NULL
, что вполне можно ожидать (она вовсе может быть не определена), __kmap_local_page_prot()
вызовет __kmap_local_pfn_prot(page_to_pfn(page), prot)
для отображения страницы.
И вот только в void *__kmap_local_pfn_prot(unsigned long pfn, pgprot_t prot)
из файла mm/highmem.c
произойдёт настоящая работа по отображению страницы. Сначала будут запрещены миграция и вытеснение, несмотря на то, что мы уже запретили как минимум одно из них. Приведу здесь код, он небольшой:
idx = arch_kmap_local_map_idx(kmap_local_idx_push(), pfn);
vaddr = __fix_to_virt(FIX_KMAP_BEGIN + idx);
kmap_pte = kmap_get_pte(vaddr, idx);
BUG_ON(!pte_none(*kmap_pte));
pteval = pfn_pte(pfn, prot);
arch_kmap_local_set_pte(&init_mm, vaddr, kmap_pte, pteval);
arch_kmap_local_post_map(vaddr, pteval);
current->kmap_ctrl.pteval[kmap_local_idx()] = pteval;
Итак, сначала вычисляется индекс страницы внутри буфера небольшого размера, который выделен специально под временное отображение памяти. По сути, вызов arch_kmap_local_map_idx()
можно записать следующим образом:
idx = current->kmap_ctrl.idx++ + KM_MAX_IDX * smp_processor_id();
Только надо понимать, что инкремент current->kmap_ctrl.idx
происходит внутри функции kmap_locl_idx_push()
, и в ней есть проверка BUG_ON(current->kmap_ctrl.idx >= KM_MAX_IDX)
, которая остановит работу ядра в случае, если индекс вылез за пределы выделенного буфера.
Кстати, в этой арифметике участвует также номер текущего CPU. Именно поэтому мы и отключаем миграцию между CPU.
Сам KM_MAX_IDX
довольно небольшой — позволяет отобразить 16 страниц. Однако, предполагается, что за один временной промежуток на конкретном CPU пользоваться временным отображением должен только один поток исполнения (например, обработчик аппаратного прерывания).
Далее берётся виртуальный адрес кадра памяти, который будет использоваться для отображения. Для этого делается отступ от начала буфера на полученный индекс страницы. Сам FIX_KMAP_BEGIN
и так является индексом страницы, определяется в перечислении enum fixed_addresses
для каждой архитектуры. Для arch/arm
это делается в файле arch/arm/include/asm/fixmap.h
. А вот макрос __fix_to_virt()
уже арифметически вычисляет виртуальный адрес из индекса путём вычитания относительного адреса кадра ((FIX_KMAP_BEGIN + idx) << PAGE_SHIFT
) из адреса последнего кадра (FIXADDR_TOP
). Конкретные адреса также определяются для архитектуры. Для arch/arm
они определены в файле arch/arm/include/asm/fixmap.h
:
#define FIXADDR_START 0xffc80000UL
#define FIXADDR_END 0xfff00000UL
#define FIXADDR_TOP (FIXADDR_END - PAGE_SIZE)
Далее берётся запись таблицы страниц (Page Table Entry
), соответствующая вычисленному кадру памяти. Если запись не пуста, то сработает макрос BUG_ON()
. Затем, вычисляется значение для этой записи, соответствующее запрошенной странице. На этом этапе всё готово для реальной записи нового адреса в таблицу страниц.
Следующие два вызова могут быть определены для архитектуры:
arch_kmap_local_set_pte(&init_mm, vaddr, kmap_pte, pteval);
arch_kmap_local_post_map(vaddr, pteval);
Для arch/arm
функция arch_kmap_local_set_pte()
не определена, поэтому будет использоваться функция set_pte_at()
, которая и запишет новое значение. А вот функция arch_kmap_local_post_map
определена: она очищает TLB на текущем CPU.
Всё, временное отображение прошло успешно, осталось только записать новое значение таблицы трансляций в current->kmap_ctrl.pteval[]
для учёта. После этого делается вызов preemt_enable()
, который разрешит вытеснение для PREEMPT_RT
ядер или просто вернёт вложенность к прежнему значению.
kunmap_atomic
Сам функция static inline void __kunmap_atomic(void *addr)
определена в файле include/linux/highmem-internal.h
. Первым делом она вызовет kunmap_local_indexed
, который сделает всю грязную работу. В самом начале этой функции нас встречает ветка кода, которая выполнится при следующем условии:
if (addr < __fix_to_virt(FIX_KMAP_END) ||
addr > __fix_to_virt(FIX_KMAP_BEGIN)) {
...
Может показаться, что она выполниться, если FIX_KMAP_BEGIN < addr < FIX_KMAP_END
, но, как мы помним, адрес кадра получается путём вычитания из адреса старшего кадра, то есть эта ветка отработает для адресов, которые не входят в буфер для временного отображения памяти. Зачем эта ветка кода? — для страниц памяти, которые мы позаимствовали у постоянного отображения с помощью функции kmap_high_get()
. В таком случае, нам нужно работать с ней как с остальными страницы постоянного отображения — через kunmap_high()
.
Для всех остальных страниц код зеркален коду __kmap_local_pfn_prot()
:
idx = arch_kmap_local_unmap_idx(kmap_local_idx(), addr);
WARN_ON_ONCE(addr != __fix_to_virt(FIX_KMAP_BEGIN + idx));
kmap_pte = kmap_get_pte(addr, idx);
arch_kmap_local_pre_unmap(addr);
pte_clear(&init_mm, addr, kmap_pte);
arch_kmap_local_post_unmap(addr);
current->kmap_ctrl.pteval[kmap_local_idx()] = __pte(0);
kmap_local_idx_pop();
Сначала из адреса вычисляется индекс страницы, которую мы использовали для отображения. Затем очищается запись таблицы страниц. Для arch/arm
в функции arch_kmap_local_pre_unmap()
сбрасывается кэш процессора для переданного адреса, а в функции arch_kmap_local_post_unmap()
для него же очищается TLB. Последним шагом идёт работа с учётом: очищается значение current->kmap_ctrl.pteval[]
и декрементируется current->kmap_ctrl.idx
.
После того, как страница была освобождена от отображения, __kunmap_atomic()
включит обработку исключений Page fault
и вытеснение задач (или миграцию задачи между CPU, если CONFIG_PREEMPT_RT
включен).