Processes, threads, how do they work?

TL;DR why don’t spawned threads have the same PID returned by clone3? Test with this in each of the projects, one with std::threads::spaw and the other with tokio::spawn.

strace -e trace="fork,clone,clone3,exit,execve" <executable>

Repository: https://codeberg.org/allonsyeet/threads_how_do_they_work

Output for executable using only stdlib

➜  threads_how_do_they_work git:(main) ✗  strace -e trace=fork,clone,clone3,exit,execve stdonly/target/debug/pulling_on_this_thread 
execve("stdonly/target/debug/pulling_on_this_thread", ["stdonly/target/debug/pulling_on_"...], 0x7ffdf11baad0 /* 108 vars */) = 0
clone3({flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTI
D, child_tid=0x7fc6d0d70990, parent_tid=0x7fc6d0d70990, exit_signal=0, stack=0x7fc6d0b70000, stack_size=0x1fff00, tls=0x7fc6d0d706c0} => 
{parent_tid=[8628]}, 88) = 8628
8625 => 8627
clone3({flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTI
D, child_tid=0x7fc6d0b6f990, parent_tid=0x7fc6d0b6f990, exit_signal=0, stack=0x7fc6d096f000, stack_size=0x1fff00, tls=0x7fc6d0b6f6c0} => 
{parent_tid=[8629]}, 88) = 8629
8625 => 8627
clone3({flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTI
D, child_tid=0x7fc6d096e990, parent_tid=0x7fc6d096e990, exit_signal=0, stack=0x7fc6d076e000, stack_size=0x1fff00, tls=0x7fc6d096e6c0} => 
{parent_tid=[8630]}, 88) = 8630
8625 => 8627
clone3({flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTI
D, child_tid=0x7fc6d076d990, parent_tid=0x7fc6d076d990, exit_signal=0, stack=0x7fc6d056d000, stack_size=0x1fff00, tls=0x7fc6d076d6c0} => 
{parent_tid=[8631]}, 88) = 8631
8625 => 8627
+++ exited with 0 +++

Output for tokio:

➜  threads_how_do_they_work git:(main) ✗  strace -e trace=fork,clone,clone3,exit,execve ./tokio_drifting/target/debug/tokio_drifting mult
i
execve("./tokio_drifting/target/debug/tokio_drifting", ["./tokio_drifting/target/debug/to"..., "multi"], 0x7ffc055bf958 /* 108 vars */) =
 0
clone3({flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTI
D, child_tid=0x7f58d055c990, parent_tid=0x7f58d055c990, exit_signal=0, stack=0x7f58d035c000, stack_size=0x1ffe40, tls=0x7f58d055c6c0} => 
{parent_tid=[8687]}, 88) = 8687
clone3({flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTI
D, child_tid=0x7f58d035b990, parent_tid=0x7f58d035b990, exit_signal=0, stack=0x7f58d015b000, stack_size=0x1ffe40, tls=0x7f58d035b6c0} => 
{parent_tid=[8688]}, 88) = 8688
clone3({flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTI
D, child_tid=0x7f58cbfff990, parent_tid=0x7f58cbfff990, exit_signal=0, stack=0x7f58cbdff000, stack_size=0x1ffe40, tls=0x7f58cbfff6c0} => 
{parent_tid=[8689]}, 88) = 8689
clone3({flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTI
D, child_tid=0x7f58cbdfe990, parent_tid=0x7f58cbdfe990, exit_signal=0, stack=0x7f58cbbfe000, stack_size=0x1ffe40, tls=0x7f58cbdfe6c0} => 
{parent_tid=[8690]}, 88) = 8690
8686, 8684
8686, 8684
8686, 8684
8686, 8684
+++ exited with 0 +++

Why isn’t the output of getpid the same as the output of clone3?

UPDATE: After reading the man page, getpid will give the thread group ID, not the process ID of the current thread. For that, use gettid.

stdonly:

➜  threads_how_do_they_work git:(main) ✗  strace -e trace=fork,clone,clone3,exit,execve ./stdonly/target/debug/pulling_on_this_threadexecve("./stdonly/target/debug/pulling_on_this_thread", ["./stdonly/target/debug/pulling_o"...], 0x7fff5a3dd160 /* 105 vars */) = 0
Root pid: 10837
clone3({flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID, child_tid=0x7f62699e8990, parent_tid=0x7f62699e8990, exit_signal=0, stack=0x7f62697e8000, stack_size=0x1fff00, tls=0x7f62699e86c0} =>{parent_tid=[10840]}, 88) = 10840
(gettid, getpid, getppid) = (10840, 10839, 10837)
clone3({flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID, child_tid=0x7f62697e7990, parent_tid=0x7f62697e7990, exit_signal=0, stack=0x7f62695e7000, stack_size=0x1fff00, tls=0x7f62697e76c0} =>{parent_tid=[10841]}, 88) = 10841
(gettid, getpid, getppid) = (10841, 10839, 10837)
clone3({flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID, child_tid=0x7f62695e6990, parent_tid=0x7f62695e6990, exit_signal=0, stack=0x7f62693e6000, stack_size=0x1fff00, tls=0x7f62695e66c0} =>{parent_tid=[10842]}, 88) = 10842
(gettid, getpid, getppid) = (10842, 10839, 10837)
clone3({flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID, child_tid=0x7f62693e5990, parent_tid=0x7f62693e5990, exit_signal=0, stack=0x7f62691e5000, stack_size=0x1fff00, tls=0x7f62693e56c0} =>{parent_tid=[10843]}, 88) = 10843
(gettid, getpid, getppid) = (10843, 10839, 10837)
+++ exited with 0 +++

tokio:

➜  threads_how_do_they_work git:(main) ✗  strace -e trace=fork,clone,clone3,exit,execve ./tokio_drifting/target/debug/tokio_drifting multi
execve("./tokio_drifting/target/debug/tokio_drifting", ["./tokio_drifting/target/debug/to"..., "multi"], 0x7ffd82a298d8 /* 105 vars */) = 0
clone3({flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID, child_tid=0x7f7f461a7990, parent_tid=0x7f7f461a7990, exit_signal=0, stack=0x7f7f45fa7000, stack_size=0x1ffe40, tls=0x7f7f461a76c0} =>{parent_tid=[13274]}, 88) = 13274
clone3({flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID, child_tid=0x7f7f45fa6990, parent_tid=0x7f7f45fa6990, exit_signal=0, stack=0x7f7f45da6000, stack_size=0x1ffe40, tls=0x7f7f45fa66c0} =>{parent_tid=[13275]}, 88) = 13275
clone3({flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID, child_tid=0x7f7f45da5990, parent_tid=0x7f7f45da5990, exit_signal=0, stack=0x7f7f45ba5000, stack_size=0x1ffe40, tls=0x7f7f45da56c0} =>{parent_tid=[13276]}, 88) = 13276
clone3({flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID, child_tid=0x7f7f45ba4990, parent_tid=0x7f7f45ba4990, exit_signal=0, stack=0x7f7f459a4000, stack_size=0x1ffe40, tls=0x7f7f45ba46c0} =>{parent_tid=[13277]}, 88) = 13277
Root PID = 13273
(gettid, getpid, getppid) = (13277, 13273, 13271)
(gettid, getpid, getppid) = (13276, 13273, 13271)
(gettid, getpid, getppid) = (13275, 13273, 13271)
(gettid, getpid, getppid) = (13274, 13273, 13271)
+++ exited with 0 +++

Mystery solved I suppose, if CLONE_THREAD is set, these things get weird:

  1. getpid will return the thread group ID
  2. gettid will return the thread ID within the thread group
  3. getppid will return the thread leader ID.

If you want to understand more, check out the manual pages for clone3, and specifically CLONE_THREAD section, getpid and gettid

 

allonsyeet

started out doing physics, stuck doing biostats because I need money.


2026-05-10