вторник, 4 октября 2022 г.

POSIX на контроллере Cortex-M

POSIX - Portable Operating System Standard. Стандарт описывает переносимый (портируемый) программный интерфейс для юниксоподобной операционной системы. Юникс разработан вместе с языком Си, язык включает ряд библиотечных функций для работы с операционной системой. POSIX включет в себя стандартные библиотеки Си. Включает разделы посвященные файловой системе и работе с периферийными устройствами, PTHREAD - POSIX Thread и механизмы синхронизации тредов так же являются частью POSIX. Кроме того, стандарт наполнен комментариями почему так сделано и как должны работать те или иные функции операционной системы для переносимости прикладного кода.

Стандарт операционной системы POSIX.1-2017 в тоже время является стандартом IEEE Std 1003.1™-2017, и представлен на сайте The Open Group, как Technical Standard Base Specifications, Issue 7.

С некоторых пор мы реализуем поддержку POSIX в контроллере наравне с поддержкой библиотек стандарта C11, С2x и CMSIS-RTOS. Нужно это для переносимости прикладного программного обеспечения туда и обратно.

Первое впечатление - стандарт обширный - занимает около 4тыс. страниц в стиле man, сотни и сотни различных функций. Кажется, если такой объем, даже его конкретную часть, попробовать затолкать в контроллер, ничего не выйдет, памяти не хватит. Теперь, после нескольких месяцев изысканий, могу опровергнуть это утверждение. Создать POSIX совместимую ОС под контроллеры Cortex-M возможно.

1. C11 определяет интерфейс API для работы с тредами, мьюетексами и некондицией, и множество других библиотечных функций. C11 треды мы приняли за основу. Они выглядят проще чем posix pthread и в тоже время вполне по-взрослому, не хуже чем родные треды в OS Solaris. Отличие только в том, что у них нет атрибутов, а возвращаемое значение при ожидании завершения треда - int, целое число. Под Linux и Windows мы используем обертку для представления тредов в виде C11, под которой скрывается pthread и наоборот под RTOS мы можем поддерживать pthread без атрибутов, под которым скрываются треды C11. Атрибуты тредов не сильно важная деталь, по большей части она влияет на возможность настраивать алгоритм планировщика и на возможность предоставления доступа другим процессам через Shared Memory. При детальном рассмотрении атрибуты для предоставления доступа другим процессам PROCESS SHARED не требуются, поскольку этот механизм задействован автоматически при создании объекта поверх Shared Memory.

2. Для переносимости приложений связанных с обработкой данных или обработкой протоколов управления и сбора данных оказалось важным в точности соблюдать способы работы с системным временем, уметь точно настраивать интервалы. см C11 time.h. В частности мы широко используем измерение интервалов времени с использованием clock_gettime и nanosleep. (CS, _POSIX_CLOCK_SELECTION (Clock Selection option)) -- эти функции перекочевали в контроллер.

3. Близко по смыслу расположены таймеры, их можно использовать для генерации сигналов и для периодического запуска функций обработки. (TMR) _POSIX_TIMERS (Timers). В POSIX таймерные функции можно запускать в режиме треда. В итоге мы переписываем API RTOS в пользу POSIX таймеров.

4. _POSIX_SEMAPHORES простые семафоры для подсчета ресурсов, простые PLAIN мьютексы. Почему простые. Простые предполагают работу в общей памяти и их можно реализовать на базе атомарных операций. Концепция семафоров POSIX совместима c RTOS.

5. Есть примитивы синхронизации, которые мы не применяли до сих пор в контроллерах, потому что они предназначены прежде всего для синхронизации процессов в многоядерных архитектурах, но возможно они понадобятся когда-нибудь. Первое - это спинлоки, втрое - барьеры синхронизации. Я понимаю, что барьеры синхронизации возникают в задачах конвейерной обработки данных, это то, что пригодится на многоядерной системе. Про спинлоки такого сказать не могу, при прочих равных мы выбираем неблокирующие методы и стараемся не использовать такие методы. Все что можно сделать на спинлоках можно реализовать с использованием семафоров. В чем разница между спинлоками и мьютексами. Мьютекс подразумевает обладание, использовать мьютекс может только процесс владелец этого мьютекса. В нашей реализации нет разницы между мьютексом и спинлоком. RWLock - это еще один вид объектов, которые не задействованы в контроллерах. Мы все время ищем некоторый способ описать проблему Reader-Writer таким образом, чтобы читатели вообще никак не прерывали свою работу и не уходили в ожидание. В этой связи формулируем метод основанный на отложенных изменениях (отложенных транзакциях) и версиях структуры объекта (списка). В настоящее время у нас есть заготовка, модель интерфейса RWLock, без одной функции, она позволяет не блокировать читателей. Т.е. фактически мы говорим об отказе от интерфейса RWLock в пользу неблокирующих методов, и о маскировке под видом RWLock некоторого другого механизма типа STM(Транзакций памяти) или RCU. Транзакции памяти можно описать через манипуляцию страницами виртуальной памяти, атомарно подменяя страницы памяти.

6. Модель работы с флагами событий, сигналы _POSIX_REALTIME_SIGNALS. Сигналы вызывают некоторое замешательство... При внешнем сходстве, механизмы распространения сигналов в POSIX существенно более сложные, чем в RTOS. Поддержка традиционных сигналов и Realtime кажется громоздкой. Сигналы POSIX распространяются и доставляются тремя путями: Традиционно, через signal handler на уровне процесса, с привязкой к pid; сигналы ставятся в очередь, очередь привязана к pid и наследуется в какой-то мере тредами, реалтайм сигналы можно задействовать по маске и привязать к треду. Но в конечном счете весь набор этих возможностей ведет к медленной системе с обработкой структуры дерева процессов(тредов). Мы говорим о том, чтобы приспособить интерфейсы POSIX под наши задачи, под флаги процессов, а не о том, чтобы реализовать все возможности доставки сигналов описанные в POSIX.

// Обработка сигналов в CMSIS-RTOS
result = osSignalWait(маска флагов, таймаут)
if (result == osEventTimeout) {// обработка исключений
	. . .
} else {// обработка событий
	if (событие 1) ...
	if (coбытие 2) ...
}
Что-то похожее предлагает (POSIX REALTIME SIGNALS)
// POSIX REALTIME SIGNALS
signo = sigtimedwait(маска сигналов, таймаут)
if (signo<0) { // обработка ошибок
	if (errno==EAGAIN)...
	if (errno==EBUSY)... 
	if (errno==EINRT)...
} else { // обработка событий
	if (coбытие 1) ...
	if (coбытие 2) ...
}

Пояснения:
Выглядит близко к тексту. Сигналы в POSIX распространяются странными путями [см. POSIX]. Мне ясны три пути. Первый путь - традиционный, свойственный функции kill(), сигналы возникают в процессе, процесс может быть остановлен с ошибкой или обработать сигнал, используя, один из вариантов обработки. Существует множество предопределенных сигналов, которые генерируются по разным событиям. Вторым слоем идет метод поставить обработку сигналов в очередь. Каждому процессу приписываем входящую очередь сигналов. При этом сигнал уже может быть с параметром. Третий нарост, все ломается при использовании тредов, не ясно какой тред будет обрабатывать очередь сигналов. Предлагается использовать маски сигналов в тредах, и не ясно каким чудом сигнал будет доставлен в тред. Четвертый нарост - это возможность запускать обратные вызовы по случаю прихода сигналов. Эта возможность понятным образом реализуется применительно к Асинхронному вводу/выводу (AIO), Таймерам (TMR) и очередям сообщений (MSG).

Если не копаться в тонкостях внутренней реализации механизмов доставки сигналов, интерфейс sigtimedwait() позволяет описать механизм доставки через флаги процесса. У RTOS флаги процесса в каждом треде свои, а у POSIX не ясно. Таким образом, можно применить интерфейс к RTOS, но это может работать несколько иначе в POSIX системе. Для совместимости с POSIX следует использовать маски сигналов, и учитывать тот факт, что свободными для использования считаются сигналы в диапазоне [SIGRTMIN, SIGRTMAX]. Кстати, переносимость на этом заканчивается, Сигналы далеко не везде поддерживаются.

В POSIX широко используется передача кодов ошибки через errno. Критика: errno - это пережиток старины. Errno - это "костыль" для старой концепции юниксо-подобной ОС. Концепция была в том, что у процесса есть некоторый контекст, который включает ряд переменных: errno, environ, текущую рабочую директорию, идентификацию пользователя и т.д., включая, между прочим, контекст разбора множества функций, которые ныне не попадают в категорию Thread-Safe и Reentrant. Результат работы функции ожидания может включать код ошибки. Более экономичный способ передачи кода ошибки - передавать в отрицательных числах результата.

Сама по себе система заполнения и расшифровки errno вызывает лишние действия, как в библиотеке, так и в приложении. Хотя в нашей системе RTOS мы поддерживаем существование errno, я считаю - это пережиток прошлого и от него надо избавляться.

7. Вместо работы с файлами, у нас есть база данных, директория (иерархия системных объектов в форме дерева, в узлах - директории, на ветвях - файлы или устройства; POSIX под директорией понимает список файлов в узле дерева). В дереве размещаются объекты предопределенных типов. Мы можем запросить объект и получить его образ в памяти используя путь в дереве. Альтернативой является поиск в хеш-таблице или в дереве по уникальному идентификатору объекта. В POSIX определено сочетание {ino_t, dev_t} которое дает уникальный идентифкатор системного объекта в рамках системы или носителя. В нашем случае все данные, кроме разве что картинок и медиа-файлов, располагаются в памяти, отображаются в память. Использовать интерфейс open-read не целесообразно, он ведет к многократному копированию данных, в то время, как чтение возможно напрямую из флеш-памяти.

Для исключения копирования файлов мы поддерживаем ряд функций свойственных POSIX: отображение файлов в память _POSIX_MAPPED_FILES, защиту памяти _POSIX_MEMORY_PROTECTION. Мы готовы поддерживать обращение в стиле open() к директории (базе), но только с нечеловеческими путями. Нет смысла поддерживать текстовые пути. Возможно использовать коллекции объектов и обращаться к объектам с использованием типизованных идентификаторов {ino_t, dev_t}. Использование типизованных идентификаторов может быть вполне совместимо с представлением мультибайтовых строк c11. Только в нашем случае каждый символ мультибайтовой строки - это идентификатор объекта в структуре директории (в дереве устройств). Мы говорим о том, чтобы поддерживать систему имен в иерархии объектов, чтобы адресовать объекты по их уникальным идентификаторам. С точки зрения реализации, директорию (иерархию объектов в форме дерева) поддерживать оказывается проще, чем дерево с уникальными идентификаторами. Хеш таблица или бинарное дерево идентификаторов может располагаться как в каждой директории, так и в точке монтирования.

Мы говорим о расширении концепции поиска в дереве (см. [Раздел 4.13 POSIX.1-2017]) через представление идентификаторов в форме мультибайтововой строки, и представление уникального идентификатора объекта в форме {ino_t, dev_t}.

В POSIX удвоили количество системных функций относящихся к файловой системе, чтобы иметь возможность адресовать элементы поддерева, блокируя удаление фрагментов дерева. Это такая замечательная идея, суть которой в том, что в процессе поиска файлов том можно размонтировать, том или директорию можно удалить. Спасение от этого видится в увеличении числа ссылок на объект иерархии в процессе поиска.. Два параметра нужны, чтобы блокировать на изменение фрагмент дерева, пока ведется поиск в дереве. В тоже время такой способ позволяет адресовать объекты без разрешения путей, с использованием уникальных идентификаторов. В результате мы выделяем три идентификатора: корневую директорию (/) директорию системных устройств (/dev) и (CWD - рабочую директорию, где создаются обычные файлы). Нам нужна функция поиска в директории, с использованием хеш таблиц, которая с одной стороны следует стандарту POSIX, с другой стороны удовлетворяет нашим требованиям - использует идентификаторы вместо имен.

При создании файлов с использованием путей мы хотим иметь возможность автоматически именовать системные устрйства и файлы с использованием уникальных идентификаторов {ino_t, dev_t}, ino - уникальный номер объекта для данного типа устройства dev.

В итоге мы добавляем с систему RTOS следующий функционал, для регистрации объектов (системных устройсв и файлов) в дереве:
1. Разрешение имен в дереве относительно выбранных директорий.
dtree_path(at_dev, path, filename) -- разрешение имен файлов
dtree_link(at_dev, filename, dev) -- устанавливает ссылку линк в дереве.
dtree_nlink(dev) -- атомарно увеличивает число ссылок на системный объект.
dtree_unref(dev) -- уменьшает число ссылок, удаляет неиспользуемые объекты.
dtree_mknod(at_dev, filename, mode, OID) -- объект создается в режиме mode
Если OID при создании содержит только идентификатор типа, то нумерация объекта выполянется последовательно. filename - может быть NULL, в таком случае нумерация объектов выполняется последовательно с использованием уникального идентификатора и не использует имена файлов.

Начальная загрузка. Нужна начальная инициализация иерархии системных объектов, структуры директорий, файлов и регистрация системных устройств. Мы в связи с этим сформулировали сопособ загрузки из архива, архив - это файловая система ориентированная на чтение и прямое отображение в память. Можно использовать традиционный формат типа cpio, описанный в POSIX. А можно сделать, чтобы начальная загрузка использовала разметку данных от файловой системы. Для переносимости нужно поддержать один из форматов cpio.

Мы хотим регистрировать записи в файловой системе (RFS - Read-only FS) статически, определяя объекты в исходном коде как макросы, определяя константы в специальном сегменте данных, без какого либо дополнительного кода, без утилиты для создания архива.

Мы определяем записи файловой системы, как данные относящиеся к сегменту памяти "начальная загрузка". На этапе компиляции формируется сегмент данных, дописываемых в конец образа загрузки. Для этого мы определяем RFS - макросы для статической регистрации системных объектов в Read-only File System файловой системе, отображаемой в память. Таким образом, мы хотим создавать начальную структуру директорий и регистрировать системные устройства и системные объекты, включая Shared Memory Objects, Semaphores, MQueue, FIFO, ... Мне почему то хочется сразу заложить в функционал RFS возможность шифрования (криптографической защиты) данных на носителе, и возможность упаковки данных типа LZ4, LZO, LZJB2.

В POSIX принято все системные объекты называть устройствами. Файловая иерархия, - дерево системных объектов, направленный граф. В узлах дерева располагаются директории - списки объектов, директории наследуют информацию об устройстве в точке монтирования, файл привязан к классу драйвера устройства через директорию. Файл это системный объект, а вот приложения работают с дескриптором файла - идентификатором, дескриптору файла соответствует некоторый объект Open File Description, который собственно содержит информацию о позиции записи/чтения и ссылается на системный объект типа Файл. В то же время для работы с большинством объектов не нужно создавать отдельные сущности для поддержки сессии и по дескриптору можно напрямую получить сам системный объект. Всего насчитывается конечное число объектов около десятка: Файл, Директория, Разделяемый объект в памяти, сегмент памяти, семафор, очередь, FIFO, Symlink, Сокет. POSIX кроме этого определяет еще ряд непонятных устрйоств типа блочное устройство и символьное устройство. Концептуально POSIX разделяет понятие STREAM - поток данных в понимании POSIX и stream - объект типа FILE в понимании стандарта Си <stdio.h>. STREAM подразумевает очередь транзакций чтения/запись.

Общее впечатление от детального изучения стандарта - он не додуманный, обширный и не дододуманный. Я смотрел драфты на стандарт, в него продолжают добавлять заплатки, чтобы закрыть проблемы работы уже стандартизированных интерфейсов. Одна из таких проблем - синхронизация процессов в Realtime и монотонное время. Другая - сигналы. Третья - thread-safe, signal-safe, reentrancy, многие функции не обладают этим свойством. В результате количество фукнций удваивается и утраивается.

Комментариев нет:

Отправить комментарий