Рекомендации по модулю поставщиков

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

Модуль может быть библиотекой или драйвером .

  • Библиотечные модули — это библиотеки, которые предоставляют 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 строку или aux-данные устройства вместо регистрации отдельных драйверов. В качестве альтернативы вы можете разделить модуль драйвера на два модуля.

Исключения функций init и exit

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

Используйте макрос MODULE_DEVICE_TABLE

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

Избегайте несоответствий 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() .

Не анализировать узлы devicetree с совместимыми свойствами

Если узел дерева устройств (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 свойство (возможно, только если оно не было передано в upstream). Чтобы обсудить альтернативы, свяжитесь с командой Android Kernel Team по адресу 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 — это то, что позволяет пользовательскому пространству инициировать отвязку драйвера от его устройства.

  • Чтобы запретить выгрузку модуля, убедитесь, что модуль имеет [permanent] в lsmod . Если не использовать 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 ), но не оценивается как true, когда 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 .