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

Пишем свою операционную систему. Начало реализации многозадачности


Во-первых, вот исправление функции list_remove из stdlib.c, чтобы она нормально компилировалась:

void list_remove(ListItem *item) {
	mutex_get(&(item->list->mutex), true);
	if (item->list->first == item) {
		item->list->first = item->next;
		if (item->list->first == item) {
			item->list->first = NULL;
		}
	}
	item->next->prev = item->prev;
	item->prev->next = item->next;
	item->list->count--;
	mutex_release(&(item->list->mutex));
}

Во-вторых, сегодня мы начнём писать функции для поддержки многозадачности. Как обычно вначале определимся с прототипами функций в заголовочном файле multitasking.h:

#ifndef MULTITASKING_H
#define MULTITASKING_H

#include "stdlib.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;
	void *stack_pointer;
} 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();

Thread *create_thread(Process *process, void *entry_point, size_t stack_size, bool kernel, bool suspend);

#endif

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

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

Начинается файл multitasking.c с простой функции init_multitasking, которая создаёт структуры процесса и нити ядра, а также устанавливает обработчик прерывания таймера:

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

void task_switch_int_handler();

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);
	kernel_process->address_space.page_dir = 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;
	set_int_handler(irq_base, task_switch_int_handler, 0x8E);
}

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

Дальше следует новый обработчик прерывания таймера:

IRQ_HANDLER(task_switch_int_handler) {
	asm("movl %%esp, %0":"=a"(current_thread->stack_pointer));
	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));
	asm("movl %0, %%esp"::"a"(current_thread->stack_pointer));
}

Вот так просто переключаются задачи. Запоминаем текущий указатель стека, ищем следующую нить для выполнения, переключаем каталог страниц, переключаем стек. Всё! Мы теперь находимся в контексте новой нити. Однако на самом деле такой код использовать не получится - как создавать новые нити? Функция create_thread должна заполнить стек новой нити как-будто он находится в середине обработчика прерывания. То, что помещается в стек процессором (адрес возврата, регистр флагов), а также ассемблерной вставкой (все регистры процессора) известно, но вот поведение самого task_switch_int_handler - нет. При разных настройках компилятора он может как создавать стековый фрейм, так и нет. В итоге в стеке будет на одно двойное слово больше или меньше, а это всё портит. До следующего выпуска предстоит придумать как с этим бороться. Я пока не знаю красивого решения на Си (если бы мы писали ОС на чистом Assembler решение проблемы было бы элементарно и я так уже много раз делал).

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

void switch_task() {
	asm("hlt");
}

Сразу же приведу пример, где такая команда уместна - в функции in_char tty.c:

#include <stdarg.h>
#include "stdlib.h"
#include "memory_manager.h"
#include "interrupts.h"
#include "multitasking.h"
#include "tty.h"
#include "scancodes.h"

 ...

char in_char(bool wait) {
	static bool shift = false;
	uint8 chr;
	do {
		chr = in_scancode();
		switch (chr) {
			case 0x2A:
			case 0x36:
				shift = true;
				break;
			case 0x2A + 0x80:
			case 0x36 + 0x80:
				shift = false;
				break;
		}
		if (chr & 0x80) {
			chr = 0;
		}
		if (shift) {
			chr = scancodes_shifted[chr];
		} else {
			chr = scancodes[chr];
		}
		if ((!chr) && wait) {
			asm("hlt");
		}
	} while (wait && (!chr));
	return chr;
} 

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

Ну вот осталась последняя функция файла multitasking.c - 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;
	// ... вот тут должно быть создание и заполнение стека нити ...
	list_append((List*)&thread_list, (ListItem*)thread);
	process->thread_count++;
	return thread;
} 

Это заготовка, но не готовая функция. Её вызов в том виде, в котором она сейчас есть с suspend != false приведёт к краху системы на следующем тике таймера, потому что мы не создали для нити нормальный стек. Нам ещё многое предстоит написать.

Пока добавим в kernel_main инициализацию многозадачности:

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

typedef struct {
	uint64 base;
	uint64 size;
} BootModuleInfo;

void kernel_main(uint8 boot_disk_id, void *memory_map, BootModuleInfo *boot_module_list) {
	init_memory_manager(memory_map);
	init_interrupts();
	init_multitasking();
	init_tty();
	set_text_attr(15);
	printf("Welcome to MyOS!\n");
	char string[10];
	in_string(string, sizeof(string));
	out_string(string);
} 

Ну и наконец новый Makefile:

ifdef OS
	LDFLAGS = -mi386pe
else
	LDFLAGS = -melf_i386
endif

CFLAGS = -m32 -ffreestanding -O3

all: script.ld startup.o stdlib_asm.o stdlib.o main.o memory_manager.o interrupts.o multitasking.o tty.o
	ld $(LDFLAGS) -T script.ld -o kernel.bin startup.o stdlib_asm.o stdlib.o main.o memory_manager.o interrupts.o multitasking.o tty.o
	objcopy kernel.bin -O binary
startup.o: startup.i386.asm
	fasm startup.i386.asm startup.o
stdlib.o: stdlib.c stdlib.h
	gcc -c $(CFLAGS) -o stdlib.o stdlib.c
stdlib_asm.o: stdlib.i386.asm
	fasm stdlib.i386.asm stdlib_asm.o
main.o: main.c stdlib.h interrupts.h multitasking.h tty.h
	gcc -c $(CFLAGS) -o main.o main.c
memory_manager.o: memory_manager.c memory_manager.h stdlib.h
	gcc -c $(CFLAGS) -o memory_manager.o memory_manager.c
interrupts.o: interrupts.c interrupts.h memory_manager.h stdlib.h
	gcc -c $(CFLAGS) -o interrupts.o interrupts.c
multitasking.o: multitasking.c multitasking.h interrupts.h memory_manager.h stdlib.h
	gcc -c $(CFLAGS) -o multitasking.o multitasking.c
tty.o: tty.c tty.h multitasking.h interrupts.h stdlib.h
	gcc -c $(CFLAGS) -o tty.o tty.c
clean:
	rm -v *.o kernel.bin

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

Заключение

У меня сразу две хороших новости - во-первых, моя рассылка получила статус "Серебряной", во-вторых, у неё уже 152 подписчика (вероятно, столь быстрый рост их числа связан с появлением рассылки в общем каталоге из-за изменения статуса).

Теперь я могу прикладывать к выпускам файлы, поэтому сразу же воспользуюсь этим, приложив полный архив исходных текстов нашей ОС, который точно работоспособен. Я не стал убирать скомпилированный образ диска (всё равно ZIP его очень не плохо сжал), чтобы можно было легко проверить систему.

Раз вас стало больше, я могу вновь задать свои вопросы:

1) Как должна называться наша ОС?
2) Как по вашему мнению лучше реализовать переключение задач (сделать стек при обработке прерывания более предсказуемым)?

И вообще, пишите любые свои замечания, предложения, пожелания, вопросы мне на адрес kiv.apple@gmail.com. До встречи!


В избранное