You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

针对uint8_t类型变量使用Linux futex系统调用的内存访问未定义行为相关技术问询

针对uint8_t类型变量使用Linux futex系统调用的内存访问未定义行为相关技术问询

我来一步步拆解你的问题,帮你理清这些关于futex和C++未定义行为(UB)的疑问:


1. 标记的代码行确实是UB吗?程序里还有其他UB吗?

先看你标记的这行:

uint32_t futex_val = *futex_addr;

这行绝对是UB,主要有两个核心原因:

  • 违反严格别名规则:C++标准规定,只有char/unsigned char类型的指针可以合法访问任意类型的对象内存,其他类型的指针不能跨类型访问。这里futex_addr是uint32_t*,但它指向的内存实际是由uint8_t类型的全局变量(before、wait_condition、after)组成的,用uint32_t*解引用读取这些内存,直接违反了严格别名规则——编译器会基于这个规则做激进优化,完全无视你的代码逻辑。
  • 非法访问对象外内存:futex_addr是包含wait_condition的4字节对齐块指针,这意味着它必然会覆盖wait_condition之外的内存(比如before或after的字节)。你通过*futex_addr读取不属于wait_condition的变量内存,属于非法访问对象的内存范围,这也是明确的UB。

那程序里还有其他UB吗?有的:

  • calculate_futex_ptr返回的uint32_t*指针,并没有指向一个合法的uint32_t对象。C标准要求指针必须指向有效的对象(或对象末尾的下一个地址),所以这个指针本身就是“无效”的——虽然Linux futex系统调用只认地址数值,但从C标准的角度,这属于UB。
  • futex_wake中传递的uint32_t*指针同样指向的不是合法的uint32_t对象,本质也是UB,只是因为futex_wake不需要解引用该指针,所以在大多数情况下不会立刻出问题,但风险依然存在。

2. 这种UB会导致哪些实际的异常结果?

UB的可怕之处在于行为完全不可预测,举几个实际场景你就明白了:

  • 编译器优化导致逻辑混乱:因为严格别名规则,编译器会认为uint32_t*的访问和uint8_t变量的修改是完全无关的。比如它可能把*futex_addr的读取操作提前到线程修改wait_condition之前,导致你获取的是旧值,futex_wait会一直阻塞;或者直接优化掉这个读取操作,让futex_val变成随机值。
  • 内存越界导致崩溃:如果wait_condition刚好位于一个内存页的最后1字节,calculate_futex_ptr返回的指针会指向相邻的内存页。如果那个页没有被系统映射(比如是空白页),解引用这个指针会直接触发段错误,程序崩溃。
  • 意外读取其他变量的值:假设after变量被另一个线程频繁修改,当你读取*futex_addr时,会把after的当前值也读进来,拼凑出的futex_val和实际预期的wait_condition值完全不符,导致futex_wait要么提前唤醒,要么永远阻塞。

3. 怎么避免这些UB?

给你几个可行的方案,按推荐程度排序:

方案一:直接使用uint32_t原子变量

这是最简单的解决办法,既然futex要求4字节对齐的uint32_t,那直接用std::atomic<uint32_t>来存储你的条件值,地址天然对齐,完全符合futex的要求,没有任何UB:

std::atomic<uint32_t> wait_condition{1};

后续调用futex时直接用它的地址即可,不需要任何指针计算。

方案二:用Union包装变量

C++17及以后,union的成员访问是合法的(只要不同时修改多个成员),你可以把uint8_t变量和uint32_t变量放在同一个union里,确保能合法访问4字节的futex word:

union FutexableUint8 {
    std::atomic<uint32_t> futex_word;
    std::atomic<uint8_t> value;
};

FutexableUint8 wait_condition{.value = 1};

调用futex时直接用&wait_condition.futex_word作为uaddr,读取值时也可以合法地读取futex_word的内容,再修改对应的字节。

方案三:用memcpy避免严格别名问题

如果你必须用uint8_t变量,可以用memcpy来读取4字节的内存——这是C++标准允许的,因为memcpy是字节级操作,不受严格别名规则限制:

long futex_wait(uint8_t* uaddr, uint8_t val) {
    const auto futex_addr = calculate_futex_ptr(uaddr);
    uint32_t futex_val;
    // 用memcpy替换直接解引用,避免严格别名UB
    std::memcpy(&futex_val, futex_addr, sizeof(futex_val));
    
    uint8_t* dest_addr = reinterpret_cast<uint8_t*>(&futex_val) + calculate_offset(uaddr);
    *dest_addr = val;
    
    return futex(futex_addr, FUTEX_WAIT_PRIVATE, futex_val, nullptr, nullptr, 0);
}

这样既避免了严格别名问题,也不会非法访问对象内存(只要futex_addr指向的4字节内存都是程序合法拥有的,比如全局变量的连续内存)。


4. 标准库是怎么处理这个问题的?

以GCC的libstdc++为例,std::atomic<T>::wait/notify_one的实现针对小类型(比如uint8_t)做了特殊处理:

  • 锁池机制:多个小尺寸的atomic对象会共享一个全局的锁池(每个池项是4字节对齐的uint32_t),每个atomic对象对应池里的一个固定条目。当调用wait时,标准库会找到对应的池项,用合法的方式读取4字节值(比如memcpy),再调用futex_wait。
  • 合法内存访问:标准库会严格遵守C++标准,用char类型指针或memcpy来操作内存,彻底避免严格别名问题;同时确保传递给futex的指针是指向合法的uint32_t对象,不会有任何UB。
  • 功能兼容:标准库的wait函数已经内置了超时逻辑,但要注意std::atomic::wait是线程间同步用的,不支持跨进程——跨进程同步需要用POSIX的共享内存+信号量,或者自己用shm_open配合futex实现。

你提供的代码整理版(修复UB后的版本)

#include <linux/futex.h>
#include <sys/syscall.h>
#include <unistd.h>
#include <atomic>
#include <chrono>
#include <cstdint>
#include <thread>
#include <cstring> // 新增memcpy依赖的头文件

long futex(uint32_t* uaddr, int futex_op, uint32_t val, const struct timespec* timeout, uint32_t* uaddr2, uint32_t val3) {
    return syscall(SYS_futex, uaddr, futex_op, val, timeout, uaddr2, val3);
}

//! Get the 4-byte aligned pointer where uaddr is included.
uint32_t* calculate_futex_ptr(uint8_t* uaddr) {
    const auto addr = reinterpret_cast<uintptr_t>(uaddr);
    return reinterpret_cast<uint32_t*>(addr & ~(0x3));
}

//! Get offset (0-3) where uaddr is in relation to calculate_futex_ptr(uaddr).
int calculate_offset(uint8_t* uaddr) {
    return reinterpret_cast<uintptr_t>(uaddr) & 0x3;
}

long futex_wake(uint8_t* uaddr) {
    const auto futex_addr = calculate_futex_ptr(uaddr);
    return futex(futex_addr, FUTEX_WAKE_PRIVATE, 123, nullptr, nullptr, 0);
}

long futex_wait(uint8_t* uaddr, uint8_t val) {
    const auto futex_addr = calculate_futex_ptr(uaddr);
    uint32_t futex_val;
    // 用memcpy替换直接解引用,避免严格别名UB
    std::memcpy(&futex_val, futex_addr, sizeof(futex_val));
    
    uint8_t* dest_addr = reinterpret_cast<uint8_t*>(&futex_val) + calculate_offset(uaddr);
    *dest_addr = val;
    
    return futex(futex_addr, FUTEX_WAIT_PRIVATE, futex_val, nullptr, nullptr, 0);
}

uint8_t before{6};
uint8_t wait_condition{1};
uint8_t after{5};

void thread_func() {
    std::this_thread::sleep_for(std::chrono::seconds{1});
    std::atomic_ref(wait_condition).store(3);
    futex_wake(&wait_condition);
}

int main() {
    std::thread thd(thread_func);
    futex_wait(&wait_condition, 1);
    thd.join();
}

内容来源于stack exchange

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.07 07:09:35