Следуйте приведенным ниже рекомендациям, чтобы повысить надежность и стабильность модулей поставщиков. Многие рекомендации помогают определить правильный порядок загрузки модулей и порядок, в котором драйверы должны проверять устройства.
Модуль может быть библиотекой или драйвером.
Библиотечные модули – это библиотеки, которые предоставляют API для других модулей. Обычно такие модули не зависят от оборудования. Примеры модулей библиотеки: модуль шифрования по алгоритму AES, фреймворк
remoteproc, скомпилированный как модуль, и модуль logbuffer. Код модуля вmodule_init()выполняется для настройки структур данных, но никакой другой код не выполняется, если он не активирован внешним модулем.Модули драйверов – это драйверы, которые проверяют или привязывают определенный тип устройства. Такие модули предназначены для определенных устройств. Примеры модулей драйверов: UART, PCIe и аппаратное обеспечение видеокодера. Модули драйверов активируются только тогда, когда в системе присутствует связанное с ними устройство.
Если устройство отсутствует, выполняется только код модуля
module_init(), который регистрирует драйвер в основной структуре драйвера.Если устройство присутствует и драйвер успешно выполняет проверку или привязку к нему, может быть запущен другой код модуля.
Правильно используйте инициализацию и выход из модуля
Модули драйвера должны регистрировать драйвер в module_init() и отменять регистрацию драйвера в module_exit(). Один из способов обеспечить соблюдение этих ограничений – использовать макросы-обертки, которые позволяют избежать прямого использования макросов module_init(), *_initcall() или module_exit().
Для модулей, которые можно выгрузить, используйте команду
module_subsystem_driver(). Примеры:module_platform_driver(),module_i2c_driver()иmodule_pci_driver().Для модулей, которые нельзя выгрузить, используйте
builtin_subsystem_driver(). Примеры:builtin_platform_driver(),builtin_i2c_driver()иbuiltin_pci_driver().
Некоторые модули драйверов используют module_init() и module_exit(), поскольку они регистрируют более одного драйвера. Если в модуле драйвера для регистрации нескольких драйверов используются module_init() и module_exit(), попробуйте объединить драйверы в один. Например, можно использовать строку compatible или вспомогательные данные устройства, а не регистрировать отдельных водителей.
Также можно разделить модуль драйвера на два модуля.
Исключения функций инициализации и выхода
Модули библиотек не регистрируют драйверы и освобождаются от ограничений на использование module_init() и module_exit(), поскольку эти функции могут понадобиться им для настройки структур данных, рабочих очередей или потоков ядра.
Как использовать макрос MODULE_DEVICE_TABLE
Модули драйверов должны включать макрос MODULE_DEVICE_TABLE, который позволяет пользовательскому пространству определять устройства, поддерживаемые модулем драйвера, до загрузки модуля. Android может использовать эти данные, чтобы оптимизировать загрузку модулей, например не загружать модули для устройств, которых нет в системе. Примеры использования макроса можно найти в исходном коде.
Как избежать несоответствия CRC из-за предварительно объявленных типов данных
Не включайте заголовочные файлы, чтобы получить доступ к типам данных, объявленным заранее.
Некоторые структуры, объединения и другие типы данных, определенные в заголовочном файле (header-A.h), можно предварительно объявить в другом заголовочном файле (header-B.h), в котором обычно используются указатели на эти типы данных. Такой шаблон кода означает, что ядро намеренно пытается сохранить структуру данных конфиденциальной для пользователей header-B.h.
Пользователи header-B.h не должны включать header-A.h для прямого доступа к внутренним структурам данных, объявленным заранее. Это приводит к проблемам с несоответствием CONFIG_MODVERSIONS CRC (что вызывает проблемы с совместимостью ABI), когда другое ядро (например, ядро GKI) пытается загрузить модуль.
Например, struct fwnode_handle определен в include/linux/fwnode.h, но предварительно объявлен как struct fwnode_handle; в include/linux/device.h, поскольку ядро пытается скрыть детали struct fwnode_handle от пользователей include/linux/device.h. В этом случае не добавляйте #include <linux/fwnode.h> в модуль, чтобы получить доступ к элементам struct fwnode_handle. Если в вашем проекте приходится использовать такие заголовочные файлы, это говорит о плохой архитектуре.
Не обращайтесь напрямую к основным структурам ядра.
Прямой доступ к основным структурам данных ядра или их изменение могут привести к нежелательным последствиям, включая утечки памяти, сбои и несовместимость с будущими выпусками ядра. Структура данных является основной, если она соответствует любому из следующих условий:
Структура данных определяется в разделе
KERNEL-DIR/include/. Например, допустимы значенияstruct deviceиstruct dev_links_info. Структуры данных, определенные вinclude/linux/soc, не учитываются.Структура данных выделяется или инициализируется модулем, но становится видимой для ядра, поскольку передается косвенно (через указатель в структуре) или напрямую в качестве входных данных в функцию, экспортируемую ядром. Например, модуль драйвера
cpufreqинициализируетstruct cpufreq_driver, а затем передает его в качестве входных данных вcpufreq_register_driver(). После этого модуль драйвераcpufreqне должен напрямую изменятьstruct cpufreq_driver, поскольку вызовcpufreq_register_driver()делаетstruct cpufreq_driverвидимым для ядра.Структура данных не инициализирована модулем. Например,
struct regulator_dev, возвращенныйregulator_register().
Доступ к основным структурам данных ядра должен осуществляться только через функции, экспортируемые ядром, или через параметры, явно передаваемые в качестве входных данных для хуков поставщика. Если у вас нет API или хука поставщика для изменения частей основной структуры данных ядра, скорее всего, это сделано намеренно, и вам не следует изменять структуру данных из модулей. Например, не изменяйте поля внутри struct device или struct device.links.
Чтобы изменить
device.devres_head, используйте функциюdevm_*(), напримерdevm_clk_get(),devm_regulator_get()илиdevm_kzalloc().Чтобы изменить поля в
struct device.links, используйте API для связи устройств, напримерdevice_link_add()илиdevice_link_del().
Не анализировать узлы дерева устройств с совместимым свойством
Если у узла дерева устройств (DT) есть свойство compatible, для него автоматически выделяется struct device или когда of_platform_populate() вызывается в родительском узле DT (обычно драйвером родительского устройства). По умолчанию (за исключением некоторых устройств, инициализированных ранее для планировщика) предполагается, что у узла DT со свойством compatible есть struct device и соответствующий драйвер устройства. Все остальные исключения уже обрабатываются кодом выше.
Кроме того, fw_devlink (ранее of_devlink) считает узлы DT с атрибутом compatible устройствами с выделенным struct device, которое проверяется драйвером. Если у узла ДУ есть свойство compatible, но выделенный struct device не проверяется, fw_devlink может заблокировать проверку устройств-потребителей или вызовы sync_state() для устройств-поставщиков.
Если ваш драйвер использует функцию of_find_*() (например, of_find_node_by_name() или of_find_compatible_node()), чтобы напрямую найти узел ДУ со свойством compatible, а затем проанализировать этот узел, исправьте модуль. Для этого напишите драйвер устройства, который может проверить устройство, или удалите свойство compatible (возможно, только если оно не было передано в вышестоящую ветку). Чтобы обсудить альтернативные варианты, свяжитесь с командой разработчиков ядра Android по адресу kernel-team@android.com и будьте готовы обосновать свои варианты использования.
Как искать поставщиков с помощью дескрипторов DT
По возможности ссылайтесь на поставщика, используя псевдоним (ссылку или указатель на узел DT) в DT. Использование стандартных привязок ДУ и дескрипторов phandle для ссылок на поставщиков позволяет fw_devlink (ранее – of_devlink) автоматически определять зависимости между устройствами, анализируя ДУ во время выполнения. После этого ядро может автоматически проверять устройства в правильном порядке, что устраняет необходимость в упорядочивании загрузки модулей или использовании MODULE_SOFTDEP().
Устаревший сценарий (ядро ARM не поддерживает DT)
Раньше, до того как поддержка DT была добавлена в ядра ARM, потребители, например сенсорные устройства, искали поставщиков, например регуляторы, с помощью глобально уникальных строк.
Например, драйвер ACME PMIC может зарегистрировать или объявить несколько регуляторов (например, acme-pmic-ldo1–acme-pmic-ldo10), а драйвер сенсорного экрана может найти регулятор с помощью regulator_get(dev, "acme-pmic-ldo10").
Однако на другой плате LDO8 может питать сенсорное устройство, что создает неудобную систему, в которой одному и тому же драйверу сенсорного устройства необходимо определять правильную строку поиска для регулятора на каждой плате, на которой используется сенсорное устройство.
Текущий сценарий (поддержка DT в ядре ARM)
После того как поддержка DT была добавлена в ядра ARM, потребители могут идентифицировать поставщиков в DT, ссылаясь на узел дерева устройств поставщика с помощью phandle.
Потребители также могут называть ресурс в зависимости от того, для чего он используется, а не от того, кто его предоставляет. Например, драйвер сенсорного экрана из предыдущего примера может использовать regulator_get(dev, "core") и regulator_get(dev, "sensor"), чтобы получить поставщиков, которые обеспечивают работу ядра и датчика сенсорного устройства. Связанное с таким устройством ДУ похоже на следующий пример кода:
touch-device {
compatible = "fizz,touch";
...
core-supply = <&acme_pmic_ldo4>;
sensor-supply = <&acme_pmic_ldo10>;
};
acme-pmic {
compatible = "acme,super-pmic";
...
acme_pmic_ldo4: ldo4 {
...
};
...
acme_pmic_ldo10: ldo10 {
...
};
};
Наихудший сценарий
Некоторые драйверы, перенесенные из более старых ядер, включают устаревшее поведение в ДУ, которое берет худшую часть устаревшей схемы и принудительно применяет ее к более новой схеме, которая должна упростить задачу. В таких драйверах драйвер-потребитель считывает строку, которая будет использоваться для поиска, с помощью свойства DT, зависящего от устройства, а поставщик использует другое свойство, зависящее от поставщика, чтобы определить имя, которое будет использоваться для регистрации ресурса поставщика. Затем потребитель и поставщик продолжают использовать ту же старую схему использования строк для поиска поставщика. В этом худшем из возможных сценариев:
Драйвер сенсорного экрана использует код, похожий на следующий:
str = of_property_read(np, "fizz,core-regulator"); core_reg = regulator_get(dev, str); str = of_property_read(np, "fizz,sensor-regulator"); sensor_reg = regulator_get(dev, str);В дереве устройств используется код, похожий на следующий:
touch-device { compatible = "fizz,touch"; ... fizz,core-regulator = "acme-pmic-ldo4"; fizz,sensor-regulator = "acme-pmic-ldo4"; }; acme-pmic { compatible = "acme,super-pmic"; ... ldo4 { regulator-name = "acme-pmic-ldo4" ... }; ... acme_pmic_ldo10: ldo10 { ... regulator-name = "acme-pmic-ldo10" }; };
Не изменяйте ошибки API фреймворка
API фреймворка, например regulator, clocks, irq, gpio, phys и extcon, возвращают -EPROBE_DEFER в качестве значения ошибки, чтобы указать, что устройство пытается выполнить проверку, но в данный момент это невозможно, и ядро должно повторить попытку позже. Чтобы функция .probe() устройства в таких случаях работала корректно, не заменяйте и не переназначайте значение ошибки.
Замена или переназначение значения ошибки может привести к удалению -EPROBE_DEFER и тому, что ваше устройство никогда не будет проверено.
Используйте варианты API devm_*()
Когда устройство получает ресурс с помощью API devm_*(), ядро автоматически освобождает ресурс, если устройство не может выполнить проверку или успешно выполняет ее, а затем отключается. Благодаря этому код обработки ошибок в функции probe() становится более понятным, поскольку не требует переходов goto для освобождения ресурсов, полученных devm_*(), и упрощает операции отмены привязки драйвера.
Как обрабатывать отмену привязки драйвера устройства
Отвязывайте драйверы устройств намеренно и не оставляйте отвязку неопределенной, поскольку это не означает запрет. Вы должны либо полностью реализовать отмену привязки драйвера устройства, либо явно отключить ее.
Как реализовать отмену привязки драйвера устройства
При полном отключении драйверов устройств отвязывайте их корректно, чтобы избежать утечек памяти или ресурсов и проблем с безопасностью. Чтобы привязать устройство к драйверу, вызовите функцию probe() драйвера, а чтобы отменить привязку, вызовите функцию remove() драйвера. Если функции remove() нет, ядро все равно может отменить привязку устройства. Основной драйвер предполагает, что при отмене привязки драйверу не нужно выполнять никаких действий по очистке. Драйверу, который отвязан от устройства, не нужно выполнять явную очистку, если выполняются оба следующих условия:
Все ресурсы, полученные функцией
probe()драйвера, предоставляются через APIdevm_*().Аппаратному устройству не требуется последовательность завершения работы или перевода в неактивное состояние.
В этом случае ядро драйвера обрабатывает освобождение всех ресурсов, полученных через API devm_*(). Если хотя бы одно из этих утверждений неверно, драйвер должен выполнить очистку (освободить ресурсы и выключить или приостановить работу оборудования) при отмене привязки к устройству. Чтобы устройство могло корректно отменить привязку модуля драйвера, используйте один из следующих вариантов:
Если для оборудования не требуется последовательность выключения или перевода в состояние покоя, измените модуль устройства, чтобы получать ресурсы с помощью API
devm_*().Реализуйте операцию драйвера
remove()в той же структуре, что и функцияprobe(), а затем выполните очистку с помощью функцииremove().
Явно отключить отмену привязки драйвера устройства (не рекомендуется)
Если вы хотите явно запретить отвязку драйвера устройства, вам нужно запретить отвязку и выгрузку модуля.
Чтобы запретить отмену привязки, установите для флага
suppress_bind_attrsзначениеtrueв файлеstruct device_driverдрайвера. В результате файлыbindиunbindне будут показываться в каталогеsysfsдрайвера. Файлunbindпозволяет пространству пользователя отменить привязку драйвера к устройству.Чтобы запретить выгрузку модуля, убедитесь, что в
lsmodдля него указано[permanent]. Если не использоватьmodule_exit()илиmodule_XXX_driver(), модуль будет отмечен как[permanent].
Не загружайте встроенное ПО из функции probe.
Драйвер не должен загружать встроенное ПО из функции .probe(), поскольку у него может не быть доступа к встроенному ПО, если драйвер проверяет устройство до монтирования файловой системы на основе флеш-памяти или постоянного хранилища. В таких случаях API request_firmware*() может долго блокироваться, а затем завершиться с ошибкой, что может неоправданно замедлить процесс загрузки. Вместо этого отложите загрузку встроенного ПО до момента, когда клиент начнет использовать устройство. Например, драйвер дисплея может загружать встроенное ПО при открытии устройства отображения.
Использование .probe() для загрузки встроенного ПО может быть допустимо в некоторых случаях, например в драйвере часов, которому для работы требуется встроенное ПО, но устройство не доступно для пользовательского пространства. Возможны и другие варианты использования.
Как реализовать асинхронное зондирование
Поддерживайте и используйте асинхронное зондирование, чтобы воспользоваться будущими улучшениями, такими как параллельная загрузка модулей или зондирование устройств, которые могут быть добавлены в Android в будущих выпусках. Модули драйверов, которые не используют асинхронное зондирование, могут снизить эффективность таких оптимизаций.
Чтобы указать, что драйвер поддерживает и предпочитает асинхронное зондирование, задайте поле probe_type в элементе struct device_driver драйвера. В примере ниже показано, как включить поддержку для драйвера платформы.
static struct platform_driver acme_driver = {
.probe = acme_probe,
...
.driver = {
.name = "acme",
...
.probe_type = PROBE_PREFER_ASYNCHRONOUS,
},
};
Чтобы драйвер работал с асинхронным зондированием, не требуется специальный код. Однако при добавлении поддержки асинхронного зондирования следует учитывать следующее.
Не делайте предположений о ранее проверенных зависимостях. Проверьте напрямую или косвенно (большинство вызовов фреймворка) и верните значение
-EPROBE_DEFER, если один или несколько поставщиков ещё не готовы.Если вы добавите детские устройства в функцию зондирования родительского устройства, не предполагайте, что зондирование детских устройств будет выполнено немедленно.
Если зондирование не удалось, обработайте ошибку и выполните очистку (см. Используйте варианты API devm_*()).
Не используйте MODULE_SOFTDEP для упорядочивания проверок устройств
Функция MODULE_SOFTDEP() не является надежным решением для обеспечения порядка опроса устройств и не должна использоваться по следующим причинам:
Отложенный зонд. При загрузке модуля проверка устройства может быть отложена, если один из поставщиков не готов. Это может привести к несоответствию между порядком загрузки модулей и порядком проверки устройств.
Один драйвер, множество устройств Модуль драйвера может управлять определенным типом устройства. Если в системе несколько устройств одного типа и для каждого из них требуется разный порядок опроса, вы не сможете соблюсти эти требования, используя порядок загрузки модулей.
Асинхронное зондирование. Модули драйверов, выполняющие асинхронное зондирование, не сразу зондируют устройство при загрузке модуля. Вместо этого зондирование устройства выполняется параллельным потоком, что может привести к несоответствию между порядком загрузки модуля и порядком зондирования устройства. Например, если основной модуль драйвера I2C выполняет асинхронное зондирование, а модуль драйвера сенсорного экрана зависит от PMIC, который находится на шине I2C, то даже если драйвер сенсорного экрана и драйвер PMIC загружаются в правильном порядке, зондирование драйвера сенсорного экрана может быть выполнено до зондирования драйвера PMIC.
Если у вас есть модули драйверов, использующие функцию MODULE_SOFTDEP(), исправьте их, чтобы они не использовали эту функцию. Чтобы помочь вам, команда Android внесла изменения, которые позволяют ядру обрабатывать проблемы с упорядочиванием без использования MODULE_SOFTDEP(). В частности, вы можете использовать fw_devlink, чтобы обеспечить порядок зондирования, и (после того как все потребители устройства будут прозондированы) использовать обратный вызов sync_state() для выполнения любых необходимых задач.
Используйте #if IS_ENABLED() вместо #ifdef для конфигураций
Используйте #if IS_ENABLED(CONFIG_XXX) вместо #ifdef CONFIG_XXX, чтобы код внутри блока #if продолжал компилироваться, если в будущем конфигурация изменится на трехзначную. Ниже перечислены различия между ними.
#if IS_ENABLED(CONFIG_XXX)возвращает значениеtrue, если для параметраCONFIG_XXXзадано значение module (модуль) [=m] или built-in (встроенный) [=y].#ifdef CONFIG_XXXоценивается какtrue, когда дляCONFIG_XXXзадано значение built-in (=y), но не когда дляCONFIG_XXXзадано значение module (=m). Используйте это, только если вы уверены, что хотите сделать то же самое, когда для конфигурации задано значение module или она отключена.
Используйте правильный макрос для условной компиляции
Если для параметра CONFIG_XXX задано значение module (=m), система сборки автоматически определяет CONFIG_XXX_MODULE. Если драйвер управляется параметром CONFIG_XXX и вы хотите проверить, компилируется ли он как модуль, следуйте инструкциям ниже.
В файле C (или любом исходном файле, который не является заголовочным файлом) для вашего драйвера не используйте
#ifdef CONFIG_XXX_MODULE, поскольку это излишне ограничивает возможности и приводит к ошибке, если файл конфигурации переименован вCONFIG_XYZ. Для любого исходного файла, не являющегося заголовком, который компилируется в модуль, система сборки автоматически определяетMODULEдля области действия этого файла. Поэтому, чтобы проверить, компилируется ли C-файл (или любой другой исходный файл, не являющийся заголовком) как часть модуля, используйте#ifdef MODULE(без префиксаCONFIG_).В заголовочных файлах выполнить такую проверку сложнее, поскольку они не компилируются непосредственно в двоичный код, а компилируются как часть файла C (или других исходных файлов). При работе с заголовочными файлами соблюдайте следующие правила:
Для заголовочного файла, в котором используется
#ifdef MODULE, результат зависит от того, какой исходный файл его использует. Это означает, что один и тот же заголовочный файл в одной и той же сборке может иметь разные части кода, скомпилированные для разных исходных файлов (модуль, встроенный код или отключенный код). Это может быть полезно, если вам нужно определить макрос, который будет разворачиваться по-разному для встроенного кода и для модуля.Если заголовочный файл должен компилироваться в фрагменте кода, когда для параметра
CONFIG_XXXзадано значение module (независимо от того, является ли исходный файл, включающий его, модулем), в заголовочном файле необходимо использовать#ifdef CONFIG_XXX_MODULE.