Как ОС обычно управляет памятью ядра и обработкой страниц? - PullRequest
6 голосов
/ 07 ноября 2008

Я работаю над дизайном ядра, и у меня есть несколько вопросов, касающихся подкачки.

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

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

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

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

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

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

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

Ответы [ 2 ]

12 голосов
/ 07 ноября 2008

Хорошая отправная точка для всех этих вопросов - посмотреть, как это делает 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.

6 голосов
/ 07 ноября 2008

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

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

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

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

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

Я настоятельно рекомендую вам взглянуть на некоторые материалы из класса MIT Operating Systems . Проверьте раздел ссылок, там много хороших вещей.

...