Produkty Novinky Články Návody Kontakty

Práce s časem

Práci s časem se nelze při vývoji ovladačů zařízení prakticky vyhnout. Vždy je třeba někde chvíli počkat, provádět nějaké úlohy periodicky apod. Jádro (Linux) dává programátorům ovladačů k dispozici celou řadu nástrojů. Až tolik, že je obtížné se v nich vyznat a zvolit ten správný pro danou potřebu. V následujícím textu si proto ukážeme ty nejzákladnější postupy při práci s časem, které budou pro jednoduché ovladače bohatě dostačovat.

Měření času

V případě měření času bychom si měli nejprve položit otázku jakého času. V jádru systému totiž rozeznáváme čas reálný - to je ten, který používáme my lidé, proto reálný. Může se měnit - posouvat dopředu či dozadu. Je vhodný pro plánování úloh, které mají proběhnout v daný čas. Pak je zde čas monotónní - to je čas, který běží (postupně roste) od začátku běhu systému až do jeho vypnutí bez ohledu na korekce reálného času. Je tak zaručeno, že novější časová hodnota je vždy vyšší než ta předchozí. Proto se hodí pro počítání uběhnutého času, plánování úloh relativně k aktuálnímu času apod. Poslední čas je čas spotřebovaný procesorem. Ten se používá pro měření zátěže systému atd. Nás bude v dalším textu zajímat monotónní čas.
Monotónní čas je v naprosté většině systémů postaven na systémovém časovači, což je speciální obvod mimo procesor, který v pravidelných konfigurovatelných intervalech generuje přerušení, tzv. systémový tik. Procesor potom v obsluze přerušení od systémového časovače nedělá v podstatě nic jiného, než že jen zvýší počítadlo těchto přerušení o jedničku.
V Linuxu je toto počítadlo reprezentováno proměnnou jiffies, resp. jiffies_64. Její deklarace je uvedená v hlavičkovém souboru <linux/jiffies.h>. Proměnná jiffies_64 je 64bitová proměnná. Pod názvem jiffies je dostupná její 32bitová spodní polovina, která pro většinu úloh postačuje a přístup k ní je atomický (je zajištěno, že vyčteme celých 32bitů naráz). Pro atomický přístup k 64bitové hodnotě musíme využít pouze k tomu určené funkce definované taktéž v <linux/jiffies.h>:
u64 get_jiffies_64(void);
Hodnoty počítadla jiffies můžeme bez problémů ukládat do proměnných deklarovaných jako unsigned long.
Příklad:
unsigned long jTime;
​
jTime = jiffies;
Perioda přerušení systémového časovače je v Linuxu dána převrácenou hodnotou symbolické konstanty HZ, která je definována v <linux/param.h>. Hodnota HZ je typicky 100 - 1000 Hz, tedy 100 až 1000 přerušení od systémového časovače za sekundu. Tím je určeno mimo jiné i rozlišení systémového časovače - doba, která odpovídá jednomu jiffyInformationJe to sice mírně matoucí, ale jiffies je jednak proměnná, kde je dostupný aktuální počet přerušení systémového časovače a jednak se tento výraz používá pro vyjádření časového kvanta přrestavovaného jedním systémovým tikem. Tedy jako jakési ”jednotky” času. Význam lze obvykle rozlišit z kontextu. My budeme kurzívou značit proměnou a normálním písmem ”jednotku”. - 10 ms až 1ms. Vyšší hodnota konstanty HZ znamená lepší rozlišení a i plynulejší běh systému (časové kvantum přidělované procesům je kratší), ale zase se zvyšuje režie v souvislosti s obsluhou vyššího počtu přerušení.
S ohledem na to, že proměnná jiffies přeteče každých zhruba 50 dní, musíme mít při měření a porovnávání časových údajů v jiffies tuto skutečnost na paměti. Nebo musíme používat funkce či makra, která za nás tuto nepříjemnost vyřeší.
Makra pro porovnávání časových údajů, která možnost přetečení berou v úvahu, jsou definována v <linux/jiffies.h> a mají tyto prototypy:
int time_after(unsigned long a, unsigned long b);  // = 1 kdyz a > b
int time_before(unsigned long a, unsigned long b);      // = 1 kdyz a < b
int time_after_eq(unsigned long a, unsigned long b);    // = 1 kdyz a >= b
int time_before_eq(unsigned long a, unsigned long b);   // = 1 kdyz a <= b
Jestliže potřebujeme znát rozdíl mezi dvěma časy v jiffies s ohledem na možné přetečení počítadla, tak můžeme použít stejný trik jako uvedená makra - přetypujeme časy na long a odečteme je od sebe:
unsigned long diff = (long) jTime2 - (long) jTime1;  
Často potřebujeme převádět časové údaje v jiffies na milisekundy a naopak. I na to jsou v jádře dostupná makra viz <linux/jiffies.h>:
unsigned int jiffies_to_msecs(const unsigned long j);
unsigned long msecs_to_jiffies(const unsigned int m);
Pozor si musíme dát na možné přetečení návratové hodnoty v důsledku příliš vysoké hodnoty vstupního parametru - platí pro architektury, kde má unsigned int menší šířku než unsigned long.
Sami si můžeme jiffies převést na milisekundy podle vzorce:

Čekání

Jednou z nejčastějších úloh, která souvisí s časem, je čekání. Podle doby čekání (z pohledu procesoru) si čekání a způsoby jeho implementace rozdělíme na dlouhé čekání - řádově jednotky až stovky milisekund (někdy až jednotky sekund) a krátké čekání - mikrosekundy až stovky mikrosekund, případně nanosekund.
Dlouhá čekání
Způsobů jak čekat řádově milisekundy a déle je hned několik. Prvním z nich je, že v se kódu ovladače vzdáme na nějaký čas procesoru. Jinými slovy řekneme plánovači úloh, ať naší úloze přidělí procesor až za nějakou dobu. Předtím však než tak učiníme, musíme změnit stav naší úlohy z TASK_RUNNING na TASK_INTERRUPTIBLE (čekání úlohy lze přerušit signálem) nebo na TASK_UNINTERRUPTIBLE (čekání úlohy nelze přerušit signálem). Implementace tohoto způsobu čekání pak stojí a padá s funkcí schedule_timeout() deklarovanou v <linux/sched.h>:
signed long schedule_timeout(signed long timeout);
kde timeout je v jiffies. Návratová hodnota je 0 nebo zbývající čas, pokud bylo čekání přerušeno signálem.
Celý kód pak vypadá nějak takto:
// cekani nemuze prerusit signal
set_current_state(TASK_UNINTERRUPTIBLE);
​
// cekani 100ms
schedule_timeout(msec_to_jiffies(100));
Platí zde stejně jako pro další funkce zajišťující dlouhé čekání, že se čeká minimálně požadovanou dobu nebo o trochu déle.
Další funkce, které lze použít pro delší čekání je funkce msleep() pro čekání v řádech jednotek až stovek milisekund a funkce ssleep() pro čekání v řádu jednotek sekund. Jejich prototypy jsou deklarované v <linux/delay.h>:
void msleep(unsigned int msecs);
​
void ssleep(unsigned int seconds);
Použití je myslím jasné. Samozřejmě, že funkce msleep() může nahradit funkci ssleep() v případě čekání delších jak 1 sekundu, např. při nutnosti čekat 1,5 sekundy.
Chování obou funkcí je stejné jako v předchozím případě - čeká se minimálně požadovanou dobu nebo o trochu déle. Čekání je pasivní - nezatěžuje procesor a nemá tak vliv na jiné úlohy a je nepřerušitelné (s vyjímkou funkce msleep_interruptible()).
Krátká čekání
Pro krátká čekání jsou vyhrazeny tři funkce, které se liší jen jednotkami, v kterých se udává prodleva. Všechny funkce jsou deklarovány v hlavičkové souboru <linux/delay.h>:
void ndelay(unsigned long nsecs);
void udelay(unsigned long usecs);
​
// Nepouzivat! Nahradit funkci msleep()
void mdelay(unsigned long msecs);
Ve všech třech uvedených funkcí je čekání implementováno jako aktivní, tj. čeká se v cyklu a daná úloha se nevzdává procesoru, protože se počítá, že čekání je tak krátké, že odevzdání procesoru a zpětné zařazení úlohy zpátky do fronty běžících úloh by zabralo více času než samotné čekání. Proto je použití funkce mdelay() víceméně nesprávné, protože čekání v cyklu po dobu několika milisekund vážně naruší plynulost běhu systému - jiné úlohy se po dobu čekání nedostanou (nepreemtivní jádro) nebo velmi těžko (preemtivní jádro) k procesoru. Proto by doba čekání neměla přesáhnout více jak stovky mikrosekund.
Jelikož rozlišení v jednotkách nanosekund je zatím spíše utopií, je zvykem používat především funkci udelay(). Také se doporučuje vybírat z uvedených funkcí tu, kde doba čekání nepřekračuje časovou jednotku jejího parametru - např. čekání 5000ns je lepší realizovat funkcí udelay() s dobou 5 mikrosekund než funkcí ndelay().

Časovače

Časovače (timers) v jádře jsou nástrojem, který nám umožňuje vykonávat určité úlohy v ovladači nezávisle na volání z uživatelského procesu (tj. asynchronně) a to v určeném čase relativně k aktuálnímu času. Případně tyto úlohy můžeme nechat vykonávat i periodicky. To se hodí na celou řadu věcí - pravidelná kontrola stavu zařízení, periodické vyčítání stisknuté klávesy apod.
Fakt, že se určitý kód ovladače vykonává nezávisle na volání z uživatelského prostoru, má však svá úskalí. Dosud jsme probírali pouze souborové operace a jejich implementaci. Tyto souborové operace, resp. funkce ovladače, které je implementovaly, byly volány vždy v kontextu uživatelského procesu jako reakce na systémové volání.
V případě kódu volaného časovačem je tomu jinak. Tento kód není volaný v kontextu žádného procesu, je volaný v kontextu softwarového přerušení. Platí pro něj tedy všechna omezení jako pro kód normálního (hardwarového) přerušení. To znamená, že kód volaný časovačem:
Časovače jsou v Linuxu popsány strukturou time_list definovanou v hlavičkovém souboru <linux/timer.h> stejně jako všechny funkce pro obsluhu časovačů. Nás budou zajímat její tři členové:
struct timer_list 
{         
        /* ... */         
        unsigned long expires;         
        void (*function)(unsigned long);         
        unsigned long data; 
};
kde expires je monotónní čas, kdy má časovač vypršet v jiffies, function je ukazatel na uživatelskou funkci, která bude provedena v okamžiku vypršení (obsluhy) časovače a data je proměnná, kam se si můžeme uložit uživatelská data nebo ukazatel na tyto data, která se použijí jako parametr při volání uživatelské funkce časovačem. Hodnoty členů struktury time_list lze měnit jen před spuštěním časovače (funkce add_timer() viz dále). Jinak je nutné časovač zastavit, upravit hodnoty a pak jej zase spustit.
Časovač inicializujeme funkcí init_timer(), případně makrem TIMER_INITIALIZER():
void init_timer(struct timer_list * timer);
​
struct timer_list timerVar = TIMER_INITIALIZER(function, expires, data);
Jakmile je časovač inicializován, můžeme jej spustit (přidat do seznamu aktivních časovačů) voláním funkce add_timer(). Po vypršení časovače se tento sám deaktivuje (odstraní se ze seznamu časovačů). Pro jeho opětovnou aktivaci musíme funkci add_timer() volat vždy znovu.
Ovšem pozor! Volání funkce add_timer() na již aktivovaný časovač je vážnou chybou. Proto v případech, kdy se má časovač spouštět periodicky, tj. aktivovat se sám (nejsnáze z uživatelské funkce volané při jeho obsluze), použijeme raději funkci mod_timer(), která umí bezpečně aktualizovat člen expires jak u aktivního tak i u neaktivního časovače (ten zároveň aktivuje).
Aktivní časovač lze před jeho vypršením deaktivovat voláním funkce del_timer(), kterou můžeme použít i na již neaktivní časovač. Návratová hodnota 0 říká, že časovač už nebyl aktivní, 1 pak, že časovač byl aktivní (byl naplánován).
U periodicky spouštěných časovačů musíme před odstraněním časovače zajistit, že nebude opětovně aktivován v rámci volání uživatelské funkce. K tomu se hodí atomická proměnná, kterou by měla uživatelská funkce testovat ještě před znovuspuštěním časovače. Prototypy uvedených funkcí:
void add_timer(struct timer_list * timer); 
int del_timer(struct timer_list * timer);
​
int mod_timer(struct timer_list *timer, unsigned long expires);
Na výpise 34 je vidět úprava hlavičky zdrojového kódu našeho modulu dskel pro případ použití časovače timer jako člena struktury dskel_t. Člen timer_stop je atomická proměnná, která slouží pro zablokování automatického znovuspouštění časovače, a člen timer_period uchovává nastavenou periodu pro automatické znovuspouštění časovače. Všimněte si, že samotný časovač se inicializuje až v rámci inicializační funkce.
1/* ######## prechozi include ######## */
2#include <linux/jiffies.h> 
3#include <linux/timer.h>
45//——————————————————————--
6struct dskel_t
7{
8      /* ######## predchozi clenove struktury ######## */
9      // casovac
10     struct timer_list       timer;
11     // perioda casovace (milisec)
12     atomic_t                        timer_period;
13     // promenna pro deaktivaci casovace
14     // (1 = deaktivuj casovac, 0 = casovac byl aktivovan
15     atomic_t                        timer_stop;
16};
1718static struct dskel_t dskel = 
19{
20     /* ######## predchozi clenove struktury ######## */
21     .timer_period = ATOMIC_INIT(1000),
22     .timer_stop = ATOMIC_INIT(1),   
23}
Výpis zdrojového kódu ovladače - hlavička (doplněk kvůli časovači)
Kromě hlavičky je nutné ještě před začleněním modulu do jádra inicializovat časovač (viz výpis 34 části inicializační funkce).
1// inicializace casovace (zbytek se nastavuje pri spusteni casovace)
2dskel.timer.function = &timer_userfunction;
3dskel.timer.data = (unsigned long) NULL;
4init_timer(&dskel.timer);
56/* ######## pridani ovladace do jadra ######## */
Výpis zdrojového kódu ovladače - funkce init() (doplněk kvůli časovači)
Pro inspiraci uvádíme ve výpisu 34 dvě funkce modulu dskel, které umožňují spustit (funkce timer_on()) a zastavit (funkce timer_off()) časovač a vedle toho i uživatelskou funkci timer_userfunction(), která se volá vždy při obsluze časovače při jeho vypršení.
Vidíme, že si ve funkci timer_userfunction() kontrolujeme stav atomické proměnné dskel.timer_stop, která je 1, pokud potřebujeme zablokovat znovuspouštění časovače při jeho odstraňování.
1//——————————————————————-- 
2static void timer_on(unsigned int period)
3{
4      // vynulujeme si deaktivacni promenou
5      atomic_set(&dskel.timer_stop, 0);
67      // zapamatujeme si periodu (milisecs)
8      atomic_set(&dskel.timer_period, period);
910     // nastavime si znacku, kdy ma casovac vyprset
11     dskel.timer.expires = jiffies + msecs_to_jiffies(period);
1213     // spustime casovac
14     add_timer(&dskel.timer);
15}
1617//——————————————————————-- 
18static void timer_off(void)
19{
20     atomic_set(&dskel.timer_stop, 1);
2122     del_timer(&dskel.timer);
23}
2425//——————————————————————-- 
26void timer_userfunction(unsigned long data)
27{
28     /* nejaky uzitecny kod */
29     pr_alert("dskel: timer_userfunction()\n");
3031     if ( !atomic_read(&dskel.timer_stop) )
32     {
33             mod_timer(&dskel.timer,
34                               jiffies + msecs_to_jiffies(atomic_read(&dskel.timer_period)));
35     }
36}
Výpis uživatelské funkce s periodickým spouštěním časovače
Při práci s časovači nezapomeneme aktivní časovače při ukončování práce s modulem ovladače deaktivovat. Nejčastěji se tak děje ve funkci release(), nejpozději však v ukončovací, resp. úklidové funkci.