Aufgrund der oben erwähnten Eigenschaften der RTC wird in Unix wie auch in anderen Betriebssystemen eine Uhr in Software realisiert. Dazu wird ein regelmäßiger Interrupt benutzt, der das Betriebssystem regelmäßig die dafür eingerichtete Interrupt-Routine ausführen läßt. In dieser Interrupt-Routine werden viele Aufgaben erledigt, z.B. das Prozeß-Scheduling und das Weiterstellen der Software-Uhr.
Im Gegensatz zur Hardware-Uhr , auf die mit
hwclock
(8) zugegriffen werden kann, wird die im Unix
kernel implementierte Software-Uhr mit dem Kommando
date
(1) ausgelesen bzw. gesetzt. date
läßt aber die Hardware-Uhr unverändert.
Die Software-Uhr ist im kernel als eine Struktur vom Typ
struct timeval
gespeichert, die folgendermaßen
definiert ist:
typedef struct timeval { long tv_sec; long tv_usec; };
Die erste Komponente tv_sec
enthält die Sekunden,
die seit einem bestimmten festgelegten Zeitpunkt vergangen sind.
Dieser Zeitpunkt ist festgelegt als der 1. Januar 1970, 0:00:00 UTC.
UTC wurde zwar erst 1972 eingeführt, so daß dieser
Zeitpunkt genaugenommen gar nicht existiert, aber es wird diese Zeit
einfach im nachhinein definiert, wobei angenommen wird, daß es
keine Schaltsekunden vor
1972 gab.
Die zweite Komponente Diese Zeitdefinition ist im POSIX-Standard festgehalten, wobei der
POSIX-Standard auch festlegt, daß die Schaltsekunden bei den
Zeitangaben in einem struct timeval nicht mitgezählt
werden. Das halte ich für einen ziemlichen Design-Fehler in
POSIX. Es hat zwar den Vorteil, daß in POSIX jeder Tag genau
86400 Sekunden lang ist, bringt aber auch mit sich, daß
bestimmte Zeiten, wie z.B. 1972-06-30 23:59:60 UTC, gar nicht
dargstellt werden können.
tv_usec
enthält die
Mikrosekunden, seit dem Beginn der letzten Sekunde.
Die folgenden Zeiten werden also in POSIX so dargestellt:
1970-01-01 00:00:00 UTC | 0.000000 | |
1970-01-01 00:00:01 UTC | 1.000000 | |
1970-01-02 00:00:00 UTC | 86400.000000 | |
1969-12-31 23:59:59 UTC | -1.000000 | |
1967-01-31 16:27:00 UTC | -92043180.000000 | |
1972-01-01 00:00:00 UTC | 63072000.000000 | |
1999-06-18 12:13:14 UTC | 929707994.000000 | (*) |
(*) in Wirklichkeit sind zu diesem Zeitpunkt aber schon 929708016 Sekunden seit 1970-01-01 00:00:00 UTC vergangen, weil es 22 Schaltsekunden zwischen 1972 und 1999-06-18 gegeben hat. POSIX zählt diese aber wie gesagt nicht mit.
Auf die Software-Uhr des Unix kernel's kann mit zwei system calls zugegriffen werden:
int gettimeofday(struct timeval *tv, struct timezone *tz); int settimeofday(struct timeval *tv, struct timezone *tz);
Der zweite Parameter für die Zeitzone wird heute eigentlich
nicht mehr verwendet, da der Unix kernel mit Zeitzonen absolut nichts
zu tun hat. Mit den beiden system calls kann die Zeit des kernel's
gelesen bzw. gesetzt werden, wobei sie natürlich nur vom super
user root gesetzt werden darf. Es muß dazu ein Zeiger auf eine
Struktur vom Typ timeval
übergeben werden, in der
die zu setzende Zeit steht bzw. in die die Zeit vom kernel geschrieben
wird.
Vor gettimeofday
(2) und settimeofday
(2),
die erst in 4.2 BSD eingeführt wurden, konnte die Zeit nur mit
einer Genauigkeit von 1 Sekunde vom kernel abgefragt werden. Der
dafür definierte time_t
typedef long time_t;findet aber auch heute noch in vielen Funktionen Verwendung. Der Linux kernel stellt auf allen Architekturen außer Alpha auch heute noch (zusätzlich zu
gettimeofday
(2)/settimeofday
(2)) das alte
Interface bestehend aus den Funktionen
time_t time(time_t *tp); int stime(time_t *tp);zur Verfügung.
Das oben erwähnte date
-Kommando benutzt die
system calls time(2)
und stime(2)
, um die
Systemzeit zu lesen bzw. zu setzen.
Auf Alphas sind time
(2) und stime
(2) in
der C library implementiert und rufen die system calls
gettimeofday
(2) bzw. settimeofday
(2) auf.
Auf den anderen Architekturen sind time
(2) und
stime
(2) nur noch aus Kompatibilitätsgründen im
kernel.
In beiden Fällen werden aber die Sekunden seit 1970-01-01
00:00:00 UTC heute als vorzeichenbehafteter 32bit-integer
(signed long
) dargestellt. Damit wird der Zeitbereich
von -231 bis 231-1 abgedeckt, d.h. von
Fri Dec 13 20:45:52 1901 UTCbis
Tue Jan 19 03:14:07 2038 UTC
Das heißt, Unix hat (im kernel) kein Jahr-2000-Problem, sondern ein Jahr-2038-Problem. Dies läßt sich jedoch ganz leicht (im kernel und in den meisten Anwendungen) durch einfaches Ersetzen von
typedef long time_t;durch
typedef long long time_t;in einem einzigen header file und durch Neuübersetzen lösen.
Danach tritt das Problem dann erst wieder in 5.8*1011 Jahren auf :-)
Was passiert nun in der Interrupt-Routine, von der oben die Rede war. Auf Intel-Architekturen wird sie mit 100 Hz aufgerufen. Dies wird durch einen Timer-Chip, den Intel 8253 oder Intel 8254, realisiert. Heute ist der Chip allerdings nicht mehr einzeln auf dem Mainboard zu finden, sondern in irgendeinen Chip-Satz integriert. Dieser Chip enthält einen 16-bit counter, der mit einer Frequenz von 1.139180 MHz abwärts zählt. Jedesmal, wenn dieser counter bei 0 ankommt, wird er wieder mit einem bestimmten Anfangswert geladen und ein Interrupt an der CPU ausgelöst. Der Anfangswert wird so festgelegt, daß möglichst genau 100 Hz Interruptfrequenz herauskommen, nämlich auf 11392. Die genaue Frequenz beträgt dann
1.139180 MHz / 11392 = 99.998244 Hzd.h. die Interrupt-Routine wird alle 10.000176 ms aufgerufen. Die kleine Abweichung von genau 100 Hz beachten wir vorerst nicht.
Zwischen zwei Aufrufen der Interrupt-Routine vergehen also 10000 µs, d.h. die Software-Uhr sollte also um diese Zeit weitergestellt werden. Genau das passiert auch in der Interrupt-Routine:
tick = 10000; xtime.tv_usec += tick; if (xtime.tv_usec >= 1000000) { xtime.tv_usec -= 1000000; xtime.tv_sec++; }
Auf Alpha-Maschinen wird statt 100 Hz eine Interrupt-Frequenz von 1024 Hz verwendet. Hier besteht das Problem, daß
1000000 / 1024 Hz = 976.5625 µskein ganzzahliges Vielfaches von 1 µs ist. Man könnte aber z.B. 1023 mal die Variable xtime um 977 µs und beim 1024. Mal um 529 µs erhöhen, denn
1000000 = 1023 * 977 + 529
Das hat aber den Nachteil, daß die Zeit dann ein mal pro Sekunde für einen tick deutlich langsamer läuft, nämlich mit nur 529 µs/tick statt 977 µs/tick, d.h. die Uhrfrequenz wäre nicht stabil.
Micro$oft hat an dieser Stelle wohl schon längst aufgegeben, sich um gute und stabile Zeit zu kümmern, weshalb NT server auch keine guten NTP server abgeben. Denn da sind solche Überlegungen und eine äußerst stabile Zeit durchaus wichtig.
Um die Frequenzstabilität zu erreichen, wird eine weitere
Variable im kernel gehalten, die die Ungenauigkeiten bei jedem tick
mit einer Genauigkeit von 1 µs/220, also
ungefähr 1 ps (pico = 10-12) aufaddiert. Wenn diese
aufsummierten Abweichungen mehr als 1 µs betragen, wird
xtime
sofort in dem entsprechenden tick um diese 1
Mikrosekunde korrigiert, so daß die Ungenauigkeit nie mehr als 1
µs beträgt und die Uhrfrequenz um nicht mehr als 1
µs/tick schwankt.
Bei 1024 Hz Interrupt-Frequenz wird also die Uhr bei jedem Interrupt um 976 µs oder 977 µs weitergestellt, so daß sie nie um mehr als 1 µs von der "richtigen" Zeit abweicht.
Wir haben damit eine interrupt-gesteuerte Software-Uhr, die zu den Zeitpunkten der Interrupts einen sehr genauen Wert enthält. Sie wird aber in großen Schritten von 10000 µs oder 976 µs weitergestellt und verändert sich zwischen den Interrupts nicht.
Beim Aufruf von gettimeofday
(2) wäre aber eine
Zeit, die genauer ist als ±0.01 s, wünschenswert. Dies
wird folgendermaßen erreicht (für Intel-PCs):
Beim Aufruf von gettimeofday
(2) enthält
xtime
die genaue Zeit zum Zeitpunkt des letzten
Interrupts. Der Hardware-Zähler, der mit 1.139180 MHz
dekrementiert wird und die Interrupts auslöst, kann aber auch
ausgelesen werden. Die Differenz seines Anfangswertes 11392 und
dem aktuellen Wert ist die Anzahl der 1.139180 MHz-Takte, die seit dem
letzten Interrupt vergangen sind. Damit kann die Zeit seit dem
letzten Interrupt auf 1/1.139180 MHz = 0.84 µs genau bestimmt
werden und auf xtime
addiert werden. Damit erhält
man eine Uhr, die nicht nur die Auflösung sondern auch eine
Genauigkeit von 1 µs besitzt.
Das Auslesen des Hardware-Zählers ist eine recht langsame
Operation, für die es bei manchen CPUs eine bessere Alternative
gibt. Bei Intel Pentium CPUs und bei manchen Modellen anderer
Hersteller gibt es einen 64-bit-Zähler, der nach dem Reset auf 0
gesetzt wird und dann mit der Taktfrequenz der CPU hochzählt. Ob
die verwendete CPU diesen cycle counter besitzt, kann man leicht mit
cat /proc/cpuinfo | grep flags
feststellen. Das flag
tsc
steht für diesen Zähler. (Dieser
Zähler kann übrigens auch mit einem kleinen C Programm ausgelesen werden.)
Der Linux kernel benutzt diesen Zähler, sofern er vorhanden
ist. Die 100Hz-Interuptfrequenz wird weiterhin vom 8254 mit dem
Teilerwert 11392 erzeugt. In jedem Timer-Interrupt liest der kernel
den 64-bit cycle counter und merkt sich seinen Wert. Beim Aufruf von
gettimeofday
(2) wird der cycle counter gelesen und die
Differenz zum gemerkten Wert aus der Interrupt-Routine gebildet.
Damit kann die Zeit, die seit dem letzten Timer-Interrupt vergangen
ist, ermittelt werden und auf xtime
addiert werden.
OK, you probably get the idea. For more details goto
/usr/src/linux/kernel/{time.c,sched.c}
and
/usr/src/linux/arch/i386/kernel/time.c
which makes for
really interesting reading.