Как насчет множества адресов, не представленных в файле /proc/$pid/maps?

#linux #paging #virtual-memory

#linux #подкачка #виртуальная память

Вопрос:

Краткая версия: в каком состоянии находятся адреса, не представленные в файле maps? Принадлежат ли они нераспределенным виртуальным страницам или выделены из анонимного файла или других?

Подробная версия, которую я изучаю о виртуальной машине. В моей книге (CS: APP) я узнал, что все виртуальные страницы можно разделить на три набора: нераспределенные, выделенные, но не кэшированные, выделенные и кэшированные.У меня есть несколько вопросов о том, «что такое выделенные страницы и нераспределенные страницы? Когда выделяются страницы? » А также, принадлежат ли стек и куча выделенным страницам или нераспределенным или выделяются только при использовании? Пытаясь решить эти проблемы, я прочитал файл /proc/ $pid/maps, хотя, думаю, я могу получить из него все, что захочу. На мой взгляд, файл содержит все отношения сопоставления памяти. Но нет информации о том, кэшируется ли он (я знаю, может быть, его нельзя увидеть из пользовательского режима …), И являются ли нераспределенные страницы нераспределенными?

Комментарии:

1. Я не уверен в терминологии вашей книги, но области адресов, не перечисленные в файле maps , просто не отображаются. Эти страницы помечены как «отсутствуют» в таблицах страниц. Если процесс должен получить доступ к такой странице, произойдет ошибка страницы; ядро не будет обращаться к какой-либо физической памяти или выделять ее в ответ, а просто передаст сигнал SIGSEGV, обычно завершающий процесс (т. Е. segfault).

Ответ №1:

Честно говоря, я действительно не знаю о файле maps. Что я знаю, так это то, что информация на каждой странице постоянно хранится в page структурах. Я собираюсь взять x86-64 в качестве примера.

Для x86-64 в Linux у вас есть глобальный каталог страниц (PGD), Верхний каталог страниц (PUD), средний каталог страниц (PMD) и каталог страниц (PD). Адрес нижней части таблицы PGD хранится в регистре CR3. PGD содержит адреса PUD, PUD содержат адреса PMD, PMD содержат адреса PDS, а PDS содержат адреса физических страниц.

Виртуальный адрес, из которого используется только 48 бит, разбивается на 5 частей. 12 младших значащих битов — это смещение на физической странице. Следующий фрагмент из 9 бит — это смещение в PD. Следующий фрагмент — это смещение в PMD и т. Д. Например, предположим, что у вас есть виртуальный адрес 0x0000000000000123 . Этот виртуальный адрес будет преобразован MMU в CPU путем просмотра записи (смещения) 0 PGD, записи 0 PUD, записи 0 PMD, записи 0 PD и, наконец, смещения 0x123 на фактической физической странице в ОЗУ. Каждый виртуальный адрес имеет 64 бита, из которых будут использоваться только 48 младших значащих битов.

При загрузке ядро проверяет, сколько памяти доступно. Затем он соответствующим образом строит свои структуры ядра.

При загрузке ядра оно помечает все страницы как нераспределенные в своих собственных структурах (за исключением потребностей ядра). Для page этого важна структура. Ядро имеет page структуру C для каждой страницы в системе (https://linux-kernel-labs.github.io/refs/heads/master/labs/memory_mapping.html и https://elixir.bootlin.com/linux/v4.6/source/include/linux/mm_types.h ). Эта структура информирует ядро о том, выделена страница или нет.

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

Сначала страницы в основном нераспределены. Когда вы запускаете новый процесс, запуская исполняемый файл от имени пользователя системы, для вашего процесса выделяются страницы. В Linux исполняемые файлы — это файлы ELF. ELF — это обычный формат, который разделяет код на загружаемые сегменты. Каждый сегмент получает виртуальный адрес, по которому он будет загружен в виртуальное адресное пространство.

Допустим, у вас есть файл elf с одним сегментом, который должен быть загружен по виртуальному адресу 0x400000. Когда вы запускаете этот исполняемый файл ELF, ядро Linux вызовет определенные функции, которые будут смотреть на размер кода и соответствующим образом распределять страницы. Ядро посмотрит на свои структуры и с помощью алгоритмов определит, где процесс будет размещен в оперативной памяти. Затем он настроит таблицы страниц в соответствии с тем, где виртуальные адреса для этого процесса должны находиться в реальной физической памяти.

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

Ядро содержит task_struct для каждого процесса, запущенного в системе. task_struct содержит поле с именем pgd, которое является указателем на PGD процесса. У каждого процесса есть свой собственный PGD. Если вы разыменовываете указатель на PGD, вы получаете фактическое значение первой записи PGD. Эта первая запись — адрес PUD. С помощью этого единственного указателя ядро может получить доступ к каждой таблице, принадлежащей процессу, и изменять их по своему усмотрению.

Во время выполнения процесса он может запросить больше памяти. Это называется динамическим распределением памяти. У ядра нет возможности заранее узнать, сколько памяти будет запрашивать процесс, поскольку он динамический (выполняется во время выполнения кода). Когда процесс запрашивает больше памяти, ядро определяет, какую страницу предоставить процессу, в зависимости от алгоритма. Затем он помечает эту страницу как выделенную для этого процесса. task_struct содержит поле mm, которое имеет тип mm_struct (https://manybutfinite.com/post/how-the-kernel-manages-your-memory /). Это дескриптор памяти для этого процесса, чтобы ядро могло знать, какую память использует процесс. Сам по себе процесс не нуждается в этой информации, поскольку процесс должен полагаться только на себя, чтобы правильно запрашивать память у операционной системы и не прыгать куда-то в ОЗУ, где ему не место.

Вы спрашиваете о куче и стеке. Стек для процесса выделяется в начале процесса, и я думаю, что он имеет фиксированный размер. Если вы переполните стек, вы выдадите исключение CPU, которое побудит ядро завершить ваш процесс. Каждое ядро процессора имеет специальный регистр, называемый RSP. Это указатель стека. Он указывает на вершину стека (стек растет вниз по направлению к нехватке памяти). Когда ядро выделяет стек для процесса, который вы запускаете, оно настроит этот регистр так, чтобы он указывал на его начало. Указатель стека содержит виртуальный адрес. Таким образом, он будет переведен с использованием таблиц страниц, как и любой адрес.

Куча выделяется и полностью управляется ОС. У него нет специальных регистров, таких как стек. Он выделяется только тогда, когда процесс запрашивает больше памяти во время выполнения кода. Ядро заранее знает, сколько памяти требуется процессу. Все это записано в исполняемом файле ELF. Вся статическая память выделяется во время компиляции, и, таким образом, ядро знает все о размере статической памяти. Единственный момент, который требуется для выделения новой памяти процессу, — это когда процесс действительно запрашивает ее. В C вы используете ключевое new слово для динамического запроса памяти кучи. Если вы не используете это ключевое слово, то ядро заранее знает, где будут размещены ваши переменные (где они будут находиться в памяти). Статическая память будет использовать только стек.