How to Kill Processes by Name using script

Linux and some Unixes have a very helpful command called killall, which allows you to kill all running applications that match a specified pattern. It can be quite helpful when you want to kill nine mingetty daemons, or even just to send a SIGHUP signal to xinetd to prompt it to reread its configuration file. Systems that don't have killall can emulate it in a shell script, built around ps for identification of matching processes and kill to send the specified signal.

The tricky part of the script is that the output format from ps varies significantly from OS to OS. For example, consider how differently Mac OS X and Red Hat Linux show running processes in the default ps output:

OSX $ ps
PID TT STAT TIME COMMAND
485 std S 0:00.86 -bash (bash)
581 p2 S 0:00.01 -bash (bash)
RHL9 $ ps
PID TTY TIME CMD
8065 pts/4 00:00:00 bash
12619 pts/4 00:00:00 ps

Worse, rather than model its ps command after a typical Unix command, the GNU ps command accepts BSD-style flags, SYSV-style flags, and GNU-style flags. A complete mishmash!

Fortunately, some of these inconsistencies can be sidestepped in this particular script by using the -cu flag, which produces consistent output that includes the owner of the process, the command name (as opposed to -bash (bash), as in the default Mac OS X output just shown), and the process ID, the lattermost of which is what we're really interested in identifying.

The Code
#!/bin/sh

# killall - Sends the specified signal to all processes that match a
# specific process name.
# By default it only kills processes owned by the same user, unless
# you're root. Use -s SIGNAL to specify a signal to send to the process,
# -u user to specify the user, -t tty to specify a tty,
# and -n to only report what'd be done, rather than doing it.

signal="-INT" # default signal
user="" tty="" donothing=0

while getopts "s:u:t:n" opt; do
case "$opt" in
# Note the trick below: kill wants -SIGNAL but we're asking
# for SIGNAL so we slip the '-' in as part of the assignment
s ) signal="-$OPTARG"; ;;
u ) if [ ! -z "$tty" ] ; then
echo "$0: error: -u and -t are mutually exclusive." >&2
exit 1
fi
user=$OPTARG; ;;
t ) if [ ! -z "$user" ] ; then
echo "$0: error: -u and -t are mutually exclusive." >&2
exit 1
fi
tty=$2; ;;
n ) donothing=1; ;;
? ) echo "Usage: $0 [-s signal] [-u user|-t tty] [-n] pattern" >&2
exit 1
esac
done

shift $(( $OPTIND - 1 ))

if [ $# -eq 0 ] ; then
echo "Usage: $0 [-s signal] [-u user|-t tty] [-n] pattern" >&2
exit 1
fi

if [ ! -z "$tty" ] ; then
pids=$(ps cu -t $tty | awk "/ $1$/ { print \$2 }")
elif [ ! -z "$user" ] ; then
pids=$(ps cu -U $user | awk "/ $1$/ { print \$2 }")
else
pids=$(ps cu -U ${USER:-LOGNAME} | awk "/ $1$/ { print \$2 }")
fi

if [ -z "$pids" ] ; then
echo "$0: no processes match pattern $1" >&2; exit 1
fi

for pid in $pids
do
# Sending signal $signal to process id $pid: kill might
# still complain if the process has finished, the user doesn't
# have permission, etc., but that's okay.
if [ $donothing -eq 1 ] ; then
echo "kill $signal $pid"
else
kill $signal $pid
fi
done

exit 0

How It Works
Because this script is so aggressive, I've put some effort into minimizing false pattern matches, so that a pattern like sh won't match output from ps that contains bash or vi crashtest.c, or other values that embed the pattern. This is done by the pattern-match prefix on the awk command:

awk "/ $1$/ { print \$2 }"

Left-rooting the specified pattern, $1, with a leading space and right-rooting the pattern with a trailing $, causes the script to search for the specified pattern 'sh' in ps output as ' sh$'.

Running the Script
This script has a variety of starting flags that let you modify its behavior. The -s signal flag allows you to specify a signal other than the default interrupt signal, SIGINT, to send to the matching process or processes. The -u user and -t tty flags are useful primarily to the root user in killing all processes associated with a specified user or TTY device, respectively. And the -n flag gives you the option of having the script report what it would do without actually sending any signals. Finally, a pattern must be specified.

The Results
To kill all the csmount processes on my Mac OS X system, I can now use the following:

$ ./killall -n csmount
kill -INT 1292
kill -INT 1296
kill -INT 1306
kill -INT 1310
kill -INT 1318

Hacking the Script
There's an unlikely, though not impossible, bug in this script. To match only the specified pattern, the awk invocation outputs the process ID only of processes that match the pattern plus a leading space that occurs at the end of the input line. However, it's theoretically possible to have two processes running, one called, say, bash and the other emulate bash. If killall is invoked with bash as the pattern, both of these processes will be matched, although only the former is a true match. Solving this to give consistent cross-platform results would prove quite tricky.

If you're motivated, you could also write a script based heavily on the killall script that would let you renice jobs by name, rather than just by process ID. The only change required would be to invoke renice rather than kill.