Il modulo microsock
Lo scopo del modulo microsock è quello di permettere la gestione di operazioni di I/O sui socket in un ambiente multithread cooperativo.
In un tale ambiente è necessario evitare l'uso di chiamate di sistema bloccanti per l'intero processo, come sono le chiamate di I/O sui socket.
Quando viene creato un socket usando il wrapper ntk.wrap.sock.Sock, viene in effetti creato anche un oggetto asyncore.dispatcher che lo incapsula e questo viene registrato nella mappa di asyncore (un modulo standard di python stackless); inoltre viene avviato un microthread nel modulo microsock.
Il microthread avviato esegue la funzione ManageSockets. Questa richiama asyncore.poll(0.05) ciclicamente (intervallando con micro_block per passare lo scheduler agli altri) fin quando esiste un socket (nel caso di Netsukuku per sempre) nella mappa di asyncore.
La mappa di asyncore associa il fd (file descriptor, un numero intero) ad una istanza di asyncore.dispatcher che incapsula il vero oggetto sul quale vanno fatte le operazioni di I/O (che può essere un file, o un socket, ...).
Nota: La funzione asyncore.poll è bloccante. Il parametro passato è il numero di secondi dopo i quali cui si vuole cha la chiamata faccia ritorno.
Al suo interno la funzione richiama time.sleep oppure select.select, a seconda se ci sono o meno socket da controllare. In entrambi i casi queste funzioni usano chiamate di sistema che non impegnano la CPU, ma nemmeno permettono agli altri microthread di venire schedulati.
Per questo dobbiamo richiamarla con piccoli timeout intervallando con chiamate a micro_block. Vedi anche la trattazione dell'argomento nella pagina del modulo xtime.
Il metodo asyncore.poll, per prima cosa fa uso dei due metodi readable e writable di asyncore.dispatcher su tutti i dispatcher registrati nella sua mappa. Questi metodi possono essere overridati; nella implementazione base restituiscono True. Il loro obiettivo è quello di identificare i socket che hanno intenzione di inviare (writable) o di leggere (readable) qualcosa.
Di seguito, per i socket che sono readable o writable secondo i significati sopra esposti, tramite il metodo select.select scopre quali hanno dei dati da ricevere oppure sono subito in grado di inviare dati. Cioè per quali socket le chiamate recv e send non risulterebbero bloccanti.
Nel modulo microsock viene definita la classe microsock.dispatcher che è una classe derivata di asyncore.dispatcher. Grazie a questa classe possiamo definire come deve agire il programma quando un microthread richiama i classici metodi di I/O su un presunto socket. Per una analisi di dettaglio rimandiamo alle pagine sulla creazione di un socket, connessione di due socket e trasmissione dati tra due socket.
Il microthread che esegue ManageSockets
Abbiamo detto che fin dal primo momento in cui esiste un socket viene avviato un microthread che esegue la funzione ManageSockets presente in questo modulo.
La vita di questo microthread è un aspetto critico di tutto il programma Netsukuku. Se per un qualsiasi motivo questo microthread dovesse bloccarsi in attesa di un evento che tarda a verificarsi (o addirittura non si verificherà mai) l'intera struttura di comunicazione di Netsukuku si blocca.
Per questo motivo va usata estrema cautela in tutti i punti del codice (in particolare in questo modulo) in cui questo microthread passa e che fanno uso di chiamate bloccanti.
Le chiamate bloccanti usate in questo microthread sono di due tipi.
Chiamate che bloccano l'intero programma. Sono tutte quelle che riguardano l'I/O sul socket reale. Ci sono le chiamate accept per i server, le chiamate connect per i client, le chiamate send, sendto e le chiamate recv, recvfrom.
Chiamate che bloccano questo microthread ma lasciano spazio agli altri microthread. Sono le chiamate send e receive dei stackless.channel o della nostra classe Channel che li incapsula.
Per quanto riguarda le chiamate di I/O sul socket, ci si affida alla funzione select.select la quale ci dice con certezza (e senza bloccarsi indefinitamente) se una certa chiamata risulterebbe bloccante. Quindi se usata correttamente ci può guidare ad un uso non bloccante delle altre chiamate.
La funzione select.select è chiamata nella funzione asyncore.poll; noi ce ne avvaliamo facendo l'override nella classe microsock.dispatcher dei metodi di asyncore.dispatcher che vengono chiamati dalla funzione asyncore.poll stessa. Vedi le pagine citate sopra.
Per quanto riguarda la trasmissione di messaggi sui channel, bisogna:
Per le chiamate receive accertarsi che qualcuno stia inviando qualcosa; usare quindi l'attributo balance. In realtà al momento non ne vengono fatte di chiamate receive all'interno del microthread in questione.
Per le chiamate send si può agire in due modi:
Di norma non è essenziale sapere il momento esatto in cui sono state portate a compimento; si possono eseguire quindi in un separato microthread. Per questo motivo la classe Channel può venire istruita (usando il costruttore con micro_send=True) di eseguire tutte le chiamate send sul channel incapsulato in un nuovo microthread. TODO: Domanda: molteplici chiamate a send in microthread distinti verranno poi rilette sicuramente nell'ordine in cui sono state inviate?
Oppure vanno fatte solo se si è sicuri che una altra tasklet sia in attesa. In questo caso va preferito il costruttore della classe Channel con prefer_sender=True, perché così la chiamata send trasmette i dati, sblocca la tasklet ricevente, ma mantiene la schedulazione al microthread corrente.