Отправляет email-рассылки с помощью сервиса Sendsay
  Все выпуски  

Пишем свою операционную систему. Улучшение обработки IRQ прерываний и многозадачности


Давненько не писал новых выпусков... пришло время это исправить.

Сегодня мы сильно изменим структуру обработки IRQ-прерываний. Раньше, каждое прерывание описывалось отдельно. Теперь мы сведём обработку всех прерываний в одну функцию. Если сейчас выигрыш от этого не очевиден, то потом он будет заметнее. Ведь в конечном счёте ядро должно при возникновении IRQ-прерывания отправить сообщение программе-драйверу (у нас же микроядро). И меняться в этом сообщении будет только номер прерывания. Не будем же мы писать 16 разных функций, различающихся лишь 1 цифрой?

 Для начала создадим файл interrupts.asm, который будет содержать кое-какие полезные для нас описания, которые нельзя написать на Си ввиду их низкоуровневости.

format ELF public irq_handlers extrn irq_handler section ".text" executable macro IRQ_handler index { IRQ # index # _handler: push eax mov eax, index - 1 jmp common_irq_handler } rept 16 i { IRQ_handler i } ; Обработчик всех IRQ прерываний common_irq_handler: push ebx ecx edx esi edi ebp push ds es fs gs mov ecx, 16 mov ds, cx mov es, cx mov fs, cx mov gs, cx mov edx, esp push edx push eax call irq_handler add esp, 2 * 4 pop gs fs es ds pop ebp edi esi edx ecx ebx mov al, 0x20 out 0x20, al out 0xA0, al pop eax iretd section ".data" writable ; Таблица обработчиков IRQ прерываний irq_handlers: rept 16 i { dd IRQ # i # _handler } 

С помощью макроса создаётся обработчик для каждого из 16 IRQ-прерываний (быстро программно узнать какой номер у текущего прерывания, насколько мне известно, способа нет), который очень простой - запихнуть в стек регистр EAX, поместить в EAX номер прерывания, перейти на основной обработчик (этот код занимает всего 10 байт для одного IRQ прерывания, или 160 байт для всех прерываний, так что можете не беспокоится, что ядро слишком растолстеет).

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

Теперь модифицируем interrupts.c:

#include "stdlib.h"
#include "memory_manager.h"
#include "interrupts.h"
#include "multitasking.h"
#include "tty.h"

void (*irq_handlers[])();
void irq_handler(uint32 index, Registers *regs);

typedef struct {
	uint16 address_0_15;
	uint16 selector;
	uint8 reserved;
	uint8 type;
	uint16 address_16_31;
} __attribute__((packed)) IntDesc;

typedef struct {
	uint16 limit;
	void *base;
} __attribute__((packed)) IDTR;

IntDesc *idt;

void init_interrupts() {
	idt = alloc_virt_pages(&kernel_address_space, NULL, -1, 1, PAGE_PRESENT | PAGE_WRITABLE | PAGE_GLOBAL);
	memset(idt, 0, 256 * sizeof(IntDesc));
	volatile IDTR idtr = {256 * sizeof(IntDesc), idt};
	asm("lidt (,%0,)"::"a"(&idtr));
	irq_base = 0x20;
	irq_count = 16;
	outportb(0x20, 0x11);
	outportb(0x21, irq_base);
	outportb(0x21, 4);
	outportb(0x21, 1);
	outportb(0xA0, 0x11);
	outportb(0xA1, irq_base + 8);
	outportb(0xA1, 2);
	outportb(0xA1, 1);
	int i;
	for (i = 0; i < 16; i++) {
		set_int_handler(irq_base + i, irq_handlers[i], 0x8E);
	}
	asm("sti");
}

void set_int_handler(uint8 index, void *handler, uint8 type) {
	size_t saved_flags;
	asm("pushf \n popl %0 \n cli":"=a"(saved_flags));
	idt[index].selector = 8;
	idt[index].address_0_15 = (size_t)handler & 0xFFFF;
	idt[index].address_16_31 = (size_t)handler >> 16;
	idt[index].type = type;
	idt[index].reserved = 0;
	asm("pushl %0 \n popf"::"a"(saved_flags));
}

void irq_handler(uint32 index, Registers *regs) {
	switch (index) {
		case 0:
			switch_task(regs);
			break;
		case 1:
			keyboard_interrupt();
			break;
	}
}

Теперь обработчик в interrupts.c решает кому обрабатывать какие прерывания. В нём, например, задано, что IRQ это должно уйти системе многозадачности, а IRQ1 в обработчик драйвера клавиатуры. В будущем лишь IRQ0 будет иметь специальную обработку, а все остальные прерывания будут преобразовываться в сообщения пользовательским программам-драйверам.

После interrupts.c меняется и interrupts.h:

#ifndef INTERRUPTS_H #define INTERRUPTS_H #include "stdlib.h" typedef struct { uint32 gs, fs, es, ds; uint32 ebp, edi, esi, edx, ecx, ebx, eax; uint32 eip, cs; uint32 eflags; uint32 esp, ss; } Registers; uint8 irq_base; uint8 irq_count; void init_interrupts(); void set_int_handler(uint8 index, void *handler, uint8 type); #endif  

Тут ничего особенного, лишь добавилось описание нового типа - структуры Registers и исчез ненужный макрос IRQ_HANDLER.

Драйвер клавиатуры также нуждается в доработке. Во-первых, необходимо убрать вызов set_int_handler из init_tty, во-вторых, заменить обработчик IRQ timer_int_handler на следующий код:

void keyboard_interrupt() {
	uint8 key_code;
	inportb(0x60, key_code);
	if (key_buffer_tail >= KEY_BUFFER_SIZE) {
		key_buffer_tail = 0;
	}
	key_buffer_tail++;
	key_buffer[key_buffer_tail - 1] = key_code;
	uint8 status;
	inportb(0x61, status);
	status |= 1;
	outportb(0x61, status);
} 

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

Ну и наконец самое интересное. Наконец-то, у нас заработает многозадачность как надо. При IRQ0 вызывается функция switch_task, которой передаётся указатель на структуру regs. Функции остаётся лишь заменить эту структуру на новую и обработчик прерывания выйдет в другую задачу.

Подправим multitasking.h: 

#ifndef MULTITASKING_H #define MULTITASKING_H #include "stdlib.h" #include "interrupts.h" typedef struct { ListItem list_item; AddressSpace address_space; bool suspend; size_t thread_count; char name[256]; } Process; typedef struct { ListItem list_item; Process *process; bool suspend; void *stack_base; size_t stack_size; Registers state; } Thread; List process_list; List thread_list; Process *current_process; Thread *current_thread; Process *kernel_process; Thread *kernel_thread; void init_multitasking(); void switch_task(Registers *regs); Thread *create_thread(Process *process, void *entry_point, size_t stack_size, bool kernel, bool suspend); #endif  

Изменился описатель нити - указатель стека нам больше не нужно хранить, зато нужно хранить структуру с регистрами.

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

А вот и новая реализация переключения задач:

#include "stdlib.h"
#include "memory_manager.h"
#include "interrupts.h"
#include "multitasking.h"

bool multitasking_enabled = false;

void init_multitasking() {
	list_init(&process_list);
	list_init(&thread_list);
	kernel_process = alloc_virt_pages(&kernel_address_space, NULL, -1, 1, PAGE_PRESENT | PAGE_WRITABLE);
	init_address_space(&kernel_process->address_space, kernel_page_dir);
	kernel_process->suspend = false;
	kernel_process->thread_count = 1;
	strncpy(kernel_process->name, "Kernel", sizeof(kernel_process->name));
	list_append((List*)&process_list, (ListItem*)kernel_process);
	kernel_thread = alloc_virt_pages(&kernel_address_space, NULL, -1, 1, PAGE_PRESENT | PAGE_WRITABLE);
	kernel_thread->process = kernel_process;
	kernel_thread->suspend = false;
	kernel_thread->stack_size = PAGE_SIZE;
	list_append((List*)&thread_list, (ListItem*)kernel_thread);
	current_process = kernel_process;
	current_thread = kernel_thread;
	multitasking_enabled = true;
}

void switch_task(Registers *regs) {
	if (multitasking_enabled) {
		memcpy(&current_thread->state, regs, sizeof(Registers));
		do {
			current_thread = (Thread*)current_thread->list_item.next;
			current_process = current_thread->process;
		} while (current_thread->suspend || current_process->suspend);
		asm("movl %0, %%cr3"::"a"(current_process->address_space.page_dir));
		memcpy(regs, &current_thread->state, sizeof(Registers));
	}
} 

Переключение задач стало проще. Также, наконец то адресное пространство ядра в описателе ядерного процесса настраивается до конца. Появился вызов новой функции - init_address_space. Её следует разместить в конце файла memory_manager.c:

void init_address_space(AddressSpace *address_space, phyaddr page_dir) {
	address_space->page_dir = page_dir;
	address_space->start = USER_MEMORY_START;
	address_space->end = USER_MEMORY_END;
	address_space->block_table_size = PAGE_SIZE / sizeof(VirtMemoryBlock);
	address_space->blocks = alloc_virt_pages(&kernel_address_space, NULL, -1, 1, PAGE_PRESENT | PAGE_WRITABLE | PAGE_GLOBAL);
	address_space->block_count = 0;
}

Ну вот и осталось только написать новую, работающую функцию create_thread:

Thread *create_thread(Process *process, void *entry_point, size_t stack_size, bool kernel, bool suspend) { Thread *thread = alloc_virt_pages(&kernel_address_space, NULL, -1, 1, PAGE_PRESENT | PAGE_WRITABLE); thread->process = process; thread->suspend = suspend; thread->stack_size = stack_size; thread->stack_base = alloc_virt_pages(&process->address_space, NULL, -1, (stack_size + PAGE_SIZE - 1) & ~PAGE_OFFSET_MASK, PAGE_PRESENT | PAGE_WRITABLE | (kernel ? 0 : PAGE_USER)); memset(&thread->state, 0, sizeof(Registers)); uint32 data_selector = (kernel ? 16 : 27); uint32 code_selector = (kernel ? 8 : 35); thread->state.eflags = 0x202; thread->state.cs = code_selector; thread->state.eip = (uint32)entry_point; thread->state.ss = data_selector; thread->state.esp = (uint32)thread->stack_base + thread->stack_size; thread->state.ds = data_selector; thread->state.es = data_selector; thread->state.fs = data_selector; thread->state.gs = data_selector; list_append((List*)&thread_list, (ListItem*)thread); process->thread_count++; return thread; } 

Ну вот, многозадачность почти готова. Если сейчас попытаться создать новую нить с помощью create_thread система упадёт. Это вызвано тем, что сейчас при прерывании в стек не записывается SS и ESP (и это если прерывание произошло в привилегированном коде, если нет, то система опять же упадёт). Чтобы всё происходило так, как нужно, следует создать структуру TSS.

Изначально TSS задумывался как структура для аппаратной поддержки переключения задач. Грубо говоря, это аппаратный аналог нашей структуры Registers с некоторыми дополнительными полями. Почему же мы сразу не воспользовались им? Потому что он не очень удобен (подразумевается для каждой задачи создавать отдельных сегмент TSS) и существует лишь на архитектуре Intel. Из-за этого он не используется по прямому назначению ни в одной крупной современной ОС, а в 64-битном режиме был урезан, сохранив с себе только указатели привилегированных стеков и карту ввода-вывода (как раз эти поля этой структуры активно используются), утратив функцию хранения контекстов задач.

Опишем эту структуру в multitasking.h:

#ifndef INTERRUPTS_H #define INTERRUPTS_H #include "stdlib.h" typedef struct { uint32 gs, fs, es, ds; uint32 ebp, edi, esi, edx, ecx, ebx, eax; uint32 eip, cs; uint32 eflags; uint32 esp, ss; } Registers; typedef struct { uint32 reserved_1; uint32 esp0; uint32 ss0; uint32 esp1; uint32 ss1; uint32 esp2; uint32 ss2; uint32 cr3; uint32 eip; uint32 eflags; uint32 eax; uint32 ecx; uint32 edx; uint32 ebx; uint32 esp; uint32 ebp; uint32 esi; uint32 edi; uint32 es; uint32 cs; uint32 ss; uint32 ds; uint32 fs; uint32 gs; uint32 ldtr; uint16 reserved_2; uint16 io_map_offset; uint8 io_map[8192 + 1]; } __attribute__((packed)) TSS; uint8 irq_base; uint8 irq_count; void init_interrupts(); void set_int_handler(uint8 index, void *handler, uint8 type); #endif  

Полей у этой структуры, как видите, очень много, но нас интересует лишь 4 из них - SS0, ESP0, io_map_offset и io_map, остальные мы никогда использовать не будем. io_map мы рассмотрим несколько позднее, про io_map_offset скажу лишь, что он должен быть равен смещению io_map относительно начала структуры. А вот на SS0 и ESP0 остановимся подробнее, потому что именно они нам сейчас и нужны.

Допустим, процессор выполняет непривилегированный код. Тут происходит прерывание, причём его обработчик находится в сегменте привилегированного кода. Непривилегированный стек использовать небезопасно, поэтому производится переключение на стек ядра (его вершина - SS0:ESP0), уже туда запихиваются старые SS, ESP, EFLAGS и CS с EIP. При выходе из прерывания эти значения будут восстановлены в соответствующие регистры и следовательно указатель стека станет прежним.

Адрес стека ядра как раз и хранится в структуре TSS (там есть и вершины стеков для ring1 и ring2, но мы используем лишь 2 уровня привилегий, поэтому они нам не нужны). К тому же, такое переключение стека при настроенном TSS происходит всегда, а не только, если прерывается непривилегированный код, что упрощает работу обработчика прерывания, если ему интересен стек прерванной задачи.

Наша структура Registers как раз рассчитана на такую ситуацию, но чтобы так было, нужно обязательно создать TSS.

Если SS0 и ESP0 могут быть общими для всех процессов (да так, вообще-то, и будет), то io_map должны быть разные, поэтому TSS имеет смысл разместить в адресном пространстве приложения, чтобы он переключался при переключении задач. Поэтому, пусть TSS будет располагаться на 3-х последних страницах адресного пространства приложения. Создадим его для процесса ядра:

#include "stdlib.h"
#include "memory_manager.h"
#include "interrupts.h"
#include "multitasking.h"

TSS *tss = (void*)(USER_MEMORY_END - PAGE_SIZE * 3 + 1);

bool multitasking_enabled = false;

void init_multitasking() {
	list_init(&process_list);
	list_init(&thread_list);
	kernel_process = alloc_virt_pages(&kernel_address_space, NULL, -1, 1, PAGE_PRESENT | PAGE_WRITABLE);
	init_address_space(&kernel_process->address_space, kernel_page_dir);
	alloc_virt_pages(&kernel_process->address_space, tss, -1, 1, PAGE_PRESENT | PAGE_WRITABLE);
	tss->esp0 = alloc_virt_pages(&kernel_address_space, NULL, -1, 1, PAGE_PRESENT | PAGE_WRITABLE | PAGE_GLOBAL);
	tss->ss0 = 16;
	tss->io_map_offset = (uint32)((uint32)tss->io_map - (uint32)tss);
	kernel_process->suspend = false;
	kernel_process->thread_count = 1;
	strncpy(kernel_process->name, "Kernel", sizeof(kernel_process->name));
	list_append((List*)&process_list, (ListItem*)kernel_process);
	kernel_thread = alloc_virt_pages(&kernel_address_space, NULL, -1, 1, PAGE_PRESENT | PAGE_WRITABLE);
	kernel_thread->process = kernel_process;
	kernel_thread->suspend = false;
	kernel_thread->stack_size = PAGE_SIZE;
	list_append((List*)&thread_list, (ListItem*)kernel_thread);
	current_process = kernel_process;
	current_thread = kernel_thread;
	multitasking_enabled = true;
}

 ...

Осталось лишь указать процессору, где у нас находится TSS. Это делается с помощью сегмента в GDT. Добавим его (напомню, что таблица дескрипторов сегментов у нас описана в startup.asm).

format ELF public _start extrn kernel_main section ".text" executable _start: movzx edx, dl push ebx push esi push edx lgdt [gdtr] call kernel_main add esp, 3 * 4 @: ;cli ;hlt jmp @b section ".data" writable gdt: dq 0 dq 0x00CF9A000000FFFF dq 0x00CF92000000FFFF dq 0x00CFFA000000FFFF dq 0x00CFF2000000FFFF dq 0x7F4089FFD0002FFF gdtr: dw $ - gdt dd gdt 

Таблица объявлена, надо лишь загрузить в регистр TR её селектор. Это будет логично сделать в init_multitasking.

void init_multitasking() {
	list_init(&process_list);
	list_init(&thread_list);
	kernel_process = alloc_virt_pages(&kernel_address_space, NULL, -1, 1, PAGE_PRESENT | PAGE_WRITABLE);
	init_address_space(&kernel_process->address_space, kernel_page_dir);
	alloc_virt_pages(&kernel_process->address_space, tss, -1, 1, PAGE_PRESENT | PAGE_WRITABLE);
	tss->esp0 = (uint32)alloc_virt_pages(&kernel_address_space, NULL, -1, 1, PAGE_PRESENT | PAGE_WRITABLE | PAGE_GLOBAL);
	tss->ss0 = 16;
	tss->io_map_offset = (uint32)((uint32)tss->io_map - (uint32)tss);
	kernel_process->suspend = false;
	kernel_process->thread_count = 1;
	strncpy(kernel_process->name, "Kernel", sizeof(kernel_process->name));
	list_append((List*)&process_list, (ListItem*)kernel_process);
	kernel_thread = alloc_virt_pages(&kernel_address_space, NULL, -1, 1, PAGE_PRESENT | PAGE_WRITABLE);
	kernel_thread->process = kernel_process;
	kernel_thread->suspend = false;
	kernel_thread->stack_size = PAGE_SIZE;
	list_append((List*)&thread_list, (ListItem*)kernel_thread);
	current_process = kernel_process;
	current_thread = kernel_thread;
	asm("ltr %w0"::"a"(40));
	multitasking_enabled = true;
} 

Многозадачность готова. Ну вот на сегодня и хватит... 


В избранное