Pull to refresh

RS232 устройство 3-в-1 для домашнего Linux сервера: Часть 2 (Серверная)

Reading time11 min
Views19K

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


В этой части статьи будет рассмотрено как взаимодействовать с последовательным портом из пространства ядра (kernel space) и как организовать работу с несколькими подсистемами устройства через RS232 в Linux.


Устройство включает в себя следующие подсистемы:


  • Аппаратный сторожевой таймер, работающий с watchdog демоном;
  • Генератор истинно случайных чисел;
  • Радиомодуль nRF24L01+ для сбора данных с автономных датчиков.

WRN устройство


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


  • для передачи команд;
  • считывания ответов;
  • приёма потока случайных чисел;
  • получения данных от сенсоров.

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


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


В роли диспетчера здесь выступает фоновый процесс (демон) wrnd, его назначение фильтровать трафик в три FIFO канала:


  • rng.fifo — поток случайных чисел;
  • nrf.fifo — поток данных от сенсоров;
  • cmd.fifo — данные, возвращаемые командами.

Также на схеме показаны:


  • wrn_wdt — драйвер символьного устройства /dev/watchdog, управляющий сторожевым таймером;
  • wrnctrl — утилита для прошивки, мониторинга и управления устройством;
  • оранжевым обозначены сервисы, с которыми взаимодействует проект.

Команды передаются устройству в текстовом виде, в формате [C|W|R|N][0-99]:[аргумент1], где первая буква, это идентификатор подсистемы, далее номер команды и через двоеточие может следовать аргумент.


В основном программное обеспечение написано на чистом C, содержит скрипты на Bash и Makefile. Установщик предназначен для Gentoo, но при желании его легко можно адаптировать под другие дистрибутивы.


Исходный код проекта доступен на GitHub alexcustos/wrn-project в директории wrnd. В коде встречается обработка данных с сенсора, ознакомиться с которым можно по ссылке: ATtiny85: прототип беспроводного сенсора.


Далее подробнее обо всех компонентах.



Драйвер


Уже после публикации статьи в проект был добавлен канал wdt.fifo, который обслуживается wrnd демоном и дублирует файл устройства /dev/watchdog. Оба варианта подходят для работы с watchdog демоном и в целом аналогичны. Поэтому, приведённая ниже, информация всё ещё актуальна, и будет полезна при желание ознакомиться с тем, как устроен драйвер сторожевого таймера.


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


Но в данном случае, это не страшно, поскольку задача предельно простая, реализовать символьное устройство /dev/watchdog (код полностью: wrn_wdt.c). Обслуживается устройство драйверами номер 10 (miscdevice), поэтому сначала нужно определить соответствующую структуру данных:


static struct miscdevice wrn_wdt_miscdev = {
    .minor = WATCHDOG_MINOR,  // стандартное устройство watchdog
    .name = "watchdog",  // имя файла в /dev
    .fops = &wrn_wdt_fops, // обработчики операций над файлом устройства
};

Затем задать обработчики:


static const struct file_operations wrn_wdt_fops = {
    .owner = THIS_MODULE,  // указатель на владельца структуры
    .llseek = no_llseek,  // перемещение по файлу не предусмотрено
    .write = wrn_wdt_write,  // вызывается при запись в файл
    .unlocked_ioctl = wrn_wdt_ioctl, // ioctl
    .open = wrn_wdt_open,  // вызывается при открытии файла
    .release = wrn_wdt_release,  // вызывается при закрытии файла
};

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


Теперь нужно определить обработчики для операций загрузки и выгрузки модуля:


module_init(wrn_wdt_init);  // вызывается при загрузке модуля в память
module_exit(wrn_wdt_exit);  // вызывается при выгрузке модуля

При необходимости можно добавить модулю параметры через module_param(), MODULE_PARM_DESC() и для порядка указать:


MODULE_DESCRIPTION("...");  // описание
MODULE_AUTHOR("...");  // автора
MODULE_LICENSE("...");  // лицензию

Посмотреть эту информацию можно командой:


modinfo [path]/[module].ko

На этом шаге драйвер почти готов, осталось сделать его полезным. Для этого нужна возможность, как минимум, отправлять данные в последовательный порт. Сделать это на прямую нельзя, поскольку, в пространства ядра, API для этого не предусмотрен. Вариант связанный с удалением стандартного драйвера и разработкой собственного, можно сразу исключить как идеологически не верный. Поэтому остаётся два варианта:


  1. из пространства ядра обращаться к файловой системе в пространстве пользователя;
  2. зарегистрировать в пространстве ядра LDISC (line discipline) для перехвата и управления трафиком последовательного порта.

Думаю уже ясно, что я выбрал первый вариант, и нет мне в этом оправдания. Если серьёзно, то line discipline стоило бы использовать для размещения диспетчера трафика в пространстве ядра. Но, как уже отмечалось, программирование и отладка в этом пространстве, дело не тривиальное, и по возможности его нужно избегать, как и операций ввода/вывода, которые можно сделать в пространстве пользователя.


Но основная причина нецелесообразности разработки такого драйвера, заключается в том, что невозможно добиться самостоятельности, как если бы это был USB драйвер. Line discipline это всего-лишь прослойка, которой необходим код в пространстве пользователя для настройки параметров порта и установки нужного line discipline.


Как бы то ни было, выбор сделан и основной код драйвера получился следующим:


static int __init wrn_wdt_init(void)
{
    int ret;
    // здесь нет возможности настроить порт, для корректной работы нужен wrnd демон
    filp_port = filp_open(serial_port, O_RDWR | O_NOCTTY | O_NDELAY, 0);

    if (IS_ERR(filp_port)) ... // обработка ошибки
    else {
        ret = misc_register(&wrn_wdt_miscdev); // регистрация miscdevice
        if (ret == 0) wdt_timeout();  // установка таймаута сторожевого таймера
    }
    return ret;
}

static void wdt_enable(void)
{
    ... // локальные переменные

    spin_lock(&wrn_wdt_lock);  // синхронизация (блокировка доступа)
    // watchdog демон отправляет keep-alive на каждую проверку в пределах interval,
    // спамить WRN устройство не желательно, поэтому нужно ограничение
    getnstimeofday(&t);
    time_delta = (t.tv_sec - wdt_keep_alive_sent.tv_sec) * 1000;  // sec to ms
    time_delta += (t.tv_nsec - wdt_keep_alive_sent.tv_nsec) / 1000000;  // ns to ms
    if (time_delta >= WDT_MIN_KEEP_ALIVE_INTERVAL) {
        // настройка доступа к буферу cmd_keep_alive из пространства пользователя
        fs = get_fs();  // сохранение содержимого регистра FS
        set_fs(get_ds());  // запись в него KERNEL_DS (указатель на сегмент данных ядра)
        ret = vfs_write(filp_port, cmd_keep_alive, strlen(cmd_keep_alive), &pos);
        set_fs(fs);  // восстановление FS
        if (ret != strlen(cmd_keep_alive)) ... // обработка ошибки

        getnstimeofday(&wdt_keep_alive_sent);
    }
    spin_unlock(&wrn_wdt_lock);  // синхронизация (разблокирование доступа)
}

static long wrn_wdt_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
    ... // локальные переменные

    switch (cmd) {
    case WDIOC_KEEPALIVE:
        wdt_enable();
        ret = 0;
        break;

    case WDIOC_SETTIMEOUT:
        ret = get_user(t, (int *)arg);  // получение данных из пространства пользователя
        ... // проверка входных данных

        timeout = t;
        wdt_timeout();
        wdt_enable();
        /* проходим дальше */

    case WDIOC_GETTIMEOUT:
        ret = put_user(timeout, (int *)arg);  // отправка данных в пространство пользователя
        break;

    ... // остальные команды
    }
    return ret;
}

За рамками, приведённого выше фрагмента кода, осталась поддержка MAGICCLOSE. Она необходима, поскольку драйвер отключает сторожевой таймер при закрытии файла устройства. Поэтому важно распознать не штатное завершения работы watchdog демона, при котором файл будет закрыт системой. В этом случае обеспечить перезагрузку помогает механизм MAGICCLOSE. Его поддержка предусматривает деактивацию сторожевого таймера только при закрытии файла устройства непосредственно после получения специального символа, обычно это V.


Сборка драйвера производится с помощью Makefile командой make driver, за это отвечает его часть:


TARGET_WDT = wrn_wdt

ifneq ($(KERNELRELEASE),)
# определение модуля, если вызов был из системы сборки ядра
obj-m := $(TARGET_WDT).o

else
# иначе это обычный вызов командой make
KERNEL := $(shell uname -r)

# обращение к системе сборки ядра для компиляции
driver:
    $(MAKE) -C /lib/modules/$(KERNEL)/build M=$(PWD)

# обращение к системе сборки ядра для установки модуля
install: driver
    $(MAKE) -C /lib/modules/$(KERNEL)/build M=$(PWD) modules_install
endif

Чтобы после установки модуль загружался при старте системы, нужно добавить в файл /etc/conf.d/modules строчку:


modules="wrn_wdt"

Вручную модуль можно загрузить командами: modprobe wrn_wdt или insmod ./wrn_wdt.ko; выгрузить: modprobe -r wrn_wdt или rmmod wrn_wdt; убедиться, что модуль загружен: lsmod | grep wrn_wdt.


При использовании канала wdt.fifo в качестве файла устройства для watchdog демона, важно убедиться, что правильно заданы зависимости и демон wrnd стартует раньше и останавливается позже watchdog. Иначе, соответственно FIFO канал может быть ещё не создан или таймер не будет деактивирован, что может привести к нежелательной перезагрузке.



Демон


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


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


// в отличии от termios имеет два поля для установки скорости приёма/передачи данных
struct termios2 ttyopts;
memset(&ttyopts, 0, sizeof ttyopts);
// получение текущих настроек
if (ioctl(fd, TCGETS2, &ttyopts) != 0) ... // обработка ошибки

// установка произвольный скорости порта для приёма и передачи данных
ttyopts.c_cflag &= ~CBAUD;
ttyopts.c_cflag |= BOTHER;
ttyopts.c_ispeed = speed;  // unsigned int, а не константа вроде B9600
ttyopts.c_ospeed = speed;

... // установка требуемого режима работы и отключение управления трафиком

// блокировка операции чтения до получения заданного числа байт
ttyopts.c_cc[VMIN] = vmin;
// блокировка (ожидание данных) на указанное в десятых долях секунды время
ttyopts.c_cc[VTIME] = vtime;

// запись изменённых настроек
if (ioctl(fd, TCSETS2, &ttyopts) != 0) ... // обработка ошибки

Здесь важно выбрать оптимальные значения VMIN и VTIME. Если они равны нулю, опрос порта будет производится без задержек потребляя ресурсы системы без необходимости. Не нулевое значение VMIN, при отсутствии данных, может заблокировать поток на неограниченное время.


В данном случае, данные считываются по одному байту в основном потоке программы. Блокировать его надолго не хорошо, поэтому VMIN всегда равен нулю, а VTIME можно изменить через параметры, по умолчанию он равен 5 (максимальная задержка 0.5 сек).


Принятые байты поступают в буфер фиксированного размера. При получении ожидаемого числа байт, буфер преобразуются в соответствующую структуру данных (struct). Этот метод хорош, но имеет особенности, которые необходимо иметь в виду. Компилятор оптимизирует структуры данных, добавляя промежутки между полями, выравнивая их по своему усмотрению, обычно до границы слова. Поскольку данные пересылаются между разными платформами, возможны несоответствия из-за различного размера слова и порядка следования байт в нём (endian).


Чтобы избавить от выравнивания, необходимо объявить структуры данных упакованными (packed). Проекты в AtmelStudio по умолчанию собираются с ключом -fpack-struct, поэтому достаточно убедиться, что нет предупреждений об отмене действия данного ключа. Собирать wrnd проект с этим ключом не желательно, поскольку тут нет задачи экономить память за счёт скорости доступа к данным. Достаточно указать соответствующий атрибут там, где это необходимо, например:


struct payload_header {
    ... // поля структуры
} __attribute__ ((__packed__));

Процесс запускается в фоновом режиме функцией daemon:


if (arguments->daemonize && daemon(0, 0) < 0) ... // обработка ошибки

В результате создаётся копия процесса (fork) с новым PID, которая продолжает работу дальше, а текущий процесс завершает работу. В аргументах функции указанно, что для нового процесса, необходимо установить корневую директорию в качестве рабочей и перенаправить стандартные потоки ввода, вывода и ошибок в /dev/null.


Чтобы демон не завершал работу при попытке записи в FIFO, к которому не подключён читатель, необходимо игнорировать сигнал SIGPIPE:


signal(SIGPIPE, SIG_IGN);

Назначение остального кода, вполне можно перечислить следующим списком:


  • разбор переданных параметров;
  • проверка условий для запуска, создание и открытие необходимых файлов;
  • работа с лог-файлами и их переоткрытие по сигналу SIGHUP от logrotate;
  • обработка сигналов SIGINT, SIGTERM для корректного завершения работы;
  • синхронизация с устройством и его инициализация;
  • обработка поступающих данных и запись результата в соответствующий FIFO канал.

Канал rng.fifo


Данные попадают в канал на прямую в бинарном виде на скорости примерно 636 байт/сек. Для подмешивания энтропии в /dev/random используется rngd демон. Чтобы он использовал только нужный источник, ему необходимо передать параметры "--no-tpm=1 --no-drng=1 --rng-device /run/wrnd/rng.fifo".


Здесь стоит отметить, что для решения проблемы нехватки энтропии, использовать генератор истинно случайных чисел не обязательно. Достаточно запустить rngd с параметром "--rng-device /dev/urandom". Алгоритмы используемые в /dev/urandom достаточно хороши и рекомендации так не делать, как правило, полностью не обоснованы. Результаты сравнительного тестирования можно посмотреть в первой части, ближе к концу публикации.


Мой выбор в пользу генератора истинно случайных чисел прост — захотел собрать подобное устройство, и не нашёл для себя доводов против.


Канал nrf.fifo


Данные от сенсоров обрабатываются и затем отправляются в канал в виде SQL запросов на вставку записей в таблицу. В примере wrnsensors.sh показана работа с SQLite3 базой данных, но INSERT запрос универсален и должен подойти к любой SQL базе данных.


Канал cmd.fifo


Канал используется утилитой управления wrnctrl, о ней чуть ниже.


Собрать wrnd можно с помощью Makefile командой make daemon. Для сборки отладочной версии предусмотрена цель debug.



Утилита управления


Утилите wrnctrl необходим запущенный wrnd демон, поскольку данные от устройства она получает из канала cmd.fifo.


Открыть одновременно FIFO с предсказуемым результатом, можно только на запись, при этом читатель получит данные от всех источников. Если же несколько читателей откроют один FIFO, то предсказать, кто из них получит данные невозможно. Такое поведение верно по определению, но не желательно, следовательно нужно синхронизировать доступ к cmd.fifo.


Объявить файл занятым в Linux можно с помощью flock, и таким образом исключить одновременную работу с ним, но только в своём коде. Поскольку этот механизм не работает для именованных каналов (pipe), необходимо использовать дополнительный файл в /tmp (код полностью: wrnctrl):


function device_cmd()
{
    cmd=$1  # команда устройству
    # запуск новой оболочка (subshell), вывод направляется в файл с дескриптором 4
    ( 
        # блокировка файла с дескриптором 4, если он ещё не занят
        if ! flock -w ${FIFO_MAX_WAIT} 4; then return; fi  # выход если занят

        # предоставление права на запись всем, если достаточно полномочий
        if [ -O ${LOCK_NAME} ]; then chmod 666 ${LOCK_NAME}; fi

        # чтение cmd.fifo в дескриптор 3, ограничено таймаутом
        exec 3< <(timeout ${FIFO_MAX_LOCK} cat ${WRND_CMDFIFO})
        sleep 0.2  # иначе дескриптор может быть ещё недоступен
        if [ -r /dev/fd/3 ]; then
            echo "$cmd" >${WRND_DEVICE}  # запись команды в порт устройства
            # цикл прервётся, когда демон закроет FIFO или по timeout
            while read log_line <&3; do
                echo "$log_line"  # вывод ответа
            done
            exec 3>&-  # явное закрытие дескриптора 3
        fi
    ) 4>${LOCK_NAME}
    # дескриптор 4 будет закрыт после завершения работы subshell, что снимет блокировку
}

Для прошивки устройства предусмотрена команда wrnctrl flash [firmware].hex. Перед её запуском необходимо остановить демоны watchdog и wrnd. Команда использует утилиту avrdude, установить её можно через менеджер пакетов, например:


emerge -av dev-embedded/avrdude

Помимо упомянутых выше файлов, установочный пакет проекта также включает:


  • файл с настройками демона;
  • OpenRC скрипт для управления демоном;
  • файл с настройками для logrotate;
  • скрипт, запускаемый через cron, для записи в лог статусов всех подсистем устройства.

Сборка и установка проекта производится командой make install. Установка должна запускаться с правами суперпользователя (root). Файлы копируются системной утилитой install, которая позволяет сразу установить права и владельца на целевые файлы.


Заключение


Интерфейс USB для данного устройства, несомненно, был бы предпочтительнее. Но отсутствие в моём сервере доступного USB порта, привело к появлению проекта именно в таком виде. Тем не менее, получилось довольно простое и стабильно работающее устройство, которое вполне могу рекомендовать для воспроизведения.

Tags:
Hubs:
+16
Comments6

Articles