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

Linux kernel

Вышел Linux 5.8 один из величайших. Более подробный обзор

24.08.2020 14:14:04 | Автор: admin

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

Особенностью ядра 5.8 стало рекордное количество изменений за все время существование проекта. Был лишь один более крупный релиз ядра Linux 4.9, но тот был искусственно раздут из-за новой подсистемы greybus в то время, как 5.8 действительно вобрал в себя больше изменений по самым разным направлениям. Посмотрим, что есть такого полезного и интересного в Linux 5.8, а заодно и 5.8.1 главным образом для обычных пользователей и рабочих станций.

Патч размером 64 MiB содержит труд 1991 разработчиков, в т. ч. 304 новых. Усилиями сообщества добавлено 904 тыс. и удалено 553 тыс. строк кода. Если смотреть в разрезе компаний, то среди самых активных есть знакомые имена.

  • Intel;
  • Huawei Technologies;
  • Habana Labs;
  • Red Hat;
  • Google;
  • Linaro;
  • IBM;
  • AMD;
  • Mellanox;
  • SUSE;

Графика


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

Поддержка GPU драйверов Adreno 405 / 640 / 650


Благодаря обновлению открытых драйверов MSM (Freedreno), добавлена поддержка графических процессоров Qualcomm Adreno 405, 640 и 650. Эти мобильные GPU можно увидеть на некоторых из последних SoC (system on a chip), таких, как Snapdragon 855+ и Snapdragon 865. Эти чипы можно встретить в смартфонах Samsung Galaxy S10 / S20, OnePlus и многих других гаджетах.

В то же время в рамках открытых API OpenGL и Vulkan продолжают развиваться драйвера Adreno, соответственно с Freedreno Gallium3D и TURNIP. Новые изменения войдут в Mesa 20.2-devel.

Многочисленные улучшения драйверов amdgpu


AMD из кожи вон лезла, чтобы улучшить качество графических драйверов в Linux 5.8. Наряду с повышением производительности, реализована возможность использования шифрованных буферов видеопамяти (Trusted Memory Zone).

AMD Radeon Trusted Memory Zone предназначена для защиты избранных страниц от чтения со стороны центрального процессора, а также иных отличных от GPU клиентов и предотвращения записи на защищенные TMZ страницы.

Кроме того в драйвер добавлена поддержка пиксельного формата FP16 и завершена обширная работа по предоставлению однорангового совместного использования DMA буферов между устройствами (p2p DMA-BUF) без поддержки страниц системной памяти.

Файловые системы


Ожидаемые обновления вызывающей противоречивые чувства Btrfs, FAT, exFAT и других.

Разнообразные усовершенствования Btrfs


OpenSUSE и SUSE Linux одни из немногих, использующих Btrfs в качестве основной файловой системы. Не удивительно, что SUSE представила ряд патчей для этой ФС. В результате для Btrfs была улучшена обработка операций чтения в режиме I/O.

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

Сжатие с использованием алгоритма LZO-RLE в F2FS


Заметным новым дополнением для Flash Friendly File System в новой версии ядра Linux является формат сжатия LZO-RLE. LZO Run-Length Encoding нацелена на те же степени сжатия, что и у стандартного алгоритма LZO, но с более высокой производительностью. Это стало возможным после того, как реализация LZO-RLE была внесена в ядро в прошлом году ARM.

Поддержка верификации загрузочной области exFAT


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

Повышение I/O производительности SMB v3 / CIFS


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

Улучшение упреждающей загрузки элементов в FAT


Опережающее чтение для записей FAT в предыдущее версии было очень простым, но имело некоторые недостатки, из-за чего в некоторых средах оно работало не эффективно.
Благодаря механизму обновления окна опережающего чтения до полного исчерпания и настраиваемому bdi->ra_pages удалось добиться значительного повышения производительности. На медленном 2TB USB-накопителе провели тест, результат сокращение времени прохождение теста с 383 до 51 с.

Поддержка DAX для Ext4 и XFS


DAX, или Direct Acces предназначен для прямого доступа к файловой системе в обход страничного кеша. Минуя кеширования страниц в режиме DAX означает отказ от дополнительной копии для чтения / записи на устройство хранения. В свою очередь это позволяет напрямую и более эффективно отображать устройство хранения в пользовательское пространство.

Данные усовершенствования DAX кода со стороны компании Intel позволяют активировать режим прямого доступа для каждого inode, а не просто включать, или выключать для всей файловой системы. Также с помощью системного вызова statx() можно из пользовательского пространства запрашивать состояние прямого доступа к конкретному файлу.

Сетевая подсистема


TCP/IP стек ядра Linux занимает особое место в общей картины ввиду существенной сложности архитектуры и огромным возможностям по использованию в самых разнообразных устройствах. Сетевая инфраструктура больше других обеспечивает доминирование Linux ОС на серверах и продолжает развиваться семимильными шагами.

  • Netfilter, в модуле nftables обеспечено динамические обновления устройств для таблиц потоков;
  • *MRP, добавлена поддержка Media Redundancy Protocol сетевого стандарта IEC 62439-2. Протокол позволяет обеспечить устойчивость к сбоям в сети, объединив в кольцо несколько Ethernet коммутаторов, при этом работает быстрее, чем STP.
  • bluetooth, новые драйвера для Realtek RTL8761B, Intel Typhoon Peak и Qualcomm QCA6390;
  • lx5/mlx5e, сетевые карты Mellanox обычно используются в критически важных установках, где требуются скорости 10, 40 и 100 гигабит. Обширный список изменений для драйверов mlx5 и mlx5e касается улучшений в TLS, IPsec, MPLS и пр;
  • rnbd, новое сетевое блочное устройство RNBD позволяет организовать удаленный доступ к блочному при помощи RDMA транспорта RTRS. Последний поддерживает работу с несколькими путями, обеспечивает переключение ввода-вывода и балансировку нагрузки.

Обновления Wi-Fi


  • Драйвер ath10k наконец-то получил поддержку режимов VHT160 и VHT80+80. Это должно привести к значительно более высокой скорости беспроводной связи для пользователей систем с беспроводной картой Qualcomm Atheros IEEE 802.11ac и точкой доступа Wifi 5(802.11ac).
  • Драйвер iwlwifi стал поддерживать ACPI DSM (device specific method) для новых чипов Intel.

Обновления Ipv6


  • Ipv6 стек обзавелся поддержкой MPLS;
  • Добавлена инкапсуляции IPv6 для ESP через транспорт UDP и TCP;
  • Реализована поддержка RFC 6069. Стандарт описывает алгоритм TCP Long Connectivity Disruptions, обеспечивающий возможность сделать транспорт TCP более устойчивым к длительным сбоям связи.

Безопасность


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

Исправления по устранению уязвимости Spectre


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

Речь идёт о нескольких срочных исправлений для ядра Linux по обработки спекулятивного выполнения x86 / x86_64 инструкций. Это случилось после того, как инженер безопасности из Google обнаружил данную уязвимость, и тот факт, что одно из предыдущих исправлений, в качестве побочного эффекта затронула процессоры AMD.

Indirect Branch Prediction Barrier (IBPB) защита от атак целевого буфера ветвлений для второго варианта Spectre, будет принудительно выключена в Linux. Условием являются сценарии, когда STIBP недоступен или доступны косвенные ограниченные спекуляции ветвлений (IBRS).

Чипы и чипсеты


В Linux 5.8 было много изменений для архитектуры ARM.

Начальная поддержка загрузки процессоров Power 10


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

Помимо базовой поддержки загрузки для архитектуры Power 10, ядро Linux 5.8 также поддерживает набор инструкций с префиксом Power 10.

Поддержка новых ARM SoC


В соответствии с прогрессом подсистемы Linux ядра для Android к списку поддерживаемых устройств добавились:

  • Realtek RTD1195 в качестве решения на базе Arm Cortex A7;
  • Realtek RTD1395;
  • Realtek RTD1619;
  • Платформа Renesas RZ/G1H;
  • Бюджетный 64-битный Rockchips RK 3326 SoC вместе с игровой консолью Odroid-GO Advance;
  • TV-приставка Smartlabs SML-5442TW на базе AMLogic S905D;
  • ODROID-C4 на базе AMLogic S905X3;
  • TV-приставка Beelink GT-King Pro на базе AMLogic S922XH;
  • Baseboard Management Contrioller на базе Aspeed ast 2500 в Facebook x86 Yosemite V2 и YADRO OpenPower P9 Nicole;
  • Olimex A20-OLinuXino-LIME-eMMC SBC;
  • Маршрутизатор Check Point L-50;
  • Хромбуки Elm/Hana на базе Mediatek MT8173;
  • Смартфон Samsung Galaxy S2 на базе;
  • Платформа Qualcomm SDM660/SDM630 и смартфон Xiaomi Redmi Note 7;
  • TV-приставка Xnano X5 на базе Realtek RTD1295;

Новый AMD Energy драйвер


Новый и долгожданный драйвер AMD Energy для мониторинга датчиков энергии на процессорах Zen/Zen 2. Это позволяет получать отчеты об энергопотреблении по каждому сокету/ядру на процессорах AMD Zen/Zen2.

Интерфейс Thunderbolt/USB4 на процессорах Intel Tiger Lake


Всего лишь 28 строк кода в этом патче обеспечили поддержку Thunderbolt/USB4 для чипов Tiger Lake, в значительной степени благодаря повторному использованию наработок для Ice Lake.

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

Стабильный релиз Linux 5.8.1


Минорный релиз Linux 5.8.1 представляет собой небольшое обновление, которое изменяет в общей сложности 40 файлов с 548 вставками и 186 удалениями. Он в основном содержит исправления ошибок для различных драйверов:

  • GPIO, max77620;
  • LEDs;
  • SCSI;
  • MTD;
  • PCI, tegra;
  • USB;
  • звук;
  • видео;
  • Обновления для архитектуры ARM64;
  • Обновления для архитектуры PowerPC.

Который кернел наиболее полезен?


Всегда по выходу новой стабильной версии Linux ядра с множеством полезных обновлений, может возникнуть зуд незамедлительно скачать архив с kernel.org, подкрутить настройки из menuconfig, или xconfig, скомпилировать и загрузиться с новоиспеченного образа. Все же не стоит торопиться и вот почему.

Мейнтейнер стабильной ветки Грег Кроа-Хартман составил небольшой рейтинг стабильных веток. Вот как он выглядит.

  1. Стабильное и актуальное ядро вашего дистрибутива Linux.
  2. Последний стабильный релиз.
  3. Последний LTS (Long Term Support) релиз;
  4. Более ранний LTS релиз, который все еще поддерживается.

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

Учитывая эти соображения, имеет смысл дождаться пока новое ядро станет доступно для вашего дистрибутива. Для Debian Linux и RHEL, а также Android устройств возможно пройдут года прежде, чем новое ядро будет в боевой эксплуатации. Однако есть масса дистрибутивов для рабочих станций и обычных пользователей, для которых новое ядро становится доступным в течение нескольких недель.

Из новых дистрибутивов Ubuntu Linux 20.10 (Groovy Gorilla) выйдет с ядром Linux 5.8 в октябре 2020 г.

Использованные материалы



Подробнее..

Recovery mode Сборка ядра Linux 5.12.10 c LLVM 12 Clang и LTO оптимизацией

14.06.2021 18:13:31 | Автор: admin

Технический прогресс не стоит на месте, появляются новые компьютерные архитектуры, компиляторы становятся умнее и генерируют более быстрый машинный код. Современные задачи требуют все более креативного и эффективного решения. В данной статье пойдет речь, на мой взгляд, про один из самых прогрессивных тулчейнов LLVM и компиляторы на его основе Clang и Clang++, для языков программирования С и C++ соответственно. Хоть GCC конкурент Clang, может агрессивнее оптимизировать циклы и рекурсию, Clang дает на выходе более корректный машинный код, и чаще всего не ломает поведение приложений. Плюс оптимизация программ не заканчивается только оптимизацией циклов, поэтому Clang местами дает лучшую производительность. В GCC же за счет переоптимизации вероятность получить unpredictable behavior значительно выше. По этой причине на многих ресурсах не рекомендуют использовать -O3 и LTO(Link Time Optimization) оптимизации для сборки программ. Плюс в случае агрессивной оптимизации, размер исполняемых файлов может сильно увеличиться и программы на практике будут работать даже медленнее. Поэтому мы остановились на Clang не просто так и опции компиляции -O3 и LTO работают в нем более корректно. Плюс современные компиляторы более зрелые, и сейчас уже нет тех детских болячек переоптимизации и LTO.

Что меня побудило написать эту статью? В первую очередь это несколько фактов:

  1. Впервые прочел про сборку ядра Linux с LTO оптимизацией и Clang из новостей, где упоминалась компания Google. Она использует Clang и LTO оптимизацию для сборки ядра Linux и получения лучшей производительности. Компания Google для меня является синонимом инноваций, лучших программистов в мире и поэтому для меня ее опыт является самым авторитетным. Плюс она привнесла очень много в развитие open source, и ее наработками пользуются тысячи компаний во всем мире.
  2. Хоть компания Google начала использовать Clang и LTO оптимизацию раньше, только с выходом ядра Linux 5.12.6 и 5.12.7 было закрыто большое количество багов, и сборка ядра c LTO оптимизаций стала доступна многим. До этого при сборке ядра с LTO оптимизацией многие драйвера давали сбой.
  3. Мною уже протестирована работа ядра с LTO на Ryzen 9 3900x + AMD Radeon 5700 XT. Плюс уже давно использую LLVM 12 и Clang для сборки системных программ. Инструментарий LLVM12 и Clang стали основными в моей системе по причине лучшей поддержки моего процессора и нужные мне программы работают быстрее при сборке с помощью Clang. Для программистов Clang дает лучший контроль ошибок, оптимизации и unpredictable behavior. -fdebug-macro, -fsanitize=address, -fsanitize=memory, -fsanitize=undefined, -fsanitize=thread, -fsanitize=cfi, -fstack-protector, -fstack-protector-strong, -fstack-protector-all, -Rpass=inline, -Rpass=unroll, -Rpass=loop-vectorize, -Rpass-missed=loop-vectorize, -Rpass-analysis=loop-vectorize и т.д.
  4. Данная возможность толком нигде не была описана в связи с п.2 и есть подводные моменты, которые будут рассмотрены в данной статье.


В этой статье будет описана сборка ядра Linux 5.12.10 c LLVM 12 + Clang и LTO оптимизацией. Но так как статья получилась бы короткой, то так же бонусом будет рассмотрен вопрос как сделать утилиты LLVM 12 и Clang сборочным инструментарием по умолчанию, и какие программы и библиотеки имеет смысл собрать вручную, чтобы получить лучший отклик и производительность от системы. GCC имеет более лояльную лицензию на использование, и поэтому он установлен во многих дистрибутивах по умолчанию.

Так как в новом ядре фиксится немалое количество багов для работы с моим оборудованием(Ryzen 9 3900x + AMD Radeon 5700 XT) будет рассмотрен вопрос автоматизации сборки и установки нового ядра, чтобы это сильно не отвлекало и занимало минимум времени. Думаю многим это будет полезно. Будет рассмотрен принцип работы моего сборочного скрипта. Все действия будут проводиться в Arch Linux. Если статья будет хорошо оценена, то она станет вводной частью в серию статей про оптимизацию Linux, где будут рассмотрены внутренние механизмы ОС, и как оптимизировать их работу, будут рассмотрены вредные советы и ошибки оптимизации, и будет дан ответ на вопрос оптимизации системы Что для русского хорошо, то для немца смерть!.

Хоть тема оптимизации описывалась многократно, не мало где дают вредные советы, и некоторые механизмы ОС описаны с ошибками. Чаще всего это происходит из-за сложностей перевода или минимальной документации в интернете к компонентам ядра Linux. Где-то информация вовсе устарела. Плюс некоторые вещи понимают программисты, но не понимают системные администраторы, и наоборот. Изначально после установки Linux работает относительно медленно, но благодаря оптимизации и гибкой настройке, можно добиться более высокой производительности и значительно улучшить отклик системы. Arch Linux у меня используется как основная система, и отклик системы, производительность лучше, чем в Windows 10.
Внимание, автор статьи не несет ответственность за причиненный вред в следствии использования данной статьи! Все действия вы выполняете на свой страх и риск! Все действия должны выполнять только профессионалы!


Немного теории



LTO или Link Time Optimization это оптимизация на этапе линковки(компоновки). Чтобы понять, что такое LTO рассмотрим как работают компиляторы. В большинстве компиляторов используется двух этапная модель: этап компиляции и этап линковки.

На этапе компиляции:

Парсятся исходные тексты программ, строится AST Абстрактное Синтаксическое Дерево.

  • Оптимизируется Абстрактное Синтаксическое Дерево. Оптимизируются циклы, удаляется мертвый код, результат которого нигде не используется. Раскрываются выражения, например 2+5 можно заменить на 7, чтобы при работе приложения не вычислять его значение каждый раз и тем самым сделать его быстрее и т.д.
  • Оптимизированное дерево может быть преобразовано в машинный псевдокод понятный компилятору. Псевдокод используется для дополнительной оптимизации, упрощает разработку универсального компилятора для разных архитектур процессора, например для x86-64 и ARMv7\. Так же как ASM листинг, этот псевдокод еще используется, чтобы понять, как компилятор генерирует машинный код, и служит для понимания работы компилятора, поиска ошибок, например, ошибок оптимизации и unpredictable behavior. Стоит заметить этот этап не является обязательным и в некоторых компиляторах отсутствует.
  • Происходит векторизация. Векторизация ,Automatic Vectorization, SIMD
  • Генерируется объектный файл. Объектный файл содержит в себе машинный код для компьютера, и специальные служебные структуры, в которых все еще есть неизвестные адреса функций и данных, поэтому этот файл все еще не может быть запущен на исполнение. Чтобы разрешить неизвестные адреса, был добавлен этап линковки.


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

На этапе линковки:

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


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

В Clang используется два вида LTO Оптимизации: Full LTO и Thin LTO. Full LTO это классическая реализация LTO оптимизации, которая обрабатывает конечный исполняемый файл за раз целиком и использует много оперативной памяти. Отсюда эта оптимизация занимает много времени, но дает на выходе самый быстрый код. Thin LTO это развитие LTO оптимизации, в которой нет оптимизации всего файла целиком, а вместо этого вместе с объектными файлами записывают дополнительные метаданные, и LTO оптимизатор работает с этими данными, что дает более высокую скорость получения оптимизированного исполняемого файла (скорость сравнима с линковкой файла без LTO оптимизации) и код сравнимый или чуть уступающий в производительности Full LTO. Но самое главное Full LTO может значительно увеличить размер файла, и код наоборот может из-за этого работать медленнее. Thin LTO лишен этого недостатка и в некоторых приложениях на практике мы можем получить лучшую производительность! Поэтому наш выбор будет сборка ядра Linux с Thin LTO.

Дополнительная информация:



Установка LLVM 12 и Clang



Поставить llvm и clang можно выполнив в консоли под root команду:

pacman -Syu base-devel llvm clang lld vim

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

Прошлая версия
На момент написания статьи, в дистрибутиве Arch Linux используются LLVM и Clang версии 11\. А LLVM и Clang версии 12 находятся в staging репозитории Arch Linux [LLVM](http://personeltest.ru/aways/archlinux.org/packages/staging/x86_64/llvm/). Staging репозиторий это репозиторий, где находятся версии пакетов, которые ломают приложения, зависящие от прошлой версии. Он используется для компиляции всех зависящих программ, и когда все они будут собраны, все пакеты за раз переходит в общий репозиторий. Например, в Arch Linux от LLVM и Clang версии 11 зависят blender, rust и qt creator и т.д. Если мы поставим LLVM и Clang версии 12, то они перестанут работать.
Upd. Пакет уже перешел в основной репозиторий. Так как мною одним из первых была произведена миграция на LLVM и Clang 12, то было придумано простое решение, создать пакет [llvm11-libs](http://personeltest.ru/aways/aur.archlinux.org/packages/llvm11-libs-bin/) с необходимыми библиотеками для обратной совместимости, который позволяет оставить зависимые программы рабочими. Но данный пакет работает только с моим сборочным пакетом [llvm12-git](http://personeltest.ru/aways/aur.archlinux.org/packages/llvm12-git/). Поэтому мы будем собирать LLVM и Clang 12 из исходников. Но вы можете дождаться, когда LLVM и Clang 12 появятся в основном репозитории Arch Linux или использовать 11 версию. Лично предпочитают новые версии ПО, и LLVM и Clang 12 лучше поддерживают мой процессор Ryzen 9 3900X. Плюс git версия закрыла часть багов компилятора и даже стабильнее релиза. Релизный архив с официального сайта у меня не проходит больше тестов при сборке чем git версия. Не стоит пугаться того, что часть тестов компилятор провалил, там нет критических багов для x84-64 архитектуры, и большая часть затрагивают другие компоненты, например openmp и lldb. За очень долгое время тестирования llvm и clang 12 мною не было замечено ни одного бага влияющего на работу системы. Стоит заметить, на данный момент 13 версия является очень сырой и нам не подходит!

Поставим llvm и clang 11 версии(Если 12 версия появилась в основном репозитории, то поставится 12я версия) можно выполнив в консоли под root команду:

pacman -Syu base-devel llvm clang lld libclc vim

Обновить Arch Linux и поставить новые версии программ можно командой(это будет полезно тем кто будет ждать официального выхода 12 версии, думаю это произойдет уже через пару дней):

pacman -Syu

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


Cборка LLVM 12 из Arch User Repository



Для сборки нам понадобиться git и нам надо будет собрать программу yay.

Поставим необходимые зависимости, для этого нам будут нужны права root: pacman -Syu base-devel git go vim

Если вы хотите собрать llvm 12 с помощью clang 11, то надо поставить еще их: pacman -S llvm clang

Отредактируем конфигурационный файл сборщика пакетов makepkg в Arch Linux и увеличим количество потоков для сборки программ. Это ускорит скорость сборки. Под root выполним: vim /etc/makepkg.conf

Найдем строки MAKEFLAGS и NINJAFLAGS. Нажмем латинскую букву A. Нам после -j надо указать количество потоков для сборки. Рекомендуется ставить ваше количество ядер или потоков процессора, если ядер 4, то ставим 4 или 8\. У меня это 20, 12 ядер 24 потока, 4 остаются запасными для других задач. Или используем автоматическое определение $(nproc).

В итоге получим:

MAKEFLAGS="-j20"NINJAFLAGS="-j20"

или

MAKEFLAGS="-j$(nproc)"NINJAFLAGS="-j$(nproc)"


Нажмем ESC, дальше SHIFT + :(буква Ж). Внизу появится : строка для ввода команд, вводим wq. w write, записать изменения в файл. q quit, выйти из vim. q! выход из vim без сохранения файла. Кому сложно разобраться с vim, в Linux есть замечательная программа, называется она vimtutor. Если у вас настроена правильно локаль, то vimtutor будет на русском, запустить его можно командой vimtutor. Стоит заметить, вопреки распространенному мнению, обучение у вас не займет много времени. Обычно новичков пугают мифом: vi и vim люди изучают очень долго, и осилить их могут только единицы. На самом деле это не так и там нет ничего сложного.

Под обычным пользователем клонируем репозиторий yay, собираем и устанавливаем:
git clone https://aur.archlinux.org/yay.git && cd yay && makepkg -cfi

Импортирует открытый gpg ключ, он необходим для проверки подписи llvm12-git:
gpg --keyserver pgp.mit.edu --recv-keys 33ED753E14757D79FA17E57DC4C1F715B2B66B95

Поставим LLVM 12 и библиотеки совместимости с 11 версией. Стоит заметить, мой пакет LLVM 12 уже содержит все необходимые утилиты, включая Clang и LLD и их не надо ставить отдельно. Под обычным пользователем выполним команду: yay -Syu llvm12-git. Если llvm 12 есть в официальном репозитории, то llvm11-libs-bin не нужно ставить. Команда yay задаст вам несколько вопросов, нажмите Enter в ответ на все. Сборщик LLVM задаст 3 вопроса:

  • Build with clang and llvm toolchain? Собрать с помощью llvm и clang? Отвечаем Y или Enter если да, и N если нет. Рекомендую собирать LLVM с помощью Clang.
  • Skip build tests? Пропустить сборку тестов? Отвечаем Y или Enter. Так как во время сборки, не все тесты проходят проверку, то сборка будет прекращена. Поэтому мы пропускаем сборку тестов, и на самом деле сборка будет идти даже быстрее.
  • Skip build documentation? Пропустить сборку документации? Отвечаем Y или Enter если да, и N если нет. Если вам не нужна документация, то можно пропустить, это ускорит сборку. Лучше читать документацию на официальном сайте, это удобнее.
  • Skip build OCaml and Go bindings? Пропустить сборку OCaml и Go биндингов? Отвечаем Y или Enter если да, и N если нет. Для большинства ответ Y и их сборку можно смело пропустить в угоду скорости сборки. Для тех кому они нужны, а это очень маленькое количество людей могут ответить N.


Сборка может занять от 20 минут до пары часов. Ждете и в конце отвечаете Y на вопрос: хотите ли вы поставить собранные пакеты?

После установка LLVM надо собрать libclc12-git yay -S libclc12-git. libclc необходим для компиляции opencl и для сборки mesa.

Делаем LLVM и Clang сборочным тулчейном по умолчанию в Arch Linux



Большинство программ в Arch Linux собираются с помощью команды makepkg: man makepkg и PKGBUILD файлов. Поэтому в первую очередь внесем изменения в конфигурационный файл /etc/makepkg.conf. Выполним под root в консоли команду: vim /etc/makepkg.conf. Перейдем к строке CHOST="x86_64-pc-linux-gnu" поставим курсор на следующей пустой строке и нажмем латинскую букву A, и вставим после строки:

export CC=clangexport CXX=clang++export LD=ld.lldexport CC_LD=lldexport CXX_LD=lldexport AR=llvm-arexport NM=llvm-nmexport STRIP=llvm-stripexport OBJCOPY=llvm-objcopyexport OBJDUMP=llvm-objdumpexport READELF=llvm-readelfexport RANLIB=llvm-ranlibexport HOSTCC=clangexport HOSTCXX=clang++export HOSTAR=llvm-arexport HOSTLD=ld.lld

Дальше заменим строки CPPFLAGS, CXXFLAGS, LDFLAGS на содержимое ниже:

CFLAGS="-fdiagnostics-color=always -pipe -O2 -march=native -fstack-protector-strong"CXXFLAGS="-fdiagnostics-color=always -pipe -O2 -march=native -fstack-protector-strong"LDFLAGS="-Wl,-O1,--sort-common,--as-needed,-z,relro,-z,now"

Если вкратце мы используем -O2 оптимизацию для всех программ, -fstack-protector-strong используем улучшенную защиту стека, что снижает вероятность потенциально опасных ошибок при работе со стеком в программах, она же включена у меня в ядре. Плюс на моем процессоре при сборке с Clang с -fstack-protector-strong код при работе с целыми числами работает чуть быстрее, при работе с числами с плавающей запятой есть небольшой оверхед. В GCC наоборот есть более заметный оверхед и производительность снижается. -march=native есть смысл заменить на ваш, у меня это -march=znver2 gcc.gnu.org/onlinedocs/gcc/x86-Options.html.

Изменим количество потоков в MAKEFLAGS и NINJAFLAGS для сборки программ. Это помогает ускорить сборку программ. После -j надо указать количество потоков для сборки. Рекомендуется ставить ваше количество ядер или потоков процессора, если ядер 4, то ставим 4 или 8\. У меня это 20, 12 ядер, 24 потока, 4 остаются запасными для других задач. Или используем автоматическое определение $(nproc).

В итоге получим:

MAKEFLAGS="-j20"
NINJAFLAGS="-j20"


или

MAKEFLAGS="-j$(nproc)"
NINJAFLAGS="-j$(nproc)"


Из DEBUG_CFLAGS и DEBUG_CXXFLAGS надо удалить -fvar-tracking-assignments. LLVM не поддерживает данный параметр.

Файл должен будет принять примерно такой вид:

CARCH="x86_64"CHOST="x86_64-pc-linux-gnu"CARCH="x86_64"CHOST="x86_64-pc-linux-gnu"#-- Compiler and Linker Flagsexport CC=clangexport CXX=clang++export LD=ld.lldexport CC_LD=lldexport CXX_LD=lldexport AR=llvm-arexport NM=llvm-nmexport STRIP=llvm-stripexport OBJCOPY=llvm-objcopyexport OBJDUMP=llvm-objdumpexport READELF=llvm-readelfexport RANLIB=llvm-ranlibexport HOSTCC=clangexport HOSTCXX=clang++export HOSTAR=llvm-arexport HOSTLD=ld.lldCPPFLAGS="-D_FORTIFY_SOURCE=2"CFLAGS="-fdiagnostics-color=always -pipe -O2 -march=native -fstack-protector-strong"CXXFLAGS="-fdiagnostics-color=always -pipe -O2 -march=native -fstack-protector-strong"LDFLAGS="-Wl,-O1,--sort-common,--as-needed,-z,relro,-z,now"RUSTFLAGS="-C opt-level=2"#-- Make Flags: change this for DistCC/SMP systemsMAKEFLAGS="-j20"NINJAFLAGS="-j20"#-- Debugging flagsDEBUG_CFLAGS="-g"DEBUG_CXXFLAGS="-g"#DEBUG_CFLAGS="-g -fvar-tracking-assignments"#DEBUG_CXXFLAGS="-g -fvar-tracking-assignments"#DEBUG_RUSTFLAGS="-C debuginfo=2"

Нажмем ESC, дальше SHIFT + :(буква Ж). Внизу появится: строка для ввода команд, вводим wq. w write, записать изменения в файл. q quit, выйти из vim. q! выход из vim без сохранения файла. Кому сложно разобраться с vim, в Linux есть замечательная программа, называется она vimtutor. Если у вас настроена правильно локаль, то vimtutor будет на русском, запустить его можно командой `vimtutor`. Стоит заметить, вопреки распространенному мнению, обучение у вас не займет много времени. Обычно новичков пугают мифом: vi и vim люди изучают очень долго, и осилить их могут только единицы. На самом деле это не так и там нет ничего сложного.

Следующим этапом можно добавить настройки в файл .bashrc текущего пользователя. Не root, сборка программ под root очень плохая идея! Это относительно вредный совет и с помощью clang будут собираться все программы! Поэтому делайте это только если хорошо понимаете зачем это вам. Это можно сделать командой:

cat << 'EOF' >> "${HOME}/.bashrc"export CARCH="x86_64"export CHOST="x86_64-pc-linux-gnu"export CC=clangexport CXX=clang++export LD=ld.lldexport CC_LD=lldexport CXX_LD=lldexport AR=llvm-arexport NM=llvm-nmexport STRIP=llvm-stripexport OBJCOPY=llvm-objcopyexport OBJDUMP=llvm-objdumpexport READELF=llvm-readelfexport RANLIB=llvm-ranlibexport HOSTCC=clangexport HOSTCXX=clang++export HOSTAR=llvm-arexport HOSTLD=ld.lldexport CPPFLAGS="-D_FORTIFY_SOURCE=2"export CFLAGS="-fdiagnostics-color=always -pipe -O2 -march=native -fstack-protector-strong"export CXXFLAGS="-fdiagnostics-color=always -pipe -O2 -march=native -fstack-protector-strong"export LDFLAGS="-Wl,-O1,--sort-common,--as-needed,-z,relro,-z,now"export RUSTFLAGS="-C opt-level=2"export MAKEFLAGS="-j20"export NINJAFLAGS="-j20"export DEBUG_CFLAGS="-g"export DEBUG_CXXFLAGS="-g"EOF


Список системных библиотек и программ которые стоит собирать вручную


Внимание, сборка всех программ и все консольные команды надо выполнять под обычным пользователем, перед установкой у вас попросит пароль root. Сборка всех библиотек и программ из списка не занимает много времени. Все кроме Mesa у меня собирается в районе 1 минуты. Список дан в той в последовательности в которой рекомендуется сборка! К примеру от zlib-ng и zstd зависит Mesa, а от Mesa зависит xorg-server.

Самое первое, что надо сделать в Arch Linux это заменить zlib на zlib-ng. Это дает хороший выигрыш производительности в приложениях, которые зависят от zlib. Больше всего это заметно на веб браузерах и веб серверах, которые используют gzip сжатие для передачи данных. На высоко нагруженных серверах это дает очень значительную прибавку к производительности. Сборка довольно быстрая. Поставить можно командой(под обычным пользователем): yay -Syu zlib-ng. На вопрос хотите ли вы удалить zlib отвечайте Y. Не бойтесь библиотеки полностью взаимозаменяемы, и ничего не сломается!

Дальше у нас идет zstd это вторая по популярности библиотека используемая в ядре и в программах для сжатия данных. Поэтому имеет смысл собрать так же ее. Чтобы собрать, вам нужно скопировать содержимое zstd, создать директорию, например zstd, а в ней создать файл PKGBUILD и в него вставить содержимое по ссылке. Дальше в консоли перейти в директорию содержащую PKGBUILD, выполнить команду makepkg -cfi .

libjpeg-turbo Библиотека для работы c jpeg файлами. Ее очень часто используют браузеры и программы рабочего стола. libjpeg-turbo собранный с clang дает у меня лучшую производительность. Действия такие же, как в zstd. Создать директорию, и вставить в файл PKGBUILD содержимое по ссылке libjpeg-turbo. Дальше в консоли перейдите в директорию содержащую PKGBUILD, выполнить команду makepkg -cfi.

libpng Библиотека для работы с PNG файлами. По сборке и установке все то же самое. libpng. Для сборки вам понадобится патч: 72fa126446460347a504f3d9b90f24aed1365595.patch, его надо положить в одну директорию с файлом PKGBUILD. Для сборки надо внести изменения в PKGBUILD, заменить source и sha256sums на строки ниже, и добавить функцию prepare.

source=("https://downloads.sourceforge.net/sourceforge/$pkgname/$pkgname-$pkgver.tar.xz"  "72fa126446460347a504f3d9b90f24aed1365595.patch")sha256sums=('505e70834d35383537b6491e7ae8641f1a4bed1876dbfe361201fc80868d88ca'  '84298548e43976265f414c53dfda1b035882f2bdcacb96ed1bc0a795e430e6a8')prepare() {  cd $pkgname-$pkgver  patch --forward --strip=1 --input="${srcdir:?}/72fa126446460347a504f3d9b90f24aed1365595.patch"}


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

Для сборки вам надо отредактировать файл mesa.conf и установить необходимые вам драйвера dri, gallium, vulkan для сборки. У меня сборка только под новые видеокарты AMD. Подглядеть можно тут: Mesa OpenGL, mesa-git package, Mesa Documentation. При выходе новой версии Mesa не забудьте сменить 21.1.2 на новую версию. А после смены версии обновите контрольные суммы файлов, выполнив в директории с PKGBUILD команду updpkgsums.

xorg-server X сервер с которым взаимодействуют почти все среды рабочего стола. Сборка дает заметное улучшение отклика рабочего стола. Сборка такая же через mapkepkg -cfi. Скачать необходимые файлы для сборки можно тут: xorg-server Сборочный пакет немного кривой и собирает пакет без оптимизаций. Поэтому его надо пропатчить. Для это после строки arch-meson ${pkgbase}-$pkgver build \ надо добавить строки:

  -D debug=false \  -D optimization=2 \  -D b_ndebug=true \  -D b_lto=true \  -D b_lto_mode=thin \  -D b_pie=true \

Полный список критических важных программ влияющих на производительность системы вы можете посмотреть в поем github репозитории arch-packages. Список был создан с помощью системного профилировщика perf. Все сборочные файлы оптимизированы для сборки с помощью llvm и сборка полностью автоматизирована. На моем ryzen 9 3900x сборка всего занимает около 20 минут. Единственный пакет который невозможно собрать с помощью clang и llvm это glibc. Его надо собирать вручную, и с оптимизацией -march= под ваш процессор, это самая часто вызываемая библиотека. Сборку glibc могут проводить только профессионалы, понимающие, что они делают. Не правильная сборка может сломать систему!

Для того, что бы воспользоваться автоматизированной сборкой надо выполнить(под обычным пользователем):
git clone https://github.com/h0tc0d3/arch-packages.git && cd arch-packages && chmod +x build.sh

Дальше нам надо установить все gpg сертификаты и зависимости необходимые для сборки, выполним ./build.sh --install-keys, а затем ./build.sh --install-deps

Для сборки программ достаточно просто запустить скрипт: ./build.sh --install, скрипт вам будет задавать вопросы, какие программы хотите собрать и поставить. На вопрос: хотите ли вы отправить все ваши деньги и пароли автору статьи? хотите ли вы заменить программы?(например, zlib-ng и zlib конфликтуют. Удалить zlib? [y/N] ) ответьте Y . Если вам нужна принудительная пересборка всех программ, то надо выполнить ./build.sh --install --force. По умолчанию, если пакет был уже собран и найден с нужной версией, то он не собирается, а просто устанавливается.

Для сборки mesa надо отредактировать файл mesa/mesa.conf и установить необходимые вам драйвера dri, gallium, vulkan для сборки.

С помощью команды ./build.sh --check можно проверить различия версий в моем репозитории и в официальном, помогает быстро адаптировать сборочные файлы и собрать актуальные версии программ. Слева версия в моем репозитории, справа от стрелки в официальном. Мой репозиторий может служить удобной тренировочной точкой на пути к созданию своего дистрибутива, создания LFS и развитию навыка пересборки ПО не ломая систему.

[+] zstd 1.5.0-1[+] libpng 1.6.37-3[+] libjpeg-turbo 2.1.0-1[+] mesa 21.1.2-1[+] pixman 0.40.0-1[-] glib2 2.68.3-1 -> 2.68.2-1[+] gtk2 2.24.33-2[+] gtk3 1:3.24.29-2[+] gtk4 1:4.2.1-2[+] qt5-base 5.15.2+kde+r196-1[+] icu 69.1-1[+] freetype2 2.10.4-1[+] pango 1:1.48.5-1[+] fontconfig 2:2.13.93-4[+] harfbuzz 2.8.1-1[+] cairo 1.17.4-5[+] wayland-protocols 1.21-1[+] egl-wayland 1.1.7-1[+] xorg-server 1.20.11-1[+] xorgproto 2021.4-1[+] xorg-xauth 1.1-2[+] xorg-util-macros 1.19.3-1[+] xorg-xkbcomp 1.4.5-1[+] xorg-setxkbmap 1.3.2-2[+] kwin 5.22.0-1[+] plasma-workspace 5.22.0-2[+] glibc 2.33-5


Сборка Ядра с помощью LLVM и Clang с LTO оптимизацией


Внимание! Сборку ядра необходимо выполнять под обычным пользователем. Перед установкой ядра у вас попросит sudo пароль. Не рекомендуется использовать патчи ядра linux-ck, linux-zen, MuQSS и т.д. Мною были протестированы все, при кажущемся увеличении производительности системы, происходят кратковременные лаги и снижается стабильность системы, некоторые подсистемы ядра работают не стабильно! С выходом ядра 5.11 стандартный планировщик работает не хуже и значительно стабильнее! Единственный патч который мною применяется это патч для применения оптимизации под процессор github.com/graysky2/kernel_gcc_patch Выбрать ваш процессор можно в меню конфигуратора ядра Processor type and features-->Processor family.

Сборка ядра с помощью LLVM описана в официальной документации Linux Kernel Build with LLVM. Но там есть несколько подводных моментов, которые не описаны. Первый подводный момент заключается в OBJDUMP=llvm-objdump, тут идет переопределение objdump, но так как параметры objdump в llvm имеет другой синтаксис, то при сборке будет пропущена часть тестов для проверки корректности сборки, и будет warning ругающийся на objdump. Правильно будет оставить родной objdump OBJDUMP=objdump

Неправильно:

make CC=clang LD=ld.lld AR=llvm-ar NM=llvm-nm STRIP=llvm-strip \  READELF=llvm-readelf HOSTCC=clang HOSTCXX=clang++ \  HOSTAR=llvm-ar HOSTLD=ld.lld OBJCOPY=llvm-objcopy OBJDUMP=llvm-objdump


Правильно:

make CC=clang LD=ld.lld AR=llvm-ar NM=llvm-nm STRIP=llvm-strip \  READELF=llvm-readelf HOSTCC=clang HOSTCXX=clang++ \  HOSTAR=llvm-ar HOSTLD=ld.lld OBJCOPY=llvm-objcopy OBJDUMP=objdump

Второй подводный момент заключается в том, что если мы не добавим LLVM_IAS=1 в строку make, то нам не будет доступна LTO оптимизация в конфигураторе ядра!

Поэтому полная строка для сборки с LTO будет:

export BUILD_FLAGS="LLVM=1 LLVM_IAS=1 CC=clang CXX=clang++ LD=ld.lld AR=llvm-ar NM=llvm-nm STRIP=llvm-strip READELF=llvm-readelf HOSTCC=clang HOSTCXX=clang++ HOSTAR=llvm-ar HOSTLD=ld.lld OBJCOPY=llvm-objcopy OBJDUMP=objdump"make ${BUILD_FLAGS} -j$(nproc)

Полный список команд для сборки ядра. /tmp
надо заменить на вашу директорию куда будут распакованы исходные файлы ядра, а mykernel
надо заменить на ваш постфикс для имени ядра.

export BUILD_FLAGS="LLVM=1 LLVM_IAS=1 CC=clang CXX=clang++ LD=ld.lld AR=llvm-ar NM=llvm-nm STRIP=llvm-strip READELF=llvm-readelf HOSTCC=clang HOSTCXX=clang++ HOSTAR=llvm-ar HOSTLD=ld.lld OBJCOPY=llvm-objcopy OBJDUMP=objdump"tar -xf linux-5.12.10.tar.xz -C /tmpcd /tmp/linux-5.12.10zcat /proc/config.gz > .config # Берем конфигурацию запущенного ядра из /proc/config.gz и используем ее для сборкиecho "-mykernel" > .scmversionmake ${BUILD_FLAGS} oldconfigmake ${BUILD_FLAGS} -j$(nproc) nconfig

C помощью oldconfig конфигурация адаптируется под новое ядро и запускается конфигуратор nconfig. Подробнее о конфигураторах ядра можно прочесть в официальной документации [Kernel configurator](http://personeltest.ru/aways/www.kernel.org/doc/html/latest/kbuild/kconfig.html).

В конфигураторе переходим в General architecture-dependent option --> Link Time Optimization (LTO) и выбираем Clang ThinLTO (EXPERIMENTAL). Для дополнительной защиты стека в General architecture-dependent options ставим \* напротив Stack Protector buffer overflow detection и Strong Stack Protector. Жмем F9 и сохраняем новый конфигурационный файл. Далее идет список команд для сборки и установки нового ядра.

make ${BUILD_FLAGS} -j$(nproc)make ${BUILD_FLAGS} -j$(nproc) modulessudo make ${BUILD_FLAGS} -j$(nproc) modules_installsudo cp -v arch/x86_64/boot/bzImage /boot/vmlinuz-mykernel

Следующий подводный момент заключается в DKMS, после установки ядра собранного с помощью Clang, DKMS пытается собрать модули ядра с помощью GCC. По этой причине сборка и установка DKMS модулей в новое ядро завершается ошибкой. Решение проблемы заключается в передаче DKMS компилятора Clang таким образом:

sudo ${BUILD_FLAGS} dkms install ${dkms_module} -k 5.12.10-mykernel


Автоматизация сборки ядра Linux



Для автоматизации сборки ядра мы будем использовать мой bash скрипт github.com/h0tc0d3/kbuild. Клонируем репозиторий и перейдем в рабочую директорию: git clone https://github.com/h0tc0d3/kbuild.git && cd kbuild && chmod +x kbuild.sh

Отредактируем файл build.sh или поместим содержимое ниже в файл ${HOME}/.kbuild. Рекомендуется второй способ vim "${HOME}/.kbuild" т.к. при обновлении скрипта наши настройки сохранятся. Если использовалось клонирование репозитория git, то в директории со скриптом можно выполнить команду git pull, чтобы обновить скрипт. Ниже даны параметры по умолчанию, они формируют поведение скрипта по умолчанию, если соответствующий параметр не был передан. Эти параметры в дальнейшем можно будет переопределить с помощью параметров командной строки для скрипта. Так же можно добавить команду в ваш .bashrc. Для этого в директории со скриптом kbuild.sh надо выполнить echo "alias kbuild='${PWD}/kbuild.sh" >> "${HOME}/.bashrc", ${PWD} автоматом заменит на текущую директорию. Или из любой другой директории можно указать полный пусть к скрипту echo "alias kbuild='полный-путь/kbuild.sh'" >> "${HOME}/.bashrc" После редактирования .bashrc необходимо перезапустить терминал! Теперь можно будет запускать скрипт командой kbuild --help .

KERNEL_VERSION='5.12.10'         # Версия Linux для сборки. Любая версия с официального сайта kernel.org, включая rc версии.KERNEL_POSTFIX='noname'         # Постфикс для названия ядра. Ядро будет иметь имя версия-постфикс, 5.12.10-noname, нужно для разделения в системе ядер с одной версией.KERNEL_CONFIG='/proc/config.gz' # Конфигурационный файл ядра. Поддерживает любые текстовые файлы и с жатые с расширением gz.KERNEL_CONFIGURATOR='nconfig'   # Конфигуратор ядра nconfig, menuconfig, xconfig.# Рекомендую использовать nconfig, он лучше menuconfig.# Можно писать полную строку, например MENUCONFIG_COLOR=blackbg menuconfig# Дополнительную информацию можно найти в документации к ядру https://www.kernel.org/doc/html/latest/kbuild/kconfig.htmlMKINITCPIO=1 # Запускать "mkinitcpio -p конфигурационный_файл" После сборки? 0 - Нет, 1 - Да.MKINITCPIO_CONFIG="${KERNEL_POSTFIX}" # Имя конфигурационного файла mkinitcpio, по умолчанию равно постфиксу.CONFIGURATOR=0      # Запускать конфигуратор ядра? 0 - Нет, 1 - Да. Если вам не нужно конфигурировать ядро, то можно поставить 0.LLVM=0              # Использовать LLVM Для сборки? 1 - Да, 0 - Нет(Будет использован GCC или другой системный компилятор по умолчанию)THREADS=8           # Количество поток для сборки. Ускоряет сборку. Для автоматического определения надо заменить на $(nproc)BUILD_DIR='/tmp'    # Директория в которой будет проходить сборки ядра. У меня 32gb оперативной памяти и сборка происходит в tmpfs.DOWNLOAD_DIR=${PWD} # Директория для сохранения архивных файлов с исходниками ядра. ${PWD} - в папке из которой запущен скрипт сборки.DIST_CLEAN=0    # Если директория с исходниками существует выполнять make disclean перед сборкой? 0 - Нет, 1 - ДаCLEAN_SOURCE=0  # Выполнять make clean после сборки ядра? 0 - Нет, 1 - ДаREMOVE_SOURCE=1 # Удалять директорию с исходными файлами ядра после сборки? 0 - Нет, 1 - Да.SYSTEM_MAP=0    # Копировать System.map в /boot После сборки? 0 - Нет, 1 - Да.PATCH_SOURCE=1                          # Применять патчи ядра? 0 - Нет, 1 - Да.PATCHES=("${HOME}/confstore/gcc.patch") # Список патчей ядра. Нельзя поменять с помощью параметров скрипта.DKMS_INSTALL=1                                        # Выполнять DKMS Install? 0 - Нет, 1 - Да.DKMS_UNINSTALL=1                                      # Выполнять DKMS Uninstall? 0 - Нет, 1 - Да.DKMS_MODULES=('openrazer-driver/3.0.1' 'digimend/10') # Список DKMS модулей, который нужно собрать и установить. Нельзя поменять с помощью параметров скрипта.

Внимание! Сборку ядра необходимо выполнять под обычным пользователем. Перед установкой ядра у вас попросит sudo пароль. Не рекомендуется использовать патчи ядра linux-ck, linux-zen, MuQSS и т.д. Мною были протестированы все, при кажущемся увеличении производительности системы, происходят кратковременные лаги и снижается стабильность системы, некоторые подсистемы ядра работают не стабильно. С выходом ядра 5.11 стандартный планировщик работает не хуже и значительно стабильнее! Единственный патч который мною применяется это патч для применения оптимизации под процессор github.com/graysky2/kernel_gcc_patch. Нас интересует файл more-uarches-for-kernel-5.8+.patch. Путь к нему имеет смысл указать в PATCHES. Выбрать ваш процессор можно в меню конфигуратора ядра Processor type and features-->Processor family.

Принцип работы скрипта:

1) set -euo pipefail скрипт переходит в строгий режим, в случае ошибок скрипт завершается с ошибкой. Является хорошим тоном, при написании bash скриптов. Скрипт проверяет запущен ли он под рут, если запущен под рут, то выдает ошибку и завершается. Загружается настройки пользователя из файла ${HOME}/.kbuild

2) Скрипт проверяет существование директории linux-версия в директории BUILD_DIR. Если существует, то исходники распакованы. Перед сборкой может выполняться команда make distclean, поведение задается переменной DIST_CLEAN. Если этой директории не существует, то проверяется существование файла linux-версия.tar.gz

или linux-версия.tar.xz. Если файл найден, то он распаковывается в BUILD_DIR. Иначе файл скачивается с kernel.org в директорию DOWNLOAD_DIR.

3) Скрипт применяет патчи ядра и устанавливает постфикс для версии ядра(записывает его в файл .scmversion ).

4) Скрипт копирует настройки ядра из файла KERNEL_CONFIG в .config и выполняет make oldcofig для адаптации настроек под новое ядро и запускает конфигуратор ядра.

5) Скрипт собирает ядро и модули.

6) Скрипт удаляет модули DKMS из ядра которое сейчас запущено, если это необходимо. Это необходимо, чтобы в списке dkms status не отображались мертвые ядра. Удаляет директорию `/lib/modules/версия-постфикс` если она существует. Она существует в том случае, если мы собираем одну и туже версию несколько раз. Это дополнительная защита от unpredictable behavior .

7) Скрипт устанавливает модули ядра, копирует ядро в /boot/vmlinuz-постфикс.

8) Скрипт собирает DKMS модули и устанавливает их. Копирует System.map в /boot/System-постфикс.map, если это необходимо.

9) Обновляет загрузочный img файл для ядра. Выполняет mkinitcpio -p конфиг.

10) Выполняет make clean если необходимо. Удаляет директорию linux-версия в директории BUILD_DIR, если это необходимо.

Собрать ядро с llvm можно командой ./kbuild.sh -v 5.12.10 --llvm --start или kbuild -v 5.12.10 --llvm --start, если был установлен alias. -v 5.12.10 указывает версию ядра для сборки, --llvm указывает собирать ядро с помощью llvm и clang. --start указывает, что надо запускать конфигуратор ядра. Получить справку по параметрам скрипта можно выполнив команду kbuild --help.

Русская справка
Параметры: Описание: Пример:
--version, -v Версия ядра для сборки --version 5.12.10 | -v 5.13-rc4
--postfix, -p Постфикс ядра --postfix noname | -p noname
--config, -c Файл конфигурации ядра --config /proc/config.gz | -c /proc/config.gz
--dir, -d Директории сборки --dir /tmp | -d /tmp
--download, -z Директория загрузки --download /tmp | -z /tmp
--threads, -t Количество потоков сборки --threads 8 | -t 8
--configurator, -x Конфигуратор ядра --configurator nconfig | -x "MENUCONFIG_COLOR=blackbg menuconfig"

--start, -s Запускать конфигуратор
--disable-start, -ds Не запускать конфигуратор

--mkinitcpio, -mk Запускать mkinitcpio после установки ядра
--disable-mkinitcpio, -dmk Не запускать mkinitcpio после установки ядра
--mkinitcpio-config, -mc Конфиг mkinitcpio --mkinitcpio-config noname | -mc noname

--llvm, -l Использовать LLVM
--disable-llvm, -dl Не использовать LLVM

--patch, -ps Применять патчи ядра
--disable-patch, -dp Не применять патчи ядра

--map, -m Копировать System.map в /boot/System-постфикс.map
--disable-map, -dm Не копировать System.map

--clean, -cs Чистить исходники после сборки. make clean
--disable-clean, -dc Не чистить исходники после сборки.
--distclean, -cd Чистить исходники перед сборкой. make distclean
--disable-distclean, -dd Не чистить исходники перед сборкой.
--remove, -r Удалять директорию с исходниками после сборки
--disable-remove, -dr Не удалять директорию с исходниками после сборки

--dkms-install, -di Устанавливать DKMS модули
--disable-dkms-install, -ddi Не устанавливать DKMS модули
--dkms-uninstall, -du Деинсталлировать DKMS модули перед их установкой
--disable-dkms-uninstall, -ddu Не деинсталлировать DKMS модули перед их установкой

Список параметров которые помогают отлавливать ошибки на разных этапах и продолжить вручную:

--stop-download, -sd Стоп посл загрузки файла
--stop-extract, -se Стоп после распаковки архива с исходниками
--stop-patch, -sp Стоп после применения патчей ядрей
--stop-config, -sc Стоп после конфигуратора ядра
--stop-build, -sb Стоп после сборки ядра
--stop-install, -si Стоп после установки нового ядра и модулей




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

Всем кто дочитал до конца, спасибо! Комментарии и замечания приветствуются!


Подробнее..

Карантин для динамической памяти ядра Linux

30.12.2020 02:06:40 | Автор: admin

2020 год. Повсюду карантин. И эта статья тоже про карантин, но он другого рода.


Я расскажу об экспериментах с карантином для динамической памяти ядра Linux. Это механизм безопасности, противодействующий использованию памяти после освобождения (use-after-free или UAF) в ядре Linux. Я также подведу итоги обсуждения моей патч-серии в списке рассылки ядра (Linux Kernel Mailing List, LKML).


image


Использование памяти после освобождения в ядре Linux


UAF в ядре Linux очень популярный для эксплуатации тип уязвимостей. Есть множество публичных прототипов ядерных эксплойтов для UAF:



Для эксплуатации UAF обычно применяется техника heap spraying. Цель данной техники разместить данные, контролируемые атакующим, в определенном участке динамической памяти, которая также называется кучей. Техника heap spraying для эксплуатации UAF в ядре Linux основана на том, что при вызове kmalloc() slab-аллокатор возвращает адрес участка памяти, который был недавно освобожден:


image


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


image


Примечание: heap spraying для эксплуатации переполнения буфера в куче отдельная техника, которая работает иначе.


Идея


В июле 2020 года у меня возникла идея, как можно противостоять технике heap spraying для эксплуатации UAF в ядре Linux. В августе я нашел время поэкспериментировать. Я выделил карантин для slab-аллокатора из функциональностиKASANи назвал его SLAB_QUARANTINE.


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


image


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


Свойства безопасности SLAB_QUARANTINE


Для исследования свойств безопасности карантина для динамической памяти ядра я разработал два тестаlkdtm(опубликованы в серии патчей).


Первый тест называетсяlkdtm_HEAP_SPRAY. Он выделяет и освобождает один объект из отдельногоkmem_cache, а затем выделяет 400 000 аналогичных объектов. Другими словами, этот тест имитирует оригинальную технику heap spraying для эксплуатации UAF:


#define SPRAY_LENGTH 400000    ...    addr = kmem_cache_alloc(spray_cache, GFP_KERNEL);    ...    kmem_cache_free(spray_cache, addr);    pr_info("Allocated and freed spray_cache object %p of size %d\n",                    addr, SPRAY_ITEM_SIZE);    ...    pr_info("Original heap spraying: allocate %d objects of size %d...\n",                    SPRAY_LENGTH, SPRAY_ITEM_SIZE);    for (i = 0; i < SPRAY_LENGTH; i++) {        spray_addrs[i] = kmem_cache_alloc(spray_cache, GFP_KERNEL);        ...        if (spray_addrs[i] == addr) {            pr_info("FAIL: attempt %lu: freed object is reallocated\n", i);            break;        }    }    if (i == SPRAY_LENGTH)        pr_info("OK: original heap spraying hasn't succeeded\n");

Если отключитьCONFIG_SLAB_QUARANTINE, освобожденный объект немедленно реаллоцируется и переписывается:


  # echo HEAP_SPRAY > /sys/kernel/debug/provoke-crash/DIRECT   lkdtm: Performing direct entry HEAP_SPRAY   lkdtm: Allocated and freed spray_cache object 000000002b5b3ad4 of size 333   lkdtm: Original heap spraying: allocate 400000 objects of size 333...   lkdtm: FAIL: attempt 0: freed object is reallocated

Если включитьCONFIG_SLAB_QUARANTINE, 400 000 новых аллокаций не переписывают освобожденный объект:


  # echo HEAP_SPRAY > /sys/kernel/debug/provoke-crash/DIRECT   lkdtm: Performing direct entry HEAP_SPRAY   lkdtm: Allocated and freed spray_cache object 000000009909e777 of size 333   lkdtm: Original heap spraying: allocate 400000 objects of size 333...   lkdtm: OK: original heap spraying hasn't succeeded

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


Поэтому я разработал второй тест под названиемlkdtm_PUSH_THROUGH_QUARANTINE. Он выделяет и освобождает один объект из отдельногоkmem_cacheи затем выполняет kmem_cache_alloc()+kmem_cache_free()для этого кэша 400000 раз.


    addr = kmem_cache_alloc(spray_cache, GFP_KERNEL);    ...    kmem_cache_free(spray_cache, addr);    pr_info("Allocated and freed spray_cache object %p of size %d\n",                    addr, SPRAY_ITEM_SIZE);    pr_info("Push through quarantine: allocate and free %d objects of size %d...\n",                    SPRAY_LENGTH, SPRAY_ITEM_SIZE);    for (i = 0; i < SPRAY_LENGTH; i++) {        push_addr = kmem_cache_alloc(spray_cache, GFP_KERNEL);        ...        kmem_cache_free(spray_cache, push_addr);        if (push_addr == addr) {            pr_info("Target object is reallocated at attempt %lu\n", i);            break;        }    }    if (i == SPRAY_LENGTH) {        pr_info("Target object is NOT reallocated in %d attempts\n",                    SPRAY_LENGTH);    }

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


  # echo PUSH_THROUGH_QUARANTINE > /sys/kernel/debug/provoke-crash/   lkdtm: Performing direct entry PUSH_THROUGH_QUARANTINE   lkdtm: Allocated and freed spray_cache object 000000008fdb15c3 of size 333   lkdtm: Push through quarantine: allocate and free 400000 objects of size 333...   lkdtm: Target object is reallocated at attempt 182994  # echo PUSH_THROUGH_QUARANTINE > /sys/kernel/debug/provoke-crash/   lkdtm: Performing direct entry PUSH_THROUGH_QUARANTINE   lkdtm: Allocated and freed spray_cache object 000000004e223cbe of size 333   lkdtm: Push through quarantine: allocate and free 400000 objects of size 333...   lkdtm: Target object is reallocated at attempt 186830  # echo PUSH_THROUGH_QUARANTINE > /sys/kernel/debug/provoke-crash/   lkdtm: Performing direct entry PUSH_THROUGH_QUARANTINE   lkdtm: Allocated and freed spray_cache object 000000007663a058 of size 333   lkdtm: Push through quarantine: allocate and free 400000 objects of size 333...   lkdtm: Target object is reallocated at attempt 182010

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


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


   lkdtm: Target object is reallocated at attempt 107884   lkdtm: Target object is reallocated at attempt 265641   lkdtm: Target object is reallocated at attempt 100030   lkdtm: Target object is NOT reallocated in 400000 attempts   lkdtm: Target object is reallocated at attempt 204731   lkdtm: Target object is reallocated at attempt 359333   lkdtm: Target object is reallocated at attempt 289349   lkdtm: Target object is reallocated at attempt 119893   lkdtm: Target object is reallocated at attempt 225202   lkdtm: Target object is reallocated at attempt 87343

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


Поэтому важно очищать объекты ядерной кучи до помещения их в карантин. Более того, заполнение их нулями в некоторых случаях позволяет обнаружить использование памяти после освобождения: происходит разыменование нулевого указателя. Такой функционал уже существует в ядре и называетсяinit_on_free.Я интегрировал его с CONFIG_SLAB_QUARANTINE.


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


Для более глубокого понимания того, как работает CONFIG_SLAB_QUARANTINE с рандомизацией, я подготовил дополнительный патч, с подробным отладочным выводом (патч не для принятия в mainline). Пример такого отладочного вывода:


quarantine: PUT 508992 to tail batch 123, whole sz 65118872, batch sz 508854quarantine: whole sz exceed max by 494552, REDUCE head batch 0 by 415392, leave 396304quarantine: data level in batches:  0 - 77%  1 - 108%  2 - 83%  3 - 21%  ...  125 - 75%  126 - 12%  127 - 108%quarantine: whole sz exceed max by 79160, REDUCE head batch 12 by 14160, leave 17608quarantine: whole sz exceed max by 65000, REDUCE head batch 75 by 218328, leave 195232quarantine: PUT 508992 to tail batch 124, whole sz 64979984, batch sz 508854...

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


А что с производительностью?


Я провел несколько тестов производительности моего прототипа на реальном оборудовании и на виртуальных машинах:


  1. Тестирование пропускной способности сети с помощьюiperf:
    сервер: iperf -s -f K
    клиент: iperf -c 127.0.0.1 -t 60 -f K
  2. Нагрузочный тест ядерного планировщика задач:
    hackbench -s 4000 -l 500 -g 15 -f 25 -P
  3. Сборка ядра в конфигурации по умолчанию:
    time make -j2

Я тестировал ванильное ядро Linux в трех режимах:


  • init_on_free=off
  • init_on_free=on (механизм из официального ядра)
  • CONFIG_SLAB_QUARANTINE=y (включает в себя init_on_free)

Тестирование пропускной способности сети с помощьюiperfпоказало, что:


  • init_on_free=on дает пропускную способность на28% ниже, чем init_on_free=off.
  • CONFIG_SLAB_QUARANTINE дает пропускную способность на2%ниже, чем init_on_free=on.

Нагрузочный тест ядерного планировщика задач:


  • hackbench работает на5,3%медленнее с init_on_free=on по сравнению с init_on_free=off.
  • hackbench работает на 91,7%медленнее с CONFIG_SLAB_QUARANTINE по сравнению с init_on_free=on. При этом тестирование на виртуальной машине QEMU/KVM показало снижение производительности на 44%, что существенно отличается от результатов тестирования на реальном оборудовании (Intel Core i7-6500U CPU).

Сборка ядра в конфигурации по умолчанию:


  • При init_on_free=on сборка ядра осуществлялась на 1,7% медленнее, чем с init_on_free=off.
  • При CONFIG_SLAB_QUARANTINEсборка ядра осуществлялась на 1,1% медленнее, чем с init_on_free=on.

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


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


Контратака


В LKML получилось интересное обсуждение CONFIG_SLAB_QUARANTINE. Спасибо разработчикам ядра, которые уделили время и дали детальную обратную связь на мою серию патчей. Это Кейс Кук (Kees Cook), Андрей Коновалов, Александр Потапенко, Мэттью Уилкокс (Matthew Wilcox), Дэниел Майкей (Daniel Micay), Кристофер Ламетер (Christopher Lameter), Павел Мачек (Pavel Machek) и Эрик Бидерман (Eric W. Biederman).


Особенно я благодарен Яну Хорну (Jann Horn) из команды Google Project Zero. Он придумал контратаку, с помощью которой все-таки удается обойти CONFIG_SLAB_QUARANTINE и проэксплуатировать UAF в ядре Linux.


Примечательно, что наша дискуссия с Яном состоялась одновременно со стримом Кейса в Twitch, в ходе которого он тестировал мои патчи (рекомендую посмотреть запись).


Цитата из переписки с идеей контратаки:


On 06.10.2020 21:37, Jann Horn wrote:> On Tue, Oct 6, 2020 at 7:56 PM Alexander Popov wrote:>> So I think the control over the time of the use-after-free access doesn't help>> attackers, if they don't have an "infinite spray" -- unlimited ability to store>> controlled data in the kernelspace objects of the needed size without freeing them.   [...]>> Would you agree?>> But you have a single quarantine (per CPU) for all objects, right? So> for a UAF on slab A, the attacker can just spam allocations and> deallocations on slab B to almost deterministically flush everything> in slab A back to the SLUB freelists?Aaaahh! Nice shot Jann, I see.Another slab cache can be used to flush the randomized quarantine, so eventuallythe vulnerable object returns into the allocator freelist in its cache, andoriginal heap spraying can be used again.For now I think the idea of a global quarantine for all slab objects is dead.

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


Я сразу поделился этой перепиской в чате стрима Кейса в Twitch. Он доработал мой тест PUSH_THROUGH_QUARANTINE по идее Яна и выполнил атаку. Бабах!


Очень советую прочитать эту переписку в LKML целиком. Там обсуждаются новые идеи противодействия эксплуатации UAF в ядре.


Заключение


Я исследовал свойства безопасности карантина для динамической памяти ядра Linux, провел эксперименты, показывающие его влияние на эксплуатацию уязвимостей use-after-free. Получился быстрый и интересный проект. Надежное средство защиты, примененное в mainline, создать не удалось, но мы получили полезные результаты и идеи, которые пригодятся в дальнейших работах по защите ядра Linux.


А пока что позвольте закончить небольшим стихотворением, которое пришло мне в голову перед сном:


  Quarantine patch version three  Won't appear. No need.  Let's exploit use-after-free  Like we always did ;)    -- a13xp0p0v
Подробнее..

Перевод Серьёзная безопасность всплывшие спустя 15 лет баги в ядре Linux

22.03.2021 22:08:50 | Автор: admin

12 марта исследователи кибербезопасности GRIMM опубликовали информацию о трёх интересных багах в ядре Linux. В коде, который игнорировали около 15 лет. К счастью, кажется, всё это время никто не присматривался к коду; по крайней мере, не так усердно, чтобы заметить ошибки. CVE из списка ниже уже исправлены.


  • CVE-2021-27365. Переполнение буфера динамической памяти из-за sprintf().

  • CVE-2021-27363. Утечка адреса ядра из-за указателя в качестве уникального ID.

  • CVE-2021-27364. Чтение памяти за пределами буфера, приводящее к утечке данных или к панике ядра.

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

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

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

За исключением, конечно, того, что большинство (или, по крайней мере, многие) системы Linux поставляются с сотнями и даже тысячами модулями ядра в папке lib/modules; они готовы к использованию, когда понадобится, и даже сконфигурированы так, чтобы авторизованные приложения по требованию могли запускать загрузку модуля.

Примечание: насколько нам известно, эти баги исправлены в следующих официально поддерживаемых ядрах Linux, датированных 7 марта 2021 года: 5.11.4, 5.10.21, 5.4.103, 4.19.179, 4.1.4.224, 4.9.260, 4.4.260. Если ваше ядро изменено вендором, оно неофициальное, или его нет в этом списке, проконсультируйтесь с разработчиком своего ядра. Чтобы проверить версию ядра, выполните в терминале uname -r

Мой Linux поставлялся с около 4500 модулями ядра просто на всякий случай:

   root@slack:/lib/modules/5.10.23# find . -name '*.ko'   ./kernel/arch/x86/crypto/aegis128-aesni.ko   ./kernel/arch/x86/crypto/blake2s-x86_64.ko   ./kernel/arch/x86/crypto/blowfish-x86_64.ko   [...4472 lines deleted...]   ./kernel/sound/usb/usx2y/snd-usb-usx2y.ko   ./kernel/sound/x86/snd-hdmi-lpe-audio.ko   ./kernel/virt/lib/irqbypass.ko     #

И, хотя я действительно не возражал бы против крутой звуковой карты Tascam Ux2y (например, US122, US224, US428), у меня нет в ней необходимости или места для неё, поэтому сомневаюсь, что мне когда-нибудь понадобится любой драйвер snd-usb-usx2y.ko.

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

Неплохо посмотреть дважды

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

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

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

  • Сбой ядра, а значит, и всей системы.

  • Считывание фрагментов данных из памяти ядра, когда предполагается, что они недосягаемы.

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

Даже фрагментированный и неструктурированный поток конфиденциальных данных, периодически похищаемый из привилегированного процесса (помните печально известную ошибку Heartbleed?), может раскрыть наши секреты. Не забывайте о том, насколько легко программы распознаёт и "наскребает" паттерны данных, пролетающие в RAM: номера кредитных карт, адреса электронной почты.

Посмотрим на баги внимательнее

Выше упоминалось, что первая ошибка вызвана применением sprintf(). Это функция языка Си, сокращение от formatted print into string форматированная печать в строку, так текстовое сообщение распечатывают в блок памяти, чтобы воспользоваться им позже. Посмотрим на этот код:

   char buf[64];      /* Reserve a 64-byte block of bytes           */   char *str = "42";  /* Actually has 3 bytes, thus: '4'  '2'  NUL  */                      /* Trailing zero auto-added:   0x34 0x32 0x00 */   sprintf(buf,"Answer is %s",str)

Он резервирует блок памяти buf, содержащий 12 символов "Answer is 42", за которым следует нулевой терминальный нулевой байт ASCII NUL, а в конце 64-байтового буфера 51 нетронутый байт.

Однако sprintf() опасна и не должна использоваться никогда: она не проверяет, достаточно ли распечатанным данным места в конечном блоке памяти. Выше, если строка в переменной str длиннее 54 байт, включая нулевой байт в конце, то вместе с текстом "Answer is" она не поместится в buf..

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

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

Не выдавайте свои адреса

Вторая ошибка возникла из-за использования адресов памяти в качестве уникальных идентификаторов.

Идея звучит хорошо: если нужно обозначить в коде ядра объект данных с ID без конфликтов с другими ID, можно воспользоваться числами 1, 2, 3 и так далее.

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

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

Современные ядра используют так называемое KASLR, сокращение от kernel address space layout randomisation (рандомизация адресного пространства ядра), специально для того, чтобы помешать непривилегированным пользователям выяснить структуру ядра.

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

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

Что делать?

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

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

  • Чтобы избежать сюрпризов, блокируйте загрузку модулей ядра. Если вы установите системную переменную Linux kernel.modules_disable=1 после того, как ваш сервер загрузился и корректно заработал, то не загрузятся никакие модули; ни случайно, ни по замыслу. Эта настройка отключается только при перезагрузке. Вот два варианта:

    sysctl -w kernel.modules_disable=1echo 1 > /proc/sys/kernel/modules_disable
    
  • Поймите, какие модули ядра необходимы, и используйте только их. Можно собрать статическое ядро, скомпилировав только нужные модули, или создать пакет с ядром для своих серверов, при этом удалив все ненужные модули. При желании со статическим ядром загрузку модулей можно отключить полностью.

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

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

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

Linux kernel development для самых маленьких

13.10.2020 10:19:44 | Автор: admin


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


0. Подготовка


Как и перед любой инженерной операцией, всё начинается с подготовки своего рабочего места. И первейшее здесь действие это завести себе аккаунт с адекватным именем. В идеальном мире это будет просто транскрипция имени и фамилии. Если за учётку вроде MamkinC0d$r или Developer31337 в других местах пальцем в вас тыкать не будут, то правила LKC (Linux kernel community) такое прямо запрещают инкогнито контрибьютить в ядро не принято.


Далее вам понадобится место на локальной машине. Сама папка Linux со скачанными исходниками весит чуть меньше 3-х гигов. Но если ядро пробовать собирать, то вместе с модулями займёт все 30 GB.
Захотелось собрать несколько веток? Умножаем 30 на число веток.
И помним скорость сборки прямо связана с количеством доступных ядер! Больше ядер быстрее соберётся. Так что не стесняйтесь выделять под это самую мощную машину.


1. Mail


Самый спорный и поэтому регулярно вызывающий споры момент это канал коммуникации с LKC. Он безальтернативно один. Почта. Причём сообщения отправляются по классике через smtp. Хотя есть варианты и с imap, но я никогда не пробовал и не могу сказать, есть ли там подводные камни.


Вокруг необходимости делать всё через плейнтекст в почте есть масса споров. Недавно в сети была очередная громкая статья на эту тему. Суть материала: письма это, конечно, здорово, но пихать туда всё, включая куски кода это вам (т.е. LKC) популярности не добавляет и даже наоборот, отпугивает новичков. С одной стороны вроде и да, если ты не можешь спокойно и структурировано изложить свои мысли в голом тексте, то в низкоуровневой разработке ловить будет особо нечего. С другой стороны, слать в письмах сорсы патчей это даже архаизмом назвать уже сложно.
Но, как принято в уютном мирке ядра, Линус хлопнул кулаком по столу и все пишут письма. Возможно, буквально завтра это изменится, но на момент выхода статьи это письма и только письма.


Какой email-client выбрать есть рекомендации. Самым рекомендуемым почтовым агентом для LKC остаётся mutt. Да, тот самый текстовый почтовый клиент, от которого сводит олдскулы. Для начала mutt нужно поставить (я думаю, со своим пакетным менеджером вы и сами справитесь), а потом задать параметры в файле ~/.muttrc.


## folders##set imap_authenticators="gssapi:login"#set ssl_starttls=yesset mbox_type=Maildirset folder="imaps://my_name@imap.my_server.ru/"set spoolfile="=INBOX"set mbox="="set record="=Sent"set postponed="=Drafts"set trash="=Trash"set imap_list_subscribed=yes # which mailboxes to list in the sidebarmailboxes =INBOX =Sent =Sent/SentOld =Drafts =Templates =Trash =my/friends =my/family## SMTP#set smtp_url="smtps://my_name@smtp.my_server.ru:my_smtp_port/"set from = "My Name <my_name@my_server.ru>"set envelope_from = "yes"

Но почты недостаточно. Без Git никуда.


2. Git


Прежде чем что-то делать с исходниками ядра, нужно настроить Git. Можно конфигурировать файлы напрямую, но есть упрощающая жизнь утилита git config, через которую можно регулировать все аспекты работы Git'a.
Внутри есть три уровня настроек: общие для всех пользователей системы и для всех репозиториев (git config --system), общие для всех репозиториев конкретного пользователя (git config --global), отдельные для каждого репозитория (git config --local).


Глобальные настройки хранятся в /etc/gitconfig, настройки пользователя в ~/.gitconfig или ~/.config/git/config, а настройки отдельных репозиториев хранятся в файле config в каталоге .git/config.


В общем случае будет достаточно законфигурировать файл для пользователя ~/.gitconfig. Основная идея: при отправке коммитов должно отображаться ваше корректное имя.


[user]    name = My Name    email = my_name@my_server.ru[sendemail]    smtpencryption = tls    smtpserver = smtp.my_server.ru    smtpuser = my_name@my_server.ru    smtpserverport = 465    confirm = always    suppress-cc = misc-by[color]    ui = auto[format]    signOff = true[http]    sslVerify = true[pull]    rebase = false

Примечательно, что параметр sslVerify = true препятствует работе с Git напрямую через git://git.kernel.org и форсит доступ только через https://git.kernel.org. Так, наверное, секьюрнее, хотя какого-то смысла я в этом не вижу. Но, может, чего-то не знаю?


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


Отправка патча выполняется командой git send-email. У git send-email есть несколько параметров с участием smtp, которые можно (и нужно) переопределить.
Полезный параметр --smtp-debug=1. Осуществляет логирование SMTP запросов и ответов, что помогает при разборе проблем с настройками почты. Например, я столкнулся с тем, что почтовый сервер, на котором есть у меня почтовый ящик, не поддерживает TLS. Возможны проблемы с аутентификацией, а сообщения об ошибке git send-email выдаёт не особо разнообразные и адекватные.


Можно задавать пароль к почте через параметр --smtp-pass=p4ssw0rd или вообще захардкорить в конфиге, но это это для тех, кому терять нечего. Но если каждый раз вводить пароль лень, то есть некий хак: если username был задан (через --smtp-user или sendmail.smtpUser), а пароль не указан, тогда пароль получается через git-credential.


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


3. Coding


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


cd ~/ git clone https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git linux-<branch name>git checkout v5.9-rc2 -b <my new branch name>

Операция довольно долгая, так что смело можно идти за кофе или на обед. А если попробовать ускорить процесс, отказавшись от истории, то работать с "этим" будет невозможно.


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


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


И вот свой небольшой и эффективный код вы написали, отладили, всё протестировали и готовы отправлять на рассмотрение. Но не спешите этого делать. Для начала обязательно проверьтесь на code style. В этом вам поможет ./script/checkpatch.pl. Для этого сделаем патч и отправим его на проверку.


git add --allgit diff HEAD^ --staged >my-branch-name.patch./scripts/checkpatch.pl my-branch-name.patch

После того, как пройдёт первое удивление и вы доустановите необходимые компоненты для python2 типа ply и git (который у меня так и не установился), наступит чудесное время исправления ошибок и ворнингов. По результатам которых вы а) поймёте, что красивый код писать вы не умеете б) потеряете всякое желание что-то куда-то отправлять. Ведь даже если отбросить все шутки, ещё можно как-то смириться с тем, что длина строк ограничена 100 символами (это начиная с версии 5.7, раньше так было вообще 80). Но вот такие места оставляют неизгладимое впечатление:


WARNING: Improper SPDX comment style for 'block/blk-filter-internal.h', please use '/*' instead#85: FILE: block/blk-filter-internal.h:1:+// SPDX-License-Identifier: GPL-2.0

Для .h файлов строка с информацией о лицензии должна быть в ремарках / * */, а для *.c файлов должна быть в ремарках //. Такое запросто выбьет кого угодно из душевного равновесия. Вопрос: "Зачем?!" до сих пор болтается в моей голове, хотя есть вера в то, что это не просто ошибка в скриптах.


Кстати, чтобы просто проверит один файл достаточно вызвать


./scripts/checkpatch.pl --file <file name> 

Можно прикрутить этот вызов к git, чтобы автоматичски запускался этот скрипт при попытке что-то зачекинить.


Ещё важное замечание: clang-format добрался и ядра Linux. Файл .clang-format расположился в корне ядра в кучке с другими конфигами. Но я не советую добавлять его в хук для git. Лучше всего корректно настроить среду разработки и запомнить code style. Лично мне не понравилось как он делает переносы длинных строк. Если вдруг сторока оказалась длиннее допустимой, то скорее всего функция, в которой эта строка расположилась, является кандидатом на рефакторинг, и лучше его не откладывать. С другой стороны, если у вас много уже готового кода который нужно адаптировать для ядра clang-format может может сильно облегчить вам задачу.


4. Kernel build


Несмотря на то, что процесс описан в других статьях тут и тут, я все же повторюсь.
По шагам процесс сборки ядра довольно прост, если не вдаваться в детали. Для начала ставим необходимые пакеты (использовался Debian 10):


apt install libncurses-dev flex bison bc libelf-dev libssl-dev

Это без компилятора и обычного для С/С++ разработчика набора программ.
Запускаем конфигурацию:


make menuconfig

Тут есть интересный аспект: в качестве шаблона будет браться config ядра от вашего боевого ядра, которое, скорее всего, подготовлено дистрибьютером. Для Debian 10 сборка проходит успешно, если в конфиге потереть информацию о встраиваемых в ядро сертификатах.


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


make -j <число ядер>

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


 make -j <число ядер> modules

Если какой-то модуль не собирается, просто вырубите его в ближайшем Makefile-е (если 100% уверены, что не пытались в нём что-то улучшить). Наверняка он вам не пригодится, и тратить время на исправления смысла нет.


Теперь можно деплоить то, что получилось, на эту же систему.


sudo make modules_installsudo make install

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


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


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


update-grub

5. Patches


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


git commit --all --signoff -m "message" -m "new message line" 

Ещё можно комментарии к коммиту дополнить в человеческом текстовом редакторе.


git commit --amend

И теперь его можно оформить в виде того самого письма. Правила хорошего тона, или best practice, если угодно это 75 символов на строку.


git format-patch <имя базовой ветки, например v5.9-rc2> --signoff --cover-letter

В результате получите два файла. В первом 000-cover-letter.patch нужно указать заголовок письма "Subject" и основное описание патча. В описании патча пишем, для чего он создавался, кому он сделает жизнь на нашей планете лучше и каким образом. Только не словоблудим про космические корабли в Большом театре, а пишем лаконично и по делу. И не в коем случае не пишите корпоративную лабуду а-ля "Без этого патча мой бизнес встанет, меня уволят, а дети мои умрут от голода". Нет, строго по существу: "Увидел вот такую проблему вот тут, починить решил вот таким образом, исходники патча прилагаю". Всё, вы восхитительны! А если не превысили 75 символов на строку, то восхитительны в квадрате.
А ещё один волшебный скриптик ./scripts/getmaintainers.pl <patch file> позволит узнать, кому письмо отправлять.
И вот он, момент отправления письма, ради которого всё и затевалось:


git send-email --to=<list of maintainers emails> ./0000-cover-letter.patch ./0001-<my commit message>.patch

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


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


git checkout mastergit branch -D <branch-name>git push origin --delete <branch-name>

6. Debuging


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


Как правило, при ошибке вы получаете лог с calltrace-ом. Там указываются имена функций и смещения. Примерно вот так:


[  457.517480] BUG: kernel NULL pointer dereference, address: 000000000000003c[  457.517499] RIP: 0010:tracking_submit_bio+0xac/0x140 [veeamsnap]

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


gdb ./veeamsnap.ko

Важно, чтобы в модуле сохранились символы (stripped модуль вам тут не поможет).


Выполнив команду list


list *tracking_submit_bio+0xac

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


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

Подробнее..

Звук. От механических колебаний до ALSA SoC Layer

22.09.2020 10:23:41 | Автор: admin


Мы в SberDevices делаем устройства, на которых можно послушать музыку, посмотреть кино и ещё много всего. Как вы понимаете, без звука это всё не представляет интереса. Давайте посмотрим, что происходит со звуком в устройстве, начиная со школьной физики и заканчивая ALSA-подсистемой в Linux.

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

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

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


Фонограф и его изобретатель Томас Эдисон
Источник фото

Тут всё просто. Брали какой-нибудь цилиндр, обматывали фольгой. Потом брали что-нибудь конусообразное (чтобы было погромче) с мембраной на конце. К мембране присоединена маленькая иголка. Иголку прислоняли к фольге. Потом специально обученный человек крутил цилиндр и что-нибудь говорил в резонатор. Иголка, приводимая в движение мембраной, делала в фольге углубления. Если достаточно равномерно крутить цилиндр, то получится намотанная на цилиндр зависимость амплитуды колебаний мембраны от времени.



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

Переход к электричеству


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



Чтобы можно было хранить такое представление колебаний в памяти компьютера, их надо дискретизировать. Этим занимается специальная железка аналогово-цифровой преобразователь (АЦП). АЦП умеет много раз за одну секунду запоминать значение напряжения (с точностью до разрешения целочисленной арифметики АЦП) на входе и записывать его в память. Количество таких отсчётов за секунду называется sample rate. Типичные значения 8000 Hz 96000 Hz.

Не будем вдаваться в подробности работы АЦП, потому что это заслуживает отдельной серии статей. Перейдём к главному весь звук, с которым работают Linux-драйверы и всякие устройства, представляется именно в виде зависимости амплитуды от времени. Такой формат записи называется PCM (Pulse-code modulation). Для каждого кванта времени длительностью 1/sample_rate указано значение амплитуды звука. Именно из PCM состоят .wav-файлы.

Пример визуализации PCM для .wav-файла с музыкой, где по горизонтальной оси отложено время, а по вертикальной амплитуда сигнала:



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



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

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

  1. Bit Clock(BCLK) тактирующий сигнал (или клок), по которому аппаратура определяет, когда надо отправить следующий бит.
  2. Frame Clock (FCLK или его ещё называют LRCLK) тактирующий сигнал, по которому аппаратура понимает, когда надо начать передавать другой канал.
  3. Data сами данные.



Например, у нас есть файл со следующими характеристиками:
  • sample width = 16 bits;
  • sampling rate = 48000 Hz;
  • channels = 2.

Тогда нам надо выставить следующие значения частот:
  • FCLK = 48000 Hz;
  • BCLK = 48000 * 16 * 2 Hz.

Чтобы передавать ещё больше каналов, используется протокол TDM, который отличается от I2S тем, что FCLK теперь не обязан иметь скважность 50%, и восходящий фронт лишь задаёт начало пакета сэмплов, принадлежащих разным каналам.

Общая схема


Под рукой как раз оказалась плата amlogic s400, к которой можно подключить динамик. На неё установлено ядро Linux из upstream. На этом примере и будем работать.

Наша плата состоит из SoC (amlogic A113x), к которому подключен ЦАП TAS5707PHPR. И общая схема выглядит следующим образом:

Что умеет SoC:
  • SoC имеет 3 пина: BCLK, LRCLK, DATA;
  • можно сконфигурировать CLK-пины через специальные регистры SoC, чтобы на них были правильные частоты;
  • ещё этому SoC можно сказать: Вот тебе адрес в памяти. Там лежат PCM-данные. Передавай эти данные бит за битом через DATA-линию. Такую область памяти будем называть hwbuf.

Чтобы воспроизвести звук, Linux-драйвер говорит SoC, какие нужно выставить частоты на линиях BCLK и LRCLK. К тому же Linux-драйвер подсказывает, где находится hwbuf. После этого ЦАП (TAS5707) получает данные по DATA-линии и преобразует их в два аналоговых электрических сигнала. Эти сигналы потом передаются по паре проводов {analog+; analog-} в два динамика.

Переходим к Linux


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

Пользовательская ALSA-библиотека взаимодействует с ядерной частью с помощью интерфейса ioctl. При этом используются созданные в директории /dev/snd/ устройства pcmC{x}D{y}{c,p}. Эти устройства создаёт драйвер, который должен быть написан вендором SoC. Вот, например, содержимое этой папки на amlogic s400:

# ls /dev/snd/controlC0    pcmC0D0p   pcmC0D0с   pcmC0D1c   pcmC0D1p   pcmC0D2c

В названии pcmC{x}D{y}{c,p}:
X номер звуковой карты (их может быть несколько);
Y номер интерфейса на карте (например, pcmC0D0p может отвечать за воспроизведение в динамики по tdm интерфейсу, а pcmC0D1c за запись звука с микрофонов уже по другому аппаратному интерфейсу);
p говорит, что устройство для воспроизведения звука (playback);
c говорит, что устройство для записи звука (capture).

В нашем случае устройство pcmC0D0p как раз соответствует playback I2S-интерфейсу. D1 это spdif, а D2 pdm-микрофоны, но о них мы говорить не будем.

Device tree


Описание звуковой карты начинается с device_tree [arch/arm64/boot/dts/amlogic/meson-axg-s400.dts]:
sound {    compatible = "amlogic,axg-sound-card";    model = "AXG-S400";    audio-aux-devs = <&tdmin_a>, <&tdmin_b>,  <&tdmin_c>,             <&tdmin_lb>, <&tdmout_c>;                      dai-link-6 {        sound-dai = <&tdmif_c>;        dai-format = "i2s";        dai-tdm-slot-tx-mask-2 = <1 1>;        dai-tdm-slot-rx-mask-1 = <1 1>;        mclk-fs = <256>;        codec-1 {            sound-dai = <&speaker_amp1>;        };    };               dai-link-7 {        sound-dai = <&spdifout>;        codec {            sound-dai = <&spdif_dit>;        };    };    dai-link-8 {        sound-dai = <&spdifin>;        codec {            sound-dai = <&spdif_dir>;        };    };    dai-link-9 {        sound-dai = <&pdm>;        codec {            sound-dai = <&dmics>;        };    };};&i2c1 {    speaker_amp1: audio-codec@1b {        compatible = "ti,tas5707";        reg = <0x1b>;        reset-gpios = <&gpio_ao GPIOAO_4 GPIO_ACTIVE_LOW>;        #sound-dai-cells = <0>;                   };};&tdmif_c {    pinctrl-0 = <&tdmc_sclk_pins>, <&tdmc_fs_pins>,            <&tdmc_din1_pins>, <&tdmc_dout2_pins>,            <&mclk_c_pins>;    pinctrl-names = "default";    status = "okay";};

Тут мы видим те 3 устройства, которые потом окажутся в /dev/snd: tdmif_c, spdif, pdm.

Устройство, по которому пойдёт звук, называется dai-link-6. Работать оно будет под управлением TDM-драйвера. Возникает вопрос: вроде мы говорили про то, как передавать звук по I2S, а тут, вдруг, TDM. Это легко объяснить: как я уже писал выше, I2S это всё тот же TDM, но с чёткими требованиями по скважности LRCLK и количеству каналов их должно быть два. TDM-драйвер потом прочитает поле dai-format = i2s; и поймёт, что ему надо работать именно в I2S-режиме.

Далее указано, какой ЦАП (внутри Linux они входят в понятие кодек) установлен на плате с помощью структуры speaker_amp1. Заметим, что тут же указано, к какой I2C-линии (не путать с I2S!) подключен наш ЦАП TAS5707. Именно по этой линии будет потом производиться включение и настройка усилителя из драйвера.

Структура tdmif_c описывает, какие пины SoC будут выполнять роли I2S-интерфейса.

ALSA SoC Layer


Для SoC, внутри которых есть поддержка аудио, в Linux есть ALSA SoC layer. Он позволяет описывать кодеки (напомню, что именно так называется любой ЦАП в терминах ALSA), позволяет указывать, как эти кодеки соединены.

Кодеки в терминах Linux kernel называются DAI (Digital Audio Interface). Сам TDM/I2S интерфейс, который есть в SoC, тоже называется DAI, и работа с ним проходит схожим образом.

Драйвер описывает кодек с помощью struct snd_soc_dai. Самая интересная часть в описании кодека операции по выставлению параметров передачи TDM. Находятся они тут: struct snd_soc_dai -> struct snd_soc_dai_driver -> struct snd_soc_dai_ops. Рассмотрим самые важные для понимания поля (sound/soc/soc-dai.h):

struct snd_soc_dai_ops {    /*     * DAI clocking configuration.     * Called by soc_card drivers, normally in their hw_params.     */    int (*set_sysclk)(struct snd_soc_dai *dai,        int clk_id, unsigned int freq, int dir);    int (*set_pll)(struct snd_soc_dai *dai, int pll_id, int source,        unsigned int freq_in, unsigned int freq_out);    int (*set_clkdiv)(struct snd_soc_dai *dai, int div_id, int div);    int (*set_bclk_ratio)(struct snd_soc_dai *dai, unsigned int ratio);    ...
Те самые функции, с помощью которых выставляются TDM-клоки. Эти функции обычно имплементированы вендором SoC.

...int (*hw_params)(struct snd_pcm_substream *,    struct snd_pcm_hw_params *, struct snd_soc_dai *);...
Самая интересная функция hw_params().
Она нужна для того, чтобы настроить всё оборудование SoC согласно параметрам PCM-файла, который мы пытаемся проиграть. Именно она в дальнейшем вызовет функции из группы выше, чтобы установить TDM-клоки.

...int (*trigger)(struct snd_pcm_substream *, int,    struct snd_soc_dai *);...
А эта функция делает самый последний шаг после настройки кодека переводит кодек в активный режим.

ЦАП, который будет выдавать аналоговый звук на динамик, описывается ровно такой же структурой. snd_soc_dai_ops в этом случае будут настраивать ЦАП на прием данных в правильном формате. Такая настройка ЦАП как правило осуществляется через I2C-интерфейс.

Все кодеки, которые указаны в device tree в структуре,
dai-link-6 {    ...    codec-1 {        sound-dai = <&speaker_amp1>;    };};

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

Каждый кодек должен сказать какие PCM-параметры он поддерживает. Это он делает с помощью структуры:
struct snd_soc_pcm_stream {    const char *stream_name;    u64 formats;            /* SNDRV_PCM_FMTBIT_* */    unsigned int rates;     /* SNDRV_PCM_RATE_* */    unsigned int rate_min;      /* min rate */    unsigned int rate_max;      /* max rate */    unsigned int channels_min;  /* min channels */    unsigned int channels_max;  /* max channels */    unsigned int sig_bits;      /* number of bits of content */};

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

Соответствующую реализацию TDM-драйвера для amlogic s400 можно посмотреть в sound/soc/meson/axg-tdm-interface.c. А реализацию драйвера кодека TAS5707 в sound/soc/codecs/tas571x.c

Пользовательская часть


Теперь посмотрим что происходит, когда пользователь хочет проиграть звук. Удобный для изучения пример реализации пользовательской ALSA это tinyalsa. Исходный код, относящийся ко всему нижесказанному, можно посмотреть там.
В комплект входит утилита tinyplay. Чтобы проиграть звук надо запустить:

bash$ tinyplay ./music.wav -D 0 -d 0
(-D и -d параметры говорят, что звук надо проигрывать через /dev/snd/pcmC0D0p).

Что происходит?
Вот краткая блок-схема, а потом будут пояснения:



  1. [userspace] Парсим .wav header, чтобы узнать PCM-параметры (sample rate, bit width, channels) воспроизводимого файла. Складываем все параметры в struct snd_pcm_hw_params.
  2. [userspace] Открываем устройство /dev/snd/pcmC0D0p.
  3. [userspace] Обращаемся к ядру с помощью ioctl(, SNDRV_PCM_IOCTL_HW_PARAMS ,), чтобы узнать поддерживаются такие PCM-параметры или нет.
  4. [kernel] Проверяем PCM-параметры, которые пытается использовать пользователь. Тут есть два типа проверок:
    • на общую корректность и согласованность параметров;
    • поддерживает ли каждый задействованный кодек такие параметры.
  5. настраиваем под них все кодеки, которые прикреплены к /dev/snd/pcmC0D0p интерфейсу (но пока не включаем), возвращаем успех.
  6. [userspace] выделяем временный буфер, куда будем класть PCM-данные.
  7. [userspace] отдаем заполненный временный буфер ядру с помощью ioctl(, SNDRV_PCM_IOCTL_WRITEI_FRAMES, ). Буква I в конце слова WRITEI указывает, что PCM-данные хранятся в interleaved-формате.
  8. [kernelspace] включаем кодеки, которые прикреплены к /dev/snd/pcmC0D0p интерфейсу, если они еще не включены.
  9. [kernelspace] копируем пользовательский буфер buf в hwbuf (см. пункт Общая схема) с помощью copy_from_user().
  10. [userspace] goto 6.

Реализацию ядерной части ioctl можно посмотреть, поискав по слову SNDRV_PCM_IOCTL_*

Заключение


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

Перевод Что нового в ядре Linux

28.09.2020 12:18:44 | Автор: admin

image


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


Linux работает практически на всем: все 500 из 500 самых быстрых суперкомпьютеров мира; большинство общедоступных облаков, даже Microsoft Azure; и 74 процента смартфонов. Действительно, благодаря Android, Linux является самой популярной операционной системой для конечных пользователей, чуть обойдя Windows на 4 процента (39% против 35%).


Итак, что же будет дальше с Linux? После освещения Linux на протяжении всех 29 лет его истории и зная практически любого, кто хоть как-то связан с разработкой Linux, включая Линуса Торвальдса, я думаю, что у меня есть ответ на этот вопрос.


Недавние улучшения ядра Linux


Самое большое недавнее улучшение и ничто другое даже близко не стоит это то, что Linux теперь станет платформой для виртуальных частных сетей (VPN). Хотя Linux, во многом благодаря OpenVPN, долгое время был важным игроком в сфере VPN, добавление WireGuard, революционного подхода к VPN, меняет всё.


Торвальдсу очень, очень нравится WireGuard, вот что он писал: Могу ли я еще раз заявить о своей любви к нему и надеяться на то, что в скором времени он будет объединен с ядром?. Чего хочет Линус, то он и получает. WireGuard был объединен с ядром Linux 5.6 в марте 2020 года.


Почему он так важен? WireGuard обладает редким качеством в надежности программы: его код чист и прост. Кроме того, он поддерживает передовые технологии криптографии: Noise Protocol Framework, Curve25519, ChaCha20, Poly1305, BLAKE2, SipHash24 и HKDF.


Что еще более важно для пользователей, так это то, что он намного быстрее своих конкурентов. Эталонные тесты показали, что WireGuard быстрее OpenVPN более чем в два раза. Кроме того, он кроссплатформенный, так что вы можете запустить его на Linux сервере, а ваши клиенты могут быть на Windows, macOS, BSD, iOS, Android, и конечно же на Linux.


Ядро Linux 5.6 также может похвастаться и другими значительными улучшениями. Особо выделим то, что для 32-х разрядных систем, Linux решил свою проблему конца времени. Видите ли, во вторник 19 января 2038 года в 03:14:08 по Гринвичу (GMT, также известное как Всемирное координированное время) наступит конец света.


Случиться то, что значение времени в 32-х разрядных операционных системах на основе UNIX, такие, как Linux и более старые версии macOS, исчерпает 32-х битные числа. После этого, отсчёт времени пойдет в обратную сторону с отрицательными числами. А Вы думали, что ошибка 2000 года была страшной!


Всё началось с того, что UNIX, отец Linux, датирует начало времени в секундах от 1 января 1970 года 00:00:00 GMT. Проблема была в том, что UNIX начиналась как 32-х разрядная операционная система, которая хранила время в виде одного 32-х разрядного целого числа со знаком. Это много секунд, но этого недостаточно. Исправление позволяет даже 32-х разрядному Linux использовать 64-х разрядные числа. Конечно это только задерживает проблему до воскресенья 4 декабря 29227702659 года 5:30:08 GMT, но меня это вполне устраивает.


Android и Linux синхронизируются


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


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


Для Вас это может показаться не важным, но это важно, и вот почему. Сегодня, прежде чем Linux окажется на этом новом, стильном Android-телефоне, который у Вас в руках, он проходит три отдельных, трудоемких этапа. Сначала Google берет ядро Linux с долгосрочной поддержкой (LTS) и добавляет в него специфичный для Android код, для того что бы сделать общее ядро Android. Затем, Google отправляет его производителю систем на кристалле (SoC), например, Qualcomm. OEM-производитель адаптирует ядро под конкретную SoC и чипсет. Наконец, адаптированное общее ядро Android (SoC ядро), передается производителю смартфона. Производитель уже добавляет свои доморощенные, проприетарные драйверы для дисплея, камеры и Wi-Fi/сотового модема. В результате получается ядро устройства это то, что у Вас на телефоне.


Попутно, каждый телефон вбирает в себя, буквально, миллионы строк кода ядра, не являющиеся частью стандартного дистрибутива. Во многом это драйверы устройств, потому что, каждый смартфон или планшет на Android поставляется со своим набором драйверов. Вот почему так сложно сделать по-настоящему универсальный дистрибутив для Android-смартфона, такой как операционная система /e/. Каков результат? В этом стильном, новом телефоне, который у Вас в кармане, установлено ядро Linux, которому уже 2 года. Благодаря этим древним ядрам, LTS-ядра Linux теперь поставляются с шести летней поддержкой.


Google и поставщики не более чем Вы стремятся перенести исправления безопасности на пыльные, старые ядра. Поэтому Google, совместно с сообществом разработчиков, пытается привести поставляемые версии Android, в соответствие с основными текущими ядрами Linux.


Конечно, это займет некоторое время, но пара вечных проблем Android отказ разработчиков Linux поддерживать стабильный двоичный интерфейс приложения (ABI) и столь же твердый отказ производителей оборудования открыть исходные коды своих драйверов остаются с нами и по сей день. ABI определяет как программа взаимодействует с оборудованием, когда Вы не знаете как работать с ним на уровне кода приложения. Разработчики открытого исходного кода хотят работать на уровне API, в то время как производители оборудования довольно часто хотят оставить свои устройства в виде таинственного черного ящика, доступ к которому возможен только через ABI. Несмотря на это, мы наконец-то приблизились к ускорению интеграции Linux в производственный конвейер Android, а это означает, что Android будет более безопасным из коробки.


eBPF: Межсетевой экран, который стал отладчиком Linux


Корбет также рассказал о росте интереса к расширенному фильтру пакетов Беркли (eBPF), используемого как мощный инструмент отладки ядра Linux. Вы спросите, как такое могло произойти? Смотрите.


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


Вы можете запустить eBPF в ответ на действия в tracepoints, kprobes и perf-событиях. Это позволяет Вам отлаживать проблемы ядра и выполнять анализ производительности. Более того, поскольку eBPF программы могут иметь доступ к структурам данных ядра, Вы можете писать, добавлять и тестировать новый отладочный код без перекомпиляции ядра. Для занятых разработчиков это большая экономия времени. Теперь, системные администраторы и программисты, не входящие в круг тех, кто работает непосредственно с ядром Linux, начинают использовать возможности eBPF.


Rust становится вторым языком программирования Linux


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


На виртуальной конференции 2020 Linux Plumbers Conference, где ведущие разработчики ядра Linux обсуждают будущее Linux, говорилось о введении Rust в качестве второго языка ядра. Rust это системный язык программирования высокого уровня, спонсируемый Mozilla, являющейся материнской компанией Firefox. Верите Вы, или нет, но эта идея получает широкую поддержку. Хотя сам Торвальдс уверен в том, что Linux не будет написан на Rust. Однако ведь цель не в этом. Никто не собирается переписывать на Rust, 25 миллионов строк ядра, написанных на Си.


Использование Rust, языка системного уровня, внутри ядра, было предложено Джошем Триплеттом, ведущим специалистом по языку Rust, и Ником Десолнерсом, инженером Google. Почему Rust? Потому что он гораздо безопаснее, чем C, особенно когда дело доходит до работы с памятью.


Как пояснил Райан Левик, главный защитник облачных разработчиков Microsoft, Rust полностью безопасен для памяти. Если принять во внимание, что примерно две трети проблем безопасности могут быть вызваны плохой работой с памятью, то это довольно серьезное улучшение. Кроме того, Райан Левик говорит, что Rust предотвращает эти проблемы, как правило, без добавления каких-либо накладных расходов во время выполнения.


Торвальдс видит в этом свои преимущества. Хотя он поощряет медленный, но неуклонный подход к внедрению Rust в Linux, он также сказал, что использование интерфейсов Rust для драйверов и других программ, не связанных с ядром, имеет смысл: Я убежден, что это произойдет. Это может быть и не Rust, но может случиться так, что у нас будут разные модели для написания подобных вещей, и Си не будет единственной.


Пока разработчики ядра медленно двигаются вперед, другие разработчики дистрибутивов Linux, не теряя времени, приняли Rust. Amazon Web Services (AWS) объявила, что ее только что выпущенный Bottlerocket Linux для контейнеров в основном написан на Rust.


Подобно Microsoft и Firefox, если назвать еще две компании, включающие Rust в свои планы разработки, AWS нравится Rust за его функции защиты памяти. Самарта Чандрашекар, менеджер по продукту AWS, сказала, что это помогает обеспечить безопасность потоков и предотвратить ошибки, связанные с памятью, например, переполнение буфера, которое может привести к уязвимостям в безопасности.


Забегая вперед, становится ясно, что Rust скоро будет играть важную роль как в разработке ядра Linux, так и в разработке дистрибутивов. Кто бы мог подумать, что Си будет хоть немного вытеснен в этой области программирования?


Ядро Linux 6.0 и выше


К концу этого года появится Linux 6.0. Но не стоит сильно обращать внимание на это число. В свое время, Торвальдс сказал о выпуске 5.0 следующее: Я хотел бы отметить (ещё раз), что мы не делаем функциональные релизы, и что 5.0 не означает ничего большего, чем то, что числа 4.х стали такими большими, что у меня не хватает пальцев на руках и ногах, чтобы сосчитать их.


Забегая вперед, можно сказать, что Linux версии 6.0, как и большинство предыдущих релизов, будет состоять из новых и улучшенных драйверов оборудования. Однако все заметят одно перспективное изменение: Linux наконец-то будет поддерживать набор инструкций AMD и Intel FSGSBASE. Хорошо, если вы серьезно не заядлый разработчик, то я только что потерял Вас. В общем, если коротко, то Ваша машина будет работать гораздо быстрее под управлением Linux, поддерживающий этот набор команд.


Вы можете резонно спросить меня, насколько быстрее? Я отвечу, ранние тесты производительности, проводимые на бета-версии ядра Linux 5.9, показали значительное улучшение ввода/вывода. С Redis, хранилищем структур данных в памяти, с открытым исходным кодом, которая часто используется как база данных, прирост производительности составил более 50 процентов. Что не так уж и плохо!


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


Кто знает, возможно, 2021 год станет годом рабочего стола Linux? В конце концов, 2020 год был годом Linux на рабочем столе Windows.

Подробнее..

Нам нужно поговорить про Linux IIO

24.09.2020 14:19:46 | Автор: admin

IIO (промышленный ввод / вывод) это подсистема ядра Linux для аналого-цифровых преобразователей (АЦП), цифро-аналоговых преобразователей (ЦАП) и различных типов датчиков. Может использоваться на высокоскоростных промышленных устройствах. Она, также, включает встроенный API для других драйверов.



Подсистема Industrial I/O Linux предлагает унифицированную среду для связи (чтения и записи) с драйверами, охватывающими различные типы встроенных датчиков и несколько исполнительных механизмов. Он также предлагает стандартный интерфейс для приложений пользовательского пространства, управляющих датчиками через sysfs и devfs.


Вот несколько примеров поддерживаемых типов датчиков в IIO:


  • АЦП / ЦАП
  • акселерометры
  • магнетометры
  • гироскопы
  • давление
  • влажность
  • температура
  • дальнометры

IIO может использоваться во многих различных случаях:


  • Низкоскоростная регистрация для медленно меняющегося входного сигнала (пример: запись температуры в файл)
  • Высоко-скоростной сбор данных с использованием АЦП, DFSDM или внешних устройств (например, аудио, измеритель мощности)
  • Считывание положения вращающегося элемента, используя интерфейс квадратурного энкодера TIM или LPTIM
  • Управление аналоговым источником через ЦАП
  • Внешние устройства подключенные через SPI или I2C

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


Сосредоточимся на моментах почему IIO это хорошо


Все наверняка встречали/пользовались конструкциями типа:


# https://www.kernel.org/doc/Documentation/i2c/dev-interfaceopen("/dev/i2c-1", O_RDWR);# https://www.kernel.org/doc/Documentation/spi/spidev.rstopen("/dev/spidev2.0", O_RDWR);

У данного способа много недостатков, я перечислю те которые считаю основными:


  1. нет прерываний
  2. способ доступа для данных индивидуален для каждого устройства

Ну как говориться зачем всё это если есть драйвера ?


Здесь мы опять сталкиваемся с "индивидульностью" каждого устройства (как допустим способ калибровки или размерность).


Собственно IIO даёт нам во-первых универсальность, во-вторых возможность poll по поступлению новых данных.


Сам IIO разделен на два уровня абстракции устройства и каналы измерений.


Выделим два основных способа доступа поддержанных в официальном ядре.


Простое использование IIO


Мы можем читать данные через sysfs (допустим для акселерометра):


# cat /sys/bus/iio/devices/iio\:device0/in_accel_x_raw-493

Это мы прочитали "сырые" измерения, их еще надо привести к общему виду.


Либо через read():


# Включим захват измерений для каждого канала(cd /sys/bus/iio/devices/iio:device0/scan_elements/ && for file in *_en; do echo 1 > $file; done)

Тогда мы можем свести взаимодействие к виду :


int fd = open("/dev/iio:device0");read(fd, buffer, scan_size);# где scan_size это сумма размера всех заказанных измерений, то есть для всех 1 в /sys/bus/iio/devices/iio:device0/scan_elements/*_en

Размер прочитанного блока всегда кратен scan_size, мы получаем "сырые" измерения, которые надо привести к общему виду, об этом позже.


Внутреннее устройство


Каналы


Любой драйвер IIO предоставляет информацию о возможных измерениях в виде стандартного описания каналов struct iio_chan_spec:


IIO types


Пример для датчика BME280


/* https://elixir.bootlin.com/linux/v5.9-rc1/source/drivers/iio/pressure/bmp280-core.c#L132*/static const struct iio_chan_spec bmp280_channels[] = {    {        .type = IIO_PRESSURE,        .info_mask_separate = BIT(IIO_CHAN_INFO_PROCESSED) |                      BIT(IIO_CHAN_INFO_OVERSAMPLING_RATIO),    },    {        .type = IIO_TEMP,        .info_mask_separate = BIT(IIO_CHAN_INFO_PROCESSED) |                      BIT(IIO_CHAN_INFO_OVERSAMPLING_RATIO),    },    {        .type = IIO_HUMIDITYRELATIVE,        .info_mask_separate = BIT(IIO_CHAN_INFO_PROCESSED) |                      BIT(IIO_CHAN_INFO_OVERSAMPLING_RATIO),    },};

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


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


Кольцевой буфер


Собственно это не так интригующее как звучит, основан на kfifo делает всё что положено кольцевому буфферу.


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


Метка времени


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


Представлена в наносекундах, является CLOCK_REALTIME.


IIO Triggered Buffers


Триггеры


Представляет из себя "внешнее" событие, которое инициирует захват данных с последующей передачей наверх в user space.


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


Назначить триггер устройству:


# cat /sys/bus/iio/devices/iio\:device0/trigger/current_triggericm20608-dev0# echo > /sys/bus/iio/devices/iio\:device0/trigger/current_trigger# cat /sys/bus/iio/devices/iio\:device0/trigger/current_trigger# echo "icm20608-dev0" > /sys/bus/iio/devices/iio\:device0/trigger/current_trigger

Official Trigger Documentation


IIO sysfs trigger


Industrial IIO configfs support


Triggered buffer support trigger buffer support for IIO subsystem of Linux device driver


Device owned triggers


Данный класс триггеров относиться к собственным триггерам устройства, они определяются в device tree:


icm20608: imu@0 {    ...    interrupt-parent = <&gpio5>;    interrupts = <11 IRQ_TYPE_EDGE_RISING>;    ...};

Это даст нам соответствующий триггер с именем:


cat /sys/bus/iio/devices/trigger0/nameicm20608-dev0

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


Interrupt triggers (also known as gpio trigger)


iio-trig-interrupt


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


Данный драйвер не поддержан в ядре в полном виде, ввиду сомнений текущего maintainer'a IIO Jonathan Cameron, хотя он так же является его автором.


Единственный способ задания в официальном ядре через платформенный код необходимый для этого платформенный код вы можете подсмотреть тут Triggered buffer support trigger buffer support for IIO subsystem of Linux device driver
.


Но кому очень хочется может воспользоваться серией патчей:


[v3,1/6] dt-bindings: iio: introduce trigger providers, consumers


Тогда задание через device tree будет выглядеть приблизительно так:


trig0: interrupt-trigger0 {    #io-trigger-cells = <0>;    compatible = "interrupt-trigger";    interrupts = <11 0>;    interrupt-parent = <&gpioa>;};

sysfs trigger


iio-trig-sysfs


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


Создание триггера:


# echo 10 > /sys/bus/iio/devices/iio_sysfs_trigger/add_trigger

Число используется для генерации имени триггера в виде "sysfstrig%d", его же мы используем при задании триггера устройству.


High resolution timer trigger


Представляет из себя таймер с минимальным возможным разрешением в 1 наносекунду.


# mkdir /sys/kernel/config/iio/triggers/hrtimer/my_trigger_name# cat /sys/bus/iio/devices/trigger4/namemy_trigger_name# cat /sys/bus/iio/devices/trigger4/sampling_frequency100

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


loop trigger


iio-trig-loop


Экспериментальный триггер предположительно инициированный PATCH v1 5/5 iio:pressure:ms5611: continuous sampling support
.


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


iio:trigger: Experimental kthread tight loop trigger.


Опять же нет поддержки DT, так что либо добавлять через патч, либо через платформенный код.


Device tree


Здесь я хочу обратить особое внимание на возможность задать label для узла, которую лучше всего использовать если у вас много однотипных устройств, всегда текущие значения заданные в узле можно подсмотреть в директории of_node для каждого iio:device /sys/bus/iio/devices/iio\:device0/of_node/.


Какой общей рекомендации не существует всё индивидуально и описано в https://elixir.bootlin.com/linux/v5.9-rc1/source/Documentation/devicetree/bindings/iio


Типы каналов измерений


Многие датчики, который раньше существовали как отдельные сущности были перенесены на инфраструктуру IIO, так что похоже тут enum iio_chan_type можно найти почти любой тип измерений. Расшифровку можно посмотреть тут iio_event_monitor.


Формат данных


IIO умеет сообщать в каком формате нам передаются данные iio-buffer-sysfs-interface.


[be|le]:[s|u]bits/storagebitsXrepeat[>>shift]

Живой пример для icm20608:


# cat /sys/bus/iio/devices/iio\:device0/scan_elements/*_typebe:s16/16>>0be:s16/16>>0be:s16/16>>0be:s16/16>>0be:s16/16>>0be:s16/16>>0le:s64/64>>0

Тут более ли менее все понятно:


  • первым идёт порядок байт le или be соответственно мы должны позаботиться о том что порядок совпадает с нашей архитектурой или c выбранным нами порядком байт
  • затем идет тип знаковое или без знаковое, s или u соответственно
  • затем идет длина значения в битах и через / длина поля в котором содержится значение опять же в битах, кратное количеству битов в байте
  • последним идет сдвиг

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


be:u4/8>>0be:u4/8>>4

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


Scaling and offset


Как я уже говорил ранее прочитанные данные в сыром виде необходимо привести к общему виду:


/sys/bus/iio/devices/iio:deviceX/in_*_raw/sys/bus/iio/devices/iio:deviceX/in_*_offset/sys/bus/iio/devices/iio:deviceX/in_*_scale

В общем случае преобразование будет иметь вид (raw + offset)*scale, для какого то из типов датчиков offset'a может и не быть.


How to do a simple ADC conversion using the sysfs interface


iio_simple_dummy


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


  • IIO_VOLTAGE
  • IIO_ACCEL
  • IIO_ACTIVITY

The iio_simple_dummy Anatomy


iio_simple_dummy


libiio


Если вышеприведенное показалось вам сложным на помощь к вам идет libiio от Analog Devices.


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


У неё есть интересная особенность в виде возможности работы в виде сервера/клиента, в таком случае устройство с датчиками служит в качестве сервера данных, а клиент может располагаться на Linux, Windows или Mac машине, и соединяться через USB, Ethernet или Serial.


Соединение с удаленным узлом iiod:


On remote :


host # iiod

On local :


local $ iio_info -n [host_address]local $ iio_attr -u ip:[host_address] -dlocal $ iio_readdev -u ip:[host_address] -b 256 -s 0 icm20608

Отдельно хочется отметить поддержку Matlab, а так же интересный проект осциллографа.


Пример программы для чтения акселерометра


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


https://github.com/maquefel/icm20608-iio


Работа без использования libiio


Я не буду касаться банальной работы с sysfs так, что в общих чертах для чтения необходимо сделать следующее:


  • Поиск устройства, здесь мы ориентируемся на /sys/bus/iio/iio:deviceN/name, соответственно /sys/bus/iio/iio:deviceN будет совпадать с /dev/iio:deviceN
  • Инициализация каналов в /sys/bus/iio/iio:deviceN/scan_elements/, нам будут передаваться измерения только с тех каналов, которые мы заказали в *_en
  • Инициализация буфера /sys/bus/iio/iio:deviceN/enable

В примере есть минимум необходимый для работы.


Выравнивание


Eго придется делать самим если мы хотим обойтись без libiio.


https://elixir.bootlin.com/linux/v5.9-rc1/source/drivers/iio/industrialio-buffer.c#L574


Простой код для вычисления смещения для каждого канала:


    # bytes - всего длина всего пакета в байтах    # length - длина канала в байтах    # offset - смещения относительно начала пакета для канала в байтах    if (bytes % length == 0)        offset = bytes;    else        offset = bytes - bytes % length + length;    bytes = offset + length;

Что в случае без libiio, что в противоположном случае измерение необходимо привести к окончательному виду:


  • привести порядок байт в соответствие с используемым
  • сдвинуть на необходимое значение
  • обрезать лишнее
  • если знаковое, то проделать расширение знака (Sign extension)
  • если есть offset, то прибавить до применения шкалы
  • если есть scale, то применить шкалу

    input = is_be ? betoh(input) : letoh(input);    input >>= shift;    input &= BIT_MASK(bits);    value = is_signed ? (float)sext(input, bits) : (float)input;    if(with_offset) value += offset;    if(with_scale) value *= scale;

Примечание: Расширение знака (Sign extension) в примере представлен самый простой непортируемый вариант. Дополнительно по теме можно глянуть тут SignExtend.


Работа с использованием libiio


Пример работы можно глянуть тут libiio-loop.c
.


Приведу псевдокод с комментариями:


# Создать контекст из uri# uri = "ip:127.0.0.1"# uri = "local:"# uri = "usb:"ctx = iio_create_context_from_uri(uri);# Найти устройство# допустим device = icm20608dev = iio_context_find_device(ctx, device);# Количество доступных каналовnb_channels = iio_device_get_channels_count(dev);# Включить каждый каналfor(int i = 0; i < nb_channels; i++)    iio_channel_enable(iio_device_get_channel(dev, i));# buffer_size = SAMPLES_PER_READ, количество последовательных измерений (по всем каналам)buffer = iio_device_create_buffer(dev, buffer_size, false);# Задать блокирующий режим работыiio_buffer_set_blocking_mode(buffer, true);while(true) {    # Заполнить буфер    iio_buffer_refill(buffer);    # Способов несколько - можно читать и без использования libiio    # Приведу в качестве примера "каноничный" способ, который заключается в том что предоставленная нами функция    # вызывается для каждого канала    # ssize_t print_sample(const struct iio_channel *chn, void *buffer, size_t bytes, __notused void *d)    # const struct iio_channel *chn - текущий канал который мы обрабатываем    # void *buffer - указатель на буфер содержащий измерения для данного канала    # size_t bytes - длина измерения в байтах    # __notused void *d - пользовательские данные которые мы передаем вместе с вызовом iio_buffer_foreach_sample    iio_buffer_foreach_sample(buffer, print_sample, NULL);}# освободить буферiio_buffer_destroy(buffer);# освободить контекстiio_context_destroy(ctx);

Пара слов об альтернативном механизме для чтения данных


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


Всё это относиться к методам обработки высокоскоростного потока данных.


Сравнение методов (тезисы из презентации):


Решение первое Блоки


  • Группировать несколько измерений в блок
  • Генерировать одно прерывание на один блок
  • Уменьшить расходы на управление
  • Размер блока должен быть конфигурируемым
  • Позволить пользовательского приложению выбирать между задержкой и накладными расходами

Решение второе DMA + mmap()


  • Использовать DMA чтобы перемещать данные от устройства к выделенному блоку памяти
  • Использовать mmap() чтобы иметь доступ к памяти из пользовательского пространства
  • Избежать копирования данных
  • "Бесплатное" демультиплексирование в пользовательском пространстве

High-speed Data Acquisition
using the
Linux Industrial IO framework


По мне так это отличное решения для SDR.


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


Автор любезно предоставил данные изменения для ядра 4.19 и 5.4.


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


Рекомендуемые материалы


https://bootlin.com/pub/conferences/2012/fosdem/iio-a-new-subsystem/iio-a-new-subsystem.pdf


https://archive.fosdem.org/2012/schedule/event/693/127_iio-a-new-subsystem.pdf


https://events19.linuxfoundation.org/wp-content/uploads/2017/12/Bandan-Das_Drone_SITL_bringup_with_the_IIO_framework.pdf


https://programmer.group/5cbf67db154ab.html


https://elinux.org/images/b/ba/ELC_2017_-_Industrial_IO_and_You-_Nonsense_Hacks%21.pdf


https://elinux.org/images/8/8d/Clausen--high-speed_data_acquisition_with_the_linux_iio_framework.pdf


Для дополнительного изучения


https://linux.ime.usp.br/~marcelosc/2019/09/Simple-IIO-driver


P.S.


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

Подробнее..

BPF для самых маленьких, часть вторая разнообразие типов программ BPF

22.11.2020 18:15:46 | Автор: admin

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


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


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



Типы программ и оглавление


Все существующие типы программ BPF регистрируются в файле include/uapi/linux/bpf.h ядра Linux. В следующих разделах я постарался сгруппировать их по логическим группам (звездочками отмечены технические ликбез-подразделы):



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


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


Коммит Автор Дата Тип программы
0975 Alexei\ Starovoitov 2014-09-26 BPF_PROG_TYPE_UNSPEC
ddd8 Alexei Starovoitov 2014-12-01 BPF_PROG_TYPE_SOCKET_FILTER
2541 Alexei Starovoitov 2015-03-25 BPF_PROG_TYPE_KPROBE
96be Daniel Borkmann 2015-03-01 BPF_PROG_TYPE_SCHED_CLS
94ca Daniel Borkmann 2015-03-20 BPF_PROG_TYPE_SCHED_ACT
98b5 Alexei Starovoitov 2016-04-06 BPF_PROG_TYPE_TRACEPOINT
6a77 Brenden Blanco 2016-07-19 BPF_PROG_TYPE_XDP
0515 Alexei Starovoitov 2016-09-01 BPF_PROG_TYPE_PERF_EVENT
0e33 Daniel Mack 2016-11-23 BPF_PROG_TYPE_CGROUP_SKB
6102 David Ahern 2016-12-01 BPF_PROG_TYPE_CGROUP_SOCK
3a0a Thomas Graf 2016-11-30 BPF_PROG_TYPE_LWT_IN
3a0a Thomas Graf 2016-11-30 BPF_PROG_TYPE_LWT_OUT
3a0a Thomas Graf 2016-11-30 BPF_PROG_TYPE_LWT_XMIT
4030 Lawrence Brakmo 2017-06-30 BPF_PROG_TYPE_SOCK_OPS
b005 John Fastabend 2017-08-15 BPF_PROG_TYPE_SK_SKB
ebc6 Roman Gushchin 2017-11-05 BPF_PROG_TYPE_CGROUP_DEVICE
4f73 John Fastabend 2018-03-18 BPF_PROG_TYPE_SK_MSG
c4f6 Alexei Starovoitov 2018-03-28 BPF_PROG_TYPE_RAW_TRACEPOINT
4fba Andrey Ignatov 2018-03-30 BPF_PROG_TYPE_CGROUP_SOCK_ADDR
004d Mathieu\ Xhonneux 2018-05-20 BPF_PROG_TYPE_LWT_SEG6LOCAL
f436 Sean Young 2018-05-27 BPF_PROG_TYPE_LIRC_MODE2
2dbb Martin KaFai Lau 2018-08-08 BPF_PROG_TYPE_SK_REUSEPORT
d58e Petar Penkov 2018-09-14 BPF_PROG_TYPE_FLOW_DISSECTOR
7b14 Andrey Ignatov 2019-02-27 BPF_PROG_TYPE_CGROUP_SYSCTL
9df1 Matt Mullins 2019-04-26 BPF_PROG_TYPE_RAW_TRACEPOINT_WRITABLE
0d01 Stanislav\ Fomichev 2019-06-27 BPF_PROG_TYPE_CGROUP_SOCKOPT
f1b9 Alexei Starovoitov 2019-10-30 BPF_PROG_TYPE_TRACING
27ae Martin KaFai Lau 2020-01-08 BPF_PROG_TYPE_STRUCT_OPS
be87 Alexei Starovoitov 2020-01-20 BPF_PROG_TYPE_EXT
fc61 KP Singh 2020-03-29 BPF_PROG_TYPE_LSM
e9dd Jakub Sitnicki 2020-07-17 BPF_PROG_TYPE_SK_LOOKUP

Микроядро Linux


В 1992 году случился известный спор между Эндрю Танненбаумом (по-русски его бы звали Андрей Елочка) и Линусом Бенедиктом Торвальдсом (как он тогда подписывался). Танненбаум знатно набросил, начав тред под названием LINUX is obsolete про то, что Linux устарел (в 1992 году) потому, что он не микроядро. Я взял слово спор в кавычки потому, что, вообще говоря, Линус сразу согласился с Танненбаумом:


Да, linux монолитен, и я согласен, что микроядра милее. Был бы заголовок менее спорным, я бы, наверное, согласился с большей частью сказанного вами. С теоретической (и эстетической) точки зрения linux проигрывает. Если бы прошлой весной ядро GNU было доступно, то я бы не потрудился даже начинать свой проект: но факт в том, что как оно не было готово тогда, так и не готово до сих пор. Linux побеждает, значительно опередив ядро GNU по очкам в игре "доступно сейчас"


Примерно двадцать и еще десять лет спустя мы видим, как теория превращается в практику BPF постепенно проращивает в монолитном ядре Linux ростки микроядер. В начале 2020 года Martin KaFai Lau добавил в ядро возможность переписывать произвольные структуры, реализующие логику. Такие структуры это типичная объектно-ориентированная конструкция в ядре, когда структура содержит набор функций, определяющих набор операций над объектом или реализующих какой-то конкретный интерфейс.


Реализуется это при помощи нового типа программ BPF: BPF_PROG_TYPE_STRUCT_OPS. Пользовательский интерфейс довольно запутанный, Daniel Borkman даже предлагал его поменять, добавив новый тип объектов BPF модули, но не срослось.


Этот механизм не позволяет переписать произвольную структуру, просто написав код BPF. Чтобы иметь возможность переписывать какую-то конкретную структуру, нужна поддержка со стороны ядра. В качестве примера и первого приложения была добавлена возможность реализовывать на BPF структуру tcp_congestion_ops, которая определяет какой алгоритм использовать для TCP congestion control. Были добавлены и два примера реализацииDCTCP и CUBIC на BPF.


Я решил начать именно с этого примера, так как он иллюстрирует то, к чему стремится BPF, а именно к тому, чтобы стать платформой, позволяющей динамически и безопасно (например, BPF гарантирует отсутствие бесконечных циклов и неправомерный доступ к памяти) расширять и исправлять алгоритмы ядра. Кроме безопасности такой подход, и мы еще увидим похожие примеры ниже в статье, позволяет делать боевые системы более эффективными использовать специализированные алгоритмы вместо ванильных и отрезать ненужную функциональность. См. также недавний доклад Алексея Старовойтова на BPF Summit.


Программы BPF для трассировки и сбора информации


Почти сразу после появления нового BPF известный товарищ Brendan Gregg, которого мы в дальнейшем будем называть просто БГ, рассмотрел в нем огромный потенциал для трассировки систем под управлением Linux и начал использовать его на практике. В результате появилось с сотню утилит bcc, скриптов для bpftrace, книжка BPF Performance Tools, несколько стартапов, бизнес которых основан на применении BPF, и т.п. В компаниях Facebook и Netflix, в которой сейчас работает БГ, на каждом боевом сервере работают десятки трассировочных программ BPF, 24x7. BPF это легко позволяет запуск трассировки под BPF сравнительно дешевый.


Почему БГ так сильно воодушевился? Давайте посмотрим. BPF позволяет написать и подцепить программу произвольной сложности на следующие события:


  • вызов и возврат (почти) любой функций ядра Linux
  • запуск конкретной инструкции в ядре или в пользовательском коде
  • на любой tracepoint в ядре
  • любому событию perf, software и hardware

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


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


#! /usr/bin/env bpftrace#include <linux/skbuff.h>#include <linux/ip.h>k:icmp_echo {  $skb = (struct sk_buff *) arg0;  $iphdr = (struct iphdr *) ($skb->head + $skb->network_header);  @pingstats[ntop($iphdr->saddr), ntop($iphdr->daddr)]++;}

Эта программа из нескольких строчек строит статистику о том, кто пингует наш хост. Мы подцепляемся к kprobe на входе в функцию icmp_echo, которая вызывается на приход ICMPv4 пакета типа echo request. Ее первый аргумент, arg0 в нашей программе, это указатель на sk_buff, описывающий пакет. Из этой структуры мы достаем IP адреса и увеличиваем соответствующий счетчик в словаре @pingstats. Все, теперь у нас есть полная статистика о том, кто и как часто пинговал наши IP адреса! Раньше для написания такой программы вам пришлось бы писать модуль ядра, регистрировать в нем обработчик kprobe, а также придумывать механизм взаимодействия с user space, чтобы хранить и читать статистику.


Перечислим типы программ BPF, используемые для tracing:


  • BPF_PROG_TYPE_KPROBE: позволяет подцепить программу BPF к kprobe, kretprobe, uprobe или uretprobe. Это позволяет подцепиться к вызову или возврату из любой функции ядра, любому адресу внутри ядра (т.е. к конкретным инструкциям внутри функций), а также любому адресу внутри пользовательского кода, например, к функциям любой разделяемой библиотеки.
  • BPF_PROG_TYPE_PERF_EVENT: позволяет подцепить программу BPF к любому событию perf.
  • BPF_PROG_TYPE_TRACEPOINT: позволяет подцепить программу BPF к любому tracepoint. Зачем это нужно, если мы можем делать это при помощи kprobes? Дело в том, что tracepoints это стабильный API ядра (что означает, что при попытке удаления/изменения tracepoint на автора патча будет оказываться сильное психологическое давление) и значит утилиты, созданные на основе tracepoints могут считаться более стабильными (требовать меньше поддержки в будущем).
  • BPF_PROG_TYPE_RAW_TRACEPOINT: при вызове tracepoints аргументы для обработчика конструируются в момент выполнения и на это тратится некоторое количество ресурсов. При помощи raw tracepoints можно запускать программы BPF без создания удобных и переносимых аргументов, что позволяет оптимизировать время выполнения для тех программ, которым это важно
  • BPF_PROG_TYPE_RAW_TRACEPOINT_WRITABLE: tracepoints с доступным на запись контекстом (см. здесь)
  • BPF_PROG_TYPE_TRACING: новый тип программ, которые можно подключать в нескольких вариантах: к tracepoints, входам и выходам из функций, к функциям, которые позволяют переписывать возвращаемое значение (список таких функций можно получить так: sudo cat /sys/kernel/debug/error_injection/list), а также для создания итераторов. Отличительной особенностью этого типа программ является то, что использование BTF при их загрузке обязательно.

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


Linux Security Modules


В ядре Linux повсюду раскиданы хуки (security hooks), которые проверяются при взаимодействии с разными объектами, например, при создании и изменении инодов, процессов и т.п. Механизм модулей безопасности Linux (Linux Security Modules или LSM) позволяет писать системы наподобие SELinux, AppArmor, и т.д., использующие эти хуки.


Пару лет назад в гугле задумались над созданием механизма, позволяющего собирать информацию и быстро реагировать на возможные атаки. Подробнее о том, почему нужно было добавлять новые API можно послушать в докладе Kernel Runtime Security Instrumentation с конференции LSS-NA 2019. В результате появился новый тип программ BPF, BPF_PROG_TYPE_LSM, название которого подсказывает, что теперь программу BPF можно подключать к любому LSM хуку. Это позволяет создавать кастомные системы для аудита и немедленного реагирования, причем, так как мы в мире BPF, это безопасно и дешево и не требует загрузки модулей и т.п.


В процессе разработки этого типа программ появилось несколько интересных фич, например, особый подтип программ BPF, которые могут засыпать. Эта функциональность нужна при анализе пользовательских данных, которые могут быть не подгружены в память на момент запуска программы BPF. Еще одна особенность заключается в том, что программы LSM нужно иногда погружать в момент загрузки системы. Делается это посредством user mode helper, так как для загрузки программ BPF требуется libbpf, а тянуть функциональность libbpf в ядро мантейнеры не хотят.


Краткую историю создания KRSI и небольшой туториал можно посмотреть в этом недавнем докладе KP Singh, автора KRSI, на BPF Summit.


Динамическое расширение программ BPF


Tail calls


В незапамятные времена, то есть еще пару лет тому назад, программы BPF были ограничены по размеру. А именно, программе BPF позволялось иметь до 4096 инструкций. Однако, еще существовала возможность из одной программы BPF прыгнуть в другую. Именно прыгнуть возможность возврата не предусматривалась. Поэтому механизм получил название tail calls.


Интерфейс у tail calls следующий. Допустим, в зависимости от контекста, программа Э хочет вызвать одну из программ Ю или . Для этого при загрузке создается специальный мап типа BPF_MAP_TYPE_PROG_ARRAY, в котором значениями по индексу являются программы BPF (из пространства пользователя мы пихаем в этот мап соответствующие файловые дескрипторы):



Дальше программа Э должна использовать функцию-помощник bpf_tail_call. Например, чтобы вызвать программу , программа Э должна сказать bpf_tail_call(&map, ctx, 1), где ctx это указатель на контекст, который мы хотим передать следующей программе. После прыжка, реализованного как long jump, новая программа запускается со старым стеком. Количество прыжков ограничено 32, так что мы гарантированно завершаем работу, даже если программа вызывает саму себя в цикле.


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


В некоторых случаях, однако, tail calls добавляют головной боли. Например, что сделает bpf_tail_call, если индекс указывает на несуществующую программу? Правильный ответ ничего, просто провалится на следующую инструкцию.


XDP Features

Убрано под спойлер, так как про XDP мы говорим ниже.


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


Динамическая подмена подпрограмм


Помимо tail calls существует еще один механизм расширения функциональности. Новый тип программ BPF, BPF_PROG_TYPE_EXT, позволяет динамически переписывать существующие глобальные функции. Для этого используется механизм BPF trampoline и поэтому мы не сможем подцепить программу типа TRACING к функции, которая была динамически подменена и наоборот если функция трэйсится, то ее нельзя заменить.


Этот механизм используется, например, в xdp-dispatcher механизме, который позволяет подцеплять несколько программ XDP на один интерфейс. Для этого создается главная программа с функциями-заглушками, а когда мы хотим подцепить одну или несколько XDP программ, функции-заглушки заменяются на эти программы. См. доклад Multiple XDP programs on a single interfacestatus and next steps от Toke Hiland-Jrgensen на Linux Plumbers 2020.


LIRC: Linux Infrared Remote Control


Если у вас есть инфракрасный приемник, то вы можете использовать программы BPF типа BPF_PROG_TYPE_LIRC_MODE2 для декодирования сигнала. На lwn есть отличный туториал от Sean Young, автора данного типа программ, о том, как это можно использовать на практике.


Вкратце, программа BPF сажается на приемник и запускается в тот момент, когда приемник обнаружил пульс и/или померил время между двумя пульсами. Программа BPF читает эти события и обновляет свое состояние, которое она хранит, например, в map. Когда программа понимает, что произошло какое-то конкретное событие, она может сообщить об этом при помощи специальных функций-помощников. Например, если декодировано нажатие кнопки, то она может вызвать bpf_rc_keydown и в пространство пользователя отправится событие:



Возникает вопрос, а зачем это нужно, если мы можем декодировать эти события в пространстве пользователя при помощи lirc? Но Sean Young говорит, что использование BPF это, скорее, попытка упростить API: теперь все IR устройства могут обрабатываться из userspace как будто бы ядро раскодировало сигнал (и это правда).


Программы BPF для сетевого программирования


Классический BPF создавался сетевыми инженерами из Berkeley Labs, новый BPF создавался сетевыми инженерами Linux, и поэтому не удивительно, что значительная часть типов программ BPF являются программами для работы с сетями.


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


Сетевой стек Linux: прибытие пакета


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


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



Прерывания в Linux обрабатываются в два этапа top half и bottom half. Первый этап это, собственно, обработчик прерывания (top half), задача которого подтвердить получение прерывания и запланировать отложенную задачу (bottom half), которая будет выполнена позднее в контексте softirq или даже потока ядра. Из соображений производительности, все сетевые bottom halves, соответствующие входящим пакетам, обрабатываются в специальной softirq под названием NET_RX.


В softirq драйвер определяет границы памяти, в которой находится пакет и создает экземпляр структуры struct sk_buff. Структура sk_buff, акроним от socket buffer, это одна из двух центральных структур в сетевом стеке Linux. В первом приближении каждому сетевому пакету в Linux соответствует экземпляр структуры sk_buff. Структура содержит множество полей, описывающих пакет: указатели на начало и конец данных, указатели на заголовки различных уровней, устройство ввода-вывода, метаданные и т.д., и т.п. Драйвер заполняет некоторые из полей, а остальные заполняются в процессе движения sk_buff вверх по сетевому стеку.



Здесь мы видим структуру данных и некоторые из полей. Например, head и end указывают на начало и конец доступной памяти, data и tail указывают на начало и конец данных, net_header и transport_hdr указывают на заголовки третьего и четвертого уровня, соответственно, и т.п. Указатель data будет передвигаться вправо в процессе продвижения пакета по стеку для разных уровней стека слово данные означает разные куски пакета.


Дальше драйвер при помощи функции netif_receive_skb передает пакет дальше по стеку и возвращается к своим драйверным делам. Что происходит с пакетом в стеке дальше? Мне нравится вот эта картинка из Netfilter wiki:



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


Однако, вернемся немного назад и посмотрим на предыдущую картинку внимательнее (вы можете кликнуть на нее). В первом узле после start, начала обработки в softirq, мы видим слова eBPF и XDP и подозрительные стрелочки, ведущие в обход стека...


Сети на стероидах Express Data Path


Создание структуры sk_buff это довольно дорогое удовольствие и во многих ситуациях для того, чтобы принять решение о судьбе пакета нам не нужно ничего, кроме заголовков или метки VLAN и т.п. С появлением программ BPF типа XDP (Express Data Path) стало возможным решать судьбу пакета без создания sk_buff.


Программы XDP подсоединяются к конкретному сетевому устройству и, как мы видели на картинке выше, запускаются сразу после того, как пакет был доставлен в RAM и драйвер о нем узнал. В качестве контекста программам предоставляется виртуальная структура struct xdp_md, содержащая указатели на начало и конец пакета, а также некоторую дополнительную информацию, которая нам сейчас не интересна. Основываясь на содержании контекста данных и метаданных конкретного пакета программа XDP может дропнуть пакет (XDP_DROP), отправить его назад на то же устройство, с которого пакет пришел (XDP_TX), перенаправить его на другое устройство (XDP_REDIRECT), а также пропустить вверх по стеку (XDP_PASS):



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


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



На картинке я попытался изобразить схему работы одного сокета типа AF_XDP: каждому сокету AF_XDP соответствует ровно одна очередь устройства (rx queue), отмеченная на рисунке зелененьким. Программа XDP, работающая на устройстве смотрит на входящие пакеты и пересылает нужные прямо в сокет. Остальные пакеты, как из зелененькой очереди, так и из остальных, могут отправляться в сетевой стек. (Вообще говоря, это тоже требует поддержки драйвера, так как нам нужно сконфигурировать сетевую карточку так, что интересующие нас пакеты приходят на одну и ту же очередь. Например, если мы хотим получать UDP пакеты с портом доставки 65784 в сокет типа AF_XDP, посаженный на очередь 13, то мы можем, например, настроить карточку так: ethtool -N flow-type udp4 dst-port 65784 action 13.)


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


На данный момент есть два классических способа использования XDP, которые работают на отлично: защита от DDoS и балансировка нагрузки. Каждый пакет, который приходит в Facebook, проходит через load balancer katran, написанный на XDP, Cloudfare использует XDP как основу для защиты от DDoS и для load balancing, cilium также использует XDP для обоих случаев, и т.п. Также идут разговоры и R&D на тему того, как скрестить XDP и P4 для того, чтобы создавать эффективные сетевые решения, программируемые на языках высокого уровня (для машин под управлением мифического NPU Networking Processing Unit).


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


Структура struct __sk_buff


Перед тем как смотреть на остальные типы сетевых программ BPF, нам нужно объяснить одну техническую деталь. Как мы уже знаем, у каждого пакета в Linux есть соответствующая структура sk_buff, которая создается в момент прихода пакета на интерфейс и затем постепенно заполняется и передается на уровни выше. Она содержит поля типа len длина пакета, network_header указатель на заголовок уровня L3, dev указатель типа struct net_device на устройство ввода или вывода, и так далее.


Так как сетевые программы работают с пакетами, а пакет это sk_buff (кроме XDP, в случае которого sk_buff еще не создан), было бы разумно, если бы большинство сетевых программ BPF получали в качестве контекста указатель на sk_buff. Однако, все не так просто по соображениям безопасности мы не хотим давать программам BPF доступ ко всей структуре и поэтому им выдается указатель на виртуальную структуру struct __sk_buff:


struct __sk_buff {    __u32 len;    __u32 pkt_type;    __u32 mark;    __u32 queue_mapping;    __u32 protocol;    __u32 vlan_present;    ...};

Поля структуры __sk_buff соответствуют существующим полям структуры sk_buff, но их значительно меньше и для каждого из них Verifier знает, как именно их отзеркалировать. В своем коде программы BPF используют структуру __sk_buff как существующую на самом деле:


int bpf_prog(struct __sk_buf *ctx){    __u32 len = ctx->len;    __u32 type = ctx->pkt_type;    ...}

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



Заметьте, что Verifier привел типы. В частности, для pkt_type, битового поля длины 3, Verifier подчистил все, не относящиеся к делу биты.


Как сгенерить такой листинг

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


Берем исходный файл skbuff.c с бессмысленной программой (но мы должны использовать поля как-то, чтобы компилятор их не отоптимизировал):


#include <linux/bpf.h>__attribute__((section("socket/test")))int bpf_prog(struct __sk_buff *ctx){    __u32 len = ctx->len;    __u32 type = ctx->pkt_type;    return len + type;}

Компилируем:


clang -target bpf -O2 skbuff.c -o skbuff.o -c

Загружаем (заметьте, кстати, что это одна из немногих типов программ, которые можно загрузить без рута):


mkdir mntsudo mount -t bpf none ./mntbpftool prog load ./skbuff.o ./mnt/xxx

Посмотрим на исходник:


$ llvm-objdump -D ./skbuff.o --section socket/test  0:    61 12 00 00 00 00 00 00 r2 = *(u32 *)(r1 + 0)  1:    61 10 04 00 00 00 00 00 r0 = *(u32 *)(r1 + 4)  2:    0f 20 00 00 00 00 00 00 r0 += r2  3:    95 00 00 00 00 00 00 00 exit

Посмотрим на то, что загрузилось:


$ sudo bpftool prog dump xlated pinned ./mnt/xxx   0: (61) r2 = *(u32 *)(r1 +104)   1: (71) r0 = *(u8 *)(r1 +120)   2: (54) w0 &= 7   3: (0f) r0 += r2   4: (95) exit

Управление трафиком в Linux


Если мы вернемся к схеме сетевого стека Linux, то увидим, что сразу после того, как для входящего пакета был создан sk_buff, он проходит через узел ingress qdisc. Аналогично, и на самом деле, более важно, что egress qdisc это последнее, что происходит с пакетом перед отправкой с устройства, и происходит это до/после всех возможных хуков netfilter.


Qdisc здесь расшифровывается как queueing discipline и является точкой входа в систему контроля трафиком в Linux Traffic Control (TC). Для исходящего трафика egress qdisc определяет алгоритм работы планировщика очередей сетевого устройства выставляет приоритеты и задержки при отправке пакетов, дропает лишние. Для входящего трафика, исторически и из-за его природы, возможности более ограниченные.


Кьюдисциплины бывают двух типов classful и classless поддерживающие классификацию и нет. Вторые более простые, так как алгоритм классификации в них встроен. Например, дефолтный egress qdisc, pfifo_fast, организует приоритетную очередь на основе поля TOS пакетов IPv4 и IPv6 (подробнее см. lartc 9.2):



Другой простой пример это qdisc noqueue, который отправляет пакет немедленно, если устройство не занято, а в противном случае дропает его.


Classful qdiscs являются строительными блоками для более изощренных систем. Они позволяют подразделять трафик на классы и к каждому из классов применять свой собственный алгоритм или подсоединять другие qdiscs. Таким образом, мы можем выстраивать деревья разбора произвольной сложности. Построение такого дерева не определяет как именно трафик распределяется по классам. Для того, чтобы деревья зажили активной сетевой жизнью, необходимы еще два строительных блока: классификаторы (classifiers) и действия (actions). Классификаторы позволяют программировать алгоритмы, по которым пакет отправляется в тот или оной класс, а действия позволяют решить, что делать с пакетом дальше, есть фильтр нашел совпадение. Примеры классификаторов: u32, flower и т.п. Примеры действий: drop (дропнуть пакет), reclassify (отправить на повторную проверку, например, после того, как мы отрезали VLAN tag), и т.п.


Итого, при помощи qdiscs и классов мы можем выстраивать деревья, а при помощи классификаторов и действий вдыхать в них жизнь. Но зачем нам выстраивать сложные деревья при помощи разных типов qdiscs и классов, если мы можем написать произвольную логику на C в рамках одной программы? Для этого в ядро были добавлены два типа программ BPF,BPF_PROG_TYPE_SCHED_CLS и BPF_PROG_TYPE_SCHED_ACT, позволяющие писать классификаторы и действия, соответственно. На практике второй из этих типов не нужен, так как существует специальный qdisc под названием clsact, который можно ставить как на egress, так и на ingress, и подключать к ней программы BPF типа BPF_PROG_TYPE_SCHED_CLS с флагом direct action. Этот флаг позволяет классификатору программе BPF возвращать значения actions, т.е. принимать окончательное решение о судьбе пакета в рамках только лишь одной программы.


Если вам интересно посмотреть на практическое применение BPF TC, то главные ресурсы это BPF Reference Guide от Daniel Borkman и исходники cilium самого мощного CNI для kubernetes, который теперь используется в Alibaba и Google.


Откуда есть пошел BPF


Оригинальной задачей классического BPF была фильтрация пакетов программа BPF подсоединялась к сокету и запускалась для каждого пакета, проходящего через него. Так как eBPF разрабатывался как усовершенствованная версия cBPF, не удивительно, что первый поддерживаемый тип программ eBPF повторял функциональность cBPF. А именно, программу BPF типа BPF_PROG_TYPE_SOCKET_FILTER можно подсоединить к любому открытому сокету при помощи опции SO_ATTACH_BPF. Интересной особенностью этого типа, является то, что ее может использовать обычный процесс без привилегий CAP_SYS_ADMIN.


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


  • Как уже было сказано, аналогично классическому BPF, программа BPF, подсоединенная к сокету вызывается для каждого следующего куска данных, пришедшего на сокет (для каждого sk_buff) и может делать только следующее: обрезать данные, в том числе до нуля (так, что пакет не будет доставлен). Обычно это используется для разнообразных снифферов, которые создают RAW сокет и при помощи прикрепленной программы получают только интересующие их пакеты, может быть обрезанные только до хедеров. Некоторые более интересные варианты использования программ типа BPF_PROG_TYPE_SOCKET_FILTER с сокетами можно найти в докладе Evil eBPF In-Depth с DEFCONF 27.


  • У сокетов типа AF_PACKET есть замечательная опция под названием PACKET_FANOUT, которая позволяет объединить несколько сокетов в группу и распределять нагрузку между ними. Это нужно, например, для реализации систем типа DPI. Про использование этой опции можно почитать на хабре в статье Захват пакетов в Linux на скорости десятки миллионов пакетов в секунду. В 2015 к fanout была добавлена опция PACKET_FANOUT_DATA, которая позволяет распределять нагрузку между сокетами при помощи программы BPF.


  • В 2007 году был написал модуль xt_bpf подсистемы netfilter. В качестве аргумента он принимал классическую программу BPF, которая решала судьбу пакета. В 2016 была добавлена поддержка eBPF теперь судьбу пакета может решать и eBPF программа типа BPF_PROG_TYPE_SOCKET_FILTER.


  • Фильтры можно подсоединять не только к сокетам, но, например, к tun устройствам, что, например, позволяет делать более эффективную фильтрацию пакетов, направляющихся в VM. См. TUNSETFILTEREBPF.


  • Для тех же tun устройств при помощи программ BPF можно выбирать в какую очередь направится пакет. См. TUNSETSTEERINGEBPF.


  • Kernel Connection Multiplexor позволяет создавать пул TCP сокетов и поверх них создавать пул из datagram сокетов (см. lwn, kcm). Поток байтов конкретного TCP соединения парсится при помощи обязательной BPF программы типа BPF_PROG_TYPE_SOCKET_FILTER, подсоединенной к специальному сокету типа AF_KCM при помощи SIOCKCMATTACH (см. случайный пример, который я загуглил).


  • Наконец, программы типа BPF_PROG_TYPE_SOCKET_FILTER можно присоединять к сокетам при помощи SO_ATTACH_REUSEPORT_EBPF, см. статью Perfect locality and three epic SystemTap scripts.



Рассекатель потоков и flower


Функция ядра __skb_flow_dissect, довольно объемная, реализует в ядре Linux flow dissector функциональность по вытаскиванию мета-данных из пакета. Она используется в нескольких местах ядра, в частности, одним из популярных ingress фильтров системы контроля трафиком Linux, flower.


Эта функция, однако, слишком умная, иможет быть небезопасной. Для ее оптимизации и защиты периметра был добавлен новый тип программ BPF BPF_PROG_TYPE_FLOW_DISSECTOR, который позволяет заменить эту функцию ядра на программу BPF. Сделать это можно отдельно для каждого из сетевых namespace.


BPF и контрольные группы


Контрольные группы (cgroups) позволяют создавать группы процессов и иерархию между ними и приписывать различным группам разное количество ресурсов. И всем дочитавшим до сюда уже должно быть ясно, как BPF может быть тут использован: мы можем писать и подгружать программы BPF для динамического управления процессами внутри одной cgroup. В основном, программы из этой серии (и из следующей, сокетной) добавляются для решения конкретных бизнес-задач, но механизмы получаются довольно универсальными. Интерфейсы cgroups и, тем более, сокетов создавались довольно давно, их ванильной функциональности зачастую недостаточно, но менять сами базовые интерфейсы под конкретные бизнес-нужды никто не хочет. И тут на помощь приходит BPF. Давайте посмотрим на то, чем мы можем управлять.


Программы типа BPF_PROG_TYPE_CGROUP_SKB позволяют подсоединять фильтры BPF ко всему трафику приходящему (ingress) или выходящему (egress) из конкретной группы. Если программа возвращает 1, то пакет пропускается, если 0, то дропается. Если группа входит в иерархию, то к пакетам также применяются фильтры из родительской группы. Таким образом мы можем фильтровать трафик для контейнеров, виртуалок, и т.п. Пример: как писать и подсоединять фильры BPF к сервисам systemd.


Помимо фильтрации, программы BPF могут применяться для управления логикой создания и управления ресурсов. Например, программы BPF типа BPF_PROG_TYPE_CGROUP_SOCK позволяют влиять на процесс создания и удаления сокетов, подправляя нужные поля в структуре struct sock. Оригинально этот тип был добавлен для управления полем sk_bound_dev_if структуры сокета, позволяющем привязать сокет к конкретному сетевому устройству. Этот же тип программы может присоединяться в момент системного вызова bind(2) и разрешать/запрещать определенные значения параметров или собирать информацию для дальнейшей обработки.


Для управления разными вопросами, касающимися адресации, существуют программы BPF типа BPF_PROG_TYPE_CGROUP_SOCK_ADDR. Изначально они позволяли подсоединяться к bind и подправлять IP адреса и порты, к которым программа байндилась (конкретный use case был такой: процессы внутри одной cgroup должны использовать один конкретный адрес из нескольких, доступных на сервере, см. коммит). В дальнейшем была добавлена возможность подсоединять программы такого типа к системным вызовам connect, getpeername, getsockname, sendmsg и recvmsg. Как иллюстрацию о том, зачем это нужно, можно посмотреть доклад о том, как cilium избавлялись от iptables в k8s.


Программы типа BPF_PROG_TYPE_CGROUP_SOCKOPT позволяют управлять поведением системного вызова setsockopt.


Программы типа BPF_PROG_TYPE_CGROUP_DEVICE позволяют для cgroupv2 реализовать контроллер, аналогичный контроллеру device в cgroupsv1.


Программы типа BPF_PROG_TYPE_CGROUP_SYSCTL позволяют подсоединяться к конкретным sysctl и разрешать, запрешать, записывать поведение процессов, работающих в конкретной cgroup и пытающихся поменять настройки системы.


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


BPF и сокеты


Программы типа BPF_PROG_TYPE_SK_SKB позволяют перенаправлять пакеты между сокетами. При этом подсоединяются они интересным способом: создается специальный мап типа SOCKMAP, к которому подсоединяется программа. Дальше, в этот мап можно помещать один и более сокетов и тогда, когда у этих сокетов случается recvmsg, то вызывается программа BPF, которая может перенаправить данный sk_buff на другой сокет. Этот тип программ, оригинально разработанные Isovalent для построения их CNI cilium для k8s, нашли свое применение и в компании Cloudfare, см. статью SOCKMAP TCP splicing of the future.


Добавленные вскоре после BPF_PROG_TYPE_SK_SKB, программы типа BPF_PROG_TYPE_SK_MSG нужны для того, чтобы запускаться на каждый sendmsg или sendpage и позволяющий анализировать данные, пересылаемые через сокет и реализовывать файерволы уровня L7 пакеты могут быть либо пропущены дальше, либо отброшены. Аналогично с предыдущим типом программ BPF_PROG_TYPE_SK_SKB, программы данного типа подсоединяются к мапу типа sockmap и сокеты активируются при помощи добавления в этот мап.


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


Интересный тип программ BPF_PROG_TYPE_SK_LOOKUP позволяет выбирать сокет для доставки пакета в тот момент, когда он уже попал в транспортный уровень. Два оригинальных примера использования: пересылать на один и тот же сокет пакеты, приходящие на диапазон IP адресов, пересылать в один сокет пакеты, приходящие на определенный адрес, вне зависимости от порта. Программы этого типа подсоединяются к сетевым namespaces.


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


LWT: туннелирование в один клик


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


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



Для того, чтобы туннелировать пакет в Linux, исторически необходимо было создать специальный виртуальный интерфейс: ip link add name ipip0 type ipip... и т.п. В 2015 году в Linux появилась возможность привязывать правила инкапсуляции прямо к маршрутам. Это оказывается сильно дешевле, но, конечно, не всегда применимо иногда хочется настраивать параметры отдельных сетевых устройств.


Однако, через год, в 2016 году, разработчики добавили возможность реализовывать любую логику! А именно, в качестве правил стало можноуказывать программы BPF трех следующих типов:



Здесь input вызывается для входящих пакетов, output для выходящих, а xmit прямо перед отправкой пакета на устройство. В качестве контекста программам выдается указатель на struct __sk_buff, на основе которого они могут его пропустить (BPF_OK), дропнуть (BPF_DROP), отправить на повторный поиск маршрута (BPF_REDIRECT) или, наконец, направить на другое устройство вывода (BPF_DROP). Кроме этого, программы типа xmit могут менять содержимое пакета добавлять и дропать заголовки, переписывать данные.


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


ip route add 10.0.0.0/24 encap bpf xmit obj <prog.o> section <section> dev <dev>

где <prog.o> ваш BPF ELF, <section> секция с вашей программой в этом объектнике.


В 2018 году был добавлен новый тип программ, BPF_PROG_TYPE_LWT_SEG6LOCAL, который позволяет запускать программы в качестве функции туннеля типа seg6local, подробнее см. Using SRv6.


Программы невидимого фронта


Остался только один не рассмотренный нами тип программ BPF: BPF_PROG_TYPE_UNSPEC. Этот тип программы, как следует из названия, используется в качестве дефолтного/неопределенного типа. Загрузить программу такого типа в ядро при помощи системного вызова bpf(2) не получится.


Однако, программы этого типа являются одними из самых используемых на практике! И с вероятностью 99% вы тоже их использовали, может даже сегодня. Дело в том, что классический BPF в Linux теперь всегда транслируется в программу типа BPF_PROG_TYPE_UNSPEC на новом BPF, а это значит, что когда вы, например, используете tcpdump или wireshark на Linux, вы запускаете такую программу.


Арест, заключение и ссылки


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


В следующих статьях мы начнем более детально знакомиться с конкретными приложениями и начнем со статьи (или, скорее, нескольких) про трассировку Linux программах BPF для работы с kprobes, tracepoints и perf events, а также доступных средствах разработки и готовых решениях libbpf, bcc и bpftrace.


Ссылки


Предыдущие статьи этого цикла


  1. BPF для самых маленьких, часть нулевая: classic BPF
  2. BPF для самых маленьких, часть первая: extended BPF

2,5 книжки


  • Brendan Gregg, BPF Performance Tools. БГ распознал потенциал BPF в деле трассировки Linux сразу после его появления и данная книжка описывает результаты работы последних пяти лет отладочные утилиты из пакета BCC, около сотни которых было написано специально для этой книжки. Книжка является отличным справочным дополнением по BPF к следующей.


  • Brendan Gregg, Systems Performance: Enterprise and the Cloud, 2nd Edition (2020). Еще не вышедшее второе издание знаменитой Systems Performance. Главные изменения: добавлен материал по BPF, выкинут материал по Solaris, а сам БГ стал на пять лет опытнее. Если книжка BPF Performance Tools отвечает на вопрос как?, то эта книжка отвечает на вопрос почему?


  • David Calavera and Lorenzo Fontana, Linux Observability with BPF. Эта книжка мне не понравилась. Авторы явно торопились написать Первую Книгу про BPF, но ничего нового, по сравнению с уже существующими на тот момент блогами, не написали.



Online-ресурсы, статьи и блоги


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


Подробнее..

Пишем драйвер фреймбуфера для Raspberry Pi с LCD

05.01.2021 00:04:18 | Автор: admin

Прочитав монументальную серию статей о подключении LCD экрана к роутеру мне захотелось сделать то же самое. Однако многообразие используемого стека (openwrt, stm32, usb) в сочетании с отсутствием полных исходников кода но может плохо искал несколько затруднило задачу. Я решил начать с малого написать свою реализацию framebuffer для raspberry и вывести графическую среду raspberry на LCD. Что из этого получилось, описываю далее.


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


LCD


LCD 320x240 с контроллером ILI9341. Передача данных по 8 битной шине.


Запись данных в LCD осуществляется следующим образом (стр.28):


1 на RD и 1 на RESET после старта LCD держим все время. Перед передачей данных подаем 0 на CS, выставляем 8 бит данных на шине, устанавливаем 1 или 0 на RS (D/CX на графике) в зависимости от типа передачи данные / команда, сбрасываем WR в 0, затем устанавливаем в 1. После окончания передачи данных выставляем CS в 1.


Код передачи данных / команд
/* файл lcd.c */void LCD_write(u8 VAL){    LCD_CS_CLR;    DATAOUT(VAL);    LCD_WR_CLR;    LCD_WR_SET;    LCD_CS_SET;}/* передача команды */void LCD_WR_REG(u8 data){    LCD_RS_CLR;    LCD_write(data);}/* передача данных */void LCD_WR_DATA(u8 data){    LCD_RS_SET;    LCD_write(data);}/* запись значения в регистр */void LCD_WriteReg(u8 LCD_Reg, u8 LCD_RegValue){    LCD_WR_REG(LCD_Reg);    LCD_WR_DATA(LCD_RegValue);}/* передача 16 бит данных */void Lcd_WriteData_16Bit(u16 Data){    LCD_RS_SET;    LCD_CS_CLR;    DATAOUT((u8)(Data>>8));    LCD_WR_CLR;    LCD_WR_SET;    DATAOUT((u8)Data);    LCD_WR_CLR;    LCD_WR_SET;    LCD_CS_SET;}

Основной код управления LCD (для STM32), в основном взят отсюда и адаптирован для raspberry. Цвет каждого пикселя на LCD задается 16 битами в формате RGB565 (5 бит на красный цвет, 6 на зеленый, 5 на синий).


Код управления LCD
/* файл lcd.h */#define LCD_W 320#define LCD_H 240/* файл lcd.c *//* индикация того, что далее передаются данные для видеобуфера */void LCD_WriteRAM_Prepare(void){    LCD_WR_REG(0x2C);}/* задаем прямоугольник на экране, который будем отрисовывать */void LCD_SetWindows(u16 xStart, u16 yStart,u16 xEnd,u16 yEnd){    LCD_WR_REG(0x2A);    LCD_WR_DATA(xStart>>8);    LCD_WR_DATA(0x00FF&xStart);    LCD_WR_DATA(xEnd>>8);    LCD_WR_DATA(0x00FF&xEnd);    LCD_WR_REG(0x2B);    LCD_WR_DATA(yStart>>8);    LCD_WR_DATA(0x00FF&yStart);    LCD_WR_DATA(yEnd>>8);    LCD_WR_DATA(0x00FF&yEnd);    LCD_WriteRAM_Prepare();}/* ресет экрана */void LCD_RESET(void){    LCD_RST_CLR;    delay(100);    LCD_RST_SET;    delay(50);}/* инициализация экрана */void LCD_Init(void){    LCD_RESET();    LCD_WR_REG(0xCF);    LCD_WR_DATA(0x00);    LCD_WR_DATA(0xC9);    LCD_WR_DATA(0X30);    LCD_WR_REG(0xED);    LCD_WR_DATA(0x64);    LCD_WR_DATA(0x03);    LCD_WR_DATA(0X12);    LCD_WR_DATA(0X81);    LCD_WR_REG(0xE8);    LCD_WR_DATA(0x85);    LCD_WR_DATA(0x10);    LCD_WR_DATA(0x7A);    LCD_WR_REG(0xCB);    LCD_WR_DATA(0x39);    LCD_WR_DATA(0x2C);    LCD_WR_DATA(0x00);    LCD_WR_DATA(0x34);    LCD_WR_DATA(0x02);    LCD_WR_REG(0xF7);    LCD_WR_DATA(0x20);    LCD_WR_REG(0xEA);    LCD_WR_DATA(0x00);    LCD_WR_DATA(0x00);    LCD_WR_REG(0xC0);        LCD_WR_DATA(0x1B);       LCD_WR_REG(0xC1);        LCD_WR_DATA(0x00);       LCD_WR_REG(0xC5);        LCD_WR_DATA(0x30);       LCD_WR_DATA(0x30);       LCD_WR_REG(0xC7);       LCD_WR_DATA(0XB7);    LCD_WR_REG(0x36);        LCD_WR_DATA(0x08);    LCD_WR_REG(0x3A);    LCD_WR_DATA(0x55);    LCD_WR_REG(0xB1);    LCD_WR_DATA(0x00);    LCD_WR_DATA(0x1A);    LCD_WR_REG(0xB6);        LCD_WR_DATA(0x0A);    LCD_WR_DATA(0xA2);    LCD_WR_REG(0xF2);        LCD_WR_DATA(0x00);    LCD_WR_REG(0x26);        LCD_WR_DATA(0x01);    LCD_WR_REG(0xE0);        LCD_WR_DATA(0x0F);    LCD_WR_DATA(0x2A);    LCD_WR_DATA(0x28);    LCD_WR_DATA(0x08);    LCD_WR_DATA(0x0E);    LCD_WR_DATA(0x08);    LCD_WR_DATA(0x54);    LCD_WR_DATA(0XA9);    LCD_WR_DATA(0x43);    LCD_WR_DATA(0x0A);    LCD_WR_DATA(0x0F);    LCD_WR_DATA(0x00);    LCD_WR_DATA(0x00);    LCD_WR_DATA(0x00);    LCD_WR_DATA(0x00);    LCD_WR_REG(0XE1);        LCD_WR_DATA(0x00);    LCD_WR_DATA(0x15);    LCD_WR_DATA(0x17);    LCD_WR_DATA(0x07);    LCD_WR_DATA(0x11);    LCD_WR_DATA(0x06);    LCD_WR_DATA(0x2B);    LCD_WR_DATA(0x56);    LCD_WR_DATA(0x3C);    LCD_WR_DATA(0x05);    LCD_WR_DATA(0x10);    LCD_WR_DATA(0x0F);    LCD_WR_DATA(0x3F);    LCD_WR_DATA(0x3F);    LCD_WR_DATA(0x0F);    LCD_WR_REG(0x2B);    LCD_WR_DATA(0x00);    LCD_WR_DATA(0x00);    LCD_WR_DATA(0x01);    LCD_WR_DATA(0x3f);    LCD_WR_REG(0x2A);    LCD_WR_DATA(0x00);    LCD_WR_DATA(0x00);    LCD_WR_DATA(0x00);    LCD_WR_DATA(0xef);    LCD_WR_REG(0x11);     delay(120);    LCD_WR_REG(0x29);     LCD_WriteReg(0x36,(1<<3)|(1<<5)|(1<<6)); }/* заполняем экран одним цветом */void LCD_Clear(u16 Color){    unsigned int i;    LCD_SetWindows(0,0,LCD_W-1,LCD_H-1);    for(i=0;i<LCD_H*LCD_W;i++)    {        Lcd_WriteData_16Bit(Color);    }}/* рисуем картинку из raw файла (в нем подряд идут цвета пикселей в формате RGB565) */void LCD_draw_image(char *file){    int fd = open(file, O_RDWR);    if(fd < 0){        perror("Open file");        exit(1);    }    u16 buffer[128];    LCD_SetWindows(0,0,LCD_W-1,LCD_H-1);    while(1){        int nread = read(fd, buffer, 256);        if(nread == 0 || nread < 0)            break;        /* buffer[i] - 2 байта, поэтому пишем nread/2 раз */        for(int i=0; i < nread/2; i++){            Lcd_WriteData_16Bit(buffer[i]);        }    }    close(fd);}

Raspberry


Я использую raspberry pi 3 с установленным raspbian lite (версия ядра 4.14). GUI добавлено установкой пакетов lxde и xinit.


sudo apt-get install lxde xinit

Расположение GPIO



Подключение LCD к raspberry


  • LCD Data 0 -> GPIO 12
  • LCD Data 1 -> GPIO 13
  • ...
  • LCD Data 7 -> GPIO 19
  • LCD CS -> GPIO 20
  • LCD RS -> GPIO 21
  • LCD RST -> GPIO 22
  • LCD WR -> GPIO 23
  • LCD RD -> GRPIO 24
  • LCD 5V -> 5V
  • LCD GND -> Ground

Управление GPIO


В raspberry GPIO можно управлять через прямое обращение к памяти. Из мануала к BCM 2837 32 битные регистры GPFSEL0-5 используются для установки режима GPIO. На каждый GPIO пин отводится 3 бита. Пину 0 соответствуют биты 2-0 в GPFSEL0, пину 1 биты 5-3 и т.д. Каждый регистр управляет 10 GPIO. Биты 000 соответствуют режиму input, биты 001 режиму output. Установку режима можно описать следующим образом:


/* файл rpi_gpio.h *//* установка input режима */#define INP_GPIO(g) *(gpio+((g)/10)) &= ~(7<<(((g)%10)*3))/* установка output режима */#define OUT_GPIO(g) *(gpio+((g)/10)) |=  (1<<(((g)%10)*3))

Для пинов 0 31 в режиме output установка 1 делается через регистр GPSET0. Чтобы установить GPIO n в 1, в регистр нужно записать число, n-ый бит в котором равен 1. Например, для установки 1 в GPIO 10 и 11 в регистр GPSET0 необходимо записать число 0b11 << 10.


Аналогично, установка 0 осуществляется через регистр GPCLR0.


/* устанавливаем 1 на GPIO, например, 1 на GPIO 10 - GPIO_SET = 1<<10 */#define GPIO_SET *(gpio+7)/*  устанавливаем 0 на GPIO, например, 0 на GPIO 10 - GPIO_CLR = 1<<10 */#define GPIO_CLR *(gpio+10)

gpio содержит виртуальный адрес физического адреса 0x3F200000 (отображенного посредством mmap в виртуальную память процесса). *gpio позволяет обратиться к GPFSEL0. *(gpio+7) к GPSET0. *(gpio+10) к GPCLR0.


Код установки gpio
/* файл rpi_gpio.c */int setup_rpi_gpio(){    unsigned int gpio_base_addr = 0x3F200000;   /* open /dev/mem */   if ((mem_fd = open("/dev/mem", O_RDWR|O_SYNC) ) < 0) {      printf("can't open /dev/mem \n");      return -1;   }   /* mmap GPIO */   gpio_map = mmap(      NULL,             //Any adddress in our space will do      BLOCK_SIZE,       //Map length      PROT_READ|PROT_WRITE,// Enable reading & writting to mapped memory      MAP_SHARED,       //Shared with other processes      mem_fd,           //File to map      gpio_base_addr    //Offset to GPIO peripheral   );   close(mem_fd); //No need to keep mem_fd open after mmap   if (gpio_map == MAP_FAILED) {      printf("mmap error %d\n", (int)gpio_map);//errno also set!      return -1;   }   // Always use volatile pointer!   gpio = (volatile uint32_t *)gpio_map;   return 0;}

Управление LCD c raspberry


Пинами LCD управляем следующим образом:
/* файл lcd.h */#define BIT_BASE 12#define CS   20#define RS   21#define RST  22#define WR   23#define RD   24#define LCD_CS_SET  GPIO_SET=(1<<CS)#define LCD_RS_SET  GPIO_SET=(1<<RS)#define LCD_RST_SET GPIO_SET=(1<<RST)#define LCD_WR_SET  GPIO_SET=(1<<WR)#define LCD_RD_SET  GPIO_SET=(1<<RD)#define LCD_CS_CLR  GPIO_CLR=(1<<CS)#define LCD_RS_CLR  GPIO_CLR=(1<<RS)#define LCD_RST_CLR GPIO_CLR=(1<<RST)#define LCD_WR_CLR  GPIO_CLR=(1<<WR)#define LCD_RD_CLR  GPIO_CLR=(1<<RD)#define DATAOUT(x) GPIO_SET=(x<<BIT_BASE);GPIO_CLR=(x<<BIT_BASE)^(0xFF<<BIT_BASE)

Проверка работы с LCD в user space


Перед тем как бросаться в пучину kernel, проверим работу с LCD в user space. Подготовим картинку image.jpg в формате raw 320x240. В output.raw содержатся подряд идущие 16 битные значения цвета каждого пикселя (RGB565):


mogrify -format bmp -resize 320 -crop 320x240 image.jpgffmpeg -vcodec bmp -i image.bmp -vcodec rawvideo -f rawvideo -pix_fmt rgb565 output.raw

Выведем output.raw на LCD:


/* файл main.c */int main(int argc , char *argv[]){    if( setup_rpi_gpio() ) {        printf("Cannot map GPIO memory, probably use <sudo>\n");        return -1;    }    for(int i = BIT_BASE; i <= RD; i++){        INP_GPIO(i);        OUT_GPIO(i);    }    //set BITS_BASE - RD to 1    GPIO_SET = 0xFFF<<12;    GPIO_SET = 1 << RD;    LCD_Init();    if(argc >= 2){        LCD_draw_image(argv[1]);    }}

gcc main.c rpi_gpio.c lcd.c -o mainsudo ./main output.raw


Подготовка окружения


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


Заголовки ядра со скриптами сборки для текущей версии ядра в raspbian так просто не поставить, поэтому скачаем исходный код linux, скомпилируем и установим ядро, и будем использовать эти заголовки со скриптами для компиляции драйвера. Основной reference по этому процессу здесь. Версия сорцов ядра подобрана под мою версию raspbian.


git clone --depth=1 -b rpi-4.14.y https://github.com/raspberrypi/linux.gitcd linuxKERNEL=kernel7make bcm2709_defconfigmake -j4 zImage modules dtbssudo make modules_installsudo cp arch/arm/boot/dts/*.dtb /boot/sudo cp arch/arm/boot/dts/overlays/*.dtb* /boot/overlays/sudo cp arch/arm/boot/dts/overlays/README /boot/overlays/sudo cp arch/arm/boot/zImage /boot/$KERNEL.img

Компиляцию драйвера в дальнейшем выполняем командой make, поместив в директорию с драйвером вот такой Makefile:


Makefile
ifeq ($(KERNELRELEASE),)    KERNELDIR ?= /lib/modules/$(shell uname -r)/build        PWD := $(shell pwd)modules:    $(MAKE) -C $(KERNELDIR) M=$(PWD) modulesmodules_install:    $(MAKE) -C $(KERNELDIR) M=$(PWD) modules_installclean:    rm -rf *.o *~ core .depend .*.cmd *.ko *.mod.c .tmp_versions.PHONY: modules modules_install cleanelse    # имя драйвера, если компилируем vfb.c, заменим на vfb.o        obj-m := lcd_drv_simple.oendif

Драйвер фреймбуфера


Теория фреймбуферов хорошо расписана здесь и здесь, поэтому повторяться не буду.


Начнем с виртуального фреймбуфера (vfb.c). Он выделяет область памяти, в которую пишет изображение, направленное в /dev/fbX (X номер устройства). Это изображение потом можно легко прочитать через cat /dev/fbX. Этот драйвер удобен для тестирования (в нашем случае того, что компиляция и установка драйвера проходит успешно).


Код берем отсюда. Далее


makesudo cp vfb.ko /lib/modules/$(uname -r)/extra/# просим систему обновить зависимостиsudo depmod# загружаем драйверsudo modprobe vfb_enable=1# устанавливаем размер экрана и глубину цвета (16 бит, режим RGB565)fbset -fb /dev/fb1 -g 320 240 320 240 16

Должно появиться новое framebuffer устройство (/dev/fb1). Запишем в него какое-нибудь изображение,


sudo apt-get install fbi# fbi требует запуска из полноценной консоли, если запускаем под ssh используем sudo и -T 1 для указания первой консоли sudo fbi -a -d /dev/fb1 -T 1 image.jpg

считаем его


cat /dev/fb1 > scrn.raw

и откроем в gimp как файл raw rgb565. Убедимся, что изображение есть.


Простой драйвер


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


Установку режима и уровня (1/0) пинов модифицируем следующим образом (просто прямой доступ к I/O памяти в ядре не работает):


/* файл lcd_drv_simple.c */static void inp_gpio(u32 g){    u32 *addr = gpio+g/10;    u32 val = readl(addr);    u32 tmp =  ~(7<<((g%10)*3));    val &= tmp;    writel(val,addr);}static void out_gpio(u32 g){    u32 *addr = gpio+g/10;    u32 val = readl(addr);    u32 tmp =  (1<<(((g)%10)*3));    val |= tmp;    writel(val,addr);}static void GPIO_SET(u32 val){    writel(val,gpio+7);}static void GPIO_CLR(u32 val){    writel(val,gpio+10);}

Адрес gpio получаем вызовом ioremap:


gpio = ioremap(PORT, RANGE);

Параметры драйвера описываются в структурах:
u32 *gpio;static unsigned PORT = 0x3F200000;static unsigned RANGE =  0x40;#define W 320#define H 240static struct fb_fix_screeninfo ili9341_fix  = {        .type        = FB_TYPE_PACKED_PIXELS,        .visual      = FB_VISUAL_TRUECOLOR,        .accel       = FB_ACCEL_NONE,        .line_length = W * 2,};static struct fb_var_screeninfo ili9341_var  = {        .xres        = W,        .yres        = H,        .xres_virtual    = W,        .yres_virtual    = H,        .width        = W,        .height        = H,        .bits_per_pixel = 16,        .red         = {11, 5, 0}, /* смещение 11 бит, 5 битов на красный цвет */        .green         = {5, 6, 0}, /* смещение 5 бит, 6 битов на зеленый цвет */        .blue         = {0, 5, 0}, /* смещение 0 бит, 5 битов на синий цвет */        .activate     = FB_ACTIVATE_NOW,        .vmode     = FB_VMODE_NONINTERLACED,};/* используем готовую реализацию операций с фреймбуфером */static struct fb_ops ili9341_fbops = {        .owner        = THIS_MODULE,        .fb_write     = fb_sys_write,        .fb_fillrect  = sys_fillrect,        .fb_copyarea  = sys_copyarea,        .fb_imageblit = sys_imageblit,        .fb_setcolreg   = ili9341_setcolreg,};/* ссылки на функции probe и remove */struct platform_driver ili9341_driver = {        .probe = ili9341_probe,        .remove = ili9341_remove,        .driver = { .name = "my_fb_driver" }};/* задаем функцию ili9341_update, обновляющую экран (частота обновления задается в параметре delay) */static struct fb_deferred_io ili9341_defio = {        .delay          = HZ / 25,        .deferred_io    = &ili9341_update,};

Основные функции:
static int  ili9341_probe(struct platform_device *dev){    int ret = 0;    struct ili9341 *item;    struct fb_info *info;    unsigned char  *videomemory;    printk("ili9341_probe\n");    /*выделяем память под вспомогательную структуру для хранения указателей */    item = kzalloc(sizeof(struct ili9341), GFP_KERNEL);    if (!item) {        printk(KERN_ALERT "unable to kzalloc for ili9341\n");        ret = -ENOMEM;        goto out;    }    /* заполняем ее */    item->dev = &dev->dev;    dev_set_drvdata(&dev->dev, item);    /* получаем ссылку на минимально инициализированный fb_info */    info = framebuffer_alloc(0, &dev->dev);    if (!info) {        ret = -ENOMEM;        printk(KERN_ALERT "unable to framebuffer_alloc\n");        goto out_item;    }    item->info = info;    /* заполняем структуру fb_info нашими данными */    info->par = item;    info->dev = &dev->dev;    info->fbops = &ili9341_fbops;    info->flags = FBINFO_FLAG_DEFAULT;    info->fix = ili9341_fix;    info->var = ili9341_var;    info->fix.smem_len = VIDEOMEM_SIZE; // размер буфера видеопамяти    info->pseudo_palette = &pseudo_palette;    /* выделяем память под видеобуфер, в который пишут приложения, использующие /dev/fbX */    videomemory=vmalloc(info->fix.smem_len);    if (!videomemory)    {        printk(KERN_ALERT "Can not allocate memory for framebuffer\n");        ret = -ENOMEM;        goto out_info;    }    /* прописываем его в структуре fb_info и сохраняем в нашей структуре ili9341 для дальнейшего использования */    info->fix.smem_start =(unsigned long)(videomemory);    info->screen_base = (char __iomem *)info->fix.smem_start;    item->videomem = videomemory;    /* заполняем информацию об отложенном обновлении экрана */    info->fbdefio = &ili9341_defio;    fb_deferred_io_init(info);    /* передаем заполненную структуру fb_info ядру */    ret = register_framebuffer(info);    if (ret < 0) {        printk(KERN_ALERT "unable to register_frambuffer\n");        goto out_pages;    }    if (ili9341_setup(item)) goto out_pages;    return ret;    out_pages:    kfree(videomemory);    out_info:    framebuffer_release(info);    out_item:    kfree(item);    out:    return ret;}int ili9341_setup(struct ili9341 *item){    int i;    /* отображаем адрес для работы с портами GPIO в gpio */    gpio = ioremap(PORT, RANGE);    if(gpio == NULL){        printk(KERN_ALERT "ioremap error\n");        return 1;    }    /* инициализируем LCD */    for(i = BIT_BASE; i <= RD; i++){        inp_gpio(i);        out_gpio(i);    }    GPIO_SET(0xFFF<<12);    GPIO_SET(1 << RD);    LCD_Init();    printk("ili9341_setup\n");    return 0;}static void ili9341_update(struct fb_info *info, struct list_head *pagelist){    /* получаем ссылку на нашу структуру с указателями */    struct ili9341 *item = (struct ili9341 *)info->par;    /* адрес видеопамяти */    u16 *videomemory = (u16 *)item->videomem;    int i, j, k;    /* заполняем весь экран */    LCD_SetWindows(0,0,LCD_W-1,LCD_H-1);        for(i = 0; i < LCD_W * LCD_H; i++){        /* читаем данные из видеопамяти попиксельно и записываем их в LCD */        Lcd_WriteData_16Bit(readw(videomemory));        videomemory++;    }}

Запускаем графическую оболочку на LCD


Проверим работу драйвера. Скомпилируем, установим и загрузим его


makesudo cp lcd_drv_simple.ko /lib/modules/$(uname -r)/extra/sudo depmodsudo modprobe lcd_drv_simple

Выведем случайное изображение:


cat /dev/urandom > /dev/fb1

Выведем на соответствующий /dev/fbX картинку или видео:


sudo fbi -a -d /dev/fb1 -T 1 image.jpgmplayer -vo fbdev:/dev/fb1 video.mp4

Запустим графическую оболочку на LCD. Если Desktop environment (DE) еще не установлено (например, серверный вариант raspbian), его можно поставить:


sudo apt-get install lxde

Создадим файл /etc/X11/xorg.conf:


Section "Device"    Identifier "FBDEV"    Driver "fbdev"    Option "fbdev" "/dev/fb1"EndSection

и добавим в /etc/rc.local:


/sbin/modprobe lcd_drv_simple

После перезагрузки на LCD должна появиться графическая оболочка.


Ускоряем работу драйвера


Предыдущий вариант драйвера прост, но не очень быстр. Полная перерисовка экрана заметна. Deferred_io хорошо тем, что ядро передает в функцию ili9341_update список измененных страниц видеопамяти, которые и нужно перерисовать на экране. Т.е. необходимо понять, какая область экрана соответствует заданным 4096 байтам (размер страницы памяти).


  • Первые 4096 байтов соответствуют полным 6 линиям и 128 пикселям 7ой линии, т.к. 4096 = 320*2*6 + 128*2 (2 байта на каждый пиксель)
  • Вторые 4096 байтов начинаются с 129 пикселя 7ой линии, требуют 384 байта для завершения линии (128*2 + 384 = 640), затем идут 5 полных линий и 256 пикселей в 6 линии (4096 = 384 + 640*5 + 512).

Аналогично продолжаем рассуждения дальше, получается, что каждые 5 страниц ситуация повторяется. Поэтому достаточно прописать 5 вариантов отрисовки страницы памяти на экране. Отдельно прописываем работу с последней страницей номер 37, т.к. она занимает 2048 байтов:


Код драйвера
/* файл lcd_drv_fast.c *//* далее используем атомарные операции, которые по факту не очень нужны, т.к. метод ili9341_touch на raspberry ни разу не вызывался (т.е. нет ситуации нескольких потоков выполнения, изменяющих toUpdate одновременно */ static void ili9341_update(struct fb_info *info, struct list_head *pagelist){    struct ili9341 *item = (struct ili9341 *)info->par;    struct page *page;    int i;        /* для измененных страниц вычитаем 1 из toUpdate атомарно, toUpdate для этих страниц принимает значение -2 */     list_for_each_entry(page, pagelist, lru)    {        atomic_dec(&item->videopages[page->index].toUpdate);    }    for (i=0; i<FP_PAGE_COUNT; i++)    {        /* для всех страниц увеличиваем toUpdate на 1. Если страница не измененена, то вычтем 1 обратно и получим -1. Если изменена, то также получим -1 после инкремента, но в этом случае еще и выполним отрисовку измененной страницы */        if(atomic_inc_and_test(&item->videopages[i].toUpdate)){            atomic_dec(&item->videopages[i].toUpdate);        }        else        {            draw(item, i);                  }    }}static void draw(struct ili9341 *item, int page){    int xs,ys,i;    /* рассчитываем адрес страницы в видеопамяти */    u16 *videomemory = (u16*)(item->videomem + PAGE_SIZE*page);    /* строка LCD, с которой начинается страница */    ys = (((unsigned long)(PAGE_SIZE*page)>>1)/W);    /* короткая страница памяти, обрабатываем отдельно */    if (page == 37){        // write PAGE_SIZE / 2;        //write 128 bytes        LCD_SetWindows(256, ys, LCD_W-1, ys);        for(i = 0; i < 128 / 2; i++){            Lcd_WriteData_16Bit(readw(videomemory));            videomemory++;        }        //write 3 lines        LCD_SetWindows(0, ys+1, LCD_W-1, ys+6);        for(i = 0; i < 640 * 3 / 2; i++){            Lcd_WriteData_16Bit(readw(videomemory));            videomemory++;        }    }    else{        switch (page % 5){        //xs = 0. write full six lines and 256 bytes        //640 * 6 + 256        case 0:            //write 6 lines            LCD_SetWindows(0,ys,LCD_W-1,ys + 5);            for(i = 0; i < 640 * 6 / 2; i++){                Lcd_WriteData_16Bit(readw(videomemory));                videomemory++;            }            //write 256 bytes            LCD_SetWindows(0, ys+6, 256/2-1, ys + 6); //7th line from x = 0 to x = 256/2            for(i = 0; i < 256 / 2; i++){                Lcd_WriteData_16Bit(readw(videomemory));                videomemory++;            }            break;        //xs = 128 (256 bytes). write 384 bytes, 5 full lines and 512 bytes        //384 + 640 * 5 + 512        case 1:            //write 384 bytes            LCD_SetWindows(256/2, ys, LCD_W-1, ys);            for(i = 0; i < 384 / 2; i++){                Lcd_WriteData_16Bit(readw(videomemory));                videomemory++;            }            //write 5 lines            LCD_SetWindows(0, ys+1, LCD_W-1, ys+5);            for(i = 0; i < 640 * 5 / 2; i++){                Lcd_WriteData_16Bit(readw(videomemory));                videomemory++;            }            //write 512 bytes            LCD_SetWindows(0, ys+6, 512/2-1, ys+6);            for(i = 0; i < 512 / 2; i++){                Lcd_WriteData_16Bit(readw(videomemory));                videomemory++;            }            break;        //xs = 256 (512 bytes). write 128 bytes, then 6 full lines and 128 bytes        //128 + 640*6 + 128        case 2:            //write 128 bytes            LCD_SetWindows(256, ys, LCD_W-1, ys);            for(i = 0; i < 128 / 2; i++){                Lcd_WriteData_16Bit(readw(videomemory));                videomemory++;            }            //write 6 lines            LCD_SetWindows(0, ys+1, LCD_W-1, ys+6);            for(i = 0; i < 640 * 6 / 2; i++){                Lcd_WriteData_16Bit(readw(videomemory));                videomemory++;            }            //write 128 bytes            LCD_SetWindows(0, ys+7, 128/2-1, ys+7);            for(i = 0; i < 128 / 2; i++){                Lcd_WriteData_16Bit(readw(videomemory));                videomemory++;            }            break;        //xs = 64 (128 /2). write 512 bytes, then 5 lines and 384 bytes        //512 + 640*5 + 384        case 3:            //write 512 bytes            LCD_SetWindows(64, ys, LCD_W-1, ys);            for(i = 0; i < 512 / 2; i++){                Lcd_WriteData_16Bit(readw(videomemory));                videomemory++;            }            //write 5 lines            LCD_SetWindows(0, ys+1, LCD_W-1, ys+5);            for(i = 0; i < 640 * 5 / 2; i++){                Lcd_WriteData_16Bit(readw(videomemory));                videomemory++;            }            //write 384 bytes            LCD_SetWindows(0, ys+6, 384/2-1, ys+6);            for(i = 0; i < 384 / 2; i++){                Lcd_WriteData_16Bit(readw(videomemory));                videomemory++;            }            break;        //xs = 384/2. write 256 bytes, then 6 full lines        //256 + 640*6        case 4:            //write 256 bytes            LCD_SetWindows(384/2, ys, LCD_W-1, ys);            for(i = 0; i < 256 / 2; i++){                Lcd_WriteData_16Bit(readw(videomemory));                videomemory++;            }            LCD_SetWindows(0, ys+1, LCD_W-1, ys+6);            for(i = 0; i < 640 * 6 / 2; i++){                Lcd_WriteData_16Bit(readw(videomemory));                videomemory++;            }            break;        default: break;        }    }}

Также небольшие изменения в структуре ili9341 и функции ili9341_probe:


struct videopage{    atomic_t                toUpdate;};struct ili9341 {    struct device *dev;    struct fb_info *info;    unsigned char *videomem;    /* здесь отмечаем изменения в страницах памяти */    struct videopage videopages[FP_PAGE_COUNT];};static int  ili9341_probe(struct platform_device *dev){    ...    /* инициализируем массив для отслеживания изменений страниц памяти */    for(i=0;i<FP_PAGE_COUNT;i++)    {        atomic_set(&item->videopages[i].toUpdate, -1);          }}

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


Код
static struct fb_ops ili9341_fbops = {        .owner        = THIS_MODULE,        .fb_write     = ili9341_write,        .fb_fillrect  = ili9341_fillrect,        .fb_copyarea  = ili9341_copyarea,        .fb_imageblit = ili9341_imageblit,        .fb_setcolreg   = ili9341_setcolreg,};static ssize_t ili9341_write(struct fb_info *p, const char __user *buf, size_t count, loff_t *ppos){    ssize_t retval;    printk("ili9341_write\n");    retval=fb_sys_write(p, buf, count, ppos);    ili9341_touch(p, 0, 0, p->var.xres, p->var.yres);    return retval;}static void ili9341_fillrect(struct fb_info *p, const struct fb_fillrect *rect){    printk("ili9341_fillrect\n");    sys_fillrect(p, rect);    ili9341_touch(p, rect->dx, rect->dy, rect->width, rect->height);}static void ili9341_imageblit(struct fb_info *p, const struct fb_image *image){    printk("ili9341_imageblit\n");    sys_imageblit(p, image);    ili9341_touch(p, image->dx, image->dy, image->width, image->height);}static void ili9341_copyarea(struct fb_info *p, const struct fb_copyarea *area){    printk("ili9341_copyarea\n");    sys_copyarea(p, area);    ili9341_touch(p, area->dx, area->dy, area->width, area->height);}static void ili9341_touch(struct fb_info *info, int x, int y, int w, int h){    struct ili9341 *item = (struct ili9341 *)info->par;    int firstPage;    int lastPage;    int i;    printk("touch x %d, y %d, w %d, h %d",x,y,w,h);    firstPage=((y*W)+x)*BYTE_DEPTH/PAGE_SIZE-1;    lastPage=(((y+h)*W)+x+w)*BYTE_DEPTH/PAGE_SIZE+1;    if(firstPage<0)        firstPage=0;    if(lastPage>FP_PAGE_COUNT)        lastPage=FP_PAGE_COUNT;    for(i=firstPage;i<lastPage;i++)        atomic_dec(&item->videopages[i].toUpdate);    schedule_delayed_work(&info->deferred_work, info->fbdefio->delay);}

Система с двумя экранами


Немного поэксперементируем. Подключим к raspberry два экрана. В качестве основного экрана используем экран / телевизор, подключенный к HDMI. В качестве второго экрана используем LCD.


Чтобы перетаскивание окошек между экранами было лучше видно, я "увеличил" размер экрана LCD, которые видит linux до 640x480. В ядре я регистрирую экран 640x480, однако на сам LCD пишу каждый второй пиксель в строке и пропускаю каждую вторую строку. Измененный код ili9341_update:


/* файл lcd_drv_simple_640_480.c */#define W 320*2#define H 240*2/* изменения в ili9341_update на примере простого драйвера */for(j = 0; j < H; j++){    if (j % 2 == 1){ //skip        videomemory += W;    }    else{        for(i = 0; i < W; i += 2){            Lcd_WriteData_16Bit(readw(videomemory));            videomemory += 2;                       }    }}

Для работы с двумя экранами глубина цвета на них должна быть одинаковой. Для этого добавляем в /boot/config.txt:


[all]framebuffer_depth=16

Ставим xinerama для перетаскивания окон между экранами:


sudo apt-get install libxinerama-dev

Заменяем конфигурационный файл /etc/X11/xorg.conf


xorg.conf
Section "Device"        Identifier      "LCD"        Driver          "fbdev"        Option          "fbdev" "/dev/fb1"        Option          "ShadowFB" "off"        Option          "SwapbuffersWait" "true"EndSectionSection "Device"        Identifier      "HDMI"        Driver          "fbdev"        Option          "fbdev" "/dev/fb0"        Option          "ShadowFB" "off"        Option          "SwapbuffersWait" "true"EndSectionSection "Monitor"        Identifier      "LCD-monitor"        Option          "RightOf" "HDMI-monitor"EndSectionSection "Monitor"        Identifier      "HDMI-monitor"        Option          "Primary" "true"        EndSectionSection "Screen"        Identifier      "screen0"        Device          "LCD"        Monitor         "LCD-monitor"EndSectionSection "Screen"        Identifier      "screen1"        Device          "HDMI"         Monitor         "HDMI-monitor"EndSectionSection "ServerLayout"        Identifier      "default"        Option          "Xinerama" "on"        Option          "Clone" "off"        Screen 0        "screen0" RightOf "screen1"        Screen 1        "screen1" EndSection

Результат:


Заключение


Надеюсь было интересно. Код на github.

Подробнее..

Перевод Глубокое погружение в Linux namespaces, часть 4

31.03.2021 04:09:55 | Автор: admin

В завершающем посте этой серии мы рассмотрим Network namespaces. Как мы упоминали в вводном посте, network namespace изолирует ресурсы, связанные с сетью: процесс, работающий в отдельном network namespace, имеет собственные сетевые устройства, таблицы маршрутизации, правила фаервола и т.д. Мы можем непосредственно увидеть это на практике, рассмотрев наше текущее сетевое окружение.


Команда ip


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

$ ip link list1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:002: ens33: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000    link/ether 00:0c:29:96:2e:3b brd ff:ff:ff:ff:ff:ff

Звездой шоу здесь является команда ip швейцарский армейский нож для работы с сетью в Linux и мы будем активно использовать её в этом посте. Прямо сейчас мы только что выполнили подкоманду link list, чтобы увидеть, какие сетевые устройства в настоящее время доступны в системе (здесь есть lo loopback-интерфес и `ens33, ethernet-интерфейс LAN.


Как и со всеми другими пространствами имён, система стартует с начальным network namespace, которому принадлежат все процесс процессы, если не задано иное. Выполнение команды ip link list как есть показывает нам сетевые устройства, принадлежащие изначальному пространству имён (поскольку и наш шелл, и команда ip принадлежат этому пространству имён).


Именованные пространства имён Network


Давайте создадим новый network namespace:


$ ip netns add coke$ ip netns listcoke

И снова мы использовали команду ip. Подкоманда netns позволяет нам играться с пространствами имён network: например, мы можем создавать новые сетевые пространства network с помощью подкоманды add команды netns и использовать list для их вывода.
Вы могли заметить, что list возвращал только наш вновь созданный namespace. Разве он не должен возвращать по крайней мере два, одним из которых был бы исходным namespace, о котором мы упоминали ранее? Причина этого в том, что ip создаёт то, что называется named network namespace, который является просто network namespace, идентифицируемый уникальным именем (в нашем случае coke). Только именованные пространства имён network отображаются подкомандой list, а изначальный network namespace не именованный.


Проще всего получить именованные пространства имён network. Например, в каждом именованном network namespace создаётся файл в каталоге /var/run/netns и им сможет воспользоваться процесс, который хочет переключиться на свой namespace. Другое свойство именованных пространств имён network заключается в том, что они могут существовать без наличия какого-либо процесса в отличие от неименованных, которые будут удалены как только завершатся все принадлежащие им процессы.


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


Мы будем использовать приглашение командной строки C$ для обозначения шелла, работающего в дочернем network namespace.

$ ip netns exec coke bashC$ ip link list1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN mode DEFAULT group default qlen 1000    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00

Запуск субкоманды exec $namespace $command выполняет $command в именованном network namespace $namespace. Здесь мы запустили шелл внутри пространства имён coke и посмотрели доступные сетевые устройства. Мы видим, что, по крайней мере, наше устройство ens33 исчезло. Единственное устройство, которое видно, это лупбек и даже этот интерфейс погашен.


C$ ping 127.0.0.1connect: Network is unreachable

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


C$ ip link set dev lo upC$ ping 127.0.0.1PING 127.0.0.1 (127.0.0.1) 56(84) bytes of data.64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.034 ms...

Сетевое изолирование


Мы уже начинаем понимать, что запустив процесс во вложенном network namespace, таком как coke, мы можем быть уверены, что он изолирован от остальной системы в том, что касается сети. Наш шелл-процесс, работающий в coke, может общаться только через loopback. Это означает, что он может общаться только с процессами, которые также являются членами пространства имён coke, но в настоящее время других таких процессов нет (и, во имя изолированности, мы хотели бы, чтобы так и оставалось), так что он немного одинок. Давайте попробуем несколько ослабить эту изолированность. Мы создадим туннель, через который процессы в coke смогут общаться с процессами в нашем исходном пространстве имён.


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


Устройства veth


Для выполнения этого нашего требования, мы будем использовать сетевое устройство virtual ethernet (или сокращённо veth). Устройства veth всегда создаются как пара устройств, связанных по принципу туннеля, так что сообщения, отправленные на одном конце, выходят из устройства на другом. Вы могли бы предположить, что мы могли бы легко иметь один конец в исходном network namespace, а другой в нашем дочернем network namespace, а всё общение между пространствами имён network проходило бы через соответствующее оконечное устройство veth (и вы были бы правы).


# Создание пары veth (veth0 <=> veth1)$ ip link add veth0 type veth peer name veth1# Перемещение veth1 в новое пространство имён$ ip link set veth1 netns coke# Просмотр сетевых устройств в новом пространстве имёнC$ ip link list1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:007: veth1@if8: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000    link/ether ee:16:0c:23:f3:af brd ff:ff:ff:ff:ff:ff link-netnsid 0

Наше устройство veth1 теперь появилось в пространстве имён coke. Но чтобы заставить пару veth работать, нам нужно назначить там IP-адреса и поднять интерфейсы. Мы сделаем это в каждом соответствующем network namespace.


# В исходном пространстве имён$ ip addr add 10.1.1.1/24 dev veth0$ ip link set dev veth0 up# В пространстве имён cokeC$ ip addr add 10.1.1.2/24 dev veth1C$ ip link set dev veth1 upC$ ip addr show veth17: veth1@if8: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000    link/ether ee:16:0c:23:f3:af brd ff:ff:ff:ff:ff:ff link-netnsid 0    inet 10.1.1.2/24 scope global veth1       valid_lft forever preferred_lft forever    inet6 fe80::ec16:cff:fe23:f3af/64 scope link       valid_lft forever preferred_lft forever

Мы должны увидеть, что интерфейс veth1 поднят и имеет назначенный нами адрес 10.1.1.2. Тоже самое должно произойти с veth0 в исходном пространстве имён. Теперь у нас должна быть возможность сделать интер-namespace ping между двумя процессами, запущенными в обоих пространствах имён.


$ ping -I veth0 10.1.1.2PING 10.1.1.2 (10.1.1.2) 56(84) bytes of data.64 bytes from 10.1.1.2: icmp_seq=1 ttl=64 time=0.041 ms...C$ ping 10.1.1.1PING 10.1.1.1 (10.1.1.1) 56(84) bytes of data.64 bytes from 10.1.1.1: icmp_seq=1 ttl=64 time=0.067 ms...

Реализация


Исходный код к этому посту можно найти здесь.

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


  1. Выполнить команду в новом network namespace.
  2. Создать пару veth (veth0 <=> veth1).
  3. Переместить устройство veth1 в новый namespace.
  4. Назначить IP-адреса обоим устройствам и поднять их.

Шаг 1 прост: мы создаём наш командный процесс в новом пространстве имён network путём добавления флага CLONE_NEWNET к clone:


int clone_flags = SIGCHLD | CLONE_NEWUTS | CLONE_NEWUSER | CLONE_NEWNS | CLONE_NEWNET;


Для остальных шагов мы будем преимущественно использовать Netlink интерфейс чтобы общаться с Linux. Netlink в основном используется для связи между обычными приложениями (вроде isolate) и ядром Linux. Он предоставляет API поверх сокетов на основе протокола, который определяет структуру и содержание сообщения. Используя этот протокол, мы можем отправлять сообщения, которые получает Linux и преобразует в запросы вроде создать пару veth с именами veth0 и veth1.


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


int create_socket(int domain, int type, int protocol){    int sock_fd = socket(domain, type, protocol);    if (sock_fd < 0)        die("cannot open socket: %m\n");    return sock_fd;}int sock_fd = create_socket(  PF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_ROUTE);


Сообщение в netlink это четырехбайтовый выровненный блок данный, содержащий заголовок (struct nlmsghdr) и полезную нагрузку. Формат заголовка описан здесь. Модуль The Network Interface Service (NIS) определяет формат (struct ifinfomsg), с которого должна начинаться полезная нагрузка, относящаяся к управлению сетевым интерфейсом.


Наш следующий запрос будет представлен следующей структурой C:


#define MAX_PAYLOAD 1024struct nl_req {    struct nlmsghdr n;     // Заголовок сообщения Netlink    struct ifinfomsg i;    // Полезная нагрузка начинается с информации модуля NIS    char buf[MAX_PAYLOAD]; // Остальная полезная нагрузка};


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


Полезная нагрузка в сообщении Netlink будет закодирована как список атрибутов (где любой такой атрибут, в свою очередь, может иметь вложенные атрибуты), а у нас будет несколько вспомогательных функций для заполнения его атрибутами. В коде атрибут представлен в заголовочном файле linux/rtnetlink.h структурой rtattr как:


struct rtattr {  unsigned short  rta_len;  unsigned short  rta_type;};

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


В попытке собрать всё это вместе, давайте посмотрим, как isolate делает netlink запрос для создания для создания пары veth с помощью следующей функции create_veth, которая выполняет шаг 2:


// ip link add ifname type veth ifname name peernamevoid create_veth(int sock_fd, char *ifname, char *peername){    __u16 flags =            NLM_F_REQUEST  // Это сообщение запроса            | NLM_F_CREATE // Создание устройства, если оно не существует            | NLM_F_EXCL   // Если оно уже существует, ничего не делать            | NLM_F_ACK;   // Ответ с подтверждением или ошибкой    // Инициализация сообщения запроса.    struct nl_req req = {            .n.nlmsg_len = NLMSG_LENGTH(sizeof(struct ifinfomsg)),            .n.nlmsg_flags = flags,            .n.nlmsg_type = RTM_NEWLINK, // Это сообщение netlink            .i.ifi_family = PF_NETLINK,    };    struct nlmsghdr *n = &req.n;    int maxlen = sizeof(req);    /*     * Создание атрибута r0 с информацией о veth. Например, если ifname - veth0,     * тогда нижеследующее будет добавлено к сообщению     * {     *   rta_type: IFLA_IFNAME     *   rta_len: 5 (len(veth0) + 1)     *   data: veth0\0     * }     */    addattr_l(n, maxlen, IFLA_IFNAME, ifname, strlen(ifname) + 1);    // Добавление вложенного атрибута r1 в r0, содержащего информацию iface    struct rtattr *linfo =            addattr_nest(n, maxlen, IFLA_LINKINFO);    // Указание типа устройства veth    addattr_l(&req.n, sizeof(req), IFLA_INFO_KIND, "veth", 5);    // Добавление еще одного вложенного атрибута r2    struct rtattr *linfodata =            addattr_nest(n, maxlen, IFLA_INFO_DATA);    // Следующий вложенный атрибут r3 содержит имя соседнего устройства, например veth1    struct rtattr *peerinfo =            addattr_nest(n, maxlen, VETH_INFO_PEER);    n->nlmsg_len += sizeof(struct ifinfomsg);    addattr_l(n, maxlen, IFLA_IFNAME, peername, strlen(peername) + 1);    addattr_nest_end(n, peerinfo); // конец вложенного атрибута r3    addattr_nest_end(n, linfodata); // конец вложенного атрибута r2    addattr_nest_end(n, linfo); // конец вложенного атрибута r1    // Отправка сообщения    send_nlmsg(sock_fd, n);}

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


Далее, для шага 3, этот метод, который, учитывая имя интерфейса ifname и network namespace файлового дескриптора netns, перемещает устройство, связанное с этим интерфейсом, в указанный network namespace.


// $ ip link set veth1 netns cokevoid move_if_to_pid_netns(int sock_fd, char *ifname, int netns){    struct nl_req req = {            .n.nlmsg_len = NLMSG_LENGTH(sizeof(struct ifinfomsg)),            .n.nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK,            .n.nlmsg_type = RTM_NEWLINK,            .i.ifi_family = PF_NETLINK,    };    addattr_l(&req.n, sizeof(req), IFLA_NET_NS_FD, &netns, 4);    addattr_l(&req.n, sizeof(req), IFLA_IFNAME,              ifname, strlen(ifname) + 1);    send_nlmsg(sock_fd, &req.n);}

После создания пары veth и перемещения одного конца в наш целевой network namespace, на шаге 4 мы назначаем IP-адреса обоим конечным устройствам и поднимаем их интерфейсы. Для этого у нас есть вспомогательная функция if_up, которая, учитывая имя интерфейса ifname и IP-адрес ip, назначает ip устройству ifname и поднимает его. Для краткости мы не показываем их тут, но вместо этого они могут быть найдены здесь.


Наконец, мы объединяем эти методы, чтобы подготовить наш network namespace для нашего командного процесса.


static void prepare_netns(int child_pid){    char *veth = "veth0";    char *vpeer = "veth1";    char *veth_addr = "10.1.1.1";    char *vpeer_addr = "10.1.1.2";    char *netmask = "255.255.255.0";    // Создание нашего сокета netlink    int sock_fd = create_socket(            PF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_ROUTE);    // ... и нашей пары veth veth0 <=> veth1.    create_veth(sock_fd, veth, vpeer);    // veth0 находится в нашем текущем (исходном) namespace    // так что мы можем сразу поднять его.    if_up(veth, veth_addr, netmask);    // ... veth1 будет перемещен в namespace команды.    // Для этого нам нужно получить файловый дескриптор    // и перейти в namespace команды, но сначала мы должны    // запомнить наш текущий namespace, чтобы мы могли вернуться в него    // когда закончим.    int mynetns = get_netns_fd(getpid());    int child_netns = get_netns_fd(child_pid);    // Перемещение veth1 в network namespace команды.    move_if_to_pid_netns(sock_fd, vpeer, child_netns);    // ... и переход туда    if (setns(child_netns, CLONE_NEWNET)) {        die("cannot setns for child at pid %d: %m\n", child_pid);    }    // ... и поднятие veth1-интерфейса    if_up(vpeer, vpeer_addr, netmask);    // ... перед возвращением в наш исходный network namespace.    if (setns(mynetns, CLONE_NEWNET)) {        die("cannot restore previous netns: %m\n");    }    close(sock_fd);}

Затем мы можем вызвать prepare_netns сразу после того, как мы закончим настройку нашего user namespace.


    ...    // Получение записываемого конца пайпа.    int pipe = params.fd[1];    prepare_userns(cmd_pid);    prepare_netns(cmd_pid);    // Сигнал командному процессу, что мы закончили настройку.    ...

Давайте попробуем!


$ sudo ./isolate sh===========sh============$ ip link list1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN qlen 1000    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:0031: veth1@if32: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue state UP qlen 1000    link/ether 2a:e8:d9:df:b4:3d brd ff:ff:ff:ff:ff:ff# Проверим связность между пространствами имён$ ping 10.1.1.1PING 10.1.1.1 (10.1.1.1): 56 data bytes64 bytes from 10.1.1.1: seq=0 ttl=64 time=0.145 ms
Подробнее..

Перевод Глубокое погружение в Linux namespaces, часть 3

31.03.2021 04:09:55 | Автор: admin

Mount namespaces изолируют ресурсы файловых систем. Это по большей части включает всё, что имеет отношение к файлам в системе. Среди охватываемых ресурсов есть файл, содержащий список точек монтирования, которые видны процессу, и, как мы намекали во вступительном посте, изолирование может обеспечить такое поведение, что изменение списка (или любого другого файла) в пределах некоторого mount namespace инстанса M не будет влиять на этот список в другом инстансе (так что только процессы в M увидят изменения)


Точки монтирования


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


Мы можем видеть точки монтирования, видимые для процесса с id $pid посредством файла /proc/$pid/mounts его содержимое одинаково для всех процессов, принадлежащих к тому же mount namespace, что и $pid:


$ cat /proc/$$/mounts.../dev/sda1 / ext4 rw,relatime,errors=remount-ro,data=ordered 0 0...

В списке, полученном на моей системе, видно устройство /dev/sda1, смонтированное в / (ваше может быть другим). Это дисковое устройство, на котором размещена корневая файловая система, которая содержит всё что нужно для запуска и правильной работы системы, поэтому было бы здорово, если бы isolate запускала команды без ведома о таких файловых системах.


Давайте начнём с запуска терминала в его собственном mount namespace:


Строго говоря, нам не понадобится доступ уровня суперпользователя для работы с новыми пространствами имён mount, поскольку мы добавим процедуры настройки user namespace из предыдущего поста. В результате в этом посте мы предполагаем, что только команды unshare в терминале выполняются от суперпользователя. Для isolate в таком предположении необходимости нет.

# Флаг -m создаёт новый mount namespace.$ unshare -m bash$ cat /proc/$$/mounts.../dev/sda1 / ext4 rw,relatime,errors=remount-ro,data=ordered 0 0...

Хммм, мы всё еще можем видеть тот же самый список, что и в корневом mount namespace. Особенно после того, как в предыдущем посте стало ясно, что новый user namepace начинается с чистого листа, может показаться, что флаг -m, который мы передали unshare, не дал никакого эффекта.
Процесс шелла фактически выполняется в другом mount namespace (мы можем убедиться в этом, сравнив файл симлинка ls -l /proc/$$/mnt с файлом другой копии шелла, работающей в корневом mount namespace). Причина, по которой мы все еще видим тот же список, заключается в том, что всякий раз, когда мы создаем новый mount namespace (дочерний), в качестве дочернего списка используется копия точек монтирования mount namespace, в котором происходило создание (родительского). Теперь любые изменения, которые мы вносим в этот файл (например, путём монтирования файловой системы), будут невидимы для всех других процессов.
Однако изменение практически любого другого файла на этом этапе будет влиять на другие процессы, поскольку мы всё ещё ссылаемся на те же самые файлы (Linux только делает копии особых файлов, таких как список точек монтирования). Это означает, что сейчас у нас минимальная изолированность. Если мы хотим ограничить то, что будет видеть наш командный процесс, мы должны сами обновить этот список.


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


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


Хорошо, в теории это звучит хорошо и для реализации этого мы сделаем следующее:


  1. Создадим копию зависимостей и системных файлов, необходимых команде.
  2. Создадим новый mount namespace.
  3. Заменим корневую файловую систему в новом mount namespace на ту, которая содержит копии наших системных файлов.
  4. Выполним программу в новом mount namespace.

Корневые файловые системы


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


Вот если бы только у людей уже была такая же проблема и они собрали набор системных файлов, в целом достаточный, чтобы служить базой прямо из коробки для большинства программ? К счастью, есть много проектов, что реализовали это! Одним из них является проект Alpine Linux (его основное предназначение, это когда вы начинаете свой Dockerfile с FROM alpine:xxx). Alpine предоставляет корневые файловые системы, которые мы можем использовать для наших целей. Если вы последуете инструкциями, то сможете получить копию их минимальной корневой файловой системы (MINI ROOT FILESYSTEM) для x86_64 здесь. Последней версией на момент написания поста и которую мы будем использовать, является v3.10.1.


$ wget http://dl-cdn.alpinelinux.org/alpine/v3.10/releases/x86_64/alpine-minirootfs-3.10.1-x86_64.tar.gz$ mkdir rootfs$ tar -xzf alpine-minirootfs-3.10.1-x86_64.tar.gz -C rootfs$ ls rootfsbin  dev  etc  home  lib  media  mnt  opt  proc  root  run  sbin  srv  sys  tmp  usr  var

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


$ ls rootfs/{mnt,dev,proc,home,sys}# пусто

Отлично! Мы можем дать команду, которая запустится в копии этого окружения, и она может быть даже sudo rm -rf /, но нас это не будет волновать, а никто другой не пострадает.


Pivot root


Имея наш новый mount namespace и копию системных файлов, мы хотели бы смонтировать эти файлы в корневом каталоге нового mount namespace не выбивая землю из под наших ног. Linux предлагает нам системный вызов pivot_root (есть соответствующая команда), который позволяет нам контролировать то, что именно процессы видят как корневую файловую систему.
Команда принимает два аргумента: pivot_root new_root put_old, где new_root это путь к файловой системе, будущей вскоре корневой файловой системой, а put_old путь к каталогу. Это работает так:


  1. Монтирование корневой файловой системы вызывающего процесса в put_old.
  2. Монтирование new_root в качестве корневой файловой системы в /.

Давайте посмотрим на это в действии. В нашем новом mount namespace мы начинаем с создания файловой системы из наших файлов alpine:


$ unshare -m bash$ mount --bind rootfs rootfs

Затем мы делаем pivot root:


$ cd rootfs$ mkdir put_old$ pivot_root . put_old$ cd /# Теперь у нас должен быть новый корневой каталог. Например, если мы сделаем:$ ls proc# proc пуст# И старый корневой каталог теперь в put_old$ ls put_oldbin   dev  home        lib    lost+found  mnt  proc  run   srv  tmp  varboot  etc  initrd.img  lib64  media       opt  root  sbin  sys  usr  vmlinuz

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


$ umount -l put_old

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


Реализация


Исходный код к этому посту можно найти здесь.

Мы можем повторить в коде то, что делали выше, заменив команду pivot_root соответствующим системным вызовом. Сначала мы создаем наш команды процесс в новом mount namespace, добавляя флаг CLONE_NEWNS для clone.


int clone_flags = SIGCHLD | CLONE_NEWUTS | CLONE_NEWUSER | CLONE_NEWNS;

Затем мы создаём функцию prepare_mntns которая, получив путь до каталога, содержащего системные файлы (rootfs), настраивает текущий mount namespace посредством pivoting'а корневого каталога текущего процесса на rootfs, что мы создали ранее.


static void prepare_mntns(char *rootfs){    const char *mnt = rootfs;    if (mount(rootfs, mnt, "ext4", MS_BIND, ""))        die("Failed to mount %s at %s: %m\n", rootfs, mnt);    if (chdir(mnt))        die("Failed to chdir to rootfs mounted at %s: %m\n", mnt);    const char *put_old = ".put_old";    if (mkdir(put_old, 0777) && errno != EEXIST)        die("Failed to mkdir put_old %s: %m\n", put_old);    if (syscall(SYS_pivot_root, ".", put_old))        die("Failed to pivot_root from %s to %s: %m\n", rootfs, put_old);    if (chdir("/"))        die("Failed to chdir to new root: %m\n");    if (umount2(put_old, MNT_DETACH))        die("Failed to umount put_old %s: %m\n", put_old);}

Нам нужно вызвать эту функцию из нашего кода и это должно быть выполнено нашим командным процессом в cmd_exec (поскольку он работает в новом mount namespace) до фактического начала выполнения команды.


    ...    // Ожидание сигнала 'настройка завершена' от основного процесса.    await_setup(params->fd[0]);    prepare_mntns("rootfs");    ...

Давайте попробуем это:


$ ./isolate sh===========sh============$ ls put_old# put_old пуст. Ура!# Как выглядит наш новый список монтирования?$ cat /proc/$$/mountscat: cant open '/proc/1431/mounts': No such file or directory# Хммм, а какие ещё процессы запущены?$ ps auxPID   USER     TIME  COMMAND# Пусто! А?

Этот вывод показывает что-то странное: мы не можем проверить список монтирования, за который так тяжело боролись, и ps говорит нам, что нет процессов, запущенных в системе (нет даже текущего процесса или самого ps?). Более вероятно, что мы что-то сломали при настройке mount namespace.


PID Namespaces


Мы уже несколько раз упоминали каталог /proc в этой серии постов, и если вы были знакомы с ним, то, вероятно, не будете удивлены тому, что вывод ps оказался пустым, поскольку мы видели ранее, что каталог был пуст в этом mount namespace (когда мы получили его из корневой файловой системы alpine).


Каталог /proc в Linux обычно используется для доступа к специальной файловой системе (называемой файловой системой proc), которой управляет сам Linux. Linux использует его для предоставления информации обо всех процессах, запущенных в системе, а также другой системной информации, касающейся устройств, прерываний и так далее. Всякий раз, когда мы запускаем такую команду, как ps, выдающую сведения о процессах в системе, она обращается к этой файловой системе для получения информации.
Другими словами, нам нужно завести файловую систему proc. К счастью, в основном для это потребуется лишь сообщить Linux, что она нам нужна, причем желательно смонтированная в /proc. Но пока мы не можем этого сделать, поскольку наш командный процесс всё ещё зависит от той же файловой системы proc, что и isolate и любой другой обычный процесс в системе. Чтобы избавиться от этой зависимости, нам нужно запустить его внутри собственного PID namespace.


PID namespace изолирует ID процессов в системе. Одним из следствий тут является то, что выполняющиеся в разных пространствах имён PID процессы могут иметь одинаковые идентификаторы процесса, не конфликтуя друг с другом. Допусти, мы изолируем это пространство имён потому, что мы хотим обеспечить как можно большую изолированность нашей запущенной команде. Однако более интересная причина, по которой мы рассматриваем это здесь, заключается в том, что монтирование файловой системы proc требует привилегий пользователя root, а текущий PID namespace принадлежит пользователю root, где у нас нет достаточных привилегий (если вы помните из предыдущего поста, root у командного процесса на самом деле не root). Итак, мы должны работать в PID namespace, владельцем которого является пользователь пространства имён, которое считает наш командный процесс запущенным от root.


Мы можем создать новый PID namespace, передав CLONE_NEWPID для clone:


int clone_flags = SIGCHLD | CLONE_NEWUTS | CLONE_NEWUSER | CLONE_NEWNS | CLONE_NEWPID;

Затем мы добавляем функцию prepare_procfs, которая настраивает файловую систему proc, монтируя её в текущих пространствах имён mount и pid.


static void prepare_procfs(){    if (mkdir("/proc", 0555) && errno != EEXIST)        die("Failed to mkdir /proc: %m\n");    if (mount("proc", "/proc", "proc", 0, ""))        die("Failed to mount proc: %m\n");}

Наконец, мы вызываем функцию прямо перед размонтированием put_old в нашей функции prepare_mntns, после того, как мы настроили mount namespace и перешли в корневой каталог.


static void prepare_mntns(char *rootfs){  ...    prepare_procfs();    if (umount2(put_old, MNT_DETACH))        die("Failed to umount put_old %s: %m\n", put_old);  ...}

Мы можем воспользоваться isolate для очередного запуска:


$ ./isolate sh===========sh============$ psPID   USER     TIME  COMMAND    1 root      0:00 sh    2 root      0:00 ps

Это выглядит намного лучше! Шелл считает себя единственным процессом, запущенным в системе и работающем с PID 1(поскольку это был первый процесс, запущенный в этом новом PID namespace)


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

Подробнее..

Насколько маленьким может быть ядро linux?

02.02.2021 00:22:58 | Автор: admin
Некоторое время назад я научился конвертировать виртуальные машины в oracle cloud из ubuntu 20.04 в gentoo. Машины предоставляемые в рамках always free tier весьма маломощны. Это в частности приводит к тому, что перекомпиляция ядра превращается в достаточно длительный процесс. У исходного ядра ubuntu 20.04 в конфиге было 7904 параметра. После того, как я сделал
make localmodconfig && make localyesconfig

число параметров уменьшилось до 1285. Мне стало интересно попробовать выбросить из ядра все лишнее и посмотреть, что получится.
Я буду компилировать ванильное ядро 5.4.0, потому что именно эта версия используется на моей установке gentoo. Для ускорения процесса я компилирую ядро на своей рабочей машине (i7, 8 cores, 64Gb RAM, tmpfs). Готовое ядро я копирую на машину в oracle cloud. Начинать процесс надо с команды
make tinyconfig

В результате у вас появится файл .config для самого минимального ядра текущей архитектуры. В моем случае в этом файле оказалось 284 непустых строк, не являющихся комментариями. Давайте скомпилируем его и посмотрим на размер ядра:
$ yes ""|make -j$(nproc)$ ll arch/x86/boot/bzImage && grep -v ^# .config|grep -c .-rw-rw-r-- 1 kvt kvt 441872 Jan 31 18:09 arch/x86/boot/bzImage284$
Это ядро абсолютно бесполезно. Оно не только не может загрузиться но даже не имеет возможности сообщить о возникших проблемах. Давайте это исправим. Для активации параметров я буду использовать команду
script/config -e config_parameter_name

Итак, наше ядро будет 64-х битным, мы активируем вывод диагностики ядра, включаем поддержку терминала, конфигурируем последовательный порт и консоль на нем:
./scripts/config -e CONFIG_64BIT -e CONFIG_PRINTK -e CONFIG_TTY -e CONFIG_SERIAL_8250 -e CONFIG_SERIAL_8250_CONSOLE

Машина в oracle cloud загрузиться с этим ядром не сможет, вывода на консоль не будет. Как оказалось, надо добавить поддержку EFI и ACPI, являющуюся ее зависимостью. Скрипт ./scripts/config не реализует логику добавления обратных зависимостей т.е. если добавить только CONFIG_EFI, то make выкинет этот параметр из конфига. Также стоит отметить, что включение опций часто включает нижележащие опции. Так в случае с включением CONFIG_ACPI автоматически включается, к примеру, поддержка кнопки включения/выключения.
./scripts/config -e CONFIG_ACPI -e CONFIG_EFI -e CONFIG_EFI_STUB

Собираем ядро
$ yes ""|make -j$(nproc)$ ll arch/x86/boot/bzImage && grep -v ^# .config|grep -c .-rw-rw-r-- 1 kvt kvt 1036960 Jan 31 18:13 arch/x86/boot/bzImage409$

Это ядро по прежнему не может завершить процесс загрузки, но по крайней мере способно сообщить об этом:
Kernel panic - not syncing: No working init found.  Try passing init= option to kernel. See Linux Documentation/admin-guide/init.rst for guidance.Kernel Offset: 0x22000000 from 0xffffffff81000000 (relocation range: 0xffffffff80000000-0xffffffffbfffffff)---[ end Kernel panic - not syncing: No working init found.  Try passing init= option to kernel. See Linux Documentation/admin-guide/init.rst for guidance. ]---

Давайте добавим необходимые параметры:
# virtual guest support and pci./scripts/config -e CONFIG_PCI -e CONFIG_VIRTIO_PCI -e CONFIG_VIRTIO -e CONFIG_VIRTIO_MENU -e CONFIG_PARAVIRT -e CONFIG_HYPERVISOR_GUEST  # disk support./scripts/config -e CONFIG_BLOCK -e CONFIG_SCSI -e CONFIG_BLK_DEV_SD -e CONFIG_SCSI_VIRTIO# filesystems./scripts/config -e CONFIG_EXT4_FS -e CONFIG_PROC_FS -e CONFIG_SYSFS -e CONFIG_DEVTMPFS -e CONFIG_DEVTMPFS_MOUNT# executable formats./scripts/config -e CONFIG_BINFMT_ELF -e CONFIG_BINFMT_SCRIPT# network./scripts/config -e CONFIG_NET -e CONFIG_VIRTIO_NET -e CONFIG_PACKET -e CONFIG_UNIX -e CONFIG_INET -e CONFIG_NET_CORE -e CONFIG_NETDEVICES -e CONFIG_VIRTIO_NET

Собираем ядро:
$ ll arch/x86/boot/bzImage && grep -v ^# .config|grep -c .-rw-rw-r-- 1 kvt kvt 1950368 Jan 31 18:18 arch/x86/boot/bzImage616$

И загружаемся. На этот раз ядро успешно находит корневой диск, но в консоли мы видим ошибки:
The futex facility returned an unexpected error code.... * Call to flock failed: Function not implemented

Пытаемся залогиниться:
(none) login: rootprocess 182 (login) attempted a POSIX timer syscall while CONFIG_POSIX_TIMERS is not setPassword:setgid: Function not implemented

Тоже не получается, но зато нам недвусмысленно подсказывают какой параметр надо добавить. Добавляем его и остальные необходимые параметры:
./scripts/config -e CONFIG_POSIX_TIMERS -e CONFIG_FUTEX -e CONFIG_FILE_LOCKING -e CONFIG_MULTIUSER

Собираем ядро:
$ ll arch/x86/boot/bzImage && grep -v ^# .config|grep -c .-rw-rw-r-- 1 kvt kvt 1979040 Jan 31 18:25 arch/x86/boot/bzImage623$

На этот раз нам удается залогиниться:
instance-20210124-1735 login: rootPassword:Last login: Mon Feb  1 02:25:10 UTC 2021 from 73.239.106.74 on sshroot@instance-20210124-1735:~#

Ура! Ssh тоже работает. Тем не менее в консоли опять есть ошибки:
 * Some local filesystem failed to mount...hwclock: Cannot access the Hardware Clock via any known method.hwclock: Use the --verbose option to see the details of our search for an access method. * Failed to set the system clock

А в dmesg находим еще:
[    2.910198] udevd[360]: inotify_init failed: Function not implemented

Добавляем поддержку часов реального времени, файловой системы vfat и inotify:
./scripts/config -e CONFIG_RTC_CLASS -e CONFIG_DNOTIFY -e CONFIG_INOTIFY_USER -e CONFIG_VFAT_FS

Собираем ядро:
$ ll arch/x86/boot/bzImage && grep -v ^# .config|grep -c .-rw-rw-r-- 1 kvt kvt 2015904 Jan 31 18:36 arch/x86/boot/bzImage643$

Хммм, vfat диск по прежнему не может подмонтироваться:
 * Some local filesystem failed to mount

А вот и ответ почему в dmesg заодно с еще одной ошибкой:
[    3.782884] udevd[527]: error creating signalfd[    4.107698] FAT-fs (sda15): codepage cp437 not found

Добавляем параметры:
./scripts/config -e CONFIG_SIGNALFD -e CONFIG_NLS_CODEPAGE_437 -e CONFIG_NLS_ISO8859_1

Собираем ядро:
$ ll arch/x86/boot/bzImage && grep -v ^# .config|grep -c .-rw-rw-r-- 1 kvt kvt 2015904 Jan 31 18:41 arch/x86/boot/bzImage646$

Загружаемся, в консоли ошибок нет, но появилась новая ошибка в dmesg:
[    2.756136] udevd[360]: error creating epoll fd: Function not implemented

Исправляем:
./scripts/config -e CONFIG_EPOLL

Собираем ядро:
$ ll arch/x86/boot/bzImage && grep -v ^# .config|grep -c .-rw-rw-r-- 1 kvt kvt 2020000 Jan 31 19:13 arch/x86/boot/bzImage647$

Перезагружаемся и на этот раз не видим новых ошибок :)!

На моей рабочей машине (i7, 8 cores, 64gb RAM, tmpfs) финальная конфигурация собирается за 1m 16s. В oracle cloud с двумя ядрами и на обычном диске этот же процесс занимает 19m 51s.

Получившееся ядро не является абсолютно минимальным. Так, например, включение поддержки сети добавляет кучу разных сетевых адаптеров. Я не стал заниматься перфекционизмом и вычищать абсолютно все, что не нужно. Кроме того хочу предупредить, что хотя загрузка и быстрое тестирование не выявило проблем с отсутствием дополнительных важных компонент ядра скорее всего таковые существуют. Так что если вдруг вы решите переиспользовать мой конфиг пожалуйста тщательно протестируйте ядро для вашего конкретного случая и при необходимости добавьте нужные параметры ядра.
Подробнее..
Категории: Настройка linux , Gentoo , Linux kernel

Как теперь процессить kernel crash и bug report? Или несколько слов о разнице между интересным и удивительным

30.04.2021 22:19:48 | Автор: admin

Широко известен исторический анекдот о том, что царица Екатерина II писала простое русское слово из 3 букв с 4 ошибками. Куда менее известно, что эта ошибка вовсе не уникальна. Дети европейских экспатов, изучающие русский язык, запросто могут в диктанте слово ёжик написать как Й-О-Ш-Е-Г.

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

Автор этих строк много лет пилит гирю OpenVz linux kernel maintenance. OpenVz сейчас потихоньку переезжает с ядра RHEL7 на RHEL8. Ядро в Red Hat за 5 лет сильно изменилось, поэтому мы очередной раз пересматриваем наши старые патчи и думаем, что с ними делать: перетащить как есть, переделать получше или дропнуть за неактуальностью.

В рамках этой большой задачи я разбирался с memcg accounting. OpenVZ аккаунтит различные kernel objects практически с самого рождения с v2.2.x ядер далекого 2001 года. Зачем и почему мы решили аккаунтить тот или иной объект, сейчас сходу понять довольно затруднительно, приходится поднимать историю по старым багам и коммитам.

В стародавние времена мы использовали свою собственную систему accounting, так называемые user beancounters. Там была пара десятков разных параметров, и правильно их настраивать было затруднительно для админов. В upstream эту подсистему тоже принимать не особо хотели, для них приходилось изобретать namespaces и cgroups, а для своих ядер скрещивать ежа, ужа и трепетную лань.

Пересмотрев наш memcg accounting из Virtuozzo Hybrid Server 7.5, я большую их часть перетащил в Virtuozzo Hybrid Server 8, посмотрел, что из этого до сих пор отсутствует в последнем upstream и подготовил соответствующий патчсет. Если кому интересно, кстати говоря, вот он: https://lkml.org/lkml/2021/4/28/70

Перед отсылкой в upstream изменения полагается хотя бы минимально проверить. Я скомпилил свое ядро, взгромоздил его на свою тестовую VM с Fedora Rawhide и давай его по-всякому тестить.

Чтобы разобраться в деталях memcg аккаунтинга в ядро в свое время засунули per-memcg sysfs файлик memory.kmem.slabinfo. В нем показывается количество тех или иных SLAB объектов, которые нашлись в соответствующей memory cgroup, нечто типа обычного /proc/slabinfo. В новых upstream ядрах из соответствующие файлики тоже были, однако из них почему то вообще ничего не вычитывалось. Я посмотрел на своем ядре, посмотрел на оригинальном ядре Fedora то же самое: файл есть, контента нет.

Стал разбираться. Выяснилось, что с полгода назад memcg подсистему очередной раз переделали, но с выводом контента в memory.kmem.slabinfo возникли сложности. Поэтому вывод решили обнулить, а для тех, кому он все таки интересен, в ядро закоммитили drgn скрипт tools/cgroup/memcg_slabinfo.py.

Обычно с ядром разбираются посредством crash но на живом ядре это довольно тяжелый способ. сrash долго стартует, потребляет кучу ресурсов, и на сильно загруженной production node такое делать стремно можно запросто разбудить OOM-killerа. Можно пытаться заюзать ftrace, perf или systemtap но у каждого из них свои недостатки и неудобства.

drgn их легковесная альтернатива. Позволяет добраться до внутренностей ядра, и удобным образом переходить по ссылкам структур ядра. Много говорить про него не буду, кому интересно -- посмотрите-покрутите самостоятельно. В целом, по-моему, удобно, впечатления от использования положительные, рекомендую. Исходник здесь: https://github.com/osandov/drgn.

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

Я проверил исправленным скриптом свое ядро со своими фиксами accounting все отработало нормально. Потом решил, что стоит посмотреть на то, как вело себя непропатченное ядро. Откатывать свои патчи и перекомпилировать свое ядро заново мне было лениво. Тем более, что у меня уже было другое upstream ядро от Fedora Rawhide aka fc35. Я специально проапдейтился, загрузил самое последнее ядро, запускаю скрипт а он не работает. И проблема похоже даже не в самом скрипте: drgn сам по себе не запускается.

[root@localhost test]# rpm -q drgn

drgn-0.0.11-2.fc35.x86_64

[root@localhost test]# drgn -s /usr/lib/debug/lib/modules/5.12.0-0.rc8.191.fc35.x86_64/vmlinux

Traceback (most recent call last):

File "/usr/bin/drgn", line 33, in

sys.exit(load_entry_point('drgn==0.0.11', 'console_scripts', 'drgn')())

File "/usr/lib64/python3.9/site-packages/drgn/internal/cli.py", line 119, in main

prog.load_debug_info(args.symbols, **args.default_symbols)

Exception: /usr/lib/debug/usr/lib/modules/5.12.0-0.rc8.191.fc35.x86_64/vmlinux: .debug_info+0x7704ab: unknown DWARF CU version 5

Ставлю ядро от fc34, чуть более раннее не помогает.

Ну хорошо, думаю, раз не получается залезть в эти ядра drgn, давай попробую crash.

А он тоже не запускается!

[root@localhost ~]# crash -d 1 (без дебага dwarf error не виден)

...

Dwarf Error: wrong version in compilation unit header (is 5, should be 2, 3, or 4) [in module /usr/lib/debug/usr/lib/modules/5.11.12-300.fc34.x86_64/vmlinux]

crash: /usr/lib/debug/lib/modules/5.11.12-300.fc34.x86_64/vmlinux: no debugging data available

Рассылаю багрепорты во все стороны, выясняю:

Fedora 34 переехала на новый gcc 11, который для всего подряд генерит debuginfo в новом формате DWARF version 5. Обычный userspace с ним вполне нормально работает, gdb этому формату уже давно обучили.

Однако для ядра это оказалось поистине катастрофично, потому что:

  • crash использует внутри старую версию gdb, которая DWARF 5 еще не понимает

  • drgn поддержку DWARF 5 еще не прикрутил,

  • и у systemtap, похоже, аналогичные проблемы.

И пока я рассылал во все стороны багрепорты, Fedorа 34 благополучно зарелизилась.

М-да! Как они собираются процессить kernel crashes и bug reports? Возможно, у них есть хитрый план?

Связался с девелоперами crash и drgn - ни там, ни там быстро прикрутить поддержку DWARF 5 не обещают. Так что похоже, хитрого плана все-таки нет. Возможно, недосмотрели. Возможно, даже, специально закрыли на это глаза, чтобы посмотреть что именно развалится и собрать багрепорты. В конце концов, Fedora не Red Hat и не CentOS. Она как раз и предназначена для обкатки новых технологий. Удивительно, как оно так получилось. Однако при этом совершенно неинтересно, почему.

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

Во-первых, можно перекомпилировать ядро с DWARF 4, в ядре для этого есть соответствующий CONFIG_DEBUG_INFO_DWARF4. Полагаю, Fedora именно так и поступит в ближайшем будущем.

Во-вторых, можно установить ядро от предыдущей Fedora 33. Брать, например, здесь: https://koji.fedoraproject.org/koji/buildinfo?buildID=1738749. Проверил, вроде как то работает.

В-третьих, вспомнить, что Fedora для production не предназначена, и смириться с тем, что в ней багов больше обычного.

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

Подробнее..

Категории

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

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