Real-Time Linux II

ArticleCategory:

Kernel Corner

AuthorImage:

[Photo of the Author]

TranslationInfo:

original in es Ismael Ripoll 

en to nl Tom Uijldert

AboutTheAuthor:

Behaalde een graad aan de Polytechnische Universiteit van Valencia in 1996. Professor in operating systemen op het departement van DISCA. Onderzoeksgebieden omvatten onder andere real-time scheduling en operating systemen. Linux gebruiker vanaf 1994. Hobbies: met de rugzak door de Pyreneën, skiën en hobby electronica.

Abstract:

In dit tweede artikel over RT-Linux zal ik proberen wat meer in te gaan op de praktijk. Voordat we hieraan beginnen echter, zal ik eerst een kakelvers real-time operating systeem bespreken, genaamd Linux KURT.

ArticleIllustration:

[Illustration]

ArticleBody:

KU Real-Time Linux (KURT)

Begin dit jaar (1998) is er een nieuw real-time operating systeem uitgekomen, gebouwd op Linux. KURT is een "zacht" real-time operating systeem. Met andere woorden, de scheduler (= verdeler van de processortijd over de taken) probeert tegemoet te komen aan de wens om binnen een bepaalde tijd taken te laten executeren maar als een taak toevallig zijn werk te laat af moet ronden dan is er geen man overboord en gebeuren er geen erge dingen. Real-Time taken onder KURT kunnen alle beschikbare middelen van Linux gebruiken. Dit in contrast met RT-Linux taken. De verbeteringen --modificaties-- zijn: De real-time taken zijn modules die dynamisch kunnen worden ingeladen.

Het meest karakteristiek aan KURT is zijn scheduling mechanisme. Er is besloten voor een cyclische scheduler. Een dergelijk type scheduler gebruikt altijd een tabel, plan genaamd, die alle verdeel-acties bevat: Moment van activeren, uit te voeren taak, duur van de taak, etc. De tabel wordt aangemaakt in de ontwerpfase. Tijdens executie bestaat het werk eruit om eenvoudig de tabel uit te lezen en de instructies hieruit op te volgen. Aan het einde van de tabel aangekomen springt de scheduler terug naar het begin en het ritueel kan opnieuw beginnen -- vandaar de term "cyclisch". Een dergelijke scheduler heeft vele voordelen:

Belangrijkste obstakel hier is het maken van de tabel. Wanneer verder iets verandert in deze parameters dan moet de hele tabel opnieuw worden gebouwd. Het geheugenbeslag is daarbij meestal fors.

Waarom eigenlijk Real-Time?

Wellicht denkt men dat real-time technieken alleen bij NASA worden gebruikt of voor raketten of iets dergelijks. Hoewel dit vroeger misschien op ging, vandaag de dag is niets minder waar - en de ontwikkeling zet nog steeds door - vooral vanwege verdergaande integratie van informatiesystemen en elektronica met het leven van alledag. Deze alledaagsheid van Real-Time komen we vooral tegen op het gebied van telecommunicatie en multimedia. Als we bijvoorbeeld een geluidsbestand op onze harde schijf opnieuw willen beluisteren moet een programma continu (liever nog; periodiek) data lezen, uitpakken en naar de geluidskaart sturen. Wanneer we in de tussentijd met een applicatie bezig zijn, bijvoorbeeld een tekstverwerker of het compileren van de Linux kernel, zullen er zeker stiltes vallen tijdens het afspelen omdat de processor bezig is met een andere taak. Als we video nemen in plaats van geluid dan zou het resultaat af en toe bevroren beelden zijn. Dit type systemen staat bekend als zachte real-time (overschrijding van de executietijd is niet onoverkomelijk maar heeft wel zijn invloed op de beschikbaarheid van het systeem).

RT-Linux applicaties gaan verder. Hiermee kunnen we de besturing van de PC overnemen (Ik zeg hier PC en niet computer omdat er op dit moment nog geen implementatie van RT-Linux is voor andere platformen) net als bij MS-DOS. Tijdens het uitvoeren van een real-time taak is het mogelijk om alle PC-poorten te benaderen, interrupt-handlers te installeren en deze tijdelijk uit te zetten. Met andere woorden, we kunnen het systeem "crashen" alsof we onder Windows zitten. Deze mogelijkheid is zeer aantrekkelijk voor diegenen die er lol in scheppen om elektronische apparaatjes aan de computer te koppelen.

Loadable Modules

Om RT-Linux te kunnen begrijpen en gebruiken is het noodzakelijk om iets te weten over dynamisch te laden modules onder Linux. Matt Welsh heeft een compleet artikel geschreven waarin de werking van modules tot in detail wordt uitgelegd.

Wat zijn dat?

Bij de meeste Unix implementaties kan men alleen de hardware (poorten, geheugen, interrupts etc.) aanspreken via speciale bestanden, nadat specifieke aansturingsprogramma's zijn geïnstalleerd. Hoewel er goede boeken zijn geschreven over het maken van dit soort aansturingsprogramma's, blijft het een vervelende en langdradige taak om alle functies te maken die nodig zijn voor het aanknopen van het systeem.

Modules zijn "delen van het operating systeem" die in een operationeel systeem kunnen worden ingebracht en er weer uitgehaald. Wanneer een programma, dat bestaat uit diverse codebestanden, wordt gecompileerd dan wordt eerst van ieder bestand een objectfile ".o" gemaakt, waarna deze objecten aan elkaar worden geknoopt (linken) en alle verwijzingen worden opgelost om één executeerbaar bestand te maken (executable). Laten we even aannemen dat de objectfile waarin main() staat direct uit kan worden gevoerd en dat het operating systeem daarbij in staat is om dit in geheugen te laden, tezamen met de andere benodigde objectfiles, waarbij verwijzingen worden opgelost daar waar dat nodig is. Welnu, dat kan de kernel ook met zichzelf doen. Tijdens opstart wordt alleen het executeerbare bestand vmlinux ingeladen, waarin alleen de onmisbare basisfunctionaliteit aanwezig is. Later, in de operationele toestand, kan het modules inladen en verwijderen al naar gelang de behoefte.

Het gebruik van modules is optioneel in Linux, deze optie moet mee worden gecompileerd in de kernel. In alle distributies die ik ken is dat al gebeurd.

Het is zelfs mogelijk om nieuwe modules aan te maken en direct in te laden zonder dat het systeem een herstart nodig heeft of een nieuwe compilatie.

Als een module eenmaal is geladen, is het een onderdeel van de kernel geworden, daarom:

Zoals we kunnen zien heeft een dergelijke module al karakteristieken van een real-time programma: geen vertragingen door page faults en toegang tot alle hardware.

Hoe ze te maken en gebruiken?

Een module wordt aangemaakt met "C"-code. Hieronder is een voorbeeld van een micromodule (om de commando's die nu volgen uit te kunnen voeren is het nodig om super-user of root te zijn):

example1.c

  #define MODULE
  #include <linux/module.h>
  #include <linux/cons.h>
  static int output=1;

  int init_module(void) {
    printk("Output= %d\n",output);
    return 0;
  } 
  void cleanup_module(void){   
    printk("Adi�s, Bye, Chao, Ovuar, \n");
  }
Voor het compileren gebruiken we de volgende parameters:
  # gcc -I /usr/src/linux/include/linux -O2 -Wall -D__KERNEL__ -c example1.c
De optie -c geeft aan gcc door dat hij moet stoppen na het genereren van de objectfile en ook niet moet linken. Het resultaat hiervan is het bestand example1.o.

De kernel kent geen standard output, waardoor we de functie printf() niet kunnen gebruiken. In plaats daarvan heeft de kernel een functie genaamd printk() die bijna identiek is behalve dan dat de uitvoer naar een ring wordt gestuurd. Al dit soort uitvoer verdwijnt in deze buffer. Dit zijn feitelijk de boodschappen die we zien als het systeem op wordt gestart. Op ieder gewenst moment kunnen we de inhoud van de buffer bekijken met het commando dmesg of door direct in het bestand /proc/kmsg te kijken.

Merk op dat er geen functie main() is en in plaats daarvan de functie init_module(), zonder parameters. cleanup_module() is de laatste functie die aan wordt geroepen voordat de module wordt verwijderd. Het laden van de module kan met het commando insmod.

  # insmod example1.o
Op dit moment hebben we de module example1 geïnstalleerd en de functie init_module() uitgevoerd. Zie voor het resultaat:
  # dmesg | tail -1
  Output= 1   
Het commando lsmod geeft een overzicht van de modulen die op dat moment geladen zijn.
# lsmod
Module    Pages   Used by:
example1      1          0
sb            6          1
uart401       2  [sb]    1
sound        16  [sb uart401]  0 (autoclean) 
Als laatste gebruiken we nu rmmod om de module te verwijderen.
  # rmmod example1
  # dmesg | tail -2
  Output= 1
  Adi�s, Bye, Chao, Orvua,     
De uitvoer van dmesg laat zien dat de functie cleanup_module() is uitgevoerd.

We hoeven nu alleen te weten hoe we parameters aan een module doorgeven. Niets is minder makkelijk. We kunnen globale variabelen een waarde geven door parameters door te geven aan insmod. Bijvoorbeeld:

  # insmod example1.o output=4
  # dmesg | tail -3
  Output= 1
  Ad�os, Bye, Chao, Orvua,
  Output= 4                
We weten nu alle relevante dingen van modules, laten we verder gaan met RT-Linux.

Ons eerste Real-Time programma

Voor het gebruik van RT-Linux moesten we Linux voorbereiden op de ondersteuning van Real- Time modules --dit is uitgelegd in het vorige artikel.

Er zijn twee manieren om RT-Linux te gebruiken:

  1. Als een klassiek real-time systeem met een scheduler die werkt met vaste prioriteitsstellingen.
  2. Als een kale PC, iets wat lijkt op wat je ook onder DOS kunt doen: interrupts afvangen en de computer volledig besturen.
Nu gaan we in op het geval met vaste prioriteitsstellingen. Het voorbeeld wat we gaan zien doet niets "nuttigs"; het handelt alleen maar een real-time taak af (een simpele lus):

example2.c

  #define MODULE
  #include <linux/module.h>
  #include <linux/kernel.h>
  #include <linux/version.h>
  #include <linux/rt_sched.h>
  RT_TASK task;
	  
  void fun(int computo) {
    int loop,x,limit;
    limit = 10;
    while(1){
      for (loop=0; loop<computo; loop++)
        for (x=1; x<limit; x++);
      	
      rt_task_wait();
    }
  }
  
  int init_module(void) {

    RTIME now = rt_get_time(); 

    rt_task_init(&task,fun, 50 , 3000, 1);
    rt_task_make_periodic(&task,
          now+(RTIME)(RT_TICKS_PER_SEC*4000)/1000000,
	 (RTIME)(RT_TICKS_PER_SEC * 100)/1000000);
    return 0;
  }

  void cleanup_module(void){
    rt_task_delete(&task);
  }
Wederom gaan we dit voorbeeld compileren met het commando:
# gcc -I /usr/src/linux/include/linux -O2 -Wall -D__KERNEL__ -D__RT__ -c example2.c 
Omdat het een module betreft is de ingangsfunctie init_module(). De eerste actie is het inlezen van de tijd en dit opslaan in een lokale variabele; de functie rt_get_time() geeft het aantal RT_TICKS_PER_SEC terug, verstreken sinds het opstarten van het systeem (in de huidige implementatie is RT_TICKS_PER_SEC gelijk aan 1.193.180, wat een resolutie oplevert van 0,838 microseconden). Met behulp van rt_task_init() wordt de structuur "task" geïnitialiseerd maar de taak wordt nog niet opgestart. Het programma van deze taak is fun(), die mee wordt gegeven als tweede parameter. De volgende parameter is een waarde die door wordt gegeven aan de nieuwe taak wanneer deze start. Merk op dat fun() een int variabele verwacht. De volgende parameter geeft de grootte van de stack voor deze taak; omdat iedere taak zelfstandig uit wordt gevoerd heeft iedere taak een éigen stack. De laatste parameter is de prioriteit; in dit geval, met slechts één taak op het systeem, kunnen we een willekeurige waarde opgeven.

rt_task_make_periodic() maakt de taak periodiek met twee tijdswaarden als parameters. De eerste geeft de absolute tijd aan waarop de taak moet worden gestart. De tweede waarde geeft de periode aan waarop hij vervolgens bij herhaling wordt gestart.

De real-time taak (de functie fun()) is een oneindige lus met slechts twee acties: een lus die alleen maar tijd verdoet en dan rt_task_wait() aanroept. rt_task_wait() is een routine die de geactiveerde taak laat pauzeren tot de volgende activering, op welk moment hij door zal gaan met de instructie die op rt_task_wait() volgt. De lezer moet zich realiseren dat een periodieke taak niet steeds bij het begin start maar dat de taak steeds zichzelf moet stoppen (na de gedane arbeid) en moet wachten op het volgende startsignaal. Met dit mechanisme kan men dus een taak maken die zichzelf alleen maar initialiseert bij de eerste ronde.

Voor het uitvoeren van example2 moeten we eerst de module rt_prio_sched installeren omdat ons programma de functies rt_task_make_periodic(), rt_task_delete() en rt_task_init() nodig heeft. De functie rt_get_time() zit niet in de module maar in de Linux kernel. We hoeven hem daarom niet te installeren voor gebruik.

  # modprobe rt_prio_sched
  # insmod ./example2.o
Omdat rt_prio_sched een systeemmodule is, is hij aangemaakt tijdens compilatie van de Linux kernel en staat het bestand dus in de directory /var/modules/2.0.33. We gebruiken hier het commando modprobe omdat dit makkelijker is met het inladen van modules (hij zoekt naar de modules op de divers standaard plaatsen) (zie modprobe(1)).

Als alles goed is gegaan zien we met lsmod dat beide modules zijn geladen.

En dan hebben we nu dus een real-time programma lopen. Valt je iets op? Bij een trage processor zal het de lezer reeds zijn opgevallen dat Linux nu trager reageert dan normaal. Je kunt proberen het aantal omlopen van de lus in fun() te vergroten door andere waarden voor de derde parameter van rt_task_init() te gebruiken. Het is aan te bevelen om hierbij het programma ico te draaien om te zien hoeveel minder processortijd er over is want dat is het effect van het real-time programma op Linux, het lijkt alleen maar alsof de processor traag is geworden. Linux gelooft als het ware dat de processen meer tijd nodig hebben om hun werk te doen. Als de rekentijd van de taak groter wordt dan 100 microseconden dan zal Linux "hangen" omdat Linux een achtergrondtaak is en de real-time taak gebruikt dan 100% van de processortijd. Eigenlijk "hangt" Linux niet echt, het krijgt alleen de processor niet.

Communicatie tussen taken

Er is slechts één methode van communiceren onder RT-Linux: real-time FIFO (FIFO: First In First Out, de eerst binnenkomende data wordt ook weer als eerste verstuurd). Het mechanisme lijkt erg op dat van Unix PIPEs, een datastroom zonder structuur. Een FIFO is een buffer met een vastgesteld aantal bytes waarin kan worden geschreven of uit gelezen.

Met FIFOs kan er dus worden gecommuniceerd tussen taken of met gewone taken.

Gezien vanuit een gewoon proces is een FIFO een speciaal, karakter-georiënteerd, bestand. Meestal onder de naam /dev/rtf0, /dev/rtf1 enz. Deze bestanden zijn normaal gesproken niet aanwezig op Linux dus moeten ze als volgt worden aangemaakt:

  # for i in 0 1 2 3; do mknod  /dev/rtf$i c 63 $i; done 
Als er nog meer FIFOs nodig zijn kunnen deze eenvoudig extra worden aangemaakt. Het bestand vormt het interface met een afhandelingprogramma van het operating systeem. Dit programma moet er dan wél in zitten, anders is het bestand niets waard. Sterker nog; voor ieder van dit soort bestanden geldt dat het openen ervan een foutmelding oplevert als er geen afhandelingprogramma achter zit.

DrawObject

FIFOs worden nu benaderd als gewone bestanden (open, read/write, close). Voordat een gewoon Linux proces ze kan gebruiken moet een real-time programma de FIFO eerst aan hebben gemaakt.

Gezien vanuit een real-time taak worden de FIFOs gebruikt via bepaalde functieaanroepen:

Laten we naar een voorbeeld kijken wat deze functies gebruikt. Dit voorbeeld is een aangepaste versie van één van de voorbeelden uit de RT-Linux distributie (sound):

example3.c

  #define MODULE
  #include <linux/module.h>
  #include <linux/rt_sched.h> 
	 
  #include <linux/rtf.h>
  #include <asm/io.h>

  RT_TASK task;  

  static int filter(int x){
    static int oldx;
    int ret;
    if (x & 0x80) {
      x = 382 - x;
    }
    ret = x > oldx;
    oldx = x;
    return ret;
  }

  void fun(int dummy) {
    char data;
    char temp;
    while (1) {
      if (rtf_get(0, &data, 1) >  0) {
        data = filter(data);
        temp = inb(0x61);            
        temp &= 0xfd;
        temp |= (data & 1) <<  1;
        outb(temp,0x61);
      }
      rt_task_wait();
    }
  }

  int init_module(void){
    rtf_create(0, 4000);
    
    /* enable counter 2 */
    outb_p(inb_p(0x61)|3, 0x61);
    
    /* to ensure  that the output of the counter is 1 */
    outb_p(0xb0, 0x43);
    outb_p(3, 0x42);
    outb_p(00, 0x42);
    
    rt_task_init(&task, fun,  0 , 3000, 1);   
    rt_task_make_periodic(&task, 
                   (RTIME)rt_get_time()+(RTIME)1000LL, 
                   (RTIME)(RT_TICKS_PER_SEC / 8192LL));

    return 0;
  } 

  void cleanup_module(void){
    rt_task_delete(&task);  
    rtf_destroy(0);
  }
Net als in het tweede voorbeeld hebben we de module rt_prio_sched nodig, alleen moeten we nu ook de module rt_fifo_new inladen om van de FIFOs gebruik te kunnen maken.

Er wordt een periodieke, real-time taak gecreëerd met een frequentie van 8192 Hz. Deze taak leest bytes in vanuit FIFO 0 en stuurt dit door naar de luidspreker van de PC. Als we nu een geluidsbestand in ".au" formaat kopiëren naar /dev/rtf0 dan kunnen we er naar luisteren. Het moge duidelijk zijn dat dit niet om aan te horen is omdat de hardware van de PC slechts één bit heeft voor modulatie van het signaal. De testing/sound directory heeft een bestand linux.au dat kan worden gebruikt voor het testen.

Om dit te compileren en uit te voeren:

    # gcc -I /usr/src/linux/include/linux -O2 -Wall -D__KERNEL__ -D__RT__ -c example3.c
    # modprobe rt_fifo_new
    # modprobe rt_prio_sched
    # insmod example3.o
    # cat linux.au > /dev/rtf0
Merk op dat het cat commando kan worden gebruikt voor het schrijven naar welk bestand dan ook, dus ook naar device files. We kunnen ook het commando cp gebruiken.

Om te kunnen ervaren hoe het real-time gedrag van invloed is op de kwaliteit, hoeven we alleen een programma te maken wat hetzelfde doet in een gewoon Linux proces.

example4.c

  #include <unistd.h>
  #include <asm/io.h>
  #include <time.h>

  static int filter(int x){
    static int oldx;
    int ret;
    if (x & 0x80)
      x = 382 - x;
    ret = x > oldx;
    oldx = x;
    return ret;
  }
  espera(int x){
    int v;
    for (v=0; v<x; v++);
  }
  void fun() {
    char data;
    char temp;

    while (1) {
      if (read(0, &data, 1) >  0) {
        data = filter(data);
        temp = inb(0x61);
        temp &= 0xfd;
        temp |= (data & 1) << 1;
        outb(temp,0x61);
      }
      espera(3000);
    }
  }

  int main(void){
    unsigned char dummy,x;
    ioperm(0x42, 0x3,1); ioperm(0x61, 0x1,1);

    dummy= inb(0x61);espera(10);
    outb(dummy|3, 0x61);

    outb(0xb0, 0x43);espera(10);

    outb(3, 0x42);espera(10);
    outb(00, 0x42);

    fun();
  }
Dit programma kan worden gecompileerd als ieder ander programma:
  # gcc -O2 example4.c -o  example4
en om het uit te voeren:
  # cat linux.au | example4
Om poorten vanuit een normaal programma te kunnen benaderen moeten we permissie krijgen vanuit het operating systeem. Dit is een normale (en noodzakelijke) veiligheidsmaatregel om te voorkomen dat ieder programma bijvoorbeeld direct naar schijf gaat schrijven. De aanroep ioperm() vraagt het operating systeem toestemming voor het benaderen van bepaalde in- en uitvoer adressen. Alleen programma's met root permissie zullen toestemming krijgen. Een ander interessant detail is de manier waarop de frequentie van 8192 Hz voor het moduleren van het geluid wordt gegenereerd. Hoewel er een systeemaanroep is met de naam nanodelay() is de resolutie hiervan slechts enkele milliseconden, waardoor we gebruik moeten maken van een temporele klok met wachtlus. De wachtlus is dusdanig ingesteld dat het min of meer werkt op een 100 MHz Pentium.

Ik stel nu voor om example4 te testen tegelijk met het uitvoeren van het ico programma. Hoe hoor je het nu? Hoe is dit ten opzichte van de real-time versie? Heeft real-time hiermee zijn nut bewezen?

Tenslotte

Dit tweede artikel ging in op het programmeren van real-time taken. De gebruikte voorbeelden waren simpel en blonken niet uit in bruikbaarheid. In het volgende artikel zal ik een bruikbare applicatie presenteren. We zullen dan de TV kunnen besturen via Linux en, verrassing, de Linux-doos kunnen besturen via de afstandsbediening!

Referenties: