Часть 1. Платформа
Часть 2. Архитектура и библиотека ядра
Часть 3. Ключевые подсистемы ядра
Архитектура L4Xpresso
Теперь настало время перейти собственно к разработке ядра. Разберемся сначала с его архитектурой, Архитектурной основой моей ОС выступило микроядро 2-го поколения L4. Оно привлекло интересными особенностями дизайна, и к тому же я с ним работал в ходе своей бакалаврского диплома, и написать свою реализацию L4 было бы интересно. За основу я взял спецификацию L4 Version X.2 Reference Manual
Архитектура микроядра L4Xpresso (кликабельно).
Верхний уровень представляют из себя интерфейсы ядра: первый состоит из 13 системных вызовов, второй интерфейс - KIP (Kernel Information page) - служит для получения информации из ядра без собственно совершения системного вызова - соответсвующая область памяти всегда отображена в адресное пространство процесса. KIP описан в файле kernel/include/l4/kip.h Средний уровень представлен основными подсистемами ядра: в первую очередь менеджерами виртуальной памяти и процессов (задач). На нижнем уровне же располагаются библиотека ядра, слой аппаратных абстракций, отладчик ядра и код инициализации и загрузки системы. Разберем нижний уровень микроядра L4Xpresso подробнее.
Слой аппаратных абстракций (HAL) и библиотека ядра
Слой аппаратных абстракций основан на библиотеке CMSIS, поставляемой вместе с средой LPCXpresso, и использует:
-
Функцию SystemInit(), предназначенную для инициализации контроллера перед запуском L4Xpresso - аналогичную роль играет BIOS в IBM PC
-
Интерфейс для работы с системным таймером SysTick
-
Драйвер UART3, используемый в качестве интерфейса для логов и отладки ядра
Платформо-зависимый код расположен в директориях kernel/src/platform и kernel/src/lpc. Кроме CMSIS к нему относятся:
-
Микрооперации: атомарные операции и спин-блокировки (они зарезервированы, а также операции test-and-set для слова и бита.
-
Код для работы с прерываниями
-
Код для управлением Memory Protection Unit
Хотя в поставке LPCXpresso две реализации стандартной библиотеки C: Redlib и Newlib, я не стал использовать ни одну из них, т.к. при разработке микроядра большая часть их функционала не потребовалась. С другой стороны, некоторые вещи все же пришлось написать. Во-первых это код для работы с очередями (FIFO) - оно необходимо для того, чтобы сделать вывод в отладочный UART буферизованным. Интерфейс fifo довольно простой:
struct fifo_t {
uint8_t* q_data; /*!< Указатель на первый байт очереди*/
uint32_t q_top; /*!< Голова очереди*/
uint32_t q_end; /*!< Конец очереди*/
size_t q_size; /*!< Размер очереди*/
};
/*Инициализирует очередь, данные которой располаются по адресу addr (выделяется отдельно)*/
uint32_t fifo_init(struct fifo_t* queue, uint8_t* addr, size_t size);
/*Помещает и извлекает элемент из очереди*/
uint32_t fifo_push(struct fifo_t* queue, uint8_t el);
uint32_t fifo_pop(struct fifo_t* queue, uint8_t* el);
/*Возвращает состояние очереди переполнена - FIFO_OVERFLOW, пуста - FIFO_EMPTY или FIFO_OK*/
uint32_t fifo_state(struct fifo_t* queue);
/*Возвращает длину очереди*/
uint32_t fifo_length(struct fifo_t* queue);
Во-вторых это код работы с битовыми массивами. В микроконтроллерах семейства Cortex-M3 есть специальный Bit-Band region, каждому биту которого соответствует отдельный адрес, что позволяет значительно упростить обход всех битов. Таким образом, модуль bitmap содержит две реализации битовых массивов, но с единым интерфейсом. Для обхода битового массива используется итератор типа bitmap_cursor_t.
#define DECLARE_BITMAP(name, size)
/*Инициализатор для курсора, где bitmap - битовый массив, а bit - номер бита*/
#define bitmap_cursor(bitmap, bit)
/*Возвращает текущую позицию курсора*/
#define bitmap_cursor_id(cursor)
/*Передвигает курсор на позицию вперед*/
#define bitmap_cursor_goto_next(cursor)
void bitmap_set_bit(bitmap_cursor_t cursor);
void bitmap_clear_bit(bitmap_cursor_t cursor);
int bitmap_get_bit(bitmap_cursor_t cursor);
int bitmap_test_and_set_bit(bitmap_cursor_t cursor);
Основное применение битовых массивов bitmap - в модуле ktable, который отвечает за выделение ядром памяти. Упомянутые библиотеки Newlib и Redlib содержат функцию malloc, но к сожалению, ее использование не всегда оптимально. В частности, такой подход приемлим, когда мы обладаем имеем доступ ко всей оперативной памяти, однако для ядра без MMU это не так - мы должны уже при компоновке разделить память на выделенную ядру и пользовательским процессам. Внутри же выделенной ядру памяти лишь часть требует динамического распределения: это разного рода таблицы: процессов, адресных пространств и страниц. Для них была придумана концепция SLAB-ового аллокатора, аналогично ему (но несколько упрощенно) написан и модуль ktable.
struct ktable
{
char* tname;
bitmap_ptr_t bitmap;
ptr_t data;
size_t num;
size_t size;
};
typedef struct ktable ktable_t;
/*Создает таблицу размером в num_ элементов типа type, с именем name*/
#define DECLARE_KTABLE(type, name, num_)
/*Инициализирует таблицу kt*/
void ktable_init(ktable_t* kt);
/*Возвращает 1 если элемент с номером i выделен, 0 - если нет и -1 в случае выхода за пределы таблицы*/
int ktable_is_allocated(ktable_t* kt, int i);
/*Выделяет элемент таблицы по номеру или первый свободный*/
void* ktable_alloc_id(ktable_t* kt, int i);
void* ktable_alloc(ktable_t* kt);
/*Освобождает элемент*/
void ktable_free(ktable_t* kt, void* element);
/*Возвращает номер элемента по его адресу*/
uint32_t ktable_getid(ktable_t* kt, void* element);
/*Обходит последовательно все выделенные элементы таблицы*/
#define for_each_in_ktable(el, idx, kt)
Еще одна важная функция ядра - контроль ошибок, а учитывая повышенные требования к надежности ОСРВ - этой функции надо уделить максимум внимания. Ошибки можно разделить на следующие группы:
-
Нефатальные ошибки пользовательского потока, которые происходят при невозможности обработать системный вызов. В этом случае необходимо установить код ошибки в UTCB. За нее отвечает пара функций set_user_error, set_caller_error
-
Фатальные ошибки пользовательского потока (например обращение к недопустимому адресу). Такие ситуации должны завершаться перезагрузкой потока. В данный момент не реализовано.
-
Фатальные ошибки ядра (так называемая паника). Она связана с ситуациями, выходящими за рамки нормального поведения ядра, например передача NULL-указателя там, где его не должно быть. Такая ситуация может также возникать на этапе разработки и контролироваться т.н. утверждениями (assertions), например:
fpage_t* fpage = (fpage_t*) ktable_alloc(&fpage_table);
assert(fpage != NULL);
В случае если свободные слоты в fpage_table закончились, ktable_alloc вернет NULL, сработает утверждение и произойдет паника системы. Код обработчиков ошибок находится в файле kernel/src/error.c
Отладчик ядра
И наконец, отладчик ядра. Хотя в отличие от традиционных систем, в которых отладка внешними средствами затруднена и требуется полная реализация отладчика внутри ядра (kmdb в Solaris, kgdb в Linux), в случае L4Xpresso это можно выполнить средствами среды LPCXpresso. Однако такие вещи, как например состояние управляющих структур ядра лучше реализовывать отдельно. Для этого в L4Xpresso и предусмотрен отладчик. Он срабатывает на передаваемый по UART3 (этот же интерфейс используется и для отладочной печати символ и печатает в удобном виде текущее состояние системы, например список текущих потоков, как показано на скриншоте.
Пример отладочной печати и работы отладчика KDB (кликабельно)
Основной код отладчика находится в файле kernel/src/kdb.c, а внешние обработчики в соответствующих исходниках. Само меню вызывается по знаку вопроса.
Исходники проекта располагаются на github: https://github.com/myaut/l4xpresso