linux内核同步之每CPU变量、原子操作、内存屏障、自旋锁【转】

转自:http://blog.csdn.net/goodluckwhh/article/details/9005585

版权声明:本文为博主原创文章,未经博主允许不得转载。

 

目录(?)[-]

  1. 一每CPU变量
  2. 二原子操作
  3. 三优化和内存屏障
  4. 四自旋锁
    1. 自旋锁
    2. 自旋锁的数据结构和宏函数
    3. 读写自旋锁
    4. 读写自旋锁的相关函数

 

linux内核中的各种“任务”都能看到内核地址空间,因而它们之间也需要同步和互斥。linux内核支持的同步/互斥手段包括:

 

技术 功能 作用范围
每CPU变量 为每个CPU复制一份数据 所有CPU
原子操作 原子的读-修改-写一个计数器的指令 所有CPU
内存屏障 避免指令被重新排序 本地CPU或所有CPU
自旋锁  上锁并忙等待 所有CPU
信号量 上锁并阻塞等待(sleep) 所有CPU
顺序锁 基于访问计数器上锁 所有CPU
RCU 不上锁的情况下通过指针访问共享数据结构 所有CPU
completion 通知/(等待另)一个任务完成 所有CPU
关闭本地中断 在单个CPU上关闭中断(本CPU)    本地CPU
关闭本地软中断 在单个CPU(本CPU)上禁止可延迟函数的执行 本地CPU

一、每CPU变量

首先必须明确最好的同步/互斥技术就是不许要同步/互斥。所有的同步/互斥技术都有性能上的代价。
每-CPU变量是最简单的同步手段,它实际上是数据结构的数组,系统的每个CPU对应数组中的一个元素。
使用每CPU变量时,每个CPU只能访问与它相关联的元素,因此每-CPU变量只能在特殊情形下被使用。
每-CPU变量会在主存中对其以确保它们会映射到不同的硬件cashe行。这样就可以确保并发访问每-CPU变量不会导致高速缓存的snooping和invalidation(这种操作会带来高昂的系统开销)。
虽然每CPU变量可以保护从不同CPU的并发访问,但是它并不能保护异步访问,比如中断和可延迟函数。另外,如果支持内核抢占,则每CPU变量可能会存在竞态。因而内核在访问每CPU变量时应该禁止内核抢占。
使用每CPU变量的宏和函数:

  • DEFINE_PER_CPU(type, name) :该宏静态的分配一个名字为name类型为type的每-CPU变量。
  • per_cpu(name, cpu):该宏选取名字为name的每CPU变量的对应于指定的cpu的元素
  • _ _get_cpu_var(name) :该宏选择名字为name的每CPU变量的对应于本地cpu的元素
  • get_cpu_var(name) :该宏关闭内核抢占,然后选择名字为name的每CPU变量的对应于本地cpu的元素
  • put_cpu_var(name) :该宏打开内核抢占,未使用name
  • alloc_percpu(type) :该宏动态分配一个类型为type的每CPU变量并返回其地址
  • free_percpu(pointer) :该宏释放动态分配的每CPU变量,pointer为每CPU变量的地址
  • per_cpu_ptr(pointer, cpu):该宏返回存放于地址pointer的每CPU变量对应于cpu的元素的地址

二、原子操作

有不少汇编指令是"读-修改-写"的类型的,也就是说这种指令要访问内存两次,一次读来获取旧的值,一次写来写入新的值。如果有两个或两个以上CPU同时发起了这种类型的操作,最终的结构就可能是错误的(每个CPU都读到了旧的值,然后做修改再写,这样最后的写会取胜,如果是两次加1的话,这种情形下,最终只会加一次1)。最简单的避免这种问题的方式是在芯片级保证这种操作是原子的。
当我们写代码时,我们无法确保编译器会使用原子的指令。因此lnux提供了一种特殊的类型atomic_t以及一些特殊的函数和宏,这样函数和宏作用于atomic_t的类型,并且被实现为单独的、原子的汇编指令。
linux中的原子操作:

  • atomic_read(v) :返回*v的值
  • atomic_set(v,i) :设置*v的值为i
  • atomic_add(i,v) :将*v的值加i
  • atomic_sub(i,v):将*v的值减i
  • atomic_sub_and_test(i, v) :将*v的值减i并检查更新后的*v是否是0,如果是0则返回1
  • atomic_inc(v) :将*v的值加1
  • atomic_dec(v):将*v的值减1
  • atomic_dec_and_test(v):将*v的值减1并检查更新后的*v是否是0,如果是0则返回1 
  • atomic_inc_and_test(v) :将*v的值加1并检查更新后的*v是否是0,如果是0则返回1 
  • atomic_add_negative(i, v) :将*v的值加i并检查更新后的*v是否是负值,如果是则返回1 
  • atomic_inc_return(v):将*v的值加1并返回更新后的*v的值 
  • atomic_dec_return(v):将*v的值减1并返回更新后的*v的值
  • atomic_add_return(i, v) :将*v的值加i并返回更新后的*v的值 
  • atomic_sub_return(i, v) :将*v的值减i并返回更新后的*v的值

还有一些原子操作作用于位掩码:

  • test_bit(nr, addr) :返回*addr的第nr比特
  • set_bit(nr, addr) :设置*addr的第nr比特为1
  • clear_bit(nr, addr)  :将 *addr的第nr比特清为0
  • change_bit(nr, addr):将*addr的第nr比特取反
  • test_and_set_bit(nr, addr) :将*addr的第nr比特设置为1,并返回其旧值
  • test_and_clear_bit(nr, addr):将*addr的第nr比特设置为0,并返回其旧值
  • test_and_change_bit(nr, addr): 将*addr的第nr比特取反,并返回其旧值
  • atomic_clear_mask(mask, addr) :将*addr中对应于mask的所有比特都清0
  • atomic_set_mask(mask, addr):将*addr中对应于mask的所有比特都设置为1

三、优化和内存屏障

如果启用了编译器优化,指令的执行顺序和其在代码中的顺序不一定相同。此外,现代CPU通常会并行执行多条指令,并且可能重新安排内存访问。
然而在涉及同步时,指令重排可能会带来问题,如果放在同步原语之后的指令在同步原语之前被执行了,就可能会出问题。事实上所有的同步原语都起优化和内存屏障的作用。
优化屏障原语用于告诉编译器,保存在CPU寄存器中、在屏障之前有效的所有内存地址,在屏障之后都将失效。因而编译器在屏障之前发出的读写请求完成之前,不会处理屏障之后的任何读写请求。barrier( )宏是linux中的优化屏障原语。注意,这个原语并不保证CPU执行它们的顺序(由于并行执行的特性,后执行的指令可能先结束)。
内存屏障原语确保放在原语之前的语句在原语之后的语句开始执行之前结束执行。
linux使用了几个内存屏障原语,这些内存屏障原语也可以作为优化屏障。读内存屏障只适用于读操作,写内存屏障只适用于写操作。

  • mb( ):用作单处理器以及多处理器架构上的内存屏障
  • rmb( ) :用作单处理器以及多处理器架构上的内存读屏障
  • wmb( ) :用作单处理器以及多处理器架构上的内存写屏障
  • smp_mb( ):用作多处理器架构上的内存屏障
  • smp_rmb( ) :用作多处理器架构上的内存读屏障
  • smp_wmb( ):用作多处理器架构上的内存写屏障

四、自旋锁

1.自旋锁

自旋锁是广泛使用的同步技术,当内核要访问共享数据结构或者进入临界区时就要自己获取一把锁。当内核想要访问由锁保护的资源时,就要尝试获取这把锁,如果没有人当前持有这把锁,则它就能获得这把锁,然后它就可以访问这个资源了;如果有人已经持有了这把锁,则它就无法获取这把锁,也就无法访问这个资源了。很显然锁是协作性质的,即要求访问资源的所有任务都遵循先获取允许,再使用,再释放资源的原则。
自旋锁是用在多处理环境下的特殊的锁。使用自旋锁时,如果当前锁被锁住而无法获取锁,则请求锁的任务一直循环等待该锁被释放(表现为当前CPU一直循环等待锁的释放)。
一般来说,由自旋锁保护的临界区要禁止内核抢占。在单处理器系统上,自旋锁不起锁的作用,此时自旋锁原语仅仅是禁止或启用内核抢占。另外需要注意的是在自旋锁忙等期间,内核抢占还是有效的,因此等待自旋锁被释放的任务可能被更高优先级的任务所替代。
自旋锁除了忙等之外,还有另外一个需要注意的影响:由于自旋锁主要是在SMP之间进行同步,因而操作自旋锁的CPU都需要看到自旋锁所在的内存的最新的值,因而它对高速缓存也有影响。自旋锁只适用于保护短的代码片段。

2.自旋锁的数据结构和宏、函数

Linux自旋锁由spinlock_t数据结构表示,它主要包括一个域:

  • slock: 表示自旋锁的状态,1表示“未加锁”状态,0和负值都表示“加锁”状态

自旋锁相关的宏(这些宏都基于原子操作):

  • spin_lock_init( ) :将自旋锁初始化为1
  • spin_lock( ):获取自旋锁,如果没办法获取就一直循环等待直到获取到自旋锁
  • spin_unlock( ) :释放自旋锁
  • spin_unlock_wait( ) :等待自旋锁被释放
  • spin_is_locked( ) :如果自旋锁是上锁的,则返回0,否则返回1
  • spin_trylock( ) :尝试获取自旋锁,如果无法获取就立即返回而不阻塞。获取到锁时会返回非0;否则返回0

除了这些版本外,还有可用于中断和软中断环境下的版本(中断版本:spin_lock_irq,会保存中断状态字的中断版本:spin_lock_irqsave,软中断版本:spin_lock_bh)。

3. 读写自旋锁

读写自旋锁是为了提高内核的并发能力。只要没有内核路径在修改数据结构,就可以允许多个内核路径同时读该数据结构。如果有内核路径想写该数据结构就必须获得写锁。简单的说就是写独占,读共享。
读写自旋锁由rwlock_t数据结构表示,它的lock域是一个32比特的字段,并且可以分为两个部分:

  • 一个24比特的计数器,表示对受保护的数据结构并发的进行读访问的内核控制路径的个数,计数器的补码放在比特0-23。
  • “未锁”标志字段,当没有内核控制路径在读或写时设置该位,否则清0。位于比特24

因而0x1000000表示未上锁,0x00000000表示写上锁,0x00ffffff表示一个读者,0xfffffe表示两个读者...

4.读写自旋锁的相关函数

 

  • read_lock:为读获取自旋锁,它类似于spin_lock(也会禁止内核抢占),区别在于它运行并发读。它原子的把自旋锁的值减1,如果得到一个非负值,就获得自旋锁,否则就原子的增加自旋锁的值以取消减去的1,然后循环等待lock的值变为正值,lock的值变为正值后会继续尝试获取读自旋锁。
  • read_unlock :为读释放自旋锁。它原子的减小lock字段的值,然后重新使能内核抢占。

 

注意:内核可能不支持抢占,这个时候可以忽略禁止和使能内核抢占的动作

 

  • write_lock :为写获取自旋锁,它类似于spin_lock( ) 和read_lock( )(也会禁止内核抢占)。它原子的从lock字段减去0x1000000,如果得到一个0,就获得写锁,否则函数原子的在自旋锁的值上加0x1000000以取消减操作。接着等待lock的值变为0x01000000,条件满足后会继续尝试获取读自旋。
  • write_unlock:为写释放自旋锁,它原子的给lock字段加上0x1000000,然后重新使能内核抢占。

和自旋锁类似,读写自旋锁也存在适用于中断和软中断的版本(中断版本:read_lock_irq,会保存中断状态字的中断版本:read_lock_irqsave,软中断版本:read_lock_bh)。

时间: 2024-11-08 22:53:03

linux内核同步之每CPU变量、原子操作、内存屏障、自旋锁【转】的相关文章

Linux内核同步机制之(四):spin lock【转】

转自:http://www.wowotech.net/kernel_synchronization/spinlock.html 一.前言 在linux kernel的实现中,经常会遇到这样的场景:共享数据被中断上下文和进程上下文访问,该如何保护呢?如果只有进程上下文的访问,那么可以考虑使用semaphore或者mutex的锁机制,但是现在中断上下文也参和进来,那些可以导致睡眠的lock就不能使用了,这时候,可以考虑使用spin lock.本文主要介绍了linux kernel中的spin loc

Linux 内核同步之自旋锁与信号量的异同【转】

转自:http://blog.csdn.net/liuxd3000/article/details/8567070 Linux 设备驱动中必须解决的一个问题是多个进程对共享资源的并发访问,并发访问会导致竞态,linux 提供了多种解决竞态问题的方式,这些方式适合不同的应用场景.   Linux 内核是多进程.多线程的操作系统,它提供了相当完整的内核同步方法.内核同步方法列表如下: 中断屏蔽 原子操作 自旋锁 读写自旋锁 顺序锁 信号量 读写信号量 BKL (大内核锁) Seq 锁 一.并发与竞态

Linux内核同步机制之(三):memory barrier【转】

转自:http://www.wowotech.net/kernel_synchronization/memory-barrier.html 一.前言 我记得以前上学的时候大家经常说的一个词汇叫做所见即所得,有些编程工具是所见即所得的,给程序员带来极大的方便.对于一个c程序员,我们的编写的代码能所见即所得吗?我们看到的c程序的逻辑是否就是最后CPU运行的结果呢?很遗憾,不是,我们的"所见"和最后的执行结果隔着: 1.编译器 2.CPU取指执行 编译器将符合人类思考的逻辑(c代码)翻译成了

linux内存屏障浅析

内存屏障是一个很神奇的东西,之前翻译了linux内核文档memory-barriers.txt,对内存屏障有了一定有理解.现在用自己的方式来整理一下. 在我看来,内存屏障主要解决了两个问题:单处理器下的乱序问题和多处理器下的内存同步问题. 为什么会乱序 现在的CPU一般采用流水线来执行指令.一个指令的执行被分成:取指.译码.访存.执行.写回.等若干个阶段.然后,多条指令可以同时存在于流水线中,同时被执行. 指令流水线并不是串行的,并不会因为一个耗时很长的指令在"执行"阶段呆很长时间,而

Linux内核剖析 之 内核同步

主要内容     1.内核请求何时以交错(interleave)的方式执行以及交错程度如何.     2.内核所实现的基本同步机制.     3.通常情况下如何使用内核提供的同步机制. 内核如何为不同的请求服务     哪些服务?     ====>>>     为了更好地理解内核是如何执行的,我们把内核看做必须满足两种请求的侍者:一种请求来自顾客,另一种请求来自数量有限的几个不同的老板.对于不同的请求,侍者采用如下的策略:     1.老板提出请求时,如果侍者空闲,则侍者开始为老板服务

大话Linux内核中锁机制之原子操作、自旋锁【转】

转自:http://blog.sina.com.cn/s/blog_6d7fa49b01014q7p.html 多人会问这样的问题,Linux内核中提供了各式各样的同步锁机制到底有何作用?追根到底其实是由于操作系统中存在多进程对共享资源的并发访问,从而引起了进程间的竞态.这其中包括了我们所熟知的SMP系统,多核间的相互竞争资源,单CPU之间的相互竞争,中断和进程间的相互抢占等诸多问题. 通常情况下,如图1所示,对于一段程序,我们的理想是总是美好的,希望它能够这样执行:进程1先对临界区完成操作,然

大话Linux内核中锁机制之原子操作、自旋锁

转至:http://blog.sina.com.cn/s/blog_6d7fa49b01014q7p.html 很多人会问这样的问题,Linux内核中提供了各式各样的同步锁机制到底有何作用?追根到底其实是由于操作系统中存在多进程对共享资源的并发访问,从而引起了进程间的竞态.这其中包括了我们所熟知的SMP系统,多核间的相互竞争资源,单CPU之间的相互竞争,中断和进程间的相互抢占等诸多问题. 通常情况下,如图1所示,对于一段程序,我们的理想是总是美好的,希望它能够这样执行:进程1先对临界区完成操作,

Linux内核的内存屏障

内容: 抽象的内存访问模型 设备操作 保障 什么是内存屏障? 内存屏障的种类 什么是内存屏障不能确保的? 数据依赖屏障 控制依赖 SMP屏障配对 内存屏障顺序的例子 read内存屏障与load预取 传递性 显式内核屏障 编译屏障 CPU内存屏障 MMIO write屏障 隐式内核内存屏障 锁功能 中断禁用功能 休眠和唤醒功能 其它函数 CPU之间的锁屏障效应 锁与内存访问 锁与I/O访问 什么地方需要内存障碍? 多处理器间的交互 原子操作 设备访问 中断 内核的I/O屏障效应 假想的最小执行顺序

内核同步机制-优化屏障和内存屏障

优化屏障 编译器编译源代码时,会将源代码进行优化,将源代码的指令进行重排序,以适合于CPU的并行执行.然而,内核同步必须避免指令重新排序,优化屏障(Optimization barrier)避免编译器的重排序优化操作,保证编译程序时在优化屏障之前的指令不会在优化屏障之后执行. Linux用宏barrier实现优化屏障,gcc编译器的优化屏障宏定义列出如下(在include/linux/compiler-gcc.h中):  #define barrier() __asm__ __volatile_