Глава 10. Правильное использование типов данных
- Содержание:
- Использование стандартных типов языка Си
- Использование типов с явным указанием размера данных
- Interface-Specific Types
- Other Portability Issues
- Linked Lists
- Quick Reference
Перед тем как перейти к более сложным темам, необходимо обратить внимание на некоторые вопросы переносимости кода. Современные версии ядра Linux в высокой степени переносимы и поддерживают список архитектур, имеющих между собой значительные различия. Учитывая многоплатформенную природу Linux, драйвера, расчитанные на серьезное использование также должны иметь хорошую переносимость.
Код ядра должен уметь с одной строны обеспевать доступ к данным известной длины (например, структуры данных файловой системы или регистры на плате устройства), а с другой стороны использовать особенности различных процессоров (32-битной, 64-битной, и, возможно, 16-битной архитектур).
Некоторые проблемы, обнаруженные разработчиками ядра при переносе кода с архитектуры x86 на новые архитектуры, были связаны с некоректной типизацией данных. Строгое соблюдение правил типизации данных и компиляция с флагами -Wall и -Wstrict-prototypes может предотвратить многие проблемы.
Типы данных используемые в ядре делятся на три основных класса: стандартные типы языка Си (такие как int), типы с явно заданным размером (такие как u32) и типы используемые для специфических объектов ядра (такие как pid_t). Далее будет рассмотрено, как могут быть использованы типы каждого из трех классов. В заключении данной главы будут рассмотрены некоторые другие типичные проблемы, которые можно встретить при портировании кода драйвера с платформы x86 на другие платформы. Там же можно будет ознакомиться со средствами поддержки связанных списков, представленными в последних заголовочных файлах ядра.
Если следовать представленным рекомендациям, то ваш драйвер будет полными и будет работать даже на тех платформах, на которых нет возможности его протестировать.
Использование стандартных типов языка Си
Хотя большинство программистов привыкли не задумываясь использовать стандартные типы из ряда int, long и пр., написание драйверов требует более вдумчивого подхода к использованию типов, что позволит избежать конфликтов типизации и запутанных ошибок.
Проблема заключается в том, что стандартные типы данных языка Си, могут не иметь один и тот же размер на всех архитектурах. Для демонстрации этого можно воспользоваться программкой datasize, которая включена в файлы примеров FTP-узла O’Reilly в директории misc-progs. Ниже приведен пример запуска этой программки на персональном компьютере (четыре последних типа будут разъяснены в следующем разделе):
morgana% misc-progs/datasize arch Size: char shor int long ptr long-long u8 u16 u32 u64 i686 1 2 4 4 4 8 1 2 4 8
Ниже представлен вызов этой программки на компьютерах различных архитектур, работающих под операционной системой Linux. Обратите внимание, что размеры длинного целого (long) и указателей (ptr) могут различаться.
arch Size: char shor int long ptr long-long u8 u16 u32 u64 i386 1 2 4 4 4 8 1 2 4 8 alpha 1 2 4 8 8 8 1 2 4 8 armv4l 1 2 4 4 4 8 1 2 4 8 ia64 1 2 4 8 8 8 1 2 4 8 m68k 1 2 4 4 4 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
Интересно заметить, что пространство приложений пользователя на платформе Linux-sparc64 является 32-х разрядным, поэтому указатели имеют 32 бита ширины в пространстве пользователя и 64 бита ширины в пространстве ядра. Это можно проверить загрузкой модуля kdatasize, который так же доступен в директории misc-modules примеров. Модуль сообщает информацию о размере стандартных типов языка Си, через вызов printk в функции инициализации модуля и возвращает ошибку (таким образом модуль не будет загружен):
kernel: arch Size: char short int long ptr long-long u8 u16 u32 u64 kernel: sparc64 1 2 4 8 8 8 1 2 4 8
Следует быть внимательным при смешивании различных типов данных. Иногда существуют значимые причины для этого. Одна из таких ситуация связана с адресацией памяти, разрядность которой может быть задана в ядре по разному. Хотя концептуально, адреса являются указателями, управление памятью выполняется удобнее с использованием беззнакового целого типа. Ядро интерпретирует физическую память как огромный массив, а адрес в памяти это просто индекс внутри этого массива. Кроме того, указатели проще разыменовываются. При условии прямой работы с адресами памяти через переменные целого типа, наверное, не возникнет желания разыменовать их, что позволит избежать некоторых ошибок. Поэтому, адреса в ядре описываются беззнаковым типом long, на основе того факта, что указатели и тип long всегда имеют один и тот же размер. По крайней мере на всех платформах, поддерживаемых, в данный момент Linux.
Стандарт языка C99 определяет типы intptr_t и uintptr_t для переменных целого типа содержащих значение указателя. Эти типы практически не используются в ядре 2.4, но в дальнейшем, появление этих типов в коде ядра не должно вызывать удивление.
Использование типов с явным указанием размера данных
Иногда, код ядра требует элементы данных заданного размера, либо для соответствия предопределенным бинарным структурам[39], либо для выравнивания данных внутри структур, вставкой полей "заполнителей" (пожалуйста, ознакомьтесь с разделом "Выравнивание данных" ("Data Alignment") далее в этой главе, для получения сведений о вопросах выравнивания).
[39] Это требуется при чтении таблиц разделов, при исполнении бинарных файлов или при декодировании сетевых пакетов.
Ядро предлагает следующие типы данных для использования в случаях когда важен размер определяемых данных. Все эти типы объявлены в заголовочном файле <asm/types.h>, который включается через включение заголовочного файла <linux/types.h>:
u8; /* unsigned byte (8 bits) */ u16; /* unsigned word (16 bits) */ u32; /* unsigned 32-bit value */ u64; /* unsigned 64-bit value */
Эти типы данных доступны только из кода ядра (т.е. символ __KERNEL__ должен быть определен перед инструкцией включения заголовочного файла <linux/types.h>). Существуют, но реже используются, и соответствующие знаковые типы. Чтобы получить знаковый тип, следует заменить литеру u (unsigned) в типе на литеру s (signed).
Для использования этих типов в пространстве пользователя, необходимо добавить к их имени префикс из двух знаков подчеркивания: __u8 и другие типы определены независимо от символа __KERNEL__. Если, например, драйверу нужно обменять бинарную структуру с программой, работающей в пространстве пользователя через механизм ioctl, то заголовочные файлы могут объявить 32-х битовые поля как __u32.
Важно помнить, что эти типы специфичны для Linux, и их использование будет препятствовать портированию программного обеспечения на другие системы Unix. Системы использующие последние компиляторы будут поддерживать стандартные типы C99, такие как uint8_t и uint32_t. По возможности следует использоват эти типы и в Linux, но если код предназначен и для ядер 2.0, то использование этих типов невозможно, так как только старые компиляторы работают с ядром 2.0).
Можно заметить, что иногда, ядра используют логические типы, такие как unsigned int, для элементов, емкость которых не зависит от архитектуры. Обычно это делается для обратной совместимости. Типы u32 и аналогичные были внесены в версию 1.1.67, но разработчики не могут переписать старые структуры данных на новые типы, так как компилятор ответит на это предупреждениями о несоответствии типов между полями структуры и значениями, которые были им назначены [40]. Линус не ожидал, что ОС, которая писалась им для собственного использования станет межплатформенной. В результате, старые структуры все еще используются.
[40] Компилятор сообщает о несоответствии типов даже если два типа отличаются только именами, как, например, unsigned long и u32 на персональном компьютере.
Interface-Specific Types
Многие из повсеместно используемых типов данных в ядре имеют свои собственные описания в операторе typedef, что позволяет избежать проблем переносимости. Например, идентификатор процесса (pid) обычно обозначается типом pid_t, а не типом int. Использование типа pid_t скрывает возможные различия в реальной типизации данных. Будем использовать выражение "библиотечные" или "interface-specific" типы данных, чтобы обозначить типы, объявленные в библиотеке, для описания интерфейса к специфическим структурам данных.
Даже если не определено никаких библиотечных типов данных, важно всегда использовать типы соответствующие уже написанной части кода ядра. Например, счетчик джиффисов (jiffy count) всегда объявляется типом unsigned long, независимо от его реального размера, та
Even when no interface-specific type is defined, it’s always important to use the proper data type in a way consistent with the rest of the kernel. A jiffy count, for instance, is always unsigned long, independent of its actual size, so the unsigned long type should always be used when working with jiffies. In this section we concentrate on use of «_t» types.
The complete list of _t types appears in
Whenever your driver uses functions that require such «custom» types and you don’t follow the convention, the compiler issues a warning; if you use the -Wall compiler flag and are careful to remove all the warnings, you can feel confident that your code is portable.
The main problem with _t data items is that when you need to print them, it’s not always easy to choose the right printk or printf format, and warnings you resolve on one architecture reappear on another. For example, how would you print a size_t, which is unsigned long on some platforms and unsigned int on some others?
Whenever you need to print some interface-specific data, the best way to do it is by casting the value to the biggest possible type (usually long or unsigned long) and then printing it through the corresponding format. This kind of tweaking won’t generate errors or warnings because the format matches the type, and you won’t lose data bits because the cast is either a null operation or an extension of the item to a bigger data type.
In practice, the data items we’re talking about aren’t usually meant to be printed, so the issue applies only to debugging messages. Most often, the code needs only to store and compare the interface-specific types, in addition to passing them as arguments to library or kernel functions.
Although _t types are the correct solution for most situations, sometimes the right type doesn’t exist. This happens for some old interfaces that haven’t yet been cleaned up.
The one ambiguous point we’ve found in the kernel headers is data typing for I/O functions, which is loosely defined (see the section «Platform Dependencies» in Chapter 8, «Hardware Management»). The loose typing is mainly there for historical reasons, but it can create problems when writing code. For example, one can get into trouble by swapping the arguments to functions like outb; if there were a port_t type, the compiler would find this type of error.