Управление памятью в Linux
Cистема управления памятью в Linux осуществляет подкачку страниц по
обращению в соответствии с COPY-ON-WRITE стратегией, основанной на
механизме подкачки, который поддерживается 386-м процессором. Процесс
получает свои таблицы страниц от родител я (при выполнении fork()) со
входами, помеченными как READ-ONLY или замещаемые. Затем, если процесс
пытается писать в эту область памяти, и страница является COPY-ON-WRITE
страницей, она копируется и помечается как READ-WRITE. Инструкция
exec() приводит к считыванию страницы или то же самое происходит при
выполнении программы. В дальнейшем процессу затруднительно получить
доступ к другой странице. Каждый процесс имеет директорию страниц, что
подразумевает возможность доступа к 1КВ таблиц страниц, указывающих на
1МВ 4-х килобайтных страниц, которые размещаются в 4GB памяти.
Директория страниц процесса инициализируется при выполнении
распараллеливания посредством copy_page_tables(). Директория страниц
холостого процесса инициализируется путем выполнения инициализирующей
последовательности.
Каждый пользовательский процесс имеет локальную таблицу дескриптора,
которая содержит сегмент кода и сегмент данные-стек. Сегментам
пользователя отводится память от 0 до 3GB(0xc0000000). В
пользовательском пространстве линейные адреса (см. сноску1) и
логические адреса (см.сноску2) идентичны.
--- сноска1. В процессоре 80386 значение линейного адреса лежит в
пределах 0GB - 4GB. Линейный адрес указывает на область памяти в этом
пространстве. Линейный адрес отличен от физического адреса, он является
виртуальным.
--- сноска 2. Логический адрес состоит из селектора и смещения.
Селектор указывает на сегмент, а смещение говорит о том, где размещена
область внутри сегмента.
Код ядра и сегменты данных являются привилегированными сегментами,
определяются в таблице глобального дескриптора и размещаются в области
от 3GB до 4GB. Установлена директория страниц программы подкачки
(swapper_pg_dir) и таким образом логические и физические адреса в
пространстве ядра являются идентичными.
Пространство выше 3GB появляется в директории страниц процесса как
указатели на таблицы страниц ядра. Эта область прозрачна для процессов
в режиме пользователя, однако, планирование распределения памяти
становится уместным при привилегированном режиме, например, при
выполнении системного вызова.
Режим супервизора вводится внутри контекста текущего процесса, так что
трансляция адреса происходит относительно каталога страниц процесса, но
используя сегменты ядра. Это идентично тому, как происходит управление
памятью с использованием swapper _pg_dir и сегменты ядра как и
директории страниц используют те же таблицы страниц в этом
пространстве.
Только task[0] (холостая задача (см.сноску 3) [Этот термин должен был
быть определен ранее] ) использует swapper_pg_dir напрямую.
---сноска 3. В силу исторических причин иногда называется задача подкачки, хотя при работе Linux с подкачкой она не связана.
- Для пользовательского процесса segment_base = 0x00, page_dir для процесса своя.
- процесс пользователя выполняет системный вызов: segment_base=0xc0000000 page_dir=та же самая page _dir пользователя
- swapper_pg_dir содержит распределение памяти для всех
физических страниц от 0xc0000000 до 0xc0000000 + end_mem, так что
первые 768 входов в swapper_pg_dir раны 0 и затем имеются 4 или более
входов, которые указывают на таблицы страниц ядра
- Директории страниц пользователя имеют те же входы, как tt
swapper_pg_dir свыше 768. Первые 768 входов представляют пространство
пользователя.
В результате всегда, когда линейный адрес превышает 0xc0000000, используются те же таблицы страниц ядра.
Пользовательский
стек размещается на вершине сегмента данных пользователя и
увеличивается вниз. Стек ядра не является точным представлением
структуры данных или сегмента, относительно которой я бы мог сказать
"Вы находитесь в стеке ядра". Kernel_stack_frame (страница) связывается
с каждым вновь создаваемым процессом и используется всякий раз, когда
ядро выполняет действия с контекстом процесса. Неприятности могут
произойти, если стек ядра будет расти ниже его текущего кадра.[Где
размещен стек ядра? Я знаю, что для каждого процесса существует свой
стек, но где он расположен, когда процесс не используется?]
Страницы пользователя могут заниматься или замещаться. Страница
пользователя отражается ниже 3GB в таблице страниц пользователя. Эта
область не содержит директорий страниц или таблиц страниц. Меняются
местами (замещаются) только грязные страницы.
Необходимы незначительные изменения в некоторых местах (например,
контроль ограничений на память процесса), чтобы обеспечить возможность
программисту определять свои сегменты.
Ядро ОС Linux :: 6.2 Физическая память
Ниже представлена карта физической памяти перед тем, как будет выполнен
любой процесс. Левый столбец представляет начальный адрес инструкции,
отмеченные значения являются приблизительными. Средний столбец включает
в себя название инструкции. Крайний правый столбец представляет имя
соответствующей процедуры или переменной или комментарий входа.
0x110000 свободна memory_end или high_memory
mem_map mem_init()
inode_table inode_init()
ннформ. устройства device_init()+
0x100000 добав. pg_tables paging_init()
0x0A0000 не используется
0x060000 свободна
low_memory_start
0x006000 код ядра + данные
floppy_track_buffer
bad_pg_table занято page_fault_handlers для
bad_page уничтожения процесса, если он
находится вне памяти.
0x002000 pg0 первая таблица страниц в ядре
0x001000 swapper_pg_dir каталог страниц ядра
0x000000 нулевая страница
+ устройство, захватывающее память (main.c): profil_buffer, con_init,
psaux_init, rd_init, scsi_dev_init. Заметьте, что не вся память
помечена как FREE или RESERVRVED (mem_init).
Страницы, помеченные как RESERVED принадлежат ядру и никогда не освобождаются или переставляются.
Ядро ОС Linux :: 6.3 Память пользовательского процесса
0xc0000000 невидимое ядро не используется
начальный стек
место для расширения стека 4 страницы
0x60000000 стабильно записанные биьлиотеки
brk не используется
распределенная память
end_data не инициализированные данные
end_code инициализированные данные
0x00000000 текст
Как сегмент кода, так и сегмент данных в каждом случае размещаются в
области от 0х00 - 3GB. Обычно программа контроля равильности
использования страниц do_wp_page проверяет, чтобы процесс не производил
запись в область кода. Однако, если перехватить SEGV сигнал, то
появляется возможность писать в область кода, инициируя возникновение
COPY-ON-WRITE. Программа управления do_no_page гарантирует, что ни одна
новая страница, выделенная процессу, не будет принадлежать ни
исполняемой области, ни разделяемой библиотеке, ни стеку, ни попадет
внутрь brk значения. Пользовательский процесс может сбросить свое brk
значение посредством вызова sbrk(). Это то, что делает malloc(), когда
это необходимо. Части текста и данных размещаются в отдельных
страницах, если не установлена опция -N компилятора. Адреса загрузки
разделяемой библиотеки обычно сами берутся из разделяемого
пространства. Такой адрес лежит между 1.5GB и 3GB за исключением особых
случаев.
своппируемая стабильная
страницы кода Y Y
страницы информации Y N?
стек Y N
pg_dir N N
кодовая/информационная
page_table N N
стек page_table N N
task_struct N N
kernel_stack_frame N N
shlib page_table N N
несколько shlib страниц Y Y?
[Что означают отметки в виде вопроса? Должны ли они означать вашу неуверенность или альтернативность решения?]
Стек, разделяемые библиотеки и данные слишком удалены друг от друга,
чтобы перекрываться одной таблицей страниц. Все page_tables ядра
разделяются всеми процессами и поэтому не приведены в списке.
Перемещаются только грязные страницы. Чистые страницы могут заниматься,
таким образом процесс может читать их снова из исполняемой области в
случае такой необходимости. Обычно разделяются только чистые страницы.
Грязная страница перестает разделяться в случае распараллеливания пока
родитель или потомок не станет снова производить в нее запись.
Ядро ОС Linux :: 6.4 Данные управления памятью в таблице процессов
Ниже приводится краткое описание некоторых данных, содержащихся в
таблице процессов, которые используются для управления памятью: [Это
должно быть документировано значительно лучше. Необходима значительно
большая детализация]
- Ограничения на память процесса: ulong start_code, end_code, end_data, brk, start_stack;
- Определение нарушения страницы: ulong min_flt, maj_flt, cmin_flt, cmaj_flt;
- Локальная таблица дескриптора: struct desc_struct ltd[32] представляет собой локальную таблицу дескриптора задачи.
- rss количество резидентных страниц.
- swappable: если - 0, тогда страницы процесса не замещаются.
- kernel_stack_page: указатель на страницу, размещенную при распараллеливании.
- saved_kernel_stack: V86 режим работы.
- struct tss
- Сегмент стека esp0 указатель на стек ядра (kernel_stack_page)
ss0 сегмент стека ядра (0х10)
esp1 = ss1 = esp2 = ss2 = 0
неиспользуемые привилегированные уровни.
- Секторы сегмента: ds = es = fs = gs = ss = 0x17, cs = 0x0f все указатели на сегменты в текущем ltd[].
- cr3: указывает на директорию страниц для данного процесса.
- ltd: _LTD(n) селектор для LTD текущей задачи.
Ядро ОС Linux :: 6.5 Инициализация памяти
В start_kernel (main.c) имеются 3 переменные, связанные с инициализацией памяти:
- memory_start начинается от 1MB. Изменяется посредством инициализации устройства.
- memory_end конец физической памяти: 8MB, 16MB и т.д.
- low_memory_start конец кода и данных ядра, загружаемых первоначально.
Каждое устройство при инициализации по-своему берет memory_stsrt и
возвращает измененное значение, если оно выделяет пространство начиная
с memory_stsrt (просто захватывая его). paging_init() инициализирует
таблицы страниц в swapper_pg_dir (начинающиеся с 0хс0000000), чтобы
накрыть всю физическую память начиная с memory_start и кончая
memory_end. В действительности первые 4МВ обрабатываются в startup_32
(head.s). memory_start увеличивается, если добавляется какая-либо новая
page_tables. При пребывании по обращению по пустому указателю в ядре
первая страница обнуляется.
В shed_init() ltd и tss дескрипторы задачи task[0] устанавливаются в
GDT, и загружаются в TR и DTR (единственный случай, когда это делается
явно). TRAP GATE (0х80) устанавливается для system_call(). Флаг
вложенной задачи сбрасывается при подготовке к переходу в
пользовательский режим. Таймер включается. task_struct для task[0] в
полном объеме появляется в .
Далее с помощью mem_init() создается mem_map, чтобы отражать текущее
использование физических страниц. Это состояние, которое отражается в
карте физической памяти, описанной в предыдущем разделе.
Linux переходит в пользовательский режим посредством iret после
сохранения в стеке ss, esp и т.п. Естественно, что сегменты
пользователя для task[0] управляются прямо через сегменты ядра, т.о.
выполнение продолжается точно с того места, где оно было прервано.
task[0]:
- pg_dir = swapper_pg_dir, что означает, что управление адресами происходит только в области от 3GB до 3GB + high_memory/
- LTD[1] = код пользователя, base=0xc0000000, size=640K
- LTD[2] = данные пользователя, base=0xc0000000, size=640K
Первый вызов exec() устанавливает входы LTD для task[1] в
пользовательские значения с base=0x0, limit= TASK_SIZE = 0xc0000000.
Согласно этому ни один процесс не видит сегменты ядра пока находится в
пользовательском режиме.
Ядро ОС Linux :: 6.5.1. Процессы и программа управления памятью
Работа, связанная с памятью, производимая посредством fork():
Выделение памяти
- 1 страница для task_struct.
- 1 страница для стека ядра.
- 1 для pg_dir и несколько для pg_tables (co[py_page_tables)
Другие изменения
- ss0 установить на адрес сегмента стека ядра (0х10) чтобы быть уверенным?
- esp0 установит на вершину вновь созданного kernel_stack_page
- cr3 устанавливается посредством copy_page_tables() для указания на вновь размещенную директорию страниц.
- ltd=_LTD(task_nr) создает новый дескриптор ltd.
- дескриптор устанавливается в gdt для нового tss и ltd[].
- Остальные регистры наследуются от родителя.
Процессы прекращают разделение их сегментов данных и кода (хотя они
имеют раздельные локальные таблицы дескрипторов, входы указывают на те
же сегменты). Страницы стека и данных будут скопированы, когда родитель
или наследник будет писать в них (СOPY-ON-WRITE)
Работа, связанная с памятью, производимая посредством exec():
- Выделение памяти
- 1 страница для exec-заголовка полного файла для omagic
- 1 страница или больше для стека (MAX_ARG_PAGES)
- clear_page_tables() используется для удаления старых страниц.
- change_ltd() устанавливает дескрипторы в новом LTD[]
- ltd[1] = code base = 0x00, limit = TASK_SIZE
- ltd[2] = data base = 0x00, limit = TASK_SIZE
Эти сегменты представляются DPL=3,P=1,S=1,G=1 type=a(code) или 2(data)
- До MAX_ARG_PAGES грязные страницы argv и envp размещаются и
сохраняются на вершине сегмента данных для вновь созданного
пользовательского стека.
- Установить указатель инструкции вызывающей программы eip = ex.a_entry.
- Установить указатель стека вызывающей программы на созданный
стек (esp = stack pointer) Данные будут выбраны из стека при
возобновлении вызывающей программы.
- Редактирование ограничений на память
end_coe = ex.a_text
end_data = end_code + ex.a_data
brk = end_data + ex.a_bss
Программные и аппаратные прерывания управляются внутри
контекста текущей задачи. Более детально, директория страниц текущего
процесса используется при трансляции адреса. Сегменты, однако, являются
сегментами ядра и таким образом все линейные адреса указывают в область
памяти ядра. Предположим, например, что пользовательский процесс
выполняет системный вызов и ядро хочет получить доступ к переменной по
адресу 0х01. Линейный адрес будет равен 0хс0000001 (использование
сегментов ядра) и физический адрес - 0х01. Буква здесь присутствует
вследствие того, что директория страниц процесса отображает этот
диапазон точно как page_pg_dir.
Область ядра (0хс0000000 + high_memory) отображается через таблицы
страниц ядра, которые являются частью RESURVED памяти. Поэтому они
разделяются всеми процессами. Во время распараллеливания
copy_page_tables() напрямую обращается к таблицам RESERVED страниц. Она
устанавливает указатели в директориях страниц процесса, чтобы указывать
на таблицы страниц ядра и не размещает в памяти новых таблиц страниц,
как это обычно делается. В качестве примера kernel_stack_page (который
размещен где-то в области ядра) не нуждается в связанной page_table,
размещенной в p_dir процесса, чтобы отобразить ее.
Инструкция прерывания устанавливает указатель стека и сегмент стека из
привилегированных 0 значений, сохраненных в tss текущей задачи.
Заметьте, что стек ядра в действительности является фрагментированным
объектом - это не единственный объект, а группа стековых фреймов,
каждый из которых размещается в памяти при создании процесса и
освобождает память при окончании его. Стек ядра никогда не должен расти
внутри контекста процесса слишком быстро, чтобы не расширится за
пределы текущего фрейма.
Ядро ОС Linux :: 6.6. Выделение и освобождение памяти: политика страничной организации
Когда любой процедуре ядра требуется память, она прекращает работу,
вызывая get_free_page(). Это представляет собой более низкий уровень,
чем kmalloc() (в действительности kmalloc() использует get_free_page(),
когда возникает необходимость в расширении памяти).
get_free_mem() использует один параметр, приоритет. Допустимыми
значениями являются GFP_BUFFER, GFP_KERNEL и GFP_ATOMIC. Она берет
страницу из free_page_list, редактирует mem_map, обнуляет страницу и
возвращает физический адрес страницы (заметте, что kmalloc() возвращает
физический адрес. Логика mm зависит от идентичности отображения между
логическими и физическими адресами).
Само по себе это достаточно просто. В действительности проблема состоит
в том, что free_pagr_list может быть пуст. Если вы не испытывали
потребности в использовании примитивных операций на этой стадии, вы
попадаете в область вопросов изъятия стр аниц, которую мы будем
тщательно рассматривать немного позже. Как крайнее средство (и это
касается примитивных операций) страница изымалась из
secondary_page_list (как вы могли догадаться, когда страницы
освобождаются, в первую очередь происходит заполнение
secondary_page_list).
В действительности манипуляции с page_lists и mem_map происходят в
результате действий этого загадочного макроса, называемого
REMOVE_FROM_MEM_QUEUE(), внутрь которого вы, возможно, никогда не
захотите заглянуть. Достаточно сказать, что прерывания запрещены.
Теперь немного назад, к вопросу изъятия страниц. get_free_page()
вызывает try_to_free_page, которая повторяет вызов shrink_buffers() и
swap_out() в данной последовательности до тех пор, пока освобождение
страниц не будет закончено успешно. Приоритет снижается при каждой
успешной итерации, так что эти две процедуры чаще выполняют свои циклы
по изъятию страниц.
Рассмотрим один проход swap_out():
- Пройти по таблице процесса и получить ответ Q от замещаемой задачи.
- Найти таблицу страницы пользователя (не RESERVED) в области, принадлежащей Q.
- Для каждой page в таблице выполнить try_to_swap_out(page).
- Выход, когда страница освобождена.
Отметим, что swap_out() (вызываемая из try_to_free_page()) поддерживает
статические переменные, таким образом можно получить ответ на вопрос
где она была освобождена при предыдущем вызове.
try_to_swap_out() сканирует таблицы страниц всех пользовательских процессов и проводит следующую политику изъятия:
- Не относиться легкомысленно к RESERVED страницам.
- Подвергать старению страницу, если она помечена как доступная (1 бит).
- Не трогать страницы, выделенные ранее (last_free_pages[]).
- Оставить грязные страницы с map_counts> 1.
- Снизить значение map_count для чистых страниц.
- Освободить чистые страницы, если для них не установлено соответствие.
- Заместить грязные страницы с map_count = 1.
Результатом действий 6 и 7 будет остановка процесса при текущем
освобождении физической страницы. Действие 5 вызывает потерю одним из
процессов чистой неразделяемой страницы, к которой ранее не было
доступа (снижение Q->rss), что вовсе неплохо, но действия накопления
за несколько циклов могут значительно замедлить процесс. В данном
случае происходит 6 итераций, таким образом страница, разделяемая
шестью процессами, может быть изъята, если она чистая.
Входы таблицы страниц изменяются и TLB становится
недействительной.[Последнее вызывает интерес. В этом, кажется, нет
необходимости т.к. доступные страницы не удаляются и многие таблицы
страниц могут быть просмотрены между итерациями.... возможно, в
результате прерывания, если такое произошло и имела место попытка
урезать наиболее раннюю страницу?]
Текущая работа по освобождению страницы выполняется free_page(),
являющегося дополнением get_free_page(). Она игнорирует RESERVED
страницы, редактирует mem_map, затем освобождает страницу и изменяет
page_lists, если для него не установлено соответствие. Для подкачки
(см. п.6 выше) write_swap_pege() принимает вызов, но не делает ничего
значительного с точки зрения дальнейшего управления памятью.
Рассмотрение деталей shrink_buffers() увело бы нас далеко в сторону. В
первую очередь она следит за свободными буферами, затем списывает
грязные буфера, далее занимается занятыми буферами и вызывает
free_page(), когда у нее появляется возможность освободить все буфера
на странице.
Заметим, что директории страниц и таблицы страниц наряду с RESERVED
страниц не становятся изменяемыми, не изымаются и не стареют. Они
отображаются в директории страниц процесса через зарезервированные
таблицы страниц. Они освобождаются только пр и выходе из процесса.
Ядро ОС Linux :: 6.7 Программы контроля корректности использования страниц
Когда процесс создается посредством распараллеливания, он стартует со
своей директорией страниц и своей страницей. Таким образом программа
контроля корректности использования страниц следит почти за всей
памятью процесса.
Программа контроля do_page_fault() считывает некорректный адрес из
регистра cr2. Код ошибки (считанный в sys_call.S) позволяет определить
режим доступа - пользователя/супервизора и причину ошибки - защита
записи или неправильная страница. Формирователь управляется
do_wp_page() и позднее do_no_page().
Если нарушение адреса превышает TASK_SIZE, процесс получает SIGKILL.
[Зачем этот контроль? Такое может произойти только в режиме ядра из-за
защиты на уровне сегмента]
Эти процедуры обладают тонкостями т.к. они могут вызываться по прерыванию. Вы не можете предположить, что это текущая задача.
do_no_page() контролирует три возможные ситуации:
- Страница замещается.
- Страница принадлежит исполняемой или разделяемой библиотеке.
- Страница некорректна - страница данных, которая не была загружена.
Во всех случаях в первую очередь вызывается get_empty_pgtable() чтобы
гарантировано определить существование таблицы страниц, которая
накрывает некорректный адрес. В случае 3 get_empty_page() вызывается
чтобы обеспечить страницу с требуемым адресом и в случае замещаемой
страницы вызывается swap_in().
В случае 2 программа контроля вызывает share_page(), чтобы посмотреть
является ли страница разделяемой каким либо другим процессом. В случае
неудачи она считывает страницу из исполняемой программы или библиотеки
(Она повторяет вызов shre_page() в случае, если другой процесс делал
тем временем то же самое). Любая часть страницы сверх значения brk
обнуляется.
Считывание страницы с диска вычисляется как основная ошибка (mjr_flt).
Это происходит с swap_in() или когда происходит считывание из
выполняемой программы или библиотеки. Другие случаи интерпретируются
как второстепенные ошибки (min_flt).
Когда найдена разделяемая страница, то она защищена для записи.
Процесс, который пишет в разделяемую страницу, затем должен будет
пройти через do_wp_page(), которая выполняет COPY-ON-WRIGHT.
do_wp_page() выполняет следующее:
- посылает SIGSEGV, если какой-либо пользовательский процесс пишет в текущую code_space.
- Если старая страница не разделяется, она становится
незащищенной. Иначе get_free_page() и copy_page(). Для страницы
устанавливается грязный флаг из старой страницы. Уменьшается значение
счетчика карты старой страницы.
Ядро ОС Linux :: 6.8. Листание (paging) Листание (paging)
оперирует со страницей в отличии от подкачки (swapping), которая
используется в отношении любых процессов. Мы будем использовать здесь
термин "подкачка" для того, чтобы ссылаться на листание, т.к. Linux
только листает и не замещает, но более привычным является термин
"замещать" (swap), чем "листать" (page). Страницы ядра никогда не
замещаются. Очищенные страницы также не читаются для замещения. Они
освобождаются и перезагружаются, когда требуется. Программа подкачки
поддержи вает один бит информации старения, являющимся битом
PAGE_ACCESSED во входах таблицы страниц.
Linux использует множество файлов подкачки или устройства, которые
могут быть включены и выключены посредством системных вызовов swapon и
swapoff. Каждый файл подкачки или устройство описано посредством struct
swap_info_struct (swap.c)
static struct swap_info_struct {
unsigned long flags;
struct inode *swap_file;
unsigned int swap_device;
unsigned char *swap_map;
char *swap_lockmap;
int lowest_bit;
int highest bit;
} swap_info[MAX_SWAPFILES];
Поле флагов (SWP_USED или SWP_WRITEOK) используется для управления
доступом к файлам подкачки. Когда флаг SWP_WRITEOK сброшен, для этого
файла не будет выделяться пространство. Это используется системным
вызовом swapoff, когда он делает невозможным использование файла. Когда
вызов swapon добавляет новый файл подкачки, то устанавливается
SWP_USED. Статическая переменная nr_swapfiles содержит количество
активных файлов подкачки. Поля lowest_bit и highest_bit связывают
свободные области в файле подкачки и используются, чтобы увеличить
скорость поиска свободной области подкачки.
Программа пользователя mkswap инициализирует устройство подкачки или
файл. Первая страница включает заголовок ("SWAP-SPACE") в последних 10
байтах и содержит область битмап. Первоначальные нулевые значения в
битмап сигнализируют о плохих страница х. "1" в битмап означает, что
соответствующая страница свободна. Такой странице память никогда не
выделяется, таким образом сразу необходимо провести инициализацию.
Системный вызов swapon() осуществляется из программы пользователя
обычно из /etc/rc. Пара страниц памяти выделяется для swap_map и
swap_lockmap.
swap_map содержит один байт на каждую страницу файла подкачки.
Инициализируется из битмап и содержит 0 для допустимых страниц и 128
для неиспользуемых страниц. Используется для поддержки счета запросов
на замещение каждой страницы в файле подкачки. swap_lockmap содержит
один бит на каждую страницу, который используется, чтобы гарантировать
взаимное исключение при чтении или записи в файл подкачки.
Когда страница памяти готова к тому, чтобы быть замещенной, индекс
области замещения получается путем вызова get_swap_page(). Этот индекс
затем загружается в биты 1-31 входа таблицы страниц, таким образом
замещаемая страница может быть размещена, когда это необходимо,
программой контроля корректности использования страниц do_no_page().
Верхние 7 битов индекса дают файл подкачки (или устройство), а нижние
24 бита дают номер страницы на этом устройстве. Это позволяет создавать
до 128 файлов, каждый из которых предоставляет область до 64GB, но
пространство свыше этого требует, что бы swap_map было бы большим.
Вместо этого размер файла подкачки ограничивается 16MB потому, что
swap_map тогда занимает 1 страницу.
Функция swap_duplicate() используется в copy_page_tables(), чтобы
разрешить дочернему процессу наследовать замещаемые страницы во время
распараллеливания. Это сразу увеличивает значение счетчика для этой
страницы, поддержанного в swap_map. Каждый процесс будет замещен в
отдельной копии страницы при обращении к ней.
swap_free() уменьшает счетчик, поддерживаемый в swap_map. Когда счетчик
сбрасывает в 0, страница может быть перезагружена с помощью
get_swap_page(). Эта функция вызывается каждый раз, когда замещаемая
страница читается в память (swap_in()) или когда страница готова к
тому, чтобы быть сброшенной (free_one_table() и т.п.).
Ядро ОС Linux :: 6.9 Управление памятью в 80386
Логический адрес, задаваемый в инструкции в первую очередь
транслируется в линейный адрес посредством аппаратных средств
сегментации. Этот линейный адрес затем транслируется в физический адрес
модулем страничной организации (paging unit).
Ядро ОС Linux :: 6.9.1 Страничная организация (paging) в 386
Существует два уровня косвенной адресации при трансляции адреса в
модуле страничной организации. Директория страниц содержит указатели на
1024 таблицы страниц. Каждая таблица страниц содержит указатели на 1024
страницы. Регистр CR3 содержит физический базовый адрес директории
страницы и загружается как часть TSS в task_struct и поэтому
загружается при каждом переключении задачи. 32-х битный линейный адрес
разделяется следующим образом:
31 22 21 12 11 0
DIR TABLE OFFSET
Физический адрес затем вычисляется (аппаратно) таким образом:
CR3+DIR указатель на table_base
table_base+TABLE указатель на page_base
physical_address= page_base + OFFSET
Директории страниц (таблицы страниц) это страница, выровненная так, что
нижние 12 бит используются для загрузки полезной информации о таблице
страниц (страница), указатель на которую задается посредством входа.
Формат входов директории страниц и таблицы страниц:
31 12 11 9 8 7 6 5 4 3 2 1 0
ADDRESS OS 0 0 D A 0 0 U/S R/W P
D - "1" означает, что страница грязная (неопределенно для входа директории страниц).
R/W - "0" означает для пользователя "только для чтения".
U/S - "1" означает страницу пользователя.
P - "1" означает, что страница находится в памяти.
А - "1" означает, что к странице был доступ (устанавливается в 0 при старении).
OS - биты могут использоваться для LRU и т.п. и определяются OS.
Соответствующие определения для Linux находятся в .
Когда
страница замещается, используются биты 1-31 входа таблицы страниц,
чтобы отметить, куда при замещении помещается страница (бит "0" должен
иметь значение 0).
Страничная организация (paging) делается доступной путем установки
старшего бита в CR0 [в head.S?]. В каждой фазе трансляции адреса
проверяются разрешения доступа, страницы не присутствуют в памяти и
нарушение защиты приводит к их отсутствию. Затем программа контроля
корректности использования страниц (в memory.c) или вносит новую
страницу, или снимает защиту страницы, или делает все необходимое, что
должно быть сделано.
Информация о некорректной работе со страницей
- Регистр CR2 содержит линейный адрес, в котором было вызвано нарушение страницы.
- Коды нарушения страницы (16 бит):
бит сброшен установлен
0 страница не существует защита уровня страницы
1 нарушение при чтении нарушение при записи
2 режим супервизора режим пользователя
Остальные биты не определены. Приведенная информация является выдержкой из sys_call.S
Translation Lookside Buffer (TLB) представляет собой аппаратную
кэш-память для физических адресов, которые соответствуют ранее
используемым виртуальным адресам. Когда транслируется виртуальный
адрес, 386 в первую очередь просматривает TLB, чтобы узнать - является
ли доступной необходимая информация. Если нет, то для того, чтобы
получить страницу, он должен создать пару ссылок на память для доступа
к директории страниц и затем таблице страниц. Три ссылки на физическую
память для трансляции адрес а для каждой ссылки на логическую память
убили бы систему и, следовательно, TLB.
TLB заполняется, если загружен CR3 или по переключению задач, в
результате которого изменяетсяCR0. В Linux она заполняется путем вызова
invalidate(), которая как раз и перезагружает CR3.
Ядро ОС Linux :: 6.9.2 Сегменты в 80386
Сегментные регистры используются при трансляции адреса для генерации линейного адреса из логического (виртуального) адреса.
linear_address = segment_base + logical_address
Линейный адрес транслируется затем в физический адрес посредством аппаратуры страничной организации (paging)
Каждый сегмент в системе описан 8-ми байтным дескриптором сегмента, в
котором содержится вся необходимая информация (база, ограничение, тип,
привилегии).
Имеют место следующие сегменты:
Обычные сегменты
сегменты кода и данных
Системные сегменты
(TSS) cегменты состояния задачи
(LDT) таблицы локальных дескрипторов
Характеристики системных сегментов:
- Системные сегменты являются спецификаторами задач
- Имеется TSS, связанный с каждой задачей в системе. Он содержит
tss_struct (sched.h). Размер этого сегмента соответствует размеру
tss_struct, исключая i387_union (232 байта). Он содержит всю
информацию, необходимую для перезапуска задачи.
- Таблицы LDT содержат дескрипторы обычных сегментов,
принадлежащих задаче. В Linux каждой задаче соответствует одна LDT. B
Linux task_struct предусмотрено пространство для 32-х дескрипторов.
Обычная LDT, созданная в Linux, имеет размер 24 байта и, следовательно,
пространство для 3-х входов. Ее содержимое:
- LDT[0] Null (принудительно)
- LDT[1] дескриптора сегмента пользовательского кода
- LDT[2] дескриптор сегмента пользовательских данных/стека
Все сегменты пользователя имеют базу 0х00, так что линейный адрес тот же самый, что и логический.
Для
получения доступа ко всем этим сегментам 386 использует таблицу
глобальных дескрипторов (GDT), которая устанавливается в памяти
системой (местоположение задается регистром GDT). GDT содержит
дескрипторы сегментов для каждого сегмента состоян ия задачи, каждого
локального дескриптора и обычных сегментов. Linux GDT содержит входы
двух обыкновенных сегментов:
- GDT[0] нулевой дескриптор
- GDT[1] дескриптор сегмента кода ядра
- GDT[2] дескриптор сегмента данных/стека ядра
Оставшаяся область GDT заполнена TSS и LDT дескрипторами системы.
- GDT[3] ???
- GDT[4] = TSS0, GDT[5] = LDT0
- GDT[6] = TSS1, GDT[7] = LDT1
..... и т.д......
Заметьте LDT[n] != LDTn
- LDT[n] = n-й дескриптор в LDT текущей задачи
- LDTn = дескриптор в GDT для LDT n-й задачи
В данном случае GDT имеет 256 входов, пространство для 126 задач.
Сегменты ядра имеют базу 0хс0000000, которая задает местонахождение
ядра в линейном представлении. Прежде, чем сегмент может быть
использован, содержимое дескриптора для этого сегмента должно быть
загружено в сегментный регистр. 386 имеет множество сложных критериев,
ограничивающих доступ к сегментам, так что вы не сможете просто
загрузить дескриптор в сегментный регистр. Также эти сегментные
регистры имеют невидимые для программиста участки. Видимые участки -
это то, что обычно называется сегментными регистрами cs, ds, es, fs, gs
и ss.
Программист загружает один из этих регистров 16-ти битным значением,
называемым селектором. Селектор однозначно идентифицирует дескриптор
сегмента в одной из таблиц. Доступ подтверждается и соответствующий
дескриптор загружается посредством аппар атных средств.
Обычно в Linux игнорируется комплексная защита на уровне сегмента
(чрезмерная?), предоставляемая 386. Она базируется на основе аппаратных
средств страничной организации и объединенной защитой на уровне
страницы. Правила на уровне сегмента, которые применяются к
пользовательским процессам, состоят в следующем:
- Процесс не может напрямую обращаться к данным ядра или сегментам кода.
- Всегда имеет место контроль ограничения, однако, условие, что
каждый пользовательский сегмент размещается от 0х00 до 0хс0000000,
неудобно для применения. [Это изменено и нуждается в редактировании]
Ядро ОС Linux :: 6.9.3 Селекторы в 80386 Селектор сегмента
загружается в сегментный регистр (cs, ds и т.д.), чтобы через
сегментный регистр задать один из обычных сегментов в системе как один
адрес.
Формат селектора сегмента:
15 3 2 1 0
индекс TI RPL
TI - индикатор таблицы:
0 означает, что индексы селектора относятся к GDT
1 означает, что индексы селектора относятся к LDT
RPL - привилегированный уровень. Linux использует только два
привилегированных уровня
0 означает ядро
1 означает пользователя
Примеры:
Сегмент кода ядра:
TI = 0, индекс = 1, RPL = 0 поэтому сектор = 0х08 (GDT[1])
Сегмент данных пользователя
TI = 1, индекс = 2, RPL = 3 поэтому сектор = 0х17 (LDT[2])
Cелекторы, используемые в Linux:
TI index RPL selector segment
0 1 0 0x08 код ядра GDT[1]
0 2 0 0x10 данные/стек ядра GDT[2]
0 3 0 ??? ??? GDT[3]
1 1 3 0x0F код пользователя LDT[1]
1 2 3 0x17 данные/стек пользователя LDT[2]
Селекторы для сегментов системы не предназначены для прямой загрузки в
сегментные регистры. Напротив, должны быть загружены TR или LDTR.
На входе системного вызова:
- ds и es устанавливаются на сегмент данных ядра (0х10)
- fs устанавливается на сегмент данных пользователя (0х17) и
используется для доступа к данным, на которые указывают аргументы
системного вызова
- Указатель на вершину и сегмент стека автоматически
устанавливаются в ss0 по прерыванию и старые значения восстанавливаются
при возврате из системного вызова.
Ядро ОС Linux :: 6.9.4 Дескрипторы сегментов Имеется
дескриптор сегмента, используемый для описания каждого сегмента в
системе. Имеются обычные и системные дескрипторы. Ниже представлен
дескриптор во всей своей красоте. Такой странный формат создан
специально для того, чтобы обеспечить совместимость с 286. Заметьте,
что он занимает 8 байт
63-54 55 54 53 52 51-48 47 46 45 44-40 39-16 15-0
Base G D R U Limit P DPL S TYPE Segmet Base Segmet Limit
31-24 19-16 23-0 15-0
Разъяснения:
R | зарезервирован (0) |
DPL | 0 означает ядро, 3 означает пользователя |
G | 1 означает гарантировано 4К (В Linux установлен всегда) |
D | 1 означает 32-х битное значение операнда по умолчанию |
U | определяется программистом |
P | 1 означает присутствие в физической памяти |
S | 0 означает системный сегмент, 1 означает обычный сегмент кода или данных |
TYPE | Существует много возможностей. Прерывания различны для системных и обычных дескрипторов. |
Системные дескрипторы в Linux
TSS: P=1,DPL=0,S=0, type=9, limit=231 пространство для 1 tss_struct
LDT: P=1,DPL=0,S=1, type=2, limit=23 пространство для 3
дескрипторов сегментов
База устанавливается при выполнении fork(). Для каждой задачи есть свой TSS и LDT.
Обычные дескрипторы ядра в Linux (head.S):
код: P=1,DPL=0,S=1,G=1,D=1, type=a, base=0xc0000000, limit=0x3ffff
данные: P=1,DPL=0,S=1,G=1,D=1, type=2, base=0xc0000000, limit=0x3ffff
Состав LDT для task[0] (sched.h):
код: P=1,DPL=3,S=1,G=1,D=1, type=a, base=0xc0000000, limit=0x9f
данные: P=1,DPL=3,S=1,G=1,D=1, type=a, base=0xc0000000, limit=0x9f
LDT, устанавливаемые по умолчанию для оставшихся задач (exec()):
код: P=1,DPL=3,S=1,G=1,D=1, type=a, base=0, limit=0xbffff
данные: P=1,DPL=3,S=1,G=1,D=1, type=2, base=0, limit=0xbffff
Размер сегментов ядра 0х40000 страниц (4КВ страниц с G=1=1GB) тип
подразумевает, что для кодового сегмента разрешается read-exec, а для
сегмента данных read-write.
Регистры, объединенные посредством сегментации.
Формат сегментного регистра: (для программиста видимым является только селектор)
16 -бит 32-бит 32-бит
селектор база физического адреса ограничения сегмента атрибуты
Невидимую часть сегментного регистра более удобно рассматривать в
формате, используемом во входах таблицы дескрипторов, которые
устанавливает программист. Таблицы дескрипторов имеют связанные с ними
регистры, которые используются при их размещении в памяти. GDTR (и
IDTR) инициализируются при запуске и вместе с этим определяются
таблицы. LDTR загружается при каждом переключении задачи.
Формат GDTR (иIDTR):
32-бит 16-бит
Линейный базовый адрес ограничение таблицы
TR и LDTR загружаются из GDT и таким образом имеют формат других
сегментных регистров. Регистр задач (TR) содержит дескриптор для TSS
текущей выполняемой задачи. Выполнение перехода на селектор TSS
вызывает сохранение состояния в старом TSS, в TR загружается новый
дескриптор и регистры перезагружаются новым TSS. Эти действия
реализуются программой-шедуллером при переключении задач пользователя.
Отметим, что поле tss_struct.ltd содержит селектор для LDT этой задачи.
Он используется для того, чтобы загрузить LDTR. (sched.h)
Ядро ОС Linux :: 6.9.5 Макросы, используемые при установке дескрипторов
В sched.h и system.h определены несколько ассемблерных макросов для
простого доступа и установки дескрипторов. Каждый вход TSS и LDT
занимает 8 байт.
Манипулирование GDT дескрипторами системы:
- _TSS(n)
_LDT(n) предоставляют индекс в GDT для n-ой задачи.
- _LDT(n) загружается в поле ldt структуры tss_struct при распараллеливании.
- _set_tssldt_desc(n, addr, limit, type)
ulong*n указывает на вход GDT для установки (см. fork.c).
База сегмента (TSS или LDT) устанавливается в 0хс0000000 + addr. Вот
специфичные экземпляры описанного выше, где ltypy ссылается на байт,
содержащий P, DPL, S, и тип:
set_ldt_desc(n,addr) ltype=0x82
P=1, DPL=0, S=0, type=2 означает вход LDT. limit=23=?
пространство для 3 дескрипторов сегмента
set_ldt_desc(n,addr) ltype=0x89
P=1, DPL=0, S=0, type=9 означает доступный 80386 TSS
limit=231 пространство для 1 tss_struct
- load_TR(n), load_ldt(n) загружает дескрипторы для задачи с номером n в регистр задачи и ldt регистр.
- ulong get_base(struct desk_struct ldt) берет базу из дескриптора
- ulong get_limit (ulong segment) берет ограничение (размер) из селектора сегмента. Возвращает размер сегмента в байтах.
- set_base(struct desk_struct ldt, ulong base), set_base(struct desk_struct ldt, ulong limit)
Установит базу и ограничения для дескрипторов (4К неделимых сегментов).
Ограничением здесь является действительно существующий размер сегмента
в байтах.
- _set_seg_desc(gate_addr, type, dpl, base, limit)
Значения по умолчанию 0х00408000 = ? D=1,P=1,G=0 В данный
момент рабочий размер 32 бита и максимальный размер 1М. gate_addr
должен быть (ulong*)
|