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

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

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

  • Библиотечные модули — это библиотеки, предоставляющие 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 , но в include/linux/device.h она объявлена ​​как struct fwnode_handle; поскольку ядро ​​пытается сохранить конфиденциальность информации о 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 для поиска поставщиков.

По возможности, обращайтесь к поставщику, используя 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 установлено значение module ( =m ) или built-in ( =y ).

  • #ifdef CONFIG_XXX возвращает true , когда CONFIG_XXX установлено на built-in ( =y ), но не возвращает true, когда 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 .