
Перед вами четвёртая часть серии, посвящённой написанию собственной ОС. Здесь мы реализуем и запустим собственный исполняемый файл в пространстве пользователя, а также системные вызовы для вывода символов на экран и их считывания при вводе на клавиатуре.
Навигация по частям
Приложение
В этой главе мы подготовим первый исполняемый файл приложения и выполним его с помощью нашего ядра.
▍ Структура памяти
В предыдущей главе мы реализовали изолированные пространства виртуальных адресов, используя механизм страничной организации памяти. Теперь нужно подумать, где в этом пространстве адресов нам следует разместить будущее приложение.
Создайте новый скрипт компоновщика (user.ld
), который будет определять место размещения приложения в памяти:
user.ld
ENTRY(start)
SECTIONS {
. = 0x1000000;
/* машинный код */
.text :{
KEEP(*(.text.start));
*(.text .text.*);
}
/* данные только для чтения */
.rodata : ALIGN(4) {
*(.rodata .rodata.*);
}
/* данные с изначальными значениями */
.data : ALIGN(4) {
*(.data .data.*);
}
/* данные, которые при запуске приложения должны заполняться нулями */
.bss : ALIGN(4) {
*(.bss .bss.* .sbss .sbss.*);
. = ALIGN(16);
. += 64 * 1024; /* 64KB */
__stack_top = .;
ASSERT(. < 0x1800000, "too large executable");
}
}
Очень похоже на скрипт компоновщика ядра, не так ли? Ключевым отличием здесь выступает базовый адрес (0x1000000
), обеспечивающий, чтобы приложение не пересекалось с пространством адресов ядра.
ASSERT
— это инструкция утверждения, которая отменяет компоновку, если условие в первом аргументе не выполняется. Здесь она обеспечивает, чтобы конец секции .bss
, завершающей выделенную под приложение память, не выходил за 0x1800000
. Это делается, чтобы исполняемый файл случайно не превысил допустимый размер.
▍ Библиотека пространства пользователя
Далее мы создадим библиотеку для пользовательских программ. Чтобы не усложнять, начнём с минимальной функциональности, необходимой для запуска приложения:
user.c
#include "user.h"
extern char __stack_top[];
__attribute__((noreturn)) void exit(void) {
for (;;);
}
void putchar(char c) {
/* Доделать*/
}
__attribute__((section(".text.start")))
__attribute__((naked))
void start(void) {
__asm__ __volatile__(
"mv sp, %[stack_top] n"
"call main n"
"call exit n"
:: [stack_top] "r" (__stack_top)
);
}
Выполнение приложения начинается с функции start
. По аналогии с процессом загрузки ядра она устанавливает указатель стека и вызывает функцию main
приложения.
Мы также подготавливаем функцию exit
для завершения приложения. Но пока что она просто будет уходить в бесконечный цикл.
Кроме того, мы определяем функцию putchar
, на которую ссылается функция printf
в common.c
. Реализуем мы её позже.
В отличие от процесса инициализации ядра, здесь мы не очищаем секцию .bss
нулями, так как это уже будет сделано ядром (посредством alloc_pages
).
Подсказка
В типичной ОС аллоцированные области памяти тоже заполняются нулями. В противном случае может получиться, что память будет содержать чувствительную информацию (например, учётные данные) из других процессов, создавая критическую уязвимость безопасности.
Наконец, подготовим для библиотеки заголовочный файл (user.h
):
user.h
#pragma once
#include "common.h"
__attribute__((noreturn)) void exit(void);
void putchar(char ch);
▍ Первое приложение
Что ж, пора перейти к созданию первого приложения. К сожалению, у нас до сих пор нет способа для вывода символов, и мы не можем начать с программы “Hello World!”
. Вместо этого мы создадим простой бесконечный цикл:
shell.c
#include "user.h"
void main(void) {
for (;;);
}
▍ Сборка
Приложения будут собираться отдельно от ядра, и далее мы создадим новый скрипт (run.sh
) для сборки текущего:
run.sh
OBJCOPY=/opt/homebrew/opt/llvm/bin/llvm-objcopy
# Сборка оболочки (приложения)
$CC $CFLAGS -Wl,-Tuser.ld -Wl,-Map=shell.map -o shell.elf shell.c user.c common.c
$OBJCOPY --set-section-flags .bss=alloc,contents -O binary shell.elf shell.bin
$OBJCOPY -Ibinary -Oelf32-littleriscv shell.bin shell.bin.o
# Сборка ядра
$CC $CFLAGS -Wl,-Tkernel.ld -Wl,-Map=kernel.map -o kernel.elf
kernel.c common.c shell.bin.o
Первый вызов $CC
очень похож на скрипт сборки ядра. Скомпилируйте файлы С и линкуйте их с помощью user.ld
.
Первая подкоманда $OBJCOPY
преобразует исполняемый файл (ELF) в двоичный. Его двоичная форма — это фактическое содержимое, которое будет записано в память из базового адреса (в данном случае 0x1000000
). Для подготовки приложения в памяти операционной системе достаточно скопировать содержимое этого двоичного файла. Обычно в ОС используются форматы вроде ELF, в которых содержимое памяти и информация для отображения разделены, но в этом руководстве для упрощения мы используем простой двоичный файл.
Вторая подкоманда $OBJCOPY
преобразует двоичный исполняемый образ в формат, который можно встроить в код C. Посмотрим, что там внутри, используя команду llvm-nm
:
$ llvm-nm shell.bin.o
00000000 D _binary_shell_bin_start
00010260 D _binary_shell_bin_end
00010260 A _binary_shell_bin_size
Префикс _binary_
сопровождается именем файла, после чего идут start
, end
и size
. Эти символы указывают начало, конец и размер исполняемого образа. На практике они используются так:
extern char _binary_shell_bin_start[];
extern char _binary_shell_bin_size[];
void main(void) {
uint8_t *shell_bin = (uint8_t *) _binary_shell_bin_start;
printf("shell_bin size = %dn", (int) _binary_shell_bin_size);
printf("shell_bin[0] = %x (%d bytes)n", shell_bin[0]);
}
Эта программа выводит размер файла shell.bin
и первый байт его содержимого. Иными словами, можете рассматривать переменную _binary_shell_bin_start
так, будто в ней хранится содержимое этого файла:
char _binary_shell_bin_start[] = "<shell.bin contents here>";
Переменная _binary_shell_bin_size
содержит размер файла. Однако используется она несколько необычным образом. Давайте ещё раз заглянем внутрь с помощью llvm-nm
:
$ llvm-nm shell.bin.o | grep _binary_shell_bin_size
00010454 A _binary_shell_bin_size
$ ls -al shell.bin ← примечание: не путайте с shell.bin.o!
-rwxr-xr-x 1 seiya staff 66644 Oct 24 13:35 shell.bin
$ python3 -c 'print(0x10454)'
66644
Первый столбец в выводе llvm-nm
— это адрес символа. Шестнадцатеричное значение 10454
соответствует размеру файла, но это не совпадение. Как правило, значения каждого адреса в файле .o
определяются компоновщиком. Но _binary_shell_bin_size
в этом плане особенная.
A
во втором столбце сообщает, что адрес _binary_shell_bin_size
является таким символом (абсолютным), который компоновщик менять не должен. Это значит, в данной переменной в качестве адреса вшит размер файла.
Если определить эту переменную как массив произвольного типа, например, char _binary_shell_bin_size[]
, то _binary_shell_bin_size
будет рассматриваться как указатель, хранящий его адрес. Однако, поскольку здесь мы используем в качестве адреса размер файла, его приведение даст нам этот размер файла. Это типичный приём (можно сказать, костыль) с использованием формата объектного файла.
Наконец, мы добавили shell.bin.o
к аргументам clang
при компиляции ядра. Таким образом мы встроим исполняемый файл в образ ядра.
▍ Дизассемблинг исполняемого файла
Дизассемблинг исполняемого файла показывает, что секция .text.start
находится в его начале. Функцию start
нужно поместить по адресу 0x1000000
:
$ llvm-objdump -d shell.elf
shell.elf: file format elf32-littleriscv
Disassembly of section .text:
01000000 <start>:
1000000: 37 05 01 01 lui a0, 4112
1000004: 13 05 05 26 addi a0, a0, 608
1000008: 2a 81 mv sp, a0
100000a: 19 20 jal 0x1000010 <main>
100000c: 29 20 jal 0x1000016 <exit>
100000e: 00 00 unimp
01000010 <main>:
1000010: 01 a0 j 0x1000010 <main>
1000012: 00 00 unimp
01000016 <exit>:
1000016: 01 a0 j 0x1000016 <exit>
Режим пользователя
Далее мы запустим созданное выше приложение.
▍ Извлечение исполняемого файла
В форматах исполняемых файлов вроде ELF адрес загрузки сохраняется в заголовке файла (в ELF это заголовок программы). Но поскольку исполняемый образ нашего приложения является двоичным файлом, нужно подготовить его, используя фиксированное значение:
kernel.h
// Базовый виртуальный адрес образа приложения. Должен соответствовать стартовому адресу, определённому в `user.ld`.
#define USER_BASE 0x1000000
Далее определим символы для использования этого встроенного двоичного кода в shell.bin.o
:
kernel.c
extern char _binary_shell_bin_start[], _binary_shell_bin_size[];
Также обновите функцию create_process
для запуска приложения:
kernel.c
void user_entry(void) {
PANIC("not yet implemented");
}
struct process *create_process(const void *image, size_t image_size) {
/* код опущен */
*--sp = 0; // s3
*--sp = 0; // s2
*--sp = 0; // s1
*--sp = 0; // s0
*--sp = (uint32_t) user_entry; // ra (изменился!)
uint32_t *page_table = (uint32_t *) alloc_pages(1);
// Отображение страниц памяти ядра.
for (paddr_t paddr = (paddr_t) __kernel_base;
paddr < (paddr_t) __free_ram_end; paddr += PAGE_SIZE)
map_page(page_table, paddr, paddr, PAGE_R | PAGE_W | PAGE_X);
// Отображение страниц памяти пространства пользователя.
for (uint32_t off = 0; off < image_size; off += PAGE_SIZE) {
paddr_t page = alloc_pages(1);
// Обработка случая, в котором копируемые данные меньше размера страницы.
size_t remaining = image_size - off;
size_t copy_size = PAGE_SIZE <= remaining ? PAGE_SIZE : remaining;
// Заполнение и отображение страницы.
memcpy((void *) page, image + off, copy_size);
map_page(page_table, USER_BASE + off, page,
PAGE_U | PAGE_R | PAGE_W | PAGE_X);
}
Мы изменили create_process
, чтобы в качестве аргументов получать указатель на исполняемый образ (image
) и размер этого образа. Эта функция постранично копирует исполняемый образ в соответствии с указанным размером и отображает его в таблицу страниц процессов. Кроме того, она устанавливает место перехода для первого переключения контекста на user_entry
. Пока оставим эту функцию пустой.
Предупреждение
Если вы отобразите исполняемый образ напрямую без копирования, процессы одного приложения будут совместно использовать одни и те же физические страницы. Прощай, изоляция памяти.
Наконец, измените код, вызывающий функцию create_process
, и сделайте так, чтобы она создавала процесс пользователя:
kernel.c
void kernel_main(void) {
memset(__bss, 0, (size_t) __bss_end - (size_t) __bss);
printf("nn");
WRITE_CSR(stvec, (uint32_t) kernel_entry);
idle_proc = create_process(NULL, 0); // изменено!
idle_proc->pid = -1; // бездействует
current_proc = idle_proc;
// добавлено!
create_process(_binary_shell_bin_start, (size_t) _binary_shell_bin_size);
yield();
PANIC("switched to idle process");
}
Теперь запустим программу и с помощью монитора QEMU проверим, правильно ли отображается исполняемый образ:
(qemu) info mem
vaddr paddr size attr
-------- ---------------- -------- -------
01000000 0000000080265000 00001000 rwxu---
01001000 0000000080267000 00010000 rwxu---
Здесь мы видим, что физический адрес 0x80265000
отображается в виртуальный 0x1000000
(USER_BASE
). Посмотрим, что хранится по этому физическому адресу. Для вывода содержимого физической памяти используйте команду xp
:
(qemu) xp /32b 0x80265000
0000000080265000: 0x37 0x05 0x01 0x01 0x13 0x05 0x05 0x26
0000000080265008: 0x2a 0x81 0x19 0x20 0x29 0x20 0x00 0x00
0000000080265010: 0x01 0xa0 0x00 0x00 0x82 0x80 0x01 0xa0
0000000080265018: 0x09 0xca 0xaa 0x86 0x7d 0x16 0x13 0x87
Похоже, здесь есть какие-то данные. Проверим содержимое shell.bin
, чтобы убедиться в их соответствии:
$ hexdump -C shell.bin | head
00000000 37 05 01 01 13 05 05 26 2a 81 19 20 29 20 00 00 |7......&*.. ) ..|
00000010 01 a0 00 00 82 80 01 a0 09 ca aa 86 7d 16 13 87 |............}...|
00000020 16 00 23 80 b6 00 ba 86 75 fa 82 80 01 ce aa 86 |..#.....u.......|
00000030 03 87 05 00 7d 16 85 05 93 87 16 00 23 80 e6 00 |....}.......#...|
00000040 be 86 7d f6 82 80 03 c6 05 00 aa 86 01 ce 85 05 |..}.............|
00000050 2a 87 23 00 c7 00 03 c6 05 00 93 06 17 00 85 05 |*.#.............|
00000060 36 87 65 fa 23 80 06 00 82 80 03 46 05 00 15 c2 |6.e.#......F....|
00000070 05 05 83 c6 05 00 33 37 d0 00 93 77 f6 0f bd 8e |......37...w....|
00000080 93 b6 16 00 f9 8e 91 c6 03 46 05 00 85 05 05 05 |.........F......|
00000090 6d f2 03 c5 05 00 93 75 f6 0f 33 85 a5 40 82 80 |m......u..3..@..|
Хмм, в шестнадцатеричной форме понять трудно. Давайте дизассемблируем этот машинный код для наглядности:
(qemu) xp /8i 0x80265000
0x80265000: 01010537 lui a0,16842752
0x80265004: 26050513 addi a0,a0,608
0x80265008: 812a mv sp,a0
0x8026500a: 2019 jal ra,6 # 0x80265010
0x8026500c: 2029 jal ra,10 # 0x80265016
0x8026500e: 0000 illegal
0x80265010: a001 j 0 # 0x80265010
0x80265012: 0000 illegal
Он вычисляет/устанавливает изначальное значение стека, после чего вызывает две разных функции. Если мы сравним это с результатом дизассемблинга shell.elf
, то убедимся, что всё действительно совпадает:
$ llvm-objdump -d shell.elf | head -n20
shell.elf: file format elf32-littleriscv
Disassembly of section .text:
01000000 <start>:
1000000: 37 05 01 01 lui a0, 4112
1000004: 13 05 05 26 addi a0, a0, 608
1000008: 2a 81 mv sp, a0
100000a: 19 20 jal 0x1000010 <main>
100000c: 29 20 jal 0x1000016 <exit>
100000e: 00 00 unimp
01000010 <main>:
1000010: 01 a0 j 0x1000010 <main>
1000012: 00 00 unimp
▍ Переход в режим пользователя
Для выполнения приложений мы используем пользовательский режим работы процессора, который на языке RISC-V называется U-Mode. Переключиться на него легко:
kernel.h
#define SSTATUS_SPIE (1 << 5)
kernel.c
// ↓ __attribute__((naked)) очень важен!
__attribute__((naked)) void user_entry(void) {
__asm__ __volatile__(
"csrw sepc, %[sepc] n"
"csrw sstatus, %[sstatus] n"
"sret n"
:
: [sepc] "r" (USER_BASE),
[sstatus] "r" (SSTATUS_SPIE)
);
}
Переключение из S-Mode в U-Mode производится с помощью инструкции sret
. Только прежде, чем менять рабочий режим, она выполняет две записи в CSR:
- Устанавливает в регистре
sepc
значение счётчика команд на момент перехода в U-Mode. То есть записывает адрес, куда переходитsret
. - Устанавливает в регистре
sstatus
битSPIE
. Его установка активирует аппаратные прерывания при переходе в U-Mode, в результате которых вызывается обработчик, прописанный в регистреstvec
.
Подсказка
В этом руководстве мы вместо аппаратных прерываний используем механизм опроса, поэтому устанавливать бит
SPIE
не обязательно. Тем не менее лучше сделать это явно, чем молча игнорировать прерывания.
▍ Проверяем режим пользователя
Пора проверить, что у нас получилось. Однако, поскольку shell.c
просто выполняет бесконечный цикл, по экрану нельзя понять, правильно ли этот скрипт работает. Но у нас есть для этого монитор QEMU:
(qemu) info registers
CPU#0
V = 0
pc 01000010
Похоже, процессор непрерывно выполняет содержимое 0x1000010
. Работает программа корректно, но чутьё подсказывает, что чего-то не хватает. Проверим, удастся ли нам пронаблюдать поведение, характерное для U-Mode. Добавьте в shell.c
буквально одну строку:
shell.c
#include "user.h"
void main(void) {
*((volatile int *) 0x80200000) = 0x1234; // new!
for (;;);
}
Значение 0x80200000
представляет область памяти ядра, отображаемую в таблицу страниц. Но поскольку это страница памяти ядра, для которой бит U
в записи таблицы страниц не установлен, должно возникать исключение (отказ страницы) и, следовательно, паника ядра. Проверим:
$ ./run.sh
PANIC: kernel.c:71: unexpected trap scause=0000000f, stval=80200000, sepc=0100001a
Исключение 15 (scause = 0xf = 15
). Оно соответствует ошибке «Store/AMO page fault». Что ж, ожидаемое исключение случилось. Кроме того, счётчик команд в sepc
указывает на строку, которую мы добавили в shell.c
:
$ llvm-addr2line -e shell.elf 0x100001a
/Users/seiya/dev/os-from-scratch/shell.c:4
Поздравляю! Вы успешно запустили своё первое приложение. Разве не удивительно, насколько легко реализуется режим пользователя? Ядро устроено очень похожим на приложение образом — просто у него чуть больше привилегий.
Системные вызовы
В этом разделе мы реализуем механизм системных вызовов, позволяющий приложениям обращаться к функциям ядра. Пришло время «Поприветствовать мир ядра» (имеется в виду «Hello World!») из пространства пользователя.
▍ Библиотека пространства пользователя
Активация системного вызова аналогична уже знакомой нам реализации вызова SBI:
user.c
int syscall(int sysno, int arg0, int arg1, int arg2) {
register int a0 __asm__("a0") = arg0;
register int a1 __asm__("a1") = arg1;
register int a2 __asm__("a2") = arg2;
register int a3 __asm__("a3") = sysno;
__asm__ __volatile__("ecall"
: "=r"(a0)
: "r"(a0), "r"(a1), "r"(a2), "r"(a3)
: "memory");
return a0;
}
Функция syscall
устанавливает номер системного вызова в регистре a3
и его аргументы в регистрах от a0
до a2
, после чего выполняет инструкцию ecall
. ecall
— это особая инструкция, используемая для передачи обработки ядру. При её выполнении вызывается обработчик исключений, и управление передаётся ядру. Возвращаемое ядром значение записывается в регистр a0
.
В качестве первого системного вызова мы реализуем putchar
. Он будет выводить символ, получаемый в виде своего первого аргумента. Для второго и последующих неиспользуемых аргументов мы установим 0:
common.h
#define SYS_PUTCHAR 1
user.c
void putchar(char ch) {
syscall(SYS_PUTCHAR, ch, 0, 0);
}
▍ Обработка инструкции ecall
в ядре
Теперь обновим обработчик исключений, чтобы он также мог обрабатывать инструкцию ecall
:
kernel.h
#define SCAUSE_ECALL 8
kernel.c
void handle_trap(struct trap_frame *f) {
uint32_t scause = READ_CSR(scause);
uint32_t stval = READ_CSR(stval);
uint32_t user_pc = READ_CSR(sepc);
if (scause == SCAUSE_ECALL) {
handle_syscall(f);
user_pc += 4;
} else {
PANIC("unexpected trap scause=%x, stval=%x, sepc=%xn", scause, stval, user_pc);
}
WRITE_CSR(sepc, user_pc);
}
Определить, был ли совершён вызов ecall
, можно путём проверки значения scause
. Помимо вызова функции handle_syscall
, мы также добавим 4 (размер инструкции ecall
) к значению sepc
. Дело в том, что sepc
указывает на вызвавший исключение счётчик команд, который указывает на инструкцию ecall
. Если мы её не изменим, ядро будет возвращаться в одно и то же место, и выполнение инструкции ecall
будет раз за разом повторяться.
▍ Обработчик системных вызовов
Ниже описан обработчик системных вызовов, который вызывается из обработчика исключений и получает сохранённую в нём карту «регистров на момент исключения».
kernel.c
void handle_syscall(struct trap_frame *f) {
switch (f->a3) {
case SYS_PUTCHAR:
putchar(f->a0);
break;
default:
PANIC("unexpected syscall a3=%xn", f->a3);
}
}
Этот обработчик определяет тип системного вызова, проверяя значение регистра a3
. Теперь у нас есть всего один системный вызов, SYS_PUTCHAR
, который просто выводит символ, сохранённый в регистре a0
.
▍ Проверка системного вызова
Вот мы и реализовали системный вызов. Пора его проверить!
Помните реализацию функции printf
из common.c
? Она вызывает putchar
для вывода символов. А поскольку мы только что реализовали функцию putchar
в библиотеке пространства пользователя, можно использовать её так:
shell.c
void main(void) {
printf("Hello World from shell!n");
}
И на экране отобразится приветливое сообщение:
$ ./run.sh
Hello World from shell!
Поздравляю! Вы успешно реализовали системный вызов. Предлагаю реализовать ещё!
▍ Получение ввода с клавиатуры (системный вызов getchar
)
Нашей следующей целью будет реализация оболочки. Для этого нам понадобится получать символы, вводимые на клавиатуре.
SBI предоставляет интерфейс для считывания «ввода в отладочную консоль». Если ввода нет, возвращается -1
:
kernel.c
long getchar(void) {
struct sbiret ret = sbi_call(0, 0, 0, 0, 0, 0, 0, 2);
return ret.error;
}
Реализуется системный вызов getchar
так:
common.h
#define SYS_GETCHAR 2
user.c
int getchar(void) {
return syscall(SYS_GETCHAR, 0, 0, 0);
}
user.h
int getchar(void);
kernel.c
void handle_syscall(struct trap_frame *f) {
switch (f->a3) {
case SYS_GETCHAR:
while (1) {
long ch = getchar();
if (ch >= 0) {
f->a0 = ch;
break;
}
yield();
}
break;
/* код опущен */
}
}
Реализация системного вызова getchar
продолжает вызывать SBI, пока вводятся символы. Однако, если просто повторять вызов этой функции, другие процессы будут вынуждены ждать. Поэтому мы вызываем системный вызов yield
, уступая процессор для других задач.
Примечание
Строго говоря, SBI считывает символы не с клавиатуры, а с последовательного порта, к которому она (или стандартный ввод QEMU) подключена.
▍ Написание оболочки
Теперь напишем оболочку, использующую простую команду hello
для вывода приветствия Hello world from shell!
:
shell.c
void main(void) {
while (1) {
prompt:
printf("> ");
char cmdline[128];
for (int i = 0;; i++) {
char ch = getchar();
putchar(ch);
if (i == sizeof(cmdline) - 1) {
printf("command line too longn");
goto prompt;
} else if (ch == 'r') {
printf("n");
cmdline[i] = '';
break;
} else {
cmdline[i] = ch;
}
}
if (strcmp(cmdline, "hello") == 0)
printf("Hello world from shell!n");
else
printf("unknown command: %sn", cmdline);
}
}
Здесь мы считываем символы, пока не встретим символ перевода строки, и проверяем, соответствует ли введённая строка имени команды.
Предупреждение
Имейте в виду, что в консоли отладки символом перевода строки является
'r'
.
Теперь введём команду hello
:
$ ./run.sh
> hello
Hello world from shell!
Ваша ОС всё больше начинает походить на настоящую, и потребовалось для этого не так много времени!
▍ Завершение процесса (системный вызов exit
)
Наконец, мы реализуем системный вызов exit
, завершающий процесс:
common.h
#define SYS_EXIT 3
user.c
c
__attribute__((noreturn)) void exit(void) {
syscall(SYS_EXIT, 0, 0, 0);
for (;;); // На всякий случай
}
kernel.h
#define PROC_EXITED 2
kernel.c
void handle_syscall(struct trap_frame *f) {
switch (f->a3) {
case SYS_EXIT:
printf("process %d exitedn", current_proc->pid);
current_proc->state = PROC_EXITED;
yield();
PANIC("unreachable");
/* код опущен */
}
}
Этот системный вызов изменяет состояние процесса на PROC_EXITED
и вызывает yield
, освобождая процессор для других задач. Планировщик будет выполнять только процессы в состоянии PROC_RUNNABLE
, так что к этому процессу он не вернётся. Тем не менее на случай, если он всё же это сделает, мы добавили макрос PANIC
.
Подсказка
Чтобы не усложнять, мы отмечаем процесс просто как завершённый (
PROC_EXITED
). Если же вы будете создавать реальную рабочую ОС, то нужно обязательно освобождать занимаемые процессом ресурсы, такие как таблицы страниц и выделенная память.
Добавьте в оболочку команду exit
:
shell.c
if (strcmp(cmdline, "hello") == 0)
printf("Hello world from shell!n");
else if (strcmp(cmdline, "exit") == 0)
exit();
else
printf("unknown command: %sn", cmdline);
Готово! Проверим:
$ ./run.sh
> exit
process 2 exited
PANIC: kernel.c:333: switched to idle process
При выполнении exit
процесс оболочки завершается через системный вызов, и ни одного доступного для выполнения процесса не остаётся. В итоге планировщик будет выбирать бездействующий процесс, вызывая панику.
На этом очередная часть серии завершается. В следующей, последней части мы реализуем дисковый ввод/вывод и файловую систему, после чего подведём итоги нашего увлекательного проекта и наметим его возможные доработки.
Автор: Bright_Translate