Операционная система реального времени Xenomai 4

в 6:15, , рубрики: EVL, real time, Xenomai, осрв

В данной статье подробно рассмотрим, как собрать ядро, поддерживающее EVL core, и библиотеку, реализующую пользовательский API для этого ядра. А также разберем некоторые аспекты реализации драйвера устройства и приложения под Xenomai 4.

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

Мы будем работать с Xenomai версии 4. Xenomai 4 имеет архитектуру с двумя ядрами. Первое ядро Linux: для задач, отличных от реального времени, и ядро Xenomai: для задач реального времени. Ядро Linux и ядро реального времени работают практически асинхронно, оба выполняют свой собственный набор задач, всегда отдавая последнему приоритет над первым. Для осуществления доступа к основным сервисам реального времени в проекте Xenomai предусмотрена библиотека С, известная как libevl.

Xenomai поддерживает множество архитектур, таких как PowerPC, Blackfin, ARM, x86, x86_64 др. В данной статье мы используем компьютер c архитектурой x86_64 (Процессор: 12th Gen Intel® Core™ i5-12400 × 12, память: 32,0 ГиБ), операционной системой Debian GNU/Linux 12 (bookworm).

Установка ядра EVL

В статье не будем приводить информацию по установке ОС Debian GNU/Linux 12 (bookworm), можете найти информацию по установке на сайте www.debian.org.

Установка:

$ sudo tar -xf linux-evl-v6.10-evl-rebase.tar.gz -C /usr/src

Для сборки понадобятся следующие пакеты: ncurses-dev, fakeroot, build-essential, dpkg.

$ sudo apt update
$ sudo apt install ncurses-dev fakeroot build-essential dpkg
  • Создать конфигурацию ядра .config на основе текущей конфигурации;

$ cd /usr/src/linux-evl-v6.10-evl-rebase
$ sudo make localmodconfig
  • Настройка параметров ядра EVL в файле .config;

Название

По умолчанию

Назначение

CONFIG_EVL

N

Включить ядро EVL (установить y)

CONFIG_EVL_SCHED_QUOTA

N

Включить политику планирования на основе квот (установить y)

CONFIG_EVL_SCHED_TP

N

Включить политику планирования с разделением по времени (установить y)

CONFIG_EVL_SCHED_TP_NR_PART

4

Количество временных разделов для CONFIG_EVL_SCHED_TP (оставить по умолчанию)

CONFIG_EVL_HIGHT_PERCPU_CONCURRENCY

N

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

CONFIG_EVL_RUNSTATS

Y

Сбор статистики времени выполнения о потоках (оставить по умолчанию)

CONFIG_EVL_COREMEM_SIZE

2048

Размер кучи памяти ядра (в килобайтах) (оставить по умолчанию)

CONFIG_EVL_NR_THREADS

256

Максимальное количество потоков EVL

CONFIG_EVL_NR_MONITORS

512

Максимальное количество мониторов EVL (мьютексы + семафоры + флаги + события) (оставить по умолчанию)

CONFIG_EVL_NR_CLOCKS

8

Максимальное количество часов EVL (оставить по умолчанию)

CONFIG_EVL_NR_XBUFS

16

Максимальное количество перекрестных буферов (оставить по умолчанию)

CONFIG_EVL_NR_PROXIES

64

Максимальное количество EVL прокси (оставить по умолчанию)

CONFIG_EVL_OBSERVABLES

64

Максимальное количество наблюдаемых EVL (не включает потоки) (оставить по умолчанию)

CONFIG_EVL_LATENCY_USER

0

Предварительно установленное значение математического ожидания основного таймера для пользовательских потоков (0 означает использовать предварительно откалиброванного значения) (оставить по умолчанию)

CONFIG_EVL_LATENCY_KERNEL

0

Предварительно установленное значение математического ожидания таймера для потоков ядра (0 означает использование предварительно откалиброванного значения) (оставить по умолчанию)

CONFIG_EVL_LATENCY_IRQ

0

Предварительно установленное значение математического ожидания таймера для обработчиков прерываний (0 означает использование предварительно откалиброванного значения) (оставить по умолчанию)

CONFIG_EVL_DEBUG

N

Включить функции отладки (оставить по умолчанию)

CONFIG_EVL_DEBUG_CORE

N

Включить основные утверждения отладки (оставить по умолчанию)

CONFIG_EVL_DEBUG_MEMORY

N

Включить проверки отладки в core memory allocator — это параметр увеличивает значительные накладные расходы, включающие на показатели задержки (оставить по умолчанию)

CONFIG_EVL_DEBUG_WOLI

N

Включить контрольные точки с предупреждением о несогласованности при блокировке (оставить по умолчанию)

CONFIG_EVL_WATCHDOG

Y

Включить сторожевой таймер (оставить по умолчанию)

CONFIG_EVL_WATCHDOG_TIMEOUT

4

Значение таймаута сторожевого таймера (в секундах) (оставить по умолчанию)

CONFIG_GPIOLIB_OOB

n

Включить поддержку запросов на внеполосную обработку линий GPIO (оставить по умолчанию)

CONFIG_SPI_OOB, CONFIG_SPIDEV_OOB

n

Включить поддержку передач SPI в реальном времени (оставить по умолчанию)

  • Сборка ядра;

$ sudo make

Сборка ядра может занять значительное время.

  • Сборка deb пакетов ядра;

$ sudo make bindeb-pkg
  • Установка ядра;

$ cd ./..
$ sudo dpkg -i linux-libc-dev_6.1.97-0_amd64.deb
$ sudo dpkg -i linux-headers-6.1.97_6.1.97-0_amd64.deb
$ sudo dpkg -i linux-image-6.1.97_6.1.97-0_amd64.deb

Перезагрузите компьютер, при загрузке выберете ядро 6.1.97.

Установка библиотеки libevl

Установка:

$ sudo tar -xf libevl-master.tar.gz -C /usr/src

Для сборки библиотеки на понадобятся следующие пакеты: meson, ninja-build.

$ sudo apt -y install  meson  ninja-build
  • Сборка библиотеки;

Устанавливаем настройки meson

$ cd /usr/src/libevl-master
$ sudo meson setup -Duapi=/usr/src/linux-evl-v6.1.y-evl-rebase -Dbuildtype=release -Dprefix=/usr/lib/evl /usr/src/libevl-master/bin /usr/src/libevl-master
  • Установка библиотеки;

$ cd /usr/src/libevl-master/bin
$ sudo ninja install

Разработка EVL-драйверов

EVL-драйвер представляет собой обычный драйвер Linux, такой же как драйвер символьного устройства или драйвер протокола сокетов, который также реализует набор операций ввода-вывода с гарантированными временными параметрами, описанных в дескрипторе файловой операции (struct file_operations). Эти запросы ввода-вывода с гарантированными временными параметрами доступны только для потоков EVL.

struct file_operations {
	... 
    ssize_t (*oob_read) (struct file *, char __user *, size_t); 	
    ssize_t (*oob_write) (struct file *, const char __user *, size_t); 	
    long (*oob_ioctl) (struct file *, unsigned int, unsigned long);
	long (*compat_oob_ioctl) (struct file *, unsigned int, unsigned long);
	__poll_t (*oob_poll) (struct file *, struct oob_poll_wait *);
    ...
} __randomize_layout;
Схема обработки запросов

Схема обработки запросов

Когда приложение выполняет системные вызовы open() или close() с файловым дескриптором, ссылающимся на файл, управляемый драйвером EVL, запрос обычно проходит через переключатель виртуальной файловой системы (VFS) как обычно, попадая в .open() и .release() обработчики, определенные драйвером в его struct file_operations дескрипторе. Тоже самое относится к mmap(), ioctl(), read(), write().

Когда приложение выполняет системные вызовы oob_read(), oob_write() или oob_ioctl() через библиотеку EVL, направляется вызов ядру EVL (вместо VFS), которое в свою очередь, запускает соответствующие обработчики, определенные дескриптором struct file_operations драйвера: т. е. oob_read(), oob_write() и oob_ioctl(). Эти обработчики должны использовать API ядра EVL или основные службы ядра.

EVL-драйвер был разработан для PXIe модуля MP-2861 многофункциональной измерительной платформы. Модуль предназначен для приема дискретных сигналов по ГОСТ IEC 61131-2 и разовых команд по ГОСТ 18977-79, производства АО «ОКБ «Аэрокосмические системы».

Разберем некоторые аспекты написания EVL-драйвера. Драйвер представляет собой драйвер символьного устройства.

Структура файловых операций выглядит следующим образом:

static const struct file_operations dev_fops = {
        .owner	= THIS_MODULE,
        .open		= dev_open,
        .release	= dev_release,
    	.read		= dev_read,
        .oob_read	= oob_dev_read,
    	.write		= dev_write,
        .oob_write	= oob_dev_write,
        .llseek		= dev_lseek
};

Добавлены операции с гарантированными временными параметрами чтения oob_read и записи oob_write.

ssize_t oob_read(int efd, void *buf, size_t count)

Это строгий эквивалент стандартного вызова read() для отправки запроса с гарантированными временными параметрами драйверу EVL. Другими словами, oob_read() пытается считать count байт из файла дескриптора fd в буфер, начиная с buf, на этапе выполнения операции с гарантированными временными параметрами.

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

efd — дескриптор файла, полученный при открытии драйвера, работающего в реальном времени, которого мы хотим выполнить чтение.

buf — буфер для приема данных.

count — максимальное количество считываемых байт, которые должны поместиться в buf.

ssize_t oob_write(int efd, const void *buf, size_t count)

Это строгий эквивалент стандартного системного вызова write() для отправки запроса с гарантированными временными параметрами драйверу EVL. oob_write() пытается записать до count байт в файловый дескриптор fd из буфера, начинающегося с buf, на этапе выполнения операции с гарантированными временными параметрами.

efd — дескриптор файла, полученный при открытии драйвера, работающего в реальном времени, которого мы хотим выполнить чтение.

buf — буфер, содержащие данные, подлежащие записи.

count — количество байтов для записи.

Реализация функций oob практически ничем не отличается от обычных. Заголовочные файлы берутся из ядра со встроенным EVL ядром.

static ssize_t oob_dev_read(struct file *file, char __user *buf, size_t count)
{
	struct private_open_file *pof = file->private_data;
	struct evl_driver_priv *drv_access_local = pof->workprivate;
	int * srcbuff_local = pof->readbuff;

	memcpy_fromio(srcbuff_local, (drv_access_local->axibar + pof->dev_off), count);

	copy_to_user(buf, srcbuff_local, count);

	return count;
}

memcpy_fromio копирует блок данных из памяти ввода-вывода, copy_to_user копирует count байт данных в пользовательское пространство.

static ssize_t oob_dev_write(struct file *file, const char __user *buf, size_t count)
{
	struct private_open_file *pof = file->private_data;
	struct evl_driver_priv * drv_access_local = pof->workprivate;
	u32 mvv_local = 0;
	copy_from_user( &mvv_local, buf, sizeof(u32) );
	iowrite32(mvv_local, drv_access_local->axibar + pof->dev_off );
	return sizeof(u32);
}

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

Разработка EVL-приложений

Первое что хотелось бы отметить при разработке EVL-приложений это то что все взаимодействия с ядром EVL осуществляется через POSIX потоки pthread_t.

Объявляем поток:

pthread_t   		m_thread;           	///< Идентификатор потока
pthread_attr_t      m_attr;             	///< Атрибуты потока

Устанавливаем значения атрибутов по умолчанию:

pthread_attr_init(&m_attr);

Создаем поток:

int err = pthread_create(&m_thread, NULL, recieveThread, this);

Присоединяем вызывающий поток к ядру EVL:

int efd;
efd = evl_attach_self("/thread:%d", getpid());

При успешном присоединении вызывающего потока к ядру EVL данный поток должен быть в каталоге /dev/evl/thread

total 0
crw-rw---- 1 root root 246, 1 Jan 1 	1970 clone
crw-rw---- 1 root root 244, 0 Sep 19 	10:45 thread:2102

Устанавливаем атрибуты потока:

struct evl_sched_attrs attrs;
int ret;

attrs.sched_policy = SCHED_FIFO;
attrs.sched_priority = 1; /* [1-99] */

ret = evl_set_schedattr(efd, &attrs);

SCHED_FIFO распространенная политика реального времени «первым пришел — первым ушел», фиксированный приоритет упреждающего планирования. При использовании SCHED_FIFO планировщик всегда выбирает выполняемый поток с наивысшим приоритетом, который дольше всех ждал освобождения процессора. EVL предоставляет 99 фиксированных уровней приоритета, начиная с 1, которые в точности соответствуют реализации SCHED_FIFO в ядре (указывается в sched_policy).

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

Куча EVL состоит как минимум из одного так называемого экстента памяти, который представляет собой непрерывную область ОЗУ, из которой выделяются и куда освобождаются фрагменты. Каждый экстент имеет длину не более 4 ГБ - 512 байт. Первый экстент, прикрепленный к любой заданной куче, передается во время создания в evl_init_heap(). Затем вы можете добавить больше места для хранения, присоединив больше экстентов ОЗУ к такой куче с помощью evl_extend_heap(). Не существует произвольного ограничения на количество экстентов, формирующих кучу; однако, поскольку эти экстенты отслеживаются в связанном списке, который может потребоваться просканировать на предмет наличия места при выделении фрагментов из переполненной кучи, вы можете ограничить количество активных экстентов разумным числом (кажется, что менее десяти будет уместным). Не существует произвольного ограничения на количество куч, которые может создать приложение.

Выделяем память:

static struct evl_heap runtime_heap_buf;
static struct evl_heap runtime_heap_data;
static struct evl_heap runtime_heap_fd;
const size_t raw_size = EVL_HEAP_RAW_SIZE(m_sizeBlock);
const size_t data_size = EVL_HEAP_RAW_SIZE(8);
const size_t fd_size = EVL_HEAP_RAW_SIZE(4);
void *buf, *data, *fd;

buf = malloc(raw_size);
data = malloc(data_size);
fd = malloc(fd_size);
ret = evl_init_heap(&runtime_heap_buf, buf, raw_size);
ret = evl_init_heap(&runtime_heap_data, data, data_size);
ret = evl_init_heap(&runtime_heap_fd, fd, fd_size);

buf используется для сохранения данных в файл, data используется для чтения данных из модуля, fd для дескриптора файла модуля.

Распространенная проблема в системах с двумя ядрами связана с требованием не выполнять системные вызовы в рабочем режиме при выполнении критически важного кода в нерабочем режиме. В нашем случае необходимо полученные данные из модуля сохранять на диск. Поэтому выполнение этого с помощью стандартных процедур stdio() напрямую невозможно из-за задержек.

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

Открываем файл и связываем с прокси-сервером:

m_file = open(m_pathFile.toLocal8Bit(), O_WRONLY | O_APPEND);
m_proxyfd = evl_new_proxy(m_file, 1024*m_sizeBlock, "sfile:%d", getpid());

Запись в файл:

oob_write(m_proxyfd, buf, it);

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

Каждый таймер EVL идентифицируется файловым дескриптором. API таймера EVL по большей части имитирует API timerfd, который состоит из:

  • Создание таймера;

  • Настройка даты тайм-аута, будь он oneshot или повторяющийся;

  • Ожидание истечения (следующего) тайм-айта с помощью oob_read().

struct itimerspec value, ovalue;
__u64 ticks;
struct timespec now;
int tmfd;

tmfd = evl_new_timer(EVL_CLOCK_MONOTONIC);

ret = evl_read_clock(EVL_CLOCK_MONOTONIC, &now);

timespecAddNs(&value.it_value, &now, 10000ULL);
value.it_interval.tv_sec = 0;
value.it_interval.tv_nsec = 10000;


while (m_started) {
	ret = oob_read(tmfd, &ticks, sizeof(ticks));
}

Чтение данных из модуля осуществляется в следующем порядке, открываем файл устройства в режиме O_OOB:

int *fd_int = (int*)fd;
*fd_int = open(m_pathDevice.toLocal8Bit(), O_OOB);

затем устанавливаем смещение:

lseek(*fd_int, BASE_ADDRESS + offset_rkvalue_1, SEEK_END);

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

И непосредственно чтение:

while (m_started) {
  ret = oob_read(tmfd, &ticks, sizeof(ticks));

  …

  oob_read(*fd_int, data_c, 8);
}

Полученный результат

Для полной загрузки системы использовали утилиту stress, для утилиты установки необходимо:

$ sudo apt install stress

Запускаем для всех ядер:

stress --cpu 8 --timeout 1180

В эксперименте на вход модуля подавался прямоугольный импульс с частотой 1 кГц и скважностью 50% имитируя разовую команду. Оценивалось отклонение фронтов во времени, и оно составило менее 10 микросекунд. Пропуски не были обнаружены.

Список литературы

  1. https://www.opensourceforu.com/2015/10/the-xenomai-project-a-linux-based-rtos/

  2. https://v4.xenomai.org/overview/

  3. https://www.kernel.org/doc./htmldocs/kernel-hacking/index.html

Автор: BuyalskyKL

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js