转自: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变量、原子操作、内存屏障、自旋锁【转】的更多相关文章

  1. linux内核中的每cpu变量

    一.linux中的每cpu变量 看linux内核代码的时候,会发现大量的per_cpu(name, cpu),get_cpu_var(name)等出现cpu字眼的语句.从语句的意思可以看出是要使用与当 ...

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

    转自:http://blog.csdn.net/fzubbsc/article/details/37736683?utm_source=tuicool&utm_medium=referral ...

  3. Linux内核同步

    Linux内核剖析 之 内核同步 主要内容 1.内核请求何时以交错(interleave)的方式执行以及交错程度如何. 2.内核所实现的基本同步机制. 3.通常情况下如何使用内核提供的同步机制. 内核 ...

  4. Linux内核同步:自旋锁

    linux内核--自旋锁的理解 自旋锁:如果内核配置为SMP系统,自旋锁就按SMP系统上的要求来实现真正的自旋等待,但是对于UP系统,自旋锁仅做抢占和中断操作,没有实现真正的“自旋”.如果配置了CON ...

  5. 浅析Linux内核同步机制

    非常早之前就接触过同步这个概念了,可是一直都非常模糊.没有深入地学习了解过,最近有时间了,就花时间研习了一下<linux内核标准教程>和<深入linux设备驱动程序内核机制>这 ...

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

    转自:http://www.wowotech.net/linux_kenrel/per-cpu.html 一.源由:为何引入Per-CPU变量? 1.lock bus带来的性能问题 在ARM平台上,A ...

  7. Linux内核同步 - Per-CPU变量

    一.源由:为何引入Per-CPU变量? 1.lock bus带来的性能问题 在ARM平台上,ARMv6之前,SWP和SWPB指令被用来支持对shared memory的访问: SWP <Rt&g ...

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

    Linux内核同步机制之(一):原子操作 http://www.wowotech.net/linux_kenrel/atomic.html 一.源由 我们的程序逻辑经常遇到这样的操作序列: 1.读一个 ...

  9. Linux内核同步机制

    http://blog.csdn.net/bullbat/article/details/7376424 Linux内核同步控制方法有很多,信号量.锁.原子量.RCU等等,不同的实现方法应用于不同的环 ...

随机推荐

  1. 第七篇数字&字符串之练习题

    1.执行Python脚本的两种方式2.简述位.字节的关系3.简述ascii.unicode.utf-­‐8.gbk的关系4.请写出“李杰”分别用utf-­‐8和gbk编码所占的位数5.Pyhton单行 ...

  2. LAXCUS对数据存储的优化

        LAXCUS兼容行存储(NSM)和列存储(DSM)两种数据模型,实现了混合存储.同时在分布环境里,做到将数据的分发和备份自动处理,这样就不再需要人工干预了.     行存储,为了兼容广大用户对 ...

  3. ZooKeeper完全分布式安装与配置

    Apache ZooKeeper是一个为分布式应用所设计开源协调服务,其设计目是为了减轻分布式应用程序所承担的协调任务.可以为用户提供同步.配置管理.分组和命名服务. 1.环境说明 在三台装有cent ...

  4. Python 学习笔记之—— PIL 库

    PIL,全称 Python Imaging Library,是 Python 平台一个功能非常强大而且简单易用的图像处理库.但是,由于 PIL 仅支持到Python 2.7,加上年久失修,于是一群志愿 ...

  5. BZOJ 4004 JLOI2015 装备购买 高斯消元+线性基

    题目链接:http://www.lydsy.com/JudgeOnline/problem.php?id=4004 Description 脸哥最近在玩一款神奇的游戏,这个游戏里有 n 件装备,每件装 ...

  6. NO2——最短路径

    [Dijkstra算法] 复杂度O(n2) 权值必须非负 /* 求出点beg到所有点的最短路径 */ // 邻接矩阵形式 // n:图的顶点数 // cost[][]:邻接矩阵 // pre[i]记录 ...

  7. 软件工程项目组Z.XML会议记录 2013/09/25

    软件工程项目组Z.XML会议记录 [例会时间]2013年9月25日周三21:30-22:10 [例会形式]小组讨论 [例会地点]三号公寓楼会客厅 [例会主持]李孟 [会议记录]肖俊鹏 会议整体流程 签 ...

  8. url解析字符串

    课程链接:http://www.imooc.com/video/6711/0

  9. eniac世界第二台计算机

    ENIAC,全称为Electronic Numerical Integrator And Computer,即电子数字积分计算机.ENIAC是世界上第一台通用计算机,也是继ABC(阿塔纳索夫-贝瑞计算 ...

  10. java线程(1)——三种创建线程的方式

    前言 线程,英文Thread.在java中,创建线程的方式有三种: 1.Thread 2.Runnable 3.Callable 在详细介绍下这几种方式之前,我们先来看下Thread类和Runnabl ...