Ein nächtlicher Importlauf braucht seit Wochen acht Minuten, und im Team fragt jeder dasselbe: Rechnet das Skript so lange, oder wartet es? Eine Stoppuhr beantwortet das nicht. Sie liest die Uhr an der Wand ab und sagt nur, wie viel Zeit vergangen ist. PHP getrusage() liefert die andere Zahl, nämlich die Zeit, in der der Prozessor wirklich für dieses Skript gearbeitet hat. Wie lange ein Vorgang insgesamt dauert, steht in Scriptlaufzeit messen mit microtime und in PHP microtime zum Messen der Ausführungszeit.
Der lange helle Balken im Bild ist die verstrichene Zeit, der kurze orange Balken die Rechenzeit. Alles dazwischen ist Warten, und woran das Skript wartet, entscheidet die nächste Maßnahme.
Was PHP getrusage() misst und was microtime() misst
Verstrichene Zeit ist die Uhr an der Wand. Sie wächst weiter, wenn das Skript auf eine Datenbankantwort wartet, und sie wächst auch dann weiter, wenn der Prozessor gerade einen ganz anderen Prozess bedient. CPU-Zeit zählt dagegen nur die Abschnitte, in denen der Prozessor an diesem einen Skript gearbeitet hat.
Der Unterschied wird an Zahlen greifbar. Ein Skript läuft fünf Sekunden und verbraucht dabei 0,2 Sekunden CPU-Zeit. Dann hat es 4,8 Sekunden lang nichts getan außer zu warten, und eine doppelt so schnelle Maschine würde daran keine Zehntelsekunde ändern. PHP getrusage() liefert genau diese 0,2 Sekunden, aufgeteilt nach eigenem Rechenaufwand und Kernelarbeit.
<?php
$d = getrusage();
print_r($d);
/* Array
(
[ru_oublock] => 0
[ru_inblock] => 0
[ru_msgsnd] => 0
[ru_msgrcv] => 0
[ru_maxrss] => 29380
[ru_ixrss] => 0
[ru_idrss] => 0
[ru_minflt] => 3295
[ru_majflt] => 0
[ru_nsignals] => 0
[ru_nvcsw] => 136
[ru_nivcsw] => 1
[ru_nswap] => 0
[ru_utime.tv_usec] => 31398
[ru_utime.tv_sec] => 0
[ru_stime.tv_usec] => 15699
[ru_stime.tv_sec] => 0
) */
Das ist die vollständige Ausgabe von PHP getrusage() in einem frisch gestarteten Skript unter Linux, siebzehn Einträge in einem flachen assoziativen Array. Zehn davon stehen auf null, und das bleibt auch so: Der Kernel füllt längst nicht jedes Feld der C-Struktur struct rusage, deren Namen die Schlüssel tragen. Auf einem anderen Betriebssystem ist die Liste sogar kürzer, dazu weiter unten mehr. Eine Null ist also kein Fehler und kein Anlass zur Fehlersuche.
Syntax: die Signatur im Original
Die Signatur von PHP getrusage() ist kurz. Der einzige Parameter entscheidet, wessen Verbrauch gemeldet wird.
Gemeldet wird immer der ganze Prozess mit allen seinen Threads, nicht ein einzelner Thread. Wer nebenläufig arbeitet, bekommt also eine Summe und keine Aufschlüsselung.
getrusage(int $mode = 0): array|false
/* mode - 0 for the calling process itself,
1 for the resource usage of all
terminated child processes
Return Values, wie im Handbuch:
"Returns an associative array containing the data
returned from the system call. All entries are
accessible by using their documented field names.
Returns false on failure." */
Der Standardwert 0 fragt nach dem laufenden Prozess, der Wert 1 nach den beendeten Kindprozessen. In der C-Bibliothek heißen diese beiden Modi RUSAGE_SELF und RUSAGE_CHILDREN, und so erklärt sie auch das Handbuch. Gleichnamige PHP-Konstanten gibt es aber nicht: defined('RUSAGE_CHILDREN') liefert false, und wer den Namen trotzdem in den Aufruf schreibt, bekommt einen Error: Undefined constant. Im Code steht deshalb die nackte Zahl. Der wichtigste Satz im Block ist der über die Feldnamen: Die Einträge sind unter ihrem dokumentierten Namen erreichbar, und dieser Name enthält bei den Zeitfeldern einen Punkt.
Return Value: assoziatives Array oder false
Im Regelfall kommt ein assoziatives Array zurück. Die Signatur lässt daneben false zu, nämlich dann, wenn das Betriebssystem die Auskunft verweigert. Unter Linux und Windows ist dieser Fall nicht zu provozieren, in abgeschotteten Umgebungen schon. Eine Prüfung mit is_array() vor der ersten Auswertung verhindert, dass ein Protokolleintrag mit einer Fehlermeldung statt mit einer Zahl endet.
Die Zeitfelder lesen: Sekunden und Mikrosekunden zusammensetzen
Das Array hält jede der beiden Zeiten in zwei Feldern. ru_utime.tv_sec trägt die vollen Sekunden, ru_utime.tv_usec den Rest in Mikrosekunden. Der Teiler ist also eine Million und nicht tausend. Wer hier Milli- und Mikrosekunden verwechselt, liegt um den Faktor 1000 daneben und wundert sich über absurd hohe Messwerte.
Der Punkt im Schlüsselnamen sieht nach einer Verschachtelung aus, ist aber keine. Das Array bleibt flach. Ein Zugriff der Form $d['ru_utime']['tv_sec'] geht ins Leere und bringt eine Meldung über einen undefinierten Index. Eine kleine Hilfsfunktion nimmt diese Stolperstelle ein für alle Mal aus dem Weg.
<?php
function cpu_sekunden(array $d, string $feld): float
{
$sek = (int) $d[$feld . '.tv_sec'];
$usek = (int) $d[$feld . '.tv_usec'];
return $sek + $usek / 1000000;
}
$d = getrusage();
$user = cpu_sekunden($d, 'ru_utime');
$system = cpu_sekunden($d, 'ru_stime');
printf("User %.4f s\n", $user);
printf("System %.4f s\n", $system);
/* User 0.0314 s
System 0.0157 s */
Die beiden Zahlen gehören zur Ausgabe von oben: 31398 Mikrosekunden ergeben 0,0314 Sekunden, 15699 ergeben 0,0157. Diese Funktion begleitet den Rest des Tutorials. Sie nimmt das Array, das PHP getrusage() zurückgibt, und den Feldnamen ohne Endung, also ru_utime oder ru_stime. Heraus kommt eine Fließkommazahl in Sekunden, mit der sich direkt rechnen lässt.
User-Zeit und System-Zeit auseinanderhalten
Die beiden Zeiten beschreiben verschiedene Arten von Arbeit. ru_utime ist die Zeit im eigenen Code: Schleifen, Zeichenkettenarbeit, Sortieren, das Rendern einer Vorlage. ru_stime ist die Zeit, die der Kernel stellvertretend aufgewendet hat, also Dateizugriffe, Netzaufrufe und Speicheranforderungen. Beide Summen zusammen ergeben die CPU-Zeit, die PHP getrusage() ausweist.
flowchart TD
A[Skriptlaufzeit] --> B[User-Zeit ru_utime]
A --> C[System-Zeit ru_stime]
A --> D[Wartezeit auf DB und Platte]
B --> E[CPU-Zeit]
C --> E
E --> F[Wall-Clock minus CPU ist Warten]
Aus dem Verhältnis der beiden Werte folgt die nächste Maßnahme. Viel System-Zeit bei wenig User-Zeit bedeutet fast immer zu viele kleine Ein- und Ausgaben. Der Klassiker ist eine Schleife, die jede einzelne Zeile in eine Datei schreibt. Jeder dieser Schreibvorgänge kostet einen Systemaufruf, und zehntausend davon summieren sich spürbar. Wer stattdessen alle tausend Zeilen einmal schreibt, drückt den Wert deutlich.
Wall-Clock gegen CPU-Zeit: die Lücke deuten
Ein einzelner Aufruf am Skriptende sagt wenig. Interessant ist die Differenz zweier Aufrufe, denn sie beschreibt genau den Abschnitt dazwischen. Daneben läuft microtime(true) als Vergleichswert mit, und erst aus dem Paar entsteht eine Aussage.
<?php
$t0 = microtime(true);
$r0 = getrusage();
/* hier steht der zu messende Abschnitt */
sleep(2);
$t1 = microtime(true);
$r1 = getrusage();
$wall = $t1 - $t0;
$cpu = cpu_sekunden($r1, 'ru_utime')
- cpu_sekunden($r0, 'ru_utime')
+ cpu_sekunden($r1, 'ru_stime')
- cpu_sekunden($r0, 'ru_stime');
printf("Wall %.3f s, CPU %.3f s\n", $wall, $cpu);
/* Wall 2.000 s, CPU 0.000 s */
Zwei Sekunden vergangen, und auf drei Nachkommastellen genau keine Rechenzeit. Deutlicher lässt sich Wartezeit nicht zeigen. Im Alltag heißt derselbe Befund selten sleep(), sondern Datenbank, HTTP-Aufruf oder Plattenzugriff. Drei Muster kommen immer wieder vor, und PHP getrusage() trennt sie sauber voneinander.
CPU-Zeit fast so hoch wie die Laufzeit Das Skript rechnet tatsächlich. Hier lohnt der Blick in den Algorithmus, in verschachtelte Schleifen und in Funktionen, die dieselbe Arbeit mehrfach machen. Schnellere Hardware hilft hier wirklich.
Kleine CPU-Zeit bei langer Laufzeit Das Skript wartet. Kandidaten sind fehlende Datenbankindizes, langsame externe Dienste, Netzlaufwerke und Dateisperren. Wer parallel schreibende Prozesse hat, findet die Hintergründe in PHP flock und Dateisperren beim parallelen Schreiben.
Beide Werte schwanken von Lauf zu Lauf Auf einer stark ausgelasteten Maschine steigt die verstrichene Zeit, weil andere Prozesse die Rechenkerne belegen. Die CPU-Zeit bleibt dabei ungefähr gleich. Diese Kombination ist ein Hinweis auf den Server, nicht auf den Code.
Eine Messfunktion für Protokolle
Einmal messen ist Diagnose, dauerhaft messen ist Beobachtung. Zwei kleine Funktionen reichen, um jeden Auftrag mit verstrichener Zeit, User-Zeit, System-Zeit und Spitzenspeicher ins Protokoll zu schreiben.
<?php
function mess_start(): array
{
return [microtime(true), getrusage()];
}
function mess_ende(array $start, string $name): string
{
[$t0, $r0] = $start;
$r1 = getrusage();
$wall = microtime(true) - $t0;
$u = cpu_sekunden($r1, 'ru_utime')
- cpu_sekunden($r0, 'ru_utime');
$s = cpu_sekunden($r1, 'ru_stime')
- cpu_sekunden($r0, 'ru_stime');
$rss = (int) $r1['ru_maxrss'];
return sprintf(
'%s wall=%.3f user=%.3f sys=%.3f rss=%dk',
$name, $wall, $u, $s, $rss,
);
}
$start = mess_start();
/* der Auftrag, hier 300000 Hashes erzeugen,
sortieren und wegschreiben */
$zeilen = [];
for ($i = 0; $i < 300000; $i++) {
$zeilen[] = md5((string) $i);
}
sort($zeilen);
$txt = implode("\n", $zeilen);
file_put_contents('/tmp/import.txt', $txt);
error_log(mess_ende($start, 'import'));
/* import wall=0.270 user=0.211 sys=0.059 rss=70564k */
Hier liegen verstrichene Zeit und CPU-Zeit dicht beieinander, der Auftrag rechnet also wirklich. Nach ein paar Nächten steht in der Logdatei eine Reihe, die mehr sagt als jede Einzelmessung. Steigt die verstrichene Zeit, während User- und System-Zeit gleich bleiben, ist die Umgebung langsamer geworden. Steigen alle drei gemeinsam, hat die Datenmenge zugenommen.
Eine Grenze bleibt: PHP getrusage() sagt, welcher Art die Last ist, nicht welche Zeile sie verursacht. Für die teuerste Codezeile braucht es einen Profiler wie Xdebug oder einen Sampling-Profiler. Die Messung grenzt den Suchbereich ein, mehr soll sie auch nicht leisten.
Kindprozesse mit Modus 1 messen
Ruft ein Skript ein externes Programm auf, taucht dessen Rechenzeit im Standardmodus nirgends auf. Ein Packer, der einen Datenbankauszug zusammenschnürt, kann die Maschine minutenlang auslasten, und die eigene Messung zeigt davon nichts. Dafür kennt PHP getrusage() einen zweiten Modus.
<?php
$vorher = getrusage(1);
exec('gzip -9 -c gross.sql > gross.sql.gz');
$nachher = getrusage(1);
$kind = cpu_sekunden($nachher, 'ru_utime')
- cpu_sekunden($vorher, 'ru_utime');
printf("Kind-CPU %.3f s\n", $kind);
/* Kind-CPU 0.480 s, gemessen an einem
Auszug von 35 MB */
Der Zeitpunkt der Messung entscheidet hier alles. Gezählt wird erst, wenn das Kind beendet und vom Elternprozess eingesammelt ist. Wer während der Laufzeit des Kindes misst, bekommt Nullen und hält den Modus für kaputt. Mit proc_open() lässt sich das vorführen: Dieselbe Messung ergab mitten im Lauf 0,000 Sekunden und nach proc_close() dann 0,501 Sekunden. Erst schließen, dann PHP getrusage() mit dem Modus 1 befragen.
Eine zweite stille Nullmessung entsteht, wenn das Skript pcntl_signal(SIGCHLD, SIG_IGN) gesetzt hat. Dann sammelt der Kernel die Kindprozesse selbst ein, und für den Elternprozess bleibt nichts mehr zu zählen übrig. Derselbe Packlauf, der ohne diese Zeile 0,380 Sekunden ausweist, meldet mit ihr 0,000 Sekunden.
Speicher, Seitenfehler und Swaps: ru_maxrss, ru_minflt, ru_majflt
Neben den Zeiten liefert PHP getrusage() eine Reihe weiterer Felder. Die folgende Übersicht nennt die, die sich im Alltag wirklich lesen lassen.
| Feld | Bedeutung | Worauf ein hoher Wert deutet |
ru_utime.* | Rechenzeit im eigenen Code | Algorithmus und Schleifen ansehen |
ru_stime.* | Zeit, die der Kernel aufgewendet hat | zu viele kleine Ein- und Ausgaben |
ru_oublock | Schreibblöcke auf den Datenträger | die Schleife schreibt Zeile für Zeile |
ru_inblock | Leseblöcke vom Datenträger | bestätigt hohe System-Zeit als echten Plattenzugriff |
ru_maxrss | höchster belegter Arbeitsspeicher, unter Linux in Kilobyte | große Datenmengen im Speicher gehalten |
ru_minflt | kleine Seitenfehler ohne Plattenzugriff | nichts, fünfstellige Werte sind Alltag |
ru_majflt | große Seitenfehler mit Plattenzugriff | Speicherdruck und Auslagerung |
ru_nswap | Auslagerungen, unter Linux dauerhaft 0 | nichts, das Feld wird dort nicht gefüllt |
ru_nvcsw | freiwillige Kontextwechsel | viele Wartepunkte im Ablauf |
ru_nivcsw | erzwungene Kontextwechsel | die Maschine ist überbucht |
Die beiden Blockzähler, die PHP getrusage() unter Linux mitliefert, gehören zur Deutung der System-Zeit dazu. Zwanzigtausend einzeln weggeschriebene Zeilen trieben ru_oublock in einer Messung von 0 auf 448, während ru_inblock bei 0 blieb, weil der Seitenzwischenspeicher jeden Lesezugriff bediente. Bleiben beide Zähler bei 0 und die System-Zeit steigt trotzdem, liegt die Last nicht auf der Platte, sondern bei Netz oder Speicherverwaltung.
<?php
$d = getrusage();
$gross = (int) $d['ru_majflt'];
/* Kleine Seitenfehler sind Alltag, grosse nicht.
Ab einem dreistelligen Wert lohnt ein Blick
auf den freien Speicher der ganzen Maschine. */
if ($gross > 100) {
error_log("Seitenfehler gross: $gross");
}
printf("RSS %d KB\n", (int) $d['ru_maxrss']);
/* RSS 29380 KB */
Ein Wort zur Abgrenzung: memory_get_peak_usage() zählt nur den Speicher, den PHP selbst verwaltet. ru_maxrss misst den gesamten Prozess samt Interpreter, geladenen Erweiterungen und allem, was eine Bibliothek im Hintergrund anfordert. Der zweite Wert liegt deshalb immer höher, und für die Frage, ob ein Speicherlimit auf dem Server reicht, ist er der richtige.
Linux gegen Windows: welche Felder fehlen
Unter Windows läuft PHP getrusage() ebenfalls, und es misst auch wirklich. User-Zeit, System-Zeit, ru_maxrss und ru_majflt kommen mit echten Werten zurück. Nur ist das Array kürzer: Gemessen mit PHP 8.4.8 unter Windows 11 stehen genau sechs Einträge darin, unter Linux siebzehn. Es fehlen also elf, darunter ru_minflt, ru_inblock, ru_oublock, ru_nswap und die beiden Zähler für Kontextwechsel. Sie stehen nicht auf null, sie sind gar nicht vorhanden, und ein ungeprüfter Zugriff darauf bringt eine Meldung über einen undefinierten Index.
Dazu kommt die grobe Auflösung. Die Zeitwerte springen in Schritten von 15625 Mikrosekunden, also gut fünfzehn Millisekunden. Ein kurzer Abschnitt misst sich damit zu 0 oder zu 15625, nie dazwischen. Für einen Nachtlauf von acht Minuten reicht das, für eine einzelne Schleife nicht.
<?php
$d = getrusage();
/* Unter Windows fehlen elf der siebzehn Felder.
Nicht blind zugreifen, sondern vorher fragen. */
if (!isset($d['ru_inblock'])) {
echo "Blockzugriffe misst dieses System nicht\n";
}
printf("User %.4f s\n", $d['ru_utime.tv_sec']
+ $d['ru_utime.tv_usec'] / 1000000);
/* Blockzugriffe misst dieses System nicht
User 0.0312 s */
Diese Prüfung ist mehr als Kosmetik. Ohne sie bricht der nächtliche Lauf auf dem Entwicklungsrechner an einem Feld ab, das es dort gar nicht gibt, und die Messung fällt genau dann aus, wenn jemand sie ausprobieren will. Die Zeitfelder sind davon nicht betroffen: Wer nur User- und System-Zeit liest, bekommt auch unter Windows brauchbare Zahlen, nur eben grober gerastert.
Fazit
PHP getrusage() beantwortet eine Frage, die eine Stoppuhr nicht beantworten kann: Hat der Prozessor gearbeitet, oder hat der Prozess gewartet? Die Antwort steht in ru_utime und ru_stime, jeweils zusammengesetzt aus Sekunden und Mikrosekunden, und sie wird erst im Vergleich mit der verstrichenen Zeit aussagekräftig.
Gemessen wird als Differenz zweier Aufrufe, nicht als Einzelwert am Skriptende. Für Kindprozesse steht der Modus 1 im Aufruf und nicht der Name RUSAGE_CHILDREN, den es als PHP-Konstante nicht gibt, und gezählt wird erst nach dem Ende des Kindes. Wer plattformübergreifend misst, fragt die Felder mit isset() ab, weil unter Windows elf der siebzehn fehlen. Wer die Messfunktion einmal ins Protokoll einbaut, hat bei der nächsten Beschwerde über einen langsamen Nachtlauf eine Zahl statt einer Vermutung.