Erst du, dann du, dann du, dann du

;widows: 2;-webkit-text-stroke-width: 0px;word-spacing:0px“> Von Mathias Weidner

Vor einiger Zeit kam ein Kollege auf mich zu und fragte, wie er am besten mehrere Backup-Jobs gleichzeitig starten könne. Außerdem sollten ein paar andere Jobs später laufen, wenn die ersten fertig sind. Das Ganze wollte er möglichst schnell und ohne zusätzliche Programme erledigen – also in der Shell.

Mehrere Jobs in der Shell

MATHIAS WEIDNER

Mehrere Jobs in der Shell

Die Abbildung veranschaulicht das Problem: Zum Zeitpunkt t0 starten die ersten Jobs. Sie sind zum Zeitpunkt t1 fertig. Die nächsten Jobs sollen zum Zeitpunkt t2 starten. Eine dritte Staffel sollte frühestens zum Zeitpunkt t3 starten, wenn alle Jobs der zweiten Staffel fertig sind. Außerdem sollen t1 und t2 nicht allzu weit auseinander liegen – es ging ja um Backup-Jobs, die über Nacht laufen und am Morgen fertig sein sollen.

Die erste Idee, die Jobs mit & in den Hintergrund zu schicken, bringt einige Probleme mit sich:

  • Bei Hintergrund-Jobs bekommt ein Shell-Skript nicht mit, wann sie fertig sind. Man muss t1 und t3 daher schätzen und verschenkt Zeit, weil zuviel Reserve bleibt oder die nächste Staffel zu früh startet, während die vorherige noch läuft.
  • Die Ausgaben der einzelnen Jobs vermischen sich. Man kann nicht vorhersagen, wessen Ausgabe an welcher Stelle erscheint.
  • Man bekommt keine Rückgabewerte der Jobs und kann somit im Skript nicht darauf reagieren.

Für diese Probleme gibt es Abhilfen, auch mit den Mitteln der Shell selbst. Doch die meisten benötigen Interprozesskommunikation und temporäre Dateien. Außerdem blähen sie das Skript auf, bis am Ende die eigentlichen Jobs kaum noch zu finden sind. Das möchte man nicht an einen Kollegen weitergeben.

Synchronisation mit Pipes

Es gibt allerdings eine einfache Möglichkeit, Jobs in der Shell gleichzeitig zu starten, bei der die Shell automatisch wartet, bis der letzte der Jobs geendet hat: Prozesse, die über Pipes miteinander verbunden sind, startet die Shell gleichzeitig.

Die Standardausgabe jeder dieser Jobs ist mit der Standardeingabe des nächsten verbunden, und die Standardeingabe mit der Standardausgabe des vorigen Jobs. Auf diese Art reichen die Ausgaben aber nur zum jeweils nächsten Job, und die Shell sieht nur die Ausgabe des jeweils letzten. Die Jobs davor würden zudem blockieren, wenn ihre Ausgabe nicht gelesen wird.

Nun ließe sich die Ausgabe der Jobs mit cat in eine Datei leiten und diese so entsperren:

( cat >> $logfile &; jobx )

Damit würden zwar alle Jobs problemlos laufen. Aber die Ausgabe würde wieder durcheinander in die Logdatei geschrieben. Besser ist es, wenn jeder Job die Ausgabe des vorigen Jobs an den nächsten weiterleiten würde. Das heißt, jeder Job, mit Ausnahme des ersten, müsste etwa so aussehen:

( jobx; cat )

Die runden Klammern schicken die Jobs in eine Subshell, sodass das Semikolon nicht die Pipe auseinanderreißt. Auf diese Weise erscheinen die Ausgaben allerdings in umgekehrter Reihenfolge:

$ echo 1|(echo 2; cat)|(echo 3; cat)
3
2
1

Außerdem kann Job Nummer 2 seine Ausgabe erst vollständig schreiben, wenn Job 3 beendet ist und cat die Ausgabe weiterleitet. Dito für Job 1: Zwar starten alle Jobs zur gleichen Zeit, aber die ersten blockieren, bis die letzten fertig sind. Das lässt sich zwar lösen, wenn die Reihenfolge des cat-Befehls und des Jobs getauscht ist:

$ echo 1|(cat; echo 2)|(cat; echo 3)
1
2
3

Nun stimmt zwar die Reihenfolge. Aber Job 2 startet erst, wenn Job 1 seine Ausgabe schließt. Und Job 3 startet erst, wenn Job 2 seine Standardausgabe schließt.

Hier hilft der Trick, die Ausgabe jedes Jobs in Variablen zwischenzuspeichern und sie erst am Ende auszugeben. Abgesehen davon, dass alle Jobs unabhängig voneinander laufen können, lässt sich die Ausgabe nun auch in die richtige Reihenfolge bringen. Damit es übersichtlicher wird, kapselt eine Shell-Funktion den Aufruf von cat und den Job, der ich den Job als Argument übergebe:

01 in_pipe() {
02   CMD="$*"
03   BEFORE=$(date)
04   LOG=$($CMD)
05   RESULT=$?
06   AFTER=$(date)
07   
08   cat
09   echo $BEFORE
10   echo "$CMD"
11   echo "-----"
12   echo "$LOG"
13   echo "-----"
14   echo "RESULT=$RESULT"
15   echo $AFTER
16   echo "====="
17   return $RESULT
18 }

Diese Funktion zieht alle Register. Zeile 02 speichert alle übergebenen Argumente in einer Variablen, die als Befehl für den Job dient. Die Zeilen 03 und 06 notieren die Zeit vor und nach dem Aufruf des Jobs in zwei weiteren Variablen. Zeile 04 speichert die Ausgaben des Jobs in einer Shell-Variablen, während er läuft, und Zeile 05 hebt den Rückgabewert für später auf. Zeile 08 enthält den Aufruf von cat, der die Ausgabe des vorherigen Jobs durchleitet: Dadurch entspricht die Reihenfolge der Ausgaben der Reihenfolge der Jobs in der Pipe. Ab Zeile 09 kommt die Ausgabe aller gesammelten Werte des aktuellen Jobs, gefolgt von einer Trennzeile. In Zeile 17 schließlich gibt die Funktion den Rückgabewert des Jobs an die aufrufende Shell zurück.

Diese Funktion findet nun wie folgt Einsatz:

01 echo "=====" 
     | in_pipe sleep 3 
     | in_pipe echo Hallo 
     | in_pipe sleep 2
02 date

Die Ausgabe sieht folgendermaßen aus:

=====
Mi 21. Jan 10:01:58 CET 2015
sleep 3
-----
-----
RESULT=0
Mi 21. Jan 10:02:01 CET 2015
=====
Mi 21. Jan 10:01:58 CET 2015
echo Hallo
-----
Hallo
-----
RESULT=0
Mi 21. Jan 10:01:58 CET 2015
=====
Mi 21. Jan 10:01:58 CET 2015
sleep 2
-----
-----
RESULT=0
Mi 21. Jan 10:02:00 CET 2015
=====
Mi 21. Jan 10:02:01 CET 2015

Alle Jobs starten also zur gleichen Zeit, werden aber zu unterschiedlichen Zeiten fertig. Das Skript fährt fort, nachdem der langsamste Job endete.

Probleme und Einschränkungen

Ein Problem gibt es noch, das mit folgendem Aufruf deutlich wird:

01 echo "=====" 
    | in_pipe false 
    | in_pipe sleep 1
02 echo $?

Das Problem wird in der letzten Zeile der Ausgabe offenbar:

=====
Mi 21. Jan 10:09:08 CET 2015
false
-----
-----
RESULT=1
Mi 21. Jan 10:09:08 CET 2015
=====
Mi 21. Jan 10:09:08 CET 2015
sleep 1
-----
-----
RESULT=0
Mi 21. Jan 10:09:09 CET 2015
=====
0

Die Ausgabe zeigt: Obwohl ein Job einen Fehlerwert gemeldet hat, registriert die Shell diesen nicht, weil der letzte Job in der Pipe ohne Fehler endete.

Bei der POSIX– oder Bourne-Shell muss man mit diesem Umstand leben – oder die Ausgabe der Pipe weiter untersuchen. Verwendet man jedoch bash, ksh oder zsh, erscheint folgende Lösungsanweisung im Skript vor der Pipe:

set -o pipefail

Allerdings hat auch diese Lösung Einschränkungen.

  • Pro Job lässt sich nur ein Befehl aufrufen. Immerhin kann man Argumente mitgeben.
  • Da die Ausgabe der Jobs in Variablen zwischenlagert, sollte die Ausgabe sich auf den maximalen Wert für Shell-Variablen beschränken. Wer mehr braucht, muss mit temporären Dateien arbeiten.
  • Keine Ausgabeumleitungen lassen sich an die Funktion durchreichen. Das könnte man allerdings in die Funktion selbst einbauen, wodurch sie für alle Jobs gelten würde.

Wer das im Hinterkopf behält, kann verschiedene Jobs gleichzeitig aufrufen, ohne dass sich aufeinanderfolgende Staffeln von Jobs überlappen oder zwischendurch Zeit zu verschenken.

Autoreninformation

Mathias Weidner studierte Ende der 1980-er Jahre Automatisierungstechnik in Leipzig. Nach verschiedenen Stellen in der Software-Entwicklung arbeitet er als Administrator für Unix/Linux, Netzwerke und Sicherheitsthemen. Seit einiger Zeit schreibt er hin und wieder Bücher dazu, die unter buecher.mamawe.net[1] zu finden sind.

Dieser Artikel ist zuerst erschienen in UpTimes[2], Mitgliederzeitschrift des GUUG e.V., Ausgabe 2016-2. Veröffentlichung mit freundlicher Genehmigung.

Linkverweise:


[1] buecher.mamawe.net 
[2] www.guug.de/uptimes/

http://www.pro-linux.de/images/NB3/base/print/nb3_logo.png

Mediadaten RSS/Feeds Datenschutz Impressum

© 2017 Pro-Linux

 

Schreibe einen Kommentar