.\" Automatically generated by Pod::Man 2.25 (Pod::Simple 3.16) .\" .\" Standard preamble: .\" ======================================================================== .de Sp \" Vertical space (when we can't use .PP) .if t .sp .5v .if n .sp .. .de Vb \" Begin verbatim text .ft CW .nf .ne \\$1 .. .de Ve \" End verbatim text .ft R .fi .. .\" Set up some character translations and predefined strings. \*(-- will .\" give an unbreakable dash, \*(PI will give pi, \*(L" will give a left .\" double quote, and \*(R" will give a right double quote. \*(C+ will .\" give a nicer C++. Capital omega is used to do unbreakable dashes and .\" therefore won't be available. \*(C` and \*(C' expand to `' in nroff, .\" nothing in troff, for use with C<>. .tr \(*W- .ds C+ C\v'-.1v'\h'-1p'\s-2+\h'-1p'+\s0\v'.1v'\h'-1p' .ie n \{\ . ds -- \(*W- . ds PI pi . if (\n(.H=4u)&(1m=24u) .ds -- \(*W\h'-12u'\(*W\h'-12u'-\" diablo 10 pitch . if (\n(.H=4u)&(1m=20u) .ds -- \(*W\h'-12u'\(*W\h'-8u'-\" diablo 12 pitch . ds L" "" . ds R" "" . ds C` "" . ds C' "" 'br\} .el\{\ . ds -- \|\(em\| . ds PI \(*p . ds L" `` . ds R" '' 'br\} .\" .\" Escape single quotes in literal strings from groff's Unicode transform. .ie \n(.g .ds Aq \(aq .el .ds Aq ' .\" .\" If the F register is turned on, we'll generate index entries on stderr for .\" titles (.TH), headers (.SH), subsections (.SS), items (.Ip), and index .\" entries marked with X<> in POD. Of course, you'll have to process the .\" output yourself in some meaningful fashion. .ie \nF \{\ . de IX . tm Index:\\$1\t\\n%\t"\\$2" .. . nr % 0 . rr F .\} .el \{\ . de IX .. .\} .\" .\" Accent mark definitions (@(#)ms.acc 1.5 88/02/08 SMI; from UCB 4.2). .\" Fear. Run. Save yourself. No user-serviceable parts. . \" fudge factors for nroff and troff .if n \{\ . ds #H 0 . ds #V .8m . ds #F .3m . ds #[ \f1 . ds #] \fP .\} .if t \{\ . ds #H ((1u-(\\\\n(.fu%2u))*.13m) . ds #V .6m . ds #F 0 . ds #[ \& . ds #] \& .\} . \" simple accents for nroff and troff .if n \{\ . ds ' \& . ds ` \& . ds ^ \& . ds , \& . ds ~ ~ . ds / .\} .if t \{\ . ds ' \\k:\h'-(\\n(.wu*8/10-\*(#H)'\'\h"|\\n:u" . ds ` \\k:\h'-(\\n(.wu*8/10-\*(#H)'\`\h'|\\n:u' . ds ^ \\k:\h'-(\\n(.wu*10/11-\*(#H)'^\h'|\\n:u' . ds , \\k:\h'-(\\n(.wu*8/10)',\h'|\\n:u' . ds ~ \\k:\h'-(\\n(.wu-\*(#H-.1m)'~\h'|\\n:u' . ds / \\k:\h'-(\\n(.wu*8/10-\*(#H)'\z\(sl\h'|\\n:u' .\} . \" troff and (daisy-wheel) nroff accents .ds : \\k:\h'-(\\n(.wu*8/10-\*(#H+.1m+\*(#F)'\v'-\*(#V'\z.\h'.2m+\*(#F'.\h'|\\n:u'\v'\*(#V' .ds 8 \h'\*(#H'\(*b\h'-\*(#H' .ds o \\k:\h'-(\\n(.wu+\w'\(de'u-\*(#H)/2u'\v'-.3n'\*(#[\z\(de\v'.3n'\h'|\\n:u'\*(#] .ds d- \h'\*(#H'\(pd\h'-\w'~'u'\v'-.25m'\f2\(hy\fP\v'.25m'\h'-\*(#H' .ds D- D\\k:\h'-\w'D'u'\v'-.11m'\z\(hy\v'.11m'\h'|\\n:u' .ds th \*(#[\v'.3m'\s+1I\s-1\v'-.3m'\h'-(\w'I'u*2/3)'\s-1o\s+1\*(#] .ds Th \*(#[\s+2I\s-2\h'-\w'I'u*3/5'\v'-.3m'o\v'.3m'\*(#] .ds ae a\h'-(\w'a'u*4/10)'e .ds Ae A\h'-(\w'A'u*4/10)'E . \" corrections for vroff .if v .ds ~ \\k:\h'-(\\n(.wu*9/10-\*(#H)'\s-2\u~\d\s+2\h'|\\n:u' .if v .ds ^ \\k:\h'-(\\n(.wu*10/11-\*(#H)'\v'-.4m'^\v'.4m'\h'|\\n:u' . \" for low resolution devices (crt and lpr) .if \n(.H>23 .if \n(.V>19 \ \{\ . ds : e . ds 8 ss . ds o a . ds d- d\h'-1'\(ga . ds D- D\h'-1'\(hy . ds th \o'bp' . ds Th \o'LP' . ds ae ae . ds Ae AE .\} .rm #[ #] #H #V #F C .\" ======================================================================== .\" .IX Title "KILLER 1" .TH KILLER 1 "2014-11-29" " " " " .\" For nroff, turn off justification. Always turn off hyphenation; it makes .\" way too many mistakes in technical documents. .if n .ad l .nh .SH "NAME" killer \- Background job killer .SH "SYNOPSIS" .IX Header "SYNOPSIS" killer [\fB\-h\fR] [\fB\-V\fR] [\fB\-n\fR] [\fB\-d\fR] .SH "DESCRIPTION" .IX Header "DESCRIPTION" \&\fIkiller\fR is a perl script that gets rid of background jobs. Background jobs are defined as processes that belong to users who are not currently logged into the machine. Jobs can be run in the background (and are expempt from \fIkiller\fR's acctions) if their scheduling priority has been reduced by increasing their \fInice\fR\|(1) value or if they are being run through \&\fIcondor\fR. For more details, see the \fI\s-1PACKAGE\s0 main\fR section of this document. .PP The following sections describe the \fIperl\fR\|(1) packages that make up the killer program. I don't expect that the version that works for me will work for everyone. I think that the ProcessTable and Terminals packages offer enough flexibility that most modifications can be done in the main package. .PP Command line options .IP "\-h" 4 .IX Item "-h" Tell me how to get help .IP "\-V" 4 .IX Item "-V" Display version number .IP "\-n" 4 .IX Item "-n" Do not kill, just print what would be killed .IP "\-d" 4 .IX Item "-d" Enable debug output .SH "PACKAGE ProcessTable" .IX Header "PACKAGE ProcessTable" Each ProcessTable object contains hashes (or associative arrays) that map various aspects of a job to the process \s-1ID\s0 (\s-1PID\s0). The following hashes are provided: .IP "pid2user" 12 .IX Item "pid2user" Login name associated with the effective \s-1UID\s0 that the process is running as. .IP "pid2ruser" 12 .IX Item "pid2ruser" Login name associate with the real \s-1UID\s0 that the process is running as. .IP "pid2uid" 12 .IX Item "pid2uid" Effective \s-1UID\s0 that the process is running as. .IP "pid2ruid" 12 .IX Item "pid2ruid" Real \s-1UID\s0 that the process is running as. .IP "pid2tty" 12 .IX Item "pid2tty" Terminal associated with the process. .IP "pid2ppid" 12 .IX Item "pid2ppid" Parent process of the process .IP "pid2nice" 12 .IX Item "pid2nice" \&\fInice\fR\|(1) value of the process. .IP "pid2comm" 12 .IX Item "pid2comm" Command name of the process. .PP Additionally, the \f(CW%remainingprocs\fR hash provides the list of processes that will be killed. .PP The intended use of this package calls for \fIreadProcessTable\fR to be called to fill in all of the hashes defined above. Then, processes that meet specific requirements are removed from the \f(CW%remainingprocs\fR hash. Those that are not removed are considered to be background processes and may be killed. .SS "new" .IX Subsection "new" This function creates a new \fIProcessTable\fR object. .PP Example: .PP .Vb 1 \& my $ptable = new ProcessTable; .Ve .SS "initialize" .IX Subsection "initialize" This function (re)initializes arrays and any environment variables for external commands. It generally will not need to be called, as it is invoked by \&\fInew()\fR. .PP Example: .PP .Vb 2 \& # Empty out the process table for reuse \& $ptable\->initialize(); .Ve .SS "readProcessTable" .IX Subsection "readProcessTable" This function executes the \fIps\fR\|(1) command to figure out which processes are running. Note that it requires a \s-1SYSV\s0 style \fIps\fR\|(1). .PP Example: .PP .Vb 2 \& # Get a list of processes from the OS \& $ptable\->readProcessTable(); .Ve .SS "cleanForkBombs" .IX Subsection "cleanForkBombs" This function looks for a large number of processes owned by one user, and assumes that it is someone that is using \fIfork()\fR for the first time. An effective way to clean up such a mess is to \*(L"kill \-STOP\*(R" each process then \&\*(L"kill \-KILL\*(R" each process. .PP Note this function ignores such mistakes by root. If root is running a \&\fIfork\fR\|(2) bomb, this script wouldn't run, right? Also, you should be sure that the number of processes mentioned below (490) is less (equal to would be better, right?) than the maximum number of processes per user. Also, the \s-1OS\s0 should have a process limit at least a couple hundred higher than any individual. Otherwise, you will have to use the power switch to get rid of fork bombs. .PP Each time a process is sent a signal, it is logged via syslog(3C). .PP Example: .PP .Vb 2 \& # Get rid of fork bombs. Keep track of who did it in @idiots. \& my @idiots = $ptable\->cleanForkBombs(); .Ve .SS "getUserProcessIds user" .IX Subsection "getUserProcessIds user" This returns the list of process \s-1ID\s0's where the login associated with the real \&\s-1UID\s0 of the process matches the argument to the function. .PP Example: .PP .Vb 2 \& # Find all processes owned by httpd \& my @webservers = $ptable\->getUserProcessIds(\*(Aqhttpd\*(Aq); .Ve .SS "getUniqueTtys" .IX Subsection "getUniqueTtys" This function returns a list of terminals in use. Note that the format will be the same as given by \fIps\fR\|(1), which will generally lack the leading \&\*(L"/dev/\*(R". .PP Example: .PP .Vb 2 \& # Get a list of all terminals that processes are attached to \& my @ttylist = $ptable\->getUniqueTtys(); .Ve .SS "removeProcessId pid" .IX Subsection "removeProcessId pid" This function removes pid from the list of processes to be killed. That is, it gets rid of a process that should be allowed to run. Most likely this will only be called by other functions in this package. .PP Example: .PP .Vb 2 \& # For some reason I know that PID 1234 should be allowed to run \& $ptable\->removeProcessId(1234); .Ve .SS "removeProcesses psfield, psvalue" .IX Subsection "removeProcesses psfield, psvalue" This function removes processes that possess certain traits. For example, if you want to get rid of all processes owned by the user \*(L"lp\*(R" or all processes that have /dev/console as their controlling terminal, this is the function for you. .PP psfield can be any of the following .IP "pid" 8 .IX Item "pid" Removes process id given in second argument. .IP "user" 8 .IX Item "user" Removes processes with effective \s-1UID\s0 associated with login name given in second argument. .IP "ruser" 8 .IX Item "ruser" Removes processes with real \s-1UID\s0 associated with login name given in second argument. .IP "uid" 8 .IX Item "uid" Removes processes with effective \s-1UID\s0 given in second argument. .IP "ruid" 8 .IX Item "ruid" Removes processes with real \s-1UID\s0 given in second argument. .IP "tty" 8 .IX Item "tty" Removes processes with controlling terminal given in second argument. Note that it should \s-1NOT\s0 start with \*(L"/dev/\*(R". .IP "ppid" 8 .IX Item "ppid" Removes children of process with \s-1PID\s0 given in second argument. .IP "nice" 8 .IX Item "nice" Removes children with a nice value equal to the second argument. .IP "comm" 8 .IX Item "comm" Removes children with a command name that is the same as the second argument. .PP Examples: .PP .Vb 2 \& # Allow all imapd processes to run \& $ptable\->removeProcesses(\*(Aqcomm\*(Aq, \*(Aqimapd\*(Aq); \& \& # Be sure not to kill print jobs \& $ptable\->removeProcesses(\*(Aqruser\*(Aq, \*(Aqlp\*(Aq); .Ve .SS "removeChildren pid" .IX Subsection "removeChildren pid" This function removes all decendents of the given pid. That is, if the pid argument is 1, it will ensure that nothing is killed. .PP Example: .PP .Vb 4 \& # Be sure not to kill off any mail deliveries (assumes you have \& # written getSendmailPid()). (Sendmail changes uid when it does \& # local delivery.) \& $ptable\->removeChildren(getSendmailPid); .Ve .SS "removeCondorChildren" .IX Subsection "removeCondorChildren" Condor is a batch job system that allows migration of jobs between machines (see http://www.cs.wisc.edu/condor/). This ensures that condor jobs are left alone. .PP Example: .PP .Vb 2 \& # Be nice to the people that are running their jobs through condor. \& $ptable\->removeCondorChildren(); .Ve .SS "findChildProcs pid" .IX Subsection "findChildProcs pid" This function finds and returns a list of all of the processess that are descendents of a the \s-1PID\s0 given in the first argument. .PP Example: .PP .Vb 2 \& # Find the processes that are decendents of PID 1234 \& my @procs = $ptable\->findChildProcs(1234); .Ve .SS "getTtys user" .IX Subsection "getTtys user" This function returns a list of tty's that are in use by processes owned by a particular user. .PP Example: .PP .Vb 2 \& # find all tty\*(Aqs in use by gerdts. \& my @ttylist = getTtys(\*(Aqgerdts\*(Aq); .Ve .SS "getUsers" .IX Subsection "getUsers" This function lists all the users that have active processes. .PP Example: .PP .Vb 2 \& # Get all users that are logged in \& my @lusers = $ptable\->getUsers() .Ve .SS "removeNiceJobs" .IX Subsection "removeNiceJobs" This function removes all jobs that have a nice value greater than 9. That is, they have a lower sceduling priority than the default (0). .PP Example: .PP .Vb 3 \& # Allow people to run background jobs so long as they yield to \& # those with "foreground" jobs \& $ptable\->removeNiceJobs(); .Ve .SS "printProcess filehandle, pid" .IX Subsection "printProcess filehandle, pid" This function displays information about the process, kinda like \*(L"ps | grep\*(R" would. .PP Example: .PP .Vb 2 \& # Print info about init to STDERR \& $ptable\->printProcess(\e*STDERR, 1); .Ve .SS "printProcessTable" .IX Subsection "printProcessTable" .SS "printProcessTable filehandle" .IX Subsection "printProcessTable filehandle" This function prints info about all the processes discoverd by \&\fIreadProcessTable\fR. If an argument is given, it should be a file handle to which the output should be printed. .PP Examples: .PP .Vb 2 \& # Print the process table to stdout \& $ptable\->printProcessTable(); \& \& # Mail the process table to someone \& open MAIL \*(Aq|/usr/bin/mail someone\*(Aq; \& $ptable\->printProcessTable(\e*MAIL); \& close(MAIL); .Ve .SS "printRemainingProcesses" .IX Subsection "printRemainingProcesses" .SS "printRemainingProcesses filehandle" .IX Subsection "printRemainingProcesses filehandle" This function prints info about all the processes discoverd by \&\fIreadProcessTable\fR, but not removed from \f(CW%remainingprocs\fR. If an argument is given, it should be a file handle to which the output should be printed. .PP Examples: .PP .Vb 2 \& # Print the jobs to be killed to stdout \& $ptable\->printRemainingProcesses(); \& \& # Mail the jobs to be killed to someone \& open MAIL \*(Aq|/usr/bin/mail someone\*(Aq; \& $ptable\->printRemainingProcesses(\e*MAIL); \& close(MAIL); .Ve .SS "getRemainingProcesses" .IX Subsection "getRemainingProcesses" Returns a list of processes that are likely background jobs. .PP Example: .PP .Vb 2 \& # Get a list of the processes that I plan to kill \& my @procsToKill = $ptable\->getRemainingProcesses(); .Ve .SS "killAll signalNumber" .IX Subsection "killAll signalNumber" Sends the specified signal to all the processes listed. A syslog entry is made for each signal sent. .PP Example: .PP .Vb 5 \& # Send all of the remaining processes a TERM signal, then a \& # KILL signal \& $ptable\->killAll(15); \& sleep(10); # Give them a bit of a chance to clean up \& $ptable\->killAll(9); .Ve .SH "PACKAGE Terminals" .IX Header "PACKAGE Terminals" The Terminals package provides a means for figuring out how long various users have been idle. .SS "new" .IX Subsection "new" This function is used to instantiate a new Terminals object. .PP Example: .PP .Vb 2 \& # Get a new Terminals object. \& my $term = new Terminals; .Ve .SS "initialize" .IX Subsection "initialize" This function figures out who is on the system and how long they have been idle for. It will generally only be called by \fInew()\fR. .PP Example: .PP .Vb 2 \& # Refresh the state of the terminals. \& $term\->initialize(); .Ve .SS "showConsoleUser" .IX Subsection "showConsoleUser" This function returns the login of the person that is physically sitting at the machine. .PP Example: .PP .Vb 2 \& # Print out the login of the person on the console \& printf "%s is on the console\en", $term\->showConsoleUser(); .Ve .SS "initializeTty terminal statparts" .IX Subsection "initializeTty terminal statparts" This initializes internal structures for the given terminal. .SS "getX11IdleTime user" .IX Subsection "getX11IdleTime user" Figure out how long a user has been idle in X11. Return the seconds of idle time. .SS "getIdleTime user" .IX Subsection "getIdleTime user" Figure out how long a user has been idle. This is accomplished by examining all terminals that the user owns and returns the amount of time since the most recently accessed one was used. Additionally, if the user is at the console it is possible that he/she is not typing, yet is quite active with the mouse or typing into an application that does not use a terminal. .PP Example: .PP .Vb 2 \& # Figure out how long the user on the console has been idle \& my $consoleIdle = $term\-getIdleTime($term\->showConsoleUser()); .Ve .SS "printEverything" .IX Subsection "printEverything" Prints to stdout who is on what terminal and how long they have been idle. Only useful for debugging. .PP Example: .PP .Vb 3 \& # Take a look at the contents of structures in my \& # Terminals object \& $term\->printEverything(); .Ve .SH "PACKAGE main" .IX Header "PACKAGE main" The main package is the version used on the Unix workstations at the University of Wisonsin's Computer-Aided Engineering Center (\s-1CAE\s0). I suspect that folks at places other than \s-1CAE\s0 will want to do things slightly differently. Feel free to take this as an example of how you can make effective use of the processTable and Terminals packages. .SS "Configuration options" .IX Subsection "Configuration options" .ie n .IP "$forkadmin" 12 .el .IP "\f(CW$forkadmin\fR" 12 .IX Item "$forkadmin" Email address to notify of fork bombs .ie n .IP "$killadmin" 12 .el .IP "\f(CW$killadmin\fR" 12 .IX Item "$killadmin" Email address to notify of run-of-the-mill kills .ie n .IP "$fromaddr" 12 .el .IP "\f(CW$fromaddr\fR" 12 .IX Item "$fromaddr" Who do email messages claim to be from? .ie n .IP "$stubbornadmin" 12 .el .IP "\f(CW$stubbornadmin\fR" 12 .IX Item "$stubbornadmin" Email address to notify when jobs will not die .ie n .IP "@validusers" 12 .el .IP "\f(CW@validusers\fR" 12 .IX Item "@validusers" These are the folks that you should never kill off .ie n .IP "$minuid" 12 .el .IP "\f(CW$minuid\fR" 12 .IX Item "$minuid" Do not kill processes of users with uid lower than this value. .ie n .IP "$maxidletime" 12 .el .IP "\f(CW$maxidletime\fR" 12 .IX Item "$maxidletime" The maximum number of seconds that a user can be idle without being classified as having \*(L"background\*(R" jobs. .PP If I am a user really trying to avoid a background job killer, I would likely include a signal handler that would wait for signal 15. When I saw it, I would fork causing the parent to die and the child would continue on to do my work. .PP Assuming that everyone thinks like me, I figure that I will need to make at least two complete passes to clear up the bad users. The first pass is relatively nice (sends a signal 15, followed a bit later by a signal 9). A well-written program will take the signal 15 as a sign that it should clean up and then shut down. When a process gets a signal 9, it has no choice but to die. .PP The second pass is not so nice. It finds all background processes, sends them a signal 23 (\s-1SIGSTOP\s0), then a signal 9 (\s-1SIGKILL\s0). This pretty much (but not absolutely) guarantees that processes are unable to find a way around the background job killer. .SS "gatherInfo" .IX Subsection "gatherInfo" This function gathers information from the Terminals and ProcessTable packages, then based on that information decides which jobs should be allowed to run. Specifically it does the following: .IP "\(bu" 2 Instantiates new ProcessTable and Terminals objects. Note that Terminals::new fills in all the necessary structures to catch users that have logged in between calls to \fIgatherinfo\fR. .IP "\(bu" 2 Reads the process table .IP "\(bu" 2 Removes condor processes and condor jobs from the list of processes to be killed. .IP "\(bu" 2 Removes all jobs belonging to all users in the configuration array \&\f(CW@validusers\fR from the list of processes to be killed. .IP "\(bu" 2 Removes all \fInice\fR\|(1) jobs from the list of jobs to be killed. .IP "\(bu" 2 Removes all jobs belonging to users where the user has less than \&\f(CW$maxidletime\fR idle time on at least one terminal. Additionally, jobs associated with ttys that are owned by users that have less than \&\f(CW$maxidletime\fR idle time on at least one terminal are preserved. This makes it so that if luser uses \fIsu\fR\|(1) to gain the privileges of boozer, processes owned by boozer will not be killed. .IP "\(bu" 2 Removes all processes of users with uid lower than the \f(CW$minuid\fR value. .IP "\(bu" 2 Finally, the process table and terminal objects are returned. .SH "BUGS" .IX Header "BUGS" There is a small window of opportunity for a user that reaches \f(CW$maxidletime\fR in the middle of this script to get unfair treatment. This could probably be reconciled by shaving some time off of maxidletime for the second call to main::gatherInfo. .PP It is still possible to get around the background job killer by having a lot of proceses that watch each other to be sure that they are still responding (have not yet gotten a signal 23). As soon as a stopped process is found, the still running process could \fIfork()\fR, thus leaving a background process that is not going to be killed. .PP Different operating systems have different notions of nice values. Some go from \-20 to +19. Some go from 0 to 39. Solaris and HP-UX (using System V ps command) report nice values between 0 and 39. .PP It is bad to assume that all systems that run this have the same number of processes per user. The script should ask the \s-1OS\s0 how many processes normal (non-root) users can run. .SH "TODO" .IX Header "TODO" The configuration is quite minimalistic. It should be made possible to have per-host configuration directives so that you can, for instance, allow certain people to run background jobs on certain hosts. .PP People that really care about finding habitual offenders will probably want to have a way to add entries to a database and flag those that pop up too often. .PP Thoroughly test on more operating systems. A very close relative of this code has performed well on about 60 Solaris 2.5.1 machines. It has been lightly tested on HP-UX 10.20 as well. .PP Make mailing to someone optional. If you have a lot of workstations killing off boring stuff all the time, too much meaningless mail traffic is generated. .PP If you plan to run this on a machine that runs special processes like a \s-1POP\s0 or \s-1IMAP\s0 server, it would be handy to be able to check multiple conditions easily. Perhaps .PP .Vb 3 \& $ptable\->removeProcesses( { comm => \*(Aqimapd\*(Aq, \& parentComm => \*(Aqinetd\*(Aq, \& parentUser => \*(Aqroot\*(Aq } ); .Ve .PP This would make it so that people don't rename the crack binary imapd to escape the wrath of killer. .SH "LICENSE" .IX Header "LICENSE" This program is released under the terms of the General Public License (\s-1GPL\s0) version 2. The the file \s-1COPYING\s0 with the distribution. If you have lost your copy, you can get a new one at http://www.gnu.org/copyleft/gpl.html. In particular remember that this code is distributed for free without warranty. .PP If you make use of this code, please send me some email. While I am open to suggestions to improvement, I by no means guarantee that I will implement them. .SH "SEE ALSO" .IX Header "SEE ALSO" \&\fInice\fR\|(1) \fIperl\fR\|(1) \fIps\fR\|(1) \fIsu\fR\|(1) \fIwho\fR\|(1) \fIfork\fR\|(2) \fIsignal\fR\|(5) .PP http://www.cs.wisc.edu/condor/ .PP http://www.cae.wisc.edu/~gerdts/killer/ .SH "AUTHOR" .IX Header "AUTHOR" killer was written by Mike Gerdts, gerdts@cae.wisc.edu.