Search for processes by start time

Python

We used the psutil [4] library for an attempt with Python. psutil provides a large number of functions and delivers bundles of information about processes (e.g., a process's PID, run time, owner, and memory requirements). As was revealed when we read the library's source code, psutil also ultimately accesses information from the /proc filesystem.

The script in Listing 8 includes two functions. The first function, getListOfProcesses(), scans the process list and returns a list of the individual processes. Each list entry contains four data fields: PID, program or call name, time of creation, and username. The second function, calculateTimestamp(), calculates the time, which serves as a limit filter to filter out irrelevant processes later.

Listing 8

Python Variant

01 import psutil
02 import datetime
03
04 # Define global variables
05 listOfProcessNames = []
06
07 def getListOfProcesses(createTime=10):
08   # Deliver list of running processes as dictionary
09   # PID, program name, creation time and process owner
10
11   # Define upper limit of interval
12   intervalTime = calculateTimestamp(createTime)
13
14   for proc in psutil.process_iter():
15     pInfoDict = proc.as_dict(attrs=['pid', 'name', 'create_time', 'username'])
16     # Create time values from
17     currentCreateTime = pInfoDict["create_time"]
18
19     # Is process outside of time interval?
20     if currentCreateTime < intervalTime:
21       listOfProcessNames.append(pInfoDict)
22   return
23
24 def calculateTimestamp(daysValue=10):
25   # Compute time interval (default: ten days)
26
27   # Determine current timestamp
28   currentTimestamp = datetime.datetime.now()
29
30   # Compute time interval
31   dateRange = datetime.timedelta(days=daysValue)
32   targetTimestamp = currentTimestamp - dateRange
33   unixTime = targetTimestamp.timestamp()
34
35   # Return as UNIX timestamp
36   return unixTime
37
38   getListOfProcesses()
39
40   # Sort list by create time and PID
41   listOfProcessNames = sorted(
42   listOfProcessNames,
43   key = lambda i: (i['create_time'], i['pid'])
44   )
45
46   # Process list values from
47   for currentProcess in listOfProcessNames:
48     # Extract process details
49     username = currentProcess["username"]
50     pid = currentProcess["pid"]
51     creationTime = currentProcess["create_time"]
52     creationTimeString = datetime.datetime.fromtimestamp(creationTime).strftime('%d.%m.%Y %H:%M:%S')
53     processName = currentProcess["name"]
54
55   # Output process information
56   print(
57     "User name: %s, PID: %8i, Program: %s" % (username, pid, processName),
58     ", created on",
59     creationTimeString
60   )

The main program first calls the getListOfProcesses() function and then sorts the list of processes by their creation times and PIDs. This results in the output shown in Listing 9, which contains all the processes identified with their owners, PIDs, program names, and creation times. If you want to search for all Bash processes in the results, grep can help you filter the output.

Listing 9

Python Script Output

$ python3 list-processes2.py | grep bash
User name: frank, PID:   3428, Program: bash , created on 08.03.2020 21:49:09
User name: frank, PID:  10438, Program: bash , created on 16.03.2020 21:12:18
User name: frank, PID:   5919, Program: bash , created on 25.03.2020 12:13:29

Perl

As with Python, you would not want to program everything yourself in Perl, although this would certainly be possible by browsing the /proc filesystem. Instead, you should first look at the Comprehensive Perl Archive Network (CPAN) [5], since there may already be a Perl module for accessing the process table. And, lo and behold, there is: Proc::ProcessTable [6].

In Perl, you first create an instance of Proc::ProcessTable and retrieve a reference to a data structure with the entire process table in it. You could certainly iterate through the table with loops. However, if you like functional programming (à la Lisp), you can use a Schwartzian transform [7]. This works almost like a pipe at the command line or in shell scripts, only backwards: The data source is at the end (Listing 10).

Listing 10

Perl Variant

01 #!/usr/bin/perl
02
03 # Boiler plate to avoid bugs
04 use strict;
05 use warnings;
06
07 # Use modern "say" instead of "print"
08 use 5.010;
09
10 # Minimal parameter parsing: If a number is passed as parameter
11 # output this number of processes, otherwise 10.
12 my $max = @ARGV ? $ARGV[0] : 10;
13
14 # Use the Proc::ProcessTable module
15 use Proc::ProcessTable;
16
17 # Create a disposable object and save the process table it generated
18 my $table = Proc::ProcessTable->new->table;
19
20 # Schwartzian transform of table
21 my @result =
22   # Sort the list, first by start time and then by PID
23   sort { ($a->[0] <=> $b->[0]) or ($a->[1] <=> $b->[1]) }
24   # Use only the start time, PID and UID of the process
25   map { [ $_->start, $_->pid, $_->uid ] }
26   # The array following the dereferenced scalar is the data source
27   @$table;
28
29 # Output the results by classical iteration
30 foreach my $p (@result[0..$max-1]) {
31   say sprintf('PID: %6i  |  Start: %s  |  UID: %s',
32               $p->[1], ''.localtime($p->[0]), $p->[2]);
33 }

Listing 11 shows a more compact Perl variant without comments, boiler plate, or command-line parsing (it outputs all processes, sorted) – all of this in just one Schwartzian transform.

Listing 11

Compact Perl Variant

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

Go

The Go programming language has recently gained in popularity among developers [8], which is why we offer an appropriate solution in Go. Our solution is based on two modules, go-ps [9] and go-sysconf [10], which provide functions for reading processes and system information. Further information from the /proc filesystem, which neither of the two modules currently support, is used.

Our Go script has about 150 lines; we have split it into several listings for clarity. The first step (Listing 12) contains the package definition and imports the required modules. The following steps, which are part of the main function, include the variable definitions and parameters (Listing 13), time frame and boot time (Listing 14), CLK_TCK (Listing 15), and routines for retrieving (Listing 16) and evaluating the process list (Listing 17).

Listing 12

Import Required Modules

01 package main
02
03 import (
04   // import standard modules
05   "bufio"
06   "fmt"
07   "io/ioutil"
08   "log"
09   "os"
10   "strconv"
11   "strings"
12   "time"
13   // import additional modules
14   ps "github.com/mitchellh/go-ps"
15   "github.com/tklauser/go-sysconf"
16 )
17
18 func main () {
19   ...
20 }

Listing 13

Variables

01 var bootTime string
02 var userId string
03
04 // Suppress date and time output in log.
05 log.SetFlags(0)
06
07 // Set default value of ten days
08 timeLimit64 := int64(10)
09
10 // Read command line parameters
11 args := os.Args[1:]
12 if len(args) > 0 {
13   // Convert string to number
14   timeLimitArg, err := strconv.ParseInt(args[0], 10, 64)
15   if err != nil {
16     log.Fatalf("Error: %v\n", err)
17   }
18   timeLimit64 = timeLimitArg
19 }
20 log.Printf("Set time limit to %d days\n", timeLimit64)

Listing 13 covers the definition of the required variables and evaluation of the command-line parameters. If nothing else is specified, the program sets the default value to 10.

With the data already determined, the code sets the time frame and consequently defines the relevant processes. It then determines the boot time: the number of seconds since January 1, 1970 (Listing 14). To evaluate time stamps correctly, the clock ticks are determined with the sysconf module as shown in Listing 15.

Listing 14

Time Frame

01 // Compute time frame
02 // Current time - days * 24h * 60min * 60s
03 timeBoundary := time.Now().Unix() - timeLimit64*24*60*60
04
05 // Determine boot time from /proc/stat in seconds since 1.1.1970
06 // available in /proc/stat in the line starting with btime
07 fileHandle, err := os.Open("/proc/stat")
08 if err != nil {
09   log.Fatalf("Error calling os.Open(): %v\n", err)
10 }
11 defer fileHandle.Close()
12
13 scanner := bufio.NewScanner(fileHandle)
14 for scanner.Scan() {
15   currentLine := scanner.Text()
16   if strings.HasPrefix(currentLine, "btime") {
17     dataFields := strings.Fields(currentLine)
18     bootTime = dataFields[1]
19     break
20   }
21 }
22
23 // Convert string to numeric value
24 bootTime64, err := strconv.ParseInt(bootTime, 10, 64)
25 if err != nil {
26   log.Fatalf("Error: %v\n", err)
27 }

Listing 15

CLK_TCK

01 // Reference value stored for CLK_TCK
02 // Values per second
03 clkTck, err := sysconf.Sysconf(sysconf.SC_CLK_TCK)
04 if err != nil {
05   log.Fatalf("Error calling Sysconf")
06 }

In the next step, the user scans the processes and creates a list (Listing 16). A for loop then browses this list and analyzes each process with regard to the user and the process run time. If a process is within the period under consideration, information to that effect is displayed (Listing 17).

Listing 16

Process List

01 // Get process list
02 processList, err := ps.Processes()
03 if err != nil {
04   log.Fatalf("Error in call to ps.Processes()")
05 }

Listing 17

Analyze Processes

01 // Iterate through process list
02 for _, process := range processList {
03  // Read process list
04  // Extract PID and executed program
05  pid := process.Pid()
06  exec := process.Executable()
07
08  // Read user ID from /proc/<pid>/status
09  // Available in column 2 of the line starting with Uid
10  // Go counts with an index of 0; therefore data field 1
11  statusPath := fmt.Sprintf("/proc/%d/status", pid)
12  fileHandle, err := os.Open(statusPath)
13  if err != nil {
14    log.Fatalf("Error calling os.Open(): %v\n", err)
15  }
16  defer fileHandle.Close()
17
18  scanner = bufio.NewScanner(fileHandle)
19  for scanner.Scan() {
20    currentLine := scanner.Text()
21    if strings.HasPrefix(currentLine, "Uid") {
22      uidFields := strings.Fields(currentLine)
23      userId = uidFields[1]
24      break
25    }
26  }
27
28  // Read process status from /proc/<pid>/stat
29  procPath := fmt.Sprintf("/proc/%d/stat", pid)
30  dataBytes, err := ioutil.ReadFile(procPath)
31  if err != nil {
32    log.Fatalf("Error: %v\n", err)
33  }
34  // Break line down into data fields
35  dataFields := strings.Fields(string(dataBytes))
36
37  // Compute process start time
38  // Read number of clock ticks since the system booted
39  // Available in column 22 of /proc/<pid>/stat
40  // Go counts with an index of 0; therefore data field 21
41  executionTime := dataFields[21]
42  executionTime64, err := strconv.ParseInt(executionTime, 10, 64)
43  if err != nil {
44  log.Fatalf("Error: %v\n", err)
45  }
46
47  // Divide the number of clock ticks passed by the stored kernel value
48  // Gives you seconds since booting
49  // And add the boot time
50  executionTime64 = (executionTime64 / clkTck) + bootTime64
51
52  // Check time frame
53  if executionTime64 < timeBoundary {
54    // Compute the start time as a date
55    startDate := time.Unix(executionTime64, 0)
56
57    // Output the information for the process
58    fmt.Printf("User ID: %s, Process ID: %8d, Program name: %s, Started on %s\n", userId, pid, exec, startDate)
59  }
60}

Listing 18 shows the output, where the script was called with the parameter 1 and then processed via a pipe with grep to find all Bash instances called in the process list.

Listing 18

Go Script Output

$ ./list-processes 1 | grep bash
User ID: 1000, Process ID:   604, Program name: bash, Started on 2020-04-14 11:31:51 +0200 CEST
User ID: 1000, Process ID:  5318, Program name: bash, Started on 2020-04-16 16:15:21 +0200 CEST
User ID: 1000, Process ID:  6984, Program name: bash, Started on 2020-04-16 19:04:52 +0200 CEST
User ID: 1000, Process ID:  6998, Program name: bash, Started on 2020-04-16 19:08:57 +0200 CEST

Buy this article as PDF

Express-Checkout as PDF
Price $2.95
(incl. VAT)

Buy Linux Magazine

SINGLE ISSUES
 
SUBSCRIPTIONS
 
TABLET & SMARTPHONE APPS
Get it on Google Play

US / Canada

Get it on Google Play

UK / Australia

Related content

  • Exploring /proc

    The Linux /proc virtual filesystem offers a window into a running system – look inside for information on processes and kernel activity.

  • Command Line: Processes

    Innumerable processes may be running on your Linux system. We’ll show you how to halt, continue, or kill tasks, and we’ll examine how to send the remnants of crashed programs to the happy hunting grounds.

  • Command Line: sort

    sort helps you organize file lists and program

    output. And if you like, you can even use this small

    but powerful tool to merge and sort multiple files.

  • Digital Forensics and Incident Response

    When it's too late to stop an attack, the next urgent task is to find out what happened and assess the damage.

  • Command Line: Process Control

    What is happening on your Linux machine? Various shell commands give you details about system processes and help you control them.

comments powered by Disqus
Subscribe to our Linux Newsletters
Find Linux and Open Source Jobs
Subscribe to our ADMIN Newsletters

Support Our Work

Linux Magazine content is made possible with support from readers like you. Please consider contributing when you’ve found an article to be beneficial.

Learn More

News