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.
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.