linux内核同步之每CPU变量、原子操作、内存屏障、自旋锁【转】
转自:http://blog.csdn.net/goodluckwhh/article/details/9005585
版权声明:本文为博主原创文章,未经博主允许不得转载。
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)。
linux内核同步之每CPU变量、原子操作、内存屏障、自旋锁【转】的更多相关文章
- linux内核中的每cpu变量
一.linux中的每cpu变量 看linux内核代码的时候,会发现大量的per_cpu(name, cpu),get_cpu_var(name)等出现cpu字眼的语句.从语句的意思可以看出是要使用与当 ...
- [内核同步]浅析Linux内核同步机制
转自:http://blog.csdn.net/fzubbsc/article/details/37736683?utm_source=tuicool&utm_medium=referral ...
- Linux内核同步
Linux内核剖析 之 内核同步 主要内容 1.内核请求何时以交错(interleave)的方式执行以及交错程度如何. 2.内核所实现的基本同步机制. 3.通常情况下如何使用内核提供的同步机制. 内核 ...
- Linux内核同步:自旋锁
linux内核--自旋锁的理解 自旋锁:如果内核配置为SMP系统,自旋锁就按SMP系统上的要求来实现真正的自旋等待,但是对于UP系统,自旋锁仅做抢占和中断操作,没有实现真正的“自旋”.如果配置了CON ...
- 浅析Linux内核同步机制
非常早之前就接触过同步这个概念了,可是一直都非常模糊.没有深入地学习了解过,最近有时间了,就花时间研习了一下<linux内核标准教程>和<深入linux设备驱动程序内核机制>这 ...
- Linux内核同步机制之(二):Per-CPU变量
转自:http://www.wowotech.net/linux_kenrel/per-cpu.html 一.源由:为何引入Per-CPU变量? 1.lock bus带来的性能问题 在ARM平台上,A ...
- Linux内核同步 - Per-CPU变量
一.源由:为何引入Per-CPU变量? 1.lock bus带来的性能问题 在ARM平台上,ARMv6之前,SWP和SWPB指令被用来支持对shared memory的访问: SWP <Rt&g ...
- Linux内核同步机制--转发自蜗窝科技
Linux内核同步机制之(一):原子操作 http://www.wowotech.net/linux_kenrel/atomic.html 一.源由 我们的程序逻辑经常遇到这样的操作序列: 1.读一个 ...
- Linux内核同步机制
http://blog.csdn.net/bullbat/article/details/7376424 Linux内核同步控制方法有很多,信号量.锁.原子量.RCU等等,不同的实现方法应用于不同的环 ...
随机推荐
- Ajax请求被缓存的几种处理方式
Ajax请求被缓存的几种处理方式 我们都知道IE会针对ajax请求的地址缓存请求结果,直到缓存过期之前,针对相同地址发出的请求,只有第一次会请求会真正发送到服务端.在某种情况下,这种缓存机制确实能提高 ...
- 在阿里云上遇见更好的Oracle(四)
2016.5.13,北京,第七届数据库技术大会. 从最初的itpub社区,到后来被it168收购,DBA社区的线下聚会发展成2010年第一届数据库技术大会(DTCC).第一届大会汇聚了社区内活跃的各位 ...
- CF 55D
Volodya is an odd boy and his taste is strange as well. It seems to him that a positive integer numb ...
- 机器学习/逻辑回归(logistic regression)/--附python代码
个人分类: 机器学习 本文为吴恩达<机器学习>课程的读书笔记,并用python实现. 前一篇讲了线性回归,这一篇讲逻辑回归,有了上一篇的基础,这一篇的内容会显得比较简单. 逻辑回归(log ...
- KMP板子+Trie板子
KMP算法是一个字符串匹配算法,最直白的用法就是在一个长度为n的字符串T中查找另一个长度为m字符串P的匹配(总之就是用于文本中进行单个字符串的匹配). 对于这个问题,暴力算法是很好做的,直接对于T的每 ...
- 隐马尔可夫模型HMM
隐马尔可夫模型HMM的探究 1 HMM基本概念1.1 定义1.2 观测序列生成过程1.3 HMM的三个问题2 概率计算算法2.1 直接计算算法2.2 前向算法forward algorithm2.3 ...
- (转载)Hadoop示例程序WordCount详解
最近在学习云计算,研究Haddop框架,费了一整天时间将Hadoop在Linux下完全运行起来,看到官方的map-reduce的demo程序WordCount,仔细研究了一下,算做入门了. 其实Wor ...
- [洛谷P3919]【模板】可持久化数组
题目大意:有两个操作,1:在第x次操作后的版本上修改一个值,2:查询在第x次操作后的版本上的一个节点的值 即: 你需要维护这样的一个长度为N的数组,支持如下几种操作 1.在某个历史版本上修改某一个位置 ...
- [AHOI2013]作业 & Gty的二逼妹子序列 莫队
---题面--- 题解: 题目要求统计一个区间内数值在[a, b]内的数的个数和种数,而这个是可以用树状数组统计出来的,所以可以考虑莫队. 考虑区间[l, r]转移到[l, r + 1],那么对于维护 ...
- codevs 1191 线段树 区间更新(水)
题目描述 Description 在一条数轴上有N个点,分别是1-N.一开始所有的点都被染成黑色.接着我们进行M次操作,第i次操作将[Li,Ri]这些点染成白色.请输出每个操作执行后剩余黑色点的个数. ...