libsystem_kernel.dylib tcdrain hangs on MacOS

The following Python program hangs forever on MacOS, but does not hang on Linux:

import os, termios, time
device_fd, tty_fd  = os.openpty()

def loop():
    while True:
        data = os.read(device_fd, 1)
        print(data)  # <--- never gets here
        time.sleep(.1)

from threading import Thread
thread = Thread(target=loop, daemon=True)
thread.start()

os.write(tty_fd, b'123')
termios.tcdrain(tty_fd)  # <--- gets stuck here
time.sleep(3)  # allow thread to read all data

When I sample the process using Activity Monitor I always get the following at the bottom of the call graph for the main thread:

+ 2553 termios_tcdrain  (in termios.cpython-39-darwin.so) + 56  [0x1048dedcc]
+   2553 tcdrain  (in libsystem_c.dylib) + 48  [0x19f27c454]
+     2553 ioctl  (in libsystem_kernel.dylib) + 36  [0x19f30b0c0]
+       2553 __ioctl  (in libsystem_kernel.dylib) + 8  [0x19f30b0d4]

Could someone help me understand why the program gets stuck on MacOS? I'm struggling to debug the problem beyond this point.

I've attached the full sample. Other details:

  • Tested on MacOS 12.5.1 (ARM CPU).
  • Also tested on MacOS with an Intel CPU.

This is a very Python-specific issue. Looking at the sample you posted I see that:

  • Your main thread has blocked in tcdrain waiting for your secondary thread to read the data from the pseudo terminal.

  • Your secondary thread has blocked in os.read() trying to acquire the global interpreter lock (take_gil).

Presumably your main thread is holding the GIL and that’s why you’ve deadlocked.

I’ve no idea why this works on Linux but the hang you’re seeing makes sense to me from my limited understanding of how the Python runtime works. My recommendation as to your next step is to drop Python and move to Swift (-: Just kidding! I recommend that you escalate this via Python’s support resources, where you’re more likely to connect with experience with Python threading.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Thanks for the speedy reply Quinn!

You're right the program is quite high-level - I don't think I have the C skills to go any lower :-).

On reflection I agree it makes sense to get Python support in the first instance. Although surprising, I suppose it's possible the call to tcdrain somehow acquires the GIL but never releases it. For reference, the C binding / implementation for that call is here (as far as I can tell).

Let's see if Python pros can see what's going on: https://github.com/python/cpython/issues/97001.

libsystem_kernel.dylib tcdrain hangs on MacOS
 
 
Q