原子操作与位操作:那些年寄存器里踩过的坑

张开发
2026/4/14 22:12:52 15 分钟阅读

分享文章

原子操作与位操作:那些年寄存器里踩过的坑
从一次诡异的设备故障说起上周调一个传感器驱动现象很诡异设备运行几小时后偶尔会丢数据。用逻辑分析仪抓时序波形完全正常查中断响应延迟也在合理范围内。最后盯着一段操作状态寄存器的代码看了半小时突然脊背发凉——问题就出在这个看似简单的位操作上。// 这是出问题的代码反面教材voidsensor_set_mode(uint32_tmode){uint32_tregreadl(REG_CTRL);reg~(0x35);// 清掉原来的模式位reg|(mode0x3)5;// 设置新模式writel(reg,REG_CTRL);}单看这段代码似乎没问题但在多核环境下两个CPU可能同时执行这个函数。假设CPU A刚读完寄存器CPU B也读完然后各自修改、回写——后写回的那个会把前一个的修改覆盖掉。这种竞态问题在中断和主程序共享寄存器时也会出现。原子操作不是你想的那样简单很多人以为“原子操作”就是“不可中断的操作”这个理解太浅了。在Linux驱动里原子操作的核心是保证操作的完整性和可见性。// 内核提供的原子整数操作常用部分atomic_tcounterATOMIC_INIT(0);// 自增这个绝对不会被中断打断atomic_inc(counter);// 带返回值的自增注意这个返回值是自增后的值intnew_valatomic_inc_return(counter);// 条件递减只有值大于0时才减if(atomic_dec_and_test(counter)){// 减到0了可以做些清理工作}// 原子读写这个特别有用intoldatomic_read(counter);atomic_set(counter,100);但这里有个坑原子操作只保证这个变量本身的操作是原子的。如果你需要保护一个结构体里的多个字段还得用锁。我见过有人用atomic_t保护一个链表结果链表节点之间的指针关系全乱了——原子变量不是万能药。位操作寄存器编程的基本功嵌入式驱动工程师天天跟寄存器打交道位操作是看家本领。但越是基础的东西越容易写出坑。// 清晰的位操作示例#defineREG_ENABLEBIT(0)// 第0位使能位#defineREG_MODE_MSK(0x31)// 第1-2位模式位#defineREG_MODE_LOW(0x01)// 低功耗模式#defineREG_MODE_HIGH(0x11)// 高性能模式voidconfigure_device(void){uint32_treg0;// 设置模式位先清后设这是标准做法reg~REG_MODE_MSK;// 把1-2位清零reg|REG_MODE_HIGH;// 设为高性能模式// 设置使能位直接置1reg|REG_ENABLE;// 一次性写入减少寄存器访问次数writel(reg,DEVICE_REG);}新手常犯的错误是位偏移算错。(1 n)表示第n位从0开始但有些芯片手册从1开始数位容易搞混。我的经验是永远以手册的位编号为准但在代码里加注释说明。原子位操作内核的利器当位操作遇到并发就需要原子位操作函数了。这些函数在底层用CPU的原子指令实现比如ARM的LDREX/STREX。// 原子位操作实战unsignedlongflags0;// 设置第n位这个保证在多核下也不会冲突set_bit(n,flags);// 测试并设置返回原来的值if(!test_and_set_bit(n,flags)){// 原来为0现在设成了1// 这里可以安全地执行一些只能做一次的操作}// 清除位同样也是原子的clear_bit(n,flags);// 最强大的原子地修改多个位unsignedlongold,new;do{oldflags;newold|(BIT(3)|BIT(5));// 设置3、5位new~BIT(7);// 清除第7位}while(cmpxchg(flags,old,new)!old);cmpxchg比较并交换是原子操作的瑞士军刀。它检查变量的值是否还是old如果是就换成new否则重试。这个模式在实现无锁数据结构时特别有用。内存屏障看不见的守护者原子操作还有个兄弟叫内存屏障。没有它在多核CPU上可能会出乱序执行的问题。// 一个需要内存屏障的场景set_bit(DEVICE_READY,dev_flags);wmb();// 写屏障保证上面的写操作先于下面的写操作writel(DATA_REG,some_data);wmb()保证它之前的写操作都完成之后才执行后面的写操作。对应的还有rmb()读屏障和mb()全屏障。在x86这种强内存模型的架构上很多屏障是空操作但ARM、PowerPC等弱内存模型架构必须用。实战建议来自调试血泪史寄存器操作一定要用read-modify-write直接writel(value, reg)会覆盖整个寄存器。我见过有人这样写把其他配置位全冲掉了设备当然不工作。并发场景先想共享变量写驱动时看到全局变量、静态变量就要警惕中断会不会访问另一个CPU会不会访问如果有要么用原子操作要么加锁。位域bitfield要慎用C语言的位域语法很诱人但它的内存布局是编译器决定的而且没有原子保证。操作硬件寄存器时老实用位操作宏。调试原子问题用perf怀疑有原子操作竞争时perf lock命令可以分析锁争用情况。虽然原子操作不是锁但争用模式类似。ARM平台的特殊注意ARMv7之后有LDREX/STREX指令但不同核心的缓存一致性延迟不同。操作外设寄存器时如果涉及多个相关寄存器可能需要dsb()数据同步屏障。最后说个真事我曾经调过一个驱动设备偶尔会卡死。最后发现是某个状态标志位用普通变量中断和主程序同时修改。改成原子位操作后问题消失。这种bug最难查——它不总是出现出现时现象也不一样。所以现在我的习惯是只要变量可能被多个上下文访问第一反应就是上原子操作。驱动开发就像在雷区走路原子操作和位操作是你脚下的探雷器。用好了代码稳如泰山用不好调试生不如死。

更多文章