C library Funktionen -- Zeit im user land

Die für die Software-Uhr beschriebenen Mechanismen sind komplett im Unix kernel implementiert. Diese Uhr stellt die Zeit als die Anzahl der Sekunden dar, die seit einem bestimmten, in POSIX festgelegten Zeitpunkt vergangen sind. Damit ist diese Uhr komplett unabhängig von Zeitzonen und sollte auf jedem Unix-Rechner den gleichen Wert haben. Außerdem ist mit Zeiten dieser Uhr leicht umzugehen, da eine Zeitangabe einfach nur aus einer ganzen Zahl besteht, und nicht aus mehreren Angaben für Jahr, Monat, Tag, Stunde, Minute und Sekunde.

Auch in normalen Programmen können Zeiten natürlich in diesem Format dargestellt und bearbeitet werden, wie sie sie vom kernel bekommen. Für die Ein- und Ausgabe von Zeiten vom bzw. zum Benutzer sollten die Zeitangaben normalerweise in die gängige Darstellung gebracht werden und zwar abhängig von der lokalen Zeitzone.

Dieses Umrechnen kann sehr aufwendig werden, wenn die Regeln für die lokale Zeitzone berücksichtigt werden sollen und weil verschieden lange Monate und Schaltjahre berücksichtigt werden müssen.

Damit diese Routinen nicht für jedes Programm, das mit Zeiten zu tun hat, neu implementiert werden müssen, sind eine Reihe von Routinen schon in der Standard C library enthalten. Diese Routinen laufen also komplett im user space ab, d.h. der Unix kernel hat hiermit absolut nichts mehr zu tun. Außer dem Typ

	typedef long time_t;
mit dem die Zeiten in der Systemzeit des kernels dargestellt werden, wird für die Routinen der C library ein neuer Typ definiert:
	typedef struct tm {
		int     tm_sec;         /* seconds           0..61 */
		int     tm_min;         /* minutes           0..59 */
		int     tm_hour;        /* hours             0..23 */
		int     tm_mday;        /* day of the month  1..31 */
		int     tm_mon;         /* month             0..11 */
		int     tm_year;        /* year - 1900*/
		int     tm_wday;        /* day of the week   0..6   */
		int     tm_yday;        /* day in the year   0..365 */
		int     tm_isdst;       /* daylight saving time */
	};

Die C library stellt nun folgende Routinen zur Verfügung:

	struct tm *gmtime(const time_t *tp);
	struct tm *localtime(const time_t *tp);
Diese beiden Funktionen wandeln eine Zeitangabe in Form eines time_t in Datum und Uhrzeit in Form eines struct tm um. gmtime() liefert UTC, während localtime() die Zeit der lokalen Zeitzone berechnet, wenn die lokale Zeitzone richtig eingestellt wurde.

Die beiden Funktionen

	char *asctime(const struct tm *tp);
	char *ctime(const time_t *tp);
erwarten eine Zeitangabe in Form eines time_t bzw. struct tm und liefern die entsprechende Darstellung der Zeit als String der Form
	Wed Jun 30 21:49:08 1993\n
Die Funktion ctime() wandelt dazu zunächst den time_t in die Zeit der lokalen Zeitzone um und hängt damit natürlich auch von der korrekten Einstellung der Zeitzone ab. ctime(tp) ist äquivalent zu asctime(localtime(tp)).

Die Funktion

	time_t mktime(struct tm *tp);
ist die inverse Funktion zu localtime(). Sie erwartet eine Zeitangabe der eingestellten lokalen Zeitzone und berechnet daraus die Zeitangabe in Form eines time_t.

Eine weitere interessante Funktion ist

	size_t strftime(const char *s, size_t maxsize, const char *format, const struct tm *tp);
Diese Funktion erwartet einen Zeiger auf eine als struct tm angegebene Zeit und konvertiert sie in einen String. Im Gegensatz zu asctime(3) und ctime(3) kann bei strftime(3) jedoch ein beliebiges Format, ähnlich wie bei *printf(3), angegeben werden. Das Format von asctime(3) und ctime(3) erhält man mit der Format-Spezifikation
	"%a %b %d %H:%M:%S %Y\n"
während die Spezifikation
	"%Y-%m-%d %H:%M:%S"
die Zeit im ISO 8601-Format liefert.

Beispiele

Beispiele für die Verwendung der hier beschriebenen Funktionen sind in hier zu finden. Es handelt sich um ein paar kleine Programme

Außerdem gibt's noch die shell scripts ex und time-of-unix, die ein paar Beispiele mit diesen kleinen Programmen zeigen.

Ein minimales date-Kommando habe ich auch noch auf die Schnelle implementiert, mit einem kleinen Beispiel-Script date-ex. Der Programmcode date.c ist ausführlich kommentiert (hat am meisten Zeit gekostet :-) und ist damit kein real-world-program mehr :-)

Einstellen der Zeitzone

Die Umrechnung von Zeitdarstellungen in Form eines time_t und lokaler Zeitzonen ist natürlich von der richtigen Einstellung der Zeitzone abhängig. Außerdem müssen die für die Umrechnung verantwortlichen Funktionen Informationen über die lokale Zeitzone und Dinge wie Sommerzeit und Winterzeit (Daylight Saving Time in den USA) haben.

Eine Möglichkeit, die auch auf manchen Unixen noch verwendet wird, obwohl sie zu unflexibel ist, besteht darin, diese Informationen in die environment-Variable TZ (timezone) zu schreiben.

In der einfachsten Form sieht das dann so aus:

	TZ=PST8PDT; export TZ
für eine zur Bourne shell (sh) kompatible shell (z.B. bash, zsh, ksh) bzw.
	setenv TZ PST8PDT
in der C shell (csh) und deren Derivate (z.B. tcsh). Damit werden die Namen der Standardzeit (hier PST für Pacific Standard Time), der Offset gegenüber UTC (hier 8 Stunden), sowie der Name einer alternativen Zeitrepräsentation (hier PDT für Pacific Daylight Saving Time) angegeben.

Es wird per default davon ausgegangen, daß die alternative Zeit um eine Stunde gegenüber der Standardzeit verschoben ist, und daß sie immer zwischen dem ersten Sonntag im April und dem letzten Sonntag im Oktober, jeweils 2:00:00 Uhr Standardzeit gilt. Das sind nämlich die Regeln für die Daylight Saving Time in den USA.

In der Mitteleuropäischen Zeit gelten jedoch andere Regeln, die man auch in der Variable TZ formulieren kann:

	TZ=CET-1CEST,M3.5.0/2:00,M10.5.0/3:00
Diese Angabe bedeutet, daß

Diese Methode hat aber immer noch den Nachteil, daß sie Änderungen der Regeln nicht berücksichtigen kann. So galt z.B. vor 1996 in der Mitteleuropäischen Zeit, daß die Sommerzeit am letzten Sonntag im September endet. D.h. time stamps, die älter sind als 1996, z.B. modification dates an einer Datei, werden evtl. nicht richtig umgewandelt. Hier ein kleines Beispiel (mit dem GNU ls, das die --full-time option kennt). Ich habe eine Datei, die am 17. Oktober 1993 um 13:04:29 UTC erzeugt wurde:

	$ TZ=UTC ls -l --full-time bib
	-rw-r--r--   1 urs      users       11370 Sun Oct 17 13:04:29 1993 bib
Mit der Zeitzonenbeschreibung von oben mit den Regeln ab 1996 wird die Zeit aber falsch anzeigt:
	$ TZ=CET-1CEST,M3.5.0/2:00,M10.5.0/3:00 ls -l --full-time bib
	-rw-r--r--   1 urs      users       11370 Sun Oct 17 15:04:29 1993 bib
Am 17. Oktober 1993 war die Sommerzeit des Jahres 1993 schon vorbei, so daß der offset gegenüber UTC nur 1 Stunde war. Hier wird aber Sommerzeit und damit ein Unterschied von 2 Stunden angenommen.

Außerdem gab es in verschiedenen Gebieten während des zweiten Weltkriegs und in den ersten Jahren danach ganz unregelmäßige Zeitzonen. Informationen zu Sommerzeiten in Deutschland sind bei der PTB zu finden: vor 1950 und nach 1980.

In den USA gilt während der Präsidentschaftswahlen im ganzen Land einheitlich die "US Presidential Election Time", damit'se da alle gleichzeitig vor der Glotze hocken, und es gibt noch 'ne Menge mehr Unregelmäßigkeiten.

Die library von Arthur David Olson

Der bessere Weg ist also, die Informationen über solche Umrechnungen in Dateien abzulegen, in einem Format, das alle Unregelmäßigkeiten und Änderungen leicht erfassen kann.

Genau das macht die Implementierung der oben beschriebenen Funktionen in der library von Arthur David Olson, die auch in der libc-5 und in der glibc-2 (aka libc-6) für Linux übernommen wurde.

Für jede Zeitzone existiert eine eigene Datei im directory /usr/share/zoneinfo (bei glibc-2) bzw. in /usr/lib/zoneinfo (bei libc-5). In diesen Dateien können alle oben genannten Regeln mit beliebig häufigen Änderungen beschrieben und von den library-Funktionen benutzt werden.

Welche dieser Dateien benutzt wird, kann mit der environment-Variable TZ festgelegt werden. TZ braucht dazu einfach nur auf den Namen der zu benutzenden Datei, relativ zu /usr/share/zoneinfo, gesetzt werden. Mit

	TZ=CET date
wird das Datum und Uhrzeit in Mitteleuropäischer Zeit, die in /usr/share/zoneinfo/CET beschrieben ist, angezeigt. Mit
	TZ=US/Pacific date
	TZ=Iran date
wird die Zeit an der Westküste der USA (beschrieben in /usr/share/zoneinfo/US/Pacific) bzw. im Iran (/usr/share/zoneinfo/Iran) angezeigt. Genauso gilt das natürlich für alle anderen tools, die Zeiten anzeigen oder einlesen, wie z.B. ls -l, touch, at, tar tv, emacs (in der mode line), etc. Möchte man also beim Hacken oder mail Schreiben immer sehen, wie spät es gerade in Japan ist, kein Problem: TZ=Japan emacs.

Um alle Zeitangaben in der eigenen, lokalen Zeitzone passend angezeigt zu bekommen, braucht man aber nicht für alle Prozesse (also z.B. in ~/.bash_login) die Variable TZ zu setzen. Ist TZ nämlich nicht gesetzt, wird /etc/localtime (bzw. /usr/lib/zoneinfo/localtime bei libc-5) gelesen. Daher muß nur ein symbolischer link von der gewünschten Zeitzonenbeschreibung in /usr/share/zoneinfo nach /etc/localtime gesetzt zu werden (bzw. entsprechend für libc-5), also z.B. durch

	ln -s /usr/share/zoneinfo/CET /etc/localtime
In den Konfigurationstools der Linux-Distributionen kann man die gewünschte Zeitzone meist in einem Menü eintragen, und das Konfigurationstool setzt dann genau diesen symbolischen link.

(Es kann unter Umständen auch sinnvoll sein, die Datei zu kopieren statt einen symlink zu setzen, damit die Information auch schon beim booten zugreifbar sind, wenn /usr noch nicht gemounted ist.)

So, neugierig geworden, wie denn nun unsere Regeln für Sommerzeit tatsächlich definiert sind? ... less /usr/share/zoneinfo/CET ... )-: Sieht ja ziemlich unleserlich aus. Wer hat das bloß geschrieben?

Einfache Antwort: zic hat das geschrieben. zic ist der zone info compiler, der die Beschreibungen der Regeln in einer bestimmten Syntax liest und die zoneinfo files in /usr/share/zoneinfo daraus erzeugt. Die Beschreibungen, die zic einliest, sind z.B. in den glibc-sourcen enthalten, bei mir also in /usr/src/glibc-2.1.3/timezone/. Sie sind mit etlichen Kommentaren zu Quellen der Zeitinformationen versehen. Ist wirklich interessant zu lesen, besonders der Abschnitt über die Festlegungen für Great Britain, ... die spinnen die Briten.

Das von zic erzeugte file kann man sich auch noch nachträglich ansehen, dann natürlich ohne die Kommentare und die genaue Formulierung der Regeln. Das geht mit zdump, z.B. durch den Aufruf zdump -v CET.

Wer jetzt tatsächlich mal etwas in /usr/share/zoneinfo herumgeguckt hat, wird dort evtl. (je nach Installation) zwei subdirectories entdeckt haben: posix/ und right/.

Im directory posix/ sind nocheinmal alle Dateien aus /usr/share/zoneinfo/ enthalten. Sie enthalten die eben beschrieben Informationen, wobei Schaltsekunden nach POSIX nicht berücksichtigt werden. Im directory right/ sind Dateien desselben Namens zu finden. Sie enthalten aber Informationen, wann Schaltsekunden in UTC auftreten, und diese sind natürlich auch bei den Angaben über Zeitpunkte des Wechsels von Sommer-/Winterzeit berücksichtigt. Den Namen right/ halte ich zur Abgrenzung zu posix/ für richtig gewählt :-)

Man kann auch die Dateien im directory right/ benutzen, z.B. mit TZ=right/CET date und wird dann den Unterschied von zur Zeit 22 Sekunden sehen (gut zu sehen ist der Unterschied mit date; TZ=right/CET date), falls die zoneinfo files aktuell genug sind (wie gesagt werden Schaltsekunden erst ca. ein halbes Jahr im voraus festgelegt). Das alles nützt aber wenig, wenn man nicht mit der restlichen Welt inkompatibel sein will, z.B. bei time stamps in tar files, weil die restliche Unix-Welt die brain-damaged POSIX time stamps verwendet.