Aus Linux-Magazin 09/2020

Prozesse nach Zeitpunkt finden

© Alexander Raths, 123RF

Welche Prozesse laufen seit einem bestimmten Zeitpunkt auf einem Linux-System? Die Frage klingt harmlos, aber die Antwort ist kniffliger, als es zunächst scheint.

Ausgangspunkt der Knobeleien war ein praktisches Problem: Als Betreuer eines Rechenclusters [1] stellt einer der Autoren seinen Benutzern auch kommerzielle Software nach dem Fair-Use-Prinzip zur Verfügung. Für diese Software liegen Lizenzschlüssel in einer begrenzten Menge vor, beispielsweise 10 Schlüssel für die Simulationssoftware Matlab [2]. Allerdings können die Berechnungen damit auch einmal eine Woche dauern.

Ist die Berechnung beendet und der Prozess abgeschlossen, wird der Lizenzschlüssel automatisch wieder in den Pool der freien Schlüssel zurückgegeben und kann von einem anderen Benutzer verwendet werden. Vergessen Anwender aber das Beenden ihrer Prozesse, gehen nach einer bestimmten Zeit die Lizenzschlüssel aus. Um das zu verhindern, wollten die Admins automatisch nach Prozessen fahnden, die zum Beispiel älter als zehn Tage sind. Falls sie dabei fündig würden, könnten sie mit den Benutzern klären, was mit dem Prozess passieren soll.

Der Linux-Kernel verwaltet die Prozesse und macht dem Anwender die Informationen darüber via »/proc«-Dateisystem zugänglich. Auf der Kommandozeile ist »ps« die verlässliche Schnittstelle zum Prozessmanagement. Dummerweise kennt Ps Dutzende Optionen, und seine Ausgabe ist oft auch nicht gerade übersichtlich. Abhilfe lässt sich mit ein wenig Shell-Code schaffen – oder lohnt der Griff zu einer Skriptsprache? Der vorliegende Beitrag stellt etliche Lösungsvarianten einander gegenüber – geschrieben in der Bourne-Shell, als Python- und Perl-Skript sowie mithilfe der Programmiersprache Go.

Der Fokus des Beitrags liegt auf der Methode, die noch laufende Prozesse entdeckt, die vor mindestens zehn Tagen gestartet wurden. Das ergibt eine Liste, die das Skript dann zeitlich absteigend sortiert. Die Ausgabe umfasst darüber hinaus den Login-Namen des Benutzers oder seine User-ID, die PID, das ausgeführte Programm sowie den Zeitpunkt, zu dem der jeweilige Prozess gestartet wurde. Dabei sollten möglichst nur Werkzeuge zum Zuge kommen, die zum Standardumfang einer Linux-Installation gehören (Bordmittel). Die vorgestellten Lösungen auf Basis der Shell funktionieren seit dem betagten Release Procps 3.3.0; früheren Versionen fehlen hier verwendete Features.

Variante 1

Basis für eine erste offensichtliche Lösung ist das Kommando Ps in Kombination mit Awk, Date, Sed und Sort. Ps kennt ein optionales Ausgabefeld »lstart«, das die Startzeit (und das Datum) eines Prozesses im einheitlichen, langen Format ausgibt. Zusätzlich muss die Option »-h« verwendet werden, um die Kopfzeilen der Ps-Ausgabe vollständig zu unterdrücken.

Die Lösung (Listing 1) fand sich schnell und wurde flink implementiert. Allerdings ist das Parsen der Ausgabe von Ps nicht ganz trivial, was das Skript relativ unleserlich und auch recht lang macht. Auf folgende Probleme sind die Autoren gestoßen:

Man muss durch das Setzen der Umgebungsvariable »LC_TIME« dafür sorgen, dass nicht plötzlich lokalisierte Monatsnamen auftauchen (»env LC_TIME=C«).

  • Beim Tag des Monats befinden sich zusätzliche Leerzeichen vor den einstelligen Zahlen. Zum Sortieren müssen diese durch eine Null ersetzt werden (erste zwei Zeilen des Sed-Parameters).
  • Das Startdatum enthält die Monate in Buchstaben statt Zahlen, deshalb muss man sie in Ziffern umwandeln. Das gelingt mit den restlichen Zeilen des Sed-Aufrufs.
  • Die Reihenfolge der Datumsbestandteile eignet sich nicht zum Sortieren (erst Monat, dann Tag, dann Uhrzeit und am Ende das Jahr). Die Reihenfolge dieser vier Angaben stellt Awk um.
  • Dasselbe gilt für das Filtern ab einem bestimmten Datum, da Awk mit »<« auch Zeichenketten vergleichen kann.
  • Das passende Vergleichsdatum generiert das Skript gleich zu Beginn mit Date, zumal es Daten auch mit relativen Angaben berechnen kann. Die Angabe “heute vor zehn Tagen” liefert der Aufruf von »date -d ‘now -10 days’«. Date kann die Ausgabe sehr flexibel formatieren.
  • Gibt man beim Aufruf des Skripts keine Parameter an, zeigt es alle Prozesse an, die älter als zehn Tage sind.
  • Schlussendlich müssen noch alle numerisch zu betrachtenden Felder bei Sort explizit angegeben werden, sonst betrachtet Sort nur das erste Feld als numerisch.

Listing 1

Erster Ansatz

#!/bin/sh
if [ -n "$1" ]; then limit=$1; else limit=10; fi
date="$(date '+%Y %m %d %T' -d "now -$limit days")"
env LC_TIME=C ps -eaxho pid,lstart,user,cmd | \
  sed -e 's/^ *//;
          s/  \([1-9]\) / 0\1 /;
          s/Jan/01/;
          s/Feb/02/;
          s/Mar/03/;
          s/Apr/04/;
          s/May/05/;
          s/Jun/06/;
          s/Jul/07/;
          s/Aug/08/;
          s/Sep/09/;
          s/Oct/10/;
          s/Nov/11/;
          s/Dec/12/' | \
  awk '$6" "$3" "$4" "$5" "$1 < "'"$date"'" {print $6" "$3" "$4" "$5" "$1" "$7" "$8}' | \
  sort -n -k1 -k2 -k3 -k4 -k5

Die Ausgabe dieses Skripts ohne die Sort-Parameter mit »-k« sieht dann zum Beispiel bei einem Rechner, der zuletzt am 3. April 2020 gebootet wurde, so aus wie in Listing 2.

Listing 2

Ausgabe des ersten Skripts

$ ./list-processes1.sh | head
2020 04 03 22:32:34 1 root init
2020 04 03 22:32:34 10 root [ksoftirqd/0]
2020 04 03 22:32:34 104 root [kintegrityd]
2020 04 03 22:32:34 105 root [kblockd]
2020 04 03 22:32:34 106 root [blkcg_punt_bio]
2020 04 03 22:32:34 11 root [rcu_sched]
2020 04 03 22:32:34 12 root [migration/0]
2020 04 03 22:32:34 13 root [cpuhp/0]
2020 04 03 22:32:34 14 root [cpuhp/1]
2020 04 03 22:32:34 15 root [migration/1]

Es fällt sofort auf, dass die Reihenfolge der Prozesse nicht stimmen kann. Das liegt daran, dass die Zeitstempel im Feld »lstart« nur auf die Sekunde genau sind, nicht auf die Mikro- oder Nanosekunde. Sortiert man ganz am Ende die Ausgabe nach den Prozessnummern, löst sich dieses Problem zum größten Teil. Dabei muss man im Sort-Aufruf, wie oben gezeigt, alle Felder bis einschließlich der Prozessnummer angeben. Die Ausgabe sieht dann aus wie in Listing 3.

Listing 3

Sortierte Ausgabe

$ ./list-processes1.sh | head
2020 04 03 22:32:34 1 root init
2020 04 03 22:32:34 2 root [kthreadd]
2020 04 03 22:32:34 3 root [rcu_gp]
2020 04 03 22:32:34 4 root [rcu_par_gp]
2020 04 03 22:32:34 6 root [kworker/0:0H-kblockd]
2020 04 03 22:32:34 9 root [mm_percpu_wq]
2020 04 03 22:32:34 10 root [ksoftirqd/0]
2020 04 03 22:32:34 11 root [rcu_sched]
2020 04 03 22:32:34 12 root [migration/0]
2020 04 03 22:32:34 13 root [cpuhp/0]

Hier liegt das Skript nur noch dann falsch, wenn innerhalb einer Sekunde so viele Prozesse gestartet wurden, dass die Prozessnummern wieder von vorn vergeben wurden. Die Grenze dafür lag lange Zeit bei 65 535 Prozessen – mittlerweile kommen Linux-Systeme auch mit größeren Prozess-IDs zurecht.

Variante 2

Ein vertieftes Studium der Manpage von Ps brachte noch weitere für die vorliegende Aufgabe nützliche Felder zum Vorschein, wie etwa das Ausgabefeld »etimes«. Es gibt die Anzahl der Sekunden seit dem Start des Prozesses an. Das verringert die Komplexität erheblich, da man nun keine Monatsnamen mehr parsen und keine Felder mehr umsortieren muss. Dadurch verkürzt sich das Kommando auf einen echten Einzeiler. Listing 4 liefert alle Prozesse, die älter als zwei Tage sind.

Listing 4

Einzeiler

$ ps -eaxho etimes,pid,user,cmd | sort -nr | awk '$1 > 2*24*60*60 {print}' | head
 227081  106 root  [blkcg_punt_bio]
 227081  105 root  [kblockd]
 227081  104 root  [kintegrityd]
 227081   57 root  [khugepaged]
 227081   56 root  [ksmd]
 227081   55 root  [kcompactd0]
 227081   54 root  [writeback]
 227081   53 root  [oom_reaper]
 227081   52 root  [khungtaskd]
 227081   51 root  [kauditd]

Allerdings arbeitet auch diese Variante lediglich auf die Sekunde genau. Da der Code rückwärts sortiert, fällt das noch mehr auf, denn die PID 1 erscheint nicht am Anfang der Liste. Das lässt sich mit genauerem Nachlesen der Optionen des Befehls Sort noch so weit flicken, dass bei identischem Prozessalter die PID als Sortierkriterium greift, in aufsteigender Reihenfolge. Dafür sorgt die Parameterangabe »k1nr,2n« (Listing 5).

Listing 5

Verbesserter Einzeiler

$ ps -eaxho etimes,pid,user,cmd | sort -k1nr,2n | awk '$1 > 2*24*60*60 {print}' | head
 226597   1 root  init [2]
 226597   2 root  [kthreadd]
 226597   3 root  [rcu_gp]
 226597   4 root  [rcu_par_gp]
 226597   6 root  [kworker/0:0H-kblockd]
 226597   9 root  [mm_percpu_wq]
 226597  10 root  [ksoftirqd/0]
 226597  11 root  [rcu_sched]
 226597  12 root  [migration/0]
 226597  13 root  [cpuhp/0]

Der bisherige Aufruf enthält die Berechnung der Sekunden durch Awk in ausführlicher Form: »2*24*60*60« entspricht zwei Mal 24 Stunden zu je 60 Minuten mit jeweils 60 Sekunden. Stattdessen kann man den Wert auch direkt als »172800« hinschreiben.

Zur Parametrisierung im Skript eignet sich der Wert »86400« für die Anzahl der Sekunden pro Tag. Listing 6 akzeptiert einen Parameter für die Anzahl der Tage. Den übergebenen Zahlenwert multiplizieren Sie dann mit 86 400.

Listing 6

Mit Tagesanzahl als Parameter

#!/bin/sh
if [ -n "$1" ]; then
  limit=$1;
else
  limit=10;
fi
ps -eaxho etimes,pid,user,cmd | sort -k1nr,2n | awk '$1 > '"$limit"'*86400 {print}'

Geben Sie im Aufruf keinen Zahlenwert als Parameter an, verwendet das Skript den Zahlenwert 10 als Standardfall (für zehn Tage).

Variante 3

Das Fehlen der Sekundenbruchteile wurmte einen der Autoren dennoch so, dass er noch einen dritten Versuch unternahm. Statt des Kommandos »ps« dienen dabei Einträge aus dem »/proc«-Dateisystem als Basis.

Die benötigte Angabe findet sich in Feld Nummer 22 (»starttime«) in der Datei »/proc/<pid>/stat«. Sie gibt an, wie viele Clock Ticks nach dem Start des Linux-Kernels ein Prozess gestartet wurde. Die Angabe der Clock Ticks ist trickreich; sie basiert auf der Annahme eines Takts von 100 Hz, also 100 Ticks pro Sekunde [3] (Listing 7).

Listing 7

Basis der Clock Ticks

$ getconf CLK_TCK
100

Daran halten sich jedoch nicht alle Distributionen: Manche verwenden intern stattdessen etwa 250 oder 1000 Hz. Nach außen hin melden sie dennoch stets die Angabe 100 Hz. Warum das so ist, konnten die Autoren nicht klären. Unter Debian GNU/Linux sind beide Werte identisch 100 Hz.

Analog zu den vorherigen Shell-Skripten liest das Exemplar aus Listing 8 zuerst einmal wieder einen Parameter aus und verwendet, falls keine Zeitspanne angegeben wurde, zehn Tage als Vorgabe. Danach liest Awk die beiden Felder 1 und 22 (User-ID des Prozesses und Anzahl der Clock Ticks) aus, und zwar in zwei Aufrufen. Der erste ermittelt die Werte für den eigenen Prozess (dessen Prozess-ID in Shells typischerweise in »$$« steht), der zweite die aktuelle Zeit in Clock Ticks seit dem Start des Rechners.

Anschließend liest Awk die »stat«-Dateien aller laufenden Prozesse aus; das gelingt mit der Angabe »/proc/[1-9]*/stat«. Die Anzahl der Clock Ticks pro Sekunde (100) und Sekunden pro Tag (86 400) werden hier der Einfachheit halber als Wert hart verdrahtet.

Da die Ausgabe als Fließkommazahl auch schön aussehen soll, wird sie mittels »printf« mit nur zwei Nachkommastellen ausgegeben – genauer sind die Clock Ticks sowieso nicht. Sort sortiert dann die beiden relevanten Felder als Spalten numerisch. Die erste numerische Spalte führt die Anzahl der Clock Ticks auf, die zweite die User-ID.

Listing 8

Mit Clock Ticks

#!/bin/sh
if [ -n "$1" ]; then
  limit=$1;
else
  limit=10;
fi
now=$(awk '{print $22}' /proc/$$/stat)
awk '$22 < '$now'-(100*86400*'$limit') {printf "Sec. since boot: %.2f - PID: %i\n", $22/100, $1}' /proc/[1-9]*/stat | sort -n -k4 -k7

Die Lösung kommt dem Ziel schon recht nahe, kann aber die Benutzernamen zum Prozess nicht anzeigen. Einzelne Prozesse, die definitiv lange nach dem Systemstart gestartet wurden (zum Beispiel der Tor-Browser), erscheinen zudem unerwarteterweise so, als wären sie null Sekunden nach Systemstart angelaufen. Der Prozess »init« dagegen startete gemäß der Ausgabe erst 468 Clock Ticks beziehungsweise 4,68 Sekunden nach dem Systemstart. Im Testfall war das vermutlich so, weil vorher noch das Passwort für die Festplattenverschlüsselung eingegeben werden musste.

Wer Awk aus dem Code entfernt und die passenden Felder 22 und 1 direkt als Parameter des Sort-Kommandos angibt, macht alles noch einen Tick einfacher. Das Ergebnis ist allerdings eine unleserliche Ausgabe mit enorm vielen Daten.

Ärgerlicherweise fallen auch hier die Zeitangaben immer noch zu ungenau aus, um auf eine abschließende Sortierung nach Prozess-ID verzichten zu können. Die Angaben sollten zwar theoretisch präziser sein als bei den vorherigen Varianten, da Clock Ticks eine genauere Angabe liefern als ganze Sekunden. Dennoch besteht das Problem der Ungenauigkeit bei einem Prozess-ID-Überlauf offensichtlich noch immer. Insgesamt erscheint die Variante mit Ps dann doch als der bessere Weg.

Lösung in Python

Beim nächsten Versuch mit Python kommt die Bibliothek Psutil [4] zum Einsatz. Sie stellt sehr viele Funktionen und Informationen zu Prozessen bereit, beispielsweise die Prozess-IDs sowie Laufzeit, Eigentümer und Speicherbedarf des Prozesses. Wie sich beim Lesen des Quelltexts der Bibliothek zeigte, greift auch Psutil letztlich auf die Informationen des »/proc«-Dateisystems zu.

Das Skript in Listing 9 umfasst zwei Funktionen: Die erste, »getListOfProcesses()«, durchstöbert die Prozessliste und liefert eine Liste mit den einzelnen Prozessen zurück. Jeder Listeneintrag enthält die vier Datenfelder PID, Programm- beziehungsweise Aufrufname, Zeitpunkt der Erzeugung und Benutzername. Die zweite, »calculateTimestamp()«, berechnet den Zeitpunkt, der als Filtergrenze dient, mit der später die nicht relevanten Prozesse herausgefiltert werden.

Listing 9

Python-Version

import psutil
import datetime
# definiere globale Variablen
listOfProcessNames = []
def getListOfProcesses(createTime=10):
  # Liefere Liste der laufenden Prozesse als Dictionary aus
  # PID, Programmname, Erstellzeitpunkt und Prozesseigentuemer
  # Lege obere Grenze des Intervalls fest
  intervalTime = calculateTimestamp(createTime)
  for proc in psutil.process_iter():
    pInfoDict = proc.as_dict(attrs=['pid', 'name', 'create_time', 'username'])
    # Werte Erzeugungszeit aus
    currentCreateTime = pInfoDict["create_time"]
    # Liegt Prozess ausserhalb des Zeitintervalls?
    if currentCreateTime < intervalTime:
      listOfProcessNames.append(pInfoDict)
  return
def calculateTimestamp(daysValue=10):
  # Berechne das Zeitintervall (default: zehn Tage)
  # Ermittle aktuellen Zeitpunkt
  currentTimestamp = datetime.datetime.now()
  # Berechne das Zeitintervall
  dateRange = datetime.timedelta(days=daysValue)
  targetTimestamp = currentTimestamp - dateRange
  unixTime = targetTimestamp.timestamp()
  # Rueckgabe als UNIX-Zeitstempel
  return unixTime
  getListOfProcesses()
  # Liste nach Erzeugungszeit und PID sortieren
  listOfProcessNames = sorted(
  listOfProcessNames,
  key = lambda i: (i['create_time'], i['pid'])
  )
  # Werte Prozessliste aus
  for currentProcess in listOfProcessNames:
    # Extrahiere Prozessdetails
    username = currentProcess["username"]
    pid = currentProcess["pid"]
    creationTime = currentProcess["create_time"]
    creationTimeString = datetime.datetime.fromtimestamp(creationTime).strftime('%d.%m.%Y %H:%M:%S')
    processName = currentProcess["name"]
  # Gib Prozessinformation aus
  print(
    "Benutzername: %s, PID: %8i, Programm: %s" % (username, pid, processName),
    ", erzeugt am",
    creationTimeString
  )

Das Hauptprogramm ruft zuerst die Funktion »getListOfProcesses()« auf und sortiert danach die Liste der Prozesse nach deren Erzeugungszeit und PID. Das Ergebnis ist eine Ausgabe wie in Listing 10. Sie enthält alle gefundenen Prozesse mit Eigentümer, PID, Programmnamen und Erstellzeitpunkt. Sucht man nach allen Bash-Prozessen aus dem Ergebnis, hilft Grep beim Filtern der Ausgabe.

Listing 10

Ausgabe des Python-Skripts

$ python3 list-processes2.py | grep bash
Benutzername: frank, PID:   3428, Programm: bash , Erzeugt am 08.03.2020 21:49:09
Benutzername: frank, PID:  10438, Programm: bash , Erzeugt am 16.03.2020 21:12:18
Benutzername: frank, PID:   5919, Programm: bash , Erzeugt am 25.03.2020 12:13:29

Lösungen in Perl

So wie bei Python will der Admin auch in Perl nicht alles selbst programmieren, wobei das sicher durch das Abgrasen des »/proc«-Dateisystems möglich wäre. Stattdessen gilt der erste Blick dem Comprehensive Perl Archive Network (CPAN [5]): Vielleicht gibt es ja schon ein Perl-Modul für den Zugriff auf die Prozesstabelle. Und siehe da, es gibt »Proc::ProcessTable« [6], das deshalb zum Zug kommt.

Der Perl-Programmierer erzeugt zuerst eine Instanz von »Proc::ProcessTable« und erhält eine Referenz auf eine Datenstruktur mit der gesamten Prozesstabelle darin. Man könnte hier sicher auch prozedural mit Schleifen durch die Tabelle wandern; wer aber die funktionale Programmierung mag (Lisp lässt grüßen), der greift zur Schwartzschen Transformation [7]. Das funktioniert fast wie eine Pipe auf der Kommandozeile oder in Shell-Skripten, nur rückwärts: Die Datenquelle steht am Ende (Listing 11).

Listing 11

Perl-Variante

#!/usr/bin/perl
# Boilerplate, um Fehler zu vermeiden
use strict;
use warnings;
# Nutze das moderne "say" statt "print"
use 5.010;
# Minimales Parameter-Parsen: Wird eine Zahl übergeben,
# gib diese Anzahl an Prozessen aus, sonst 10.
my $max = @ARGV ? $ARGV[0] : 10;
# Nutze das Proc::ProcessTable-Modul
use Proc::ProcessTable;
# Erstelle ein Wegwerfobjekt und speichere dessen generierte Prozesstabelle
my $table = Proc::ProcessTable->new->table;
# Schwartzsche Transformation der Tabelle
my @result =
  # Sortiere die Liste erst nach der Startzeit und dann nach PID
  sort { ($a->[0] <=> $b->[0]) or ($a->[1] <=> $b->[1]) }
  # Verwende nur Startzeitpunkt, PID und UID des Prozesses
  map { [ $_->start, $_->pid, $_->uid ] }
  # Das Array hinter dem dereferenzierten Skalar ist die Datenquelle
  @$table;
# Gib das Ergebnis durch klassisches Iterieren aus
foreach my $p (@result[0..$max-1]) {
  say sprintf('PID: %6i  |  Start: %s  |  UID: %s',
              $p->[1], ''.localtime($p->[0]), $p->[2]);
}

Listing 12 zeigt eine kompaktere Variante ohne Kommentare, ohne Boilerplate, ohne Kommandozeilen-Parsen (gibt alle Prozesse sortiert aus) – und alles in einer einzigen Schwartzschen Transformation.

Listing 12

Verkürzte Perl-Variante

#!/usr/bin/perl
use Proc::ProcessTable;
print
  map { sprintf("PID: %6i  |  Start: %s  |  UID: %s\n",
                $_->[1], ''.localtime($_->[0]), $_->[2]) }
  sort { ($a->[0] <=> $b->[0]) or ($a->[1] <=> $b->[1]) }
  map { [ $_->start, $_->pid, $_->uid ] }
  @{ Proc::ProcessTable->new->table };

Versuch mit Go

Die Programmiersprache Go hat bei Entwicklern in der letzten Zeit stark an Akzeptanz gewonnen [8], weshalb hier eine entsprechende Lösung nicht fehlen darf. Sie setzt auf den beiden Modulen Go-ps [9] und Go-sysconf [10] auf, die Funktionen zum Lesen der Prozesse und der Systeminformationen bereitstellen. Zurückgegriffen wird auf weitere Informationen aus dem »/proc«-Dateisystem, die beide Module (noch) nicht liefern.

Das Listing umfasst etwa 150 Zeilen, weswegen es hier in mehrere Schritte zerlegt wurde. Im ersten erfolgen die Paketdefinition und der Import der benötigten Module (Listing 13). Der zweite umfasst die Hauptfunktion samt Variablendefinition und Parametern, Zeitrahmen und Boot-Zeit, »CLK_TCK« sowie dem Beziehen und Auswerten der Prozessliste.

Listing 13

Import der nötigen Module

package main
import (
  // importiere Standardmodule
  "bufio"
  "fmt"
  "io/ioutil"
  "log"
  "os"
  "strconv"
  "strings"
  "time"
  // importiere zusätzliche Module
  ps "github.com/mitchellh/go-ps"
  "github.com/tklauser/go-sysconf"
)
func main () {
  ...
}

Die nachfolgenden Listings gehören alle zur Hauptfunktion. Der erste Schritt umfasst die Definition der benötigten Variablen sowie die Auswertung der Kommandozeilenparameter. Gibt man nichts anderes an, setzt das Programm den Standardwert auf 10 (Listing 14).

Listing 14

Hauptfunktion – Variablen

  var bootTime string
  var userId string
  // Ausgabe von Datum und Zeit in Log-Ausgabe unterdrücken.
  log.SetFlags(0)
  // setze Standardwert von zehn Tagen
  timeLimit64 := int64(10)
  // lese Kommandozeilenparameter
  args := os.Args[1:]
  if len(args) > 0 {
    // konvertiere Zeichenkette in Zahlenwert
    timeLimitArg, err := strconv.ParseInt(args[0], 10, 64)
    if err != nil {
      log.Fatalf("Fehler: %v\n", err)
    }
    timeLimit64 = timeLimitArg
  }
  log.Printf("Setze Zeitlimit auf %d Tage\n", timeLimit64)

Mit den bereits ermittelten Daten setzt der Code den Zeitrahmen und definiert damit die relevanten Prozesse. Danach ermittelt er die Boot-Zeit, also die Anzahl der Sekunden seit dem 1. Januar 1970 (Listing 15). Zum korrekten Auswerten der Zeitstempel folgt das Ermitteln der Clock Ticks mit dem Sysconf-Modul, wie in Listing 16 gezeigt.

Listing 15

Hauptfunktion – Zeitrahmen

  // berechne Zeitrahmen
  // aktuelle Zeit - Tage * 24h * 60min * 60s
  timeBoundary := time.Now().Unix() - timeLimit64*24*60*60
  // ermittle Boot-Zeitpunkt aus /proc/stat in Sekunden seit 1.1.1970
  // steht in /proc/stat in der Zeile, die mit btime beginnt
  fileHandle, err := os.Open("/proc/stat")
  if err != nil {
    log.Fatalf("Fehler im Aufruf von os.Open(): %v\n", err)
  }
  defer fileHandle.Close()
  scanner := bufio.NewScanner(fileHandle)
  for scanner.Scan() {
    currentLine := scanner.Text()
    if strings.HasPrefix(currentLine, "btime") {
      dataFields := strings.Fields(currentLine)
      bootTime = dataFields[1]
      break
    }
  }
  // konvertiere Zeichenkette in Zahlenwert
  bootTime64, err := strconv.ParseInt(bootTime, 10, 64)
  if err != nil {
    log.Fatalf("Fehler: %v\n", err)
  }

Listing 16

Hauptfunktion – CLK_TCK

  // beziehe den hinterlegten Wert für CLK_TCK
  // Werte pro Sekunde
  clkTck, err := sysconf.Sysconf(sysconf.SC_CLK_TCK)
  if err != nil {
    log.Fatalf("Fehler im Aufruf von Sysconf")
  }

Im nächsten Schritt durchforstet der Anwender die Prozesse und erstellt eine Liste (Listing 17). In einer For-Schleife geht er nun diese Liste durch und analysiert jeden Prozess in Bezug auf den Benutzer sowie die Prozesslaufzeit. Falls ein Prozess innerhalb des betrachteten Zeitraums liegt, wird die zugehörige Information ausgegeben (Listing 18).

Listing 17

Hauptfunktion – Prozessliste

  // beziehe Prozessliste
  processList, err := ps.Processes()
  if err != nil {
    log.Fatalf("Fehler im Aufruf von ps.Processes()")
  }

Listing 18

Hauptfunktion – Prozesse analysieren

  // durchlaufe die Prozessliste
  for _, process := range processList {
    // lese Prozessliste aus
    // extrahiere PID und ausgeführtes Programm
    pid := process.Pid()
    exec := process.Executable()
    // lese User ID aus /proc/<pid>/status
    // steht in Spalte 2 der Zeile, die mit Uid beginnt
    // Go zählt mit Index 0, daher Datenfeld 1
    statusPath := fmt.Sprintf("/proc/%d/status", pid)
    fileHandle, err := os.Open(statusPath)
    if err != nil {
      log.Fatalf("Fehler im Aufruf von os.Open(): %v\n", err)
    }
    defer fileHandle.Close()
    scanner = bufio.NewScanner(fileHandle)
    for scanner.Scan() {
      currentLine := scanner.Text()
      if strings.HasPrefix(currentLine, "Uid") {
        uidFields := strings.Fields(currentLine)
        userId = uidFields[1]
        break
      }
    }
    // lese Prozessstatus aus /proc/<pid>/stat
    procPath := fmt.Sprintf("/proc/%d/stat", pid)
    dataBytes, err := ioutil.ReadFile(procPath)
    if err != nil {
      log.Fatalf("Fehler: %v\n", err)
    }
    // zerlege Zeile in Datenfelder
    dataFields := strings.Fields(string(dataBytes))
    // berechne Startzeitpunkt des Prozesses
    // lese die Anzahl Clock Ticks, die seit dem Booten des Systems vergangen sind
    // steht in Spalte 22 von /proc/<pid>/stat
    // Go zählt mit Index 0, daher Datenfeld 21
    executionTime := dataFields[21]
    executionTime64, err := strconv.ParseInt(executionTime, 10, 64)
    if err != nil {
      log.Fatalf("Fehler: %v\n", err)
    }
    // teile die Anzahl vergangener Clock Ticks durch den hinterlegten Kernel-Wert
    // ergibt Sekunden seit dem Booten
    // und rechne Boot-Zeit hinzu
    executionTime64 = (executionTime64 / clkTck) + bootTime64
    // prüfe Zeitrahmen
    if executionTime64 < timeBoundary {
      // berechne den Startzeitpunkt als Datum
      startDate := time.Unix(executionTime64, 0)
      // Ausgabe der Information für den Prozess
      fmt.Printf("Benutzer-ID: %s, Prozess-ID: %8d, Programmname: %s, Gestartet am %s\n", userId, pid, exec, startDate)
    }
  }

Als Ergebnis erhält man eine Ausgabe wie in Listing 19. Hier wurde das Skript mit dem Parameter »1« aufgerufen und anschließend noch über eine Pipe mit Grep bearbeitet, um in der Prozessliste alle aufgerufenen Bash-Instanzen zu finden.

Listing 19

Ausgabe des Go-Scripts

$ ./list-processes 1 | grep bash
Benutzer-ID: 1000, Prozess-ID:   604, Programmname: bash, Gestartet am 2020-04-14 11:31:51 +0200 CEST
Benutzer-ID: 1000, Prozess-ID:  5318, Programmname: bash, Gestartet am 2020-04-16 16:15:21 +0200 CEST
Benutzer-ID: 1000, Prozess-ID:  6984, Programmname: bash, Gestartet am 2020-04-16 19:04:52 +0200 CEST
Benutzer-ID: 1000, Prozess-ID:  6998, Programmname: bash, Gestartet am 2020-04-16 19:08:57 +0200 CEST

Danksagung

Die Autoren bedanken sich bei Tobias Klauser für sein Paket Go-sysconf und seine Unterstützung bei der Optimierung der Go-Variante.

Fazit

Vergleicht man alle Lösungen hinsichtlich der Funktionalität und dem eingangs formulierten Ziel miteinander, liefern alle Spielarten mit Ausnahme der Variante 3 (Bash) ein brauchbares Ergebnis. Bezüglich der Programmgröße gewinnt Variante 2 (Bash); die Go-Variante ist mit mehr als 140 Zeilen am längsten. Die Python-Implementierung liegt im unteren Mittelfeld.

Bei der Verständlichkeit und Lesbarkeit gehen die Meinungen deutlich auseinander. Insbesondere bei Listing 12 (Perl) benötigen selbst eingefleischte Perl-Programmierer einen Moment (und die Doku zum verwendeten Modul), um es zu verstehen. Die Implementierungen in Python oder Go mögen zwar länger ausfallen, lassen sich jedoch in kurzer Zeit auch von Einsteigern nachvollziehen.

Bezogen auf die Laufzeit stellten die Autoren bei den Lösungen keine nennenswerten Unterschiede fest; alle lieferten zumeist innerhalb von ein bis maximal eineinhalb Sekunden ein Ergebnis. Das genügt für den Alltagsbetrieb.

Sowohl das Python-Skript als auch die Perl- und die Go-Implementierung greifen auf eine jeweils passende Bibliothek zurück, die die Prozessinformationen bequem bereitstellt. Die Bibliotheken für Python und Perl erweisen sich als die umfangreichsten. Die Go-Bibliothek ist hingegen noch ausbaufähig: Funktionen, die die Python-Bibliothek bereits integriert, mussten in der Go-Variante selbst gebaut werden. (jcb/jlu)

Die Autoren

Frank Hofmann arbeitet zumeist unterwegs, bevorzugt aus Berlin, Genf und Kapstadt, als Entwickler, Trainer und Autor. Derzeit betreut er als Linux-Systemadministrator den Cluster für wissenschaftliches Rechnen am Mésocentre de calcul an der Université de Franche-Comté in Besançon.

Axel Beckert arbeitet als Linux-Systemadministrator und Spezialist für Netzwerksicherheit bei den Informatikdiensten der ETH Zürich. Nebenher ist er ehrenamtlich bei der Linux-Distribution Debian, in der Linux User Group Switzerland (LUGS), bei der Radiosendung und dem Podcast Hackerfunk sowie in diversen Open-Source-Projekten aktiv.

Die beiden Autoren sind die Verfasser des Debian-Paketmanagement-Buchs*[11].

DIESEN ARTIKEL ALS PDF KAUFEN
EXPRESS-KAUF ALS PDFUmfang: 9 HeftseitenPreis €0,99
(inkl. 19% MwSt.)
LINUX-MAGAZIN KAUFEN
EINZELNE AUSGABE Print-Ausgaben Digitale Ausgaben
ABONNEMENTS Print-Abos Digitales Abo
TABLET & SMARTPHONE APPS Readly Logo
E-Mail Benachrichtigung
Benachrichtige mich zu:
0 Kommentare
Älteste
Neuste Beste Bewertung
Inline Feedbacks
Alle Kommentare anzeigen
Nach oben