authorgravatar for pat.github@tullmann.orgPat Tullmann <pat.github@tullmann.org> 2025-04-02 16:14:50-07:00
committergravatar for pat.github@tullmann.orgPat Tullmann <pat.github@tullmann.org> 2025-06-17 22:06:39-07:00
logcfe5defd025b79ade5f9243a7d6ebaad176cb6ad
tree7f85cd11d09dac313910d64610a8ceae649360bc
parent850655f06b7dd8dc6e637d4980e6b7f9ac1b43d9

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).

4 files changed, 190 insertions(+), 38 deletions(-)

lib/std/Thread.zig+3-3
...@@ -1539,10 +1539,10 @@ const LinuxThreadImpl = struct {...@@ -1539,10 +1539,10 @@ const LinuxThreadImpl = struct {
1539 continue;1539 continue;
1540 }1540 }
15411541
1542 switch (linux.E.init(linux.futex_wait(1542 switch (linux.E.init(linux.futex_4arg(
1543 &self.thread.child_tid.raw,1543 &self.thread.child_tid.raw,
1544 linux.FUTEX.WAIT,1544 .{ .cmd = .WAIT, .private = false },
1545 tid,1545 @bitCast(tid),
1546 null,1546 null,
1547 ))) {1547 ))) {
1548 .SUCCESS => continue,1548 .SUCCESS => continue,
lib/std/Thread/Futex.zig+8-8
...@@ -262,10 +262,10 @@ const LinuxImpl = struct {...@@ -262,10 +262,10 @@ const LinuxImpl = struct {
262 ts.nsec = @as(@TypeOf(ts.nsec), @intCast(timeout_ns % std.time.ns_per_s));262 ts.nsec = @as(@TypeOf(ts.nsec), @intCast(timeout_ns % std.time.ns_per_s));
263 }263 }
264264
265 const rc = linux.futex_wait(265 const rc = linux.futex_4arg(
266 @as(*const i32, @ptrCast(&ptr.raw)),266 &ptr.raw,
267 linux.FUTEX.PRIVATE_FLAG | linux.FUTEX.WAIT,267 .{ .cmd = .WAIT, .private = true },
268 @as(i32, @bitCast(expect)),268 expect,
269 if (timeout != null) &ts else null,269 if (timeout != null) &ts else null,
270 );270 );
271271
...@@ -284,10 +284,10 @@ const LinuxImpl = struct {...@@ -284,10 +284,10 @@ const LinuxImpl = struct {
284 }284 }
285285
286 fn wake(ptr: *const atomic.Value(u32), max_waiters: u32) void {286 fn wake(ptr: *const atomic.Value(u32), max_waiters: u32) void {
287 const rc = linux.futex_wake(287 const rc = linux.futex_3arg(
288 @as(*const i32, @ptrCast(&ptr.raw)),288 &ptr.raw,
289 linux.FUTEX.PRIVATE_FLAG | linux.FUTEX.WAKE,289 .{ .cmd = .WAKE, .private = true },
290 std.math.cast(i32, max_waiters) orelse std.math.maxInt(i32),290 @min(max_waiters, std.math.maxInt(i32)),
291 );291 );
292292
293 switch (linux.E.init(rc)) {293 switch (linux.E.init(rc)) {
lib/std/os/linux.zig+89-27
...@@ -673,12 +673,34 @@ pub fn fallocate(fd: i32, mode: i32, offset: i64, length: i64) usize {...@@ -673,12 +673,34 @@ pub fn fallocate(fd: i32, mode: i32, offset: i64, length: i64) usize {
673 }673 }
674}674}
675675
676pub fn futex_wait(uaddr: *const i32, futex_op: u32, val: i32, timeout: ?*const timespec) usize {676// The 4th parameter to the v1 futex syscall can either be an optional
677 return syscall4(.futex, @intFromPtr(uaddr), futex_op, @as(u32, @bitCast(val)), @intFromPtr(timeout));677// pointer to a timespec, or a uint32, depending on which "op" is being
678// performed.
679pub const futex_param4 = extern union {
680 timeout: ?*const timespec,
681 /// On all platforms only the bottom 32-bits of `val2` are relevant.
682 /// This is 64-bit to match the pointer in the union.
683 val2: usize,
684};
685
686/// The futex v1 syscall, see also the newer the futex2_{wait,wakeup,requeue,waitv} syscalls.
687///
688/// The futex_op parameter is a sub-command and flags. The sub-command
689/// defines which of the subsequent paramters are relevant.
690pub fn futex(uaddr: *const anyopaque, futex_op: FUTEX_OP, val: u32, val2timeout: futex_param4, uaddr2: ?*const anyopaque, val3: u32) usize {
691 return syscall6(.futex, @intFromPtr(uaddr), @as(u32, @bitCast(futex_op)), val, @intFromPtr(val2timeout.timeout), @intFromPtr(uaddr2), val3);
692}
693
694/// Three-argument variation of the v1 futex call. Only suitable for a
695/// futex_op that ignores the remaining arguments (e.g., FUTUX_OP.WAKE).
696pub fn futex_3arg(uaddr: *const anyopaque, futex_op: FUTEX_OP, val: u32) usize {
697 return syscall3(.futex, @intFromPtr(uaddr), @as(u32, @bitCast(futex_op)), val);
678}698}
679699
680pub fn futex_wake(uaddr: *const i32, futex_op: u32, val: i32) usize {700/// Four-argument variation on the v1 futex call. Only suitable for
681 return syscall3(.futex, @intFromPtr(uaddr), futex_op, @as(u32, @bitCast(val)));701/// futex_op that ignores the remaining arguments (e.g., FUTEX_OP.WAIT).
702pub fn futex_4arg(uaddr: *const anyopaque, futex_op: FUTEX_OP, val: u32, timeout: ?*const timespec) usize {
703 return syscall4(.futex, @intFromPtr(uaddr), @as(u32, @bitCast(futex_op)), val, @intFromPtr(timeout));
682}704}
683705
684/// Given an array of `futex_waitv`, wait on each uaddr.706/// Given an array of `futex_waitv`, wait on each uaddr.
...@@ -3385,29 +3407,6 @@ pub const FALLOC = struct {...@@ -3385,29 +3407,6 @@ pub const FALLOC = struct {
3385 pub const FL_UNSHARE_RANGE = 0x40;3407 pub const FL_UNSHARE_RANGE = 0x40;
3386};3408};
33873409
3388pub const FUTEX = struct {
3389 pub const WAIT = 0;
3390 pub const WAKE = 1;
3391 pub const FD = 2;
3392 pub const REQUEUE = 3;
3393 pub const CMP_REQUEUE = 4;
3394 pub const WAKE_OP = 5;
3395 pub const LOCK_PI = 6;
3396 pub const UNLOCK_PI = 7;
3397 pub const TRYLOCK_PI = 8;
3398 pub const WAIT_BITSET = 9;
3399 pub const WAKE_BITSET = 10;
3400 pub const WAIT_REQUEUE_PI = 11;
3401 pub const CMP_REQUEUE_PI = 12;
3402
3403 pub const PRIVATE_FLAG = 128;
3404
3405 pub const CLOCK_REALTIME = 256;
3406
3407 /// Max numbers of elements in a `futex_waitv` array.
3408 pub const WAITV_MAX = 128;
3409};
3410
3411pub const FUTEX2 = struct {3410pub const FUTEX2 = struct {
3412 pub const SIZE_U8 = 0x00;3411 pub const SIZE_U8 = 0x00;
3413 pub const SIZE_U16 = 0x01;3412 pub const SIZE_U16 = 0x01;
...@@ -3418,6 +3417,69 @@ pub const FUTEX2 = struct {...@@ -3418,6 +3417,69 @@ pub const FUTEX2 = struct {
3418 pub const PRIVATE = FUTEX.PRIVATE_FLAG;3417 pub const PRIVATE = FUTEX.PRIVATE_FLAG;
3419};3418};
34203419
3420// Futex v1 API commands. See futex man page for each command's
3421// interpretation of the futex arguments.
3422pub const FUTEX_COMMAND = enum(u7) {
3423 WAIT = 0,
3424 WAKE = 1,
3425 FD = 2,
3426 REQUEUE = 3,
3427 CMP_REQUEUE = 4,
3428 WAKE_OP = 5,
3429 LOCK_PI = 6,
3430 UNLOCK_PI = 7,
3431 TRYLOCK_PI = 8,
3432 WAIT_BITSET = 9,
3433 WAKE_BITSET = 10,
3434 WAIT_REQUEUE_PI = 11,
3435 CMP_REQUEUE_PI = 12,
3436};
3437
3438/// Futex v1 API command and flags for the `futex_op` parameter
3439pub const FUTEX_OP = packed struct(u32) {
3440 cmd: FUTEX_COMMAND,
3441 private: bool,
3442 realtime: bool = false, // realtime clock vs. monotonic clock
3443 _reserved: u23 = 0,
3444};
3445
3446/// Futex v1 FUTEX_WAKE_OP `val3` operation:
3447pub const FUTEX_WAKE_OP = packed struct(u32) {
3448 cmd: FUTEX_WAKE_OP_CMD,
3449 /// From C API `FUTEX_OP_ARG_SHIFT`: Use (1 << oparg) as operand
3450 arg_shift: bool = false,
3451 cmp: FUTEX_WAKE_OP_CMP,
3452 oparg: u12,
3453 cmdarg: u12,
3454};
3455
3456/// Futex v1 cmd for FUTEX_WAKE_OP `val3` command.
3457pub const FUTEX_WAKE_OP_CMD = enum(u3) {
3458 /// uaddr2 = oparg
3459 SET = 0,
3460 /// uaddr2 += oparg
3461 ADD = 1,
3462 /// uaddr2 |= oparg
3463 OR = 2,
3464 /// uaddr2 &= ~oparg
3465 ANDN = 3,
3466 /// uaddr2 ^= oparg
3467 XOR = 4,
3468};
3469
3470/// Futex v1 comparison op for FUTEX_WAKE_OP `val3` cmp
3471pub const FUTEX_WAKE_OP_CMP = enum(u4) {
3472 EQ = 0,
3473 NE = 1,
3474 LT = 2,
3475 LE = 3,
3476 GT = 4,
3477 GE = 5,
3478};
3479
3480/// Max numbers of elements in a `futex_waitv` array.
3481pub const FUTEX2_WAITV_MAX = 128;
3482
3421pub const PROT = struct {3483pub const PROT = struct {
3422 /// page can not be accessed3484 /// page can not be accessed
3423 pub const NONE = 0x0;3485 pub const NONE = 0x0;
lib/std/os/linux/test.zig+90
...@@ -207,6 +207,96 @@ test "sysinfo" {...@@ -207,6 +207,96 @@ test "sysinfo" {
207 try expect(info.mem_unit <= std.heap.page_size_max);207 try expect(info.mem_unit <= std.heap.page_size_max);
208}208}
209209
210comptime {
211 std.debug.assert(128 == @as(u32, @bitCast(linux.FUTEX_OP{ .cmd = @enumFromInt(0), .private = true, .realtime = false })));
212 std.debug.assert(256 == @as(u32, @bitCast(linux.FUTEX_OP{ .cmd = @enumFromInt(0), .private = false, .realtime = true })));
213
214 // Check futex_param4 union is packed correctly
215 const param_union = linux.futex_param4{
216 .val2 = 0xaabbcc,
217 };
218 std.debug.assert(@intFromPtr(param_union.timeout) == 0xaabbcc);
219}
220
221test "futex v1" {
222 var lock: std.atomic.Value(u32) = std.atomic.Value(u32).init(1);
223 var rc: usize = 0;
224
225 // No-op wait, lock value is not expected value
226 rc = linux.futex(&lock.raw, .{ .cmd = .WAIT, .private = true }, 2, .{ .timeout = null }, null, 0);
227 try expectEqual(.AGAIN, linux.E.init(rc));
228
229 rc = linux.futex_4arg(&lock.raw, .{ .cmd = .WAIT, .private = true }, 2, null);
230 try expectEqual(.AGAIN, linux.E.init(rc));
231
232 // Short-fuse wait, timeout kicks in
233 rc = linux.futex(&lock.raw, .{ .cmd = .WAIT, .private = true }, 1, .{ .timeout = &.{ .sec = 0, .nsec = 2 } }, null, 0);
234 try expectEqual(.TIMEDOUT, linux.E.init(rc));
235
236 rc = linux.futex_4arg(&lock.raw, .{ .cmd = .WAIT, .private = true }, 1, &.{ .sec = 0, .nsec = 2 });
237 try expectEqual(.TIMEDOUT, linux.E.init(rc));
238
239 // Wakeup (no waiters)
240 rc = linux.futex(&lock.raw, .{ .cmd = .WAKE, .private = true }, 2, .{ .timeout = null }, null, 0);
241 try expectEqual(0, rc);
242
243 rc = linux.futex_3arg(&lock.raw, .{ .cmd = .WAKE, .private = true }, 2);
244 try expectEqual(0, rc);
245
246 // CMP_REQUEUE - val3 mismatch
247 rc = linux.futex(&lock.raw, .{ .cmd = .CMP_REQUEUE, .private = true }, 2, .{ .val2 = 0 }, null, 99);
248 try expectEqual(.AGAIN, linux.E.init(rc));
249
250 // CMP_REQUEUE - requeue (but no waiters, so ... not much)
251 {
252 const val3 = 1;
253 const wake_nr = 3;
254 const requeue_max = std.math.maxInt(u31);
255 var target_lock: std.atomic.Value(u32) = std.atomic.Value(u32).init(1);
256 rc = linux.futex(&lock.raw, .{ .cmd = .CMP_REQUEUE, .private = true }, wake_nr, .{ .val2 = requeue_max }, &target_lock.raw, val3);
257 try expectEqual(0, rc);
258 }
259
260 // WAKE_OP - just to see if we can construct the arguments ...
261 {
262 var lock2: std.atomic.Value(u32) = std.atomic.Value(u32).init(1);
263 const wake1_nr = 2;
264 const wake2_nr = 3;
265 const wake_op = linux.FUTEX_WAKE_OP{
266 .cmd = .ANDN,
267 .arg_shift = true,
268 .cmp = .LT,
269 .oparg = 4,
270 .cmdarg = 5,
271 };
272
273 rc = linux.futex(&lock.raw, .{ .cmd = .WAKE_OP, .private = true }, wake1_nr, .{ .val2 = wake2_nr }, &lock2.raw, @bitCast(wake_op));
274 try expectEqual(0, rc);
275 }
276
277 // WAIT_BITSET
278 {
279 // val1 return early
280 rc = linux.futex(&lock.raw, .{ .cmd = .WAIT_BITSET, .private = true }, 2, .{ .timeout = null }, null, 0xfff);
281 try expectEqual(.AGAIN, linux.E.init(rc));
282
283 // timeout wait
284 const timeout: linux.timespec = .{ .sec = 0, .nsec = 2 };
285 rc = linux.futex(&lock.raw, .{ .cmd = .WAIT_BITSET, .private = true }, 1, .{ .timeout = &timeout }, null, 0xfff);
286 try expectEqual(.TIMEDOUT, linux.E.init(rc));
287 }
288
289 // WAKE_BITSET
290 {
291 rc = linux.futex(&lock.raw, .{ .cmd = .WAKE_BITSET, .private = true }, 2, .{ .timeout = null }, null, 0xfff000);
292 try expectEqual(0, rc);
293
294 // bitmask must have at least 1 bit set:
295 rc = linux.futex(&lock.raw, .{ .cmd = .WAKE_BITSET, .private = true }, 2, .{ .timeout = null }, null, 0);
296 try expectEqual(.INVAL, linux.E.init(rc));
297 }
298}
299
210test {300test {
211 _ = linux.IoUring;301 _ = linux.IoUring;
212}302}