Forum: Mikrocontroller und Digitale Elektronik C/C++ - RT fähige Implementierung von localtime/gmtime


von Philip (Gast)


Lesenswert?

Hallo,

die Funktionen der C Standardbibliothek sind unter Linux nicht 
echtzeitfähig (verwendet FUTEX), ich vermute mal, weil Sie Informationen 
wie die Zeitzone vom OS benötigen.
Ich bin auf der Suche nach einer Implementierung die ich in einer RT 
Umgebung verwenden kann. Kennt jemand eine?

Gruß
Philip

von Jörg W. (dl8dtl) (Moderator) Benutzerseite


Lesenswert?

Philip schrieb:
> die Zeitzone vom OS

Das OS hat keine Zeitzone. Die gibt's nur auf Anwendungsebene. Die 
Systemzeit tickt in UTC.

Zum Auslesen der Uhr (Auflösung jenseits der normalen Taktrate) muss man 
sicherstellen, dass nur ein Prozess aktiv ist.

Reentrante Versionen von localtime/gmtime gibt es als localtime_r / 
gmtime_r. Die beiden müssen ja nichts im OS selbst machen, die laufen 
nur in der Bibliothek.

von Christian B. (casandro)


Lesenswert?

Welche Funktionen brauchst Du genau? Was willst Du machen? Willst Du 
wirklich eine Ortszeit haben oder reicht Dir UTC?

Wenn Dir UTC ausreicht, kannst Du das in einer halben Stunde selber 
schreiben. Der einfachste Algorithmus trennt Datum von Uhrzeit 
(Division) und zählt dann das Datum jahrweise, dann monatsweise hoch.

Grob formuliert ist das ungefähr wie folgt:

int schaltjahr(int jahr)
{
   if(jahr%4!=0) return 0;
   return 1; //ggf weitere Regeln beachten
}

int tageprojahr(int jahr)
{
   if (schaltjahr(jahr)==1) return 366;
   return 365;
}

int tagepromonat(int jahr, int monat)
{...
}


int x=Unix-Epoche eingangsvariable
int jahr=1970; //Ausgabevariable
int monat=0; //Ausgabevariable 0=Januar
int tag=0; //0=1.

int datum=x/(24*60*60);
int zeit=x%(24*60*60);

int stunde=zeit/3600;
int minute=(zeit/60)%60;
int sekunde=zeit%60;

int y=0; //Zählvariable
while ((y+tageprojahr(jahr))<x) {
   jahr=jahr+1;
   y=y+tageprojahr(jahr);
}
while ((y+tagepromonat(jahr,monat))<x) {
   monat=monat+1;
   y=y+tagepromonat(jahr, monat);
}
tag=x-y;



Willst Du Ortszeit, so wird das ein sehr viel größeres Projekt.

Allerdings kannst Du natürlich auch murksen und nur die Sommer- und 
Winterzeitumstellung für eine Region und jetzt beachten, dann wird das 
relativ simpel.

von Rolf M. (rmagnus)


Lesenswert?

Ansonsten gäbe es noch clock_gettime(). Die Funktion gehört zur 
Realtime-Bibliothek.

von Philip (Gast)


Lesenswert?

Jörg W. schrieb:
> Das OS hat keine Zeitzone. Die gibt's nur auf Anwendungsebene.
Aber irgendwo muss die Zeitzone doch zentral gespeichert sein.

Auch für die reentrant Varianten habe ich vom OS-Hersteller (ist eine 
firmeninterne RT-Variante von Linux) die Info bekommen, dass sie FUTEXe 
verwenden.

Jörg W. schrieb:
> Zum Auslesen der Uhr (Auflösung jenseits der normalen Taktrate) muss man
> sicherstellen, dass nur ein Prozess aktiv ist.
Auf die laufenden Prozesse habe ich keinen Einfluss. Es geht aber auch 
nicht um das Auslesen der Uhr. Ich habe einen Zeitstempel im EpochTime 
Format, den ich in eine lesbare Zeit umwandeln muss.

von Philip (Gast)


Lesenswert?

Christian B. schrieb:
> Wenn Dir UTC ausreicht, kannst Du das in einer halben Stunde selber
> schreiben.
UTC würde mir reichen. Danke für die Anregung. Ich hatte die eigene 
Implementierung als letzte Option natürlich auf dem Schirm. Aber 
Zeitberechnungen haben eben so ihre Fallstricke, schon deshalb suche ich 
erstmal nach einem erprobten Algorithmus.

von Rolf M. (rmagnus)


Lesenswert?

Philip schrieb:
> Jörg W. schrieb:
>> Das OS hat keine Zeitzone. Die gibt's nur auf Anwendungsebene.
> Aber irgendwo muss die Zeitzone doch zentral gespeichert sein.

Ja, in /etc/localtime.

> Auf die laufenden Prozesse habe ich keinen Einfluss. Es geht aber auch
> nicht um das Auslesen der Uhr.

Ok, das habe ich dann auch falsch verstanden.

von Jörg W. (dl8dtl) (Moderator) Benutzerseite


Lesenswert?

Philip schrieb:
> Jörg W. schrieb:
>> Das OS hat keine Zeitzone. Die gibt's nur auf Anwendungsebene.
> Aber irgendwo muss die Zeitzone doch zentral gespeichert sein.

Es gibt eine Default-Zeitzone, deren Speicherort hat dir Rolf schon 
geschrieben. Die lässt sich aber durch eine Environmentvariable 
überschreiben.

> Auch für die reentrant Varianten habe ich vom OS-Hersteller (ist eine
> firmeninterne RT-Variante von Linux) die Info bekommen, dass sie FUTEXe
> verwenden.

Hast du denn keinen Sourcecode? Linux sollte doch Opensource sein.

Habe bei FreeBSD mal nachgeschaut, die benutzen die übliche 
"Olson"-Bibliothek dafür. Dort gibt es in der Tat einen Lock, aber das 
ist lediglich ein read lock, der nur dann blockiert, wenn jemand zur 
gleichen Zeit irgendwas mit tzset() anstellt. Sofern du also garantieren 
kannst, dass niemand tzset() oder tzsetwall() jemals aufruft, sollte 
dich dieser Lock nicht stören.

UTSL :-)

von Christian B. (casandro)


Lesenswert?

Philip schrieb:
> Aber
> Zeitberechnungen haben eben so ihre Fallstricke, schon deshalb suche ich
> erstmal nach einem erprobten Algorithmus.

Wenn Du so was wie die "UNIX-Epoche" verwendest rechnest Du einfach mit 
Integern. Somit ist das relativ einfach.

Um die Zeit selbst zu zählen brauchst Du halt eine ISR. In dieser machst 
Du im Prinzip so was hier:

static int zeit=0;
static int usec=0;
static int intervall=geschätzter ISR Intervall in µsec

void isr()
{
  usec=usec+intervall;
  if (usec>1000000) {
     zeit=zeit+1;
     usec=usec-1000000;
  }
}

Wenn Du die Zeit abgleichst, kannst Du einen Teil des Zeitfehlers auf 
zeit und usec anpassen, und einen anderen Teil auf intervall, so dass Du 
den Gangfehler Deines Quarzes ausgleichen kannst. Besonders bei kleinen 
Fehlern bietet es sich an nur intervall anzupassen.

von M.K. B. (mkbit)


Lesenswert?

Ich weiß nicht welchen Standard dein Compiler kann, aber vielleicht 
findest du hier was.

Das baut auf std::chrono auf und benötigt C++11.

https://howardhinnant.github.io/date/date.html

von Jiri D. (Gast)


Lesenswert?

Das ist nicht so ganz einfach. Die richige Funktion für dich ist 
vermutlich  clock_gettime(). Das ganze drumherum ist in der glibc, also 
userspace. Auch die ganzen C++ std::chrono Klassen sind mit 
clock_gettime implementiert (einfach mal in die sourcen schauen...).

clock_gettime() und einige andere dergleichen Funkionen sehen vielleicht 
wie syscalls aus, machen auf einem normalen System aber überhaupt keine. 
Syscalls (bzw. die userspace/kernelspace switche) brauchen lange, was 
schlecht ist. Der Kernel stellt jedem Prozess daher die "vdso" zur 
Verfügung. Das ist eine normale shared library, die nicht physisch auf 
Platte liegt, sondern vom Kernel/Loader kommt. Diese implementiert die 
clock_gettime() und andere. Dazu mappt der Kernel den "vvar" Bereich in 
den Prozess. Da ist ein Kernel-struct drinnen, das die ganzen 
Kompensationsparameter für die Uhrzeitberechnung enthält.

Siehe die letzten beiden Einträge:
1
$ cat /proc/self/maps 
2
55f1d4a85000-55f1d4a8d000 r-xp 00000000 fd:01 16135                      /bin/cat
3
55f1d4c8c000-55f1d4c8d000 r--p 00007000 fd:01 16135                      /bin/cat
4
55f1d4c8d000-55f1d4c8e000 rw-p 00008000 fd:01 16135                      /bin/cat
5
55f1d5417000-55f1d5438000 rw-p 00000000 00:00 0                          [heap]
6
7f1572cd1000-7f1572fec000 r--p 00000000 fd:03 49629                      /usr/lib/locale/locale-archive
7
7f1572fec000-7f1573181000 r-xp 00000000 fd:01 96471                      /lib/x86_64-linux-gnu/libc-2.24.so
8
7f1573181000-7f1573381000 ---p 00195000 fd:01 96471                      /lib/x86_64-linux-gnu/libc-2.24.so
9
7f1573381000-7f1573385000 r--p 00195000 fd:01 96471                      /lib/x86_64-linux-gnu/libc-2.24.so
10
7f1573385000-7f1573387000 rw-p 00199000 fd:01 96471                      /lib/x86_64-linux-gnu/libc-2.24.so
11
7f1573387000-7f157338b000 rw-p 00000000 00:00 0 
12
7f157338b000-7f15733ae000 r-xp 00000000 fd:01 96416                      /lib/x86_64-linux-gnu/ld-2.24.so
13
7f157357f000-7f1573581000 rw-p 00000000 00:00 0 
14
7f157358c000-7f15735ae000 rw-p 00000000 00:00 0 
15
7f15735ae000-7f15735af000 r--p 00023000 fd:01 96416                      /lib/x86_64-linux-gnu/ld-2.24.so
16
7f15735af000-7f15735b0000 rw-p 00024000 fd:01 96416                      /lib/x86_64-linux-gnu/ld-2.24.so
17
7f15735b0000-7f15735b1000 rw-p 00000000 00:00 0 
18
7ffc8b0d3000-7ffc8b0f4000 rw-p 00000000 00:00 0                          [stack]
19
7ffc8b1be000-7ffc8b1c1000 r--p 00000000 00:00 0                          [vvar]
20
7ffc8b1c1000-7ffc8b1c3000 r-xp 00000000 00:00 0                          [vdso]

vvar ist (read-only für deinen Prozess) Kernel-Speicher und der Kernel 
aktualisiert regelmäßig die Parameter. Die clock_gettime() 
Implementierung verwendet hauptsächlich die CPU counter (rdtsc). Die TSC 
Geschwindigkeit kann sich aber ändern (driften), z.B. mit der 
Temperatur, daher braucht man regelmäßig einen Abgleich des Taktes. Und 
diese Parameter legt der Kernel in regelmäßigen Abständen in das struct.

Auf diese Art und Weise braucht clock_gettime() normalerweise (99.99%... 
keine Ahnung viel viel genau, aber wirklich fast immer!) sehr wenig 
Zeit, so ca. 28 ns. Das wäre mit syscalls nicht zu machen. AFAIK ist das 
gleich bei normalen und -RT Linux Kernels (kann das sicher aber nur für 
linux-rt und Server CPUs sagen).

Jetzt der schwierigere Teil:
clock_gettime() ist nur meistens so schnell. In seltenen Fällen braucht 
ein einzelner Aufruf auch mal ~10 us (drei Größenordnungen!). Das 
passiert wenn der Kernel das struct im vvar aktualisiert und es dazu 
locken muss (zu viele Daten für atomaren Zugriff). Dein userspace 
clock_gettime() wird also so lange warten, bis der Kernel das struct 
wieder freigegeben hat.

Siehe der code im do{}while() hier:
https://elixir.bootlin.com/linux/latest/source/arch/x86/entry/vdso/vclock_gettime.c#L159
1
notrace static int do_hres(clockid_t clk, struct timespec *ts)
2
{
3
  struct vgtod_ts *base = &gtod->basetime[clk];
4
  u64 cycles, last, sec, ns;
5
  unsigned int seq;
6
7
  do {
8
    seq = gtod_read_begin(gtod);
9
    cycles = vgetcyc(gtod->vclock_mode);
10
    ns = base->nsec;
11
    last = gtod->cycle_last;
12
    if (unlikely((s64)cycles < 0))
13
      return vdso_fallback_gettime(clk, ts);
14
    if (cycles > last)
15
      ns += (cycles - last) * gtod->mult;
16
    ns >>= gtod->shift;
17
    sec = base->sec;
18
  } while (unlikely(gtod_read_retry(gtod, seq)));
19
20
  /*
21
   * Do this outside the loop: a race inside the loop could result
22
   * in __iter_div_u64_rem() being extremely slow.
23
   */
24
  ts->tv_sec = sec + __iter_div_u64_rem(ns, NSEC_PER_SEC, &ns);
25
  ts->tv_nsec = ns;
26
27
  return 0;
28
}

Weiter unten in der selben Datei findet man auch die anderen Aufrufe, 
die im vdso sind, z.B. time, clock_gettime, gettimeofday...

Und du kannst auch sehen, dass der Kernel auf den traditionellen syscall 
urückfällt, wenn du den vdso-Support nicht hinein kompiliert hast. Zudem 
funktioniert das vdso auch nur bei manchen Uhrtypen, z.B. 
CLOCK_MONOTONIC und CLOCK_REALTIME. Beispielsweise wird bei CLOCK_TAI 
aber ein syscall gemacht.

Wenn du gelegentlich mal clock_gettime() aufrufst, dann kann dir der 
Ausreißer in der Ausführungsdauer vermutlich egal sein. Der Rest deines 
Systems wird viel mehr Jittern und die Uhr driften. Wenn du ein mit PTP 
auf <1 us genau synchronisiertes Regelungssystem hast und auf wenige us 
genau Dinge machen möchtest, dann kann das schon mal blöd sein (oder man 
muss damit leben).

von Jiri D. (Gast)


Lesenswert?

Philip schrieb:
> Christian B. schrieb:
>> Wenn Dir UTC ausreicht, kannst Du das in einer halben Stunde selber
>> schreiben.
> UTC würde mir reichen. Danke für die Anregung. Ich hatte die eigene
> Implementierung als letzte Option natürlich auf dem Schirm. Aber
> Zeitberechnungen haben eben so ihre Fallstricke, schon deshalb suche ich
> erstmal nach einem erprobten Algorithmus.

Würde ich niemals selbst implementieren. Die ganzen Sonderfälle 
(Schaltjahre, Schaltsekunden) wird man selber nicht richtig hinbekommen.

Bei UTC: sei dir im klaren, das die UTC Schaltsekunden besitzt. Die 
POSIX clock wird in dem Fall zwar nicht eine Sekunde zurückspringen, 
aber sie wird den Tick während der Schaltsekunde halb so schnell machen. 
Also zwei SI-Sekunden lang. Wenn du eine kontinuierliche und absolute 
Zeitbasis brauchst, wirst du um TAI (Temps Atomique International, 
International Atomic Time) nicht herum kommen.

Zuständig für Schaltsekunden ist übrigens der International Earth 
Rotation and Reference Systems Service (IERS, https://www.iers.org/). 
Die beobachten den Fehler der UTC zur UT1 (die ΔUT1 oder DUT1) und 
entscheiden jedes habe Jahr, ob zum Ende des Halbjahres (Juni/Dezember) 
eine weitere Schaltsekunde fällig wird. Die Entscheidungen werden im 
Bulletin C bekannt gegeben.

https://www.iers.org/SharedDocs/News/EN/BulletinC.html

Und der aktuelle:
https://datacenter.iers.org/data/latestVersion/16_BULLETIN_C16.txt
1
NO leap second will be introduced at the end of December 2019.
2
 The difference between Coordinated Universal Time UTC and the
3
 International Atomic Time TAI is :
4
 
5
     from 2017 January 1, 0h UTC, until further notice : UTC-TAI = -37 s

Also alles ganz unspektakulär dieses Jahr...

von Jörg W. (dl8dtl) (Moderator) Benutzerseite


Lesenswert?

Jiri D. schrieb:
> Die richige Funktion für dich ist vermutlich  clock_gettime().

Nein: er will gar keine Uhr auslesen, sondern nur einen Timestamp in 
lesbare Form bringen.

Siehe oben, nach Review des Olson-Sourcecodes bin ich der Meinung, dass 
er das machen kann, da es zwar einen read lock gibt, aber die Bedingung, 
unter welcher dieser blockieren könnte, völlig klar und überschaubar 
ist.

von Philip (Gast)


Lesenswert?

Vielen Dank für die rege Beteiligung.

Um es noch mal deutlich zu machen - ich habe einen Zeitstempel, der 
bereits im UTC Format vorliegt. Damit sollte eine einfache Umrechnung 
unter Berücksichtigung der Schaltjahre ja eigentlich kein Problem sein.

Warum die Verwendung von gmtime_r() Probleme macht kann ich nicht sagen. 
Aber ich kann sehen, dass andere Prozesse/Threads ein nicht akzeptables 
Zeitverhalten haben, wenn ich sie verwende. Ich ich denke werde jetzt 
also die Umrechnung auf Basis des Vorschlags von Christian B. selbst 
implementieren.

@Jörg W.
möglicherweise wird in unserem Fall ja nicht die Olson-Bibliothek 
verwendet. Ich versuche mal das bei den OS-Entwicklern in Erfahrung zu 
bringen.

von Jim M. (turboj)


Lesenswert?

Philip schrieb:
> Warum die Verwendung von gmtime_r() Probleme macht kann ich nicht sagen.

Einfach mal in die Quelle schauen. GLibC ist Open Source.
Dort findet man relativ bald die Ursache im Time Zone Handling:
1
/* Return the `struct tm' representation of *TIMER in the local timezone.
2
   Use local time if USE_LOCALTIME is nonzero, UTC otherwise.  */
3
struct tm *
4
__tz_convert (const time_t *timer, int use_localtime, struct tm *tp)
5
6
// [... snip ...]
7
8
__libc_lock_lock (tzset_lock);
9
10
// [...] Code für das TZ Handling
11
// der u.a. auch das TZ File liesst und parst
12
13
14
__libc_lock_unlock (tzset_lock);

Also kein Wunder dass das im Real Time Pfad knallt. Die Umrechnung von 
Zeitzonen ist überraschend aufwändig.

Daher auch die Verwunderung hier dass Du das ausgerechnet in Real Time 
machen willst. Dort sollte man sowas vermeiden wenn irgendwie möglich. 
Die Umrechnung sollte erst nahe am oder im UI erfolgen, das dann nicht 
mehr Real Time sein muss.

von Jörg W. (dl8dtl) (Moderator) Benutzerseite


Lesenswert?

Jim M. schrieb:
> Also kein Wunder dass das im Real Time Pfad knallt.

Was mich wundert: __libc_lock_lock() scheint nicht zwischen read lock 
und write lock zu unterscheiden, oder täusche ich mich da?

Wenn dem so ist, dann ist die glibc hier drastisch schlechter als die 
Olson-Bibliothek. Diese hat, wie beschrieben, an dieser Stelle nur einen 
read lock, der nur dann ausbremsen würde, wenn zur gleichen Zeit jemand 
in einem anderen Thread versucht, die Zeitzone neu einzustellen. Lesend 
kann man die einmal vorgenommene Einstellung beliebig oft parallel 
zugreifen.

Vielleicht ja ansonsten einfach auf die Olson-Bibliothek dafür 
ausweichen, die ist schließlich ebenso Opensource (wird im FreeBSD nur 
als 3rd-party-Code hinzugezogen).

von Chris (Gast)


Lesenswert?

Ich bin jetzt nicht sicher, ob genau das mit der "Olson Bibliothek" 
gemeint ist, aber die IANA bietet selber auch etwas Source-Code für ihre 
Timezone-Datenbanken an:

https://www.iana.org/time-zones

Der Coding-Style ist arg gewöhnungsbedürftig, aber damit kann man sich 
etwas bauen, was erstmal unabhängig von den C-Bibliotheken ist, und 
zumindest unter Berücksichtgung der Fallstricke richtig rechnet 
(netterweise auch, wenn es doch mal was anderes als UTC sein soll, was 
es ja fast automatisch wird, wenn irgend ein Altags-Mensch das ganze mal 
zu Gesicht bekommen soll.)

von Jörg W. (dl8dtl) (Moderator) Benutzerseite


Lesenswert?

Yep, das ist sie. Zitat aus dem README:
1
Thanks in particular to Arthur David Olson, the project's founder and first
2
maintainer, to whom the time zone community owes the greatest debt of all.
3
None of them are responsible for remaining errors.

von Jörg W. (dl8dtl) (Moderator) Benutzerseite


Lesenswert?

Wobei ich gerade sehe, dass sie dort auch nur einen globalen Lock auf 
alles drin haben. :-(

Die FreeBSD-Version davon sieht so aus:
1
#define _RWLOCK_RDLOCK(x)                                               \
2
                do {                                                    \
3
                        if (__isthreaded) _pthread_rwlock_rdlock(x);    \
4
                } while (0)
5
6
#define _RWLOCK_WRLOCK(x)                                               \
7
                do {                                                    \
8
                        if (__isthreaded) _pthread_rwlock_wrlock(x);    \
9
                } while (0)
10
11
#define _RWLOCK_UNLOCK(x)                                               \
12
                do {                                                    \
13
                        if (__isthreaded) _pthread_rwlock_unlock(x);    \
14
                } while (0)
15
// ...
16
17
struct tm *
18
localtime_r(const time_t *const timep, struct tm *tmp)
19
{
20
        _RWLOCK_RDLOCK(&lcl_rwlock);
21
        tzset_basic(1);
22
        tmp = localsub(timep, 0L, tmp);
23
        _RWLOCK_UNLOCK(&lcl_rwlock);
24
        return tmp;
25
}

von Jörg W. (dl8dtl) (Moderator) Benutzerseite


Lesenswert?

Falls die zu benutzende Zeitzone von vornherein feststeht, könnte man 
sich natürlich eine Variante bauen, in der genau diese statisch 
eincompiliert wird. Dann braucht man keine Locks mehr.

Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.