linux中关于原子操作

本文深入探讨了Linux系统中原子操作的实现原理,特别是在ARM架构下的具体实现方式。文章详细介绍了整型原子操作和位原子操作的函数接口及其应用场景,并通过具体的代码示例展示了如何使用原子操作来解决设备并发访问的问题。
AI助手已提取文章相关产品:

原子操作:就是在执行某一操作时不被打断。

linux原子操作问题来源于中断、进程的抢占以及多核smp系统中程序的并发执行。

对于临界区的操作可以加锁来保证原子性,对于全局变量或静态变量操作则需要依赖于硬件平台的原子变量操作。

因此原子操作有两类:一类是各种临界区的锁,一类是操作原子变量的函数。

对于arm来说,单条汇编指令都是原子的,多核smp也是,因为有总线仲裁所以cpu可以单独占用总线直到指令结束,多核系统中的原子操作通常使用内存栅障(memory barrier)来实现,即一个CPU核在执行原子操作时,其他CPU核必须停止对内存操作或者不对指定的内存进行操作,这样才能避免数据竞争问题。但是对于load update store这个过程可能被中断、抢占,所以arm指令集有增加了ldrex/strex这样的实现load update store的原子指令。

但是linux种对于c/c++程序(一条c编译成多条汇编),由于上述提到的原因不能保证原子性,因此linux提供了一套函数来操作全局变量或静态变量。

一.整型原子操作
定义于#include<asm/atomic.h>
分为 定义,获取,加减,测试,返回。

void atomic_set(atomic_t *v,int i);    //设置原子变量v的值为i
atomic_t v = ATOMIC_INIT(0);     //定义原子变量v,并初始化为0;

atomic_read(atomic_t* v);     //返回原子变量v的值;

void atomic_add(int i, atomic_t* v);     //原子变量v增加i;
void atomic_sub(int i, atomic_t* v);    

void atomic_inc(atomic_t* v);     //原子变量增加1;
void atomic_dec(atomic_t* v);     

int atomic_inc_and_test(atomic_t* v);        //先自增1,然后测试其值是否为0,若为0,则返回true,否则返回false;
int atomic_dec_and_test(atomic_t* v);        
int atomic_sub_and_test(int i, atomic_t* v);     //先减i,然后测试其值是否为0,若为0,则返回true,否则返回false;
注意:只有自加,没有加操作

int atomic_add_return(int i, atomic_t* v);   //v的值加i后返回新的值;
int atomic_sub_return(int i, atomic_t* v);  
int atomic_inc_return(atomic_t* v);     //v的值自增1后返回新的值;
int atomic_dec_return(atomic_t* v);    

二.位原子操作
定义于#include<asm/bitops.h>
分为 设置,清除,改变,测试

void set_bit(int nr, volatile void* addr);        //设置地址addr的第nr位,所谓设置位,就是把位写为1;
void clear_bit(int nr, volatile void* addr);      //清除地址addr的第nr位,所谓清除位,就是把位写为0;

void change_bit(int nr, volatile void* addr);     //把地址addr的第nr位反转;

int test_bit(int nr, volatile void* addr);    //返回地址addr的第nr位;

int test_and_set_bit(int nr, volatile void* addr);    //测试并设置位;若addr的第nr位非0,则返回true; 若addr的第nr位为0,则返回false;
int test_and_clear_bit(int nr, volatile void* addr);    //测试并清除位;
int test_and_change_bit(int nr, volatile void* addr);    //测试并反转位;
上述操作等同于先执行test_bit(nr,voidaddr)然后在执行xxx_bit(nr,voidaddr)


举个简单例子:
为了实现设备只能被一个进程打开,从而避免竞态的出现

static atomic_t scull_available = ATOMIC_INIT(1);      //init atomic

在scull_open 函数和scull_close函数中:

int scull_open(struct inode *inode, struct file *filp)
{
    struct scull_dev *dev;         // device information

    dev = container_of(inode->i_cdev, struct scull_dev, cdev);
    filp->private_data = dev;         // for other methods 
     if(!atomic_dec_and_test(&scull_available)){
        atomic_inc(&scull_available);
        return -EBUSY;
    }
    return 0;         // success 
}

int scull_release(struct inode *inode, struct file *filp)
{
     atomic_inc(&scull_available);
    return 0;
}


假设原子变量的底层实现是由一个汇编指令实现的,这个原子性必然有保障。但是如果原子变量的实现是由多条指令组合而成的,那么对于SMP和中断的介入会不会有什么影响呢?我在看ARM的原子变量操作实现的时候,发现其是由多条汇编指令(ldrex/strex)实现的。在参考了别的书籍和资料后,发现大部分书中对这两条指令的描诉都是说他们是支持在SMP系统中实现多核共享内存的互斥访问。但在UP系统中使用,如果ldrex/strex和之间发生了中断,并在中断中也用ldrex/strex操作了同一个原子变量会不会有问题呢?就这个问题,我认真看了一下内核的ARM原子变量源码和ARM官方对于ldrex/strex的功能解释,总结如下:

 

一、ARM构架的原子变量实现结构

    对于ARM构架的原子变量实现源码位于:arch/arm/include/asm/atomic.h

    其主要的实现代码分为ARMv6以上(含v6)构架的实现和ARMv6版本以下的实现。

该文件的主要结构如下:

  1. #if __LINUX_ARM_ARCH__ >= 6

  2. ......(通过ldrex/strex指令的汇编实现)

  3. #else /* ARM_ARCH_6 */

  4. #ifdef CONFIG_SMP
  5. #error SMP not supported on pre-ARMv6 CPUs
  6. #endif

  7. ......(通过关闭CPU中断的C语言实现)

  8. #endif /* __LINUX_ARM_ARCH__ */
  9. ...... 

  10.  #ifndef CONFIG_GENERIC_ATOMIC64

  11. ......(通过ldrexd/strexd指令的汇编实现的64bit原子变量的访问)

  12. #else /* !CONFIG_GENERIC_ATOMIC64 */

  13. #include <asm-generic/atomic64.h>

  14. #endif

  15. #include <asm-generic/atomic-long.h>

      这样的安排是依据ARM核心指令集版本的实现来做的:

(1)在ARMv6以上(含v6)构架有了多核的CPU,为了在多核之间同步数据和控制并发,ARM在内存访问上增加了独占监测(Exclusive monitors)机制(一种简单的状态机),并增加了相关的ldrex/strex指令。请先阅读以下参考资料(关键在于理解local monitor和Global monitor):

1.2.2. Exclusive monitors

4.2.12. LDREX  STREX

(2)对于ARMv6以前的构架不可能有多核CPU,所以对于变量的原子访问只需要关闭本CPU中断即可保证原子性。 

对于(2),非常好理解。

但是(1)情况,我还是要通过源码的分析才认同这种代码,以下我仅仅分析最具有代表性的atomic_add源码,其他的API原理都一样。如果读者还不熟悉C内嵌汇编的格式,请参考ARM GCC 内嵌汇编手册》

 

二、内核对于ARM构架的atomic_add源码分析


  1. /*
  2. * ARMv6 UP 和 SMP 安全原子操作。 我们是用独占载入和
  3. * 独占存储来保证这些操作的原子性。我们可能会通过循环
  4. * 来保证成功更新变量。
  5. */

  6. static inline void atomic_add(int i, atomic_t *v)
  7. {
  8. unsigned long tmp;
  9. int result;
  10. __asm__ __volatile__("@ atomic_add\n"
  11. "1: ldrex %0, [%3]\n"
  12. " add %0, %0, %4\n"
  13. " strex %1, %0, [%3]\n"
  14. " teq %1, #0\n"
  15. " bne 1b"
  16. : "=&r" (result), "=&r" (tmp), "+Qo" (v->counter)
  17. : "r" (&v->counter), "Ir" (i)
  18. : "cc");
  19. }

源码分析: 

注意:根据内联汇编的语法,result、tmp、&v->counter对应的数据都放在了寄存器中操作。如果出现上下文切换,切换机制会做寄存器上下文保护。

 (1)ldrex %0, [%3]

意思是将&v->counter指向的数据放入result中,并且(分别在Local monitor和Global monitor中)设置独占标志。

(2)add %0, %0, %4

result = result + i

(3)strex %1, %0, [%3]

意思是将result保存到&v->counter指向的内存中,此时 Exclusive monitors会发挥作用,将保存是否成功的标志放入tmp中。

(4) teq %1, #0

测试strex是否成功(tmp == 0 ??)

(5)bne 1b

如果发现strex失败,从(1)再次执行。

      通过上面的分析,可知关键在于strex的操作是否成功的判断上。而这个就归功于ARM的Exclusive monitors和ldrex/strex指令的机制。以下通过可能的情况分析ldrex/strex指令机制。(请阅读时参考4.2.12. LDREX  STREX

 

1、UP系统或SMP系统中变量为非CPU间共享访问的情况 

    此情况下,仅有一个CPU可能访问变量,此时仅有Local monitor需要关注。

    假设CPU执行到(2)的时候,来了一个中断,并在中断里使用ldrex/strex操作了同一个原子变量。则情况如下图所示:

  • A:处理器标记一个物理地址,但访问尚未完毕
  • B:再次标记此物理地址访问尚未完毕(与A重复)
  • C:进行存储操作,清除以上标记,返回0(操作成功)
  • D:不会进行存储操作,并返回1(操作失败) 

也就是说,中断例程里的操作会成功,被中断的操作会失败重试。 

 

2、SMP系统中变量为CPU间共享访问的情况

  

    此情况下,需要两个CPU间的互斥访问,此时ldrex/strex指令会同时关注Local monitor和Global monitor。

(i)两个CPU同时访问同个原子变量(ldrex/strex指令会关注Global monitor。)

  • A:将该物理地址标记为CPU0独占访问,并清除CPU0对其他任何物理地址的任何独占访问标记。
  • B:标记此物理地址为CPU1独占访问,并清除CPU1对其他任何物理地址的任何独占访问标记。
  • C:没有标记为CPU0独占访问,不会进行存储,并返回1(操作失败)。
  • D:已被标记为CPU1独占访问,进行存储并清除独占访问标记,并返回0(操作成功)。

 也就是说,后执行ldrex操作的CPU会成功。

 

(ii)同一个CPU因为中断,“嵌套”访问同个原子变量(ldrex/strex指令会关注Local monito)

  • A:将该物理地址标记为CPU0独占访问,并清除CPU0对其他任何物理地址的任何独占访问标记。
  • B:再次标记此物理地址为CPU0独占访问,并清除CPU0对其他任何物理地址的任何独占访问标记。
  • C:已被标记为CPU0独占访问,进行存储并清除独占访问标记,并返回0(操作成功)。
  • D:没有标记为CPU0独占访问,不会进行存储,并返回1(操作失败)。

也就是说,中断例程里的操作会成功,被中断的操作会失败重试。

 

(iii)两个CPU同时访问同个原子变量,并同时有CPU因中断“嵌套”访问改原子变量(ldrex/strex指令会同时关注Local monitor和Global monitor)

虽然对于人来说,这种情况比较BT。但是在飞速运行的CPU来说,BT的事情随时都可能发生。

  • A:将该物理地址标记为CPU0独占访问,并清除CPU0对其他任何物理地址的任何独占访问标记。
  • B:标记此物理地址为CPU1独占访问,并清除CPU1对其他任何物理地址的任何独占访问标记。
  • C:再次标记此物理地址为CPU0独占访问,并清除CPU0对其他任何物理地址的任何独占访问标记。
  • D:已被标记为CPU0独占访问,进行存储并清除独占访问标记,并返回0(操作成功)。
  • E:没有标记为CPU1独占访问,不会进行存储,并返回1(操作失败)。
  • F:没有标记为CPU0独占访问,不会进行存储,并返回1(操作失败)。

 

    当然还有其他许多复杂的可能,也可以通过ldrex/strex指令的机制分析出来。从上面列举的分析中,我们可以看出:ldrex/strex可以保证在任何情况下(包括被中断)的访问原子性。所以内核中ARM构架中的原子操作是可以信任的。



您可能感兴趣的与本文相关内容

<think>首先,用户的问题是:“我想了解在Linux应用中使用原子操作的方法 请问Linux应用中原子操作的使用方法”。我需要参考提供的引用,并确保我的回答基于这些引用。 回顾系统级指令: - 所有行内数学表达式使用$...$格式。 - 独立公式使用$$...$$$$格式并单独成段。 - LaTeX语法正确。 - 使用中文回答。 - 生成相关问题。 - 回答中引用的段落末尾自然地添加引用标识。 - 回答结构清晰,帮助用户逐步解决问题。 - 保证回答真实可靠。 提供的引用: - 引用[1]:讨论了原子操作Linux中的应用,如在资源计数中使用(类似于C++的智能指针引用计数),也可以用于锁定资源。 - 引用[2]:讨论了信号量机制,作为自旋锁的替代,用于避免忙等。 - 引用[3]和[4]:涉及Linux安装和子系统管理,与原子操作问题无关。 用户的问题是关于原子操作的使用方法,所以我应该聚焦在引用[1]上。 关键点从引用[1]: - 原子操作用于实现资源计数,例如在Linux内核中的引用计数(如fork.c)。 - 可以通过原子操作来上锁资源:在资源开始计数加1并进行防护判定,结束时释放计数减1(但这不常见)。 - 提到硬件原子操作的缘由。 我的回答应该基于这个解释原子操作Linux应用中的使用方法。既然用户在应用层,我应该包括用户空间和内核空间的原子操作?引用[1]说的是Linux内核(/linux/fork.c),所以可能重点在内核部分。但用户说“Linux应用”,可能包括用户空间应用。在Linux中,原子操作可以通过各种API使用,如在内核中使用atomic_t类型和相关函数,在用户空间可以使用C的atomic操作或库如glibc的原子操作。 为了真实可靠,我应该回忆标准知识: - 在Linux内核中,原子操作通过atomic_t类型和函数如atomic_add()、atomic_inc()等实现。 - 在用户空间,C11引入了_Atomic类型和原子操作函数,或者使用Linux提供的系统调用如futex,但原子操作本身通常是通过硬件指令实现的内联函数。 引用[1]提到“原子操作实现(类似于C++中智能指针的引用)”,所以可以类比到C++的atomic。 结构化回答: 1. **介绍原子操作**:解释什么是原子操作,为什么在Linux应用中使用。 2. **使用场景**:基于引用[1],讨论资源计数和可能的锁定。 3. **实现方法**: - 内核空间:使用内核提供的API(如atomic.h)。 - 用户空间:使用C语言的原子操作或其他库。 4. **示例代码**:提供简单的代码示例。 5. **注意事项**:提到引用[2]的信号量机制作为替代方案,避免忙等。 6. **生成相关问题**:在最后添加相关问题。 确保使用中文回答。 添加引用标识:在引用[1]的段落末尾添加[^1]。 生成相关问题:基于主题,如优化、常见问题、信号量比较等。 完整回答结构: - 引言:解释原子操作。 - 使用方法:分步骤解释。 - 代码示例:使用$$...$$格式,因为是独立代码段。 - 注意事项:对比信号量。 - 相关问题:在结尾。 数学表达式:用户问题中没有提到数学,但如果有,使用格式。例如,涉及计数时可以提到加法和减法操作原子的。 现在,草拟内容: 1. **引言**:原子操作是保证在多线程或多处理器环境中操作不可中断的机制,常用于共享资源的计数和同步。在Linux中,原子操作广泛应用于内核和用户空间应用[^1]。 2. **使用场景**:基于引用[1],原子操作常用于实现引用计数,例如在资源管理中计数引用数以避免竞争条件。也可以用于简单的锁定机制,但不常见,因为信号量更高效[^1]。 3. **实现方法**: - **内核空间**:在Linux内核中,使用`atomic_t`类型和相关函数。例如: ```c #include <linux/atomic.h> atomic_t ref_count = ATOMIC_INIT(1); void increment_ref() { atomic_inc(&ref_count); } ``` - **用户空间**:在C应用中,使用C11的原子操作或glibc扩展。例如: ```c #include <stdatomic.h> atomic_int ref_count = ATOMIC_VAR_INIT(1); void increment_ref() { atomic_fetch_add(&ref_count, 1); } ``` 4. **注意事项**:原子操作适用于简单计数器;对于更复杂的同步,如等待资源可用,应考虑信号量(semaphore)机制,以避免忙等浪费CPU时间[^2]。 5. **相关问题**:在结尾生成。 添加引用:在提到引用[1]和[2]的地方添加[^1]和[^2]。 确保LaTeX:如果有公式,例如原子操作的计数,可以用$...$,但代码段用代码块。 最后,生成相关问题: 1. 原子操作Linux内核中的具体实现原理是什么? 2. 如何在用户空间应用中高效使用原子操作? 3. 原子操作与信号量的优缺点比较? 输出格式: - 回答主体。 - §§相关问题§§列表。 </think>以下我将逐步解释在Linux应用中使用原子操作的方法。原子操作是一种保证操作在多线程或多处理器环境中不可中断的机制,常用于共享资源的计数和同步,以避免竞争条件。参考提供的引用[1],原子操作Linux中广泛应用于资源计数(如引用计数),类似于C++中智能指针的实现机制。原子操作的核心是确保操作原子性——即操作要么完整执行,要么完全不执行,不会部分完成[^1]。下面我将详细介绍使用方法,包括内核空间和用户空间的不同实现方式。 ### 1. **原子操作的使用场景** - **资源计数**:原子操作最常见的应用是实现引用计数,例如在Linux内核的文件描述符或进程管理中(如`fork.c`)。每次资源被引用时,计数原子性加1;释放时原子性减1。计数归零时表示资源可安全释放,避免数据竞争[^1]。 - **简单锁定机制**:原子操作也可用于实现轻量级锁定(如自旋锁替代品)。在资源访问开始时,原子性加1并进行判定(如果计数>0则允许访问),结束时原子性减1。但这不常见,因为信号量机制更适合处理长时间持有的资源(避免忙等浪费CPU)[^1][^2]。 - **适用场景**:原子操作最适合低开销的计数器操作(如整数加减)。对于复杂同步(如等待队列),应优先使用信号量机制[^2]。 ### 2. **原子操作的核心方法** 原子操作的使用依赖于硬件支持(如CPU的CAS指令),Linux提供了标准API来简化开发。以下是内核空间和用户空间的具体实现方法。 #### **2.1 内核空间原子操作** 在Linux内核模块中(如驱动或系统调用),使用内核头文件`<linux/atomic.h>`定义的`atomic_t`类型和相关函数。这些函数在底层使用硬件原子指令,确保操作原子性。 - **初始化原子变量**: 使用`ATOMIC_INIT`宏初始化变量。 - **原子加/减操作**: 函数如`atomic_inc()`(加1)、`atomic_dec()`(减1)、`atomic_add()`(通用加法)。 - **原子读取和比较**: 使用`atomic_read()`获取值,`atomic_cmpxchg()`进行比较并交换(CAS)。 示例代码(内核模块中): ```c #include <linux/module.h> #include <linux/atomic.h> static atomic_t ref_count = ATOMIC_INIT(0); // 初始化原子计数为0 void increment_ref(void) { atomic_inc(&ref_count); // 原子性加1 } int decrement_ref(void) { if (atomic_read(&ref_count) <= 0) { return -1; // 防护判定:计数<=0时返回错误 } atomic_dec(&ref_count); // 原子性减1 return 0; } EXPORT_SYMBOL(increment_ref); EXPORT_SYMBOL(decrement_ref); ``` 在此示例中: - `increment_ref`函数原子性地增加计数。 - `decrement_ref`函数首先读取当前值进行判定,然后原子性减1,适合资源释放逻辑[^1]。 #### **2.2 用户空间原子操作** 在用户空间应用(如C/C++程序)中,可以使用C11标准库的原子操作API(`<stdatomic.h>`)或Linux特定的扩展(如`__atomic`内置函数)。这些在底层调用硬件指令,性能高效。 - **初始化原子变量**: 使用`atomic_init()`或`ATOMIC_VAR_INIT`宏。 - **原子加/减操作**: 函数如`atomic_fetch_add()`(加并返回旧值)、`atomic_fetch_sub()`。 - **原子读取和比较**: 使用`atomic_load()`获取值,`atomic_compare_exchange_weak()`实现CAS。 示例代码(用户空间C程序): ```c #include <stdatomic.h> #include <stdio.h> atomic_int ref_count = ATOMIC_VAR_INIT(0); // 初始化原子计数为0 void increment_ref(void) { atomic_fetch_add(&ref_count, 1); // 原子性加1 } int decrement_ref(void) { int old_value = atomic_load(&ref_count); if (old_value <= 0) { return -1; // 防护判定 } atomic_fetch_sub(&ref_count, 1); // 原子性减1 return 0; } int main() { increment_ref(); printf("Current count: %d\n", atomic_load(&ref_count)); if (decrement_ref() == 0) { printf("Release successful.\n"); } return 0; } ``` 在此示例中: - `atomic_fetch_add`和`atomic_fetch_sub`确保加/减操作原子性。 - 结合防护判定,模拟资源释放逻辑,类似于内核中的引用计数机制[^1]。 ### 3. **注意事项和最佳实践** - **硬件支持**:原子操作依赖于CPU的原子指令(如x86的`LOCK`前缀)。确保目标架构支持这些指令,否则使用软件回退机制。 - **避免忙等**:原子操作本身不会阻塞线程(如计数操作),但如果用于锁定(如自旋锁),可能导致忙等浪费CPU。对于长时间等待的资源,优先使用信号量机制(`semaphore`),它允许线程睡眠在等待队列上,提高效率[^2]。 - **性能考量**:原子操作开销低,适合高频率计数场景。但对于复杂数据结构(如链表),使用锁或RCU(读写拷贝更新)更安全。 - **调试工具**:Linux提供`perf`工具监控原子操作性能,或用`KCSAN`检测竞争条件。 ### 4. **与其他同步机制对比** - **与信号量比较**:原子操作简单高效但不支持等待队列;信号量机制允许线程阻塞和唤醒,适合资源持有时间不确定的场景[^2]。例如: - 原子操作:$O(1)$时间开销,但可能忙等。 - 信号量:$O(1)$唤醒开销,避免忙等。 - **与应用场景关系**:在容器或子系统管理(如引用[3]-[4]提到的WSL环境)中,原子操作可用于跨虚拟机资源计数,但需注意隔离边界。 通过上述方法,您可以在Linux应用中高效实现原子操作。建议从计数场景入手,逐步扩展到更复杂同步,并参考内核源码(如`linux/fork.c`)学习实际用例[^1]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值