Skip to content

Commit 346cbd3

Browse files
authored
bpo-16500: Allow registering at-fork handlers (#1715)
* bpo-16500: Allow registering at-fork handlers * Address Serhiy's comments * Add doc for new C API * Add doc for new Python-facing function * Add NEWS entry + doc nit
1 parent f931fd1 commit 346cbd3

File tree

15 files changed

+365
-68
lines changed

15 files changed

+365
-68
lines changed

Doc/c-api/sys.rst

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,52 @@ Operating System Utilities
2626
one of the strings ``'<stdin>'`` or ``'???'``.
2727
2828
29+
.. c:function:: void PyOS_BeforeFork()
30+
31+
Function to prepare some internal state before a process fork. This
32+
should be called before calling :c:func:`fork` or any similar function
33+
that clones the current process.
34+
Only available on systems where :c:func:`fork` is defined.
35+
36+
.. versionadded:: 3.7
37+
38+
39+
.. c:function:: void PyOS_AfterFork_Parent()
40+
41+
Function to update some internal state after a process fork. This
42+
should be called from the parent process after calling :c:func:`fork`
43+
or any similar function that clones the current process, regardless
44+
of whether process cloning was successful.
45+
Only available on systems where :c:func:`fork` is defined.
46+
47+
.. versionadded:: 3.7
48+
49+
50+
.. c:function:: void PyOS_AfterFork_Child()
51+
52+
Function to update some internal state after a process fork. This
53+
should be called from the child process after calling :c:func:`fork`
54+
or any similar function that clones the current process.
55+
Only available on systems where :c:func:`fork` is defined.
56+
57+
.. versionadded:: 3.7
58+
59+
.. seealso::
60+
:func:`os.register_at_fork` allows registering custom Python functions
61+
to be called by :c:func:`PyOS_BeforeFork()`,
62+
:c:func:`PyOS_AfterFork_Parent` and :c:func:`PyOS_AfterFork_Child`.
63+
64+
2965
.. c:function:: void PyOS_AfterFork()
3066
3167
Function to update some internal state after a process fork; this should be
3268
called in the new process if the Python interpreter will continue to be used.
3369
If a new executable is loaded into the new process, this function does not need
3470
to be called.
3571
72+
.. deprecated:: 3.7
73+
This function is superseded by :c:func:`PyOS_AfterFork_Child()`.
74+
3675
3776
.. c:function:: int PyOS_CheckStack()
3877

Doc/library/os.rst

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3280,6 +3280,31 @@ written in Python, such as a mail server's external command delivery program.
32803280
subprocesses.
32813281

32823282

3283+
.. function:: register_at_fork(func, when)
3284+
3285+
Register *func* as a function to be executed when a new child process
3286+
is forked. *when* is a string specifying at which point the function is
3287+
called and can take the following values:
3288+
3289+
* *"before"* means the function is called before forking a child process;
3290+
* *"parent"* means the function is called from the parent process after
3291+
forking a child process;
3292+
* *"child"* means the function is called from the child process.
3293+
3294+
Functions registered for execution before forking are called in
3295+
reverse registration order. Functions registered for execution
3296+
after forking (either in the parent or in the child) are called
3297+
in registration order.
3298+
3299+
Note that :c:func:`fork` calls made by third-party C code may not
3300+
call those functions, unless it explicitly calls :c:func:`PyOS_BeforeFork`,
3301+
:c:func:`PyOS_AfterFork_Parent` and :c:func:`PyOS_AfterFork_Child`.
3302+
3303+
Availability: Unix.
3304+
3305+
.. versionadded:: 3.7
3306+
3307+
32833308
.. function:: spawnl(mode, path, ...)
32843309
spawnle(mode, path, ..., env)
32853310
spawnlp(mode, file, ...)

Include/intrcheck.h

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,17 @@ extern "C" {
77

88
PyAPI_FUNC(int) PyOS_InterruptOccurred(void);
99
PyAPI_FUNC(void) PyOS_InitInterrupts(void);
10-
PyAPI_FUNC(void) PyOS_AfterFork(void);
10+
#ifdef HAVE_FORK
11+
PyAPI_FUNC(void) PyOS_BeforeFork(void);
12+
PyAPI_FUNC(void) PyOS_AfterFork_Parent(void);
13+
PyAPI_FUNC(void) PyOS_AfterFork_Child(void);
14+
#endif
15+
/* Deprecated, please use PyOS_AfterFork_Child() instead */
16+
PyAPI_FUNC(void) PyOS_AfterFork(void) Py_DEPRECATED(3.7);
1117

1218
#ifndef Py_LIMITED_API
1319
PyAPI_FUNC(int) _PyOS_IsMainThread(void);
20+
PyAPI_FUNC(void) _PySignal_AfterFork(void);
1421

1522
#ifdef MS_WINDOWS
1623
/* windows.h is not included by Python.h so use void* instead of HANDLE */

Include/pystate.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,11 @@ typedef struct _is {
7474
PyObject *import_func;
7575
/* Initialized to PyEval_EvalFrameDefault(). */
7676
_PyFrameEvalFunction eval_frame;
77+
#ifdef HAVE_FORK
78+
PyObject *before_forkers;
79+
PyObject *after_forkers_parent;
80+
PyObject *after_forkers_child;
81+
#endif
7782
} PyInterpreterState;
7883
#endif
7984

Lib/multiprocessing/forkserver.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -210,11 +210,6 @@ def _serve_one(s, listener, alive_r, handlers):
210210
# send pid to client processes
211211
write_unsigned(child_w, os.getpid())
212212

213-
# reseed random number generator
214-
if 'random' in sys.modules:
215-
import random
216-
random.seed()
217-
218213
# run process object received over pipe
219214
code = spawn._main(child_r)
220215

Lib/multiprocessing/popen_fork.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,9 +68,6 @@ def _launch(self, process_obj):
6868
if self.pid == 0:
6969
try:
7070
os.close(parent_r)
71-
if 'random' in sys.modules:
72-
import random
73-
random.seed()
7471
code = process_obj._bootstrap()
7572
finally:
7673
os._exit(code)

Lib/random.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
from hashlib import sha512 as _sha512
4747
import itertools as _itertools
4848
import bisect as _bisect
49+
import os as _os
4950

5051
__all__ = ["Random","seed","random","uniform","randint","choice","sample",
5152
"randrange","shuffle","normalvariate","lognormvariate",
@@ -763,5 +764,9 @@ def _test(N=2000):
763764
setstate = _inst.setstate
764765
getrandbits = _inst.getrandbits
765766

767+
if hasattr(_os, "fork"):
768+
_os.register_at_fork(_inst.seed, when='child')
769+
770+
766771
if __name__ == '__main__':
767772
_test()

Lib/test/test_posix.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"Test posix functions"
22

33
from test import support
4+
from test.support.script_helper import assert_python_ok
45
android_not_root = support.android_not_root
56

67
# Skip these tests if there is no posix module.
@@ -187,6 +188,45 @@ def test_waitid(self):
187188
res = posix.waitid(posix.P_PID, pid, posix.WEXITED)
188189
self.assertEqual(pid, res.si_pid)
189190

191+
@unittest.skipUnless(hasattr(os, 'fork'), "test needs os.fork()")
192+
def test_register_after_fork(self):
193+
code = """if 1:
194+
import os
195+
196+
r, w = os.pipe()
197+
fin_r, fin_w = os.pipe()
198+
199+
os.register_at_fork(lambda: os.write(w, b'A'), when='before')
200+
os.register_at_fork(lambda: os.write(w, b'B'), when='before')
201+
os.register_at_fork(lambda: os.write(w, b'C'), when='parent')
202+
os.register_at_fork(lambda: os.write(w, b'D'), when='parent')
203+
os.register_at_fork(lambda: os.write(w, b'E'), when='child')
204+
os.register_at_fork(lambda: os.write(w, b'F'), when='child')
205+
206+
pid = os.fork()
207+
if pid == 0:
208+
# At this point, after-forkers have already been executed
209+
os.close(w)
210+
# Wait for parent to tell us to exit
211+
os.read(fin_r, 1)
212+
os._exit(0)
213+
else:
214+
try:
215+
os.close(w)
216+
with open(r, "rb") as f:
217+
data = f.read()
218+
assert len(data) == 6, data
219+
# Check before-fork callbacks
220+
assert data[:2] == b'BA', data
221+
# Check after-fork callbacks
222+
assert sorted(data[2:]) == list(b'CDEF'), data
223+
assert data.index(b'C') < data.index(b'D'), data
224+
assert data.index(b'E') < data.index(b'F'), data
225+
finally:
226+
os.write(fin_w, b'!')
227+
"""
228+
assert_python_ok('-c', code)
229+
190230
@unittest.skipUnless(hasattr(posix, 'lockf'), "test needs posix.lockf()")
191231
def test_lockf(self):
192232
fd = os.open(support.TESTFN, os.O_WRONLY | os.O_CREAT)

Lib/test/test_random.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import unittest
22
import unittest.mock
33
import random
4+
import os
45
import time
56
import pickle
67
import warnings
@@ -902,6 +903,24 @@ def __init__(self, newarg=None):
902903
random.Random.__init__(self)
903904
Subclass(newarg=1)
904905

906+
@unittest.skipUnless(hasattr(os, "fork"), "fork() required")
907+
def test_after_fork(self):
908+
# Test the global Random instance gets reseeded in child
909+
r, w = os.pipe()
910+
if os.fork() == 0:
911+
try:
912+
val = random.getrandbits(128)
913+
with open(w, "w") as f:
914+
f.write(str(val))
915+
finally:
916+
os._exit(0)
917+
else:
918+
os.close(w)
919+
val = random.getrandbits(128)
920+
with open(r, "r") as f:
921+
child_val = eval(f.read())
922+
self.assertNotEqual(val, child_val)
923+
905924

906925
if __name__ == "__main__":
907926
unittest.main()

Misc/NEWS

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,8 @@ Extension Modules
341341
Library
342342
-------
343343

344+
- bpo-16500: Allow registering at-fork handlers.
345+
344346
- bpo-30470: Deprecate invalid ctypes call protection on Windows. Patch by
345347
Mariatta Wijaya.
346348

0 commit comments

Comments
 (0)