Search

Fork and Exec with Perl

0 views

Building a Perl Daemon Launcher with Fork and Exec

When a system needs to keep a handful of scripts running continuously, but also requires the flexibility to restart them with new options, a lightweight launcher can do the trick. In this guide you’ll learn how to build a small Perl program that reads a configuration file, starts and stops child processes with fork and exec, monitors their status, and records a log for each job. The design mirrors the Unix philosophy of “do one thing well and let it talk to other programs.”

At its core the launcher has three responsibilities:

  1. Parse a configuration file that lists the jobs and their parameters.
  2. Spawn a child process for each job when the user asks, passing the parameters straight to the executable.
  3. Keep track of the process identifiers, stop processes on demand, and display the current state of each job.

    The sample configuration file looks like this:

    Prompt
    [NWFTP]</p> <p>mainprog=ftpget.pl</p> <p>datasource=ftpssite.com/pub</p> <p>hours=*</p> <p>timing=5,35</p> <p>conversion=toscv</p> <p>outputname=kansas</p> <p>mylog=ftpgetlog</p> <p>[PFIELD]</p> <p>mainprog=httpget.pl</p> <p>datasource=http://xyz.com?dd=foo</p> <p>hours=0,4,8,12,16,20,24</p> <p>timing=4</p> <p>conversion=none</p> <p>outputname=fran</p> <p>mylog=pfieldlog</p>

    Each bracketed section names a job; the key/value pairs under that name define the command to run and the arguments that will be forwarded to it. The launcher reads this file into two data structures: a hash of job names to zero (placeholder for the PID) and a hash keyed by “job|parameter” that holds the value.

    The main loop prints a menu showing every job and whether it is currently running. It then waits for user input. If the user selects a job and presses “S” the program calls a routine that forks a child and execs the desired script. If “K” is pressed, the launcher sends a SIGTERM to the job’s PID, falls back to SIGKILL if the process does not die, and logs the action. Pressing “L” shows the log file for that job.

    Because the launcher never needs to know the exit status of its children, it sets $SIG{CHLD} = 'IGNORE'. This prevents zombie processes: the kernel cleans up the child’s resources automatically.

    When the launcher forks, the parent receives the child’s PID while the child receives 0. In the child we build an array of arguments: the job name followed by “key=value” pairs from the configuration. Then we call exec $prog, @args. If exec succeeds, the child image is replaced by the target script and control never returns to the launcher. If it fails - perhaps the target file is missing or the name is misspelled - the child writes a message to the log, exits, and the launcher remains safe. Forgetting to exit in this case can leave two copies of the menu running and cause confusing double output.

    A robust fork routine also deals with temporary “resource temporarily unavailable” errors. If the system is busy, fork may return undef and set $! to EAGAIN. The routine sleeps for a few seconds and retries, looping until a child is successfully created or a permanent error occurs.

    Below is the complete Perl script. It is intentionally minimal; real deployments would benefit from configuration validation, signal handling for graceful shutdown, and optional use of perl -w for debugging. The comments in the code explain each step.

    Prompt
    #!/usr/bin/perl</p> <p>use strict;</p> <p>use warnings;</p> <p>use Errno qw(ESRCH EPERM);</p> <p>use POSIX qw(strftime);</p> <p>$SIG{CHLD} = 'IGNORE';</p> <h1>Load the configuration file</h1> my $config_file = 'config';</p> <p>open my $fh, ' <p>my (%jobs, %settings, $current_job);</p> <p>while (my $line = ) {</p> <p> chomp $line;</p> <p> next if $line =~ /^\s*$/; # skip blanks</p> <p> if ($line =~ /^\[(\w+)\]$/) { # new job section</p> <p> $current_job = $1;</p> <p> $jobs{$current_job} = 0; # placeholder for PID</p> <p> next;</p> <p> }</p> <p> if ($line =~ /^(\w+)\s<em>=\s</em>(.*)$/) { # key/value pair</p> <p> my ($key, $value) = ($1, $2);</p> <p> $settings{"$current_job|$key"} = $value;</p> <p> }</p> <p>}</p> <p>close $fh;</p> <h1>Main interactive loop</h1> while (1) {</p> <p> system 'clear';</p> <p> print "Choose a job: ";</p> <p> my $index = 1;</p> <p> foreach my $job (sort keys %jobs) {</p> <p> # Verify if the stored PID is still alive</p> <p> my $pid = $jobs{$job};</p> <p> my $alive = $pid && kill 0, $pid;</p> <p> $jobs{$job} = 0 unless $alive;</p> <p> my $status = $alive ? "[running ($pid)]" : "[not running]";</p> <p> printf "%2d) %-10s %s ", $index++, $job, $status;</p> <p> }</p> <p> print " Enter number (or q to quit): ";</p> <p> my $choice = <STDIN>;</p> <p> last if defined $choice && $choice =~ /^\s*q/i;</p> <p> chomp $choice;</p> <p> next unless $choice =~ /^\d+$/ && $choice >= 1 && $choice <p> my $selected_job = (sort keys %jobs)[$choice-1];</p> <p> job_menu($selected_job);</p> <p>}</p> <h1>Subroutine that presents a submenu for the selected job</h1> sub job_menu {</p> <p> my ($job) = @_;</p> <p> print " ---- $job - ";</p> <p> foreach my $k (sort keys %settings) {</p> <p> my ($j, $param) = split /\|/, $k, 2;</p> <p> next unless $j eq $job;</p> <p> printf "%s: %s ", $param, $settings{$k};</p> <p> }</p> <p> my $pid = $jobs{$job};</p> <p> my $status = $pid ? "[running ($pid)]" : "[not running]";</p> <p> print " $status ";</p> <p> print "Options: (S)tart (K)ill (L)og (B)ack ";</p> <p> print "Choose: ";</p> <p> my $action = <STDIN>;</p> <p> chomp $action;</p> <p> if ($action =~ /^\s*s$/i) {</p> <p> start_job($job);</p> <p> } elsif ($action =~ /^\s*k$/i) {</p> <p> kill_job($job);</p> <p> } elsif ($action =~ /^\s*l$/i) {</p> <p> show_log($job);</p> <p> }</p> <p>}</p> <h1>Start a job</h1> sub start_job {</p> <p> my ($job) = @_;</p> <p> print "Starting $job... ";</p> <p> my $pid = fork_job($job);</p> <p> if ($pid) {</p> <p> $jobs{$job} = $pid;</p> <p> # Give the child a moment to fail if it will</p> <p> sleep 2;</p> <p> unless (kill 0, $pid) {</p> <p> print "Failed to start $job. ";</p> <p> $jobs{$job} = 0;</p> <p> }</p> <p> } else {</p> <p> # In child: the fork_job routine does everything</p> <p> }</p> <p>}</p> <h1>Kill a running job</h1> sub kill_job {</p> <p> my ($job) = @_;</p> <p> return unless $pid;</p> <p> my $result = kill 1, $pid; # SIGTERM</p> <p> $result ||= kill 9, $pid; # fallback to SIGKILL</p> <p> my $alive = kill 0, $pid;</p> <p> $jobs{$job} = 0 unless $alive;</p> <p> my $logfile = $settings{"$job|mylog"};</p> <p> open my $log, '>>', $logfile or warn "Can't write to $logfile: $!";</p> <p> my $now = strftime("%Y-%m-%d %H:%M:%S", localtime);</p> <p> print $log $alive ? "killed $job $pid $now "</p> <p> : "couldn't kill $job $pid $now ";</p> <p> close $log;</p> <p> print $alive ? "Job $job stopped. " : "Job $job not running. ";</p> <p>}</p> <h1>Show a job's log file</h1> sub show_log {</p> <p> my ($job) = @_;</p> <p> open my $log, ' <p> while (my $line = ) {</p> <p> print $line;</p> <p> }</p> <p> close $log;</p> <p> print " Press Enter to return. ";</p><STDIN>;</p> <p>}</p> <h1>Fork a child and exec the target script</h1> sub fork_job {</p> <p> my ($job) = @_;</p> <p> while (1) {</p> <p> my $pid = fork();</p> <p> if (defined $pid) { # parent</p> <p> return $pid;</p> <p> } elsif ($! == EAGAIN) { # retry if temporary failure</p> <p> sleep 3;</p> <p> next;</p> <p> } else { # permanent error</p> <p> warn "Failed to fork for $job: $! ";</p> <p> return 0;</p> <p> }</p> <p> # Child</p> <p> my $prog = $settings{"$job|mainprog"};</p> <p> my @args = ($job);</p> <p> foreach my $k (sort keys %settings) {</p> <p> my ($j, $param) = split /\|/, $k, 2;</p> <p> next unless $j eq $job;</p> <p> push @args, "$param=$settings{$k}";</p> <p> }</p> <p> open my $log, '>>', $settings{"$job|mylog"} or warn "Can't write to log: $!";</p> <p> print $log "Running $prog with args: @args ";</p> <p> close $log;</p> <p> exec $prog, @args;</p> <p> # If exec fails, log and exit</p> <p> open my $log_fail, '>>', $settings{"$job|mylog"} or warn "Can't write to log: $!";</p> <p> print $log_fail "Failed to exec $prog for $job: $! ";</p> <p> close $log_fail;</p> <p> exit 0;</p> <p> }</p> <p>}</p>

    This launcher gives you a full‑featured way to manage short‑lived scripts without installing heavyweight daemon supervisors. By keeping the code in a single, readable Perl file you retain control over how jobs are started, stopped, and logged. The use of fork and exec keeps resource usage low, while $SIG{CHLD} = 'IGNORE' protects the system from zombie processes. Feel free to extend the script with features such as non‑blocking input, periodic status refreshes, or automatic restarts when a job dies unexpectedly.

Suggest a Correction

Found an error or have a suggestion? Let us know and we'll review it.

Share this article

Comments (0)

Please sign in to leave a comment.

No comments yet. Be the first to comment!

Related Articles