Пишем свою операционную систему. Улучшение стандартной библиотеки
Причина падения ОС при включении оптимизации была найдена - memset (как и memcpy) меняет значение регистра EDI, который считается неизменным по соглашению вызова C (вызываемая функция обязана сохранить его значение).
Я принял достаточно радикальное решение - часть функций стандартной библиотеки будет вынесена в ассемблерный файл для лучшей оптимизации (строковые операции компилятор делает неэффективно) и простоты написания. Теперь в нашем проекте появляется файл stdlib.i386.asm:
format ELF
public memset
public memsetw
public memcpy
public memcmp
public memchr
section ".text" executable
; void memset(void *mem, char value, size_t count);
memset:
push edi
mov edi, [esp + 8]
mov edx, [esp + 12]
mov al, dl
shl eax, 8
mov al, dl
shl eax, 8
mov al, dl
shl eax, 8
mov al, dl
mov edx, [esp + 16]
mov ecx, edx
shr ecx, 2
rep stosd
mov ecx, edx
and ecx, 3
rep stosb
pop edi
ret
; void memsetw(void *mem, uint16 value, size_t count);
memsetw:
push edi
mov edi, [esp + 8]
mov edx, [esp + 12]
mov ax, dx
shl eax, 16
mov ax, dx
mov edx, [esp + 16]
mov ecx, edx
shr ecx, 1
rep stosd
mov ecx, edx
and ecx, 1
rep stosw
pop edi
ret
; void memcpy(void *dest, void *src, size_t count);
memcpy:
push esi edi
mov edi, [esp + 12]
mov esi, [esp + 16]
mov edx, [esp + 20]
cmp edi, esi
jb .reversed
mov ecx, edx
shr ecx, 2
rep movsd
mov ecx, edx
and ecx, 3
rep movsb
.exit:
pop edi esi
ret
.reversed:
add esi, edx
add edi, edx
dec esi
dec edi
std
mov ecx, edx
and ecx, 3
rep movsb
mov ecx, edx
shr ecx, 2
rep movsd
cld
jmp .exit
; int memcmp(void *mem1, void *mem2, size_t count);
memcmp:
push esi edi
mov edi, [esp + 12]
mov esi, [esp + 16]
mov ecx, [esp + 20]
repe cmpsb
seta al
setb dl
sub al, dl
movzx eax, al
pop edi esi
ret
; void *memchr(void *mem, char value, size_t count);
memchr:
push edi
mov edi, [esp + 8]
mov eax, [esp + 12]
mov ecx, [esp + 16]
repne scasb
jne .not_found
dec edi
mov eax, edi
.exit:
pop edi
ret
.not_found:
xor eax, eax
jmp .exit
Заголовочный файл stdlib.h остаётся неизменным, а вот stdlib.c укорачивается:
К тому же я избавился от ряда не очень "красивых" ассемблерных вставок оставив лишь полностью корректные (в том числе и в других файлах - memory_manager.c, interrupts.c).
Теперь наша ОС работает полностью корректно и не падает при любом уровне оптимизации (O0, O1, O2, O3, Os).
Ещё немного о многозадачности
Оставшееся время расскажу ещё немного о реализации многозадачности. Для переключения нитей можно использовать самые разные варианты, мой выбор остановился на одном
из них.
В обработчике прерывания от таймера я собираюсь сохранить в стек все регистры процессора, а также адрес temp_page, затем записать текущий указатель стека в структуру описателя нити. После этого производится поиск следующей нити для выполнения (в том числе это может оказаться и та же самая). Когда она найдена, из её описателя восстанавливается её указатель стека, а затем все регистры из стека. В итоге весь контекст задачи хранится в её стеке и не нужны дополнительные структуры данных. Для создания
новой нити нужно заполнить её стек так, как будто только что случилось прерывание таймера (запихнуть туда адрес возврата, регистр флагов, регистры общего назначения и т. д.). Именно поэтому нам нужна полная предсказуемость содержимого стека. Возможно, обработчик прерывания таймера придётся писать на Assembler.
Двунаправленные связанные списки
Как список процессов, так и список их нитей нужно как-то хранить. Причём в форме удобной для быстрого переключения, добавления и удаления нитей. Я собираюсь
использовать для этого двунаправленные связанные кольцевые списки (на примере файловой системы и менеджера физической памяти вы уже могли догадаться, что я к ним крайне не равнодушен). Каждое переключение достаточно брать следующий элемент списка. Переход из начала в конец произойдёт сам собой.
Чтобы работать со списками было удобно, реализую три функции стандартной библиотеки специально для них:
Внимание! Для перебора списка захват семафора не требуется!
Как это работает? Мы создаём новую структуру данных (например, описатель процесса или нити), самым первым полем структуры должно являться поле с любым именем и типом ListItem. После этого мы можем передавать указатель на новую структуру функциям list_add и list_remove в качестве параметра item, совершая явное приведение типов.
Новый Makefile
Мы добавили новый файл - stdlib.i386.asm. Следует
описать правило для его сборки:
Ну вот и всё. У нас есть практически полноценный менеджер памяти и стандартная библиотека с поддержкой многопоточности, то есть всё, чтобы приступить к реализации многозадачности. Кстати, в качестве "домашнего задания" можете добавить потокобезопасность в драйвер экрана (tty.c).