Skip to content

Commit 4cc5ba4

Browse files
committed
linux: futex v1 API cleanup
* Use `packed struct` for flags arguments. So, instead of `linux.FUTEX.WAIT` use `.{ .cmd = .WAIT, .private = true }` * rename `futex_wait` and `futex_wake` which didn't actually specify wait/wake, as `futex_3arg` and `futex_4arg` (as its the number of parameters that is different, the `op` is whatever is specified. * expose the full six-arg flavor of the syscall (for some of the advanced ops), and add packed structs for their arguments. * Use a `packed union` to support the 4th parameter which is sometimes a `timespec` pointer, and sometimes a `u32`. * Add tests that make sure the structure layout is correct and that the basic argument passing is working (no actual futexes are contended).
1 parent 84c9cee commit 4cc5ba4

File tree

4 files changed

+190
-38
lines changed

4 files changed

+190
-38
lines changed

lib/std/Thread.zig

+3-3
Original file line numberDiff line numberDiff line change
@@ -1539,10 +1539,10 @@ const LinuxThreadImpl = struct {
15391539
continue;
15401540
}
15411541

1542-
switch (linux.E.init(linux.futex_wait(
1542+
switch (linux.E.init(linux.futex_4arg(
15431543
&self.thread.child_tid.raw,
1544-
linux.FUTEX.WAIT,
1545-
tid,
1544+
.{ .cmd = .WAIT, .private = false },
1545+
@bitCast(tid),
15461546
null,
15471547
))) {
15481548
.SUCCESS => continue,

lib/std/Thread/Futex.zig

+8-8
Original file line numberDiff line numberDiff line change
@@ -262,10 +262,10 @@ const LinuxImpl = struct {
262262
ts.nsec = @as(@TypeOf(ts.nsec), @intCast(timeout_ns % std.time.ns_per_s));
263263
}
264264

265-
const rc = linux.futex_wait(
266-
@as(*const i32, @ptrCast(&ptr.raw)),
267-
linux.FUTEX.PRIVATE_FLAG | linux.FUTEX.WAIT,
268-
@as(i32, @bitCast(expect)),
265+
const rc = linux.futex_4arg(
266+
&ptr.raw,
267+
.{ .cmd = .WAIT, .private = true },
268+
expect,
269269
if (timeout != null) &ts else null,
270270
);
271271

@@ -284,10 +284,10 @@ const LinuxImpl = struct {
284284
}
285285

286286
fn wake(ptr: *const atomic.Value(u32), max_waiters: u32) void {
287-
const rc = linux.futex_wake(
288-
@as(*const i32, @ptrCast(&ptr.raw)),
289-
linux.FUTEX.PRIVATE_FLAG | linux.FUTEX.WAKE,
290-
std.math.cast(i32, max_waiters) orelse std.math.maxInt(i32),
287+
const rc = linux.futex_3arg(
288+
&ptr.raw,
289+
.{ .cmd = .WAKE, .private = true },
290+
@min(max_waiters, std.math.maxInt(i32)),
291291
);
292292

293293
switch (linux.E.init(rc)) {

lib/std/os/linux.zig

+89-27
Original file line numberDiff line numberDiff line change
@@ -647,12 +647,34 @@ pub fn fallocate(fd: i32, mode: i32, offset: i64, length: i64) usize {
647647
}
648648
}
649649

650-
pub fn futex_wait(uaddr: *const i32, futex_op: u32, val: i32, timeout: ?*const timespec) usize {
651-
return syscall4(.futex, @intFromPtr(uaddr), futex_op, @as(u32, @bitCast(val)), @intFromPtr(timeout));
650+
// The 4th parameter to the v1 futex syscall can either be an optional
651+
// pointer to a timespec, or a uint32, depending on which "op" is being
652+
// performed.
653+
pub const futex_param4 = extern union {
654+
timeout: ?*const timespec,
655+
/// On all platforms only the bottom 32-bits of `val2` are relevant.
656+
/// This is 64-bit to match the pointer in the union.
657+
val2: usize,
658+
};
659+
660+
/// The futex v1 syscall, see also the newer the futex2_{wait,wakeup,requeue,waitv} syscalls.
661+
///
662+
/// The futex_op parameter is a sub-command and flags. The sub-command
663+
/// defines which of the subsequent paramters are relevant.
664+
pub fn futex(uaddr: *const anyopaque, futex_op: FUTEX_OP, val: u32, val2timeout: futex_param4, uaddr2: ?*const anyopaque, val3: u32) usize {
665+
return syscall6(.futex, @intFromPtr(uaddr), @as(u32, @bitCast(futex_op)), val, @intFromPtr(val2timeout.timeout), @intFromPtr(uaddr2), val3);
666+
}
667+
668+
/// Three-argument variation of the v1 futex call. Only suitable for a
669+
/// futex_op that ignores the remaining arguments (e.g., FUTUX_OP.WAKE).
670+
pub fn futex_3arg(uaddr: *const anyopaque, futex_op: FUTEX_OP, val: u32) usize {
671+
return syscall3(.futex, @intFromPtr(uaddr), @as(u32, @bitCast(futex_op)), val);
652672
}
653673

654-
pub fn futex_wake(uaddr: *const i32, futex_op: u32, val: i32) usize {
655-
return syscall3(.futex, @intFromPtr(uaddr), futex_op, @as(u32, @bitCast(val)));
674+
/// Four-argument variation on the v1 futex call. Only suitable for
675+
/// futex_op that ignores the remaining arguments (e.g., FUTEX_OP.WAIT).
676+
pub fn futex_4arg(uaddr: *const anyopaque, futex_op: FUTEX_OP, val: u32, timeout: ?*const timespec) usize {
677+
return syscall4(.futex, @intFromPtr(uaddr), @as(u32, @bitCast(futex_op)), val, @intFromPtr(timeout));
656678
}
657679

658680
/// Given an array of `futex_waitv`, wait on each uaddr.
@@ -3253,29 +3275,6 @@ pub const FALLOC = struct {
32533275
pub const FL_UNSHARE_RANGE = 0x40;
32543276
};
32553277

3256-
pub const FUTEX = struct {
3257-
pub const WAIT = 0;
3258-
pub const WAKE = 1;
3259-
pub const FD = 2;
3260-
pub const REQUEUE = 3;
3261-
pub const CMP_REQUEUE = 4;
3262-
pub const WAKE_OP = 5;
3263-
pub const LOCK_PI = 6;
3264-
pub const UNLOCK_PI = 7;
3265-
pub const TRYLOCK_PI = 8;
3266-
pub const WAIT_BITSET = 9;
3267-
pub const WAKE_BITSET = 10;
3268-
pub const WAIT_REQUEUE_PI = 11;
3269-
pub const CMP_REQUEUE_PI = 12;
3270-
3271-
pub const PRIVATE_FLAG = 128;
3272-
3273-
pub const CLOCK_REALTIME = 256;
3274-
3275-
/// Max numbers of elements in a `futex_waitv` array.
3276-
pub const WAITV_MAX = 128;
3277-
};
3278-
32793278
pub const FUTEX2 = struct {
32803279
pub const SIZE_U8 = 0x00;
32813280
pub const SIZE_U16 = 0x01;
@@ -3286,6 +3285,69 @@ pub const FUTEX2 = struct {
32863285
pub const PRIVATE = FUTEX.PRIVATE_FLAG;
32873286
};
32883287

3288+
// Futex v1 API commands. See futex man page for each command's
3289+
// interpretation of the futex arguments.
3290+
pub const FUTEX_COMMAND = enum(u7) {
3291+
WAIT = 0,
3292+
WAKE = 1,
3293+
FD = 2,
3294+
REQUEUE = 3,
3295+
CMP_REQUEUE = 4,
3296+
WAKE_OP = 5,
3297+
LOCK_PI = 6,
3298+
UNLOCK_PI = 7,
3299+
TRYLOCK_PI = 8,
3300+
WAIT_BITSET = 9,
3301+
WAKE_BITSET = 10,
3302+
WAIT_REQUEUE_PI = 11,
3303+
CMP_REQUEUE_PI = 12,
3304+
};
3305+
3306+
/// Futex v1 API command and flags for the `futex_op` parameter
3307+
pub const FUTEX_OP = packed struct(u32) {
3308+
cmd: FUTEX_COMMAND,
3309+
private: bool,
3310+
realtime: bool = false, // realtime clock vs. monotonic clock
3311+
_reserved: u23 = 0,
3312+
};
3313+
3314+
/// Futex v1 FUTEX_WAKE_OP `val3` operation:
3315+
pub const FUTEX_WAKE_OP = packed struct(u32) {
3316+
cmd: FUTEX_WAKE_OP_CMD,
3317+
/// From C API `FUTEX_OP_ARG_SHIFT`: Use (1 << oparg) as operand
3318+
arg_shift: bool = false,
3319+
cmp: FUTEX_WAKE_OP_CMP,
3320+
oparg: u12,
3321+
cmdarg: u12,
3322+
};
3323+
3324+
/// Futex v1 cmd for FUTEX_WAKE_OP `val3` command.
3325+
pub const FUTEX_WAKE_OP_CMD = enum(u3) {
3326+
/// uaddr2 = oparg
3327+
SET = 0,
3328+
/// uaddr2 += oparg
3329+
ADD = 1,
3330+
/// uaddr2 |= oparg
3331+
OR = 2,
3332+
/// uaddr2 &= ~oparg
3333+
ANDN = 3,
3334+
/// uaddr2 ^= oparg
3335+
XOR = 4,
3336+
};
3337+
3338+
/// Futex v1 comparison op for FUTEX_WAKE_OP `val3` cmp
3339+
pub const FUTEX_WAKE_OP_CMP = enum(u4) {
3340+
EQ = 0,
3341+
NE = 1,
3342+
LT = 2,
3343+
LE = 3,
3344+
GT = 4,
3345+
GE = 5,
3346+
};
3347+
3348+
/// Max numbers of elements in a `futex_waitv` array.
3349+
pub const FUTEX2_WAITV_MAX = 128;
3350+
32893351
pub const PROT = struct {
32903352
/// page can not be accessed
32913353
pub const NONE = 0x0;

lib/std/os/linux/test.zig

+90
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,96 @@ test "sigset_t" {
152152
try expectEqual(linux.sigismember(&sigset, linux.SIG.USR2), false);
153153
}
154154

155+
comptime {
156+
std.debug.assert(128 == @as(u32, @bitCast(linux.FUTEX_OP{ .cmd = @enumFromInt(0), .private = true, .realtime = false })));
157+
std.debug.assert(256 == @as(u32, @bitCast(linux.FUTEX_OP{ .cmd = @enumFromInt(0), .private = false, .realtime = true })));
158+
159+
// Check futex_param4 union is packed correctly
160+
const param_union = linux.futex_param4{
161+
.val2 = 0xaabbcc,
162+
};
163+
std.debug.assert(@intFromPtr(param_union.timeout) == 0xaabbcc);
164+
}
165+
166+
test "futex v1" {
167+
var lock: std.atomic.Value(u32) = std.atomic.Value(u32).init(1);
168+
var rc: usize = 0;
169+
170+
// No-op wait, lock value is not expected value
171+
rc = linux.futex(&lock.raw, .{ .cmd = .WAIT, .private = true }, 2, .{ .timeout = null }, null, 0);
172+
try expectEqual(.AGAIN, linux.E.init(rc));
173+
174+
rc = linux.futex_4arg(&lock.raw, .{ .cmd = .WAIT, .private = true }, 2, null);
175+
try expectEqual(.AGAIN, linux.E.init(rc));
176+
177+
// Short-fuse wait, timeout kicks in
178+
rc = linux.futex(&lock.raw, .{ .cmd = .WAIT, .private = true }, 1, .{ .timeout = &.{ .sec = 0, .nsec = 2 } }, null, 0);
179+
try expectEqual(.TIMEDOUT, linux.E.init(rc));
180+
181+
rc = linux.futex_4arg(&lock.raw, .{ .cmd = .WAIT, .private = true }, 1, &.{ .sec = 0, .nsec = 2 });
182+
try expectEqual(.TIMEDOUT, linux.E.init(rc));
183+
184+
// Wakeup (no waiters)
185+
rc = linux.futex(&lock.raw, .{ .cmd = .WAKE, .private = true }, 2, .{ .timeout = null }, null, 0);
186+
try expectEqual(0, rc);
187+
188+
rc = linux.futex_3arg(&lock.raw, .{ .cmd = .WAKE, .private = true }, 2);
189+
try expectEqual(0, rc);
190+
191+
// CMP_REQUEUE - val3 mismatch
192+
rc = linux.futex(&lock.raw, .{ .cmd = .CMP_REQUEUE, .private = true }, 2, .{ .val2 = 0 }, null, 99);
193+
try expectEqual(.AGAIN, linux.E.init(rc));
194+
195+
// CMP_REQUEUE - requeue (but no waiters, so ... not much)
196+
{
197+
const val3 = 1;
198+
const wake_nr = 3;
199+
const requeue_max = std.math.maxInt(u31);
200+
var target_lock: std.atomic.Value(u32) = std.atomic.Value(u32).init(1);
201+
rc = linux.futex(&lock.raw, .{ .cmd = .CMP_REQUEUE, .private = true }, wake_nr, .{ .val2 = requeue_max }, &target_lock.raw, val3);
202+
try expectEqual(0, rc);
203+
}
204+
205+
// WAKE_OP - just to see if we can construct the arguments ...
206+
{
207+
var lock2: std.atomic.Value(u32) = std.atomic.Value(u32).init(1);
208+
const wake1_nr = 2;
209+
const wake2_nr = 3;
210+
const wake_op = linux.FUTEX_WAKE_OP{
211+
.cmd = .ANDN,
212+
.arg_shift = true,
213+
.cmp = .LT,
214+
.oparg = 4,
215+
.cmdarg = 5,
216+
};
217+
218+
rc = linux.futex(&lock.raw, .{ .cmd = .WAKE_OP, .private = true }, wake1_nr, .{ .val2 = wake2_nr }, &lock2.raw, @bitCast(wake_op));
219+
try expectEqual(0, rc);
220+
}
221+
222+
// WAIT_BITSET
223+
{
224+
// val1 return early
225+
rc = linux.futex(&lock.raw, .{ .cmd = .WAIT_BITSET, .private = true }, 2, .{ .timeout = null }, null, 0xfff);
226+
try expectEqual(.AGAIN, linux.E.init(rc));
227+
228+
// timeout wait
229+
const timeout: linux.timespec = .{ .sec = 0, .nsec = 2 };
230+
rc = linux.futex(&lock.raw, .{ .cmd = .WAIT_BITSET, .private = true }, 1, .{ .timeout = &timeout }, null, 0xfff);
231+
try expectEqual(.TIMEDOUT, linux.E.init(rc));
232+
}
233+
234+
// WAKE_BITSET
235+
{
236+
rc = linux.futex(&lock.raw, .{ .cmd = .WAKE_BITSET, .private = true }, 2, .{ .timeout = null }, null, 0xfff000);
237+
try expectEqual(0, rc);
238+
239+
// bitmask must have at least 1 bit set:
240+
rc = linux.futex(&lock.raw, .{ .cmd = .WAKE_BITSET, .private = true }, 2, .{ .timeout = null }, null, 0);
241+
try expectEqual(.INVAL, linux.E.init(rc));
242+
}
243+
}
244+
155245
test {
156246
_ = linux.IoUring;
157247
}

0 commit comments

Comments
 (0)