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:
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.
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.
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:
Il confronto con tutto il resto:
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