Avanti Indietro Indice

4. Temporizzazione ad elevata precisione

4.1 Ritardi

Innanzi tutto è bene precisare che, a causa della natura multitasking di Linux, non è possibile garantire che un processo che gira in modo utente abbia un preciso controllo delle temporizzazioni. Un processo potrebbe essere sospeso per un tempo che può variare dai, circa, dieci millisecondi, fino ad alcuni secondi (in un sistema molto carico). Comunque, per la maggior parte delle applicazioni che usano le porte I/O, ciò non ha importanza. Per minimizzare tale tempo è possibile assegnare al processo un più elevato valore di priorità (vedere la pagina di man di nice(2)), oppure si può usare uno scheduling in real time (vedere sotto).

Per temporizzazioni più precise di quelle disponibili per i processi che girano in modo utente, ci sono delle "forniture" per il supporto dell'elaborazione real time in modo utente. I kernel 2.x di Linux forniscono un supporto per il soft real time; per i dettagli vedere la pagina di man di sched_setscheduler(2). C'è un kernel speciale che supporta l'hard real time; per maggiori informazioni vedere http://luz.cs.nmt.edu/~rtlinux/.

Pause: sleep() e usleep()

Incominciamo con le più semplici chiamate di funzioni di temporizzazione. Per ritardi di più secondi la scelta migliore è, probabilmente, quella di usare sleep(). Per ritardi dell'ordine delle decine di millisecondi (il ritardo minimo sembra essere di circa 10 ms) dovrebbe andar bene usleep(). Queste funzioni cedono la CPU agli altri processi (vanno a dormire: "sleep"), in modo che il tempo di CPU non venga sprecato. Per i dettagli vedere le pagine di man di sleep(3) e usleep(3).

Per ritardi inferiori a, circa, 50 millisecondi (dipendentemente dal carico del sistema e dalla velocità del processore e della macchina) il rilascio della CPU richiede troppo tempo; ciò perché (per le architetture x86) lo scheduler generalmente lavora, almeno, dai 10 ai 30 millisecondi prima di restituire il controllo ad un processo. Per questo motivo, per i piccoli ritardi, usleep(3) in genere ritarda un po' più della quantità specificatagli nei parametri, almeno 10 ms circa.

nanosleep()

Nei kernel Linux della serie 2.0.x, c'è una nuova chiamata di sistema, nanosleep() (vedere la pagina di man di nanosleep(2)), che permette di "dormire" o ritardare per brevi periodi di tempo (pochi microsecondi o più).

Per ritardi fino a 2 ms, se (e solo se) il processo è impostato per lo scheduling in soft real time (usando sched_setscheduler()), nanosleep() usa un ciclo di attesa, altrimenti dorme ("sleep"), proprio come usleep().

Il ciclo di attesa usa udelay() (una funzione interna del kernel usata da parecchi kernel driver) e la lunghezza del ciclo viene calcolata usando il valore di BogoMips (la velocità di questo tipo di cicli di attesa è una delle cose che BogoMips misura accuratamente). Per i dettagli sul funzionamento vedere /usr/include/asm/delay.h.

Ritardare tramite l'I/O sulle porte

Un altro modo per realizzare un ritardo di pochi microsecondi è di effettuare delle operazioni di I/O su una porta. La lettura dalla, o la scrittura sulla (come si fa è stato descritto precedentemente), porta 0x80 di un byte impiega quasi esattamente un microsecondo, indipendentemente dal tipo e dalla velocità del processore. È possibile ripetere tale operazione più volte per ottenere un'attesa di alcuni microsecondi. La scrittura sulla porta non dovrebbe avere controindicazioni su nessuna macchina standard (e infatti qualche driver del kernel usa questa tecnica). Questo è il metodo normalmente usato da {in|out}[bw]_p() per realizzare il ritardo (vedere asm/io.h).

In effetti una istruzione di I/O su una qualunque delle porte nell'intervallo 0-0x3ff impiega quasi esattamente un microsecondo; quindi se, per esempio, si sta accedendo direttamente alla porta parallela, si possono semplicemente effettuare degli inb() in più per ottenere il ritardo.

Ritardare usando le istruzioni assembler

Se si conosce il tipo e la velocità del processore della macchina su cui girerà il programma, è possibile sfruttare del codice basato sull'hardware, che usa certe istruzioni assembler, per realizzare dei ritardi molto brevi (ma ricordare che lo scheduler può sospendere il processo in qualsiasi momento, quindi i ritardi potrebbero essere impredicibilmente più lunghi del previsto). Nella tabella seguente la velocità interna del processore determina il numero di cicli di clock richiesti. Ad esempio, per un processore a 50 MHz (tipo un 486DX-50 o un 486DX2-50) un ciclo di clock dura 1/50.000.000 di secondo (pari a 200 nanosecondi).

Istruzione      cicli di clock      cicli di clock
                  su un i386          su un i486
xchg %bx,%bx          3                   3
nop                   3                   1
or %ax,%ax            2                   1
mov %ax,%ax           2                   1
add %ax,0             2                   1

I cicli di clock dei Pentium dovrebbero essere gli stessi degli i486, ma nei Pentium Pro/II add %ax, 0 potrebbe richiedere solo 1/2 ciclo di clock. Si può rimediare usando un'altra istruzione (a causa dell'esecuzione fuori ordine non è necessario che tale istruzione sia quella immediatamente successiva nel codice).

Le istruzioni nop e xchg, indicate nella tabella, non dovrebbero avere effetti collaterali. Le altre potrebbero modificare i flag dei registri, ma ciò non dovrebbe essere un problema poiché gcc dovrebbe accorgersene. Per realizzare istruzioni di ritardo xchg %bx, %bx è una buona scelta.

Per usarle bisogna chiamare asm("istruzione"). La sintassi delle istruzioni è come nella tabella precedente. Se si vogliono mettere più istruzioni in un singolo asm() bisogna separarle con dei punti e virgola. Ad esempio asm("nop ; nop ; nop ; nop") esegue quattro istruzioni nop, generando un ritardo di quattro cicli di clock sui processori i486 o Pentium (o 12 cicli di clock su un i386).

Gcc traduce asm() in codice assembler inline, per cui si risparmiano i tempi per la chiamata di funzione.

Ritardi più brevi di un ciclo di clock sono impossibili con le architetture Intel x86.

rdtsc per i Pentium

Con i Pentium è possibile ottenere il numero di cicli di clock trascorsi dall'ultimo riavvio del sistema con il seguente codice C (che esegue l'istruzione RDTSC):


   extern __inline__ unsigned long long int rdtsc()
   {
     unsigned long long int x;
     __asm__ volatile (".byte 0x0f, 0x31" : "=A" (x));
     return x;
   }

Si può sondare tale valore per ritardare del numero di cicli di clock desiderato.

4.2 Misurare il tempo

Per tempi della precisione dell'ordine del secondo è probabilmente più facile usare time(). Per tempi più precisi, gettimeofday() è preciso fino a circa un microsecondo (ma vedere quanto già detto riguardo lo scheduling). Con i Pentium, il frammento di codice sopra (rdtsc) ha una precisione pari a un ciclo di clock.

Se si desidera che un processo riceva un segnale dopo un certo quanto di tempo, usare setitimer() o alarm(). Per i dettagli vedere le pagine di man delle suddette funzioni.


Avanti Indietro Indice