Написание ОС: Многозадачность

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

Что же, вы скорее всего не можете представить себе однозадачную ОС в 2018 году, по этому я решил поговорить о реализации многозадачности в моей ОС. И так, первое — вам надо определиться с типом многозадачности, я выбрал вытесняющую.
Что она из себя представляет? Вытесняющая многозадачность представляет собой систему распределения вычислительной мощности процессора между процессами: у каждого есть свой квант времени, у кажого есть свой приоритет. И первая проблема — какой квант по длине выбрать, как останавливать выполнение процесса по истечению кванта? На самом деле всё легко как никогда! Мы будем использовать PIT с изначально выставленной частотой в 10026 с копейками прерываний в секунду, тут же мы решаем еще одну проблему: мы уже останавливаем предыдущий процесс. И так, начнем с PIT’а.

PIT


PIT — Programmable Interval Timer — счетчик, который по достижению какого-либо запрограммированного количества инкрементов выдаёт сигнал. Так же при помощи этого таймера можно пищать пищалкой в компьютере (той штукой, что пищит после прохождения теста устройств). И так, он считает с частотой 1193182 герц, это значит, что нам надо запрограммировать его на 119(1193182/119 примерно равно 10026). Для этого надо в порт первого генератора отправить 2 байта, сначала младший байт, а потом старший:

 unsigned short hz = 119;
        outportb(0x43, 0x34);
        outportb(0x40, (unsigned char)hz & 0xFF); //Low
        outportb(0x40, (unsigned char)(hz >> 8) & 0xFF); //Hight, about 10026 times per second

Теперь стоит приступить к программированию прерывания от PIT, оно имеет IRQ 0, и после ремапа PIC’а будет 0×20 м. Для IRQ первого PIC’а я написал вот такой макрос:

//PIC#0; port 0x20
#define IRQ_HANDLER(func) char func = 0x90;\
__asm__(#func ": \npusha \n call __"#func " \n movb $0x20,\
 %al \n outb %al, $0x20 \n popa  \n iret \n");\
void _## func()

Структура и процессы


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


typedef struct _pralloc
{
        void * addr;
        struct _pralloc * next;
} processAlloc;
typedef struct
{
        void * entry;
        processAlloc *allocs;
} ELF_Process;
typedef struct __attribute__((packed)) _E {
        unsigned int eax;//4
        unsigned int ebx;//8
        unsigned int ecx;//12
        unsigned int edx;//16
        unsigned int ebp;//20
        unsigned int esp;//24
        unsigned int esi;//28
        unsigned int edi;//32
        unsigned int eflags;//36
        unsigned int state;//40
        void * startAddr;//44
        void * currentAddr;//48
        void * stack;//52
        unsigned int sse[4 * 8];//
        unsigned int mmx[2 * 8];//244
        unsigned int priority;//248
        unsigned int priorityL;//252
        void * elf_process;//256
        char ** argv;//260
        unsigned int argc;//264
        unsigned int runnedFrom;//268
        char * workingDir;//272
        unsigned int cs;//276 - pop is 4 byte in IRET
        unsigned int ds;//280
} Process;

Для начала, нам надо понять следующее: мы можем где-нибудь по глобальному адресу, к примеру, по 0xDEAD положить номер текущего запущенного процесса, тогда при выполнении любого кода мы можем быть уверены: у нас есть номер текущего запущенного процесса, это значит, что при обращении к malloc мы знаем, кому выделяем память, и сразу можем добавить адрес выделенной памяти в список allocs.

void addProcessAlloc(ELF_Process * p, void * addr)
{
        void * z = p->allocs;
        p->allocs = malloc_wo_adding_to_process(sizeof(processAlloc));
        p->allocs->addr = addr;
        p->allocs->next = z;
}

Что же, структуру таблицы с описанием процессов мы написали, что дальше, как переключать задачи?
Для начала хочу заметить, что к примеру, в обработчике локальные переменные хранятся в стеке, а значит после входа в обработчик компилятор портит нам esp. Чтобы такого не произошло создадим переменную с абсолютным адресом, и перед вызовом обработчика будем засовывать ESP туда. В обработчике нам необходимо отослать EOI первому PIC’у и найти процесс, на который нам надо переключиться (не буду описывать механизм приоритетов: он прост, как пробка). Далее — нам надо сохранить все регистры и флаги текущего процесса, по этому сразу перед засовыванием ESP в переменную сохраним все регистры (в том числе сегментные) в стек. В самом обработчике нам очень аккуратно надо их вынуть из стека, так же сохранив флаги и адрес возврата. Хочу заметить, что стек растет вверх (т.е. ESP уменьшается), это значит, что последний регистр, который вы сохранили в стек будет лежать по адресу ESP, предпоследний — ESP +4 и т.п:
image
Теперь нам остаётся засунуть в регистры значения регистров процесса, на который мы переключились и выполнить IRET. Profit!

Запуск процессов


При запуске процесса нам достаточно выделить стек для процесса, после чего положить в него argc и argv, адрес функции, которая будет отдано управление после завершения процесса. Так же надо установить флаги процессора в нужное вам значение, к примеру, для моей ОС это 0×216, про регистр флагов можно прочитать на википедии.

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

© Habrahabr.ru