Подтвердить что ты не робот

Ядро операционной системы и процессы в основной памяти

Продолжая мои усилия по исследованию развития ОС, я сконструировал почти полную картину в моей голове. Меня все еще ускользает.

Вот основной процесс загрузки, исходя из моего понимания:

1) BIOS/Bootloader выполняет необходимые проверки, инициализирует все.

2) Ядро загружается в ОЗУ.

3) Ядро выполняет инициализацию и запускает задачи планирования.

4) Когда задача загружается, ему предоставляется виртуальное адресное пространство, в котором оно находится. Включая .text,.data,.bss, кучу и стек. Эта задача "поддерживает" свой собственный указатель стека, указывая на свой "виртуальный" стек.

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

В этой абстракции ядро ​​является "материнским" процессом, внутри которого размещаются все остальные процессы. Я попытался выразить свое лучшее понимание на следующей диаграмме:

enter image description here

Вопрос в том, правильно ли эта простая модель?

Во-вторых, как исполняемая программа узнала о своем виртуальном стеке? Это задание ОС для вычисления указателя виртуального стека и размещения его в соответствующем регистре CPU? Остальная учетная запись стека выполняется с помощью команды pop и push команды?

Является ли в самом ядре свой собственный основной стек и куча?

Спасибо.

4b9b3361

Ответ 1

Вопрос в том, правильно ли эта простая модель?

Ваша модель чрезвычайно упрощена, но по существу правильна - обратите внимание, что последние две части вашей модели на самом деле не считаются частью процесса загрузки, а ядро ​​не является процессом. Может быть полезно визуализировать его как одно, но оно не соответствует определению процесса и не ведет себя как один.

Во-вторых, как исполняемая программа узнала о своем виртуальном стеке? Это задание ОС для вычисления указателя виртуального стека и размещения его в соответствующем регистре CPU? Остаток бухгалтерии стека сделанные с помощью команды pop и push команды?

Исполняемая программа C не должна "знать о ее виртуальном стеке". Когда программа C скомпилирована в исполняемый файл, локальные переменные обычно ссылаются относительно указателя стека - например, [ebp - 4].

Когда Linux загружает новую программу для выполнения, она использует макрос start_thread (который вызывается из load_elf_binary) для инициализации регистров процессора. Макрос содержит следующую строку:

regs->esp = new_esp;   

который инициализирует регистр указателя стека процессора на виртуальный адрес, назначенный ОС стеку потоков.

Как вы сказали, после загрузки указателя стека команды сборки, такие как pop и push, изменят свое значение. Операционная система отвечает за обеспечение наличия физических страниц, соответствующих адресам виртуальных стеков, - в программах, использующих много стековой памяти, количество физических страниц будет возрастать по мере продолжения выполнения программы. Для каждого процесса существует предел, который можно найти с помощью команды ulimit -a (на моей машине максимальный размер стека составляет 8 МБ или 2 КБ страниц).

Является ли в самом ядре свой собственный основной стек и куча?

Здесь визуализация ядра как процесса может запутаться. Прежде всего, потоки в Linux имеют пользовательский стек и стек ядра. Они по существу одинаковы, отличаются только защитой и местоположением (стек ядра используется при выполнении в режиме ядра и стек пользователя при выполнении в пользовательском режиме).

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

При запуске системы функция start_kernel инициализирует поток ядра init, который затем создаст другие потоки ядра и начнет инициализацию пользовательских программ. Поэтому после запуска системы указатель стека процессора будет инициализирован, чтобы указать на стек ядра init.

Что касается кучи, вы можете динамически распределять память в ядре с помощью kmalloc, которая попытается найти свободную страницу в памяти - ее внутренняя реализация использует get_zeroed_page.

Ответ 2

Вы забыли один важный момент: Виртуальная память реализована аппаратно, обычно известная как MMU (модуль управления памятью). Это MMU, который преобразует виртуальные адреса в физические адреса.

Ядро обычно загружает адрес базы таблицы страниц для конкретного процесса в регистр в MMU. Это то, что задает - переключает пространство виртуальной памяти из одного процесса в другой. На x86 этот регистр CR3.

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

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

Обратите внимание, что, хотя ядро ​​может иметь собственные потоки, которые полностью исполняются в пространстве ядра, ядро ​​не должно действительно считаться "материнским процессом", который выполняется независимо от ваших программ пользовательского режима. Ядро в основном является "другой половиной" вашей программы пользовательского режима! Всякий раз, когда вы выдаете системный вызов , CPU автоматически переходит в режим ядра и начинает выполнение в заранее определенном месте, продиктованном ядром. Обработчик вызова системного ядра затем выполняется от вашего имени, в контексте режима ядра вашего процесса. Время, затрачиваемое на обработку обработчика вашего запроса, учитывается и "заряжается" вашим процессом.

Ответ 3

Полезные способы мышления о ядре в контексте отношений с процессами и потоками

Модель, предоставленная вами, очень упрощена, но правильна в целом. В то же время образ мышления о ядре как о "материнском процессе" не самый лучший, но он все еще имеет некоторый смысл. Я хотел бы предложить еще две лучшие модели.

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

    а. Ядро - это "библиотека" , которая разделяется каждым процессом в системе.

    б. Ядро - это "библиотека" , которая разделяет не только раздел кода, но и раздел данных.

    с. Ядро - специально защищенная "библиотека" . Ваш процесс не может напрямую обращаться к коду ядра и данным. Вместо этого он вынужден управлять ядрами с помощью специальных "ворот вызова".

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

  • Еще один полезный способ думать об организации вычислений на вашем компьютере - это рассматривать его как сеть "виртуальных" компьютеров, которая не поддерживает виртуальную память. Вы можете рассматривать процесс как виртуальный многопроцессорный компьютер, который выполняет только одну программу, которая имеет доступ ко всей памяти. В этой модели каждый "виртуальный" процессор будет представлен потоком исполнения. Как и у вас есть компьютер с несколькими процессорами (или с многоядерным процессором), вы можете иметь несколько текущих потоков в вашем процессе. Как и на вашем компьютере, все процессоры имеют общий доступ к пулу физической памяти, и все потоки вашего процесса имеют доступ к одному и тому же виртуальному адресному пространству. И как отдельные компьютеры физически изолированы друг от друга, ваши процессы также изолированы друг от друга, но логически. В этой модели ядро ​​представлено сервером, имеющим прямые подключения к каждому компьютеру в сети со звездообразной топологией. Подобно сетевым серверам, ядро ​​имеет две основные цели:

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

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

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

Как выполняется управление стеком и какая роль играет ядро ​​в этом процессе

Когда начинается новый процесс, ядро, используя подсказки от исполняемого изображения, решает, где и сколько виртуального адресного пространства зарезервировано для стека пользовательского режима начальной нити процесса. Получив это решение, ядро ​​устанавливает начальные значения для набора регистров процессора, которые будут использоваться основным потоком процесса сразу после начала выполнения. Эта настройка включает настройку начального значения указателя стека. После фактического начала выполнения процесса сам процесс становится ответственным за указатель стека. Более интересным фактом является то, что процесс отвечает за инициализацию указателей стека каждого созданного им нового потока. Но обратите внимание, что ядро ​​ядра отвечает за распределение и управление стеком режима ядра для каждого потока в системе. Обратите также внимание на то, что ядро ​​доступно для распределения физической памяти для стека и обычно выполняет эту работу лениво по требованию, используя ошибки страницы в качестве подсказок. Указатель стека бегущей нити управляется самой нитью. В большинстве случаев управление указателем стека выполняется компилятором при создании исполняемого изображения. Компилятор обычно отслеживает значение указателя стека и поддерживает его согласованность, добавляя и отслеживая все инструкции, относящиеся к стеку. Такие инструкции не ограничиваются только "push" и "pop". Существует множество инструкций процессора, которые влияют на стек, например "call" и "ret", "sub ESP" и "add ESP" и т.д. Как вы можете видеть, фактическая политика управления указателем стека в основном статична и известна до выполнения процесса. Иногда программы имеют особую часть логики, которая выполняет специальное управление стеками. Например, реализация сопротитов или длинных прыжков в C. Фактически, вы можете делать все, что хотите, указателем стека в своей программе, если хотите.

Архитектуры стека ядра

Я знаю о трех подходах к этой проблеме:

  • Отдельный стек ядра на поток в системе. Это подход, применяемый большинством известных ОС на основе монолитного ядра, включая Windows, Linux, Unix, MacOS. В то время как этот подход приводит к значительным накладным расходам в памяти и ухудшает использование кеша, но он улучшает защиту ядра, что имеет решающее значение для монолитных ядер с долговременными системными вызовами, особенно в многопроцессорной среде. На самом деле, давным-давно, у Linux был только один общий стек ядра, и все ядро ​​было покрыто блокировкой Big Kernel Lock, которая ограничивает количество потоков, которые могут одновременно выполнять системный вызов только одним потоком. Но разработчики ядра Linux быстро поняли, что блокирование выполнения одного процесса, который хочет знать, например, его PID, потому что другой процесс уже начал отправлять большой пакет через очень медленную сеть, полностью неэффективен.

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

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

Спасибо.