Prof. Dr.-Ing. Oliver Radfelder
Informatik / Wirtschaftsinformatik
Hochschule Bremerhaven
bash advanced

builtins

Sowohl auf hopper als auch in den Docker-Containern ist das Paket bash-builtins installiert. Die Bash ist mit ein wenig C-Kenntnissen gut erweiterbar. In der Variablen BASH_LOADABLES_PATH stehen Verzeichnisse, in denen nach Lib-Objekten gesucht wird, wenn mit enable eine Funktionalität aktiviert werden soll. Unter debian werden einige Beispiele unter /usr/lib/bash installiert.

> ls /usr/lib/bash
accept    dsv      id            Makefile.sample  pathchk   rm       stat       tty
basename  fdflags  ln            mkdir            print     rmdir    strftime   uname
csv       finfo    loadables.h   mkfifo           printenv  seq      sync       unlink
cut       getconf  logname       mktemp           push      setpgid  tee        whoami
dirname   head     Makefile.inc  mypid            realpath  sleep    truefalse
      

Mit enable können dann die einzelnen Kommandos aktiviert und deaktiviert werden:

> enable -f /usr/lib/bash/mkdir mkdir
> help mkdir
mkdir: mkdir [-p] [-m mode] directory [directory ...]
    Create directories.

    Make directories. ...
      

Ein kleiner Vergleich:

> enable -d mkdir
> time for i in {1..1000}; do mkdir /dev/shm/dir-$i; done

real  0m1.496s
user  0m1.009s
sys   0m0.595s

> rmdir /dev/shm/dir-*
> enable -f /usr/lib/bash/mkdir mkdir 
> time for i in {1..1000}; do mkdir /dev/shm/dir-$i; done

real  0m0.015s
user  0m0.007s
sys   0m0.008s

      

Wie man sieht, ist durch das Aktivieren des Loadables der Aufruf von mkdir zusammen mit dem Arbeiten im Shared-Memory /dev/shm etwa zwei Größenordnungen schneller als jedes Mal einen Prozess zu starten. Prozessstarts sind eben recht teuer.

Besonders interessant sind mkdir, rmdir, rm und sleep, mit denen sich recht passabel eine effiziente und robuste Warteschlange im Dateisystem mit multiplen Konsumenten implementieren lässt.

Ebenfalls interessant: accept, csv und dsv. Mit Ihnen werden nicht die bekannten Standardbefehle ersetzt, sondern neue hinzugefügt. Beispielsweise ist dsv als Ersatz für das oft hakelige cut für das Zerschneiden von csv-Dateien besser geeignet und durch das Schreiben in ein Array statt auf stdout ausgesprochen schnell.

> enable -f /usr/lib/bash/dsv dsv 
> line='"feld""eins",feld zwei,"feld,drei",*'
> dsv "$line"
> for i in "${DSV[@]}"; do echo "$i"; done
feld"eins
feld zwei
feld,drei
*

Da eine der teuersten Operationen in der Shell die Kommandosubstitution ist, weil dann immer eine Subshell und damit ein Prozess gestartet wird, sucht man gerne nach Möglichkeiten, sie zu umgehen. printf beispielsweise hat den Switch v, um das Ergebnis nicht nach Stdout sondern in die übergebene Variable zu schreiben.

> time for i in {1..1000}; do line=$(printf "%4d\n" $i); done 

real  0m1.132s
user  0m0.782s
sys   0m0.427s

> time for i in {1..1000}; do printf -v line "%4d\n" $i;  done

real	0m0.008s
user	0m0.008s
sys	0m0.000s

Diese Möglichkeit haben aber wenige Kommandos - externe Programme schon gar nicht, weil sie als eigene Prozesse aus Designgründen gar nicht die Umgebung der aufrufenden Shell beeinflussen dürfen.

> x=a; (echo $x; x=b; echo $x); echo $x
a
b
a

Für manche erstaunlich ist, dass das Schreiben in eine Datei und wieder Auslesen mit read erheblich schneller ist als Kommandosubstitution - wenn man verhindern kann, dass ein Prozess gestartet wird:

#!/bin/bash
seq 1000 > /tmp/file-1000
enable -f /usr/lib/bash/head head
time for i in {1..1000}; do
  content=$(head -n1 /tmp/file-1000)
done
#1.176 sec


procvar=/dev/shm/procvar-$$
mkdir -m 700 $procvar
time for i in {1..1000}; do
  head -n1 /tmp/file-1000 > $procvar/content
  IFS= read -r content < $procvar/content
done
#0.046 sec
rm -rf $procvar

Etwa Faktor 25 - ob es das wert ist, muss man wohl von Fall zu Fall entscheiden ...

Wer sich seine eigenen Builtins/Loadables schreiben möchte, checkt die passende Bash-Version aus, wechselt in das Verzeichnis bash und ruft configure und make auf, um sich die Bash zu bauen:

> git clone https://git.savannah.gnu.org/git/bash.git
> cd bash
> ./configure && make
> ./bash
> cd examples/loadables

Dort findet sich dann der Code für die Beispiele in /usr/lib/bash und ein Template für eigene Loadables.

flock

Neben dem atomaren Locking per mkdir/rmdir und sleep gibt es eine Implementierung unter Linux, mit der sich Locking effizienter bewerkstelligen lässt: flock.

Ein exklusiver Lock, der einen Fehler verursacht, wenn er nicht erlangt werden kann:

flock -xn thelockfile.lock pwd || echo fail

Das funktioniert auch, wenn flock innerhalb einer Kommandosubstitution steht:

var=$(flock -xn thelockfile.lock exit 1) || echo fail

Wenn flock ohne -n aufgerufen wird, wird gewartet, bis der Lock verfügbar ist:

flock -x thelockfile.lock pwd

Alternativ kann eine maximale Wartezeit angegeben werden:

flock -w 3 thelockfile.lock pwd 

Bei etwas anspruchvolleren Abfolgen ist es nicht unüblich, einen Filedescriptor zu nutzen:

(
if ! flock -x -w 0.5 200; then
  echo could not get lock after 500ms
  exit 1
fi
read num < /usr/share/hbv/num
((num++))
sleep 3
echo $num > /usr/share/hbv/num
echo $num
) 200 > lockfile

Oder in der einfacheren Variante:

exec 4<>thelock
flock -x 4 
exec 4<&-

Filedescriptoren werden am Ende des Programmes automatisch geschlossen.