Home > PHP > Tutorial: Cross-platform background process forking without extensions in PHP

Tutorial: Cross-platform background process forking without extensions in PHP

February 9, 2007 Leave a comment Go to comments

There are plenty of ways to fork new processes in PHP. You can use shell functions like exec() or shell(), the pcntl_* function set in the Process Control extension or stream/filesystem functions such as popen(). In this article, we’re going to look at the latter of these, and learn:

  • How to make it work on both Windows and Linux
  • How to make the forked process run in the background
  • How to communicate with the forked process
  • How to fork more than one process
  • How to cleanly terminate the forked process from the parent script

Why would we want to fork processes from PHP anyway?

The most common reason is because we want to get results from an external application and use them in the main script. We may also want to run a process that performs some action that is awkward in PHP, for example creating tarballs in backup scripts or modifying system configuration in maintenance scripts. You may want to create worker threads which do a long-running task and periodically report back their status. Finally, we may want to run processes periodically on a timer (see my PHPCron article for details on that).

Some things are better served by using threads due to the reduced overhead and lack of need to communicate between processes, but threading support in PHP is quite disastrous so forking a new process is a bit more bulletproof.

Forking a new process

You can fork a new process like this:

function startFork($path, $file, $args)
{
  chdir($path);
  if (substr(PHP_OS, 0, 3) == 'WIN')
    $proc = popen('start /b php "' . $path . '\\' . $file . '" ' . $args, 'r');
  else
    $proc = popen('php ' . $path . '/' . $file . ' ' . $args . ' &', 'r');
return $proc;
}

This particular example assumes you’re running another PHP script; if you’re not, just remove the references to 'php ' from the code above. Note that changing the current directory with chdir() might have unpredictable consequences, so only do so if necessary, or change the directory back afterwards.

In Windows, start /b forks a new process without opening a new shell window, and makes it run in the background. In UNIX, appending the call with & also makes the process run in the background. The appropriate command is generated depending which platform PHP is running on by examining the PHP_OS constant.

Receiving data from the process

popen() returns a stream resource which can be manipulated just like a regular file. As the process outputs data, you can retrieve it as follows:

while (!feof($proc)) {
  $data = fgets($proc);
  // Do something with the data
}

The ‘end of the file’ will never be reached until the process stops running, so the while loop will merely stall until data is received. fgets() will stall until a newline is read, thereby retrieving one line of data at a time. You can use other stream reading functions instead if you want different behaviour.

If you’re writing the child process yourself, you can make life easier by designing it to output data in a machine-readable format that can be easily parsed by PHP with functions like split() or regular expression functions.

When the process ends, call:

pclose($proc);

to close the stream.

Notice that if an error occurs, popen() will still return a resource handle so that the error text can be read. Make sure you check for this when parsing the incoming data.

Under UNIX, if the errors are sent to stderr, append 2>&1 to the line which forks the process to cause stderr to be redirected to stdout.

Running a background process and ignoring its output

If you don’t care what happens to the child process, you can simply use the following line to invoke it:

pclose(startFork($path, $file, $args));

and the output will be discarded. The child process will run in the background without stalling the parent script.

Terminating a forked process from the parent script

There are various ways to do this but a simple one is to just create a so-called “kill file” that is monitored by the child process and triggers it to quit when found.

In the parent process:

$killFile = dirname(__FILE__) . '/child.die';
...
function stopFork()
{
  global $proc, $killFile;
  touch($killFile);
  sleep(2);
  pclose($proc);
}

In the child process:

$killFile = dirname(__FILE__) . '/child.die';
while (!file_exists($killFile)) {
  // Do worker task and output status
}
while (file_exists($killFile)) {
  @unlink($killFile);
  sleep(1);
  clearstatcache();
}

The child process runs its repetitive task in the first while loop, continuing as long as the kill file hasn’t been created. Once it has been created, it moves to the second while loop which keeps trying to delete it until it succeeds, then the child process exits (make sure your directory permissions are set correctly to avoid an infinite loop).

When stopFork() is called in the parent process, the kill file is created with touch(), a short pause is made and then the process handle is closed. Not introducing a pause before closing the stream can cause the kill file not to be deleted and the child process to hang.

The above code assumes that the parent and child are both running in the same directory. If this is not the case, you will need to alter the definition of $killFile appropriately.

Ensuring child processes are always killed when the parent script ends

It’s vital that child processes which act as worker threads aren’t left hanging around after the main script has finished executing. If you leave orphan processes running, they may behave undesirably, create memory leaks, and on Windows if you call the CLI version of PHP the prompt will become unusable. To avoid this, attach stopFork() to the shutdown handler as follows:

register_shutdown_function('stopFork');

This will make sure that – however the main script is ended – the child processes are always killed off first.

Forking more than one process

This is as simple as repeating the above steps for as many processes as you need; however, if you are using the kill file technique to signal the child processes to end, you need to use a different kill file for each process. If you are forking the same process multiple times (multiple identical worker threads), you will want to pass the name of the kill file for each as an argument to the child process, eg:

startFork($path, $file, '/path/to/killfile1 ' . $args);
startFork($path, $file, '/path/to/killfile2 ' . $args);

In the child process, retrieve the name of the kill file to monitor as follows:

$killFile = $argv[1];

Usage example

I use the techniques above to measure the bitrate of several streaming audio servers. The rate of incoming data must be measured per second for several servers and therefore this requires background worker threads (or processes in this case). The background worker is another PHP script which is instanced once for each server to measure.

The child processes return one line of data every second in a format like:

X Y Z

where X is a count of the amount of data received in the last second, and Y and Z are moving averages. The intervals of each moving average to measure are passed as parameters to the child script. To parse the data in the parent is easy:

$movingAverages = split(' ', fgets($proc));
$lastSecond = array_shift($movingAverages);

This leaves X in $lastSecond and all the remaining numbers in the $movingAverages array. This is a perfect of example of why it’s important to format your child process output as something that is easily parsable by the parent script.

I hope you found this article useful. Please leave any comments below!

Advertisements
  1. No comments yet.
  1. No trackbacks yet.

Share your thoughts! Note: to post source code, enclose it in [code lang=...] [/code] tags. Valid values for 'lang' are cpp, csharp, xml, javascript, php etc. To post compiler errors or other text that is best read monospaced, use 'text' as the value for lang.

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: