Русский
Русский
English
Статистика
Реклама

Системное программирование

Ускоряем нейросеть на уровне железа интервью с разработчиком компиляторов

25.03.2021 18:04:40 | Автор: admin

Обыденное представление о Deep Learning состоит в том, что для достижения успеха нужно хорошо знать математику и уметь программировать на Python. Но все становится немного сложнее, как только мы начинаем говорить о реализации нейросетевых решений в железе, где критична производительность. Мы пообщались с руководителем направления российского Исследовательского центра Samsung Вячеславом Гарбузовым, чтобы понять, как ускоряют работу нейросетей на аппаратном уровне, при чем тут компиляторы и какие знания требуются в этой редкой профессии. И самое интересное - какие вакансии в его подразделении открыты в настоящий момент.

Примеры работы нейросети на смартфоне: оптимизация сцены,классификация изображений, подсказка лучшей композиции кадраПримеры работы нейросети на смартфоне: оптимизация сцены,классификация изображений, подсказка лучшей композиции кадра

Слава, привет! Расскажи о себе, чем занимается твоя команда сейчас.

Привет! Я руковожу управлением разработки ПО для систем на кристалле Исследовательского центра Samsung. Мы занимаемся разработкой SDK для ускорения исполнения моделей глубинного обучения (Deep Learning)на процессорах Exynos.

Кто твои непосредственные заказчики?

Наша работа связана с компонентным бизнесом и нашим заказчиком является Samsung Semiconductor. Мы ближе к земле.

Правильно ли я понимаю, чтомобильныйпроцессор Exynosв основном используется в телефонах Samsung и больше нигде?

Вовсе нет. Exynos используется в смартфонах других производителей. Кроме того, Exynos - это не только мобильные системы на кристалле (SoC). Есть микроконтроллеры, компоненты Интернета вещей. Крупные игроки на автомобильном рынке тоже заинтересованы в наших продуктах.

Расскажи про новый Exynos и AI-ускоритель в нем

Разработкой Exynos SoCи SDK к нему занимается подразделение Samsung System LSI (large-scale integration - высокоинтегрированные чипы). Узнать подробнее про новый Exynos 2100 можно извидеопрезентации. В разделе AI and Camera кратко рассказывается, что такое AI-ускоритель. Это железо для ускорения работы нейросетей. Обучение сети производится заранее, а исполнением (inference) как раз занимается это железо.

Что такое inference, что значит выполнить сеть на устройстве? Есть нейросеть, она уже натренирована. На вход готовой нейросети мы подаем картинку с собачкой или кошечкой, а на выходе это устройство дает 0 или 1. То есть наш сопроцессор не занимается обучением нейросети самостоятельно, его задача просто отработать готовую сеть.

Для работы нейросетевого сопроцессора нужен программный инструментарий. Такой инструмент есть, он называется Samsung Neural SDK.

Для каких задач это всё используется?

Применения в телефоне в основном связаны с камерой: живой фокус, ночная съемка, Bixby Vision, обнаружение лиц, улучшающее картинку.

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

Сегментация людей и животных на фотоСегментация людей и животных на фото

Расскажи, как устроен этот AI-ускоритель.

Он состоит из двух частей:

  1. NPU (Neural Processing Unit - обработчик нейросетей). Фактически это ускоритель операций с тензорами. Он умеет быстро делать свертки (convolution), пулинги (pooling) - набор операций, популярных в глубинном обучении.

  2. DSP (digital signal processor - цифровой обработчик сигналов).Это процессор, специализированный под выполнение определенных задач. Его разрабатывают изначально под конкретные алгоритмы. Ребята проектируют этот DSP под распознавание лиц или под более широкий круг задач.

Это единый кластер в составе одной системы на кристалле. Для него мы и разрабатываемSDK. У нас две команды, одна работает над NPU, другая, соответственно, над DSP.

Какие компиляторные задачи у вас с NPU?

Компилятор для NPU - это та штука, которая превращает граф на выходе Deep Learning-фреймворка в последовательность процессорных команд. Отличие от обычного компилятора в том, что мы генерируем код не для CPU, а для нейросетевого ускорителя. Это другой процессор со своим языком. И чтобы вся система работала быстрее, мы оптимизируем ее на уровне компилятора.

В чем суть оптимизации? По большей части это memory allocation (оптимизация работы с памятью) и instruction scheduling (параллелизм на уровне инструкций). Наш процессор может несколько инструкций выполнять одновременно, например, считать ту же самую свертку и загружать данные для свертки. Мы должны сгенерировать код для этого процессора так, чтобы оптимизировать работу с памятью и максимизировать параллелизм.

А что с DSP? Какие задачи там?

Это уже более-менее похоже на традиционный процессор. Если свертку наш NPU умеет делать на уровне железа, то здесь мы должны эту свертку описать на языке C++ и исполнить на DSP. Зачем нужен отдельный сопроцессор, чтобы выполнять ту же самую свертку? Например, NPU занят в какой-то момент, и мы хотим параллельно решать другую задачу. Некоторые операции мы в принципе на NPU выполнить не можем.

У нас достаточно простой DSP, основанный на VLIW-архитектуре (very long instruction word очень длинная машинная команда). Особенность нашего DSP в том, что он аппаратно достаточно простой, и от компилятора требуется серьезная оптимизация.Мы делаем на базе LLVM компилятор для этого DSP.

Поговорим о других вещах. Где ты работал до Samsung?

Непосредственно до Samsung я работал в Topcon Positioning Systems и в Lynx Software Technologies. Занимался разработкой RTOS и инструментов.

Где и на кого ты учился?

Учился в МГУ на физика. Занимался ускорителями элементарных частиц, электронов в частности. Занимался автоматизацией физического эксперимента, системой управления для промышленного ускорителя.

Как помогает образование физика в твоей профессии?

В профессии руководителя это очень сильно помогает, позволяет смотреть на вещи широко.

Работая в твоем отделе, насколько важно хорошо разбираться в железе?

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

А в глубинном обучении?

Базовое представление надо иметь. Я полагаю, что современные выпускники вузов это всё уже знают на определенном уровне. Это всегда хорошо иметь в бэкграунде. Например, курс Нейронные сети и компьютерное зрение Samsung Research Russia на Stepik я добавил в закладки, но пока не прошел. И кстати, вчера в рамках этого курса былалекцияна YouTube про Embedded Inference как раз на эту тему - "Мобильные архитектуры нейросетей и фреймворки для их запуска".

Когда мы начинали этот проект в 2018 году, мне сказали: нужен компилятор для Deep Learning. Нам потребовалось найти людей, которые одновременно умеют и в Deep Learning, и в железо, и в компиляторы. И это сложно, потому что таких людей очень мало. Потом мы поняли, что требование знания Deep Learning не столь критично, всё-таки заказчики от нас просили только компилятор.

С выпускниками каких вузов тебе интересно работать?

Мне приятно работать с выпускниками МФТИ, особенно с теми, которые прошли через базовые кафедры ИСП РАН или Intel. У нас в отделе достаточно много ребят из Intel. По факультетам - ФУПМ, ФРКТ. Если говорить о других вузах, то это и МГУ - забавно, что много моих знакомых компиляторщиков заканчивали физфак. Также это ВШЭ, где есть МИЭМ, там учат проектировать железо, FPGA. А компиляторы можно условно рассматривать как часть железа в принципе.

В нашем Исследовательском центре мы проводили вечернюю школуSamsung Compiler Bootcamp, и , в основном, в ней учились студенты из Бауманки, МГУ и Вышки.

На тему FPGA - полезно ли это изучать?

Как бэкграунд - да, это правильно.

А вообще, много ли таких центров в Москве, где занимаются компиляторами?

Intel, JetBrains, Positive Technologies, Huawei. Из российских - МЦСТ, которые Эльбрус, они тоже компиляторы делают. Например, Роман Русяев, наш коллега из Исследовательского центра Samsung и разработчик компиляторов, как раз оттуда пришел (см. егостатьюна Хабре о Concept-Based Polymorphism), он часто выступает на конференциях и пишет статьи.Он активный участник C++ Community. Например, вот пара его выступлений где затрагивается тема оптимизации при помощи компилятора :"Исключения C++ через призму компиляторных оптимизаций","Настоящее и будущее copy elision".

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

О каких мировых трендах в компиляторов можно сейчас говорить?

Можно выделить такие тренды:

  1. Доминирование проекта LLVM

  2. Обобщение компилятора для различных предметных областей посредством универсального промежуточного представления (MLIR)

  3. Объединение различных инструментов для анализа и преобразования кода (компиляторов, анализаторов, performance estimators, линтеров и пр.) в рамках одного проекта

  4. Активные попытки использования высокой науки в промышленных компиляторах (formal verification, polyhedral optimizations, более подробно встатье)

Какие требования к соискателям, будущим разработчикам компиляторов, ты бы озвучил?

Обязательные требования: знание С/С++ на хорошем уровне. Понимание того, как устроены компиляторы, опыт их разработки. Понимание устройства операционной системы. Умение разбираться в больших объемах чужого кода. Навыки отладки embedded-устройств. Знание практик программной инженерии - непрерывная интеграция, ревизия кода, отслеживание задач. Владение скриптовыми языками - Bash или Python.

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

Работая в международной компании, как складывается коммуникация с иностранными коллегами? Как вы решаете вопросы взаимодействия с коллегами в пандемию?

Мы активно взаимодействуем с командами из других стран Корея, Китай, Индия, Израиль, США. До карантина они частенько приезжали к нам в гости, а мы к ним.

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

Какие книжки о компиляторах ты бы посоветовал?

Коллеги рекомендуют начинать с "Modern Compiler Implementation in ML", автор Andrew W. Appel.

Какие твои любимые книги о программировании вообще?

Керниган и Ричи Язык программирования С. Они классные. Еще Керниган и Пайк, Практика программирования. Там настолько все четко сделано.

Что скажешь об онлайн-курсах?

Если говорить о курсах по смежным темам, то по глубинному обучению это курс Samsung о нейронных сетях в компьютерном зрении, и известный курс Эндрю на (Andrew Ng). Полезенкурс по С++от Яндекса.

LLVM или GCC - что полезнее изучать?

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

Какие инструменты командной работы используете?

Используемgit, точнее корпоративный github. Важно сказать, что мы делаем Code Review, и это неотъемлемая часть работы наших инженеров. Здорово, что все друг другу помогают и делятся знаниями. Также мы делимся знаниями с помощью Confluence, у нас есть вики-портал с внутренней документацией по нашим разработкам. Есть Jira для отслеживания задач. И есть свой чат на основе Mattermost, то есть практически Slack - без него на удаленке мы бы вообще не выжили. Исповедуем ContinuousIntegration, а также автоматизируем все, что можно автоматизировать.

А что насчет методов Agile?

Мы не привязаны к какой-то конкретной методологии. Берем полезные практики, которые подходят нашему проекту, из разных методологий. Например, из скрама мы берем Daily Scrum - ежедневные собрания. У нас есть итеративное планирование. И так далее.

Не могу не спросить. А вот во время пандемии, когда все по видео общались, вы все равно Daily Scrum стоя проводили?

Ну нет, всё-таки все сидели.

Сколько у вас длится Daily Scrum?

От 15 минут до часа, потому что иногда он перетекает в технические дискуссии.

Что еще интересного бывает?

Регулярно проходят онлайн-семинары. Коллеги из разных центров компании рассказывают о своих задачах, обсуждаются технические решения. Мы тоже участвуем, конечно. Также проводим внутренние семинары для сотрудников московской команды. Обмениваемся знаниями, изучаем опыт других. Например, в настоящий момент у нас в Исследовательском центре проходит серия семинаров про алгоритмы аллокации памяти.

----

А сейчас самое интересное: ВАКАНСИИ!

У нас открыты две вакансии, соответственно поNPUи поDSP. Если вас заинтересовало, откликайтесь на вакансию прямо на HeadHunter, и возможно, мы с вами встретимся на собеседовании.

Вопросы задавала: Татьяна Волкова, куратор трека по Интернету вещей социально-образовательной программы для вузов IT Академия Samsung

Отвечал: Вячеслав Гарбузов, руководитель направления, российский Исследовательский центр Samsung

Подробнее..

Fiddler удобный сниффер прокси сервер

03.05.2021 14:08:04 | Автор: admin

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

Зачем это делать ?

Пример 1. Анализ трафика.
Пользователи вашей сети пользуются вашим прокси-сервером. Вы можете увидеть на какие сайты заходят пользователи, запрещать дальнейшие переходы на эти сайты.

Пример 2. Сбор данных.
Ваши пользователи пользуются через вас некоторыми веб-ресурсами. Например, они вводят vin-номер своего автомобиля на сайте дилера авто и получают в ответ данные этого автомобиля. Вы можете сохранять эти данные в свою базу данных.

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

Пример 4. Подмена POST-данных.
Вам нужно подправить данные передаваемые на веб-сервер через POST-запрос. Существует множество информации передаваемой в POST-запросах. Пример: отправка логина/пароля на сервер в процессе авторизации. Или онлайн тест отправляет на сервер результаты вашего теста.

Установка Fiddler

  1. Переходим на https://www.telerik.com/download/fiddler, скачиваем Fiddler Classic.

  2. Установка простая и быстрая.

  3. Запускаем программу.

Настройка Fiddler

В меню File есть опция "Capture Traffic". По умолчанию опция включена. Это означает что Fiddler прописывает в реестре Windows себя в качестве прокси-сервера. Браузеры Internet Explorer, Edge, Chrome используют данную настройку, а значит HTTP-пакеты от этих браузеров пойдут через Fiddler.

Если опция "File -> Capture Traffic" выключена, то Fiddler перестает работать как системный прокси-сервер и перехватывает только те пакеты, которые идут непосредственно на адрес Fiddler. Это может быть когда вы настроили ваше приложение или браузер сами для работы через ip/port Fiddler. По умолчанию Fiddler слушает на порту 127.0.0.1:8888

Опция "Keep: All sessions".
В данном режиме Fiddler не очищает журнал собранных HTTP-пакетов. Если требуется продолжительная работа Fiddler, то при большой нагрузке этих пакетов будет очень много и Fiddler скушает всю доступную оперативную память компьютера. Чтобы этого не случилось переключите в режим "Keep: 100 sessions".

Опция "Decode".
По умолчанию выключена. В процессе анализа собранных пакетов рекомендуется включить чтобы пакеты автоматически декодировались. Либо можно выделить собранные пакеты через Ctrl+A, вызвать меню нажатием правой кнопки мыши по выделенным пакетам и нажать "Decode Selected Sessions".

Основные настройки

Переходим в "Tools -> Options...".

Вкладка "HTTPS".
После установки Fiddler не собирает HTTPS-трафик, это необходимо включить. Ставим галочку в опции "Decrypt HTTPS traffic". После этого Fiddler сгенерирует самоподписанный сертификат и спросит хотите ли установить данный сертификат. Отвечаем да.

Опция "Ignore server certificate errors (unsafe)" - сразу можно не включать. На некоторых порталах бывают ошибки сертификатов, но это редко. Как увидите так включите )
Настройка протоколов. По умолчанию стоит значение "<client>;ssl3;tls1.0". Советую сразу установить значение на "<client>;ssl3;tls1.0;tls1.1;tls1.2". После изменения настроек необходимо перезапустить программу чтобы настройки вступили в силу.

Кнопка "Actions":

"Trust Root Certificate" - если сгенерированный Fiddler сертификат вы не установили после включения опции "Decrypt HTTPS traffic", то можно это сделать здесь.

"Export Root Certificate to Desktop" - если вы планируете использовать Fiddler как прокси-сервер локальной сети, то на каждом устройстве пользователя необходимо установить сгенерированный выше сертификат. С помощью этой опции сохраняете сертификат на ваш рабочий стол.

"Reset All Certificates" - в некоторых случаях необходимо сгенерировать новый сертификат взамен старого. В этом случае сбрасываем все Fiddler-сертификаты и генерируем новый сертификат.

Вкладка "Connections".
Здесь устанавливаем на каком порту Fiddler работает как прокси-сервер. Порт по умолчанию "8888".

"Allow remote computers to connect" - включаем опцию чтобы Fiddler начал принимать подключения от других компьютеров.

"Act as system proxy on startup" - по умолчанию опция включена. Если включена, то при запуске опция "File -> Capture Traffic" включена.

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

Вкладка "Gateway".
Здесь устанавливаем куда Fiddler отправляет входящие пакеты, какой прокси использует.

"Use System Proxy (recommended)" - использование системного прокси из реестра текущего пользователя.

"Manual Proxy Configuration" - возможность задать вручную прокси-сервер.

"No proxy" - задаем что выход в Интернет напрямую, без использования прокси.

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

Установка сертификатов на Windows устройствах

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

Для установки сертификата используем консоль управления MMC: в коммандной строке вводим команду "mmc".

В меню файл выбираем "Добавить или удалить оснастку". Из доступных оснасток выбираем "Сертификаты" и с помощью кнопки "Добавить" выбираем данную оснастку. Нажимаем "Ок" и выбираем "учетной записи компьютера". Это нужно чтобы открыть сертификаты которые установлены для всего компьютера, а затем установить сертификат Fiddler именно в это хранилище. Если открыть сертификаты "моей учетной записи пользователя", то после установки сертификата Fiddler в это хранилище другие пользователи данного компьютера не смогут подключиться к Fiddler.

Установку сертификата производим в "Доверенные корневые центры сертификации".

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

Анализ трафика

В процессе работы Fiddler сниффит все HTTP-запросы и их обычно много. Для поиска необходимых запросов можно использовать фильтры. Правой кнопкой мыши выбираем лишний запрос, выбираем "Filter Now" и "Hide '...'" чтобы скрыть запросы к данному домену. Можно удалять вручную выделенные запросы используя кнопку "Delete".

Кроме использования фильтров можно искать отдельный текст в теле запросов/ответов: "Ctrl+F" для открытия меню поиска. Найденные запросы подсвечиваются по умолчанию желтым цветом.

Изменение данных запросов

В Fiddler существует инструмент "Fiddler ScriptEditor" (Редактор скриптов) для создания правил модификации трафика. Запуск редактора скриптов через "Ctrl+R" или выбора пункта меню "Rules -> Customize Rules...".

В редакторе скриптов есть два основных метода: "OnBeforeRequest" и "OnBeforeResponse":

"OnBeforeRequest" - выполнение скриптов в этом методе происходит перед отправкой пакетов на веб-сервер.

"OnBeforeResponse" - выполнение скриптов в этом методе происходит после получения ответа от веб-сервера.

Ниже приведены примеры скриптов с указанием в каком методе их расположить.

Задача 1: Запрет сайта

Запрещаем переход на адрес сайта содержащий строку.

// OnBeforeRequestif (oSession.uriContains("//ya.ru/")) oSession.host = 'access.denied';

Задача 2: Запрет загрузки ресурса

Запрещаем загрузку ".svg" файлов для заданного адреса сайта.

// OnBeforeRequestif (oSession.uriContains("yastatic.net") && oSession.url.EndsWith(".svg")){oSession.host = 'na.derevnu.dedushke';}// или// OnBeforeRequestif (oSession.uriContains("yastatic.net") && oSession.url.EndsWith(".svg")){oSession.responseBodyBytes = new byte[0];}// OnBeforeResponseif (oSession.uriContains("yastatic.net") && oSession.url.EndsWith(".svg")){oSession.ResponseBody = null;}

Задача 3: Переадресация запроса

Переадресация запроса на адрес сайта содержащий строку.

// OnBeforeRequestif (oSession.uriContains("//ya.ru/")) oSession.url = "yandex.ru"

Задача 4: Сбор данных

Пользователи подключаются через данный прокси-сервер и делают в браузерах некоторые запросы вида "https://myhost.ru?key=abcd&vin=VF38BLFXE81078232&lang=ru". Задача записать в базу данных событие поиска и передать значение vin-номера. Данный скрипт создает файлы с названием включающем vin-номер. Кроме скрипта необходимо создать утилиту/службу, которая раз в заданный интервал читает каталог "C:\vinsearch\" и записывает данные в базу данных.

// OnBeforeResponseif(oSession.uriContains("https://myhost.ru?key=") && oSession.uriContains("&vin=")){oSession.utilDecodeResponse();// поиск позиции индекса начала vin-номераvar startVin = oSession.url.IndexOf("vin=") + 4;// поиск позиции индекса конца vin-номераvar endVin = oSession.url.IndexOf("&", startVin);  // поиск подстроки зная индекс начала и индекс конца подстрокиvar vin = oSession.url.Substring(startVin, endVin - startVin);// создание файла с именем типа vin_текущееЗначениеМиллисекунд.txtoSession.SaveResponseBody("C:\\vinsearch\\" + vin + "_" + DateTime.Now.Millisecond + ".txt");}

Задача 5: Изменить текст в ответе

В данном примере меняем текст "Иванов" на "Петров".

// OnBeforeResponseif (oSession.uriContains("https://myhost.ru")){oSession.utilDecodeResponse();  oSession.utilReplaceInResponse('Иванов','Петров');}

Задача 6: Заменить ресурс веб-портала на локальный ресурс

Заменим картинку веб-портала на картинку расположенною на локальном диске.

// OnBeforeResponseif (oSession.uriContains("/Static/app/img/world.svg")){oSession.LoadResponseFromFile("c:/scripts/lang.png");}

Задача 7: Изменение свойств HTML-объектов

Например, есть картинка с заданными размерами в HTML и нужно эти размеры изменить.

// OnBeforeResponseoSession.utilReplaceInResponse('/Static/app/img/world.svg" height="15" width="15" style="height: 15px','/Static/app/img/world.svg" height="1" width="1" style="height: 1px');

Задача 8: Скрыть элементы по className меняя css-файлы

В данном примере скрываем элементы зная их className в css-файле добавляя свойство "visibility: hidden;"

// OnBeforeResponseoSession.utilDecodeResponse();oSession.utilReplaceInResponse("#header_Area_Right {", "#header_Area_Right { visibility: hidden; ");

Задача 9: Заставить страницу открываться в текущем окне

Пример: существует JavaScript, который открывает ссылку в новом окне. Нужно сделать чтобы ссылка открывалась в текущем окне.

// OnBeforeResponseif (oSession.uriContains("myhost.ru") && oSession.uriContains(".js")){oSession.utilDecodeResponse();oSession.utilReplaceInResponse("window.open(url, '_blank', option);", "window.open(url);");}

Задача 10: Выполнение скриптов для определенных IP

В данном примере меняем текст "Иванов" на "Петров" только для IP = "192.168.0.100"

// OnBeforeResponseif (oSession.clientIP == '192.168.0.100'){oSession.utilDecodeResponse();oSession.utilReplaceInResponse('Иванов','Петров');}

Задача 11: Меняем css-стили портала

Css-файлы веб-портала можно сохранить на локальном диске, отредактировать и настроить скрипт отдавать стили с локального диска, а не с портала.

// OnBeforeResponseif (oSession.uriContains("/banner.css")){oSession.LoadResponseFromFile("c:/scripts/banner.css");}

Задача 12: Запрет PUT-команды и аналогичных

Запрет команды по ее типу: "PUT", "DELETE", etc.

// OnBeforeRequestif (oSession.HTTPMethodIs("PUT") && oSession.uriContains("https://myhost.ru/")){oSession.host = 'access.denied';}

Задача 13: Изменение тела POST-запроса

Изменить тело POST-запроса для заданного портала. При авторизации на данном портале вне зависимости от введенных пользователем данных на веб-портал отправятся данные из скрипта.

// OnBeforeRequestif (oSession.uriContains("https://myhost.ru/") && oSession.RequestMethod == "POST"){oSession.utilSetRequestBody("username=xxx&password=yyy");}

Задача 14: Меняем заголовки HTTP-пакета

Заголовки пакетов можно легко редактировать: удалять, добавлять, изменять.

// OnBeforeRequest// Удалить заголовок с именем 'User-Agent'oSession.oRequest.headers.Remove("User-Agent");// Добавить заголовок 'xxx' со значением 'yyy'oSession.oRequest.headers.Add("xxx", "yyy");// Изменить значение заголовка с именем 'User-Agent' на значение 'xxx' oSession.oRequest.headers["User-Agent"] = "xxx";

Задача 15: Меняем Cookie

Работа с Cookie: добавление, удаление, редактирование

// OnBeforeRequest - добавить в запрос CookieoSession.oRequest["Cookie"] = (oSession.oRequest["Cookie"] + ";mycookie=xxx");// OnBeforeRequest - изменить значение Cookie 'JSESSIONID' на 'xxx'oSession.oRequest['Cookie'] = oSession.oRequest['Cookie'].Replace("JSESSIONID=","ignoredCookie=") + ";JSESSIONID=xxx";// OnBeforeRequest - удалить Cookie 'JSESSIONID'oSession.oRequest['Cookie'] = oSession.oRequest['Cookie'].Replace("JSESSIONID=","ignoredCookie=");
// Всем удачи на полях сниффинга данных )
Подробнее..

Как мы верифицированный полетный контроллер для квадрокоптера написали. На Ada

30.03.2021 12:10:29 | Автор: admin

Однажды на новогодних каникулах, лениво листая интернет, бракоделы в нашем* R&D офисе заметили видео с испытаний прототипа роботакси. Комментатор отзывался восторженным тоном революция, как-никак. Здорово, да, но время сейчас такое кругом революции, и ИТ их возглавляет.

Но тренированное ухо расслышало в шуме с испытательной площадки еще кое-что. Контроллер скорости (штука для управления тягой винтов) сыграл мелодию при старте, как это любят делать пилоты дронов, которые часто используют полётный контроллер Betaflight. Неужели там бета-флайт? Похоже, что да. Ну, или какая-то из ее разновидностей - открытых полетных контроллеров всего около полдюжины.

Перед глазами побежали флешбеки, где-то из глубин подсознания всплыла забытая уже информация о прошивках для Тойоты на миллионы тысяч строк Си и 2 тысячи глобальных переменных (Toyota: 81564 ошибки в коде).

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

Но ведь можно иначе? Можно, решили мы!

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

Так как же быть иначе?

После быстрого совещания возникли вот такие мысли:

  • нам нужен другой подход

  • язык и подход должны друг друга дополнять

  • академический подход не подойдет, нужны практические применения.

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

При выборе языка мы поставили себе вот какими требования:

  • это должно быть что-то близкое к embedded

  • Нам нужен хороший богатый runtime с возможностями RTOS, но при этом брать и интегрировать RTOS не хочется

  • Он не должен заметно уступать в производительности тому, что используется сейчас.

Оказалось, что из практических инструментов в эти требования хорошо подходит один очень старый, незаслуженно забытый инструмент. Да, это Ada. А точнее, его модерновое, регулярно обновляемое ядро SPARK. В [SRM] описаны основные отличия SPARK от Ada, их не так много.

Что такое SPARK, будет ясно дальше, мы покажем, как именно оно было применено, почему Ада понравилась больше, чем С, как работает прувер, и почему мы при этом ничего не потеряли, а только приобрели. И почему мы не взяли Rust :)

Иной подход

Иной подход это верификация ПО. Обычно при этих словах люди начинают думать об абстрактных монадах, академический манускриптах, докладах на конференциях и пыльных трудах института системного программирования РАН, которые как будто бы от жизни отстают лет на 30, а то и 50. Но оказалось, что не все так плохо.

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

В случае с SPARK, верификация базово предоставляет нам гарантии:

  • отсутствия переполнения массивов и переменных

  • отсутствия выхода за границы в типах и диапазонах

  • отсутствия разыменования null-указателей

  • отсутствие выброса исключений.

  • гарантию неприменения инструментов, проверку которых выполнить нельзя.

  • гарантию выполнения всех инвариантов, которые мы опишем. А опишем мы много!

    Круто, да?

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

SPARK также учитывает ограничения на типы, которые описаны в Ada. В случае обычного исполнения ошибка несоответствия типов упадет в Runtime; SPARK же позволяет статически доказать, что ограничения на типы не могут быть нарушены никаким потоком исполнения.

Например:

Или другой пример:

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

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

Сам SPARK делит верификацию на уровни: от "каменного" (Stone level) через "Бронзовый" и "Серебряный" уровни до "Золотого" (Gold) и "Платинового". Каждый из уровней усиливает гарантии:

Stone

Мы в принципе знаем, что есть SPARK

Bronze

Stone + верификация потоков исполнения и детерминизм/отсутствие неинициализированных переменных

Silver

Bronze + доказательное отсутствие runtime-ошибок

Gold

Silver + гарантии целостности - не-нарушения инвариантов локальных и глобальных состояний

Platinum

Gold + гарантия функциональной целостности

Мы остановились на уровне Gold, потому что наш квадрокоптер все-таки не Boing 777 MAX.

Как работает верификация в SPARK: прувер собирает описание контрактов и типов, на их основе генерирует правила и ограничения, и далее передает их в солвер (SMT - Z3), который проверяет выполнимость ограничений. Результат решения прувер привязывает к конкретным строкам, в которых возникает невыполнимость.

Более подробно можно почитать в [SUG]

Иной язык

Несмотря на то, что сейчас "рулят" си-подобные ECMA-языки, мы нормально отнеслись к тому, что от этого придется отказаться. Более того, кажется, что чем больше программа, тем больше вредит укорочение ключевых слов и конструкций. Что касается Rust, то он - субъективно - в отношении минимализма издалека сильно напоминает Perl, к сожалению.

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

Профили

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

Мы используем профиль Ravenscar, специально для embedded-разработки. Он включает пару дюжин ограничений, которые делают вашу разработку для микроконтроллеров более удобной и верифицируемой: нельзя на ходу переназначать приоритеты задач-потоков, переключать обработчики прерываний, сложные объекты из stdlib-ы и такое.

Вот ограничения профиля Ravescar, для примера

Runtime

Когда в embedded необходимо создать более-менее сложное приложение, там всегда заводится RTOS, и ее выбор и интеграция это отдельная песня. В случае с Ada с этим больше нет сложностей - сама Ada включает в себя минимальную исполняемую среду с вытесняющим планировщиком потоков (в Ada это tasks, задачи), с интегрированными в сам язык средствами синхронизации (семафоры, рандеву, называются entries) и даже средствами избегания дедлоков и инверсии приоритетов. Это оказалось очень удобно для квадрокоптера, как станет понятно ниже.

Для embedded-разработчика доступны на выбор также разные рантаймы:

  • zero-footprint - с минимальными ограничениями и даже без многопоточности; зато минимальная программа не превышает пары килобайт, влезает даже в TO MSP430

  • small footprint - доступна большая часть функций Ada, но и требования побольше, несколько десятков килобайт RAM

  • full ravenscar - доступны все функции в рамках профиля Ravenscar/Extended Ravenscar

Вот пример описания пустой задачи

Хочется обратить внимание, что эти задачи - это по сути легковесные green threads, так как под ними нет никаких настоящих потоков не существует. Поэтому мы не страдаем от отсутствия корутин, ведь задачи не тяжелее них, зато встроены в язык.

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

Почему не rustRustRUST!

Когда мы говорим, про гарантии в языках программирования, сразу вспоминается Rust и его гарантии относительно указателей. Почему тут не он? Нам кажется, что Spark мощнее.

Ada не очень любит указатели - там они называются access types, и в большинстве случаев там они не нужны, но если нужны, то - в Spark также есть проверки владения, как в Rust. Если вы аллоцировали объект по указателю, то простое копирование указателя означает передачу владения (которую проконтролирует компилятор/верификатор), а передачу во временное владение (или доступ на чтение) верификатор также понимает и контролирует.

В общем, концепция владения объектом по указателю, и уровень доступа через этот указатель - есть не только в Rust, и его преимуществами можно пользоваться и в других инструментах, в частности, в Ada/SPARK. Подробно можно почитать в [UPS]

Вот пример кода с владением

Почему мы пишем, что в Ada/SPARK не нужны указатели? В отличие от Си, где базовым инструментом является указатель (хочешь ссылку - вот указатель, хочешь адрес в памяти - вот указатель, хочешь массив - вот указатель - ну вы поняли?), в Ada для всего этого есть строгий тип. Если не хватает встроенных операций, их допустимо переопределять (например, реализовать инлайновый автоинкремент), аналогично можно создать placement constructor, используя т.н. limited-типы - типы, которые компилятор запрещает копировать.

Если уже и этого мало, есть интероп с СИ то есть код можно компилировать совместно, и слинковать на этапе сборки. Но в этом случае гарантии поведения модуля как черного ящика остаются на разработчике. Для интеропа используются атрибуты - вот так, например, мы оборачиваем функцию на Си в доступ для Ada.

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

IDE

Для работы доступна вполне приятная и удобная IDE, но всегда можно использовать и VSCode с плагинами, и другие текстовые редакторы.

О производительности и надежности

Вполне валидным аргументом может быть вопрос с эффективностью ПО. Что касается эффективности, то в интернете доступно свежее исследование [EFF], из которого хочется привести табличку, показывающую, что старичок Ada еще огого:

Если говорить о надежности, то SPARK/Ada известен как один из языков с наименьшим количеством ошибок. В планируемом на 21 запуске кубсатов [LIC] полетное ПО планируется реализовывать на Ada, предыдущий спутник BasiLEO тоже на Ada был единственным среди 12, кому удалось приступить к планируемой миссии.

А теперь - о самом полетном контроллере

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

Структурная схема управляющего ПО показана на рисунке

Как видно из рисунка, ПО состоит из двух частей:

  • Veriflight - собственно, верифицированный полетный контроллер с алгоритмами.

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

Так как тратить много времени не хотелось, то драйвер для USB в STM32 был взят прямо нативный и при помощи Interop был слинкован с оберткой на Ada.

Плата оказалась оснащена минимальным количеством периферийных устройств:

  • STM32F405 микроконтроллер на 168 МГц (192кб RAM, 1Mб flash)

  • трансивером S.BUS на USART1

  • 6-осевым гиро-акселерометром без магнитного компаса

  • токовыми усилителями PWM

  • USB-интерфейсом, PHY-часть которого реализована на самом микроконтроллере платы.

Полетный контроллер реализован по простой схеме и крутит 2 цикла:

  1. внешний

  2. внутренний

Внешний цикл это цикл опроса периферии (CMD task на рисунке) в ожидании команд с радиопередатчика. Если команды нет, он передает признаки сохраняем высоту, держим горизонт. Если команда с пульта есть, передаем ее - целевой угол наклона, целевую мощность на пропеллеры. Частота внешнего цикла 20 Гц.

Внутренний цикл - цикл опроса гиро-акселерометра и распределения мощности на двигатели. Цикл оборудован 3 PID-регуляторами, и математикой Махони для расчета текущего положения по сигналам с гироскопов. В расчетах внутри используем кватернионы, для генерации управляющего сигнала - углы Эйлера. Частота размыкания внутреннего цикла - 200 Гц. Да, Ада без проблем успевает диспетчеризировать с такой скоростью.

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

Внутренний цикл реализует опрос PID и стабилизацию аппарата:

  • Считали затребованные пилотом углы

  • Запросили у математики расчетные углы положения

  • Нашли расхождение между желаемыми и настоящими

  • Пересчитали текущее положение на основании сигналов с гиро-акселерометров

  • Зарядили PID-регуляторы на новую коррекцию, если пришли новые затребованные углы

  • Запросили у PID-пакетов текущие импульсы коррекции

  • На основании них, а также запрошенной пилотом мощности на двигатели, сформировали необходимое распределение скоростей вращения на двигателях

Забавно, что большинство опен-сорсных реализаций Махони (для Arduino и не только) - на Cи и Wiring оказались содержащими разнообразные баги. Это мешало системе заработать. После того, как было выпито пол-ящика лимонада и съедена корзина круассанов, алгоритм воссоздали с нуля по описанию из [MHN], и система тут же заработала.

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

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

Но для первого приближения вышло отлично, хотя и совсем не подходит для акробатического квадрокоптера.

Статья получилась неожиданно длинная, поэтому придется разбить ее на две части. Мы в первой части постарались познакомить вас с инструментами предков, которые на удивление неплохи: по крайней мере, мы сделали выводы, что дальнейшие проекты для эмбеддед хочется делать на Ada, а не на Си.

Итог на текущий момент

Квадрокоптер с прошивкой на Ada/SPARK прошел тесты на подпружиненном стенде и полетах в закрытом помещении, собираются логи, схема стабилизации работает в соответствии с ожиданиями, но в рамках ограничений на углы маневров, как описано выше.

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

И конечно, на время испытательных полетов все runtime-проверки все равно останутся включены, хотя конечный итоговый результат - проверки не нужны, так как заведомо известно, что они выполняются.

Для себя мы сделали вывод, что для embedded будем стараться писать только на Ada.

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

Литература для дальнейшего изучения

[SUG] SPARK user guide https://docs.adacore.com/spark2014-docs/html/ug/index.html

[SRM] SPARK reference manual (https://docs.adacore.com/live/wave/spark2014/html/spark2014_rm/index.html)

[FC] Frama-C - платформа для модульного анализа кода С https://frama-c.com/

[UPS] https://blog.adacore.com/using-pointers-in-spark

[MHN] https://nitinjsanket.github.io/tutorials/attitudeest/mahony

[EFF] https://greenlab.di.uminho.pt/wp-content/uploads/2017/10/sleFinal.pdf

[LIC] https://en.wikipedia.org/wiki/Lunar_IceCube

Подробнее..

Перевод Планирование редакции Rust 2021

05.03.2021 22:04:25 | Автор: admin

Рабочая группа Rust 2021 Edition рада сообщить, что следующая редакция Rust Rust 2021 запланирована на этот год. Пока что формально описывающий её RFC остаётся открытым, но мы ожидаем, что в скором времени он будет принят. Планирование и подготовка уже начались, и мы идём по графику!


Если вам интересно, какие новшества появятся в Rust 2021 или когда эта редакция выйдет в стабильной версии, читайте нашу статью!


Что входит в эту редакцию?


Конечный список нововведений, которые войдут в Rust 2021, ещё не определён до конца. В целом мы планируем, что выпуск Rust 2021 будет намного меньше, чем Rust 2018, по следующим причинам:


  • Ритм выпусков стал регулярным Это значит, что мы будем активно использовать плюсы "цепочечной" модели на уровне редакций Rust.
  • Редакция Rust 2018 выбилась из модели "минимального стресса" выпусков.
  • Сейчас просто нужно меньше фундаментальных изменений, чтобы язык продолжал развиваться.

Более подробно о развитии концепции редакций вы можете почитать в RFC.


Решение, войдёт ли та или иная функциональность в Rust 2021, является частью процесса RFC поэтому список ожидаемых функций может и будет меняться. Это будет происходить до самого момента выпуска, но тем не менее, уже сейчас мы можем рассмотреть список функций, которые, скорее всего, в неё войдут.


Изменения в прелюдии


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


Сейчас в редакцию Rust 2021 предложено включить следующие трейты:


  • TryFrom/TryInto
  • FromIterator

RFC с этими изменениями можно найти тут. Обратите внимание, что RFC ещё не принят состав новой прелюдии активно обсуждается.


Новые правила захвата


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


Новый распознаватель функциональности в Cargo по умолчанию


В Rust 1.51 будет стабилизирован новый распознаватель функциональности в Cargo, который разрешит зависимостям пакета использовать разную функциональность в разных контекстах. Например, пакет с #[no_std] сможет использовать одну и ту же зависимость и во время сборки (build-dependencies с включённым std), и как обычную зависимость (без std). Пока что это приводит к тому, что std будет включена в обоих случаях, так как функциональность находится в глобальном пространстве имён.


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


Прочие изменения


Другие предложенные изменения включают унификацию работы panic в std и core и обновление уровня некоторых проверок с предупреждений до ошибок.


Полный список обсуждаемых функций вы можете найти здесь.


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


Примерный график


Итак, когда же мы планируем выпустить новую редакцию? Вот график основных этапов, к которому мы стремимся:


  • 1 апреля все релевантные редакции RFC или приняты, или в хорошем состоянии (т. е. все основные вопросы решены, и принятие RFC произойдёт в ближайшие недели).
  • 1 мая все нововведения, включённые в Rust 2021, находятся в Nightly с соответствующими feature-флагами.
  • 1 июня все проверки добавлены в Nightly.
  • 1 сентября редакция стабилизирована в Nightly.
  • 21 октября редакция полностью стабилизирована.

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


Приглашаем к участию


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


  • rustfix миграции для всех соответствующих функций,
  • тестирование всех функций и путей их миграции,
  • сообщения в блогах и другие маркетинговые материалы.

От переводчиков


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


Данную статью совместными усилиями перевели blandger, TelegaOvoshey, funkill и andreevlex.

Подробнее..

Добавляем modbus в Embox RTOS и используем на STM32 и не только

17.03.2021 18:23:31 | Автор: admin
image
Нас часто спрашивают, чем Embox отличается от других ОС для микроконтроллеров, например, FreeRTOS? Сравнивать проекты между собой, конечно, правильно. Но параметры, по которым порой предлагают сравнение, лично меня повергают в легкое недоумение. Например, сколько нужно памяти для работы Embox? А какое время переключения между задачами? А в Embox поддерживается modbus? В данной статье на примере вопроса про modbus мы хотим показать, что отличием Embox является другой подход к процессу разработки.

Давайте разработаем устройство, в составе которого будет работать в том числе modbus server. Наше устройство будет простым. Ведь оно предназначено только для демонстрации modbus, Данное устройство будет позволять управлять светодиодами по протоколу Modbus. Для связи с устройством будем использовать ethernet соединение.

Modbus открытый коммуникационный протокол. Широко применяется в промышленности для организации связи между электронными устройствами. Может использоваться для передачи данных через последовательные линии связи RS-485, RS-422, RS-232 и сети TCP/IP (Modbus TCP).

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

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

Наш проект будем вести в отдельном репозитории. При желании все можно скачать и воспроизвести самостоятельно.

Разработка прототипа на Linux


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

libmodbus-$(LIBMODBUS_VER).tar.gz:    wget http://libmodbus.org/releases/libmodbus-$(LIBMODBUS_VER).tar.gz$(BUILD_BASE)/libmodbus/lib/pkgconfig/libmodbus.pc : libmodbus-$(LIBMODBUS_VER).tar.gz    tar -xf libmodbus-$(LIBMODBUS_VER).tar.gz    cd libmodbus-$(LIBMODBUS_VER); \    ./configure --prefix=$(BUILD_BASE)/libmodbus --enable-static --disable-shared; \    make install; cd ..;


Собственно из параметров конфигурации мы используем только prefix чтобы собрать библиотеку локально. А поскольку мы хотим использовать библиотеку не только на хосте, соберем ее статическую версию.

Теперь нам нужен modbus сервер. В проекте libmodbus есть примеры, давайте на основе какого-нибудь простого сервера сделаем свою реализацию.

    ctx = modbus_new_tcp(ip, port);    header_len = modbus_get_header_length(ctx);    query = malloc(MODBUS_TCP_MAX_ADU_LENGTH);    modbus_set_debug(ctx, TRUE);    mb_mapping = mb_mapping_wrapper_new();    if (mb_mapping == NULL) {        fprintf(stderr, "Failed to allocate the mapping: %s\n",                modbus_strerror(errno));        modbus_free(ctx);        return -1;    }    listen_socket = modbus_tcp_listen(ctx, 1);    for (;;) {        client_socket = modbus_tcp_accept(ctx, &listen_socket);        if (-1 == client_socket) {            break;        }        for (;;) {            int query_len;            query_len = modbus_receive(ctx, query);            if (-1 == query_len) {                /* Connection closed by the client or error */                break;            }            if (query[header_len - 1] != MODBUS_TCP_SLAVE) {                continue;            }            mb_mapping_getstates(mb_mapping);            if (-1 == modbus_reply(ctx, query, query_len, mb_mapping)) {                break;            }            leddrv_updatestates(mb_mapping->tab_bits);        }        close(client_socket);    }    printf("exiting: %s\n", modbus_strerror(errno));    close(listen_socket);    mb_mapping_wrapper_free(mb_mapping);    free(query);    modbus_free(ctx);


Здесь все стандартно. Пара мест, которые представляют интерес, это функции mb_mapping_getstates и leddrv_updatestates. Это как раз функционал, который и реализует наше устройство.

static modbus_mapping_t *mb_mapping_wrapper_new(void) {    modbus_mapping_t *mb_mapping;    mb_mapping = modbus_mapping_new(LEDDRV_LED_N, 0, 0, 0);    return mb_mapping;}static void mb_mapping_wrapper_free(modbus_mapping_t *mb_mapping) {    modbus_mapping_free(mb_mapping);}static void mb_mapping_getstates(modbus_mapping_t *mb_mapping) {    int i;    leddrv_getstates(mb_mapping->tab_bits);    for (i = 0; i < mb_mapping->nb_bits; i++) {        mb_mapping->tab_bits[i] = mb_mapping->tab_bits[i] ? ON : OFF;    }}


Таким образом, нам нужны leddrv_updatestates, которая задает состояние светодиодов, и leddrv_getstates, которая получает состояние светодиодов.
static unsigned char leddrv_leds_state[LEDDRV_LED_N];int leddrv_init(void) {    static int inited = 0;    if (inited) {        return 0;    }    inited = 1;    leddrv_ll_init();    leddrv_load_state(leddrv_leds_state);    leddrv_ll_update(leddrv_leds_state);    return 0;}...int leddrv_getstates(unsigned char leds_state[LEDDRV_LED_N]) {    memcpy(leds_state, leddrv_leds_state, sizeof(leddrv_leds_state));    return 0;}int leddrv_updatestates(unsigned char new_leds_state[LEDDRV_LED_N]) {    memcpy(leddrv_leds_state, new_leds_state, sizeof(leddrv_leds_state));    leddrv_ll_update(leddrv_leds_state);    return 0;}


Так как мы хотим чтобы наше ПО работало и на плате и на хосте, нам понадобятся разные реализации функций установки и получения состояния светодиодов. Давайте для хоста хранить состояние в обычном файле. Это позволит получать состояние светодиодов в других процессах.

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

void leddrv_ll_update(unsigned char leds_state[LEDDRV_LED_N]) {    int i;    int idx;    char buff[LEDDRV_LED_N * 2];        for (i = 0; i < LEDDRV_LED_N; i++) {        char state = !!leds_state[i];        fprintf(stderr, "led(%03d)=%d\n", i, state);        buff[i * 2] = state + '0';        buff[i * 2 + 1] = ',';    }    idx = open(LED_FILE_NAME, O_RDWR);    if (idx < 0) {        return;    }    write(idx, buff, (LEDDRV_LED_N * 2) - 1);    close(idx);}...void leddrv_load_state(unsigned char leds_state[LEDDRV_LED_N]) {    int i;    int idx;    char buff[LEDDRV_LED_N * 2];    idx = open(LED_FILE_NAME, O_RDWR);    if (idx < 0) {        return;    }    read(idx, buff, (LEDDRV_LED_N * 2));    close(idx);        for (i = 0; i < LEDDRV_LED_N; i++) {        leds_state[i] = buff[i * 2] - '0';    }}


Нам нужно указать файл где будет сохранено начальное состояние светодиодов. Формат файла простой. Через запятую перечисляются состояние светодиодов, 1 светодиод включен, а 0 -выключен. В нашем устройстве 80 светодиодов, точнее 40 пар светодиодов. Давайте предположим, что по умолчанию четные светодиоды будут выключены а нечетные включены. Содержимое файла

0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1


Запускаем сервер
./led-serverled(000)=0led(001)=1...led(078)=0led(079)=1


Теперь нам нужен клиент для управления нашим устройством. Его тоже очень просто разрабатываем на базе примера из libmodbus

ctx = modbus_new_tcp(ip, port);    if (ctx == NULL) {        fprintf(stderr, "Unable to allocate libmodbus context\n");        return -1;    }    modbus_set_debug(ctx, TRUE);    modbus_set_error_recovery(ctx,            MODBUS_ERROR_RECOVERY_LINK |            MODBUS_ERROR_RECOVERY_PROTOCOL);    if (modbus_connect(ctx) == -1) {        fprintf(stderr, "Connection failed: %s\n",                modbus_strerror(errno));        modbus_free(ctx);        return -1;    }    if (1 == modbus_write_bit(ctx, bit_n, bit_value)) {        printf("OK\n");    } else {        printf("FAILED\n");    }    /* Close the connection */    modbus_close(ctx);    modbus_free(ctx);


Запускаем клиент. Установим 78 светодиод, который по умолчанию выключен

./led-client set 78Connecting to 127.0.0.1:1502[00][01][00][00][00][06][FF][05][00][4E][FF][00]Waiting for a confirmation...<00><01><00><00><00><06><FF><05><00><4E><FF><00>OK


На сервере увидим
...led(076)=0led(077)=1led(078)=1led(079)=1Waiting for an indication...ERROR Connection reset by peer: read


То есть светодиод установлен. Давайте выключим его.
./led-client clr 78Connecting to 127.0.0.1:1502[00][01][00][00][00][06][FF][05][00][4E][00][00]Waiting for a confirmation...<00><01><00><00><00><06><FF><05><00><4E><00><00>OK


На сервере увидим сообщение об изменении
...led(076)=0led(077)=1led(078)=0led(079)=1Waiting for an indication...ERROR Connection reset by peer: read


Запустим http сервер. О разработке веб-сайтов мы рассказывали в статье. К тому же веб-сайт нам нужен только для более удобной демонстрации работы modbus. Поэтому не буду сильно вдаваться в подробности. Сразу приведу cgi скрипт

#!/bin/bashecho -ne "HTTP/1.1 200 OK\r\n"echo -ne "Content-Type: application/json\r\n"echo -ne "Connection: close\r\n"echo -ne "\r\n"if [ $REQUEST_METHOD = "GET" ]; then    echo "Query: $QUERY_STRING" >&2    case "$QUERY_STRING" in        "c=led_driver&a1=serialize_states")            echo [ $(cat ../emulate/conf/leds.txt) ]            ;;        "c=led_driver&a1=serialize_errors")            echo [ $(printf "0, %.0s" {1..79}) 1 ]            ;;        "c=led_names&a1=serialize")            echo '[ "one", "two", "WWWWWWWWWWWWWWWW", "W W W W W W W W " ]'            ;;    esacelif [ $REQUEST_METHOD = "POST" ]; then    read -n $CONTENT_LENGTH POST_DATA    echo "Posted: $POST_DATA" >&2fi


И напомню что запустить можно с помощью любого http сервера с поддержкой CGI. Мы используем встроенный в python сервер. Запускаем следующей командой
python3 -m http.server --cgi -d .


Откроем наш сайт в браузере


Установим 78 светодиод с помощью клиента
./led-client -a 127.0.0.1 set 78Connecting to 127.0.0.1:1502[00][01][00][00][00][06][FF][05][00][4E][FF][00]Waiting for a confirmation...<00><01><00><00><00><06><FF><05><00><4E><FF><00>OK


сбросим 79 светодиод
./led-client -a 127.0.0.1 clr 79Connecting to 127.0.0.1:1502[00][01][00][00][00][06][FF][05][00][4F][00][00]Waiting for a confirmation...<00><01><00><00><00><06><FF><05><00><4F><00><00>OK


На сайте увидим разницу


Собственно все, на Linux наша библиотека прекрасно работает.

Адаптация к Embox и запуск на эмуляторе


Библиотека libmodbus


Теперь нам нужно перенести код в Embox. начнем с самого проекта libmodbus.
Все просто. Нам нужно описание модуля (Mybuild)
package third_party.lib@Build(script="$(EXTERNAL_MAKE)")@BuildArtifactPath(cppflags="-I$(ROOT_DIR)/build/extbld/third_party/lib/libmodbus/install/include/modbus")module libmodbus {    @AddPrefix("^BUILD/extbld/^MOD_PATH/install/lib")    source "libmodbus.a"    @NoRuntime depends embox.compat.posix.util.nanosleep}


Мы с помощью аннотации Build(script="$(EXTERNAL_MAKE)") указываем что используем Makefile для работы с внешними проектами.

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

И говорим что нам нужна библиотека source libmodbus.a

PKG_NAME := libmodbusPKG_VER  := 3.1.6PKG_SOURCES := http://libmodbus.org/releases/$(PKG_NAME)-$(PKG_VER).tar.gzPKG_MD5     := 15c84c1f7fb49502b3efaaa668cfd25ePKG_PATCHES := accept4_disable.patchinclude $(EXTBLD_LIB)libmodbus_cflags = -UHAVE_ACCEPT4$(CONFIGURE) :    export EMBOX_GCC_LINK=full; \    cd $(PKG_SOURCE_DIR) && ( \        CC=$(EMBOX_GCC) ./configure --host=$(AUTOCONF_TARGET_TRIPLET) \        prefix=$(PKG_INSTALL_DIR) \        CFLAGS=$(libmodbus_cflags) \    )    touch $@$(BUILD) :    cd $(PKG_SOURCE_DIR) && ( \        $(MAKE) install MAKEFLAGS='$(EMBOX_IMPORTED_MAKEFLAGS)'; \    )    touch $@


Makefile для сборки тоже простой и очевидный. Единственное, отмечу что используем внутренний компилятор ($(EMBOX_GCC) ) Embox и в качестве платформы (--host) передаем ту, которая задана в Embox ($(AUTOCONF_TARGET_TRIPLET)).

Подключаем проект к Embox


Напомню, что для удобства разработки мы создали отдельный репозиторий. Для того чтобы подключить его к Embox достаточно указать Embox где лежит внешний проект.
Делается это с помощью команды
make ext_conf EXT_PROJECT_PATH=<path to project> 

в корне Embox. Например,
 make ext_conf EXT_PROJECT_PATH=~/git/embox_project_modbus_iocontrol


modbus-server


Исходный код modbus сервера не требует изменений. То есть мы используем тот же код, который разработали на хосте. Нам нужно добавить Mybuild
package iocontrol.modbus.cmd@AutoCmd@Build(script="true")@BuildDepends(third_party.lib.libmodbus)@Cmd(name="modbus_server")module modbus_server {    source "modbus_server.c"    @NoRuntime depends third_party.lib.libmodbus}


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

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

Нам также нужно собрать нашу систему вместе с modbus сервером
Добавляем наши модули в mods.conf
    include iocontrol.modbus.http_admin    include iocontrol.modbus.cmd.flash_settings    include iocontrol.modbus.cmd.led_names    include third_party.lib.libmodbus    include iocontrol.modbus.cmd.modbus_server    include iocontrol.modbus.cmd.led_driver    include embox.service.cgi_cmd_wrapper(cmds_check=true, allowed_cmds="led_driver led_names flash_settings")    include iocontrol.modbus.lib.libleddrv_ll_stub


А наш файл leds.txt со статусами светодиодов кладем в корневую файловую систему. Но так как нам нужен изменяемый файл, давайте добавим RAM disk и скопируем наш файл на этот диск. Содержимое system_start.inc
"export PWD=/","export HOME=/","netmanager","service telnetd","service httpd http_admin","ntpdate 0.europe.pool.ntp.org","mkdir -v /conf","mount -t ramfs /dev/static_ramdisk /conf","cp leds.txt /conf/leds.txt","led_driver init","service modbus_server","tish",


Этого достаточно запустим Embox на qemu
./scripts/qemu/auto_qemu

modbus и httpd сервера запускаются автоматически при старте. Установим такие же значения с помощью modbus клиента, только указав адрес нашего QEMU (10.0.2.16)
./led-client -a 10.0.2.16 set 78Connecting to 10.0.2.16:1502[00][01][00][00][00][06][FF][05][00][4E][FF][00]Waiting for a confirmation...<00><01><00><00><00><06><FF><05><00><4E><FF><00>OK


и соответственно
./led-client -a 10.0.2.16 clr 79Connecting to 10.0.2.16:1502[00][01][00][00][00][06][FF][05][00][4F][00][00]Waiting for a confirmation...<00><01><00><00><00><06><FF><05><00><4F><00><00>


Откроем браузер


Как и ожидалось все тоже самое. Мы можем управлять устройством через modbus протокол уже на Embox.

Запуск на микроконтроллере


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

Но на плате STM32F4-discovery всего 4 светодиода. Было бы удобно задавать количество светодиодов, чтобы не модифицировать исходный код В Embox есть механизм позволяющий параметризировать модули. Нужно в описании модуля (Mybuild) добавить опцию
package iocontrol.modbus.libstatic module libleddrv {    option number leds_quantity = 80...}


И можно будет использовать в коде
#ifdef __EMBOX__#include <framework/mod/options.h>#include <module/iocontrol/modbus/lib/libleddrv.h>#define LEDDRV_LED_N OPTION_MODULE_GET(iocontrol__modbus__lib__libleddrv,NUMBER,leds_quantity)#else#define LEDDRV_LED_N 80#endif


При этом менять этот параметр можно будет указав его в файле mods.conf
    include  iocontrol.modbus.lib.libleddrv(leds_quantity=4)


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

Нам нужно еще управлять реальными линиями вывода. Код следующий
struct leddrv_pin_desc {    int gpio; /**< port */    int pin; /**< pin mask */};static const struct leddrv_pin_desc leds[] = {    #include <leds_config.inc>};void leddrv_ll_init(void) {    int i;    for (i = 0; i < LEDDRV_LED_N; i++) {        gpio_setup_mode(leds[i].gpio, leds[i].pin, GPIO_MODE_OUTPUT);    }}void leddrv_ll_update(unsigned char leds_state[LEDDRV_LED_N]) {    int i;    for (i = 0; i < LEDDRV_LED_N; i++) {        gpio_set(leds[i].gpio, leds[i].pin,                leds_state[i] ? GPIO_PIN_HIGH : GPIO_PIN_LOW);    }}


В файле mods.conf нам нужна конфигурация для нашей платы. К ней добавляем наши модули
    include iocontrol.modbus.http_admin    include iocontrol.modbus.cmd.flash_settings    include iocontrol.modbus.cmd.led_names    include third_party.lib.libmodbus    include iocontrol.modbus.cmd.modbus_server    include iocontrol.modbus.cmd.led_driver    include embox.service.cgi_cmd_wrapper(cmds_check=true, allowed_cmds="led_driver led_names flash_settings")    include iocontrol.modbus.lib.libleddrv(leds_quantity=4)    include iocontrol.modbus.lib.libleddrv_ll_stm32_f4_demo


По сути дела, те же модули как и для ARM QEMU, за исключением конечно драйвера.

Собираем, прошиваем, запускаем. И с помощью того же modbus клиента управляем светодиодами. Нужно только поставить правильный адрес, и не забыть что у нас всего 4 светодиода на плате.

Работу на плате stm32f4-discovery можно увидеть на этом коротком видео


Выводы


На этом простом примере мы постарались показать, в чем же основное отличие Embox от других ОС для микроконтроллеров. В том числе которые имеют POSIX совместимость. Ведь мы по сути дела, взяли готовый модуль, разработали бизнес логику на Linux используя при этом несколько приложений. И запустили все это на нашей целевой платформе. Тем самым существенно упростив и ускорив саму разработку.

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

Перевод Rust 1.51.0 const generics MVP, новый распознаватель функциональности Cargo

26.03.2021 20:17:43 | Автор: admin

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


Если вы установили предыдущую версию Rust средствами rustup, то для обновления до версии 1.51.0 вам достаточно выполнить следующую команду:


rustup update stable

Если у вас ещё не установлен rustup, вы можете установить его с соответствующей страницы нашего веб-сайта, а также посмотреть подробные примечания к выпуску на GitHub.


Что было стабилизировано в 1.51.0


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


Константные обобщения (Const Generics MVP)


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


struct FixedArray<T> {              // ^^^ Определение обобщённого типа.    list: [T; 32]        // ^ Где мы использовали его.}

Если затем мы используем FixedArray<u8>, компилятор создаст мономорфизированную версию FixedArray, которая выглядит так:


struct FixedArray<u8> {    list: [u8; 32]}

Этот полезный функционал позволяет писать повторно используемый код без дополнительных затрат во время выполнения. Однако до этого выпуска у нас не было возможности легко объединять значения таких типов. Это наиболее заметно в массивах, где длина указывается в определении типа ([T; N]). Теперь в версии 1.51.0 вы можете писать код, который будет обобщённым для значений любого числа, типа bool или char! (Использование значений struct и enum по-прежнему не стабилизировано.)


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


struct Array<T, const LENGTH: usize> {    //          ^^^^^^^^^^^^^^^^^^^ Определение константного обобщения.    list: [T; LENGTH]    //        ^^^^^^ Мы использовали его здесь.}

Теперь если мы используем Array<u8, 32>, компилятор создаст мономорфизированную версию Array, которая выглядит так:


struct Array<u8, 32> {    list: [u8; 32]}

Константные обобщения добавляют важный новый инструмент для разработчиков библиотек, чтобы создавать новые, мощные и безопасных API во время компиляции. Если вы хотите узнать больше о константных обобщениях, можете почитать статью в блоге Const Generics MVP Hits Beta для получения дополнительной информации об этой функции и её текущих ограничениях. Нам не терпится увидеть, какие новые библиотеки и API вы создадите!


Стабилизация array::IntoIter


Как часть стабилизации константных обобщений, мы также стабилизировали использующее их новое API std::array::IntoIter. IntoIter позволяет вам создать поверх массива итератор по значению. Ранее не было удобного способа итерироваться по самим значениям, только по ссылкам.


fn main() {  let array = [1, 2, 3, 4, 5];  // Раньше  for item in array.iter().copied() {      println!("{}", item);  }  // Теперь  for item in std::array::IntoIter::new(array) {      println!("{}", item);  }}

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


Новый распознаватель функциональности Cargo


Управление зависимостями сложная задача, и одна из самых сложных её частей выбор версии зависимости, когда от неё зависят два разных пакета. Здесь учитывается не только номер версии, но и то, какая функциональность была включена или выключена для пакета. По умолчанию Cargo объединяет функциональные флаги (features) для одного пакета, если он встречается в графе зависимостей несколько раз.


Например, у вас есть зависимость foo с функциональными флагами A и B, которые используются пакетами bar and baz, но bar зависит от foo+A, а baz от foo+B. Cargo объединит оба флага и соберёт foo как foo+AB. Выгода здесь в том, что foo будет собран только один раз и далее будет использован и для bar, и для baz.


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


Общим примером этого из экосистемы может служить опциональная функциональность std во многих #![no_std] пакетах, которая позволяет этим пакетам предоставить дополнительную функциональность, если она включена. Теперь представим, что вы хотите использовать #![no_std] версию foo в вашей #![no_std] программе и использовать foo во время сборки в build.rs. Так как во время сборки вы зависите от foo+std, то и ваша программа тоже зависит от foo+std, а значит более не может быть скомпилирована, так как std не доступна для вашей целевой платформы.


Это была давняя проблема в Cargo, и с этим выпуском появилась новая опция resolver в вашем Cargo.toml, где вы можете установить resolver="2", чтобы попробовать новый подход к разрешению функциональных флагов. Вы можете ознакомиться с RFC 2957 для получения подробного описания поведения, которое можно резюмировать следующим образом.


  • Dev dependencies когда пакет используется совместно как обычная зависимость и dev, возможности dev-зависимости включаются только в том случае, если текущая сборка включает dev-зависимости.
  • Host Dependencies когда пакет совместно используется как обычная зависимость и зависимость сборки или процедурный макрос, features для нормальной зависимости сохраняются независимо от зависимости сборки или процедурного макроса.
  • Target dependencies когда у пакета включены зависимые от платформы features, и он присутствует в графе сборки несколько раз, будут включены только features, подходящие текущей платформе сборки.

Хотя это может привести к компиляции некоторых пакетов более одного раза, это должно обеспечить гораздо более интуитивный опыт разработки при использовании функций с Cargo. Если вы хотите узнать больше, вы также можете прочитать раздел Feature Resolver в Cargo Book для получения дополнительной информации. Мы хотели бы поблагодарить команду Cargo и всех участников за их тяжёлую работу по разработке и внедрению нового механизма!


[package]resolver = "2"# Или если вы используете workspace[workspace]resolver = "2"

Разделение отладочной информации


Хоть это и нечасто освещается в релизах, команда Rust постоянно работает над сокращением времени компиляции. В этом выпуске вносится самое крупное улучшение за долгое время для Rust на macOS. Отладочная информация исходного кода содержится в собранном бинарнике, и за счет этого программа может дать больше информации о том, что происходит во время исполнения. Раньше в macOS отладочная информация собиралась в единую директорию .dSYM при помощи утилиты dsymutil, что могло занимать много времени и дискового пространства.


Сбор всей отладочной информации в эту директорию помогал найти её во время выполнения, особенно если бинарник перемещался. Однако у такого решения есть и обратная сторона: если вы сделали небольшое изменение в вашей программе, то dsymutil необходимо запустить над всем собранным бинарником, чтобы собрать директорию .dSYM. Иногда это могло сильно увеличить время сборки, особенно для крупных проектов, поскольку надо перебирать все зависимости, но это важный шаг, без которого стандартная библиотека Rust не знает, как загружать отладочную информацию на macOS.


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


Вы можете включить новое поведение, установив флаг -Csplit-debuginfo=unpacked при запуске rustc или задав опцию split-debuginfo в unpacked раздела [profile] в Cargo. С опцией "unpacked" rustc будет оставлять объектные файлы (.o) в директории сборки вместо их удаления и пропустит запуск dsymutil. Поддержка бэктрейсов Rust достаточно умна, чтобы понять, как найти эти .o файлы. Такие инструменты, как lldb, также знают, как это делается. Это должно работать до тех пор, пока вам не понадобится переместить бинарники в другое место и сохранить отладочную информацию.


[profile.dev]split-debuginfo = "unpacked"

Стабилизированные API


Итого: в этом выпуске было стабилизировано 18 новых методов для разных типов, например slice и Peekable. Одним из примечательных дополнений является стабилизация ptr::addr_of! и ptr::addr_of_mut!, которая позволяет вам создавать сырые указатели для полей без выравнивания. Ранее это было невозможно, так как Rust требовал, чтобы &/&mut были выровнены и указывали на инициализированные данные. Из-за этого преобразование &addr as *const _ приводило к неопределённому поведению, так как &addr должно быть выровнено. Теперь эти два макроса позволяют вам безопасно создать невыровненные указатели.


use std::ptr;#[repr(packed)]struct Packed {    f1: u8,    f2: u16,}let packed = Packed { f1: 1, f2: 2 };// `&packed.f2` будет создана ссылка на невыровненную память, таким образом это неопределённое поведение!let raw_f2 = ptr::addr_of!(packed.f2);assert_eq!(unsafe { raw_f2.read_unaligned() }, 2);

Следующие методы были стабилизированы:



Другие изменения


Синтаксис, пакетный менеджер Cargo и анализатор Clippy также претерпели некоторые изменения.


Участники 1.51.0


Множество людей собрались вместе, чтобы создать Rust 1.51.0. Мы не смогли бы сделать это без всех вас. Спасибо!


От переводчиков


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


Данную статью совместными усилиями перевели andreevlex, TelegaOvoshey, blandger, nlinker и funkill.

Подробнее..

Запуск QT на STM32. Часть 2. Теперь с псевдо 3d и тачскрином

05.04.2021 20:15:29 | Автор: admin
Мы в проекте Embox некоторое время назад запустили Qt на платформе STM32. Примером было приложение moveblocks анимация с четырьмя синими квадратами, которые перемещаются по экрану. Нам захотелось большего, например, добавить интерактивность, ведь на плате доступен тачскрин. Мы выбрали приложение animatedtiles просто потому, что оно и на компьютере круто смотрится. По нажатию виртуальных кнопок множество иконок плавно перемещаются по экрану, собираясь в различные фигуры. Причем выглядит это вполне как 3d анимация и у нас даже были сомнения, справится ли микроконтроллер с подобной задачей.

Сборка


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

Первый запуск на плате


Размер экрана у STM32F746G-Discovery 480x272, при запуске приложение нарисовалось только в верхнюю часть экрана. Нам естественно захотелось выяснить в чем дело. Конечно можно уйти в отладку прямо на плате, но есть более простое решение. запустить приложение на Линукс с теми же самыми размерами 480x272 с виртуальным фреймбуфером QVFB.

Запускаем на Линукс


Для запуска на Linux нам потребуется три части QVFB, библиотека Qt, и само приложение.

QVFB это обычное приложение, которое предоставит нам виртуальный экран для работы Qt. Собираем его как написано в официальной документации.

Запускаем с нужным размером экрана:
./qvfb -width 480 -height 272 -nocursor


Далее, собираем библиотеку Qt как embedded, т.е. С указанием опции -embedded. Я еще отключил разные модули для ускорения сборки, в итоге конфигурация выглядела вот так:
./configure -opensource -confirm-license -debug \    -embedded -qt-gfx-qvfb -qvfb \    -no-javascript-jit -no-script -no-scripttools \    -no-qt3support -no-webkit -nomake demos -nomake examples


Далее собираем приложение animatedtiles (qmake + make). И запускаем скомпилированное приложение, указав ему наш QVFB:
./examples/animation/animatedtiles/animatedtiles -qws -display QVFb:0


После запуска я увидел, что на Линуксе также рисуется только в часть экрана. Я немного доработал animatedtiles, добавив опцию -fullscreen, при указании которой приложение стартует в полноэкранном режиме.

Запуск на Embox


Модифицированный исходный код приложения будем использовать в Embox. Пересобираем и запускаем. Приложение не запустилось, при этом появились сообщения о нехватке памяти в Qt. Смотрим в конфигурацию Embox и находим что размер кучи установлен 2Мб и его явно не хватает. Опять же, можно попробовать выяснить этот момент с прямо на плате, но давайте сделаем это с удобствами в Линукс.

Для этого запускаем приложение следующим образом:
$ valgrind --tool=massif --massif-out-file=animatedtiles.massif ./examples/animation/animatedtiles/animatedtiles -qws -fullscreen$ ms_print animatedtiles.massif > animatedtiles.out


В файле animatedtiles.out видим максимальное значение заполненности кучи порядка 2.7 Мб. Отлично, теперь можно не гадать, а вернуться в Embox и поставить размер кучи 3Мб.

Animatedtiles запустилось.

Запуск на STM32F769I-Discovery.


Давайте попробуем еще усложнить задачу, и запустим тот же пример на подобном микроконтроллере, но только с большим разрешением экрана STM32F769I-Discovery (800x480). То есть теперь под фреймбуфер потребуется в 1.7 раз больше памяти (напомню, что у STM32F746G экран 480x272), но это компенсируется в два раза большим размером SDRAM (16 Мб против 8Мб доступной памяти SDRAM у STM32F746G).

Для оценки размера кучи, как и выше, сначала запускаем Qvfb и наше приложение на Линуксе:
$ ./qvfb -width 800 -height 480 -nocursor &$ valgrind --tool=massif --massif-out-file=animatedtiles.massif ./examples/animation/animatedtiles/animatedtiles -qws -fullscreen$ ms_print animatedtiles.massif > animatedtiles.out


Смотрим расход памяти в куче около 6 МБ (почти в два раза больше, чем на STM32F746G).

Осталось выставить нужный размер кучи в mods.conf и пересобрать. Приложение запустилось сразу и без проблем, что и продемонстрировано этом коротком видео


Традиционно можно воспроизвести результаты самостоятельно. Как это сделать описано у нас на wiki.

Данная статья впервые была нами опубликована на английском языке на embedded.com.
Подробнее..

Перевод Изучаем внутренние компоненты Docker Объединённая файловая система

09.04.2021 16:11:40 | Автор: admin

Создавать, запускать, просматривать, перемещать контейнеры и образы с помощью интерфейса командной строки Docker (Docker CLI) проще простого, но задумывались ли вы когда-нибудь, как на самом деле работают внутренние компоненты, обеспечивающие работу интерфейса Docker? За этим простым интерфейсом скрывается множество продвинутых технологий, и специально к старту нового потока курса по DevOps в этой статье мы рассмотрим одну из них объединённую файловую систему, используемую во всех слоях контейнеров и образов. Маститым знатокам контейнеризации и оркестрации данный материал навряд ли откроет что-то новое, зато будет полезен тем, кто делает первые шаги в DevOps.


Что такое объединённая файловая система?

Каскадно-объединённое монтирование это тип файловой системы, в которой создается иллюзия объединения содержимого нескольких каталогов в один без изменения исходных (физических) источников. Такой подход может оказаться полезным, если имеются связанные наборы файлов, хранящиеся в разных местах или на разных носителях, но отображать их надо как единое и совокупное целое. Например, набор пользовательских/корневых каталогов, расположенных на удалённых NFS-серверах, можно свести в один каталог или можно объединить разбитый на части ISO-образ в один целый образ.

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

  • Начнём с исходной объединённой файловой системы, а именно UnionFS. На данный момент поддержка файловой система UnionFS прекращена, последнее изменение кода было зафиксировано в августе 2014 года. Более подробная информация об этой файловой системе приведена здесь: unionfs.filesystems.org.

  • aufs альтернативная версия исходной файловой системы UnionFS с добавлением множества новых функций. Данную файловую систему нельзя использовать в составе ванильного ядра Linux. Aufs использовалась в качестве файловой системы по умолчанию для Docker на Ubuntu/Debian, однако со временем она была заменена на OverlayFS (для ядра Linux >4.0). По сравнению с другими объединёнными файловыми системами эта система имеет ряд преимуществ, описанных в Docker Docs.

  • Следующая система OverlayFS была включена в ядро Linux Kernel, начиная с версии 3.18 (26 октября 2014 года). Данная файловая система используется по умолчанию драйвером overlay2 Docker (это можно проверить, запустив команду docker system info | grep Storage). Данная файловая система в целом обеспечивает лучшую, чем aufs, производительность и имеет ряд интересных функциональных особенностей, например функцию разделения страничного кэша.

  • ZFS объединённая файловая система, разработанная Sun Microsystems (в настоящее время эта компания называется Oracle). В этой системе реализован ряд полезных функций, таких как функция иерархического контрольного суммирования, функция обработки снимков, функция резервного копирования/репликации или архивирования и дедупликации (исключения избыточности) внутренних данных. Однако, поскольку автором этой файловой системы является Oracle, её выпуск осуществлялся под общей лицензией на разработку и распространение (CDDL), не распространяемой на программное обеспечение с открытым исходным кодом, поэтому данная файловая система не может поставляться как часть ядра Linux. Тем не менее можно воспользоваться проектом ZFS on Linux (ZoL), который в документации Docker описывается как работоспособный и хорошо проработанный..., но, увы, непригодный к промышленной эксплуатации. Если вам захочется поработать с этой файловой системой, её можно найти здесь.

  • Btrfs ещё один вариант файловой системы, представляющий собой совместный проект множества компаний, в том числе SUSE, WD и Facebook. Данная файловая система выпущена под лицензией GPL и является частью ядра Linux. Btrfs файловая система по умолчанию дистрибутива Fedora 33. В ней также реализованы некоторые полезные функции, такие как операции на уровне блоков, дефрагментация, доступные для записи снимки и множество других. Если вас не пугают трудности, связанные с переходом на специализированный драйвер устройств памяти для Docker, файловая система Btrfs с её функциональными и производительными возможностями может стать лучшим вариантом.

Если вы хотите более глубоко изучить характеристики драйверов, используемых в Docker, в Docker Docs можно ознакомиться с таблицей сравнения разных драйверов. Если вы затрудняетесь с выбором файловой системы (не сомневаюсь, есть и такие программисты, которые знают все тонкости файловых систем, но эта статья предназначена не для них), возьмите за основу файловую систему по умолчанию overlay2 именно её я буду использовать в примерах в оставшейся части данной статьи.

Почему именно она?

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

Многие образы, используемые для запуска контейнеров, занимают довольно большой объём, например, ubuntu занимает 72 Мб, а nginx 133 Мб. Было бы довольно разорительно выделять столько места всякий раз, когда потребуется из этих образов создать контейнер. При использовании объединённой файловой системы Docker создаёт поверх образа тонкий слой, а остальная часть образа может быть распределена между всеми контейнерами. Мы также получаем дополнительное преимущество за счёт сокращения времени запуска, так как отпадает необходимость в копировании файлов образа и данных.

Объединённая файловая система также обеспечивает изоляцию, поскольку контейнеры имеют доступ к общим слоям образа только для чтения. Если контейнерам когда-нибудь понадобится внести изменения в любой файл, доступный только для чтения, они используют стратегию копирования при записи copy-on-write (её мы обсудим чуть позже), позволяющую копировать содержимое на верхний слой, доступный для записи, где такое содержимое может быть безопасно изменено.

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

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

. upper    code.py  # Content: `print("Hello Overlay!")`    script.py lower     code.py  # Content: `print("This is some code...")`     config.yaml

В терминологии объединённого монтирования такие каталоги называются ветвями. Каждой из таких ветвей присваивается свой приоритет. Приоритет используется для того, чтобы решить, какой именно файл будет отображаться в объединённом представлении, если в нескольких исходных ветках присутствуют файлы с одним и тем же именем. Если проанализировать представленные выше файлы и каталоги, станет понятно, что такой конфликт может возникнуть, если мы попытаемся использовать их в режиме наложения (файл code.py). Давайте попробуем и посмотрим, что у нас получится:

~ $ mount -t overlay \    -o lowerdir=./lower,\       upperdir=./upper,\       workdir=./workdir \    overlay /mnt/merged~ $ ls /mnt/mergedcode.py  config.yaml  script.py~ $ cat /mnt/merged/code.pyprint("Hello Overlay!")

В приведённом выше примере мы использовали команду mount с type overlay, чтобы объединить нижний каталог (только для чтения; более низкий приоритет) и верхний каталог (чтение-запись; более высокий приоритет) в объединённое представление в каталоге /mnt/merged. Мы также включили опцию workdir=./workdir. Этот каталог служит местом для подготовки объединённого представления нижнего каталога (lowerdir) и верхнего каталога (upperdir) перед их перемещением в каталог /mnt/merged.

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

Теперь мы знаем, как объединить два каталога и что произойдёт при возникновении конфликта. Но что произойдёт, если попытаться изменить определенные файлы в объединённом представлении? Здесь в игру вступает функция копирования при записи (CoW). Что именно делает эта функция? CoW это способ оптимизации, при котором, если две вызывающих программы обращаются к одному и тому же ресурсу, можно дать им указатель на один и тот же ресурс, не копируя его. Копирование необходимо только тогда, когда одна из вызывающих программ пытается осуществить запись собственной "копии" отсюда в названии способа появилось слово "копия", то есть копирование осуществляется при (первой попытке) записи.

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

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

Мы много говорили о принципах объединённого монтирования, но как все эти принципы работают на платформе Docker и её контейнерах? Рассмотрим многоуровневую архитектуру Docker. Песочница контейнера состоит из нескольких ветвей образа, или, как мы их называем, слоёв. Такими слоями являются часть объединённого представления, доступная только для чтения (lowerdir), и слой контейнера тонкая верхняя часть, доступная для записи (upperdir).

Не считая терминологических различий, речь фактически идёт об одном и том же слои образа, извлекаемые из реестра, представляют собой lowerdir, и, если запускается контейнер, upperdir прикрепляется поверх слоев образа, обеспечивая рабочую область, доступную для записи в контейнер. Звучит довольно просто, не так ли? Давайте проверим, как всё работает!

Проверяем

Чтобы показать, как Docker использует файловую систему OverlayFS, попробуем смоделировать процесс монтирования Docker контейнеров и слоев образа. Прежде чем мы приступим, нужно вначале очистить рабочее пространство и получить образ, с которым можно работать:

~ $ docker image prune -af...Total reclaimed space: ...MB~ $ docker pull nginxUsing default tag: latestlatest: Pulling from library/nginxa076a628af6f: Pull complete0732ab25fa22: Pull completed7f36f6fe38f: Pull completef72584a26f32: Pull complete7125e4df9063: Pull completeDigest: sha256:10b8cc432d56da8b61b070f4c7d2543a9ed17c2b23010b43af434fd40e2ca4aaStatus: Downloaded newer image for nginx:latestdocker.io/library/nginx:latest

Итак, у нас имеется образ (nginx), с которым можно работать, далее нужно проверить его слои. Проверить слои образа можно, либо запустив проверку образа в Docker и изучив поля GraphDriver, либо перейдя в каталог /var/lib/docker/overlay2, в котором хранятся все слои образа. Выполним обе эти операции и посмотрим, что получится:

~ $ cd /var/lib/docker/overlay2~ $ ls -ltotal 0drwx------. 4 root root     55 Feb  6 19:19 3d963d191b2101b3406348217f4257d7374aa4b4a73b4a6dd4ab0f365d38dfbddrwx------. 3 root root     47 Feb  6 19:19 410c05aaa30dd006fc47d8c23ba0d173c6d305e4d93fdc3d9abcad9e78862b46drwx------. 4 root root     72 Feb  6 19:19 685374e39a6aac7a346963bb51e2fc7b9f5e2bdbb5eac6c76ccdaef807abc25ebrw-------. 1 root root 253, 0 Jan 31 18:15 backingFsBlockDevdrwx------. 4 root root     72 Feb  6 19:19 d487622ece100972afba76fda13f56029dec5ec26ffcf552191f6241e05cab7edrwx------. 4 root root     72 Feb  6 19:19 fb18be50518ec9b37faf229f254bbb454f7663f1c9c45af9f272829172015505drwx------. 2 root root    176 Feb  6 19:19 l~ $ tree 3d963d191b2101b3406348217f4257d7374aa4b4a73b4a6dd4ab0f365d38dfbd/3d963d191b2101b3406348217f4257d7374aa4b4a73b4a6dd4ab0f365d38dfbd/ diff    docker-entrypoint.d        20-envsubst-on-templates.sh link lower work~ $ docker inspect nginx | jq .[0].GraphDriver.Data{  "LowerDir": "/var/lib/docker/overlay2/fb18be50518ec9b37faf229f254bbb454f7663f1c9c45af9f272829172015505/diff:    /var/lib/docker/overlay2/d487622ece100972afba76fda13f56029dec5ec26ffcf552191f6241e05cab7e/diff:    /var/lib/docker/overlay2/685374e39a6aac7a346963bb51e2fc7b9f5e2bdbb5eac6c76ccdaef807abc25e/diff:    /var/lib/docker/overlay2/410c05aaa30dd006fc47d8c23ba0d173c6d305e4d93fdc3d9abcad9e78862b46/diff",  "MergedDir": "/var/lib/docker/overlay2/3d963d191b2101b3406348217f4257d7374aa4b4a73b4a6dd4ab0f365d38dfbd/merged",  "UpperDir": "/var/lib/docker/overlay2/3d963d191b2101b3406348217f4257d7374aa4b4a73b4a6dd4ab0f365d38dfbd/diff",  "WorkDir": "/var/lib/docker/overlay2/3d963d191b2101b3406348217f4257d7374aa4b4a73b4a6dd4ab0f365d38dfbd/work"}

Если внимательно посмотреть на полученные результаты, можно заметить, что они весьма похожи на те, что мы уже наблюдали после применения команды mount, не находите?

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

  • MergedDir: объединённое представление всех слоев образа и контейнера.

  • UpperDir: слой для чтения и записи, на котором записываются изменения.

  • WorkDir: рабочий каталог, используемый Linux OverlayFS для подготовки объединённого представления.

Сделаем ещё один шаг запустим контейнер и изучим его слои:

~ $ docker run -d --name container nginx~ $ docker inspect container | jq .[0].GraphDriver.Data{  "LowerDir": "/var/lib/docker/overlay2/59bcd145c580de3bb3b2b9c6102e4d52d0ddd1ed598e742b3a0e13e261ee6eb4-init/diff:    /var/lib/docker/overlay2/3d963d191b2101b3406348217f4257d7374aa4b4a73b4a6dd4ab0f365d38dfbd/diff:    /var/lib/docker/overlay2/fb18be50518ec9b37faf229f254bbb454f7663f1c9c45af9f272829172015505/diff:    /var/lib/docker/overlay2/d487622ece100972afba76fda13f56029dec5ec26ffcf552191f6241e05cab7e/diff:    /var/lib/docker/overlay2/685374e39a6aac7a346963bb51e2fc7b9f5e2bdbb5eac6c76ccdaef807abc25e/diff:    /var/lib/docker/overlay2/410c05aaa30dd006fc47d8c23ba0d173c6d305e4d93fdc3d9abcad9e78862b46/diff",  "MergedDir": "/var/lib/docker/overlay2/59bcd145c580de3bb3b2b9c6102e4d52d0ddd1ed598e742b3a0e13e261ee6eb4/merged",  "UpperDir": "/var/lib/docker/overlay2/59bcd145c580de3bb3b2b9c6102e4d52d0ddd1ed598e742b3a0e13e261ee6eb4/diff",  "WorkDir": "/var/lib/docker/overlay2/59bcd145c580de3bb3b2b9c6102e4d52d0ddd1ed598e742b3a0e13e261ee6eb4/work"}~ $ tree -l 3 /var/lib/docker/overlay2/59bcd145c580de3bb3b2b9c6102e4d52d0ddd1ed598e742b3a0e13e261ee6eb4/diff  # The UpperDir/var/lib/docker/overlay2/59bcd145c580de3bb3b2b9c6102e4d52d0ddd1ed598e742b3a0e13e261ee6eb4/diff etc    nginx        conf.d            default.conf run    nginx.pid var     cache         nginx             client_temp             fastcgi_temp             proxy_temp             scgi_temp             uwsgi_temp

Из представленных выше выходных данных следует, что те же каталоги, которые были перечислены в выводе команды docker inspect nginx ранее как MergedDir, UpperDir и WorkDir (с id 3d963d191b2101b3406348217f4257d7374aa4b4a73b4a6dd4ab0f365d38dfbd), теперь являются частью LowerDir контейнера. В нашем случае LowerDir составляется из всех слоев образа nginx, размещённых друг на друге. Поверх них размещается слой в UpperDir, доступный для записи, содержащий каталоги /etc, /run и /var. Также, раз уж мы выше упомянули MergedDir, можно видеть всю доступную для контейнера файловую систему, в том числе всё содержимое каталогов UpperDir и LowerDir.

И, наконец, чтобы эмулировать поведение Docker, мы можем использовать эти же каталоги для ручного создания собственного объединённого представления:

~ $ mount -t overlay -o \lowerdir=/var/lib/docker/overlay2/59bcd145c580de3bb3b2b9c6102e4d52d0ddd1ed598e742b3a0e13e261ee6eb4-init/diff:    /var/lib/docker/overlay2/3d963d191b2101b3406348217f4257d7374aa4b4a73b4a6dd4ab0f365d38dfbd/diff:    /var/lib/docker/overlay2/fb18be50518ec9b37faf229f254bbb454f7663f1c9c45af9f272829172015505/diff:    /var/lib/docker/overlay2/d487622ece100972afba76fda13f56029dec5ec26ffcf552191f6241e05cab7e/diff:    /var/lib/docker/overlay2/685374e39a6aac7a346963bb51e2fc7b9f5e2bdbb5eac6c76ccdaef807abc25e/diff:    /var/lib/docker/overlay2/410c05aaa30dd006fc47d8c23ba0d173c6d305e4d93fdc3d9abcad9e78862b46/diff,\upperdir=/var/lib/docker/overlay2/59bcd145c580de3bb3b2b9c6102e4d52d0ddd1ed598e742b3a0e13e261ee6eb4/diff,\workdir=/var/lib/docker/overlay2/59bcd145c580de3bb3b2b9c6102e4d52d0ddd1ed598e742b3a0e13e261ee6eb4/work \overlay /mnt/merged~ $ ls /mnt/mergedbin   dev                  docker-entrypoint.sh  home  lib64  mnt  proc  run   srv  tmp  varboot  docker-entrypoint.d  etc                   lib   media  opt  root  sbin  sys  usr~ $ umount overlay

В нашем случае мы просто взяли значения из предыдущего фрагмента кода и передали их в качестве соответствующих аргументов в команду mount. Разница лишь в том, что для объединённого представления вместо /var/lib/docker/overlay2/.../merged мы использовали /mnt/merged.

Именно к этому сводится действие файловой системы OverlayFS в Docker на множестве уложенных друг на друга слоев может использоваться одна команда монтирования. Ниже приводится отвечающая за это часть кода Docker заменяются значения lowerdir=...,upperdir=...,workdir=..., после чего следует команда unix.Mount.

// https://github.com/moby/moby/blob/1ef1cc8388165b2b848f9b3f53ec91c87de09f63/daemon/graphdriver/overlay2/overlay.go#L580opts := fmt.Sprintf("lowerdir=%s,upperdir=%s,workdir=%s", strings.Join(absLowers, ":"), path.Join(dir, "diff"), path.Join(dir, "work"))mountData := label.FormatMountLabel(opts, mountLabel)mount := unix.MountmountTarget := mergedDirrootUID, rootGID, err := idtools.GetRootUIDGID(d.uidMaps, d.gidMaps)// ...

Заключение

Интерфейс Docker может показаться чёрным ящиком с большим количеством скрытых внутри непонятных технологий. Эти технологии пусть непонятные довольно интересны и полезны. Я вовсе не хочу сказать, что для эффективного использования Docker нужно досконально знать все их тонкости, но, на мой взгляд, если вы потратите немного времени и поймёте, как они работают, это пойдёт вам только на пользу. Ясное понимание принципов работы инструмента облегчает принятие правильных решений в нашем случае речь идёт о повышении производительности и возможных аспектах, связанных с безопасностью. Кроме того, вы сможете ознакомиться с некоторыми продвинутыми технологиями, и кто знает, в каких областях знаний они могут вам пригодится в будущем!

В этой статье мы рассмотрели только часть архитектуры Docker файловую систему. Есть и другие части, с которыми стоит ознакомиться более внимательно, например контрольные группы (cgroups) или пространства имен Linux. Если вы их освоите можно уже задуматься о переходе в востребованный DevOps. А с остальными знаниями, необходимыми для данной профессии мы поможем на курсе по профессии DevOps-инженер.

Узнайте, как прокачаться в других специальностях или освоить их с нуля:

Другие профессии и курсы
Подробнее..

Перевод Rust 1.52.0 улучшения Clippy и стабилизация API

07.05.2021 14:19:42 | Автор: admin

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


Если вы установили предыдущую версию Rust средствами rustup, то для обновления до версии 1.52.0 вам достаточно выполнить следующую команду:


rustup update stable

Если у вас ещё не установлен rustup, вы можете установить его с соответствующей страницы нашего веб-сайта, а также посмотреть подробные примечания к выпуску на GitHub.


Что было стабилизировано в 1.52.0


Самое значительное изменение этого выпуска не касается самого языка или стандартной библиотеки. Это улучшения в Clippy.


Ранее запуск cargo clippy после cargo check не запускал Clippy: кэширование в Cargo не видело разницы между ними. В версии 1.52 это поведение было исправлено, а значит, теперь пользователи будут получать то поведение, которое ожидают, независимо от порядка запуска этих команд.


Стабилизированные API


Следующие методы были стабилизированы:



Следующие ранее стабилизированные API стали const:



Другие изменения


Синтаксис, пакетный менеджер Cargo и анализатор Clippy также претерпели некоторые изменения.


Участники 1.52.0


Множество людей собрались вместе, чтобы создать Rust 1.52.0. Мы не смогли бы сделать это без всех вас. Спасибо!


От переводчиков


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


Данную статью совместными усилиями перевели Belanchuk, TelegaOvoshey, blandger, nlinker и funkill.

Подробнее..

Перевод Rust 1.53.0 IntoIterator для массивов, quotquot в шаблонах, Unicode-идентификаторы, поддержка имени HEAD-ветки в Cargo

18.06.2021 18:20:53 | Автор: admin

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


Если вы установили предыдущую версию Rust средствами rustup, то для обновления до версии 1.53.0 вам достаточно выполнить следующую команду:


rustup update stable

Если у вас ещё не установлен rustup, вы можете установить его с соответствующей страницы нашего веб-сайта, а также посмотреть подробные примечания к выпуску на GitHub.


Что было стабилизировано в 1.53.0


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


IntoIterator для массивов


Это первый выпуск Rust, в котором массивы реализуют типаж IntoIterator. Теперь вы можете итерироваться в массиве по значению:


for i in [1, 2, 3] {    ..}

Раньше это было возможно только по ссылке, с помощью &[1, 2, 3] или [1, 2, 3].iter().


Аналогично вы теперь можете передать массив в методы, ожидающие T: IntoIterator:


let set = BTreeSet::from_iter([1, 2, 3]);

for (a, b) in some_iterator.chain([1]).zip([1, 2, 3]) {    ..}

Это не было реализовано ранее из-за проблем с совместимостью. IntoIterator всегда реализуется для ссылок на массивы и в предыдущих выпусках array.into_iter() компилировался, преобразовываясь в (&array).into_iter().


Начиная с этого выпуска, массивы реализуют IntoIterator с небольшими оговорками для устранения несовместимости кода. Компилятор, как и прежде, преобразовывает array.into_iter() в (&array).into_iter(), как если бы реализации типажа ещё не было. Это касается только синтаксиса вызова метода .into_iter() и не затрагивает, например, for e in [1, 2, 3], iter.zip([1, 2, 3]) или IntoIterator::into_iter([1, 2, 3]), которые прекрасно компилируются.


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


"Или" в шаблонах


Синтаксис шаблонов был расширен поддержкой |, вложенного в шаблон где угодно. Это позволяет писать Some(1 | 2) вместо Some(1) | Some(2).


match result {     Ok(Some(1 | 2)) => { .. }     Err(MyError { kind: FileNotFound | PermissionDenied, .. }) => { .. }     _ => { .. }}

Unicode-идентификаторы


Теперь идентификаторы могут содержать не-ASCII символы. Можно использовать все действительные идентификаторы символов Unicode, определённые в UAX #31. Туда включены символы из многих разных языков и письменностей но не эмодзи.


Например:


const BLHAJ: &str = "";struct  {    : String,}let  = 1;

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


warning: identifier pair considered confusable between `` and `s`

Поддержка имени HEAD-ветки в Cargo


Cargo больше не предполагает, что HEAD-ветка в git-репозитории называется master. А следовательно, вам не надо указывать branch = "main" для зависимостей из git-репозиториев, в которых ветка по умолчанию main.


Инкрементальная компиляция до сих пор отключена по умолчанию


Как ранее говорилось в анонсе 1.52.1, инкрементальная компиляция была отключена для стабильных выпусков Rust. Функциональность остаётся доступной в каналах beta и nightly. Метод включения инкрементальной компиляции в 1.53.0 не изменился с 1.52.1.


Стабилизированные API


Следующие методы и реализации типажей были стабилизированы:



Другие изменения


Синтаксис, пакетный менеджер Cargo и анализатор Clippy также претерпели некоторые изменения.


Участники 1.53.0


Множество людей собрались вместе, чтобы создать Rust 1.53.0. Мы не смогли бы сделать это без всех вас. Спасибо!




От переводчиков


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


Данную статью совместными усилиями перевели TelegaOvoshey, blandger, Belanchuk и funkill.

Подробнее..

Парсинг логов при помощи Fluent-bit

25.03.2021 18:04:40 | Автор: admin

Не так давно передо мной встала задача организации логгирования сервисов, разворачиваемых с помощью docker контейнеров. В интернете нашел примеры простого логгирования контейнеров, однако хотелось большего. Изучив возможности Fluent-bit я собрал рабочий пайплайн трансформации логов. Что в сочетании с Elasticsearch и Kibana, позволило быстро искать и анализировать лог-сообщения.

Цель туториала: организовать логгирование docker контейнеров. Также необходимо структурировать записи логов, и обеспечить поиск и фильтрацию по их полям.

Кому интересно, добро пожаловать под кат)

Необходимы базовые знания bash, docker-compose, Elasticsearch и Kibana.

Обзор используемого стека

Тестовое приложение будем запускать с помощьюdocker-compose.

Для организации логгирования воспользуемся следующими технологиями:

  • fluent-bit- осуществляет сбор, обработку и пересылку в хранилище лог-сообщений.

  • elasticsearch- централизованно хранит лог-сообщения, обеспечивает их быстрый поиск и фильтрацию.

  • kibana- предоставляет интерфейс пользователю, для визуализации данных хранимых в elasticsearch

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

Подготовка тестового приложения

Для примера организуем логгирование веб-сервера Nginx.

Подготовка Nginx

  1. Создадим директорию с проектом и добавим в нее docker-compose.yml, в котором будем задавать конфигурацию запуска контейнеров приложения.

  2. Определим формат логов Nginx. Для этого создадим директорию nginx c файлом nginx.conf. В нем переопределим стандартный формат логов:

    user  nginx;worker_processes  1;error_log  /var/log/nginx/error.log warn;pid        /var/run/nginx.pid;events {    worker_connections  1024;}http {    include       /etc/nginx/mime.types;    default_type  application/octet-stream;log_format  main  'access_log $remote_addr "$request" '                  '$status "$http_user_agent"';access_log  /var/log/nginx/access.log  main;sendfile        on;keepalive_timeout  65;include /etc/nginx/conf.d/*.conf;}
    
  3. Добавим сервисwebв docker-compose.yml:

    version: "3.8"services:  web:    container_name: nginx    image: nginx    ports:      - 80:80    volumes:      # добавляем конфигурацию в контейнер      - ./nginx/nginx.conf:/etc/nginx/nginx.conf
    

Подготовка fluent-bit

Для начала организуем самый простой вариант логгирования. Создадим директорию fluent-bit c конфигурационным файлом fluent-bit.conf. Про формат и схему конфигурационного файла можно прочитатьздесь.

  1. Fluent-bit предоставляет большое количество плагинов для сбора лог-сообщений из различных источников. Полный список можно найтиздесь. В нашем примере мы будем использовать плагинforward.

    Плагин выводаstdoutпозволяет перенаправить лог-сообщения в стандартный вывод (standard output).

    [INPUT]    Name              forward[OUTPUT]    Name stdout    Match *
    
  2. Добавим в docker-compose.yml сервисfluent-bit:

    version: "3.8"services:  web:    ...  fluent-bit:    container_name: fluent-bit    image: fluent/fluent-bit    ports:      # необходимо открыть порты, которые используются плагином forward      - 24224:24224      - 24224:24224/udp    volumes:      # добавляем конфигурацию в контейнер      - ./fluent-bit/fluent-bit.conf:/fluent-bit/etc/fluent-bit.conf
    
  3. Добавим настройки логгирования для сервисаweb:

    version: "3.8"services:  web:    ...    depends_on:      - fluent-bit    logging:      # используемый драйвер логгирования      driver: "fluentd"      options:        # куда посылать лог-сообщения, необходимо что бы адрес         # совпадал с настройками плагина forward        fluentd-address: localhost:24224        # теги используются для маршрутизации лог-сообщений, тема         # маршрутизации будет рассмотрена ниже        tag: nginx.logs  fluent-bit:    ...
    
  4. Запустим тестовое приложение:

    docker-compose up
    

    Сгенерируем лог-сообщение, откроем еще одну вкладку терминала и выполним команду:

    curl localhost
    

    Получим лог-сообщение в следующем формате:

    [    1616473204.000000000,    {"source"=>"stdout",    "log"=>"172.29.0.1 "GET / HTTP/1.1" 200 "curl/7.64.1"",    "container_id"=>"efb81a754706b1ece6948072934df85ea44466305b326cd45",    "container_name"=>"/nginx"}]
    

    Сообщение состоит из:

    • временной метки, добавляемой fluent-bit;

    • лог-сообщения;

    • мета данных, добавляемых драйвером fluentd.

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

 docker-compose.yml fluent-bit    fluent-bit.conf nginx     nginx.conf

Кратко о маршрутизации лог-сообщиний в fluent-bit

Маршрутизация в fluent-bit позволяет направлять лог-сообщения через различные фильтры, для их преобразования, и в конечном итоге в один или несколько выходных интерфейсов. Для организации маршрутизации используется две основные концепции:

  • тег (tag) - человеко читаемый индикатор, позволяющий однозначно определить источник лог-сообщения;

  • правило сопоставления (match) - правило, определяющее куда лог-сообщение должно быть перенаправлено.

Выглядит все следующим образом:

  1. Входной интерфейс присваивает лог-сообщению заданные тег.

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

Подробнее можно прочитать вофициальной документации.

Очистка лог-сообщений от мета данных.

Мета данные для нас не представляют интерес, и только загромождают лог сообщение. Давайте удалим их. Для этого воспользуемся фильтромrecord_modifier. Зададим его настройки в файле fluent-bit.conf:

[FILTER]    Name record_modifier    # для всех лог-сообщений    Match *    # оставить только поле log    Whitelist_key log

Теперь лог-сообщение имеет вид:

[    1616474511.000000000,    {"log"=>"172.29.0.1 "GET / HTTP/1.1" 200 "curl/7.64.1""}]

Отделение логов запросов от логов ошибок

На текущий момент логи посылаемые Nginx можно разделить на две категории:

  • логи с предупреждениями, ошибками;

  • логи запросов.

Давайте разделим логи на две группы и будем структурировать только логи запросов. Все логи-сообщения от Nginx помечаются тегом nginx.logs. Поменяем тег для лог-сообщений запросов на nginx.access. Для их идентификации мы заблаговременно добавили в начало сообщения префикс access_log.

Добавим новый фильтрrewrite_tag. Ниже приведена его конфигурация.

[FILTER]    Name rewrite_tag    # для сообщений с тегом nginx.logs    Match nginx.logs    # применить правило: для лог-сообщений поле log которых содержит строку    # access_log, поменять тег на nginx.access, исходное лог-сообщение отбросить.    Rule $log access_log nginx.access false

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

Парсинг лог-сообщения

Давайте структурируем наше лог-сообщение. Для придания структуры лог-сообщению его необходимо распарсить. Это делается с помощью фильтраparser.

  1. Лог-сообщение представляет собой строку. Воспользуемся парсеромregex, который позволяет с помощью регулярных выражений определить пары ключ-значение для информации содержащейся в лог-сообщении. Зададим настройки парсера. Для этого в директории fluent-bit создадим файл parsers.conf и добавим в него следующее:

    [PARSER]    Name   nginx_parser    Format regex    Regex  ^access_log (?<remote_address>[^ ]*) "(?<method>\S+)(?: +(?<path>[^\"]*?)(?: +\S*)?)?" (?<status>[^ ]*) "(?<http_user_agent>[^\"]*)"$    Types  status:integer
    
  2. Обновим конфигурационный файл fluent-bit.conf. Подключим к нему файл с конфигурацией парсера и добавим фильтр parser.

    [SERVICE]    Parsers_File /fluent-bit/parsers/parsers.conf[FILTER]    Name parser    # для сообщений с тегом nginx.access    Match nginx.access    # парсить поле log    Key_Name log    # при помощи nginx_parser    Parser nginx_parser
    
  3. Теперь необходимо добавить файл parsers.conf в контейнер, сделаем это путем добавления еще одного volume к сервису fluent-bit:

    version: "3.8"services:  web:    ...  fluent-bit:    ...    volumes:      - ./fluent-bit/fluent-bit.conf:/fluent-bit/etc/fluent-bit.conf
    
  4. Перезапустим приложение, сгенерируем лог-сообщение запроса. Теперь оно имеет следующую структуру:

    [  1616493566.000000000,  {    "remote_address"=>"172.29.0.1",    "method"=>"GET",    "path"=>"/",    "status"=>200,    "http_user_agent"=>"curl/7.64.1"  }]
    

Сохранение лог-сообщений в elasticsearch

Теперь организуем отправку лог-сообщений на хранения в elasticsearch.

  1. Добавим два выходных интерфейса в конфигурацию fluent-bit, один для лог-сообщений запросов, другой для лог-сообщений ошибок. Для этого воспользуемся плагиномes.

    [OUTPUT]    Name  es    Match nginx.logs    Host  elasticsearch    Port  9200    Logstash_Format On    # Использовать префикс nginx-logs для логов ошибок    Logstash_Prefix nginx-logs[OUTPUT]    Name  es    Match nginx.access    Host  elasticsearch    Port  9200    Logstash_Format On    # Использовать префикс nginx-access для логов запросов    Logstash_Prefix nginx-access
    
  2. Добавим в docker-compose.yml сервисы elasticsearch и kibana.

    version: "3.8"services:  web:    ...  fluent-bit:    ...    depends_on:      - elasticsearch  elasticsearch:    container_name: elasticsearch    image: docker.elastic.co/elasticsearch/elasticsearch:7.10.2    environment:      - "discovery.type=single-node"  kibana:    container_name: kibana    image: docker.elastic.co/kibana/kibana:7.10.1    depends_on:      - "elasticsearch"    ports:      - "5601:5601"
    

На текущем этапе структура проекта выглядит следующим образом:

 docker-compose.yml fluent-bit    fluent-bit.conf    parsers.conf nginx     nginx.conf

Финальную версию проекта можно найти в репозитории.

Результаты

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

  • показать только лог-сообщения запросов;

  • показать лог-сообщения запросов с http статусом 404;

  • отображать не все поля лог-сообщения.

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

Всем спасибо! Надеюсь туториал был полезен.

Подробнее..

Перевод Почему античитерское ПО блокирует инструменты разгона?

17.04.2021 20:18:06 | Автор: admin

Кто из нас не пользовался читами в играх? Whosyourdaddy, thereisnospoon, hesoyam помните? Но обращали ли вы внимание, почему, когда игрок пытается разогнать процессор или изменить настройки ПО, срабатывают некоторые программы против читеров вплоть до блокировки? В этой статье, которая будет полезна для читателей, не обладающих глубокими техническими знаниями в области использования ПО для читеров, против читеров, драйверов и того, что с ними связано, попробуем разобраться почему инструменты мониторинга/разгона блокируются античитерским ПО.


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

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

В нашем случае код для переработки берётся с таких сайтов, как kernelmode.info, OSR Online, и других. Особую обеспокоенность вызывают используемые таким программным обеспечением драйверы. Если бы я захотел причинить вред большому количеству людей (отличной мишенью для моей атаки могли бы стать геймеры и компьютерные энтузиасты), я бы в первую очередь использовал драйверы, входящие в состав некоторых программных инструментов, о которых расскажу далее. В статье я пишу только о некоторых драйверах, на самом деле их гораздо больше кодонезависимыми десятки, если не сотни. Драйверы, о которых пойдёт речь, использовались сообществом читеров ранее или используются сейчас. Попытаемся понять, зачем вообще в такое программное обеспечение включаются драйверы.

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

Зачем нужны драйверы?

За последние 510 лет в связи с развитием индустрии профессионального гейминга и повышением технических требований к запуску определённых игр всё большую популярность приобретают инструменты мониторинга оборудования и повышения тактовой частоты процессора. Такие инструменты опрашивают различные компоненты системы, такие как GPU, CPU, датчики температуры и прочее, однако обычному пользователю получить эту информацию не так просто.

Например, чтобы отправить запрос на цифровой датчик температуры для получения данных о температуре процессора, приложение должно выполнить чтение из моделезависимого регистра процессора. Доступ к таким регистрам процессора и внутренним механизмам чтения/записи возможен только с более высоким уровнем привилегий, например ring0 (на этом уровне работают драйверы). Моделезависимый регистр процессора (MSR) это тип регистра, представляющий собой часть набора команд x86. Как следует из названия регистра, на процессорах одной модели имеются одни регистры, на процессорах другой модели другие, что делает их моделезависимыми. Такие регистры используются в первую очередь для хранения специальной информации о платформе и особенностях процессора; они также могут использоваться для мониторинга показателей производительности или значений тепловых датчиков.

Intel приняла решение включить в набор инструкций x86 две инструкции, позволяющие привилегированному ПО (операционной или другой системе) считывать или записывать данные в MSR. Инструкции rdmsr и wrmsr позволяют привилегированной программе-агенту запрашивать или изменять состояние одного из таких регистров. Для процессоров Intel и AMD имеется обширный перечень доступных MSR, которые можно найти в соответствующих SDM/APM. Тут важно отметить, что большая часть информации в таких моделезависимых регистрах не должна меняться никакими задачами не важно, привилегированные они или нет. Но даже при написании драйверов устройств необходимость в этом возникает крайне редко.

Многие драйверы, создаваемые с целью программного мониторинга оборудования, позволяют задаче без привилегий (если под привилегиями понимать привилегии администратора) считывать/записывать произвольные MSR.

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

Клиентское приложение, например десктопное приложение CPUZ, использует функцию WinAPI под названием DeviceIoControl. Говоря простым языком, CPUZ вызывает функцию DeviceIoControl с помощью известного разработчикам управляющего кода ввода/вывода, чтобы выполнить операцию чтения MSR, например, данных накристального цифрового датчика температуры.

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

Но, опять скажете вы, если коды известны только разработчикам, в чём же проблема? Плодотворным начинанием будет реверс-инжинеринг: всё, что нужно сделать злоумышленнику, получить копию драйвера, загрузить её в любой дизассемблер, скажем, в IDA Pro, и проанализировать обработчик IOCTL.

Ниже представлен код IOCTL в драйвере CPUZ, используемый для отправки двух байтов с двух различных портов ввода/вывода, 0xB2 (широковещательный SMI) и 0x84 (выходной порт 4). Вот это уже становится интересно, так как SMI можно заставить использовать порт 0xB2, позволяющий войти в режим управления системой. Не хочу утверждать, что с этой функцией можно натворить дел, просто отмечаю интересную особенность. Порт SMI используется в первую очередь для отладки.

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

Недокументированный драйвер Intel

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

Примечание: под непривилегированным приложением понимается приложение, выполняемое с низким уровнем привилегий, ring-3; между тем, чтобы выполнить запрос DeviceIoControl, требуются права администратора.

Помимо прочего, драйвер предоставляет непосредственный доступ к порту ввода-вывода для записи, а эта операция должна быть доступна только привилегированным приложениям. Доступом к записи вполне можно злоупотребить во вред конечному пользователю. Вредоносная программа-агент может использовать драйвер, чтобы запустить отказ в обслуживании (denial-of-service) посредством записи в порт ввода-вывода, такая запись может использоваться для аппаратного сброса процессора.

В диагностическом инструменте Intel такие операции имеют определённый смысл. Однако драйвер подписан, входит в состав официально поставляемого инструмента, и, если он попадёт в нечистоплотные руки, его можно использовать для причинения вреда в нашем случае игровым приложениям. Возможность чтения и записи в физическую память означает, что злоумышленник может получить доступ к памяти игры в обход традиционных методов доступа, например, без получения доступа к процессу и без использования Windows API для чтения виртуальной памяти. Злоумышленнику, конечно, придётся постараться, но разве когда-нибудь такая мелочь останавливала мотивированного человека?

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

HWMonitor

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

На скриншоте ниже показан другой метод чтения части физической памяти через функцию MmMapIoSpace. Эта функция часто используется злоумышленниками под видом доверенного инструмента для мониторинга оборудования. А как обстоят дела с записью в моделезависимые регистры процессора? Этот инструмент не предполагает запись ни в какие MSR, тем не менее, правильно переработанный код позволяет записывать данные в любой моделезависимый регистр процессора. Ниже приводятся два примера различных блоков IOCTL в HWMonitor.

Отметим, что используемый HWMonitor драйвер это тот же самый драйвер, который использует CPUZ! ПО против читерства, естественно, может просто запретить запуск HWMonitor, но у злоумышленника есть выход он может с таким же успехом воспользоваться драйвером из CPUZ.

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

Возможность доступа к таким регистрам через любой непроверенный интерфейс дает злоумышленникам возможность изменять системные данные, к которым у них ни в коем случае не должно быть доступа. Через эту уязвимость злоумышленники могут обходить защитные механизмы, устанавливаемые третьими сторонами, например ПО против читеров. Такое ПО может фиксировать обратные вызовы, например ExCbSeImageVerificationDriverInfo, что позволяет драйверу получать информацию о загруженном драйвере. При помощи доверенного драйвера злоумышленникам удаётся скрывать свои действия. Античитерское ПО логирует/отмечает/делает дамп довольно большого количество подписанных пользователями драйверов, но всё же считает доверенными некоторые драйверы из состава WHQL или продуктов Intel. К слову, античитерское ПО само использует операцию обратного вызова, чтобы запретить загрузку драйверов, например упакованного драйвера для CPUZ (иногда античитерское ПО не запрещает загрузку драйвера, а просто фиксирует факт его наличия, даже если имя драйвера было изменено).

MSI Afterburner

Теперь вам должно быть понятно, почему загрузка многих таких драйверов блокируется античитерским ПО. Про MSI Afterburner лучше всего почитать в exploit-db. С ним проблемы те же, что и с вышеописанными драйверами, и для сохранения целостности системы и игровых приложений загрузку этого ПО разумно будет запретить.

Справедливости ради следует сказать, что описанные уязвимости уже устранены, но я всего лишь привёл пример того, как неожиданно могут повернуться многие, казалось бы, полезные инструменты. Несмотря на то, что MSI отреагировала соответствующим образом и обновила Afterburner, были обновлены не все инструменты OC/мониторинга.

Заключение

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

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

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

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

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

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

Узнайте, как прокачаться и в других специальностях или освоить их с нуля:

Другие профессии и курсы
Подробнее..

Перевод Что такое модули Terraform и как они работают?

21.04.2021 14:19:32 | Автор: admin

Многие новички пропускают настройку модулей Terraform, чтобы облегчить процесс настройки. По крайней мере, они так думают, что облегчили себе задачу. Рассмотрим что такое модули Terraform и как они работают.


Я предполагаю, что вы уже знаете некоторые основы Terraform и даже пытались использовать его раньше. Если нет, ознакомьтесь с этим обзором Terraform и этим видеоуроком, прежде чем продолжить чтение.


Обратите внимание: я намеренно не использую реальные примеры кода с некоторыми конкретными поставщиками, такими как AWS или Google для простоты понимания.


Модули Terraform


Вы уже пишете модули


Даже если вы не создаете модуль намеренно, если вы используете Terraform, вы уже пишете модуль так называемый корневой модуль.


Любой файл конфигурации Terraform (.tf) в каталоге, даже один, образует модуль.


Что делает модуль?


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


Предположим, у нас есть виртуальный сервер с некоторыми функциями, размещенными в облаке. Какой набор ресурсов может описывать этот сервер? Например:


  • сама виртуальная машина, созданная из некоторого образа


  • подключенное блочное устройство указанного размера для дополнительного хранилища


  • статический общедоступный IP-адрес, сопоставленный с виртуальным сетевым интерфейсом сервера


  • набор правил брандмауэра, который будет прикреплен к серверу


  • другие вещи, такие как другое блочное устройство, дополнительный сетевой интерфейс и т. д.




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


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


Вызвать модуль означает использовать его в файле конфигурации.


Здесь мы создаем 5 экземпляров сервера, используя единый набор конфигураций (в модуле):


module "server" {    count         = 5    source        = "./module_server"    some_variable = some_value}

Terraform поддерживает "счетчик" для модулей, начиная с версии 0.13.


Организация модуля: дочерний и корневой


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


  • сеть, подобная виртуальному частному облаку (VPC)


  • хостинг статического контента (т.е. bucket)


  • балансировщик нагрузки и связанные с ним ресурсы


  • конфигурация журналирования


  • или что-то еще, что вы считаете отдельным логическим компонентом инфраструктуры



Допустим, у нас есть два разных модуля: серверный модуль и сетевой модуль. В модуле под названием сеть мы определяем и настраиваем нашу виртуальную сеть и размещаем в ней серверы:


module "server" {    source        = "./module_server"    some_variable = some_value}module "network" {      source              = "./module_network"    some_other_variable = some_other_value}

Два разных дочерних модуля, вызываемые в корневом модуле


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



Дочерний модуль может быть получен из нескольких мест:


  • локальные пути


  • официальный реестр Terraform если вы знакомы с другими реестрами, такими как реестр Docker, то вы уже понимаете идею


  • репозиторий Git (пользовательский или GitHub/BitBucket)


  • HTTP URL-адрес архива .zip с модулем



Но как передать сведения о ресурсах между модулями?


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


Вот тут и появляется инкапсуляция.


Инкапсуляция модуля


Инкапсуляция в Terraform состоит из двух основных концепций: области модуля и явного доступа к ресурсам.


Scope (область видимости) модуля


Все экземпляры ресурсов, имена и, следовательно, видимость ресурсов изолированы в области действия модуля. Например, модуль A по умолчанию не видит и не знает о ресурсах в модуле B.


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


module.server[0].resource_type.resource_namemodule.server[1].resource_type.resource_namemodule.server[2].resource_type.resource_name...

Адреса ресурсов модуля, созданные с помощью мета-аргумента count


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


module "server-alpha" {        source        = "./module_server"    some_variable = some_value}module "server-beta" {    source        = "./module_server"    some_variable = some_value}

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


В этом случае имя или адрес ресурсов будет следующим:


module.server-alpha.resource_type.resource_namemodule.server-beta.resource_type.resource_name

Явное раскрытие ресурсов


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


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



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


Модуль сервер должен объявить variable, которая будет использоваться позже в качестве входных данных:



Имена output и variable могут отличаться, но для ясности я предлагаю использовать одни и те же имена.


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


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


network_id = module.network.network_id

Обратите внимание на выходной адрес 'network_id' здесь мы указываем, где он находится


Вот как будет выглядеть окончательный код для вызова наших дочерних модулей:


module "server" {    count         = 5    source        = "./module_server"    some_variable = some_value    network_id    = module.network.network_id}module "network" {      source              = "./module_network"    some_other_variable = some_other_value}

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


Подведение итогов


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


Если вы находитесь в самом начале пути к Terraform, вот несколько советов по дальнейшим действиям.


Я рекомендую вам ознакомиться с этим коротким руководством от HashiCorp, создателя Terraform, о модулях: "Organize Configuration".


Кроме того, есть отличное комплексное учебное пособие, охватывающее все, от новичка до продвинутых понятий о Terraform: "Study Guide Terraform Associate Certification".


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


Если вам понравилась статья, подпишитесь на меня в Twitter (@vasylenko), где я иногда делюсь своими выводами и советами по Terraform, AWS, Ansible и другим технологиям, связанным с DevOps.

Подробнее..

Альтернативное собеседование на позицию разработчика ПО

07.04.2021 12:05:29 | Автор: admin

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

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

Предлагалось сделать следующее:

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

  2. Провести code-review, указать на подозрительные и плохие места и предложить, как можно их улучшить или переделать. Можно задавать любые вопросы и гуглить все что угодно.

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

class SomeServiceClient{ public:  SomeServiceClient();  virtual ~SomeServiceClient();  bool CallAsync(const std::string& uri,                 const std::string& param,                 const misc::BusServiceClient::ResponseCB& callback);  bool CallSync(const std::string& uri,                const std::string& param,                const misc::BusServiceClient::ResponseCB& callback); private:  misc::BusServiceClient ss_client_;  static const int kSleepMs = 100;  static const int kSleepCountMax = 50;};class SpecificUrlFetcher : public UrlFetcher { public:  SpecificUrlFetcher();  virtual ~SpecificUrlFetcher();  SomeData FetchData(const URL& url, const UrlFetcher::ResponseCB& callback); private:  bool SsResponse_returnValue{false};  char SsResponse_url[1024];  void SsResponseCallback(const std::string& response);  SomeServiceClient* ss_client_;};...static const char ss_getlocalfile_uri[] =    "bus://url_replace_service";namespace net {pthread_mutex_t g_url_change_callback_lock = PTHREAD_MUTEX_INITIALIZER;SomeBusServiceClient::SomeBusServiceClient()    : ss_client_(misc::BusServiceClient::PrivateBus) {}SomeBusServiceClient::~SomeBusServiceClient() {}bool SomeBusServiceClient::CallAsync(    const std::string& uri,    const std::string& param,    const misc::BusServiceClient::ResponseCB& callback) {  bool bRet;  bRet = ss_client_.callASync(uri, param, callback);  return bRet;}bool SomeBusServiceClient::CallSync(    const std::string& uri,    const std::string& param,    const misc::BusServiceClient::ResponseCB& callback) {  boold bRet  bRet = false;  int counter;  pthread_mutex_lock(&g_url_change_callback_lock);   ss_client_.callASync(uri, param, callback);  counter = 0;  for (;;) {    int r = pthread_mutex_trylock(&g_url_change_callback_lock);    if (r == 0) {      bRet = true;      pthread_mutex_unlock(&g_url_change_callback_lock);    } else if (r == EBUSY) {      usleep(kSleepMs);      counter++;      if (counter >= kSleepCountMax) {        pthread_mutex_unlock(&g_url_change_callback_lock);        break;      } else        continue;    }    break;  }  return bRet;}/**************************************************************************/SpecificUrlFetcher::SpecificUrlFetcher() {}SpecificUrlFetcher::~SpecificUrlFetcher() {}void SpecificUrlFetcher::SsResponseCallback(const std::string& response) {  std::unique_ptr<lib::Value> value(lib::JSONReader::Read(response));  if (!value.get() || !value->is_dict()) {    pthread_mutex_unlock(&g_url_change_callback_lock);    return;  }  lib::DictionaryValue* response_data =      static_cast<lib::DictionaryValue*>(value.get());  bool returnValue;  if (!response_data->GetBoolean("returnValue", &returnValue) || !returnValue) {    pthread_mutex_unlock(&g_url_change_callback_lock);    return;  }  std::string url;  if (!response_data->GetString("url", &url)) {    pthread_mutex_unlock(&g_url_change_callback_lock);    return;  }  SsResponse_returnValue = true;  size_t array_sz = arraysize(SsResponse_url);  strncpy(SsResponse_url, url.c_str(), array_sz);  SsResponse_url[array_sz - 1] = 0;  pthread_mutex_unlock(&g_url_change_callback_lock);}SomeData SpecificUrlFetcher::FetchData(const URL& url, const UrlFetcher::ResponseCB& callback) {lib::DictionaryValue dictionary;std::string ss_request_payload;misc::BusServiceClient::ResponseCB response_cb =lib::Bind(&SpecificUrlFetcher::SsResponseCallback, this);SomeBusServiceClient* ss_client_ =new SomeBusServiceClient();dictionary.SetString("url", url.to_string());lib::JSONWriter::Write(dictionary, &ss_request_payload);SsResponse_returnValue = false;SsResponse_url[0] = 0x00;ss_client_->CallSync(ss_getlocalfile_uri, ss_request_payload, response_cb);URL new_url;if (SsResponse_returnValue) {  new_url = URL::from_string(SsResponse_url);}delete ss_client_;return UrlFetcher::FetchData(new_url, callback);}}  // namespace net

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

Итак, ответы.
  1. У нас есть какой-то класс UrlFetcher, задача которого, судя по всему -- получать какие-то данные по какому-то URL'у. Унаследованный у него класс делает то же самое, только перед запросом обращается по какой-то шине сообщений к какому-то внешнему сервису, отправляя ему запрошенный URL, и вместо него получает от этого сервиса некий другой URL, который и используется дальше. Этакий паттерн Decorator.

  2. Сначала по мелочам:

    1. ss_getlocalfile_uri - глобальная переменная. Зачем? Можно было объявить ее внутри одного из классов.

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

    3. Странный стиль именования переменных и полей, например SsResponse_returnValue Далее по-серьезнее:

    4. Используется pthread-функции, при том что есть стандартные std::thread, которых в данном случае более чем достаточно.

    5. Используются Си-строки с методами типа strncpy(); по факту тут можно использовать std::string без каких-либо проблем.

    6. ss_client_ хранится в сыром указателе и удаляется вручную. Лучше использовать std::unique_ptr.

    7. Вместо usleep() лучше все-таки использовать std::this_thread::sleep()

    Еще серьезнее:

    8. В цикле в SomeBusServiceClient::CallSync если колбэк с ответом придет менее чем за kSleepMs до kSleepCountMax, то мы откинем ответ и не выполним задачу. Это плохо.

    А теперь еще серьезнее:

    9. Мы отправляем асинхронный запрос в message bus и ждем. Отправленный запрос по истечении таймаута не отменяется. Неизвестно, как работает этот message bus, но если вдруг у класса работы с ним есть какой-то таймаут по умолчанию, то стоит использовать его как kSleepCountMax*kSleepMs, а если ничего такого нет, то нужно как-то отменять уже отправленный запрос когда он нам стал не нужен (возможно callASync возвращает какой-нибудь id запроса?). Потому что если вдруг по какой-то причине ответ придет сильно позже, когда мы уже не ждем, а начали получать следущий URL, то случится полный бардак.

    9. В функции FetchData нет проверки на ошибку, new_url в любом случае передается в метод базового класса, даже если он пустой.

    10. Метод FetchUrl, судя по сигнатуре, изначально асинхронный. В наследуемом классе же по факту из асинхронного метода делается синхронный, потом блокируется до получения ответа, а уже потом вызывает действительно асинхронный метод родительского класса -- WTF? Почему нельзя было сразу сделать все асинхронно?

    11. Судя по логике работы (вызов FetchUrl синхронный и блокирует тред), SsResponseCallback должен выполниться в другом треде. При этом получается, что мы разблокируем мьютекст не в том потоке, где мы его блокировали. Для pthread это явный undefined behavior.

Ответы и замечания от кандидата позволяли составить представление о его уровне владения современными стандартами C++ и хорошими практиками, понимании асинхронности и многопоточности, дотошности на ревью и умении "отлаживать" код в голове. Ну и задать темы для дальнейшего разговора по душам.

Подробнее..

Rust сохраняем безразмерные типы в статической памяти

11.06.2021 14:22:51 | Автор: admin

Не так давно в качестве хобби я решил погрузиться в изучение embedded разработки на Rust и через какое-то время мне захотелось сделать себе логгер, который бы просто писал логи через UART, но при этом не знал какая конкретно реализация используется. Вот тут я быстро осознал, именно в этом конкретном случае я не могу полагаться на статический полиморфизм и мономорфизацию, ведь компилятор не знает сколько нужно памяти выделять под конкретную реализацию. Фактически это означает, что нам нужно как-то уметь сохранять в памяти типы, размер которых неизвестен на этапе компиляции. Такой способностью обладает типBox, но он использует для этого динамическое выделение памяти из кучи. В итоге возникла идея написать свой аналог данного типа, но хранящий объект в предоставленном пользователем буфере, а не в глобальной куче.

А почему бы просто не взять какой-нибудьlinked_list_allocatorот Фила, дать ему пару килобайт памяти и воспользоваться обычнымBoxтипом, или даже взять какой-нибудь простейший bump аллокатор, ведь мы хотим использовать его лишь для того, чтобы создать несколько глобальных объектов, но есть множество сценариев, когда куча не используется принципиально? Это и дополнительная зависимость от целогоallocкрейта и дополнительные риски, что использование кучи выйдет за рамки строго детерминированных сценариев, что будет приводить к трудноуловимым ошибкам.

С другой стороны, мы можем просто принимать&'static dyn Traitи таким образом переложить заботу о том, как получить такую ссылку, на конечного пользователя, но чтобы обеспечить потом доступ к этой ссылке, нам необходимо использовать примитивы синхронизации или же воспользоваться unsafe кодом, с другой стороны, конечный пользователь тоже должен воспользоваться ими, чтобы создать такую ссылку. В конечном итоге у нас получается или двойная работа или unsafe в публичном API, что довольно плохо. Да и в целом, Box обладает гораздо более широкой областью применения, например, его можно использовать для организации очереди задач в очередном futures executor.

Что же такое безразмерные типы?

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

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

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

#[repr(C)]pub(crate) union PtrRepr<T: ?Sized> {    pub(crate) const_ptr: *const T,    pub(crate) mut_ptr: *mut T,    pub(crate) components: PtrComponents<T>,}#[repr(C)]pub(crate) struct PtrComponents<T: ?Sized> {    pub(crate) data_address: *const (),    pub(crate) metadata: <T as Pointee>::Metadata,}

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

pub struct DynMetadata<Dyn: ?Sized> {    vtable_ptr: &'static VTable,    phantom: crate::marker::PhantomData<Dyn>,}/// The common prefix of all vtables. It is followed by function pointers for trait methods.////// Private implementation detail of DynMetadata::size_of etc.#[repr(C)]struct VTable {    drop_in_place: fn(*mut ()),    size_of: usize,    align_of: usize,}

Таким образом, в текущей реализации, размер&dyn Displayна x86_64 составляет 16 байт, а когда мы пишем такой вот код:

let a: u64 = 42;let dyn_a: &dyn Display = &a;

Компилятор генерирует объектVTableи сохраняет его где-то в статической памяти, а обычную ссылку заменяет на широкую, содержащую кроме адреса еще и указатель на таблицу виртуальных функций. Ссылка на таблицу виртуальных функций статическая и не зависит от места расположения значения, таким образом, для того, чтобы создать желаемыйBox<dyn Display>из искомого значенияa, нам необходимо извлечь метаданные из ссылки наdyn_aи все это вместе скопировать в заранее приготовленный для этого буфер. Чтобы все это сделать, нам необходимо использовать nightly features:unsizeиptr_metadata.

Для получения&dyn Tиз&Valueиспользуется специальный маркерный трейтUnsize, который выражает отношение междуSizedтипом и его безразмерным альтер-эго. То есть,TэтоUnsize<dyn Trait>в том случае, еслиTреализуетTrait.

А чтобы работать с метаданными указателя используется функцияcore::ptr::metadataи типажPointee, который связывает тип указателя и тип его метаданных, в случае с безразмерными типами метаданные имеют типDynMetadata<T>, гдеTэто искомый безразмерный тип.

#[inline]fn meta_offset_layout<T, Value>(value: &Value) -> (DynMetadata<T>, Layout, usize)where    T: ?Sized + Pointee<Metadata = DynMetadata<T>>,    Value: Unsize<T> + ?Sized,{    // Get dynamic metadata for the given value.    let meta = ptr::metadata(value as &T);    // Compute memory layout to store the value + its metadata.    let meta_layout = Layout::for_value(&meta);    let value_layout = Layout::for_value(value);    let (layout, offset) = meta_layout.extend(value_layout).unwrap();    (meta, layout, offset)}

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

Обратите внимание, что мы беремLayoutот ссылки на метаданные, а неDynMetadata<Dyn>::layout, последний описывает размещениеVTable, но нас интересует размещение самогоDynMetadata, будьте внимательны!

Пишем свой Box

Вот, теперь у нас есть все необходимое, чтобы написать нашBox, его код довольно простой:

Конструктор, который копирует данные и метаданные в предоставленный буфер, используя довольно удобное API указателя.

impl<T, M> Box<T, M>where    T: ?Sized + Pointee<Metadata = DynMetadata<T>>,    M: AsRef<[u8]> + AsMut<[u8]>,{    pub fn new_in_buf<Value>(mut mem: M, value: Value) -> Self    where        Value: Unsize<T>,    {        let (meta, layout, offset) = meta_offset_layout(&value);        // Check that the provided buffer has sufficient capacity to store the given value.        assert!(layout.size() > 0);        assert!(layout.size() <= mem.as_ref().len());        unsafe {            let ptr = NonNull::new(mem.as_mut().as_mut_ptr()).unwrap();            // Store dynamic metadata at the beginning of the given memory buffer.            ptr.cast::<DynMetadata<T>>().as_ptr().write(meta);            // Store the value in the remainder of the memory buffer.            ptr.cast::<u8>()                .as_ptr()                .add(offset)                .cast::<Value>()                .write(value);            Self {                mem,                phantom: PhantomData,            }        }    }}

А вот и код, который собирает байты назад в&dyn Trait:

    #[inline]    fn meta(&self) -> DynMetadata<T> {        unsafe { *self.mem.as_ref().as_ptr().cast() }    }    #[inline]    fn layout_meta(&self) -> (Layout, usize, DynMetadata<T>) {        let meta = self.meta();        let (layout, offset) = Layout::for_value(&meta).extend(meta.layout()).unwrap();        (layout, offset, meta)    }    #[inline]    fn value_ptr(&self) -> *const T {        let (_, offset, meta) = self.layout_meta();        unsafe {            let ptr = self.mem.as_ref().as_ptr().add(offset).cast::<()>();            ptr::from_raw_parts(ptr, meta)        }    }    #[inline]    fn value_mut_ptr(&mut self) -> *mut T {        let (_, offset, meta) = self.layout_meta();        unsafe {            let ptr = self.mem.as_mut().as_mut_ptr().add(offset).cast::<()>();            ptr::from_raw_parts_mut(ptr, meta)        }    }

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

impl<T, M> Deref for Box<T, M>where    T: ?Sized + Pointee<Metadata = DynMetadata<T>>,    M: AsRef<[u8]> + AsMut<[u8]>,{    type Target = T;    #[inline]    fn deref(&self) -> &T {        self.as_ref()    }}impl<T, M> DerefMut for Box<T, M>where    T: ?Sized + Pointee<Metadata = DynMetadata<T>>,    M: AsRef<[u8]> + AsMut<[u8]>,{    #[inline]    fn deref_mut(&mut self) -> &mut T {        self.as_mut()    }}
running 8 teststest tests::test_box_dyn_fn ... oktest tests::test_box_nested_dyn_fn ... oktest tests::test_box_in_provided_memory ... oktest tests::test_box_trait_object ... oktest tests::test_box_move ... oktest tests::test_drop ... oktest tests::test_layout_of_dyn ... oktest tests::test_box_insufficient_memory ... ok

Miri

Казалось бы, все замечательно, можно использовать библиотеку в боевом коде... Но, постойте, мы же написали unsafe код, как мы вообще можем быть уверены в том, что нигде не нарушили никакие инварианты? К счастью, существует такой проект, как Miri, который интерпретирует промежуточное представление MIR, генерируемое компилятором rustc, используя специальную виртуальную машину. Таким образом, можно находить очень многие ошибки в unsafe коде, подробнее об этом можно почитать в этойстатье. Давайте попробуем запустить наши тесты используя Miri.

cargo miri test   Compiling static-box v0.0.1 (/home/aleksey/Projects/opensource/static-box)    Finished test [unoptimized + debuginfo] target(s) in 0.40s     Running unittests (target/x86_64-unknown-linux-gnu/debug/deps/static_box-e2c02215f3157959)running 8 teststest tests::test_box_dyn_fn ... error: Undefined Behavior: accessing memory with alignment 1, but alignment 8 is required   --> /home/aleksey/.rustup/toolchains/nightly-2021-04-25-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/ptr/mod.rs:886:9    |886 |         copy_nonoverlapping(&src as *const T, dst, 1);    |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ accessing memory with alignment 1, but alignment 8 is required    |    = help: this indicates a bug in the program: it performed an invalid operation, and caused Undefined Behavior    = help: see https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html for further information

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

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

        // Construct a box to move the specified memory into the necessary location.        // SAFETY: This code relies on the fact that this method will be inlined.        let mut new_box = Self {            align_offset: 0,            mem,            phantom: PhantomData,        };        let raw_ptr = new_box.mem.as_mut().as_mut_ptr();        // Compute the offset that needs to be applied to the pointer in order to make        // it aligned correctly.        new_box.align_offset = raw_ptr.align_offset(layout.align());

Вот собственно и все, после этого Miri больше не показывает ошибок выравнивания.

cargo miri test   Compiling static-box v0.1.0 (/home/aleksey/Projects/opensource/static-box)    Finished test [unoptimized + debuginfo] target(s) in 0.30s     Running unittests (target/x86_64-unknown-linux-gnu/debug/deps/static_box-ce23f69c165cf930)running 11 teststest tests::test_box_dyn_fn ... oktest tests::test_box_in_provided_memory ... oktest tests::test_box_in_static_mem ... oktest tests::test_box_in_unaligned_memory ... oktest tests::test_box_insufficient_memory ... oktest tests::test_box_move ... oktest tests::test_box_nested_dyn_fn ... oktest tests::test_box_trait_object ... oktest tests::test_drop ... oktest tests::test_layout_of_dyn_split_at_mut ... oktest tests::test_layout_of_dyn_vec ... oktest result: ok. 11 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out   Doc-tests static-boxrunning 2 teststest src/lib.rs - (line 24) ... oktest src/lib.rs - (line 48) ... oktest result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.15s

Хочу еще сказать несколько слов относительно типаLayout, в нем содержится два поляsize, которое содержит размер памяти в байтах, необходимый для размещения объекта, иalign- это число (причем всегда степень двойки), которому должен быть кратен указатель на объект данного типа. И таким образом, чтобы починить выравнивание, мы просто вычисляем сколько нам нужно прибавить к адресу начала буфера, чтобы получить адрес кратныйalign. Дополнительно довольно доступно написано про выравнивание уу Фила.

Заключение

Ура, теперь мы можем писать вот такой вот код!

use static_box::Box;struct Uart1Rx {    // Implementation details...}impl SerialWrite for Uart1Rx {    fn write(&mut self, _byte: u8) {        // Implementation details    }}let rx = Uart1Rx { /* ... */ };SOME_GLOBAL_WRITER.init_once(move || Box::<dyn SerialWrite, [u8; 32]>::new(rx));// A bit of code later.SOME_GLOBAL_WRITER.lock().unwrap().write_str("Hello world!");

Итак, мы при помощи unsafe и некоторого количества nightly фич смогли написать тип, позволяющий размещать полиморфные объекты на стеке или в статической памяти без использования кучи, что может быть полезным во многих случаях. Хотя, конечно, каждый раз при получении ссылки на объект приходится дополнительно вычислять адрес метаданных и значения, но мы не можем просто так взять и сохранить эти адреса как поля структуры, в этом случае она станет самоссылающиеся, что довольно неприятно в Rust контексте, это не работает с семантикой перемещения. В целом, если воспользоваться pin API, и сделать нашBox неперемещаемым, то можно будет позволить себе эту оптимизацию, а заодно и обеспечить возможность работать с любыми Future типами.

Хочу еще сказать напоследок, что не стоит бояться писать низкоуровневый unsafe код, но стоит 10 раз подумать над его корректностью и обязательно использовать Miri вCIтестах, он отлавливает довольно много ошибок, а разработка низкоуровневого кода требует очень большой внимательности к деталям всевозможным граничным случаям. В конечном счете, именно знания того, как в реальности реализована та или иная языковая абстракция, позволяет перестать воспринимать её как черную магию. Часто все намного проще и очевиднее, чем кажется, стоит просто копнуть чуть поглубже.

А еще важно иногда выходить за рамки stable Rust, чтобы быть в курсе, куда же язык дальше развивается и тем самым расширять свой кругозор.

Ссылка на крейт

Подробнее..

Асинхронная работа с libusb 1.0

09.03.2021 16:05:49 | Автор: admin
Несколько статей назад мы рассмотрели методику работы с USB-устройством при помощи библиотеки libusb. Данные в устройстве у нас формировались по таймеру, поэтому мы были не просто уверены, что рано или поздно они придут к нам, но даже могли предсказать, через какой срок это произойдёт. Однако в анализаторе (который является конечной целью разработки) данные идут непредсказуемо. Будут данные или нет зависит от поведения объекта контроля.

Поэтому, во-первых, было бы полезно видеть, какой их объём уже прошёл в буфер, чтобы представлять, работает система или нет. Ну, и во-вторых, если данных не предвидится, а всё интересное уже попало к нам в память, надо иметь возможность прекратить приём и начать разбор того, что уже накопилось. Ни то, ни другое невозможно при использовании функций, которые были рассмотрены в той статье. По крайней мере, со стороны PC. Без читов, добавленных в прошивку ПЛИС.

Сегодня мы научимся обращаться к библиотеке libusb асинхронным методом. Это позволит и грубо отслеживать объём уже пришедших данных, и прерывать работу в любой момент, и даже повысить общую производительность системы. Причём всё это будет сделано только за счёт вызова штатных функций libusb. Код для FX3 и ПЛИС мы для этого дорабатывать не будем. Итак, приступаем.



Предыдущие статьи цикла:
  1. Начинаем опыты с интерфейсом USB 3.0 через контроллер семейства FX3 фирмы Cypress
  2. Дорабатываем прошивку USB 3.0, используя анализатор SignalTap, встроенный в среду разработки Quartus
  3. Учимся работать с USB-устройством и испытываем систему, сделанную на базе контроллера FX3
  4. Боремся с таймаутами при использовании USB 3.0 через контроллер FX3, возникающими при определенных условиях
  5. Добавляем поддержку Vendor-команд к USB3.0 устройству на базе FX3
  6. Делаем блок SPI to AVALON_MM для USB-устройства на базе FX3

1 Зачем всё это


Напомню, что в предыдущей статье про libusb я читал данные из FX3 так:
int res = libusb_bulk_transfer(tester.m_hUsb,0x81,(uint8_t*)pData,bytesCnt,&actualLength,10000);

И всё. До истечения таймаута (а для анализатора он может быть огромным) текущий поток будет исполняться внутри данной функции. Управления мне никто не вернёт. Мы будем ждать, ждать и ждать, как бедняга на вступительном рисунке, отмеченном крестиком.

В противовес такой работе (её ещё называют синхронной), в любой уважающей себя USB-библиотеке должна быть ещё и асинхронная. Там мы вызываем функцию чтения, и управление немедленно возвращается нам. Что будет дальше, зависит от конкретной библиотеки. Где-то в нужный момент будет переведён в активное состояние объект Событие. В libusb будет вызвана Callback-функция. Короче, нам поступит сообщение о том, что наш запрос выполнен, как на вступительном рисунке с галочкой.

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

Скоро мы рассмотрим, как это работает, но сначала я объясню, почему не стал рассказывать об асинхронной работе немедленно после синхронной. Дело в том, что при проверке асинхронной работы, мы должны провоцировать ситуации с таймаутами. А значит, наш таймер должен не молотить от начала жизни и до бесконечности, а выдавать строго дозированные порции данных. Чтобы это делать, мы должны управлять им. Собственно, в предыдущих двух статьях мы и осваивали этот процесс. Теперь мы вполне можем посылать запросы на шину AVALON_MM. Поэтому, подключив таймер к этой шине, мы сможем управлять и им. Приступаем.

2 Дорабатываем таймер


Да, я обещал, что мы будем работать только средствами libusb. Но этот раздел ничему не противоречит. Он показывает не работу, а доработку средств тестирования. Итак, верилоговский модуль таймера, формирующего воздействия, несколько изменился. В него была добавлена шина AVALON_MM. Из самого счётчика убраны все особенности поведения. Раньше после переполнения приёмника он стартовал не сразу. Теперь всё просто. При старте он стоит. По шине AVALON_MM попросили сделать N тиков перешлёт ровно столько слов, сколько попросили. Переслал снова стоит.

Таким образом, наша программа сможет вырабатывать строго заданное количество 16-разрядных слов, которые уйдут в шину AVALON_ST, дальше в FIFO, а из него в FX3 и USB 3.0.



Под катом получившийся Verilog код таймера.
Смотреть код таймера.
module Timer_ST (  input              clk,  input              reset,  input [2:0]   avalon_mm_address,  input [31:0]  avalon_mm_data_in,  input         avalon_mm_wr,  input  logic       source_ready,  output logic       source_valid,  output logic[15:0] source_data);    logic [31:0] cnt = 0;    logic [31:0] counter = 0;    always @ (posedge clk)    begin         // На том конце очередь переполнена        // Значит, когда она освободится - начнём        // слать данные не сразу...        if (reset == 1)        begin            counter <= 0;            cnt <= 0;        end else        begin            if (avalon_mm_wr)            begin                cnt <= avalon_mm_data_in;               counter <= 0;            end else            begin               counter <= counter + 1;               if ((source_ready==1)&&(cnt != 0))               begin                  cnt <= cnt - 1;               end             end        end    end    assign source_valid = (source_ready!=0) && (cnt != 0);    assign source_data [15:0] = counter [15:0];endmodule


3 Первый эксперимент


3.1 Подготовка


Сегодня мы будем черпать вдохновение на соответствующей странице документации библиотеки libusb.
Чтобы сделать простейший асинхронный запрос, надо проделать следующий путь:
  1. Выделить память для структуры libusb_transfer, для чего имеется функция libusb_alloc_transfer();
  2. Заполнить поля созданной структуры. Причём не надо мучиться с ручным заполнением. В зависимости от типа транзакции мы можем использовать функции libusb_fill_control_setup(), libusb_fill_control_transfer(), libusb_fill_bulk_transfer(), libusb_fill_interrupt_transfer() или libusb_fill_iso_transfer(). Есть ещё заполнение потоковой структуры, но врать не буду, я с нею не разобрался.
  3. На шаге 2 заполняется указатель на функцию обратного вызова. Эту функцию надо написать.
  4. Запустить передачу в работу, вызвав libusb_submit_transfer().

3.2 Работа


Собственно, после вызова функции libusb_submit_transfer() хост начнёт попытку инициировать обмен с устройством. Когда процесс завершится (либо возникнет таймаут или ещё какая нештатная ситуация вплоть до вытаскивания устройства из разъёма), будет вызвана функция обратного вызова.

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



То есть, после отработки libusb_submit_transfer(), Callback-функция, получив управление, под конец своей работы снова вызывает libusb_submit_transfer() и завершается. Через некоторое время она снова получает управление, снова вызывает libusb_submit_transfer() и т.д.

При этом функция libusb_submit_transfer() возвращает управление немедленно, а следующий вызов CallBаck-функции произойдёт через существенный промежуток времени. Таким образом, всё это время основная программа может продолжать своё выполнение. Мало того, CallBack-функция по своей сути похожа на обработчик прерывания микроконтроллера. Она совершенно не изменяет ход работы основной программы. Появились данные функция была вызвана. Функция отработала управление вернулось в то место, где программа была в момент прихода данных. А как эта функция взаимодействует с основной программой всё в руках программиста.

3.3 Очень важный момент


Когда я написал первое тестовое приложение и попробовал его отлаживать, оказалось, что создать описанное выше колесо необходимо, но недостаточно. Надо ещё добавить мотор, который будет его вращать. Для этого надо постоянно вызывать функцию libusb_handle_events(). Причём сама по себе функция вполне себе блокирующая. Правда, если углубиться в документацию, то выяснится, что на самом деле, это всего лишь обёртка. Из неё вызывается libusb_handle_events_timeout() с таймаутом 60 секунд. Так что блокировка будет максимум на минуту. При желании, можно вызывать libusb_handle_events_timeout() с любым своим таймаутом, зависящем от желаемой скорости реакции на возможное прерывание процесса.

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



3.4 Окончание работы


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

3.5 Практический пример


Если честно, то без доработок описанный выше механизм неприемлем для USB3.0. По этой шине данные летят широким потоком, а мы надолго прерываем приём. Пока мы войдём в функцию обратного вызова, пока она отработает, пока запустит приём новых данных Медленно всё это! Поэтому практический пример я покажу не для USB3, а для изохронных передач USB2. Вдохновение я черпал в файле \libusb-1.0.23\examples\sam3u_benchmark.c, который шёл в комплекте с библиотекой. Тот код можно считать эталонным, но я покажу свою перепевку (которая может что-то не учитывать).

Вот такую я сделал функцию обратного вызова. Она не выполняет никаких полезных действий, я просто ставил там точки останова и изучал в отладчике структуры пришедших данных. Ну, и статистику она мне строит. Тест мне был нужен, чтобы убедиться, что я понимаю принципы работы. В конце, как я уже и говорил, вызывается libusb_submit_transfer() для запуска приёма новой посылки. Объёмы изохронного трафика от микрофона (а ловил я именно их) таковы, что заботиться о скорости мне не требовалось. Там буквально килобайты в секунду идут.
Смотреть код.
void LIBUSB_CALL CIsoWriteTest::cb_xfr(struct libusb_transfer *xfr){    CIsoWriteTest* pForm = (CIsoWriteTest*) xfr->user_data;    if (xfr->status == LIBUSB_TRANSFER_COMPLETED)    {        uint minBlockSize = 100000;        uint maxBlockSize = 0;        uint total = 0;        int nBlocks = 0;        for (int i=0;i<xfr->num_iso_packets;i++)        {            if (xfr->iso_packet_desc[i].status == LIBUSB_TRANSFER_COMPLETED)            {                if (xfr->iso_packet_desc[i].actual_length > maxBlockSize)                {                    maxBlockSize = xfr->iso_packet_desc[i].actual_length;                }                if (xfr->iso_packet_desc[i].actual_length < minBlockSize)                {                    minBlockSize = xfr->iso_packet_desc[i].actual_length;                }                nBlocks += 1;                total += xfr->iso_packet_desc[i].actual_length;            }        }        pForm->ui->m_lblSize->setText(QString ("%1 Byttes transfered in %2 blocks").arg(total).arg(nBlocks));        pForm->ui->m_lblMinBlockSize->setText(QString ("Min Block Size = %1 bytes").arg(minBlockSize));        pForm->ui->m_lblMaxBlockSize->setText(QString ("Max Block Size = %1 bytes").arg(maxBlockSize));        pForm->ui->m_lblBytesPerSec->setText("***");    } else    {        pForm->ui->m_lblSize->setText(DecodeTransferStatus(xfr->status));        pForm->ui->m_lblMinBlockSize->setText("***");        pForm->ui->m_lblMaxBlockSize->setText("***");        pForm->ui->m_lblBytesPerSec->setText("***");    }    if (libusb_submit_transfer(xfr) < 0)    {        // todo Catch Errors    }}

Теперь, как запускается процесс. Вот я создаю структуру:
    // Allocate transfer    m_xfr = libusb_alloc_transfer(TEST_NUM_PACKETS);    if (m_xfr == 0)    {        QMessageBox::critical (this,"Error","Cannot Allocate Transfer");        return;    }


Вот я создаю собственный буфер данных (можно его создать и средствами библиотеки, но я предпочитаю выделять память так, чтобы она самоосвободилась в деструкторе класса), после чего заполняю ранее созданную структуру, передав ей указатели на буфер и функцию обратного вызова:
    m_buffer.resize(m_epParams.epMmaxPacketSize * TEST_NUM_PACKETS);    libusb_fill_iso_transfer(m_xfr, m_epParams.hDev, m_epParams.nEndPoint,(unsigned char*) m_buffer.constData(),            m_buffer.size(), TEST_NUM_PACKETS, cb_xfr, this, 1000);    libusb_set_iso_packet_lengths(m_xfr, m_epParams.epMmaxPacketSize);

где
#define TEST_NUM_PACKETS 128

Дальше я запускаю процесс ожидания и приёма посылки:
    int res = libusb_submit_transfer(m_xfr);    if (res != 0)    {        QMessageBox::critical(this,"Error",libusb_error_name(res));    }

И не забываю запустить отдельный поток, который выполняет роль мотора:
    m_thread.start();

сам поток прост, так как я просто изучал принципы работы и совершенно не задумывался о функциональности, так что он совершенно не обрабатывает ошибочные ситуации:
void CMonitorIsoTransactionThread::run(){    while (!isInterruptionRequested())    {        libusb_handle_events(NULL);    }}

4 Пользуемся очередью запросов


Как решить главную проблему, описанную выше (большой временной участок, когда не идёт приём), мне подсказал VelocidadAbsurda в комментариях к этой статье. Надо просто создать несколько структур типа libusb_transfer и запустить много операций обмена. Они встанут в очередь. Как только выполнится одна в игру вступит следующая. Пока мы находимся в обработчике, система не будет простаивать. Во время экспериментов я запускал 16 передач одновременно. Эту работу я рассмотрю уже на реальном примере для целевой шины USB3.

4.1 Работа с буфером


Опишу кратко, какую стратегию работы с буфером я выбрал для данного конкретного проекта. Можно было бы сопоставить каждой структуре libusb_transfer собственный буфер памяти, а по факту прихода данных, копировать их в мой огромный буфер:



Но мой опыт говорит, что при скоростной работе с огромными объёмами данных функции копирования памяти отнимают большой процент времени. Поэтому я пошёл другим путём. Я нарезаю буфер на маленькие кусочки (размером с одну транзакцию) и настраиваю структуру libusb_transfer на приём сразу туда, куда нужно мне. Поэтому дополнительные копирования уже не требуются. Функция обратного вызова просто перенаправляет указатель на очередной участок буфера и снова ставит его в очередь.



4.2 Функция обратного вызова


Снова начнём обсуждение кода с функции обратного вызова. Давайте пока не будем обсуждать ветку LIBUSB_TRANSFER_CANCELLED в ней. Она нас отвлечёт. Разберём основной ход.
Как я уже говорил, эта функция как-то должна взаимодействовать с основной программой. Для этого нужна кучка переменных. Я предпочёл объединить их в единую структуру:
    struct asyncParams    {        // Указатель на большой буфер, в который        // мы принимаем массив. Он может быть размером        // в сотни мегабайт        uint8_t* pData;        // Каждая транзакция принимает маленький кусочек.        // Например, 64 килобайта. В этой переменной лежит        // текущее смещение. Поставили транзакцию в очередь -         // сдвинулись в буфере. Короче, это указатель         // запрошенной части буфера.        int dataOffset;        // Размер буфера в байтах. Полезен, чтобы знать,        // когда следует прекращать работу. Если смещение        // дошло до этой величины - функция обратного вызова        // перестанет создавать новые запросы на передачу        int dataSizeInBytes;        // Размер одной передачи        int transferLen;        // Используется для отображения заполненности буфера,        // увеличивается по факту прихода новой порции данных.        // То есть, это указатель фактически заполненного буфера        int actualTranfered;    };

И функция, пользующаяся этой структурой, выглядит так:
void SpiToAvalonDemo::ReadDataTranfserCallback(libusb_transfer *transfer){    SpiToAvalonDemo* pClass = (SpiToAvalonDemo*) transfer->user_data;    switch (transfer->status )    {    case LIBUSB_TRANSFER_COMPLETED:        // Отметили, что принят очередной блок данных        pClass->m_asyncParams.actualTranfered += transfer->length;        // Если буфер принят ещё не весь        if (pClass->m_asyncParams.dataOffset < pClass->m_asyncParams.dataSizeInBytes)        {            // Новая пачка ляжет вот сюда            transfer->buffer = pClass->m_asyncParams.pData+pClass->m_asyncParams.dataOffset;            // Сдвигаем указатель на следующий блок в буфере            pClass->m_asyncParams.dataOffset += pClass->m_asyncParams.transferLen;            // Запустили передачу            libusb_submit_transfer(transfer);        }        break;    case LIBUSB_TRANSFER_CANCELLED:    {        pClass->m_cancelCnt -= 1;    }        break;    default:        break;    }}

Она простая. Есть буфер, вдоль которого мы скользим, принимая фрагменты потока данных. Например, буфер 120 мегабайт, а фрагменты по 64 килобайта. У нас есть указатель головы буфера. И задача функции просто посмотреть, не достигла ли голова конца буфера. Нет тогда установили новый указатель в структуре libusb_transfer. Прочие её поля не трогаем, они же уже заполнены в основной программе. Затем сдвигаем голову и проворачиваем колесо, вызвав libusb_submit_transfer(). Всё!

4.3 Запуск процесса и моторная часть


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

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

Сначала я заполняю поля структуры, которая используется для связи с функцией обратного вызова:
    m_asyncParams.pData = (uint8_t*) pData;    m_asyncParams.dataOffset = 0;    m_asyncParams.dataSizeInBytes = bytesCnt;    m_asyncParams.transferLen = transferSize;    m_asyncParams.actualTranfered = 0;

Теперь создаём нужное количество структур libusb_transfer, которые указывают на функцию обратного вызова и на участок буфера. При этом не забываем сдвигать указатель на буфер:
    for (int i=0;i<nTransfersInParallel;i++)    {        m_transfers[i] = libusb_alloc_transfer(0);        libusb_fill_bulk_transfer (m_transfers[i],m_tester.m_hUsb,0x81,                                   m_asyncParams.pData+m_asyncParams.dataOffset,transferSize,ReadDataTranfserCallback,                                   this,60000);        m_asyncParams.dataOffset += transferSize;    }

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

Дальше я активирую все передачи, ставя их в очередь (которая существует где-то в недрах библиотеки). Я вынес это в отдельный цикл не столько для наглядности, сколько из-за того, что я измеряю производительность теста при помощи таймера, и здесь он уже тикает. А время инициализации этим таймером не учитывалось.
    QElapsedTimer timer;    timer.start();    // Separated loop for more careful time checking    for (int i=0;i<nTransfersInParallel;i++)    {        libusb_submit_transfer(m_transfers[i]);    }

Теперь возникает следующая ситуация. По мере прихода данных будет проворачиваться рабочее колесо. И функция обратного вызова всё время будет увеличивать поле m_asyncParams.actualTranfered. Когда все данные придут, оно станет равно m_asyncParams.dataSizeInBytes. Поэтому моторная часть у меня выглядит так:
   while (m_asyncParams.actualTranfered<m_asyncParams.dataSizeInBytes)   {      libusb_handle_events(m_tester.m_ctx);   }

Я по-прежнему игнорирую ошибки. Это тестовый пример. А так ошибка возникнет, даже если устройство выдернут из разъёма. Но обработчики ошибок сильно усложнят понимание статьи, так что я просто ещё раз напоминаю тем, кто захочет работать на практике, что мои примеры надо рассматривать творчески.
Дальше я вычисляю и вывожу скорость (код можно посмотреть в полном варианте функции), а затем начинаю освобождать ресурсы.
    for (int i=0;i<nTransfersInParallel;i++)    {        libusb_free_transfer(m_transfers[i]);    }

Собственно, всё!

Для справки, полный текст функции под катом.
bool SpiToAvalonDemo::AsyncStep(uint16_t *pData, const int bytesCnt, const int transferSize, const int nTransfersInParallel){    QElapsedTimer timer;    m_asyncParams.pData = (uint8_t*) pData;    m_asyncParams.dataOffset = 0;    m_asyncParams.dataSizeInBytes = bytesCnt;    m_asyncParams.transferLen = transferSize;    m_asyncParams.actualTranfered = 0;    // Allocate Transfers    for (int i=0;i<nTransfersInParallel;i++)    {        m_transfers[i] = libusb_alloc_transfer(0);        libusb_fill_bulk_transfer (m_transfers[i],m_tester.m_hUsb,0x81,                                   m_asyncParams.pData+m_asyncParams.dataOffset,transferSize,ReadDataTranfserCallback,                                   this,60000);        m_asyncParams.dataOffset += transferSize;    }    timer.start();    // Separated loop for more careful time checking    for (int i=0;i<nTransfersInParallel;i++)    {        libusb_submit_transfer(m_transfers[i]);    }    while (m_asyncParams.actualTranfered<m_asyncParams.dataSizeInBytes)    {        libusb_handle_events(m_tester.m_ctx);    }    quint64 after = timer.nsecsElapsed();    quint64 size = bytesCnt;    size *= 1000000000;    quint64 speed = size/after;    qDebug() << nTransfersInParallel << "," << transferSize << "," << speed;    int from = 0xc001;    uint16_t prevData = pData[from];    for (int i=from+1;i<bytesCnt/2;i++)    {        if (pData[i] != ((prevData + 1)&0xffff))        {            qDebug() << Qt::hex << i << " : " << prevData << ", " << pData[i];        }        prevData = pData[i];    }    // Release Resources    for (int i=0;i<nTransfersInParallel;i++)    {        libusb_free_transfer(m_transfers[i]);    }    return true;}


4.4 Пример вызова тестовой функции


Тестовая функция завязана исключительно на библиотеку libusb. Вся подготовка аппаратуры и выделение буферов вынесены из неё. Давайте рассмотрим один пример, как эта функция вызывается. Так я тестирую зависимость скорости передачи от числа передач, поставленных в очередь.
void SpiToAvalonDemo::on_m_btnAsync128M_clicked(){    static const int len = 128 * 1024 * 1024;    // Set Up Timer to 128 megabytes    QByteArray ar1;    ar1.resize(len);    // Заполнили буфера FX3, иначе подвиснет    m_tester.WriteDword(0,0x10000);    for (int i=1;i<16;i++)    {        // Set Transfer Size for timer        m_tester.WriteDword(0,len/2);        AsyncStep ((uint16_t*)ar1.constData(),len,65536,i);    }}

При прогоне этого теста я получил очень интересный результат. При некоторых прогонах я вижу ошибки в принятых буферах:



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

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



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



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

Однако, когда количество одновременно запрошенных передач близко к шестнадцати, всё более-менее хорошо. Так что повторю, с собой лично договориться о том, что при измерениях не двигать особо мышку, я смогу. Но анализатор и измеряемая программа явно должны быть запущены на разных машинах. Ну, или всё-таки надо будет ставить в ПЛИСовую часть большое буферное ОЗУ

5 Лирическое отступление о размере одной посылки


В далёком 2009-м году участвовал я в разработке программатора ПЗУ на базе FX2LP. Там требовалось выжать из скорости максимум. И вот тогда, после ряда опытов, я выяснил, что хоть физически по шине USB2 бегают пакеты, не больше, чем 512 байт, всё равно надо запрашивать сразу большие объёмы данных. Это связано с тем, что кроме размера пакета, есть ещё разбиение временной оси на кадры. В пределах одного кадра устройство может обменяться более, чем одним пакетом, если это запланировано хостом. Но если запрос пришёл слишком поздно, он может быть вписан в планы на следующую пятилетку следующий кадр. Чтобы избежать этого, надо сразу просить много. Тогда планы будут свёрстаны с учётом наших потребностей.

С тех пор я упоминаю об этом к месту и не к месту. Соответствующий график для USB2, реализованной средствами ПЛИС, я публиковал в своей статье про логические ограничения производительности шин. Сегодня я просто обязан построить такой же график для шины USB3, работающей через FX3.
Графики строил я примерно так.
void SpiToAvalonDemo::on_m_btnAsync128M_clicked(){    static const int len = 128 * 1024 * 1024;    // Set Up Timer to 128 megabytes    QByteArray ar1;    ar1.resize(len);    // Заполнили буфера FX3, иначе подвиснет    m_tester.WriteDword(0,0x10000);    qDebug()<<"";    qDebug()<<"";    qDebug()<<"";//    for (int i=1024;i<512*1024;i*=2)    for (int i=1024;i<32*1024;i+=1024)    {        // Set Transfer Size for timer        m_tester.WriteDword(0,len);        AsyncStep ((uint16_t*)ar1.constData(),len-128*1024,i,1);    }    qDebug()<<"";    qDebug()<<"";    qDebug()<<"";    //    for (int i=1024;i<512*1024;i*=2)    for (int i=1024;i<32*1024;i+=1024)    {        // Set Transfer Size for timer        m_tester.WriteDword(0,len);        AsyncStep ((uint16_t*)ar1.constData(),len-128*1024,i,2);    }    qDebug()<<"";    qDebug()<<"";    qDebug()<<"";    //    for (int i=1024;i<512*1024;i*=2)    for (int i=1024;i<32*1024;i+=1024)    {        // Set Transfer Size for timer        m_tester.WriteDword(0,len);        AsyncStep ((uint16_t*)ar1.constData(),len-128*1024,i,4);    }}


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

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



В общем, не стоит работать блоками меньше, чем 8 килобайт. Правда, я меньше 64 килобайт обычно и не использую.

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



Отдельно хочется отметить, что скорость передачи возросла по сравнению с аналогичной, полученной при синхронной работе. То, что скорость чуть превышает 120 миллионов байт в секунду, не является признаком ошибки измерений. Я отмечал в предыдущих статьях, что по осциллографу видно, что ULPI выдаёт тактовую частоту чуть выше, чем 60 МГц.

6 Как я провоцировал остановки шины


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

Для этой функции я впервые вынес моторную функциональность в обособленное место:
void SpiToAvalonDemo::UsbLoop(uint32_t timeIn_ms){    timeval tv;    tv.tv_sec = 0;    tv.tv_usec = 500000;    QElapsedTimer timer;    timer.start();    while (timer.elapsed()<timeIn_ms)    {        libusb_handle_events_timeout(m_tester.m_ctx,&tv);    }}

И ключевой участок тестового кода выглядит так:
    UsbLoop (500);    m_tester.WriteDword(0,len/4);    UsbLoop (500);    m_tester.WriteDword(0,(len/4)-2);    UsbLoop (1500);    m_tester.WriteDword(0,2);

А вся функция так.
void SpiToAvalonDemo::on_m_btnWithLatencyProblem_clicked(){    static const int len = 0x10000;    // Set Up Timer to 128 megabytes    QByteArray ar1;    ar1.resize(0x100000);    m_asyncParams.pData = (uint8_t*)ar1.constData();    m_asyncParams.dataOffset = 0;    m_asyncParams.dataSizeInBytes = len;    m_asyncParams.transferLen = len;    m_asyncParams.actualTranfered = 0;    libusb_transfer* transfer = libusb_alloc_transfer(0);    libusb_fill_bulk_transfer (transfer,m_tester.m_hUsb,0x81,                               m_asyncParams.pData+m_asyncParams.dataOffset,len,ReadDataTranfserCallback,                               this,60000);    m_asyncParams.dataOffset += len;    libusb_submit_transfer(transfer);    UsbLoop (500);    m_tester.WriteDword(0,len/4);    UsbLoop (500);    m_tester.WriteDword(0,(len/4)-2);    UsbLoop (1500);    m_tester.WriteDword(0,2);    while (m_asyncParams.actualTranfered<m_asyncParams.dataSizeInBytes)    {        libusb_handle_events(m_tester.m_ctx);    }    libusb_free_transfer(transfer);}


Как этот тест изменил Verilog код преобразователя AVALON_ST в FX3, я уже рассказывал. Собственно, практически полностью изменил.

7 Отображаем текущее состояние и добавляем остановку процесса


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

7.1 Таймер, отображающий текущий процент и отлавливающий факт успешного завершения работы


Функцию обратного вызова я оставил ту же самую. Но так как сейчас у нас всё должно быть весьма асинхронно, я добавил в класс виртуальную функцию обработки таймера. В Qt это делается именно так. Собственно, функция проста, а её логика нам знакома. Мы анализируем поле, в котором функция обратного вызова сохраняет объём фактически прокачанных данных. Если ещё не 100% отображаем значение и выходим. Если 100% останавливаем процесс.
void SpiToAvalonDemo::timerEvent(QTimerEvent *event){    Q_UNUSED(event)    static int prevPercent = 0;    int percent = (int)(((int64_t)m_asyncParams.actualTranfered * 100LL)/(int64_t)m_asyncParams.dataSizeInBytes);    if (prevPercent != percent)    {        ui->m_progressForCancel->setValue(percent);        prevPercent = percent;    }    if (percent == 100)    {        StopProcess();    }}

7.2 Функция завершения работы


Так как остановка процесса возможна не только автоматически, но ещё и по кнопке Cancel, она вынесена в отдельную функцию, чтобы её могли вызывать обе ветки. Там мы посылаем запрос на остановку моторного потока, дожидаемся фактической остановки, удаляем все структуры libusb_transfer и останавливаем таймер. Всё!
void SpiToAvalonDemo::StopProcess(){    // Всё, больше мотор не нужен! Глушим его!    m_transactionsThread.requestInterruption();    while (m_transactionsThread.isRunning())    {        QThread::msleep(10);    }    // Освободили память    for (int i=0;i<m_dataTranfersInParallel;i++)    {        libusb_free_transfer(m_transfers[i]);    }    ui->m_progressForCancel->setValue(0);    // Таймер, собственно, тоже стал не нужен    killTimer(m_timeId);}

7.3 Моторный поток


Раз уж зашла речь о моторном потоке, то рассмотрим его подробнее. Здесь нужен поток и только поток. Реализуем его, согласно правилам Qt. Каждые 500 миллисекунд (если точнее, то 500 000 микросекунд) библиотека возвращает нам управление, чтобы мы могли проверить, не пора ли завершить работу с устройством. Я по-прежнему не обрабатываю критические ошибки в угоду читаемости:
void CTransactionThread::run(){    timeval tv;    tv.tv_sec = 0;    tv.tv_usec = 500000;    while (!isInterruptionRequested())    {        libusb_handle_events_timeout(m_libusb_ctx,&tv);    }}

7.4 Кнопка Cancel


Кнопка Cancel, согласно теории, должна завершить незавершённые передачи. Звучит красиво, но на практике, мне пришлось посидеть. Я гарантирую, что всё сработает в Windows, но не удивлюсь, если в других ОС поведение будет чуть иным. Дело в том, что в любой момент времени, все передачи имеют состояние Успешно завершена. Идёт ожидание, не идёт Завершена и всё тут. Поэтому я просто пытаюсь загасить все передачи. Если передача действительно завершена, функция вернёт ошибку. Если же она в процессе работы функция отмены завершится успешно. И вот тогда я увеличиваю счётчик активных транзакций на единицу:
    m_cancelCnt = 0;    for (int i=0;i<m_dataTranfersInParallel;i++)    {        int res = libusb_cancel_transfer(m_transfers[i]);        if (res >= 0)        {            m_cancelCnt += 1;        }    }

Этот счётчик не простой. Он атомарно доступный. Я объявил его так:
    QAtomicInt m_cancelCnt;

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

data += 1;

обычно распадается минимум на три ассемблерные команды:
1) загрузка из памяти в регистр,
2) увеличение регистра,
3) выгрузка регистра в память (обычно потому что на PDP-11 можно было обойтись одной ассемблерной командой, но её времена уже прошли).

В теории поток может быть прерван в любой момент. И возможна ситуация, когда в регистр попало некое значение, после чего работа потока была прервана. Другой поток изменил значение в памяти. Управление вернулось. Но у нас в регистре по-прежнему лежит нечто, и наш поток понятия не имеет о том, что произошло в другом. Поэтому мы завершим инкремент старого значения и положим в память его результат. Данные в памяти станут неверными. Вот чтобы этого избежать, и используются атомарные операции. Они гарантированно завершатся перед прерыванием потока. Так как переменная m_cancelCnt модифицируется в функции обратного вызова, правила активации которой нам неизвестны, я и применил атомарный тип библиотеки Qt для этой переменной.


Помните, в функции обратного вызова я предлагал отложить рассмотрение участка на потом? Пришло время сделать это!
    case LIBUSB_TRANSFER_CANCELLED:    {        pClass->m_cancelCnt -= 1;    }

Когда мы запросили отмену транзакции нам всё равно вызовут соответствующую ей Callback-функцию. И, согласно документации на библиотеку, мы должны дождаться фактического завершения всех транзакций. Тут мы уже всего дождались и отмечаем это. А вот как основной поток ждёт того, что все транзакции успешно завершились:
    // Ждём фактической отмены пересылки    while (m_cancelCnt != 0)    {        QThread::msleep(100);    }

Ну, а потом завершаем работу, вызвав уже рассмотренную ранее функцию:
    // Остановили работу.    StopProcess();

Для справки, полный текст функции, обрабатывающей нажатие кнопки Cancel.
void SpiToAvalonDemo::on_m_btnCancel_clicked(){    m_cancelCnt = 0;    for (int i=0;i<m_dataTranfersInParallel;i++)    {        int res = libusb_cancel_transfer(m_transfers[i]);        if (res >= 0)        {            m_cancelCnt += 1;        }    }    // Ждём фактической отмены пересылки    while (m_cancelCnt != 0)    {        QThread::msleep(100);    }    // Остановили работу.    StopProcess();}


7.5 Запускаем процесс


Уффф. С инфраструктурой разобрались. Теперь запуск. Я сделал возможность задавать, сколько слов недосылать таймеру, чтобы можно было провоцировать различные ситуации. Например, висим с одной недокачанной передачей, висим, когда все передачи недокачаны и т.п.



С учётом этого старт работы выглядит так.

Уже известное нам заполнение параметров и выделение буфера:
    m_analyzerData.resize(120*1024*1024);    m_asyncParams.pData = (uint8_t*)m_analyzerData.constData();    m_asyncParams.dataOffset = 0;    m_asyncParams.dataSizeInBytes = m_analyzerData.size()*sizeof(uint16_t);    m_asyncParams.transferLen = 0x20000;    m_asyncParams.actualTranfered = 0;    for (int i=0;i<m_dataTranfersInParallel;i++)    {        m_transfers[i] = libusb_alloc_transfer(0);    }

Уже приевшееся выделение памяти для структур libusb_transfer. Но в нём кое-что поменялось. Я заменил значение таймаута. Давайте сначала посмотрим на код, а потом я порассуждаю про эти таймауты.
    for (int i=0;i<m_dataTranfersInParallel;i++)    {        libusb_fill_bulk_transfer (m_transfers[i],m_tester.m_hUsb,0x81,                                   m_asyncParams.pData+m_asyncParams.dataOffset,m_asyncParams.transferLen,                                   ReadDataTranfserCallback,this,0x7fffffff);        m_asyncParams.dataOffset += m_asyncParams.transferLen;    }

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

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

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

Возвращаемся к коду. Классический запуск передач:
    for (int i=0;i<m_dataTranfersInParallel;i++)    {        libusb_submit_transfer(m_transfers[i]);    }

Запускаем моторный поток. Ради интереса, я задал ему высокий приоритет:
    m_transactionsThread.m_libusb_ctx =  m_tester.m_ctx;    m_transactionsThread.start(QThread::HighestPriority);

Запуск таймера, отображающего процесс:
    m_timeId = startTimer(100);

И только сейчас запуск ПЛИСового таймера, который формирует нам данные. С учётом того, что он нам недошлёт:
    uint64_t left = ui->m_txtWordsLeft->text().toInt(0,16);    m_tester.WriteDword(0,m_analyzerData.size()-left);

Для справки, полная функция.
void SpiToAvalonDemo::on_m_btnCancelTest_clicked(){    m_analyzerData.resize(120*1024*1024);    m_asyncParams.pData = (uint8_t*)m_analyzerData.constData();    m_asyncParams.dataOffset = 0;    m_asyncParams.dataSizeInBytes = m_analyzerData.size()*sizeof(uint16_t);    m_asyncParams.transferLen = 0x20000;    m_asyncParams.actualTranfered = 0;    for (int i=0;i<m_dataTranfersInParallel;i++)    {        m_transfers[i] = libusb_alloc_transfer(0);    }    for (int i=0;i<m_dataTranfersInParallel;i++)    {        libusb_fill_bulk_transfer (m_transfers[i],m_tester.m_hUsb,0x81,                                   m_asyncParams.pData+m_asyncParams.dataOffset,m_asyncParams.transferLen,                                   ReadDataTranfserCallback,this,0x7fffffff);        m_asyncParams.dataOffset += m_asyncParams.transferLen;    }    for (int i=0;i<m_dataTranfersInParallel;i++)    {        libusb_submit_transfer(m_transfers[i]);    }    m_transactionsThread.m_libusb_ctx =  m_tester.m_ctx;    m_transactionsThread.start(QThread::HighestPriority);    m_timeId = startTimer(100);    uint64_t left = ui->m_txtWordsLeft->text().toInt(0,16);    m_tester.WriteDword(0,m_analyzerData.size()-left);}


7.6 Практические опыты


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

Если оставить значение недосланных слов, какое вписано по умолчанию (800 числа здесь шестнадцатеричные), то остановка произойдёт на девяноста девяти процентах. При этом, как я уже говорил, у всех структур libusb_transfer будет состояние завершено. Это можно проверить, поставив точку останова в обработчике таймера.



Но давайте я поставлю точку останова вот сюда:


То же самое текстом.
void SpiToAvalonDemo::on_m_btnCancel_clicked(){    m_cancelCnt = 0;    for (int i=0;i<m_dataTranfersInParallel;i++)    {        int res = libusb_cancel_transfer(m_transfers[i]);        if (res >= 0)        {            m_cancelCnt += 1;        }    }    // Ждём фактической отмены пересылки    while (m_cancelCnt != 0)    {        QThread::msleep(100);    }


и нажму на Cancel. О-па! Одна структура перешла в состояние CANCELLED:



И я проверял, один раз будет вызвана CallBack-функция. Причём часть данных будет отмечена, как переданная:



Так что всё в порядке. Выбранный вариант поведения работает. Но я проверял только в Windows.

А теперь мы недошлём существенно больше. Не 0x800, а 0x800800 слов. Теперь всё остановится на отметке 93%.



И в точке останова получим такую картинку:



Все транзакции были активны, но так и не дождались своего завершения.

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

8 Заключение


В статье было показано, как можно работать с библиотекой libusb 1.0 через асинхронные запросы. Показаны основные моменты, без которых ничего не заработает. Раскрыта работа множеством транзакций, помещённых в очередь.

Показано, что в некоторых случаях, фактическая скорость передачи по шине всё-таки проседает. Поэтому выбранная автором стратегия перекачки больших объёмов данных без сохранения в буферном ОЗУ для ЭВМ общего использования с произвольным пользователем, неприемлема. Либо пользователь должен отдавать себе отчёт и не выполнять опасных действий во время работы, либо должна использоваться ЭВМ, не дающая выполнить эти действия (на ней должна запускаться только программа, обслуживающая оборудование).

Рассмотрен также типовой метод отображения процесса приёма данных и его прерывания в произвольный момент.

Материалы, получившиеся при написании данной статьи, можно скачать тут.

P.S. Собственно, это вся теория, которую я хотел рассказать про работу с USB через FX3. Само собой, это всё только верхушка айсберга, но повторяю вновь и вновь: я не собираюсь становиться гуру в работе с этим контроллером. Я хотел взять типовой пример и сделать мост я сделал это. Всё! Остальное мне пока не нужно.

Но само собой, надо раскрыть интригу сезона: получился анализатор или нет. Получился! Мелочи, которые позволили собрать все прежние наработки в кучу и итоговые файлы, я оформлю в следующей статье. Она и станет заключительной в цикле. Но выйдет она через одну публикацию. А следующей будет выложена статья, где я расскажу не о своих, а о чужих наработках. Побуду корреспондентом в среде разработчиков, делающих сервис All Hardware. Перескажу с их слов, как можно пробрасывать UART из Линукса по сети (правда, примеры я там написал свои, так как все слова предпочитаю перепроверять).

Та статья написана ещё в прошлом году, но было желание пустить её уже после цикла про FX3. Но ситуация изменилась. Компания-разработчик сервиса All-hardware, который предоставляет бесплатный доступ к отладочным платам, объявила конкурс на разработку прошивок для плат, размещенных в сервисе, поэтому сейчас самое лучшее время для публикации статьи про сервис, возможно она будет полезна участникам. Конкурс продлится до 9 апреля. Но лучше не затягивать со стартом, вот она и вклинится вне очереди.
Подробнее..

Новый sd-bus API от systemd

09.04.2021 00:16:01 | Автор: admin

В новом выпуске systemd v221 мы представляем API sd-bus, поставляемый со стабильной версией systemd. sd-bus - это наша минимальная библиотека D-Bus IPC на языке программирования Си, поддерживающая в качестве бэкэндов как классическую D-Bus на основе сокетов, так и kdbus. Библиотека была частью systemd в течение некоторого времени, но использовалась только внутри проекта, поскольку мы хотели свободно вносить изменения в API, не затрагивая внешних пользователей. Однако теперь, начиная с v221, мы уверены, что сделали стабильный API.

В этом посте я предоставляю обзор библиотеки sd-bus, краткое повторение основ D-Bus и его концепций, а также несколько простых примеров того, как писать клиенты и сервисы D-Bus с её помощью.

Что такое D-Bus?

Давайте начнем с быстрого напоминания, что на самом деле представляет собой D-Bus. Это мощная универсальная система IPC для Linux и других операционных систем. Он определяет такие понятия, как шины, объекты, интерфейсы, методы, сигналы, свойства. Она предоставляет вам детальный контроль доступа, богатую систему типов, лёгкое обнаружение, самодиагностику, мониторинг, надежную многоадресную рассылку, запуск служб, передачу файловых дескрипторов и многое другое. Есть привязки для многих языков программирования, которые используются в Linux.

D-Bus является основным компонентом систем Linux более 10 лет. Это, безусловно, наиболее широко распространенная локальная система IPC высокого уровня в Linux. С момента создания systemd - это была система IPC, в которой она предоставляла свои интерфейсы. И даже до systemd это была система IPC, которую Upstart использовал для своих интерфейсов. Она используется GNOME, KDE и множеством системных компонентов.

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

D-Bus в основном используется как локальный IPC поверх сокетов AF_UNIX. Однако протокол также можно использовать поверх TCP/IP. Он изначально не поддерживает шифрование, поэтому использование D-Bus напрямую через TCP обычно не является хорошей идеей. Можно объединить D-Bus с транспортом, таким как ssh, чтобы защитить его. systemd использует это, чтобы сделать многие из своих API доступными удаленно.

Часто задаваемый вопрос о D-Bus: почему он вообще существует, учитывая, что сокеты AF_UNIX и FIFO уже есть в UNIX и долгое время успешно используются. Чтобы ответить на этот вопрос, давайте сравним D-Bus с популярными сегодня веб-технологиями: AF_UNIX/FIFO для D-Bus тоже самое, что TCP для HTTP/REST. В то время, как сокеты AF_UNIX/FIFO только перекладывают необработанные байты между процессами, D-Bus определяет фактическую кодировку сообщений и добавляет такие концепции, как транзакция вызова методов, система объектов, механизмы безопасности, многоадресная передача сообщений и многое другое.

Из нашего более чем 10-летнего опыта работы с D-Bus сегодня мы знаем, что, хотя есть некоторые области, в которых мы можем что-то улучшить (и мы работаем над этим, как с kdbus, так и с sd-bus), в целом это очень хорошо спроектированная система, которая выдержала испытание временем, выдержала хорошо и получила широкое признание. Если бы сегодня мы сели и разработали совершенно новую систему IPC, включающую весь опыт и знания, полученные с помощью D-Bus, я уверен, что результат был бы очень близок к тому, что уже есть в D-Bus.

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

Для чего подходит sd-bus?

Давайте обсудим, для чего написана библиотека sd-bus, как она соотносится с другими библиотеками D-Bus и почему она может стать библиотекой для вашего проекта.

Для языка программирования Си существуют две популярные библиотеки D-Bus: libdbus, поставляемая в эталонной реализации D-Bus, а также GDBus, компонент GLib, низкоуровневой инструментальной библиотеки GNOME.

Из этих двух библиотек libdbus намного старше, так как она была написана во время составления спецификации. Она была написана с упором на то, чтобы быть переносимой и полезной в качестве серверной части для привязок языков более высокого уровня. Обе эти цели требовали, чтобы API был очень универсальным, в результате чего получился относительно сложный в использовании API в котором отсутствуют элементы, которые делают его легким и интересным для использования из Си. Он предоставляет строительные блоки, но в нём мало инструментов, чтобы упростить строительство дома из них. С другой стороны, библиотека подходит для большинства случаев использования (например, она OOM безопасна, что делает ее подходящей для написания системного программного обеспечения самого низкого уровня) и переносима в операционные системы, такие как Windows или более экзотические UNIX.

GDBus - это гораздо более новая реализация. Она была написана после значительного опыта использования оболочки GLib/GObject вокруг libdbus. GDBus реализована с нуля, не имеет общего кода с libdbus. Её дизайн существенно отличается от libdbus, он содержит генераторы кода, чтобы упростить размещение объектов GObject на шине или взаимодействие с объектами D-Bus как с объектами GObject. Она переводит типы данных D-Bus в GVariant, который является мощным форматом сериализации данных GLib. Если вы привыкли к программированию в стиле GLib, тогда вы почувствуете себя как дома, использовать сервисы и клиенты D-Bus с её помощью намного проще, чем с libdbus.

С sd-bus мы теперь предоставляем третью реализацию, не разделяющую кода ни с libdbus, ни с GDBus. Для нас основное внимание было уделено обеспечению своего рода промежуточного звена между libdbus и GDBus: низкоуровневой библиотекой Си, с которой действительно интересно работать, которая имеет достаточно синтаксического сахара, чтобы упростить создание клиентов и сервисов, но, с другой стороны, более низкоуровневой, чем GDBus/GLib/GObject/GVariant. Чтобы использовать её в различных компонентах системного уровня systemd, она должен быть компактной и безопасной для OOM. Еще одним важным моментом, на котором мы хотели сосредоточиться, была поддержка бэкэнда kdbus с самого начала в дополнение к транспорту сокетов исходной спецификации D-Bus (dbus1). Фактически, мы хотели спроектировать библиотеку ближе к семантике kdbus, чем к dbus1, где-то были бы отличия, но при этом чтобы хорошо охватывались оба транспорта. В отличие от libdbus или GDBus, переносимость не является приоритетом для sd-bus, вместо этого мы стараемся максимально использовать платформу Linux и раскрывать конкретные концепции Linux везде, где это выгодно. Наконец, производительность также была проблемой (хотя и второстепенной): ни libdbus, ни GDBus не побьют рекорды скорости. Мы хотели улучшить производительность (пропускную способность и задержку), но для нас важнее простота и правильность. Мы считаем, что результат нашей работы вполне соответствует нашим целям: библиотеку приятно использовать, она поддерживает kdbus и сокеты в качестве серверной части, относительно минимальна, а производительность существенно выше, чем у libdbus и GDBus.

Вот краткие рекомендации, чтобы решить, какой из трех API использовать для вашего проекта на Си:

  • Если вы разрабатываете проект GLib/GObject, GDBus определенно будет вашим лучшим выбором.

  • Если для вас важна переносимость на ядра, отличные от Linux, включая Windows, Mac OS и другие UNIX, используйте либо GDBus (что более или менее означает использование GLib/GObject), либо libdbus (что требует большого количества ручной работы).

  • В противном случае я бы рекомендовал использовать sd-bus.

(Я не рассматриваю здесь C++, речь идет только о простом Си. Но обратите внимание: если вы используете Qt, то QtDBus является предпочтительным API D-Bus, являясь оболочкой для libdbus.)

Введение в концепции D-Bus

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

  • Шина - это то место, где вы ищете услуги IPC. Обычно существует два типа шин: системная шина, одна на систему, на которой располагаются системные службы; и пользовательская шина, одна на каждого пользователя, на которой располагаются пользовательские службы, такие как адресная книга или почтовый клиент. (Первоначально пользовательская шина была на самом деле сеансовой это значит, что вы получаете несколько шин, если входите в систему много раз как один и тот же пользователь, и в большинстве настроек так и остается, но мы работаем над истинной пользовательской шиной, которая существует в единственном экземпляре для каждого пользователя в системе, независимо от того, сколько раз этот пользователь входит в систему.)

  • Сервис - это программа, которая предлагает некоторый IPC API на шине. Служба идентифицируется именем в обратной нотации доменного имени. Таким образом, служба org.freedesktop.NetworkManager на системной шине - это то место, где доступны API-интерфейсы NetworkManager, а org.freedesktop.login1 на системной шине - это место, где доступны API-интерфейсы systemd-logind.

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

  • Путь к объекту - это идентификатор объекта в определенной службе. В некотором смысле это сравнимо с указателем Си, поскольку именно так вы обычно ссылаетесь на объект Си, если пишите объектно-ориентированные программы на Си. Однако указатели Си - это просто адреса памяти, и передача адресов памяти другим процессам не имеет смысла, поскольку они относятся к адресному пространству сервиса и клиент не может получить достап к ним. Таким образом, разработчики D-Bus придумали концепцию пути к объекту, который представляет собой просто строку, которая выглядит как путь в файловой системе. Пример: /org/freedesktop/login1 - это путь к объекту менеджер сервиса org.freedesktop.login1 (который, как мы помним из вышеизложенного, является сервисом systemd-logind). Поскольку пути к объектам структурированы как пути файловой системы, их можно аккуратно упорядочить в виде дерева и получить полное дерево объектов. Например, вы найдете все пользовательские сеансы, которыми управляет systemd-logind в ветке /org/freedesktop/login1/session, к примеру: /org/freedesktop/login1/session/_7, /org/freedesktop/login1/session./_55 и так далее. Как сервисы именуют свои объекты и размещают их в дереве, полностью зависит от их разработчиков.

  • Каждый объект, который определяется путем, имеет один или несколько интерфейсов. Интерфейс - это набор сигналов, методов и свойств (вместе называемых членами), которые связаны друг с другом. Концепция интерфейсов D-Bus на самом деле в значительной степени идентична тому, что вы знаете из языков программирования, таких как Java, которые её поддерживают. Какие интерфейсы реализует объект, определяют разработчики сервиса. Имена интерфейсов имеют обратную нотацию доменных имен, как и имена сервисов. (Да, это, по общему признанию, сбивает с толку, поскольку для простых сервисов довольно часто встречается использование строки имени сервиса также в качестве имени интерфейса.) Тем не менее, несколько интерфейсов стандартизированы, и вы найдете их доступными для многих объектов, реализуемых различными сервисами. В частности, это org.freedesktop.DBus.Introspectable, org.freedesktop.DBus.Peer и org.freedesktop.DBus.Properties.

  • Интерфейс может содержать методы. Слово метод более или менее просто причудливое определение для функции, и этот термин используется почти так же в объектно-ориентированных языках, таких как Java. Наиболее распространенное взаимодействие между узлами D-Bus заключается в том, что один узел вызывает метод на другом узле и получает ответ. Метод D-Bus принимает и возвращает несколько параметров. Параметры передаются безопасным для типов способом, а информация о типе включается в данные интроспекции, которые вы можете запросить у каждого объекта. Обычно имена методов (и других типов членов) следуют синтаксису CamelCase. Например, systemd-logind предоставляет метод ActivateSession в интерфейсе org.freedesktop.login1.Manager, который доступен в объекте /org/freedesktop/login1 сервиса org.freedesktop.login1.

  • Сигнатура описывает набор параметров, которые принимает или возвращает функция (или сигнал, свойство, см. ниже). Это последовательность символов, каждый из которых определяет тип соответствующего параметра. Набор доступных типов довольно мощный. Например, есть более простые типы, такие как s для строки или u для 32-битного целого числа, но также и сложные типы, такие как as для массива строк или a(sb) для массива структур, состоящих из одной строки и одного логического значения. См. Спецификацию D-Bus, где приводится полное описание системы типов. Упомянутый выше метод ActivateSession принимает одну строку в качестве параметра (сигнатура параметра, следовательно, равна s) и ничего не возвращает (сигнатура возврата, следовательно, является пустой строкой). Конечно, сигнатура может быть намного сложнее, другие примеры см. ниже.

  • Сигнал - это еще один тип элемента, определяемый в объектной системе D-Bus. Как и у метода, у него есть сигнатура. Однако они служат разным целям. В то время как в вызове метода один клиент отправляет запрос к одному сервису, и этот сервис возвращает ответ клиенту, сигналы предназначены для общего уведомления узлов. Сервисы отправляют их, когда хотят сообщить одному или нескольким узлам на шине, что что-то произошло или изменилось. В отличие от вызовов методов и их ответов, они обычно транслируются по всей шине. В то время как вызовы/ответы методов используются для дуплексной связи один-к-одному, сигналы обычно используются для симплексной связи один-ко-многим (обратите внимание, что это не является обязательным требованием, их также можно использовать один-к-одному). Пример: systemd-logind передает сигнал SessionNew от своего объекта-менеджера каждый раз, когда пользователь входит в систему, и сигнал SessionRemoved каждый раз, когда пользователь выходит из системы.

  • Свойство - это третий тип элементов, определяемый в объектной системе D-Bus. Это похоже на концепцию свойств, известную в таких языках, как C#. Свойства также имеют сигнатуру. Они представляют собой переменные, предоставляемые объектом, которые могут быть прочитаны или изменены клиентами. Пример: systemd-logind предоставляет свойство Docked с сигнатурой b (логическое значение). Оно отражает, считает ли systemd-logind, что система в настоящее время находится в док-станции (применимо только к ноутбукам).

D-Bus определяет много различных концепций. Конечно, все эти новые концепции могут быть непонятными. Давайте посмотрим на них с другой точки зрения. Я предполагаю, что многие из читателей имеют представление о сегодняшних веб-технологиях, в частности о HTTP и REST. Попробуем сравнить концепцию HTTP-запроса с концепцией вызова метода D-Bus:

  • HTTP-запрос, который вы отправляете в определенной сети. Это может быть Интернет, ваша локальная сеть или корпоративный VPN. В зависимости от того, в какой сети вы отправляете запрос, вы сможете общаться с определённым набором серверов. Это мало чем отличается от шинной концепции D-Bus.

  • Затем в сети вы выбираете конкретный HTTP-сервер для общения. Это примерно сопоставимо с выбором сервиса на конкретной шине.

  • Затем на HTTP-сервере вы запрашиваете конкретный URL-адрес. Часть URL-адреса, определяющая путь (под которой я подразумеваю все после имени хоста сервера, вплоть до последнего /) очень похожа на путь к объекту D-Bus.

  • Файловая часть URL-адреса (под которой я подразумеваю все, что находится после последней косой черты, следующее за путём, который описан выше), определяет фактический вызов, который нужно сделать. В D-Bus это может быть сопоставлено с именем интерфейса и метода.

  • Наконец, параметры HTTP-вызова следуют в пути после знака ?, Они отображаются на сигнатуру вызова D-Bus.

Конечно, сравнение HTTP-запроса с вызовом метода D-Bus похоже на сравнение яблок и апельсинов. Тем не менее, я думаю, что полезно получить представление о том, что чему соответствует.

Из оболочки

Так много о концепциях и стоящей за ними серой теории. Давайте сделаем это увлекательным, давайте посмотрим, как это ощущается в реальной системе.

Некоторое время назад в systemd был включен инструмент busctl, который полезен для изучения и взаимодействия с объектной системой D-Bus. При вызове без параметров он покажет вам список всех узлов, подключенных к системной шине. (Вместо этого используйте --user, чтобы увидеть узлы вашей пользовательской шины):

$ busctlNAME                                       PID PROCESS         USER             CONNECTION    UNIT                      SESSION    DESCRIPTION:1.1                                         1 systemd         root             :1.1          -                         -          -:1.11                                      705 NetworkManager  root             :1.11         NetworkManager.service    -          -:1.14                                      744 gdm             root             :1.14         gdm.service               -          -:1.4                                       708 systemd-logind  root             :1.4          systemd-logind.service    -          -:1.7200                                  17563 busctl          lennart          :1.7200       session-1.scope           1          -[]org.freedesktop.NetworkManager             705 NetworkManager  root             :1.11         NetworkManager.service    -          -org.freedesktop.login1                     708 systemd-logind  root             :1.4          systemd-logind.service    -          -org.freedesktop.systemd1                     1 systemd         root             :1.1          -                         -          -org.gnome.DisplayManager                   744 gdm             root             :1.14         gdm.service               -          -[]

(Я немного сократил вывод, чтобы быть кратким).

Список начинается с узлов, подключенных в данный момент к шине. Они идентифицируются по именам, например ":1.11". В номенклатуре D-Bus они называются уникальными именами. По сути, каждый узел имеет уникальное имя, и они назначаются автоматически, когда узел подключается к шине. Если хотите, они очень похожи на IP-адрес. Вы заметите, что несколько узлов уже подключены, включая сам наш небольшой инструмент busctl, а также ряд системных сервисов. Затем в списке отображаются все текущие сервисы на шине, идентифицируемые по именам сервисов (как обсуждалось выше; чтобы отличить их от уникальных имен, они также называются хорошо известными именами). Во многих отношениях хорошо известные имена похожи на имена хостов DNS, то есть они являются более удобным способом ссылки на узел, но на нижнем уровне они просто сопоставляются с IP-адресом или, в этом сравнении, с уникальным именем. Подобно тому, как вы можете подключиться к хосту в Интернете либо по его имени, либо по его IP-адресу, вы также можете подключиться к узлу шины либо по его уникальному, либо по его общеизвестному имени. (Обратите внимание, что каждый узел может иметь сколько угодно хорошо известных имен, подобно тому, как IP-адрес может иметь несколько имен хостов, ссылающихся на него).

Ладно, это уже круто. Попробуйте сами, на своем локальном компьютере (все, что вам нужно, это современный дистрибутив на основе systemd).

Теперь перейдем к следующему шагу. Посмотрим, какие объекты на самом деле предлагает сервис org.freedesktop.login1:

$ busctl tree org.freedesktop.login1/org/freedesktop/login1  /org/freedesktop/login1/seat   /org/freedesktop/login1/seat/seat0   /org/freedesktop/login1/seat/self  /org/freedesktop/login1/session   /org/freedesktop/login1/session/_31   /org/freedesktop/login1/session/self  /org/freedesktop/login1/user    /org/freedesktop/login1/user/_1000    /org/freedesktop/login1/user/self

Красиво, не правда ли? Что на самом деле еще приятнее и чего не видно в выводе, так это то, что доступно полное автозавершение слов из командной строки: когда вы нажимаете TAB, оболочка автоматически заполняет имена служб за вас. Это замечательный инструмент для исследования объектов D-Bus!

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

$ busctl introspect org.freedesktop.login1 /org/freedesktop/login1/session/_31NAME                                TYPE      SIGNATURE RESULT/VALUE                             FLAGSorg.freedesktop.DBus.Introspectable interface -         -                                        -.Introspect                         method    -         s                                        -org.freedesktop.DBus.Peer           interface -         -                                        -.GetMachineId                       method    -         s                                        -.Ping                               method    -         -                                        -org.freedesktop.DBus.Properties     interface -         -                                        -.Get                                method    ss        v                                        -.GetAll                             method    s         a{sv}                                    -.Set                                method    ssv       -                                        -.PropertiesChanged                  signal    sa{sv}as  -                                        -org.freedesktop.login1.Session      interface -         -                                        -.Activate                           method    -         -                                        -.Kill                               method    si        -                                        -.Lock                               method    -         -                                        -.PauseDeviceComplete                method    uu        -                                        -.ReleaseControl                     method    -         -                                        -.ReleaseDevice                      method    uu        -                                        -.SetIdleHint                        method    b         -                                        -.TakeControl                        method    b         -                                        -.TakeDevice                         method    uu        hb                                       -.Terminate                          method    -         -                                        -.Unlock                             method    -         -                                        -.Active                             property  b         true                                     emits-change.Audit                              property  u         1                                        const.Class                              property  s         "user"                                   const.Desktop                            property  s         ""                                       const.Display                            property  s         ""                                       const.Id                                 property  s         "1"                                      const.IdleHint                           property  b         true                                     emits-change.IdleSinceHint                      property  t         1434494624206001                         emits-change.IdleSinceHintMonotonic             property  t         0                                        emits-change.Leader                             property  u         762                                      const.Name                               property  s         "lennart"                                const.Remote                             property  b         false                                    const.RemoteHost                         property  s         ""                                       const.RemoteUser                         property  s         ""                                       const.Scope                              property  s         "session-1.scope"                        const.Seat                               property  (so)      "seat0" "/org/freedesktop/login1/seat... const.Service                            property  s         "gdm-autologin"                          const.State                              property  s         "active"                                 -.TTY                                property  s         "/dev/tty1"                              const.Timestamp                          property  t         1434494630344367                         const.TimestampMonotonic                 property  t         34814579                                 const.Type                               property  s         "x11"                                    const.User                               property  (uo)      1000 "/org/freedesktop/login1/user/_1... const.VTNr                               property  u         1                                        const.Lock                               signal    -         -                                        -.PauseDevice                        signal    uus       -                                        -.ResumeDevice                       signal    uuh       -                                        -.Unlock                             signal    -         -                                        -

Как и раньше, команда busctl поддерживает автозавершение командной строки, поэтому и имя службы, и путь к объекту легко объединяются в оболочке простым нажатием TAB. Вывод показывает методы, свойства, сигналы одного из объектов сеанса, который в настоящее время доступен через systemd-logind. Есть раздел для каждого интерфейса, который известен объекту. Во втором столбце указывается тип члена. В третьем столбце отображается сигнатура члена. В случае методов, она описывает входные параметры. Четвертый столбец показывает возвращаемые параметры. Для свойств четвертый столбец кодирует их текущее значение.

Пока что мы только исследовали. Теперь сделаем следующий шаг: станем активными - вызовем метод:

# busctl call org.freedesktop.login1 /org/freedesktop/login1/session/_31 org.freedesktop.login1.Session Lock

Я не думаю, что мне нужно больше об этом упоминать, но в любом случае: снова доступно полное автозавершение командной строки. Третий аргумент - это имя интерфейса, четвертый - имя метода, оба могут быть легко заполнены нажатием TAB. В этом случае мы выбрали метод Lock, который активирует блокировку экрана для определенного сеанса. И оп, в тот момент, когда я нажал Enter в этой строке, у меня включилась блокировка экрана (это работает только на оконных менеджерах, которые правильно подключаются к systemd-logind. GNOME работает нормально, и KDE тоже должен работать).

Выбранный нами вызов метода Lock очень прост, поскольку он не принимает никаких параметров и не возвращает их. Конечно, для некоторых вызовов всё может быть сложнее. Вот еще один пример, на этот раз с использованием одного из вызовов на собственной шине systemd для запуска произвольного системного модуля:

# busctl call org.freedesktop.systemd1 /org/freedesktop/systemd1 org.freedesktop.systemd1.Manager StartUnit ss "cups.service" "replace"o "/org/freedesktop/systemd1/job/42684"

Этот вызов принимает две строки в качестве входных параметров, как описано в сигнатуре, которая следует за именем метода (как обычно, автозавершение командной строки помогает вам понять, как ввести параметры правильно). Следующие за сигнатурой два параметра - это просто две передаваемые строки. Таким образом, сигнатура указывает, какие параметры будут дальше. Вызов метода StartUnit systemd принимает имя модуля для запуска в качестве первого параметра и режим, в котором он запускается, в качестве второго. Вызов возвращает значение пути к объекту. Он кодируется так же, как входной параметр: сигнатура (только o для пути к объекту), за которой следует фактическое значение.

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

busctl поддерживает ряд других операций. Например, вы можете использовать его для мониторинга трафика D-Bus по мере его возникновения (включая создание файла .cap для использования с Wireshark!) Или вы можете установить или получить определенные свойства. Тем не менее, этот пост должен быть о sd-bus, а не busctl, поэтому давайте кратко остановимся здесь и позвольте мне направить вас на страницу руководства на случай, если вы хотите узнать больше об этом инструменте.

busctl (как и остальная часть системы) реализован с использованием API sd-bus. Таким образом, он раскрывает многие особенности самой sd-bus. Например, вы можете использовать его для подключения к удаленным или контейнерным шинам. Он поддерживает как kdbus, так и классический D-Bus, и многое другое!

sd-bus

Но хватит! Вернемся к теме, поговорим о самой sd-bus.

Набор API sd-bus в основном содержится в заголовочном файле sd-bus.h.

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

  • Поддерживает как kdbus, так и dbus1 в качестве серверной части.

  • Имеет высокоуровневую поддержку подключения к удаленным шинам по ssh и шинам локальных контейнеров ОС.

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

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

  • Клиент строит эффективное дерево решений, чтобы определить, каким обработчикам доставить входящее сообщение шины.

  • Автоматически переводит ошибки D-Bus в ошибки стиля UNIX и обратно (хотя с потерями), чтобы обеспечить лучшую интеграцию D-Bus в низкоуровневые программы Linux.

  • Мощная, но легкая объектная модель для отображения локальных объектов на шине. При необходимости автоматически генерирует самоанализ.

В настоящее время API полностью не документирован, но мы работаем над завершением набора справочных страниц. Подробности смотрите на всех страницах, начинающихся с sd_bus_.

Вызов метода из Си с помощью sd-bus

Так много о библиотеке в целом. Вот пример подключения к шине и выполнения вызова метода:

#include <stdio.h>#include <stdlib.h>#include <systemd/sd-bus.h>int main(int argc, char *argv[]) {  sd_bus_error error = SD_BUS_ERROR_NULL;  sd_bus_message *m = NULL;  sd_bus *bus = NULL;  const char *path;  int r;  /* Connect to the system bus */  r = sd_bus_open_system(&bus);  if (r < 0) {    fprintf(stderr, "Failed to connect to system bus: %s\n", strerror(-r));    goto finish;  }  /* Issue the method call and store the respons message in m */  r = sd_bus_call_method(bus,                         "org.freedesktop.systemd1",           /* service to contact */                         "/org/freedesktop/systemd1",          /* object path */                         "org.freedesktop.systemd1.Manager",   /* interface name */                         "StartUnit",                          /* method name */                         &error,                               /* object to return error in */                         &m,                                   /* return message on success */                         "ss",                                 /* input signature */                         "cups.service",                       /* first argument */                         "replace");                           /* second argument */  if (r < 0) {    fprintf(stderr, "Failed to issue method call: %s\n", error.message);    goto finish;  }  /* Parse the response message */  r = sd_bus_message_read(m, "o", &path);  if (r < 0) {    fprintf(stderr, "Failed to parse response message: %s\n", strerror(-r));    goto finish;  }  printf("Queued service job as %s.\n", path);finish:  sd_bus_error_free(&error);  sd_bus_message_unref(m);  sd_bus_unref(bus);  return r < 0 ? EXIT_FAILURE : EXIT_SUCCESS;}

Сохраните этот пример как bus-client.c, а затем соберите его с помощью:

$ gcc bus-client.c -o bus-client `pkg-config --cflags --libs libsystemd`

Будет сгенерирован исполняемый файл bus-client, который вы теперь можете запустить. Обязательно запускайте его как root, поскольку доступ к методу StartUnit является привилегированным:

# ./bus-clientQueued service job as /org/freedesktop/systemd1/job/3586.

И это уже наш первый пример. Он показал, как мы вызывали метод на шине. Фактически вызов метода очень близок к инструменту командной строки busctl, который мы использовали ранее. Я надеюсь, что отрывок из кода не требует дополнительных пояснений. Он должен дать вам представление о том, как писать клиентов D-Bus с помощью sd-bus. Для получения дополнительной информации, пожалуйста, просмотрите заголовочный файл, страницу руководства или даже исходники sd-bus.

Реализация сервиса на Си с помощью sd-bus

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

#include <stdio.h>#include <stdlib.h>#include <errno.h>#include <systemd/sd-bus.h>static int method_multiply(sd_bus_message *m, void *userdata, sd_bus_error *ret_error) {  int64_t x, y;  int r;  /* Read the parameters */  r = sd_bus_message_read(m, "xx", &x, &y);  if (r < 0) {    fprintf(stderr, "Failed to parse parameters: %s\n", strerror(-r));    return r;  }  /* Reply with the response */  return sd_bus_reply_method_return(m, "x", x * y);}static int method_divide(sd_bus_message *m, void *userdata, sd_bus_error *ret_error) {  int64_t x, y;  int r;  /* Read the parameters */  r = sd_bus_message_read(m, "xx", &x, &y);  if (r < 0) {    fprintf(stderr, "Failed to parse parameters: %s\n", strerror(-r));    return r;  }  /* Return an error on division by zero */  if (y == 0) {    sd_bus_error_set_const(ret_error, "net.poettering.DivisionByZero", "Sorry, can't allow division by zero.");    return -EINVAL;  }  return sd_bus_reply_method_return(m, "x", x / y);}/* The vtable of our little object, implements the net.poettering.Calculator interface */static const sd_bus_vtable calculator_vtable[] = {  SD_BUS_VTABLE_START(0),  SD_BUS_METHOD("Multiply", "xx", "x", method_multiply, SD_BUS_VTABLE_UNPRIVILEGED),  SD_BUS_METHOD("Divide",   "xx", "x", method_divide,   SD_BUS_VTABLE_UNPRIVILEGED),  SD_BUS_VTABLE_END};int main(int argc, char *argv[]) {  sd_bus_slot *slot = NULL;  sd_bus *bus = NULL;  int r;  /* Connect to the user bus this time */  r = sd_bus_open_user(&bus);  if (r < 0) {    fprintf(stderr, "Failed to connect to system bus: %s\n", strerror(-r));    goto finish;  }  /* Install the object */  r = sd_bus_add_object_vtable(bus,                               &slot,                               "/net/poettering/Calculator",  /* object path */                               "net.poettering.Calculator",   /* interface name */                               calculator_vtable,                               NULL);  if (r < 0) {    fprintf(stderr, "Failed to issue method call: %s\n", strerror(-r));    goto finish;  }  /* Take a well-known service name so that clients can find us */  r = sd_bus_request_name(bus, "net.poettering.Calculator", 0);  if (r < 0) {    fprintf(stderr, "Failed to acquire service name: %s\n", strerror(-r));    goto finish;  }  for (;;) {    /* Process requests */    r = sd_bus_process(bus, NULL);    if (r < 0) {      fprintf(stderr, "Failed to process bus: %s\n", strerror(-r));      goto finish;    }    if (r > 0) /* we processed a request, try to process another one, right-away */      continue;    /* Wait for the next request to process */    r = sd_bus_wait(bus, (uint64_t) -1);    if (r < 0) {      fprintf(stderr, "Failed to wait on bus: %s\n", strerror(-r));      goto finish;    }  }finish:  sd_bus_slot_unref(slot);  sd_bus_unref(bus);  return r < 0 ? EXIT_FAILURE : EXIT_SUCCESS;}

Сохраните этот пример как bus-service.c, а затем соберите его с помощью:

$ gcc bus-service.c -o bus-service `pkg-config --cflags --libs libsystemd`

А теперь запустим:

$ ./bus-service

В другом терминале попробуем с ним поговорить. Обратите внимание, что эта служба теперь находится на пользовательской шине, а не на системной шине, как раньше. Мы делаем это для простоты: доступ к сервисам на системной шине строго контролируется, поэтому непривилегированные клиенты не могут запрашивать привилегированные операции. Однако на пользовательской шине все проще: поскольку только процессы пользователя, владеющего шиной, могут подключиться, дальнейшее применение политики не усложнит этот пример. Поскольку служба находится на пользовательской шине, мы должны передать параметр --user в командной строке busctl. Начнем с просмотра дерева объектов сервиса.

$ busctl --user tree net.poettering.Calculator/net/poettering/Calculator

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

$ busctl --user introspect net.poettering.Calculator /net/poettering/CalculatorNAME                                TYPE      SIGNATURE RESULT/VALUE FLAGSnet.poettering.Calculator           interface -         -            -.Divide                             method    xx        x            -.Multiply                           method    xx        x            -org.freedesktop.DBus.Introspectable interface -         -            -.Introspect                         method    -         s            -org.freedesktop.DBus.Peer           interface -         -            -.GetMachineId                       method    -         s            -.Ping                               method    -         -            -org.freedesktop.DBus.Properties     interface -         -            -.Get                                method    ss        v            -.GetAll                             method    s         a{sv}        -.Set                                method    ssv       -            -.PropertiesChanged                  signal    sa{sv}as  -            -

Как упоминалось выше, библиотека sd-bus автоматически добавила пару универсальных интерфейсов. Но первый интерфейс, который мы видим, на самом деле тот, который мы добавили! Он показывает наши два метода, и оба принимают xx (два 64-битных целых числа со знаком) в качестве входных параметров и возвращают один x. Отлично! Но правильно ли это работает?

$ busctl --user call net.poettering.Calculator /net/poettering/Calculator net.poettering.Calculator Multiply xx 5 7x 35

Вау! Мы передали два целых числа 5 и 7, и служба фактически умножила их для нас и вернула одно целое число 35! Попробуем другой метод:

$ busctl --user call net.poettering.Calculator /net/poettering/Calculator net.poettering.Calculator Divide xx 99 17x 5

Ух ты! Он может даже делать целочисленное деление! Фантастика! Но давайте обманем его делением на ноль:

$ busctl --user call net.poettering.Calculator /net/poettering/Calculator net.poettering.Calculator Divide xx 43 0Sorry, can't allow division by zero.

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

И это действительно всё, что у меня есть на сегодня. Конечно, примеры, которые я показал, короткие, и я не буду вдаваться в подробности того, что именно делает каждая строка. Однако этот пост должен быть кратким введением в D-Bus и sd-bus, и это уже слишком много для него...

Надеюсь, этот пост был вам полезен. Если вы заинтересованы в использовании sd-bus для своих собственных программ, я надеюсь, он поможет вам. Если у вас есть дополнительные вопросы, посмотрите (неполные) страницы руководства и спросите нас в IRC или в списке рассылки systemd. Если вам нужно больше примеров, взгляните на дерево исходных текстов systemd, все многочисленные шинные сервисы systemd широко используют sd-bus.

Подробнее..

Разработчики встраиваемых систем не умеют программировать

02.05.2021 18:15:06 | Автор: admin

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

Редко когда речь заходит об обратной проблеме, имеющей место в куда более узких кругах разработчиков встраиваемых систем, включая системы повышенной отказоустойчивости. Есть основания полагать, что ранний опыт использования MCS51/AVR/PIC оказывается настолько психически травмирующим, что многие страдальцы затем продолжают считать байты на протяжении всей карьеры, даже когда объективных причин для этого не осталось. Это, конечно, не относится к случаям, где жёсткие ценовые ограничения задают потолок ресурсов вычислительной платформы (микроконтроллера). Но это справедливо в случаях, где цена вычислительной платформы в серии незначительна по сравнению со стоимостью изделия в целом и стоимостью разработки и верификации его нетривиального ПО, как это бывает на транспорте и сложной промышленной автоматизации. Именно о последней категории систем этот пост.

Обычно здесь можно встретить упрёк: "Ты чё пёс А MISRA? А стандарты AUTOSAR? Ты, может, и руководства HIC++ не читал? У нас тут серьёзный бизнес, а не эти ваши побрякушки. Кран на голову упадёт, совсем мёртвый будешь." Тут нужно аккуратно осознать, что адекватное проектирование ПО и практики обеспечения функциональной корректности в ответственных системах не взаимоисключающи. Если весь ваш софт проектируется по V-модели, то вы, наверное, в этой заметке узнаете мало нового хотя бы уже потому, что ваша методология содержит пункт под многозначительным названием проектирование архитектуры. Остальных эмбедеров я призываю сесть и подумать над своим поведением.

Не укради

Что, в конечном итоге, говорят нам вышеупомянутые стандарты в кратком изложении? Примерно вот что:

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

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

  • Делай свои намерения явными и избегай неявных предположений. Это касается проверки инвариантов, исключения платформно-зависимых конструкций, исключения UB, unsafe и схожих граблей, заботливо разложенных языком программирования и средой исполнения.

  • Не забывай об асимптотической сложности. Ответственные системы обычно являются системами реального времени. Адептов C++ призывают воздержаться от злоупотреблений RTTI и использования динамической памяти (хотя последнее к реальному времени относят ошибочно, потому что подобающим образом реализованные malloc() и free() выполняются за постоянное время и даже с предсказуемой фрагментацией кучи).

  • Не игнорируй ошибки. Если что-то идёт не так, обрабатывай как следует, а не надейся на лучшее.

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

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

Я имел несчастье ознакомиться с некоторым количеством встраиваемого ПО реального времени, к надёжности которого предъявляются повышенные требования, и в пугающем числе случаев я ощущал, как у меня шевелятся на голове волосы. Меня, например, сегодня уже не удивляет старая байка об ошибках в системе управления Тойоты Приус, или байка чуть поновее про Boeing 737MAX (тот самый самолёт, который проектировали клоуны под руководством обезьян). В нашем новом дивном мире скоро каждая первая система станет программно-определяемой, что (безо всякой иронии) здорово, потому что это открывает путь к решению сложных проблем затратой меньших ресурсов. Но с повальной проблемой качества системоопределяющего ПО нужно что-то делать.

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

  • Класс-бог, отвечающий за всё сущее.

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

  • Utils или helpers, без них никуда.

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

Инфоцыгане

Косвенным образом масла в огонь подливают некоторые поставщики программных инструментов для разработчиков встраиваемого ПО: Mbed, Arduino, и т.п. Их маркетинговые материалы вполне могут заставить начинающего специалиста поверить, что суть этой работы заключается в низкоуровневом управлении железом, потому что именно на этом аспекте диспропорционально фокусируются упомянутые поставщики ПО. Вот у меня на соседнем рабочем столе открыт в CLion проект ПО для одной встраиваемой системы; проект собирается из чуть более чем ста тысяч строк кода. Из этой сотни примерно три тысячи приходятся на драйверы периферии, остальное приходится на бизнес-логику и всякий матан. Моя скромная практика показывает, что за исключением простых устройств сложность целевой бизнес-логики приложения несопоставима с той его частью, что непосредственно работает с железом.

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

Смотри, что я нашёл! Есть крутая новая система, Mbed называется, значит, для эмбедеров. Гляди, как можно быстро прототипы лепить! Клац, клац, и мигалка готова! Вот же, на видео. А ты, Илья, свой алгоритм оптимизации CAN фильтров пилишь уже неделю, не дело это, давай переходить на Mbed.

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

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

Когда один бэкэндер лучше двух эмбедеров

Ранее я публиковал большую обзорную статью о нашем открытом проекте UAVCAN (Uncomplicated Application-level Vehicular Computing And Networking), который позволяет строить распределённые вычислительные системы (жёсткого) реального времени в бортовых сетях поверх Ethernet, CAN FD или RS-4xx. Это фреймворк издатель-подписчик примерно как DDS или ROS, но с упором на предсказуемость, реальное время, верификацию, и с поддержкой baremetal сред.

Для организации распределённого процесса UAVCAN предлагает предметно-ориентированный язык DSDL с помощью которого разработчик может указать типы данных в системе и базовые контракты, и вокруг этого затем соорудить бизнес-логику. Это работает примерно как REST эндпоинты в вебе, XMLRPC, вот это вот всё. Если взять одного обычного бэкендера человека, измученного сервис-ориентированным проектированием и поддержкой сложных распределённых комплексов и объяснить ему суть реального времени, то он в короткие сроки начнёт выдавать хорошие, годные интерфейсы на UAVCAN.

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

Допустим, ответ подопытного является вариацией на тему "измерение воздушной скорости, барометрической высоты и статического давления". Тогда на свет появляются примерно следующие строки DSDL:

# Calibrated airspeeduavcan.time.SynchronizedTimestamp.1.0 timestampuavcan.si.unit.velocity.Scalar.1.0    calibrated_airspeedfloat16                               error_variance
# Pressure altitudeuavcan.time.SynchronizedTimestamp.1.0 timestampuavcan.si.unit.length.Scalar.1.0      pressure_altitudefloat16                               error_variance
# Static pressure & temperatureuavcan.time.SynchronizedTimestamp.1.0 timestampuavcan.si.unit.pressure.Scalar.1.0    static_pressureuavcan.si.unit.temperature.Scalar.1.0 outside_air_temperaturefloat16[3] covariance_urt# The upper-right triangle of the covariance matrix:#   0 -- pascal^2#   1 -- pascal*kelvin#   2 -- kelvin^2

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

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

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

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

uint16 differential_pressure_readinguint16 static_pressure_readinguint16 outside_air_temperature_reading

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

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

Художника каждый может обидеть

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

Коллеги, одумайтесь.

Я вижу, как нашим микроскопом заколачивают ржавые гвозди, и представляю, сколько ещё подобного происходит за пределами моего поля зрения. В прошлом году уровень отчаяния в нашей скромной команде был столь высок, что мы опубликовали наноучебник, где объясняется, как выглядит сетевой сервис здорового человека: UAVCAN Interface Design Guidelines. Это, конечно, капля в море, но в один прекрасный день я всё-таки переведу его на русский язык ради подъёма уровня профессиональной грамотности.

Непонимание основ организации распределённых вычислений затрудняет внедрение новых стандартов на замену устаревших подходов. Наши наработки в рамках стандарта DS-015 (созданного в коллаборации с небезызвестными NXP Semiconductors и Auterion AG) встречают определённое сопротивление ввиду своей непривычности для целевой аудитории, в то время как ключевые принципы, на которых они основаны, известны индустрии информационных технологий уже не одно десятилетие. Этот разрыв должен быть устранён.

Желающие принять участие в движении за архитектурную чистоту и здравый смысл могут причаститься в телеграм-канале uavcan_ru или на форуме forum.uavcan.org.

Подробнее..

NAPI в сетевых драйверах Linux

10.05.2021 20:18:52 | Автор: admin

Привет, Хабр!
Поговорим о драйверах сетевых устройств Linux, механизме NAPI и его изменениях в ядре 5.12.

Сетевая подсистема Linux (рисунок) построена по примеру стека BSD, в ней прием и передача данных на транспортном и сетевом уровнях происходит с помощью интерфейса сокетов. В отличие от unix-сокетов для межпроцессного взаимодействия, TCP/IP сокеты используют для работы сетевой протокол и при создании (sys_socket) принимают параметры домен, тип, локальные и удаленные IP-адрес и порт. Буфер сокета (sk_buff) - фактически, пакет. Связный список экземпляров таких структур составляет очередь сетевого интерфейса (tx_queue, rx_queue).

Упрощенно некоторые важные поля sk_buff:

struct sk_buff {union {struct {    /* Двусвязный список */struct sk_buff*next;struct sk_buff*prev;struct net_device*dev;};struct list_headlist;};struct sock*sk;unsigned intlen,data_len;__u16mac_len,hdr_len;/* Часть NAPI-интерфейса */#if defined(CONFIG_NET_RX_BUSY_POLL) || defined(CONFIG_XPS)union {unsigned intnapi_id;unsigned intsender_cpu;};#endif__u8inner_ipproto;__u16inner_transport_header;__u16inner_network_header;__u16inner_mac_header;__be16protocol;__u16transport_header;__u16network_header;__u16mac_header;sk_buff_data_ttail;sk_buff_data_tend;unsigned char*head,*data;unsigned inttruesize;};

Драйвера отвечают за реализацию канального уровня (разрешение MAC-адресов) и предоставление интерфейса между системными вызовами ядра и сетевой картой. Обработка входящих и исходящих пакетов происходят с помощью функций xmit и rx, от одновременного доступа они защищены спин блокировками, как и обновление статистики stats и изменение параметров передачи. Сам интерфейс определяется структурой net_device, для создания и регистрации вызываются функции alloc_netdev и register_netdev.

Важные поля net_device:

struct net_device {charname[IFNAMSIZ];    // Строка в стиле printfunsigned longmem_end;unsigned longmem_start;unsigned longbase_addr;unsigned longstate;struct list_headdev_list;struct list_headnapi_list;unsigned intflags;unsigned intpriv_flags;const struct net_device_ops *netdev_ops;unsigned shorthard_header_len;unsigned intmtu;struct net_device_statsstats; atomic_long_trx_dropped;atomic_long_ttx_dropped;atomic_long_trx_nohandler;const struct ethtool_ops *ethtool_ops;const struct header_ops *header_ops;unsigned charif_port;unsigned chardma;/* Interface address info. */unsigned charperm_addr[MAX_ADDR_LEN];unsigned short          dev_id;unsigned short          dev_port;spinlock_taddr_list_lock;intirq;unsigned char*dev_addr;struct netdev_rx_queue*_rx;unsigned intnum_rx_queues;struct netdev_queue*_tx ____cacheline_aligned_in_smp;unsigned intnum_tx_queues;struct timer_listwatchdog_timer;intwatchdog_timeo;};

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

Схематичные действия в обработчике прерываний для очистки очереди входящих пакетов: (драйвер intel Ethernet e1000):

static bool e1000_clean_rx_irq(struct e1000_adapter *adapter,  // Сетевое устройство       struct e1000_rx_ring *rx_ring, // Очередь входящих пакетов       int *work_done, int work_to_do){while (rx_desc->status & E1000_RXD_STAT_DD) {struct sk_buff *skb;u8 *data;u8 status;        if (netdev->features & NETIF_F_RXALL) {    total_rx_bytes += (length - 4);     total_rx_packets++;    e1000_receive_skb(adapter, status, rx_desc->special, skb);}     }if (cleaned_count)    // Создание нового буфераadapter->alloc_rx_buf(adapter, rx_ring, cleaned_count);    // Обновление статистикиadapter->total_rx_packets += total_rx_packets;adapter->total_rx_bytes += total_rx_bytes;netdev->stats.rx_bytes += total_rx_bytes;netdev->stats.rx_packets += total_rx_packets;return cleaned;}

До ядер версии 2.3 после самого обработчика прерывания (top half) для выполнения основных задач использовались нижние половины (bottom half) и очереди задач (task queue). Начиная с версии 2.3 на замену интерфейсу BH пришли отложенные прерывания (softirq), тасклеты (tasklet) и очереди отложенных действий (work queue). Преимущество softirq в том, что они могут одновременно выполняться на разных процессорах. Они напрямую используются в сетевой подсистеме.

Немного о NAPI

Пока сетевой трафик был умеренным, механизм прерываний при получении пакета эффективно справлялся со своей задачей. С ростом трафика и появлением высоконагруженных систем постоянная обработка прерываний стала приводить к нехватке процессорного времени для пользовательских программ и потере пакетов. Решение проблемы было предложено в 2001 году и появилось в виде интерфейса New API в ядрах серии 2.4. (В оригинальной статье результаты тестирования для SMP-системы, генератор трафика наподобие pktgen).

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

В NAPI-совместимых драйверах прерывания отключаются, когда на интерфейс приходит пакет. Обработчик в этом случае только вызывает rx_schedule, гарантирующий, что обработка пакетов произойдет в дальнейшем. Когда приходящие пакеты заполняют буфер (предельное количество budget), для обработки вызывается метод dev->poll. Метод poll будет вызываться одновременно не более, чем на одном процессоре, что упрощает синхронизацию. Если нагрузка падает, снова разрешаются прерывания. Это позволяет динамически регулировать производительность в зависимости от нагрузки интерфейса. Метод poll может использоваться также и для передачи пакетов.

Пример poll из драйвера e1000:

static void e1000_netpoll(struct net_device *netdev){struct e1000_adapter *adapter = netdev_priv(netdev);if (disable_hardirq(adapter->pdev->irq))e1000_intr(adapter->pdev->irq, netdev);enable_irq(adapter->pdev->irq);}

При реализации NAPI-совместимого драйвера должны быть выполнены некоторые требования:

  • Возможность хранения входящих пакетов в кольце DMA или буфере в самой карте

  • Возможность отключить прерывания

  • В методе poll должна быть реализована возможность забрать несколько пакетов за раз

  • Так как метод poll работает в контексте softirq и управляется демоном ksoftirqd, в системах с высокой загрузкой нужно менять приоритет поллинга для обеспечения баланса ресурсов между обработчиком прерываний и пользовательскими программами.

Недостатки NAPI:

  • В некоторых случаях в системе могуть быть задержки, если весь обработчик прерываний помещен в dev->poll

  • Маскировка прерываний может быть медленной

  • Возможно состояние IRQ-гонки, если пакет приходит во время проверки бита наличия новых пакетов и включения прерываний.

Что нового у NAPI в 5.12?

В серии патчей в ядре 5.12 метод poll из softirq контекста перенесен в поток ядра.

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

Изменения затронули в основном net/core/dev.c. Обновлен метод __napi_poll, вызываемый из контекста napi_poll. Появился новый sysfs атрибут в net_device для включения/выключения поточного режима опроса для всех экземпляров napi данного сетевого устройства без необходимости вызова up/down.

В napi_struct добавлено поле threaded для реализации опроса внутри потока, причем для включения поддержки потоков после создания kthread нужно вызвать napi_set_threaded (флаг NAPI_STATE_THREADED).

Обновленная структура napi_struct:

struct napi_struct {        struct list_head        dev_list;        struct hlist_node       napi_hash_node;        unsigned int            napi_id;        struct task_struct      *thread; };

Создание потока ядра:

static int napi_kthread_create(struct napi_struct *n){       int err = 0;       /* Create and wake up the kthread once to put it in        * TASK_INTERRUPTIBLE mode to avoid the blocked task        * warning and work with loadavg.        */       n->thread = kthread_run(napi_threaded_poll, n, "napi/%s-%d",                               n->dev->name, n->napi_id);       if (IS_ERR(n->thread)) {               err = PTR_ERR(n->thread);               pr_err("kthread_run failed with err %d\n", err);               n->thread = NULL;       }       return err;}

В связи с добавлением поточности появился новый метод napi_thread_wait.

Wei Wang получил следующие результаты сравнения эффективности softirq, kthread и очередей отложенных действий:

Основные источники - LDD3 и статьи:

NAPI polling in kernel threads
Threadable NAPI polling, softirqs, and proper fixes
Reworking NAPI
Driver porting: Network drivers

Заранее спасибо за уточнения и указания на ошибки!

Подробнее..

Предельная скорость USB на STM32F103, чем она обусловлена?

24.05.2021 14:14:07 | Автор: admin
У данной статьи тяжёлая история. Мне надо было сделать USB-устройства, не выполняющие никакой функции, но работающие на максимальной скорости. Это были бы эталоны для проверки некоторых вещей. HS-устройство я сделал на базе ПЛИС и ULPI, загрузив туда прошивку на базе проекта Daisho. Для FS-устройства, разумеется, была взята голубая пилюля. Скорость получалась смешная. Прямо скажем, черепашья скорость.



Но я же крутой, у меня теперь есть USB-анализатор (его разработка была описана в блоке из нескольких предыдущих статей). Посмотрев логи, я понял, что похоже, знаю, почему скорость невозможно поднять, даже если STM32F103 обслуживает исключительно USB и ничего больше. Даже начал писать о результатах Но потом решил, что кому-то это будет не интересно, а кто-то скажет, что и так это знал.

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

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

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

Подготовка проекта STM32


Создаём проект


Итак. Чтобы все могли повторить мои действия, я скачал самую свежую на момент написания статьи версию 6.2. Правда, они выходят с такой частотой, что на момент, когда всё будет выложено на Хабр, всё может уже измениться. Но так или иначе. Скачиваем, устанавливаем.

Создаём новый проект для STM32F103C8Tx. Добавляем туда USB-Device.



Теперь, когда есть USB, добавляем CDC-устройство. При этом заменим ему VID и PID. Дело в том, что у меня есть inf Файл, который ставит драйвер winusb именно на эту пару идентификаторов. При работе через драйвер usbser скорость была ещё ниже. Я решил исключить всё, что может влиять. Буду замерять скорость без каких-либо прослоек.



Теперь добавляем RCC для работы с тактовыми сигналами:



После всего этого (добавили USB и добавили RCC) можно и тактовые частоты настроить, но сначала спасём себя от самоотключающегося блока отладки. Он спрятан надёжно! Вот так сейчас всё выглядит по умолчанию:



А вот так надо



Прекрасно! Теперь можно настроить тактовые частоты. Я всегда это делаю опытным путём. Системная частота должна стать 72 МГц, а частота USB 48 МГц. Это я в состоянии запомнить. Остальное каждый раз заново вывожу.



Ну всё. Для тестового проекта настроек, вроде, достаточно. Заполняем свойства проекта и сохраняем. Лично я в формате MDK ARM. Он же Кейл. Мне так проще.

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

Донастраиваем проект в среде разработки


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



Дописываем код проекта


Наш проект должен просто принимать данные из USB и И всё! Принимать, принимать, принимать! Не будем тратить время на какую-то обработку этих данных. Просто приняли и забыли, приняли и забыли. Обработчик события данные приняты в типовом CDC проекте живёт здесь:



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

Подготовка проекта под Windows


Вариант честной работы с UART мы опустим. Дело в том, что совсем скоро мы будем искать причины тормозов. А вдруг они вызваны драйвером usbser.sys? Нет. Мы возьмём проверенный временем драйвер winusb и будем работать с ним через библиотеку libusb. Кому нравится Linux сможет работать через эту же библиотеку там. Мы тренировались работать с нею в этой статье. А в этой учились работать с нею в асинхронном режиме.

Сначала я вёл работу через блокирующие функции, так как их написать было проще. Мало того, черновые замеры, которые я делал ещё до начала работы над текстом, были вполне красивые. Это ещё не всё, первая метрика, снятая для статьи, тоже была прекрасна и полностью отражала черновые результаты! Потом что-то случилось. График стал каким-то удивительным, правда, чуть ниже я эту удивительность объясню. При работе с блоками меньше чем 64 байта программа съедала 25% процессорного времени, а именно на блоке 64 байта был излом. Мне казалось, что кто-то обязательно напишет в комментариях, что сделай я всё на асинхронных функциях, всё станет намного лучше. В итоге, я взял и всё переписал на асинхронный вариант. Процент потребления процессорного времени на малых блоках действительно изменился. Теперь программа потребляет 28% вместо двадцати пяти Цифры скоростей же не изменились Но асинхронная работа более правильная сама по себе, так что я покажу именно её. Вся теория уже рассматривалась мною в тех статьях про libusb.

Я завожу всё ту же вспомогательную структуру:
    struct asyncParams    {        uint8_t* pData;        uint32_t dataOffset;        uint32_t dataSizeInBytes;        uint32_t transferLen;        uint32_t actualTranfered;        QElapsedTimer timer;        quint64       timerAfter;    };    asyncParams m_asyncParams;

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

Ну, и указатели на объекты передача имеются, куда же без них:
    static const int m_nTransfers = 32;    libusb_transfer* m_transfers [m_nTransfers]; 

Функция обратного вызова отличается от описанной в предыдущих статьях как раз тем, что она считывает показание таймера, если передавать больше нечего. Это произойдёт не единожды, а для каждой из передач (тридцати двух в случае мелких блоков, если блоки крупные их будет меньше, но всё равно не одна). Но на самом деле, это не страшно. Мы это значение будем анализировать только после последнего вызова этой функции. В остальном там всё то же, что и раньше, так что просто покажу код, не объясняя его. Объяснения все были в предыдущих статьях.
void MainWindow::WriteDataTranfserCallback(libusb_transfer *transfer){    MainWindow* pClass = (MainWindow*) transfer->user_data;    switch (transfer->status )    {    case LIBUSB_TRANSFER_COMPLETED:        pClass->m_asyncParams.actualTranfered += transfer->length;        // Still need transfer data        if (pClass->m_asyncParams.dataOffset < pClass->m_asyncParams.dataSizeInBytes)        {            transfer->buffer = pClass->m_asyncParams.pData+pClass->m_asyncParams.dataOffset;            pClass->m_asyncParams.dataOffset += pClass->m_asyncParams.transferLen;            libusb_submit_transfer(transfer);        } else        {            pClass->m_asyncParams.timerAfter = pClass->m_asyncParams.timer.nsecsElapsed();        }        break;/*    case LIBUSB_TRANSFER_CANCELLED:    {        pClass->m_cancelCnt -= 1;    }*/    default:        break;    }}

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

Ну, а параметр blockSize у функции это я в своих статьях уже набил оскомину высказыванием, что при работе с USB скорость зависит от размера блока. До определённого значения она ниже нормальной. Это связано с тем, что хост посылает пакеты медленнее, чем их может обработать устройство. Поэтому я всегда строю графики и смотрю, где они входят в насыщение. Сегодня я буду делать то же самое. Правда, сегодня график в дополнение к банальному росту, имеет непривычную для меня форму, что и сподвигло меня на переделку программы с блокирующего на асинхронный режим. Итак, функция, измеряющая скорость, выглядит так:
Её текст я скрыл под катом.
quint64 MainWindow::MeasureSpeed2(uint32_t totalSize, uint32_t blockSize, uint32_t avgCnt){    std::vector<qint64> gist;    gist.resize(avgCnt);    QByteArray data;    data.resize(totalSize);    m_asyncParams.dataSizeInBytes = totalSize;    m_asyncParams.transferLen = blockSize;    uint32_t nTranfers = m_nTransfers;    if (totalSize/blockSize < nTranfers)    {        nTranfers = totalSize/blockSize;    }    for (uint32_t i=0;i<avgCnt;i++)    {        m_asyncParams.dataOffset = 0;        m_asyncParams.actualTranfered = 0;        m_asyncParams.pData = (uint8_t*)data.constData();        // Готовим структуры для передач        for (uint32_t i=0;i<nTranfers;i++)        {            m_transfers[i] = libusb_alloc_transfer(0);            libusb_fill_bulk_transfer (m_transfers[i],m_usb.m_hUsb,0x02,//,0x01,                                       m_asyncParams.pData+m_asyncParams.dataOffset,m_asyncParams.transferLen,WriteDataTranfserCallback,                                            // No need use timeout! Let it be as more as possibly                                       this,0x7fffffff);            m_asyncParams.dataOffset += m_asyncParams.transferLen;        }        m_asyncParams.timerAfter = 0;        m_asyncParams.timer.start();        for (uint32_t i=0;i<nTranfers;i++)        {            int res = libusb_submit_transfer(m_transfers[i]);            if (res != 0)            {                qDebug() << libusb_error_name(res);            }        }        timeval tv;        tv.tv_sec = 0;        tv.tv_usec = 500000;        while (m_asyncParams.actualTranfered < totalSize)        {            libusb_handle_events_timeout (m_usb.m_ctx,&tv);        }        quint64 size = totalSize;        size *= 1000000000;        gist [i] = size/m_asyncParams.timerAfter;    }    for (uint32_t i = 0;i<nTranfers;i++)    {        libusb_free_transfer(m_transfers[i]);        m_transfers[i] = 0;    }    qint64 avgSpeed = 0;    for (uint32_t i=0;i<avgCnt;i++)    {        avgSpeed += gist [i];    }    avgSpeed /= avgCnt;    if (avgCnt < 4)    {        return avgSpeed;    }    for (uint32_t i=0;i<avgCnt;i++)    {        if (gist [i] < (avgSpeed * 3)/4)        {            gist [i] = 0;        }        if (gist [i] > (avgSpeed * 5)/4)        {            gist [i] = 0;        }    }    avgSpeed = 0;    int realAvgCnt = 0;    for (uint32_t i=0;i<avgCnt;i++)    {        if (gist[i]!= 0)        {            avgSpeed += gist [i];            realAvgCnt += 1;        }    }    if (realAvgCnt == 0)    {        return 0;    }    return avgSpeed/realAvgCnt;}


Сборку статистики в файл csv я делаю так:
void MainWindow::on_m_btnWriteStatistics_clicked(){    QFile file ("speedMEasure.csv");    if (!file.open(QIODevice::WriteOnly))    {        QMessageBox::critical(this,"Error","Cannot create csv file");        return;    }    QTextStream out (&file);    QApplication::setOverrideCursor(Qt::WaitCursor);    for (int blockSize=0x8;blockSize<=0x20000;blockSize *= 2)    {        quint64 speed = MeasureSpeed(0x100000,blockSize,10);        out << blockSize << "," << speed << Qt::endl;    }    out.flush();    file.close();    QApplication::restoreOverrideCursor();}


Первый результат


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



Где-то после размера блока 4 килобайта, скорость упирается в 560 килобайт в секунду. Давайте я грубо умножу это на 8. Получаю условные 4.5 мегабита в секунду. Условность состоит в том, что на самом деле, там ещё бывают вставные биты, да и на пакеты оверхед имеется. Но всё равно, это отстоит очень далеко от 12 мегабит в секунду, положенных на скорости Full Speed (кстати, именно поэтому на вступительном рисунке стоит знак 120, он символизирует данный теоретический предел).

Почему результат именно такой


Будучи человеком вооружённым (не зря же я всех полгода пытал статьями про изготовление анализатора), я взял и рассмотрел детали трафика. И вот что у меня получилось (данные я чуть обрезал, там реально всегда уходит по 64 байта):


То же самое текстом.
*** +4 usOUT Addr: 16 (0x10), EP: 1E1 90 40 *** +3 usDATA0C3 0D F0 AD BA 0D F0 AD BA 0D F0 AD BA 0D F0 AD BA 0D F0 AD BA 0D F0 AD *** +45 usACKD2 *** +4 usOUT Addr: 16 (0x10), EP: 1E1 90 40 *** +3 usDATA14B 0D F0 AD BA 0D F0 AD BA 0D F0 AD BA 0D F0 AD BA 0D F0 AD BA 0D F0 AD *** +45 usNAK5A *** +4 usOUT Addr: 16 (0x10), EP: 1E1 90 40 *** +3 usDATA14B 0D F0 AD BA 0D F0 AD BA 0D F0 AD BA 0D F0 AD BA 0D F0 AD BA 0D F0 AD*** +45 usACKD2 *** +5 usOUT Addr: 16 (0x10), EP: 1E1 90 40 *** +3 usDATA0C3 0D F0 AD BA 0D F0 AD BA 0D F0 AD BA 0D F0 AD BA 0D F0 AD BA 0D F0 AD*** +45 usNAK5A


И так до бесконечности на всех участках, что я смог осмотреть глазами, возвращается то ACK, то NAK. Причём в режиме FS каждая пачка данных передаётся целиком. Принялась она или нет, а всё равно передаётся целиком. Хорошо, что это не роняет всю шину USB, так как до последнего хаба данные бегут на скорости HS в виде SPLIT транзакции. А дальше уже хаб мелко шинкует её на пакеты по 64 байта и пытается отослать на скорости FS.

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

Причины падения скорости, честно говоря, написаны прямо под носом. А именно в комментариях к функции, где нам следует вставлять обработку данных. Вот их текст:



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

А почему при блоке 64 байта скорость выше? Я долго думал, и нашёл следующее объяснение: 64 байта это тот размер блока, после которого при работе с FS-устройствами через HS-хабы начинается использование SPLIT-транзакций. Что это такое можно посмотреть в стандарте, там этому посвящён не один десяток страниц. Но если коротко: до хаба запрос идёт на скорости HS, а уже хаб обеспечивает снижение скорости и нарезание данных на FS-блоки. Основная шина при этом не тормозит.

Выше мы видели, что уже через 5 микросекунд после прихода примитива ACK, пошёл следующий пакет, который не был обработан контроллером. А что будет, если мы будем работать блоками по 64 байта? Я начну с примитива SOF.
Смотреть код.
*** +1000 usSOF 42.0 (0x2a)A5 2A 50 *** +3 usOUT Addr: 29 (0x1d), EP: 1E1 9D F0 *** +3 usDATA0C3 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 *** +45 usACKD2 *** +37 usOUT Addr: 29 (0x1d), EP: 1E1 9D F0 *** +3 usDATA14B 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00*** +45 usACKD2 *** +43 usOUT Addr: 29 (0x1d), EP: 1E1 9D F0 *** +3 usDATA0C3 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00*** +45 usACKD2


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

По этой же причине, если мы воткнём между материнской платой и устройством дешёвый USB2-хаб, марку которого я не скажу, так как сильно поругался с магазином, владеющим именем бренда, но судя по ID, чип там VID_05E3&PID_0608, то статистика окажется намного лучше, чем при прямом подключении к материнке:



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

Пробуем двухбуферную систему


Будучи опытным программистом для микроконтроллеров, я знаю, что обычно такая проблема решается путём применения двухбуферной схемы. Пока обрабатывается один буфер, данные передаются во второй. А какая схема используется здесь? Ответ на мой вопрос мы получим из следующего кода:
USBD_StatusTypeDef USBD_LL_Init(USBD_HandleTypeDef *pdev){  /* USER CODE BEGIN EndPoint_Configuration */  HAL_PCDEx_PMAConfig((PCD_HandleTypeDef*)pdev->pData , 0x00 , PCD_SNG_BUF, 0x18);  HAL_PCDEx_PMAConfig((PCD_HandleTypeDef*)pdev->pData , 0x80 , PCD_SNG_BUF, 0x58);  /* USER CODE END EndPoint_Configuration */  /* USER CODE BEGIN EndPoint_Configuration_CDC */  HAL_PCDEx_PMAConfig((PCD_HandleTypeDef*)pdev->pData , 0x81 , PCD_SNG_BUF, 0xC0);  HAL_PCDEx_PMAConfig((PCD_HandleTypeDef*)pdev->pData , 0x01 , PCD_SNG_BUF, 0x110);  HAL_PCDEx_PMAConfig((PCD_HandleTypeDef*)pdev->pData , 0x82 , PCD_SNG_BUF, 0x100);

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

The USB peripheral's HAL driver (PCD) has a known limitation: it directly maps EPnR register numbers with endpoint addresses. This works if all OUT and IN endpoints with the same number (e.g. 0x01 and 0x81) are the same type, and not double buffered. However, isochronous endpoints have to be double buffered, therefore you have to use an endpoint number that's unused in the other direction. E.g. in your case, set AUDIO_OUT_EP1 to 0x01, and AUDIO_IN_EP1 to 0x82. (Or you can drop this USB stack altogether as I did.)


Попробовал разнести точки результат тот же. В общем, анализ показал, что для точек типа BULK это если и возможно, то только путём переписывания MiddleWare. Короче, не зря там везде одиночные буферы выбраны. Разработчики знали, что делают.

Тормоза в обработчике прерывания


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

Немного потрассируем код. Давайте я поставлю точку останова в функции CDC_Receive_FS, в которую мы попадаем ради каждого блока в 64 байта или меньше. Ну, потому что транзакции, больше чем на 64 байта, будут разрезаны на пакеты такого размера. Вот такой у нас получается стек возвратов в обработчик прерываний. А это ещё часть функций заинлайнилась!



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

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

Но и это ещё не всё! Ради всё тех же каждых 64 байт, мы вызываем функцию USBD_CDC_SetRxBuffer(), а затем USBD_CDC_ReceivePacket(), которая также расслоится на кучку вызовов, каждый из которых всего лишь чуть-чуть перетасовывает данные. Оцените стек возвратов, когда мы находимся в самом глубоком слое!



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

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

Проверяем теорию осциллографом


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

Объявим, скажем, ножку Pa0 для этой цели:
#include <iopins.h>typedef Mcucpp::IO::Pa0 oscOut;//PC13

За что люблю эту библиотеку, так за её простоту. Инициализация ножки выглядит так:
oscOut::ConfigPort::Enable();oscOut::SetDirWrite();

Включили тактирование аппаратных блоков, назначили ножку на выход. Собственно, всё.

И добавим пару строк в функцию обработки прерывания (первая взведёт ножку в единицу, вторая сбросит в ноль):


То же самое текстом.
void USB_LP_CAN1_RX0_IRQHandler(void){  /* USER CODE BEGIN USB_LP_CAN1_RX0_IRQn 0 */oscOut::Set();  /* USER CODE END USB_LP_CAN1_RX0_IRQn 0 */  HAL_PCD_IRQHandler(&hpcd_USB_FS);  /* USER CODE BEGIN USB_LP_CAN1_RX0_IRQn 1 */oscOut::Clear();  /* USER CODE END USB_LP_CAN1_RX0_IRQn 1 */}


Типичный период следования прерываний от 70 до 100 микросекунд. При этом в прерывании мы находимся чуть меньше чем 19 микросекунд. На первом рисунке показан случай, когда данные идут помедленнее (расстояния велики), на втором побыстрее (расстояния меньше). Обе осциллограммы сняты при подключении через хаб.





А вот хорошая и не очень хорошая ситуации, снятые при прямом подключении к материнке. Собственно, на плохом варианте видно, что расстояния удваиваются. Один из пакетов уходит с NAKом, в прерывание мы не попадаем.





Осталось понять, как эти прерывания располагаются относительно USB-примитивов. В этом нам поможет Reference Manual. Момент прихода прерывания я выделил.



Вот теперь всё сходится. После формирования ACKа мы не готовы принимать новые пакеты на протяжении 18 микросекунд. Когда они приходят через 3-5 микросекунд (а именно это мы видим в текстовом логе анализатора выше для плохого случая), контроллер их просто игнорирует, посылая NAK. Когда через 30-40 (что мы наблюдаем в текстовом логе для случая хорошего, хотя, точно подойдёт любое значение, больше чем 19) обрабатывает.

Плюс из текстовых логов мы видим, что влево от прерывания около пятидесяти микросекунд занимает сама OUT-транзакция на аппаратном уровне. Кстати. У нас же скорость FS. Такие сигналы можно ловить обычным осциллографом. Давайте я добавлю активность на шине USB в виде голубого луча. Что получим?

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



Вот такая картинка была, когда я получил производительность 814037 байт в секунду. Извините, но быстрее никак. Либо по шине идут данные, либо мы обрабатываем прерывание. Простоев нет!




Причём 64 байта, с учётом, что я передаю все нули, а значит там может быть вставленный бит это примерно 576 бит. При частоте 12 МГц их передача займёт 48 миллисекунд. То есть, когда между пакетами примерно по 50 миллисекунд, мы имеем дело с пределом скорости. Тут даже NAKов нет.




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




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

Мне показалось, что там они решили проблему, полностью переделав логику работы Middleware USB. Они там рассуждают, что дико сократили время пребывания внутри прерывания. Но если честно, у меня не стояло задачи выжать из этого контроллера всё. Так что я даже сильно не вчитывался в доводы авторов. Правда, при работе через хаб всё попроще. Короче, результаты у китайских специалистов если и есть, они также будут работать только на определённой конфигурации, ведь на паузы от хаба повлиять не сможет никто.

Неожиданное следствие из снятой осциллограммы


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



А при коротких пакетах, бывает и такое:



Правда, это только во время передачи данных. Если передача не идёт, мы видим только SOFы (на предыдущей осциллограмме короткая иголка слева это как раз SOF). Но когда идёт обмен данными, процентов 20 времени контроллер не готов нас обслуживать. Он находится в контексте прерывания. Так что если и обслужит, то только прерывания с бОльшим приоритетом.

Выводы


Ну что, пришла пора делать выводы. Как видим, контроллер STM32F103C8T6 не может выжать всю производительность даже из шины USB 2.0 FS.

Хорошо это или плохо? Ни то, ни другое. Есть тысяча и одна задача, где не надо гнаться за производительностью USB, и этот копеечный контроллер прекрасно с ними справляется. Вот там его и надо использовать. (Дополнение: пока статья дежала в столе, на Хабре появилась статья, что уже не копеечный. Цена у местных поставщиков, согласно той статье, выросла в 10 раз. Надеюсь, это временное явление.)

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

А для производительных вещей я в ближайшее время собираюсь изучить контроллер, у которого USB обрабатывается по стандарту EHCI. Там всё на дескрипторах. Заполнил адрес, длину Когда пришло прерывание данные уже готовы Надеюсь Если будет что-то интересное сделаю статью. А здесь сам подход к обработке приходящих данных (они помещаются в выделенную память, а затем программно изымаются оттуда, причём в контексте прерывания) не даёт развить высоких скоростей. По крайней мере, на кристалле F103.

Следующий вывод: добавленный в систему дешёвый USB-хаб даёт неожиданный прирост производительности. Это связано с тем, что он шлёт пакеты с паузами 20-25 микросекунд (в статье подтверждающий лог не приводится для экономии места, но его можно скачать здесь для самостоятельного изучения). Получаем грубо 20 микросекунд задержки, 50 микросекунд передачи. Итого 5/7 от полной производительности. Как раз 700-800 килобайт в секунду при теоретическом максимуме 1000-1100. Так что любой FS-контроллер, включённый через этот хаб, не сможет выдать больше.

Дальше: видно, что, когда по USB передаются данные, контроллер довольно большой процент времени находится в обработчике прерывания USB. Это также надо иметь в виду, проектируя систему. Прерываниям UART, SPI и прочим, где данные ждать невозможно, а обработка будет быстрой, стоит задавать приоритет выше, чем у прерывания USB. Ну, или использовать DMA.

И, наконец, мы выяснили, что для FS устройств на базе STM32 нет чёткого критерия оптимальной работы со стороны прикладной программы. Одна и та же система, в которую то добавляли, то исключали внешний USB-хаб, работала с максимальной производительностью либо при длине блока 64 байта (без хаба), либо более четырёх килобайт (с хабом). При разработке прикладных программ для PC, требующих высокой производительности, следует учитывать и этот аспект. Вплоть до калибровки параметров под конкретную конфигурацию оборудования.

Послесловие. А что там в режиме HS?


Ради интереса приведу результаты, снятые для HS-устройства на базе ПЛИС. Второй столбец скорость при прямом подключении к материнской плате, третий через тот самый дешёвый хаб. Мне уже интересно, почему там хаб даёт такую просадку, но с этим я разберусь как-нибудь в другой раз.



А пока статья лежала в столе, я взял типовой пример CDC-прошивки от NXP, немного доработал его (без доработки он зависнет при односторонней передаче), залил в плату Teensy 4.1 и снял метрики там. У него контроллер EHCI и скорость HS.



Причина та же самая. Аппаратура, вроде, позволяет поставить в очередь несколько запросов (как для асинхронной работы), но увы, программная часть это явно запрещает:
usb_status_t USB_DeviceCdcAcmRecv(class_handle_t handle, uint8_t ep, uint8_t *buffer, uint32_t length){    if (1U == cdcAcmHandle->bulkOut.isBusy)    {        return kStatus_USB_Busy;    }    cdcAcmHandle->bulkOut.isBusy = 1U;

Правда, там, за счёт резервов аппаратуры, удалось с минимальной доработкой улучшить графики до таких:



Но это уже тема для другой статьи.
Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru