Bei der Verwendung von Kernel-Timern habe ich über die Jahre viele verschiedene Fehler gesehen, die zum Teil extrem schwer zu debuggen sind. Hier sind die aus meiner Sicht wichtigsten How-Not-To-Dos (Stand 2014):
- Freigeben des timer_list-Speichers solange der Timer noch läuft. Dies führt oft zu BUGs in cascade oder get_next_timer_interrupt. Der Inhalt der timer_list-Struktur wird nicht kopiert und an Ort und Stelle vom Kernel verwendet. Ein häufig gemachter Fehler besteht darin den Timer nicht korrekt zu beenden, bevor der Speicherbereich freigegeben oder das Kernelmodul entladen wird.
- Falsches Verständnis von timer_pending. Die Funktion prüft aus Performance-Gründen nur, ob der Next-Pointer in der timer_list ungleich NULL ist und nicht ob der Timer tatsächlich eingehängt ist.
- Unsichere Benutzung von del_timer: Die Funktion prüft nicht, ob der Timer gerade läuft (und sich eventuell selbst wieder einhängt). Schlauere Programmierer nutzen hier del_timer_sync. Hier wird gewartet, bis der eventuell laufende Timer abgeschlossen wird. Was die meisten hier vergessen, ist dass sich der gerade noch laufende Timer auch hier wieder einhängen kann. Es wird allgemein empfohlen, das wiedereinhängen via atomic_t-Variable zu sichern. Das schlimmste, was ich hier bisher gesehen habe, ist ein „del_timer_sync(&timer); del_timer_sync(&timer); „, was zwar meist hilft, aber nicht sicher ist.
- Falsches Verständnis der Arbeitsweise von del_timer: Die Funktion prüft wie timer_pending nicht, ob und wo ein Timer in die Liste eingehängt ist. Wenn der next-Pointer in der struct list_head ungleich NULL ist, wird prev->next mit ->next überschrieben. Nicht mehr und nicht weniger. Ein del_timer einer uninitialisierten struct timer_list hat somit katastrophale Folgen.
- Kein Schutz gegen versehentliches Überschreiben: Am Anfang der struct timer_list liegt die struct list_head entry; welche für die interne Verwaltung der Timer verwendet wird. Oft sehe ich, dass eine struct timer_list in einer anderen Datenstruktur liegt und direkt vor der timer_list Arrays oder Puffer angeordnet werden. Dies kann durch den Programmierer oder auch durch den Compiler geschehen. Man sollte dies z.B. über das Programm „pahole“ verifizieren. Arrays „tendieren“ dazu über ihre Grenzen hinweg beschrieben zu werden, was dann wiederum zu Fehlern führt, die sehr schwer zu finden sind. Mein Tipp: timer_lists gehören an den Anfang der Struktur, zumindest jedoch nicht hinter Arrays.
- Mangelndes Wissen darüber, was alles kernintern als Timer abgebildet wird: msleep, mleep_interruptable, queue_delayed_work werden intern über Timer realisiert. Das heißt, dass z.B. delayed_work ebenfalls sehr empfindlich auf Freigabe des Speichers reagiert.
Im Folgenden ist ein kleines Beispiel, das versuch die wichtigsten Klippen bei der Timer-Nutzung zu umschiffen:
//Timer list object to be used by the Linux Kernel struct timer_list example_timer; // atomic_t example_timer_valid; static void example_timer_function(void *arg) { // Do some work //if needed, re-arm the timer, but only when it is still valid if (atomic_read(&example_timer_valid)) { timer->expires = jiffies + msecs_to_jiffies( DELAY ); add_timer(&example_timer); } } void example_timer_init(void) { //initialize data structure init_timer(&example_timer); //set the function to be called via timer timer.function = example_timer_function; //mark the timer as valid atomic_set(&example_timer_valid, 1); //Data portion to be used as argument for the timer timer->data = NULL; } void example_timer_start(void *arg) { if (atomic_read(&example_timer_valid) && !(timer_pending(&example_timer)) { timer->expires = jiffies + msecs_to_jiffies( DELAY ); add_timer(&example_timer); } } void example_timer_stop(void *arg) { if (atomic_read(&example_timer_valid)) { //mark the timer as invalid, prevent re-arming atomic_set(&example_timer_valid, 0); //wait for the timer to finish if it's running del_timer_sync(&example_timer); } }