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