ARMv7NEON

Невозможное возможно, или как использовать NEON в модуле ядра

ARMv7NEONШироко распространенным мнением является то, что работа с плавающей точкой в режиме ядра (модуле ядра) является невозможной. Строго говоря, работа с ними все же нежелательна по нескольким причинам, которые рассмотрим чуть ниже, но иногда из-за архитектурных просчетов или другим причинам необходимо обработать данные на уровне ядра. В статье рассмотрим простейший пример использования операций с плавающей точкой и сопроцессора 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

Вычислительный модуль

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

В заголовочном файле 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. При вызове команды вы увидите сообщения с увеличивающимся счетчиком (в зависимости от настройки ОС непосредственно в терминале и/или в системных логах).