Операционные системы с нуля; Уровень 1 (младшая половина)

  • Tutorial

Эта часть посвящена улучшению навыков работы с Rust и написанию парочки полезных утилиток и библиотек. Напишем драйверы для GPIO, UART и встроенного таймера. Реализуем протокол XMODEM. Используя это всё, напишем простенький шелл и загрузчик. Перед прочтением настоятельно рекомендуется убедиться в прочтении Книги. По крайней мере от начала и до конца. Для ленивых, но чуть более опытных можно рекомендовать это. На русском можно поковырять вот тут.


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


Полезные материалы


  • Книга v2.0 по Rust. Перевод на русский в пути. Теребите команду переводчиков (и помогайте им). Эту книжицу определённо стоит читать как минимум тем, кто не читал.
  • Документация по стандартной библиотеке Rust. Там всё готовенькое, что есть в стандартной поставке.
  • docs.rs — там можно читать документацию по различным библиотекам.
  • Маны online скачать бесплатно без регистрации и смс. RTFM!
  • Вики статейка по протоколу XMODEM. Для общего развития и тем, кто интересуется историей возникновения. Для реализации поможет мало, т.е. почти никак.
  • Ещё документ по XMODEM.
  • BCM2837 — это про проц малинки. Там же, где и в прошлый раз.

Фаза 0: Начало работы


На всякий случай ещё раз убедитесь, что используете совместимое с курсом программно-аппаратное обеспечение:


  • Современная 64-битная Юниксподобная ОСь: Linux, macOS или BSD
  • У вас есть подходящий USB-разъём (неаугментированные могут использовать навесное оборудование)

Кроме того должно быть установлены следующие проги: git, wget, tar, screen, make и всё, что требовалось для нулевого уровня. Для этой части потребуется доустановить socat.


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


Получение кода


Склонировать код для этой части можно вот таким образом:


git clone https://web.stanford.edu/class/cs140e/assignments/1-shell/skeleton.git 1-shell

Не стесняйтесь исследовать содержимое репозитория самостоятельно.


Вопросы


В этой и всех следующих лабах будут вопросы. Отвечать на них можно прямо в комментариях используя спойлеры. Вот пример:


Как мы настраиваем и используем другие GPIO-контакты? [assignment0]

В прошлый раз мы использовали 16 пин GPIO во имя мигания светодиодом. Используя при этом регистры GPFSEL1, GPSET0 и GPCLR0. А если будем использовать пин 27, то какие регистры нам пригодятся? И какой физический контакт у этого самого 27 GPIO-пина?

В квадратных скобочках указано имя файла внутри каталогов questions/. Нам это не особо важно ибо отвечать следует в комментариях. Не читайте чужие ответы до тех пор, пока не уверены, что ответили сами. Иначе не интересно же. А вот эти теги можно использовать в качестве заголовков для спойлеров. Впрочем советую сначала писать в этих файликах. Для удобства.


Фаза 1: Ferris Wheel (игра слов не переводится)



(Эту часть можно целиком и полностью пропустить, если уже есть достаточно глубокие знания раста.)


Ради тренировки будем редактировать проги на Rust с некоторыми корыстными целями. Некоторые должны скомпилироваться после редактирования. Другие не должны компилироваться. Для третьих должны успешно завершиться тесты.


В недрах каталога ferris-wheel/ можно найти следующее:


  • compile-fail — содержит код, который надо поломать так, чтоб не компилялось
  • compile-pass — содержит код, который надо пофиксить ровно до компилируемости
  • run-pass — содержит код с тестами, которые должны стать зелёненькими
  • questions — по идее для вопросов, но мы уже договорились, что можно это всё поместить в комментарии

Там ещё есть скриптик test.sh. Оный проверяет правильность выполнения заданий. Если его запустить, то он достаточно популярно объяснит, где и что не совсем так, как ожидалось. Что-то вроде:


$ ./test.sh
ERROR: compile-pass/borrow-1.rs failed to compile!

ERROR: compile-pass/const.rs failed to compile!

...

0 passes, 25 failures

Помимо этого скриптик принимает флаг -v. Если этот самый флаг передать сценарию, то будут показаны ошибки, которые выплёвывает компилятор:


$ ./test.sh -v
ERROR: compile-pass/borrow-1.rs failed to compile!
---------------------- stderr --------------------------
warning: unused variable: `z`
 --> /.../ferris-wheel/compile-pass/borrow-1.rs:9:9
  |
9 |     let z = *y;
  |         ^
  |
  = note: #[warn(unused_variables)] on by default
  = note: to avoid this warning, consider using `_z` instead

error[E0507]: cannot move out of borrowed content
 --> /.../ferris-wheel/compile-pass/borrow-1.rs:9:13
  |
9 |     let z = *y;
  |             ^^
  |             |
  |             cannot move out of borrowed content
  |             help: consider using a reference instead: `&*y`

error: aborting due to previous error

...

0 passes, 25 failures

Ещё этот скрипт принимает строку в качестве фильтра. При его наличии будут проверены только те пути к файлам ($directory/$filename), которые этому фильтру соответствуют. Например:


./test.sh trait
ERROR: compile-pass/trait-namespace.rs failed to compile!

ERROR: run-pass/trait-impl.rs failed to compile!

0 passes, 2 failures

Одно другому не мешает и можно комбинировать фильтр и ключик -v. Что-то вроде такого: ./test.sh -v filter.


Сколько можно менять?


Каждый файлик содержит комментарий, в котором указано сколько можно его портить (diff budget). Т.е. максимальное количество изменений, которые можно внести, чтоб исправить прогу. Решения, которые не укладываются в эти рамки можно считать не пройденными.


Для примера. В файлике compile-pass/try.rs есть такой комментарий:


// FIXME: Make me compile. Diff budget: 12 line additions and 2 characters.

В нём сказано, что можно добавить не более 12 строчек кода (пустые строки тоже считаются). И изменить (добавить/поменять/удолить) 2 символа. Можно пользовать git diff для того, чтоб увидеть построчные изменения. И git diff --word-diff-regex=. для того же, но посимвольно.


Ещё пример:


// FIXME: Make me compile! Diff budget: 1 line.

Оно какбэ говорит нам, что можно изменить (добавить/поменять/удолить) только одну сточку кода.


Общие правила


После изменений должна сохраняться предполагаемая функциональность программ. Допустим, если тело некой функции надобно изменить таким образом, чтоб оно компилялось, недостаточно будет добавить туда unimplemented!(). Если пребываете в сомненьях — попробуйте лучшее из того, на что способны. Ну или спросите в комментариях.


По мимо этого совершенно не рекомендуется поступать следующими грязными методами:


  • Изменять все эти assert!ы
  • Модифицировать всё, что помечено как "do not modify"
  • Менять комментарии о том, сколько и чего можно менять
  • Перемещать, переименовывать или добавлять какие либо файлики

Когда все задания будут выполнены test.sh выведет 25 passes, 0 failures


Подсказка: в имени файла может содержаться ключ к решению.

Подсказка: в этом уютном чатике ответят быстрее на вопросы о Rust. Быстрее, чем в комментариях к этой статье.

Что случилось? Чем чинили? Почему это работает? [имя_файлика]

Для каждой проги из этой части следует объяснить, что было не так с исходным кодом. Затем пояснить по хардкору, какие были внесены изменения и чому эти исправления делают своё грязное дело. Хорошие годные объяснения приветствуются. Если считаете, что всё для вас итак очевидно, то можно не писать. Если лень — можно не писать ничего вообще.

Фаза 2: Oxidation (Окисление)



На этом этапе напишем парочку библиотек и одну утилитку для командной строки. Работать будем в подкаталогах stack-vec, volatile, ttywrite и xmodem. Тут тоже будет некоторое количество вопросов, на которые можно поотвечать, ежели не влом. Каждая часть управляется при помощи Cargo. По меньшей мере вот эти команды можно назвать полезными:


  • cargo build — сборка проги или библиотеки
  • cargo test — запуск тестов
  • cargo run — запуск приложения
  • cargo run -- $флаги — примерно таким образом можно передать флаги при запуске приложения

О Cargo есть отдельная книжица: Cargo Book. Оттуда можно почерпнуть необходимую инфу о том как оно всё работает в деталях.


Субфаза A: StackVec


Одной из самых важных фич, которыми занимаются операционные системы, является управление выделением памяти. Когда C, Rust, Java, Python или вообще практически любое приложение вызывает malloc(), то при нехватке памяти в конечном итоге используется системный вызов, который запрашивает у операционной системы дополнительную память. Операционная система определяет, имеется ли ещё незанятая никем память. Если да, то из этой памяти ОСь отсыпет процессору чуточку.


Распределение памяти — non penis canis est

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

Структуры вроде Vec, String и Box внутри используют malloc() для выделения памяти под собственные нужды. Это означает, что для этих структур требуется поддержка со стороны операционной системы. В частности они требуют, чтоб ОСь умела выделять память. Эту часть мы ещё даже не начали (смотрите в следующей серии), так то управления памятью у нас нет ни в каком виде. Соответственно все эти Vec мы не можем (пока) использовать.


Это концентрированная лажа ибо Vec — хорошая годная во всех отношениях абстракция! Оная позволяет нам мыслить в терминах .push() и .pop() без необходимости помнить о всяких тонкостях. Можем ли мы получить что-то похожее на Vec без полноценного распределителя памяти?



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


В этой подфазе мы будем реализовывать структуру StackVec, которая предоставляет api, похожий на тот, который предоставляет Vec из стандартной библиотеки. Но использует при этом заранее выделенный кусочек памяти. Этот самый StackVec пригодится нам при реализации командной строки (в фазе 3). Работать будем в подкаталоге stack-vec. В оном уже можно найти следующие штуки:


  • Cargo.toml — конфигурационный файлик для Cargo
  • src/lib.rs — тут мы будем дописывать необходимый код
  • src/tests.rs — тесты, которые будут выполняться при запуске cargo test
  • questions/ — заготовки для файликов с вопросами (нас не сильно интересуют)

Интерфейс StackVec


StackVec<T> создаётся путём вызова StackVec::new(). В качестве аргумента для ф-ии new выступает срез типа T. Тип StackVec<T> реализует многие методы, которые используются практически таким же способом, как аналогичные из Vec. Для примера возьмём StackVec<u8>:


let mut storage = [0u8; 1024];
let mut vec = StackVec::new(&mut storage);

for i in 0..10 {
    vec.push(i * i).expect("can push 1024 times");
}

for (i, v) in vec.iter().enumerate() {
    assert_eq!(*v, (i * i) as u8);
}

let last_element = vec.pop().expect("has elements");
assert_eq!(last_element, 9 * 9);

Тип StackVec уже объявлен вот в таком виде:


pub struct StackVec<'a, T: 'a> {
    storage: &'a mut [T],
    len: usize,
}

Понимание StackVec


Есть парочка вопросов об устройстве StackVec:


Почему push возвращает Result? [push-fails]

Метод push из Vec, который из стандартной библиотеки, не имеет какого либо возвращаемого значения. Однако push из StackVec имеет: он возвращает результат, указывающий, что может быть какая-то ошибка. Почему StackVec::push() может завершаться ошибочно в отличии от Vec?

Почему нам надо ограничивать T временем жизни 'a? [lifetime]

Компилятор отклонит вот такое объявление StackVec:
struct StackVec<'a, T> { buffer: &'a mut [T], len: usize }


Если мы добавим ограничение 'a к типу T, то всё заработает:
struct StackVec<'a, T: 'a> { buffer: &'a mut [T], len: usize }


Зачем это ограничение требуется? Что будет происходить, если Rust не будет следовать этому ограничению?

Почему StackVec требует T: Clone для метода pop? [clone-for-pop]

Метод pop из Vec<T> стандартной библиотеки реализован для любого T, однако метод pop для нашего StackVec реализуется только когда T реализует свойство Clone. Почему это должно быть так? Что не так, если удолить это ограничение?

Реализация StackVec


Реализуйте все unimplemented!() методы из StackVec в файле stack-vec/src/lib.rs. Каждый метод уже имеет документацию (из неё понятно, чего от вас требуют например). По мимо этого есть тесты в файле src/tests.rs, которые помогают гарантировать правильность вашей реализации. Запустить тесты можно при помощи команды cargo test. Кроме того вам нужно реализовать трейты Deref, DerefMut и IntoIterator для класса StackVec. И трейт IntoIterator для &StackVec. Без реализации этих трейтов тесты не пройдут. Как только будете уверены, что ваша реализация правильна и вы в состоянии ответить на предложенные вопросы — переходите к следующей подфазе.


Какие тесты требуют реализации Deref? [deref-in-tests]

Прочитайте весь код тестов из файлика str/tests.rs. Какие тесты не хотели компиляться, если не было реализации Deref? А что на счёт DerefMut? Why?

На самом деле тесты являются не полными

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

Подсказка: решение из задания liftime нулевой фазы может оказаться полезным.

Субфаза B: volatile



В этой части мы поговорим о volatile-обращениях к памяти и почитаем код в подкаталоге volatile/. Свой код писать не будем, но зато тут есть вопросы для самопроверки.


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


fn f() {
    let mut x = 0;
    let y = &mut x;
    *y = 10;
}

Компилятор может чуточку подумать и здраво рассудить, что *y после записи никогда не читается. По этой причине компилятор может просто исключить эту часть из результирующего бинарного файла. Продолжая рассуждать в таком ключе, компилятор находит годным выпилить само объявление y и затем x. В конце концов и вызов f() пойдёт под нож.


Оптимизации такого вида очень полезны и ценны. Благодаря им программы ускоряются без влияния на результаты. Правда в некоторых случаях подобные махинации могут иметь непредвиденные последствия. Для примера y будет указывать на какой либо регистр, доступный только на запись. В таком случае запись в *y будет иметь вполне себе наблюдаемые эффекты без необходимости чтения *y. Если компилятор этого не знает, то оный просто удолит эту часть на этапе оптимизации и наша прога будет работать не так, как ожидается.


Как нам убедить компилятор в том, что чтения/записи чего-то такого влияют на наш уютный мирок сами по себе? Вот именно и подразумевается под volatile-обращениями к памяти. Компейлятор божится не оптимизировать доступ к таким участкам.


Ржавый volatile


В Rust мы можем использовать методы read_volatile и write_volatile для чтения и записи сырых указателей.


Что за сырые указатели такие?

До текущего момента мы успели близко познакомиться со ссылками (которые &T и &mut T). Сырые (raw) указатели в Rust (*const T и *mut T) — это суть те же самые ссылки без отслеживания времени жизни borrow checker'ом. Чтения/записи с использованием этих самых сырых указателей может приводить к тем же самым травмам ног, какие можно часто наблюдать у любителей C и C++. Rust считает такие операции небезопасными. Соответсвенно это всё в обязательном порядке помечать unsafe-меткой. Подробнее о сырых указателях есть в документации.

Писать вызовы read_volatile и write_volatile каждый раз достаточно грустно (помимо того, что это может привести к досадным ошибкам на почве депрессии). На наше счастье Rust предоставляет нам возможность сделать нашу жизнь проще и безопаснее. С одной стороны мы можем просто сделать volatile-обёртку (почти как ключевое слово volatile в няшном си) и гарантировать, что каждое чтение/запись останутся в нашем коде. Бонусом мы можем определить обёртку только для чтения или только для записи (в няшном такого нет, дали ствол и крутись как хочешь).


Введение в Volatile, ReadVolatile, WriteVolatile и UniqueVolatile


Крейт volatile в каталоге volatile/ (кто бы мог подумать?) реализует эти четыре типа, которые делают примерно то, что очевидно из их названия. Подробнее можно читнуть в документации. Вызовите cargo doc --open прямо в каталоге volatile/ для собственно чтения этой самой документации в удобном виде.


Почему тут есть UniqueVolatile? [unique-volatile]

Как Volatile, так и UniqueVolatile позволяют работать с volatile-обращениями к памяти. Исходя из документации, в чём между этими двумя типами разница?

Откройте код src/lib.rs. Почитайте код в муру собственных умений. После этого (почитывая код) ответьте на следующие пару вопросов. Как закончите — можно переходить к следующей подфазе.


Как организованно ограничение на чтение и запись? [enforcing]

Типы ReadVolatile и WriteVolatile делают невозможными соответственно чтение и запись указателя. Каким способом это осуществляется?

В чём преимущество использования трейтов вместо обычных методов? [traits]

При внимательном рассмотрении можно заменить, что каждый из типов реализует только один собственный метод new. Все остальные методы так или иначе относятся к реализациям Readable, Writeable и ReadableWriteable. Какой от этого всего профит? Опишите по крайней мере два плюса такого подхода.

Почему read и write безопасны, а new небезопасно? [safety]

Что должно быть верно в отношении к new чтоб read и write можно было считать безопасными? Было бы безопасно вместо этого помечать new как безопасный, а read и write напротив небезопасными?
Подсказка: прочтите документацию ко всем этим методам.

Почему мы принуждаем использовать new? [pub-constructor]


Если бы тип Volatile был объявлен следующим образом:


struct Volatile<T>(pub *mut T);

то значение типа Volatile можно было бы создать при помощи Volatile(ptr) вместо вызова new. Какая польза от того, что мы создаём нашу обёртку с помощью статического вызова new?


Подсказка: рассмотрите последствия на утверждения о безопасности для обоих вариантов.




Что делают макросы? [macros]

Что делают макросы readable!, writeable! и readable_writeable!?

Субфаза C: xmodem


В этой подфазе реализуем протокол передачи файлов XMODEM (подкаталог xmodem/). Основная работа идёт в файле xmodem/src/lib.rs.


XMODEM — простой протокол для передачи файлов, разработанный в 1977 году. В нём есть контрольные суммы пакетов, отмена передачи и возможность автоматически повторить передачу при ошибках. Он достаточно широко применяется для передачи информации через последовательные интерфейсы вроде UART. Главная плюшка протокола — простота. Подробнее можно читнуть в вики: XMODEM (желающие могут перевести статью на русский язык).


Протокол


Сам протокол достаточно подробно описан в текстовом файлике Understanding The X-Modem File Transfer Protocol. Кое-что из описания мы повторим прямо тут.


Не основывайте свою имплементацию на объяснении из Википедии!

Хотя объяснение из педевикии будет полезно на высоком уровне, многие детали будут отличаться от того, чего мы будем реализовывать тут. Используйте педевикию только как обзор протокола.

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


Для начала определим несколько констант:


const SOH: u8 = 0x01;
const EOT: u8 = 0x04;
const ACK: u8 = 0x06;
const NAK: u8 = 0x15;
const CAN: u8 = 0x18;

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


После того, как передача началась, приём и передача пакетов идентичны. Пакеты нумеруются последовательно начиная с 1. Когда размер одного байта исчерпывает себя (т.е. после 255), то начинаем считать с 0.



Чтобы отправить пакет, отправитель:


  1. Отправляет байт SOH
  2. Отправляет номер пакета
  3. Отправляет обратное значение номера пакета (255 - $номер_пакета)
  4. Отправляет сам пакет
  5. Отправляет контрольную сумму пакета
    • Контрольная сумма представляет собой сумму всех байтов по модулю 256
  6. Ждёт один байт от принимающей стороны:
    • Если это NAK байт, то пробуем отправить пакет ещё раз (до 10 раз)
    • Если это ACK байт, то можно отправлять следующий пакет

В тоже время для приёма пакета, получатель выполняет обратную задачу:


  1. Ожидает от отправителя байт SOH или байт EOT
    • Если принимается другой байт, то приёмник отменяет передачу
    • Если принимается EOT байт — передача завершается
  2. Считывает следующий байт и сравнивает его с текущим номером пакета
    • Если получен неправильный номер пакета — отменяем передачу
  3. Считываем байтик и сравниваем его с обратным номером пакета
    • Если принят неправильный номер, то отменяем передачу
  4. Читаем сам пакет (128 байт)
  5. Вычисляем контрольную сумму для пакета
    • Т.е. сумму всех байтов в пакете по модулю 256
  6. Читаем ещё один байтик и сравниваем его с контрольной суммой
    • Если контрольные суммы отличается, то отправляем байт NAK и повторяем приём пакета
    • Если контрольные суммы одинаковы, то отправляем байт ACK и получаем следующий пакет

Для того, чтоб отменить передачу, либо отправителем, либо получателем отправляется байт CAN. Когда одна из сторон получает байт CAN — выдаём ошибку и прерываем соединение.


Для завершения передачи, отправитель:


  1. Отправляет байт EOT
  2. Ожидает байт NAK (Если получен другой байт — ошибка отправителя)
  3. Отправляет второй байт EOT
  4. Ожидает байт ACK (Если получен другой байт — ошибка отправителя)

Для завершения передачи, приёмник (после приёма первого EOT):


  1. Отправляет байт NAK
  2. Ожидает второй байт EOT (Если принимается другой байт, приемник отменяет передачу)
  3. Отправляет байт ACK

Реализация XMODEM


Предоставляется незавершенная реализация протокола XMODEM в одноимённом каталоге. Задача состоит в том, чтоб эту самую реализацию завершить. Дописываем методы expect_byte, expect_byte_or_cancel, read_packet и write_packet в файлике src/lib.rs. В реализации должно использоваться внутреннее состояние типа Xmodem: packet и started. Перед началом работы крайне рекомендуется почитать код, который уже есть там.


Советую начать с реализации методов expect_byte и expect_byte_or_cancel. Затем использовать все четыре вспомогательных метода (включая read_byte и write_byte) для реализации read_packet и write_packet. Чтоб узнать, как эти методы можно эксплуатировать, читните функции transmit и receive. Оные передают/получают полный поток данных с использованием нашего протокола. Не забывайте о том, что комментарии содержат достаточно много полезной информации. Протестировать собственную реализацию можно с помощью cargo test. После того, как всё в этой части работает — переходите к следующей части.


Не используйте дополнительные элементы из std.

В вашей реализации должны использоваться только элементы из std::io. Другие компоненты из stdили внешние библиотеки использоваться не должны.

Подсказки:
Моя эталонная реализация для {read, write}_packet содержит примерно 33 строки кода.

Документация по io::Read и io::Write может быть весьма полезна (как, тащемто, и любая другая документация для малознакомых штук).

Побольше используйте оператор ?.

Чтение кода тестов поможет понять, что оные от вас хотят.

Субфаза D: ttywrite


В этой подфазе будем писать утилитку для командной строки ttywrite. Она позволит нам отправлять данные на малинку в сыром необработанном виде и используя протокол XMODEM. Тут нам как раз пригодится библиотека xmodem из прошлой части. Весь код пишем в ttywrite/src/main.rs. Во имя нужд тестирования предоставляется скриптик test.sh. Для работы этого самого скрипта потребуется упомянутый где-то там в начале socat.


Что такое последовательное устройство?

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

Что такое TTY?
TTY — это телетайп (TeltTYpe writer). Это рудиментарный олдфажный термин, который изначально относился к компьютерным терминалам. Термин позже (в силу привычки) стал более общим и теперь означает любые устройства связи с последовательным интерфейсом. Именно по этой причине имя файлика из /dev/, который закреплён за малинкой начинается с tty.

Интерфейс командной строки


Заготовка кода для ttywrite уже анализирует и проверяет на проф-пригодность аргументы командной строки. При этом используется крейт structopt, который внутри себя использует clap. Если вы действительно пользуетесь советом свободно изучать внутренности репозитория, то заметите, что эта штука присутствует как зависимость в Cargo.toml. structopt вообще говоря основной своей целью ставит генерацию кода. Мы тупо описываем структуру того, чего хотим получить и объявляем необходимые поля, а structopt генерирует весь необходимый код.


Если захочется увидеть, какие флаги там нагенерированны, можно вызвать приложение с флагом --help. Не лишним будет повторить, что при использовании того же cargo run для передачи флагов самому приложению надо использовать -- в качестве разделителя. Например вот так: cargo run -- --help. Взгляните на эту справочку сейчас. После поглядите на содержимое main.rs. Больше всего нас интересует структура Opt. Сравните это с выводом справки по ключам нашего приложения.


Что будет, ежели будут переданы неправильный флаги? [invalid]

Попробуйте передать какие либо недопустимые флаги с некорректными параметрами. Например установите -f как idk. Откуда structopt знает, что на подобное надобно обругать пользователя?

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


Общение с последовательным устройством


В main можно увидеть вызов serial::open. Это функция open из крейта serial, что тащемто очевидно из названия. Функция open возвращает TTYPort, который позволяет нам читать/писать из/в последовательного устройства (ибо реализует io::Read и io::Write). Ну и позволяет устанавливать различные настройки для последовательного порта (через реализацию трейта SerialDevice).


Написание кода


Имплементируйте утилитку ttywrite. Реализация должна по меньшей мере установить все необходимые параметры, переданные через командную строку, сохранённую в переменной opt из main. Если имя входного файла не было передано, то следует читать из stdin. Ну или из входного файлика в противном случае. Данные следует перенаправить на заявленное последовательное устройство. Если установлен флаг -r, то данные следует передавать как есть безо всяких махинаций. Если этого флага нет, то нужно будет использовать реализацию xmodem из предыдущей подфазы. После всего этого надобно напечатать количество заботливо переданных байтиков (при успешной передаче).


Для передачи по протоколу XMODEM код должен использовать методы Xmodem::transfer или Xmodem::transmit_with_progress из очевидно какой библиотеки. Рекомендую transmit_with_progress ибо так можно запилить подсчёт скорости передачи. В самом убогом простейшем варианте это будет выглядеть примерно вот так:


fn progress_fn(progress: Progress) {
    println!("Progress: {:?}", progress);
}

Xmodem::transmit_with_progress(data, to, progress_fn)

Проверить минимальную корректность реализации можно при помощи скриптика test.sh из каталога ttywrite. Когда ваша реализация будет отдалённо напоминать правильную вы сможете увидеть примерно вот такое:


Opening PTYs...
Running test 1/10.
wrote 333 bytes to input
...
Running test 10/10.
wrote 232 bytes to input
SUCCESS

Подсказки

Получить обёртку дескриптора stdin можно соответсвующей функцией io::stdin().

Скорее всего io::copy() окажется весьма юзабельной.

Функция main() в конечной реализации вполне может уложиться в примерно 35 строчек кода.

Докуметация по TTYPort можно не закрывать во время написания кода.



Почему скрипт test.sh всегда устанавливает ключик -r? [bad-test]

Предоставленый тестовый скриптик всегда устанавливает нашей проге ключик -r. Другими словами он не проверяет на вшивость использование протокола XMODEM. Почему это должно быть таким, каким оно есть? Почему на самом деле тестирование XMODEM не сильно нужно? Почему такое тестирование может быть сложнее?



UPD Старшая половина доступна для прохождения. Там будет самое вкусное от этой части. Про шелл и загрузчик.

  • +33
  • 31k
  • 5
Поделиться публикацией
Комментарии 5
    0

    Ferris Wheel — это ж Чёртово колесо просто

    +1
    О! Эта статья удовлетворяет всё моё любопытство по неизведанным областям разом и требует самостоятельной работы. То, что доктор прописал!
      0
      И все-таки телетайп — это
      Приёмно-передающий телеграфный аппарат с клавиатурой, записывающий принимаемое сообщение буквами.

      Печатал он на ленту, как принимаемый текст, так и «эхо» вводимого. От него, да, произошли терминалы, командная строка, и вообще текст, как данные.

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

      Самое читаемое