Pull to refresh

Comments 89

Интересная статья!
Будет ли организована защита памяти процессов с помощью MPU?
инверсия приоритетов
Расскажите как избежать.
Ваша ОС будет полноценной (процессы, нити, исполняемые файлы типа «exe» и динамическая линковка типа «dll»)?

В РТОС обычно нет ни процессов, ни загрузки таксков, ни (особенно) динамической линковки. Потому что РТОС использую на железе, где нет аппаратного memory-management unit (не путать с memory-protection unit) и поэтому не поддерживает виртуальную память, которая для всего этого по-хорошему нужна.

"Нити", потоки в РТОС это Таски и есть.

Загружаемые таски сделать, конечно можно при помощи статически слинкованных и использующих position-independent code, но обычно для таких целей уже лучше иапользовать железо, которое тянет Embedded Linux с аппаратным или программным MMU

В РТОС обычно нет ни процессов, ни загрузки таксков, ни (особенно) динамической линковки. Потому что РТОС использую на железе, где нет аппаратного memory-management unit (не путать с memory-protection unit) и поэтому не поддерживает виртуальную память, которая для всего этого по-хорошему нужна.
Как MMU способствует линковке? Оно ведь только позволяет избежать фрагментации общесистемной памяти (и расширить её) при создании новых heap создав виртуальное адресное пространство.
(не путать с memory-protection unit)
Так вы собираетесь его задействовать по прямому назначению?

Как MMU способствует линковке? Оно ведь только позволяет избежать
фрагментации общесистемной памяти (и расширить её) при создании новых
heap создав виртуальное адресное пространство.

Ответил ниже.

Так вы собираетесь его задействовать по прямому назначению?

Не совсем уверен, что правильно понимаю Ваш вопрос. Но поясню в чем MMU и MPU можно перепутать, кроме очевидной ошибки в одном слове/букве и MMU и MPU выполняют функцию изоляции информации между процессами. Но при этом, MMU дополнительно предоставляет механизм вирутальной памяти. И казалось бы все просто, но вот только из некоторых MPU при желании можно сделать очень неэффективный software MMU. Для этого необходимо заблокировать процессу прямой доступ ко всем диапазонам памяти, а в обработчике прерывания ошибки доступа декодировать невыполненную инструкцию, выполнить ее "вручную" с подменой целевого адреса через софтовый же MMU/TLB и верунть исполнение программе дальше.

Но при этом, MMU дополнительно предоставляет механизм виртуальной памяти.
Меня интересует конкретно механизм защиты памяти, а не её виртуализация. Память будет защищена в этой ОС?

Если у Вас обычным образом настроенный MMU, то будет. Механизм простой - на железном уровне зарпашиваемый виртуальный адрес транслируется в физический. При этом запросы на чтение могут идти на одни и те же физические адреса в случае расшареных библиотек, но запись (если она вообще разрешена флагами доступа, который присвоены странице) в большинстве современных ОС вызовет механизм Copy on Write (CoW) и Ваша страница будет размножена для достижения уникальности.

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

Если у Вас обычным образом настроенный MMU, то будет.
У меня нет MMU, есть только MPU. Что мне делать?

Смотря чего Вы вообще пытаетесь добиться? Настраивать MPU при переключении задач, чтобы каждый таск в РТОС мог писать/читать только свою область памяти и вызывал ошибку при попытке влезть в чужое?

Да, возможно. Это то, для чего MPU предназначены.

Хорошо что мы с этим разобрались. Спасибо.

MMU мало связан с загрузкой и линковкой кода. Оно вполне осуществляется на любом контроллере который поддерживает выполнение кода из оперативной памяти.

Но да, ограничений в случае мелкого контроллера больше чем хотелось бы.

Назовите вкратце, какие ограничения (кроме фрагментации памяти) в МК мешают динамической линковке? Хочется разобраться.

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

Если Вы попытаетесь загрузить ее и "слинковать" вручную, то Вам каким-то образом нужно будет перехватить все запросы к адресам в блоке Х и подменить адреса так, чтобы они ушли в блок Y с корректными смещениями. Если у Вас есть MMU, то это тривиально. Вы просто подставляете в таблице виртуальных адресов адресам для диапазона X корректные физические адреса, на которых загружена библоитека.

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

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

И это все равно будет работать только до тех пор, пока Ваша библиотека thread-safe. Если только нет, то ее разделение между тасками потребует кучу дополнительных оверхедов для менеджмента синхронизации. В случае с MMU этой проблемы нет, потому что когда нужно, страница в которую поток попытается что-то записать будет просто скопирована и станет уникальной для этого потока.

Вот здесь немного о том, как можно делать shared библиотеки без MMU http://xflat.sourceforge.net/NoMMUSharedLibs.html

Если у Вас есть MMU, то это тривиально. Вы просто подставляете в таблице виртуальных адресов адресам для диапазона X корректные физические адреса, на которых загружена библиотека.
Т.е. при переходе от библиотеки к библиотеке наш MMU постоянно перезагружается?
В общем это просто сложнее. И я не слышал, чтобы это реально кто-то применял. Тем более, что когда вы делаете встраиваемую систему куда проще просто скомпилировать свой код, ...
А если мне не просто помигать светодиодом, но и GUI и WebCam и Webserver с Websocket и JS, C# и uPyton интерпретатор и чтобы всё одновременно и оперативы у меня 64МБ и QSPI на 1Gb, то всё, я в пролёте?

Т.е. при переходе от библиотеки к библиотеке наш MMU постоянно перезагружается?

MMU это не устройство, которому требуется "перезагружаться" от библиотеки к библиотеки. Это не одна пара подстановок адрес-Zадрес, а целая таблица недавно запрошенных подстановок (Translation Lookaside Buffer - TLB), огромная древообразная структура в оперативной памяти, которая сохраянет все возможные значения подстановок и обычно небольшое количество указателей на эти структуры в памяти, которые определяют контекст подстановок в разных режимах работы процессора (у процесса свой указатель, у ОС свой, у гипервизора и драйверов свои).

MMU требуется только сбрасывать данные из TLB при переключении между процессами. Это одна из причин, почему в больших ОС переключение задач значительно медленнее, чем в РТОС, в которых этого механизма просто нет.

А если мне не просто помигать светодиодом, но и GUI и WebCam и Webserver
с Websocket и JS, C# и uPyton интерпретатор и чтобы всё одновременно и
оперативы у меня 64МБ и QSPI на 1Gb, то всё, я в пролёте?

Самый главный вопрос тут: а что Вам вообще с Вашей точки зрения даст динамическая линковка, если Вам нужно чтобы "все одновременно"? Это же не какая-то магия, чтобы при динамической линковке все резко стало меньше места занимать. Статически у Вас даже больше опций будет, потому что сможете вручную свои 64 Мб расписать между программами, а выделить общие библиотеки, чтобы избежать дублирования можно и без динамической линковки. Так что либо в обоих случаях влезет, либо не влезет, какую линковку не используй.

Если Вы попытаетесь загрузить ее и «слинковать» вручную, то Вам каким-то образом нужно будет перехватить все запросы к адресам в блоке Х и подменить адреса так, чтобы они ушли в блок Y с корректными смещениями. Если у Вас есть MMU, то это тривиально.
MMU это не устройство, которому требуется «перезагружаться» от библиотеки к библиотеки.
Эти два утверждения не вяжутся. Т.к. динамические библиотеки не знают о существовании друг друга, то их виртуальные адреса будут совпадать, а MMU мы не перезагружаем то он накладываются.
Самый главный вопрос тут: а что Вам вообще с Вашей точки зрения даст динамическая линковка, если Вам нужно чтобы «все одновременно»?
Я хочу написать приложение для МК и не знать что там вообще происходит с другими процессами, если есть под мою программу место то — запускается, если нет то сообщить о недостатке памяти. А также хочу скомпилировать её один раз и чтобы она работала на всех МК с архитектурой ARMv7-M и ARMv8-M (Mainline). Как то так.

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

Это некорректное утверждение. Загрузка двух динамических библиотек для одного процесса не будет никогда произведена на одни и те же виртуальные адреса.

Я хочу написать приложение для МК и не знать что там вообще происходит с
другими процессами, если есть под мою программу место то — запускается,
если нет то сообщить о недостатке памяти.

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

Альтернатива только девайс с виртуальной памятью. То есть либо аппаратный, либо хотя бы софтовый MMU. Иначе никак из-за "не знать что там вообще происходит с другими процессами"

А также хочу скомпилировать её один раз и чтобы она работала на всех МК с архитектурой ARMv7-M и ARMv8-M (Mainline). Как то так.

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

Это некорректное утверждение. Загрузка двух динамических библиотек для одного процесса не будет никогда произведена на одни и те же виртуальные адреса.
Ага, значит виртуальные адреса никогда не совпадают, и при этом они также загружены в разные физические участки RAM. Так что нам мешает в МК прилинковать не к виртуальным а к физическим и MMU нам и не понадобился.
Если Вы заранее знаете весь набор приложений
Набор всех приложений не известен.
Это к разговору о динамической линковке точно не относится.
Эти приложения могут находится в разных участках памяти и к ним нужно прилинковать ОС которая тоже может находится где угодно.
попытки написать что-то универсально кросс-платформенное
Здесь общая архитектура, а не платформа.

Ага, значит виртуальные адреса никогда не совпадают

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

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

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

Так что нам мешает в МК прилинковать не к виртуальным а к физическим и MMU нам и не понадобился.

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

Исключение - использование Position-Independent code, в котором все внутренние вызовы и ссылки на "глобальные переменные" привязаны к значению счетчика команд (Program Counter, PC). Такой код сам по себе можно загрузить в любое (почти) место в памяти и он будет работать. И к такому коду можно вручную "прилинковать" вызов библиотечных функций через указатели, как Вы предлагаете ниже. НО это работает с рядом оговорок, многие из которых я уже упоминал выше и поэтому я не стал бы это называть динамической линковкой, потому что это примерно как назвать велосипед автомобилем, только потому что формально и то и другое - транспортное средство.

Здесь общая архитектура, а не платформа.

https://en.wikipedia.org/wiki/Cross-platform_software
"Platform can refer to the type of processor (CPU) or other hardware on which an operating system (OS) or application runs, the type of OS, or a combination of the two.[4]"

Эти приложения могут находится в разных участках памяти и к ним нужно прилинковать ОС которая тоже может находится где угодно.

Пожалуйста, обрабтите внимание на то, что я цитирую и на что отвечаю.

Виртуальные адреса не совпадают у разных библиотек загружаемых в один процесс.
Физические адреса тоже не совпадают, иначе мы бы не смогли их держать в памяти одновременно. Процесс заранее не знает где будут библиотеки до их линковки. Это в точности повторяет работу МК без виртуальной памяти.
В этом и состоит суть концепции виртуальной памяти — с точки зрения процесса он исполняется на устройстве монопольно.
Так же как и процесс на МК, тоже не знает о существовании других процессов. Ведь он точно так же не имеет права исполнять, писать и читать то чего ему не выделили.
разные процессы, скомпилированные в расчете на виртуальную память будут каждый ожидать разное содержимое одних и тех же виртуальных адресов.
Это не так, процессы работают только с той памятью о которой им сообщат динамически, т.е. им заранее ничего не известно. Только часть заранее загруженных библиотек API у всех процессов совпадают в VM, и только по тому что процесс их линковки при запуске одинаков для всех, не более.
Ничего не мешает, но это уже не динамическая линковка.
Динамичнее уже некуда.
физическое адресное пространство не резиновое.
1ГБ как минимум можно примапить к МК, гуляй сколько хочешь.
использование Position-Independent code… НО это работает с рядом оговорок
Можно услышать хоть одну внятную оговорочку?

Платформа очень широкое понятие, в отличие от архитектуры, и в нашем контексте не уместно.

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

Если у Вас всего один процесс, то все так. А теперь просто пресдтавьте как это все будет работать (даже безотносительно линковки и библиотек), если процессы скомипилированы с использованием одних и тех же адресов. Что тогда Ваша ОС будет делать?

Так же как и процесс на МК, тоже не знает о существовании других
процессов. Ведь он точно так же не имеет права исполнять, писать и
читать то чего ему не выделили.

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

Это не так, процессы работают только с той памятью о которой им сообщат динамически, т.е. им заранее ничего не известно.

Это Вы откуда такую информацию взяли?

Динамичнее уже некуда.

Ну, если мы с Вами разные вещи называем тапками, то, кажется, продолжать обсуждение тапок довольно бесполезно.

1ГБ как минимум можно примапить к МК, гуляй сколько хочешь.

Зачем Вам в таком случае вообще библиотеки динамически линковать? Скомпилируйте все свои таски с нужными им библиотеками статически и будет Вам простое и надежное решение.

Можно услышать хоть одну внятную оговорочку?

Иcпользуемые библиотеки должны быть thread-safe.

Платформа очень широкое понятие, в отличие от архитектуры, и в нашем контексте не уместно.

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

если процессы скомипилированы с использованием одних и тех же адресов. Что тогда Ваша ОС будет делать?
Не надо их так компилировать.
то они скорее всего будет скопилированы с использованием пересекающихся адресов.
Они не будут так скомпилированы если им этого не позволять.
Это Вы откуда такую информацию взяли?
Давно где то читал, где — уже не вспомню.
обсуждение тапок довольно бесполезно.
Согласен, это бессмысленно и не нужно.
Зачем Вам в таком случае вообще библиотеки динамически линковать?
Мне так кажется более удобно и экономично при переиспользовании повторяющегося кода для приложений которые компилируются независимо.
библиотеки должны быть thread-safe.
Хорошо, делаем их такими где требуется.
Я не знаю, что уместно в Вашем контексте да и вообще теряюсь уже в чем он вообще состоит
Хочу полноценную ОС для МК без виртуальных машин.

Не надо их так компилировать.

Они не будут так скомпилированы если им этого не позволять.

А как надо?

Давно где то читал, где — уже не вспомню.

В таком случае советую почитать про форматы хранения файлов, например стурктуры заголовков ELF или EXE файлов. Мне кажется, Вас там ждет новая информация.

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

Удобно делать лишние действия? Видимо определение слова удобно у нас с Вами тоже не совпадают.

Хорошо, делаем их такими где требуется.

Вот так вот просто берем и делаем, да? Возможно я чего-то не знаю и где-то в документации на какой-нибудь gcc пропустил флаг --do-your-magic-and-make-it-thread-safe

Хочу полноценную ОС для МК без виртуальных машин.

Зависит от того, что Вы подразумеваете под "полноценной ОС". Но если брать максимально полноценные варианты, то:

Если у Вашего МК есть MMU -> Linux
Если у Вашего МК нет MMU -> uCLinux https://en.wikipedia.org/wiki/ΜClinux

А как надо?
С позиционно независимым флагом.
ELF или EXE файлов. Мне кажется, Вас там ждет новая информация.

Читаем из нашей любимой википедии:
Файлы PE не содержат позиционно-независимого кода. Вместо этого они скомпилированы для предпочтительного базового адреса, и все адреса, генерируемые компилятором/компоновщиком, заранее фиксированы. Если PE-файл не может быть загружен по своему предпочтительному адресу (потому что он уже занят чем-то ещё), операционная система будет перебазировать его.
Это отличает формат PE от ELF, который использует полностью позиционно-независимый код и глобальную таблицу смещений, которая жертвует временем выполнения в пользу расходования памяти.
Всё верно, как и предполагалось.
Видимо определение слова удобно у нас с Вами тоже не совпадают.
Да это удобно, разве нет?
Вот так вот просто берем и делаем, да?
Да, и ОС предоставит нам для этого все необходимые интерфейсы.
Если у Вашего МК нет MMU -> uCLinux
Как то вяло он разрабатывается. И зачем нам линукс на мк если от него там только одно название и останется.

Для таких целей можно использовать виртуальную машину, например, pawn https://www.compuphase.com/pawn/pawn.htm. Однажды собранная программа будет работать везде: линукс, микроконтроллер, Windows и т. д. Она поддерживается на ARMv7. Имеет достаточно хорошую производительность, позволяет писать свои нативные функции и имеет СИ похожий синтаксис. И главное - она влазит даже в самый мелкий cortex.

я не слышал, чтобы это реально кто-то применял


Я реализовал динамическую загрузку кода в Flipper Zero, оно на стадии пруф оф концепт, но до доведения до юзабельного состояния правок самого механизма загрузки и линковки не требуется, вопрос в составлении ABI ядра и билд-системе.

Библиотекой в таком подходе выступает ядро, приложением - загружаемый код. Механизмы синхронизации те же самые что использует ядро (у нас ядро умеет вкомпиливать в себя приложения, поэтому оно обязано быть thread safe).

правок самого механизма загрузки и линковки не требуется
Сам код компилируется в заранее известное место в ROM памяти? А если два человека скомпилируют, то одновременно их программы работать не будут?
А глобальные переменные процесса как обнаруживаются?
У приложений есть глобальные переменные, и процесс должен знать где в памяти они находятся чтобы ими пользоваться. Или вы их вообще не задействуете?

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

Вполне достаточно передать адрес на таблицу относительных адресов функций. 4 байта затрат на динамическую библиотеку.
Либо в ROM, либо в RAM грузим. После этого спрашиваем у неё адрес на таблицу и работаем.

Сотни килобайт в RAM? Даже в ROM оно не всегда помещается.

Таблица относительных адресов функций (интерфейсов динамической библиотеки) будет весить 4*N байт, где N — количество импортируемых функций. Находится она в самой библиотеке. Для вызова достаточно хранить в ОЗУ только 4 байта (на одну либу) с адресом положения этой таблицы.

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

Плюс механизм верификации совместимости abi в этом случае будет очень не гибким.

Невозможно
Ну ладно, уговорили.
Точнее можно будет
Всё таки можно. Тогда продолжаем.
грязно и небезопасно
Здесь могу предложить сделать всё наоборот.
Плюс механизм верификации совместимости abi в этом случае будет очень не гибким.
Тогда нужно использовать стандарт abi для ARM.

А если серьёзно, то Вы так и не назвали в чём именно состоит проблема, кроме как отговорки, что типа сложно и никто так не делает и мы тогда тоже не будем. Если бы было сказано, что это попросту Вам, да и вообще никому не нужно, то и вопросы сразу же отпали бы.

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

Я продолжу диалог после того как вы приведете пример кода приложения которое бы вызывало функцию printf из ядра ОС и могло быть как вкомпилировано в ядро, так и запущено извне.

Вы не понимаете что такое abi.
Скорее всего я не понимаю, что именно Вы этим abi считаете.
вызывало функцию printf из ядра ОС и могло быть как вкомпилировано в ядро, так и запущено извне.
Это очень странное требование. Ядро не должно не то что вызывать printf и подобные ей, но и даже ничего о ней не знать. По хорошему, каждое приложение и динамическая библиотека должны иметь собственную реализацию таких функций как printf и использовать для вывода API ядра. Но если Вас интересует именно динамическая линковка библиотеки с набором стандартных функций (для экономии памяти приложений и библиотек, и это правильно), то достаточно один раз их обернуть в функции «заглушки» и в статической библиотеке (для автоматической линковки dll) прописать их уже реальные названия получив позиции «заглушек» из динамической таблицы.
Я продолжу диалог после того как вы приведете пример кода
Если есть какая то проблема, которая не позволит мне написать такой код, то я этого сделать не смогу и буду зря тратить время на реализацию того что невозможно. Если бы озвучили этот проблемный момент, то сразу бы всё стало на свои места. Возможно мне не хватает квалификации и опыта в данном вопросе и я просто не могу понять что такая проблем реально существует.

Спасибо за Ваш труд. Интерес, конечно, есть (по крайней мере у многих).

Насчёт переключений задач: когда-то делал однозадачную ОС, можно было попробовать и многозадачность, но показалось неэффективно. Вместо этого, когда появился реальный проект, сделал задачи - каждую на своем ядре. Там надо было отслеживать датчики и выдавать воздействия в жестком реальном времени, на сохранение/извлечение контекста просто не было времени, это бы нарушило все временные характеристики. Правда, плата была RPi 3 A+ (или B+) с 4-мя ядрами Cortex-A53. Кстати, всё это работает и на Zero 2 W, которая вполне себе компактная.

Тут же встает вопрос, если ядер 8 штук (или 10), то можно ведь 10 процессов запустить без переключений задач... И RISC-V (Kendryte K210) тоже прекрасно справляется с двумя совершенно разными задачами на своих 2 ядрах (детектор объектов с камеры и распознавание голосовых команд с микрофона).

Я рассматривал именно одноядерные микроконтроллеры, например как stm32F103. Если ядер больше одного, то все маленько усложняется. Я даже не встречал, чтобы использовали N ядерные системы на cortex-m3 или cortex-m4.
Cortex-A53 - это вполне мощное ядро на котором уже запускают Linux. Но Linux - это не Real Time (в отличие от QNX) на нем сложно добится даже 100 микросекундного отклика на события, нпаример, реакция на внешнее прерывание.
Если же Embdedded RTOS (на том же stm32F103) спроектирована правильно, то она обладает очень хорошей реакцией на события (в разы лучше чем Linux). При написание RTOS я делал упор именно на низкую латентность.

Я даже не встречал, чтобы использовали N ядерные системы на cortex-m3 или cortex-m4.
Sony's CXD5602 microcontroller (ARM® Cortex®-M4F × 6 cores) with a clock speed of 156 MHz. Sony Spresense Main Board

RP2040 из Rpi pico имеет два ядра ARM Cortex-M0+.

nRF5240 имеет ядро cortex-m4 для пользователей, но и вроде cortex-m0 для радиомодема.

И другие.

Cortex-A53 - я не имел ввиду Linux, а только "голое железо". И только жёсткий реал-тайм. Тот, где надо "дрыгать ногой" с частотой в (десятки) МегаГерц или следить за чей-то "ногой" с той же скоростью.

Интересно, сколько микросекунд занимает переключение задач при работе на одном ядре в cortex-m0.

Там всё просто, в программе на ассемблере разделяете ядра и направляете каждое ядро "на его путь истинный", например, на main0, main1,... или как хотите их назовите на языке C.

  1. Мне кажется это разные задачи. Я не работал с Rpi pico, но у меня был опыт с atsam4c и он имеет два ядра, но они не предназначены для rtos и имеют разную архитектуру. Их нужно рассматривать как два ядра в одном корпусе и они не равноправны, имеют доступ к разной периферии, одно dma может работать с одним ядром с другим нет, одно ядро имеет доступ ко всей RAM другое только к её части, одно ядро может выполнять код из flash - другое только из RAM и. д.т. Возможно существуют полноценные N ядерные микроконтроллеры, но в своей практике мне не приходилось с ними работать. Обычно если более одного ядра, то второе - это специализированное ядро для особых задач.

  2. Для cortex-m0 время переключения можно посчитать по асемблерным командам, но я думаю даже на частоте 20-40 МГц, оно будет либо соизмеримо либо меньше 1 микро секунды.

Правильно ли я понял, что у вас функция void PendSV_Handler(void) да еще с вызовом планировщика выполнится в худшем случае за 1 мкс?

Смотря какая частота и надо считать по асм инструкциям. Но именно про void PendSV_Handler(void) я имел в виду. Работа планировщик тоже происходит очень быстро, когда происходит какое-нибудь событие, не важно где в прерывания или в задаче, устанавливается флаг программного прерывания. Если это прерывание, то поскольку PendSV имеет более низкий приоритет, прерывание заканчивается на PendSV (без дополнительного входа-выхода из irq, это тоже одна из фикеш cortex)

а разве нет какого-нибудь счетчика тактов ядра? Тогда и частота не нужна.

ну тогда может кто-то и измерит. У меня , к сожалению, никакого кортекса нет :)

Не буду оценивать Ваши слова о том что Вы "не Пушкин и даже не Лермонтов" (то есть второй все таки пониже первого - сильное заявление, требует доказательств), но (простите великодушно) Вы и не Кнут. И если первое можно понять и простить, то невнимательность к техническим деталям на техническом ресурсе не допустима.

Итак, поехали:
0. Наличие двух стеков (все таки двух указателей стеков), изменяемых приоритетов прерываний и программного прерывания никак не является необходимым условием для написания RTOS, существуют многочисленные примеры обратного. Более того, наличие двух стеков создает определенные трудности, хотя и может быть полезным.
1. реализация двусвязного (не надо придумывать собственную терминологию) списка включает в себя данные полезной нагрузки, иначе список превращается в "вещь в себе".
2. при работе со списками задач необходима критическая секция, иначе Вам обеспечены непредсказуемые и неповторяемые баги.
3. использование абсолютно ничего не говорящего идентификатора nlst, несомненно, является фирменным стилем
4. и так далее и тому подобное.

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

Честно сказать не совсем понятен коментарий.

  1. Два стека позволяют существенно экономить на памяти. Их я использую, потому что cortex позволяет это делать. Конечно это не является обязательным условием, но если микроконтроллер это поддерживает на аппаратном уровне почему бы это не использовать.

  1. По поводу двусвязного списка - это стандартное определение. Есть односвязный, двусвязный и XOR списки вещи вполне стандартные https://ru.wikipedia.org/wiki/Связный_список. В статье я показываю, как использовать двусвязный список и почему именно его. Это тоже вполне стандартная практика, посмотрите хотя бы ядро линукс, там он везде используется.

  2. Критические секции по коду используются везде, где нужно, за это отвечаю функции
    EM_DISABLE_TASK() и EM_ENABLE_TASK(). Почему я не описал критические именно в этой статье, потому что в ней я хотел рассказать об общих принципах. Использование критических секций планировалось описать в следующей статье

  3. Вот это если честно я вообще не понял, что не так-то?

Сложилось в печатление, что Вы даже не открывали код самой RTOS хотя ссылка на нее есть в самом начале статьи. Давайте еще раз ее продублирую https://github.com/IvanShipaev/EmTask.git

Два стека позволяют существенно экономить на памяти. Их я использую, потому что cortex позволяет это делать. Конечно это не является обязательным условием, но если микроконтроллер это поддерживает на аппаратном уровне почему бы это не использовать

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

В общем, все верно у вас, с комментатором выше не согласен. Можно было еще оптимизировать немного.

вместо отнимания 1

int rt_em_find_first_bit(int value)
{
	__asm volatile ( "clz	%0, %0" : "=r" (value) );
	return 31 - value;
}

сделать так:

return __CLZ(__RBIT(taskStatus));

А так, такие операционки с двумя стеками контекст переключают долго, даже у вас это будет довольно долго, несмотря на оптимальный алгоритм. В Run to comlition там буквально пару строк на ассемблере, если событие приходит из прерывания, и после прерывания надо переключиться в высокоприоритетную задачу. А если переключаться просто из низко-приоритеной в высоко-приритеной - так вообще сразу практически, как только нашел. Но это, с учетом, если я правильно понял мысль вышестоящего комментатора.

А так, такие операционки с двумя стеками контекст переключают долго, даже у вас это будет довольно долго, несмотря на оптимальный алгоритм.
Вы имеете в виду долгое переключение между PSP и MSP? С чем это связано?

Да вообще, ну смотрите, если в РанТокоплишен, переключение делается примерно вот так:

while (nextTaskId < activeTaskId)
      {
        activeTaskId = nextTaskId;
        CallTask(nextTaskId); // вызываем задачу и сбрасываем установленное событие
        nextTaskId = GetFirstActiveTaskId(); // вдруг есть еще активные задачи
      }

Т.е. по факту, просто вызываем функцию высокопритетной задачи прямо на этом же стеке.

То в обычной, придется все делать через PendSv и иже с ним, что пойдет после прерывания таймера и вызова планировщика - ну и соотвественно вызывать планировщик с каждым тиком тоже надо, а это тоже затраты. В РанТокоплишен можно на каждую задачу навешать свой таймер и он будет вызывать её чисто в тот момент когда она должна запуститься, т.е. планировщик будет вызываться только в момент, когда реально задача или несколько задач готовы.

Я вот тут, ну совсем примитивный способ переключения описывал https://habr.com/ru/post/506414/

Многозадачность не вытесняющая?

Если я Вас правильно понял, то что Вы описываете - это кооперативная многозадачность. Вытесняемая многозадачность устроена по другому. Данной RTOS - вытесняемая многозадачность.

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

Принцип работы такой же как и у стандартных прерываний? Тогда если будет несколько задач (больших и маленьких) с одинаковыми приоритетами то в этот момент многозадачность становится кооперативная для этой группы.

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

Если задача должна выполнятся раз в 1 мс, то заводится таймер, который раз в 1 милисекунду посылает событие задаче. После выхода из прерывания таймера вызывается планировщик, он ищет самую высокопритеную и запускает её.

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

Если задачи имеют одинаковый приоритет, то пока не выполнится одна, вторая не запустится, но тоже самое будет и у вас, пока вы не поставите, что то типа Sleep, но по сути это означает, что вы уже сделали всё что хотели и следующую порцию будете делать через некоторое время. В рантукоплишен задаче это делается так - выполнить работу, запустить таймер и выйти, таймер через заданное время снова пошлет задаче событие. Она тут же запустится.

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

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

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

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

Вытесняющий планировщик разделит время на все процессы с одинаковым приоритетом (условно по 10млс на задачу), даже если там будет бесконечный цикл, всё равно всем достанется

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

При кооперативной многозадачности все приложения делят процессорное время, периодически передавая управление следующей задаче

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

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

return __CLZ(__RBIT(taskStatus));

Спасибо за комментарий, но как этот код позволит определить номер установленного старшего бита? Он определяет номер установленного младшего бита, но нам не это нужно.

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

Если две задачи равного приеритета?

Тоже самое будет, что и у вас, только меджик намбер уйдёт 31. Ну и с - проблем не будет.

Но все-таки в чем оптимизация? И там и там 2 инструкции на ассемблере. Проблем тоже вроде нет.

По поводу п.0 , думаю, что GarryC не понравилась Ваша стремная фраза "для RTOS в нем есть все необходимое " :) Возможно, если бы вы написали не так категорично, а,например,что-то типа "для RTOS есть интересные возможности", то ответ был бы иным ;)

По поводу 2-х указателей стека. Мне нравится такое решение. Я использовал такой же подход для FreeRTOS, портируя её на DSP 1967ВН044.

Могу объяснить свою фразу. Дело в том, что я достаточно долго работал с AVR, stm8, pic16, mcs51 и т.д. В них также реализовывал различного рода переключатели контекстов. И как бы так сказать, там все как-то не очень удобно. И поэтому многие вещи, которые заложены в Cortex-m (также как и в либом ARM) , я думаю они закладывались специально при проектировании ядра с учётом того, чтобы для него было удобно писать rtos.

А вот как на Ваш взгляд - RISC-V проектировался чтобы удобно было писать RTOS? Хотелось бы чтобы Вы продолжили свою тему, т.к. Вы рассмотрели пока только простые вещи, а вот интересно как раз посмотреть реализацию чего-то более сложного.

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

UFO just landed and posted this here

Кстати, вот вы упомянули про аж целых два прерывания, а на самом деле нужно только одно и второе во фриртосе используется исключительно для поддержания совместимости с ядрами в которых есть ММU. И в порте для кортекса М3, например, vPortSVCHandler только один раз вызывается при старте планировщика, и более никогда. Они и сами, кстати, тоже самое что и я объясняют. Вообще supervisor call это именно для ММU, ну насколько я в этом для себя разобрался. То что вы смотрите на фриртос это хорошо, но для обобщения лучше иметь больше одного примера. Посмотрите еще на TNeo (это развитие энтузиастом проекта TnKernel).

Согласен, что можно сделать и через одно прерывание, об этом я тоже писал в статье, но смысла в этом особого нет, я думаю, что с двумя код выглядит гораздо лаконичнее (хотя это наверное на любителя).
Я очень хорошо знаком с TNKernel даже использовал ее в своих проектах когда-то, но насколько я знаю проект уже давно не поддерживается, про TNeo тоже слышал - это форк TNKernel в нем немного оптимизировали код и добавили поддержку pic32 (по моему), а в остальном тоже самое, что и TNKernel. Если рассматривать стороние RTOS, то мне больше нравится RT-Thread, в ней действительно очень много интересных вещей https://github.com/RT-Thread/rt-thread

В TNeo мне больше понравилось то, что автор покрыл его тестами и внедрил фраймворк Ceedling. Также у него есть несколько статей по проведенным оптимизациям. И отдельно есть статья наподобие вашей про организацию прерываний и стэка прерываний, с особенностями реализации на PIC где нет таких удобств как у Cortex-M3.

Это все очень хорошо, но на сколько я помню в стандартной TNKernel не была решена проблема инверсии приоритетов для мьютексов путем наследия приоритета. Вернее там было решение, но оно было одноранговое, то есть когда выстраивалась сложная цепочка из заблокированных мьютексов и задач, наследие приоритета уже не работало. Во FreeRTOS эта проблема тоже не решена. Вот кстати, что они пишут по этому поводу https://forums.freertos.org/t/priority-inheritance-proposal/11175, https://www.freertos.org/FreeRTOS_Support_Forum_Archive/June_2017/freertos_Nested_mutexes_and_priority_inheritance_again_30f4e9ebj.html.
TNeo я так глубоко не изучал, но интересно было бы посмотреть, как это реализовано у них.
Но в моей RTOS, если Вы уже посмотрели исходники, все сделано полностью для цепочек любой сложности ))

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

Мое личное наблюдение, что далеко не каждый разработчик знает чем отличаются мьютекс и семафор, и уж тем более сможет сходу объяснить зачем нужно наследование приоритетов на классическом примере из трех задач.

Ну а зачем тогда вообще использовать вытесняемую RTOS если не различать семафоры, мьютексы и не использовать наследие приоритетов? Это ведь основной плюс реалтайма. Тогда лучше использовать contiki https://github.com/contiki-ng/contiki-ng. Кстати тоже очень оригинальное решение, все задачи - это по сути машины состояний (автоматы), только в линейном виде протопотоки (вот тут про них хорошо написанно https://bsvi.ru/protopotoki-protothreads/), как плюс ООЧЕНЬ маленкий расход RAM, как минус нет вытеснения и нельзя организовать жесткий реал тайм.

Это просто наблюдение из жизни, у нас большая команда разработчиков, но большинство сходу не объяснят наследование приоритетов. Трудно сказать о чем это говорит, ну а RTOS используется потому что есть, "Я человек простой: вижу API - использую". Но про наследование приритетов реально не все вникают, а встретить его на этапе проектирования - вообще редкость, максимум это когда уже решаешь какую-то проблему с отзывчивостью.

За ссылки, спасибо, почитаю!

Придется другие комменты заплюсовать - замолить грехи)))

Мне из последнего про операционки, архитектуру и обработку событий понравилась книжка Miro Samek - PSiCC2. Сначала я нашел его сайт (state-machine.com), потом статьи, потом книжку. Сам его фрэймворк мне не оч зашел по различным причинам, но идеи я использую. Его подход к использованию такого инструмента как RTOS мне ближе всего оказался.

Sign up to leave a comment.

Articles