[About] [Copertina] |
Articoli
Nonostante un driver intelligente debba essere capace di riconoscere l'hardware di cui si occupa, l'autodetection non e' sempre l'implementazione piu' adatta, perche' puo' essere difficile da realizzare. Una scelta saggia e' permettere di specificare i dettagli relativi all'hardware durante il caricamento del modulo, in modo da provare il funzionamento del driver prima di affrontare l'implementazione della `autodetection'. Inoltre, l'autodetection puo' fallire se nel calcolatore esiste hardware `simile' a quello cercato. E un piccolo progetto puo' tranquillamente evitare di implementare l'autodetection. Per configurare un driver durante il caricamento useremo la capacita' di insmod di assegnare dei valori arbitrari alle variabili del modulo. Dichiareremo quindi una variabile globale per ogni elemento configurabile. e ci assicureremo che il valore di default serva per scatenare l'autodetection. La configurazione di piu' di una scheda durante il caricamento del modulo e' lasciato come esercizio per il lettore (dopo aver letto la pagina del manuale di insmod); l'implementazione mostrara qui permette di specificare i parametri per una singola scheda: al fine di mantenere semplice il codice le ulteriori schede possono solo essere raggiunte tramite autodetection.
La scelta dei nomiIl kernel e' un'applicazione complessa, ed e' importante mantenere il suo spazio dei nomi il piu' pulito possibile. Questo significa che bisogna sia usare simboli privati (static) non appena possibile, sia usare un prefisso comune per tutti i simboli che vengono definiti. Il dirver nella sua forma finale esportera' solo i simboli init_module() e cleanup_module(), che vengono usati per caricare e scaricare il module, unitamente alle variabili di configurazione a load-time. Niente altro deve essere pubblico, in quanto il modulo viene utilizzato tramite i puntatori registrati nel kernel, non tramite il nome delle funzioni. Ciononostante, durante lo sviluppo e la prova del codice, e' buona cosa vedere tutte le funzioni e le strutture dati nella tabella dei simboli del kernel, in modo da poterle vedere con il proprio debugger favorito. Il modo piu' facile per ottenere questo duplice scopo e` usare sempre il prefisso scelto per i propri nomi, dichiarare tutti i simboli come linee all'inizio del driver:
#ifdef DEBUG_modulename # define Static /* nothing */ #else # define Static static #endifI simboli veramente statici (come le variabili locali persistenti) possono quindi essere dichiarati static, mentre i simboli che possono essere usati dal debugger (come le funzioni e le variabili globali) saranno dichiarati Static. La funzione init_module()
In questa pagina viene mostrato il codice completo per la funzione di inizializzazione del modulo. Si tratta di codice esemplificativo (`skeletal'), come suggerito dal prefisso skel che viene usato nei simboli. Solitamente un vero dispositivo hardware offre qualcosa di piu' di due porte di I/O. La cosa piu' importante da ricordare durante la scrittura di questa funzione e' di rilasciare tutte le risorse che sono gia' state allocate ogni volta che si riscontra un errore. Questo tipo di compito e' gestito bene usando il costrutto goto, che rimane sconsigliabile in tutti gli altri casi. La duplicazione del codice di rilascio delle risorse in caso di errore viene evitata saltando in fondo alla funzione, dove di trova tutta la disallocazione. Il frammento di codice che viene mostrato accetta la configurazione a load-time del `major number', dell'indirizzo base per le porte di I/O della scheda e per il numero di interrupt usato. Per ogni scheda possibile nello spazio di I/O, viene invocata la funzione di autodetection. Se non viene trovata alcuna scheda init_module() ritornera' -ENODEV, per notificare a insmod che non esistono devices da controllare, provocando quindi la rimozione del modulo dalla memoria. Talvolta conviene permettere al driver di venire caricato anche se il suo hardware non e' presente sul calcolatore. Ho usato personalmente questo codice per sviluppare parti dei miei driver a casa, senza avere le corrispondenti schede installate nel mio calcolatore. Il trucco sta nell'avere una variabile di configurazione (skel_fake) che permetta di simulare una scheda inesistente. Simulare la presenza dell'hardware e' un ottimo modo per iniziare a scrivere il codice prima di avere l'hardware da controllare, o per provare il codice che gestisce due schede contemporaneamente anche se se ne possiede solo una. Il ruolo di cleanup_module e' quello di spegnere il dispositivo (se possibile) e rilasciare ogni risorsa allocata da init_module. Il codice esemplificativo mostrato sotto scandisce il vettore delle schede e rialscia tutte le porte di I/O e le linee di interrupt (se usate); alla fine viene rialsciato il major number. Il codice completo per init_module() e cleanup_module e' mostrato sotto. Il prefisso skel_ viene usato per tutti i nomi non locali; il codice e' abbastanza semplificato, in quanto mancano alcuni controlli di errore che dovrebbero essere presenti nella versione definitiva di un driver.
int skel_major = 0; /* default: dynamic */ int skel_base = 0; /* default: autodetect */ int skel_irq = -1; /* default: autodetect */ Static int skel_boards = 0; /* how many of them are there */ typedef struct Skel_Hw { int base; /* I/O port */ int irq; /* IRQ being used */ int hwirq; /* The detected one */ int irqcount; int usecount; /* .... */ } Skel_Hw; Skel_Hw skel_hw[SKEL_MAX_BOARDS]; #define PORT0(board) ((board)->base+0) #define PORT1(board) ((board)->base+1) /* ... */ Static file_operations skel_fops; /* defined later on */ int init_module (void) { int base, err, i; /* Look for a major */ err = register_chrdev (skel_major, "skel", &skel_fops); if (err < 0) { printk(KERN_NOTICE "skel init: error %d\n", -err); return err; } if (skel_major==0) skel_major=err; /* dynamic */ /* * Look for ports: PORT_MIN, PORT_STEP, PORT_MAX define * the range of (consecutive) addresses supported by the board */ base = skel_base ? skel_base : PORT_MIN; do { if (check_region(base, PORT_STEP) != 0) continue; /* in use */ request_region(base, PORT_STEP, "skel"); skel_hw[skel_boards].base=base; if ( (err=skel_find(skel_hw+skel_boards)) == 0) { /* found one */ skel_boards++; continue; } release_region(base, PORT_STEP); } /* if autodetecting skel_base is 0, otherwise, do it only once */ while (skel_base==0 && (base+=PORT_STEP) < PORT_MAX); if (skel_boards==0) { printk(KERN_NOTICE "skel init: no boards found\n"); return -ENODEV; } /* do other initialization here */ if ( (err=request_resource_1()) != 0 ) goto fail_resource_1: if ( (err=request_resource_2()) != 0 ) goto fail_resource_2: if ( (err=request_resource_3()) != 0 ) goto fail_resource_3: return 0; /* success */ fail_resource_3: free_resource_2() fail_resource_2: free_resource_1() fail_resource_1: printk(KERN_NOTICE "skel init: error %i\n", -err); /* release your boards */ for (i=0; iAutodetection= 0) free_irq(skel_hw[b].irq); } unregister_chrdev(skel_major, "skel"); return; }
La funzione init_module() mostrata sopra invoca skel_find() per svolgere il `brutto' compito di cercare l'hardware da controllare. Ovviamente l'implementazione di tale funzione dipende dalla scheda cui si riferisce il driver, quindi non cerchero' di mostrare il codice completo della funzione, ma solo l'autodetection della linea di IRQ. Nota del traduttore: il kernel offre supporto per la ricerca della linea di interrupt, ma l'autore ignorava tale risorsa. Il lettore si puo' sentire libero di lamentarsi con l'autore e chiedere dettagli al traduttore (che ne sa piu' dell'autore). Sfortunatamente, alcune periferiche non sono in grado di dire quale linea IRQ utilizzeranno, e costringono quindi l'utente a scrivere tale numero sulla linea di comando di insmod, oppure esplicitarlo nel driver stesso. Entrambi questi approcci non sono il massimo dello stile. perche' di solito il driver non viene caricato in memoria subito dopo avere configurato la scheda: il numero di interrupt selezionato tramite i jumper sulla scheda e' di solito un valore ignoto all'utente. L'unico modo per capire quale linea viene usata da questi dispositivi (quelli che non lo dicono esplicitamente) e' un ciclo di tentativi ed errori; ovviamente tale tecnica dara' dei risultati solo se l'hardware puo' generare interrupts sotto il controllo del software, indipendentemente da segnali esterni (il driver puo' essere caricato in qualsiasi momento, e dipendere dalla presenza di un segnale esterno non e' buona norma). Il codice seguente mostra skel_find() con l'autodetection della linea di IRQ. Alcuni dettagli della gestione delle interruzioni saranno oscuri per alcuni lettori, ma verranno spiegati nel prossimo articolo. In sintesi, questo codice tenta e' un ciclo su tutte le possibili interruzioni che chiede di installare un gestore di interrupt e controlla se la scheda genera effettivamente l'interruzione richiesta. Altra nota del traduttore: dalla versione 1.3.70 in poi, sono cambiati i prototipi di request_irq(), free_irq() e dell'handler. L'aggiornamento del codice mostrato qui sotto viene lasciato al lettore come esercizio. Il campo hwirq nella struttura che descrive l'hardware rappresenta la line di IRQ utilizzabile, mentre il campo irq e' valido solo mentre la linea e' attiva (dopo request_irq()). Come spiegato nel precedente articolo, non e' sensato trattenere una linea di interruzione quando il dispositivo non e' in uso; di conseguenza devono essere usati due campi nella struttura dati. Bisogna ricordare che ho scritto questo codice per supplire ai limiti di una delle mie schede: se l'hardware da controllare e' in grado di dire quali linea IRQ verra' usata, e' preferibile usare quella informazione. Il codice, comunque, e' abbastanza stabile quando e' possibile adattarlo al dispositivo specifico. Fortunatamente, la maggior parte delle schede e' in grado di dire la propria configurazione.
/* * This function only counts interrupts, and is used for probing */ Static void skel_trial_fn(int irq, struct pt_regs *unused) { int i; Skel_Hw *board; for (i=0, board=skel_hw; ifops e filpirq==irq) break; if (i==cxg_boards) /* not mine... shouldn't happen */ return; skel_acknowledge_the_interrupt() board->irqcount++; } /* * the autodetection function, which probes the possible interrupt lines */ Static int skel_find(Skel_Hw *board) { if ( failed_first_probe ) return -ENODEV; /* do any more probing here... */ if ( failed_last_probe ) return -ENODEV; /* found */ if (board==skel_hw && skel_irq>=0) { /* first board, and explicit irq */ board->hwirq=skel_irq; /* trust it */ board->irq=-1; } else { static int tryings[]={3,4,5,7,-1}; /* irq lines to try */ int i, trial, err; for (i=0; (trial=tryings[i]) >= 0; i++) { if (request_irq(trial, skel_trial_fn, SA_INTERRUPT, "skel") != 0) continue; /* irq line busy */ board->hwirq = board->irq = trial; board->irqcount = 0; tell_the_device_to_generate_interrupts sleep_for_enough_time tell_the_device_NOT_to_generate_interrupts free_irq(board->irq); board->irq = -1; if (board->irqcount > 0) /* did I get interrupts on this line? */ break; else board->hwirq = -1; } } if (board->hwirq == -1) { printk(KERN_NOTICE "skel: found board but no irq line\n"); return -EADDRNOTAVAIL; /* or accept it, at your will */ } return 0; /* all right */ }
Dopo il caricamento del modulo e l'inizializzazione dell'hardware controllato, bisogna vedere come si puo' utilizzare il nuovo dispositivo. Questo significa presentare il ruolo di fops e filp: queste bestiole sono le strutture dati piu' importanti (piu' corretamente: i nomi comunemente usari per tali variabili) nell'interfacciamento tra il kernel e il nostro device driver. fops e' il nome che viene comunemente dato alla struttura file_operations. Tale struttura e' una `jump table' (una serie di puntatori a funzione), e ogni campo della struttura si riferisce ad una delle possibili operazioni che vengono effettuate su un file (open(), read(), ioctl() eccetera). Un puntatore alle proprie operazioni viene passato al kernel tramite register_chrdev(), in tal modo le funzione del nuovo driver verranno chiamate ogniqualvolta un utente agisce su di un device che si riferisce a questo driver. Abbiamo gia' visto il codice che invoca register_chrdev(), quello che non ho ancora mostrato e' la vera struttura dati. Eccola:
struct file_operations skel_fops { skel_lseek, skel_read, skel_write, NULL, /* skel_readdir */ skel_select, skel_ioctl, skel_mmap, skel_open, skel_close };Ogni valire posto a NULL nelle fops indica che tale funzionalita' non \'e disponibile per il dispositivo in questione (select si comporta diversamente, ma non approfondiro' la qustione); ogni valore diverso da NULL deve essere un puntatore ad una funzione che implementi la specifica operazione per lo specifico dispositivo. Ancora il traduttore: anche ioctl(), open(), lseek e close(), se posti a NULL fanno si che il kernel esegua delle azioni di default, ma l'autore non aveva controllato prima di scrivere. Lo perdoniamo? In verit\'a, esistono alcuni altri campi nella struttura, ma il nostro esempio usera' il valore di default (NULL) per tali campi. Bisogna ricordare che il compilatore C riempie con degli zeri una struttura non completamente dichiarata, senza lamentarsi. Chi sia veramente interessato a tali campi aggiuntivi puo' guardare la definizione della struttura in
Nell'articolo precedente ho introdotto il concetto di `minor number', ed e' arrivato il momento di vedere come si usano questi numeri. Se il driver che si sta scrivendo e' in grado di gestire dispositivi multipli, oppure un dispositivo solo ma in differenti modalita' operative, si possono creare diversi nodi in /dev, tutti con lo stesso `major number' ma con differenti minors. Quando skel_open() viene invocata, poi, si puo' esaminare il numero del dispositivo che viene aperto in modo da eseguire le operazioni appropriate. I prototipi di open e close sono i seguenti:
int skel_open (struct inode *inode, struct file *filp); void skel_close (struct inode *inode, struct file *filp);Il `monor number' (un valore senza segno, attualmente limitato a 8 bits), e' disponibile come MINOR(inode->i_rdev). La macro MINOR e le strutture dati correlate sono definite in
#define SKEL_BOARD(dev) (MINOR(dev)&0x0F) #define SKEL_MODE(dev) ((MINOR(dev)>>4)&0x0F)I nodi in /dev verranno creati con i seguenti comandi (all'interno dello script skel_load, descritto nel precedente articolo). mknod skel0 c $major 0 mknod skel0raw c $major 1 mknod skel1 c $major 16 mknod skel1raw c $major 17 Ma torniamo al codice. La funzione skel_open() mostrata in seguito guarda dentro al minor number e rende disponibile l'informazione all'interno del filp, in modo da evitare perdite di tempo all'interno di read() o write(). Questo obiettivo viene raggiunto utilizzando una struttura Skel_Clientdata che contenga ogni informazione specifica ad un filp, e cambiando il puntore a fops all'interno del filp (cioe' filp->f_op). Cambiare i valori all'interno di un filp puo' sembrare una brutta tecnica, e nella maggior parte dei casi lo e'; rimane comunque un'idea valida quando si ha a che fare con le fops. Il campo f_op punta comunque ad una struttura dati statica, e puo' quindi essere modificato a cuor leggero, a patto che punti sempre ad una struttura valida; ogni operazione che verra' effettuata sul file in seguito sara instradata tramite la nuova tabella, e verranno evitate un sacco di istruzioni condizionali. Questa tecnica viene anche usata all'interno del kernel vero e proprio per implementare i diversi dispositivi che leggono la memoria utilizzando un solo `major number'. Il codice completo per skel_open() e skel_close() e' mostrato qui sotto; il campo flags all'interno di Skel_Clientdata verra' usato dall'operazione skel_ioctl(). E' utile notare che la funzione close() mostrata qui dovrebbe essere la stessa per entrambe le fops. Se occorre codificare differenti implemtazioni per close(), il codice mostrato qui dovra' essere duplicato.
typedef struct Skel_Clientdata { Skel_Hw *board; int flags; /* .... */ } Skel_Clientdata; struct file_operations skel_fops; struct file_operations skel_raw_fops; int skel_open (struct inode *inode, struct file *filp) { Skel_Hw *board; Skel_Clientdata *data; int err; if (SKEL_BOARD(inode->i_rdev) >= skel_boards) return -ENODEV; board = skel_hw + SKEL_BOARD(inode->i_rdev); switch (SKEL_MODE(inode->i_rdev)) /* node selection */ { case 0: break; /* normal mode */ case 1: filp->f_op = skel_raw_fops; break; /* raw mode */ default: return -ENODEV; } data = kmalloc(sizeof(Skel_Clientdata), GFP_KERNEL); if (!data) return -ENOMEM; filp->private_data = data; data->board = board; data->flags = SKEL_DEFAULT_FLAGS; /* fill_any_further_field... */ if (board->usecount == 0) { /* first open */ if (board->hwirq >= 0) { if ( (err=request_irq(board->hwirq,skel_interrupt,0,"skel")) != 0) { kfree(data); return err; /* or go on, at your will */ } board->irq=board->hwirq; } /* initialize_the_board ... */ } board->usecount++; MOD_INC_USE_COUNT; return 0; } void skel_close (struct inode *inode, struct file *filp) { Skel_Clientdata *data=filp->private_data; Skel_Hw *board=data->board; if (board->usecount == 1) { /* last close */ if (board->irq) { free_irq(board->irq); board->irq = -1; } /* shutdown_board ... */ } kfree(data); filp->private_data=NULL; board->usecount--; MOD_DEC_USE_COUNT; return; }Single-open o meno?
Un device driver dovrebbe essere un prodotto policy-free (intraducibile, come `single-open'), questo perche' le scelte di policy (politica d'uso) devono essre fatte dalle applicazioni. In effetti l'abitudine a separare le politiche d'uso (policy) e le funzionalita' offerte (mechanism) e' una delle caretteristiche forti di Unix. Sfortunatamente, l'implementazione di skel_open() porta ad effettuare schelte di politica: e' corretto permettere aperture concorrenti? Se si, come deve essere gestito l'accesso concorrente nel driver? Sia l'approccio single-open che quello concorrente hanno dei punti a loro favore. Il codice mostrato per skel_open() iimplementa una terza via, che si pone in qualche modo in mezzo tra le due. Se si sceglie di implementare un dispositivo single-open, il codice risulta molto semplificato: non occorrera' usare strutture dinamiche perche' una singola struttura statica sara' sufficiente; non c'e' quindi il rischio di avere `fughe di memoria' causate da errori nel driver. Inoltre, si puo' semplificare l'implementazione di select() e delle funzioni di raccolta dati perche' si ha la sicurezza che un solo processo per volta stara' leggendo i dati generati dal dispositivo. Un device single-open usa una variabile booleana per sapere se e' `occupato', e ritornera' -EBUSY quando open() viene chiamata quando il dispositivo e' gia' in uso. Si possono vedere esempi di questa codifica semplificata nel kernel ufficiale: i driver per i bus-mouse e il driver per la stampante sono ad apertura singola. Un driver ad apertura concorrente, al contrario, e' piu' difficile da implementare, ma molto piu' potente da usare da parte del programmatore di applicazioni. Per esempio, il debugging delle applicazioni e' semplificato dalla possibilita' di tenere aperto un programma di monitoraggio del stessa. Cosi' pure, si puo' modifcare il comportamento del dispositivo mentre l'applicazione sta girando, e utilizzare una varieta' di semplici scripts come strumenti di sviluppo, invece di un unico programma complesso che faccia tutto. Siccome la computazione ditribuita e' comune al giorno d'oggi, se si permette di aprire il dispositivo piu' di una volta, si e' gia' pronti a supportare un gruppo di processi cooperanti che usino il dispositivo come periferica di input o output. Gli svantaggi nell'uso di un'implementazione convenzionale che permette l'apertura multipla stanno principalemtne nella maggiore complessita' del codice. In aggiunta alla necessita' di usare strutture dinamiche (come private_data di cui si e' parlato prima), ci si scontrera' con i problemi connessi all'implentazione di una vero flusso di dati, unitamente alla gestione di un buffer e i problemi di lettura e scrittura bloccante/nonbloccante. Questi argomenti verranno affrontati nel prossimo articolo. Al livello dell'utente, uno svantaggio dell'apertura multipla e' la possibilita' di interferenza tra processi non cooperanti: questa situazione e' simile all'eseguire cat di un terminale da un altro terminale quello che viene digitato puo' andare alla shell oppure al comando cat, ed e' impossibile dire in anticipo chi ricevera' i dati. Nonostante un dispositivo ad apertura multipla permetta che diversi processi (e diversi utenti) accedano contemporaneamente al dispositivo, spesso l'uso concorrente dello stesso hardware da parte di piu' utenti non e' una cosa desiderabile. Una soluzione a questo problema puo' essere quella di guardare la uid (user identity) del primo processo che apre il device, e permettere ulteriori aperture solo allo stesso utente (o a root). Questa funzionalita' non e' implementata nel codice di esempio riportato in questa sede, ma la sua implementazione si limita a controllare current->euid, e a ritornare -EBUSY in caso se l'identita' non corrisponde. Questo tipo di politica e' simile a quella usata per i terminali: il processo login cambia il proprietario del file di controllo del terminale all'id dell'utente che si e' collegato al sistema. Chiaramente nel caso di un device non possiamo pretendere che le aperture del dispositivo siano controllate da un processo privilegiato. L'implementazione di skel qui riprodotta e' un'implementazione ad apertura multipla, con una piccola aggiunta: si assicura che il device sia inizializzato quando viene aperto per la prima volta, e che venga spento (se possibile) quando viene chiuso per l'ultima volta. Questa implementazione e' particolarmente utile per quei dispositivi che vengono usati raramente: se il frame-grabber viene usato solo una volta al giorno, non e' piacevole ereditare degli strani settaggi dall'ultima volta che e' stato usato. Inoltre non e' desiderabile continuare a far lavorare il grabber per acquisire immagini che nessuno sta leggendo. D'altro canto, inizializzazione e spegnimento delle periferiche possono essere operazioni lunghe, specialmente se occorre cercare una linea di interrupt, quindi questa tecnica puo' essere una cattiva scelta per alcuni driver. Nel caso di skel, il campo usecount all'interno della struttura che descrive l'hardware viene usato per sapere quando `accendere' la periferica, e quando `spegnerla'. La stessa politica viene utilizzata per la linea di IRQ: quando il dispositivo non e' in uso, l'interrupt resta disponibile per altre periferiche (se anche queste si comportano nello stesso modo amichevole, cioe' se rilasciano l'interrupt quando non sono in uso. Gli svantaggi di questa implementazione sono le attese dovute ad acnsioni/spegnimenti della periferica e l'impossibilita' di configurare il device con un programma per il successivo utilizzo da parte di un altro programma. Se occorre uno stato persistente nella periferica, o se semplicemente di vogliono evitare i cicli accensione/spegnimento, si puo' tenere il dispositivo aperto tramite un semplice comando come il seguente:
sleep 1000000 < /dev/skel0 &Come dovrebbe trasparire dalla precedente discussione, ogni implementazione delle chiamate open() e close() ha le sue peculiarita' e la scelta della migliore dipende dal dispositivo che si sta controllando e dall'uso al quale e' destinato. Anche i tempi di sviluppo andrebbero tenuti in considerazione, a meno che non si tratti di un grosso progetto, per cui abbondino le risorse. L'implementazione di skel mostrata qui puo' benissimo non essere la migliore per un particolare driver: d'altronde si tratta solo di un esempio, uno tra tante possibilita' diverse. Informazioni aggiuntive
Nota del traduttore: questa parte e' stata copiata dalla traduzione dell'articolo precedente, senza modifiche. Quando si dice la pigrizia... I prossimi articoli andranno avanti con questo argomento, e i sorgenti del kernel sono pieni di esempi interessanti. Altri moduli sono disponibili nei vari archivi ftp di linux. In particolare, quello che dico e' basato sulla mia esperienza personale con i device drivers: sia ceddrv-0.xx e cxdrv-0.xx assomigliano al codice che descrivo. ceddrv e' stato scritto da Georg Zezschwitz e me, e si tratta di un driver per un macchinario A/D D/A da laboratorio. Il cxdrv e' piu' semplice, e pilota un frame grabber molto semplice. L'ultima versione di entrambi si trova tramite ftp sotto iride.unipv.it:/pub/linux, ceddrv e' anche sotto tsx-11, e cxdrv e' anche sotto sunsite. Entrambi sono molto vecchi, pero', in quanto non ho avuto tempo di aggiornarli. Esistono dei libri sui device driver, ma spesso sono troppo specifici per un sistema particolare, e descrivono intrefacce molto ostiche. Linux e' molto piu' facile. Se occorrono informazioni io consiglio libri generici sulla struttura interna di Unix, e certamente il sorgente di Linux stesso. I seguenti libri sono molto interessanti. So che esiste una traduzione italiana almeno del primo e del terzo (entrambe della Jackson), ma non ho con me i dati e sono troppo pigro per cercarli. Maurice J. Bach, The Design of the UNIX Operating System, Prentice Hall, 1986 Andrew S. Tanenbaum, Operating Systems: Design and Implementation, Prentice Hall, 1987 Andrew S. Tanenbaum, Modern Operating Systems, Prentice Hall, 1992 Se volete contattarmi per qualunque motivo, il mio indirizzo e' "rubini@systemy.it". Cerco sempre di rispondere alla posta che arriva, ma talvolta qualche messaggio viene perso (non sono ordinato nemmeno nella gestione del mailbox); se qualcuno rimane vittima del mio disordine e' pregato di ritentare...
[About] [Copertina] |