mirror of
https://github.com/StepanovPlaton/OSin1000Lines.git
synced 2026-04-03 12:20:46 +04:00
Compare commits
2 Commits
2c70ec98d8
...
059019f458
| Author | SHA1 | Date | |
|---|---|---|---|
| 059019f458 | |||
| c44fc166ed |
95
README.md
Normal file
95
README.md
Normal file
@@ -0,0 +1,95 @@
|
||||
# 🖥️ Операционная система в 1000 строках кода на С
|
||||
|
||||
> Минимальная операционная система для архитектуры RISC-V 32-bit, реализованная по книге [Operating System in 1,000 Lines](https://operating-system-in-1000-lines.vercel.app/en/)
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## 📋 Стек
|
||||
|
||||
- **Архитектура:** RISC-V 32-bit
|
||||
- **Компилятор:** Clang с поддержкой RISC-V
|
||||
- **Язык:** C11 (freestanding)
|
||||
- **Виртуализация:** QEMU (для эмуляции)
|
||||
|
||||
---
|
||||
|
||||
## 🚀 О проекте
|
||||
|
||||
- **Минимальная операционная система** с полным набором базовых функций
|
||||
- **Управление памятью** с поддержкой виртуальной адресации и страничной организации памяти (SV32)
|
||||
- Выделение страниц памяти
|
||||
- Маппинг виртуальных адресов на физические
|
||||
- Изоляция адресных пространств процессов
|
||||
- **Многозадачность** с поддержкой до 8 процессов одновременно
|
||||
- Переключение контекста между процессами
|
||||
- Планировщик задач с round-robin алгоритмом
|
||||
- Изоляция процессов через отдельные page tables
|
||||
- **Системные вызовы** для взаимодействия пользовательских программ с ядром
|
||||
- `SYS_PUTCHAR` — вывод символа в консоль
|
||||
- `SYS_GETCHAR` — чтение символа из консоли (с блокировкой)
|
||||
- `SYS_EXIT` — завершение процесса
|
||||
- `SYS_READFILE` — чтение файла
|
||||
- `SYS_WRITEFILE` — запись в файл
|
||||
- **Файловая система** на основе формата tar (ustar)
|
||||
- Поддержка чтения и записи файлов
|
||||
- Автоматическая загрузка файлов из образа диска при старте
|
||||
- Сохранение изменений на диск через VirtIO
|
||||
- **Работа с диском** через интерфейс VirtIO Block Device
|
||||
- Чтение и запись секторов диска
|
||||
- Поддержка виртуальных очередей VirtIO
|
||||
- **Обработка исключений и прерываний** через trap handler
|
||||
- Обработка системных вызовов через инструкцию `ecall`
|
||||
- Переключение между режимами supervisor и user
|
||||
- **Интерактивная оболочка (shell)** в пользовательском пространстве
|
||||
- Команда `hello` — приветствие
|
||||
- Команда `exit` — выход из shell
|
||||
- Команда `readfile` — чтение файла
|
||||
- Команда `writefile` — запись в файл
|
||||
- **Разделение на kernel и user space**
|
||||
- Ядро работает в режиме supervisor
|
||||
- Пользовательские программы работают в режиме user с ограниченными правами
|
||||
- Защита памяти через page tables
|
||||
|
||||
---
|
||||
|
||||
## 📝 Структура проекта
|
||||
|
||||
```
|
||||
OSIn1000Lines/
|
||||
├── src/
|
||||
│ ├── kernel.c # Основной код ядра (управление процессами, системные вызовы)
|
||||
│ ├── kernel.h # Заголовочные файлы ядра
|
||||
│ ├── kernel.ld # Скрипт линковки ядра
|
||||
│ ├── user.c # Пользовательское пространство (системные вызовы)
|
||||
│ ├── user.h # Заголовочные файлы пользовательского пространства
|
||||
│ ├── user.ld # Скрипт линковки пользовательских программ
|
||||
│ ├── shell.c # Код интерактивной оболочки
|
||||
│ ├── common.c # Общие функции (printf, memset, memcpy, strcmp)
|
||||
│ ├── common.h # Общие определения
|
||||
│ ├── Makefile # Сборка проекта
|
||||
│ ├── disk/ # Файлы для файловой системы
|
||||
│ │ ├── hello.txt
|
||||
│ │ └── meow.txt
|
||||
│ └── run.sh # Скрипт запуска в QEMU для Linux
|
||||
│ └── run.bat # Скрипт запуска в QEMU для Windows
|
||||
└── README.md # Этот файл
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Сборка и запуск
|
||||
|
||||
### Требования
|
||||
|
||||
- **Clang** с поддержкой RISC-V target
|
||||
- **LLVM objcopy** (llvm-objcopy)
|
||||
- **QEMU** с поддержкой RISC-V 32-bit
|
||||
|
||||
### Запуск
|
||||
|
||||
```bash
|
||||
cd src
|
||||
./run.sh
|
||||
```
|
||||
BIN
screenshot.png
Normal file
BIN
screenshot.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 38 KiB |
@@ -2,10 +2,13 @@ CC = clang
|
||||
OBJCOPY=llvm-objcopy
|
||||
CFLAGS = -std=c11 -O2 -g3 -Wall -Wextra --target=riscv32 -ffreestanding -nostdlib
|
||||
|
||||
kernel: shell
|
||||
kernel: shell disk.tar
|
||||
$(CC) $(CFLAGS) -Wl,-Tkernel.ld -Wl,-Map=kernel.map -o kernel.elf kernel.c common.c shell.bin.o
|
||||
|
||||
shell:
|
||||
$(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
|
||||
|
||||
disk.tar: $(wildcard disk/*.txt)
|
||||
cd disk && tar cf ../disk.tar --format=ustar ./*.txt
|
||||
|
||||
@@ -31,6 +31,13 @@ typedef uint32_t vaddr_t;
|
||||
#define USER_BASE 0x1000000
|
||||
|
||||
#define SSTATUS_SPIE (1 << 5)
|
||||
#define SSTATUS_SUM (1 << 18)
|
||||
|
||||
#define SYS_PUTCHAR 1
|
||||
#define SYS_GETCHAR 2
|
||||
#define SYS_EXIT 3
|
||||
#define SYS_READFILE 4
|
||||
#define SYS_WRITEFILE 5
|
||||
|
||||
void *memset(void *buf, char c, size_t n);
|
||||
void *memcpy(void *dst, const void *src, size_t n);
|
||||
|
||||
167
src/kernel.c
167
src/kernel.c
@@ -21,6 +21,10 @@ struct sbiret sbi_call(long arg0, long arg1, long arg2, long arg3, long arg4,
|
||||
}
|
||||
|
||||
void putchar(char ch) { sbi_call(ch, 0, 0, 0, 0, 0, 0, 1); }
|
||||
long getchar(void) {
|
||||
struct sbiret ret = sbi_call(0, 0, 0, 0, 0, 0, 0, 2);
|
||||
return ret.error;
|
||||
}
|
||||
|
||||
// ===== MEMORY ALLOCATION =====
|
||||
paddr_t alloc_pages(uint32_t n) {
|
||||
@@ -134,15 +138,22 @@ __attribute__((naked)) __attribute__((aligned(4))) void kernel_entry(void) {
|
||||
"sret\n");
|
||||
}
|
||||
|
||||
void handle_syscall(struct trap_frame *f);
|
||||
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=%x\n", scause, stval,
|
||||
user_pc);
|
||||
}
|
||||
|
||||
WRITE_CSR(sepc, user_pc);
|
||||
}
|
||||
|
||||
// ===== Multiprocessing =====
|
||||
__attribute__((naked)) void switch_context(uint32_t *prev_sp,
|
||||
uint32_t *next_sp) {
|
||||
@@ -189,7 +200,9 @@ __attribute__((naked)) void user_entry(void) {
|
||||
"csrw sstatus, %[sstatus]\n"
|
||||
"sret\n"
|
||||
:
|
||||
: [sepc] "r"(USER_BASE), [sstatus] "r"(SSTATUS_SPIE));
|
||||
: [sepc] "r"(USER_BASE),
|
||||
[sstatus] "r"(SSTATUS_SPIE | SSTATUS_SUM) // updated
|
||||
);
|
||||
}
|
||||
|
||||
struct process *create_process(const void *image, size_t image_size) {
|
||||
@@ -380,6 +393,148 @@ void read_write_disk(void *buf, unsigned sector, int is_write) {
|
||||
memcpy(buf, blk_req->data, SECTOR_SIZE);
|
||||
}
|
||||
|
||||
// ===== Filesystem =====
|
||||
struct file files[FILES_MAX];
|
||||
uint8_t disk[DISK_MAX_SIZE];
|
||||
|
||||
int oct2int(char *oct, int len) {
|
||||
int dec = 0;
|
||||
for (int i = 0; i < len; i++) {
|
||||
if (oct[i] < '0' || oct[i] > '7')
|
||||
break;
|
||||
|
||||
dec = dec * 8 + (oct[i] - '0');
|
||||
}
|
||||
return dec;
|
||||
}
|
||||
|
||||
void fs_init(void) {
|
||||
for (unsigned sector = 0; sector < sizeof(disk) / SECTOR_SIZE; sector++)
|
||||
read_write_disk(&disk[sector * SECTOR_SIZE], sector, false);
|
||||
|
||||
unsigned off = 0;
|
||||
for (int i = 0; i < FILES_MAX; i++) {
|
||||
struct tar_header *header = (struct tar_header *)&disk[off];
|
||||
if (header->name[0] == '\0')
|
||||
break;
|
||||
|
||||
if (strcmp(header->magic, "ustar") != 0)
|
||||
PANIC("invalid tar header: magic=\"%s\"", header->magic);
|
||||
|
||||
int filesz = oct2int(header->size, sizeof(header->size));
|
||||
struct file *file = &files[i];
|
||||
file->in_use = true;
|
||||
strcpy(file->name, header->name);
|
||||
memcpy(file->data, header->data, filesz);
|
||||
file->size = filesz;
|
||||
printf("file: %s, size=%d\n", file->name, file->size);
|
||||
|
||||
off += align_up(sizeof(struct tar_header) + filesz, SECTOR_SIZE);
|
||||
}
|
||||
}
|
||||
void fs_flush(void) {
|
||||
memset(disk, 0, sizeof(disk));
|
||||
unsigned off = 0;
|
||||
for (int file_i = 0; file_i < FILES_MAX; file_i++) {
|
||||
struct file *file = &files[file_i];
|
||||
if (!file->in_use)
|
||||
continue;
|
||||
|
||||
struct tar_header *header = (struct tar_header *)&disk[off];
|
||||
memset(header, 0, sizeof(*header));
|
||||
strcpy(header->name, file->name);
|
||||
strcpy(header->mode, "000644");
|
||||
strcpy(header->magic, "ustar");
|
||||
strcpy(header->version, "00");
|
||||
header->type = '0';
|
||||
|
||||
int filesz = file->size;
|
||||
for (int i = sizeof(header->size); i > 0; i--) {
|
||||
header->size[i - 1] = (filesz % 8) + '0';
|
||||
filesz /= 8;
|
||||
}
|
||||
|
||||
int checksum = ' ' * sizeof(header->checksum);
|
||||
for (unsigned i = 0; i < sizeof(struct tar_header); i++)
|
||||
checksum += (unsigned char)disk[off + i];
|
||||
|
||||
for (int i = 5; i >= 0; i--) {
|
||||
header->checksum[i] = (checksum % 8) + '0';
|
||||
checksum /= 8;
|
||||
}
|
||||
|
||||
memcpy(header->data, file->data, file->size);
|
||||
off += align_up(sizeof(struct tar_header) + file->size, SECTOR_SIZE);
|
||||
}
|
||||
|
||||
for (unsigned sector = 0; sector < sizeof(disk) / SECTOR_SIZE; sector++)
|
||||
read_write_disk(&disk[sector * SECTOR_SIZE], sector, true);
|
||||
|
||||
printf("wrote %d bytes to disk\n", sizeof(disk));
|
||||
}
|
||||
|
||||
struct file *fs_lookup(const char *filename) {
|
||||
for (int i = 0; i < FILES_MAX; i++) {
|
||||
struct file *file = &files[i];
|
||||
if (!strcmp(file->name, filename))
|
||||
return file;
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
// ===== System calls =====
|
||||
void handle_syscall(struct trap_frame *f) {
|
||||
switch (f->a3) {
|
||||
case SYS_PUTCHAR:
|
||||
putchar(f->a0);
|
||||
break;
|
||||
case SYS_GETCHAR:
|
||||
while (1) {
|
||||
long ch = getchar();
|
||||
if (ch >= 0) {
|
||||
f->a0 = ch;
|
||||
break;
|
||||
}
|
||||
|
||||
yield();
|
||||
}
|
||||
break;
|
||||
case SYS_EXIT:
|
||||
printf("process %d exited\n", current_proc->pid);
|
||||
current_proc->state = PROC_EXITED;
|
||||
yield();
|
||||
PANIC("unreachable");
|
||||
case SYS_READFILE:
|
||||
case SYS_WRITEFILE: {
|
||||
const char *filename = (const char *)f->a0;
|
||||
char *buf = (char *)f->a1;
|
||||
int len = f->a2;
|
||||
struct file *file = fs_lookup(filename);
|
||||
if (!file) {
|
||||
printf("file not found: %s\n", filename);
|
||||
f->a0 = -1;
|
||||
break;
|
||||
}
|
||||
|
||||
if (len > (int)sizeof(file->data))
|
||||
len = file->size;
|
||||
|
||||
if (f->a3 == SYS_WRITEFILE) {
|
||||
memcpy(file->data, buf, len);
|
||||
file->size = len;
|
||||
fs_flush();
|
||||
} else {
|
||||
memcpy(buf, file->data, len);
|
||||
}
|
||||
|
||||
f->a0 = len;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
PANIC("unexpected syscall a3=%x\n", f->a3);
|
||||
}
|
||||
}
|
||||
|
||||
// ===== ENTRY POINT =====
|
||||
void kernel_main(void) {
|
||||
memset(__bss, 0, (size_t)__bss_end - (size_t)__bss);
|
||||
@@ -389,13 +544,7 @@ void kernel_main(void) {
|
||||
WRITE_CSR(stvec, (uint32_t)kernel_entry);
|
||||
|
||||
virtio_blk_init();
|
||||
|
||||
char buf[SECTOR_SIZE];
|
||||
read_write_disk(buf, 0, false);
|
||||
printf("first sector: %s\n", buf);
|
||||
|
||||
strcpy(buf, "hello from kernel!!!\n");
|
||||
read_write_disk(buf, 0, true);
|
||||
fs_init();
|
||||
|
||||
idle_proc = create_process(NULL, 0);
|
||||
idle_proc->pid = -1;
|
||||
|
||||
36
src/kernel.h
36
src/kernel.h
@@ -55,6 +55,7 @@ struct trap_frame {
|
||||
|
||||
#define PROC_UNUSED 0
|
||||
#define PROC_RUNNABLE 1
|
||||
#define PROC_EXITED 2
|
||||
|
||||
struct process {
|
||||
int pid;
|
||||
@@ -148,6 +149,41 @@ struct virtio_blk_req *blk_req;
|
||||
paddr_t blk_req_paddr;
|
||||
unsigned blk_capacity;
|
||||
|
||||
// ===== Filesystem =====
|
||||
#define FILES_MAX 2
|
||||
#define DISK_MAX_SIZE align_up(sizeof(struct file) * FILES_MAX, SECTOR_SIZE)
|
||||
|
||||
struct tar_header {
|
||||
char name[100];
|
||||
char mode[8];
|
||||
char uid[8];
|
||||
char gid[8];
|
||||
char size[12];
|
||||
char mtime[12];
|
||||
char checksum[8];
|
||||
char type;
|
||||
char linkname[100];
|
||||
char magic[6];
|
||||
char version[2];
|
||||
char uname[32];
|
||||
char gname[32];
|
||||
char devmajor[8];
|
||||
char devminor[8];
|
||||
char prefix[155];
|
||||
char padding[12];
|
||||
char data[];
|
||||
} __attribute__((packed));
|
||||
|
||||
struct file {
|
||||
bool in_use;
|
||||
char name[100];
|
||||
char data[1024];
|
||||
size_t size;
|
||||
};
|
||||
|
||||
// ===== System calls =====
|
||||
#define SCAUSE_ECALL 8
|
||||
|
||||
// ===== Macros =====
|
||||
#define READ_CSR(reg) \
|
||||
({ \
|
||||
|
||||
BIN
src/lorem.txt
BIN
src/lorem.txt
Binary file not shown.
@@ -1,6 +1,6 @@
|
||||
@echo off
|
||||
make kernel
|
||||
cd /d "C:\Program Files\qemu"
|
||||
qemu-system-riscv32.exe -machine virt -bios default -nographic -serial mon:stdio --no-reboot -drive id=drive0,file=lorem.txt,format=raw,if=none -device virtio-blk-device,drive=drive0,bus=virtio-mmio-bus.0 -kernel %~dp0kernel.elf
|
||||
qemu-system-riscv32.exe -machine virt -bios default -nographic -serial mon:stdio --no-reboot -drive id=drive0,file=disk.tar,format=raw,if=none -device virtio-blk-device,drive=drive0,bus=virtio-mmio-bus.0 -kernel %~dp0kernel.elf
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/bin/bash
|
||||
set -xue
|
||||
|
||||
make kernel && qemu-system-riscv32 -machine virt -bios default -nographic -serial mon:stdio --no-reboot -drive id=drive0,file=lorem.txt,format=raw,if=none -device virtio-blk-device,drive=drive0,bus=virtio-mmio-bus.0 -kernel kernel.elf
|
||||
make kernel && qemu-system-riscv32 -machine virt -bios default -nographic -serial mon:stdio --no-reboot -drive id=drive0,file=disk.tar,format=raw,if=none -device virtio-blk-device,drive=drive0,bus=virtio-mmio-bus.0 -kernel kernel.elf
|
||||
|
||||
35
src/shell.c
35
src/shell.c
@@ -1,6 +1,37 @@
|
||||
#include "user.h"
|
||||
|
||||
void main(void) {
|
||||
for (;;)
|
||||
;
|
||||
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 long\n");
|
||||
goto prompt;
|
||||
} else if (ch == '\r') {
|
||||
printf("\n");
|
||||
cmdline[i] = '\0';
|
||||
break;
|
||||
} else {
|
||||
cmdline[i] = ch;
|
||||
}
|
||||
}
|
||||
|
||||
if (strcmp(cmdline, "hello") == 0)
|
||||
printf("Hello world from shell!\n");
|
||||
else if (strcmp(cmdline, "exit") == 0)
|
||||
exit();
|
||||
else if (strcmp(cmdline, "readfile") == 0) {
|
||||
char buf[128];
|
||||
int len = readfile("./hello.txt", buf, sizeof(buf));
|
||||
buf[len] = '\0';
|
||||
printf("%s\n", buf);
|
||||
} else if (strcmp(cmdline, "writefile") == 0)
|
||||
writefile("./hello.txt", "Hello from shell!\n", 19);
|
||||
else
|
||||
printf("unknown command: %s\n", cmdline);
|
||||
}
|
||||
}
|
||||
|
||||
32
src/user.c
32
src/user.c
@@ -2,17 +2,41 @@
|
||||
|
||||
extern char __stack_top[];
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
void putchar(char ch) { syscall(SYS_PUTCHAR, ch, 0, 0); }
|
||||
|
||||
int getchar(void) { return syscall(SYS_GETCHAR, 0, 0, 0); }
|
||||
|
||||
int readfile(const char *filename, char *buf, int len) {
|
||||
return syscall(SYS_READFILE, (int)filename, (int)buf, len);
|
||||
}
|
||||
|
||||
int writefile(const char *filename, const char *buf, int len) {
|
||||
return syscall(SYS_WRITEFILE, (int)filename, (int)buf, len);
|
||||
}
|
||||
|
||||
__attribute__((noreturn)) void exit(void) {
|
||||
syscall(SYS_EXIT, 0, 0, 0);
|
||||
for (;;)
|
||||
;
|
||||
}
|
||||
|
||||
void putchar(char c) { /* Доделать*/ }
|
||||
|
||||
__attribute__((section(".text.start"))) __attribute__((naked)) void
|
||||
start(void) {
|
||||
__asm__ __volatile__(
|
||||
"mv sp, %[stack_top] \n"
|
||||
__asm__ __volatile__("mv sp, %[stack_top]\n"
|
||||
"call main\n"
|
||||
"call exit\n" ::[stack_top] "r"(__stack_top));
|
||||
}
|
||||
|
||||
11
src/user.h
11
src/user.h
@@ -2,5 +2,14 @@
|
||||
|
||||
#include "common.h"
|
||||
|
||||
__attribute__((noreturn)) void exit(void);
|
||||
struct sysret {
|
||||
int a0;
|
||||
int a1;
|
||||
int a2;
|
||||
};
|
||||
|
||||
void putchar(char ch);
|
||||
int getchar(void);
|
||||
int readfile(const char *filename, char *buf, int len);
|
||||
int writefile(const char *filename, const char *buf, int len);
|
||||
__attribute__((noreturn)) void exit(void);
|
||||
|
||||
Reference in New Issue
Block a user