Hardcore programmer
1.91K subscribers
9 photos
3 videos
22 links
Продвинутые темы из программирования и computer science. Особенности различных языков программирования. Глубокое погружение в software engineering.

Поддержать канал:
https://www.donationalerts.com/r/hardcore_programmer
Download Telegram
Channel created
Устройство виртуальной памяти процесса

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

Для начала давайте разберемся, что такое виртуальная память, зачем она нужна и почему мы не можем обращаться к реальной памяти напрямую.
На заре развития компьютеры исполняли лишь одну программу за раз. И даже первые операционные системы были однозадачными. Запущенная программа спокойно могла распоряжаться всеми доступными ресурсами, в том числе и оперативной памятью.
Предоставить такой программе доступ к реальной памяти не вызывало особых проблем.
Сегодня же подобными привилегиями могут похвастаться лишь программы исполняемые на голом железе, такие как ядра операционных систем и прошивки микроконтроллеров.

Если же мы имеем дело с программой исполняющейся под одной из современных операционных систем, таких как Linux, Windows, MacOS, FreeBSD и некоторых других, то мы оказываемся в многозадачной среде, в которой исполняются несколько программ параллельно.
В таких условиях при использовании реальной памяти появляется ряд проблем. Одна из них - это эффективное ограничение доступа к областям памяти принадлежащих другим процессам. Менее очевидная для разработчиков работавших только с высокоуровневыми языками проблема - мы не можем гарантировать, что исполняемый код нашей программы будет всегда загружен в оперативную память по одним и тем же адресам, что сильно затрудняет вызовы функций/методов нашей программы.
Наличие же виртуальной памяти, помимо решения вышеуказанных проблем позволяет делать еще несколько полезных трюков, вроде сброса неиспользуемой в данный момент памяти в файл/раздел подкачки или архивирование ее в ZRAM, а так же отображение виртуальной памяти в устройства отличные от оперативный памяти.

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

Мы рассмотрели самые азы работы памяти процесса, что такое виртуальная память, зачем она нужна и как работает. Однако, работа с памятью имеет огромное число нюансов, а их понимание важно для понимания того, как работают программы, которые мы пишем, ведь суть любой программы сводится к манипуляциям с данными. В последующих постах я расскажу о многих из таких нюансов более подробно.
👍50❤‍🔥5🔥32
Страничная организация памяти

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

Для начала посмотрим, как это работает.
Для каждого процесса ОС заводит несколько таблиц для хранения информации о страницах памяти. В одной таблице могут хранится только таблицы одинакового размера, а количество записей ограничено. Помимо реального адреса начала страницы, в каждой записи таблицы хранятся права доступа к данной странице, похожие на права файлов в unix - чтение, запись, выполнение, с той разницей, что ограничение на чтение обычно не имеет смысла и вместо него используется ограничение на доступ только из ядра ОС.
Несколько таблиц объединяются в каталоги, а каталоги могут объединяться в каталоги более высокого уровня. В современных 64-битных архитектурах может использоваться до 4 уровней вложенности.
Виртуальный адрес в такой модели состоит из двух частей: старшие биты указывают на индекс каталога/таблицы, а младшие - смещение в странице.

За счет фиксированного размера страниц в одной таблице количество младших бит отвечающих за смещение постоянно. При размещении страниц по реальным адресам, у которых данные биты будут нулями, нам не потребуется дополнительной арифметики со смещением от начала страницы, что и делает данный подход эффективным.
Помимо страничной организации памяти в некоторых ОС так же используется сегментная организация памяти, которая может быть более привлекательной за счет совпадения логических областей памяти с сегментами, но на деле оказывается менее эффективной, так как сегменты могут быть произвольного размера и часто требуют дополнительной арифметики при пересчете виртуальных адресов в реальные.
👍14🔥54❤‍🔥2
Области виртуальной памяти

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

Что это за области?
Я предполагаю, что большинство моих читателей хотя бы что-то слышали о таких понятиях как стек и куча. Это как раз 2 примера таких областей, однако ими дело не ограничивается.
Прежде чем мы разберемся, какие области бывают и для чего они предназначены, стоит прояснить несколько важных моментов:
Во-первых, все эти области являются всего лишь абстракциями. Это значит, что большинство из этих областей по сути не являются обязательными, но ими оперируют большинство языков программирования на уровне компиляторов и стандартных библиотек. Однако никто не мешает нам на самом низком уровне распоряжаться памятью так, как мы сочтем нужным. Хотя и здесь есть нюансы, например ОС может навязывать нам свои правила.
Во-вторых, не лишним будет знать, что виртуальная память обычно разделена на 2 пространства - пользовательское и пространство ядра. Пространство ядра как правило располагается в верхней половине адресов и недоступно из режима пользователя, в котором выполняются наши программы. Такое разделение очень важно для выполнения системных вызовов, о которых мы поговорим в будущих постах.
В третьих, некоторые области могут быть на деле разбросаны по адресному пространству, а так же присутствовать в нескольких экземплярах. То, что стек обычно рисуют сверху - не более чем грубое упрощение, таким упрощениям я посвящу следующий пост.

Перечислим часто встречающиеся области с описанием для чего они нужны:

stack - используется для локальных переменных функций, а так же для мета информации о вызовах, такой как адрес возврата. Каждый поток имеет свой собственный стек. Размер стека не изменяется в процессе выполнения и определяется ОС для главного потока и разработчиком для дочерних. Получил свое название в честь одноименной структуры данных, так как по сути представлен стеком кадров (stack frames), каждый из которых содержит информацию запущенной в текущей момент функции.

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

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

data - область для static и глобальных переменных инициализированных на старте программы. Начальное состояние загружается из файла с программой (elf/exe/mach o).

rodata - область для констант, похожа на предыдущую с той разницей, что сюда запрещена запись.

bss - область для неинициализированных static и глобальных переменных. От data отличается тем, что в файле с программой описывается лишь размер данных. В зависимости от ОС на старте программы может быть заполнена нулями или мусором.

env - область в которую ОС записывает переменные окружения и аргументы командной строки.
👍19❤‍🔥4🔥31