Как ОС обычно управляет памятью ядра и обработкой страниц?

Я работаю над дизайном ядра, и у меня есть вопросы по разбиению на страницы.

Основная идея, которой я придерживался до сих пор, такова: каждая программа получает свой собственный (или так думает) 4G памяти, за вычетом какого-то раздела, который я резервирую для функций ядра, которые программа может вызывать. Итак, ОС должна придумать способ загрузки страниц в память, которые программа должна использовать во время своей работы.

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

Итак, я предполагаю, что мой вопрос в том, как ОС вообще справляется с этим? Моя первоначальная мысль - создать функцию, которую программа вызывает для установки / освобождения страниц, которыми она затем может управлять памятью самостоятельно, но делает ли это программа обычно, или компилятор предполагает, что у нее есть свобода действий? Кроме того, как компилятор обрабатывает ситуации, когда ему необходимо выделить довольно большой сегмент памяти? Нужно ли мне предоставлять функцию, которая пытается упорядочить X страниц?

Это, очевидно, вопрос не для конкретного языка, но я неравнодушен к стандарту C и хорошо разбираюсь в C ++, поэтому я хотел бы, чтобы любые примеры кода были либо в этом, либо в сборке. (В сборке нет необходимости, я полностью намерен заставить ее работать с максимально возможным количеством C-кода и оптимизировать в качестве последнего шага.)

Еще одна вещь, на которую также должно быть проще ответить: как обычно обрабатываются функции ядра, которые программа должна вызывать? Можно ли просто иметь заданную область памяти (я думал о конце виртуального пространства), которая содержит самые основные функции / память для конкретных процессов, которые программа может вызывать? Моя мысль оттуда заключалась в том, чтобы функции ядра делали что-то очень необычное и меняли страницы (чтобы программы не могли видеть важные функции ядра в своем собственном пространстве), когда программам нужно было делать что-то важное, но я не совсем сосредоточив внимание на безопасности на этом этапе.

Так что я думаю, что меня больше беспокоят общие идеи дизайна, чем детали. Я хотел бы сделать ядро ​​полностью совместимым с GCC (каким-то образом), и мне нужно убедиться, что оно предоставляет все, что может понадобиться обычной программе.

Спасибо за любой совет.


person Nicholas Flynt    schedule 07.11.2008    source источник


Ответы (2)


Хорошей отправной точкой для всех этих вопросов является изучение того, как это делает Unix. Как гласит известная цитата: «Те, кто не понимает UNIX, обречены изобретать ее заново, но плохо».

Во-первых, о вызове функций ядра. Недостаточно просто иметь функции там, где программа может вызывать, поскольку программа, скорее всего, работает в «пользовательском режиме» (кольцо 3 на IA-32), а ядро ​​должно работать в «режиме ядра» (обычно кольцо 0 на IA-32) для выполнения своих привилегированных операций. Вы должны каким-то образом осуществить переход между обоими режимами, и это очень зависит от архитектуры.

На IA-32 традиционным способом является использование шлюза в IDT вместе с программным прерыванием (Linux использует int 0x80). У новых процессоров есть другие (более быстрые) способы сделать это, и какие из них доступны, зависит от того, какой процессор от AMD или Intel, и от конкретной модели процессора. Чтобы приспособиться к этому варианту, последние ядра Linux используют страницу кода, отображаемую ядром в верхней части адресного пространства для каждого процесса. Итак, в последних версиях Linux для выполнения системного вызова вы вызываете функцию на этой странице, которая, в свою очередь, будет делать все необходимое для переключения в режим ядра (ядро имеет более одной копии этой страницы и выбирает, какую копию использовать. при загрузке в зависимости от характеристик вашего процессора).

Теперь об управлении памятью. Это огромная тема; вы могли бы написать об этом большую книгу и не исчерпывать эту тему.

Обязательно имейте в виду, что существует как минимум два представления памяти: физический вид (реальный порядок страниц, видимый для подсистемы памяти оборудования и часто для внешних периферийных устройств) и логический. просмотр (порядок страниц, просматриваемых программами, запущенными на ЦП). Их довольно легко спутать. Вы будете выделять физические страницы и назначать им логические адреса в адресном пространстве программы или ядра. Одна физическая страница может иметь несколько логических адресов и может отображаться на разные логические адреса в разных процессах.

Память ядра (зарезервированная для ядра) обычно отображается в верхней части адресного пространства каждого процесса. Однако он настроен так, что к нему можно получить доступ только в режиме ядра. Нет необходимости в причудливых уловках, чтобы скрыть эту часть памяти; оборудование выполняет всю работу по блокированию доступа (в IA-32 это делается с помощью флагов страниц или ограничений сегмента).

Программы распределяют память в остальной части адресного пространства несколькими способами:

  • Часть памяти выделяется загрузчиком программ ядра. Это включает в себя программный код (или «текст»), инициализированные данные программы («данные»), неинициализированные данные программы («bss», заполненные нулями), стек и несколько лишних деталей. Сколько выделять, где, каким должно быть исходное содержимое, какие флаги защиты использовать и многое другое, считываются из заголовков исполняемого файла, который будет загружен.
  • Традиционно в Unix существует область памяти, которая может увеличиваться и уменьшаться (ее верхний предел можно изменить с помощью системного вызова brk()). Это традиционно используется кучей (распределитель памяти в библиотеке C, в которой malloc() является одним из интерфейсов, отвечает за кучу).
  • Часто можно попросить ядро ​​сопоставить файл с областью адресного пространства. Чтение и запись в эту область (через магию разбиения на страницы) направляются в резервный файл. Обычно это называется mmap(). С анонимным mmap вы можете выделять новые области адресного пространства, которые не поддерживаются никаким файлом, но в остальном действовать таким же образом. Загрузчик программ ядра часто использует mmap для выделения частей программного кода (например, программный код может поддерживаться самим исполняемым файлом).

Доступ к областям адресного пространства, которые никоим образом не выделены (или зарезервированы для ядра), считается ошибкой, а в Unix вызовет отправку сигнала программе.

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

Лучший способ изучить основы всего этого - прочитать одну из нескольких книг по операционным системам, в частности тех, которые используют вариант Unix в качестве примера. Он будет более подробным, чем я мог бы ответить на StackOverflow.

person CesarB    schedule 07.11.2008
comment
О ... так что в основном язык C предполагает, что его куча представляет собой одно длинное адресное пространство, и будет управлять этой памятью для себя, и все, что нужно сделать ядру, - это изменить размер этой памяти? На самом деле это звучит довольно просто и позволяет легко измерить память, потребляемую программой. - person Nicholas Flynt; 12.11.2008
comment
Не язык C, а традиционная реализация его стандартной библиотеки. Стандарт C также допускает более сложные реализации; то, как стандартная библиотека управляет своей памятью, в основном является деталью реализации, что касается стандарта C. - person CesarB; 12.11.2008

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

В x86 есть два способа реализации системных вызовов: с прерываниями и с помощью инструкций sysenter / sysexit. С помощью прерываний ядро ​​устанавливает таблицу дескрипторов прерываний (IDT), которая определяет возможные точки входа в ядро. Затем пользовательский код может использовать инструкцию int для генерации мягкого прерывания для вызова ядра. Прерывания также могут быть сгенерированы аппаратно (так называемые аппаратные прерывания); эти прерывания обычно должны отличаться от мягких прерываний, но это не обязательно.

Инструкции sysenter и sysexit - это более быстрый способ выполнения системных вызовов, поскольку обработка прерываний выполняется медленно; Я не очень знаком с их использованием, поэтому не могу комментировать, подходят ли они для вашей ситуации.

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

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

Я настоятельно рекомендую вам ознакомиться с некоторыми материалами из курса Операционные системы MIT . Загляните в раздел ссылок, там много хороших материалов.

person Adam Rosenfield    schedule 07.11.2008