由于对同一资源的访问代码可能从多个路径触发并得到执行(比如:SMP,内核线程,中断处理程序,软中断处理函数,系统调用代表的用户程序在调度中体现出的并发访问等),而在访问该资源的代码执行过程中被打断,并在打断和重新执行期间,执行了其他路径引发的同一资源访问的代码,将造成意料之外的结果。 为了更好地理解内核代码是如何执行的,我们把内核看做必须满足以下几种请求的侍者:一种请求来自普通的顾客,另一种请求来自数量有限但是拥有VIP等级资格的顾客,还有一种请求来自店老板。对不同的请求,侍者采用如下的策略:
侍者提供的服务对应于CPU处于内核态时所执行的代码。如果CPU在用户态执行,则认为侍者处于空闲状态。 普通顾客的请求则相当于用户态进程发出的系统调用或异常。VIP顾客的请求相当于中断,不同等级的VIP顾客相当于不同优先级的中断。老板的请求则相当于内核抢占。 内核抢占和内核的调度策略相关。进程是具有优先级的,这跟调度策略有关:Linux中进程的优先级是动态的,调度程序更总进程的行为,并周期性的调整它们的优先级。通常进程可以分为三类:
Linux2.6内核开始支持内核抢占,这基于以下的考虑:假设当前有两个进程在运行:一个文本编辑程序和一个编译程序——正在占用CPU。用户停留在文件编辑程序的状态,当其按动键盘的一瞬间将触发中断,内核必须马上唤醒文本编辑进程,否则用户将认为系统的状态不稳定。 如果进程进入TASK_RUNNING状态,内核检查它的动态优先级是否大于当前正在运行的进程的优先级。如果是,current的执行被中断,并调用调度程序选择另一个进程运行(通常是刚刚进入TASK_RUNNING状态的进程)。另一种情况是当前进程的时间片到期,并且它自认为自己已经处理完自己需要处理的任务,无需继续占用CPU了,此时当前进程thread_info结构体中的TIF_NEED_RESCHED标记被设置,以便在下一次中断(很大概率是系统时钟中断)ISR处理结束时被调度程序调用。 继续回到上面的例子,内核跟踪文本编辑器的行为,并确认它是一个交互进程,此时它的优先级将高于当前的编译器进程优先级,因此,编辑进程的TIF_NEED_RESCHED标志将被设置,如此强迫内核处理完中断时激活调度程序,它将选择编辑进程并执行进程切换。因此,编辑进程可以很快相应用户的操作,并且由于被设置了TIF_NEED_RESCHED标志,可以在响应完毕后马上被编译进程抢占。 TIF_NEED_RESCHED标志作用意味着当前进程自愿放弃CPU,这和内核是否支持抢占无关;另一种情况是当前进程并不打算在时间片到期前放弃CPU,而更高优先级的进程由于某些原因被唤醒,比如中断。如果内核是抢占式的,高优先级进程将替换原进程。如果内核不是抢占式的,那么除非当前进程执行完系统调用或异常处理并在恢复到用户态时才有可能因触发调度程序而被新进程抢占,否则进程切换不会发生(即便发生了中断,并在ISR处理结束时触发了调度程序,也不会因为优先级高而抢占当前还未消耗完其时间片的进程)。 使能内核抢占的目的是为了减少用户态进程的分派延迟(Dispatch latency),即从进程变为可执行状态到它实际开始运行之间的时间间隔。 当然并不是低优先级的进程在任何时候都是可被抢占的:
当被current_thread_info宏引用的thread_info描述符中的preempt_count成员大于0是,当前进程就禁止了内核抢占。该成员是一个32位的int类型,但是同时表达了三个不同的计数器:
针对这几个字段,内核提供了以下的宏方便对它们的获取操作: include/linux/hardirq.h #define hardirq_count() (preempt_count() & HARDIRQ_MASK) #define softirq_count() (preempt_count() & SOFTIRQ_MASK) #define irq_count() (preempt_count() & (HARDIRQ_MASK | SOFTIRQ_MASK)) 以下的宏用于判断是否位于硬中断,软中断或者两者之中。 #define in_irq() (hardirq_count()) #define in_softirq() (softirq_count()) #define in_interrupt() (irq_count()) 综上所述,只有当内核正在执行系统调用和异常处理,且内核抢占没有被显示地禁用时,才可能抢占内核。 内核定义了一系列宏来处理preempt_count字段的抢占计数器b[7:0]。 表 43. 处理器抢占计数器字段宏
include/linux/preempt.h #define add_preempt_count(val) do { preempt_count() += (val); } while (0) #define sub_preempt_count(val) do { preempt_count() -= (val); } while (0) #define inc_preempt_count() add_preempt_count(1) #define dec_preempt_count() sub_preempt_count(1) #define preempt_count() (current_thread_info()->preempt_count) preempt_count对preempt_count成员进行引用,而inc_preempt_count和dec_preempt_count分别对其递增和递减。 #define preempt_disable() do { inc_preempt_count(); barrier(); } while (0) #define preempt_enable_no_resched() do { barrier(); dec_preempt_count(); } while (0) preempt_disable通过inc_preempt_count实现递增preempt_count,preempt_enable_no_resched通过dec_preempt_count递减,显然这里的重点在于barrier宏,它告诉编译器不要改变C语言对应的汇编语言的顺序,所以CPU不会乱序执行,这保证了对preempt_count的操作不会因为编译器优化而发生提前或者延后,也即调用preempt_disable之后的代码一定是在preempt_count增加1后执行,反之亦然。 #define preempt_enable() do { preempt_enable_no_resched(); barrier(); preempt_check_resched(); } while (0) preempt_enable是对preempt_enable_no_resched和preempt_check_resched的封装,中间插入了内存屏障。 kernel/sched.c asmlinkage void __sched preempt_schedule(void) { struct thread_info *ti = current_thread_info(); if (likely(ti->preempt_count || irqs_disabled())) return; do { add_preempt_count(PREEMPT_ACTIVE); schedule(); sub_preempt_count(PREEMPT_ACTIVE); barrier(); } while (unlikely(test_thread_flag(TIF_NEED_RESCHED))); } #define preempt_check_resched() do { if (unlikely(test_thread_flag(TIF_NEED_RESCHED))) preempt_schedule(); } while (0) preempt_schedule用于抢占调度,当前它只被preempt_check_resched调用。对于preempt_schedule的调用,应该始终通过preempt_check_resched,而非其自身。另外应该保持这部分代码被约束在可控的范围,而避免不必要的扩散。
一些额外的宏并用在SMP系统处理中。 #define get_cpu() ({ preempt_disable(); smp_processor_id(); }) #define put_cpu() preempt_enable() #define put_cpu_no_resched() preempt_enable_no_resched() 内存屏障保证高级语言,比如C语言的编译器在优化生成的代码时能够保证内存屏障前后的代码不会乱序,而导致违背本来的程序意图。内存屏障由一个名为barrier()的宏定义: include/linux/compiler-gcc.h #define barrier() __asm__ __volatile__("": : :"memory") 要彻底理解barrier()的作用,需要首先理解内嵌汇编。字符串"memory"向GCC声明:在此之前的C语言对应的汇编语言和此之后的汇编语言在优化时不要放在一起考虑。一个实际的例子如下所示: #define barrier() __asm__ __volatile__("": : :"memory") int g_test = 0; int main() { int *tmp = &g_test; *tmp = 100; // barrier(); if(*tmp == 100) return 0; return 1; } 编译命令如下,为了得到间接的代码,参数中加上了-O2优化选项。 arm-linux-gcc test.c -o test -O2 首先编译没有内存屏障宏的代码,并反汇编得到main函数对应的汇编指令: 00008334 <main>: 8334: e59f300c ldr r3, [pc, #12] ; 8348 <main+0x14> 8338: e3a02064 mov r2, #100 ; 0x64 833c: e3a00000 mov r0, #0 ; 0x0 8340: e5832000 str r2, [r3] 8344: e12fff1e bx lr 8348: 000104fc .word 0x000104fc 这里找不到*tmp == 100对应的汇编指令,显然编译器认为这句话是多余的,因为从*tmp = 100这句话开始,*tmp的值没有被任何语句改变过,所以它尝试了优化。接下来打开barrier()。 00008334 <main>: 8334: e59f3014 ldr r3, [pc, #20] ; 8350 <main+0x1c> 8338: e3a02064 mov r2, #100 ; 0x64 833c: e5832000 str r2, [r3] 8340: e5930000 ldr r0, [r3] 8344: e0500002 subs r0, r0, r2 8348: 13a00001 movne r0, #1 ; 0x1 834c: e12fff1e bx lr 8350: 00010504 .word 0x00010504 可以看到subs和movne指令,所以确实执行了比较操作。考虑何时需要这种需求呢?一个典型的示例就是内核抢占,在可被抢占前后的代码是必须严格顺序执行的,不然禁止抢占所保护的操作将丧失本来的意义。 临界区是一段代码,在任何内核控制路径进入临界区后必须全部执行完这段代码,而不被打断。如何确定系统调用,异常处理程序,可延迟函数和内核线程中的临界区是是首要任务。一旦临界区被确定,就必须对其采用适当的保护措施,以确保在任何时刻只有一个内核控制路径处于临界区。 例如,假设两个不同的中断处理程序要访问同一个包含了几个相关变量的数据结构,比如一个缓冲区和一个表示缓冲区大小的类型变量。所有影响该数据结构的语句都必须放入一个单独的临界区。如果是单CPU系统,可以采取访问共享数据结构时关闭中断的方式来实现临界区,因为只有在开中断的情况下,才可能发生内核控制路径的嵌套。 另外,如果相同的数据结构仅被系统调用服务例程所访问,而且系统中只有一个CPU,就可以非常简单地通过在访问共享数据结构时禁用内核抢占功能来实现临界区。 但是如果该共享数据即可能被某一中断ISR访问,又可能被系统调用访问呢?事实上内核约束了这种情况的产生,它们不会操作同一数据结构,而要么是原数据结构,要么是副本。在SMP系统中,情况要复杂得多,由于多个CPU可能同时执行内核路径,因此内核开发者不能假设只要禁用内核抢占功能,而且中断,异常和软中断处理程序都没有访问过该数据结构,才能保证这个数据结构能够安全地被访问。内核提供了各种不同的同步技术。 什么时候同步是不必要的?基于以下内核约束,它使得内核同步相对简单了:
基于以上这些内核编码的约束限制,内核同步在多数时候不那么紧迫:
"适用范围"一栏表示该同步技术是适用于系统中的所有CPU还是单个CPU。例如,本地中断的禁止只适用于一个CPU(系统中的其他CPU不受影响);相反,原子操作影响系统中的所有CPU(当 访问同一个数据结构时,几个CPU上的原子操作不能交错)。
表 44. 内核使用的同步技术
最好的同步技术就是把设计无需同步的内核放在首位。事实上每种显式的同步技术都有不容忽视的性能开销。 最简单也是最重要的同步技术包括把内核变量声明为每CPU变量(per-cpu variable)。每CPU变量主要是数据结构的数组,系统的每个CPU对应数组的一个元素。 一个CPU不应该访问其他CPU对应的数据元素,另外它可以随意度或者修改它自己的元素而不用担心出现竞争条件,因为它是唯一有资格这么做的CPU。但是,也意味着每CPU变量基本上只能在特殊情况下使用,也就是当它确定在系统的CPU上的数据在逻辑上是独立的时候。 每CPU的数组元素在注册中被排列以使每个数据结构存放在硬件告诉缓存的不同行,因此,对每CPU数据的并发不会导致告诉缓存行的窃用和失效。 虽然每CPU变量为来自不同CPU的并发访问提供保护,但对来自异步函数(中断处理程序和可延迟函数)的访问不提供保护,在这种情况下需要另外的同步技术。 无论是在单处理器还是SMP系统中,内核抢占都可能使每CPU变量产生竞争条件。总的原则是内核控制路径应该在禁用抢占的情况下访问每CPU变量。 #ifdef CONFIG_SMP #define DEFINE_PER_CPU(type, name) __attribute__((__section__(".data.percpu"))) PER_CPU_ATTRIBUTES __typeof__(type) per_cpu__##name #else #define DEFINE_PER_CPU(type, name) PER_CPU_ATTRIBUTES __typeof__(type) per_cpu__##name ...... 从DEFINE_PER_CPU对每CPU变量进行定义,对于单CPU系统来说,就是简单的一个变量per_cpu__##name,但是对于SMP系统来说,却需要链接脚本的帮助:它在编译期被放在.data.percpu段中。 arch/arm/kernel/vmlinux.lds.S .init : { ...... . = ALIGN(4096); __per_cpu_start = .; *(.data.percpu) *(.data.percpu.shared_aligned) __per_cpu_end = .; ...... } 这说明__per_cpu_start和__per_cpu_end标识.data.percpu段的开头和结尾。并且,整个.data.percpu这个section都在__init_begin和__init_end之间,也就是说,该section所占内存会在系统启动后释放掉,那么系统如何为每个CPU保留这些私有数据的? 在start_kernel中调用setup_per_cpu_areas。本质上只有定义了CONFIG_SMP,并且没有定义CONFIG_HAVE_SETUP_PER_CPU_AREA才会使用内核字节定义的每CPU变量初始化函数。如果定义了CONFIG_SMP,且定义了CONFIG_HAVE_SETUP_PER_CPU_AREA,那么该函数必须在对应架构的代码中定义,比如x86。 #ifndef CONFIG_HAVE_SETUP_PER_CPU_AREA unsigned long __per_cpu_offset[NR_CPUS] __read_mostly; EXPORT_SYMBOL(__per_cpu_offset); static void __init setup_per_cpu_areas(void) { unsigned long size, i; char *ptr; unsigned long nr_possible_cpus = num_possible_cpus(); /* Copy section for each CPU (we discard the original) */ size = ALIGN(PERCPU_ENOUGH_ROOM, PAGE_SIZE); ptr = alloc_bootmem_pages(size * nr_possible_cpus); for_each_possible_cpu(i) { __per_cpu_offset[i] = ptr - __per_cpu_start; memcpy(ptr, __per_cpu_start, __per_cpu_end - __per_cpu_start); ptr += size; } } #endif /* CONFIG_HAVE_SETUP_PER_CPU_AREA */ 在该函数中,为每个CPU分配一段内存,并将.data.percpu中的数据拷贝到其中,每个CPU各有一份,其中CPU n对应的专有数据区的首地址为__per_cpu_offset[n]。这样,前述相应于__per_cpu_start的偏移量per_cpu__runqueues就变成了相应于 __per_cpu_offset[n]的偏移量,这样.data.percpu这个段在系统初始化后就可以释放了。 为每CPU变量提供的函数和宏:
#define per_cpu_var(var) per_cpu__##var 以上的大多数宏都是基于per_cpu_var宏的扩展,以下代码对应SMP系统时的定义: #ifndef SHIFT_PERCPU_PTR #define SHIFT_PERCPU_PTR(__p, __offset) RELOC_HIDE((__p), (__offset)) #endif #ifndef __per_cpu_offset extern unsigned long __per_cpu_offset[NR_CPUS]; #define per_cpu_offset(x) (__per_cpu_offset[x]) #endif #define per_cpu(var, cpu) (*SHIFT_PERCPU_PTR(&per_cpu_var(var), per_cpu_offset(cpu))) #define __get_cpu_var(var) (*SHIFT_PERCPU_PTR(&per_cpu_var(var), my_cpu_offset)) #define __raw_get_cpu_var(var) (*SHIFT_PERCPU_PTR(&per_cpu_var(var), __my_cpu_offset)) SHIFT_PERCPU_PTR调用编译器提供的偏移宏RELOC_HIDE实现从数组起始地址到当前CPU对应的元素的偏移,per_cpu_offset则引用调整后的偏移数组。对应动态分配的每CPU变量来说,SMP系统上分配时,会将size乘以CPU的个数,并将大小圆整到cache_line_size。 每CPU变量对SMP系统至关重要,它保证了CPU间的数据访问的隔离,在单CPU系统上,它总是以单个独立的变量存在的。
若干汇编语言指令序列具有"读——修改——写"的特性,它们访问存储器单元两次,第一次读原值,第二次写新值。假设运行在两个CPU上的里那个个内核控制路径试图通过执行非原子操作来同时"读——修改——写"同一存储单元。首先,两个CPU都试图读同一单元,但是存储器仲裁器(对访问RAM芯片的操作进行串行化的硬件电路)插手,只允许其中的一个访问而让另一个延迟。然而,当第一个读操作完成后,延迟的CPU从那个存储器单元正好读到同一个(旧)值。然后,两个CPU都试图向那个存储器单元写一新值,总线存储器访问在一次被存储器仲裁器串行化,最后,两个写操作都成功。但是,全局的结果是不对的,因为两个CPU写入了同一(新)值。因此,两个交错的"读——修改——写"操作成了一个单独的操作。
避免"读——修改——写"指令引起的竞争条件的最容易的办法,就是确保这样的操作在芯片级是原子的。任何一个这样的操作都必须以单个指令执行,中间不能中断,且避免其它的CPU访问同一存储器单元。这些很小的原子操作(atomic operations)可以建立在其他更灵活机制的基础之上以创建临界区。 回顾一下80x86的指令:
在编写C代码程序是,不能保证编译器会为a = a + 1或者a++这样的操作使用一个原子指令。因此,Linux内核提供了一个专门的atomic_t类型(一个原子访问计时器)和一些对应的函数和宏。这个函数和宏作用于atomic_t类型的变量,并可以当做原子的汇编语言指令来使用。 arch/arm/include/asm/atomic.h static inline void atomic_set(atomic_t *v, int i) { unsigned long tmp; __asm__ __volatile__("@ atomic_set\n" "1: ldrex %0, [%1]\n" " strex %0, %2, [%1]\n" " teq %0, #0\n" " bne 1b" : "=&r" (tmp) : "r" (&v->counter), "r" (i) : "cc"); } 可以通过锁总线指令在一个指令中完成,比如x86。对于ARM来说,从ARMv6指令集开始引入了两个锁总线访问的指令ldrex和strex。它们必须成对出现:ldrex在读取数据之前会锁定总线,这个操作被称为MarkExclusiveGlobal,而在strex中才会执行ClearExclusiveGlobal用来解锁总线,这其中的操作都是原子的。但这样做就可以避免被中断打断吗?不能,这只是保证了总线访问的锁定,但是并没有禁中断,中断总是在一个指令执行完毕后被检查,所以如果在ldrex中发生了中断,并且中断ISR尝试了对v的操作,那么这个循环就可能会执行多次,并且中断对v的操作将丢失,所以中断处理中不应该调用用于原子操作的宏函数。最新的ARM指令集支持monitor标记机制,它不会锁总线,此时原子操作包含如下四个步骤:
在linux中,在所有中断的入口都会调用clrex来清除掉monitor标记的这个tag,那么如果step 1和step 3之间有中断发生,在中断处理完成返回之后step 3会失败(因为step1中的tag已经被清除),然后又会进入step 1重新读[addr]中的值。也就是无论如何都不会发生“交错读”这种现象。因为每次strex失败之后都会重新再读一次[addr]的值。类似于atomic_set,Linux定义了一些列的宏和函数: 表 45. Linux中的原子操作
#define atomic_add(i, v) (void) atomic_add_return(i, v) #define atomic_inc(v) (void) atomic_add_return(1, v) #define atomic_sub(i, v) (void) atomic_sub_return(i, v) #define atomic_dec(v) (void) atomic_sub_return(1, v) #define atomic_inc_and_test(v) (atomic_add_return(1, v) == 0) #define atomic_dec_and_test(v) (atomic_sub_return(1, v) == 0) #define atomic_inc_return(v) (atomic_add_return(1, v)) #define atomic_dec_return(v) (atomic_sub_return(1, v)) #define atomic_sub_and_test(i, v) (atomic_sub_return(i, v) == 0) #define atomic_add_negative(i,v) (atomic_add_return(i, v) < 0) 注意到这些含有test后缀的宏,比较运算并不在原子操作中进行,也即比较时,这个值可能已经被其他CPU更新。另一类原子函数用作位掩码操作。 表 46. Linux中的原子位处理函数
一种广泛应用的同步技术是加锁(locking)。当内核控制路径必须访问共享数据结构或进入临界区时,就需要为自己获取一把"锁"。由锁机制保护的资源非常类似于限制于房间内的资源,当某人进入房间时,就把门锁上。如果内核控制路径希望访问资源,就试图获取要是"打开门"。当且仅当资源空闲时,它才能成功。然后,只要它还想使用这个资源你,门就依然锁着。当内核控制路径释放了锁时,门就打开,另一个内核控制路径就可以进入房间。 自旋锁(spin lock)是用来在多处理器环境中工作的一种特殊的锁。如果内核控制路径发现自旋锁"开着",就获取锁并继续自己的执行。相反,如果内核控制路径发现锁运行在另一个CPU上的啮合控制路径"锁着",就在周围"旋转",反复执行一条紧凑的循环指令,直到锁被释放。 自旋锁的循环指令表示"忙等"。即使等待的内核控制路径无事可做,它也在CPU上保持运行。不错,自旋锁通常非常方便,因为很多内核资源只锁1毫秒的时间片段;所以说,spin_lock的开销还是比进程调度(context switch)少得多。 一般来说,由自旋锁所保护的每个临界区都是禁止内核抢占的。在单处理器系统上,这种锁本身并不起锁的作用,自旋锁仅仅禁止或启用内核抢占。请注意,在自旋锁忙等期间,内核抢占还是有效的,因此,等待自旋锁释放的所有进程有可能被更高优先级的进程替代。 对spinlock操作的定义与体系结构息息相关,对于SMP来说,必须要易于该体系的汇编指令来实现总线锁定,而对单CPU系统来说,将简单的多,它建立在内核抢占之上,如果没有使能CONFIG_PREEMPT,那么自旋锁什么也不做。spinlock头文件的注释有详细说明。对于SMP系统来说相关的头文件如下: include/linux/spinlock.h * asm/spinlock_types.h: contains the raw_spinlock_t/raw_rwlock_t and the * initializers* * asm/spinlock.h: contains the __raw_spin_*()/etc. lowlevel * implementations, mostly inline assembly code * linux/spinlock_api_smp.h: * contains the prototypes for the _spin_*() APIs. typedef struct { volatile unsigned int lock; } raw_spinlock_t;
对于单CPU的处理相当简单,此时根本就不会编译kernel/spinlock.c文件,相关的头文件如下: * linux/spinlock_type_up.h: contains the generic, simplified UP spinlock type. * (which is an empty structure on non-debug builds) * linux/spinlock_up.h: contains the __raw_spin_*()/etc. version of UP * builds. (which are NOPs on non-debug, non-preempt * builds) * linux/spinlock_api_up.h: builds the _spin_*() APIs. typedef struct { } raw_spinlock_t;
linux/spinlock_types.h根据单系统和SMP的头文件,定义了spinlock_t的通用类型: #if defined(CONFIG_SMP) # include <asm/spinlock_types.h> #else # include <linux/spinlock_types_up.h> #endif typedef struct { raw_spinlock_t raw_lock; #ifdef CONFIG_GENERIC_LOCKBREAK unsigned int break_lock; #endif } spinlock_t; 在Linux的SMP系统中中,每个自旋锁都用spinlock_t结构表示,其中包含两个字段:
linux/spinlock.h 封装了最终对外使用的spin_*()应用函数: include/linux/spinlock.h #define spin_lock(lock) _spin_lock(lock) include/linux/spinlock_api_up.h #define __LOCK(lock) do { preempt_disable(); __acquire(lock); (void)(lock); } while (0) #define _spin_lock(lock) __LOCK(lock) 这里很清楚的可以看到只是禁用内核抢占而已,而(void)(lock)只是防止编译器提示变量未使用,这里并没有使用任何真正的spinlock。而对于SMP系统来说,则相对复杂: kernel/spinlock.c void __lockfunc _spin_lock(spinlock_t *lock) { preempt_disable(); spin_acquire(&lock->dep_map, 0, 0, _RET_IP_); LOCK_CONTENDED(lock, _raw_spin_trylock, _raw_spin_lock); } 注意这里同单系统一样首先禁止内核抢占,然后LOCK_CONTENDED将调用体系结构相关的函数_raw_spin_lock,对于ARM来说,它的实现如下: include/linux/spinlock.h # define _raw_spin_lock(lock) __raw_spin_lock(&(lock)->raw_lock) arch/arm/include/asm/spinlock.h #define __raw_spin_lock_flags(lock, flags) __raw_spin_lock(lock) static inline void __raw_spin_lock(raw_spinlock_t *lock) { unsigned long tmp; __asm__ __volatile__( "1: ldrex %0, [%1]\n" " teq %0, #0\n" #ifdef CONFIG_CPU_32v6K " wfene\n" #endif " strexeq %0, %2, [%1]\n" " teqeq %0, #0\n" " bne 1b" : "=&r" (tmp) : "r" (&lock->lock), "r" (1) : "cc"); smp_mb(); } 这里就如原子操作一样尝试在锁总线的情况下来进行循环等待,在strexeq,teqeq和bne另一个CPU将会释放锁,也即将增加lock的值,这在下一次ldrex处理中将原子的获取该锁。但是这并不能保证中断不会打断该spinlock,所以依然会进行中断处理,并且在中断处理结束时,由于禁止内核抢占而跳过调度处理(参考irq_svc的处理)。 表 47. 自旋锁宏
kernel/spinlock.c void __lockfunc _spin_unlock(spinlock_t *lock) { spin_release(&lock->dep_map, 1, _RET_IP_); _raw_spin_unlock(lock); preempt_enable(); } SMP上的解锁过程,首先通过汇编代码_raw_spin_unlock释放锁,然后使能内核抢占,注意它们的顺序。 读写自旋锁的引入是为了增加内核的并发能力。只要没有内核控制路径对数据结构进行修改,读写自旋锁就允许多个内核控制路径同时读同一数据结构。如果一个内核控制路径相对这个结构进行写操作,那么它首先获取读写锁的写锁,写锁授权独占访问这个资源。当然你,允许对数据结构并发读可以提高系统性能。 每个读写自旋锁都是一个rwlock_t结构,与spinlock类似,它在单系统和SMP上定义也不同: include/linux/spinlock_types_up.h typedef struct { /* no debug version on UP */ } raw_rwlock_t; arch/arm/include/asm/spinlock_types.h typedef struct { volatile unsigned int lock; } raw_rwlock_t; SMP系统上,lock字段是一个32位的字段,分为两个不同的部分:
注意,如果自旋锁为空(设置了"未锁"标志且无读者),那么lock字段的值为0x01000000;如果写者已经获得自旋锁("未锁"标志清0且无读者),那么lock字段的值为0;如果一个、两个或多个进程因为读获取了自旋锁,那么lock字段的值为0x00ffffff,0x00fffffe等("未锁"标志清0,读者个数的二进制补码在0~23位上)。 <figure><title>内核RAM布局</title><graphic fileref="images/mdio/p.gif"/></figure>100=1 100=1 表 15 “Memory Hierarchy” < > [19] 强调 |
|