Используйте следующие рекомендации для повышения надёжности и устойчивости модулей вашего поставщика. Соблюдение многих рекомендаций поможет упростить определение правильного порядка загрузки модулей и порядка, в котором драйверы должны проверять наличие устройств.
Модуль может быть библиотекой или драйвером .
Библиотечные модули — это библиотеки, предоставляющие 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-Ah
), могут быть предварительно объявлены в другом заголовочном файле ( header-Bh
), который обычно использует указатели на эти типы данных. Этот шаблон кода означает, что ядро намеренно пытается сохранить структуру данных в тайне для пользователей header-Bh
.
Пользователям header-Bh
не следует включать header-Ah
для прямого доступа к внутренним данным этих предварительно объявленных структур данных. Это приводит к проблемам с несоответствием CRC CONFIG_MODVERSIONS
(что приводит к проблемам соответствия 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
, которое проверяется драйвером. Если у узла DT есть свойство compatible
, но выделенное struct device
не проверяется, fw_devlink
может заблокировать проверку для своих устройств-потребителей или заблокировать вызовы sync_state()
для своих устройств-поставщиков.
Если ваш драйвер использует функцию of_find_*()
(например, of_find_node_by_name()
или of_find_compatible_node()
) для непосредственного поиска узла DT с compatible
свойством и последующего анализа этого узла DT, исправьте модуль, написав драйвер устройства, который может проверить устройство или удалить compatible
свойство (возможно, только если оно не было передано в апстрим). Чтобы обсудить альтернативные варианты, свяжитесь с командой разработчиков ядра Android по адресу kernel-team@android.com и будьте готовы обосновать свои варианты использования.
Используйте DT phandles для поиска поставщиков
Для ссылки на поставщика используйте phandle (ссылку или указатель на узел DT) в DT, когда это возможно. Использование стандартных привязок DT и phandle для ссылки на поставщиков позволяет fw_devlink
(ранее of_devlink
) автоматически определять зависимости между устройствами, анализируя DT во время выполнения. После этого ядро может автоматически проверять устройства в правильном порядке, устраняя необходимость в упорядочивании загрузки модулей или MODULE_SOFTDEP()
.
Устаревший сценарий (поддержка DT в ядре ARM отсутствует)
Ранее, до добавления поддержки 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")
для получения поставщиков, обеспечивающих работу ядра и датчика сенсорного экрана. Соответствующий DT для такого устройства аналогичен следующему примеру кода:
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, которое использует худшую часть устаревшей схемы и принудительно использует новую схему, призванную упростить работу. В таких драйверах драйвер потребителя считывает строку для поиска, используя свойство 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);
DT использует код, аналогичный следующему:
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()
осуществляются через API-интерфейсыdevm_*()
.Аппаратное устройство не требует последовательности выключения или приостановки работы.
В этой ситуации ядро драйвера обрабатывает освобождение всех ресурсов, полученных через 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()
, так как у него может не быть доступа к ней, если драйвер выполнит проверку до монтирования файловой системы на базе флеш-памяти или постоянного хранилища. В таких случаях 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
установлен на модуль (=m
) или встроенный (=y
).#ifdef CONFIG_XXX
возвращает значениеtrue
, еслиCONFIG_XXX
имеет значение «встроенный» (=y
), но не возвращает значение «истина», еслиCONFIG_XXX
имеет значение «модуль» (=m
). Используйте это только в том случае, если вы уверены, что хотите добиться того же результата, если конфигурация имеет значение «модуль» или отключена.
Используйте правильный макрос для условной компиляции
Если CONFIG_XXX
установлен как модуль ( =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
.