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

Cpp

PVS-Studio, Blender цикл заметок о пользе регулярного использования статического анализа

03.03.2021 20:06:54 | Автор: admin

PVS-Studio мониторит код Blender
В статьях мы регулярно повторяем важную мысль: статический анализатор должен использоваться регулярно. В этом случае многие ошибки выявляются на самом раннем этапе, а их исправление максимально дёшево. Однако теория это одно, но намного лучше подкреплять слова практическими примерами. Рассмотрим несколько свежих ошибок, появившихся в новом коде проекта Blender.


Недавно мы настроили регулярную проверку проекта Blender, о чём мой коллега рассказал в статье "Just for fun: команда PVS-Studio придумала мониторить качество некоторых открытых проектов". В дальнейшем планируем начать мониторить ещё некоторые интересные проекты.


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


Итак, давайте посмотрим, что найдено в свежем коде проекта Blender.


Фрагмент первый: double-checked locking


typedef struct bNodeTree {  ....  struct NodeTreeUIStorage *ui_storage;} bNodeTree;static void ui_storage_ensure(bNodeTree &ntree){  /* As an optimization, only acquire a lock if the UI storage doesn't exist,   * because it only needs to be allocated once for every node tree. */  if (ntree.ui_storage == nullptr) {    std::lock_guard<std::mutex> lock(global_ui_storage_mutex);    /* Check again-- another thread may have allocated the storage       while this one waited. */    if (ntree.ui_storage == nullptr) {      ntree.ui_storage = new NodeTreeUIStorage();    }  }}

Предупреждение PVS-Studio. V1036: Potentially unsafe double-checked locking. node_ui_storage.cc 46


Перед нами неправильная реализация блокировки с двойной проверкой. Для пояснения проблемы процитирую фрагмент статьи "C++ and the Perils of Double-Checked Locking", написанной Scott Meyers и Andrei Alexandrescu ещё в 2004 году. Как видите, проблема давно известна, но это не защищает разработчиков от того, чтобы наступать на одни и те же грабли. Хорошо, что анализатор PVS-Studio помогает выявлять подобные проблемы :). Итак, фрагмент из статьи:


Consider again the line that initializes pInstance: pInstance = newSingleton;

This statement causes three things to happen:

Step 1: Allocate memory to hold a Singleton object.

Step 2: Construct a Singleton object in the allocated memory.

Step 3: Make pInstance point to the allocated memory.

Of critical importance is the observation that compilers are not constrainedto perform these steps in this order! In particular, compilers are sometimes allowed to swap steps 2 and 3. Why they might want to do that is a question we'll address in a moment. For now, let's focus on what happens if they do.

Consider the following code, where we've expanded pInstance's initialization line into the three constituent tasks we mentioned above and where we've merged steps 1 (memory allocation) and 3 (pInstance assignment) into a single statement that precedes step 2 (Singleton construction). The idea is not that a human would write this code. Rather, it's that a compiler might generate code equivalent to this in response to the conventional DCLP source code (shown earlier) that a human would write.

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


Подобные ошибки очень коварны! Они могут очень редко проявлять себя. Программа вроде как работает, проходит все тесты и так далее. Но время от времени она неожиданно падает у пользователей, и понять причину может быть крайне проблематично. Воспроизвести такую ошибку может быть очень сложно. Другим словами, исправление этой ошибки по жалобе от пользователей может обойтись в 1000 раз дороже, чем правка кода, сделанная по результату анализа кода с помощью PVS-Studio или аналогичного инструмента.


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


Фрагмент второй: realloc


static void icon_merge_context_register_icon(struct IconMergeContext *context,                                             const char *file_name,                                             struct IconHead *icon_head){  context->read_icons = realloc(context->read_icons,    sizeof(struct IconInfo) * (context->num_read_icons + 1));  struct IconInfo *icon_info = &context->read_icons[context->num_read_icons];  icon_info->head = *icon_head;  icon_info->file_name = strdup(path_basename(file_name));  context->num_read_icons++;}

Анализатор PVS-Studio выдаёт здесь два предупреждения, и это правильно. Здесь действительно допущено сразу две ошибки различного плана.


Первая: V701: realloc() possible leak: when realloc() fails in allocating memory, original pointer 'context->read_icons' is lost. Consider assigning realloc() to a temporary pointer. datatoc_icon.c 252


Если память не удастся выделить, функция realloc вернёт значение NULL. Нулевой указатель будет записан в переменную context->read_icons, а её предыдущее значение будет потеряно. Раз предыдущее значение указателя потеряно, то и невозможно освободить ранее выделенный блок памяти, на который ссылался этот указатель. Произойдёт утечка памяти.


Вторая: V522: There might be dereferencing of a potential null pointer 'context->read_icons'. Check lines: 255, 252. datatoc_icon.c


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


Интереснее другое. На самом-то деле падение может и не произойти. Запись производится вовсе не по нулевому указателю, а куда-то дальше. Теоретически возможна ситуация, когда этот адрес уже не находится в странице памяти, защищенной от записи, и никакого падения не будет. Будут испорчены какие-то случайные данные в памяти, и программа продолжит своё выполнение. Последствия работы с испорченными данными непредсказуемы. Подробнее всё это разобрано в статье "Почему важно проверять, что вернула функция malloc".


Фрагмент третий: разыменование указателя для проверки


static int node_link_invoke(bContext *C, wmOperator *op, const wmEvent *event){  ....  bNodeLinkDrag *nldrag = node_link_init(bmain, snode, cursor, detach);  nldrag->last_picked_multi_input_socket_link = NULL;  if (nldrag) {    op->customdata = nldrag;  ....}

Предупреждение PVS-Studio: V595: The 'nldrag' pointer was utilized before it was verified against nullptr. Check lines: 1037, 1039. node_relationships.c


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


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


Кстати, нашлась ещё одна такая-же ошибка, но описывать её неинтересно. Приведу только сообщение: V595: The 'seq' pointer was utilized before it was verified against nullptr. Check lines: 373, 385. strip_add.c


Заключение


Используйте статические анализаторы кода регулярно. От этого выиграют как разработчики, так и пользователи. Вы можете скачать и попробовать PVS-Studio здесь. Спасибо за внимание.


Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Andrey Karpov. PVS-Studio, Blender: Series of Notes on Advantages of Regular Static Analysis of Code.

Подробнее..

Как портировать SDK Flutter на ТВ-приставку для разработки и запуска приложений Android TV

15.04.2021 16:11:54 | Автор: admin

Недавно мы успешно портировали фреймворк Flutter на ТВ-приставку c открытой программной платформой RDK. В этой статье расскажем о трудностях, с которыми пришлось столкнуться, и предложим решения для успешного запуска и повышения производительности.

Учитывая, что программный стек RDK или Reference Design Kit сейчас активно используется для разработки OTT-приложений, голосового управления приставками и других продвинутых функций для видео по запросу (VoD), мы хотели разобраться, сможет ли Flutter работать на ТВ-приставке. Оказалось, что да, но, как это обычно бывает, есть нюансы.

Далее мы по шагам распишем процесс портирования и запуска Flutter на встраиваемых Linux-платформах и разберемся, как этот SDK с открытым исходным кодом от Google чувствует себя на железе с ограниченными ресурсами и ARM-процессорами.

Но прежде чем переходить непосредственно к Flutter и его преимуществам скажем пару слов об исходном решении, которое было задействовано на ТВ-приставке. На плате работала связка набор библиотек EFL + протокол Wayland, а рисование примитивов было реализовано из node.js на основе плагинного нативного модуля. Это решение неплохо себя показало с точки зрения производительности при отображении кадров, однако сам EFL отнюдь не самый новый фреймворк для отрисовки. А в режиме выполнения node.js со своим огромным event-loopом казался уже не самой перспективной идеей. В то же время Flutter мог позволить нам задействовать более производительную связку рендеринга.

Для тех, кто не в теме: первую версию этого SDK с открытым кодом Google представил еще шесть лет назад. Тогда этот набор средств разработки годился только для Android. Сейчас на нем можно писать приложения для веба, iOS, Linux и даже Google Fuchsia. :-) Рабочий язык для разработки приложений на Flutter Dart, в свое время он был предложен в качестве альтернативы JavaScript.

Перед нами стоял вопрос: даст ли переход на Flutter какой-то выигрыш по производительности? Ведь подход там совершенно иной, хоть в конечном счете и имеется та же графическая подсистема Wayland + OpenGL. Ну и как там с поддержкой процессоров с neon-инструкциями? Были и другие вопросы, например, нюансы по переносу UI на dart или то, что поддержка Linux находится в стадии альфы-беты.

Сборка Flutter Engine для ТВ-приставок на базе ARM

Итак, начнем. Вначале Futter нужно запустить на чужеродной платформе с Wayland + OpenGL ES. В основе рендеринга у Flutter лежит библиотека Skia, которая прекрасно поддерживает OpenGL ES, поэтому в теории все выглядело хорошо.

При сборке Flutter под наши целевые устройства (три ТВ-приставки с RDK), к нашему удивлению, проблемы возникли только на одной. Не будем с ней сражаться, т.к. из-за старой архитектуре intel x86 она для нас не является приоритетной. Лучше сосредоточимся на оставшихся двух ARM-платформах.

Вот, с какими опциями мы собирали Flutter Engine:

./flutter/tools/gn \      --embedder-for-target \      --target-os linux \      --linux-cpu arm \      --target-sysroot DEVICE_SYSROOT      --disable-desktop-embeddings \      --arm-float-abi hard      --target-toolchain /usr      --target-triple arm-linux-gnueabihf      --runtime-mode debugninja -C out/linux_debug_unopt_arm

Большинство опций понятны: собираем под 32-битный ARM-процессор и Linux, выключая при этом все лишнее через --embedder-for-target --disable-desktop-embeddings.

Для сборки в системе должен быть установлен clang версии 9 и выше, т.е. это стандартный сборочный механизм Flutter, инструментарий кросс-компиляции gcc не пойдет. Самое важное подать корректный target-sysroot устройства с RDK.

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

Теперь можно собрать целевой проект flutter/dart с нашей библиотекой/движком. Это сделать легко:

flutter --local-engine-src-path PATH_TO_BUILDED_ENGINE_src --local-engine=host_debug_unopt build bundle

Важно! Сборка проекта должна происходить не на устройстве с собранной библиотекой, а на хостовой, т.е. x86_64!

Для этого достаточно еще раз пройти путь сборкой gn и ninja только под x86_64! Именно она указывается в параметре host_debug_unopt.

PATH_TO_BUILDED_ENGINE_src это путь, где находится engine/src/out.

За запуск Flutter Engine под системой обычно отвечает embedder, именно он конфигурирует Flutter под целевую систему и дает основные контексты рендеринга библиотеке Skia и Dart-обработчику. Не так давно в состав Flutter добавили linux-embedder, и, в частности, GTK-embedder, так что можно воспользоваться им из коробки. На нашей платформе на момент портирования это был не вариант, нужно было что-то независимое от GTK.

Рассмотрим некоторые особенности реализации, которые пришлось учесть с кастомным эмбеддером (все, кто любит разбирать не нюансы, а исходники целиком, может сразу перейти к проекту нашего форка с доработками на github.com). К тому же по производительности наш вариант немного выигрывал у версии GTK, что было крайне важно для заказчика, и не тянул за собой весь зоопарк GTK-библиотек.

Так что же вообще нужно от эмбеддера для запуска flutter-приложения? Достаточно, чтобы он просто вызывал из библотеки flutter_engine.so

FlutterEngineRun(FLUTTER_ENGINE_VERSION, &config, &args, display /* userdata */, &engine_);

где в качестве параметров идет передача настроек проекта (директория с собранным flutter bundle) FlutterProjectArgs args и аргументов рендеринга FlutterRendererConfig config.

В первой структуре как раз задается путь bundle-пакета, собранного flutter-утилитой, а во второй используются контексты OpenGL .

// пример использования на github.com/DEgITx/flutter_wayland/blob/master/src/flutter_application.cc

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

Проблемы и их решение

Теперь поговорим о нюансах, с которыми мы столкнулись на этапе портирования. А как же без них? Не только ведь библиотеки собирать :-)

1. Краш эмбеддера и замена очередности вызова функций

Первая проблема, с которой мы столкнулись краш эмбеддера под платформой. Казалось бы, инициализация egl-контекста в других приложения происходит нормально, FlutterRendererConfig инициализирован корректно, но нет эмбеддер не заводится. Значит в связке что-то явно не так. Оказалось, eglBindAPI нельзя вызывать перед eglGetDisplay, на котором происходит особая инициализация nexus-драйвера дисплея (у нас платформа базируется на чипе BCM). В обычном Linux это не проблема, но на целевой платформе оказалась иначе.

Корректная инициализация эмбеддера выглядит так:

egl_display_ = eglGetDisplay(display_);if (egl_display_ == EGL_NO_DISPLAY) {  LogLastEGLError();  FL_ERROR("Could not access EGL display.");  return false;}if (eglInitialize(egl_display_, nullptr, nullptr) != EGL_TRUE) {  LogLastEGLError();  FL_ERROR("Could not initialize EGL display.");  return false;}if (eglBindAPI(EGL_OPENGL_ES_API) != EGL_TRUE) {  LogLastEGLError();  FL_ERROR("Could not bind the ES API.");  return false;}

// github.com/DEgITx/flutter_wayland/blob/master/src/wayland_display.cc корректная реализация, т.е. помогла измененная очередность вызова функций.

Теперь, когда нюанс запуска улажен, мы рады увидеть заветное демо-окно приложения на экране :-).

2. Оптимизация производительности

Настало время проверить производительность. И, честно говоря, она нас не сильно порадовала в режиме отладки (debug mode). Что-то работало шустро, что-то наоборот, имело большие просадки по фреймам и тормозило гораздо больше, чем что-то похожее на EFL+Node.js.

Мы немного расстроились и начали копать дальше. В SDK Flutter есть специальный режим компиляции машинного кода AOT, это даже не jit, а именно компиляция в нативный код со всеми сопутствующими оптимизациями, именно это подразумевается под по релиз-версией Flutter. Такой поддержки у нас в эмбеддере пока не было, добавляем.

Необходимы определенные инструкции, поданные аргументами к FlutterEngineRun

// полная реализация github.com/DEgITx/flutter_wayland/blob/master/src/elf.cc

vm_snapshot_instructions_ = dlsym(fd, "_kDartVmSnapshotInstructions");if (vm_snapshot_instructions_ == NULL) {  error_ = strerror(errno);  break;}vm_isolate_snapshot_instructions_ = dlsym(fd, "_kDartIsolateSnapshotInstructions");if (vm_isolate_snapshot_instructions_ == NULL) {  error_ = strerror(errno);  break;}vm_snapshot_data_ = dlsym(fd, "_kDartVmSnapshotData");if (vm_snapshot_data_ == NULL) {  error_ = strerror(errno);  break;}vm_isolate_snapshot_data_ = dlsym(fd, "_kDartIsolateSnapshotData");if (vm_isolate_snapshot_data_ == NULL) {  error_ = strerror(errno);  break;}
if (vm_snapshot_data_ == NULL || vm_snapshot_instructions_ == NULL || vm_isolate_snapshot_data_ == NULL || vm_isolate_snapshot_instructions_ == NULL) {  return false;}*vm_snapshot_data = reinterpret_cast <  const uint8_t * > (vm_snapshot_data_);*vm_snapshot_instructions = reinterpret_cast <  const uint8_t * > (vm_snapshot_instructions_);*vm_isolate_snapshot_data = reinterpret_cast <  const uint8_t * > (vm_isolate_snapshot_data_);*vm_isolate_snapshot_instructions = reinterpret_cast <  const uint8_t * > (vm_isolate_snapshot_instructions_);
FlutterProjectArgs args;// передаем все необходимое в argsargs.vm_snapshot_data = vm_snapshot_data;args.vm_snapshot_instructions = vm_snapshot_instructions;args.isolate_snapshot_data = vm_isolate_snapshot_data;args.isolate_snapshot_instructions = vm_isolate_snapshot_instructions;

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

$HOST_ENGINE/dart-sdk/bin/dart \--disable-dart-dev \$HOST_ENGINE/gen/frontend_server.dart.snapshot \--sdk-root $DEVICE_ENGINE}/flutter_patched_sdk/ \--target=flutter \-Ddart.developer.causal_async_stacks=false \-Ddart.vm.profile=release \-Ddart.vm.product=release \--bytecode-options=source-positions \--aot \--tfa \--packages .packages \--output-dill build/tmp/app.dill \--depfile build/kernel_snapshot.d \package:lib/main.dart$DEVICE_ENGINE/gen_snapshot                               \    --deterministic                                             \    --snapshot_kind=app-aot-elf                                 \    --elf=build/lib/libapp.so                                   \    --no-causal-async-stacks                                    \    --lazy-async-stacks                                         \    build/tmp/app.dill

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

-Ddart.vm.profile=release \
-Ddart.vm.product=release \

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

output-dill нужен для построения нативной библитеки libapp.so.

Самыми важными для нас являются пути $DEVICE_ENGINE и $HOST_ENGINE два собранных движка под целевую (ARM) и хост-системы (x86_64) соответственно. Тут важно ничего не перепутать и убедиться, что libapp.so получается именно 32-битной ARM-версией:

$ file libapp.so libapp.so: ELF 32-bit LSB shared object, ARM, EABI5 version 1 (SYSV), dynamically linked

Запускаем и-и-и-и... вуаля! все работает!

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

3. Подключение устройств ввода

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

4. Интерфейс на ТВ-приставке под Linux и Android и как увеличить производительность в 23 раза

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

Еще отметим интересный опыт оптимизации самого dart-приложения под целевую платформу. Нас разочаровала довольно низкая производительность продуктового приложения (в отличии от демок). Мы взяли в руки профайлер и начали копать идовольно быстро обнаружили активное использование функций __brcm_cpu_dcache_flush и khrn_copy_8888_to_tf32 во время анимаций (на платформе используется чип процессора Broadcom/BCM ). Явно происходило какое-то очень жесткое пиксельное программное трансформирование или копирование во время анимаций. В итоге виновник был найден: в одной из панелей был задействован эффект размытия:

//...filter: new ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0),//...

Комментирование одного этого эффекта дало увеличение производительности приложения в дватри раза и вывело приложение на стабильные 50-60 fps. Это хороший пример того, как один специфический эффект может уничтожить производительность во всем приложении на Flutter при том, что он был зайствован всего-лишь в одной панели, которая большую часть времени была скрыта.

Итого

В результате мы получили не просто работающее продуктовое приложение, а работающее приложение с качественным фреймрейтом на Flutter на целевом устройстве. Форк и наша версия эмбеддера под RDK и другие платформы на основе Wayland находится тут: github.com/DEgITx/flutter_wayland

Надеемся, опыт нашей команды в разработке и портировании ПО для ТВ-приставок и Smart TV пригодится вам в своих проектах и послужит отправной точкой для портирования Flutter на других устройствах.

[!?] Вопросы и комментарии приветствуются. На них будет отвечать автор статьи Алексей Касьянчук, наш инженер-программист

Подробнее..

Перевод Microsoft Rust является лучшим шансом в отрасли программирования безопасных систем

14.06.2020 12:18:17 | Автор: admin

Независимо от того, сколько вложений компании-разработчики могут потратить на инструментарий и обучение своих разработчиков, C++, по своей сути, не является безопасным языком, сказал Райан Левик (Ryan Levick) 'cloud developer advocate' из Microsoft на виртуальной конференции AllThingsOpen в прошлом месяце, объясняя в виртуальной беседе почему Microsoft постепенно переходит с C/C++ на Rust для создания своего инфраструктурного программного обеспечения. И вдохновляет других гигантов индустрии программного обеспечения задуматься о том же.



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


Фактически, Microsoft сочла C++ более неприемлемым для написания критически важных программ. Отрасль крайне нуждается в переходе на производительный, безопасный для памяти язык для низкоуровневой работы систем. А лучший выбор на рынке сегодня это Rust, сказал Левик.


C/C++ не могут быть исправлены


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


Но отрасль страдает от всех ошибок, связанных с памятью многие из которых представляют угрозу безопасности, вызванные этими языками. Сейчас по словам Левика, 70% CVE созданных в Microsoft, являются проблемами безопасности памяти. Нет никакой реальной тенденции, это просто остается неизменным, сказал он. Несмотря на огромные усилия с нашей стороны, чтобы решить эту проблему, это все еще кажется обычным делом.


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


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


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


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


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


Лучший шанс для отрасли


В ответ на проблему ошибок связанных с использованием памяти Центр реагирования Microsoft Security запустил инициативу Язык программирования безопасных систем. В ней некоторая работа была посвящена укреплению C/C++. Язык Верона новый язык программирования, создаваемый для безопасного низкоуровневого программирования, также был создан здесь. Но третий аспект стратегии этого проекта, в которую они верят больше всего, заключается в том, чтобы поддержать наилучшие шансы отрасли для непосредственного решения этой проблемы.


И мы считаем, что это Rust, сказал он.


По производительности Rust находится на одном уровне с C/C++ и, возможно, даже немного быстрее. Rust повышает производительность труда разработчиков благодаря менеджеру управления пакетами, современным средам тестирования и тому подобному. И программисты за это любят Rust.


Но главная причина, по которой Microsoft так увлечена Rust, заключается в том, что это язык безопасный для памяти, с минимальными проверками во время выполнения. Rust выделяется в создании корректных программ. Корректность означает примерно то, что программа проверяется компилятором на небезопасные операции, что приводит к меньшему количеству ошибок во время выполнения. Ключевое слово 'unsafe' является опцией, но не используется по умолчанию. Небезопасный код в Rust почти всегда является подмножеством большего объема безопасного кода. Небезопасный 'unsafe' режим необходим для задач по выделению памяти, например для написания драйверов устройств. Но даже здесь небезопасные части заключены в безопасный API.


По словам Левика, эту способность безопасно программировать не следует воспринимать легкомысленно. Фактически, Rust может обеспечить более чем 10-кратное улучшение, что делает его полезным для инвестиций. Во многом это связано с тем, что практически весь код C/C++ нуждается в аудите на предмет небезопасного поведения, в то время как небезопасный код написанный на Rust, который необходимо будет проверить, является лишь небольшим подмножеством основной кодовой базы.


Хотя Microsoft оптимистично настроена на Rust, Левик признает, что разработчики ядра Microsoft не прекратят использовать C/C++ в ближайшее время.


У нас в Microsoft много C++ и этот код никуда не денется сказал он. Фактически, Microsoft продолжает писать c++ и будет писать некоторое время.


Много инструментария построено на C/C ++. В частности, двоичные файлы Microsoft сейчас почти полностью собраны компилятором Microsoft Visual C++, который создает двоичные файлы MSVC, тогда как Rust использует LLVM.


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


Тем не менее, индустрия, похоже, движется в сторону Rust. Amazon Web Services использует его, в частности для развертывания безсерверной среды выполнения (Lambda serverless runtime), а также для некоторых частей EC2. Facebook начал использовать Rust как и Apple, Google, Dropbox и Cloudflare.


YouTube: Ryan Levick - Rust at Microsoft

Объявлены даты All Things Open 2020: 20-22 октября.


Ресурс "The New Stack" не позволяет оставлять комментарии непосредственно на сайте. Мы приглашаем всех читателей, которые хотят обсудить историю или оставить комментарий, посетить нас в Twitter или Facebook. Мы также приветствуем ваши новостные советы и отзывы по электронной почте: **feedback@thenewstack.io.


Amazon Web Services является спонсором The New Stack.


2020 The New Stack. All rights reserved.

10 июня 2020 10:10 автор Joab Jackson
Подробнее..

Как можно и как нельзя использовать нулевой указатель в С

30.07.2020 16:13:15 | Автор: admin


Некоторым этот банальный вопрос уже набил оскомину, но мы взяли 7 примеров и попытались объяснить их поведение при помощи стандарта:


struct A {    int data_mem;    void non_static_mem_fn() {}    static void static_mem_fn() {}};void foo(int) {}A* p{nullptr};/*1*/ *p;/*2*/ foo((*p, 5));                     /*3*/ A a{*p};/*4*/ p->data_mem;/*5*/ int b{p->data_mem};/*6*/ p->non_static_mem_fn();/*7*/ p->static_mem_fn();

Очевидная, но важная деталь: p, инициализированный нулевым указателем, не может указывать на объект типа А, потому что его значение отлично от значения любого указателя на объект типа А conv.ptr#1.


Disclaimer: статья содержит вольный перевод терминов и выдержек из стандарта на русский язык. Мы рекомендуем английскую версию статьи на dev.to, лишенную неточностей перевода.


Пример 1


Открыть начало кода
struct A {    int data_mem;    void non_static_mem_fn() {}    static void static_mem_fn() {}};void foo(int) {}A* p{nullptr};


*p;

Синтаксически это оператор выражения (expression statement, stmt.expr#1), в котором *p является выражением с отброшенным результатом, который, тем не менее, нужно вычислить. Определение унарного оператора * expr.unary.op#1 гласит, что этот оператор осуществляет косвенное обращение (indirection), и результатом является l-значение, которое обозначает объект или функцию, на которую указывает выражение. Его семантика понятна, чего не скажешь о том, должен ли объект существовать. Нулевой указатель в определении не упоминается ни разу.


Можно попробовать зацепиться за косвенное обращение, потому что есть basic.stc#4, в котором четко написано, что поведение при косвенном обращении через недопустимое значение указателя (indirection through an invalid pointer value) не определено. Но там же дается описание недопустимого значения указателя, под которое нулевой не подходит, и дается ссылка на basic.compound#3.4, где видно, что нулевой указатель и недопустимый это различные значения указателя.


Еще есть примечание в dcl.ref#5, которое гласит, что the only way to create such a reference would be to bind it to the object obtained by indirection through a null pointer, which causes undefined behavior, т.е. единственный способ создать такую ссылку привязать ее к объекту, полученному за счет косвенного обращения через нулевой указатель, что приводит к неопределенному поведению. Но придаточное в конце может относиться не только к косвенному обращению, но и к привязать (to bind), и в этом случае неопределенное поведение вызвано тем, что нулевой указатель не указывает на объект, о чем и говорится в основном тексте пункта dcl.ref#5.


Раз стандарт вместо однозначных формулировок оставляет пространство для интерпретаций в разрезе нашего вопроса, можно обратиться к списку дефектов языковой части стандарта, где Core Working Group среди прочего поясняет текст стандарта. Наш вопрос выделен в отдельный дефект, где CWG довольно давно пришла к неформальному консенсусу (так определен статус drafting), что неопределенное поведение влечет не разыменование само по себе, а конвертация результата разыменования из l-значения в r-значение. Если неформальный консенсус CWG звучит недостаточно весомо, то есть другой дефект, в котором рассматривается пример, аналогичный нашему примеру 7. Такой код назван корректным по этой же причине в официальной аргументации CWG.


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


Пример 2


Открыть начало кода
struct A {    int data_mem;    void non_static_mem_fn() {}    static void static_mem_fn() {}};void foo(int) {}A* p{nullptr};


foo((*p, 5));  

Чтобы вызвать foo, требуется проинициализировать его параметр, для чего нужно вычислить результат оператора запятая. Его операнды вычисляются слева направо, причем все, кроме последнего, являются выражениями с отброшенным значением так же, как и в примере 1 (expr.comma#1). Следовательно, этот пример также корректен.


Пример 3


Открыть начало кода
struct A {    int data_mem;    void non_static_mem_fn() {}    static void static_mem_fn() {}};void foo(int) {}A* p{nullptr};


A a{*p};

Для инициализации a будет выбран неявный конструктор копирования, и для того, чтобы его вызвать, нужно проинициализировать параметр const A& допустимым объектом, в противном случае поведение не определено (dcl.ref#5). В нашем случае допустимого объекта нет.


Пример 4


Открыть начало кода
struct A {    int data_mem;    void non_static_mem_fn() {}    static void static_mem_fn() {}};void foo(int) {}A* p{nullptr};


p->data_mem;

Выражение этого оператора выражения при вычислении будет раскрыто в (*(p)).data_mem согласно expr.ref#2, которое обозначает (designate) соответствующий подобъект объекта, на который указывает выражение до точки (expr.ref#6.2). Параллели с примером 1 становятся особенно явными, если открыть, скажем, basic.lookup.qual#1, и увидеть, как to refer и to designate взаимозаменяемо используются в том же смысле, что и в expr.ref. Из чего мы делаем вывод, что это корректный код, однако некоторые компиляторы не согласны (см. про проверку константными выражениями в конце статьи).


Пример 5


Открыть начало кода
struct A {    int data_mem;    void non_static_mem_fn() {}    static void static_mem_fn() {}};void foo(int) {}A* p{nullptr};


int b{p->data_mem};

В продолжение предыдущего примера не будем отбрасывать результат, а проинициализируем им int. В этом случае результат нужно конвертировать в pr-значение, потому что выражения именно этой категории инициализируют объекты (basic.lval#1.2). Так как речь идет об int, будет осуществлен доступ к объекту результата (conv.lval#3.4), что в нашем случае ведет к неопределенному поведению, потому что ни одно из условий basic.lval#11 не соблюдается.


Пример 6


Открыть начало кода
struct A {    int data_mem;    void non_static_mem_fn() {}    static void static_mem_fn() {}};void foo(int) {}A* p{nullptr};


p->non_static_mem_fn();

class.mfct.non-static#1 гласит, что функции-члены разрешено вызывать для объекта типа, к которому они принадлежат (или унаследованного от него), или напрямую из определений функций-членов класса. Именно разрешено такой смысл вкладывается в глагол may be в директивах ИСО/МЭК, которым следуют все стандарты ИСО. Раз объекта нет, то и поведение при таком вызове не определено.


Пример 7


Открыть начало кода
struct A {    int data_mem;    void non_static_mem_fn() {}    static void static_mem_fn() {}};void foo(int) {}A* p{nullptr};


p->static_mem_fn();

Как говорилось в рассуждениях к примеру 1, Core Working Group считает этот код корректным. Добавить можно лишь то, что согласно сноске 59, выражение слева от оператора -> разыменовывается, даже его результат не требуется.


Проверка с помощью constexpr


Раз константные выражения не могут полагаться на неопределенное поведение (expr.const#5), то можно узнать мнение компиляторов о наших примерах. Пусть они и несовершенны, но как минимум нередко правы. Мы взяли три популярных компилятора, подправили пример под constexpr и для наглядности закомментировали те примеры, которые не компилируются, потому что сообщения об ошибках что у GCC, что у MSVC оставляют желать лучшего на данных примерах: godbolt.


Что получилось в итоге:


#
Код
Предположение
GCC 10.1
Clang 10
MSVC 19.24
1
*p;
+
+
+
+
2
foo((*p, 5));
+
+
+
+
3
A a{*p};
4
p->data_mem;
+
+
5
int b{p->data_mem};
6
p->non_static_mem_fn();
+
+
7
p->static_mem_fn();
+
+
+
+

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


Спасибо, что остались с нами до конца, чтобы проследить за приключениями нулевого указателя в С++! :-) Обычно мы делимся на Хабре кусками кода из реальных проектов по разработке встроенного ПО для электроники, но этот раз нас заинтересовали чисто философские вопросы, поэтому примеры синтетические.


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

Подробнее..

Проект arataga реальный пример использования SObjectizer и RESTinio для работы с большим количеством HTTP-соединений

18.01.2021 12:13:17 | Автор: admin

В последние 4.5 года я много рассказывал на Хабре про такие OpenSource проекты, как SObjectizer и RESTinio. Но вот об использовании SObjectizer и/или RESTinio в реальных проектах пока еще ни разу не удавалось поговорить (была лишь одна статья от стороннего автора).

Причина простая: мы не можем обсуждать те проекты, в которых мы сами применяли SObjectizer/RESTinio, ибо NDA. Равно как и не можем рассказывать о тех чужих проектах, о которых узнали в частном общении. Так что с наглядными примерами использования SObjectizer/RESTinio в реальной жизни всегда была напряженка.

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

К счастью или к несчастью, но далеко не самый удачный 2020-й год предоставил нам возможность показать как же выглядит реальный проект, в разработке которого SObjectizer и RESTinio активно используются. И в данной статье я попробую рассказать о том, как и для чего SObjectizer и RESTinio применяются в arataga, исходники которого можно найти на GitHub.

Тем, кто хочет больше узнать об arataga и причинах его появления на GitHub-е, рекомендую прочитать этот блог-пост. Здесь же в двух словах скажу лишь, что arataga -- это socks5+http/1.1 прокси-сервер, который затачивался под использование в следующих условиях (перечислены те из них, которые актуальны с точки зрения внутренней архитектуры arataga):

  • много точек входа, счет идет на тысячи (8 тысяч нужно было поддерживать сразу). У каждой точки входа уникальное сочетание IP+port;

  • подключения на одну точку входа могут идти с темпом в несколько десятков в секунду, параллельно на одной точке входа могут "висеть" сотни подключений;

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

  • аутентификация клиентов должна укладываться в единицы миллисекунд;

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

Далее в статье я сосредоточусь на архитектурных и технических моментах, не затрагивая тему "зачем это все было нужно?"

Вероятно, для того, чтобы лучше понимать изложенное ниже, нужно иметь общие представления о SObjectizer-овских агентах, диспетчерах и почтовых ящиках (mbox-ах). Старая обзорная статья может в этом помочь (хотя сам SObjectizer за это время несколько изменился).

Тезисно о принятых проектных и архитектурных решениях

Многопроцессность или многопоточность?

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

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

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

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

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

Плюс к тому, у нас в руках был большой молоток, SObjectizer, с помощью которого разрабатывать многопоточные приложения гораздо проще, чем при ручной работе с std::thread, std::mutex и std::condition_variable и т.п. Если выразить основные сущности приложения в виде SObjectizer-овских агентов, распределить этих агентов должным образом по рабочим нитям и организовать их взаимодействие на базе асинхронных сообщений, то ужасы многопоточности можно спокойно обойти стороной. Что, собственно, в очередной раз и произошло.

Сколько рабочих потоков нужно?

Здесь все было понятно изначально, еще даже до начала разработки arataga, поскольку старый прокси-сервер, на замену которого arataga и разрабатывался, использовал модель thread-per-connection. Когда счет одновременно живущих соединений идет на десятки тысяч, то thread-per-connection не есть хорошо.

Поэтому об использовании в arataga схемы thread-per-connection речь не шла вообще. Сразу же был взят курс на применение чего-то похожего на thread-per-core.

Было решено под обслуживание подключений отводить не все доступные вычислительные ядра, а использовать формулу (nCPU-2), где nCPU -- это количество ядер.

Так, если на машине 8 ядер, то в arataga будет создано 6 рабочих потоков, которые будут отвечать за обслуживание соединений. Т.е. работой с трафиком будут заняты 6 ядер. Два оставшихся ядра будут доступны для других рабочих потоков внутри arataga. Плюс и самой ОС нужно что-то оставить, а то не очень приятно удаленно подключаться к работающему серверу по ssh, когда у него все ядра загружены под 100%

Что будет агентом, а что не будет?

Изначально в работе arataga выделялось несколько типов операций, которые должны были выполняться на разных рабочих контекстах:

  • разбор конфигурации. Обработка конфигурации для нескольких тысяч точек входа может занимать некоторое время (например, 5ms). Сюда входит и парсинг/валидация конфигурации, и сохранение ее локальной копии, и удаление устаревших точек входа, которых нет в новой конфигурации, и создание новых точек входа, добавленных в обновленной конфигурации;

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

  • обработка точек входа. Т.е. создание серверных сокетов на заданных IP+port, прием новых подключений с учетом ограничений на максимальное количество параллельных подключений и т.д.;

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

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

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

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

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

Во-вторых, не очень понятно, в чем смысл использовать здесь агента. Взаимодействие с сетью предполагалось делать с использованием Asio и асинхронных операций. Т.е., если вызывается Asio-шный async_read_some, то когда-то Asio вызовет для этой операции completion_handler. И если мне нужно передать результат async_read_some агенту, то в completion_handler нужно будет отослать агенту сообщение, которое агент обработает. Что, вообще-то говоря, не бесплатно, т.к. сперва на каком-то io_context будет запланирован вызов completion_handler, а затем, когда до completion_handler дойдет очередь, будет запланирован вызов обработчика события у агента. И только затем когда-то будет вызван этот самый обработчик. Что наводит на мысль "а не слишком ли много приходится платить за концептуальную чистоту?" Тем более, что по прошлому опыту я знал, что когда операции чтения/записи сокета доставляются до агента в виде сообщений, то как-то кардинально это работу с сетью не упрощает. Может быть даже наоборот.

Добавим сюда еще и то, что регистрация/дерегистрация агентов в SObjectizer все-таки не самая дешевая операция (это связано с механизмом коопераций и гарантиями транзакционности операции регистрации). Конечно, если принимать новые подключения с темпом в 2000-3000 в секунду, то "тормоза" SObjectizer-а здесь не проявятся, SObjectizer может регистрировать/дерегистрировать кооперации с гораздо более высоким темпом. Но все-таки у частого создания/удаления агентов есть своя цена, и если ее можно не платить, то лучше ее не платить.

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

Для чего планировалось использовать RESTinio?

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

Библиотека RESTinio, действительно, была использована таким очевидным образом. А вот о паре менее очевидных способов применения RESTinio в arataga речь пойдет ниже.

Погрузимся в подробности

Некоторые детали использования SObjectizer

В этой части бегло пройдемся по некоторым моментам использования SObjectizer в коде arataga. Выбор чисто субъективный, мне показалось, что именно они наиболее показательны и/или не обычны.

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

Агент startup_manager и "глобальный таймер"

Работа arataga начинается с запуска агента startup_manager. Этот агент отвечает за последовательный запуск основных "компонентов" arataga: сперва стартует агент user_list_processor, затем агент config_processor, затем уже запускается отдельная нить с RESTinio-сервером для административного HTTP-входа.

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

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

В какой-то мере это преждевременная оптимизация.

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

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

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

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

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

Понятие io_thread и ее реализация на базе asio_one_thread-диспетчера

Выше говорилось, что в реализации arataga было решено придерживаться модели, похожей на thread-per-core, при этом (nCPU-2) рабочих потока будет выделено под выполнение операций с сетью. Такие рабочие нити получили условное название io_thread (далее io_thread == "I/O нить" и поэтому "io_thread" будет иметь женский род).

Т.к. работа с сетью в arataga работа идет посредством Asio, то для следования идее thread-per-core было решено сделать так, чтобы на каждой io_thread работал свой собственный экземпляр asio::io_context.

Т.е. нам нужна была рабочая нить, которая создает экземпляр asio::io_context, а затем вызывает для него run(). После чего на этой нити нужно было бы еще как-то разместить и агентов, реализующих точки входа. И чтобы одна и та же нить обслуживала и операции Asio, и события SObjectizer-овских агентов.

В arataga для этих целей просто используются экземпляры asio_one_thread-диспетчера из so5extra. Каждый такой диспетчер -- это отдельный рабочий поток (т.е. io_thread).

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

Агенты authentificator и dns_resolver, их дублирование на каждой io_thread

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

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

Мне показалось, что если уж мы пытаемся следовать модели thread-per-core, то логичным было бы сделать отдельную копию агентов dns_resolver и authentificator для каждой из io_thread. Поэтому эти агенты создаются сразу же вслед за очередным asio_one_thread-диспетчером и привязываются к только что созданному диспетчеру.

Распределение агентов по диспетчерам

В результате получилась следующая картинка:

У агентов config_processor и user_list_processor есть собственные рабочие потоки, которые реализуются посредством штатного диспетчера SObjectizer под названием one_thread.

Каждая io_thread представлена отдельным диспетчером asio_one_thread из so5extra. И на каждом таком диспетчере работает сразу несколько (десятков, сотен, тысяч) агентов acl_handler.

Агент config_processor

Агент config_processor отвечает за обработку конфигурации arataga, поддержание списка существующих точек входа, их распределение между io_threads, и за создание/удаление точек входа при изменении конфигурации.

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

Если же локальной копии конфигурации нет или же прочитать ее не удалось, то config_processor ждет, пока придет новая конфигурация от административного HTTP-входа.

Когда от административного HTTP-входа поступает новая конфигурация, что после ее успешной обработки config_processor рассылает уведомления об изменении параметров arataga на отдельный multi-producer/multi-consumer mbox. Так изменения в конфигурации становятся доступны всем, кто в них заинтересован.

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

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

Эта диспропорция может быть устранена при добавлении новых точек входа. В этом случае config_processor при создании новых агентов acl_handler будет привязывать их к тем диспетчерам asio_one_thread, на которых сейчас меньше всего живых acl_handler.

Но если новые точки входа не создаются, то диспропорция останется и config_processor не будет перераспределять старые acl_handler между диспетчерами asio_one_thread. По двум причинам:

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

  • во-вторых, в каждом агенте acl_handler может существовать множество Asio-шных объектов, завязанных на конкретный экземпляр asio::io_context. Если перемещать содержимое acl_handler с одной io_thread на другую, то нужно будет и переконструировать эти Asio-шные объекты.

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

Агент user_list_processor

Агент user_list_processor отвечает за работу со списком пользователей.

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

Когда от административного HTTP-входа прилетает новый список пользователей, то user_list_processor пытается его обработать. И, если это получается успешно, то обновленный список рассылается на отдельный multi-producer/multi-consumer mbox на которые подписаны агенты authentificator. Что позволяет authentificator-ам получать обновленные списки пользователей сразу после того, как user_list_processor завершит их обработку.

Агент acl_handler

Агент acl_handler является, наверное, самым сложным и объемным агентом в arataga (hpp-файл, cpp-файл). Его задача -- это создать серверный сокет для точки входа, принимать и обслуживать подключения к этому серверному сокету.

В связи с этим в рамках данной статьи можно выделить несколько аспектов в реализации acl_handler.

Во-первых, acl_handler инициирует асинхронные I/O операции с помощью Asio, в частности, вызывает async_accept у Asio-шного asio::ip::tcp::acceptor. В async_accept передается completion-handler в виде лямбды, где напрямую вызываются методы агента. Такие вызовы безопасны потому, что агент привязан к asio_one_thread диспетчеру. Этот диспетчер специально создан для того, чтобы на одном рабочем контексте можно было работать и с агентами, и с Asio. Если бы acl_handler привязывался к какому-то другому диспетчеру (например, к штатному one_thread-диспетчеру), то этого делать было бы нельзя.

Во-вторых, логика у acl_handler достаточно тривиальная, но даже и здесь нашлось место для применения иерархического конечного автомата (которые в SObjectizer-е уже довольно давно поддерживаются):

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

В-третьих, агент acl_handler не занимается сам обслуживанием принятых подключений и обработкой протоколов socks5/http. Вместо этого для каждого соединения создается т.н. connection_handler. Это объект, в который отдается принятое подключение и который уже с этим подключением непосредственно работает. Т.е. читает из него данные, пытается определить протокол, пытается обслуживать подключение согласно того или иного протокола.

Т.е. когда в результате async_connect у acl_handler появляется новый asio::ip::tcp::socket, то создается объект, реализующий интерфейс connection_handler_t, и новый socket отдается этому только что созданному connection_handler-у. Больше про новое подключение acl_handler ничего не знает. Далее acl_handler лишь хранит у себя умный указатель на connection_handler и лишь время от времени дергает у connection_handler-а метод on_timer (тот самый "глобальный таймер" о котором речь шла выше). А вот connection_handler дальше живет своей собственной и весьма непростой жизнью.

Вообще говоря, как раз тот кусок arataga, который относится к connection_handler-ам, является самым мудреным в проекте. Вероятно, далеко не самым удачным. И, скорее всего, именно он мог бы стать первым кандидатом на рефакторинг по итогам опытной эксплуатации. Поскольку первая версия arataga создавалась в высоком темпе, в условиях сжатых сроков, то ничего лучше сходу придумать не удалось. Возможно, здесь следовало бы применить stackful-короутины. Но экспериментировать с ними не было времени. Так что, если история с arataga получит продолжение (что вряд ли, но вдруг), к этому куску arataga нужно будет сделать еще один, более вдумчивый подход. Пока что получилось то, что получилось :(

Использование retained_mbox для распространения конфигурации

Примечательным моментом использования SObjectizer-овских mbox-ов в arataga является применение retained_mbox из so5extra для распространения информации о текущей конфигурации и списках пользователей.

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

В arataga это свойство retained_mbox используется для того, чтобы агенты authentificator сразу же могли получить текущий список пользователей. Сценарий работы такой:

  • стартует агент user_list_processor. Он читает локальную копию списка пользователей и отсылает сообщение с этим списком в retained_mbox. На этом этапе подписчиков у retained_mbox еще нет, но нас это не волнует;

  • затем стартует агент config_processor, который читает локальную копию конфигурации и создает io_threads вместе с authentificator-ами;

  • каждый запущенный authentificator должен получить список пользователей, для чего authentificator-ы подписываются на соответствующее сообщение из retained_mbox-а;

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

Благодаря этому authentificator-у при старте не нужно ни у кого ничего явно запрашивать.

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

Некоторые детали использования RESTinio

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

Первое применение RESTinio, самое очевидное: административный HTTP-вход

Очевидное применение, ради которого RESTinio и подтягивался в проект arataga, -- это реализация административного HTTP-входа. Как раз здесь простота асинхронной обработки входящих запросов посредством RESTinio и была нужна с самого начала.

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

Упомянуть здесь можно разве лишь то, что для взаимодействия RESTinio- и SObjectizer-частей приложения были сделаны вспомогательные интерфейсы (раз, два). Когда startup_manager запускает RESTinio-сервер, то отдает серверу реализацию интерфейса requests_mailbox_t. RESTinio-сервер через этот интерфейс передает в SObjectizer-часть arataga входящие запросы. И каждый входящий запрос сопровождается реализацией интерфейса replier_t. Посредством этого интерфейса реальные обработчики запроса могут отвечать RESTinio-серверу.

Два эти интерфейса скрывают от RESTinio-части существование SObjectizer-части arataga и наоборот. Что мне показалось полезным. Хотя бы плане сокращения времени компиляции отдельных cpp-файлов.

Второе применение RESTinio, не очевидное, но вполне ожидаемое: работа с HTTP-заголовками при проксировании HTTP-соединений

Второе применение стало для меня лично неожиданным, хотя оно было вполне себе логичным. Дело в том, что применить RESTinio в его текущем виде для проксирования HTTP-соединений не представляется возможным в силу нынешних архитектурных особенностей RESTinio.

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

И вот когда у меня руки дошли до обработки HTTP-заголовков в проксируемых соединениях, то я внезапно (c) осознал, что в RESTinio уже есть инструментарий для этого. Чем и воспользовался, хотя изначально о таком даже и не думал.

Третье применение RESTinio, совсем не очевидное: синтаксический разбор конфигурации

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

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

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

acl.io.chunk_size 16kib # Вместо 16384timeout.authentification 1200ms # Вместо 1200timeout.connect_target 7s # Вместо 7000

Ну и для того, чтобы не делать парсинг конфигов вручную только штатными средствами стандартной библиотеки C++ (очень куцей в этом плане), нужно было задействовать какой-то генератор парсеров. А зачем тащить в проект еще что-то, если нечто подобное уже есть в RESTinio?

Так что easy_parser из RESTinio, который изначально появился в RESTinio для упрощения разбора HTTP-заголовков, пригодился в arataga и для разбора конфигурации.

Любопытный, на мой взгляд, пример использования easy_parser-а из RESTinio можно найти здесь. Именно этот transfer_speed_p() используется для реализации команд конфигурации вроде:

bandlim.in 850kibbandlim.out 700kib

Заключение

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

С RESTinio это сработало на 100%. Опыт применения RESTinio в arataga дал пинка под за толчок в сторону разработки пусть приблизительного, но аналога middleware из ExpressJS. Первый результат уже доступен в 0.6.13. И, если пойти на слом API в ветке 0.7, то можно будет поддержать и цепочки асинхронных обработчиков.

Но еще более важным итогом использования RESTinio в arataga стало изменение лично моих взглядов на позиционирование RESTinio в мире аналогичных фреймворков для C++.

Так что для RESTinio испытание arataga оказалось исключительно полезным.

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

Я же надеюсь, что данная статья даст читателям, которые с любопытством посматривают на RESTinio и/или SObjectizer(+so5extra), но пока что сами эти проекты не пробовали, возможность взглянуть на реальный код, написанный с применением наших инструментов. И сделать собственные выводы о том, нравится ли увиденное или нет.

Что касается перспектив самих RESTinio и SObjectizer/so5extra, то тут все не так однозначно и без внешней поддержки, боюсь, их развитие приостановится. Впрочем, это уже совсем другая история

Подробнее..

C в Практикуме. Как обучить студентов плюсам, не отпугивая

26.01.2021 14:17:08 | Автор: admin

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

Меня зовут Антон Полднев, я уже давно пишу в Яндексе на C++ и руковожу сервисом, написанным на этом языке. Параллельно я учу других людей навыкам разработки. С 2016 года я вёл курсы на Coursera, затем мы их прокачали и сделали на их основе курс для Практикума. В этом посте я расскажу, как мы учим людей C++, а также про основные особенности этого языка.

Языковые сложности. C++ VS Python

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

Сейчас даже в школах, где уделяют внимание программированию, учат способных учеников именно Python. Конечно, многие из нас с вами на уроках информатики начинали с Pascal или Visual Basic, в ряде школ наверняка они ещё остались. Но проблема Pascal в том, что обучиться-то ему можно, а вот найти применение в реальной жизни уже сложнее. А Python и простой, и дружелюбный, и проектов на нём много.

Погоди, пост же про плюсы, скажет внимательный читатель. Да, всё верно, вот и они. Штука в том, что с Python хорошо начинать. А вот дальше всё зависит от задач и желаний программиста. Если вам хочется (и интересно) писать куда более производительный код, над которым у вас будет полный контроль, то для этого понадобится что-то низкоуровневое. Например, Java, C++, C#, в какой-то степени Go.

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

Это так себе история, поэтому за плюсы активно взялись в начале XXI века и начали развивать язык. Как итог, сейчас на C++ не просто можно писать эффективный код. На C++ стало приятно писать эффективный и безопасный код.

Обузданная мощь

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

Программы на C++ быстрее благодаря более чёткой типизацииПрограммы на C++ быстрее благодаря более чёткой типизации

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

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

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

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

Как не надо учить людей C++

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

Идеальный путь мне видится примерно таким.

  • В школе у вас было программирование, вас учили питону.

  • Затем вы аккуратно и постепенно переползли на плюсы.

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

Это если сильно всё упрощать.

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

  • В школе программируете на чём-то, что вроде бы и язык программирования, при этом совершенно бесполезный и в природе уже не встречается. Кроме учебников. Старых учебников.

  • Затем поступили в университет, а там сюрприз внезапно плюсы.

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

В общем, про метапрограммирование вы на таком курсе услышите разве что от других студентов. И то не факт.

Так нельзя делать, и вот почему. Допустим, новичку рассказали про С, попросили сначала (на курсе по C++, да) попрограммировать именно на С. Если элемент страдания на курсе возведён в культ, то студенты будут программировать ещё и на ассемблере, просто для того, чтобы ощутить ту боль и метания, которые испытывали инженеры.

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

Менее страшный академический подход будет заключаться в том, что после С вам всё же расскажут про плюсы: смотрите, вместо массива есть вектор, и у него динамически изменяется размер, и всё делается за вас. И вроде как студентам радостно должно быть: круто-то как, само всё работает. Но зачем же тогда студентов перед этим полтора семестра заставляли вот этими руками писать по 50 строк кода, когда такая круть существует?

А как надо?

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

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

Например, вот так мы объясняем студентам цикл while через хождение к холодильникуНапример, вот так мы объясняем студентам цикл while через хождение к холодильнику

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

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

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

Мы учим студентов на курсе, начиная с верхнеуровневых вещей, и потом уже раскрываем всё это подробнее, углубляясь в каждую тему. Про устройство контейнеров можно рассказывать уже через пару месяцев обучения, это тоже помогает разработчику прокачиваться. Потому что в отличие от других языков все стандартные средства, например, контейнеры C++ тоже написаны на C++. И мы даём задачки вида Вы узнали, как на самом деле работает управление памятью. Теперь напишите свой список или вектор.

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

Обучение углублённым темам

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

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

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

C++ в Яндексе

А теперь расскажу, как C++ пригождается нам внутри компании. На нём хорошо писать сервисы, которые критичны с точки зрения нагрузки и скорости ответа. Сейчас я руковожу отделом разработки баннерной системы. Это сервис, отдающий рекламу на конкретный запрос конкретному пользователю. Рекламу мы показываем и на поиске Яндекса, и на других сайтах соответственно, трафика через нас проходит огромное количество. Большую часть прибыли Яндексу приносит именно реклама. Это важный сервис, и он написан на C++.

К рекламному движку, как мы его называем, предъявляются жёсткие требования по нагрузке: за секунду нужно обрабатывать сотни тысяч запросов, и делать это нужно за десяткисотни миллисекунд. Спроектировать и поддерживать такой сервис довольно тяжело. И современный C++ нам в этом помогает.

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

Чеклист Как надо

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

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

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

  • Вообще не страшно рассказать про вектор и при этом не рассказать про шаблоны классов. Детали реализации обсудить всегда успеете.

  • А ещё не страшно рассказать про словарь и не рассказать, что такое бинарное дерево.

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

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

  • Убедитесь, что студент понимает, зачем вы ему это рассказываете. Если вы решили рассказать про словарь просто потому, что словарь это круто, студент может не понять. Покажите, зачем ему словарь.

  • Рассказывайте про новое в языке. Быть современным важно.

  • Не рассказывайте вообще всё. Это ловушка для преподавателя: когда вы давно в C++ и хорошо знаете язык, хочется рассказать про него всё. Рискуете перенасытить студентов и усложнить подачу.

А усложнять не надо.

Вот так мы и учим наших студентов. Если у вас есть вопросы, с радостью отвечу.

Подробнее..

Развитие проекта arataga пара рефакторингов по результатам натурных испытаний

27.05.2021 10:19:54 | Автор: admin

OpenSource-проект arataga -- это работающий прототип производительного socks5+http/1.1 прокси-сервера. Реализован arataga на базе Asio, SObjectizer и RESTinio. Об arataga уже рассказывалось несколько месяцев назад именно как о хорошем примере того, как выглядит реальный код на SObjectizer-е. Ведь одно дело повествовать о сильных сторонах SObjectizer-а с иллюстрациями из игрушечных примеров. Совсем другое -- иметь возможность показать почти что продакшен-код.

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

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

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

Собственная реализация взаимодействия с DNS

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

Отказ от Asio-шного async_resolve

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

Как я понял, Asio для выполнения async_resolve использует дополнительную рабочую нить, на которой делает обычные синхронные обращения к ОС для преобразования доменного имени в набор IP-адресов. И, если одно такое обращение "притормозит", то будут приостановлены и все последующие обращения к async_resolve. Кроме того, если нам нужно выполнить резолвинг сразу нескольких имен, то параллельно мы этого сделать не можем. Резолвинг будет выполняться последовательно.

В итоге, когда потребовалось обслуживать множество параллельных преобразований доменных имен в IP-адреса Asio-шный async_resolve буквально "вставал колом". Некоторым клиентам результат операции async_resolve приходилось ждать по несколько десятков секунд (максимальные значения, насколько я помню, колебались в районе от 35 до 40 секунд, в зависимости от нагрузки).

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

Работа с DNS на уровне агентов: было и стало

Не думаю, что есть смысл рассказывать о том, как именно было реализовано общение с DNS-серверами посредством UDP-датаграмм, поскольку вся эта работа сосредоточена внутри одного агента a_namesever_interactor_t и никто деталей этой работы не видит.

Гораздо интереснее рассказывать о том, как резолвинг доменных имен представлен на уровне взаимодействия агентов в arataga.

Первоначальная схема была тривиальной: был некий интерфейсный почтовый ящик (mbox) и два сообщения -- resolve_request_t и resolve_reply_t. За этим почтовым ящиком скрывался единственный агент a_dns_resolver_t, который и отвечал за процедуру резолвинга доменных имен: держал кэш уже обработанных имен, организовывал список ждущих своей очереди доменных имен, дергал async_resolve и обрабатывал результаты резолвинга.

Важный момент, на который хотелось бы обратить внимание -- это то, что само существование dns_resolver было неизвестно другим частям arataga. Механизм резолвинга доменных имен был практически в буквальном смысле "черным ящиком". Просто есть некий mbox, в который нужно отсылать сообщения resolve_request. В каждом resolve_request передается mbox, на который затем прилетает resolve_reply. И это все, что требовалось знать.

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

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

Во-первых, это агент interactor::a_nameserver_interactor_t, который непосредственно общается с DNS-серверами по UDP. Агент nameserver_interactor отвечает за преобразование конкретного запроса на резолвинг в соответствующую UDP-датаграмму и обработку ответов от DNS-серверов.

Во-вторых, это агент lookup_conductor::a_conductor_t, который принимает входящие resolve_request, проверяет наличие результатов в кэше, выстраивает в очередь запросы, для которых нужен резолвинг, и отправляет команды агенту nameserver_interactor.

А вот с conductor все не так просто :)

И самое любопытное здесь то, что на самом деле conductor-ов два.

Первоначально планировалось использовать всего один экземпляр conductor. Который бы обрабатывал и IPv4, и IPv6. По аналогии с тем, как это делал dns_resolver из первых версий arataga: dns_resolver в случае успешного результата получал список IP-адресов, в котором присутствовали и IPv4, и IPv6 адреса. И когда приходил resolve_request_t для получения IPv4 адреса, то dns_resolver выбирал из списка IPv4 адрес. А когда приходил resolve_request для IPv6 адреса, то dns_resolver искал в списке IPv6 адрес (либо же конвертировал в IPv6 один из IPv4 адресов).

Такая схема была принята потому что Asio-шный async_resolver мог вернуть в случае успеха список с двумя типами адресов.

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

Но в процессе тестирования nameserver_interactor с реальными DNS-серверами выяснилось, что работает либо запрос ресурсной записи типа A, либо запрос ресурсной записи типа AAAA. Но вот если отослать в одной UDP-датаграмме сразу два запроса (и для A, и для AAAA), то ответ придет только на один из них.

Посему имея на руках почти готовые реализации conductor и nameserver_interactor нужно было что-то оперативно решать. Понятно, что при обращении к nameserver_interactor придется явно указывать тип IP-адреса (IPv4 или IPv6). Но не понятно, как быть с кэшированием результатов и выстраиванием запросов в очереди.

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

Тратить дополнительное время на рефакторинг conductor не хотелось, поэтому в модном и молодежном стиле "фигак-фигак и в продакшен" был применен метод грубой силы: вместо одного conductor-а запускается сразу два. Один обслуживает запросы для IPv4, второй -- запросы для IPv6. И оба они подписываются на сообщения resolve_request из интерфейсного mbox-а.

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

Сделать это оказалось совсем не сложно. Были применены фильтры доставки для сообщений.

Фильтры доставки -- это предикаты, которые агент может "навесить" на сообщения из конкретного multi-consumer mbox-а. Когда сообщение отсылается в mbox, то mbox обращается к предикату с вопросом "можно ли доставлять конкретно этот экземпляр сообщения до твого владельца?" Если предикат говорит, что можно, то сообщение доставляется. Если нет, то игнорируется.

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

voida_conductor_t::so_define_agent(){// We want to receive only requests for our IP-version.so_set_delivery_filter(m_incoming_requests_mbox,[ip_ver = m_ip_version]( const resolve_request_t & req ) {return ip_ver == req.m_ip_version;} );so_subscribe( m_incoming_requests_mbox ).event( &a_conductor_t::on_resolve );

Каков результат этих изменений?

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

Таймеры для acl_handler-ов

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

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

Дело в том, что внутри arataga есть специальное сообщение one_second_timer, которое генерируется раз в секунду и которое используется acl_handler-ами для разных целей: и для контроля тайм-аутов текущих операций, и для пересчета лимитов по подключениям.

Дабы не тратить время при выпуске первой версии arataga была использована самая простая схема: каждый acl_handler просто подписывается на one_second_timer. Что означает, что когда это сообщение возникает, то оно доставляется всем подписчикам.

Т.е., если у нас создано 15K acl_handler-ов, то раз в секунду у них у всех запускается обработчик one_second_timer. Что и увеличивает потребление CPU.

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

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

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

Какие подходы нельзя назвать хорошими?

Фильтры доставки

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

Тут сразу несколько проблем.

Во-первых, отсылка сообщения one_second_timer идет с контекста специальной нити таймера, которой управляет сам SObjectizer. И на этой же нити в процессе отсылки сообщения запускаются фильтры доставки. Следовательно, если фильтр доставки хочет обратиться к каким-то потрохам acl_handler-а, то эти потроха должны быть как-то защищены с точки зрения thread-safety. Например, посредством mutex-а. Что отнюдь не бесплатно. И затрудняет реализацию самого acl_handler-а, т.к. внутри агента нужно заботиться о thread-safety, хотя агенты как раз и нужны, чтобы такими вещами не заниматься.

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

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

Подписка/отписка на/от one_second_timer

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

Это нормальный подход. По крайней мере он идеологически правильный и не ведет к каким-либо проблемам с thread-safety.

Но его нельзя назвать совсем уж дешевым. Ведь подписка на mbox -- это захват нескольких объектов синхронизации (в mbox-е и в агенте-подписчике) + несколько аллокаций памяти на создание объектов-подписок.

А хотелось бы, чтобы работа с one_second_timer была еще дешевле.

Использованный подход

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

Введено понятие timer_provider. Это интерфейс объекта, который должен присутствовать на каждой io_thread в единственном числе. Его задачей является периодический вызов метода on_timer у привязанных к этой же io_thread объектов timer_consumer. Но не у всех timer_consumer, а только у тех, кто заявил о себе timer_provider-у. Т.е., когда у timer_consumer появляется потребность в обработке таймера, он вызывает у timer_provider-а метод activate_consumer, а когда такая потребность исчезает -- вызывает метод deactivate_consumer. Таким образом у timer_provider есть список активных timer_consumer-ов.

За интерфейсом timer_provider скрывается простой агент, который подписывается на one_second_timer. Получив это сообщение timer_provider просто бежит по своему списку активных timer_consumer-ов и вызывает у них on_timer.

timer_consumer-ами являются acl_handler-ы. Когда у acl_handler-а (он же timer_consumer) появляется первое принятое подключение, то acl_handler добавляет себя в список к timer_provider-у. А когда подключений не остается acl_handler вычеркивает себя из списка timer_provider-а.

Фокус в том, что все взаимодействующие в рамках одной io_thread timer_provider и timer_consumer-ы привязанны к одному и тому же диспетчеру. Что гарантирует, что они работают на одной и той же рабочей нити. А значит могут обращаться друг к другу не боясь приключений с thread-safety.

Это отличный пример того, как при работе с SObjectizer-ом отказаться от взаимодействия с агентами только посредством асинхронного взаимодействия и выполнять синхронные вызовы методов агентов, но при этом не иметь проблем с thread-safety. Ведь диспетчеры в SObjectizer-е и были введены как раз для того, чтобы программист мог управлять тем где и как работают его агенты. И, если у программиста есть возможность директивно привязать своих агентов к конкретной нити, то почему бы этим не пользоваться. По крайней мере в условиях, когда хочется выжимать максимум.

Защита от повисших указателей

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

Когда у acl_handler-а (т.е. у timer_consumer-а) вызывается so_evt_finish (это последнее событие для агента перед дерегистрацией), то acl_handler вычеркивает себя из списка timer_provider-а. Так что у timer_provider-а не может остаться повисших указателей на timer_consumer-ы (в теории, по крайней мере).

Но что с самим указателем на timer_provider? Как сделать так, чтобы он оставался валидным до тех пор, пока жив хотя бы один acl_handler?

Очень просто: кооперация с агентом timer_provider выступает в качестве родительской для всех коопераций с acl_handler-aми. В этом случае SObjectizer гарантирует, что родительская кооперация не будет уничтожена пока есть хотя бы одна живая дочерняя кооперация. Т.е. указатель на timer_provider внутри timer_consumer-ов будет оставаться валидным.

Каков результат этих изменений?

В зависимости от конфигурации, количества ACL внутри arataga и входящей нагрузки расход CPU на тестовых прогонах снизился от 1.5 до 4-х раз.

Заключение

Для нас arataga получился интересным полигоном для еще одних натурных испытаний SObjectizer и RESTinio. Да еще и с возможностью открыто показать как выглядит реальный код на SObjectizer-е.

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

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

Так что, если у кого-то есть сомнения о том, брать или не брать SObjectizer/RESTinio/json-dto в работу, то отбросьте их и попробуйте. В случае сложностей или проблем смело обращайтесь к нам, без помощи не оставим.

Подробнее..

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

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. Приходите, будет сложно, но занимательно.

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

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

Как мы обучили сфинкса для голосового помощника

09.07.2020 12:23:49 | Автор: admin
В процессе разработки проекта голосовой помощник одним из требований была возможность распознавания управляющих команд в оффлайн режиме. Это было нужно, так как в противном случае пришлось бы постоянно слушать и посылать поток с аудиоданными на распознавание, получать ответ и анализировать его.

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



Что случилось


Изначально Pocketsphinx работал у нас на железе OrangePi, у которого были: RAM 512Mb, несколько ГБ свободного дискового пространства, а на борту крутилась Ubuntu. При этом мы использовали python интерфейс (в виде python пакета) для работы с Pocketsphinx.

Мы установили Pocketsphinx командой

`pip install pocketsphinx`

Python позволил использовать нам имеющиеся библиотеки и повысить скорость разработки кода. Однако, даже один только процесс потреблял порядка 30-40 Мб оперативной памяти.

Это могло стать проблемой, потому что выяснилось, что доступное по цене железо заметно скромнее: для операционной системы доступно 123 из 128 Мб RAM, часть которой отводится под нужды железа. Из них часть памяти уже использовалась ОС Linux. Для нашего ПО оставалось только 62 Мб RAM и несколько десятков Мб свободной флеш памяти для хранения кода и файлов. Нужно было искать альтернативу python.

Решили запустить часть с Pocketsphinx на Си.

Сборка Pocketsphinx на Си


Исходники для Pocketsphinx были взяты с сайта: cmusphinx.github.io

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

Сборку кода решено было делать без поддержки python, для чего использовали ключ

`--without-python`

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

`sphinx/lib`

Для дальнейшей работы с Си кодом требуются библиотеки:

`libpocketsphinx.so libsphinxbase.so libsphinxad.so`

Нюансы


После запуска Pocketsphinx с использованием библиотек оказалось, что Си вариант гораздо менее требователен к ресурсам. Использование RAM сводилось буквально к нескольким Mb.

Приложение после запуска занимало не более 5Mb RAM, но в нем при этом использовались еще с десяток других модулей и ряд библиотек. Если в варианте с python загрузка ядра CPU была весьма заметной (50% или даже более), то тут загрузка ядра была 7-9% (и это на все приложение, а не только на Pocketsphinx).

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

Работа с Pocketsphinx


Работа с Pocketsphinx состоит из этапов:

  1. Подготовка записей в соответствии с текущей задачей.
  2. Подготовка базы данных для модели.
  3. Построение языковой модели: обучение модели на полученных записях. Получение Pocketsphinx model and language model.
  4. Реализация алгоритма использования API Pocketsphinx.
  5. Поиск оптимальных параметров Pocketsphinx эмпирическим методом.

Остановимся на каждом этапе подробнее.

1. Подготовка записей в соответствии с текущей задачей


Подготовить базу записей в формате PCM.

Мы использовали следующие настройки:

Format settings, Endianness: Little
Bit rate: 256 Kbps
Channel(s): 1 channel
Sampling rate: 16.0 KHz
Bit depth: 16 bits

В специальном помещении для записи голоса они произносили ряд слов, каждый в нескольких интонациях (удивление, злость, грусть) и частотах (громко, тихо, нормально). Было сделано 100 записей: 60 мужских и 40 женских.

Примечание: для изменения частоты дискретизации используйте инструмент sox: sox original.wav -b 16 sample.wav каналов 1 скорость 16.

2. Подготовка базы данных для модели


etc
your_db.dic (Phonetic dictionary)
your_db.phone (Phoneset file)
your_db.jsgf (Language grammer)
your_db.filler (List of fillers)
your_db_train.fileids (List of files for training)
your_db_train.transcription (Transcription for training)
your_db_test.fileids (List of files for testing)
your_db_test.transcription (Transcription for testing)
wav
speaker_1
file_1.wav (Recording of speech utterance)
speaker_2
file_2.wav

3. Построение языковой модели


  1. Подготовить базу данных;
  2. Создать учебные файлы и полный словарь и список слов;
  3. Создать словарь из ru.dic и wordlist;
  4. Создайте файл из ru.dic;
  5. Извлечь текст из файла транскрипции;
  6. Построить языковую модель. Скачать инструмент SRILM и создать пакет для построения модели.
  7. Конвертировать файлы lm в файлы dmp.

    sphinx_lm_convert -i model.lm -o model.dmpsphinx_lm_convert -i model.dmp -ifmt dmp -o model.lm -ofmt arpasphinx_lm_convert -i model.lm -o model.lm.binsphinx_lm_convert -i model.lm.bin -ifmt bin -o model.lm -ofmt arpa
    
  8. И, наконец, обучить модель на полученных записях.

Шаги обучения


После подготовки файлов мы должны сгенерировать файл конфигурации: из корневой папки модели мы запускаем:
sphinxtrain -t setup "имя модели"

в конфигурационном файле sphinx_train.cfg:

1- set the value current to the variable CFG_CMN         $CFG_CMN = 'current';2 -$CFG_INITIAL_NUM_DENSITIES = 256; change this value to 13- $CFG_FINAL_NUM_DENSITIES = 256; change this value to 84- $CFG_N_TIED_STATES = 200;  set this value to 200$CFG_CD_TRAIN = 'yes'; set this value to no if the data set is small

Примечание: если мы изменим

$ CFG_CD_TRAIN

на нет, то нам нужно изменить

$ DEC_CFG_MODEL_NAME

на

$ CFG_EXPTNAME.ci_cont

из вашего sphinx_train.cfg. Изменить можно так:

$ DEC_CFG_MODEL_NAME = "$ CFG_EXPTNAME.ci_cont";

или

$ DEC_CFG_MODEL_NAME = "$ CFG_EXPTNAME.ci _ $ {CFG_DIRLABEL}";

Последний шаг начать тренировки нашей модели: sphinxtrain run.

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

Инструкция по создании модули с нуля можно найти на github.

4. Реализация алгоритма использования API Pocketsphinx


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

  1. Распознавание речи в записаном аудио файле;
  2. Распознавание речи в потоке данных от микрофона.

Обобщенный алгоритм взаимодействия с Pocketsphinx после инициализации необходимых ресурсов:

1. Начать распознавание речи:

`int ps_start_utt(ps_decoder_t *ps)`

2. Передать данные из входного потока в Pocketsphinx:

`int ps_process_raw(ps_decoder_t *ps, int16 const *data, size_t n_samples, int no_search, int full_utt)`

3. Закончить распознавание речи:

`int ps_end_utt(ps_decoder_t *ps)`

4. Получить результат распознавания в виде строки:

`char const *ps_get_hyp(ps_decoder_t *ps, int32 *out_best_score)`

Подробный пример взаимодействия с API Pocketsphinx, при чтении данных из микрофона:

```Cfor (;;) {    if ((k = ad_read(ad, adbuf, 2048)) < 0)        E_FATAL("Failed to read audio\n");    ps_process_raw(ps, adbuf, k, FALSE, FALSE);    in_speech = ps_get_in_speech(ps);    if (in_speech && !utt_started) {        utt_started = TRUE;        E_INFO("Listening...\n");    }    if (!in_speech && utt_started) {        /* speech -> silence transition, time to start new utterance  */        ps_end_utt(ps);        hyp = ps_get_hyp(ps, NULL );        if (hyp != NULL) {            printf("%s\n", hyp);            fflush(stdout);        }        if (ps_start_utt(ps) < 0)            E_FATAL("Failed to start utterance\n");        utt_started = FALSE;        E_INFO("Ready....\n");    }    sleep_msec(100);}```

Подробный пример взаимодействия с API Pocketsphinx, при чтении данных из файла:

```Cwhile ((k = fread(adbuf, sizeof(int16), 2048, rawfd)) > 0) {    ps_process_raw(ps, adbuf, k, FALSE, FALSE);    in_speech = ps_get_in_speech(ps);    if (in_speech && !utt_started) {        utt_started = TRUE;    }     if (!in_speech && utt_started) {        ps_end_utt(ps);        hyp = ps_get_hyp(ps, NULL);        if (hyp != NULL)    printf("%s\n", hyp);        fflush(stdout);        ps_start_utt(ps);        utt_started = FALSE;    }}```

5. Поиск оптимальных параметров Pocketsphinx эмпирическим методом


После создания или скачивая языковой модели в файлах модели возможно найти файл feat.params. Данный файл необходим для задания параметров работы Pocketshinx. Если параметра нет в данном файле, то используется значение по умолчанию, список всех параметров и их описание возможно найти в исходном коде проекта или на сайте mankier.

Пример вывода программы pocketsphinx_continuous при использовании языковой модели.

```$ pocketsphinx_continuous -samprate 8000 -lm ru.lm -dict ru.dic -hmm zero_ru.cd_cont_4000 -remove_noise no -infile decoder-test.wavINFO: pocketsphinx.c(152): Parsed model-specific feature parameters from zero_ru.cd_cont_4000/feat.paramsCurrent configuration:



```

Распознавание из аудио файла или из данных микрофона определяется ключами:

`-infile <file-name>` и `-inmic <yes/no>`

соответственно.

Ключ файл языковой модели:

`-lm ru.lm`

Файл словаря:

`-dict ru.dic`

Путь, где хранятся файлы внутренней кухни Pocketsphinx:

`-hmm zero_ru.cd_cont_4000`

также файл feat.params.

Существует возможность распознавать в речи только часть слов языковой модели. Функцию включает ключ:

`-kws <file-name.txt>`

Например, наш файл с ключевой фразой выглядит так:

```окей скай /1e-15/```

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

Итог


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

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

Работа с Pocketsphinx состоит из этапов:

  1. Подготовка записей в соответствии с текущей задачей.
  2. Подготовка базы данных для модели.
  3. Построение языковой модели: обучение модели на полученных записях. Получение Pocketsphinx model and language model.
  4. Реализация алгоритма использования API Pocketsphinx.
  5. Поиск оптимальных параметров Pocketsphinx эмпирическим методом.

Сейчас мы собираем все вместе.
Подробнее..

Создаем Swift Package на основе Cбиблиотеки

13.01.2021 02:07:50 | Автор: admin
Фото Kira auf der HeideнаUnsplashФото Kira auf der HeideнаUnsplash

Данная статья поможет вам создать свой первый Swift Package. Мы воспользуемся популярной C++ библиотекой для линейной алгебры Eigen, чтобы продемонстрировать, как можно обращаться к ней из Swift. Для простоты, мы портируем только часть возможностей Eigen.


Трудности взаимодействия C++ и Swift

Использование C++ кода из Swift в общем случае достаточно трудная задача. Все сильно зависит от того, какой именно код вы хотите портировать. Данные 2 языка не имеют соответствия API один-к-одному. Для подмножества языка C++ существуют автоматические генераторы Swift интерфейса (например,Scapix,Gluecodium). Они могут помочь вам, если разрабатывая библиотеку, вы готовы следовать некоторым ограничениям, чтобы ваш код просто конвертировался в другие языки. Тем не менее, если вы хотите портировать чужую библиотеку, то, как правило, это будет не просто. В таких ситуациях зачастую ваш единственный выбор: написать обертку вручную.

Команда Swift уже предоставляет interop дляCиObjective-Cв их инструментарии. В то же время,C++ interopтолько запланирован и не имеет четких временных рамок по реализации. Одна из сложно портируемых возможностей C++ шаблоны. Может показаться, что темплейты в C++ и дженерики в Swift схожи. Тем не менее, у них есть важные отличия. На момент написания данной статьи, Swift не поддерживает параметры шаблона не являющиеся типом, template template параметры и variadic параметры. Также, дженерики в Swift определяются для типов параметров, которые соблюдают объявленные ограничения (похоже на C++20 concepts). Также, в C++ шаблоны подставляют конкретный тип в месте вызова шаблона и проверяют поддерживает ли тип используемый синтаксис внутри шаблона.

Итого, если вам нужно портировать C++ библиотеку с обилием шаблонов, то ожидайте сложностей!


Постановка задачи

Давайте попробуем портировать вручную С++ библиотеку Eigen, в которой активно используются шаблоны. Эта популярная библиотека для линейной алгебры содержит определения для матриц, векторов и численных алгоритмов над ними. Базовой стратегией нашей обертки будет: выбрать конкретный тип, обернуть его в Objective-C класс, который будет импортироваться в Swift.

Один из способов импортировать Objective-C API в Swift это добавить C++ библиотеку напрямую в Xcode проект и написатьbridging header. Тем не менее, обычно удобнее, когда обертка компилируется в качестве отдельного модуля. В этом случае, вам понадобится помощь менеджера пакетов. Команда Swift активно продвигаетSwift Package Manager (SPM). Исторически, в SPM отсутствовали некоторые важные возможности, из-за чего многие разработчики не могли перейти на него. Однако, SPM активно улучшался с момента его создания. В Xcode 12, вы можете добавлять в пакет произвольные ресурсы и даже попробовать пакет в Swift playground.

В данной статье мы создадим SPM пакетSwiftyEigen. В качестве конкретного типа мы возьмем вещественную float матрицу с произвольным числом строк и колонок. КлассMatrixбудет иметь конструктор, индексатор и метод вычисляющий обратную матрицу. Полный проект можно найти наGitHub.


Структура проекта

SPM имеет удобный шаблон для создания новой библиотеки:

foo@bar:~$ mkdir SwiftyEigen && cd SwiftyEigenfoo@bar:~/SwiftyEigen$ swift package initfoo@bar:~/SwiftyEigen$ git init && git add . && git commit -m 'Initial commit'

Далее, мы добавляем стороннюю библиотеку (Eigen) в качестве сабмодуля:

foo@bar:~/SwiftyEigen$ git submodule add https://gitlab.com/libeigen/eigen Sources/CPPfoo@bar:~/SwiftyEigen$ cd Sources/CPP && git checkout 3.3.9

Отредактируем манифест нашего пакета,Package.swift:

// swift-tools-version:5.3import PackageDescriptionlet package = Package(    name: "SwiftyEigen",    products: [        .library(            name: "SwiftyEigen",            targets: ["ObjCEigen", "SwiftyEigen"]        )    ],    dependencies: [],    targets: [        .target(            name: "ObjCEigen",            path: "Sources/ObjC",            cxxSettings: [                .headerSearchPath("../CPP/"),                .define("EIGEN_MPL2_ONLY")            ]        ),        .target(            name: "SwiftyEigen",            dependencies: ["ObjCEigen"],            path: "Sources/Swift"        )    ])

Манифест является рецептом для компиляции пакета. Сборочная система Swift соберет два отдельных таргета для Objective-C и Swift кода. SPM не позволяет смешивать несколько языков в одном таргете. Таргет ObjCEigen использует файлы из папкиSources/ObjC, добавляет папкуSources/CPP в header search paths, и опеделяетEIGEN_MPL2_ONLY, чтобы гарантировать лицензию MPL2 при использовании Eigen. ТаргетSwiftyEigen зависит отObjCEigen и использует файлы из папкиSources/Swift.


Ручная обертка

Теперь напишем заголовочный файл для Objective-C класса в папкеSources/ObjCEigen/include:

#pragma once#import <Foundation/Foundation.h>NS_ASSUME_NONNULL_BEGIN@interface EIGMatrix: NSObject@property (readonly) ptrdiff_t rows;@property (readonly) ptrdiff_t cols;- (instancetype)init NS_UNAVAILABLE;+ (instancetype)matrixWithZeros:(ptrdiff_t)rows cols:(ptrdiff_t)colsNS_SWIFT_NAME(zeros(rows:cols:));+ (instancetype)matrixWithIdentity:(ptrdiff_t)rows cols:(ptrdiff_t)colsNS_SWIFT_NAME(identity(rows:cols:));- (float)valueAtRow:(ptrdiff_t)row col:(ptrdiff_t)colNS_SWIFT_NAME(value(row:col:));- (void)setValue:(float)value row:(ptrdiff_t)row col:(ptrdiff_t)colNS_SWIFT_NAME(setValue(_:row:col:));- (EIGMatrix*)inverse;@endNS_ASSUME_NONNULL_END

У класса есть readonly свойства rows и cols, конструктор для нулевой и единичной матрицы, способы получить и изменить отдельные значения, и метод вычисления обратной матрицы.

Дальше напишем файл реализации вSources/ObjCEigen:

#import "EIGMatrix.h"#pragma clang diagnostic push#pragma clang diagnostic ignored "-Wdocumentation"#import <Eigen/Dense>#pragma clang diagnostic pop#import <iostream>using Matrix = Eigen::Matrix<float, Eigen::Dynamic, Eigen::Dynamic>;using Map = Eigen::Map<Matrix>;@interface EIGMatrix ()@property (readonly) Matrix matrix;- (instancetype)initWithMatrix:(Matrix)matrix;@end@implementation EIGMatrix- (instancetype)initWithMatrix:(Matrix)matrix {    self = [super init];    _matrix = matrix;    return self;}- (ptrdiff_t)rows {    return _matrix.rows();}- (ptrdiff_t)cols {    return _matrix.cols();}+ (instancetype)matrixWithZeros:(ptrdiff_t)rows cols:(ptrdiff_t)cols {    return [[EIGMatrix alloc] initWithMatrix:Matrix::Zero(rows, cols)];}+ (instancetype)matrixWithIdentity:(ptrdiff_t)rows cols:(ptrdiff_t)cols {    return [[EIGMatrix alloc] initWithMatrix:Matrix::Identity(rows, cols)];}- (float)valueAtRow:(ptrdiff_t)row col:(ptrdiff_t)col {    return _matrix(row, col);}- (void)setValue:(float)value row:(ptrdiff_t)row col:(ptrdiff_t)col {    _matrix(row, col) = value;}- (instancetype)inverse {    const Matrix result = _matrix.inverse();    return [[EIGMatrix alloc] initWithMatrix:result];}- (NSString*)description {    std::stringstream buffer;    buffer << _matrix;    const std::string string = buffer.str();    return [NSString stringWithUTF8String:string.c_str()];}@end

Теперь сделаем Objective-C код видимым из Swift с помощью файла вSources/Swift(смотритеSwift Forums):

@_exported import ObjCEigen

И добавим индексирование для более чистого API:

extension EIGMatrix {    public subscript(row: Int, col: Int) -> Float {        get { return value(row: row, col: col) }        set { setValue(newValue, row: row, col: col) }    }}

Пример использования

Теперь мы можем воспользоваться классом вот так:

import SwiftyEigen// Create a new 3x3 identity matrixlet matrix = EIGMatrix.identity(rows: 3, cols: 3)// Change a specific valuelet row = 0let col = 1matrix[row, col] = -2// Calculate the inverse of a matrixlet inverseMatrix = matrix.inverse()

Наконец, мы можем составить простой проект, который продемонстрирует возможности нашего пакета, SwiftyEigen. Приложение позволит вносить значения в матрицу 2x2 и вычислять обратную матрицу. Для этого, создаем новый iOS проект в Xcode, перетаскиваем папку с пакетом из Finder в project navigator, чтобы добавить локальную зависимость, и добавляем фреймворк SwiftyEigen в общие настройки проекта. Далее пишем UI и радуемся:

Смотрите полный проект наGitHub.


Ссылки

Спасибо за внимание!

Подробнее..

Как разработать аналог Zoom для ТВ-приставок на RDK и Linux. Разбираемся с фреймворком GStreamer

29.09.2020 16:20:30 | Автор: admin
Сценарии: как использовать приложение для видеоконференций на SmartTV и ТВ-приставкахСценарии: как использовать приложение для видеоконференций на SmartTV и ТВ-приставках

Пандемия COVID-19 стала катализатором для новых полезных сервисов. Например, Zoom стал настолько успешным, что по стоимости обогнал в этом месяце IBM. Нас вдохновил этот пример, и мы решили пойти еще дальше: а что если онлайн-конференции реализовать на приставках и Smart TV, чтобы общаться не только по работе, но устраивать удаленные посиделки на диване с друзьями? Но ведь тогда можно на футболе вместе покричать, и кино посмотреть или спортом заняться под контролем тренера.

Почему-то у операторов цифрового ТВ такой услуги не оказалось, хотя с инженерной точки зрения все эти функции вполне можно реализовать на ТВ-приставках на базе Linux/Android и RDK. Мы это проверили на практике и вот теперь делимся с читателями Хабра своим рецептом создания аналога Zoom и видеоконференций через Smart TV. Разберем архитектуру решения и кодирование видеопотока с использованием GStreamer. Информацию для работы с этим фреймворком мы собирали по крупицам, но оно того стоило.

Разбираемся с архитектурой

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

Итак, с чем мы столкнемся на ТВ-приставках:

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

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

  3. Сетевые ограничения. Многие приставки работают как по Ethernet так и по wifi. Сжатие и передача видео/аудиопотока еще одно узкое место в приложениях такого рода.

  4. Безопасность передачи потока и др. вопросы безопасности данных.

  5. Поддержка камер и микрофонов на встроенных платформах.

Теперь можно разложить по полочкам и саму архитектуру. Аналог Zoom для ТВ-приставок будет состоять из нескольких больших компонентов и модулей:

  • Захват видеопотока

  • Захват аудиопотока

  • Сетевой модуль

  • Модуль кодирования видео/аудиопотока

  • Модуль декодирования видео/аудиопотока

  • Вывод видеоконференции на экран

  • Вывод звука

  • Цветовые преобразования

  • Несколько других второстепенных компонентов

В упрощенном виде один из вариантов архитектуры можно изобразить так:

Архитектура приложения для видеоконференций через Smart TVАрхитектура приложения для видеоконференций через Smart TV

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

Кодирование/декодирование потока

1) Преимущества GStreamer

Как мы уже отметили, одним из узких мест является передача видеопотока. Предположим у вас есть камера, отдающие кадры на скорости 30 кадров с секунду на небольшом разрешении 640x480. Итого, в RGB24 получается:

640 х 480 х 3 х 30 = 27 648 000 байт в секунду, т.е. более 26 Мбайт в секунду, что по понятным причинам совершенно неприемлемо в рамках сетевой передачи.

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

  1. Хорошая кроссплатформенность и отличная поддержка Linux и Android.

  2. На RDK Gstreamer является стандартом кодирования/декодирования и включен в дистрибутив по-умолчанию.

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

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

  5. API для работы С/C++ идет комплекте.

  6. Поддержка аппаратных кодеров/декодеров, в частности OpenMAX API что крайне важно при работе с ТВ-приставками.

2) Знакомимся с GStreamer разбираемся с конвейерами

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

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

gst-launch-1.0 позволяет запустить любой конвейер (pipeline).

В GStreamer используется схема декодирования, по которой поток последовательно проходит через разные компоненты, начиная с source, заканчивая sink-выводом. При этом source может быть любым файл, устройство, для вывода (sink) также можно указать нужное файл, экран, сетевые выводы и протоколы (наподобие RTP).

Подробнее про использование утилит и принцип построения конвейера можно почитать в другой статье на Хабре. Классический пример проигрывания mp4-файла:

gst-launch-1.0 filesrc location=file.mp4 ! qtdemux ! h264parse ! avdec_h264 ! videoconvert ! autovideosink

На входе принимается mp4-файл, который проходит через демуксер mp4 qtdemux, парсер h264, затем через декодер, конвертер и, наконец, идет вывод на экран.

Можно заменить autovideosink на filesink с параметром файла и вывести декодированный поток прямиком в файл.

3) Пишем приложение на GStreamer C/C++ API. Попробуем декодировать

Теперь, когда мы разобрались немного с использованием gst-launch-1.0, проделаем все тоже самое, только уже в рамках приложения. Принцип останется тем же: мы встраиваем декодирующий конвейер (pipeline), однако уже с использованием библиотеки GStreamer и glib-событий.

О простейшем примере построения filesrc в filesink с кодом хорошо рассказано в другой хабрастатье GStreamer: элементы и контейнеры. Мы же рассмотрим чуть более реалистичный и сложный живой пример H264-декодирования.

Инициализация GStreamer-приложения происходит один раз при помощи

gstinit (NULL, NULL);

Если вы хотите сразу видеть происходящее в деталях, можно выстроить уровень логирования перед инициализацией

gst_debug_set_active(TRUE);gst_debug_set_default_threshold(GST_LEVEL_LOG);

Учтите: сколько бы у вас не было конвейеров в приложении, инициализировать gstinit достаточно один раз.

Создадим новый event-loop, в котором будут обрабатываться события:

GMainLoop *loop;loop = g_main_loop_new (NULL, FALSE);

И теперь можно начать выстраивать наш конвейер:

Объявим необходимые элементы, в частности сам конвейер как тип GstElement:

GstElement *pipeline, *source, *demuxer, *parser, *decoder, *conv, *sink;pipeline = gst_pipeline_new ("video-decoder");source   = gst_element_factory_make ("filesrc",       "file-source");demuxer  = gst_element_factory_make ("qtdemux",      "h264-demuxer");parser   = gst_element_factory_make ("h264parse",      "h264-parser");decoder  = gst_element_factory_make ("avdec_h264",     "h264-decoder");conv     = gst_element_factory_make ("videoconvert",  "converter");sink     = gst_element_factory_make ("appsink", "video-output");

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

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

if (!pipeline || !source || !demuxer || !parser || !decoder || !conv || !sink) {// не инициализирован один элемент - остановкаreturn;}

Установим тот самый параметр location через gob_ject_set:

gob_ject_set (G_OBJECT (source), "location", argv[1], NULL);

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

Теперь необходим обработчик сообщений GStreamer, создадим соответствующий bus_call:

GstBus *bus;guint bus_watch_id;bus = gst_pipeline_get_bus (GST_PIPELINE (pipeline));bus_watch_id = gst_bus_add_watch (bus, bus_call, loop);gst_object_unref (bus);

gst_object_unref и другие подобные вызовы нужны для очищения выделенных объектов.

Затем объявим сам обработчик сообщений:

static gbooleanbus_call (GstBus     *bus,          GstMessage *msg,          gpointer    data){  GMainLoop *loop = (GMainLoop *) data;  switch (GST_MESSAGE_TYPE (msg)) {    case GST_MESSAGE_EOS:      LOGI ("End of stream\n");      g_main_loop_quit (loop);      break;    case GST_MESSAGE_ERROR: {      gchar  *debug;      GError *error;      gst_message_parse_error (msg, &error, &debug);      g_free (debug);      LOGE ("Error: %s\n", error->message);      g_error_free (error);      g_main_loop_quit (loop);      break;    }    default:      break;  }  return TRUE;}

А теперь самое важное: собираем и добавляем все созданные элементы в единый конвейер, тот самый что выстраивали через gst-launch. Очередность добавления, естественно, важна:

gst_bin_add_many (GST_BIN (pipeline), source, demuxer, parser, decoder, conv, sink, NULL);gst_element_link_many (source, demuxer, parser, decoder, conv, sink, NULL);

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

  gst_element_link (source, demuxer);  gst_element_link_many (parser, decoder, conv, sink, NULL);  g_signal_connect (demuxer, "pad-added", G_CALLBACK (on_pad_added), parser);static voidon_pad_added (GstElement *element,              GstPad     *pad,              gpointer    data){  GstPad *sinkpad;  GstElement *decoder = (GstElement *) data;  /* We can now link this pad with the sink pad */  g_print ("Dynamic pad created, linking demuxer/decoder\n");  sinkpad = gst_element_get_static_pad (decoder, "sink");  gst_pad_link (pad, sinkpad);  gst_object_unref (sinkpad);}

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

Ну и, наконец, переводим состояние конвейера в проигрывание:

gst_element_set_state (pipeline, GST_STATE_PLAYING);

И запускаем event-loop:

g_main_loop_run (loop);

После окончания этой процедуры все нужно почистить:

gst_element_set_state (pipeline, GST_STATE_NULL);  gst_object_unref (GST_OBJECT (pipeline));  g_source_remove (bus_watch_id);  g_main_loop_unref (loop);

4) Выбираем энкодеры и декодеры. Фоллбэки

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

Поможет нам в этом функция gst_element_factory_find, которая проверяет, есть ли у нас кодек в factory элементов:

if(gst_element_factory_find("omxh264dec"))decoder  = gst_element_factory_make ("omxh264dec",     "h264-decoder");elsedecoder  = gst_element_factory_make ("avdec_h264",     "h264-decoder");

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

Другой крайне полезной, но еще более редко используемой функцией является проверка того, что же мы на самом деле инициализировали в GstElement (какой кодек):

gst_plugin_feature_get_name(gst_element_get_factory(encoder))

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

5) Цветовые модели видео

Не стоит оставлять без внимания и цветовые модели, раз уж мы заговорили о кодировании видео с камер. И тут чаще подразумевается YUV, нежели RGB.

Камеры просто обожают цветовую модель YUYV. Но это то, с чем любит работать GStreamer, для него более привычна модель I420. На выходе, если речь не идет о выводе в gl-фрейм, у нас также будут выходить I420-кадры. Будьте готовы подставить необходимые фильтры и выполнить преобразования. Некоторые энкодеры умеют работать и с другими цветовыми моделями, но чаще это исключения из правил.

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

Разбираемся с буферами и берем данные на лету

1) Буфер ввода

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

Теперь будем работать с буферами и входами и выходами appsrc / appsink. Почему-то этому вопросу почти не уделиливнимания в официальной документации.

Как же организовать постоянный поток данных в созданных конвейерах, а точнее подать буфер на выход и получить кодированный или декодированный буфер выхода? Допустим мы получили картинку с камеры, и нам нужно ее закодировать. Мы уже определились с тем, что нам потребуется фрейм в I420. Допустим он у нас есть, что дальше? Как пропустить картинку через весь поток конвейера?

Вначале установим обработчик события need-data, который будет запускаться при необходимости подачи данных в конвейер и начнем подавать входной буфер:

g_signal_connect (source, "need-data", G_CALLBACK (encoder_cb_need_data), NULL);

Сам обработчик имеет следующий вид:

encoder_cb_need_data (GstElement *appsrc,          guint       unused_size,          gpointer    user_data){  GstBuffer *buffer;  GstFlowReturn ret;  GstMapInfo map;   int size;   uint8_t* image;  // get image  buffer = gst_buffer_new_allocate (NULL, size, NULL);  gst_buffer_map (buffer, &map, GST_MAP_WRITE);    memcpy((guchar *)map.data, image,  gst_buffer_get_size( buffer ) );  gst_buffer_unmap(buffer, &map);  g_signal_emit_by_name (appsrc, "push-buffer", buffer, &ret);  gst_buffer_unref(buffer);}

image это, условно говоря, псевдокод буфера картинки в I420.

Далее мы создаем через gst_buffer_new_allocate буфер необходимого размера, который будет соответствовать размеру буфера картинки.

При помощи gst_buffer_map устанавливаем буфер в режим записи и, используя memcpy, копируем нашу картинку в созданный буфер.

И, наконец, сигнализируем GStreamу о том, что буфер готов.

Ремарка: очень важно после записи использовать gst_buffer_unmap, а также очищать буфер после использования при помощи gst_buffer_unref. Иначе будет утечка памяти. В скудном количестве доступных примеров никто особо насчет вопроса освобождения памяти не заморачивался, а он, понятное дело, важный.

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

Делается это перед установкой обработчика сигнала need-data:

g_object_set (G_OBJECT (source),        "stream-type", 0,        "format", GST_FORMAT_TIME, NULL);g_object_set (G_OBJECT (source), "caps",gst_caps_new_simple ("video/x-raw","format", G_TYPE_STRING, "I420","width", G_TYPE_INT, 640,"height", G_TYPE_INT, 480,"framerate", GST_TYPE_FRACTION, 30, 1,NULL), NULL);

Как и все параметры GstElement, установка параметров осуществляется через g_object_set.

В данном случае мы задали тип потока, и его caps формат данных. Указав, что на выход appsrc будет поступать данные I420 c разрешением 640x480 и частотой 30 кадров в секунду.

Частота в нашем случае, да в целом, не играет роли. На практике мы не замечали, чтобы GStreamer как-то ограничивал вызовы need-data по частоте.

Готово, теперь наши кадры поступают в энкодер.

2) Буфер вывода

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

Подключаем обработчик к sink pad:

GstPad *pad = gst_element_get_static_pad (sink, "sink");  gst_pad_add_probe  (pad, GST_PAD_PROBE_TYPE_BUFFER, encoder_cb_have_data, NULL, NULL);  gst_object_unref (pad);

Похожим образом мы подключились к другому событию sink pad GST_PAD_PROBE_TYPE_BUFFER, которое будет вызываться по мере поступления буфера данных в sink pad.

static GstPadProbeReturnencoder_cb_have_data (GstPad * pad,                        GstPadProbeInfo * info,                        gpointer user_data) {  GstBuffer *buf = gst_pad_probe_info_get_buffer (info);  GstMemory *bufMem = gst_buffer_get_memory(buf, 0);  GstMapInfo bufInfo;  gst_memory_map(bufMem, &bufInfo, GST_MAP_READ);  // bufInfo.data, bufInfo.size  gst_memory_unmap(bufMem, &bufInfo);  return GST_PAD_PROBE_OK;}

Колбэк имеет похожую структуру. Теперь нужно достучаться до памяти буфера. Вначале получаем GstBuffer, затем указатель его памяти, используя gst_buffer_get_memory по индексу 0 (как правило задействован только он). И, наконец, используя gst_memory_map, получаем адрес буфера данных bufInfo.data и его размер bufInfo.size.

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

Итак, мы рассмотрели ключевые и самые интересные компоненты из приложения для Smart TV аналога Zoom для ТВ-приставок: архитектуру, модули кодирования / декодирования через GStreamer, буферы ввода / вывода и используемые цветовые преобразования.

Для операторов цифрового ТВ такая программная платформа может стать новым абонентским сервисом. Для нас инженеров новым интересным embedded-проектом для реализации на разных приставках на базе RDK, Linux и Android. А для всех остальных возможностью приятно проводить время за совместным просмотром фильмов и спортивных матчей, занятий спортом и посиделок с близкими в период карантина или удаленной работы.

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

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    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