This idea came about at Facebook while investigating ways to optimize Zend Engine calls to ap_block/unblock_alarms running under Apache 1.x. After some research Rasmus Lerdorf's original proposal to the internals list was found and it matched our plans. The purpose of which is as follows:
The Zend Engine exposes an alarm blocking mechanism via macros however the specific implementation is left up to each SAPI, many of which do not provide the necessary functionality. In a standard PHP environment this is likely to be acceptable as the PHP engine only implements a single alarm based feature, the max_execution_time timeout. A survey of bundled SAPI's shows that almost all implement STANDARD_SAPI_MODULE_PROPERTIES in their sapi_module_struct, which does not provide any alarm blocking callback functionality. Only apache and apache_hooks define the alarm blocking callbacks, and only if !PHP_WIN32. The chance of being in a critical section while timing out is low and even if so it may not be problematic as the timeout triggers an immediate shutdown.
As extensions such as APC are introduced the need for stable alarm handling increases. The likelihood of being within within a critical section increases as an extension may be maintaining a shared memory segment using a variety of locking mechanisms, some of which may in fact further increase these odds such as spinlocking.
Extensions will now be able implement the zend alarm blocking macros in their critical sections and be assured that these critical sections will be protected.
During startup the zend engine will register handlers for following signals
If any of these signals have previously registered handlers they will be stored internally.
If a signal is received, zend_signal_handler_defer() will check to see if execution is in a critical section. If not the previously registered handler will be called via zend_signal_handler(). This supports basic handlers as well as extended siginfo style handler functions.
If execution is within a critical section, execution of the previous handler will be deferred until the HANDLE_UNBLOCK_INTERRUPTIONS macro is reached. During deferred execution if any additional signals are received they are queued. All queued signals handlers will be invoked when HANDLE_UNBLOCK_INTERRUPTIONS is reached.
For optimal performance the new HANDLE_BLOCK/UNBLOCK_INTERRUPTIONS macros account for critical sections by incrementing and decrementing the zend_signal_globals_t.depth counter. No functions are called until depth==0 in HANDLE_UNBLOCK_INTERRUPTIONS.
This accounting will not work in ZTS enabled mode thus support for deferred signal handling is automatically disabled when ZTS is enabled.
The zend_signal() function allows registration of arbitrary signals within the zend engine to be deferred while executing critical sections. This is now used to register the SIGALRM and SIGPROF signals that provide the max_execution_time timeout functionality within PHP.
Using this php test designed to stress memory allocation by assigning a million integers into an array a real time time improvement is measurable.
Average of 10 runs | |
---|---|
php-5.2.5 | 938.9716 |
php-5.2.5-patched | 915.8652 |
difference | 23.1063 |
improvement | 2.46% |
Note: Although within the range of error these measurements seem consistent. The same script executed via ApacheBench showed no detectible request time improvement once transit cost was introduced.
Benchmarks using Valgrind/Callgrind show the following results on a heavyweight page with a total of almost 3 billion instructions.
Callgrind | |
---|---|
php-5.2.5 | |
total instructions | 2,707,532,005 |
ap_block_alarms calls | 4,998,851 |
ap_unblock_alarms calls | 4,998,851 |
php-5.2.5-patched | |
total instructions | 2,607,562,502 |
ap_block_alarms calls | 0 |
ap_unblock_alarms calls | 0 |
Although some measure of improved performance is expected and measurable this functionality should at the very least improve stability at no cost.
The pcntl extension allows signal handlers to be defined in PHP userspace via pctnl_signal(). The handler is installed via signal and any previously registered handlers for the specified signal are ignored. While this is technically incompatible with Zend Signals, if a handler is installed via pctnl_signal for signals SIGALRM, SIGHUP, SIGINT, SIGQUIT, SIGTERM, SIGUSR1, SIGUSR2, the deferred protection offered by Zend Signals will basically be disabled for that signal number.
Note: Although --enable-pcntl states “(CLI/CGI only)” in the config.m4, there is no actual enforcement of this at compile time. The extension can be running in any SAPI.
Update: pcntl has been modified to register signals via zend_signal() when available. Critical sections will now continue to have deferred protection even after signals are registered via pcntl.
To deal with zombied or defunct children SIGCHILD handling was added to PHP via --enable-sigchild. This is apparently very common during disconnect when using oracle libraries to connect via the BEQ interface. When enabled, a handler for SIGCHILD is installed during php_startup. This handler calls waitpid() for any children assuring that their exit status is read and the zombies will not remain until php exits.
Since Zend Signals does not install a handler for SIGCHILD there is no explicit conflict. Also the simplicity of the handler should ensure that it does not adversely affect any critical zend sections. In future versions we may want to bring this functionality into Zend and enable by default on all platforms that support SIGCHLD ass well as implement via sigaction().
Note: When calling wait() or waitpid() within a handler the global errno may be modified. I have modified the existing handler in this patch to account for this.