Разработка ядра Linux

Лав Роберт

Глава 19

Переносимость

 

 

Linux — переносимая операционная система, которая поддерживает большое количество различных компьютерных аппаратных платформ. Переносимость— это свойство, которое указывает, насколько легко можно перенести код, который выполнялся на одной аппаратной платформе, для выполнения на другой аппаратной платформе (если вообще это возможно). Известно, что ОС Linux является переносимой операционной системой, поскольку ее уже перенесли (портировали) на большое количество различных аппаратных платформ. Тем не менее переносимость не возникает сама по себе, для выполнения она требует большого количества проектных решений. Сегодня процесс перенесения ОС Linux на другую аппаратную платформу достаточно прост (в относительном смысле, конечно). В этой главе рассказывается о том, как писать переносимый код, — вопрос, о котором всегда необходимо помнить при написании нового кода ядра или драйверов устройств.

Некоторые операционные системы специально разрабатываются с учетом требований переносимости как главного свойства. По возможности минимальное количество кода выполняется зависимым от аппаратуры. Разработка на языке ассемблера сводится к минимуму, а интерфейсы и свойства выполняются принципиально общими и абстрактными, чтобы иметь возможность работать на различных аппаратных платформах. Очевидным преимуществом в этом случае является легкость поддержки новой аппаратной платформы. В некоторых случаях простые операционные системы с высокой переносимостью могут быть нормированы на новую аппаратную платформу только путем изменения нескольких сотен строк специфичного кода. Недостаток такого подхода состоит в том, что не используются специфические свойства аппаратной платформы и код не может быть в ручную оптимизирован под конкретную машину. Переносимость ставится выше оптимальности. Примером операционных систем с высокой переносимостью могут быть Minix, OpenBSD и многие исследовательские системы.

С противоположной стороны можно поставить операционные системы, в которых оптимизация кода выполняется в ущерб переносимости. Код, по возможности, пишется на языке ассемблера или специфические свойства аппаратных платформ используются каким-либо другим образом. Свойства ядра разрабатываются на основании свойств аппаратной платформы. В этом случае перенос операционной системы на другую аппаратную платформу сводится к написанию ядра с нуля. Оптимальность выполняется в ущерб переносимости. Примером таких систем могут быть DOS и Windows 9x. Сегодня таким системам нет необходимости иметь более оптимальный код, чем переносимым операционным системам, однако они предоставляют возможность в максимальной степени использовать ручную оптимизацию кода.

Операционная система Linux в плане переносимости занимает промежуточное положение. Насколько это целесообразно из практических соображений, интерфейсы и код сохраняются независимыми от аппаратной платформы и пишутся на языке С. Однако функции ядра, которые критичны к производительности, выполняются зависимыми от аппаратной платформы. Например, низкоуровневый код и код, который должен выполняться очень быстро, разрабатывается зависимым от аппаратной платформы и обычно на языке ассемблера. Такой подход позволяет сохранить переносимость ОС Linux и при этом воспользоваться оптимизациями.

В случаях, когда переносимость становится помехой производительности, производительность обычно всегда побеждает. В остальных случая сохраняется переносимость кода.

Обычно экспортируемые интерфейсы ядра независимы от аппаратной платформы. Если различные части одной подпрограммы должны быть разными для разных аппаратных платформ (из соображений производительности или по необходимости), то код выполняется в виде нескольких функций, которые вызываются, в нужных местах. Для каждой поддерживаемой аппаратной платформы реализуются свои функции, которые затем компонуются в общий исполняемый образ ядра.

Хороший пример — планировщик. Большая часть планировщика написана независимым от аппаратной платформы образом на языке С. Реализация находится в файле kernel/sched.c. Некоторые из функций планировщика, такие как переключение состояния процессора или переключение адресного пространства, очень сильно зависят от аппаратной платформы. Следовательно, функция context_switch(), которая переключает выполнение от одного процесса к другому и написана на языке С, вызывает функции switch_to() и switch_mm() для переключения состояния процессора и переключения адресного пространства соответственно.

Код функций switch_to() и switch_mm() выполнен отдельно для каждой аппаратной платформы, которую поддерживает операционная система Linux. Когда операционная система Linux портируется на новую аппаратную платформу, то для новой аппаратной платформы просто необходимо реализовать эти функции.

Файлы, которые относятся к определенной аппаратной платформе, находятся в каталоге arch/<аппаратная платформа>/ и include/asm-<аппаратная платформа>/, где <аппаратная платформа> — это короткое имя, которое представляет аппаратную платформу, поддерживаемую ядром Linux. Например, аппаратной платформе Intel x86 присвоено имя i386. Для этого типа машин файлы находятся в каталогах arch/i386 и include/asm-i386. Ядра серии 2.6 поддерживают следующие аппаратные платформы: alpha, arm, cris, h8300, i38, ia64, m68k, m68knommu, mips, mips64, parisc, ppc, ppc64, s390, sh, spare, sparc64, um, v850 и x86-64. Более полное описание приведено в табл. 19.1.

 

История переносимости Linux

Когда Линус Торвальдс впервые выпустил операционную систему Linux в ничего не подозревающий мир, эта ОС работала только на аппаратной платформе Intel i386. Хотя данная операционная система и была достаточно хорошо обобщена и хорошо написана, переносимость для нее не была основным требованием. Однажды Линус даже говорил, что операционная система Linux не будет работать ни на какой аппаратной платформе, кроме i386! Тем не менее в 1993 году началась работа по портированию ОС Linux на машины Digital Alpha. Аппаратная платформа Digital Alpha была повой высокопроизводительной RISC-платформой с поддержкой 64-разрядной адресации памяти. Она очень сильно отличалась от аппаратной платформы i386, о которой говорил Линус. Тем не менее, первоначальный перенос на аппаратную платформу Alpha занял около года, и аппаратная платформа Alpha стала первой официально поддерживаемой аппаратной платформой после x86. Это портирование было, наверное, самым сложным, потому что — первым. Вместо простого переписывания ядра для поддержки новой аппаратной платформы, части ядра были переписаны с целью введения переносимости. Хотя это и привело к выполнению большого количества работы, в результате получился более ясный для понимания код, и в будущем перенос стало выполнять более просто.

Первые выпуски ОС Linux поддерживали только платформу i386, а серия ядер 1.2 уже поддерживала Digital Alpha, Intel x86, MIPS и SPARC, хотя такая поддержка была отчасти экспериментальной.

С выпуском ядра версии 2.0 была добавлена официальная поддержка платформ Motorola 68k и PowerPC. В дополнение к этому поддержка всех аппаратных платформ, которые ранее поддерживались ядрами серии 1.2, стала официальной и стабильной.

В серию ядер 2.2 была введена поддержка еще большего количества аппаратных платформ: добавлены ARM, IBM S/390 и UltraSPARC. Через несколько лет в серии ядер 2.4 количество поддерживаемых аппаратных платформ было почти удвоено, и их количество стало равным 15. Была добавлена поддержка платформ CRIS, IA-64, 64-разрядная MIPS, HP PA-RISC, 64-разрядная IBM S/390 и Hitachi SH.

В серии 2.6 количество поддерживаемых аппаратных платформ было доведено до 20 за счет добавления платформ Motorola 68k бел устройства MMU, H8/300, IBM POWER, v850, x86-64 и версии ядра, которое работает на виртуальной машине под ОС Linux - Usermode Linux. Поддержка 64-разрядной s390 была объединена с 32- разрядной платформой s390, чтобы избежать дублирования.

Необходимо заметить, что каждая из этих аппаратных платформ поддерживает различные типы машин и микросхем. Некоторые из поддерживаемых аппаратных платформ, такие как ARM и PowerPC, поддерживают очень большое количество типов микросхем и машин. Поэтому, хотя ОС Linux и работает на 20 аппаратных платформах, она работает на гораздо большем количестве типов компьютеров!

 

Размер машинного слова и типы данных

 

Машинное слово (word) — это количество данных, которые процессор может обработать за одну операцию. Здесь можно применить аналогию документа, состоящего из символов (character, 8 бит) и страниц (много слов). Слово— это некоторое количество битов, как правило 16, 32 или 64. Когда говорят о "n-битовой" машине, то чаще всего имеют в виду размер машинного слова. Например, когда говорят, что процессор Intel Pentium — это 32-разрядный процессор, то обычно имеют в виду размер машинного слова, равный 32 бит, или 4 байт.

Размер процессорных регистров общего назначения равен размеру машинного слова этого процессора. Обычно разрядность остальных компонентов этой же аппаратной платформы в точности равна размеру машинного слова. Кроме того, по крайней мере для аппаратных платформ, которые поддерживаются ОС Linux, размер адресного пространства соответствует размеру машинного слова. Следовательно, размер указателя равен размеру машинного слова. В дополнение к этому, размер типа long языка С также равен размеру машинного слова. Например, для аппаратной платформы Alpha размер машинного слова равен 64 бит. Следовательно, регистры, указатели и тип long имеют размер 64 бит. Тип int для этой платформы имеет размер 32 бит. Машины платформы Alpha могут обработать 64 бит — одно слово с помощью одной операции.

Слова, двойные слова и путаница

Для некоторых операционных систем и процессоров стандартную порцию данных не называют машинным словом . Вместо этого, словом называется некоторая фиксированная порция данных, название которой выбрано случайным образом или имеет исторические корни. Например, в некоторых системах данные могут разбиваться на байты (byte — 8 бит), слова (word — 16 бит), двойные слова (double word — 32 бит) и четверные слова (quad word — 64 бит), несмотря на то что на самом деле система является 32-разрядной. В этой книге и вообще в контексте операционной системы Linux под машинным словом понимают стандартную порцию данных процессора, как обсуждалось ранее.

Для каждой аппаратной платформы, поддерживаемой операционной системой Linux, в файле определяется константа BITTS_PER_LONG, которая равна размеру типа long языка С и совпадает с размером машинного слова системы. Полный список всех поддерживаемых аппаратных платформ и их размеры машинного слова приведены в табл. 19.1.

Таблица 19.1. Поддерживаемые аппаратные платформы

Аппаратная платформа Описание Размер машинного слова
alpha Digital Alpha 64 бит
arm ARM и StrongARM 32 бит
cris CRIS 32 бит
h8300 H8/300 32 бит
I386 Intel x86 32 бит
ia64 IA-64 64 бит
m68k Motorola 68k 32 бит
m86knommu m68k без устройства MMU 32 бит
mips MIPS 32 бит
mips64 64-разрядная MIPS 64 бит
parisc HP PA-RISC 32 бит, или 64 бит
ppc PowerPC 32 бит
ppc64 POWER 64 бит
s390 IBM S/390 32 бит, или 64 бит
sh Hitachi SH 32 бит
sparс SPARC 32 бит
sparc64 UltraSPARC 64 бит
um Usermode Linux 32 бит, или 64 бит
v850 v850 32 бит
x86_64 X86-64 64 бит

Стандарт языка С явно указывает, что размер памяти, которую занимают переменные стандартных типов данных, зависит от аппаратной реализации, при этом также определяется минимально возможный размер типа. Неопределенность размеров стандартных типов языка С для различных аппаратных платформ имеет свои положительные и отрицательные стороны. К плюсам можно отнести то, что для стандартных типов языка С можно пользоваться преимуществами, связанными с размером машинного слова, а также отсутствие необходимости явного указания размера. Для ОС Linux размер типа long гарантированно равен размеру машинного слова. Это не совсем соответствует стандарту ANSI С, однако является стандартной практикой в ОС Linux. Как недостаток можно отметить, что при разработке кода нельзя рассчитывать на то, что данные определенного типа занимают в памяти определенный размер. Более того, нельзя гарантировать, что переменные типа int занимают столько же памяти, сколько и переменные типа long.

Ситуация еще более запутывается тем, что одни и те же типы данных в пространстве пользователя и в пространстве ядра не обязательно должны соответствовать друг другу. Аппаратная платформа sparc64 предоставляет 32-разрядное пространство пользователя, а поэтому указатели, типы int и long имеют размер 32 бит. Однако в пространстве ядра для аппаратной платформы размер типа int равен 32 бит, а размер указателей и типа long равен 64 бит. Тем не менее такая ситуация не является обычной.

Всегда необходимо помнить о следующем.

• Как и требует стандарт языка С, размер типа char всегда равен 8 бит (1 байт),

• Нет никакой гарантии, что размер типа int для всех поддерживаемых аппаратных платформ будет равен 32 бит, хотя сейчас для всех платформ он равен именно этому числу.

• То же касается и типа short, который для всех поддерживаемых аппаратных платформ сейчас равен 16 бит.

• Никогда нельзя надеяться, что тип long или указатель имеет некоторый заданный размер. Этот размер для поддерживаемых аппаратных платформ может быть равен 32, или 64 бит.

• Так как размер типа long разный для различных аппаратных платформ, никогда нельзя предполагать, что sizeof(int) == sizeof(long).

• Точно так же нельзя предполагать, что размер типа int и размер указателя совпадают.

 

Скрытые типы данных

Скрытые (opaque) типы данных — это те типы, для которых не раскрывается их внутренняя структура, или формат. Они похожи на черный ящик, насколько это можно реализовать в языке программирования С. В этом языке программирования нет какой-либо особенной поддержки для этих типов. Вместо этого, разработчики определяют новый тип данных через оператор typedef, называют его скрытым и надеются на то, что никто не будет преобразовывать этот тип в стандартный тип данных языка С. Любые использования данных этих типов возможны только через специальные интерфейсы, которые также создаются разработчиком. Примером может быть тип данных pid_t, в котором хранится информация об идентификаторе процесса. Размер этого типа данных не раскрывается, хотя каждый может смошенничать, использовать размер по максимуму и работать с этим типом, как с типом int. Если нигде явно не используется размер скрытого типа данных, то размер этого типа может быть изменен, и это не вызовет никаких проблем. На самом деле так уже однажды случилось: в старых Unix-подобных операционных системах тип pid_t был определен как short.

Еще один пример скрытого типа данных — это тип atomic_t. Как уже обсуждалось в главе 9, "Средства синхронизации в ядре", этот тип содержит данные целочисленного типа, с которыми можно выполнять атомарные операции. Хотя этот тип и соответствует типу int, использование скрытого типа данных позволяет гарантировать, что данные этого типа будут использоваться только в специальных функциях, которые выполняют атомарные операции. Скрытые типы позволяют скрыть размер типа данных, который не всегда равен полным 32 разрядам, как в случае платформы SPARC.

Другие примеры скрытых типов данных в ядре — это dev_t, gid_t и uid_t. При работе со скрытыми типами данных необходимо помнить о следующем.

• Нельзя предполагать, что данные скрытого типа имеют некоторый определенный размер в памяти.

• Нельзя преобразовывать скрытый тип обратно в стандартный тип данных.

Разрабатывать код необходимо с учетом того, что размер и внутреннее представление скрытого типа данных могут изменяться.

 

Специальные типы данных

Некоторые данные в ядре, кроме того, что представляются с помощью скрытых типов, требуют еще и специальных типов данных. Два примера — счетчик импульсов системного таймера jiffies и параметр flags, используемый для обработки прерываний. Для хранения этих данных всегда должен использоваться тип unsigned long.

При хранении и использовании специфических данных всегда необходимо обращать особенное внимание на тот тип данных, который представляет эти данные, и использовать именно его. Часто встречающейся ошибкой является использование другого типа, например типа unsigned int. Хотя для 32-разрядных аппаратных платформ это не приведет к проблемам, на 64-разрядных системах возникнут проблемы.

 

Типы с явным указанием размера

Часто при программировании необходимы типы данных заданного размера. Обычно это необходимо для удовлетворения некоторых внешних требований, связанных с аппаратным обеспечением, сетью или бинарной совместимостью. Например, звуковой адаптер может иметь 32-разрядный регистр, пакет сетевого протокола — 16-разрядное поле данных, а исполняемый файл — 8 битовый идентификатор cookie. В этих случаях тип, который представляет данные, должен иметь точно заданный размер.

В ядре типы данных явно заданного размера определены в файле , который включается из файла . В табл. 19.2 приведен полный список таких типов данных.

Таблица 19.2. Типы данных явно заданного размера

Тип Описание
s8 байт со знаком
u8 байт без знака
s16 16-разрядное целое число со знаком
u16 16-разрядное целое число без знака
s32 32-разрядное целое число со знаком
u32 32-разрядное целое число без знака
s64 64-разрядное целое число со знаком
u64 64-разрядное целое число без знака

Варианты со знаком используются редко.

Эти типы данных, с явно заданным размером, просто определены с помощью оператора typedef через стандартные типы данных языка С. Для 64-разрядной машины они могут быть определены следующим образом.

typedef signed char s8;

typedef unsigned char u8;

typedef signed short s16;

typedef unsigned short u16;

typedef signed int s32;

typedef unsigned int u32;

typedef signed long s64;

typedef unsigned long u64;

Для 32-разрядной машины их можно определить, как показано ниже.

typedef signed char s8;

typedef unsigned char u8;

typedef signed short s16;

typedef unsigned short u16;

typedef signed int s32;

typedef unsigned int u32;

typedef signed long long s64;

typedef unsigned long long u64;

 

Знак типа данных

char

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

Для большинства аппаратных платформ тип char является знаковым, а диапазон значений данных этого типа от -128 до 127. Для небольшого количества аппаратных платформ, таких как ARM, тип char по умолчанию без знака, а возможные значения данных этого типа лежат в диапазоне от 0 до 255.

Например, для систем, на которых тип char без знака, выполнение следующего кода приведет к записи в переменную i числа 255 вместо -1.

char i = -1;

На других машинах, где тип char является знаковым, этот код выполнится правильно и в переменную i запишется значение -1. Если действительно нужно, чтобы в любом случае было записано значение -1, то предыдущий код должен выглядеть следующим образом.

signed char i = -1;

Если в вашем коде используется тип данных char, то следует помнить, что этот тип может на самом деле быть как signed char, так и unsigned char. Если необходим строго определенный вариант, то это нужно явно декларировать.

 

Выравнивание данных

 

Выравнивание (alignment) соответствует размещению порции данных в памяти. Говорят, что переменная имеет естественное выравнивание (naturally aligned), если она находится в памяти по адресу, значение которого кратно размеру этой переменной. Например, переменная 32-разрядного типа данных имеет естественное выравнивание, если она находится в памяти по адресу, кратному 4 байт (т.е. два младших бита адреса равны нулю). Таким образом, структура данные размером 2n байт должна храниться в памяти по адресу, младшие n битов которого равны нулю.

Для некоторых аппаратных платформ существуют строгие требования относительно выравнивания данных. На некоторых системах, обычно RISC, загрузка неправильно выровненных данных приводит к генерации системного прерывания (trap), ошибки, которую можно обработать. На других системах с неестественно выравниваемыми данными можно работать, но это приводит к уменьшению производительности. При написании переносимого кода необходимо предотвращать проблемы, связанные с выравниванием, а данные всех типов должны иметь естественное выравнивание.

 

Как избежать проблем с выравниванием

Компилятор обычно предотвращает проблемы, связанные с выравниванием, путем естественного выравнивания всех типов данных. На самом деле, разработчики ядра обычно не должны заниматься проблемами, связанными с выравниванием, об этом должны заботиться разработчики компилятора gcc. Однако такие проблемы все же могут возникать, когда разработчику приходится выполнять операции с указателями и осуществлять доступ к данным, не учитывая того, как компилятор выполняет операции доступа к данным.

Доступ к адресу памяти, для которого выполнено выравнивание, через преобразованный указатель на тип данных большего размера может привести к проблемам выравнивания (для разных аппаратных платформ это может проявляться по-разному). Следующий код может привести к указанной проблеме.

char dog[10];

char *p = &dog[1];

unsigned long l = *(unsigned long*)p;

В этом примере указатель на данные типа unsigned char используется, как указатель на тип unsigned long, что может привести к тому, что 32-разрядное значение типа unsigned long будет считываться из памяти по адресу, не кратному четырем.

Если вы думаете: "Зачем мне это может быть нужно?", то вы, скорее всего, правы. Тем не менее, если мы такое сделали только что, то такое можно сделать и кто-нибудь еще, поэтому необходимо быть внимательными. Примеры, которые встречаются в реальной жизни, не обязательно будут так же очевидны.

 

Выравнивание нестандартных типов данных

Как уже указывалось, при выравнивании адрес стандартного типа данных должен быть кратным размеру этого типа. Нестандартные (сложные) типы данных подчиняются следующим правилам выравнивания.

• Выравнивание массива выполняется так же, как и выравнивание типа данных первого элемента (все остальные элементы будут корректно выровнены автоматически).

• Выравнивание объединения (union) соответствует выравниванию самого большого, по размеру, типа данных из тех, которые включены в объединение.

• Выравнивание структуры соответствует выравниванию самого большого, по размеру, типа данных среди типов всех полей структуры.

В структурах также могут использоваться различные способы заполнения (padding).

 

Заполнение структур

Структуры заполняются таким образом, чтобы каждый ее элемент имел естественное выравнивание. Например, рассмотрим следующую структуру данных на 32- разрядной машине.

struct animal_struct {

 char           dog; /* 1 байт */

 unsigned long  cat; /* 4 байт */

 unsigned short pig; /* 2 байт */

 char           fox; /* 1 байт */

};

Эта структура данных в памяти выглядит не так, что связано с необходимостью естественного выравнивания. В памяти компилятор создает структуру данных, которая похожа на следующую.

struct animal_struct {

 char dog;           /* 1 байт */

 u8 __pad0[3];       /* 3 байт */

 unsigned long cat;  /* 4 байт */

 unsigned short pig; /* 2 байт */

 char fox;           /* 1 байт */

 u8 __pad1;          /* 1 байт */

};

Переменные заполнения вводятся для того, чтобы обеспечить естественное выравнивание всех элементов структуры. Первая переменная заполнения вводит дополнительные затраты памяти для того, чтобы разместить поле cat на границе 4-байтового адреса. Вторая переменная используется для выравнивания размера самой структуры. Дополнительный байт гарантирует, что размер структуры будет кратен четырем байтам и что каждый элемент массива таких структур будет иметь естественное выравнивание.

Следует обратить внимание, что выражение sizeof (foo_struct) равно значению 12 для любого экземпляра этой структуры на большинстве 32-разрядных аппаратных платформ. Компилятор языка С автоматически добавляет элементы заполнения, чтобы гарантировать необходимое выравнивание.

Часто имеется возможность переставить поля структуры так, чтобы избежать необходимости заполнения. Это позволяет получить правильно выровненные данные без введения дополнительных элементов заполнения и, соответственно, структуру меньшего размера.

struct animal struct {

 unsigned long  cat; /* 4 байта */

 unsigned short pig; /* 2 байта */

 char           dog; /* 1 байт */

 char           fox; /* 1 байт */

};

Эта структура данных имеет размер 8 байт. Однако не всегда существует возможность перестановки элементов структуры местами и изменения определения структуры. Например, если структура поставляется как часть стандарта, или уже используется в существующем коде, то порядок следования полей менять нельзя. Иногда, по некоторым причинам, может потребоваться специальный порядок следования полей структуры, например специальное выравнивание переменных для оптимизации попадания в кэш. Заметим, что, согласно стандарту ANSI С, компилятор никогда не должен менять порядок следования полей в структурах данных — этим правом обладает только программист.

Разработчики ядра должны учитывать особенности заполнения при обмене структурами данных: передача структур по сети или непосредственное сохранение на диск, потому что необходимое заполнение может быть разным для различных аппаратных платформ. Это одна из причин, по которой в языке программирования С нет оператора сравнения структур. Память, которая используется для заполнения структур данных, может содержать случайную информацию, что делает невозможным побайтовое сравнение структур. Разработчики языка, программирования С правильно сделали, что оставили решение задачи сравнения структур на усмотрение программиста, который может создавать свои функции сравнения в каждом конкретном случае, чтобы использовать особенности построения конкретных структур.

 

Порядок следования байтов

 

Порядок следования байтов (byte ordering) — это порядок, согласно которому байты расположены в машинном слове. Для разных процессоров может использоваться один из двух типов нумерации байтов в машинном слове: наименее значимый (самый младший) байт является либо самым первым (самым левым, left-most), либо самым последним (самым правым, right-most) в слове. Порядок байтов называется обратным (big-endian), если наиболее значимый (самый старший) байт хранится первым, а за ним идут байты в порядке убывания значимости. Порядок байтов называется прямым (little-endian), если наименее значимый (самый младший) байт хранится первым, а за ним следуют байты в порядке возрастания значимости.

Даже не пытайтесь основываться на каких-либо предположениях о порядке следования байтов при написании кода ядра (конечно, если код не предназначен для какой-либо конкретной аппаратной платформы). Операционная система Linux поддерживает аппаратные платформы с обоими порядками байтов, включая и те машины, на которых используемый порядок байтов можно сконфигурировать на этапе загрузки системы, а общий код должен быть совместим с любым порядком байтов.

На рис. 19.1 показан пример обратного порядка следования байтов, а на рис. 19.2 — прямого порядка следования байтов.

Рис. 19.1. Обратный (big-endian) порядок следования байтов

Рис. 19.2. Прямей (little-endian) порядок следования байтов

Аппаратная платформа i386 использует прямой (little-endian) порядок байтов. Большинство других аппаратных платформ обычно использует обратный (big-endian) порядок.

Рассмотрим, что эти типы кодирования обозначают на практике и как выглядит двоичное представление числа 1027, которое хранится в виде четырехбайтового целочисленного типа данных.

00000000 00000000 00000100 00000011

Внутренние представления этого числа в памяти при использовании прямого и обратного порядка байтов отличаются, как это показано в табл. 19.3.

Таблица 19.3. Расположение данных в памяти для разных порядков следования байтов

Адрес Обратный порядок Прямой порядок
0 00000000 00000011
1 00000000 00000100
2 00000100 00000000
3 00000011 00000000

Обратите внимание на то, что для аппаратной платформы с обратным порядком байтов самый старший байт записывается в самый минимальный адрес памяти.

И наконец, еще один пример — фрагмент кода, который позволяет определить порядок байтов для той аппаратной платформы, на которой он выполняется.

int x = 1;

if (*(char*)&x == 1)

 /* прямой порядок */

else

 /* обратный порядок */

Этот пример работает как в ядре, так и в пространстве пользователя.

 

История терминов big-endian и little-endian

Термины big-endian и little-endian заимствованы из сатирического романа Джонатана Свифта "Путешествие Гулливера", который был издан в 1726 году. В этом романс наиболее важной политической проблемой народа лилипутов была проблема, с какого конца следует разбивать яйцо: с тупого (big) или острого (little). Тех, кто предпочитал тупой конец называли "тупоконечниками" (big-endian), тех же, кто предпочитал острый конец, называли "остроконечниками" (little-endian).

Аналогия между дебатами лилипутов и спорами о том, какой порядок байтов лучше, говорит о том, что это вопрос больше политический, чем технический.

 

Порядок байтов в ядре

Для каждой аппаратной платформы, которая поддерживается ядром Linux, в файле определена одна из двух констант __BIG_ENDIAN или __LITTLE_ENDIAN, в соответствии с используемым порядком байтов.

В этот заголовочный файл также включаются макросы из каталога include/linux/byteorder/, которые помогают конвертировать один порядок байтов в другой. Ниже показаны наиболее часто используемые макросы.

u32 __cpu_to_be32(u32); /* преобразовать порядок байтов текущего

                           процессора в порядок big-endian */

u32 __cpu_to_le32(u32); /* преобразовать порядок байтов текущего

                           процессора в порядок little-endian */

u32 __be32_to_cpu(u32); /* преобразовать порядок байтов big-endian в

                           порядок байтов текущего процессора */

u32 __lе32_to_cpu(u32); /* преобразовать порядок байтов little-endian

                           в порядок байтов текущего процессора */

Эти макросы выполняют преобразование одного порядка байтов в другой. В случае когда порядки байтов, между которыми выполняется преобразование, одинаковы (например, если выполняется преобразование в обратный порядку байтов и процессор тоже использует такой же порядок), то эти макросы не делают ничего. В противном случае возвращается преобразованное значение.

 

Таймер

Никогда нельзя привязываться к какой-либо конкретной частоте генерации прерывания системного таймера и, соответственно, к тому, сколько раз в секунду изменяется переменная jiffies. Всегда необходимо использовать константу HZ, чтобы корректно определять интервалы времени. Это очень важно, потому что значение частоты системного таймера может отличаться не только для разных аппаратных платформ, но и для одной аппаратной платформы при использовании разных версий ядра.

Например, константа HZ для аппаратной платформы x86 сейчас равна 1000. Это значит, что прерывание таймера возникает 1000 раз в секунду, или каждую миллисекунду. Однако до серии ядер 2.6 для аппаратной платформы x86 значение константы HZ было равно 100. Для разных аппаратных платформ эти значения отличаются: для аппаратной платформы alpha константа HZ равна 1024, а для платформы ARM — 100.

Никогда нельзя сравнивать значение переменной jiffies с числом, таким как 1000, и думать, что это всегда будет означать одно и то же. Для получения интервалов времени необходимо всегда умножать или делить на константу HZ, как в следующем примере.

HZ /* одна секунда */

(2*HZ) /* две секунды */

(HZ/2) /* полсекунды */

(HZ/100) /* 10 мс */

(2*HZ/100) /* 20 мс */

Константа HZ определена в файле . Об этом подробно рассказано в главе 10, "Таймеры и управление временем".

 

Размер страницы памяти

При работе со страницами памяти никогда нельзя привязываться к конкретному размеру страницы. Программисты, которые разрабатывают для аппаратной платформы x86, часто делают ошибку, считая, что размер страницы всегда равен 4 Кбайта. Хотя это справедливо для платформы x86, для других аппаратных платформ размер станицы может быть другим. Некоторые аппаратные платформы поддерживают несколько размеров страниц! В табл. 19.1 приведен список размеров страниц памяти для всех поддерживаемых аппаратных платформ.

Таблица 19.4. Размеры страниц памяти для разных аппаратных платформ

Аппаратная платформа Значение PAGE_SHIFT Значение PAGE_SIZE
alpha 13 8 Кбайт
arm 12, 14, 15 4 Кбайт, 16 Кбайт, 32 Кбайт
cris 13 8 Кбайт
h8300 12 4 Кбайт
i386 12 4 Кбайт
ia64 12, 13, 14, 16 4 Кбайт, 8 Кбайт, 32 Кбайт, 64 Кбайт
m68k 12, 13 4 Кбайт, 8 Кбайт
m86knommu 12 4 Кбайт
mips 12 4 Кбайт
mips64 12 4 Кбайт
parisc 12 4 Кбайт
ppc 12 4 Кбайт
ppc64 12 4 Кбайт
s390 12 4 Кбайт
sh 12 4 Кбайт
spare 12,13 4 Кбайт, 8 Кбайт
sparc64 13 8 Кбайт
v850 12 4 Кбайт
x86_64 12 4 Кбайт

При работе со страницами памяти необходимо использовать константу PAGE_SIZE, которая содержит размер страницы памяти в байтах.

Значение макроса PAGE_SHIFT — это количество битов, на которое необходимо сдвинуть влево значение адреса, чтобы получить номер соответствующей страницы памяти. Например, для аппаратной платформы x86, для которой размер страницы равен 4 Кбайт, макрос PAGE_SIZE равен 4096, а макрос PAGE_SHIFT — 12. Эти значения содержатся в заголовочном файле .

 

Порядок выполнения операций процессором

Вспомните из материала главы 9, "Средства синхронизации в ядре", что для различных аппаратных платформ процессоры в разной степени изменяют порядок выполнения программных инструкций. Для некоторых процессоров порядок выполнения операций строго соблюдается, запись данных в память и считывание данных из памяти выполняются в строго указанном в программе порядке. Другие процессоры имеют ослабленные требования к порядку выполнения операций считывания и записи данных и могут изменять порядок выполнения этих операций с целью оптимизации.

Если код зависит от порядка выполнения операций чтения-записи данных, то необходимо гарантировать, что даже процессор с самыми слабыми ограничениями на порядок выполнения чтения-записи будет выполнять эти операции в правильном порядке. Это делается с помощью соответствующих барьеров, таких как rmb() и wmb(). Более подробная информация приведена в главе 9, "Средства синхронизации в ядре".

 

Многопроцессорность, преемптивность и верхняя память

Может показаться неправильным включать поддержку симметричной многопроцессорности, возможность вытеснения процессов в режиме ядра и работу с верхней памятью в вопросы переносимости. В конце концов, это не особенности аппаратной платформы, которые влияют на операционную систему, а функции ядра Linux, которые по многом не зависят от аппаратной платформы. Тем не менее для этих функций существуют важные конфигурационные параметры, которые необходимо учитывать при разработке кода. Программировать всегда необходимо под SMP, с поддержкой преемптивности и с использованием верхней памяти, чтобы код был безопасным всегда, при любых конфигурациях. Необходимо всегда соблюдать следующие правила.

• Всегда необходимо учитывать, что код может выполняться на SMP-системе и использовать соответствующие блокировки.

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

• Всегда необходимо учитывать, что код может выполняться на системе с поддержкой верхней памяти (непостоянно отображаемая память) и при необходимости использовать функцию kmap().

 

Пару слов о переносимости

Если говорить коротко, то написание переносимого, ясного и красивого кода подразумевает следующие два момента.

• Код необходимо разрабатывать с учетом самого общего сценария: следует предполагать, что все, что может случиться, обязательно случится, и принять на этот счет все возможные меры.

• Всегда необходимо все подводить под наибольший общий знаменатель: нельзя полагаться на то, что будут доступны все возможности ядра, следует опираться только на минимум возможностей, которые доступны всем аппаратным платформам.

Написание переносимого кода требует строгого учета многих факторов: размер машинного слова, размеры типов данных, выравнивание в памяти, порядок байтов, размер страницы, изменение порядка операций процессора и т.д. В большинстве случаев при программировании ядра следует гарантировать, что типы данных используются правильно. Тем не менее время от времени все равно всплывают проблемы, связанные с особенностью той или другой аппаратной платформы. Важно понимать проблемы, связанные с переносимостью, и всегда писать четкий и переносимый код ядра.