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-10-06 00:31:06

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

[内核同步]浅析Linux内核同步机制

转自:http://blog.csdn.net/fzubbsc/article/details/37736683?utm_source=tuicool&utm_medium=referral 很早之前就接触过同步这个概念了,但是一直都很模糊,没有深入地学习了解过,近期有时间了,就花时间研习了一下<linux内核标准教程>和<深入linux设备驱动程序内核机制>这两本书的相关章节.趁刚看完,就把相关的内容总结一下.为了弄清楚什么事同步机制,必须要弄明白以下三个问题: 什么是互

浅析Linux内核同步机制

很早之前就接触过同步这个概念了,但是一直都很模糊,没有深入地学习了解过,近期有时间了,就花时间研习了一下<linux内核标准教程>和<深入linux设备驱动程序内核机制>这两本书的相关章节.趁刚看完,就把相关的内容总结一下.为了弄清楚什么事同步机制,必须要弄明白以下三个问题: 什么是互斥与同步? 为什么需要同步机制? Linux内核提供哪些方法用于实现互斥与同步的机制? 1.什么是互斥与同步?(通俗理解) 互斥与同步机制是计算机系统中,用于控制进程对某些特定资源的访问的机制. 同步

Linux内核同步机制之(二):Per-CPU变量

转自:http://www.wowotech.net/linux_kenrel/per-cpu.html 一.源由:为何引入Per-CPU变量? 1.lock bus带来的性能问题 在ARM平台上,ARMv6之前,SWP和SWPB指令被用来支持对shared memory的访问: SWP <Rt>, <Rt2>, [<Rn>] Rn中保存了SWP指令要操作的内存地址,通过该指令可以将Rn指定的内存数据加载到Rt寄存器,同时将Rt2寄存器中的数值保存到Rn指定的内存中去.

Linux内核同步 - Per-CPU变量

一.源由:为何引入Per-CPU变量? 1.lock bus带来的性能问题 在ARM平台上,ARMv6之前,SWP和SWPB指令被用来支持对shared memory的访问: SWP <Rt>, <Rt2>, [<Rn>] Rn中保存了SWP指令要操作的内存地址,通过该指令可以将Rn指定的内存数据加载到Rt寄存器,同时将Rt2寄存器中的数值保存到Rn指定的内存中去. 我们在原子操作那篇文档中描述的read-modify-write的问题本质上是一个保持对内存read和w

Linux内核同步机制

http://blog.csdn.net/bullbat/article/details/7376424 Linux内核同步控制方法有很多,信号量.锁.原子量.RCU等等,不同的实现方法应用于不同的环境来提高操作系统效率.首先,看看我们最熟悉的两种机制——信号量.锁. 一.信号量 首先还是看看内核中是怎么实现的,内核中用struct semaphore数据结构表示信号量(<linux/semphone.h>中): [cpp] view plaincopyprint? struct semaph

Linux内核同步机制--转发自蜗窝科技

Linux内核同步机制之(一):原子操作 http://www.wowotech.net/linux_kenrel/atomic.html 一.源由 我们的程序逻辑经常遇到这样的操作序列: 1.读一个位于memory中的变量的值到寄存器中 2.修改该变量的值(也就是修改寄存器中的值) 3.将寄存器中的数值写回memory中的变量值 如果这个操作序列是串行化的操作(在一个thread中串行执行),那么一切OK,然而,世界总是不能如你所愿.在多CPU体系结构中,运行在两个CPU上的两个内核控制路径同

Linux内核同步 - spin_lock

一.前言 在linux kernel的实现中,经常会遇到这样的场景:共享数据被中断上下文和进程上下文访问,该如何保护呢?如果只有进程上下文的访问,那么可以考虑使用semaphore或者mutex的锁机制,但是现在中断上下文也参和进来,那些可以导致睡眠的lock就不能使用了,这时候,可以考虑使用spin lock.本文主要介绍了linux kernel中的spin lock的原理以及代码实现.由于spin lock是architecture dependent代码,因此,我们在第四章讨论了ARM3

Linux内核同步 - Read/Write spin lock

一.为何会有rw spin lock? 在有了强大的spin lock之后,为何还会有rw spin lock呢?无他,仅仅是为了增加内核的并发,从而增加性能而已.spin lock严格的限制只有一个thread可以进入临界区,但是实际中,有些对共享资源的访问可以严格区分读和写的,这时候,其实多个读的thread进入临界区是OK的,使用spin lock则限制一个读thread进入,从而导致性能的下降. 本文主要描述RW spin lock的工作原理及其实现.需要说明的是Linux内核同步机制之

Linux内核同步机制之completion【转】

Linux内核同步机制之completion 内核编程中常见的一种模式是,在当前线程之外初始化某个活动,然后等待该活动的结束.这个活动可能是,创建一个新的内核线程或者新的用户空间进程.对一个已有进程的某个请求,或者某种类型的硬件动作,等等.在这种情况下,我们可以使用信号量来同步这两个任务.然而,内核中提供了另外一种机制--completion接口.Completion是一种轻量级的机制,他允许一个线程告诉另一个线程某个工作已经完成. 结构与初始化 Completion在内核中的实现基于等待队列(