针对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
相关产品推荐
相关产品推荐

