Руководство по модулям поставщиков

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

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

  • Библиотечные модули — это библиотеки, предоставляющие API для использования другими модулями. Такие модули обычно не зависят от аппаратного обеспечения. Примеры библиотечных модулей включают модуль шифрования AES, платформу remoteproc , скомпилированную как модуль, и модуль logbuffer. Код модуля в module_init() запускается для настройки структур данных, но никакой другой код не запускается, если только он не запущен внешним модулем.

  • Модули драйверов — это драйверы, которые проверяют или связываются с устройством определенного типа. Такие модули аппаратно-зависимы. Примеры модулей драйверов включают аппаратное обеспечение UART, PCIe и видеокодера. Модули драйверов активируются только тогда, когда в системе присутствует связанное с ними устройство.

    • Если устройство отсутствует, единственным выполняемым кодом модуля является код module_init() , который регистрирует драйвер в структуре ядра драйвера.

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

Используйте модуль int/exit правильно

Модули драйверов должны зарегистрировать драйвер в 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() .

Не анализировать узлы 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 свойство (возможно только в том случае, если оно не было воспроизведено). Чтобы обсудить альтернативы, обратитесь к команде ядра Android по адресу kernel-team@android.com и будьте готовы обосновать свои варианты использования.

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

Обращайтесь к поставщику, используя фандл (ссылку/указатель на узел DT) в DT, когда это возможно. Использование стандартных привязок DT и phandles для ссылки на поставщиков позволяет fw_devlink (ранее of_devlink ) автоматически определять зависимости между устройствами путем анализа DT во время выполнения. Затем ядро ​​может автоматически проверять устройства в правильном порядке, устраняя необходимость в упорядочении загрузки модулей или MODULE_SOFTDEP() .

Устаревший сценарий (без поддержки DT в ядре ARM)

Раньше, до того, как в ядра ARM была добавлена ​​поддержка DT, потребители, такие как сенсорные устройства, искали поставщиков, таких как регуляторы, используя глобально уникальные строки. Например, драйвер ACME PMIC может зарегистрировать или объявить несколько регуляторов (например, от acme-pmic-ldo1 до acme-pmic-ldo10 ), а сенсорный драйвер может найти регулятор с помощью regulator_get(dev, "acme-pmic-ldo10") . Однако на другой плате LDO8 может обеспечивать сенсорное устройство, создавая громоздкую систему, в которой один и тот же драйвер сенсорного экрана должен определять правильную строку поиска для регулятора для каждой платы, в которой используется сенсорное устройство.

Текущий сценарий (поддержка DT в ядре ARM)

После того как в ядра ARM была добавлена ​​поддержка DT, потребители могут идентифицировать поставщиков в 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 ), но не так, если CONFIG_XXX установлено значение module ( =m ). Используйте это только тогда, когда вы уверены, что хотите сделать то же самое, когда конфигурация установлена ​​​​на модуль или отключена.

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

Если для 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 установлен в модуль (независимо от того, является ли исходный файл, включающий его, модулем), файл заголовка должен использовать #ifdef CONFIG_XXX_MODULE .