Таблица страниц в пространстве ядра Linux во время загрузки

Я чувствую смущение в управлении таблицами страниц в ядре Linux?

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

  • В это время, после включения таблицы страниц, пространство ядра по-прежнему составляет 1 ГБ (от 0xC0000000 до 0xFFFFFFFF)?

  • И в таблицах страниц процесса ядра отображаются только записи в таблице страниц (PTE) в диапазоне от 0xC0000000 до 0xFFFFFFFF?. PTE из этого диапазона не будут отображаться, потому что код ядра никогда не прыгает туда?

  • Сопоставление адреса до и после включения таблицы страниц одинаково?

    Например. перед включением таблицы страниц виртуальный адрес 0xC00000FF отображается на физический адрес 0x000000FF, а затем после включения таблицы страниц отображение выше не изменяется. виртуальный адрес 0xC00000FF по-прежнему отображается на физический адрес 0x000000FF. Другое дело, что после включения таблицы страниц CPU обращается к таблице страниц, чтобы перевести виртуальный адрес на физический адрес, который не нужно делать раньше.

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

  • Этот механизм одинаковый для x86 32bit и ARM?

Ответ 1

Следующее обсуждение основано на 32-битном ARM Linux, а версия исходного кода ядра - 3,9
Все ваши вопросы можно решить, если вы пройдете процедуру настройки начальной таблицы страниц (которая будет перезаписана позже функцией paging_init) и включением MMU.

Когда ядро ​​запускается сначала загрузчиком, функция Assembly stext (в arch\arm\kernel\head.s) является первой выполняемой функцией. Обратите внимание, что MMU еще не включен в данный момент.

Помимо прочего, двумя заданиями импорта, выполняемыми этой функцией stext, являются:

  • создайте начальную страницу (которая будет перезаписана позже функция paging_init)
  • включить MMU
  • перейти к части кода инициализации ядра и продолжить работу

Прежде чем вникать в ваши вопросы, знать:

  • Прежде чем включить MMU, каждый адрес, выданный процессором, физический адрес
  • После включения MMU каждый адрес, выданный CPU, виртуальный адрес
  • Перед включением MMU следует настроить правильную таблицу страниц, иначе ваш код просто "сбрасывается".
  • По соглашению, ядро ​​Linux использует более высокую часть виртуального адреса на 1 ГБ, а пользовательская земля использует более низкую часть 3 ГБ

Теперь сложная часть:
Первый трюк: использование независимого от позиции кода. Сетчатая функция stext связана с адресом "PAGE_OFFSET + TEXT_OFFSET" (0xCxxxxxxx), который является виртуальным адресом, однако, поскольку MMU еще не включен, фактический адрес, где работает stext-функция сборки, - "PHYS_OFFSET + TEXT_OFFSET" ( фактическое значение зависит от вашего фактического оборудования), который является физическим адресом.

Итак, вот что: программа функции stext "думает", что она работает в адресе, таком как 0xCxxxxxxx, но фактически работает в адресе (0x00000000 + some_offeset) (скажем, ваше оборудование настраивает 0x00000000 в качестве отправной точки ОЗУ). Поэтому, прежде чем включать MMU, необходимо тщательно написать код сборки, чтобы убедиться, что во время процедуры выполнения ничего не получается. На самом деле используется техник, называемый позиционно-независимым кодом (PIC).

Чтобы пояснить выше, я извлекаю несколько фрагментов кода сборки:

ldr r13, =__mmap_switched    @ address to jump to after MMU has been enabled

b   __enable_mmu             @ jump to function "__enable_mmu" to turn on MMU

Обратите внимание, что приведенная выше инструкция "ldr" представляет собой псевдо-инструкцию, которая означает "получить (виртуальный) адрес функции __mmap_switched и поместить его в r13"

И функция __enable_mmu в свою очередь вызывает функцию __turn_mmu_on: (Обратите внимание, что я удалил несколько инструкций из функции __turn_mmu_on, которые являются важными инструкциями функции, но не наших интересов)

ENTRY(__turn_mmu_on)
    mcr p15, 0, r0, c1, c0, 0       @ write control reg to enable MMU====> This is where MMU is turned on, after this instruction, every address issued by CPU is "virtual address" which will be translated by MMU
    mov r3, r13   @ r13 stores the (virtual) address to jump to after MMU has been enabled, which is (0xC0000000 + some_offset)
    mov pc, r3    @ a long jump
ENDPROC(__turn_mmu_on)

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

  • Первое отображение, как и ожидалось, отображает диапазон адресов 0x00000000 (опять же, этот адрес зависит от конфигурации оборудования) через (0x00000000 + смещение) до 0xCxxxxxxx через (0xCxxxxxxx + смещение)
  • Второе отображение, интересно, отображает диапазон адресов 0x00000000 через (0x00000000 + смещение) к себе (то есть: 0x00000000 → (0x00000000 + смещение))

Зачем это делать? Помните, что до включения MMU каждый адрес, выданный CPU, является физическим адресом (начиная с 0x00000000) и после включения MMU каждый адрес, выданный CPU, является виртуальным адресом (начиная с 0xC0000000).
Поскольку ARM является структурой конвейера, на момент включения MMU в ARM-канале все еще есть инструкции, которые используют (физические) адреса, которые генерируются процессором до включения MMU! Чтобы избежать этих инструкций, чтобы взорваться, необходимо настроить идентичное сопоставление, чтобы удовлетворить их.

Теперь вернемся к вашим вопросам:

  • В это время, после включения таблицы страниц, пространство ядра по-прежнему составляет 1 ГБ (от 0xC0000000 до 0xFFFFFFFF)?

A: Наверное, вы имеете в виду включение MMU. Ответ: да, пространство ядра составляет 1 ГБ (на самом деле он также занимает несколько мегабайт ниже 0xC0000000, но это нас не интересует)

  1. И в таблицах страниц процесса ядра отображаются только записи таблицы страниц (PTE) в диапазоне от 0xC0000000 - 0xFFFFFFFF?. ПТ отсутствуют этого диапазона не будет отображаться, потому что код ядра никогда не прыгнет туда?

A: Хотя ответ на этот вопрос довольно сложный, поскольку он включает в себя множество подробностей относительно конкретных конфигураций ядра.
 Чтобы полностью ответить на этот вопрос, вам нужно прочитать часть исходного кода ядра, которая устанавливает начальную таблицу страниц (функция сборки __create_page_tables) и функцию, которая устанавливает итоговую таблицу страниц (функция C paging_init).
Проще говоря, в ARM есть два уровня таблицы страниц, первая таблица страниц - PGD, которая занимает 16 КБ. Ядро сначала возвращает этот PGD во время процесса инициализации и выполняет начальное отображение в функции сборки __create_page_tables. В функции __create_page_tables отображается только малая часть адресного пространства.
 После этого окончательная таблица страниц настраивается в функции paging_init, и в этой функции отображается большая часть адресного пространства. Скажем, если у вас только 512M RAM, для большинства распространенных конфигураций эта 512M-RAM будет отображать по разделу кода ядра по разделам (1 секция - 1 МБ). Если ваша оперативная память достаточно велика (скажем, 2 ГБ), будет отображаться только одна часть вашей ОЗУ. (Я остановлюсь здесь, потому что слишком много деталей относительно Вопроса 2)

  1. Сопоставление адреса до и после включения таблицы страниц одинаково?

A: Я думаю, что я уже ответил на этот вопрос в своем объяснении "Второй трюк: идентичное сопоставление при настройке начальной таблицы страниц перед включением MMU".

4. Таблица страниц в пространстве ядра является глобальной и будет весь процесс в системе, включая пользовательский процесс?

A: Да и нет. Да, потому что все процессы имеют одну и ту же копию (содержимое) таблицы страниц ядра (более высокая часть 1 ГБ). Нет, потому что каждый процесс использует свою собственную память 16 КБ для хранения таблицы страниц ядра (хотя содержимое таблицы страниц для более высокой части 1 ГБ идентично для каждого процесса).

5. Этот механизм одинаковый для x86 32bit и ARM?

В разных архитектурах используется другой механизм

Ответ 2

Когда Linux включает MMU, требуется только, чтобы виртуальный адрес пространства ядра был сопоставлен. Это происходит очень в начале загрузки. На данный момент нет пользовательского пространства. Нет никаких ограничений на то, что MMU может сопоставлять несколько виртуальных адресов с одним и тем же физическим адресом. Таким образом, при включении MMU проще всего сопоставить virt==phys для кодового пространства ядра и отображения link==phys или 0xC0000000.

  • Сопоставление адреса до и после включения таблицы страниц одинаково?

Если физический кодовый адрес Oxff, а конечный адрес ссылки 0xc00000FF, то при включении MMU мы будем иметь дублирующее отображение. И 0xff, и 0xc00000ff отображаются на одну и ту же физическую страницу. Простой jmp (jump) или b (branch) будет перемещаться из одного адресного пространства в другое. На этом этапе сопоставление virt==phys может быть удалено по мере выполнения на конечном целевом адресе.

Я думаю, что приведенное выше должно отвечать на пункты с 1 по 3. В принципе, таблицы страниц загрузки не являются окончательными таблицами страниц.

4. Таблица страниц в пространстве ядра является глобальной и будет использоваться для всех процессов в системе, включая пользовательский процесс?

Да, это большая победа с кешем VIVT и по многим другим причинам.

5. Этот механизм одинаковый для x86 32bit и ARM?

Конечно, основные механики разные. Они различны даже для разных процессоров в этих семьях; 486 против P4 против Amd-K6; ARM926 против Cortex-A5 против Cortex-A8 и т.д. Однако семантика очень похожа.

Смотрите: [email protected] - статья о фазе ранней Linux-памяти.

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