Простая файловая система

Чтобы реализовать полноценную систему пакетной обработки, нам нужно предоставить системе несколько программ. Раньше мы хранили программы в виде файлов на ramdisk, но если количество программ увеличится, нам нужно знать, где именно на ramdisk находится каждая программа. Наш ramdisk уже предоставляет интерфейс чтения и записи, что позволяет удобно обращаться к содержимому в любой позиции, — для Nanos-lite это, кажется, не составляет никакой сложности; с другой стороны, пользовательским программам тоже нужно обрабатывать данные, и эти данные тоже могут быть организованы в файлы, — так как же пользовательской программе узнать, в какой позиции ramdisk находится файл? Тем более что файлы могут динамически добавляться и удаляться, а пользовательская программа об этом ничего не знает. Это показывает, что предоставлять пользовательским программам напрямую интерфейс чтения-записи ramdisk неосуществимо. Операционной системе нужно предоставить пользовательским программам более высокоуровневую абстракцию поверх драйвера носителя хранения — а именно файл.

По своей сути файл — это последовательность байтов, а также некоторые дополнительные атрибуты. Здесь мы сначала обсудим файлы в обычном понимании. При таком подходе именно эти дополнительные атрибуты хранят соответствие файла его месту хранения на ramdisk. Чтобы управлять этими соответствиями и одновременно предоставлять вышестоящему уровню интерфейс файловых операций, нам нужно реализовать файловую систему в Nanos-lite.

Не пугайтесь слов «файловая система» — наши требования к файловой системе не такие уж сложные, поэтому мы можем определить простую файловую систему sfs (Simple File System):

  • размер каждого файла фиксирован;
  • при записи в файл нельзя превышать исходный размер файла;
  • количество файлов фиксировано, создавать новые файлы нельзя;
  • каталогов нет.

Раз количество и размер файлов фиксированы, мы, естественно, можем закрепить каждый файл за определённой позицией на ramdisk. Эти упрощения значительно снижают сложность реализации файловой системы. Разумеется, настоящие файловые системы гораздо сложнее sfs.

Договоримся, что файлы располагаются один за другим начиная с самого начала ramdisk:

0
+-------------+---------+----------+-----------+--
|    file0    |  file1  |  ......  |   filen   |
+-------------+---------+----------+-----------+--
 \           / \       /            \         /
  +  size0  +   +size1+              + sizen +

Чтобы записывать имена и размеры отдельных файлов в ramdisk, нам ещё понадобится «таблица учёта файлов».

Makefile в Nanos-lite уже содержит скрипт для ведения этой информации — сначала внесите в nanos-lite/Makefile следующее изменение:

--- nanos-lite/Makefile
+++ nanos-lite/Makefile
@@ -1,2 +1,2 @@
-#HAS_NAVY = 1
+HAS_NAVY = 1
 RAMDISK_FILE = build/ramdisk.img

После этого запуск make ARCH=$ISA-nemu update автоматически скомпилирует программы в Navy и объединит всё содержимое директории navy-apps/fsimg/ в образ ramdisk navy-apps/build/ramdisk.img, а также сгенерирует таблицу учёта файлов для этого образа ramdisk — navy-apps/build/ramdisk.h. Makefile в Nanos-lite подключит их к проекту через символические ссылки.

Не забывайте обновлять файл образа

Если вы изменили содержимое в Navy, пожалуйста, не забудьте обновить файл образа с помощью указанной выше команды.

«Таблица учёта файлов» на самом деле представляет собой массив, каждый элемент которого — структура:

typedef struct {
  char *name;         // имя файла
  size_t size;        // размер файла
  size_t disk_offset;  // смещение файла в ramdisk
} Finfo;

В sfs все три этих поля неизменны и фиксированы. При этом имена файлов немного отличаются от нашей привычной практики: поскольку в sfs нет каталогов, мы считаем разделитель каталогов / тоже частью имени файла — например, /bin/hello является полным именем файла. Такой подход, по сути, неявно задаёт иерархическую структуру каталогов, и при небольшом количестве файлов он одновременно прост и эффективен.

Имея эту информацию, уже можно реализовать самые базовые операции чтения и записи файлов:

size_t read(const char *filename, void *buf, size_t len);
size_t write(const char *filename, const void *buf, size_t len);

Но в настоящих операционных системах у такого подхода — напрямую использовать имя файла в качестве параметра операций чтения-записи — есть недостатки. Например, когда мы просматриваем файл с помощью утилиты less:

cat file | less

Утилита cat хочет записать содержимое файла в стандартный ввод утилиты less, но обозначить стандартный ввод less с помощью имени файла у нас никак не получится! На самом деле в операционной системе действительно существует немало «безымянных» файлов. Чтобы управлять ими единообразно, мы хотим обозначать файл числом — этим числом является дескриптор файла (file descriptor). Один дескриптор файла соответствует одному открытому файлу, и операционная система ведёт соответствие между дескрипторами файлов и конкретными файлами. Поэтому мы совершенно естественным образом открываем файл через системный вызов open(), который возвращает соответствующий дескриптор файла.

В Nanos-lite, поскольку количество файлов в sfs фиксировано, мы можем просто возвращать пользовательской программе в качестве дескриптора соответствующего файла индекс в таблице учёта файлов. После этого все файловые операции обозначают файл через дескриптор:

size_t read(int fd, void *buf, size_t len);
size_t write(int fd, const void *buf, size_t len);
int close(int fd);

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

Смещение файла и пользовательские программы

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

Поскольку Nanos-lite — упрощённая операционная система, описанная выше проблема пока не возникнет, и ради простоты реализации мы всё же будем хранить смещение в таблице учёта файлов.

Смещение можно изменять через системный вызов lseek(), что позволяет читать и записывать данные в любой позиции файла:

size_t lseek(int fd, size_t offset, int whence);

Чтобы пользовательским программам было удобно работать со стандартным вводом-выводом, операционная система заранее готовит три стандартных дескриптора файлов:

#define FD_STDIN 0
#define FD_STDOUT 1
#define FD_STDERR 2

Они соответствуют стандартному вводу stdin, стандартному выводу stdout и стандартному потоку ошибок stderr. Часто используемая нами printf в итоге вызывает write(FD_STDOUT, buf, len) для вывода; а scanf будет считывать ввод, вызывая read(FD_STDIN, buf, len).

file_table, определённый в nanos-lite/src/fs.c, будет подключать nanos-lite/src/files.h, а перед ним будут стоять три специальных файла: заглушки-записи для stdin, stdout и stderr. Они нужны лишь для того, чтобы обеспечить соответствие между sfs и заранее согласованными дескрипторами стандартного ввода-вывода — например, по соглашению дескриптор stdout равен 1, и после того как мы добавим три записи-заглушки, индекс 1 в таблице учёта файлов уже не будет выделен никакому другому обычному файлу.

На основе приведённой выше информации мы можем реализовать в файловой системе следующие файловые операции:

int fs_open(const char *pathname, int flags, int mode);
size_t fs_read(int fd, void *buf, size_t len);
size_t fs_write(int fd, const void *buf, size_t len);
size_t fs_lseek(int fd, size_t offset, int whence);
int fs_close(int fd);

Эти файловые операции на самом деле являются реализацией соответствующих системных вызовов внутри ядра. Их функциональность можно посмотреть через man, например

man 2 open

где 2 означает обращение к странице руководства, связанной с системными вызовами. При реализации этих файловых операций обратите внимание на следующие моменты:

  • Поскольку каждый файл в sfs фиксирован и новые файлы не создаются, ситуация «fs_open() не нашла файл, указанный pathname» является исключительной — в этом случае вам нужно завершить работу программы с помощью assertion.
  • Ради упрощения реализации мы разрешаем всем пользовательским программам читать и записывать все существующие файлы, поэтому впоследствии при реализации fs_open() можно игнорировать flags и mode.
  • Для реальных операций чтения-записи файлов используйте ramdisk_read() и ramdisk_write().
  • Поскольку размер файлов фиксирован, при реализации fs_read(), fs_write() и fs_lseek() следите за тем, чтобы смещение не выходило за границы файла.
  • За исключением записи в stdout и stderr (вывод на последовательный порт через putch()), остальные операции над тремя специальными файлами stdin, stdout и stderr можно просто игнорировать.
  • Поскольку sfs не отслеживает состояние открытых файлов, fs_close() может просто возвращать 0, показывая, что закрытие всегда происходит успешно.

Наконец, вам также нужно добавить соответствующие системные вызовы в Nanos-lite и в libos проекта Navy, чтобы вызывать соответствующие файловые операции.

Заставьте loader использовать файлы

Раньше мы заставляли loader напрямую вызывать ramdisk_read() для загрузки пользовательской программы. После увеличения количества файлов в ramdisk этот способ становится неуместным — прежде всего нам нужно дать loader воспользоваться удобствами файловой системы.

Сначала вам нужно реализовать fs_open(), fs_read() и fs_close() — тогда в loader можно будет указывать загружаемую программу по имени файла, например «/bin/hello».

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

Реализуйте полноценную файловую систему

Реализуйте fs_write() и fs_lseek(), а затем запустите тестовую программу navy-apps/tests/file-test. Чтобы её скомпилировать, нужно добавить её в переменную TESTS файла navy-apps/Makefile, тогда в итоге она будет включена в образ ramdisk. Эта тестовая программа выполняет несколько простых операций чтения, записи и позиционирования в файле. Если ваша реализация верна, вы увидите, что программа выводит сообщение PASS!!!.

Не забывайте обновлять список приложений

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

Добавьте поддержку sfs в strace

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

Всё есть файл

IOE в AM показал нам, что программам нужен ввод-вывод. Так как же пользовательской программе получить доступ к устройству на Nanos-lite? Самый прямой способ — дать операционной системе предоставить для каждого устройства отдельный системный вызов, через который пользовательские программы могли бы напрямую использовать соответствующую функциональность. Однако у такого подхода немало проблем:

  • Во-первых, типы устройств крайне разнообразны, а их функций и вовсе не счесть — реализовывать для каждого из них отдельный системный вызов, чтобы предоставить интерфейс пользовательским программам, уже само по себе малоосуществимо;
  • Кроме того, из-за значительных различий в функциональности устройств, если предоставляемые интерфейсы не унифицированы, взаимодействие между программой и устройством станет затруднительным. Поэтому нам нужен способ абстрагировать функциональность устройств, предоставив пользовательским программам единый интерфейс.

Мы уже упоминали, что по своей сути файл — это последовательность байтов. На самом деле компьютерная система целиком состоит из последовательностей байтов (как бы компьютер обрабатывал просто неупорядоченный набор байтов?), и мы легко можем привести множество примеров:

  • Память адресуется побайтно и естественным образом представляет собой последовательность байтов, из-за чего используемый нами ранее ramdisk как последовательность байтов становится ещё более очевидным.
  • Канал (| в shell-командах) — это последовательность байтов по принципу «первым пришёл — первым вышел», по сути представляющая собой буфер-очередь в памяти.
  • Диск тоже можно рассматривать как последовательность байтов: мы можем пронумеровать каждый байт на диске, например, n-й байт в z-м секторе y-й головки x-го цилиндра, и, расположив все байты диска по возрастанию номера, получим последовательность байтов.
  • Сокет (сетевой сокет) — тоже разновидность последовательности байтов: у него есть буфер, отвечающий за хранение полученных сетевых пакетов, а вышестоящее приложение рассматривает содержимое сокета как последовательность байтов и обрабатывает её через некоторые специальные файловые операции. Мы рассказывали о DiffTest в PA2 — если вы читали исходный код (RTFSC), то заметили, что qemu-diff в нём как раз общается с QEMU через сокет, а способом работы с сокетом являются fgetc() и fputc().
  • Некоторая информация операционной системы может быть представлена пользователю в виде последовательности байтов, например информация о конфигурации процессора.
  • Некоторые особые функции, предоставляемые операционной системой, например генератор случайных чисел, тоже можно рассматривать как бесконечно длинную последовательность байтов.
  • Даже некоторое нехранящее оборудование можно рассматривать как последовательность байтов: коды клавиш, набираемых нами по порядку на клавиатуре, образуют последовательность байтов; содержимое каждого пикселя на дисплее в своём порядке тоже можно рассматривать как последовательность байтов...

Раз файл — это последовательность байтов, то, естественно, все эти разнообразные последовательности байтов, перечисленные выше, тоже можно рассматривать как файлы. Именно так поступает Unix, отсюда и выражение «всё есть файл» (Everything is a file). Самое наглядное преимущество такого подхода — предоставление единого интерфейса для разных сущностей: мы можем использовать файловый интерфейс для работы со всем на компьютере, не вдаваясь в их детальные различия. Например, правило ramdisk в navy-apps/Makefile соединяет ввод и вывод различных shell-утилит через каналы, генерируя таблицу учёта файлов

wc -c $(FSIMG_FILES) | grep -v 'total$$' | sed -e 's+ ./fsimg+ +' |
  awk -v sum=0 '{print "\x7b\x22" $$2 "\x22\x2c " $$1 "\x2c " sum "\x7d\x2c";sum += $$1}' >> $(RAMDISK_H)

Просмотр содержимого диска в шестнадцатеричном виде

head -c 512 /dev/sda | hd

Проверка, подвержен ли процессор уязвимости Spectre

cat /proc/cpuinfo | grep 'spectre'

Даже пример аудио «Маленькая звёздочка», предоставленный нами в PA2, был получен грубой склейкой через простые файловые операции

cat Do.ogg Do.ogg So.ogg So.ogg La.ogg La.ogg So.ogg > little-star.ogg

А

#include "/dev/urandom"

включит содержимое устройства urandom прямо в исходный файл: поскольку устройство urandom представляет собой бесконечно длинную последовательность байтов, отправка исходного файла программы, содержащего указанное выше, приведёт к прямому краху некоторых платформ Online Judge со слабой системой проверки.

Абстракция «всё есть файл» позволяет нам легко, с помощью стандартных инструментов, выполнять некоторые задачи, которые под Windows выполнить непросто, — это, по сути, отражает часть философии Unix: каждая программа использует текстовые файлы для ввода и вывода, что облегчает взаимодействие программ друг с другом. GNU/Linux, унаследовав от Unix, естественным образом унаследовала и эту превосходную особенность. Чтобы предоставить пользовательским программам единую абстракцию, Nanos-lite тоже пытается абстрагировать IOE в виде файлов.

Виртуальная файловая система

Чтобы воплотить идею «всё есть файл», нужно расширить ранее реализованные нами файловые операции: нам нужно уметь не только читать и записывать обычные файлы, но и поддерживать операции над различными «специальными файлами». А способ расширения вам уже прекрасно знаком — это абстракция!

Мы расширяем семантику ранее реализованного API файловых операций так, чтобы он поддерживал операции над любым файлом (включая «специальные файлы»):

int fs_open(const char *pathname, int flags, int mode);
size_t fs_read(int fd, void *buf, size_t len);
size_t fs_write(int fd, const void *buf, size_t len);
size_t fs_lseek(int fd, size_t offset, int whence);
int fs_close(int fd);

У этого набора API с расширенной семантикой есть звучное название — VFS (виртуальная файловая система)открыть в новом окне. Раз есть виртуальная файловая система, значит, должны существовать и соответствующие ей «настоящие файловые системы» — под настоящей файловой системой здесь на самом деле подразумевается конкретный способ работы с определённым типом файлов. Например, в Nanos-lite обычные файлы обрабатываются через API ramdisk; в настоящих операционных системах видов настоящих файловых систем не счесть: например, знакомым с Windows наверняка известна NTFS, управляющая обычными файлами, а на GNU/Linux сейчас довольно популярна EXT4; что касается специальных файлов, их видов ещё больше, отсюда соответственно procfs, tmpfs, devfs, sysfs, initramfs... Все эти разные настоящие файловые системы по-своему реализуют конкретные способы работы с такими файлами.

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

И снова здравствуйте

Если, читая приведённый выше текст, вы вспомнили о концепции AM — вы правы, потому что идея, стоящая за VFS, — это тоже абстракция.

В Nanos-lite ключом к реализации VFS являются два указателя на функции чтения и записи в структуре Finfo:

typedef struct {
  char *name;         // имя файла
  size_t size;        // размер файла
  size_t disk_offset;  // смещение файла в ramdisk
  ReadFn read;        // указатель на функцию чтения
  WriteFn write;      // указатель на функцию записи
} Finfo;

ReadFn и WriteFn — это два типа указателей на функции, они используются для указания на функции, реально выполняющие чтение и запись, и возвращают количество успешно прочитанных или записанных байт. Имея эти два указателя на функции, нам достаточно установить в таблице учёта файлов разные функции чтения-записи для разных файлов, и тогда конкретные функции чтения-записи можно будет вызывать через f->read() и f->write().

Имитация объектно-ориентированного программирования на языке C

Реализация VFS показывает, как на языке C можно имитировать некоторые базовые концепции объектно-ориентированного программирования: например, определение класса через структуру, где обычные переменные структуры можно рассматривать как члены класса, а указатели на функции — как методы класса; присваивая указателю на функцию разные функции, можно реализовать перегрузку методов...

Это показывает, что кажущиеся эфемерными концепции ООП на самом деле не намного «выше» языка C — просто компилятор ООП делает за нас больше работы, а после компиляции в машинный код ООП попросту перестаёт существовать. Object-Oriented Programming With ANSI-Cоткрыть в новом окне — эта книга специально рассказывает о том, как имитировать различные концепции и возможности ООП на ANSI-C. В коде ядра GNU/Linux во многих местах тоже можно заметить следы ООП.

Однако в Nanos-lite, поскольку специальных файлов очень немного, мы договариваемся: когда указанный выше указатель на функцию равен NULL, это означает, что соответствующий файл является обычным файлом, и чтение-запись должны выполняться через API ramdisk, — так нам не придётся явно указывать функции чтения-записи ramdisk для большинства обычных файлов.

Мы рассматриваем файл как последовательность байтов, и большинство таких последовательностей «неподвижны» — например, файлы на ramdisk и на диске: если мы их не изменяем, они всегда остаются на одном и том же месте, и у таких последовательностей байтов есть понятие «позиции»; но некоторые особые последовательности байтов устроены иначе — например, последовательность байтов нажатых клавиш является «текучей»: после того как она прочитана, она перестаёт существовать, и у байтов в такой последовательности есть лишь отношение порядка, но их невозможно пронумеровать, а значит, у них нет понятия «позиции». Файлы, относящиеся к первому типу, поддерживают операцию lseek, а устройства, хранящие такие файлы, называются «блочными устройствами»; файлы же второго типа операцию lseek не поддерживают, а соответствующие устройства называются «символьными устройствами». В настоящих операционных системах операция lseek тоже подвергается абстрагированию, но мы упростили это в Nanos-lite и не стали реализовывать эту абстракцию.

IOE поверх операционной системы

Имея VFS, абстрагировать IOE в виде файлов становится очень просто.

Начнём, конечно же, с самого простого устройства вывода: последовательного порта. В Nanos-lite и stdout, и stderr выводятся на последовательный порт. Раньше вы, возможно, решали, писать ли sys_write() на последовательный порт, проверяя, равен ли fd значению 1 или 2. Теперь, имея VFS, нам больше не нужно, чтобы функция-обработчик системного вызова заботилась об этих особых случаях: достаточно реализовать serial_write() в nanos-lite/src/device.c, а затем установить соответствующую функцию записи в таблице учёта файлов — и указанная функциональность будет реализована. Поскольку последовательный порт является символьным устройством и у соответствующей последовательности байтов нет понятия «позиции», параметр offset в serial_write() можно игнорировать. Кроме того, Nanos-lite и не собирается поддерживать чтение из stdin, поэтому достаточно установить в таблице учёта файлов соответствующую функцию, сообщающую об ошибке.

Абстрагируйте последовательный порт в виде файла

На основе вышеизложенного добавьте в VFS поддержку записи на последовательный порт.

Что касается устройств ввода, сначала рассмотрим часы. Часы устроены несколько особо: большинство операционных систем не абстрагируют их в виде файла, а предоставляют пользовательским программам напрямую несколько системных вызовов, связанных с часами. В Nanos-lite мы тоже предоставляем системный вызов SYS_gettimeofday, через который пользовательская программа может считать текущее системное время.

Реализуйте gettimeofday

Реализуйте системный вызов gettimeofday — за значением его параметров обратитесь к RTFM. После реализации добавьте в navy-apps/tests/ новый тест timer-test, в котором через gettimeofday() получайте текущее время и каждые 0.5 секунды выводите фразу.

Чтобы лучше инкапсулировать функциональность IOE, мы предоставляем в Navy мультимедийную библиотеку под названием NDL (NJU DirectMedia Layer). Код этой библиотеки находится в navy-apps/libs/libndl/NDL.c, но большая часть функциональности пока не реализована. В коде есть кое-что, связанное с NWM_APP, — сейчас это можно игнорировать, но не изменяйте соответствующий код: вы познакомитесь с этой функциональностью в самом конце PA4. NDL предоставляет пользователю API, связанный с часами:

// Возвращает системное время в миллисекундах
uint32_t NDL_GetTicks();

Реализуйте часы NDL

Вам нужно реализовать NDL_GetTicks() с помощью gettimeofday(), а затем изменить тест timer-test, чтобы он получал текущее время через вызов NDL_GetTicks(). При необходимости вы можете добавить код инициализации и завершения в NDL_Init() и NDL_Quit() — мы договариваемся, что программа обязана вызвать NDL_Init() перед использованием функций библиотеки NDL. Если вы считаете, что добавлять код инициализации не нужно, менять их не обязательно.

Ещё одно устройство ввода — клавиатура: информация о нажатии клавиши для системы по сути является приходящим событием. Простой способ — представить событие в текстовом виде, определив следующие два вида событий:

  • событие нажатия клавиши, например kd RETURN означает нажатие клавиши Enter;
  • событие отпускания клавиши, например ku A означает отпускание клавиши A.

Названия клавиш совпадают с названиями, определёнными в AM, все они пишутся заглавными буквами. Кроме того, событие завершается символом перевода строки \n.

У использования текстовой формы описания событий есть два преимущества: во-первых, текст, очевидно, представляет собой последовательность байтов, что позволяет легко абстрагировать событие в виде файла; кроме того, текстовый способ позволяет пользовательской программе легко и удобочитаемо разбирать содержимое события. Nanos-lite и Navy договариваются абстрагировать вышеописанные события в виде специального файла /dev/events — он должен поддерживать операцию чтения, чтобы пользовательская программа могла считывать из него события нажатий клавиш, но ему не обязательно поддерживать lseek, поскольку это символьное устройство.

NDL предоставляет пользователю API, связанный с событиями клавиш:

// Считывает одну запись информации о событии и записывает её в `buf`, максимум `len` байт
// Если считано корректное событие, функция возвращает 1, иначе 0
int NDL_PollEvent(char *buf, int len);

Абстрагируйте ввод с клавиатуры в виде файла

Вам нужно:

  • Реализовать events_read() (определена в nanos-lite/src/device.c), записывая событие в buf, максимум len байт, а затем возвращая фактическую длину записанного. Названия клавиш уже определены в строковом массиве names, вам нужно воспользоваться API IOE, чтобы получить ввод устройства. Кроме того, если в данный момент нет корректной клавиши, достаточно вернуть 0.
  • Добавить в VFS поддержку /dev/events.
  • Реализовать в NDL NDL_PollEvent(), считывая события из /dev/events и записывая их в buf.

Можно предположить, что за раз будет считываться не более одного события — это упростит вашу реализацию. После реализации запустите на Nanos-lite navy-apps/tests/event-test — если реализация верна, при нажатии клавиши программа будет выводить информацию о событии клавиши.

Использовать fopen() или open()?

Это вопрос, над которым действительно стоит подумать. Вам нужно рассмотреть различия в конкретном поведении этих двух наборов API, а затем проанализировать, какую функцию следует использовать для работы со специальным файлом /dev/events. Мы уже давали некоторые подсказки в одном из синих информационных блоков.

Наконец, VGA: чтобы обновить экран, программе достаточно записать информацию о пикселях в видеопамять VGA. Соответственно, Nanos-lite нужно абстрагировать видеопамять в виде файла. Сама видеопамять тоже является участком памяти, хранящим в порядке построчного обхода пиксели, которые будут отображены на экране. Nanos-lite и Navy договариваются абстрагировать видеопамять в виде файла /dev/fb (fb означает frame buffer) — он должен поддерживать операцию записи и lseek, чтобы можно было обновлять пиксели в указанной позиции экрана.

NDL предоставляет пользователю два API, связанных с отрисовкой на экране:

// Открывает холст размером (*w) X (*h)
// Если *w и *h равны 0, весь системный экран используется как холст, а *w и *h устанавливаются равными размеру системного экрана
void NDL_OpenCanvas(int *w, int *h);

// Рисует на холсте по координатам `(x, y)` прямоугольное изображение размером `w*h` и синхронизирует эту область отрисовки с экраном
// Пиксели изображения хранятся в `pixels` в порядке построчного обхода, каждый пиксель описывается 32-битным целым числом в формате `00RRGGBB`
void NDL_DrawRect(uint32_t *pixels, int x, int y, int w, int h);

Здесь «холст» — это понятие, ориентированное на программу: координаты, которые программа использует при отрисовке, задаются именно относительно холста, — так программе не нужно беспокоиться о размере системного экрана и о том, в какую позицию системного экрана нужно рисовать изображение. NDL может, основываясь на размере системного экрана и размере холста, решать, куда «наклеить» холст — например, в левый верхний угол экрана или по центру, — тем самым записывая содержимое холста в правильную позицию во frame buffer.

Функциональность NDL_DrawRect() очень похожа на интерфейс отрисовки, представленный в PA2. Но чтобы её реализовать, NDL также нужно знать информацию о размере экрана. Nanos-lite и Navy договариваются, что информация о размере экрана получается через файл /proc/dispinfo, который должен поддерживать операцию чтения. Формат содержимого этого файла определён в navy-apps/README.md, вам нужно с ним ознакомиться. Что касается конкретного размера экрана, его нужно получать через соответствующий API IOE.

Получите размер экрана в NDL

  • Реализуйте dispinfo_read() (определена в nanos-lite/src/device.c), записывая len байт файла в buf согласно соглашению (мы считаем, что этот файл не поддерживает lseek, так что offset можно игнорировать).
  • Считайте в NDL содержимое этого файла, разберите из него размер экрана, а затем реализуйте функциональность NDL_OpenCanvas(). Пока NDL_OpenCanvas() достаточно лишь записывать размер холста, разумеется, мы требуем, чтобы размер холста не превышал размер экрана.

Запустите на Nanos-lite navy-apps/tests/bmp-test — поскольку функциональность отрисовки ещё не реализована, вывести содержимое изображения не получится, но вы можете пока вывести разобранный размер экрана через printf().

Абстрагируйте видеопамять VGA в виде файла

  • Инициализируйте размер /dev/fb в таблице учёта файлов в init_fs() (определена в nanos-lite/src/fs.c).
  • Реализуйте fb_write() (определена в nanos-lite/src/device.c), чтобы записывать len байт из buf на экран в позицию offset. Вам нужно сначала вычислить координаты на экране из offset, а затем вызвать IOE для отрисовки. Кроме того, мы договариваемся, что после каждой отрисовки содержимое frame buffer всегда немедленно синхронизируется с экраном.
  • Реализуйте в NDL NDL_DrawRect(), отрисовывая изображение путём записи информации о пикселях в правильную позицию /dev/fb. Вам нужно чётко разобраться в позиционном соотношении между системным экраном (то есть frame buffer), холстом, открытым NDL_OpenCanvas(), и областью отрисовки, указанной NDL_DrawRect().

Запустите на Nanos-lite navy-apps/tests/bmp-test — если реализация верна, вы увидите на экране логотип Project-N.

Реализуйте центрированный холст

Основываясь на размере экрана и размере холста, вы можете заставить NDL рисовать изображение по центру экрана, добиваясь лучшего визуального эффекта.