martedì 16 dicembre 2008

MINIX: I processi

Il file proc.c contiene tutte le principali operazioni compiute dai processi e per la gestione dei messaggi. La comunicazione tra processi,in MINIX, è effettuata attraverso lo scambio di messaggi, utilizzando delle procedure chiamate send e receive.

Le procedure che descriverò, contenute nel file proc.c cono:
  • system call : chiamata quando un processo o task vogliono eseguire una SEND e/o RECIVE
  • interrupt : utilizzato dalla routine delle interruzioni per inviare il messaggio di interrupt ad un task
  • ready: prende un processo della lista pronti e lo mette in esecuzione
  • unready: rimuove il processo dalla lista pronti
  • sched: se un processo è da tanto tempo in esecuzione, ne viene schedulato un altro
  • pick_proc: porta un processo in esecuzione
  • send: invia un messaggio tra pocessi
  • receive: per la ricezione di messaggi tra pocessi
interrupt
Quando occorre una interruzione viene chiamata la funzione interrupt, la quale prova ad inviare un messaggio di interruzione al task con identificatore passato come argomento
PUBLIC interrupt(task, m_ptr)

int task; /* number of task to be started */

message *m_ptr; /* interrupt message to send to the task */

{/* An interrupt has occurred. Schedule the task that handles it. */

il messaggio di interruzione ha come sorgente la costante che identifica l'hardware, definita nel file com.h,il destinatario ed il puntatore al buffer del messaggio

if (mini_send(HARDWARE, task, m_ptr) != OK) {

/* The message could not be sent to the task; it was not waiting. */

Se l'invio del messaggio di interruzione fallisce, (es. non era in attesa di riceverlo), si setta nella bitmap busy_map per ricordarsi cheil task destinatario è occupato e si salva il riferimento al messaggio di interruzione nel vettore message *task_mess[NR_TASKS+1] (contiene i puntatori ai messaggi dei task occupati).

Nel caso in cui la send è stata eseguita con successo, quindi l'interruzione hardware è stata inviata correttamente, si resetta a zero il bit nella busy_map per identificare che il task è di nuovo libero.
busy_map &= ~this_bit; /* turn off the bit in case it was on */

old_map = busy_map;


Se vi è qualche altro task che ha un interruzione pendente, allora la richiesta viene soddisfatta inviandogli il messaggio di interruzione ed si aggiorna la bitmap relativa ai task occupati
if (old_map != 0) {

for (i = 2; i <= NR_TASKS; i++) {

/* Check each task looking for one with a pending interrupt. */

if ( (old_map>>i) & 1) {
/* Task 'i' has a pending interrupt. */
n = mini_send(HARDWARE, -i, task_mess[i]);
if (n == OK) busy_map &= ~(1 << i);


Infine, si controlla se la lista pronti relativa ai taks non è vuota e se c'è
una processo utente in esecuzione, allora porto il task in esecuzione

/* If a task has just been readied and a user is running, run the task. */
if (rdy_head[TASK_Q] != NIL_PROC && (cur_proc >= 0 || cur_proc == IDLE))
pick_proc();


System Call
Solo le system call posso eseguire le procedure send e/o receive, per l'invio e/o ricezione dei messaggi.
Tali chiamate sono eseguite quando viene fatta una trap al kernel con una istruzione INT.
La trap viene catturata e chiamate le procedure send e/o receive .


le System Call prendono come parametri:
  • il tipo di funzione da eseguire
  • il chiamante, identifica il processo che ha chiamato la sys_call.
  • l'identificatore del processo mittente(in caso di receive) e/o destinatario(in caso di send) del messaggio
  • il puntatore al messaggio

PUBLIC sys_call(function, caller, src_dest, m_ptr)

int function; /* SEND, RECEIVE, or BOTH */

int caller; /* who is making this call */
int src_dest; /* source to receive from or dest to send to */

message *m_ptr; /* pointer to message */

Le uniche chiamate di sistema consentite ai processi utente sono dove sono presenti sia la send che la receive, altrimenti nel registro per il codice di ritorno da system call del processo chiamante verrà assegnato un codice di errore.

if (function != BOTH && caller >= LOW_USER) {

rp->p_reg[RET_REG] = E_NO_PERM; /* users only do BOTH */
return;
}

Negli altri casi si limita ha chiamare le procedure send e/o receive a secondo dei casi
if (function & SEND)

n = mini_send(caller, src_dest, m_ptr); /* func = SEND or BOTH */

if (function & RECEIVE)
n = mini_rec(caller, src_dest, m_ptr); /* func = RECEIVE or BOTH */

Send
La procedura send è utilizzata per inviare un messaggio tra due processi.
Se il destinatario è in attesa di questa comunicazione il messaggio viene copiato nell'area di memoria della parte DATI del processo destinatario ed in seguito ne viene fatta la sveglia.

Nel caso contrario, se il destinatario non è in attesa di ricevere o è in attesa di ricevere da un altro processo, il mittente(il chiamante della send) passa in attesa ed il messaggio viene messo in coda.
In minix i processi utente posso inviare solo al File System ed al processo
gestore della memoria.


I parametri sono:
  • identificatore processo mittente(il chiamante),
  • identificatore processo destinatario
  • il puntatore al messaggio da recapitare.
PUBLIC int mini_send(caller, dest, m_ptr)
int caller; /* who is trying to send a message? */
int dest; /* to whom is message being sent? */

message *m_ptr; /* pointer to message buffer */


Dagli identificatori, passati come argomento alla procedura, si accede alla tabella dei processi e si recuperano gli indirizzi del processo chiamante e del processo destinatario.

Vengono fatti controlli di validazione per verificare se il destinatario è ancora in vita e se l'intero messaggio può essere contenuto nel segmento DATI dell'aera di memoria del
processo chiamante('caller')

if (dest_ptr->p_flags & P_SLOT_FREE)
return(E_BAD_DEST); /*dead dest */

if (vhi < vlo || vhi - caller_ptr->p_map[D].mem_vir >= len)
return(E_BAD_ADDR);


Se il destinatario era bloccato in attesa di questo messaggio viene chiamata la routine
cp_mess[src, src_clicks, src_offset, dst_clicks, dst_offset] che esegue una copia
veloce di un messaggio da una parte ad un altro qualsiasi indirizzo di memoria e svegliato il destinatario,se il processo diventa runnable(dest_ptr->p_flags == 0) viene inserito nella propria lista pronti(ready).
N.B.Dopo la sveglia il destinatario si troverà il messaggio nella sua m
emoria DATI.
/* Check to see if 'dest' is blocked waiting for this message. */

if ( (dest_ptr->p_flags & RECEIVING) &&

(dest_ptr->p_getfrom == ANY || dest_ptr->p_getfrom == caller) ) {

/* Destination is indeed waiting for this message. */

cp_mess(caller, caller_ptr->p_map[D].mem_phys, m_ptr,

dest_ptr->p_map[D].mem_phys,
dest_ptr->p_messbuf);

dest_ptr->p_flags &= ~RECEIVING; /* deblock destination */
if (dest_ptr->p_flags == 0) ready(dest_ptr);
}

cp_mess è implementata direttamente in assembly,contenuta nel file klib88.asm, durante il trasferimento copia l'identificatore del processo mittente nella prima parole del messaggio
In tal modo il processo destinatario potra ricavare l'indirizzo del processo ac
cedendo alla tabella dei processi.
le restanti 11 parole del messaggio vengono copiate contenuto dall'area DATI del mittente al 'area DATI del destinatari.
Un messaggio è composto da 12 parole.Ricordiamo che in minix un
a parola è composta da 16-bit(2byte), di conseguenza l'intero messaggio consta di 24byte
;===========================================================================
; cp_mess
;===========================================
================================
Msize = 12 ; size of a message in 16-bit words
cp_mess:
...
mov es:[di],ax ; copy sender's process number to dest message
...

mov cx,Msize-1 ; remember, first word doesn't count

rep movsw ; iterate cx times to copy the message
...

ret ; that's all folks!
Nella situazione in cui il destinatario non è in attesa di ricevere la comunicazione viene salvato il puntatore al messaggio ed attraverso la procedura unready il caller passa in stato di attesa per invio non riuscito
/* Destination is not waiting. Block and queue caller. */

if (caller == HARDWARE) return(E_OVERRUN);

caller_ptr->p_messbuf = m_ptr;

caller_ptr->p_flags |= SENDING;

unready(caller_ptr);


Infine il puntatore al processo mittente(caller) viene inserito nella lista dei processi che hanno comunicazioni pendenti


Receive
E' la procedura complementare alla Send; utilizzata quando un processo o task vuole ricevere un messaggio.
In questo caso i parametri sono:
  • identificatore processo che vuole ricevere(il chiamante),
  • identificatore processo del mittente atteso
  • il puntatore al buffer per il messaggio
PRIVATE int mini_rec(caller, src, m_ptr)
int caller; /*process trying to get message */

int src; /* which message source is wanted (or ANY) */

message *m_ptr; /* pointer to message buffer */


Se la coda dei processi che hanno provato ad inviare un messaggio al chiamante non è vuota , (bloccati perché il destinatario non era in attesa del messaggio), si cerca un mittente con il messaggio desiderato (ANY, prende il primo messaggio presente in coda).
Tale messaggio viene acquisisce attraverso cp_mess (descritta in precedenza), che fa la copia dalla memoria fisica dell'area dati del processo sender alla memoria fisica della parte dati del processo destinatario.

Dopo la copia si sblocca il mittente, attraverso la procedura ready che inserisce il descrittore di processo mittente nella opportuna lista pronti, di conseguenza viene aggiornata la coda dei processi delle comunicazione pendenti con il chiamante, eliminando il processo mittente utilizzato per la receive.
caller_ptr = proc_addr(caller);/* pointer to caller's proc structure */

/* Check to see if a message from desired source is already available. */

sender_ptr = caller_ptr->p_callerq;

while (sender_ptr != NIL_PROC) {

sender = sender_ptr - proc - NR_TASKS;

if (src == ANY || src == sender) {

/* An acceptable message has been found. */

cp_mess(sender, sender_ptr->p_map[D].mem_phys, sender_ptr->p_messbuf,
caller_ptr->p_map[D].mem_phys, m_ptr);

sender_ptr->p_flags &= ~SENDING; /* deblock sender */

if (sender_ptr->p_flags == 0) ready(sender_ptr);


Se il messaggio richiesto non è ancora disponibile, si salva lo stato della comicazione e si blocca il chiamante.

caller_ptr->p_getfrom = src;

caller_ptr->p_messbuf = m_ptr;

caller_ptr->p_flags |= RECEIVING;

unready(caller_ptr);


Infine si verifica se ci sono segnali pendenti da parte del kernel al processo gestore memoria, quindi viene chiamata la procedura assembly inform(MM_PROC_NR) per informare il gestore

if (sig_procs > 0 && caller == MM_PROC_NR && src == ANY) inform(MM_PROC_NR);


pick_proc
Questa funzione è utilizzata principalmente per settare 'cur_proc' e 'proc_ptr',rispettivamente l'identificatore al processo corrente e il puntatore alla tabella dei processi
Se il sistema è inattivo 'cur_proc' e 'proc_ptr' vengono settati ad un valore speciale IDLE, per identificare lo stato di inattività.

Per impostare il processo corrente controlla se è presente un processo nelle liste pronti, in ordine di priorità, verifica : quella dei task, dei processi server e di quelli utente.
register int q; /* which queue to use */

if (rdy_head[TASK_Q] != NIL_PROC) q = TASK_Q;
else if (rdy_head[SERVER_Q] != NIL_PROC) q = SERVER_Q;

else q = USER_Q;

if (rdy_head[q] != NIL_PROC) {

/* Someone is runnable. */

cur_proc = rdy_head[q] - proc - NR_TASKS;

proc_ptr = rdy_head[q];


Se è in esecuzione il task per il controllo del ciclo di clock, 'cur_proc' = CLOCKTASK.

Viene settato anche il puntatore al processo bill_ptr che conta i cicli di clock della CPU.

ready
La ready inserisce il puntatore al processo(al descrittore di processo), passato come argomento, alla propria lista pronti ricavandola dall'identificatore del processo.
PUBLIC ready(rp)

lock(); /* disable interrupts */
r = (rp - proc) - NR_TASKS; /* task or proc number */

q = (r < 0 ? TASK_Q : r < LOW_USER ? SERVER_Q : USER_Q);

...

// aggiunge rp in rdy_head[q]

..

restore(); /* restore interrupts to previous state */

Tutte le operazioni vengono effettuate ad interruzioni disabilitate, chiamanto la procedura lock definita a livello assembler e alla fine viene chiamata la
procura assembly restore ripristinare le interruzioni allo stato precedente.
In tal modo siamo sicuri queste operazioni vengono eseguite in maniera indivisibile, senza essere interrotte da interruzioni.
Il codice assembly delle per le operazioni lock e restore sono presenti nel file klib88.asm

;===========================================================================

; lock
;===========================================================================

; Disable CPU interrupts.
lock:

pushf ; save flags on stack
cli ; disable interrupts
pop lockvar ; save flags for possible restoration later
ret ; return to caller

;===========================================================================
; restore
;===========================================================================
; Restore enable/disable bit to the value it had before last lock.
restore:
push lockvar ; push flags as they were before previous lock
popf ; restore flags
ret ; return to caller

unready
Rimuove il processo ,il cui puntatore è passato come parametro, dalla sua lista pronti.
Il codice è simile alla ready, ma in questo caso viene fatta la rimozione anzichè l'inserzione dalla lista rdy_head[q].
Un processo può essere in stato unready anche se un segnale di kill l'ha terminato
.
Anche in questa procedura su utilizza la lock e restore per le stesse motivazioni della procedura descritta in precedenza


sched
Descrive la politica di scheduling per i processi utente. Se un processo è da troppo tempo in esecuzione ed un altro è presente in lista pronti. Il processo corrente viene inserito in fondo alla lista pronti utente.
La procedura viene eseguita ad interruzioni disabilitae
PUBLIC sched()
{
lock(); /* disable interrupts */
if (rdy_head[USER_Q] == NIL_PROC) {
restore(); /* restore interrupts to previous state */

return;
}
/* One or more user processes queued. */
rdy_tail[USER_Q]->p_nextready = rdy_head[USER_Q];

rdy_tail[USER_Q] = rdy_head[USER_Q];

rdy_head[USER_Q] = rdy_head[USER_Q]->p_nextready;

rdy_tail[USER_Q]->p_nextready = NIL_PROC;

pick_proc();

restore(); /* restore interrupts to previous state */


Per capire meglio uso che i processi fanno delle primitive di comunicazion
e mi rifaccio al classio esempio di un processo 'APPL' che vuole leggere dal disco; 'DRIVER' è il processo gestore del disco con il quale altri processi attraverso opportune send e/o receive
Nel compilato del processo APPL ci sarà il riferimento alle procedure di comunicazione che il kernel minix mette a disposizione.


Es. In pseudocodice,nel compilato dei processi, avremo:

APPL:
...
send(driver,“LETTURA N parole”)

recieve(driver,risultato)
...

DRIVER:
...

recieve(appl,“LETTURA N parole ”)
send(appl,letti())

...

Il prossimo post su minix descriverò file mpx88.asm che contiene lo strato più basso del kernel Minix, sono quasi 300 righe di codice assembly utilizzare per implementare lo switching dei processi, gestione dei messaggi, la procedura save() che contiene il codice per salvare lo stato della macchina quando occorre una Trap o un' Int,etc

Daniele Licari

Nessun commento: