![]() |
Raspberry Pi: Programme beim Systemstart ausführenProf. Jürgen Plate |
Möchten Sie eigene Dienste, Skripte oder Programme beim Systemstart ausführen, bieten sich drei Möglichkeiten an, sofern es sich nicht um Anwendungen mit grafischer Bedieneroberfläche handelt.
In der Regel sind die Anforderungen an das Start-Programm oder -Script nicht hoch. Meinst soll nur etwas eingetragen oder aktiviert werden (z. B. irgendwelche GPIO-Pins, siehe GPIO beim Raspberry Pi) und beim Herunterfahren des System ist gar keine Aktion nötig. Für solche Zwecke kann man Kommandos und Skript-Aufrufe in die Datei /etc/rc.local eintragen. Diese Einträge werden auch immer mit root-Rechten ausgeführt.
Sie könnten jeweils ein Start-/Stopp-Skript erstellen und dazu Links in den entsprechenden Runlevel-Verzeichnissen. Werfen Sie einfach mal einen Blick in das Verzeichnis /etc/init.d/, wo all diese Skripte versammelt sind und in die Runlevel-Verzeichnisse /etc/rc1.d bis /etc/rc6.d, in denen sich symbolische Links auf die Scripte in /etc/init.d/ befinden. Für die meisten Anwendungen beim Raspberry Pi ist das aber oft mit Kanonen auf Spatzen geschossen.
Die dritte Möglichkeit lassen Sie das Programm per Cron-Dienst (crontab-Kommando) starten. Bei dieser Methode werden im Gegensatz zu den beiden vorhergehenden keine zeitlichen Abhängigkeiten oder Runlevel beachtet.
Es versteht sich von selbst, dass die meisten der im Folgenden geschilderten Tätigkeiten nur mit Root-Rechten ausgeführt werden können.
Für einfache Zwecke kann man Kommandos und Skript-Aufrufe in die Datei /etc/rc.local eintragen. Diese Einträge werden auch immer mit root-Rechten ausgeführt. Die Datei bearbeiten Sie als root mit Ihrem Lieblings-Editor. Die eigene Einträge werden vor der Zeile "exit 0" eingetragen (diese Zeile muss auch immer die letzte Zeile der Datei bleiben). Da Sie nicht wissen, welche Pfade beim Ausführen von rc.local gesetzt sind, ist die Angabe des vollständigen Pfads zum gewünschten Programm zwingend erforderlich, Sie schreiben also z. B. statt machwas immer /usr/local/bin/machwas. Normalerweise ist die Datei (bis auf exit 0 und einige Kommentare leer. Als Beispiel habe ich mal die Aktivierung des an anderer Stelle erwähnten Heartbeat eingetragen (das entsprechende Modul muss natürlich auch geladen sein):
#!/bin/sh -e # # rc.local # # This script is executed at the end of each multiuser runlevel. # Make sure that the script will "exit 0" on success or any other # value on error. # # In order to enable or disable this script just change the execution # bits. # # By default this script does nothing. # Print the IP address _IP=$(hostname -I) || true if [ "$_IP" ]; then printf "My IP address is %s\n" "$_IP" fi export USER='root' export LOGNAME='root' # Jetzt kommt die neue Zeile echo heartbeat >/sys/class/leds/led0/trigger exit 0Bei Scripts und Programmen nicht die Execute-Rechte vergessen (chmod +x foobar).
Die Vereinbarung der Variablen USER und LOGNAME ist für den Fall gedacht, dass in der rc.local aufgerufene Scripte prüfen, ob sie vom Root-User aufgerufen werden und nicht von einem Useraccount, der gar nicht genügend Rechte besitzt. Zum Zeitpunkt, wenn rc.local abgearbeitet wird, sind nur sehr wenige Shell-Variablen gesetzt.
Soll das so gestartete Programm nicht einfach ablaufen und sich dann beenden, sondern als Dienst so lange weiterlaufen, bis der Raspberry wieder heruntergefahen wird, haben Sie unter Umständen das Problem, dass die in den folgenden Zeilen aufgefürten Programme gar nicht zum Zuge kommen. Betrachten Sie dazu die folgenden drei Beispielzeilen:
... echo heartbeat >/sys/class/leds/led0/trigger /usr/bin/python3 /home/pi/scripts/loop.py /usr/local/bin/init_GPIO.sh exit 0Der echo-Befehl in der ersten Zeile wird sicher schnell beendet sein. Wenn aber das Programm loop.py ein Dienst ist, der ständig laufen soll, wird es wohl nicht beendet und die folgend Zeile mit dem Script init_GPIO.sh kommt nie zur Ausführung. Darum sollten solche Programm immer durch ein nachgestelltes "&"-Zeichen in den Hintergrund geschoben werden. Dann können die folgenden Programm weiter ausgeführt werden. Eine Ausnahme gäbe es nur, wenn loop.py seinerseits einen Hintergrundprozess (als Daemon = Dienst) starten und sich dann sofort beenden würde. Das korrekte Beispiel sähe dann so aus:
... echo heartbeat >/sys/class/leds/led0/trigger /usr/bin/python3 /home/pi/scripts/loop.py & /usr/local/bin/init_GPIO.sh exit 0
Manchmal sollen Programme erst zeitverzögert gestartet werden. Das erreichen Sie, indem Sie die Ausführung mittels sleep verzögert in einer Subshell ausführen, was auf jeden Fall im Hintergrund erfolgen muss (damit die Ausführung von rc.local weiterläuft). Zum Beispiel:
# startet das Kommando foobar nach 60 Sekunden (/bin/sleep 60 && /usr/local/bin/foobar)&
Die eingetragenen Befehle werden beim nächsten Start des Systems ausgeführt. Es sind keine weiteren Schritte nötig. Zum (Grob-)Test sollten Sie auf jeden Fall vor einem Reboot die Datei auch von Hand (als root) aufrufen:
sudo sh -vx /etc/rc.localDie Option -vx protokolliert alle Schritte auf dem Bildschirm. Da bei rc.local standardmäßig die Option -e angegeben ist, wird die Ausführung sofort abgebrochen, sobald eines der Kommandos einen Fehler meldet (Returnwert ungleich 0), so dass die Datei nicht vollständig ausgeführt wird. Um dieses Verhalten abzustellen, können Sie das "-e" löschen.
Ab der Version "Jessie" führt das System aufgrund der Umstellung von SysVinit auf Systemd nicht mehr automatisch aus. Um den gewünschten (alten) Zustand wieder herzustellen müssen Sie eine neue Systemd-Unit anlegen und aktivieren. Dazu legen Sie eine neue Datei namens /etc/systemd/system/rc-local.service mit dem folgenden Inhalt an. Ich habe das als Shellskript formuliert, dass man als Root ausführen kann:
#!/bin/bash cd /etc/systemd/system/ # Datei erzeugen cat > rc-local.service <<EOF # This file is part of systemd. # # systemd is free software; you can redistribute it and/or modify it # under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. [Unit] Description=/etc/rc.local Compatibility ConditionPathExists=/etc/rc.local [Service] Type=forking ExecStart=/etc/rc.local start TimeoutSec=0 StandardOutput=tty RemainAfterExit=yes SysVStartPriority=99 [Install] WantedBy=multi-user.target EOF # Aktivieren chmod a+X rc-local.service systemctl enable rc-local.service # Starten systemctl start rc-local.serviceNach den nächsten Reboot sollte alles laufen. Sie können den Status mit folgenddem Programm testen:
systemctl status rc-local.service
Sie könnten prinzipiell aber auch jeweils ein Start-/Stopp-Skript erstellen, wie es ausführlich im UNIX-Skript beschrieben ist und dazu Links in den entsprechenden Runlevel-Verzeichnissen. An dieser Stelle fasse ich daher die im UNIX-Skript besprochenen Dinge kurz zusammen. Interessant ist diese Methode eigentlich nur für Dienste, die dauernd laufen sollen. Hierzu legen Sie ein so genanntes Start-Script im Verzeichnis /etc/init.d/ an, das Kommandozeilen-Parameter wie "start", "stop", "restart" oder "status" akzeptiert. Der Vorteil dabei ist nicht nur eine übersichtliche Vorgehensweise, sondern auch die Möglichkeit, das Script auch nach dem Start des Raspberry zur Steuerung des Dienstes (Daemons) zu benutzen.
Netterweise existiert im Verzeichnis /etc/init.d schon ein Basis-Script namens skeleton ("Skelett"), das Sie kopieren können. In den folgenden Beispielen nenne ich das Script "foobar" (cp /etc/init.d/skeleton /etc/init.d/foobar). Jedes Startscript beginnt mit einem Kommentar-Kopf, in dem angegeben wird, für welches Programm das Script ist (Provides), welche Abhängigkeiten existieren (Required-Start, Required-Stop) und welche Runlevel gewünscht werden (Default-Start, Default-Stop). Die Voreinstellungen sollten normalerweise passen. Dazu kommen noch eine Beschreibung (Short-Description, Description) des Scripts.
Von den darauf folgenden Variablendefinitionen sind meist nur PATH und DAEMON, ggf noch PIDFILE nötig und die anderen können entfallen. Daraufhin folgen die Aktionen jeweils als Shell-Funktionen, wobei auch hier nur do_start() und do_stop zwingend notwendig sind. Damit sieht der Vorspann der Datei dann beispielsweise so aus:
#! /bin/sh ### BEGIN INIT INFO # Provides: foobar # Required-Start: $remote_fs $syslog # Required-Stop: $remote_fs $syslog # Default-Start: 2 3 4 5 # Default-Stop: 0 1 6 # Short-Description: senseless initscript # Description: Nothing important ### END INIT INFO # PATH should only include /usr/* if it runs after the mountnfs.sh script PATH=/sbin:/usr/sbin:/bin:/usr/bin DESC="Totally senseless" NAME=foobar DAEMON=/usr/local/sbin/$NAME DAEMON_ARGS="" PIDFILE=/var/run/$NAME.pid SCRIPTNAME=/etc/init.d/$NAME # Exit if the package is not installed [ -x "$DAEMON" ] || exit 0 # Load the VERBOSE setting and other rcS variables . /lib/init/vars.sh # Define LSB log_* functions. # Depend on lsb-base (>= 3.2-14) to ensure that this file is present # and status_of_proc is working. . /lib/lsb/init-functions
Die beiden Funktionen do_start() und do_stop stützen sich auf das Programm start-stop-daemon, das zur Steuerung der Erzeugung und Beendigung von Prozessen auf Systemebene verwendet wird. Durch entsprechende Abgleich-Optionen kann start-stop-daemon so konfiguriert werden, dass es alle existierenden Instanzen von einem laufenden Prozess finden und sie somit auch löschen kann. Alles Weitere kann der Manualpage des Programms entnommen werden (man start-stop-daemon).
Für Start und Stopp eines Programms ohne Besonderheiten müssen daher die beiden Shellfunktionen auch nicht verändert werden. Andererseits haben Hardcore-Programmierer die Möglichkeit, beide Funktionen komplett neu zu schreiben und auf jegliche Hilfen des Systems zu verzichten.
# # Function that starts the daemon/service # do_start() { # Return # 0 if daemon has been started # 1 if daemon was already running # 2 if daemon could not be started start-stop-daemon --start --quiet --pidfile $PIDFILE --exec $DAEMON --test > /dev/null \ || return 1 start-stop-daemon --start --quiet --pidfile $PIDFILE --exec $DAEMON -- \ $DAEMON_ARGS \ || return 2 # Add code here, if necessary, that waits for the process to be ready # to handle requests from services started subsequently which depend # on this one. As a last resort, sleep for some time. } # # Function that stops the daemon/service # do_stop() { # Return # 0 if daemon has been stopped # 1 if daemon was already stopped # 2 if daemon could not be stopped # other if a failure occurred start-stop-daemon --stop --quiet --retry=TERM/30/KILL/5 --pidfile $PIDFILE --name $NAME RETVAL="$?" [ "$RETVAL" = 2 ] && return 2 # Wait for children to finish too if this is a daemon that forks # and if the daemon is only ever run from this initscript. # If the above conditions are not satisfied then add some other code # that waits for the process to drop all resources that could be # needed by services started subsequently. A last resort is to # sleep for some time. start-stop-daemon --stop --quiet --oknodo --retry=0/30/KILL/5 --exec $DAEMON [ "$?" = 2 ] && return 2 # Many daemons don't delete their pidfiles when they exit. rm -f $PIDFILE return "$RETVAL" }
Den ganzen Rest des Skelett-Scripts können Sie fast komplett so lassen wie er ist. Die Reload-Option ist per Default auskommentiert (wird also ignoriert) und Sie sollten auch noch die Status-Option auskommentieren (siehe unten). Alles andere bezieht sich nur auf die beiden Funktionen do_start() und do_stop. In einer case-Anweisung wird je nach Kommandozeilenparameter ("start", "stop", "restart") die entsprechende Aktion ausgelöst.
case "$1" in start) [ "$VERBOSE" != no ] && log_daemon_msg "Starting $DESC" "$NAME" do_start case "$?" in 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; esac ;; stop) [ "$VERBOSE" != no ] && log_daemon_msg "Stopping $DESC" "$NAME" do_stop case "$?" in 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; esac ;; #status) #status_of_proc "$DAEMON" "$NAME" && exit 0 || exit $? #;; #reload|force-reload) # # If do_reload() is not implemented then leave this commented out # and leave 'force-reload' as an alias for 'restart'. # #log_daemon_msg "Reloading $DESC" "$NAME" #do_reload #log_end_msg $? #;; restart|force-reload) # # If the "reload" option is implemented then remove the # 'force-reload' alias # log_daemon_msg "Restarting $DESC" "$NAME" do_stop case "$?" in 0|1) do_start case "$?" in 0) log_end_msg 0 ;; 1) log_end_msg 1 ;; # Old process is still running *) log_end_msg 1 ;; # Failed to start esac ;; *) # Failed to stop log_end_msg 1 ;; esac ;; *) #echo "Usage: $SCRIPTNAME {start|stop|restart|reload|force-reload}" >&2 echo "Usage: $SCRIPTNAME {start|stop|status|restart|force-reload}" >&2 exit 3 ;; esac :
Nun benötigt die Datei foobar noch die Ausführungsrechte:
chmod +x /etc/init.d/foobarNun ist das Script fertig und kann erst einmal durch manuellen Aufruf getestet werden. Dazu wird es, gefolgt vom entsprechenden Parameter ausgeführt. Für das Beispielscript ergäben sich folgende Möglichkeiten:
/etc/init.d/foobar start /etc/init.d/foobar stop /etc/init.d/foobar restartDiese Kommandos lassen sich auch später im laufenden Betrieb zum Starten und Stoppen verwenden. Funktioniert alles zufriedenstellend, kann das Script mit Hilfe des Kommandos update-rc.d inerhalb der im Kopf definierten Runlevels verlinkt werden. Ab da wird es dann beim Systemstart automatisch gestartet:
update-rc.d foobar defaultsAuch das Programm update-rc.d hat eine umfangreiche Manual-Page.
So ähnlich wie es schon oben bei der Datei rc.local besprochen wurde, kann man das Programm auch direkt an den systemd übergeben. Dafür wird eine nei Steuer-Datei im Verzeichnis /etc/systemd/system/ erzeugt. Die Datei bekommt einen noch nich vorhandenen, sprechenden Namen mit der Endung ".service", z. B. foobar.service. Der Inhalt entspricht folgenden Muster:
[Unit] Description=Foobar Service After=multi-user.target [Service] Type=idle ExecStart=/home/pi/bin/foobar & [Install] WantedBy=multi-user.targetDer Typ "idle" stellt sicher, dass das Kommado erst ausgeführt wird, wenn alle anderen Dienste geladen sind (Stichwort: "After ..."). Danach wird die neue Datei ausführbar gemacht:
chmod +x foobar.serviceDann wird der neue Service im systemd verankert:
sudo systemctl daemon-reload sudo systemctl enable foobar.serviceNach einen Reboot sollte der Dienst dann automatisch laufen. Zum Test kann er auch schon gleich mittels sudo systemctl start foobar.service gestartet werden. Auch die gleiche Weise läßt er sich auch stoppen, einfach stat "start" im Kommando "stop" verwenden.
Der Cron-Daemon kann Programme zu definierten Zeiten automatisch ausführen. Er prüft in regelmäßigen Abständen (normalerweise jede Minute) den Inhalt der crontab-Dateien, in denen Zeitpunkte für automatisch ablaufende Programme festgelegt sind. Mit dem Kommando crontab kann jeder Benutzer eine entsprechende Datei anlegen, ändern, löschen oder auflisten (siehe Manual-Page von crontab). Die Ausgabe der Kommandos, bei denen keine Ausgabeumleitung erfolgte, wird per E-Mail an den Benutzer gesendet.
In diesem speziellen Fall kann beim Start des Raspberry Pi das gewünschte Programm oder Script vom Cron-Daemon gestartet werden. Anstelle einer Zeit-Spezifikation wird hier "@reboot" angegeben. Mit crontab -e wird die crontab-Datei des jeweiligen Users bearbeitet, zum Beispiel kann ein Programmaufruf eingetragen werden:
@reboot /home/pi/bin/foobarFalls das Programm nach dem Aufruf als Daemon weiterlaufen soll, ist wieder eine Verlagerung in den Hintergrund notwendig (Erzeugen eines Daemon-Prozesses oder Anhängen von "&" an das Kommando). Mehr über das crontab-Kommando finden Sie im UNIX-Skript.