Getting Linux Process List Without Forking Using Just A Bash Script

The ps command is extremely useful when you want to get some quick information on active system processes (hence the name), especially followed by piping it into grep and kin for some filtering. One gotcha is of course that ps doesn’t run in the current shell process, but is forked off into its own process, so what if everything goes wrong and you absolutely need to run ps aux on a system that is completely and utterly out of fresh process IDs to hand out? In that scenario, you fortunately can write a shell script that does the same, but all within the same shell, as [Isabella Bosia] did, with a Bash shell script.

The how and why is mostly covered in the shell script itself, using detailed comments. Initially the hope was to just read out and parse the contents of /proc/<pid>/status, but that doesn’t have details like CPU%. The result is a bit more parsing to get the desired result, as well as a significant amount of cussing in the comments. Even if it’s not entirely practical, as the odds of ending up on a system with zero free PIDs are probably between zero and NaN, but as an ‘entertaining’ job interview question and example of all the fun things one can do with shell scripting it’s definitely highly recommended.

16 thoughts on “Getting Linux Process List Without Forking Using Just A Bash Script

  1. Also applies to Android.
    ps on Android comes in via toybox and the output is not universal or even 100% parseable (good luck separating the columns for all cases). Going directly to /proc/{pid}/cmdline and /proc/{pid}/status is the way to go. Kernel output is more reliable.

      1. Right! Toybox is the default pack of basic useful commands written (or adapted) by google to work in android systems, almost a kind of Busybox, but more lightweight.

  2. I have a very poor understanding of how bash behaves so perhaps this is just nonsense: Can’t exec be used? ps replaces the shell, and when ps is done the shell restarts.

    1. When ps is done, the shell won’t restart. ps will exit and your SSH session will end. You would need some execline version of ps to pull this off, or to patch ps to make it exec bash again after it’s done doing its thing (another fun project idea). If you’re on a machine where something is actively trying to consume PIDs then you are unlikely to be able to log back in again without an externally triggered reboot.

    2. Regardless of how the shell behaves, that doesn’t work since the shell is no longer there after the exec. The process is now a ps process, and when ps finishes it terminates the process, it doesn’t start a new shell.

    3. Yeah. Like updatebjarni said, bash doesn’t restart. Exec makes ‘ps’ replace bash. There is no bash anymore after the exec call. When ps exits, the process exits.

  3. Funny, the script has the bash shebang at the beginning (#!/bin/bash) so normally people would invoke this with ./scriptname and still start a separate bash instance from their running shell. To run it from within the current shell, it would have to be run through source ./scriptname. You would probably need to run your bash shell as root to make sure you see all the information. But it’s certainly a nice exercise in scripting.
    This is something similar which you can source at startup and than use bashps whenever you need it

    bashps() {
    printf “%-6s %-9s %-6s %-6s %-8s %s\n” “PID” “USER” “CPU%” “MEM%” “STATE” “COMMAND”
    for pid_dir in /proc/[0-9]*; do
    pid=${pid_dir##*/}
    if [[ -r “$pid_dir/status” && -r “$pid_dir/stat” ]]; then
    # Get user
    user=$(awk ‘/^Uid:/{print $2}’ “$pid_dir/status”)
    user=$(getent passwd “$user” | cut -d: -f1)

    # Get CPU usage
    utime=$(awk ‘{print $14}’ “$pid_dir/stat”)
    stime=$(awk ‘{print $15}’ “$pid_dir/stat”)
    starttime=$(awk ‘{print $22}’ “$pid_dir/stat”)
    total_time=$((utime + stime))
    seconds=$(( $(cat /proc/uptime | cut -d. -f1) – (starttime / 100) ))
    cpu=$((1000 * total_time / seconds / 100))

    # Get memory usage
    mem=$(awk ‘/VmRSS:/{print $2}’ “$pid_dir/status”)
    total_mem=$(awk ‘/MemTotal:/{print $2}’ /proc/meminfo)
    mem_percent=$(( (mem * 100) / total_mem ))

    # Get state
    state=$(awk ‘{print $3}’ “$pid_dir/stat”)

    # Get command
    cmd=$(tr -d ‘\0’ < "$pid_dir/cmdline" | tr '\n' ' ' )
    [[ -z "$cmd" ]] && cmd="[$(tr -d '\0' < "$pid_dir/comm")]"

    printf "%-6s %-9s %-6s %-6s %-8s %s\n" "$pid" "$user" "${cpu}%" "${mem_percent}%" "$state" "$cmd"
    fi
    done
    }

    1. I agree that this is not very useful when started as a shell script, but the one from Isabella Bosia can be called via the `source` bash built-in and run in the current shell.
      But if you have access to `cat`, `tr`, `awk`, `getent`, `cut`, etc… (not counting subshells) at that point you can just use `ps` because it’s a sterile exercise in parsing the contents of proc while the interesting aspect of the one in the article is using only shell built-ins to get the whole result.

    2. Thank you so much for the /proc/[0-9]* . I have done this via $(ls -1 /proc | sed -rn ‘s/^([0-9]+)$/\1/p’) ,totally overthinking it.

      I tried your script on Android and it is very slow. Take almost a minute to run. The problem is the amount of processes that get spawned per loop. cat, awk and tr are not built-in commands.

      But for me something like all_status=$(cat /proc/[0-9]*/status) will work, thanks to your glob.

  4. Once upon a time programs like ps or top opened /dev/kmem and read the kernel symbol table to find the addresses of the process table, then seeked to that address and read the raw kernel datastructues.

    I imagine it works great on a mono-processor VAX if you can get it done in one timeslice. Not so great otherwise though.

    Unix used to be quite a bit simpler, perhaps to the point of absurdity.

    1. If you already have top running, sure. Otherwise you have to start a new process (fork), and avoiding that is the whole point of the exercise.

      Though as others have pointed out, this is still not useful unless 1) you source the script (using the “source” or “.” builtin, or whatever equivalent your shell uses), and 2) the script uses only builtins and does not fork any other processes, including not doing anything in a subshell.

  5. Hitting the OS process limit is indeed unlikely. Hitting the nproc rusage (aka “ulimit”) limit is much more likely, for example due to an inadvertent fork bomb in some program you’re working on. That’s one of the main justifications for nproc. Assuming you’re smart enough to set nproc in the first place, of course. (ulimit/setrusage is your friend.)

  6. I now have a flashback to the very late 90’s and a machine with a rogue process such that sh could not fork.
    I had three terminal windows open, and so three chances to find and kill the rogue as each sh was able to exec one command.

Leave a Reply

Please be kind and respectful to help make the comments section excellent. (Comment Policy)

This site uses Akismet to reduce spam. Learn how your comment data is processed.