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

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

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

  • Библиотечные модули — это библиотеки, предоставляющие 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 .