I am using:
pgrep -P $$
to get the child pids of $$. But I actually want a list of grandchildren and great grandchild too.
How do I
If pgrep
doesn't do what you want, you can always use ps
directly. Options will be somewhat platform-dependent.
ps -o ppid,pid |
awk -v pid=$$ 'BEGIN { parent[pid] = 1 } # collect interesting parents
{ child[$2] = $1 } # collect parents of all processes
$1 == pid { parent[$2] = 1 }
END { for (p in child)
if (parent[child[p]])
print p }'
The variable names are not orthogonal -- parent
collects the processes which are pid
or one of its children as keys, i.e. the "interesting" parents, and child
contains the parent of each process, with the process as the key and the parent as the value.
The code below will print the PIDs of the current process and all its descendants. It uses a Bash array as a queue to implement a breadth-first search of the process tree.
unprocessed_pids=( $$ )
while (( ${#unprocessed_pids[@]} > 0 )) ; do
pid=${unprocessed_pids[0]} # Get first elem.
echo "$pid"
unprocessed_pids=( "${unprocessed_pids[@]:1}" ) # Remove first elem.
unprocessed_pids+=( $(pgrep -P $pid) ) # Add child pids
done
Using nothing but bash builtins (not even ps
or pgrep
!):
#!/usr/bin/env bash
collect_children() {
# format of /proc/[pid]/stat file; group 1 is PID, group 2 is its parent
stat_re='^([[:digit:]]+) [(].*[)] [[:alpha:]] ([[:digit:]]+) '
# read process tree into a bash array
declare -g children=( ) # map each PID to a string listing its children
for f in /proc/[[:digit:]]*/stat; do # forcing initial digit skips /proc/net/stat
read -r line <"$f" && [[ $line =~ $stat_re ]] || continue
children[${BASH_REMATCH[2]}]+="${BASH_REMATCH[1]} "
done
}
# run a fresh collection, then walk the tree
all_children_of() { collect_children; _all_children_of "$@"; }
_all_children_of() {
local -a immediate_children
local child
read -r -a immediate_children <<<"${children[$1]}"
for child in "${immediate_children[@]}"; do
echo "$child"
_all_children_of "$child"
done
}
all_children_of "$@"
On my local system, time all_children_of 1 >/dev/null
(invoking the function in an already-running shell) clocks in the neighborhood of 0.018s -- typically, 0.013s for the collect_children
stage (the one-time action of reading the process tree), and 0.05s for the recursive walk of that tree triggered by the initial call of _all_children_of
.
Prior timings were testing only the time needed for the walk, discarding the time needed for the scan.
Probably a simple loop would do it:
# set a value for pid here
printf 'Children of %s:\n' $pid
for child in $(pgrep -P $pid); do
printf 'Children of %s:\n' $child
pgrep -P $child
done
I ended up doing this with node.js and bash:
const async = require('async');
const cp = require('child_process');
export const getChildPids = (pid: number, cb: EVCb<Array<string>>) => {
const pidList: Array<string> = [];
const getMoreData = (pid: string, cb: EVCb<null>) => {
const k = cp.spawn('bash');
const cmd = `pgrep -P ${pid}`;
k.stderr.pipe(process.stderr);
k.stdin.end(cmd);
let stdout = '';
k.stdout.on('data', d => {
stdout += String(d || '').trim();
});
k.once('exit', code => {
if (code > 0) {
log.warning('The following command exited with non-zero code:', code, cmd);
}
const list = String(stdout).split(/\s+/).map(v => String(v || '').trim()).filter(Boolean);
if (list.length < 1) {
return cb(null);
}
for (let v of list) {
pidList.push(v);
}
async.eachLimit(list, 3, getMoreData, cb);
});
};
getMoreData(String(pid), err => {
cb(err, pidList);
});
};
I've already posted an attempted solution. It's short and effective, and seems in line with the OP's question, so I'll leave it as it is. However, it has some performance and portability problems that mean it's not a good general solution. This code attempts to fix the problems:
top_pid=$1
# Make a list of all process pids and their parent pids
ps_output=$(ps -e -o pid= -o ppid=)
# Populate a sparse array mapping pids to (string) lists of child pids
children_of=()
while read -r pid ppid ; do
[[ -n $pid && pid -ne ppid ]] && children_of[ppid]+=" $pid"
done <<< "$ps_output"
# Add children to the list of pids until all descendants are found
pids=( "$top_pid" )
unproc_idx=0 # Index of first process whose children have not been added
while (( ${#pids[@]} > unproc_idx )) ; do
pid=${pids[unproc_idx++]} # Get first unprocessed, and advance
pids+=( ${children_of[pid]-} ) # Add child pids (ignore ShellCheck)
done
# Do something with the list of pids (here, just print them)
printf '%s\n' "${pids[@]}"
The basic approach of using a breadth-first search to build up the tree has been retained, but the essential information about processes is obtained with a single (POSIX-compliant) run of ps
. pgrep
is no longer used because it is not in POSIX and it could be run many times. Also, a very inefficient way of removing items from the queue (copy all but one element of it) has been replaced with manipulation of an index variable.
Average (real) run time is 0.050s when run on pid 0 on my oldish Linux system with around 400 processes.
I've only tested it on Linux, but it only uses Bash 3 features and POSIX-compliant features of ps
so it should work on other systems too.