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

RS232 пристрій 3-в-1 для домашнього сервера Linux: Частина 2 (Серверна)
Для усунення деяких недоліків сервера, зібраного з побутових комплектуючих, нещодавно розробив пристрій, яким хочу поділитися. Його детальний опис, зі схемою і вихідними кодами, доступно на Geektimes першої частини.
WRN пристрій
Пристрій отримав найменування WRN від складових його підсистем:
  • Апаратний сторожовий таймер, що працює з watchdog демоном;
  • Генератор істинно випадкових чисел;
  • Радіомодуль nRF24L01+ для збору даних з автономних датчиків.
У цій частині статті буде розглянуто як взаємодіяти з послідовним портом з простору ядра (kernel space) і як організувати роботу з декількома підсистемами пристрою через RS232 в Linux.
Послідовний порт, умовно кажучи, це дві кінцеві точки (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. У коді зустрічається обробка даних з сенсора, ознайомитися з якими можна на Geektimes за посиланням: ATtiny85: прототип бездротового сенсора.
Далі докладніше про всіх компонентах.

Драйвер
З точки зору програмування, драйвери 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
.

Демон
Призначення
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);

Призначення іншого коду, цілком можна перерахувати наступним списком:
  • розбір переданих параметрів;
  • перевірка умов для запуску, створення і відкриття необхідних директорій і файлів;
  • робота з лог-файлів;
  • обробка сигналів SIGINT, SIGTERM для коректного завершення роботи;
  • обробка сигналу SIGHUP для перевідкриття логів після
    logrotate
    ;
  • синхронізація з пристроєм і його ініціалізація;
  • обробка вхідних даних і запис результату в відповідний 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
дуже гарні і генеруються їм числа подобаються тестів навіть більше, ніж від даного апаратного генератора. У тестах FIPS (
rngtest
)
dieharder
різниця не значна, але, безумовно, на користь
/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}
}

Пристрій прошивається командою
wrnctrl flash [firmware].hex
при цьому демони
wrnd
та
watchdog
повинні бути зупинені. Для роботи цієї команди також потрібно утиліта
avrdude
, встановити її можна через менеджер пакетів, наприклад:
emerge -av dev-embedded/avrdude

Крім описаних вище файлів, інсталяційний пакет проекту включає також:
  • файл з налаштуваннями демона;
  • OpenRC скрипт для управління демоном;
  • файл з налаштуваннями для logrotate;
  • скрипт, який запускати через cron, для запису в лог статусів всіх підсистем пристрою.
Збірка і установка проекту здійснюється командою
make install
. Установка повинна запускатися з правами суперкористувача (root). Файли копіюються системної утилітою
install
, яка дозволяє відразу встановлювати права і власника на цільові файли.
Висновок
Інтерфейс USB для даного пристрою, безсумнівно, був би кращий. Але відсутність у моєму сервері доступного USB порту, призвело до появи проекту саме в такому вигляді. Тим не менш, вийшло досить просте і стабільно працюючий пристрій, яке цілком можу рекомендувати для відтворення.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.