Raspberry-Pi: Prozess-Priorität

Prof. Jürgen Plate

Raspberry Pi: Prozess-Priorität

Alle Prozesse des System werden von einer Komponente des Kernels verwaltet, dem Scheduler. Aufgabe des Schedulers ist das Multiplexen der Prozesse (und ggf. der CPUs), so dass alle rechenbereiten Prozesse gemäss einer Schedlung-Strategie bedient werden. Standardmäßig wird bei UNIX-Betriebssystemen "Round Robin" mit Prioritäten eingesetzt, d. h. es kommt jeder rechenbereite Prozess der Reihe nach eine Zeit lang dran ("Zeitscheibe"). Die Zeitscheibe eines Prozesses gibt an, wie lange er laufen darf ohne verdrängt zu werden. Die Größe der Zeitscheibe eines Prozesses ist von seiner Priorität abhängig: Prozesse mit hoher Priorität erhalten mehr CPU-Zeit als solche mit niedriger. Die kleinste Zeitscheibe beträgt 10, die längste 200 Millisekunden. Ein Prozess mit dem »nice«-Wert null erhält die Standard- Zeitscheibe von 100 Millisekunden.

Linux kennt standardmäßig 140 Prioritätslevels Hierbei entspricht null der höchsten und 139 der niedrigsten Priorität. Die Level von 1 .. 99 sind für Tasks mit Echtzeitpriorität reserviert. Alle anderen Prozesse erhalten eine Priorität, die ihrem "nice"-Werts (abgeleitet vom Kommando nice) entspricht, wobei die "nice"-Werte von -20 .. 19 auf den Bereich von 101 .. 140 abgebildet werden. Während des Ablaufs eines Prozesses verändert sich durch seinen Interaktivitätsgrad jedoch seine Priorität. Diese dynamische Priorität errechnet der Scheduler aus der statischen Priorität und der Prozessinteraktivität. Entsprechend seiner Interaktivität erhält ein Prozess vom Scheduler entweder einen Bonus oder einen Malus (Penalty). Interaktive Prozesse gewinnen über einen Bonus maximal fünf Prioritätslevels hinzu, während jene Prozesse, die eine geringe Interaktivität aufweisen, maximal fünf Prioritätslevels verlieren. Die dynamische Priorität eines Prozesses mit einem "nice"-Wert von 5 beträgt demnach zwischen 0 und 10.

Für Messwerterfassung und Steuerugn sind logischerweise die Prozesse mit so genannter Echtzeitprioritat (1 .. 99) interessant. Trotz dieses Namens sind Sie aber nicht mit den Prozessen eines Echtzeit- bzw. Realzeit-Betriebssystems vergleichbar. Es gibt bei Linux keine "harten" Echtzeitbedingungen (garantierte Antwortzeiten). Der Kernel stellt jedoch sicher, dass ein lauffähiger Echtzeit-Prozess immer die CPU bekommt, wenn er auf kein Ereignis warten muss, er freiwillig die CPU abgibt und wenn kein lauffähiger Echtzeitprozess höherer Priorität existiert. Ein entsprechender Prozess wird also, sofern er lauffähig ist, auch jeden anderen normalen (SCHED_OTHER-)Prozess verdrängen. Alle Echtzeit-Features können nur von root beziehungsweise von dessen Prozessen genutzt werden. Die beiden Der Scheduler vergibt für Echtzeitprozesse keine dynamischen Prioritäten. Echtzeit-Strategien des Schedulings unter Linux sind SCHED_FIFO und SCHED_RR:

Prozesse ohne Echtzeitpriorität führt der Scheduler mit der Strategie SCHED _OTHER aus.

Die Systemaufrufe, die den Scheduler in seiner Arbeitsweise beeinflussen, bzw. die Parameter abfragen werden sind:

Für das Lesen der aktuellen Prozesspriorität gibt es noch folgende Funktionen:
int sched_get_priority_min(int policy);
int sched_get_priority_max(int policy);
Zum Setzen und Lesen der Parameter dienen:
int sched_setparam(pid_t pid, const struct sched_param *param);
int sched_getparam(pid_t pid, struct sched_param *param);
Im Erfolgsfall liefern die Funktionen 0 zurück, bei Fehler -1.

Das erste Beispielprogramm liest nur mal alle Parameter und gibt sie auf dem Bildschirm aus:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sched.h>

struct sched_param param;

int main(void)
  {
  memset(&param, 0, sizeof(param));
  if (sched_getparam(0, &param) < 0)
    {
    perror("Scheduler getparam failed...\n");
    return 1;
    }
  printf("Priority: %d\n",param.sched_priority);
  printf("RR min:   %d\n",sched_get_priority_min(SCHED_RR));
  printf("RR max:   %d\n",sched_get_priority_max(SCHED_RR));
  printf("FIFO min: %d\n",sched_get_priority_min(SCHED_FIFO));
  printf("FIFO max: %d\n",sched_get_priority_max(SCHED_FIFO));
  return 0;
  }
Die Ausgabe zeigt wie erwartet die oben beschriebenen Werte:
pi@raspberrypi ~ $ ./sched1
Priority: 0
RR min:   1
RR max:   99
FIFO min: 1
FIFO max: 99

Im folgenden Programm wird einfach mal versucht, wie sich die Policy und die Priorität verändern lassen. Zur Überprüfung wird eine Funktion getinfo() verwendent, welche die Policy und die Priorität einliest und beides ausgibt. Sie wird im Hauptprogramm einmal am Anfang udn dann anch dem Ändern der Priorität aufgerufen:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sched.h>

struct sched_param param;
int scheduler;

void getinfo(void)
  {
  if (sched_getparam(0, &param) < 0)
    {
    perror("Scheduler getparam failed...\n");
    return;
    }
  printf("Priority: %d\n",param.sched_priority);
  if ((scheduler = sched_getscheduler(0)) < 0)
    {
    perror("Scheduler getscheduler failed...\n");
    return;
    }
  printf("Policy:   %d  ",scheduler);
  switch (scheduler)
    {
    case 0: printf("SCHED_OTHER\n"); break;
    case 1: printf("SCHED_FIFO\n"); break;
    case 2: printf("SCHED_RR\n");
    }
  }

int main(void)
  {
  memset(¶m, 0, sizeof(param));
  printf("Standard:\n");
  getinfo();
  param.sched_priority = 10;
  if (sched_setscheduler(0,SCHED_FIFO, &param) < 0)
    {
    perror("Scheduler setscheduler failed...\n");
    return 1;
    }
  printf("\nNach Aenderung:\n");
  getinfo();

  return 0;
  }
Die Ausgabe zeigt, dass im Normalfall die Priorität 0 ist und als Policy SCHED_OTHER verwendet wird. Die hier vorgenommene Änderung liegt mit dem Wert 10 noch im Standard-"Nice"-Bereich, sodass das Programm auch ohne root-Rechte laufen würde:
pi@raspberrypi ~ $ sudo ./sched2
Standard:
Priority: 0
Policy:   0  SCHED_OTHER

Nach Aenderung:
Priority: 10
Policy:   1  SCHED_FIFO

Nur stellt sich die Frage, ob die Echtzeit-Policy überhaupt etwas bringt. Das folgende Programm macht für alle drei Policies eine Zeitmessung. Zum Verbrauchen von Rechenzeit dient die rekursive Variante der Fakultätsberechnung (Funktion fak()), den einfache Dinge wie eine leere for-Schleife würde der Compiler wegoptimieren. Die Funktion getinfo() ist aus dem vorhergehenden Programm übernommen worden. Für die Zeitmessung wird gettimeofday() verwendet. Wer über diese mehr wissen will, kann es im Kapitel über Timer nachlesen. Für alle drei Policies wird die Info ausgegeben, dann die Rechendauer für Fakultät(100000) gemessen und diese in Mikrosekunden ausgegeben.

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sched.h>
#include <sys/time.h>

struct sched_param param;
int scheduler;
struct timeval t1, t2;
long long elapsedTime;

long long fak(long long i)
   {
   if (i <= 0) return 1;
   else return fak(i-1)*i;
   }

void getinfo(void)
  {
  if (sched_getparam(0, &param) < 0)
    {
    perror("Scheduler getparam failed...\n");
    return;
    }
  printf("Priority: %d\n",param.sched_priority);
  if ((scheduler = sched_getscheduler(0)) < 0)
    {
    perror("Scheduler getscheduler failed...\n");
    return;
    }
  printf("Policy:   %d  ",scheduler);
  switch (scheduler)
    {
    case 0: printf("SCHED_OTHER\n"); break;
    case 1: printf("SCHED_FIFO\n"); break;
    case 2: printf("SCHED_RR\n");
    }
  }

int main(void)
  {
  memset(&param, 0, sizeof(param));

  printf("Standard:\n");
  getinfo();
  gettimeofday(&t1, NULL);
  fak(100000);
  gettimeofday(&t2, NULL);

  /* Berechne die verbrauchte Zeit in Microsekunden */
  elapsedTime = ((t2.tv_sec * 1000000) + t2.tv_usec)
              - ((t1.tv_sec * 1000000) + t1.tv_usec);
  printf("Aufruf im Normalmodus dauerte  %lld us\n", elapsedTime);

  printf("\nFIFO-Modus:\n");
  param.sched_priority = 99;
  if (sched_setscheduler(0,SCHED_FIFO, &param) < 0)
    {
    perror("Scheduler setscheduler failed...\n");
    return 1;
    }
  getinfo();
  gettimeofday(&t1, NULL);
  fak(100000);
  gettimeofday(&t2, NULL);

  /* Berechne die verbrauchte Zeit in Microsekunden */
  elapsedTime = ((t2.tv_sec * 1000000) + t2.tv_usec)
              - ((t1.tv_sec * 1000000) + t1.tv_usec);
  printf("Aufruf im FIFO-Echtzeitmodus dauerte  %lld us\n", elapsedTime);

  printf("\nRR-Modus:\n");
  param.sched_priority = 99;
  if (sched_setscheduler(0,SCHED_RR, &param) < 0)
    {
    perror("Scheduler setscheduler failed...\n");
    return 1;
    }
  getinfo();
  gettimeofday(&t1, NULL);
  fak(100000);
  gettimeofday(&t2, NULL);

  /* Berechne die verbrauchte Zeit in Microsekunden */
  elapsedTime = ((t2.tv_sec * 1000000) + t2.tv_usec)
              - ((t1.tv_sec * 1000000) + t1.tv_usec);
  printf("Aufruf im RR-Echtzeitmodus dauerte  %lld us\n", elapsedTime);


  return 0;
  }
Bei einen Raspberry Pi, auf dem sonst nichts läuft, wäre der Unterschied zwischen Normal- und Echtzeit-Modus schon sichtbar, aber nicht spektakulär. Das Programm im würde im Echtzeit-Modus etwa 85% der Zeit es Normal-Modus benötigen - immerhin. Wenn ich aber boshafterweise einen Shell-Einzeiler im Hintergrund laufen lasse, der relativ viel CPU-Leistung beansprucht, zeigt sich der Unterschied ganz eklatant. Hier wurd der Normalprozess der Shell vom Programm im Echtzeit-Modus kaum noch an die CPU gelassen.
pi@raspberrypi ~ $ while :; do /bin/true; done &
[1] 2609

pi@raspberrypi ~ $ sudo ./sched3
Standard:
Priority: 0
Policy:   0  SCHED_OTHER
Aufruf im Normalmodus dauerte  87675 us

FIFO-Modus:
Priority: 99
Policy:   1  SCHED_FIFO
Aufruf im FIFO-Echtzeitmodus dauerte  27834 us

RR-Modus:
Priority: 99
Policy:   2  SCHED_RR
Aufruf im RR-Echtzeitmodus dauerte  27505 us

Für den täglichren Gebrauch kann man sich zwei Funktionen schreiben, die entweder in den Echtzeit-Modus oder zurück in den Normalmodus schalten und diese dann in allen Programmen verwenden, die es besonders notwendig haben:

int set_max_priority(void)
  {
  struct sched_param sched;
  memset(&sched, 0, sizeof(sched));
  sched.sched_priority = sched_get_priority_max(SCHED_FIFO);
  return (sched_setscheduler(0, SCHED_FIFO, &sched));
  }

int set_normal_priority(void)
  {
  struct sched_param sched;
  memset(&sched, 0, sizeof(sched));
  sched.sched_priority = 0;
  return (sched_setscheduler(0, SCHED_OTHER, &sched));
  }
Fazit: Das Wichtigste ist jedoch, dass bei SCHED_FIFO und SCHED_RR die gemessene Zeit in allen Fällen nahezu identisch ist - egal, ob andere Programme laufen oder nicht. Insofern nähern diese Policies sich schon etwas an die Echtzeitbedingungen an.


Copyright © Hochschule München, FK 04, Prof. Jürgen Plate und die Autoren
Letzte Aktualisierung: