authorgravatar for pat.github@tullmann.orgPat Tullmann <pat.github@tullmann.org> 2025-04-02 16:21:23-07:00
committergravatar for pat.github@tullmann.orgPat Tullmann <pat.github@tullmann.org> 2025-06-18 19:53:50-07:00
log89d15a8d47fdfe41ae650e399d258de3184e6b4d
tree192370e0c48677cf653c65e657e5e441718c8632
parentcfe5defd025b79ade5f9243a7d6ebaad176cb6ad

linux: futex v2 API updates

* `futex2_waitv` always takes a 64-bit timespec. Perhaps the `kernel_timespec` should be renamed `timespec64`? Its used in iouring, too. * Add `packed struct` for futex v2 flags and parameters. * Add very basic "tests" for the futex v2 syscalls (just to ensure the code compiles). * Update the stale or broken comments. (I could also just delete these they're not really documenting Zig-specific behavior.) Given that the futex2 APIs are not used by Zig's library (they're a bit too new), and the fact that these are very specialized syscalls, and they currently provide no benefit over the existing v1 API, I wonder if instead of fixing these up, we should just replace them with a stub that says 'use a 3rd party library'.

2 files changed, 219 insertions(+), 67 deletions(-)

lib/std/os/linux.zig+89-67
...@@ -703,15 +703,13 @@ pub fn futex_4arg(uaddr: *const anyopaque, futex_op: FUTEX_OP, val: u32, timeout...@@ -703,15 +703,13 @@ pub fn futex_4arg(uaddr: *const anyopaque, futex_op: FUTEX_OP, val: u32, timeout
703 return syscall4(.futex, @intFromPtr(uaddr), @as(u32, @bitCast(futex_op)), val, @intFromPtr(timeout));703 return syscall4(.futex, @intFromPtr(uaddr), @as(u32, @bitCast(futex_op)), val, @intFromPtr(timeout));
704}704}
705705
706/// Given an array of `futex_waitv`, wait on each uaddr.706/// Given an array of `futex2_waitone`, wait on each uaddr.
707/// The thread wakes if a futex_wake() is performed at any uaddr.707/// The thread wakes if a futex_wake() is performed at any uaddr.
708/// The syscall returns immediately if any waiter has *uaddr != val.708/// The syscall returns immediately if any futex has *uaddr != val.
709/// timeout is an optional timeout value for the operation.709/// timeout is an optional, absolute timeout value for the operation.
710/// Each waiter has individual flags.710/// The `flags` argument is for future use and currently should be `.{}`.
711/// The `flags` argument for the syscall should be used solely for specifying711/// Flags for private futexes, sizes, etc. should be set on the
712/// the timeout as realtime, if needed.712/// individual flags of each `futex2_waitone`.
713/// Flags for private futexes, sizes, etc. should be used on the
714/// individual flags of each waiter.
715///713///
716/// Returns the array index of one of the woken futexes.714/// Returns the array index of one of the woken futexes.
717/// No further information is provided: any number of other futexes may also715/// No further information is provided: any number of other futexes may also
...@@ -719,42 +717,43 @@ pub fn futex_4arg(uaddr: *const anyopaque, futex_op: FUTEX_OP, val: u32, timeout...@@ -719,42 +717,43 @@ pub fn futex_4arg(uaddr: *const anyopaque, futex_op: FUTEX_OP, val: u32, timeout
719/// the returned index may refer to any one of them.717/// the returned index may refer to any one of them.
720/// (It is not necessaryily the futex with the smallest index, nor the one718/// (It is not necessaryily the futex with the smallest index, nor the one
721/// most recently woken, nor...)719/// most recently woken, nor...)
720///
721/// Requires at least kernel v5.16.
722pub fn futex2_waitv(722pub fn futex2_waitv(
723 /// List of futexes to wait on.723 futexes: [*]const futex2_waitone,
724 waiters: [*]futex_waitv,724 /// Length of `futexes`. Max of FUTEX2_WAITONE_MAX.
725 /// Length of `waiters`.
726 nr_futexes: u32,725 nr_futexes: u32,
727 /// Flag for timeout (monotonic/realtime).726 flags: FUTEX2_FLAGS_WAITV,
728 flags: u32,727 /// Optional absolute timeout. Always 64-bit, even on 32-bit platforms.
729 /// Optional absolute timeout.728 timeout: ?*const kernel_timespec,
730 timeout: ?*const timespec,
731 /// Clock to be used for the timeout, realtime or monotonic.729 /// Clock to be used for the timeout, realtime or monotonic.
732 clockid: clockid_t,730 clockid: clockid_t,
733) usize {731) usize {
734 return syscall5(732 return syscall5(
735 .futex_waitv,733 .futex_waitv,
736 @intFromPtr(waiters),734 @intFromPtr(futexes),
737 nr_futexes,735 nr_futexes,
738 flags,736 @as(u32, @bitCast(flags)),
739 @intFromPtr(timeout),737 @intFromPtr(timeout),
740 @bitCast(@as(isize, @intFromEnum(clockid))),738 @intFromEnum(clockid),
741 );739 );
742}740}
743741
744/// Wait on a futex.742/// Wait on a single futex.
745/// Identical to the traditional `FUTEX.FUTEX_WAIT_BITSET` op, except it is part of the743/// Identical to the futex v1 `FUTEX.FUTEX_WAIT_BITSET` op, except it is part of the
746/// futex2 familiy of calls.744/// futex2 family of calls.
745///
746/// Requires at least kernel v6.7.
747pub fn futex2_wait(747pub fn futex2_wait(
748 /// Address of the futex to wait on.748 /// Address of the futex to wait on.
749 uaddr: *const anyopaque,749 uaddr: *const anyopaque,
750 /// Value of `uaddr`.750 /// Value of `uaddr`.
751 val: usize,751 val: usize,
752 /// Bitmask.752 /// Bitmask to match against incoming wakeup masks. Must not be zero.
753 mask: usize,753 mask: usize,
754 /// `FUTEX2` flags.754 flags: FUTEX2_FLAGS,
755 flags: u32,755 /// Optional absolute timeout. Always 64-bit, even on 32-bit platforms.
756 /// Optional absolute timeout.756 timeout: ?*const kernel_timespec,
757 timeout: ?*const timespec,
758 /// Clock to be used for the timeout, realtime or monotonic.757 /// Clock to be used for the timeout, realtime or monotonic.
759 clockid: clockid_t,758 clockid: clockid_t,
760) usize {759) usize {
...@@ -763,52 +762,55 @@ pub fn futex2_wait(...@@ -763,52 +762,55 @@ pub fn futex2_wait(
763 @intFromPtr(uaddr),762 @intFromPtr(uaddr),
764 val,763 val,
765 mask,764 mask,
766 flags,765 @as(u32, @bitCast(flags)),
767 @intFromPtr(timeout),766 @intFromPtr(timeout),
768 @bitCast(@as(isize, @intFromEnum(clockid))),767 @intFromEnum(clockid),
769 );768 );
770}769}
771770
772/// Wake a number of futexes.771/// Wake (subset of) waiters on given futex.
773/// Identical to the traditional `FUTEX.FUTEX_WAIT_BITSET` op, except it is part of the772/// Identical to the traditional `FUTEX.FUTEX_WAKE_BITSET` op, except it is part of the
774/// futex2 family of calls.773/// futex2 family of calls.
774///
775/// Requires at least kernel v6.7.
775pub fn futex2_wake(776pub fn futex2_wake(
776 /// Address of the futex(es) to wake.777 /// Futex to wake
777 uaddr: *const anyopaque,778 uaddr: *const anyopaque,
778 /// Bitmask779 /// Bitmask to match against waiters.
779 mask: usize,780 mask: usize,
780 /// Number of the futexes to wake.781 /// Maximum number of waiters on the futex to wake.
781 nr: i32,782 nr_wake: i32,
782 /// `FUTEX2` flags.783 flags: FUTEX2_FLAGS,
783 flags: u32,
784) usize {784) usize {
785 return syscall4(785 return syscall4(
786 .futex_wake,786 .futex_wake,
787 @intFromPtr(uaddr),787 @intFromPtr(uaddr),
788 mask,788 mask,
789 @bitCast(@as(isize, nr)),789 @as(u32, @bitCast(nr_wake)),
790 flags,790 @as(u32, @bitCast(flags)),
791 );791 );
792}792}
793793
794/// Requeue a waiter from one futex to another.794/// Wake and/or requeue waiter(s) from one futex to another.
795/// Identical to `FUTEX.CMP_REQUEUE`, except it is part of the futex2 family of calls.795/// Identical to `FUTEX.CMP_REQUEUE`, except it is part of the futex2 family of calls.
796///
797/// Requires at least kernel v6.7.
796pub fn futex2_requeue(798pub fn futex2_requeue(
797 /// Array describing the source and destination futex.799 /// The source and destination futexes. Must be a 2-element array.
798 waiters: [*]futex_waitv,800 waiters: [*]const futex2_waitone,
799 /// Unused.801 /// Currently unused.
800 flags: u32,802 flags: FUTEX2_FLAGS_REQUEUE,
801 /// Number of futexes to wake.803 /// Maximum number of waiters to wake on the source futex.
802 nr_wake: i32,804 nr_wake: i32,
803 /// Number of futexes to requeue.805 /// Maximum number of waiters to transfer to the destination futex.
804 nr_requeue: i32,806 nr_requeue: i32,
805) usize {807) usize {
806 return syscall4(808 return syscall4(
807 .futex_requeue,809 .futex_requeue,
808 @intFromPtr(waiters),810 @intFromPtr(waiters),
809 flags,811 @as(u32, @bitCast(flags)),
810 @bitCast(@as(isize, nr_wake)),812 @as(u32, @bitCast(nr_wake)),
811 @bitCast(@as(isize, nr_requeue)),813 @as(u32, @bitCast(nr_requeue)),
812 );814 );
813}815}
814816
...@@ -3407,16 +3409,6 @@ pub const FALLOC = struct {...@@ -3407,16 +3409,6 @@ pub const FALLOC = struct {
3407 pub const FL_UNSHARE_RANGE = 0x40;3409 pub const FL_UNSHARE_RANGE = 0x40;
3408};3410};
34093411
3410pub const FUTEX2 = struct {
3411 pub const SIZE_U8 = 0x00;
3412 pub const SIZE_U16 = 0x01;
3413 pub const SIZE_U32 = 0x02;
3414 pub const SIZE_U64 = 0x03;
3415 pub const NUMA = 0x04;
3416
3417 pub const PRIVATE = FUTEX.PRIVATE_FLAG;
3418};
3419
3420// Futex v1 API commands. See futex man page for each command's3412// Futex v1 API commands. See futex man page for each command's
3421// interpretation of the futex arguments.3413// interpretation of the futex arguments.
3422pub const FUTEX_COMMAND = enum(u7) {3414pub const FUTEX_COMMAND = enum(u7) {
...@@ -3477,8 +3469,38 @@ pub const FUTEX_WAKE_OP_CMP = enum(u4) {...@@ -3477,8 +3469,38 @@ pub const FUTEX_WAKE_OP_CMP = enum(u4) {
3477 GE = 5,3469 GE = 5,
3478};3470};
34793471
3480/// Max numbers of elements in a `futex_waitv` array.3472/// Max numbers of elements in a `futex2_waitone` array.
3481pub const FUTEX2_WAITV_MAX = 128;3473pub const FUTEX2_WAITONE_MAX = 128;
3474
3475/// For futex v2 API, the size of the futex at the uaddr. v1 futex are
3476/// always implicitly U32. As of kernel v6.14, only U32 is implemented
3477/// for v2 futexes.
3478pub const FUTEX2_SIZE = enum(u2) {
3479 U8 = 0,
3480 U16 = 1,
3481 U32 = 2,
3482 U64 = 3,
3483};
3484
3485/// As of kernel 6.14 there are no defined flags to futex2_waitv.
3486pub const FUTEX2_FLAGS_WAITV = packed struct(u32) {
3487 _reserved: u32 = 0,
3488};
3489
3490/// As of kernel 6.14 there are no defined flags to futex2_requeue.
3491pub const FUTEX2_FLAGS_REQUEUE = packed struct(u32) {
3492 _reserved: u32 = 0,
3493};
3494
3495/// Flags for futex v2 APIs (futex2_wait, futex2_wake, futex2_requeue, but
3496/// not the futex2_waitv syscall, but also used in the futex2_waitone struct).
3497pub const FUTEX2_FLAGS = packed struct(u32) {
3498 size: FUTEX2_SIZE,
3499 numa: bool = false,
3500 _reserved: u4 = 0,
3501 private: bool,
3502 _undefined: u24 = 0,
3503};
34823504
3483pub const PROT = struct {3505pub const PROT = struct {
3484 /// page can not be accessed3506 /// page can not be accessed
...@@ -9343,17 +9365,17 @@ pub const PTRACE = struct {...@@ -9343,17 +9365,17 @@ pub const PTRACE = struct {
9343 pub const GET_SYSCALL_INFO = 0x420e;9365 pub const GET_SYSCALL_INFO = 0x420e;
9344};9366};
93459367
9346/// A waiter for vectorized wait.9368/// For futex2_waitv and futex2_requeue. Arrays of `futex2_waitone` allow
9347pub const futex_waitv = extern struct {9369/// waiting on multiple futexes in one call.
9348 // Expected value at uaddr9370pub const futex2_waitone = extern struct {
9371 /// Expected value at uaddr, should match size of futex.
9349 val: u64,9372 val: u64,
9350 /// User address to wait on.9373 /// User address to wait on. Top-bits must be 0 on 32-bit.
9351 uaddr: u64,9374 uaddr: u64,
9352 /// Flags for this waiter.9375 /// Flags for this waiter.
9353 flags: u32,9376 flags: FUTEX2_FLAGS,
9354 /// Reserved member to preserve alignment.9377 /// Reserved member to preserve alignment.
9355 /// Should be 0.9378 __reserved: u32 = 0,
9356 __reserved: u32,
9357};9379};
93589380
9359pub const cache_stat_range = extern struct {9381pub const cache_stat_range = extern struct {
lib/std/os/linux/test.zig+130
...@@ -297,6 +297,136 @@ test "futex v1" {...@@ -297,6 +297,136 @@ test "futex v1" {
297 }297 }
298}298}
299299
300comptime {
301 std.debug.assert(2 == @as(u32, @bitCast(linux.FUTEX2_FLAGS{ .size = .U32, .private = false })));
302 std.debug.assert(128 == @as(u32, @bitCast(linux.FUTEX2_FLAGS{ .size = @enumFromInt(0), .private = true })));
303}
304
305test "futex2_waitv" {
306 const locks = [_]std.atomic.Value(u32){
307 std.atomic.Value(u32).init(1),
308 std.atomic.Value(u32).init(1),
309 std.atomic.Value(u32).init(1),
310 };
311
312 const futexes = [_]linux.futex2_waitone{
313 .{
314 .val = 1,
315 .uaddr = @intFromPtr(&locks[0].raw),
316 .flags = .{ .size = .U32, .private = true },
317 },
318 .{
319 .val = 1,
320 .uaddr = @intFromPtr(&locks[1].raw),
321 .flags = .{ .size = .U32, .private = true },
322 },
323 .{
324 .val = 1,
325 .uaddr = @intFromPtr(&locks[2].raw),
326 .flags = .{ .size = .U32, .private = true },
327 },
328 };
329
330 const timeout = linux.kernel_timespec{ .sec = 0, .nsec = 2 }; // absolute timeout, so this is 1970...
331 const rc = linux.futex2_waitv(&futexes, futexes.len, .{}, &timeout, .MONOTONIC);
332 switch (linux.E.init(rc)) {
333 .NOSYS => return error.SkipZigTest, // futex2_waitv added in kernel v5.16
334 else => |err| try expectEqual(.TIMEDOUT, err),
335 }
336}
337
338// Futex v2 API is only supported on recent kernels (v6.7), so skip tests if the syscalls
339// return ENOSYS.
340fn futex2_skip_if_unsupported() !void {
341 const lock: u32 = 0;
342 const rc = linux.futex2_wake(&lock, 0, 1, .{ .size = .U32, .private = true });
343 if (linux.E.init(rc) == .NOSYS) {
344 return error.SkipZigTest;
345 }
346}
347
348test "futex2_wait" {
349 var lock: std.atomic.Value(u32) = std.atomic.Value(u32).init(1);
350 var rc: usize = 0;
351 const mask = 0x1;
352
353 try futex2_skip_if_unsupported();
354
355 // The API for 8,16,64 bit futexes is defined, but as of kernel v6.14
356 // (at least) they're not implemented.
357 if (false) {
358 rc = linux.futex2_wait(&lock.raw, 1, mask, .{ .size = .U8, .private = true }, null, .MONOTONIC);
359 try expectEqual(.INVAL, linux.E.init(rc));
360
361 rc = linux.futex2_wait(&lock.raw, 1, mask, .{ .size = .U16, .private = true }, null, .MONOTONIC);
362 try expectEqual(.INVAL, linux.E.init(rc));
363
364 rc = linux.futex2_wait(&lock.raw, 1, mask, .{ .size = .U64, .private = true }, null, .MONOTONIC);
365 try expectEqual(.INVAL, linux.E.init(rc));
366 }
367
368 const flags = linux.FUTEX2_FLAGS{ .size = .U32, .private = true };
369 // no-wait, lock state mismatch
370 rc = linux.futex2_wait(&lock.raw, 2, mask, flags, null, .MONOTONIC);
371 try expectEqual(.AGAIN, linux.E.init(rc));
372
373 // hit timeout on wait
374 rc = linux.futex2_wait(&lock.raw, 1, mask, flags, &.{ .sec = 0, .nsec = 2 }, .MONOTONIC);
375 try expectEqual(.TIMEDOUT, linux.E.init(rc));
376
377 // timeout is absolute
378 {
379 var curr: linux.timespec = undefined;
380 rc = linux.clock_gettime(.MONOTONIC, &curr); // gettime() uses platform timespec
381 try expectEqual(0, rc);
382
383 // ... but futex2_wait always uses 64-bit timespec
384 var timeout: linux.kernel_timespec = .{
385 .sec = curr.sec,
386 .nsec = curr.nsec + 2,
387 };
388 rc = linux.futex2_wait(&lock.raw, 1, mask, flags, &timeout, .MONOTONIC);
389 try expectEqual(.TIMEDOUT, linux.E.init(rc));
390 }
391
392 rc = linux.futex2_wait(&lock.raw, 1, mask, flags, &.{ .sec = 0, .nsec = 2 }, .REALTIME);
393 try expectEqual(.TIMEDOUT, linux.E.init(rc));
394}
395
396test "futex2_wake" {
397 var lock: std.atomic.Value(u32) = std.atomic.Value(u32).init(1);
398
399 try futex2_skip_if_unsupported();
400
401 const rc = linux.futex2_wake(&lock.raw, 0xFF, 1, .{ .size = .U32, .private = true });
402 try expectEqual(0, rc);
403}
404
405test "futex2_requeue" {
406 try futex2_skip_if_unsupported();
407
408 const locks = [_]std.atomic.Value(u32){
409 std.atomic.Value(u32).init(1),
410 std.atomic.Value(u32).init(1),
411 };
412
413 const futexes = [_]linux.futex2_waitone{
414 .{
415 .val = 1,
416 .uaddr = @intFromPtr(&locks[0].raw),
417 .flags = .{ .size = .U32, .private = true },
418 },
419 .{
420 .val = 1,
421 .uaddr = @intFromPtr(&locks[1].raw),
422 .flags = .{ .size = .U32, .private = true },
423 },
424 };
425
426 const rc = linux.futex2_requeue(&futexes, .{}, 2, 2);
427 try expectEqual(0, rc);
428}
429
300test {430test {
301 _ = linux.IoUring;431 _ = linux.IoUring;
302}432}