Wie kann ich eine Pfeife zeitlich festlegen?

27

Ich möchte timeeinen Befehl, der aus zwei separaten Befehlen besteht, mit einem Piping-Ausgang zu einem anderen. Betrachten Sie beispielsweise die beiden folgenden Skripte:

$ cat foo.sh
#!/bin/sh
sleep 4

$ cat bar.sh
#!/bin/sh
sleep 2

Wie kann ich timenun die Zeit foo.sh | bar.shabrufen, die vergangen ist (und ja, ich weiß, dass die Pipe hier keinen Sinn ergibt, aber dies ist nur ein Beispiel)? Es funktioniert wie erwartet, wenn ich sie nacheinander in einer Subshell ohne Piping ausführe:

$ time ( foo.sh; bar.sh )

real    0m6.020s
user    0m0.010s
sys     0m0.003s

Aber ich kann es nicht zum Laufen bringen:

$ time ( foo.sh | bar.sh )

real    0m4.009s
user    0m0.007s
sys     0m0.003s

$ time ( { foo.sh | bar.sh; } )

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

$ time sh -c "foo.sh | bar.sh "

real    0m4.006s
user    0m0.000s
sys     0m0.000s

Ich habe eine ähnliche Frage durchgelesen ( wie man Zeit mit mehreren Befehlen ausführt UND die Zeitausgabe in eine Datei schreibt? ) Und auch die eigenständige timeausführbare Datei ausprobiert :

$ /usr/bin/time -p sh -c "foo.sh | bar.sh"
real 4.01
user 0.00
sys 0.00

Es funktioniert nicht einmal, wenn ich ein drittes Skript erstelle, das nur die Pipe ausführt:

$ cat baz.sh
#!/bin/sh
foo.sh | bar.sh

Und dann mal das:

$ time baz.sh

real    0m4.009s
user    0m0.003s
sys     0m0.000s

Interessanterweise sieht es nicht so timeaus, als würde es beendet, sobald der erste Befehl ausgeführt wird. Wenn ich bar.shzu:

#!/bin/sh
sleep 2
seq 1 5

Und timeandererseits hatte ich erwartet, dass die timeAusgabe vor dem gedruckt wird, seqaber es ist nicht:

$ time ( { foo.sh | bar.sh; } )
1
2
3
4
5

real    0m4.005s
user    0m0.003s
sys     0m0.000s

Sieht timeso aus, als würde die Ausführungszeit nicht gezählt, bar.shobwohl gewartet wurde, bis der Bericht gedruckt wurde 1 .

Alle Tests wurden auf einem Arch-System und mit bash 4.4.12 (1) -release durchgeführt. Ich kann bash nur für das Projekt verwenden. Dies ist ein Teil davon. Selbst wenn zsheine andere leistungsstarke Shell es umgehen kann, ist dies für mich keine praktikable Lösung.

Wie kann ich also die Zeit ermitteln, die eine Reihe von Pipe-Befehlen für die Ausführung benötigt hat? Und wenn wir schon dabei sind, warum funktioniert es dann nicht? Es sieht so timeaus, als würde es sofort beendet, sobald der erste Befehl beendet ist. Warum?

Ich weiß, ich kann die einzelnen Zeiten mit so etwas abrufen:

( time foo.sh ) 2>foo.time | ( time bar.sh ) 2> bar.time

Aber ich würde immer noch gerne wissen, ob es möglich ist, das Ganze als einzelne Operation zu messen.


1 Dies scheint kein Pufferproblem zu sein. Ich habe versucht, die Skripte mit unbufferedund auszuführen, stdbuf -i0 -o0 -e0und die Zahlen wurden vor der timeAusgabe noch gedruckt .

terdon
quelle
Haben Sie es mit einer physischen Stoppuhr versucht?
pericynthion
@pericynthion yep schließlich habe ich. Und das zeigte auch, was die Antworten erklären: Die Zeit arbeitet tatsächlich, aber (offensichtlich genug und wie ich hätte erkennen sollen) werden die Befehle in der Pipeline gleichzeitig ausgeführt, sodass die benötigte Zeit im Wesentlichen die Zeit der langsamsten ist.
Terdon

Antworten:

33

Es ist zu arbeiten.

Die verschiedenen Teile einer Pipeline werden gleichzeitig ausgeführt. Das einzige, was die Prozesse in der Pipeline synchronisiert / serialisiert, ist IO, dh ein Prozess schreibt auf den nächsten Prozess in der Pipeline und der nächste Prozess liest, was der erste schreibt. Ansonsten arbeiten sie unabhängig voneinander.

Da zwischen den Prozessen in Ihrer Pipeline weder Lese- noch Schreibvorgänge stattfinden, dauert die Ausführung der Pipeline den längsten sleepAufruf.

Du hättest genauso gut schreiben können

time ( foo.sh & bar.sh &; wait )

Terdon hat ein paar leicht modifizierte Beispielskripte im Chat gepostet :

#!/bin/sh
# This is "foo.sh"
echo 1; sleep 1
echo 2; sleep 1
echo 3; sleep 1
echo 4

und

#!/bin/sh
# This is "bar.sh"
sleep 2
while read line; do
  echo "LL $line"
done
sleep 1

Die Abfrage lautete "Warum werden time ( sh foo.sh | sh bar.sh )4 Sekunden anstatt 3 + 3 = 6 Sekunden zurückgegeben?"

Um zu sehen, was passiert, einschließlich der ungefähren Zeit, zu der jeder Befehl ausgeführt wird, kann man dies tun (die Ausgabe enthält meine Anmerkungen):

$ time ( env PS4='$SECONDS foo: ' sh -x foo.sh | PS4='$SECONDS bar: ' sh -x bar.sh )
0 bar: sleep 2
0 foo: echo 1     ; The output is buffered
0 foo: sleep 1
1 foo: echo 2     ; The output is buffered
1 foo: sleep 1
2 bar: read line  ; "bar" wakes up and reads the two first echoes
2 bar: echo LL 1
LL 1
2 bar: read line
2 bar: echo LL 2
LL 2
2 bar: read line  ; "bar" waits for more
2 foo: echo 3     ; "foo" wakes up from its second sleep
2 bar: echo LL 3
LL 3
2 bar: read line
2 foo: sleep 1
3 foo: echo 4     ; "foo" does the last echo and exits
3 bar: echo LL 4
LL 4
3 bar: read line  ; "bar" fails to read more
3 bar: sleep 1    ; ... and goes to sleep for one second

real    0m4.14s
user    0m0.00s
sys     0m0.10s

Abschließend nimmt die Pipeline aufgrund der Pufferung der Ausgabe der ersten beiden Aufrufe von echoin 4 Sekunden und nicht 6 Sekunden in Anspruch foo.sh.

Kusalananda
quelle
1
@terdon die Werte sind die Summen, aber die Skripte benötigen sehr wenig Benutzer- und Systemzeit - sie warten nur, was nicht zählt (außer in der Zeit der Wanduhr).
Stephen Kitt
2
Beachten Sie, dass einige Shells wie die Bourne-Shell oder ksh93nur auf die letzte Komponente der Pipeline warten ( sleep 3 | sleep 1würde 1 Sekunde dauern). Die Bourne-Shell hat kein timeSchlüsselwort, aber ksh93wenn sie mit ausgeführt wird time, wird auf alle Komponenten gewartet.
Stéphane Chazelas
3
Ich sage nur, dass man überrascht sein kann, dass dies in ksh93 sleep 10 | sleep 1eine Sekunde time sleep 10 | sleep 1dauert, während es in ksh93 10 Sekunden dauert. In der Bourne-Shell time sleep 10 | sleep 1würde es eine Sekunde dauern, aber Sie würden die Zeitausgabe ( sleep 10nur für und von /usr/bin/time) 9 Sekunden später aus heiterem Himmel erhalten.
Stéphane Chazelas
1
Es geht nicht darum, etwas zu bewachen. timekorrekt mal die Pipeline, aber ändert das Verhalten der Shell in ksh93. (sleep 10 | sleep 1)dauert 1 Sekunde, time (sleep 10 | sleep 1)dauert 10 Sekunden. { (sleep 10 | sleep 1); echo x; }Ausgänge xnach 1 Sekunde, time { (sleep 10 | sleep 1); echo x; }Ausgänge xnach 10 Sekunden. Dasselbe gilt, wenn Sie diesen Code in eine Funktion einfügen und die Funktion zeitlich festlegen.
Stéphane Chazelas
1
Beachten Sie, dass Sie ksh93wie in zsh( -o promptsubsthier) tun können typeset -F SECONDS, um eine weniger ungefähre Anzahl von Sekunden zu erhalten (POSIX shhat keine SECONDS)
Stéphane Chazelas
10

Wäre das ein besseres Beispiel?

$ time perl -e 'alarm(3); 1 while 1;' | perl -e 'alarm(4); 1 while 1;'
Alarm clock

real    0m4.004s
user    0m6.992s
sys     0m0.004s

Die Skripte werden 3 bzw. 4 Sekunden lang ausgeführt, was aufgrund der parallelen Ausführung insgesamt 4 Sekunden in Echtzeit und 7 Sekunden CPU-Zeit in Anspruch nimmt. (Zumindest ungefähr.)

Oder dieses:

$ time ( sleep 2; echo) | ( read x; sleep 3 )

real    0m5.004s
user    0m0.000s
sys     0m0.000s

Diese laufen nicht parallel, die Gesamtzeit beträgt also 5 Sekunden. Es wird nur geschlafen, daher wird keine CPU-Zeit benötigt.

ilkkachu
quelle
3

Wenn Sie haben sysdig, können Sie Tracer an beliebigen Stellen einfügen , vorausgesetzt, Sie können den Code ändern, um die erforderlichen Schreibvorgänge hinzuzufügen/dev/null

echo '>::blah::' >/dev/null
foo.sh | bar.sh
echo '<::blah::' >/dev/null

(aber das scheitert an Ihrer "Einzeloperation" -Anforderung) und zeichnet dann Dinge über auf

$ sudo sysdig -w blalog "span.tags contains blah"

und dann brauchst du wahrscheinlich einen sysdig-meißel, um nur die dauer zu exportieren

description = "Exports sysdig span tag durations";
short_description = "Export span tag durations.";
category = "Tracers";

args = {}

function on_init()
    ftags = chisel.request_field("span.tags")
    flatency = chisel.request_field("span.duration")
    chisel.set_filter("evt.type=tracer and evt.dir=<")
    return true
end

function on_event()
    local tags = evt.field(ftags)
    local latency = evt.field(flatency)
    if latency then
        print(tostring(tags) .. "\t" .. tonumber(latency) / 1e9)
    end
    return true
end

die einmal in Ihrem sysdig/chiselsVerzeichnis als die Datei gespeichert wurde, spantagduration.luakann als verwendet werden

$ sysdig -r blalog -c spantagduration
...

Oder Sie können mit csysdigoder mit der JSON-Ausgabe herumspielen.

Thrig
quelle