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

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)。

一、信号量

1.信号量的概念

信号量也是一种锁,当信号量不可用时,尝试获取信号量的任务将挂起直到它拿到了信号量。由于尝试获取信号量的任务可能挂起,因而中断服务程序以及可延迟函数不能使用信号量。

对于信号量来说需要注意:

  1. 只有对信号量计数值的操作是原子的
  2. 信号量的自旋锁只用于保护信号量的等待队列
  3. 信号量是比较特殊的,其up操作不是必须由down操作的调用者发起。如果把信号量也看作是一把锁,则该锁是很特殊的,它不一定由持有锁的任务释放,任何其它任务都可以做释放动作即调用up
因此信号量的down和up是可以并发执行的。但是由于保护了信号量的计数值和等待队列,因而这种并发并不会导致up和down本身出问题。
因为down操作可能导致调用者休眠,因而不能休眠的场景是不允许调用该函数的,比如中断上下文,而up可以在任意上下文调用。如果要在中断上下文调用可以使用down_trylock,它尝试获取信号量,但是当无法获取时会返回1而不是等待,如果可以获取则返回0。

2.信号量的数据结构和相关API

1.数据结构

信号量用数据结构semaphore表示, 它包含以下域:
  • count:受该信号狼保护的资源的计数值,如果大于0表示资源可用;否则表示资源不可用。对它的操作必须是原子的。如果存在检查并更新的操作,这两个操作的组合必须也是原子的,比如down中的比较并减1。
  • wait_list:等待该信号量保护的资源可用的任务队列的链表。
  • lock:保护等待任务链表的自旋锁。

2.初始化

init_MUTEX( ) 将信号量的count域初始化为1,表示资源当前可用
init_MUTEX_LOCKED( ) 将信号量的count域初始化为0 ,表示资源当前不可用 
DECLARE_MUTEX 完成和 init_MUTEX类似的操作,但是它还多一个静态分配一个信号量的动作
DECLARE_MUTEX_LOCKED 和init_MUTEX_LOCKED类似,但是它还多一个静态分配一个信号量的动作
当然也可以将信号量的初始者设置为其它正值。

3.获取和释放信号量

up()函数用于释放信号量,如果当前信号量的等待队列为空,即没有任务在等待该信号量被释放,则它增加信号量的count值,然后返回,否则它唤醒等待队列上的第一个任务。
down用于获取信号量,如果信号量的值大于0,则它将count的值减1,并返回;否则调用者将被添加到等待队列的尾部,并等待直到被唤醒即该任务获得资源。
void down(struct semaphore *sem)
int down_trylock(struct semaphore *sem)//尝试获取信号量,但是当无法获取时会返回1而不是等待,如果可以获取则返回0
int down_interruptible(struct semaphore *sem)//获取信号量,但是在等待信号量的时候可以被打断,如果在等待过程中被打断,则返回-EINTR
int down_timeout(struct semaphore *sem, long jiffies) //用于获取信号量,但是最多等待jiffies长的时间,如果在指定的时间期限内没有获取信号量,则就返回-ETIME。
void up(struct semaphore *sem)

3. 读写信号量的概念

读写信号量类似于读写自旋锁,它是为读多写少的场景做了优化的信号量,不同于自旋锁的是,在无法获得信号量时,它挂起而不是自旋。
可以有多个任务并发的为读获取读写信号量,但是同一时间点只能有一个任务可以为写获得读写信号量。因此只有没有任何任务为读或写持有该信号量时,新的为写获取信号量的操作才能成功。
内核FIFO的方式存储等待队列中的任务:
  1. 如果读者或写者无法获取读写信号量,就会被添加到等待队列的尾部
  2. 当信号量被释放时,就唤醒等待队列中的第一个任务(先唤醒谁取决于实现采取的策略,代码最能说明问题,最好查阅实际的代码)
  3. 如果唤醒的第一个任务是写,则其它任务继续睡眠,如果唤醒的第一个任务是读,则会唤醒它之后的所有读任务直到碰到了一个写任务,该写任务以及在它之后的所有任务都继续睡眠
由于它还是信号量,因此其应用场景的限制和信号量相同。

4.读写信号量的数据结构

rw_semaphore数据结构用于表示读写信号量,它包含如下的域:
  • activity:0表示没有任务在读或者写,大于0表示有任务正在读,-1表示有一个任务正在写
  • wait_list:存放等待该信号量的任务的链表
  • wait_lock:用于保护等待任务链表的自旋锁
可以用init_rwsem(sem)初始化读写信号量,也可以用DECLARE_RWSEM(name)声明并初始化一个信号量
void down_read(struct rw_semaphore *sem)
int down_read_trylock(struct rw_semaphore *sem)//尝试为读获取信号量,但是当无法获取时会返回1而不是等待,如果可以获取则返回0
void down_write(struct rw_semaphore *sem)
int down_write_trylock(struct rw_semaphore *sem)//尝试为读获取信号量,但是当无法获取时会返回1而不是等待,如果可以获取则返回0
void up_read(struct rw_semaphore *sem)  
void up_write(struct rw_semaphore *sem)  
void downgrade_write(struct rw_semaphore *sem)//它将一个写信号量降格为读信号量,并唤醒等待队列上的读任务。因此它的调用者应该持有写信号量

二、顺序锁

1.顺序锁的概念

使用读写锁时,读锁和写锁的优先级是一样的。2.6内核引入的顺序锁和读写自旋锁相同,区别在于它给写者赋予了更高的优先级:在使用顺序锁时即便读者正在进行读操作,写者也可以进行写动作。读写锁的优点在于写者永远不会由于有读者正在进行读而等待,其缺点在于读者可能需要尝试读好多次才能读到合法的数据。

不是所有的数据类型都能用顺序锁来保护,如果要使用顺序锁,以下原则必须被遵循:

  1. 被保护的数据结构不能包含由写者保护而由读者释放的指针
  2. 读者临界区的代码不能有副作用
  3. 读者临界区应该很小,而且写者应该尽量少获取顺序锁,否则重复的读会造成一些性能损伤

2.数据结构

顺序锁使用数据结构seqlock_t表示,它包含两个域:
  • 自旋锁lock
  • 整数序列号
序列号作为顺序计数器存在。每个读者必须至少读这个值两次,一次在读数据之前,一次在读数据之后,如果两次读获取的序列号相同,则读到了一个合法的值,否则说明在读的过程中有写者更新了数据,因此读者需要重新读取。
有两种方法可以初始化顺序锁:
  1. seqlock_t lock1 = SEQLOCK_UNLOCKED;
  2. seqlock_t lock2; seqlock_init(&lock2);
这两种方法都会把顺序锁初始化未上锁状态。

1.写操作

写者必须先获取锁,再操作,然后再释放锁。
write_seqlock:用于为写获取顺序锁,它会获取顺序锁中的自旋锁,然后将顺序锁的序列号加1
write_sequnlock:用于释放顺序锁,它也会增加顺序锁的序列号,然后释放顺序锁中的自旋锁
这种设计确保了写者正在写数据并且没有完成时序列号为奇数,没有写者在修改数据时,序列号为偶数。

2.读操作

对于读者来说,它需要采取下列形式的操作序列:
    unsigned int seq;
    do {
        seq = read_seqbegin(&seqlock);
        /* ... CRITICAL REGION ... */
    } while (read_seqretry(&seqlock, seq));
read_seqbegin:返回顺序锁当前序列号的值
read_seqretry:如果指定的值和顺序锁的序列号的值不等或者顺序锁的序列号为奇数,则返回1。
需要注意的是,读者并不会关闭内核抢占,而由于写者获取了顺序锁中的自旋锁,因而它会禁止内核抢占。

三、Read-Copy Update (RCU)

RCU是另外一种被设计用来在SMP环境下保护主要操作是读操作的数据同步技术。RCU允许多个读者和多个写者同时并发操作。RCU没使用任何锁也没使用任何由多个CPU共享的计数器;相比于读写自旋锁和顺序锁,这是一个极大地优势。
RCU有极大地优势,但是也有很大的限制,它限制了它所能保护的数据结构:
  • 被保护的资源应当是动态分配的、通过指针来存取的, 并且所有对这些资源的引用必须由原子代码持有
  • 当进入由RCU保护的临界区时不能休眠
RCU操作包括读操作,写操作,以及释放旧版本的操作组成。

1.写操作

它的实现原理是:当数据结构需要改变时,写线程做一个拷贝,改变这个拷贝(这里需要一个内存屏障,以保证更新能被其它CPU看到),接着使相关的指针指向新的版本,当内核确认没有CPU还在引用旧版本时旧的版本就可以被释放.

2.读操作

当内核代码想要读取一个由RCU保护的数据结构时,它
  1. 调用rcu_read_lock(相当于preempt_disable)
  2. 进行读访问
  3. 调用rcu_read_unlock(相当于preempt_enable)
这里需要注意的是在1和3之间的代码不允许休眠。

3.释放旧的版本

在RCU中关键的是旧版本何时释放。由于其它处理器上的代码可能还有对旧的数据的引用,因而不能立即释放它。内核必须在它确保已经没有任何指向旧版本的引用时才能释放旧版本。实际上,只有当所有读者都调用了rcu_read_unlock后才能释放旧的拷贝。内核要求每个读者在开始下列动作之前调用rcu_read_unlock宏:
  • CPU进行进程切换
  • CPU开始在向用户模式转变
  • CPU开始执行idle进程
call_rcu函数由写者调用以清除旧的数据结构。该函数原型如下:
void call_rcu(struct rcu_head *head, void (*func)(void *arg), void *arg);
  • head:其中head是rcu_head类型的数据结构指针,它通常嵌入在受保护的数据结构中。
  • func:在可以释放RCU数据结构时会被调用以释放旧的数据结构
call_rcu函数会在rcu_head描述符中存放回调函数的地址和它的参数,然后将描述符插入到一个每CPU回调链表中。内核会定期检查是否可以释放RCU数据结构了,如果是的话,就会由一个tasklet调用回调函数。

四、完成量(Completions)

1.完成量的概念

内核中常见的一种场景是在当前任务启动另外一个任务,然后等待该任务完成做完某个事情。考虑使用信号量来完成这个工作:
struct semaphore sem;
init_MUTEX_LOCKED(&sem);
start_external_task(&sem);
down(&sem);
当外部完成我们期望的动作时,它调用up(&sem)。
但是信号量并不是特别适合这种场景,信号量对“可用”的情况做了优化,即使用信号量时期望在大部分情况下它是可用的。然而在上述场景中很显然down必然走的是信号量不可用的分支。
completion是用来解决这种问题的一种机制。completion允许一个任务告诉另一个任务工作已经完成。

2.数据结构和相关API

内核使用数据结构completion来表示completion
void init_completion(struct completion *x)
可以使用该函数完成completion的初始化或者通过DECLARE_COMPLETION(work)声明并初始化一个completion,INIT_COMPLETION(x)宏用户初始化completion x。
void wait_for_completion(struct completion *c);
该函数在c上等待,并且不可打断。如果调用了wait_for_completion而没有任何任务调用complete,则wait_for_completion的将永远等待。
wait_for_completion_interruptible(struct completion *x)
该函数在x上等待,但是可能在等待过程中被打断,当由于被打断而返回时,返回值为-ERESTARTSYS;否则返回0
void complete(struct completion *c);
void complete_all(struct completion *c);
这两个函数可用于唤醒在c上等待的任务。区别在于complete只唤醒一个等待的任务,而complete_all唤醒所有的。
completion 机制的典型使用是在模块退出时与内核线程的终止一起. 在这个原型例子里,
void complete_and_exit(struct completion *c, long retval);
唤醒在c上等待的任务,并且以retval退出本任务。

五、关闭本地中断

关闭中断是一种设置临界区的方法。当关闭了中断时,即便是硬件中断也无法打断代码的运行,因而它可以保护同时被中断服务程序访问的数据结构。但是需要注意的是关闭本地中断并不能保护可能被多个CPU访问的数据结构。因此在SMP架构下,往往需要用关闭中断和自旋锁想结合的方式来保护被中断服务程序使用的共享资源。
local_irq_disable() 宏用来在本地CPU关闭中断
local_irq_enable() 宏用来在本地CPU打开中断, which makes use of the of the sti assembly language instruction, enables them. As stated in the 使用这两个函数的问题在于,在需要的时候我们可以简单的关闭中断,但是简单粗暴的打开中断不一定是正确的,因为在我们关闭中断时,中断可能已经是关闭的,这时如果我们简单的打开了中断就可能导致问题。
local_irq_save:关闭中断并且保存中断状态字
local_irq_restore:以指定的中断状态字恢复中断
这两个宏就很好的解决了问题,因为local_irq_restore只是将中断恢复到了我们调用local_irq_save时的状态。

六、使能和关闭可延迟函数

由于可延迟函数在不可预期的时间点被执行,因而被可延迟函数访问的数据结构也需要进行保护。
禁止可延迟函数执行的最简单的办法是关闭本地中断,因为这样中断服务程序就没办法执行了,也就没办法启动可延迟函数。
由于软中断在处于中断状态时不会执行,而tasklet是基于软中断实现的,因而只要禁止本地的软中断就可以在本地CPU上禁止可延迟函数。
local_bh_disable宏用于将本地CPU的软中断计数加1,因而就禁止了本地的软中断。
local_bh_enable宏用于将本地CPU的软中断计数减1,用于打开本地软中断
这两个函数可以都可以被重复调用,但是调用了多少次local_bh_disable,就要相应的调用多少次local_bh_enable才能打开软中断

七、选择同步技术

有很多技术都可用于避免访问共享的数据时出现竞态的手段。但是各种手段对系统性能的影响是不同的。但是作为一条原则,应该使用在该场景下能获得最大并发等级或者说并发数量的技术手段。系统的并发等级取决于:

  • 可以并发操作的I/O设备数
  • 做有效工作的CPU数
为了最大化I/O吞度量,应该使得关闭中断的时间尽可能短。

为了提高CPU效率,应该尽可能避免使用自旋锁。因为它不仅导致自旋的CPU处于忙等状态,而且会对高速缓存造成不利的影响。

取决于访问共享数据的内核任务的类型,需要采用的同步技术也会有区别:

任务 单处理器环境下使用的同步技术 多处理器环境下使用的额外的同步技术
可休眠任务(内线线程,系统调用) 信号量 不需要额外的同步技术
中断 关闭本地中断 自旋锁
可延迟函数 不需要 不需要或者使用自旋锁(取决于不同的tasklet是否会访问相同的数据结构)
可休眠任务+中断 关闭本地中断 自旋锁
可休眠任务+可延迟函数 关闭本地软中断 自旋锁
中断+可延迟函数 关闭本地中断 自旋锁
可休眠任务+中断+可延迟函数 关闭本地中断 自旋锁


你可能感兴趣的:(linux内核同步之每CPU变量、原子操作、内存屏障、自旋锁)