среда, 28 августа 2013 г.

PhantomEx: Эксперимент в отладчике - работа системных вызовов

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

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

PhantomEx: Системные вызовы

Что нельзя, то нельзя. Но если очень хочется - то можно...!

Народная мудрость


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

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

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

вторник, 27 августа 2013 г.

PhantomEx: Переход в пользовательский режим - лабораторная работа

В прошлой статье мы переключили нашу ОС в пользовательский режим. Рассмотрим теперь этот процесс подробнее.

Для этого выполним пошаговую отладку процесса переключения. Для этого необходимо настроить удаленную отладку ядра, например используя связку Eclipse CDT  + GDB + VMware. О том как настроить эту систему я рассказывал в одной из статей цикла.

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

 

1. Процесс переключения в режим пользователя


Запустим процесс отладки и поставим точку останова на функции init_user_mode(...). Дойдя до этой точки остановимся и посмотрим что мы имеем
Инициализация пользовательского стека

понедельник, 26 августа 2013 г.

PhantomEx: Переход в пользовательский режим - практика

Теперь попробуем перейти в этот самый режим пользователя с CPL = 3, а так же организовать переключение задач с различным уровнем привилегий.

Для этого нужно сделать следующие:
  1. Создать сегмент TSS и определить функции для работы с ним, загрузить TR селектором этого сегмента;
  2. Модифицировать планировщик для обновления поля ESP0 в TSS при переключении задач.
  3. Перейти в режим пользователя.
Реализуем этот план по порядку


PhantomEx: Переход в пользовательский режим - теория

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

Однако, настоящие ОС никогда не работают в режиме ядра, за исключением разве что однозадачных систем реального режима, где понятия уровня привилегий не существует (например MS DOS). Главная задача любой ОС - обеспечивать среду выполнения пользовательских программ, выполнять их запуск, взаимодействие с ресурсами компьютера и между собой, а так же корректное их завершение. Любая ОС нужна именно для того чтобы выполнять некую полезную работу. Ядро само по себе бессмысленно без пользовательского окружения.

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

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

воскресенье, 25 августа 2013 г.

PhantomEx: Корректный выход из потока. Функция завершения потока.

Эта заметка будет совсем короткой, и посвящена она ещё одному аспекту работы с потоками.

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

Листинг 97. Поток с конечным временем выполнения (main.c)

/*-----------------------------------------------------------------
//        Поток #1
//---------------------------------------------------------------*/

void task01(void)
{
    char tmp_str[256];

    vs01 = (vscreen_t*) get_vscreen();

    vs01->cur_x = 0;
    vs01->cur_y = start_y;

    dec2dec_str(count01, tmp_str);

    vprint_text(vs01, "I'm kernel thread #1: ");

    vs01->cur_x = 22;

    vprint_text(vs01, tmp_str);
   
    count01++;

    destroy_vscreen(vs01);
}

Теперь пересоберем ядро и загрузим нашу ОС.

PhantomEx: Примитивы синхронизации. Мьютексы.

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

Речь идет о том, что в каждом потоке, хоть и однократно, вызывается функция выделения памяти

/* Создаем виртуальный экран в потоке #1 */
vs01 = (vscreen_t*) get_vscreen();
.
.
.
/* Создаем виртуальный экран в потоке #2 */
vs02 = (vscreen_t*) get_vscreen();

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

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

PhantomEx: Вывод на экран из нескольких потоков. Реализация "виртуальных" экранов

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

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


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

Такая ситуация не редкость в многозадачных системах. Использование общего ресурса несколькими потоками приводит нас к необходимости решать проблемы синхронизации доступа к общим данным потоков.

Наша проблема может быть решена двумя способами:

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

PhantomEx: Обработка процессорных исключений

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

Ошибочные действия выполняемые ядром или прикладной программой, в общем случае не должны приводить к прекращению его работы - большинство таких исключительных ситуаций в той или иной степени могут быть разрешены соответствующим обработчиком. Кроме того, некоторые механизмы реализуемые операционной системой могут быть реализованы как раз через обработку соответствующих исключений CPU. Например, реализация подкачки страниц памяти с HDD, известный как "своппинг", может быть реализован только с использованием обработчика #PF - исключения страничной памяти.

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

четверг, 22 августа 2013 г.

PhantomEx: Модификация планировщика задач (и о вреде asm volatile)

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

Рэндел Хайд "Искусство программирования на языке Assembler"



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

А началось всё с того, что после реализации потоков ядра в рабочей версии PhantomEx я начал готовить почву для перехода в пользовательский режим (Ring 3). Об этом тоже будет целая статья, даже несколько статей - если учесть что в рабочей версии уже всё это реализовано ;). Пока скажу лишь о том, то при переключении задач разного уровня привелегий используется атавизм архитектуры x86 - сегмент состояния задачи TSS, о котором я уже упоминал несколько раз. В аппаратной реализации многозадачности 286/386 процессоров он предназначался для хранения контекста выполнения задачи, на каждую задачу полагался такой дескриптор. Он являет собой структуру, сегодня от него осталось одно значимое поле:  esp0 - указатель на вершину стека ядра, который модифицируется каждый раз при переключении задач, даже если оно происходит программно. Без этого невозможно переключение задач разных уровней привелегий. И вот на этой простой операции - записи значения в поле структуры мой планировщик подложил мне жирную свинью. Но обо всём по порядку

воскресенье, 18 августа 2013 г.

PhantomEx: Многозадачность. Потоки уровня ядра.

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

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

Наконец переключение потоков удалось реализовать и отладить, используя алгоритм, предложенный парнем с ником phantom-84 с форума http://osdev.ru. Алгоритм этот чрезвычайно прост и элегантен, а  phantom-84 выражается огромная благодарность за помощь в разъяснении его узких моментов.

Как всегда начнем с теории.

PhantomEx: Память типа "куча". Динамическое выделение памяти

Теперь мы вплотную подошли к ещё одному важному аспекту работы с памятью в ядре ОС - динамическое выделение участков памяти произвольного размера.

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

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


суббота, 17 августа 2013 г.

PhantomEx: Выделение и освобождение фреймов физической памяти

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

Для реализации данной задачи необходимо:
  1. Дать ядру представление о расположении и размерах блоков доступной физической памяти.
  2. Реализовать функции выделения и освобождения страниц физической памяти.
Существует несколько алгоритмов решения данных задач, в частности James Molloy предлагал алгоритм на основе заполнения битового поля. Мы реализуем  хранение информации о доступной физической памяти в виде двунаправленного кольцевого списка блоков свободной памяти.

PhantomEx: Карта распределения физической памяти, или снова Multiboot

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

В реальном режиме на этот вопрос отвечает функция BIOS INT 15h/E820h, однако для того чтобы воспользоваться ей, нам необходимо вернуться в реальный режим, а это, мягко говоря для нас неприемлемо.

К счастью на помощь нам приходит загрузчик GRUB2, который уже проделал за нас эту работу, сформировал карту памяти и может передать её нам через Multiboot-заголовок.


пятница, 16 августа 2013 г.

PhantomEx: Реализация страничной памяти - начало

Теперь можно непосредственно приступить к организации страничной адресации в нашем учебном ядре. Изначально я в этом вопросе опирался на руководства James Malloy, однако из-за глубоких противоречий и небрежности написания приведенного там кода от тупого следования этому мануалу пришлось отказаться и писать менеджер памяти самостоятельно. Именно поэтому цикл статей задержался почти на месяц.

Рассмотрение страничной адресации в нашем ядре начнем с создания схемы распределения ВАП, от которой будем отталкиваться при формировании информационных структур, ответственных за работу системы страничной адресации.


PhantomEx: Страничная организация памяти - теория

После продолжительной паузы настал момент вернуться к написанию цикла по разработке учебной операционной системы. При разработке основного (не учебного) варианта PhantomEx возник ряд трудностей, которые требовали вдумчивого разбора.
На сегодня, после реализации в ядре PhantomEx работы с потоками накопилась уйма практического материала для продолжения, и мы будем продолжать, рассматривая тему страничной организации памяти.
До сих пор наше учебное ядро использовало сегментную модель организации памяти в защищенном режиме, однако такая схема к настоящему времени безнадежно устарела - все современные ОС на 32-разрядной архитектуре x86 используют страничную модель памяти, не говоря уже о том что в архитектуре x86-64 от сегментной модели осталось несколько рудиментарных вещей, для обратной совместимости со старым ПО, а полной поддержки сегментной модели и аппаратной многозадачности там уже нет.

Тема организации страничной памяти достаточно обширна, так что она растянется на несколько статей. Начнем с теории.