Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

monkey-patched Thread survives os.fork() #802

Open
yssrku opened this issue Jun 22, 2023 · 4 comments
Open

monkey-patched Thread survives os.fork() #802

yssrku opened this issue Jun 22, 2023 · 4 comments

Comments

@yssrku
Copy link

yssrku commented Jun 22, 2023

Since fork() only duplicates the calling thread, seeing background threads being duplicated might be surprising.
I know this is a side effect of green thread, but some code might not expect this.

import eventlet
eventlet.monkey_patch() # behavior differs when monkey_patch() is called

import os
import time
import threading


def in_thread():
    seq = 0

    while True:
        print(seq)
        seq += 1
        time.sleep(1)


threading.Thread(target=in_thread).start()

os.fork()
time.sleep(10)
@4383
Copy link
Member

4383 commented Mar 13, 2024

Hello,

Thanks for reporting your observations, and sorry for the late reply.

Indeed, child threads which are forked is a side effect of monkey patching the stdlib.

I don't know if by design of eventlet/greenlet it is voluntary or not, or if we should consider that behavior as a bug.
In all case, I think that the code in your snippet will lead at some point to a race condition related to seq increment which is not an atomic operation.

Besides, when I ran this snippet with a monkey patched stdlib, I can observe that at some point the code exit from the infinite loop created in the in_thread function. When I ran it with a vanilla stdlib, the loop continue indefinitely.

Without much available design details, lets consider it as a bug for now.

@4383
Copy link
Member

4383 commented Mar 13, 2024

Here is the strace for the parent process:

strace: Process 669 attached
clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, {tv_sec=1129850, tv_nsec=214844952}, NULL) = 0
getpid()                                = 669
write(1, "669: 6\n", 7)                 = 7
clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, {tv_sec=1129851, tv_nsec=217304793}, NULL) = 0
getpid()                                = 669
write(1, "669: 7\n", 7)                 = 7
clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, {tv_sec=1129852, tv_nsec=219498230}, NULL) = 0
getpid()                                = 669
write(1, "669: 8\n", 7)                 = 7
clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, {tv_sec=1129853, tv_nsec=222583495}, NULL) = 0
getpid()                                = 669
write(1, "669: 9\n", 7)                 = 7
clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, {tv_sec=1129854, tv_nsec=212697741}, NULL) = 0
rt_sigaction(SIGINT, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=SA_RESTORER|SA_ONSTACK, sa_restorer=0x7fefdf165050}, {sa_handler=0x7fefdf4b55f9, sa_mask=[], sa_flags=SA_RESTORER|SA_ONSTACK, sa_restorer=0x7fefdf165050}, 8) = 0
munmap(0x7fefddd67000, 417792)          = 0
munmap(0x7fefdee79000, 16384)           = 0
exit_group(0)                           = ?
+++ exited with 0 +++

And here is the strace for the forked process:

strace: Process 669 attached
clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, {tv_sec=1129850, tv_nsec=214844952}, NULL) = 0
getpid()                                = 669
write(1, "669: 6\n", 7)                 = 7
clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, {tv_sec=1129851, tv_nsec=217304793}, NULL) = 0
getpid()                                = 669
write(1, "669: 7\n", 7)                 = 7
clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, {tv_sec=1129852, tv_nsec=219498230}, NULL) = 0
getpid()                                = 669
write(1, "669: 8\n", 7)                 = 7
clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, {tv_sec=1129853, tv_nsec=222583495}, NULL) = 0
getpid()                                = 669
write(1, "669: 9\n", 7)                 = 7
clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, {tv_sec=1129854, tv_nsec=212697741}, NULL) = 0
rt_sigaction(SIGINT, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=SA_RESTORER|SA_ONSTACK, sa_restorer=0x7fefdf165050}, {sa_handler=0x7fefdf4b55f9, sa_mask=[], sa_flags=SA_RESTORER|SA_ONSTACK, sa_restorer=0x7fefdf165050}, 8) = 0
munmap(0x7fefddd67000, 417792)          = 0
munmap(0x7fefdee79000, 16384)           = 0
exit_group(0)                           = ?
+++ exited with 0 +++

We can observe that both process exited alone without manually killing them.

@4383
Copy link
Member

4383 commented Mar 13, 2024

Here is a code snippet where the pid is logged:

import eventlet
eventlet.monkey_patch() # behavior differs when monkey_patch() is called

import os
import time
import threading


def in_thread():
    seq = 0

    while True:
        print(f"{os.getpid()}: {seq}")
        seq += 1
        time.sleep(1)


threading.Thread(target=in_thread).start()

os.fork()
time.sleep(10)

@4383
Copy link
Member

4383 commented Mar 13, 2024

The sigaction() system call is used to change the action taken by a process on receipt of a specific signal. Here the signal is the SIGINT (signal interrupt). See the rt_sigaction call in the previous straces. https://linux.die.net/man/2/rt_sigaction.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants