Xinu embedded

Xinu è un sistema operativo scritto da Comer alcuni anni fa e oggi disponibile per diverse piattaforme. Esiste anche una versione per il segmento dei sistemi dedicati chiamata Embedded Xinu.

Accanto al sistema operativo Linux, sicuramente più blasonato rispetto ad altri, esistono sistemi alternativi. Senza ricorrere in maniera specifica a sistemi definiti microkernel, esistono infatti diversi sistemi operativi alternativi, come ad esempio Minix o Xinu. Come altri sistemi operativi, i programmi eseguiti in ambiente Xinu utilizzano servizi che il sistema mette a disposizione attraverso routine (API) appositamente predisposte. Per esempio, un programma o un task possono avere la necessità di scrivere su una seriale standard, per esempio il display, e per svolgere questo compito chiamano una funzione di sistema putc(). Questa funzione scrive sulla periferica di I/O e utilizza due parametri: l’identificatore della periferica e il carattere da scrivere. Il  listato 1 mostra un esempio di questo tipo e pone in evidenza un problema e cioè che un simile programma va benissimo in un ambiente dove non esiste il concetto di concorrenza.

esempio di scrittura su una periferica di I/O

main()
graffa
      putc(CONSOLE, ‘1’);
      putc(CONSOLE, ‘2’);
graffa
Listato 1

Utilizzando un sistema operativo possono esistere diversi programmi in memoria, dove ognuno occupa una porzione definita, sia di memoria che di risorse hardware. Può succedere in un ambiente di questo tipo che diversi programmi, o processi, richiedano la stessa risorsa e in questo caso senza opportuni sistemi di controllo può accadere che sulla periferica di uscita, per esempio un monitor o una porta di comunicazione, siano presenti dei messaggi privi di senso che appartengono a diversi processi in esecuzione. Xinu è un buon sistema operativo che non ha una larga diffusione e, come ogni altro sistema di questo tipo, ha una sua gestione dei processi. La figura 1 mette in evidenza, per esempio, le diverse transazioni possibili: wait, ready, current e suspended.

Figura1: diagramma a stati.

Figura 1: diagramma a stati.

In questo articolo ci occuperemo non tanto degli aspetti propri del sistema operativo, ma semmai delle considerazioni che occorre tenere presente per portare il sistema  su un’altra piattaforma hardware. Per fare questo lavoro ci occuperemo della parte fondamentale, vale a dire la creazione di un task, l’interrupt del system clock, la gestione del cambio di contesto e i servizi generali. Questi sono gli aspetti miminali da tenere presente per portare Xinu su un altro ambiente target.

Creazione di un task

Il concetto di task è puramente un’idea virtuale, perché in realtà il processore, o sistema operativo, non esegue i diversi task in maniera simultanea, ma l’elaborazione commuta in maniera trasparente e veloce tra diverse computazioni possibili. Il sistema operativo memorizza tutte le informazioni di un processo in un’apposita zona di memoria, una struttura dati denominata tabella dei processi, o task control block (TCB). Le informazioni che sono memorizzare dipendono dai servizi che il sistema operativo mette a disposizione e dalla complessità del processore in uso.   Per esempio, in Xinu ogni processo ha la sua memoria riservata di stack e i valori dei registri del processore associato al processo sono memorizzati su questa zona di memoria. In Xinu la creazione di un processo comporta la memorizzazione di alcuni valori del processore nella tabella dei processi. Così, per creare un processo e metterlo nella coda ready, in Xinu si utilizza una chiamata di sistema del tipo: resume (create (task3, 200, 20, “Task 3”, 0) ); In questo caso si sono utilizzate due chiamate al sistema operativo: resume e create. Con create si crea un processo con indirizzo della funzione, corpo del processo, task3 come primo argomento, mentre gli altri sono utilizzati per comunicare, rispettivamente, lo spazio necessario sullo stack, la priorità, il nome del processo e gli argomenti del processo. Ogni volta che si chiama create si dà luogo a un nuovo processo che comincerà a eseguire istruzioni all’indirizzo specificato dal suo primo argomento, vale a dire task3 che è poi l’entry point della funzione C associata. La primitiva create, quindi, installa  il processo lasciandolo pronto per l’esecuzione, ma temporaneamente sospeso. Questa primitiva restituisce un identificatore che rappresenta l’informazione del processo che permette di determinare in maniera univoca il processo stesso: è in sostanza un valore numerico. Nel contempo, resume che riceve in ingresso l’identificativo del processo per prima cosa pone lo stesso nello stato di ready dallo stato di sospensione voluto da create. La funzione create inserisce nella tabella dei processi i valori ricevuti in ingresso, oltre ad alcuni parametri interni. Il gestore del cambio di contesto raccoglie le informazioni sul contenuto dei registri nello stack pointer, crea i valori appropriati per lo stack pointer, program counter e il registro di stato. Il listato 2 mostra un estratto di create con le necessarie scritture per un processore Intel Pentium.

porzione di create

   numproc++;
   pptr = &proctab[pid];
   pptr->fildes[0] = 0; /* stdin set to console */
   pptr->fildes[1] = 0; /* stdout set to console */
   pptr->fildes[2] = 0; /* stderr set to console */

   for (i=3; i < _NFILE; i++) /* others set to unused */
     pptr->fildes[i] = FDFREE;
   pptr->pstate = PRSUSP;
   for (i=0 ; i<PNMLEN && (int)(pptr->pname[i]=name[i])!=0 ; i++) ;
   pptr->pprio = priority;
   pptr->pbase = (long) saddr;
   pptr->pstklen = ssize;
   pptr->psem = 0;
   pptr->phasmsg = FALSE;
   pptr->plimit = pptr->pbase - ssize + sizeof (long);
   pptr->pirmask[0] = 0;
   pptr->pnxtkin = BADPID;
   pptr->pdevs[0] = pptr->pdevs[1] = BADDEV;

          /* Bottom of stack */
   *saddr = MAGIC;
   savsp = (unsigned long)saddr;

   /* push arguments */
   pptr->pargs = nargs;
   a = (unsigned long *)(&args) + (nargs-1); /* last argument */
   for ( ; nargs > 0 ; nargs—) /* machine dependent; copy args
   */
       *—saddr = *a—;/* onto created process’ stack */
   *—saddr = (long)INITRET; /* push on return address */

   *—saddr = pptr->paddr = (long)procaddr; /* where we “ret” to */
   *—saddr = savsp; /* fake frame ptr for procaddr */
   savsp = (unsigned long) saddr;

/* this must match what ctxsw expects: flags, regs, old SP */
/* emulate 386 “pushal” instruction */
   *—saddr = 0;
   *—saddr = 0; /* %eax */
   *—saddr = 0; /* %ecx */
   *—saddr = 0; /* %edx */
   *—saddr = 0; /* %ebx */
   *—saddr = 0; /* %esp; fill in below */
   pushsp = saddr;
   *—saddr = savsp; /* %ebp */
   *—saddr = 0; /* %esi */
   *—saddr = 0; /* %edi */
   *pushsp = pptr->pesp = (unsigned long)saddr;
Listato 2

Interrupt del system-clock

Il clock è l’elemento importante di un sistema operativo. Non stiamo parlando di un RTC, il nostro clock non deve tener conto della data. Il nostro clock è un evento asincrono che regola il funzionamento  del sistema, regolale delay che ogni task può chiamare e scandisce le operazioni temporizzate. Dalla routine assembler (vedi listato 3) si nota che le variabili slnempty e sltop sono utilizzate per elaborare in maniera ottimizzata l’interrupt di clock: in questo modo si riesce a determinare se i processi in sleeping debbano essere svegliati.

interrupt del timer di sistema

                   .text
count100:          .word 100
                   .globl clkint
clkint:
                    cli
                    pushal
                    movb $EOI,%al
                    outb %al,$OCW1_2
                    incl ctr100
                    subw $1,count100
                    ja cl1
                    incl clktime
                    movw $100,count100
cl1:
                    cmpl $0,slnempty
                    je clpreem
                    movl sltop,%eax
                    decl (%eax)
                    ja clpreem
                    call wakeup
clpreem:            decl preempt
                    ja clret
                    call resched
clret:
                    popal
                    sti
                    iret
         ret
Listato 3

In caso di rescheduling del contesto, la funzione invoca resched e il processo è messo nello stato di ready e aspetta il rescheduling del contesto.

Cambio  del contesto

Il  cambio di contesto, o context switching, è la parte fondamentale utilizzata per attivare i processi. Per prima cosa ferma l’elaborazione del processo corrente e registra le sue informazioni in un’area di memoria dedicata, in modo da poter rimettere in esecuzione  il processo in un istante diverso. Il cambio del contesto è deciso all’interno della funzione del sistema operativo resched(). All’interno di reschd() è invocata la procedura assembler ctxsw. Questa procedura, in sostanza, cambia i registri di lavoro. Il program counter deve essere posizionato per ultimo rispetto agli altri registri. Non risulta regolarmente inserito, allora il processore riprenderà l’esecuzione del nuovo processo. Si ricarica il contenuto dei registri generali, in seguito il  registro di stato, lo stack pointer e il program counter. Il listato 4 mostra la parte di codice per il Pentium.

ctxsw

/*————————————————————————————————————
* ctxsw - call is ctxsw(&oldsp, &oldmask, &newsp, &newmask)
*————————————————————————————————————
*/
ctxsw:
     pushl       %ebp
     movl        %esp,%ebp

     pushl       12(%ebp)
     call        disable
     movl        20(%ebp),%eax
     movw        (%eax),%dx
     movw         %dx,newmask
     pushfl                      /* save flags */
     pushal                      /* save general regs */
     /* save segment registers here, if multiple allowed */
     movl        8(%ebp),%eax
     movl        %esp,(%eax) /* save old SP */

     movl        16(%ebp),%eax
     movl        (%eax),%esp   /* restore new SP */
     /* restore new segment registers here, if multiple allowed */
     popal                     /* restore general registers */
     popfl /                    * restore flags */
     pushl        $newmask
     call         restore
     leave
     ret
Listato 4

Inizializzazione del sistema

La fase nota come inizializzazione del sistema è il momento più cruciale di un disegno e coinvolge gli aspetti hardware e software di un’applicazione. L’inizializzazione non è però l’ultima fase del disegno. I progettisti, quando mettono a punto un sistema, devono definire  i vari aspetti che sono coinvolti. Si sono fin qui descritti gli interventi minimali che occorre fare su Xinu per permettere  il porting su una piattaforma differente. Per questa ragione, in questo articolo sono messe in evidenza solo quelle attività di inizializzazione minime. Così, inizializzare correttamente un sistema vuol dire definire tutti gli aspetti che il sistema operativo utilizza per partire correttamente. Gli aspetti che si devono tenere presente sono sicuramente lo startup del sistema, la determinazione della dimensione della memoria disponibile e l’inizializzazione delle strutture dati utilizzate dal sistema. Lo startup del sistema è quel meccanismo che viene anche chiamato il bootstrap del sistema. All’accensione, la CPU fa partire l’esecuzione del sistema partendo da una locazione fisica univoca. Su alcuni processori questa locazione è     localizzata all’indirizzo 0xFFFFFFFF e in altri a 0x0. Nella distribuzione di Xinu è presente un file chiamato startup.s; lo scopo di questo file è quello di gestire la partenza del sistema (il bootstrap). Il  listato 5 mostra un esempio per un Pentium. La funzionalità di questo file è quella di creare un ambiente C per far partire correttamente  il sistema. In sostanza, occorre disabilitare gli interrupt, creare uno stack valido, definire la quantità di memoria disponibile e saltare alla funzione principale del programma. La funzione principale del programma è la funzione nulluser(). E’ attraverso questa funzione C che il sistema inizializza la sua struttura dati e chiama, al termine, la funzione main() che trasforma il programma in un processo.

Servizi  generali

Il  sistema operativo ha la necessità di abilitare e disabilitare gli interrupt per proteggersi durante zone critiche. Questa gestione può essere fatta solo utilizzando l’assembly, perchè è necessario interagire direttamente con le risorse fisiche della macchina. Il listato 6 mostra le due funzioni indispensabili: restore e disable. Con disable() si disabilitano gli interrupt del processore non prima di aver memorizzato il  vecchio valore del registro di stato, mentre con restore() si ricarica il contenuto del registro, compreso il registro di stato. Xinu prevede poi almeno altre funzioni, che scriveremo in assembler e quindi dipendenti dal processore: halt(), pause() ed enable(). Con enable() si abilitano tutti gli interrupt, con la funzione  halt() il processore è posto in uno stato di idle e infine con pause() rimane sospeso fino all’occorrenza di un interrupt.

Conclusione

A questo punto termina il nostro percorso attraverso la modifica di Xinu. Abbiamo affrontato solo un parziale lavoro, ma è relativamente sufficiente per portare Xinu su un’altra piattaforma. La procedure di startup del sistema operativo dipendono fortemente dall’ambiente run-time del compilatore C utilizzato per costruire  il kernel.

 

 

 

Scrivi un commento

EOS-Academy
Abbonati ora!