Sui meccanismi di IPC nei 'Moderni' Sistemi Operativi

Luca Veraldi
veraldi@cli.di.unipi.it
luca.veraldi@katamail.com

Campobasso, 7 Settembre 2003




Indice:





I Moderni Sistemi Operativi offrono allo sviluppatore software una vasta gamma di meccanismi per il supporto alle comunicazioni tra processi.

Nella prima parte del presente lavoro, metteremo sotto esame le primitive di comunicazione più comuni, o più largamente utilizzate, sui sistemi operativi cosiddetti Linux-like, cercando di approfondirne il funzionamento ed evidenziando, di conseguenza, i problemi di efficienza che esse comportano.

Nella seconda parte, affronteremo il processo di ingegnerizzazione di un nuovo e più efficiente set di primitive per l'IPC.


Torna all'indice






Lo sviluppo di applicazioni parallele, ovvero di applicazioni costituite da più processi cooperanti, è un paradigma diffuso da tempo nei più diversi settori dell'informatica. E' il solito vecchio trucco di dividere un problema in sottoproblemi più semplici da affrontare e da risolvere (top-down), per poi comporre le soluzioni in qualche modo ed ottenere la soluzione del problema originario (bottom-up). Una applicazione costituita da più processi fa esattamente questo: ora, "comporre le soluzioni" significa far interagire i processi, tipicamente, permettendo loro di scambiarsi messaggi.

Anche se non ci facciamo magari più caso (o lo ignoriamo completamente, grazie ad un modello di programmazione ad alto livello), le nostre applicazioni, anche se non parallele, interagiscono comunque con una gran quantità di altre entità e processi, magari componenti del livello del Sistema Operativo. Il parallelismo è dunque la cosa più naturale ed innata, nello sviluppo di software... solo i nuclei dei Moderni Sistemi Operativi continuano ad essere sviluppati seguendo lo schema monolitico...

Di conseguenza, è importante per qualunque progettista software, alle prese con una applicazione di questo tipo, scegliere di volta in volta i meccanismi di comunicazione tra processi più opportuni e più efficienti per il progetto in esame. Ed è compito del Sistema Operativo implementare le politiche per il supporto a tali meccanismi.

Nel loro insieme, le primitive di comunicazione tra processi messe a disposizione da un Sistema Operativo prendono il nome di meccanismi di Inter Process Communication (o IPC).

In questa prima parte del lavoro, ci soffermeremo sui meccanismi di IPC disponibili su Linux e su alcuni Sistemi Operativi della stessa famiglia (o Linux-like). Analizzeremo in dettaglio le primitive ad alto livello che lo sviluppatore può utilizzare ed il modello di programmazione indotto dall'uso di tali primitive. A questo punto, dalla struttura del supporto e dai test sperimentali effettuati, appariranno evidenti i problemi di efficienza attualmente esistenti nelle comunicazioni inter-process.

Alcuni dettagli sulla Macchina Target dei nostri esperimenti...
Si tratta di un comunissimo processore pipeline superscalare della famiglia Pentium II, a 400 MHz, dotato di uno spazio di memoria della capacità di 64 Mbytes.
I Sistemi Operativi installati sulla macchina sono:

  • Linux, versione Red Hat 6.2 (che dispone del kernel 2.2.14-8, sapientemente modificato nella seconda parte del lavoro...);
  • Solaris versione 8;
  • e QNX Neutrino, che è un piccolo microkernel, parente alla lontana di BSD Linux.

Prenderemo in considerazione il seguente scenario (alto livello): abbiamo 2 processi cooperanti che vogliono scambiarsi messaggi su un canale di comunicazione simmetrico (un sender, un receiver) asincrono (il canale è una coda di messaggi di lunghezza maggiore strettamente di 1).

Canali Simmetrici Asincroni... ma non solo...

Ma le stesse considerazioni si applicano anche a canali asincroni asimmetrici in ingresso (più processi sender) o in uscita (più processi receiver).

Le prove consisteranno in send e receive tra i due processi sul canale di comunicazione e con le primitive scelte di volta in volta. I messaggi spediti saranno porzioni dello spazio di indirizzamento logico di A. Effettueremo 3 prove per ogni set di IPC:

I test


Torna all'indice






I meccanismi di IPC di gran lunga più diffusi su Linux e tutti i suoi cloni, varianti, imitazioni ecc. sono le pipe e le cosiddette primitive SYS V per le code di messaggi ed i segmenti di memoria condivisi.


Una pipe è una canale di comunicazione a grado di asincronia fisso (!!!). Il suo strepitoso successo tra le comunità di linuxiani sta nel fatto che per inizializzare, chiudere, spedire o ricevere dati in presenza di una pipe si utilizzano esattamente le stesse primitivi necessarie per aprire, chiudere, scrivere e leggere dati da un file su disco. Il fatto che tutto il mondo si riduca ad un file (i file sono file, le pipe sono file, i socket sono file, le periferiche sono file ecc.) è una delle caratteristiche carine di Linux.

E in effetti, spedire un messaggio da A a B su una pipe equivale ad una scrittura del messaggio sul file (fase di send) e ad una successiva lettura del messaggio dal file (fase di receive). Più qualche altro accorgimento per la sincronizzazione tra A e B, che si riduce a semplici sleep e wake up (condizioni di canale pieno/vuoto).

L'essenza della comunicazione su pipe è catturata dal seguente schema:

Pipe e copie fisiche

Il fulcro di tutto sono le memcpy(). Trasferire 4096 bytes da A a B implica la copia fisica del doppio dell'informazione... non è esattamente quello che si dice comunicazione zero-copy...

I risultati sperimentali sulla macchina di riferimento sono riportati nelle seguenti tabelle:

Pipe e copie fisiche

Pipe e copie fisiche

I valori indicati sono valori medi riportati con la chiamata gettimeofday() della libreria sys/time.h. Non hanno influito i ritardi indotti dalle sincronizzazioni (sleep e wake up), che aggiungono con frequenza 1/8 altri 40-60 µsec ai tempi indicati (cioè, nel caso di Linux, un ritardo dello stesso ordine di grandezza della copia fisica del messaggio, per messaggi di una pagina di memoria; un ritardo trascurabile, per messaggi di dimensione maggiore).

A giustificazione del fatto che non si è tenuto conto degli overhead di sincronizzazione, c'è da dire che il nostro interesse è focalizzato su come ottimizzare la spedizione e la ricezione di messaggi: qualunque protocollo di comunicazione implica overhead per sincronizzazione su canali pieni/vuoti. Inoltre, lo sviluppatore può (eheh... sempre ammesso che il modello di programmazione glielo permetta... :-)) fissare il grado di asincronia del canale in modo da rendere trascurabili le sleep e le wake up.

Alcune considerazioni. Innanzitutto, appare evidente che QNX Neutrino e Solaris non reggono il confronto con Linux. C'è quasi un ordine di grandezza di differenza nei tempi di completamento delle primitive. In secondo luogo, in entrambi i casi, è chiara la dipendenza lineare tra la dimensione in byte del messaggio e i tempi necessari per completare send e receive.

Pipe e copie fisiche


Torna all'indice


Le SYS V IPC per la gestione delle code di messaggi sono un modo più complicato, confuso ed inefficiente per fare la stessa cosa delle pipe. Si tratta sempre di copia fisica in spedizione e copia fisica in ricezione, ma espresso, in termini di codice C, attraverso un formalismo che è la vergogna della programmazione.

Comunque, bando ai giudizi personali, ecco i numeri (numeri, numeri... altro che parole):

SYS V Code di Messaggi

SYS V Code di Messaggi

Allora, l'idea è di avere un canale con grado di asincronia fisso (maledetto Linus Torvalds) con 32 posizioni. L'NS riportato in corrispondenza di messaggi di 40 Kbytes è dovuto al fatto che, con le meravigliose primitive SYS V, potete spedire messaggi fino a 4056 bytes (ohohhhh!!!). Meraviglia :-O!

In questo caso, l'esecuzione di primitive di sleep e wake up porta via un tempo secolare (eheh... aspetta quello che arriva dopo...): circa 200 µsec.

Il guadagno rispetto alle pipe è solo per l'instanziazione della comunicazione. Tutto il resto ha performance scadenti.

Complimenti agli ingegneri dei messaggi SYS V.


Torna all'indice


Vediamo se riusciamo a beccarne una che funzioni a dovere... (eheh... no che non ci riesci...)

I segmenti condivisi servono per implementare comunicazioni tra processi, che novità, questa volta però a partire da un punto di vista sul problema totalmente differente. Anzicchè copiare il messaggio dallo spazio di A a quello di B, permettiamo a B di accedere direttamente alle locazioni dello spazio di A che corrispondo al messaggio da inviare.

Per ulteriori dettagli su questa cosa, che si chiama passaggio dinamico di capability, vedi [VAN1] e [VAN2].

Il disegnuccio esplicativo:

SYS V Segmenti Condivisi

Cerchiamo di capire di che si tratta.

Se A vuole spedire un messaggio a B, richiede innanzitutto al Sistema Operativo un nuovo segmento condiviso, con la chiamata shmget(). A questo punto, tipicamente, A vorrà scrivere le informazioni da spedire a B nel segmento. Per farlo, lo acquisisce nello spazio di indirizzamento logico, con la chiamata shmat(). shmat restituisce un indirizzo logico, analogamente alla malloc(). A può modificare il segmento a proprio piacimento.

Quando è completa la formattazione del messaggio, B acquisisce lo stesso segmento con la shmget() e lo attacca al proprio spazio di indirizzamento logico con la shmat(). A questo punto, può leggere le informazioni che A gli ha spedito.

Chiaramente, come per tutte le situazioni di comunicazione a memoria condivisa, ci vogliono semafori di sincronizzazione. Quando B sa che A ha terminato di scrivere nel segmento condiviso? Basta che B faccia una down su un semaforo e che A, al momento opportuno, lo sblocchi con una up sullo stesso semaforo. (Neppure a dirlo, le SYS V IPC mettono a disposizione anche i semafori).

Il meccanismo funziona perché:

  • shmget() alloca tutte le strutture necessarie e lo spazio fisico nel kernel;
  • shmat() inserisce nella tabella di rilocazione del processo invocante le informazioni necessarie affinchè l'indirizzo logico restituito dalla chiamata di sistema sia rilocato a tempo di esecuzione in un indirizzo fisico che corrisponde alle pagine allocate nel kernel per quel segmento condiviso;
  • esistono identificatori unici dei segmenti condivisi (sono limitati in numero: appena 7 bit).

Le pagine corrispondenti a segmenti condivisi non sono soggette alla paginazione usuale, ovvero, per il resto del Sistema Operativo è come se fossero lockate in memoria.

I numeri:

SYS V Segmenti Condivisi

Detto sottovoce, abbiamo perso la linearità con il numero di byte del messaggio. Si intuisce che ora la complessità in tempo di una send o di una receive è lineare, ancora, sì, ma nel numero di modifiche da apportare alle tabelle di rilocazione, ovvero, nel numero di pagine del messaggio da ricevere/spedire.

Il confronto con le pipe:

Pipe vs. Segmenti Condivisi

Però, però, però. Niente male. Tuttavia, noi siamo informatici e possiamo fare meglio. Hey... cos'è quel tratto della curva rossa che cade sotto quella blu? Eh... possiamo ancora ridurre gli overhead.

E poi? Il modello di programmazione? Non auguro a nessuno di dover mai avere a che fare con le primitive SYS V. Scandalose. Dobbiamo cambiare la situazione.

Ma per farlo, bisogna sporcarsi le mani sul serio...


Torna all'indice






Il problema è chiaro.

Partiamo dallo studio di fattibilità. Prima di implementare un set di primitive per l'IPC dobbiamo porci la domanda: esistono già dei prodotti software che fanno questa cosa qui? Beh, se ne sono andate 7 pagine di Word, per parlare delle soluzioni alternative. Ma noi non siamo soddisfatti.

Allora dobbiamo chiederci: si può fare meglio? Per rispondere, cfr. [VAN1] e [VAN2].

Ok. Cominciamo.

Chi si avvicina allo sviluppo di parti del nucleo di Linux, ha due opzioni: sviluppare le funzionalità come un modulo che possa essere caricato nel nucleo dinamicamente; oppure compilare le nuove funzionalità direttamente insieme ai sorgenti del nucleo stesso. La prima strada è impossibile da percorrere, perché dobbiamo modificare strutture dati come il PCB del processi e definire nuove chiamate di sistema. Quindi, si deve ricompilare tutto ab origine.

La visione ad alto livello dei nuovi meccanismi di IPC è una struttura gerarchica a livelli (e cosa non è una struttura gerarchica a livelli, in informatica? Forse, giusto Linux :-)...).

I livelli sono 3:

I livelli di ECBM

Noi li analizzeremo dal basso verso l'alto.

L'idea è semplice. A ha un messaggio per B. Dove ce l'ha?

Nel suo proprio personale privato spazio di indirizzamento logico. B lo vuole. Per implementare il trasferimento di questa informazione, A fa una send su un canale. Cosa scrive sul canale?

La capability del messaggio secondo [VAN2]. Ovvero una struttura che è così definita:

#define physical_address_t unsigned long
#define logical_address_t unsigned long
#define prot_t unsigned long

typedef struct
{
 physical_address_t* addresses;
 unsigned long offset_start;
 int size;
 prot_t rights;
} capability;

Cioè 16 bytes. Quanto ci vuole per copiarli? 0 µsec: sono 4 load e 4 store. 2 cicli di clock della macchina sono più che sufficienti. 0 µsec.

addresses è un vettore di size posizioni. Conserva le righe della tabella di rilocazione di A relative al messaggio da spedire. 1 riga = 1 pagina del messaggio.
Per spedire 4096 byte, copio un unsigned long. E ho finito.
offset_start è l'offset del messaggio all'interno della prima pagina (potrebbe non essere allineato alla pagina: dipende dal gcc che compila A).
size è il numero di pagine del messaggio cui la capability si riferisce.
rights sono i diritti di accesso al messaggio che A vuole garantire a B, quando questi farà la receive.

Ok. B fa la receive. Legge la capability dal canale di comunicazione. Cosa ci fa?

La acquisisce. Per ogni pagina (for (count=0; count<size; ++count)) inserisce addresses[count] nella propria tabella di rilocazione. Calcola dinamicamente l'indirizzo logico per accedere al messaggio e... il gioco è fatto. Ho ampliato dinamicamente lo spazio di indirizzamento logico di B per permettergli di acquisire il messaggio di A. E' come se avessi fatto una malloc che mi ha inizializzato le pagine con le informazioni del messaggio di A..

SALVO IL FATTO CHE IL MESSAGGIO NON E' STATO DUPLICATO NE' COPIATO.

Come è fatto il canale? E' una banalissima coda FIFO:

typedef struct ch
{
 int n;
 int filled;
 int insert;
 int extract;
 capability** cap;
 struct wait_queue* full;
 struct wait_queue* empty;
} channel;

n è il grado di asincronia del canale, e non vedo perché il programmatore non debba avere la possibilità di stabilirlo per i propri scopi...
insert e extract servono per gestire la coda.
cap è la coda.
Seguono le strutture per la sincronizzazione in caso di cosa piena e vuota rispettivamente, come i nomi delle variabili lasciano intuire.

Gli algoritmi di send e receive non sto a riportarli, tanto li conosciamo a memoria. Per chi avesse dubbi, [VAN1].

Se il messaggio sta nello spazio di indirizzamento logico di A (e dopo la receive, anche di B), dove sta il canale?

Il canale è allocato dal nucleo. In stato supervisore. Insomma, sta nello spazio di indirizzamento dell'(n+1) processo del sistema. E' indirizzato con indirizzi fisici (chiedo venia :-)...).

Ogni volta che un processo richiede un canale di comunicazione, il nucleo alloca lo spazio necessario ed inserisce la struttura in una lista doppiamente linkata di canali.

Lista doppiamente linkata

Ogni canale ha un identificatore unico (a 32 bit!!! Quindi, circa 4 miliardi di canali, al massimo...) E la lista è ordinata per identificatore. Se la lista diventa più grande di ECBM_COMM_RES_MAX viene costruito un albero binario di ricerca con indice l'identificatore. L'albero è un AVL, ovvero un albero binario di ricerca autoribilanciante: insomma, ha altezza pari al logaritmo a base 2 del numero di canali in esso contenuti. (La ricerca dei canali nelle strutture è frequente. L'albero binario di ricerca bilanciato è l'ottimo, quanto a struttura dati).

A e B dovranno acquisire il canale con lo stesso identificatore. Esattamente come con le SYS V IPC dovevano chiamare la shmget con lo stesso parametro.

I canali aperti e le capability acquisite (e non ancora rilasciate) da ogni processo sono conservate in una struttura simile alla tabella dei file aperti. Gli indirizzi (fisici!!!) base della tabella dei canali e delle capability sono contenuti nel PCB del processo.

L'ultima annotazione è che le pagine che costituiscono i messaggi da spedire sul canale sono lockate in memoria finchè il processo B non rilascia esplicitamente la capability corrispondente. Notiamo sin d'ora che avere le pagine lockate in memoria non è condizione necessaria per l'applicabilità dei meccanismi di comunicazione basati su capability. Semplicemente, semplifica il mio lavoro e non mi fa impazzire con lo swapping di linux.

Ultimo passo. Il modello di programmazione ad alto livello.

Voglio che i sorgenti di A e B siano fatti così:

A::

#include <stdio.h>
#include <stdlib.h>
#include <linux/ecbm/ecbm.h>
#include "my_channel.h"

int main(int argc, char* argv[])
{
 id_t ch;
 char* buffer;

 /*
  * Costruisci il canale nel nucleo
  * Serve l'identificatore (MY_CH)
  * Serve il grado di asincronia del canale (MY_ASYN)
  */
 ch=install(MY_CH,MY_ASYN);

 /* Formatta il messaggio da spedire a B */

 /*
  * Chiama la zero-copy send
  * Serve ch
  * Serve il messaggio da spedire (buffer)
  * Serve la dimensione (strlen(buffer))
  * Servono i diritti (lettura, scrittura, esecuzione)
  */
 zc_send(ch,buffer,strlen(buffer),0777);
 return 0;
}


B::

#include <stdio.h>
#include <stdlib.h>
#include <linux/ecbm/ecbm.h>
#include "my_channel.h"

int main(int argc, char* argv[])
{
 id_t ch;
 char* buffer;

 /*
  * Acquisisci il canale nel nucleo
  * Serve l'identificatore (MY_CH)
  */
 ch=acquire(MY_CH);

 /*
  * Chiama la zero-copy receive
  * Serve ch
  * Servono i diritti (lettura, scrittura, esecuzione)
  */
 buffer=zc_receive(ch,0777);

 /* Lavora sul messaggio di A */

 /* Rilascia il segmento logico acquisito
  * E' come una free(buffer)
  * Serve l'indirizzo logico base (buffer),
  *       così come restituito dalla zc_receive
  */
 release(buffer);
 return 0;
}

Notiamo che B non alloca spazio per buffer. Se ne occupa la zc_receive().

Il file my_channel.h sarà una cosa del tipo:

my_channel.h::

#define MY_CH
#define MY_ASYN 32

Credo, quanto meno, che sia pulito e semplice.

Chiaramente, A e B possono essere compilati separatamente (cosa non possibile se utilizzo quella bella invenzione delle pipe senza nome).

Perché questo modello sia quello realmente utilizzabile, dobbiamo definire 5 nuove chiamate di sistema. Una chiamata di sistema è un frammento di codice caricato staticamente in memoria, eseguito in stato supervisore (chiedo venia :-)...), cioè a indirizzi fisici (IC, il program counter, è caricato con indirizzi fisici delle istruzioni; tutti i riferimenti ai dati sono attraverso indirizzi fisici; no controllo di protezione a Firmware ecc.).

Per eseguire una chiamata di sistema, è necessario eseguire una trap al kernel (oops...). Tutte le funzioni della libreria standard C che eseguono chiamate di sistema in realtà sono wrapper (involucri) per le system call e il loro unico scopo è formattare i parametri della system call ed eseguire la trap al kernel. Come si fa la trap al kernel?

Per ragioni storiche, eseguendo in modo utente l'istruzione assembler INT 0x80. Nel file linux/arch/i386/kernel/traps.c si legge:

984 set_trap_gate(18,&machine_check);
985 set_trap_gate(19,&simd_coprocessor_error);
987 set_system_gate(SYSCALL_VECTOR,&system_call);

dove SYSCALL_VECTOR vale 0x80. Chiamando INT 0x80, la macchina passa in stato supervisore e viene eseguito il codice il cui entry point è &system_call. Cos'è system_call?

In linux/arch/i386/kernel/entry.S (codice assembler, ohohhhh!!!. Meraviglia :-O!), si legge:

202 ENTRY(system_call)
203 pushl %eax # save orig_eax
204 SAVE_ALL
205 GET_CURRENT(%ebx)
206 testb $0x02,tsk_ptrace(%ebx) # PT_TRACESYS
207 jne tracesys
208 cmpl $(NR_syscalls),%eax
209 jae badsys
210 call *SYMBOL_NAME(sys_call_table)(,%eax,4)
211 movl %eax,EAX(%esp) # save the return value

Che detto in informatichese è nient'altro che un salto forzato, o goto calcolato. Cosa???

E' il risultato della compilazione di un enorme case:

case(sys_call_num) of
  1: call sys_exit(); break;
  2: call sys_fork(); break;
  3: call sys_read(); break;
  ...

La normale funzione di libreria (user-space) non fa altro che mettere nel posto giusto il numerino magico della chiamata di sistema ed eseguire INT 0x80. Da questo punto in poi, eseguiamo codice a indirizzi fisici, nello spazio del kernel. I numerini magici sono scritti nel file linux/include/linux/asm/unistd.h.

Le chiamate di sistema

Noi abbiamo il programma utente, l'implementazione delle chiamate di sistema (send, receive ecc) e chiaramente la system_call. Ci manca la funzione di libreria che esegua l'INT 0x80 con i giusti parametri. Ah... ci mancano anche nuovi numerini magici per le nostre chiamate...

I numerini magici li aggiungiano in linux/arch/i386/kernel/entry.S (codice assembler, ohohhhh!!!. Meraviglia :-O!): basta dire:

/*
 * System Calls for ECBM
 */

/*
 * General Resource Ids
 */
.long SYMBOL_NAME(ecbm_get_magic_number) /* 200 */
.long SYMBOL_NAME(ecbm_install_channel) /* 201 */
.long SYMBOL_NAME(ecbm_acquire_channel) /* 202 */

/*
 * Zero-Copy Messaging Support
 */
.long SYMBOL_NAME(ecbm_zc_send) /* 203 */
.long SYMBOL_NAME(ecbm_zc_receive) /* 204 */
.long SYMBOL_NAME(ecbm_release_area) /* 205 */

E anche in linux/include/linux/asm/unistd.h:

/*
 * System Call Numbers for ECBM
 */

/*
 * General Resource Ids
 */
#define __NR_get_magic_number 200
#define __NR_install 201
#define __NR_acquire 202

/*
 * Zero-Copy Messaging Support
 */
#define __NR_zc_send 203
#define __NR_zc_receive 204
#define __NR_release 205

/*
 * Copying Messaging Support
 */
#define __NR_oc_send 206
#define __NR_oc_receive 207

Le funzioni che richiamino opportunamente la system_call con l'INT 0x80 sono definite in linux/include/linux/ecbm/ecbm.h, in modo che siano disponibili ogni volta che, nei sorgenti dei nostri programmi, scriviamo #include <linux/ecbm/ecbm.h>.

Ecco le definizioni:

/*
 * Functions to install and acquire
 * a new communication channel
 * and to transfer messages among different
 * process's logical address spaces
 */
static inline _syscall0(id_t,get_magic_number)
static inline _syscall2(id_t,install,magic_number_t,magic,int,asynchrony)
static inline _syscall1(id_t,acquire,magic_number_t,magic)

static inline _syscall4(int,zc_send,id_t,id,void*,buffer,int,size,prot_t,rights)
static inline _syscall2(void*,zc_receive,id_t,id,prot_t,rights)
static inline _syscall1(int,release,void*,buffer)
static inline _syscall3(int,oc_send,id_t,id,void*,buffer,int,size)
static inline _syscall1(void*,oc_receive,id_t,id)

Ah. I meccanismi implementati prendono il nome di ECBM, ovvero Efficient Capability-Based Messaging. :-)

I numeri:

ECBM

Il confronto con tutto il resto:

Non ho parole...

Devo spedire 4 Mbyte di dati per ottenere i tempi che una send sulla pipe impiega su un argomento di 40 Kbytes.


Torna all'indice






Cosa abbiamo visto oggi? Abbiamo dimostrato un teorema ed il suo corollario.

Teorema: I meccanismi basati sul passaggio dinamico di capability costituiscono una soluzione efficiente e pulita, oltre che sicura, al problema dell'IPC.

Corollario: I meccanismi di IPC offerti da Linux e derivati sono strutturalmente inefficienti.

A parte gli scherzi, ci sono quattro considerazioni da fare, a conclusione di questo lavoro:

  • Tra i sorgenti di ECBM, ci sono le definizioni di due funzioni, oc_send e oc_receive, non implementate. Un esercizio carino sarebbe scrivere nel corpo di queste funzioni le tre righe di codice necessarie per implementare canali di comunicazione one-copy. Ovvero: chi spedisce, scrive nel canale la capability del messaggio. Chi riceve, anzicchè acquisire la capability, la utilizza per effettuare una copia fisica tra pagine di memoria: dalle pagine di A ad un opportuno buffer nello spazio logico di B. Visto che siamo nel kernel, dobbiamo effettuare copie con indirizzi fisici...

    Questa soluzione rompe la condivisione di memoria tra A e B (così accontentiamo quelli che non la sopportano), costa chiaramente di più in termini di tempo, ma sempre meno di una comunicazione sulla pipe, che di copie ne prevede due: una per la send ed una per la receive. I tempi di completamento delle nuove primitive dovrebbero essere anche minori della metà dei tempi di completamento di read e write sulle pipe.

    Quindi, ancora un guadagno rispetto ai meccanismi di IPC standard;

  • sleep e wake up, se fatte utilizzando le procedure messe a disposizione del kernel, costano un'enormità di tempo;
  • l'ECBM descritto è SMP compatibile;
  • l'ECBM descritto nasce solo a scopo dimostrativo. L'uso in applicazioni concrete è a rischio e pericolo dello sviluppatore.


Torna all'indice






Per chi voglia saperne di più, una guida inesauribile è senz'altro

[ORLL] Understanding the Linux Kernel, Daniel P. Bovet, Marco Cesati - O'REILLY

In particolare:

  • Chapter I, Introduzione
  • Chapter II, Memory Addressing (un po' di terminologia non farebbe male, però...)
  • Chapter III, Processes
  • Chapter VI, Memory Management
  • Chapter VII, Process Address Space (!!!)
  • Chapter IIX, System Calls (oops..)
  • Chapter XI, Kernel Synchronization
  • Chapter IIXX, Process Communication (bah...)

Per approfondire le capability, senza che ve lo dico

[VAN1] Appunti di Architettura degli Elaboratori I, Marco Vanneschi - SEU
[VAN2] Appunti di Architetture Parallele e Distribuite, Marco Vanneschi - SEU

Fortemente sconsigliato, perché altamente fuorviante, per quanto riguarda l'IPC (e non ridete, perché è la verità)

[---] Modern Operating Systems, Andrew S. Tanenbaum - Prentice Hall


Torna all'indice





Luca Veraldi
veraldi@cli.di.unipi.it
luca.veraldi@katamail.com

Campobasso, 7 Settembre 2003