Другие вопросы переносимости
В дополнение к типам данным есть несколько других программных вопросов, чтобы иметь ввиду при написании драйвера, если вы хотите, чтобы он был переносим между платформами Linux. Общее правило состоит в том, чтобы относиться с подозрением к явным постоянным значениям. Обычно код параметризован помощью макросов препроцессора. В этом разделе перечисляются наиболее важные проблемы переносимости. Всякий раз, когда вы сталкиваетесь другими значениями, которые были параметризованы, вы можете найти подсказки в файлах заголовков и драйверах устройств, распространяемых с официальным ядром.
В дополнение к типам данным есть несколько других программных вопросов, чтобы иметь ввиду при написании драйвера, если вы хотите, чтобы он был переносим между платформами Linux. Общее правило состоит в том, чтобы относиться с подозрением к явным постоянным значениям. Обычно код параметризован помощью макросов препроцессора. В этом разделе перечисляются наиболее важные проблемы переносимости. Всякий раз, когда вы сталкиваетесь другими значениями, которые были параметризованы, вы можете найти подсказки в файлах заголовков и драйверах устройств, распространяемых с официальным ядром.
Когда речь идёт о временных интервалах, не думайте, что есть 1000 тиков в секунду. Хотя в настоящее время для архитектуры i386 это справедливо, не каждая платформа Linux работает на этой скорости. Предположение может быть ложным даже для x86, если вы играете со значением HZ (как делают некоторые люди), и никто не знает, что произойдёт в будущих ядрах. Всякий раз, когда вы рассчитываете интервалы времени используя тики, масштабируйте ваши времена с помощью HZ (число прерываний таймера в секунду). Например, чтобы проверить ожидание в пол-секунды, сравнивайте прошедшее время с HZ/2. Более широко, число тиков, соответствующее msec миллисекунд, всегда msec*HZ/1000.
При играх с памятью помните, что память страницы - PAGE_SIZE байт, а не 4 Кб. Предполагая, что размер страницы составляет 4 Кбайт и явное указание этого значения является распространённой ошибкой среди программистов ПК, вместо этого, поддерживаемые платформы показывают размер страницы от 4 Кб до 64 Кб и иногда они различаются между разными реализациями одной и той же платформы. Соответствующими макросами являются PAGE_SIZE и PAGE_SHIFT. Последний содержит число битов для сдвига адреса, чтобы получить номер страницы. В настоящее время число составляет 12 или больше для страниц, которые 4 Кб и более. Макросы определены в ; программы пространства пользователя могут использовать библиотечную функцию getpagesize, если им когда-нибудь потребуется такая информация.
Давайте посмотрим на нетривиальную ситуацию. Если драйверу необходимо 16 Кб для временных данных, не следует указывать order как 2 для get_free_pages. Вам необходимо переносимое решение. Такое решение, к счастью, был написано разработчиками ядра и называется get_order:
#include
int order = get_order(16*1024);
buf = get_free_pages(GFP_KERNEL, order);
Помните, что аргумент get_order должно быть степенью двойки.
Будьте внимательны, чтобы не делать предположений о порядке байт. Если ПК сохраняют многобайтовые величины начиная с младшего байта (сначала младший конец, то есть little-endian), некоторые высокоуровневые платформы делают это другим способом (big-endian). Когда это возможно, ваш код должен быть написан так, чтобы не заботиться о порядке байт в данных, которыми он манипулирует. Однако, иногда драйверу необходимо построить целое число из раздельный байтов или сделать обратное, или он должен взаимодействовать с устройством, которое ожидает определённый порядок.
Подключаемый файл определяет либо __BIG_ENDIAN, либо __LITTLE_ENDIAN, в зависимости от порядка байт в процессоре. Имея дело с вопросами порядка байт, вы могли бы написать кучу условий #ifdef __LITTLE_ENDIAN, но есть путь лучше. Ядро Linux определяет набор макросов, которые занимаются переводом между порядком байтов процессора и теми данными, которые необходимо сохранять или загружать с определённым порядком байтов. Например:
u32 cpu_to_le32 (u32);
u32 le32_to_cpu (u32);
Эти два макроса преобразуют значение от любого используемого процессором в unsigned, little-endian, 32-х разрядное и обратно. Они работают независимо от того, использует ли ваш процессор big-endian или little-endian и, если на то пошло, является ли он 32-х разрядным процессором или нет. Они возвращают их аргумент неизменным в тех случаях, где нечего делать. Использование этих макросов позволяет легко писать переносимый код без необходимости использовать большое количество конструкций условной компиляции.
Существуют десятки подобных процедур; вы можете увидеть их полный список в и . Спустя некоторое время шаблону не трудно следовать. be64_to_cpu преобразует unsigned, big-endian, 64-х разрядное значение во внутреннее представление процессора. le16_to_cpus, вместо того, обрабатывает signed, little-endian, 16-ти разрядное значение. При работе с указателями вы можете также использовать функции, подобные cpu_to_le32p, которые принимают указатель на значение, которое будет преобразовано, вместо самого значения указателя. Для всего остального смотрите подключаемый файл .
Последней проблемой, заслуживающей рассмотрения при написании переносимого кода, является то, как получить доступ к невыровненным данным, например, как прочитать 4-х байтовое значение, хранящееся по адресу, который не кратен 4-м байтам. Пользователи i386 часто адресуют невыровненные элементы данных, но не все архитектуры позволяют это. Многие современные архитектуры генерируют исключение каждый раз, когда программа пытается передавать невыровненные данные; передача данных обрабатывается обработчиком исключения с большой потерей производительности. Если вам необходимо получить доступ невыровненным данным, вам следует использовать следующие макросы:
#include
get_unaligned(ptr);
put_unaligned(val, ptr);
Эти макросы безтиповые и работают для каждого элемента данных, будь они один, два, четыре, или восемь байт длиной. Они определяются в любой версии ядра.
Другой проблемой, связанная с выравниванием, является переносимость структур данных между разными платформами. Такая же структура данных (как определена в исходном файле на языке Си) может быть скомпилирована по-разному на разных платформах. Компилятор передвигает поля структуры, для соответствия соглашениям, которые отличаются от платформы к платформе.
Для записи структур данных для элементов данных, которые могут перемещаться между архитектурами, вы должны всегда следовать естественному выравниванию элементов данных в дополнение к стандартизации на определённый порядок байтов. Естественное выравнивание означает хранение объектов данных по адресу, кратному их размеру (например, 8-ми байтовые объекты располагаются по адресам, кратным 8). Для принудительного естественного выравнивания, чтобы не допустить организацию полей компилятором непредсказуемым образом, вы должны использовать поля-заполнители, во избежание оставления пустот в структуре данных.
Чтобы показать, как компилятором выполняется выравнивание, в каталоге misc-progs примеров кода распространяется программа dataalign и эквивалентный модуль kdataalign, как часть misc-modules. Это результат работы программы на нескольких платформах и результат работы модуля на SPARC64:
arch Align: char short int long ptr long-long u8 u16 u32 u64
i386 1 2 4 4 4 4 1 2 4 4
i686 1 2 4 4 4 4 1 2 4 4
alpha 1 2 4 8 8 8 1 2 4 8
armv4l 1 2 4 4 4 4 1 2 4 4
ia64 1 2 4 8 8 8 1 2 4 8
mips 1 2 4 4 4 8 1 2 4 8
ppc 1 2 4 4 4 8 1 2 4 8
sparc 1 2 4 4 4 8 1 2 4 8
sparc64 1 2 4 4 4 8 1 2 4 8
x86_64 1 2 4 8 8 8 1 2 4 8
kernel: arch Align: char short int long ptr long-long u8 u16 u32 u64
kernel: sparc64 1 2 4 8 8 8 1 2 4 8
Интересно отметить, что не на всех платформах 64-х разрядные значения выровнены по 64-х битной границе, так что вам необходимо заполнить поля для обеспечения выравнивания и обеспечения переносимости.
Наконец, необходимо учитывать, что компилятор может спокойно вставить заполнитель в структуру сам, чтобы обеспечить выравнивание каждого поля для хорошей производительности на целевом процессоре. Если вы определяете структуру, которая призвана соответствовать структуре, ожидаемой устройством, это автоматическое заполнение может помешать вашей попытке. Способом преодоления этой проблемы является сказать компилятору, что структура должна быть "упакована" без добавления наполнителей. Например, файл заголовка ядра определяет несколько структур данных, используемых для взаимодействия с BIOS x86, и включает в себя следующие определения:
struct {
u16 id;
u64 lun;
u16 reserved1;
u32 reserved2;
} __attribute__ ((packed)) scsi;
Без такого __attribute__ ((packed)) полю lun предшествовало бы два заполняющих байта или шесть, если бы мы компилировали структуру на 64-х разрядной платформе.
Многие внутренние функции ядра возвращают вызывающему значение указателя. Многие из этих функций также могут закончится неудачно. В большинстве случаев отказ указывается возвращением указателя со значением NULL. Этот метод работает, но он не может сообщить точную природу проблемы. Некоторым интерфейсам действительно необходимо вернуть собственно код ошибки, с тем, чтобы вызвавший мог сделать правильное решение, основанное на том, что на самом деле не так.
Некоторые интерфейсы ядра возвращают эту информацию кодируя код ошибки в значении указателя. Такие функции должны использоваться с осторожностью, поскольку их возвращаемое значение нельзя просто сравнить с NULL. Чтобы помочь в создании и использовании подобного вида интерфейса, предоставлен небольшой набор функций (в ).
Функция, возвращающая тип указателя может, вернуть значение ошибки с помощью:
void *ERR_PTR(long error);
где error является обычным отрицательным кодом ошибки. Для проверки, является ли возвращённый указатель кодом ошибки или нет, вызвавший может использовать IS_ERR:
long IS_ERR(const void *ptr);
Если вам необходим настоящий код ошибки, он может быть извлечён с помощью:
long PTR_ERR(const void *ptr);
Вы должны использовать PTR_ERR только для значения, для которого IS_ERR возвращает значение "истина"; любое другое значение является правильным указателем.
Ещё одной потенциальной ошибкой может быть сравнение с 0 вместо NULL. Чаще всего в качестве значения NULL действительно используется 0. Это вызвано тем, что процессор начинает выполнение программы после подачи питания с адреса 0. Таким образом, этот адрес нигде больше использоваться не может. Однако, некоторые процессоры работают по-другому. Они могут, например, начинать выполнение наоборот, с последнего физически доступного адреса. На таких платформах NULL вполне может быть не равен 0.
Комментариев нет:
Отправить комментарий
Примечание. Отправлять комментарии могут только участники этого блога.