Die Software-Uhr

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 tv_usec enthält die Mikrosekunden, seit dem Beginn der letzten Sekunde.

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.

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 UTC
bis
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 :-)

OK, now we're digging into the kernel

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 Hz
d.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 µs
kein 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.