Size: 2879
Comment:
|
← Revision 29 as of 2009-05-04 15:51:42 ⇥
Size: 6181
Comment:
|
Deletions are marked like this. | Additions are marked like this. |
Line 3: | Line 3: |
Quando viene creato un socket usando il wrapper {{{ntk.wrap.sock.Sock}}}, viene registrato nella mappa di {{{asyncore}}} e viene avviato un microthread nel modulo microsock. | Lo scopo del '''modulo microsock''' è quello di permettere la gestione di operazioni di I/O sui socket in un ambiente multithread cooperativo. |
Line 5: | Line 5: |
La mappa di asyncore associa il fd (file descriptor, un numero intero) all'oggetto (che può essere un file, o un socket, ...). | 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. |
Line 7: | Line 9: |
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. | 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. <<BR>> 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, ...). |
Line 9: | Line 13: |
''Nota'': asyncore.poll usa {{{time.sleep}}} per attendere il timeout specificato anche quando non ci sono oggetti su cui ascoltare. Questo sleep non è un busy wait per il sistema operativo. Però non passa lo scheduler agli altri microthread. Per questo dobbiamo richiamarlo con piccoli timeout intervallando con chiamate a {{{micro_block}}}. Vedi il [[../ModuloXtime|modulo xtime]]. | ''Nota'': La funzione {{{asyncore.poll}}} è bloccante. Il parametro passato è il numero di secondi dopo i quali cui si vuole cha la chiamata faccia ritorno. <<BR>> 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. <<BR>> Per questo dobbiamo richiamarla con piccoli timeout intervallando con chiamate a {{{micro_block}}}. Vedi anche la trattazione dell'argomento nella pagina del [[../ModuloXtime|modulo xtime]]. |
Line 11: | Line 19: |
Il metodo {{{asyncore.poll}}}, tramite il metodo {{{select.select}}}, scopre quali dei socket registrati nella sua mappa ha dei dati da inviare o da ricevere. | 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. |
Line 13: | Line 21: |
Vediamo quali passi segue la creazione e connessione di un socket, prima di analizzare quello che avviene quando si trasmette o riceve 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 [[../ModuloMicrosockCreazione|creazione di un socket]], [[../ModuloMicrosockConnessione|connessione di due socket]] e [[../ModuloMicrosockTrasmissione|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 [[../ClasseChannel|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. |
Line 15: | Line 38: |
Quando si crea un socket all'interno del programma (con socket.socket(...)) viene effettivamente creato un vero socket (con stdsocket.socket(...)) e anche un microsock.stacklesssocket, che è l'oggetto restituito. Questo memorizza il vero socket e una istanza dell'oggetto microsock.dispatcher, anch'esso a conoscenza del vero socket. <<BR>> In seguito tutte le chiamate {{{__getattr__}}} al finto socket (cioè all'oggetto stacklesssocket ritornato) vengono inoltrate al dispatcher (esempi di queste chiamate sono s.listen, s.bind, ...) mentre le chiamate{{{ __setattr__}}} vengono memorizzate localmente. ??? <<BR>> Nel frattempo, avendo istanziato un dispatcher, che è una derivata di {{{asyncore.dispatcher}}}, abbiamo anche fatto altro: * la classe ha i membri connectChannel, acceptChannel e recvChannel. * istanziato per recvChannel un Channel con micro_send = True. * preparato una stringa buffer e una lista per i send (sendBuffer e sendToBuffers) * preparato una stringa buffer e una lista per i recv (readBufferString e readBufferList) * preparato una costante maxreceivebuf = 65536 * memorizzato il vero socket nella mappa di asyncore (questo lo fa {{{asyncore.__init__}}} => {{{asyncore.set_socket}}} => {{{asyncore.add_channel}}}) * la sua rimozione dalla mappa avviene con {{{stacklesssocket.__del__}}} => {{{asyncore.close}}} => {{{asyncore.del_channel}}} <<BR>> Prima che un socket abbia qualcosa da inviare o ricevere, di norma viene connesso richiamando in esso il metodo connect o listen e poi accept. |
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 [[../ClasseChannel|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 [[../ClasseChannel|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. |
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.