Skip to content

Commit b7af9ed

Browse files
committed
add std.os.createThread
this adds kernel thread support to the standard library for linux. See #174
1 parent fa05cab commit b7af9ed

File tree

7 files changed

+250
-14
lines changed

7 files changed

+250
-14
lines changed

src/ir.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18407,6 +18407,7 @@ bool ir_has_side_effects(IrInstruction *instruction) {
1840718407
case IrInstructionIdAddImplicitReturnType:
1840818408
case IrInstructionIdMergeErrRetTraces:
1840918409
case IrInstructionIdMarkErrRetTracePtr:
18410+
case IrInstructionIdAtomicRmw:
1841018411
return true;
1841118412

1841218413
case IrInstructionIdPhi:
@@ -18487,7 +18488,6 @@ bool ir_has_side_effects(IrInstruction *instruction) {
1848718488
case IrInstructionIdCoroSize:
1848818489
case IrInstructionIdCoroSuspend:
1848918490
case IrInstructionIdCoroFree:
18490-
case IrInstructionIdAtomicRmw:
1849118491
case IrInstructionIdCoroPromise:
1849218492
case IrInstructionIdPromiseResultType:
1849318493
return false;

std/fmt/index.zig

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,25 +8,25 @@ const errol3 = @import("errol/index.zig").errol3;
88

99
const max_int_digits = 65;
1010

11-
const State = enum { // TODO put inside format function and make sure the name and debug info is correct
12-
Start,
13-
OpenBrace,
14-
CloseBrace,
15-
Integer,
16-
IntegerWidth,
17-
Float,
18-
FloatWidth,
19-
Character,
20-
Buf,
21-
BufWidth,
22-
};
23-
2411
/// Renders fmt string with args, calling output with slices of bytes.
2512
/// If `output` returns an error, the error is returned from `format` and
2613
/// `output` is not called again.
2714
pub fn format(context: var, comptime Errors: type, output: fn(@typeOf(context), []const u8) Errors!void,
2815
comptime fmt: []const u8, args: ...) Errors!void
2916
{
17+
const State = enum {
18+
Start,
19+
OpenBrace,
20+
CloseBrace,
21+
Integer,
22+
IntegerWidth,
23+
Float,
24+
FloatWidth,
25+
Character,
26+
Buf,
27+
BufWidth,
28+
};
29+
3030
comptime var start_index = 0;
3131
comptime var state = State.Start;
3232
comptime var next_arg = 0;

std/os/index.zig

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2384,3 +2384,132 @@ pub fn posixGetSockOptConnectError(sockfd: i32) PosixConnectError!void {
23842384
posix.ENOTSOCK => unreachable, // The file descriptor sockfd does not refer to a socket.
23852385
}
23862386
}
2387+
2388+
pub const Thread = struct {
2389+
pid: i32,
2390+
allocator: ?&mem.Allocator,
2391+
stack: []u8,
2392+
2393+
pub fn wait(self: &const Thread) void {
2394+
while (true) {
2395+
const pid_value = self.pid; // TODO atomic load
2396+
if (pid_value == 0) break;
2397+
const rc = linux.futex_wait(@ptrToInt(&self.pid), linux.FUTEX_WAIT, pid_value, null);
2398+
switch (linux.getErrno(rc)) {
2399+
0 => continue,
2400+
posix.EINTR => continue,
2401+
posix.EAGAIN => continue,
2402+
else => unreachable,
2403+
}
2404+
}
2405+
if (self.allocator) |a| {
2406+
a.free(self.stack);
2407+
}
2408+
}
2409+
};
2410+
2411+
pub const SpawnThreadError = error {
2412+
/// A system-imposed limit on the number of threads was encountered.
2413+
/// There are a number of limits that may trigger this error:
2414+
/// * the RLIMIT_NPROC soft resource limit (set via setrlimit(2)),
2415+
/// which limits the number of processes and threads for a real
2416+
/// user ID, was reached;
2417+
/// * the kernel's system-wide limit on the number of processes and
2418+
/// threads, /proc/sys/kernel/threads-max, was reached (see
2419+
/// proc(5));
2420+
/// * the maximum number of PIDs, /proc/sys/kernel/pid_max, was
2421+
/// reached (see proc(5)); or
2422+
/// * the PID limit (pids.max) imposed by the cgroup "process num‐
2423+
/// ber" (PIDs) controller was reached.
2424+
ThreadQuotaExceeded,
2425+
2426+
/// The kernel cannot allocate sufficient memory to allocate a task structure
2427+
/// for the child, or to copy those parts of the caller's context that need to
2428+
/// be copied.
2429+
SystemResources,
2430+
2431+
Unexpected,
2432+
};
2433+
2434+
pub const SpawnThreadAllocatorError = SpawnThreadError || error{OutOfMemory};
2435+
2436+
/// caller must call wait on the returned thread
2437+
/// fn startFn(@typeOf(context)) T
2438+
/// where T is u8, noreturn, void, or !void
2439+
pub fn spawnThreadAllocator(allocator: &mem.Allocator, context: var, comptime startFn: var) SpawnThreadAllocatorError!&Thread {
2440+
// TODO compile-time call graph analysis to determine stack upper bound
2441+
// https://github.com/zig-lang/zig/issues/157
2442+
const default_stack_size = 8 * 1024 * 1024;
2443+
const stack_bytes = try allocator.alloc(u8, default_stack_size);
2444+
const thread = try spawnThread(stack_bytes, context, startFn);
2445+
thread.allocator = allocator;
2446+
return thread;
2447+
}
2448+
2449+
/// stack must be big enough to store one Thread and one @typeOf(context), each with default alignment, at the end
2450+
/// fn startFn(@typeOf(context)) T
2451+
/// where T is u8, noreturn, void, or !void
2452+
/// caller must call wait on the returned thread
2453+
pub fn spawnThread(stack: []u8, context: var, comptime startFn: var) SpawnThreadError!&Thread {
2454+
const Context = @typeOf(context);
2455+
comptime assert(@ArgType(@typeOf(startFn), 0) == Context);
2456+
2457+
var stack_end: usize = @ptrToInt(stack.ptr) + stack.len;
2458+
var arg: usize = undefined;
2459+
if (@sizeOf(Context) != 0) {
2460+
stack_end -= @sizeOf(Context);
2461+
stack_end -= stack_end % @alignOf(Context);
2462+
assert(stack_end >= @ptrToInt(stack.ptr));
2463+
const context_ptr = @alignCast(@alignOf(Context), @intToPtr(&Context, stack_end));
2464+
*context_ptr = context;
2465+
arg = stack_end;
2466+
}
2467+
2468+
stack_end -= @sizeOf(Thread);
2469+
stack_end -= stack_end % @alignOf(Thread);
2470+
assert(stack_end >= @ptrToInt(stack.ptr));
2471+
const thread_ptr = @alignCast(@alignOf(Thread), @intToPtr(&Thread, stack_end));
2472+
thread_ptr.stack = stack;
2473+
thread_ptr.allocator = null;
2474+
2475+
const threadMain = struct {
2476+
extern fn threadMain(ctx_addr: usize) u8 {
2477+
if (@sizeOf(Context) == 0) {
2478+
return startFn({});
2479+
} else {
2480+
return startFn(*@intToPtr(&const Context, ctx_addr));
2481+
}
2482+
}
2483+
}.threadMain;
2484+
2485+
const flags = posix.CLONE_VM | posix.CLONE_FS | posix.CLONE_FILES | posix.CLONE_SIGHAND
2486+
| posix.CLONE_THREAD | posix.CLONE_SYSVSEM // | posix.CLONE_SETTLS
2487+
| posix.CLONE_PARENT_SETTID | posix.CLONE_CHILD_CLEARTID | posix.CLONE_DETACHED;
2488+
const newtls: usize = 0;
2489+
const rc = posix.clone(threadMain, stack_end, flags, arg, &thread_ptr.pid, newtls, &thread_ptr.pid);
2490+
const err = posix.getErrno(rc);
2491+
switch (err) {
2492+
0 => return thread_ptr,
2493+
posix.EAGAIN => return SpawnThreadError.ThreadQuotaExceeded,
2494+
posix.EINVAL => unreachable,
2495+
posix.ENOMEM => return SpawnThreadError.SystemResources,
2496+
posix.ENOSPC => unreachable,
2497+
posix.EPERM => unreachable,
2498+
posix.EUSERS => unreachable,
2499+
else => return unexpectedErrorPosix(err),
2500+
}
2501+
}
2502+
2503+
pub fn posixWait(pid: i32) i32 {
2504+
var status: i32 = undefined;
2505+
while (true) {
2506+
const err = posix.getErrno(posix.waitpid(pid, &status, 0));
2507+
switch (err) {
2508+
0 => return status,
2509+
posix.EINTR => continue,
2510+
posix.ECHILD => unreachable, // The process specified does not exist. It would be a race condition to handle this error.
2511+
posix.EINVAL => unreachable, // The options argument was invalid
2512+
else => unreachable,
2513+
}
2514+
}
2515+
}

std/os/linux/index.zig

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,22 @@ pub const STDIN_FILENO = 0;
1414
pub const STDOUT_FILENO = 1;
1515
pub const STDERR_FILENO = 2;
1616

17+
pub const FUTEX_WAIT = 0;
18+
pub const FUTEX_WAKE = 1;
19+
pub const FUTEX_FD = 2;
20+
pub const FUTEX_REQUEUE = 3;
21+
pub const FUTEX_CMP_REQUEUE = 4;
22+
pub const FUTEX_WAKE_OP = 5;
23+
pub const FUTEX_LOCK_PI = 6;
24+
pub const FUTEX_UNLOCK_PI = 7;
25+
pub const FUTEX_TRYLOCK_PI = 8;
26+
pub const FUTEX_WAIT_BITSET = 9;
27+
28+
pub const FUTEX_PRIVATE_FLAG = 128;
29+
30+
pub const FUTEX_CLOCK_REALTIME = 256;
31+
32+
1733
pub const PROT_NONE = 0;
1834
pub const PROT_READ = 1;
1935
pub const PROT_WRITE = 2;
@@ -652,6 +668,10 @@ pub fn fork() usize {
652668
return syscall0(SYS_fork);
653669
}
654670

671+
pub fn futex_wait(uaddr: usize, futex_op: u32, val: i32, timeout: ?&timespec) usize {
672+
return syscall4(SYS_futex, uaddr, futex_op, @bitCast(u32, val), @ptrToInt(timeout));
673+
}
674+
655675
pub fn getcwd(buf: &u8, size: usize) usize {
656676
return syscall2(SYS_getcwd, @ptrToInt(buf), size);
657677
}
@@ -746,6 +766,16 @@ pub fn openat(dirfd: i32, path: &const u8, flags: usize, mode: usize) usize {
746766
return syscall4(SYS_openat, usize(dirfd), @ptrToInt(path), flags, mode);
747767
}
748768

769+
/// See also `clone` (from the arch-specific include)
770+
pub fn clone5(flags: usize, child_stack_ptr: usize, parent_tid: &i32, child_tid: &i32, newtls: usize) usize {
771+
return syscall5(SYS_clone, flags, child_stack_ptr, @ptrToInt(parent_tid), @ptrToInt(child_tid), newtls);
772+
}
773+
774+
/// See also `clone` (from the arch-specific include)
775+
pub fn clone2(flags: usize, child_stack_ptr: usize) usize {
776+
return syscall2(SYS_clone, flags, child_stack_ptr);
777+
}
778+
749779
pub fn close(fd: i32) usize {
750780
return syscall1(SYS_close, usize(fd));
751781
}

std/os/linux/x86_64.zig

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -443,6 +443,9 @@ pub fn syscall6(number: usize, arg1: usize, arg2: usize, arg3: usize, arg4: usiz
443443
: "rcx", "r11");
444444
}
445445

446+
/// This matches the libc clone function.
447+
pub extern fn clone(func: extern fn(arg: usize) u8, stack: usize, flags: usize, arg: usize, ptid: &i32, tls: usize, ctid: &i32) usize;
448+
446449
pub nakedcc fn restore_rt() void {
447450
return asm volatile ("syscall"
448451
:

std/os/test.zig

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ const io = std.io;
66
const a = std.debug.global_allocator;
77

88
const builtin = @import("builtin");
9+
const AtomicRmwOp = builtin.AtomicRmwOp;
10+
const AtomicOrder = builtin.AtomicOrder;
911

1012
test "makePath, put some files in it, deleteTree" {
1113
if (builtin.os == builtin.Os.windows) {
@@ -40,3 +42,40 @@ test "access file" {
4042
assert((try os.File.access(a, "os_test_tmp/file.txt", os.default_file_mode)) == true);
4143
try os.deleteTree(a, "os_test_tmp");
4244
}
45+
46+
test "spawn threads" {
47+
if (builtin.os != builtin.Os.linux) {
48+
// TODO implement threads on macos and windows
49+
return;
50+
}
51+
52+
var direct_allocator = std.heap.DirectAllocator.init();
53+
defer direct_allocator.deinit();
54+
55+
var shared_ctx: i32 = 1;
56+
57+
const thread1 = try std.os.spawnThreadAllocator(&direct_allocator.allocator, {}, start1);
58+
const thread4 = try std.os.spawnThreadAllocator(&direct_allocator.allocator, &shared_ctx, start2);
59+
60+
var stack1: [1024]u8 = undefined;
61+
var stack2: [1024]u8 = undefined;
62+
63+
const thread2 = try std.os.spawnThread(stack1[0..], &shared_ctx, start2);
64+
const thread3 = try std.os.spawnThread(stack2[0..], &shared_ctx, start2);
65+
66+
thread1.wait();
67+
thread2.wait();
68+
thread3.wait();
69+
thread4.wait();
70+
71+
assert(shared_ctx == 4);
72+
}
73+
74+
fn start1(ctx: void) u8 {
75+
return 0;
76+
}
77+
78+
fn start2(ctx: &i32) u8 {
79+
_ = @atomicRmw(i32, ctx, AtomicRmwOp.Add, 1, AtomicOrder.SeqCst);
80+
return 0;
81+
}

std/special/builtin.zig

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,11 +57,46 @@ comptime {
5757
if (builtin.mode != builtin.Mode.ReleaseFast and builtin.os != builtin.Os.windows) {
5858
@export("__stack_chk_fail", __stack_chk_fail, builtin.GlobalLinkage.Strong);
5959
}
60+
if (builtin.os == builtin.Os.linux and builtin.arch == builtin.Arch.x86_64) {
61+
@export("clone", clone, builtin.GlobalLinkage.Strong);
62+
}
6063
}
6164
extern fn __stack_chk_fail() noreturn {
6265
@panic("stack smashing detected");
6366
}
6467

68+
// TODO we should be able to put this directly in std/linux/x86_64.zig but
69+
// it causes a segfault in release mode. this is a workaround of calling it
70+
// across .o file boundaries. fix comptime @ptrCast of nakedcc functions.
71+
nakedcc fn clone() void {
72+
asm volatile (
73+
\\ xor %%eax,%%eax
74+
\\ mov $56,%%al
75+
\\ mov %%rdi,%%r11
76+
\\ mov %%rdx,%%rdi
77+
\\ mov %%r8,%%rdx
78+
\\ mov %%r9,%%r8
79+
\\ mov 8(%%rsp),%%r10
80+
\\ mov %%r11,%%r9
81+
\\ and $-16,%%rsi
82+
\\ sub $8,%%rsi
83+
\\ mov %%rcx,(%%rsi)
84+
\\ syscall
85+
\\ test %%eax,%%eax
86+
\\ jnz 1f
87+
\\ xor %%ebp,%%ebp
88+
\\ pop %%rdi
89+
\\ call *%%r9
90+
\\ mov %%eax,%%edi
91+
\\ xor %%eax,%%eax
92+
\\ mov $60,%%al
93+
\\ syscall
94+
\\ hlt
95+
\\1: ret
96+
\\
97+
);
98+
}
99+
65100
const math = @import("../math/index.zig");
66101

67102
export fn fmodf(x: f32, y: f32) f32 { return generic_fmod(f32, x, y); }

0 commit comments

Comments
 (0)