![]() |
|
URL of the article:
Issue:
01.2004
Sticking the Fork In
PHP is not the best tool for every job - this is one of those times.
Davey Shafik
Whilst PHP is not the best tool for creating daemons, it does come fully loaded with the ability to fork, share memory, manage child processes etc. through the PCNTL, POSIX, Socket, SHMOP and System V Semaphore (sysvsem) extensions.
I realise that I've just thrown five exotic extensions at most of you, but we're going to walk through them one by one just to familiarise you with each of them, what they are capable of, and what we're going to use them for. Installation Installation of these extensions from source is done using the following configure options:
$ make $ make test $ make install PCNTL - Process CoNTroL Functions PCNTL is the extension which provides the functions we need to be able to fork our daemon process. On top of this pcntl_fork() function provides us with the functions to keep your daemon in check, handle signals and child processes. I must stress do not use these functions outside of CLI/CGI SAPI (see note). POSIX - Portable Operating System Interface for Computer Environments The POSIX extension allows you and your daemon to interface with the underlying operating system and other processes/children. Sockets - External Communication Sockets allow our applications to communicate via network or UNIX domain sockets. PHP has the ability to function fully as a client and/or server over sockets. SHMOP and SYSVSEM - Shared Memory Extensions The SHMOP and SYSVSEM extensions provide the same functionality - shared memory - albeit using different methods for doing so. Shared memory allows us to have variables that exist between parent and child processes, and between sibling processes of our daemon. The use of shared memory means that communication within your daemon is a cinch, once you get used to the idea.SAPI Stands for Server Application Program Interface. There are many SAPIs for PHP, these include the Apache Module, CGI (Common Gateway Interface) and CLI (Command Line Interface). A SAPI provides the interface between your script and the PHP parser itself. For the extensions in this article, you must only use the CLI SAPI. Although the CGI SAPI will work for most of the times, too, it is designed to be run with a webserver which is not the case in this article. The use of other SAPIs such as the Apache module will cause major mishaps when using the PCNTL extension. Fork Off Forking is the process of starting a second instance of our application that can then go off in a separate programmatic direction if we wish it to do so. When we fork, both processes are identical except that the return value for our parent (original) is seen as the PID (Process ID) for the child (copy), and in the child the return value is seen as 0 (zero). A -1 (negative one) is returned to the parent on failure. Listing 1 contains a simple example of using pcntl_fork(). Listing 1 <?php /* We need this to run as a daemon, otherwise we <br></br> timeout */ set_time_limit(0); /* Fork our process */ $pid = pcntl_fork(); if($pid == -1) { // Something went wrong echo "An Error Occured trying to fork..<br></br> Exiting\n"; exit; } elseif($pid == 0) { /* We're in the child process, here is where <br></br> we can affect the flow of our child process */ echo "Hi, I'm the child process! I will exit.<br></br> now!\n"; exit; } else { /* We're in the parent process, we can now <br></br> carry on without affecting the child in any <br></br> way whatsoever */ echo "Hi, I'm the parent process! I hope you.<br></br> enjoyed my child with the PID $pid I.<br></br> will exit now!\n"; exit; } // Stop our example becoming a run away process exit; ?> Listing 1 illustrates how to fork, and how to tell our different processes apart. It is this simple process that allows us to create our daemons. For our daemon to actually do anything useful, it needs to have something to trigger it. Common triggers include:
As mentioned earlier, sockets give us the ability of external communication from our daemon. By adding sockets to our simple script in Listing 1, we suddenly have the bare bones of a very powerful daemon. In Listing 2, we encounter quite a few new things, the first being socket_create_listen() - this is the backbone of our entire daemon. As its only required argument is the port to which the script should bind. Unless you are running the daemon with root privileges, this port should be above 1024, I usually do an mt_rand(1024,9999) to come up with a port for me (just the once!), I find this better than choosing one as I invariably stamp on some other daemons foot. Once we have our daemon bound to our port (in this case, port 4297) we enter into an endless loop, something we usually try to avoid doing. By doing this, we need to be absolutely sure that any variables are cleared up after being used, otherwise we eventually hit the memory limit. Within our endless loop the script sits and waits for a connection, this is done using socket_accept(). Once we have our connection, we fork the script and in our parent, we destroy our connection handler and end the if. Within our child process, we output a welcome message using socket_write(); and start to read our data using socket_read();It should be noted that the client should send a new line character after the data for it to actually be sent to the daemon (both windows \r\n and *nix \n work). Finally, we echo the data, and kill the process stating the child's PID using posix_getpid(). Listing 2 <?php set_time_limit(0); /* Create our listening socket */ $sh = socket_create_listen(4297); /* Make our process run endlessly */ while(TRUE) { /* Sit and wait for a connection */ while($ch = socket_accept($sh)) { /* We have our connection, lets fork so it can be dealt with in a child and we can carry on listening. */ $pid = pcntl_fork(); if($pid != 0) { /* If we're not in the child process Kill the processes connection to $ch */ socket_close($ch); break 1; } else { /* Write a Welcome message to the connecting client */ socket_write($ch,"Hello and " . "Welcome!\n"); $data = NULL; while($temp = socket_read($ch,1024)) { $data .= $temp; } echo $data; die('Data Recieved on child ' . posix_getpid(). "\n"); } } } exit; ?> To test this code, we simply run it from command line using the PHP binary and using a telnet client we connect to example.com on port 4297. We can then send some text, press enter to send it and close the client to tell the daemon we're done. The text box Daemon testing - sequential output shows you the input and output of my testing shown sequentially, as it happened. I have marked server output with a > and client input with a < to make it clear. Daemon testing - sequential output
Daemon testing
What if you want your processes to communicate with each other? Or your children to communicate with the parent? Well, here Shared Memory enters the stage. For shared memory we have two choices, SHMOP and SYSVSEM which provide the same thing in different ways. How do we chose which one to use? Personally, I recommend SHMOP because it doesn't use the system Semaphores. Sempahores are a kernel resource and are usually limited, using these for non-trivial things such as this leaves a dirty taste in my mouth. SHMOP does not use semaphores and as such this seems like a nicer solution to me. Both solutions offer shared memory which is accessable by other programs, not just PHP. When I originally delved into SHMOP, the most perplexing question I had was `what exactly should the key be?'. With the help of others and the man command I was finally able to deduce that the standard is to use ftok(). The ftok man page decribes ftok as the following: ftok - create IPC identifier from path name. This is indeed what the PHP ftok command does. You will see the use of ftok in Listing 3. Listing 3 shows the communication between two child processes using SHMOP shared memory. In this example, all that we do is notify all children of connecting clients. Listing 3 <?php set_time_limit(0); $sh = socket_create_listen(4297); $key = ftok(__FILE__,'d'); $shm = shmop_open($key,"c", 0644, 1024); shmop_write($shm, 0, 0); shmop_close($shm); while(TRUE) { while($ch = socket_accept($sh)) { $pid = pcntl_fork(); if($pid != 0) { $key = ftok(__FILE__,'d'); $shm = shmop_open($key,"c", 0644, 1024); $children = shmop_read($shm, 0, 1024); $children += 1; shmop_write($shm, $children, 0); shmop_close($shm); socket_close($ch); break 1; } else { $key = ftok(__FILE__,'d'); $shm = shmop_open($key,"c", 0644, 1024); $children = shmop_read($shm, 0, 1024); socket_write($ch, "There is currently" . "(approx) " . $children. " other" . "clients connected\n"); while($children_temp = shmop_read($shm, 0, 1024)) { if($children < $children_temp) { $children = $children_temp; if($children_temp != 1) { socket_write($ch, "Another client has " . "connected!\n"); } else { socket_write($ch, "You are now " . "successfully " . "connected!"); } } } } } } exit; ?> The code in Listing 3 cannot do anything except keep the children informed about new connections - not very useful. You may also notice that until the daemon re-starts the count never decreases, this is because we are doing nothing to detect a client disconnect or when a child process dies. The reason for this is that the connections are monitored using an endless loop, we have two ways to allow this to execute and still allow other events to happen such as client-server communication and peer communication. The first of these is using the while($temp = socket_read($ch,1024)) { from Listing 2 and placing the monitoring inside there, however you can only check/send notices when input is being sent. The second is to use Ticks to place the client monitoring into the `background' leaving the socket_read() in the `foreground'. Portability at its best. *nix Only. POSIX is The Portable Operating System Interface. It is supposed to ensure cross-platform compatibility, however, the POSIX extension for PHP is *NIX only, so we won't touch Windows here. The POSIX extension for PHP allows us to control our process and its children. With POSIX we can, for example, setup a SUEXEC type system, or send KILL signals to child processes and such like. Listing 7 shows a complete daemon which will run as the user nobody if executed by root and will die gracefully by removing all children before dying. The daemon allows the user admin to login with the password pass and issue an exit or disconnect command. Listing 4 <?php function handle_signal($signo) { switch ($signo) { case SIGTERM: if ($GLOBALS['parent'] === 0) { // we are the parent echo "[parent] Caught SIGTERM: " . "Closing down\n"; var_dump($GLOBALS['children']); foreach($GLOBALS['children'] as$key=>$pid) { echo $key . "\n"; posix_kill($key, SIGKILL); $child = pcntl_waitpid(0,$status); if ($child > 0) { echo "[parent] Cleaning up Child - " . $child . "\n"; unset($GLOBALS['children'][$child]); } } socket_close($GLOBALS['listen']); exit; } else { // we are a child echo "[child] Caught SIGTERM: Child Proccess . "should not recieve SIGTERM... \n"; exit; } break; default: echo "Uncaught signal: $signo\n"; break; } } ?> Listing 4 shows our daemons signal handler function, this function captures all signals such as SIGTERM, SIGHUP and is where we do our daemon cleanup for restarts and upon exit. Listing 5 <?php function child_ops($ch) { while($data = socket_read($ch,1024)) { $GLOBALS['buffer'] .= $data; if (strpos($GLOBALS['buffer'], "\n")) { /* new command on the buffer split off and process */ list($data, $GLOBALS['buffer']) = split("\n", $GLOBALS['buffer'], 2); switch (rtrim(strtolower($data))) { case "login": echo "[child: " . $GLOBALS['me'] . "] " . "User Login initiated\n"; $login = TRUE; socket_write($ch,"\nUsername: "); break; case "admin": if ($login == TRUE) { echo "[child: " . $GLOBALS['me'] . "] User" . "'admin' attempting to login\n"; $user = TRUE; socket_write($ch,"\nPassword: "); } break; case "pass": if ($user == TRUE) { echo "[child: " . $GLOBALS['me'] . "] . "Password for user 'admin' correct\n"; $pass = TRUE; socket_write($ch,"\nLogged In" . "Successfully!\n"); } break; case "exit": if ($pass == TRUE) { echo "[child: " . $GLOBALS['me'] . "] Kill . "command given by user 'admin'\n"; if(!posix_kill($GLOBALS['parent'], SIGTERM)) { echo "Could not send kill signal\n"; } break 2; } break; case "disconnect": case "dc"; socket_write($ch,"\nThank you, COME" . "again!\n"); break 2; } } } socket_close($ch); } ?> The child_ops() function in Listing 5 depicts the crux of our daemon, where all commands sent to our daemon are handled, here is where our user is authenticated and is then able to issue the exit or the disconnect command. Listing 6 <?php function become_nobody() { $user = posix_getpwuid(posix_getuid()); if(strtolower($user['name']) == 'root') { $nobody = posix_getpwnam('nobody'); posix_setuid($nobody['uid']); posix_setgid($nobody['gid']); if (posix_getuid() == $nobody['uid']) { return TRUE; } else { return FALSE; } } else { return TRUE; } } ?> Listing 6 shows our simple function for making sure that our daemon runs as an unprivileged user, namely nobody. By checking that the user is currently root and then finding out the relevant information for the system's nobody user we don't have to worry that the UID or GID may change across different distros. We then simply set the processes UID and GID using posix_setuid() and posix_setgid() respectively. Listing 7 <?php /* Don't forget we need the functions from the previous listings. You will find the full script on the CD */ set_time_limit(0); $GLOBALS['parent'] = 0; $GLOBALS['children'] = array(); declare(ticks=1); pcntl_signal(SIGTERM,"handle_signal"); if (!become_nobody()) { echo "Failed to become nobody, something's " . "wrong\n"; exit; } if (!$GLOBALS['listen'] =& socket_create_listen(4297)) { echo "Failed to create Listen Socket\n"; exit; } else { echo "Listening on port: 4297\n"; } while(TRUE) { if (socket_select($read = array($GLOBALS['listen']), $set_w = NULL, $set_e = NULL, 0, 0) > 0) { if ($read[0] == $GLOBALS['listen']) { echo "[parent] Incomming Connection\n"; echo "[parent] Forking Process\n"; $ch = socket_accept($GLOBALS['listen']); $pid = pcntl_fork(); if ($pid == -1) { echo "Failed to fork"; } else if ($pid) { // we are the parent $GLOBALS['children'][$pid] = true; socket_close($ch); } else { socket_close($GLOBALS['listen']); $GLOBALS['parent'] = posix_getppid(); $GLOBALS['me'] = posix_getpid(); child_ops($ch); echo "Killing Child Process..."; die(" OK\n"); } } } $child = pcntl_waitpid(0,$status,WNOHANG); if ($child > 0) { echo "[parent] Cleaning Up Child - " . $child ."\n"; unset($GLOBAL['children'][$child]); } } ?> Using Listing 7 you can create any thing you want, a full blown HTTPd, a simple RPC server, the world's your oyster. However, there's one simple thing I have yet to mention, if only because when testing it's not a wise thing to do .... Can you see my Backside in this Process? As you may have guessed, we are about to learn the most essential part of making a daemon - placing it into the background. There are obvious dangers to this - you cannot simply press ctrl+c and stop any mishaps, and you also cannot debug so easily. Listing 8 shows you the small snippet of code needed to place your daemon into the background. I hope you're surprised by its simplicity. Listing 8 <?php set_time_limit(0); // State our activity echo "Sending Process (PID: " .posix_getpid() . ") into background..."; // Immediately try to fork $pid = pcntl_fork(); if($pid == -1) { // Unable to Fork, exit die(" Failed\n"); } elseif ($pid != 0) { // Forked successfully, // parent no longer needed, exit die(" OK"); } /* This is now only executed in the child in the background */ echo " (PID: " .posix_getpid(). ")\n"; $sh = socket_create_listen(4297); while(TRUE) { while($ch = socket_accept($sh)) { $pid = pcntl_fork(); if($pid != 0) { socket_close($ch); break 1; } else { socket_write($ch,"Hello and Welcome!\n"); $data = NULL; while($temp = socket_read($ch,1024)) { $data .= $temp; } echo $data; die('Data Recieved on child ' .posix_getpid(). "\n"); } } } exit; ?> The Never-ending Process Unlike our daemons, this article is not never-ending, and indeed we have reached the end of it. I hope that by now you have seen that PHP does indeed provide all the tools needed to create fully featured daemons, as well as having learnt the basics of some of the more obscure PHP extensions. As a final note, I would like to say that I have had PHP daemons run for large(ish) amounts of time without any large memory or processor usage, however, that doesn't mean that you should start replacing Apache with phpMyHTTPd next week. Remember, ultimately, it's the right tool for the right job, and PHP has its little niche as a daemon language... somewhere. Davey Shafik is a contributor to the PEAR Project and is the chairman of the PHP and Web Standards Conference UK 2004. He is also about to start work on his first book. Davey can be reached via davey@php.net. Links and Literature
|
||||
|