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/.
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
.
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.
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 PentiumCon 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.
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.