четверг, 7 мая 2009 г.

Linux крізь осцилоскоп

Оригінал: http://linuxgazette.net/issue95/pramode.html

Вступ
Нещодавно я зробив декілька схем і перевіряв їх на своєму старому 20мГц осцилоскопі. І думав, що було б цікаво перевірити як складна, динамічна природа багатозадачної операційної системи впливає на роботу чутливого до часу виконання коду дивлячись на сигнал, що генерується такими програмами на осцилоскопі. Ця стаття описує декілька експериментів, що я зробив, спершу на “звичайному” ядрі 2.4.18, а потім на ядрі залатаному “розширенням реального часу”, що провадиться проектом RTAI. Я передбачаю, що читач має деякі базові навички у програмуванні ядра.
Інструментарій експерименту

Я перетворив старий мотлох у вигляді комп'ютеру з ЦП Cyrix у свою експериментальну платформу з “вбудованим linux”. Материнської плату було витягнуто з корпусу, НЖМД (HDD), монітор, клавіатуру та інше було видалено, залишив тільки Ethernet-карту з завантажувальною ПЗП (boot ROM) та ISA-протоплату (protoboard). Ця машина грузить повноцінну Linux-систему розташовану на відстані декількох футів. Таким чином я можу експериментувати з залізом не дуже хвилюючись, що пошкоджу щось коштовне. Ну і звичайно передбачив можливість завантажувати на вибір: чисте або залатане RTAI ядро 2.4.18.

Простий генератор сигналу
Ось невеличка програма, що робить у просторі користувача, при запуску з правами супер-користувача вона генерує сигнал на пінах паралельного порту, котрі можна бачити на осцилоскопі.
#include <asm/io.h> 

#define ON 100000
#define OFF ON*10

delay(unsigned int i)
{
while(i--);
}

main()
{
iopl(3);
while(1) {
outb(0xff, 0x378);
delay(ON); //2
outb(0x0, 0x378);
delay(OFF);
}
}

Програма робить радикально просто. Піни 2-9 паралельного порту роблять як вихідні — вони доступні через порт вводу-виводу, чия адреса 0x378. Якщо ви записуєте 0xff у 0x378, то включаєте (тобто, подаєте високий рівень, 5 В) на всі ці піни, записуючи 0x0 вимикаєте їх. Цю програму компілюємо з -O2 та запускаємо від суперкористувача (щоб заробив виклик outb, спочатку викликаємо iopl, що використовується для призначення рівню привілеїв, а щоб він виконався потрібно бути суперкористувачем).


На моїй системі, я спостерігаю сигнал з високим рівнем близько 2.5 — 2.7 мс з осцилоскопом налаштованим на 1 мс поділку. Результат може суттєво варіюватися в залежності від швидкості вашого процесору.

Чому прості речі не такі вже і прості
Кожен хто проходив базовий курс по мікропроцесорам, мабуть має знати як генерувти затримки за допомогою написання циклів. Це те саме, що ми будемо робити у даному випадку — зовсім дитяча справа.

З цікавості, я зареєструвався у іншій консолі і запустив команду “yes”, що генерує постійний потік символу “y” на екрані. Потім я подивився на осцилоскоп і побачив, що мій симпатичний сигнал перетворився у казна що. Періоди ВКЛ та ВИКЛ стали настільки подовженими, що я побачив майже гладку лінію, що змінювалася у межах від 0 В до 5 В.

Я зробив ще один експеримент. Запустив “ping -f” на систему зі швидшого комп'ютеру, і знову на осцилоскопі було видно, що сигнал суттєво збуджується.

Причину такої ситуації нескладно зрозуміти. Моя програма змагається з іншими за цикли(час) ЦП. Між виконанням циклу затримки, керування може перейти до іншої програми, збільшуючи затримку задану у моїй програмі. Повеневий(flood) пінг призводить до збільшення активності ядра системи, що також спричиняє шкідливий ефект на часові властивості моєї програми.

Вирішення проблеми просте — не турбувати програму, що генерує сигнал. Нехай вона повністю контролює ЦП. Тоді виникає питання, а навіщо взагалі складна багатозадачна система? Подивимось.

Назвемо програму, що генерує сигнал програмою “реального часу”. Візуалізуємо програму як “задачу” чия робота змінювати стан пінів паралельного порту у визначені інтервали часу. Якщо генерований сигнал використовується для керування фізичним пристроєм, як-то сервомотор (оберт сервомотору контролюється довжиною періоду високого рівня імпульсу чия загальна довжина близько 20 мс. Коли період високого рівеня змінюється від 1 мс до 2 мс, ротор сервомотору повертається на 180 о), зміни у довжині імпульса можуть мати драматичні наслідки. Мій серводвигун Futaba S2003 дико дригається коли керується програмою схожою на ту, що вище, якщо на неї впливає інший процес. Програма реального часу має граничні часові строки (timing deadlines), які МАЄ витримувати, для коректного виконання. Класичним рішенням є проектування пристроїв керування з використанням спеціалізованих мікроконтролерів та цифрових сигнальних процесорів. Та разом із здешевленням заліза ПК, значно розширився діапазон проблем коли нам необхідна можливість виконувати програми чутливі до часових строків і у той же час, робити інші справи, як-то взаємодіяти через мережу, візуалізувати дані з графічними інтерфейсами, реєструвати дані у допоміжні сховища і такі інші справи, що не потребують точного виконання за графіком, так звані “non-realtime” завдання.

Якщо можливо модифікувати ядро Linux таким чином, що часові обмеження накладаємі на деякі завдання (що створені і запущені якимось спеціальним чином) завжди виконуватимуться, навіть при присутності інших “non-realtime” завдань, то ми матимемо ідеальну ситуацію. Пізніше у статті буде показано, що таких рішень є декілька.


Засинання проти зациклення
Не дивлячись на факт, що синхронність програми дуже залежить від іншої діяльності, що відбувається у системі, ми використовуємо цикли ЦП виконуючи компактні цикли (так само, на складних мікропроцесорах як Pentium, важко обчислювати затримки підраховуючи інструкції). Чому не дозволити програмі заснути?

Використовуючі функції як “nanosleep”, ми інструктуємо Операційну Систему перевести на процес у режим сну, щоб прокинутись у визначений час. Але знову, є можливість, що наш процес не прокинеться для виконання у визначений час, тому що ОС буде зайнята виконуючі деяку дію у режимі ядра (наприклад, обробку пакетів TCP/IP, чи виконуючи операцію дискового вводу-виводу) чи інший процес буде заплановано, як раз перед тим як ядро прокине наш.

Робимо у просторі ядра
Що як ми створимо наш код генерації сигналу у вигляді модулю ядра?
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/param.h>
#include <asm/uaccess.h>
#include <asm/io.h>

static char *name = "foo";
static int major;

#define ON 100000
#define OFF ON*10

void delay(unsigned int i)
{
while(i--);
}

static int
foo_read(struct file* filp, char *buf, size_t count, loff_t *f_pos)
{
while(1) {
outb(0xff, 0x378);
delay(ON);
outb(0x0, 0x378);
delay(OFF);
}
return 0;
}

static struct file_operations fops = {
read: foo_read,
};

int init_module(void)
{
major = register_chrdev(0, name, &fops);
printk("Registered, got major = %d\n", major);
return 0;
}

void cleanup_module(void)
{
printk("Cleaning up...\n");
unregister_chrdev(major, name);
}

Виконання нескінченного циклу у ядрі має катастрофічні наслідки — поки розглядається наявність процесів рівня користувача. Жоден такий процес не зможе виконуватись, доки керування здійснюється на рівні ядра (так спроектовано ОС). А нам хочеться мати ситуацію коли завдання реального часу співіснують зі звичайними завданнями.

Хоча завдання простору користувача не можуть впливати на нашу програму, все ще можливо генерувати переривання на мережевій карті за допомогою повеневого пінгу. Оскільки переривання обслуговуються навіть при виконанні коду ядра, сигнал на осцилоскопі дещо змінюється від очикуваного.

Можливо засипати у режимі ядра — це запобігає замиканню системи та не вирішує проблеми мирного співіснування коду реального часу зі звичайним.
Введення у Linux реального часу

Що як встромити нано-ядро між Linux і нашим залізом? Це ядро буде керувати Linux і набором завдань реального часу. З Linux воно буде поводитись як з завданням дуже низького пріоритету, що буде виконуватись тільки коли жодне з високопріоритетних завдань реального часу не потребує процесору. Керування перериваннями буде у руках цього спеціалізованого ядра — запити Linux на заборону переривань будуть сприйматись таким чином, що переривання насправді не будуть заборонені, просто Linux не зможе їх побачити, при цьому завдання реального часу зможуть далі виконувати свої обробники переривань, без великої затримки.

Ця нова концепція, запропонована доктором Віктором Йодаікеном (Dr.Victor Yodaiken), призвела до появи RTLinux. Багато інших університетів і дослідницьких організацій спробували започаткувати свої власні реалізації — однією з найбільш успішних (і повністю вільною) є RTAI, що розробляється дослідниками у Департаменті Аерокосмічної Інженерії Міланської Політехніки (Dipartimento di Ingegneria Aerospaziale - Politecnico di Milano (DIAPM)).
Отримання та установка RTAI

RTAI може бути отримано звідси. У ньому є два головних компоненти:
  • HAL (шар абстракції заліза) заплата до ядра Linux,
  • набір модулів для виконання планування, міжпроцесної взаємодії, синхронізації і таке інше.
Перед латанням та інсталяцією нового ядра мають бути уважно прочитані інструкціїподані у файлі README.INSTALL (особливо ті, що стосуються деяких опцій налаштування ядра. "Встановити інформацію про версію на завантажувальні модулі" має бути відключена. Ймовірно ви використовуєте однопроцесорну систему — відповідно не забудьте відключити підтримку SMP (можливо, також відключити керування живленням)). Як тільки ви перезавантажитеся з новим ядром, можна буде компілювати головні модулі RTAI і приклади. Перед запуском будь-яких програм, вам потрібно завантажити три модулі: rtai.o, rtai_fifos.o та rtai_sched.o.


Генерація сигналу за допомогою RTAI

Давайте подивимось на програму RTAI, що генерує сигнал на вихідних пінах паралельного порту:

#include <linux/module.h>
#include <rtai.h>
#include <rtai_sched.h>

#define LPT1_BASE 0x378
#define STACK_SIZE 4096
#define TIMERTICKS 1000000 /* 1 milli second */

static RT_TASK my_task;

static void fun(int t)
{
unsigned char c = 0x0;
while(1) {
outb(c, LPT1_BASE);
c = ~c;
rt_task_wait_period();
}
}

int init_module(void)
{
RTIME tick_period, now;

rt_set_periodic_mode();
rt_task_init(&my_task, fun, 0, STACK_SIZE, 0, 0, 0);
tick_period = start_rt_timer(nano2count(TIMERTICKS));
now = rt_get_time();
rt_task_make_periodic(&my_task, now + tick_period, 2*tick_period);
return 0;
}

void cleanup_module(void)
{
stop_rt_timer();
rt_busy_sleep(10000000);
rt_task_delete(&my_task);
}


Оглянемо загальну ідею, перед тим як розбирати специфічні деталі. По-перш, нам потрібно, щоб завдання робило щось корисне. task це просто фкункція на C. Структура більшості наших завдань буде подібною до — зробити щось, заснути на деякий час, знову щось зробити, повторити. Один з способів заснути — викликати rt_task_wait_period, питання скільки ми будемо спати? Ми засинаємо на деякий фіксований період часу, що може бути добутком базового tick. Системний 8254 годинник може бути запрограмований на генерацію переривань на частоті 1 кГц (тобто, 1000 разів у секунду). Планувальник RTAI виконує планування на кожному тіку, якщо ми оберемо інтервал нашого завдання у 2 тіки і інтервал між кожним у 1 мс, то планувальник буде будити наше завдання після 2 мс.

Ми починаємо з init_module. Спочатку конфігуруємо годинник як “періодичний” (оскільки існує інший режим). Функція rt_task_init приймає адресу об'єкту типу RT_TASK, адресу нашої функції і розмір стеку, окрім деяких інших значень. Проводиться деяка “ініціалізація” і інформація зберігається у об'єкті типу RT_TASK, що може бути пізніше використаний для ідентифікації цього специфічного завдання.

Наш період тіків TICK_PERIOD дрівнює 1000000 нс (1 мс). Функція nano2count конвертує цей час у внутрішні “одиниці лічення”. Годинник запускається з періодом тіків еквівалентним 1 мс (це робить функція start_rt_timer).

Залишається тільки запустити наше завдання і виставити його період (пам'ятайте, період, що використовується rt_task_wait_period для визначення часу, через який завдання буде прокинуте). Ми встановлюємо період у 2 тіки і інструктуємо планувальник самостійно запустити його на наступному тіку.

Тіло нашого завдання дуже просте — воно просто записує значення на вихідні піни паралельного порту, значення змінної, що записується у порт інвертується і очикується наступний період (що буде 2 мс). Після прокидання, повторюється та ж сама послідовність, і знову, і знову... У кінцевому результаті ми побачимо на осцилоскопі сигнал з високим рівнем 2 мс і низьким 2 мс.

Я подивився на форму сигналу спочатку на ненавантаженій системі. Потім повторив для завантаженої повеневим пінгом. Форма сигналу залишилася незмінною. RTAI дає нам гарантію, що Linux буде завжди виконуватись як низькопріоритетне завдання, тобто лише у разі коли немає завдань реального часу, що необхідно обслужити. Просинання завдання реального часу призведе до миттєвої передачі йому керування (звичайно, тут є затримки, що виникають з причини витіснення чогось, що у цей час виконується, активацію планувальника реального часу і передачі керування назад завданню, що тільки-но виконувалось — ці затримки теж мають бути константою). Ось чому ми можемо бачити досить стабільний сигнал, навіть при навантаженні.

Ось сегмент коду, що демонструє використання функції rt_sleep:

#define LPT1_BASE 0x378
#define STACK_SIZE 4096
#define TIMERTICKS 1000000 /* 1 milli second */

#define ON_TIME 3000000 /* 3 milli seconds */
#define OFF_TIME 1000000 /* 1 milli second */

static RT_TASK my_task;
RTIME on_time, off_time;

static void fun(int t)
{
while(1) {
outb(0xff, LPT1_BASE);
rt_sleep(on_time);
outb(0x0, LPT1_BASE);
rt_sleep(off_time);
}
}

int init_module(void)
{
RTIME tick_period, now;

rt_set_periodic_mode();
rt_task_init(&my_task, fun, 0, STACK_SIZE, 0, 0, 0);
tick_period = start_rt_timer(nano2count(TIMERTICKS));
on_time = nano2count(ON_TIME);
off_time = nano2count(OFF_TIME);
now = rt_get_time();
rt_task_make_periodic(&my_task, now + tick_period, 2*tick_period);
return 0;
}


Базовий період тіку 1 мс. Наші періоди високого та низького рівнів сигналу є інтегральними добутками цього періоду (3 мс та 1 мс кожен). Виклик rt_sleep(on_time) переведе завдання у режим сну — воно прокинеться через період у 3 тіки. Зробить щось і знову перейде у сон на період у 1 тік.
Використання каналів FIFO для взаємодії між завданнями реального часу і звичними завданнями

Це може бути необхідним для передачі даних між звичною програмою простору користувача та завданням RTAI (і навпаки). Таке легко зробити з використанням каналів типу FIFO. Для прикладу, завдання RTAI може генерувати ШІМ-сигнал і ви можете контролювати його параметри (ширину) з простору користувача. При інсталяціі RTAI створюється декілька файлів пристроїв у каталозі /dev/, що називаються rtf0, rtf1 і так далі. Програма користувача позначає кожен FIFO за ім'ям, тоді як завдання RTAI номерами 0, 1, 2...

#include <linux/module.h>
#include <linux/errno.h>
#include <rtai.h>
#include <rtai_sched.h>
#include <rtai_fifos.h>


#define STACK_SIZE 4096
#define COMMAND_FIFO 0
#define FIFO_SIZE 1024


int fifo_handler(unsigned int fifo)
{
char buf[100];
int r;

r = rtf_get(COMMAND_FIFO, buf, sizeof(buf)-1);
if (r <= 0) return r;
rt_printk("handler called for fifo %d, get = %d\n", fifo, r);
buf[r] = 0;
rt_printk("data = %s\n", buf);
return 0;
}

int init_module(void)
{
/* Create fifo, set handler */
rtf_create(COMMAND_FIFO, FIFO_SIZE);
rtf_create_handler(COMMAND_FIFO, fifo_handler);

return 0;
}

void cleanup_module(void)
{
printk("cleaning up...\n");
}


У init_module, ми створюємо канал fifo і встановлюємо fifo_handler як функцію, що викликається коли хтось записує у fifo. Функція rtf_get читає дані з каналу. Після компіляції і завантаження модулю, якщо ми зробимо щось на кшталт:
echo hello > /dev/rtf0

то побачимо, що обробник викликано і він читає дані з fifo-каналу.


Що читати далі

Якщо ви зацікавились у програмуванні у реальному часі, то почати потрібно з чудового “Real Time and Embedded Guide” написаного Германом Брюнінксом (Herman Bruyninckx). Програмування за допомогою RTAI детально пояснюється у мануалі “RTAI manual” та “RTAI programming guide” доступних для завантаження з домашньої сторінки проекту.


Висновки

ОС що провадять підтримку детерміністичного виконання завдань з суворими часовими вимогами це лише частина тематики проектування систем реального часу. Після кількох днів гри з RTAI, я зрозумів, що проектування систем реального часу, це щось, що не може бути зроблене новаком як я — потрібно вкласти багато часу, зусиль і терпіння для повного розуміння своєї системи (апаратної настільки ж як і програмної частини) і використовувати всі інструменти правильно. А взагалі, це не повинно зупиняти вас від експерименту та веселощів!

Комментариев нет: