Нередко у программистов, пишущих драйверы, возникают некоторые трудности с обменом данными в 64-битном формате. Давайте разберём некоторые ситуации.
На самом деле под ширмой работы с 64-битными регистрами скрыто несколько проблем.
Во-первых, обслуживание оборудования, в котором применяются 64-битные регистры, должно быть доступно как на 64-битных ядрах, так и на 32-битных.
Во-вторых, не всякие 64-битные регистры можно записывать за одну команду, так как связано это с нюансами реализации в железе.
В-третьих, иногда приходится сравнивать 64-битное число как два 32-битных, снова же в связи с установленными стандартами и протоколами.
Посмотрим, как избежать изобретения велосипеда, и сделать код чище, лаконичнее и красивее.
Конечно же многие сталкивались с известными командами записи и чтения регистров оборудования, такими как
Они охватывают традиционные байтовые, двухбайтовые и четырёхбайтовые операции с регистрами. Что же делать, когда необходимо записать или считать сразу восемь байт?
На некоторых 64-битных архитектурах на помощь приходят команды
Традиционно программист для охватывания 32-битных и 64-битных платформ сочиняет нечто подобное в своём коде:
И соответственно для чтения.
И вот представьте, что таких копий десятки, если не сотни. Во избежание изобретений велосипеда в ядро внесли специальные файлы include/linux/io-64-nonatomic-hi-lo.h и include/linux/io-64-nonatomic-lo-hi.h.
Какие особенности у этих файлов:
Соответственно, если у нас оборудование, которое понимает только обращения вида 4 + 4, то мы применяем
В некоторых случаях возникает необходимость сравнить 64-битное число в варианте 8 с числом в варианте 4 + 4.
Решение прямо в лоб:
Ну вы поняли, как много будет кода на 32-битной архитектуре.
Можно разбить это дело на такие два сравнения:
Вроде легче, но… есть одна проблема. В ядре встречаются типы, которые непосредственно зависят от конкретной платформы, а именно: phys_addr_t, resource_size_t, dma_addr_t и им подобные.
Как вы думаете, что будет, если мы напишем такой код:
На 64-битной архитектуре, понятное дело, всё будет прекрасно. А вот на 32-битной нам пожалуется компилятор на сдвиг
Для того, чтобы и компилятор был счастлив, и пользователь особо себя не утруждал, в ядро добавили следующие макросы:
В результате сравнение будет выглядеть так:
Не забывайте, что данные операции не атомарны!
Вместо вступления
На самом деле под ширмой работы с 64-битными регистрами скрыто несколько проблем.
Во-первых, обслуживание оборудования, в котором применяются 64-битные регистры, должно быть доступно как на 64-битных ядрах, так и на 32-битных.
Во-вторых, не всякие 64-битные регистры можно записывать за одну команду, так как связано это с нюансами реализации в железе.
В-третьих, иногда приходится сравнивать 64-битное число как два 32-битных, снова же в связи с установленными стандартами и протоколами.
Посмотрим, как избежать изобретения велосипеда, и сделать код чище, лаконичнее и красивее.
Обмен 64-битными данными
Конечно же многие сталкивались с известными командами записи и чтения регистров оборудования, такими как
writel()
, readl()
.Они охватывают традиционные байтовые, двухбайтовые и четырёхбайтовые операции с регистрами. Что же делать, когда необходимо записать или считать сразу восемь байт?
На некоторых 64-битных архитектурах на помощь приходят команды
writeq()
и readq()
.Традиционно программист для охватывания 32-битных и 64-битных платформ сочиняет нечто подобное в своём коде:
static inline void writeq(u64 val, void __iomem *addr)
{
writel(val, addr);
writel(val >> 32, addr + 4);
}
И соответственно для чтения.
static inline u64 readq(void __iomem *addr)
{
u32 low, high;
low = readl(addr);
high = readl(addr + 4);
return low + ((u64)high << 32);
}
И вот представьте, что таких копий десятки, если не сотни. Во избежание изобретений велосипеда в ядро внесли специальные файлы include/linux/io-64-nonatomic-hi-lo.h и include/linux/io-64-nonatomic-lo-hi.h.
Какие особенности у этих файлов:
- Определённые функции выполняют обращения не атомарно.
- В связи с вышесказанным в ядре два файла: для записи младший-старший и наоборот — старший-младший.
- Оба файла объявляют
writeq()
иreadq()
по принципу «кто первый встал, того и тапки». Вначале проверяется не определены ли они уже? Если нет, тогда переопределяем. Соответственно порядок включения заголовочных файлов имеет значение. - Обращения, что очевидно, выполнены по формуле 8 = 4 + 4.
Соответственно, если у нас оборудование, которое понимает только обращения вида 4 + 4, то мы применяем
lo_hi_writeq()
и lo_hi_readq()
или hi_lo_writeq()
и hi_lo_readq()
. При идеальном случае просто writeq()
и readq()
.Сравнение 64-битных чисел
В некоторых случаях возникает необходимость сравнить 64-битное число в варианте 8 с числом в варианте 4 + 4.
Решение прямо в лоб:
u32 hi = Y, lo = Z;
u64 value = X, tmp;
tmp = (Y << 32) | X;
return value == tmp;
Ну вы поняли, как много будет кода на 32-битной архитектуре.
Можно разбить это дело на такие два сравнения:
return (value >> 32) == hi && (u32)value == lo;
Вроде легче, но… есть одна проблема. В ядре встречаются типы, которые непосредственно зависят от конкретной платформы, а именно: phys_addr_t, resource_size_t, dma_addr_t и им подобные.
Как вы думаете, что будет, если мы напишем такой код:
u32 hi = Y, lo = Z;
resource_size_t value = X;
return (value >> 32) == hi && (u32)value == lo;
На 64-битной архитектуре, понятное дело, всё будет прекрасно. А вот на 32-битной нам пожалуется компилятор на сдвиг
value >> 32
.Для того, чтобы и компилятор был счастлив, и пользователь особо себя не утруждал, в ядро добавили следующие макросы:
lower_32_bits()
иupper_32_bits()
для соответственно младших и старших 4 байт.В результате сравнение будет выглядеть так:
return upper_32_bits(value) == hi && lower_32_bits(value) == lo;
Не забывайте, что данные операции не атомарны!
AterCattus
Напомнило это и это.