Primer gonilnika UNIX


Na najnižjem nivoju vhodno-izhodnih komunikacij imamo gonilnike (drivers). Pri pisanju novega gonilnika je priporočljivo, da si za zgled vzamemo nek obstoječi gonilnik. Kot klasični primer si bomo ogledali gonilnik za tiskalnik. Primer velja za operacijski sistem SCO UNIX, ki ga normalno uporabljamo na PC kompatibilnih računalnikih. Za te računalnike je značilno, da poteka komunikacija s periferijami (oziroma njihovimi krmilniki) preko vrat (ports). Ustrezen krmilnik (controller) ima tri registre: podatkovnega, statusnega in krmilnega, kot to ponazoruje spodnja slika:


 
 

Predpostavimo, da bralec razume pomen posameznih bitov v navedenih registrih.

V nadaljevanju sledi koda gonilnika, ki ga bomo komentirali. Zgled bi moral biti zanimiv zaradi uporabljenih programerskih prijemov.

/* Line printer driver for SCO UNIX system */ 
#include <sys/param.h> 
#include <sys/errno.h> 
#include <sys/types.h> 
#include <sys/signal.h> 
#include <sys/dir.h> 
#include <sys/user.h> 
#include <sys/tty.h> 
#include <sys/sysmacros.h>
#define LPPRIORITY PZERO+5 
#define LOWATER 50 
#define HIWATER 250 
#define TOINT HZ/10
#define LPBASE 0x378 
#define LPDATA (LPBASE + 0) 
#define LPSTAT (LPBASE + 1) 
#define LPCTRL (LPBASE + 2)
#define LPNERR 0x08 
#define LPON 0x10 
#define LPREADY 0x80
#define LPSTROBE 0x01 
#define LPLF 0x02 
#define LPINT 0x04 
#define LPSELECT 0x08 
#define LPINTENABLE 0x10
#define LPEXIST 0x01 
#define SLEEP 0x02 
#define LPBUSY 0x04 
#define TOPEND 0x08 
#define WAIT 0x10 
#define EXCLOPEN 0x20 
#define OPEN 0x40
static unsigned short LpFlags = 0; 
static struct clist LpQueue;
/*****************************************************************/ 
/* init( ) entry point.... */ 
/* 1. check if device controller responding */ 
/* 2. if device present: write corresponding message */ 
void lpinit( ) { 
    outb(LPCTRL, 0); /* clear LP control register */ 
    if (inb(LPCTRL) != 0xE0) return; 
    outb(LPCTRL, 0xFE); /* test if control register answers */ 
    if (inb(LPCTRL) != 0xFE) return; 
    outb(LPCTRL, 0); 
    printf("Line printer driver at address %x\n", LPBASE); 
    LpFlags = LPEXIST; /* set device exists flag */ }
/*****************************************************************/ 
/* open( ) entry point ......*/ 
void lpopen(dev_t dev, int flags, int otyp) { 
    if ((LpFlags & LPEXIST) == 0) u.u_error = ENODEV; /* error: no device */ 
    else if (LpFlags & EXCLOPEN) u.u_error = EBUSY; /* error: device busy */ 
    else if (LpFlags & OPEN) { 
        if (flags & FEXCL) /* check if exclusive open requested*/ 
        u.u_error = EBUSY; 
   } 
   else { 
        outb(LPDATA, 0); 
        outb(LPCTRL, LPLF | LPINIT | LPSELECT); 
        LpFlags |= OPEN; /* set device open flag */ 
        if (flags & FEXCL) /* check if exclusive open requested*/ 
        LpFlags |= EXCLOPEN; 
    } 
}
/*****************************************************************/ 
/* close( ) entry point ...... */ 
void lpclose(dev_t dev, int flags, int otyp) { 
    int x; x = spl5( ); 
    while (LpFlags & LPBUSY) { 
        LpFlags |= WAIT; 
        sleep(&LpFlags, LPPRIORITY); 
    } LpFlags = LPEXIST; 
    outb(LPCTRL, 0); 
    splx(x); 
}
/*****************************************************************/ 
/* write( ) entry point........ */ 
void lpwrite( dev_t dev) { 
    char c; int x; 
    extern void lpwork( );
    while(u.u_count) { 
        if (copyin(u.u_base, &c, 1) == -1) { 
            u.u_error = EFAULT; 
            return; 
        } 
        u.u_base++; u,u_count--; 
        while (LpQueue.c_cc >HIWATER) { 
            x = spl5( ); lpwork( ); 
            if (LpQueue.c_cc >HIWATER) { 
                LpFlags |= SLEEP; 
                sleep (&LpQueue, LPPRIORITY); 
            } 
            splx(x); 
        } 
        putc(c,&LpQueue); 
    } 
    x = spl5( ); lpwork( ); splx(x); 
}
/*****************************************************************/ 

/* local driver routine, copy the data from the clist to the printer */ 
static void lpwork( ) { 
    int ch; 
    short spinLoop;
    extern void lprestart( );
    LpFlags |= LPBUSY; /* set printer status to BUSY */ 
    while (1) { 
        /* infinite loop until break */ 
        spinLoop = 100; 
        while ((inb(LPSTAT) & (LPNERR | LPON | LPREADY )) (LPNERR | LPON | LPREADY)) 
           && --spinLoop) ; 
        if (spinLoop == 0) break; /* timeout: exit from loop */ 
        if ((ch = getc(&LpQueue)) < 0) break; /* clist empty */ 
        outb (LPDATA, ch); /* put data into printer data register */ 
        /* then send the neccessary strobe signal into CTRL reg... */ 
        outb (LPCTRL, LPSTROBE | LPLF | LPINIT | LPSELECT); 
        outb (LPCTRL, LPLF | LPINIT | LPSELECT); 
    } 
    /* if room in clist and process is sleeping waiting for room, 
       wake up sleeping process ... */ 
    if ((LpQueue.c_cc <LOWATER) && (LpFlags & SLEEP)) { 
        LpFlags &= ~SLEEP; 
        wakeup (&LpQueue); 
    } if (LpQueue.c_cc <= 0) { 
        LpFlags &= ~LPBUSY; 
        if (LpFlags & WAIT) wakeup (&LpFlags); 
    } 
    else if (( LpFlags & TOPEND) == 0) { 
        timeout (lprestart, 0, TOINT); 
        LpFlags |= TOPEND; 
    } outb (LPCTRL, LPLF | LPINIT | LPSELECT | LPINTENABLE); 
}

/************************************************************/
/* interrupt service routine */ 
void lpintr (dev_t dev); { 
    /* exit immediately if LP driver status set to busy.. */ 
    if ((LpFlags & LPBUSY) == 0) return; 
    /* if some data waiting: write data to printer port */ 
    if ( LpQueue.c_cc >0) lpwork(); 
}

/************************************************************/

/* when the timeout occurs: use restart routine */ 
static void lprestart( ) { 
    int x; 
    LpFlags &= ~TOPEND; 
    x = spl5( ); lpwork( ); splx(x); 
}
Modul začenja takoimenovani prolog, v katerem so navedeni stavki include in define. Prvi navajajo datoteke, v katerem so definirane strukture in funkcije v jedru (kernel). Tako so v datoteki tty.h definirane takoimenovane c-liste, v datoteki sysmacros.h pa makroji inb(0). in outb( ), ki jih uporabljamo za komunikacijo z vrati krmilnika (input-bute, output-byte).

S stavki define določamo naslove krmilnikovih registrov, ki si slede od nekega naslova (LPBASE) dalje. Prav tako definiramo razne maske za (re)setiranje oziroma testiranje posameznih bitov v registrih krmilnika. Tudi sam gonilnik bo imel svoj status (LpFlags), v katerem bomo setirali ali testirali posamezne bite. Ti bodo kazali, ali proces, ki uporablja gonilnik, trenutno spi (SLEEP), čaka (WAIT) oziroma ali je gonilnik zaseden (BUSY) itd.

Sam gonilnik ima več rutin. Vse začenjajo z dogovorjeno predpono, v našem primeru lp.

Prva rutina je lpinit( ), oziroma v sploąnem init( ). To kliče sistem ob zagonu operacijskega sistema. Njena naloga je, da preveri, ali je dana periferna naprava (pri nas tiskalnik) prisotna (ali lahko vplivamo na njene registre) in nato izpiše ustrezno obvestilo.

Sledita rutini open( ) oziroma close( ) (v naąem primeru lpopen(0) in lpclose( ). Obe kličemo s primernim sistemskim klicem, na primer:

open("/dev/lp", O_WRONLY | O_EXCL);
S takim klicem odpremo tiskalnik za pisanje (saj ga drugače ne moremo) in zahtevamo ekskluzivni dostop do tiskalnika.

Znanemu sistemskemu klicu write( ) ustreza rutina lpwrite(), ki pomeni zahtevek za prepis nekega polja. Pri tem pa moramo rešiti več problemov.

Gonilnik ima dostop le do tistega dela podatkov v uporabnikovem programu, ki so zanj pomembni. Ti so shranjeni v posebni strukturi u (user), ki ima v sploąnem naslednje elemente:

u_error     Povratna koda ob morebitni napaki
u_uid        Identifikacijska številka uporabnika (UID) kličočega procesa
u_gid        Identifikacija uporabniške grupe (GID) kličočega procesa
u_base      Naslov polja (buffer) v uporabniškem naslovnem prostoru
u_count    Koliko bytov želimo prenašati
u_offset    Trenutna pozicija v datoteki, s katero komuniciramo)

Kot vemo, imamo dva režima delovanja (user mode, kernel mode). Obema ustreza drug naslovni prostor procesa (user address space, kernel address space). S tem je zagotovljeno, da uporabnik ne more kar tako naslavljati ščitenih podatkovnih struktur jedra. Za prepis podatkov med obema naslovnima prostoroma potrebujemo dve posebni rutini:

copyout (kernelAddress, userAddres, count); 
copyin (userAddress, KernelAddress, count);
Prva omogoča prepis count bytov iz polja v naslovnem prostoru jedra v polje v naslovnem prostoru uporabnika. Druga ima inverzno nalogo.

Gonilnik mora tudi rešiti problem običajne počasnosti periferne naprave, s katero komunicira. Zato poteka prepis posredno preko primernega vmesnega polja. Zaradi boljše izkoriščenosti celotnega pomnilnika uporabljajo gonilniki posebno vrsto pomnilnih polj (character-liste -> c-liste), ki predstavljajo nekakąno skupno zalogo polj. V danem trenutku niso aktivni vsi gonilniki. Tako morda tudi po več tednov ne rabimo tiskalnika. Morda tudi ne uporabljamo vseh terminalov. Pomnilna polja (c-liste) naj bodo na voljo tistim gonilnikom, ki jih v danem trenutku potrebujejo. Organizacijo c-list kaže naslednja slika:

C-liste so relativno kratka polja (nekaj 100 bytov). Zato v gonilniku prepisujemo v tak medpomnilnik znake iz uporabnikovega naslovnega prostora, dokler ne dosežemo dogovorjene kritične "gladine" (high water, HIWATER). V takem primeru rutina write( ) raje zaspi, dokler se medpomnilnik ne primerno izprazni (low water, LOWATER). Za slednje skrbi prekinitvena servisna rutina (v naąem primeru lpintr( ) ).

Sam prepis naslednjega znaka iz c-liste na tiskalnik je izveden v lokalni proceduri lpwork( ).

Zanimivo je, da imamo v gonilniku tudi nekaj zank, za katere pa moramo izkustveno ugotoviti, da niso predolge oziroma da nimajo preveč iteracij. Posebno kritična je v tem smislu rutina lpwork( ), ki jo kliče tudi lpintr( ). V tem času so namreč nadaljni prekinitveni zahtevki onemogočeni (ker jedro ob prekinitvi izvede instrukcijo "disable interrupts", kliče nato lpintr( ) in šele po povratku iz nje prekinitve spet omogoči.

Ko imamo gonilnik sprogramiran, ga moramo prevesti in vgraditi v jedro. V nekaterih primerih UNIX imamo za vgradnjo na voljo poseben program. Po ponovnem zagonu računalnika (reboot) se bo tako pognala nova verzija jedra z vgrajenim gonilnikom.

Na koncu še enkrat poudarimo, da je podani primer veljaven le pri določeni verziji SCO implementacije sistema UNIX. Iz njega lahko spoznamo le koncepte gonilnikov, podrobnosti pa se od primera do primera zelo razlikujejo.

LINUX: gonilniki naprav1  gonilniki naprav 2