Landin library reference discuss source

platform/linux/threads.ldn

1--  A thread of a static Linux program, raw (D494, D516, D517): `clone`
2--  makes it, sharing the address space, the descriptors, the signal
3--  actions and the thread group, and it ends with the thread's own `exit`,
4--  which clears a word of its creator's that a futex wait is woken on.
5--  Under --link=libc the C library owns its threads and this is not used:
6--  platform/posix starts them with `pthread_create` there.
7--
8--  The `clone` call is a naked routine, because the thread it makes
9--  returns from the system call on a stack that is not its creator's, into
10--  no frame of a Landin routine.  Its text has no label and no branch: the
11--  creator and the new thread run the same instructions after the call,
12--  and each picks its own next step from the call's result with a
13--  conditional move, the creator returning to its caller and the new
14--  thread entering the routine it was given.  Ordinary code reaches the
15--  text through `clone_thread`, the bodyless C view of the routine's
16--  symbol (D516).  Declared on Linux only, as the module is.
17
18fixed if compiler.os == linux then
19    --- `clone` flags for a thread: the address space, the file-system
20    --- information, the descriptor table, the signal actions, the thread
21    --- group and System V semaphore adjustments shared with the creator;
22    --- the new thread's ID written to the parent's word before the call
23    --- returns, and the child's word cleared, with a futex wake, when the
24    --- thread ends.  CLONE_VM, CLONE_FS, CLONE_FILES, CLONE_SIGHAND,
25    --- CLONE_THREAD, CLONE_SYSVSEM, CLONE_PARENT_SETTID and
26    --- CLONE_CHILD_CLEARTID; no exit signal.  The same on every
27    --- architecture.
28    public clone_thread_flags: u64 = 3477248
29
30    --- `clone` flag: the new thread's thread pointer is the call's `tls`
31    --- argument, CLONE_SETTLS.
32    public clone_settls: u64 = 524288
33
34    --- What a thread `clone_thread` makes runs first: a C routine called
35    --- with the argument it was given, on the stack it was given, with no
36    --- frame below it. It never returns: it ends with `exit_thread`.
37    public thread_entry: type = extern(c) (argument: usize) -> none
38
39    fixed if compiler.arch == x86_64 then
40        --  The clone itself.  The C call's arguments are rdi, rsi, rdx,
41        --  rcx, r8 and r9, and the seventh, the entry, is above the
42        --  return address; the system call takes rdi, rsi, rdx, r10 and
43        --  r8, `clone`'s flags, stack, parent word, child word and thread
44        --  pointer.  The new thread's stack is laid out first, 16 bytes
45        --  below its top: the entry, which its `ret` takes, and a zero
46        --  return address above it, so the entry is called as C calls a
47        --  routine, and a backtrace ends there.  After the call, rax is
48        --  zero in the new thread alone: it clears its frame pointer, takes
49        --  the argument from r9, which the call keeps, and returns into
50        --  the entry; the creator keeps its own and returns to its caller.
51        link(symbol: "_landin_linux_clone")
52        extern(naked) clone_text: () -> none =
53            assembler.block("""
54                movq 8(%rsp), %rax
55                subq $16, %rsi
56                movq %rax, (%rsi)
57                movq $0, 8(%rsi)
58                movq %rcx, %r10
59                movl $56, %eax
60                syscall
61                xorl %ecx, %ecx
62                testq %rax, %rax
63                cmovzq %rcx, %rbp
64                cmovzq %r9, %rdi
65                ret
66                """)
67        end clone_text
68    elsif compiler.arch == arm64 then
69        --  The clone itself.  The C call's arguments are x0 to x6; the
70        --  system call takes x0 to x4, `clone`'s flags, stack, parent
71        --  word, thread pointer and child word, the last two in the
72        --  other order from the C view's.  After the call, x0 is zero in
73        --  the new thread alone: it clears its frame pointer and link
74        --  register, takes the argument from x5 and branches to the entry
75        --  in x6, both of which the call keeps; the creator keeps its own
76        --  and branches back to its caller.
77        link(symbol: "_landin_linux_clone")
78        extern(naked) clone_text: () -> none =
79            assembler.block("""
80                mov x9, x3
81                mov x3, x4
82                mov x4, x9
83                mov x8, #220
84                svc #0
85                cmp x0, #0
86                csel x16, x6, x30, eq
87                csel x30, xzr, x30, eq
88                csel x29, xzr, x29, eq
89                csel x0, x5, x0, eq
90                br x16
91                """)
92        end clone_text
93    else
94        --  The clone itself.  The C call's arguments are a0 to a6; the
95        --  system call takes a0 to a4, `clone`'s flags, stack, parent
96        --  word, thread pointer and child word, the last two in the
97        --  other order from the C view's.  RV64 has no conditional move,
98        --  so a mask chooses: all ones in the new thread, whose a0 is
99        --  zero, and zero in the creator.  The new thread clears its frame
100        --  pointer and return address, takes the argument from a5 and
101        --  jumps to the entry in a6, both of which the call keeps; the
102        --  creator jumps back to its caller.
103        link(symbol: "_landin_linux_clone")
104        extern(naked) clone_text: () -> none =
105            assembler.block("""
106                mv t0, a3
107                mv a3, a4
108                mv a4, t0
109                li a7, 220
110                ecall
111                seqz t0, a0
112                neg t0, t0
113                not t2, t0
114                and t1, a6, t0
115                and t3, ra, t2
116                or t1, t1, t3
117                and t4, a5, t0
118                or a0, a0, t4
119                and s0, s0, t2
120                and ra, ra, t2
121                jr t1
122                """)
123        end clone_text
124    end if
125
126    --- Start a thread with `clone`: `flags` are `clone_thread_flags`, with
127    --- `clone_settls` when `tls` is the new thread's thread pointer; the
128    --- thread runs `entry(argument)` on the stack whose top, sixteen-byte
129    --- aligned, is `stack`, and the kernel writes its thread ID to
130    --- `parent` before this returns and clears `child` when it ends. The
131    --- new thread's ID, or minus the errno. The new thread starts with the
132    --- creator's signal mask, so a creator that blocks a signal around this
133    --- call starts a thread that does not receive it.
134    public extern(c) link(symbol: "_landin_linux_clone") clone_thread:
135        (flags: u64, stack: usize, parent: ptr mut u32, child: ptr mut u32,
136         tls: usize, argument: usize, entry: thread_entry)
137        -> (result: i64)
138
139    --- End the calling thread alone, the `exit` call: the kernel clears the
140    --- word `clone_thread` was given as `child` and wakes a futex waiter
141    --- on it, after which nothing of the thread runs. The process goes on
142    --- while another thread does; `exit_group` ends it.
143    public exit_thread:
144        (status: i32) -> noreturn =
145        _ = system_call(exit_number, i64(status), 0, 0, 0, 0, 0)
146        compiler.unreachable()
147    end exit_thread
148
149    --  `futex` operation FUTEX_WAIT, not private.  The kernel wakes the
150    --  word a thread's end clears without FUTEX_PRIVATE_FLAG, and a
151    --  private wait is not woken by a shared wake on the same word, so a
152    --  join cannot sleep through `futex_wait`: it slept for ever when it
153    --  did (D517).  glibc's and musl's joins wait shared for the same
154    --  reason.
155    futex_wait_shared: i64 = 0
156
157    --- Wait until the thread whose `child` word is `word` has ended: until
158    --- the kernel has cleared the word, as it does once nothing of that
159    --- thread runs. The word's acquire load that reads zero is the join's
160    --- edge with everything the thread did (D227). The word is the
161    --- kernel's to write, so it is read as a `u32` cell would be and not
162    --- through one.
163    public wait_until_cleared:
164        (word: ptr mut u32) -> none =
165        loop do
166            seen: u32 = compiler.atomic_load(word, compiler.acquire)
167            break when seen == 0
168            --  EAGAIN when the word changed first, EINTR for a signal:
169            --  both go round again.
170            _ = system_call(futex_number, i64(usize(word)),
171                            futex_wait_shared, i64(seen), 0, 0, 0)
172        end loop
173    end wait_until_cleared
174end if