Skip to content

Commit 580c5ab

Browse files
committed
Allow nested signal handling
This makes it possible to interrupt syscalls performed by signal handlers. Refactor the queue to a ring buffer as it's more concurrency friendly.
1 parent 3f4e7b8 commit 580c5ab

4 files changed

Lines changed: 141 additions & 80 deletions

File tree

ext/pcntl/pcntl.c

Lines changed: 52 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,6 @@ PHP_RINIT_FUNCTION(pcntl)
207207
{
208208
php_add_tick_function(pcntl_signal_dispatch_tick_function, NULL);
209209
zend_hash_init(&PCNTL_G(php_signal_table), 16, NULL, ZVAL_PTR_DTOR, 0);
210-
PCNTL_G(head) = PCNTL_G(tail) = PCNTL_G(spares) = NULL;
211210
PCNTL_G(async_signals) = 0;
212211
PCNTL_G(last_error) = 0;
213212
PCNTL_G(num_signals) = NSIG;
@@ -237,7 +236,6 @@ PHP_MINIT_FUNCTION(pcntl)
237236

238237
PHP_RSHUTDOWN_FUNCTION(pcntl)
239238
{
240-
struct php_pcntl_pending_signal *sig;
241239
zend_long signo;
242240
zval *handle;
243241

@@ -250,15 +248,10 @@ PHP_RSHUTDOWN_FUNCTION(pcntl)
250248

251249
zend_hash_destroy(&PCNTL_G(php_signal_table));
252250

253-
while (PCNTL_G(head)) {
254-
sig = PCNTL_G(head);
255-
PCNTL_G(head) = sig->next;
256-
efree(sig);
257-
}
258-
while (PCNTL_G(spares)) {
259-
sig = PCNTL_G(spares);
260-
PCNTL_G(spares) = sig->next;
261-
efree(sig);
251+
if (PCNTL_G(pending_signals_queue)) {
252+
efree(PCNTL_G(pending_signals_queue));
253+
PCNTL_G(pending_signals_queue) = NULL;
254+
PCNTL_G(pending_signals) = false;
262255
}
263256

264257
efree(PCNTL_G(restart_syscalls));
@@ -820,16 +813,10 @@ PHP_FUNCTION(pcntl_signal)
820813
RETURN_THROWS();
821814
}
822815

823-
if (!PCNTL_G(spares)) {
824-
/* since calling malloc() from within a signal handler is not portable,
825-
* pre-allocate a few records for recording signals */
826-
for (unsigned int i = 0; i < PCNTL_G(num_signals); i++) {
827-
struct php_pcntl_pending_signal *psig;
828-
829-
psig = emalloc(sizeof(*psig));
830-
psig->next = PCNTL_G(spares);
831-
PCNTL_G(spares) = psig;
832-
}
816+
if (!PCNTL_G(pending_signals_queue)) {
817+
zend_atomic_int_store_ex(&PCNTL_G(pending_signals_head), 0);
818+
zend_atomic_int_store_ex(&PCNTL_G(pending_signals_tail), 0);
819+
PCNTL_G(pending_signals_queue) = safe_emalloc(PCNTL_G(num_signals), sizeof(struct php_pcntl_pending_signal), 0);
833820
}
834821

835822
/* If restart_syscalls was not explicitly specified and the signal is SIGALRM, then default
@@ -1344,86 +1331,87 @@ PHP_FUNCTION(pcntl_strerror)
13441331
/* Our custom signal handler that calls the appropriate php_function */
13451332
static void pcntl_signal_handler(int signo, siginfo_t *siginfo, void *context)
13461333
{
1347-
struct php_pcntl_pending_signal *psig = PCNTL_G(spares);
1348-
if (!psig) {
1334+
/* Only pcntl_signal_handler() modifies pending_signals_tail.
1335+
* Signals are masked during execution. */
1336+
1337+
int tail = zend_atomic_int_load_ex(&PCNTL_G(pending_signals_tail));
1338+
int next_tail = (tail + 1) % PCNTL_G(num_signals);
1339+
1340+
if (next_tail == zend_atomic_int_load_ex(&PCNTL_G(pending_signals_head))) {
13491341
/* oops, too many signals for us to track, so we'll forget about this one */
13501342
return;
13511343
}
1352-
PCNTL_G(spares) = psig->next;
13531344

1354-
psig->signo = signo;
1355-
psig->next = NULL;
1345+
struct php_pcntl_pending_signal *psig = &PCNTL_G(pending_signals_queue)[tail];
13561346

1347+
psig->signo = signo;
13571348
psig->siginfo = *siginfo;
13581349

1359-
/* the head check is important, as the tick handler cannot atomically clear both
1360-
* the head and tail */
1361-
if (PCNTL_G(head) && PCNTL_G(tail)) {
1362-
PCNTL_G(tail)->next = psig;
1363-
} else {
1364-
PCNTL_G(head) = psig;
1365-
}
1366-
PCNTL_G(tail) = psig;
1350+
zend_atomic_int_store_ex(&PCNTL_G(pending_signals_tail), next_tail);
1351+
13671352
PCNTL_G(pending_signals) = true;
13681353
if (PCNTL_G(async_signals)) {
13691354
zend_atomic_bool_store_ex(&EG(vm_interrupt), true);
13701355
}
13711356
}
13721357

1358+
static bool pcntl_signal_dequeue(struct php_pcntl_pending_signal *sig)
1359+
{
1360+
/* Only pcntl_signal_dequeue() modifies pending_signals_head.
1361+
* pcntl_signal_dequeue() is never called concurrently with itself. */
1362+
1363+
int head = zend_atomic_int_load_ex(&PCNTL_G(pending_signals_head));
1364+
1365+
if (head == zend_atomic_int_load_ex(&PCNTL_G(pending_signals_tail))) {
1366+
return false; /* Queue is empty */
1367+
}
1368+
1369+
*sig = PCNTL_G(pending_signals_queue)[head];
1370+
1371+
zend_atomic_int_store_ex(&PCNTL_G(pending_signals_head), (head + 1) % PCNTL_G(num_signals));
1372+
1373+
return true;
1374+
}
1375+
13731376
zend_signal_interrupt_result pcntl_signal_dispatch(void)
13741377
{
13751378
zval params[2], *handle, retval;
1376-
struct php_pcntl_pending_signal *queue, *next;
1377-
sigset_t mask;
1378-
sigset_t old_mask;
1379+
struct php_pcntl_pending_signal sig;
13791380
bool interrupt = false;
13801381

13811382
if(!PCNTL_G(pending_signals)) {
13821383
return ZEND_SIGNAL_RESTART;
13831384
}
13841385

1385-
/* Mask all signals */
1386-
sigfillset(&mask);
1387-
sigprocmask(SIG_BLOCK, &mask, &old_mask);
1388-
1389-
/* Bail if the queue is empty or if we are already playing the queue */
1390-
// TODO: For the purpose of EINTR handling, processing next signals here would be useful
1391-
if (!PCNTL_G(head) || PCNTL_G(processing_signal_queue)) {
1392-
sigprocmask(SIG_SETMASK, &old_mask, NULL);
1393-
return ZEND_SIGNAL_RESTART;
1394-
}
1386+
PCNTL_G(pending_signals) = false;
13951387

13961388
/* Prevent switching fibers when handling signals */
13971389
zend_fiber_switch_block();
13981390

1399-
/* Prevent reentrant handler calls */
1400-
PCNTL_G(processing_signal_queue) = true;
1401-
1402-
queue = PCNTL_G(head);
1403-
PCNTL_G(head) = NULL; /* simple stores are atomic */
1404-
PCNTL_G(tail) = NULL;
1405-
1406-
while (queue) {
1407-
if ((handle = zend_hash_index_find(&PCNTL_G(php_signal_table), queue->signo)) != NULL) {
1391+
/* Consume signals in FIFO order until the queue is empty. The queue may be
1392+
* consumed during the invocation of PHP signal handlers if new signals are
1393+
* delivered and PCNTL_G(pending_signals) is set. */
1394+
while (pcntl_signal_dequeue(&sig)) {
1395+
if ((handle = zend_hash_index_find(&PCNTL_G(php_signal_table), sig.signo)) != NULL) {
14081396
if (Z_TYPE_P(handle) != IS_LONG) {
1409-
ZVAL_LONG(&params[0], queue->signo);
1397+
ZVAL_LONG(&params[0], sig.signo);
14101398
array_init(&params[1]);
1411-
pcntl_siginfo_to_zval(queue->signo, &queue->siginfo, &params[1]);
1399+
pcntl_siginfo_to_zval(sig.signo, &sig.siginfo, &params[1]);
14121400

14131401
call_user_function(NULL, NULL, handle, &retval, 2, params);
14141402
zval_ptr_dtor(&params[1]);
14151403

14161404
if (Z_TYPE(retval) == IS_OBJECT && Z_OBJCE(retval) == SignalReturn_ce) {
14171405
if (zend_enum_fetch_case_id(Z_OBJ(retval)) == ZEND_ENUM_Pcntl_SignalReturn_Interrupt) {
14181406
interrupt = true;
1419-
} else if (!zend_bitset_in(PCNTL_G(restart_syscalls), queue->signo)) {
1407+
} else if (!zend_bitset_in(PCNTL_G(restart_syscalls), sig.signo)) {
14201408
interrupt = true;
14211409
}
14221410
} else if (Z_TYPE(retval) > IS_NULL) {
14231411
zend_type_error("Signal handler must return a Pcntl\\SignalReturn or no value, %s returned",
14241412
zend_zval_value_name(&retval));
14251413
interrupt = true;
1426-
} else if (!zend_bitset_in(PCNTL_G(restart_syscalls), queue->signo)) {
1414+
} else if (!zend_bitset_in(PCNTL_G(restart_syscalls), sig.signo)) {
14271415
interrupt = true;
14281416
}
14291417

@@ -1435,32 +1423,18 @@ zend_signal_interrupt_result pcntl_signal_dispatch(void)
14351423
}
14361424
}
14371425
}
1438-
1439-
next = queue->next;
1440-
queue->next = PCNTL_G(spares);
1441-
PCNTL_G(spares) = queue;
1442-
queue = next;
14431426
}
14441427

14451428
/* drain the remaining in case of exception thrown */
1446-
while (queue) {
1447-
next = queue->next;
1448-
queue->next = PCNTL_G(spares);
1449-
PCNTL_G(spares) = queue;
1450-
queue = next;
1429+
if (EG(exception)) {
1430+
while (pcntl_signal_dequeue(&sig)) {
1431+
/* pass */
1432+
}
14511433
}
14521434

1453-
PCNTL_G(pending_signals) = false;
1454-
1455-
/* Re-enable queue */
1456-
PCNTL_G(processing_signal_queue) = false;
1457-
14581435
/* Re-enable fiber switching */
14591436
zend_fiber_switch_unblock();
14601437

1461-
/* return signal mask to previous state */
1462-
sigprocmask(SIG_SETMASK, &old_mask, NULL);
1463-
14641438
return interrupt ? ZEND_SIGNAL_INTERRUPT : ZEND_SIGNAL_RESTART;
14651439
}
14661440

ext/pcntl/php_pcntl.h

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
#define PHP_PCNTL_H
1717

1818
#include "Zend/zend_bitset.h"
19+
#include "Zend/zend_atomic.h"
1920
#include "pcntl_decl.h"
2021

2122
#if defined(HAVE_DECL_WCONTINUED) && HAVE_DECL_WCONTINUED == 1 && defined(HAVE_WIFCONTINUED)
@@ -42,13 +43,14 @@ struct php_pcntl_pending_signal {
4243

4344
ZEND_BEGIN_MODULE_GLOBALS(pcntl)
4445
HashTable php_signal_table;
45-
bool processing_signal_queue;
4646
volatile bool pending_signals;
4747
bool async_signals;
4848
/* some OSes define NSIG to be > UINT8_MAX */
4949
uint16_t num_signals;
5050
int last_error;
51-
struct php_pcntl_pending_signal *head, *tail, *spares;
51+
struct php_pcntl_pending_signal *pending_signals_queue; /* ring buffer */
52+
zend_atomic_int pending_signals_head; /* consumer position */
53+
zend_atomic_int pending_signals_tail; /* producer position */
5254
zend_bitset restart_syscalls;
5355
ZEND_END_MODULE_GLOBALS(pcntl)
5456

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
--TEST--
2+
Signals can be delivered during handler execution
3+
--EXTENSIONS--
4+
pcntl
5+
posix
6+
--FILE--
7+
<?php
8+
9+
$indent = '';
10+
11+
pcntl_signal(SIGUSR1, function ($signo) use (&$indent) {
12+
echo "{$indent}Handling SIGUSR1\n";
13+
$indent .= ' ';
14+
posix_kill(posix_getpid(), SIGUSR2);
15+
pcntl_signal_dispatch();
16+
$indent = substr($indent, 0, -1);
17+
echo "{$indent}Done handling SIGUSR1\n";
18+
});
19+
20+
pcntl_signal(SIGUSR2, function () use (&$indent) {
21+
echo "{$indent}Handling SIGUSR2\n";
22+
echo "{$indent}Done handling SIGUSR2\n";
23+
});
24+
25+
posix_kill(posix_getpid(), SIGUSR1);
26+
posix_kill(posix_getpid(), SIGUSR1);
27+
pcntl_signal_dispatch();
28+
29+
?>
30+
--EXPECT--
31+
Handling SIGUSR1
32+
Handling SIGUSR1
33+
Handling SIGUSR2
34+
Done handling SIGUSR2
35+
Handling SIGUSR2
36+
Done handling SIGUSR2
37+
Done handling SIGUSR1
38+
Done handling SIGUSR1
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
--TEST--
2+
Signals can be delivered during handler execution: I/O
3+
--EXTENSIONS--
4+
pcntl
5+
posix
6+
--SKIPIF--
7+
<?php
8+
$pair = @stream_socket_pair(STREAM_PF_UNIX, STREAM_SOCK_STREAM, 0);
9+
if (!$pair) die("skip stream_socket_pair() not available");
10+
?>
11+
--FILE--
12+
<?php
13+
14+
$indent = '';
15+
16+
[$read, $write] = stream_socket_pair(STREAM_PF_UNIX, STREAM_SOCK_STREAM, 0);
17+
stream_set_blocking($read, true);
18+
19+
pcntl_signal(SIGUSR1, function ($signo) use (&$indent, $read) {
20+
echo "{$indent}Handling SIGUSR1\n";
21+
$indent .= ' ';
22+
23+
// Queue signal
24+
posix_kill(posix_getpid(), SIGUSR2);
25+
// Delivered here
26+
var_dump(fread($read, 1));
27+
28+
$indent = substr($indent, 0, -1);
29+
echo "{$indent}Done handling SIGUSR1\n";
30+
});
31+
32+
pcntl_signal(SIGUSR2, function () use (&$indent) {
33+
echo "{$indent}Handling SIGUSR2\n";
34+
echo "{$indent}Done handling SIGUSR2\n";
35+
return Pcntl\SignalReturn::Interrupt;
36+
});
37+
38+
posix_kill(posix_getpid(), SIGUSR1);
39+
pcntl_signal_dispatch();
40+
41+
?>
42+
--EXPECT--
43+
Handling SIGUSR1
44+
Handling SIGUSR2
45+
Done handling SIGUSR2
46+
bool(false)
47+
Done handling SIGUSR1

0 commit comments

Comments
 (0)