Как написать ядро операционной системы
Перейти к содержимому

Как написать ядро операционной системы

  • автор:

Написание собственной работоспособной ОС за полгода

image

Здравствуйте! Всех категорически приветствую, сегодня хотел бы рассказать Вам о своём опыте написание работоспособной ОС под архитектуру x86.

Как-то весенней ночью у меня родилась гениальная идея — попробовать себя в написании собственной ОС, которая может позволить запускать программы, работать с устройствами, да и в общем выжимать всю мощь из Intel’овской архитектуры в своих нуждах: к примеру, для своей фабрики или чего-либо иного. Моей целью было и есть написание такой ОС, которая могла бы позволить максимальную производительность для каких-то конкретных задач, не тратя процессорное время на всяческие излишества. В основном я преследую лишь спортивный интерес, получение опыта для себя в системном программировании и написания драйверов для устройств, которые используются повсеместно. Что из этого вышло — решать вам, сразу говорю, что не надо писать комментарии про создание собственного дистрибутива линукса, и преследовал интерес написать всё «From scratch» — с нуля, дабы хорошо погрузиться в тему ОСдева. Сразу хочу выразить огромную благодарность Бенджамину Лунту и форуму OSDev, так же как их Вики. Бен помог мне разобраться с EHCI, что несомненно внесло огромный вклад в мою ОС — USB устройства, они везде! Так же передо мной стояла задача создать собственную архитектуру, удобную мне, не исключая использование стандартов ELF-файлов. Что же, давайте перейдем к сути.
UPD: всю инфу можно найти в группе тык, так же там есть пост с доками и образом(старым, сейчас дописываю доки для stable-версии)

Что сделано?

Сейчас моя ОС умеет работать с USB-флешками, мышками, клавиатурами, AHCI дисками, PCI IDE контроллером, APIC’ом и ACPI, реализована вытесняющая многозадачность, реализован запуск программ, поточная работа с файлами, SVGA драйвер, который работает на 0x118 режиме VBE, работают DNS, DHCP, TCP, UPD, IPv4, HTTP, полный драйвер FAT32, драйвер RTL8139(69) и Intel Gigabit Ethernet.

Оконная система вместе с моей реализацией SVGA позволяет выдать аж 120 FPS при полной перерисовке экрана. Давайте перейдем к тому, как это всё реализовано и вообще может работать.

Как это работает?

Для начала, я написал бутлоадер, который с FAT32 читает вторичный загрузчик с ядром. Второй загрузчик переходит в защищенный режим и прыгает в ядро, там я загружаю и настраиваю IDT, после чего инициализирую PCI-устройства, запускаю ядра и запускаю CMD.

Кто-то спросит, как же ты добился такого перфоманса с SVGA? Ответ прост: ассемблер, ассемблер и еще раз ассемблер. Не обошлось без SSE инструкций, которые очень ускоряют копирование памяти. К примеру, вот код для копирования видеобуфера в LFB(Linear Frame Buffer):

.byte 0x60#Save registers in stack mov %2,%%ecx #Repeat count to ecx mov %0,%%edi #Video memory start to edi mov %1,%%esi #Video buffer start to esi ww1sse2: movaps (%%esi),%%xmm0 #Copy 16 bytes to xmm0 from buffer movaps %%xmm0,(%%edi) #Copy from xmm0 to video memory movaps 16(%%esi),%%xmm0 #16 again, but + 16 from current movaps %%xmm0,16(%%edi) #16 again, but + 16 from current movaps 32(%%esi),%%xmm0 #16 again, but + 32 from current movaps %%xmm0,32(%%edi) #16 again, but + 32 from current movaps 48(%%esi),%%xmm0 #16 again, but + 48 from current movaps %%xmm0,48(%%edi) #16 again, but + 48 from current add $64,%%edi #Add 64 bytes to edi add $64,%%esi #Add 64 bytes to esi dec%%ecx#Decrement count #test %%ecx,%%ecx #Compare ecx with zero jnz ww1sse2 #If not zero, repeat again .byte 0x61 #Restore registers from stack 

Оконная система построена на ООП и, думаю, не нуждается в комментариях, всё почти как в шинде.

Менеджер памяти простейший — «Watermark Allocator». Распределение ресурсов осуществляется за счет того, что в ядре нет функций, которые могли бы нагадить друг другу, всё запросы сделаны через очереди и т.п.

Пока что нет никаких потоков ввода-вывода, но в ближайшее время они будут реализованы.
Система логических дисков аналогична MS-DOSу: одна буква — один диск. Поддерживаются как MBR разделы, так и GPT разделы.

Разработка драйверов устройств

Не обошлось тут и без этого — брать чужой код как-то некультурно и не практично, если раскапывать репозитории линукса — так можно и заблудиться.

Если честно, то я сторонник того, что главное в программе — функционал, но в тоже время считаю, что за графику можно и функционалом немного пожертвовать: к примеру, VIM.

Конечно, это стало самой интересной стадией разработки: прочитать тонны документации, а потом понять, что ты поставил return для отладки, из-за чего часть структуры не заполнялась вовсе. Думаю, что это достаточно хорошо заставляет тебя напрячь мозг, ибо даже после десятого прочтения доков какие-то аспекты всё равно остаются тебе непонятны, и приходиться заниматься долгой отладкой на реальном железе.

Отладка

Отладка ОС занимает огромную кучу времени: скомпилить сорсы, скопировать на виртуальный диск, сохранить образ диска, проверить на эмуляторе, после чего скопировать файлы на флешку и проверить уже на реальном железе. А главное — никаких отладчиков, выводи в текстовый режим, либо в окошечки — повисло — ставь return’ы.

Итоги

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

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

Руководство по созданию ядра для x86-системы. Часть 1. Просто ядро

Обложка поста Руководство по созданию ядра для x86-системы. Часть 1. Просто ядро

Давайте напишем простое ядро, которое можно загрузить при помощи бутлоадера GRUB x86-системы. Это ядро будет отображать сообщение на экране и ждать.

Руководство по созданию ядра для x86-системы. Часть 1. Просто ядро 1

Как загружается x86-система?

Прежде чем мы начнём писать ядро, давайте разберёмся, как система загружается и передаёт управление ядру.

В большей части регистров процессора при запуске уже находятся определённые значения. Регистр, указывающий на адрес инструкций (Instruction Pointer, EIP), хранит в себе адрес памяти, по которому лежит исполняемая процессором инструкция. EIP по умолчанию равен 0xFFFFFFF0. Таким образом, x86-процессоры на аппаратном уровне начинают работу с адреса 0xFFFFFFF0. На самом деле это — последние 16 байт 32-битного адресного пространства. Этот адрес называется вектором перезагрузки (reset vector).

Теперь карта памяти чипсета гарантирует, что 0xFFFFFFF0 принадлежит определённой части BIOS, не RAM. В это время BIOS копирует себя в RAM для более быстрого доступа. Адрес 0xFFFFFFF0 будет содержать лишь инструкцию перехода на адрес в памяти, где хранится копия BIOS.

Так начинается исполнение кода BIOS. Сперва BIOS ищет устройство, с которого можно загрузиться, в предустановленном порядке. Ищется магическое число, определяющее, является ли устройство загрузочным (511-ый и 512-ый байты первого сектора должны равняться 0xAA55).

Когда BIOS находит загрузочное устройство, она копирует содержимое первого сектора устройства в RAM, начиная с физического адреса 0x7c00; затем переходит на адрес и исполняет загруженный код. Этот код называется бутлоадером.

Бутлоадер загружает ядро по физическому адресу 0x100000. Этот адрес используется как стартовый во всех больших ядрах на x86-системах.

Все x86-процессоры начинают работу в простом 16-битном режиме, называющимся реальным режимом. Бутлоадер GRUB переключает режим в 32-битный защищённый режим, устанавливая нижний бит регистра CR0 в 1. Таким образом, ядро загружается в 32-битном защищённом режиме.

Заметьте, что в случае с ядром Linux GRUB видит протоколы загрузки Linux и загружает ядро в реальном режиме. Ядро самостоятельно переключается в защищённый режим.

Что нам нужно?

  • x86-компьютер;
  • Linux;
  • ассемблер NASM;
  • gcc;
  • ld (GNU Linker);
  • grub;

Исходники можно найти на GitHub.

Задаём точку входа на ассемблере

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

Как же нам сделать так, чтобы этот код обязательно был именно исходной точкой?

Мы будем использовать скрипт-линковщик, который соединяет объектные файлы для создания конечного исполняемого файла. В этом скрипте мы явно укажем, что хотим загрузить данные по адресу 0x100000.

Вот код на ассемблере:

;;kernel.asm bits 32 ;nasm directive - 32 bit section .text global start extern kmain ;kmain is defined in the c file start: cli ;block interrupts mov esp, stack_space ;set stack pointer call kmain hlt ;halt the CPU section .bss resb 8192 ;8KB for stack stack_space: 

Первая инструкция, bits 32 , не является x86-ассемблерной инструкцией. Это директива ассемблеру NASM, задающая генерацию кода для процессора, работающего в 32-битном режиме. В нашем случае это не обязательно, но вообще полезно.

Со второй строки начинается секция с кодом.

global — это ещё одна директива NASM, делающая символы исходного кода глобальными. Таким образом, линковщик знает, где находится символ start — наша точка входа.

kmain — это функция, которая будет определена в файле kernel.c . extern значит, что функция объявлена где-то в другом месте.

Затем идёт функция start , вызывающая функцию kmain и останавливающая процессор инструкцией hlt . Именно поэтому мы заранее отключаем прерывания инструкцией cli .

В идеале нам нужно выделить немного памяти и указать на неё указателем стека ( esp ). Однако, похоже, что GRUB уже сделал это за нас. Тем не менее, вы всё равно выделим немного места в секции BSS и переместим на её начало указатель стека. Мы используем инструкцию resb , которая резервирует указанное число байт. Сразу перед вызовом kmain указатель стека ( esp ) устанавливается на нужное место инструкцией mov .

Ядро на Си

В kernel.asm мы совершили вызов функции kmain() . Таким образом, наш “сишный” код должен начать исполнение с kmain() :

/* * kernel.c */ void kmain(void) < const char *str = "my first kernel"; char *vidptr = (char*)0xb8000; //video mem begins here. unsigned int i = 0; unsigned int j = 0; /* this loops clears the screen * there are 25 lines each of 80 columns; each element takes 2 bytes */ while(j < 80 * 25 * 2) < /* blank character */ vidptr[j] = ' '; /* attribute-byte - light grey on black screen */ vidptr[j+1] = 0x07; j = j + 2; >j = 0; /* this loop writes the string to video memory */ while(str[j] != '\0') < /* the character's ascii */ vidptr[i] = str[j]; /* attribute-byte: give character black bg and light grey fg */ vidptr[i+1] = 0x07; ++j; i = i + 2; >return; > 

Всё, что сделает наше ядро — очистит экран и выведет строку “my first kernel”.

Сперва мы создаём указатель vidptr , который указывает на адрес 0xb8000. С этого адреса в защищённом режиме начинается “видеопамять”. Для вывода текста на экран мы резервируем 25 строк по 80 ASCII-символов, начиная с 0xb8000.

Каждый символ отображается не привычными 8 битами, а 16. В первом байте хранится сам символ, а во втором — attribute-byte . Он описывает форматирование символа, например, его цвет.

Для вывода символа s зелёного цвета на чёрном фоне мы запишем этот символ в первый байт и значение 0x02 во второй. 0 означает чёрный фон, 2 — зелёный цвет текста.

Вот таблица цветов:

0 - Black, 1 - Blue, 2 - Green, 3 - Cyan, 4 - Red, 5 - Magenta, 6 - Brown, 7 - Light Grey, 8 - Dark Grey, 9 - Light Blue, 10/a - Light Green, 11/b - Light Cyan, 12/c - Light Red, 13/d - Light Magenta, 14/e - Light Brown, 15/f – White. 

В нашем ядре мы будем использовать светло-серый текст на чёрном фоне, поэтому наш байт-атрибут будет иметь значение 0x07.

В первом цикле программа выводит пустой символ по всей зоне 80×25. Это очистит экран. В следующем цикле в “видеопамять” записываются символы из нуль-терминированной строки “my first kernel” с байтом-атрибутом, равным 0x07. Это выведет строку на экран.

Связующая часть

Мы должны собрать kernel.asm в объектный файл, используя NASM; затем при помощи GCC скомпилировать kernel.c в ещё один объектный файл. Затем их нужно присоединить к исполняемому загрузочному ядру.

Для этого мы будем использовать связывающий скрипт, который передаётся ld в качестве аргумента.

/* * link.ld */ OUTPUT_FORMAT(elf32-i386) ENTRY(start) SECTIONS < . = 0x100000; .text : < *(.text) >.data : < *(.data) >.bss : < *(.bss) >> 

Сперва мы зададим формат вывода как 32-битный Executable and Linkable Format (ELF). ELF — это стандарный формат бинарных файлов Unix-систем архитектуры x86. ENTRY принимает один аргумент, определяющий имя символа, являющегося точкой входа. SECTIONS — это самая важная часть. В ней определяется разметка нашего исполняемого файла. Мы определяем, как должны соединяться разные секции и где их разместить.

В скобках после SECTIONS точка (.) отображает счётчик положения, по умолчанию равный 0x0. Его можно изменить, что мы и делаем.

Смотрим на следующую строку: .text : < *(.text) >. Звёздочка (*) — это специальный символ, совпадающий с любым именем файла. Выражение *(.text) означает все секции .text из всех входных файлов.

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

Аналогично всё происходит и с другим секциями.

Grub и Multiboot

Теперь все файлы готовы к созданию ядра. Но остался ещё один шаг.

Существует стандарт загрузки x86-ядер с использованием бутлоадера, называющийся Multiboot specification. GRUB загрузит наше ядро, только если оно удовлетворяет этим спецификациям.

Следуя им, ядро должно содержать заголовок в своих первых 8 килобайтах. Кроме того, этот заголовок должен содержать 3 поля, являющихся 4 байтами:

  • магическое поле: содержит магическое число 0x1BADB002 для идентификации ядра.
  • поле flags: нам оно не нужно, установим в ноль.
  • поле checksum: если сложить его с предыдущими двумя, должен получиться ноль.

Наш kernel.asm станет таким:

;;kernel.asm ;nasm directive - 32 bit bits 32 section .text ;multiboot spec align 4 dd 0x1BADB002 ;magic dd 0x00 ;flags dd - (0x1BADB002 + 0x00) ;checksum. m+f+c should be zero global start extern kmain ;kmain is defined in the c file start: cli ;block interrupts mov esp, stack_space ;set stack pointer call kmain hlt ;halt the CPU section .bss resb 8192 ;8KB for stack stack_space: 

Строим ядро

Теперь мы создадим объектные файлы из kernel.asm и kernel.c и свяжем их, используя наш скрипт.

nasm -f elf32 kernel.asm -o kasm.o 

Эта строка запустит ассемблер для создания объектного файла kasm.o в формате ELF-32.

gcc -m32 -c kernel.c -o kc.o 

Опция “-c” гарантирует, что после компиляции не произойдёт скрытого линкования.

ld -m elf_i386 -T link.ld -o kernel kasm.o kc.o 

Это запустит линковщик с нашим скриптом и создаст исполняемый файл, называющийся kernel.

Настраиваем grub и запускаем ядро

GRUB требует, чтобы имя ядра удовлетворяло шаблону kernel- . Поэтому переименуйте ядро. Своё я назвал kernel-701.

Теперь поместите его в директорию /boot. Для этого понадобятся права суперпользователя.

В конфигурационном файле GRUB grub.cfg добавьте следующее:

title myKernel root (hd0,0) kernel /boot/kernel-701 ro 

Не забудьте убрать директиву hiddenmenu , если она есть.

Перезагрузите компьютер, и вы увидите список ядер с вашим в том числе. Выберите его, и вы увидите:

Руководство по созданию ядра для x86-системы. Часть 1. Просто ядро 2

Это ваше ядро! В следующей части добавим систему ввода / вывода.

P.S.

  • Для любых фокусов с ядром лучше использовать виртуальную машину.
  • Для запуска ядра в grub2 конфиг должен выглядеть так:menuentry ‘kernel 7001’
  • если вы хотите использовать эмулятор qemu , используйте:qemu-system-i386 -kernel kernel

Собственная операционная система на Rust

Этот блог посвящен написанию маленькой операционной системы на языке программирования Rust. Каждый пост — это маленькое руководство, включающее в себя весь необходимый код, — вы сможете следовать ему, если пожелаете. Исходный код также доступен в соотвестующем репозитории на Github.

Последний пост: Async/Await

Bare Bones

Независимый бинарный файл на Rust

Первый шаг в создании собственного ядра операционной системы — это создание исполняемого файла на Rust, который не будет подключать стандартную библиотеку. Именно это дает возможность запускать Rust код на голом железе без слоя операционной системы.

Минимально возможное ядро на Rust

В этом посте мы создадим минимальное 64-битное ядро на Rust для архитектуры x86_64. Мы будем отталкиваться от независимого бинарного файла из предыдущего поста для создания загрузочного образа диска, который может что-то выводить на экран.

VGA Text Mode

The VGA text mode is a simple way to print text to the screen. In this post, we create an interface that makes its usage safe and simple by encapsulating all unsafety in a separate module. We also implement support for Rust’s formatting macros.

Testing

This post explores unit and integration testing in no_std executables. We will use Rust’s support for custom test frameworks to execute test functions inside our kernel. To report the results out of QEMU, we will use different features of QEMU and the bootimage tool.

Interrupts

CPU Exceptions

CPU exceptions occur in various erroneous situations, for example, when accessing an invalid memory address or when dividing by zero. To react to them, we have to set up an interrupt descriptor table that provides handler functions. At the end of this post, our kernel will be able to catch breakpoint exceptions and resume normal execution afterward.

Double Faults

This post explores the double fault exception in detail, which occurs when the CPU fails to invoke an exception handler. By handling this exception, we avoid fatal triple faults that cause a system reset. To prevent triple faults in all cases, we also set up an Interrupt Stack Table to catch double faults on a separate kernel stack.

Hardware Interrupts

In this post, we set up the programmable interrupt controller to correctly forward hardware interrupts to the CPU. To handle these interrupts, we add new entries to our interrupt descriptor table, just like we did for our exception handlers. We will learn how to get periodic timer interrupts and how to get input from the keyboard.

Memory Management

Introduction to Paging

This post introduces paging, a very common memory management scheme that we will also use for our operating system. It explains why memory isolation is needed, how segmentation works, what virtual memory is, and how paging solves memory fragmentation issues. It also explores the layout of multilevel page tables on the x86_64 architecture.

Paging Implementation

This post shows how to implement paging support in our kernel. It first explores different techniques to make the physical page table frames accessible to the kernel and discusses their respective advantages and drawbacks. It then implements an address translation function and a function to create a new mapping.

Heap Allocation

This post adds support for heap allocation to our kernel. First, it gives an introduction to dynamic memory and shows how the borrow checker prevents common allocation errors. It then implements the basic allocation interface of Rust, creates a heap memory region, and sets up an allocator crate. At the end of this post, all the allocation and collection types of the built-in alloc crate will be available to our kernel.

Allocator Designs

This post explains how to implement heap allocators from scratch. It presents and discusses different allocator designs, including bump allocation, linked list allocation, and fixed-size block allocation. For each of the three designs, we will create a basic implementation that can be used for our kernel.

Multitasking

Async/Await

In this post, we explore cooperative multitasking and the async/await feature of Rust. We take a detailed look at how async/await works in Rust, including the design of the Future trait, the state machine transformation, and pinning. We then add basic support for async/await to our kernel by creating an asynchronous keyboard task and a basic executor.

Subscribe

Receive notifications about new posts and other major changes! You can either:

  • Subscribe to our RSS/Atom Feed,
  • Subscribe to this GitHub issue, or
  • Subscribe to our email newsletter.

Status Updates

These posts give a regular overview of the most important changes to the blog and the tools and libraries behind the scenes.

  • This Month in Rust OSDev (December 2023)
  • This Month in Rust OSDev (November 2023)
  • This Month in Rust OSDev (October 2023)
  • This Month in Rust OSDev (September 2023)
  • This Month in Rust OSDev (August 2023)
  • view all »

First Edition

You are currently viewing the second edition of “Writing an OS in Rust”. The first edition is very different in many aspects, for example it builds upon the GRUB bootloader instead of using the `bootloader` crate. In case you’re interested in it, it is still available. Note that the first edition is no longer updated and might contain outdated information. read the first edition »

Support Me

Creating and maintaining this blog and the associated libraries is a lot of work, but I really enjoy doing it. By supporting me, you allow me to invest more time in new content, new features, and continuous maintenance.

The best way to support me is to sponsor me on GitHub, since they don’t charge any fees. If you prefer other platforms, I also have Patreon and Donorbox accounts. The latter is the most flexible as it supports multiple currencies and one-time contributions.

Все операционные системы написаны на С. Не пора ли переписать их на Rust?

Системный программист рассказывает, почему операционную систему нельзя написать на Python или Java, но можно на Rust.

Кадр: сериал «Мистер Робот»

Марина Демидова

Марина Демидова

Программист, консультант, специалист по документированию. Легко и доступно рассказывает о сложных вещах в программировании и дизайне.

Брайан Кантрилл

об авторе

Системный программист с двадцатилетним стажем, сооснователь и технический директор Oxide Computer Company. С 1996 по 2006 год разрабатывал ядро ОС Solaris в Sun Microsystems, а с 2010 по 2019 год — SmartOS в компании Joyent.

В докладе на конференции QCon Брайан Кантрилл объясняет, почему многим системным программистам нравится Rust и почему на этом языке будут писать современные операционные системы.

В статье мы пересказали основные идеи доклада.

Если я спрошу, не пора ли переписать все операционные системы на Rust, по закону заголовков Беттериджа вы можете сказать «нет». Но я приведу много доводов, почему стоит ответить «да». Ещё я расскажу, зачем ставлю в названии статьи вопросительный, а не восклицательный знак.

Что такое операционная система

На первый взгляд это простой вопрос, но начнём с определения программного обеспечения. Считается ли им, например, алгоритм Евклида ? Нет, потому что он появился раньше компьютеров.

То же и с операционной системой. ОС — это то, что работает поверх железа. Но не только физического, ведь её можно запустить в облаке на виртуальном оборудовании. Получается, что и здесь всё сложно.

Если говорить проще, операционная система — программа, которая позволяет запускать на железе стороннее ПО. Если ОС перестаёт работать, все остальные процессы завершаются вместе с ней.

У операционной системы есть ядро — часть с полным доступом к ресурсам процессора, памяти и другого оборудования. Но кроме ядра в ОС ещё есть библиотеки, команды и демоны .

Как развивались операционные системы

Подобия ОС писали на ассемблере уже после Второй мировой. Но полноценная операционная система появилась в 1961 году — это была MCP , написанная на языке ESPOL . Её создали для компьютера Burroughs B5000, который опередил своё время, но был коммерчески бесполезен.

В 1964 году в MIT попытались развить идею и начали разрабатывать CTSS — мультизадачную операционную систему, которая умела распределять аппаратные ресурсы между разными программами и пользователями. Её назвали Multics.

Создатели Multics хотели сделать всё правильно, но случилось то, что я называю синдромом второй системы. Они решили переписать с нуля почти все компоненты, поэтому проект получился огромный, а сроки сдачи переносились всё дальше.

ОС писали на языке PL/I , а его компилятор отдали разрабатывать аутсорсинговой компании, но у той ничего не вышло. Чтобы спасти положение, команда учёных создала язык EPL и продолжила работу на нём.

В 1969 году Bell Labs, одна из участниц программы, посчитала проект бесперспективным и вышла из него. А сотрудник Bell Labs Кен Томпсон сделал на его базе UNIX — операционную систему для мини-компьютера PDP-7.

UNIX и языки высокого уровня

Некоторые думают, что UNIX написали на С, но это не так — её полностью разработали на ассемблере. В отличие от авторов Multics, у разработчиков UNIX не было синдрома второй системы. Её создатели сначала делали работающий компонент, а потом писали к нему документацию.

В том же 1969 году в Bell Labs создали язык B. На нём написали некоторые утилиты UNIX, причём и тут не обошлось без ассемблера.

Но разработчиков UNIX не устраивала производительность языка B. Да, они считали, что операционную систему можно сделать на языке высокого уровня , но B для этого не годился — в нём не хватало байтовой адресации .

Тогда придумали язык С. В первой версии в нём не было структур — они появились только к 1973 году (понадобились для ядра UNIX). По той же причине добавили и битовые поля .

Язык дорабатывали постепенно — как раз в этом и его сила, и его слабость. Меня бесит, что в нём нет логического XOR — один из создателей языка объяснял это тем, что XOR не подходит для вычислений по короткой схеме .

Но С всё равно развивался. В нём появились макросы — а это одна из вещей, которые мне очень нравятся и в Rust. То же и с препроцессорами — многие не любят их использовать, но они важны для DTrace , ZFS и других систем.

Операционные системы в 1980–1990-е годы

В 1980-е годы С доминировал. На нём были написаны почти все операционные системы. Но в 1990-е стало популярным объектно-ориентированное программирование, и ОС на C объявили устаревшими. Считали, что скоро их заменят ОС на Java и C++. Но эти попытки провалились, и одна из причин — синдром второй системы.

Я помню 1990-е годы, когда гендиректором Apple был Джил Амелио. Компания громко рекламировала Mac OS Copland, но никак не могла её выпустить. Остальные же компании из Кремниевой долины ждали, пока Apple разорится, чтобы по дешёвке выкупить её имущество.

А Sun Microsystems разрабатывала операционную систему Spring — на C++. Ещё в студенчестве мне в руки попал CD-диск со Spring: я попробовал её установить, но компьютер не запустился. Оказалось, что у меня только 32 МБ оперативной памяти, а система требовала минимум 128 МБ. В наше время это всё равно что 2 ТБ RAM.

Не получилось сделать и ОС на Java. В этом языке нет беззнакового типа данных , без которого системе трудно взаимодействовать с железом.

Операционные системы в 2000-х

Когда появился Linux, написание систем на языке C стало мейнстримом. Много кто пробовал создать ядро ОС на C++, но из этих попыток ничего не вышло. Хотя есть исключения — та же Haiku. С одной оговоркой — всё-таки это был любительский проект, ничего серьёзного.

В то же время, в начале 2000-х, главными языками были Java и C++. А потом заметными стали Ruby, Python и JavaScript. Но все они фокусируются на увеличении скорости разработки. Никто в здравом уме не пробовал создать ОС на Ruby или Python.

JavaScript сначала был не слишком популярным. Но им стали активно пользоваться в 2006 году, когда все поняли, что в браузерах появился полноценный язык, на котором можно создавать веб-приложения. Для меня JavaScript — это замаскированный Lisp в браузере, но без многочисленных скобок.

К 2010-м годам системные программисты хотели получить язык, который был бы таким же производительным, как C, но в то же время обладал бы преимуществами других языков — например, методом split () и регулярными выражениями .

Из этого вышло много интересного. Например, появились мощные среды выполнения JavaScript, в частности движок V8, который можно было запускать на сервере. Потом появился Node.js. Мне очень нравится JS, но в мире системного программного обеспечения с ним было бы чересчур много проблем.

Некоторые сотрудники Bell Labs перешли в Google и разработали там язык Go. Он лишён многих недостатков JavaScript, но у него есть некоторые особенности.

В своём выступлении разработчик из Google Адин Сканнелл рассказал, что на Go можно создать ядро операционной системы, но у Go и JavaScript есть общая проблема — сборщики мусора . Они только замедляют системные программы, а потому не подходят для создания операционных систем.

Что особенного в языке Rust

Я написал на C++ сто тысяч строк кода и больше никогда не хочу к нему возвращаться. Меня всё сильнее и сильнее интересует Rust, потому что мне нравятся его ценности — это быстрый и безопасный язык для системного ПО.

Одна из самых интересных особенностей Rust — понятие владения. Язык статически определяет, кому что принадлежит. Компилятор выделяет вам память, а затем освобождает её, когда вы закончите с ней работать. А если он не знает, кому принадлежит память, то он её одалживает. Это называется заимствованием. Такой процесс исключает утечку ресурсов и обеспечивает безопасность — одна программа не может перезаписать память другой. И это действительно важно.

В Rust есть и другие функции, которые мне нравятся:

Алгебраические типы. Позволяют превосходно обрабатывать ошибки, работают примерно как объединение в C.

Гигиенические макросы. У меня их не было в C, и я буквально не знал, как пишется слово «гигиенический». Но такой макрос, который можно получить в выводе абстрактного синтаксического дерева вместе с другим программным кодом, — это удивительно.

Продуманные внешние функции. Интерфейс внешней функции в Rust очень хорошо продуман — вы можете взять код C и вставить его в программу на Rust или наоборот.

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

Можно ли написать операционную систему на Rust

Я думаю, что на Rust можно смело писать операционные системы. Подобный язык мы ждали последние 30 лет.

Первая попытка, о которой я знаю, — система Weenix. Её создал студент Захари Эспириту для своей дипломной работы. Он подробно описывает проблемы, с которыми столкнулся в процессе.

С 2015 года небольшие операционные системы на Rust появлялись одна за другой: вышли Redox, Tock, IntermezzOS, BlogOS, QuiltOS, Rux и другие. IntermezzOS и BlogOS — учебные проекты, а Tock создана для интернета вещей.

К сожалению, у этих ОС нет бинарной совместимости с Linux, а значит, на них не запускается большая часть линуксовых программ. Поэтому я думаю, что гибридные подходы — самый правильный способ написать ОС на Rust:

Подход В чём его смысл
Добавлять в ядра на С новые компоненты на Rust Мне нравится, что С и Rust умеют взаимодействовать друг с другом. Можно сохранить ядро, написанное на C или ассемблере, а на Rust разрабатывать драйверы, файловые системы и прошивки
Писать на Rust компоненты операционной системы В ОС входит не только ядро, но и утилиты, демоны, диспетчер служб, диспетчер устройств, управление сбоями и отладчики. Их можно с успехом переписать на Rust
Переписать прошивку с DOS на Rust Много критически важного системного ПО работает под управлением DOS. Большинство проблем этой прошивки можно решить, если переписать её на Rust. Тот, кто это сделает, окажет человечеству огромную услугу

Почему я жду появления гибридных операционных систем на С и Rust

Я очень оптимистично отношусь к Rust. Думаю, мы можем использовать его в операционных системах: в прошивке, в расширениях для ядра, при создании компонентов ОС, хотя на практике наверняка будет много трудностей.

Rust совместим с системами на C, и в этом его красота. Такой язык открывает новые возможности.

Читайте также:

  • Facebook* заржавел: зачем корпорации поддерживать язык Rust
  • Задача про трёх тестировщиков на собеседовании
  • Must read. 7 свежих англоязычных статей о FreeBSD, NetBSD и OpenBSD

Закон заголовков Беттериджа говорит, что на любой заголовок с вопросительным знаком можно ответить «нет».

Алгоритм Евклида помогает находить наибольший целый делитель двух целых чисел.

Демон (daemon) — фоновая программа в UNIX-системе. Она умеет выполнять только одну задачу. В Windows аналог демонов — службы.

MCP (Master Control Program) — операционная система, которую разработала компания Burroughs Corporation.

ESPOL (Executive Systems Problem Oriented Language) — язык, производный от ALGOL 60.

PL/I (Programming Language I) — язык программирования, который создали в 1964 году для научных, инженерных и бизнес-вычислений.

EPL стал упрощённым диалектом PL/I.

Это язык, который больше похож на человеческий, чем на машинный код. Чтобы перевести его на язык компьютера, нужен компилятор.

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

Структура — объединение нескольких объектов под одним именем. Объектами могут быть переменные, массивы, указатели и так далее.

Битовые поля (bit fields) позволяют хранить целые числа, используя меньший объём памяти.

XOR — исключающее «или».

При вычислениях по короткой схеме второй логический оператор не вычисляется, если сработал первый. Например, если левый операнд AND выдал значение false, общее значение выражения также будет false. Второй оператор проверяться не будет.

DTrace — фреймворк компании Sun Microsystems, который используют для отладки приложений.

ZFS (Zettabyte File System) — файловая система, которую создали в Sun Microsystems.

Беззнаковые числа (тип unsigned) всегда положительны и не могут принимать значение меньше нуля.

Метод split в разных языках делит строку на несколько строк по заданному разделителю.

Регулярные выражения позволяют проводить массовые операции со строками в тексте.

В языках вроде Go, JavaScript, C# и Java есть сборщики мусора — специальные процессы, которые сканируют память и удаляют те ячейки, которые уже не нужны приложению.

Системный программист разрабатывает операционные системы, утилиты, драйверы и другие похожие программы.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *