Широко распространенным мнением является то, что работа с плавающей точкой в режиме ядра (модуле ядра) является невозможной. Строго говоря, работа с ними все же нежелательна по нескольким причинам, которые рассмотрим чуть ниже, но иногда из-за архитектурных просчетов или другим причинам необходимо обработать данные на уровне ядра. В статье рассмотрим простейший пример использования операций с плавающей точкой и сопроцессора NEON в модуле ядра Linux. Предполагается, что читатель знает основы создания модулей ядра.
Почему нельзя? Какие ограничения?
Общепринятый запрет на использование VPF и Neon в режиме ядра основывается на следующем ограничении. Из соображений производительности в режиме ядра при переключения контекста не происходит сохранения (и, соответственно, восстановления) содержимого регистров VPF/NEON, в связи с этим использование этого функционала сильно затруднено. Кроме того, использование чисел с плавающей точкой в режиме ядра сильно ограничено и по причине того, что большинство математических библиотек работают только в пространстве пользователя (в том числе math.h) и сколько-то нибудь сложная обработка данных в ядре затруднена.
Кроме того, при использовании накладывается еще несколько ограничений:
- Код NEON/VFP не разрешен в контексте прерывания;
- Код NEON/VFP не может использовать засыпание (sleep);
- Код NEON/VFP выполняется с отключенной вытесняющей многозадачностью;
- Код NEON/VFP должен быть изолирован от остального кода на уровне компиляции.
И, наконец, для использования рассматриваемого функционала необходимо явно активировать и отключать модули NEON/VFP функциями kernel_neon_begin() и kernel_neon_end(), соответственно.
Создание модуля, выполняющего вычисления с плавающей точкой в NEON
В примере рассмотрен простейший модуль, производящий инкремент счетчика на 10 с помощью инструкции NEON при каждом чтении.
В связи с тем, что код NEON должен быть изолирован, необходима раздельная компиляция основного и вычислительного модуля. Поэтому структура проекта является следующей (Жирным выделены директории, курсивом — файлы):
- TestMod
- Makefile
- NeonMod
- Makefile
- neon_operation.c
- neon_operation.h
- MainMod
- Makefile
- main_mod.c
- NeonMod
Вычислительный модуль
Вычислительный модуль содержит вне функции, необходимые для вычислений. Стоит отметить, что хранение и передача данных с плавающей точкой вне этого модуля возможна только по указателю.
В заголовочном файле neon_operation.h содержится только объявление функций, которые должны быть доступны извне. В нашем случае neon_add.
#ifndef NEON_OPERATION_H #define NEON_OPERATION_H extern void neon_add(long *a, long *b); #endif
В файле neon_operation.c реализованы основные функции для реакции на загрузку/выгрузку модуля и, непосредственно, функция neon_add выполняющая инкремент операнда a на b с помощью инструкции NEON vadd_u64.
#include <linux/init.h> #include <linux/module.h> #include <linux/kernel.h> #include <linux/types.h> #include <arm_neon.h> #include "neon_operation.h" MODULE_LICENSE("GPL"); MODULE_AUTHOR("Sinitca Aleksandr"); MODULE_DESCRIPTION("Code for neon"); MODULE_VERSION("0.01"); extern void neon_add(long *a, long *b) { uint64x1_t oper_1 = *a; uint64x1_t oper_2 = *b; *a = vadd_u64 (oper_1, oper_2); return; } EXPORT_SYMBOL( neon_add ); static int __init mod_neon_init( void ) { printk( "+ module NEON start!\n" ); return 0; } static void __exit mod_neon_exit( void ) { printk( "+ module NEON unloaded!\n" ); } module_init( mod_neon_init ); module_exit( mod_neon_exit );
В строке 18 вызывается инструкция NEON.
Обратите внимание на строку 22, в которой производится экспорт символа для того, чтобы была возможность вызвать функцию из другого модуля.
Сборка модуля
Основная особенность сборки такого модуля содержится в Makefile:
MODULE_NAME := neon_mod KERNEL_SOURCE := <path to kernel> obj-m += ${MODULE_NAME}.o PWD := $(shell pwd) EXTRA_CFLAGS += -ffreestanding -mfloat-abi=softfp -mfpu=neon all: ${MAKE} -C ${KERNEL_SOURCE} SUBDIRS=${PWD} CFLAGS_MODULE=-Werror EXTRA_CFLAGS='$(EXTRA_CFLAGS)' ARCH=arm CROSS_COMPILE=/usr/bin/arm-linux-gnueabi- modules clean: ${MAKE} -C ${KERNEL_SOURCE} SUBDIRS=${PWD} clean
Обратите внимание: в строке 2 необходимо подставить ваш путь до исходников ядра.
Основным отличием от обычной сборки модуля является наличие дополнительных флагов описанных в строке 6.
Основной модуль
Функционал основного не должен вызывать вопросов, поэтому не будет подробно его рассматривать, однако рассмотрим интересующий нас кусок кода, отвечающий за вызов функции с операцией NEON:
... #include <asm/neon.h> #include "../NeonMod/neon_operation.h" ... static long number = 0; ... static ssize_t dev_read( struct file * file, char * buf, size_t count, loff_t *ppos ) { ... long bias = 10; kernel_neon_begin(); neon_add(&number, &bias); kernel_neon_end(); ... }
Тут в строке 2 импортируется необходимый для работы с neon заголовочный файл, содержащий функционал активации и отключения, в строке 3 импортируется заголовочный файл нашей библиотеки, выполняющей функции вычисления.
В строках 12 и 14 производится активация и отключение блока NEON/VFP, а в строке 13 вызывается непосредственно функция вычисления.
Полностью код модуля может выглядеть следующим образом:
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/types.h>
#include <asm/neon.h>
#include "../NeonMod/neon_operation.h"
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Sinitca Aleksandr");
MODULE_DESCRIPTION("A simple test Linux module with NEON.");
MODULE_VERSION("0.01");
static char *hello_str = "Hello, world!\n";
static long number = 0;
static int major = 0;
module_param( major, int, S_IRUGO );
#define EOK 0
static int device_open = 0;
/* Called when a process tries to write to our device */
static ssize_t dev_write(struct file *flip, const char *buffer, size_t len, loff_t *offset) {
/* This is a read-only device */
printk(KERN_ALERT "This operation is not supported.\n");
return -EINVAL;
}
static ssize_t dev_read( struct file * file, char * buf,
size_t count, loff_t *ppos ) {
int len = strlen( hello_str );
long bias = 10;
kernel_neon_begin();
neon_add(&number, &bias);
kernel_neon_end();
printk( KERN_INFO "=== read : %ld\n", number );
if( count < len ) return -EINVAL;
if( *ppos != 0 ) {
printk( KERN_INFO "=== read return : 0\n" ); // EOF
return 0;
}
if( copy_to_user( buf, &number, 4 ) ) return -EINVAL;
*ppos = len;
printk( KERN_INFO "=== read return : %d\n", len );
return len;
}
static int dev_open( struct inode *n, struct file *f ) {
if( device_open ) return -EBUSY;
device_open++;
return EOK;
}
static int dev_release( struct inode *n, struct file *f ) {
device_open--;
return EOK;
}
static const struct file_operations dev_fops = {
.owner = THIS_MODULE,
.open = dev_open,
.release = dev_release,
.read = dev_read,
};
#define DEVICE_FIRST 0
#define DEVICE_COUNT 3
#define MODNAME "main-module-test"
static struct cdev hcdev;
static int __init main_test_module_init(void) {
int ret;
dev_t dev;
if( major != 0 ) {
dev = MKDEV( major, DEVICE_FIRST );
ret = register_chrdev_region( dev, DEVICE_COUNT, MODNAME );
}
else {
ret = alloc_chrdev_region( &dev, DEVICE_FIRST, DEVICE_COUNT, MODNAME );
major = MAJOR( dev );
}
if( ret < 0 ) {
printk( KERN_ERR "=== Can not register char device region\n" );
goto err;
}
cdev_init( &hcdev, &dev_fops );
hcdev.owner = THIS_MODULE;
ret = cdev_add( &hcdev, dev, DEVICE_COUNT );
if( ret < 0 ) {
unregister_chrdev_region( MKDEV( major, DEVICE_FIRST ), DEVICE_COUNT );
printk( KERN_ERR "=== Can not add char device\n" );
goto err;
}
printk( KERN_INFO "=========== module installed %d:%d =========\n",
MAJOR( dev ), MINOR( dev ) );
err:
return ret;
}
static void __exit main_test_module_exit(void) {
cdev_del( &hcdev );
unregister_chrdev_region( MKDEV( major, DEVICE_FIRST ), DEVICE_COUNT );
printk( KERN_INFO "=============== module removed =============\n" );
}
module_init(main_test_module_init);
module_exit(main_test_module_exit);
Сборка основного модуля полностью аналогично обычному способу:
MODULE_NAME := main_module_test KERNEL_SOURCE := <path to kernel> PWD := $(shell pwd) obj-m := ${MODULE_NAME}.o all: ${MAKE} -C ${KERNEL_SOURCE} SUBDIRS=${PWD} ARCH=arm CROSS_COMPILE=/usr/bin/arm-linux-gnueabi- modules clean: ${MAKE} -C ${KERNEL_SOURCE} SUBDIRS=${PWD} clean
Тестирование модулей
Для тестирования модуля необходимо выполнить их установку на устройство и поочередно загрузить сначала модуль neon_mod, затем main_module_test. В противном случае будет получена ошибка об отсутствии символа.
После загрузки основного модуля необходимо создать для него устройство и произвести из него чтение, например, командой cat. При вызове команды вы увидите сообщения с увеличивающимся счетчиком (в зависимости от настройки ОС непосредственно в терминале и/или в системных логах).