AngoLinux

L'esperienza in Quarta

- A cura del Prof. Stefano Salvi -


Sommario

  1. Introduzione
  2. L'esercizio proposto
  3. Le funzioni per la gestione dei processi
  4. Le funzioni per creare le pipe
  5. La libreria vgalib
  6. Le strutture, le Costanti e le Variabili Globali
  7. La Funzione main ()
  8. La Funzione arrow () che Disegna la Freccia
  9. La Funzione guardia () che Gestisce la Guardia
  10. La Funzione ladro () che Gestisce il Ladro
  11. La Funzione campo () che Gestisce il Campo
  12. L'attributo "setuid"
  13. Il "Progetto" di Wpe
  14. Il sorgente Completo

Introduzione

Nella classe quarta della sperimentazione Abacus per Periti Informatici si studiano i sistemi operativi.

In particolare, si studia il multitasking, si descrivono i metodi di comunicazione tra processi e si parla anche di protezione degli accessi.

Questi argomenti (salvo la protezione degli accessi, che può essere sperimentato tramite il sistema di rete) venivano trattati in maniera puramente teorica.

L'unico modo per poter sperimentare questi argomenti in maniera semplice, senza dover introdurre una grande quantità di informazioni aggiuntive, è quelli di introdurre Linux.

L'esperienza che abbiamo introdotto in quarta è stata quella di produrre un semplice "videogioco" che sfrutti il multitasking e la comunicazione tra processi.

Per produrre questo semplice gioco è stato necessario introdurre anche l'uso della grafica in Linux tramite la libreria vgalib.

Dato inoltre che gli studenti erano abituati a programmare in C e C++ usando l'ambiente di programmazione integrato della Borland, si è deciso di utilizzare l'ambiente integrato wpe che riproduce in buona misura tale ambiente sotto Linux

Passiamo ora a descrivere il progetto.

L'esercizio proposto

L'esercizio proposto consiste in questo:

Scrivere un programma in linguaggio C che simuli una guardia che rincorre un ladro. In particolare:

Queste sono le specifiche fondamentali. A queste potremo aggiungere alcune specifiche aggiuntive per semplificare la realizzazione:

Come si vede, la posizione del problema è molto facile. Anche l'esercizio in sé non è difficile da svolgere.

Per svolgerlo servono però alcune informazioni aggiuntive sulle funzioni particolari da usare. In particolare servirà sapere come usare, sulle funzioni per la gestione dei processi, per creare le pipe e la libreria vgalib.

Le funzioni per la gestione dei processi

Per generare un nuovo processo Linux usa il metodo di creare due copie esatte del programma, che però avranno vita indipendente.

Una delle due copie sarà quella che ha deciso di generare il secondo processo e prenderà il nome di padre (parent).

La seconda copia sarà; il processo nuovo e prenderà il nome di figlio.

Per riconoscere i processi uno dall'altro Linux associa ad ognuno un numero, chiamato process identifier o più in sintesi pid.

Utilizzando il pid è possibile riferirsi ad un processo per inviargli un messaggio o un segnale.

Ogni processo quando termina ritorna al processo che lo ha generato (il padre) un codice di terminazione o di errore. Questo codice corrisponde all' errorlevel utilizzato dai programmi DOS. Il codice di terminazione è il valore di ritorno della funzione main () o il parametro della funzione exit () a seconda di come termina il programma.

Noi possiamo visualizzare i processi presenti nel sistema con il comando ps descritto nell'introduzione a Linux.

Un processo padre ha l'obbligo di leggere il codice di ritorno di tutti i suoi figli. Se necessario, un segnale indica al padre la terminazione di un figlio.

Dal momento in cui un figlio termina al momento in cui il padre ne legge il codice di terminazione, se noi chiediamo la lista dei processi con ps, vedremo che esso è marcato come <zombie>.

Per generare un processo noi dobbiamo usare la funzione fork (). Questa funzione non ha parametri e ritorna un valore. Un solo programma (il padre) invoca questa funzione. Quando invece la funzione torna, i programmi sono due, e ciascuno dei due riceve il valore di ritorno della funzione.

Come fa allora un processo a sapere se è padre o figlio?

Deve guardare il valore di ritorno della funzione fork. Se questa funzioe ritorna zero allora il processo è il figlio.

Se la funzione ritorna un numero maggiore di zero allora il processo è il padre ed il numero è il pid del figlio.

Se infine la funzione ritorna un numero minore di zero allora il processo è il padre, ma la funzione non è riuscita a generare un nuovo processo e quindi ha segnalato un errore.

Naturalmente, a seconda che il processo sia padre o figlio farà cose diverse. Per comportarsi da padre o da figlio il programma potrà fare in if sul codice di ritorno della funzione fork.

Una cosa molto importante da capire è che sia il padre che il figlio hanno le stesse variabili, ma le variabili del figlio sono una copia delle variabili del padre, fatta nel momento in cui si è chiamata la funzione fork.

Questo implica che, se il padre aveva aperto un file, la variabile che punta al file nel figlio avrà lo stesso valore, quindi punterà allo stesso file, sul quale potrà operare.

Naturalmente, però, se noi modificheremo una variabile del figlio, la corrispondente variabile nel padre (che è un'altra copia) manterrà il valore precedente.

Una volta generati dei processi, un buon padre deve aspettarne la fine. Per ottenere il valore di ritorno di un processo, un padre può utilizzare la funzione wait o la funzione waitpid.

La funzione wait è più semplice. Ha un unico parametro che deve essere un puntatore ad un intero. In questo intero la funzione lascerà il valore di terminazione del processo figlio, oltre ad un codice che indicherà se il processo è terminato regolarmente o a causa di un errore.

Il valore di ritorno di wait invece sarà il pid del processo che è terminato.

Naturalmente, se un processo ha generato più figli dovrà attendere il termine di tutti i suoi figli, tenendo in qualche modo traccia di quali sono terminati.

Un'altra funzione che attende il codice di terminazione di un processo è la waitpid. Questa funzione ha tre parametri. Il primo è il pid del processo o del gruppo di processi del quale attendere il termine. Se si indica zero come pid la waitpid attende un qualunque processo figlio (come la funzione wait).

Il secondo è come prima il puntatore alla variabile che conterrà il valore di terminazione.

Il terzo infine sarà un intero contenente le opzioni. Ce ne sono diverse. Per conoscerle si dovrà consultare il manuale, con il comando man 2 wait, indicando la sezione 2 per ottenere il manuale della funzione C e non quello del comando. L'unica opzione che indico è WNOHANG che può essere usato per fare in modo che la funzione ritorni anche se nessun processo è terminato.

Il valore di ritorno è lo stesso che per la funzione wait.

Le funzioni per creare le pipe

Il secondo strumento da utilizzare servirà per far comunicare i tre processi creati.

Per far comunicare i processi abbiamo scelto di utilizzare il meccanismo delle pipe. Una pipe si utilizza come se fosse un file, ma i dati scritti su una pipe non vengono registrati su un dispositivo di memorizzazione ed i dati letti da una pipe non vengono letti da un dispositivo di memorizzazione.

Le pipe vengono create in coppia ed i dati scritti su di un lato della pipe possono essere letti dall'altro lato della pipe.

Se noi consegniamo ad un processo l'handle (il riferimento) ad un lato della pipe ed ad un altro processo l'handle dell'altro lato della pipe i due processi potranno comunicare tra loro tramite la pipe.

Quando noi scriviamo una riga di comando come la seguente:

ls | less

la shell (vale a dire l'interprete di comandi di Linux, l'equivalente di command.com del DOS) creerà una pipe che sarà sostituita allo standard output (il file stdout, su cui scrivono le funzioni printf, puts e putchar) del comando ls ed allo standard input (il file stdin dal quale leggono le funzioni getchar, gets e scanf) del comando less.

In questo modo ciò che verrà stampato da ls verrà ricevuto da less.

Per creare una pipe in Linux si deve usare la funzione pipe. Questa funzione vuole come parametro un puntatore ad un array di due interi nei quali verranno lasciati, se la funzione ha successo, gli handle dei due file connessi agli estremi della pipe.

La funzione ritorna zero se tutto va bene, ed i due file sono connessi alla pipe. Se invece si incontra un errore, la funzione ritorna -1 e il codice dell'errore sarà lasciato nella variabile globale di sistema errno.

Naturalmente vi sarete accorti che ci siamo sempre riferiti ad handle di file (di tipo int) invece che a puntatori a file (del tipo FILE *) come siamo abituati ad usare per i nostri file.

Questo è dovuto al fatto che in genere noi usiamo l'interfaccia di alto livello verso i file (la cosiddetta interfaccia buffered) che consente un uso più efficiente per i file di testo, in quanto mantiene una parte del file in memoria (il cosiddetto buffer del file) e quindi le letture e scritture di singoli caratteri sono eseguite in maniera molto rapida.

Esiste al contrario un'interfaccia ai file di basso livello (la cosiddetta interfaccia unbuffered) che non mantiene in memoria pezzi di file, ma accede in maniera più diretta possibile al file system. Naturalmente, l'interfaccia buffered è ottenuta attraverso delle funzioni della libreria del C che gestiscono il buffer ed a loro volta, per leggere e scrivere il file sul disco, chiamano le funzioni unbuffered.

Esiste anche una funzione, tra le bufferd, che consente di associare un file buffered ad un handle di un file unbuffered, ma, dato il tipo di comunicazione che dovremo implementare, non useremo questa funzione.

Per i file unbuffered non esistono funzioni che leggano o scrivano stringhe come definite dal C ma solo blocchi di memoria, come variabili semplici, array o record, tutti di dimensione fissa.

Le due funzioni che consentono di leggere e scrivere blocchi su file unbuffered sono le funzioni read e write. Queste funzioni hanno entrambe tre parametri:

Il valore di ritorno sarà il numero di byte scritti o letti, se la funzione ha funzionato, oppure -1 in caso di errore. In questo secondo caso la variabile globale errno conterrà il codice dell'errore. È possibile che il numero di byte letti sia inferiore al richiesto. Questo non è un errore ma semplicemente che si è raggiunta la fine del file.

Per finire, quando non si ha più bisogno di un file unbuffered lo si può "chiudere" con la funzione close che avrà come solo parametro l'handle del file da chiudere.

La libreria vgalib

Per poter visualizzare i nostri "personaggi" ci serviranno pochissime funzioni grafiche. Dovremo poter entrare ed uscire dalla modalità grafica selezionata, dovremo poter disegnare una linea e poter scegliere il colore con cui disegnare.

Oltre a questo ci serviranno anche delle funzioni che ci dicano la larghezza e l'altezza dello schermo in pixel ed il numero di colori disponibili.

Vediamo quindi le funzioni che utilizzeremo:

int vga_init(void)
È la prima funizone da chiamare, per inizializzare la libreria. Questa funzione non cambia il modo corrente della scheda video, vale a dire resta in modo testo.
int vga_getdefaultmode(void)
indica quale modo grafico è preferibile utilizzare con la scheda video ed il monitor installati nel sistema.
int vga_setmode(int mode)
Con questa funzione si cambia la modalità del video. Il mode determina se si deve attivare una modalità grafica o ritornare in modo testo.
I valori che useremo per mode saranno G640x480x16 per attivare la modalità grafica in 640 per 480 pixel a 16 colori, G320x200x256 per attivare la modalità grafica in 320 per 200 pixel a 256 colori e TEXT per ritornare in modo testo.
int vga_hasmode(int mode)
controlla se il modo video mode è disponibile.
int vga_getxdim(void)
ritorna la larghezza in pixel dello schermo nella modalità corrente. Dopo aver inizializzato la modalità grafica, la funzione ci ritornerà 640. Non utilizziamo direttamente il valore 640, in modo che per cambiare risoluzione dello schermo ci basterà semplicemente inizializzare il nuovo modo grafico.
int vga_getydim(void)
ritorna l'altezza in pixel dello schermo nella modalità corrente. Dopo aver inizializzato la modalità grafica, la funzione ci ritornerà 480. Non utilizziamo direttamente il valore 480, in modo che per cambiare risoluzione dello schermo ci basterà semplicemente inizializzare il nuovo modo grafico.
int vga_getcolors(void)
ritorna il numero di colori disponibili nella palette corrente. I colori disponibili avranno indici da 0 a vga_getcolors() - 1.
int vga_setcolor(int color)
seleziona il colore con il quale si disegnerà. In genere il colore 0 corrisponderà al nero e 15 al bianco.
int vga_drawline(int x1, int y1, int x2, int y2)
disegna una linea dal punto (x1,y1) al punto (x2,y2) usando il colore selezionato con vga_setcolor.
Bisogna ricordare che l'origine degli assi è in alto a sinistra e che la X aumenta verso destra mentre la Y aumenta verso il basso.
int vga_drawpixel(int x, int y)
colora il pixel alle coordinate (x,y) usando il colore selezionato con vga_setcolor.
Bisogna ricordare che l'origine degli assi è in alto a sinistra e che la X aumenta verso destra mentre la Y aumenta verso il basso.

Le strutture, le Costanti e le Variabili Globali

Per prima cosa indicheremo i file header da utilizzare per le varie funzioni e costanti utilizzate nel programma:

#include <stdlib.h>
#include <stdio.h>
#include <vga.h>
#include <time.h>
#include <errno.h>
#include <signal.h>

Seguono alcune costanti generali che definiscono il comportamento del gioco:

#define BORDER  40      /* Bordo da rispettare in pixel */
#define ERR     7       /* Codice di errore */
#define GCOLOR  3       /* Colore della guardia */
#define LCOLOR  4       /* Colore del ladro */
#define PASSO   8       /* Ogni quanto il ladro cambia direzione */

Troviamo poi le variabili globali che descrivono lo schermo. Ricordo che queste variabili saranno copiate anche nei processi figli.

int XMax;
int YMax;
int CMax;
int VGAMODE;

Vengono indicati i prototipi delle funzioni. La definizione delle funzioni stesse verrà più avanti.

void arrow (int x,int y, int dir, int color);
void guardia (int outfile,int infile);
void ladro (int outfile,int infile);
int campo (int ginfile,int linfile, int goutfile, int loutfile);

Definiremo più avanti due array: uno che descriverà le frecce (i nostri personaggi) in base alla direzione, ed un altro nel quale indicheremo la variazione della X e della Y in base alla direzione.

Definiremo allora delle costanti che mettano in relazione le direzioni indicate in base ai punti cardinali all'indice negli array corrispondente.

/* Indici delle direzioni */
#define         O       0
#define         SO      1
#define         S       2
#define         SE      3
#define         E       4
#define         NE      5
#define         N       6
#define         NO      7

Per definire una freccia ci servono quattro punti, vale a dire gli estremi dell'asta, nell'ordine ed il termine dei due lati inclinati della freccia.

Se noi indichiamo per primo il vertice della freccia, per disegnarla basta tracciare tre linee a partire da questo punto fino a ciascuno degli altri tre.

Per finire, per descrivere un punto occorrono due interi, uno per la X ed uno per la Y.

Se noi mettiamo questi dati in un array, potremo scrivere una funzione che disegni una freccia passandole per parametro solamente l'array di punti che descrive la freccia.

Potremo mettere le frecce in un array, disposte per direzione secondo le costanti descritte più sopra. Vediamo che forma deve avere questo array.

Un punto sarà descritto come un array di due interi, una freccia come un array di quattro punti ed il nostro array sarà un array di otto frecce, quindi sarà un array di interi con tre indici.

La sue definizione sarà la seguente:

int arrows [8][4][2] = {
   { { 0 , 10 } , { 20 , 10 } , { 10 , 0 } , { 10 , 20 } },     /* Sinistra */
   { { 0 , 20 } , { 20 , 0 } , { 0 , 0 } , { 20 , 20 } },       /* Sinistra,giu */
   { { 10 , 20 } , { 10 , 0 } , { 0 , 10 } , { 20 , 10 } },     /* Giu */
   { { 20 , 20 } , { 0 , 0 } , { 0 , 20 } , { 20 , 0 } },       /* Destra,giu */
   { { 20 , 10 } , { 0 , 10 } , { 10 , 0 } , { 10 , 20 } },     /* Destra */
   { { 20 , 0 } , { 0 , 20 } , { 0 , 0 } , { 20 , 20 } },       /* Destra,su */
   { { 10 , 0 } , { 10 , 20 } , { 0 , 10 } , { 20 , 10 } },     /* Su */
   { { 0 , 0 } , { 20 , 20 } , { 20 , 0 } , { 0 , 20 } },       /* Sinistra,su */
};

Per il movimento, potremo definire alcune costanti. DELTA è lo spostamento elementare. Lo metteremo in una costante per poter variare in maniera quasi immediata la velocità dei "personaggi".

LDELTA e GDELTA sono i moltiplicatori di velocità rispettivamente del ladro e della guardia.

#define DELTA   1       /* Spostamento per 'frame' */

#define LDELTA  5       /* Spostamento ladro per frame */
#define GDELTA  3       /* Spostamento guardia per frame */

Per definire lo spostamento, in base alla direzione, dovremo indicare una quantità da aggiungere alla X ed una quantità da aggiungere alla Y in base alla direzione.

Se questa quantità sarà positiva, il personaggio andrà a destra o in basso.

Se questa quantità sarà negativa, il personaggio andrà a sinistra o in alto.

Se la quantità sarà nulla, non ci sarà spostamento in quella direzione.

Naturalmente nessuno spostamento potrà avere entrambe le componenti nulle.

Se metteremo gli spostamenti in un array, sempre in ordine di direzione, avremo un array di interi a due dimensioni, una coppia di spostamenti per ogni direzione. Vediamone la definizione:

int move [8][2] = {
   { -DELTA, 0 },       /* Sinistra */
   { -DELTA, DELTA },   /* Sinistra,giu */
   { 0, DELTA },        /* Giu */
   { DELTA, DELTA },    /* Destra,giu */
   { DELTA, 0 },        /* Destra */
   { DELTA, -DELTA },   /* Destra,su */
   { 0, -DELTA },       /* Su */
   { -DELTA, -DELTA },  /* Sinistra,su */
};

Per finire ci sarà utile una struttura che descriva il nostro "personaggio".

In questa struttura dovremo introdurre le coordinate del personaggio e la sua direzione.

Questa struttura ci sarà molto utile anche perché sarà quella che verrà trasmessa da processo a processo tramite le pipe.

Vediamo come la possiamo dichiarare:

typedef struct {
   int x,y,dir;
} point;

La Funzione main ()

La funzione main è la funzione principale di ogni programma C.

Il programma non necessita di parametri e non ha bisogno di ritornare un codice di ritorno, quindi scegliamo il prototipo void main (void) senza ne parametri ne valore di ritorno.

Di seguito alla graffa di apertura della funzione troviamo le variabili. I commenti descrivono il loro uso.

void main(void)
{
int g_c [2];    /* pipe guardia-campo */
int l_c [2];    /* pipe ladro-campo */
int c_g [2];    /* pipe campo-guardia */
int c_l [2];    /* pipe campo-ladro */
int gpid,lpid;  /* PID della guardia e del ladro */
int gret,lret;  /* valore di ritorno della guardia e del ladro */
int r;

La prima operazione che dobbiamo fare è quella di creare le pipe per far comunicare i processi fra loro.

Come descritto più sopra, usiamo la funzione pipe. Se questa funzione ritorna un valore diverso da 0, si è verificato un errore, quindi stampiamo un messaggio di errore e terminiamo l'esecuzione del programma.

    if (pipe (g_c))
    {
        printf ("Non riesco a creare il canale verso la guardia\n");
        return;
    }

Con lo stesso metodo potremo creare le altre tre pipe e verificarne la corretta creazione.

    if (pipe (l_c))
    {
        printf ("Non riesco a creare il canale verso il ladro\n");
        return;
    }

    if (pipe (c_g))
    {
        printf ("Non riesco a creare il canale dalla guardia\n");
        return;
    }

    if (pipe (c_l))
    {
        printf ("Non riesco a creare il canale dal ladro\n");
        return;
    }

A questo punto, inizializzeremo il sistema grafico.

    vga_init();

Una volta inizializzato il sistema grafico, controlliamo se esiste una modalità grafica preferenziale.

    VGAMODE = vga_getdefaultmode();

Se non c'è un modo preferenziale, la funzione vga_getdefaultmode ritorna -1. In questo caso sceglieremo come modo grafico quello a 640 per 480 pixel a 16 colori, che viene indicato dalla costante G640x480x16.

    if (VGAMODE == -1)
        VGAMODE = G640x480x16;  /* Default mode. */

È sempre possibile che la scheda grafica non possegga questa modalità grafica. Prima di creare guai cercando di impostare la scheda in una modalità che non possiede, verifichiamo con vga_hasmode se la modalità è lecita.

Se la scheda non può utilizzare la modalità grafica scelta, stampiamo un messaggio di errore e terminiamo il programma.

    if (!vga_hasmode(VGAMODE)) 
    {
        printf("Modalita' grafica non disponibile.\n");
        exit(-1);
    }

A questo punto è finalmente possibile attivare la modalità grafica. Sarebbe opportuno controllare il valore di ritorno di questa funzione, in quanto ancora alcune cose potrebbero andare male, ma per semplicità non lo facciamo. Incoraggiamo il lettore ad aggiungere questo utile controllo.

    vga_setmode(VGAMODE);

A questo punto possiamo interrogare i valori della scheda grafica, con le funzioni viste più sopra. Faccio notare che l'inizializzazione della scheda grafica viene eseguita a questo punto della funzione main piuttosto che nella funzione campo perché i dati del video (le dimensioni in particolare) devono essere conosciute anche dai processi figli guardia e ladro e, se non inizializiamo qui la grafica, le copie delle variabili nei processi figli non conterranno le dimensioni esatte.

    XMax = vga_getxdim ();
    YMax = vga_getydim ();
    CMax = vga_getcolors ();

Per avere un comportamento sempre diverso del ladro, è opportuno inizializzare il generatore di numeri casuali con un valore diverso ad ogni avvio del programma. Usiamo l'orario di avvio del programma, che ci garantisce una sufficiente varietà.

    srand (time ((time_t *) 0));

È giunto il momento di generare i processi figli. Cominciamo dalla guardia.

Per generare il processo figlio, usiamo la funzione fork descritta più sopra. Questa funzione ritorna un valore che può essere:

Nel primo caso eseguiremo esclusivamente la funzione guardia e poi termineremo il programma (che è uno dei figli). Alla funzione guardia dovranno essere passati come parametro il lato in scrittura della pipe tra guardia e campo ed il lato in lettura della pipe tra campo e guardia.

Nel secondo caso, essendosi verificato un errore, stamperemo un messaggio e termineremo il programma.

Nel terzo caso invece proseguiremo con gli altri compiti della funzione main.

    gpid = fork ();     /* crea il processo 'guardia' */
    if (!gpid)
    {   /* se e' il processo figlio, esegue la 'guardia' */
        guardia ( g_c [1] , c_g [0] );
        return;
    }
    else if (gpid < 0)
    {   /* Errore !!! */
        printf ("Non riesco a generare la guardia\n");
        return;
    }

La generazione del processo del ladro segue lo stesso schema, salvo che la funzione chiamata sarà la ladro e le saranno passati i lati opportuni delle pipe relative al ladro.

    lpid = fork ();     /* crea il processo 'ladro' */
    if (!lpid)
    {   /* se e' il processo figlio, esegue il ladro */
        ladro (l_c [1],c_l [0]);
        return;
    }
    else if (lpid < 0)
    {   /* Errore !!! */
        printf ("Non riesco a generare il ladro\n");
        return;
    }

La funzione campo verrà eseguita dal processo padre e sarà quindi eseguita qui.

A questa funzione dovranno essere passati i lati opportuni di tutte e quattro le pipe, in quanto il campo fungerà anche da centro di comunicazione per il sistema.

La funzione campo ritorna un valore che è zero se è andato tutto bene e diverso da zero se ha incontrato un errore. Immagazzineremo questo valore nella variabile r.

    r = campo (g_c [0], l_c [0], c_g [1],c_l [1]); /* Il padre fa il 'campo' */

Al termine della funzione campo il gioco è finito. Prima di uscire dalla modalità grafica attenderemo la pressione di un tasto (in realtà dovrà essere un invio) per consentire di vedere la situazione finale.

    getchar();

Finito il gioco ed ottenuta la conferma, usciamo dalla modalità grafica ritornando in modalità testo.

    vga_setmode(TEXT);

La funzione campo ha un valore di ritorno, che abbiamo immagazzinato nella variabile r. Se questo valore è diverso da zero allora è avvenuto un errore nella funzione. L'errore è un errore di sistema quindi stampiamo tramite la funzione perror il corrispondente messaggio.

    if (r)
    {
        perror ("Errore in 'campo'");
    }

Per terminare attendiamo il valore di terminazione dei due processi, dei quali conosciamo il pid. Per semplicità attendiamo il termine "per nome" invece di attendere un valore generico e controllare quale dei due processi è terminato ecc.

    waitpid (gpid,&gret,0);
    waitpid (lpid,&lret,0);

A questo punto è veramente tutto finito, quindi terminiamo il programma con la funzione exit e con codice di terminazione zero per dire che è andato tutto bene.

    exit(0);
}

La Funzione arrow () che Disegna la Freccia

Per disegnare il nostro "personaggio", scriviamo una funzione. Questa funzione verrà utilizzata per disegnare e cancellare sia il ladro che la guardia.

Per cancellare sia la guardia che il ladro basta disegnare il "personaggio" con il colore dello sfondo, vale a dire, nero.

Questa funzione ha quattro parametri:

  1. un int per la X della punta della freccia
  2. un int per la Y della punta della freccia
  3. un int per la direzione della freccia, vale a dire per l'indice nell'array delle frecce.
  4. un int per il colore della freccia. Questo parametro corrisponderà al nero se stiamo cancellando la guardia o il ladro, al colore della guardia se stiamo disegnando la guardia o al colore del ladro se infine stiamo disegnando il ladro.

La dichiarazione della funzione sarà quindi la seguente:

void arrow (int x,int y, int dir, int color)
{

La prima operazione da fare a questo punto è quella di selezionare il colore con cui disegnare la freccia. Per questo useremo la funzione vga_setcolor descritta più sopra.

Faccio presente che sia la funzione vga_sercolor che successivamente la funzione vga_drawline ritornano un codice di errore. Dato che a questo punto tutti i possibili errori sono stati controllati in anticipo, si trascura di verificare questo codice di errore.

   vga_setcolor (color);

Per finire disegniamo la freccia, con la tecnica descritta, tracciando tre linee a partire dal primo punto della freccia scelta, verso ciascuno degli altri tre.

Naturalmente nell'array la freccia è descritta con la punta nelle coordinate (0,0). Per disegnare la freccia nella posizione voluta si devono aggiungere ad ognuno dei punti le coordinate della punta, passate alla funzione come parametro.

Per questo ovviamente utilizzeremo la funzione vga_drawline.

   vga_drawline (arrows [dir][0][0] + x, arrows [dir][0][1] + y,
      arrows [dir][1][0] + x, arrows [dir][1][1] + y);
   vga_drawline (arrows [dir][0][0] + x, arrows [dir][0][1] + y,
      arrows [dir][2][0] + x, arrows [dir][2][1] + y);
   vga_drawline (arrows [dir][0][0] + x, arrows [dir][0][1] + y,
      arrows [dir][3][0] + x, arrows [dir][3][1] + y);
}

La Funzione guardia () che Gestisce la Guardia

La funzione guardia è la funzione che svolge tutto il lavoro del processo figlio della guardia.

Questa funzione viene chiamata immediatamente dopo la creazione del processo. Il processo termina immediatamente dopo il termine di questa funzione.

I parametri passati a questa funzione sono gli handle delle due pipe che serviranno per comunicare con il campo.

void guardia (int outfile,int infile)
{

Per prima cosa definiremo le variabili globali.

Le due variabili di tipo point manterranno la posizione e la direzione del ladro e della guardia. Verranno anche usate per la comunicazione tra i processi.

Lo spostamento è indicato come variazione della X (dx) e variazione della Y (dy).

Sulla tangente della direzione torneremo più avanti per descriverne il significato e l'uso.

point l,g;      /* Le strutture per il ladro e la guardia */
int dx,dy;      /* Lo spostamento da fare */
double d;       /* La tangente della direzione */

Inizializziamo ora la posizione e la direzione della guardia. La guardia all'inizio sarà in basso a destra nel campo e sarà diretta a ovest (vale a dire verso sinistra).

   g.x = XMax - BORDER;
   g.y = YMax - BORDER;
   g.dir = O;

Comincia ora il ciclo del gioco, rappresentato da un ciclo do - while del quale vedremo la condizione di terminazione alla fine.

   do {

La prima operazione da ripetere per ogni ciclo è quella di leggere dalla pipe di ingresso la posizione del ladro, per poterlo inseguire.

      read (infile,&l,sizeof (l));

Adesso calcoliamo la "mossa" decisa al passo precedente. Usando la direzione della guardia, andiamo a prendere nell'array delle mosse gli incrementi da usare per la X e per la Y.

Moltiplichiamo questi incrementi per la "velocità" GDELTA della guardia, quindi li sommiamo alla posizione della guardia stessa.

      g.x += move [g.dir][0] * GDELTA;
      g.y += move [g.dir][1] * GDELTA;

Fatta la mossa, facciamo un piccolo controllo per vedere se siamo usciti dal campo e finiti nel bordo.

Se siamo nel bordo, invertiamo la nostra direzione. Visto che le direzioni sono in ordine e sono otto, se sommiamo quattro alla direzione, troviamo la direzione opposta.

Naturalmente, visto che le direzioni sono otto, aggiungendo quattro alla direzione potremmo raggiungere direzioni non esistenti. Dato però che le direzioni sono in cerchio e dopo l'ottava ritroviamo la prima, useremo l'operazione di modulo per passare da un'eventuale direzione illecita a quella lecita che ci interessa.

      if (g.x < BORDER || g.x > XMax - BORDER ||
         g.y < BORDER || g.y > YMax - BORDER)
            g.dir = (g.dir + 4) % 8;

Solo se non siamo fuori dal campo, visto che sappiamo dove è il ladro, calcoliamo la distanza tre la guardia ed il ladro, facendo la differenza tra le coordinate.

      else
      {
         dx = g.x - l.x;        // < 0 => O; > 0 => E
         dy = l.y - g.y;        // < 0 => N; > 0 => S
                        // l'asse y ha direzione opposta (+ = basso)

Se la distanza sull'asse delle X è nulla, il ladro è sopra o sotto la guardia. Dato che questo è un caso particolare della tangente, viene gestito a parte.

         if (!dx)
         {  // Verticale

Visto che la direzione da usare è verticale, dobbiamo ora stabilire il verso. Per questo controlliamo il degno della distanza lungo l'asse Y per decidere se andare verso Nord o Sud.

            if (dy > 0)
                g.dir = N;
            else g.dir = S;
         }

A questo punto calcoliamo la tangente, che non è che il rapporto tra la distanza in Y e quella in X. La tangente indica la direzione ma non il verso della distanza.

         d = (double) dy / (double) dx;

Adesso vediamo che cosa fare di questa tangente. Per prima cosa, vediamo che segno ha questo rapporto:

In realtà questa funzione à antisimmetrica sia rispetto all'asse X che rispetto all'asse Y, vale a dire che è simmetrica ma con segno invertito. Vista questa antisimmetria, possiamo ora studiare la funzione solamente in uno dei quadranti.

Per semplicità, per non distrarci con i segni, ragioneremo sul valore assoluto della nostra funzione e nel primo quadrante.

Notiamo per prima cosa che nella direzione verticale il valore di x è 0, quindi il rapporto è infinito, ma in un programma una divisione con divisore zero darà errore di divisione per zero invece che il valore infinito. Per questo motivo abbiamo trattato come caso particolare la direzione verticale.

Se osserviamo la direzione orizzontale, invece sarà la y ad essere nulla. In questo caso la nostra tangente sarà nulla, infatti il suo segno si inverte attraversando l'asse orizzontale.

Per finire possiamo osservare la diagonale. Questa linea è definita come quella linea per la quale il valore della x è uguale al valore della y. La tangente in questa direzione varrà 1.

Nel nostro caso noi dovremo discriminare tra le direzioni degli assi e delle diagonali, che sono le nostre otto direzioni lecite. Dovremo quindi basare la nostra scelta non sugli assi o sulle diagonali ma sulle bisettrici degli angoli tra assi e diagonali.

Vediamo allora che valori assumerà la tangente su queste bisettrici:

Notiamo che la bisettrice tra la diagonale e l'asse X è quella linea per la quale il valore della x è il doppio del valore della x. La nostra tangente avrà un valore di 0,5.

Notiamo che invece la bisettrice tra la diagonale e l'asse Y è quella linea per la quale il valore della y è il doppio del valore della x, quindi la nostra tangente avrà un valore di 2.

Osserviamo quindi che se la tangente ha un valore compreso tra -0,5 e -2 assumeremo come direzione la diagonale del primo quadrante (direzione NE-SO), se ha un valore compreso tra 0,5 e 2 assumeremo come direzione la diagonale del secondo quadrante (direzione SE-NO), se avrà un valore compreso tra -0,5 e 0,5 assumeremo una direzione orizzontale (direzione E-O) ed infine se il valore sarà maggiore di 2 o minore di -2 la direzione sarà verticale (direzione N-S).

Naturalmente, una volta stabilita la direzione occorre anche definire il verso. Per questo, utilizzeremo il segno della distanza lungo l'asse X.

Vediamo ora come scrivere le disequazioni che implementano questo ragionamento:

         if (d > 2 || d < -2)
         {
            if (dy > 0)
                g.dir = S;
            else g.dir = N;
         }
         else if (d > -0.5 && d < 0.5)
         {
            if (dx > 0)
                g.dir = O;
            else g.dir = E;
         }
         else if (d > 0)
         {  /* tra .5 e 2 */
            if (dx > 0)
                g.dir = SO;
            else g.dir = NE;
         }
         else
         {  /* tra -.5 e -2 */
            if (dx > 0)
                g.dir = NO;
            else g.dir = SE;
         }
      }

A questo punto non ci resta che comunicare al campo la nuova situazione, scrivendo sulla pipe verso il campo il record che ci descrive.

      write (outfile,&g,sizeof (g));

E siamo finalmente giunti al termine del ciclo di gioco.

Al campo è demandato il compito di determinare la fine del gioco. Per segnalarla alla guardia ed al ladro, pone la direzione del ladro nel caso della guardia e della guardia ne caso del ladro a -1, valore chiaramente illegale. La condizione di terminazione sarà quindi una direzione minore di zero.

   } while (l.dir >= 0);
}

La Funzione ladro () che Gestisce il Ladro

Passiamo a descrivere la funzione che gestisce il ladro.

Il ladro deve scappare in maniera casuale. Ogni PASSO spostamenti esso cambierà direzione. Per evitare che il movimento sia troppo caotico, la direzione potrà variare al massimo di più o meno quarantacinque gradi.

La funzione, come quella della guardia, riceverà come parametri gli handle del lato opportuno delle due pipe che collegano il ladro al campo.

La pipe verso il campo servirà, come per la guardia, per comunicare la posizione del ladro al campo.

La pipe nella direzione opposta invece servirà solamente per trasportare un flag (una variabile booleana) che indicherà la fine del gioco.

Vediamo quindi la definizione:

void ladro (int outfile,int infile)
{

Passiamo alle variabili locali. Il point p, come al solito servirà per mantenere la posizione e direzione del ladro, nonché per inviarlo al campo tramite la pipe.

L' int i è il contatore della mossa. Viene utilizzata per sapere quando cambiare direzione.

L' int j serve per leggere il flag di terminazione.

point p;
int i,j;

Passiamo all'inizializzazione.

Il ladro partirà dall'angolo in alto a sinistra con direzione Est, vale a dire verso destra.

   p.x = BORDER;
   p.y = BORDER;
   p.dir = E;

Inizia ora il ciclo del gioco. Per evitare che, a causa di un errore il ladro vaghi all'infinito, poniamo un limite di 1000 mosse al gioco.

   for (i = 0;i < 1000;i ++)
   {

Per prima cosa ad ogni ciclo del gioco comunicheremo il nostro stato al campo tramite la pipe. In caso di errore (se i dati scritti nella pipe non sono tanti quanti si vorrebbe), termineremo immediatamente.

      if (write (outfile,&p,sizeof (p)) != sizeof (p))
         return;

Quindi si eseguirà la mossa (come nella guardia) moltiplicando gli spostamenti corrispondenti alla direzione per il passo del ladro LDELTA.

      p.x += move [p.dir][0] * LDELTA;
      p.y += move [p.dir][1] * LDELTA;

Il cambiamento di direzione va eseguito ogni PASSO cicli. Sceglieremo l'ultimo ciclo di ogni gruppo di PASSO cicli, vale a dire quello per il quale il ciclo modulo PASSO vale PASSO - 1, vale a dire il suo massimo.

      if (i % PASSO == PASSO - 1)
      {

Per prima cosa, ad ogni cambiamento di direzione valutiamo se il ladro sia uscito dal campo e finito nel bordo.

In questo caso, per riportarlo in campo lo faremo voltare, sempre di un solo "scatto", vale a dire sempre di più o meno quarantacinque gradi.

In questo modo il ladro entrerà nel bordo ed eventualmente ci resterà per un certo tempo, ma sicuramente ne uscirà.

Se, ad esempio, la posizione è a sinistra del bordo sinistro (x < BORDER), noi dovremo voltare verso Est.

Per fare quest incrementeremo la direzione se è prima dell'Est (da Ovest a Sud-Est), la decrementeremo altrimenti.

          if (p.x < BORDER)
          {  /* 0 -> sinistra (O) */
            if (p.dir < E)
               p.dir ++;
            else
               p.dir --;
          }

Se la posizione è a destra del bordo destro (x > XMax - BORDER), noi dovremo voltare verso Ovest.

Per fare questo decrementeremo la direzione se è prima dell'Est (da Ovest a Sud-Est), la incrementeremo altrimenti.

In questo modo è anche possibile raggiungere direzioni illegali (-1 o 8), ma in questo caso il valore sarà corretto più avanti.

          else if (p.x > XMax - BORDER)
          {  /* 4 -> Destra (E) */
            if (p.dir < E)
               p.dir --;
            else
               p.dir ++;
          }

Se la posizione è sopra il bordo superiore (y < BORDER), noi dovremo voltare verso Sud.

Per fare questo decrementeremo la direzione se è oltre il Sud prima del Nord, la incrementeremo altrimenti.

In questo modo è anche possibile raggiungere direzioni illegali (-1 o 8), ma in questo caso il valore sarà corretto più avanti.

          else if (p.y < BORDER)
          { /* 6 -> Su (N) */
            if (p.dir > S && p.dir <= N)
               p.dir --;
            else
               p.dir ++;
          }

Se la posizione è sotto il bordo inferiore (y > YMax - BORDER), noi dovremo voltare verso Nord.

Per fare questo incrementeremo la direzione se è oltre il Sud prima del Nord, la decrementeremo altrimenti.

In questo modo è anche possibile raggiungere direzioni illegali (-1 o 8), ma in questo caso il valore sarà corretto più avanti.

          else if (p.y > YMax - BORDER)
          {  /* 2 -> Giu (S) */
            if (p.dir >= S && p.dir < N)
               p.dir ++;
            else
               p.dir --;
          }

A questo punto, se il ladro era in campo, cambiamo direzione. Scegliamo tra tre possibilità, vale a dire la direzione precedente, la stessa o la successiva.

Generiamo quindi un numero casuale compreso tra 0 e 2, quindi, aggiungendo -1 lo trasformiamo in un numero tra -1 e 1.

Per finire aggiungiamo questo numero alla direzione corrente per trovare la nuova.

          else p.dir += (random () % 3) - 1;

Con le operazioni precedente abbiamo ottenuto la nuova direzione, ma potremmo anche avere una direzione nuova di -1 (se la vecchia era 0 e la variazione -1) oppure 8 (se la vecchia era 7 e la variazione 1).

Naturalmente la direzione precedente a 0 (ovest) è 7 (nord-ovest) e la successiva a 7 (nord-ovest) è 0 (ovest). In pratica dobbiamo valutare la direzione modulo otto.

Se però noi usiamo l'operatore modulo, 8 viene riportato a 0 ma -1, essendo minore di -8, resta immutato. Allora, invece di usare l'operazione modulo, otterremo lo stesso risultato facendo un and bit a bit con 7.

Questa tecnica dà gli stessi risultati, ma è applicabile solo nel caso che il modulo che ci interessa sia una potenza di due. In questo caso infatti modulo - 1 si trasforma in un numero binario composto da una serie di 0 seguito da una serie di 1.

Facendo un and bit a bit con il nostro numero, terremo solo le cifre binarie che bastano per costituire un numero nel campo voluto, azzerando le cifre di ordine superiore.

Il meccanismo che seguiamo è lo stesso che useremmo se, per prendere un numero modulo 10 considerassimo solo la cifra decimale più bassa del numero, azzerando le altre.

          p.dir &= 7;       /* se uso il 'modulo', rischio un indice negativo */
      }

Per finire leggiamo dalla pipe di ingresso il nostro flag. Se non riusciamo a leggerlo, ritorniamo immediatamente.

      if (read (infile,&j,sizeof (j)) < sizeof (j))
        return;

Se il flag è diverso da zero vale a dire vero, termineremo il loop del gioco.

      if (j)
         break;
   }

Al termine del loop del gioco, per indicare al campo ed alla guardia di terminare, inviamo un'ultima posizione, ma con la direzione illegale -1. Questa posizione servirà come segnale di terminazione.

   p.dir = -i;
   write (outfile,&p,sizeof (p));   
}

La Funzione campo () che Gestisce il Campo

La funzione campo ha il compito di mettere in comunicazione guardia e ladro, di disegnare le frecce e di verificare la condizione di "ladro catturato".

Dovendo gestire le comunicazioni, il campo riceve come parametro i lati opportuni di tutte le pipe.

Cominciamo a vedere la funzione:

int campo (int ginfile,int linfile, int goutfile, int loutfile)
{

Per le nostre operazioni servono quattro variabili locali:

int j,r;
point l,g;

Vediamo l'inizializzazione. Le uniche variabili da inizializzare sono le posizioni della guardia e del ladro.

All'inizio queste posizioni non sono note, dato che saranno la guardia ed il ladro a comunicarcele. Per questo motivo inizializzeremo la direzione a -1, valore illegale, per indicare che la posizione non è valida.

    l.dir = -1; // inizia senza freccia
    g.dir = -1; 

Entriamo ora nel loop del gioco. La condizione di terminazione la troveremo in fondo.

    do {

Per prima cosa troviamo il loop di ritardo. Questo metodo per rallentare il movimento è veramente barbaro e non andrebbe utilizzato, usando al suo posto delle primitive che sospendano il processo.

Però, per non complicare il programma e non dover spiegare ulteriori funzioni di libreria abbiamo usato questo metodo inaffidabile ed inefficiente.

Consigliamo il lettore di sostituire questo metodo con uno migliore.

Il ritardo viene ottenuto facendo un ciclo a vuoto un numero molto elevato di volte. Faccio notare che il numero 300000 può essere usato in quanto gli interi che usiamo sono tutti a 32 bit.

      for (j = 0;j < 300000; j ++);  /* Ritardo */

Per incominciare, cancelliamo il ladro, disegnandolo con colore nero, vale a dire con colore 0.

Naturalmente lo cancelleremo solo se l'avevamo disegnato, vale a dire se la direzione è legale, maggio re o uguale a 0.

      if (l.dir >= 0) arrow (l.x,l.y,l.dir,0);       /* Cancella freccia */

Ore leggiamo dal ladro, tramite la pipe la nuova posizione. Se la lettura fallisce, terminiamo il programma.

      if (read (linfile,&l,sizeof (l)) != sizeof (l))
      {
         return -1;
      }

Una volta letta la posizione, possiamo disegnare il ladro usando il colore del ladro, vale a dire LCOLOR. Naturalmente, disegneremo il ladro solo se la direzione sarà valida, altrimenti il gioco sarà finito.

      if (l.dir >= 0) arrow (l.x,l.y,l.dir,LCOLOR);  /* Disegna freccia */

Ora dobbiamo comunicare alla guardia la posizione del ladro da seguire.

Ancora, se si verifica un errore terminiamo il gioco.

      if (write (goutfile,&l,sizeof (l)) != sizeof (l))
      {
        return -1;
      }

Passiamo ad occuparci della guardia. Come per il ladro, per prima cosa la cancelliamo disegnandola in nero, ma solo se la direzione è valida.

      if (g.dir >= 0) arrow (g.x,g.y,g.dir,0);       /* Cancella freccia */

Quindi leggiamo la nuova posizione dalla pipe dalla guardia, e se non ci riusciamo, terminiamo il gioco.

      if (read (ginfile,&g,sizeof (g)) != sizeof (g))
      {
         return -1;
      }

Per finire, se la direzione è valida, disegniamo la guardia con il suo colore GCOLOR.

      if (g.dir >= 0) arrow (g.x,g.y,g.dir,GCOLOR);  /* Disegna freccia */

Finito il lavoro di disegno e comunicazione, non ci resta che calcolare se il ladro è stato catturato.

Per verificare se la guardia ha catturato il ladro, verifichiamo se le coordinate della guardia sono all'interno di un quadrato che si estende di ERR pixel in ogni direzione a partire dalle coordinate del ladro.

Il verificarsi di questa condizione viene registrato nella variabile r, che è utilizzata come variabile booleana.

      r = (g.x > l.x - ERR && g.x < l.x + ERR && 
         g.y > l.y - ERR && g.y < l.y + ERR);

Naturalmente, questa variabile è il flag di terminazione usato dal ladro, e quindi verrà inviato al ladro tramite la pipe.

      write (loutfile,&r,sizeof (r));

Abbiamo terminato le operazioni del gioco. Vediamo la condizione di terminazione del loop.

Come abbiamo visto nella funzione ladro, se termina il loop del gioco, per esaurimento mosse o cattura del ladro, questa funzione invia un'ultima posizione nella quale la direzione è minore di zero.

La condizione di terminazione sarà allora che la direzione del ladro sia minore di zero.

    } while (l.dir >= 0);

Il valore negativo nella direzione del ladro, al termine del gioco, è il numero di mosse fatte dal ladro prima di terminare. Prima di finire, quindi, lo stampiamo.

    printf ("Gara conclusa in %d mosse\n", - l.dir);

A questo punto non ci resta che finire, ritornando valore zero per tutto OK.

    return 0;    
}

L'attributo "setuid"

La libreria vgalib funziona accedendo direttamente ai registri ed alla memoria della scheda video.

Un utente qualunque del sistema non ha la possibilità di accedere a memoria di sistema ed a registri di schede di interfaccia.

Solo l'amministratore di sistema, vale a dire l'utente root ha questo diritto.

Allora, solo root potrà giocare al nostro gioco?

No, c'è un'altra soluzione.

Tra i diritti di un file ne esiste uno, che si chiama SETUID che indica che, se questo file viene eseguito, l'UID o user identifier del processo sarà cambiato in quello del file.

Se allora il proprietario dell'eseguibile del nostro gioco sarà root ed il file avrà attributo SETUID, il nostro gioco potrà essere correttamente eseguito.

Ogni utente può cambiare il proprietario dei propri file in qualunque altro tramite il comando:

chown <utente> <file>

Quindi, nel nostro caso, potremo dare il comando:

chown root game

posto che l'eseguibile del gioco si chiami game.

Un poco più complesso è dare l'attributo SETUID. Visto che ogni utente può cambiare il proprietario dei propri file a root, se fosse in grado anche di settare per questi file l'attributo setuid, ogni utente sarebbe in grado di creare programmi con il massimo dei diritti sul sistema, rendendo assolutamente inefficace il meccanismo di protezione.

Per questo grave motivo di sicurezza, solo l'utente root ha il diritto di attivare il bit SETUID per un file.

Se noi quindi volgiamo settare l'attributo SETUID per il nostro gioco dovremo fare login come root, quindi settare l'attributo SETUID del nostro file con il comando:

chmod +s,a+w game

Il parametro +s indica di settare l'attributo SETUID. Non ci dobbiamo dimenticare di settare anche a+w, in quanto a questo punto il fine non è più di nostra proprietà, quindi, se non diamo a tutti il diritto di scrittura, non saremo in grado di compilare il programma in questo file.

Il "Progetto" di Wpe

Abbiamo detto che intendiamo utilizzare l'ambiente wpe per editare e compilare il nostro gioco.

Questo ambiente ci mette a disposizione un potente editor, un sistema per raggiungere le pagine man, un sistema per invocare il compilatore e per visualizzare nell'editor gli errori prodotti dal compilatore.

Dato che il nostro programma dovrà utilizzare anche una libreria, sarà necessario creare un progetto (project) per indicare l'inclusione nel link della libreria.

Il progetto viene registrato in un file che termina con .prj. Nel nostro caso sarà il file game.prj.

In questo file sono da notare due righe: la riga LDFLAGS contiene i parametri da passare al linker. L'unico parametro che passeremo al linker sarà il nome della libreria, quindi /usr/lib/libvga.so.

La seconda è FILES che indica i file da compilare. Questa riga nel nostro caso conterà il percorso completo del nostro unico file, vale a dire il percorso completo di game.c.

#
# xwpe - project-file: /home/stefano/game/game.prj
# createted by xwpe version Version 1.4.2
#
 

CMP=    gcc
CMPFLAGS=       -g
LDFLAGS=        /usr/lib/libvga.so
EXENAME=        game
CMPSWTCH=       gnu
CMPMESSAGE=     '${?*:warning:}\"${FILE}\", line ${LINE}:* at or near * \"${COLUMN=AFTER}\"'

FILES=  /home/stefano/game/game.c   

install:

Il Sorgente Completo

Abbiamo visto, una per una, le righe del nostro programma. Riporto qui il sorgente completo, tutto di seguito.

Se volete provare il programma, potete selezionare il sorgente qui di seguito, "copiarlo" usando il menù edit o modifica del browser, quindi incollarlo in un qualunque editor e salvarlo con il nome game.c in una sottodirectory della vostra home directory.

In alternativa potete salvare l'intero documento, selezionando file di testo come formato del documento, poi cancellare dal file la relazione e tenere solo questo sorgente.

/* game.c 
 * Giochino scemo scritto usando 'vgalib'
 * e i costrutti del multitasking
 * - fork per 'figliare' i processi
 * - 'pipe' per farli comunicare
 */
#include <stdlib.h>
#include <stdio.h>
#include <vga.h>
#include <time.h>
#include <errno.h>
#include <signal.h>

#define BORDER  40      /* Bordo da rispettare in pixel */
#define ERR     7       /* Codice di errore */
#define GCOLOR  3       /* Colore della guardia */
#define LCOLOR  4       /* Colore del ladro */
#define PASSO   8       /* Ogni quanto il ladro cambia direzione */
 
int XMax;
int YMax;
int CMax;
int VGAMODE;

void arrow (int x,int y, int dir, int color);
void guardia (int outfile,int infile);
void ladro (int outfile,int infile);
int campo (int ginfile,int linfile, int goutfile, int loutfile);

/* Indici delle direzioni */
#define         O       0
#define         SO      1
#define         S       2
#define         SE      3
#define         E       4
#define         NE      5
#define         N       6
#define         NO      7

int arrows [8][4][2] = {
   { { 0 , 10 } , { 20 , 10 } , { 10 , 0 } , { 10 , 20 } },     /* Sinistra */
   { { 0 , 20 } , { 20 , 0 } , { 0 , 0 } , { 20 , 20 } },       /* Sinistra,giu */
   { { 10 , 20 } , { 10 , 0 } , { 0 , 10 } , { 20 , 10 } },     /* Giu */
   { { 20 , 20 } , { 0 , 0 } , { 0 , 20 } , { 20 , 0 } },       /* Destra,giu */
   { { 20 , 10 } , { 0 , 10 } , { 10 , 0 } , { 10 , 20 } },     /* Destra */
   { { 20 , 0 } , { 0 , 20 } , { 0 , 0 } , { 20 , 20 } },       /* Destra,su */
   { { 10 , 0 } , { 10 , 20 } , { 0 , 10 } , { 20 , 10 } },     /* Su */
   { { 0 , 0 } , { 20 , 20 } , { 20 , 0 } , { 0 , 20 } },       /* Sinistra,su */
};

#define DELTA   1       /* Spostamneto per 'frame' */

#define LDELTA  5       /* Spostamento ladro per frame */
#define GDELTA  3       /* Spostamento guardia per frame */

int move [8][2] = {
   { -DELTA, 0 },       /* Sinistra */
   { -DELTA, DELTA },   /* Sinistra,giu */
   { 0, DELTA },        /* Giu */
   { DELTA, DELTA },    /* Destra,giu */
   { DELTA, 0 },        /* Destra */
   { DELTA, -DELTA },   /* Destra,su */
   { 0, -DELTA },       /* Su */
   { -DELTA, -DELTA },  /* Sinistra,su */
};

typedef struct {
   int x,y,dir;
} point;

void main(void)
{
int g_c [2];    /* pipe guardia-campo */
int l_c [2];    /* pipe ladro-campo */
int c_g [2];    /* pipe campo-guardia */
int c_l [2];    /* pipe campo-ladro */
int gpid,lpid;  /* PID della guardia e del ladro */
int gret,lret;  /* valore di ritorno della guardia e del ladro */
int r;

    if (pipe (g_c))
    {
        printf ("Non riesco a creare il canale verso la guardia\n");
        return;
    }
   
    if (pipe (l_c))
    {
        printf ("Non riesco a creare il canale verso il ladro\n");
        return;
    }

    if (pipe (c_g))
    {
        printf ("Non riesco a creare il canale dalla guardia\n");
        return;
    }

    if (pipe (c_l))
    {
        printf ("Non riesco a creare il canale dal ladro\n");
        return;
    }

    vga_init();
    VGAMODE = vga_getdefaultmode();
    if (VGAMODE == -1)
        VGAMODE = G640x480x16;  /* Default mode. */

    if (!vga_hasmode(VGAMODE)) 
    {
        printf("Modalita' grafica non disponibile.\n");
        exit(-1);
    }

    vga_setmode(VGAMODE);
    
    XMax = vga_getxdim ();
    YMax = vga_getydim ();
    CMax = vga_getcolors ();

    srand (time ((time_t *) 0));

    gpid = fork ();     /* crea il processo 'guardia' */
    if (!gpid)
    {   /* se e' il processo figlio, esegue la 'guardia' */
        guardia ( g_c [1] , c_g [0] );
        return;
    }
    else if (gpid < 0)
    {   /* Errore !!! */
        printf ("Non riesco a generare la guardia\n");
        return;
    }
 
    lpid = fork ();     /* crea il processo 'ladro' */
    if (!lpid)
    {   /* se e' il processo figlio, esegue il ladro */
        ladro (l_c [1],c_l [0]);
        return;
    }
    else if (lpid < 0)
    {   /* Errore !!! */
        printf ("Non riesco a generare il ladro\n");
        return;
    }
   
    r = campo (g_c [0], l_c [0], c_g [1],c_l [1]); /* Il padre fa il 'campo' */
        
    getchar();

    vga_setmode(TEXT);

    if (r)
    {
        perror ("Errore in 'campo'");
    }

    waitpid (gpid,&gret,0);
    waitpid (lpid,&lret,0);

    exit(0);
}
   
void arrow (int x,int y, int dir, int color)
{
   vga_setcolor (color);
   vga_drawline (arrows [dir][0][0] + x, arrows [dir][0][1] + y,
      arrows [dir][1][0] + x, arrows [dir][1][1] + y);
   vga_drawline (arrows [dir][0][0] + x, arrows [dir][0][1] + y,
      arrows [dir][2][0] + x, arrows [dir][2][1] + y);
   vga_drawline (arrows [dir][0][0] + x, arrows [dir][0][1] + y,
      arrows [dir][3][0] + x, arrows [dir][3][1] + y);
}

void guardia (int outfile,int infile)
{
point l,g;      /* Le strutture per il ladro e la guardia */
int dx,dy;      /* Lo spostamento da fare */
double d;       /* La tangente della direzione */

   g.x = XMax - BORDER;
   g.y = YMax - BORDER;
   g.dir = O;
   
   do {
      read (infile,&l,sizeof (l));

      g.x += move [g.dir][0] * GDELTA;
      g.y += move [g.dir][1] * GDELTA;

      if (g.x < BORDER || g.x > XMax - BORDER ||
         g.y < BORDER || g.y > YMax - BORDER)
            g.dir = (g.dir + 4) % 8;
      else
      {      
          dx = g.x - l.x;       // < 0 => O; > 0 => E
          dy = l.y - g.y;       // < 0 => N; > 0 => S
                            // l'asse y ha direzione opposta (+ = basso)
      
          if (!dx)
          {  // Verticale
            if (dy > 0)
               g.dir = N;
            else g.dir = S;
          }
    
          d = (double) dy / (double) dx;
      
          if (d > 2 || d < -2)
          {
            if (dy > 0)
               g.dir = S;
            else g.dir = N;
          }
          else if (d > -0.5 && d < 0.5)
          {
            if (dx > 0)
               g.dir = O;
            else g.dir = E;
          }
          else if (d > 0)
          {  /* tra .5 e 2 */
            if (dx > 0)
               g.dir = SO;
            else g.dir = NE;
          }
          else
          {  /* tra -.5 e -2 */
            if (dx > 0)
               g.dir = NO;
            else g.dir = SE;
          }
      }
      
      write (outfile,&g,sizeof (g));
   } while (l.dir >= 0);
}

void ladro (int outfile,int infile)
{
point p;
int i,j;

   p.x = BORDER;
   p.y = BORDER;
   p.dir = E;
   
   for (i = 0;i < 1000;i ++)
   {
      if (write (outfile,&p,sizeof (p)) != sizeof (p))
         return;

      p.x += move [p.dir][0] * LDELTA;
      p.y += move [p.dir][1] * LDELTA;
      if (i % PASSO == PASSO - 1)
      {
          if (p.x < BORDER)
          {  /* 0 -> sinistra (O) */
            if (p.dir < E)
               p.dir ++;
            else
               p.dir --;
          }
          else if (p.x > XMax - BORDER)
          {  /* 4 -> Destra (E) */
            if (p.dir < E)
               p.dir --;
            else
               p.dir ++;
          }
          else if (p.y < BORDER)
          { /* 6 -> Su (N) */
            if (p.dir > S && p.dir <= N)
               p.dir --;
            else
               p.dir ++;
          }
          else if (p.y > YMax - BORDER)
          {  /* 2 -> Giu (S) */
            if (p.dir >= S && p.dir < N)
               p.dir ++;
            else
               p.dir --;
          }
          else p.dir += (random () % 3) - 1;
          p.dir &= 7;       /* se uso il 'modulo', richio un indice negativo */
      }

      if (read (infile,&j,sizeof (j)) < sizeof (j))
        return;
        
      if (j)
         break;
   }
   p.dir = -i;
   write (outfile,&p,sizeof (p));   
}

int campo (int ginfile,int linfile, int goutfile, int loutfile)
{
int j,r;
point l,g;

    l.dir = -1; // inizia senza freccia
    g.dir = -1; 
    
    do {
      for (j = 0;j < 300000; j ++);  /* Ritardo */
      if (l.dir >= 0) arrow (l.x,l.y,l.dir,0);       /* Cancella freccia */
      if (read (linfile,&l,sizeof (l)) != sizeof (l))
      {
         return -1;
      }
      if (l.dir >= 0) arrow (l.x,l.y,l.dir,LCOLOR);  /* Disegna freccia */
      
      if (write (goutfile,&l,sizeof (l)) != sizeof (l))
      {
        return -1;
      }
      
      if (g.dir >= 0) arrow (g.x,g.y,g.dir,0);       /* Cancella freccia */
      if (read (ginfile,&g,sizeof (g)) != sizeof (g))
      {
         return -1;
      }
      if (g.dir >= 0) arrow (g.x,g.y,g.dir,GCOLOR);  /* Disegna freccia */
      r = (g.x > l.x - ERR && g.x < l.x + ERR && 
         g.y > l.y - ERR && g.y < l.y + ERR);
      write (loutfile,&r,sizeof (r));
    } while (l.dir >= 0);
    printf ("Gara conclusa in %d mosse\n", - l.dir);
    return 0;    
}


[Home Page dell'ITIS "Fermi"] [Indice Angolinux] [Precedente] [Successivo]