什么是内核是什么同步

Linux内核同步机制
1. 原子操作
原子操作需要硬件的支持,因此是架构相关的,都使用汇编语言实现,因为C语言并不能实现这样的操作。原子操作主要用于实现资源计数,很多引用计数(refcnt)就是通过原子操作实现的。
原子类型定义如下:
typedef struct {
} atomic_t;
在TCP/IP协议栈的IP碎片处理中,就使用了引用计数,碎片队列结构struct
ipq描述了一个IP碎片,字段refcnt就是引用计数器,它的类型为atomic_t。
在i386中,信号量的结构
struct semaphore {
& & atomic_
& & wait_queue_head_
DECLARE_MUTEX(name)该宏声明一个信号量name并初始化它的值为0,即声明一个互斥锁。
void down(struct semaphore *
sem);该函数用于获得信号量sem,它会导致睡眠,因此不能在中断上下文(包括IRQ上下文和softirq上下文)使用该函数。
int down_interruptible(struct semaphore *
sem);该函数功能与down类似,不同之处为,down不会被信号(signal)打断,但down_interruptible能被信号打断,因此
该函数有返回值来区分是正常返回还是被信号中断,如果返回0,表示获得信号量正常返回,如果被信号打断,返回-EINTR。
int down_trylock(struct semaphore *
sem);该函数试着获得信号量sem,如果能够立刻获得,它就获得该信号量并返回0,否则,表示不能获得信号量sem,返回值为非0值。因此,它不会导致调用者睡眠,可以在中断上下文使用。
3. 读写信号量
读写信号量适于在读多写少的情况下使用,在linux内核中对进程的内存映像描述结构的访问就使用了读写信号量进行保护。在Linux中,每一个进程都用
一个类型为task_t或struct task_struct的结构来描述,该结构的类型为struct
mm_struct的字段mm描述了进程的内存映像,特别是mm_struct结构的mmap字段维护了整个进程的内存块列表,该列表将在进程生存期间被
大量地遍历或修改,因此mm_struct结构就有一个字段mmap_sem来对mmap的访问进行保护,mmap_sem就是一个读写信号量,在
proc文件系统里有很多进程内存使用情况的接口,通过它们能够查看某一进程的内存使用情况,命令free、ps和top都是通过proc来得到内存使用
信息的,proc接口就使用down_read和up_read来读取进程的mmap信息。当进程动态地分配或释放内存时,需要修改mmap来反映分配或
释放后的内存映像,因此动态内存分配或释放操作需要以写者身份获得读写信号量mmap_sem来对mmap进行更新。系统调用brk和munmap就使用
了down_write和up_write来保护对mmap的访问。
4. 自旋锁(如何实现的?)
自旋锁与互斥锁有点类似,只是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了
锁,"自旋"一词就是因此而得名。由于自旋锁使用者一般保持锁时间非常短,因此选择自旋而不是睡眠是非常必要的,自旋锁的效率远高于互斥锁。
信号量和读写信号量适合于保持时间较长的情况,它们会导致调用者睡眠,因此只能在进程上下文使用(_trylock的变种能够在中断上下文使用),而自旋锁适合于保持时间非常短的情况,它可以在任何上下文使用。
自旋锁保持期间是抢占失效的,而信号量和读写信号量保持期间是可以被抢占的。自旋锁只有在内核可抢占或SMP的情况下才真正需要,在单CPU且不可抢占的内核下,自旋锁的所有操作都是空操作。
获得自旋锁和释放自旋锁有好几个版本,因此让读者知道在什么样的情况下使用什么版本的获得和释放锁的宏是非常必要的。
5. 大内核锁
6. 读写锁(rwlock)
读写锁实际是一种特殊的自旋锁,它把对共享资源的访问者划分成读者和写者,读者只对共享资源进行读访问,写者则需要对共享资源进行写操作。这种锁相对于自
旋锁而言,能提高并发性,因为在多处理器系统中,它允许同时有多个读者来访问共享资源,最大可能的读者数为实际的逻辑CPU数。写者是排他性的,一个读写
锁同时只能有一个写者或多个读者(与CPU数相关),但不能同时既有读者又有写者。在读写锁保持期间也是抢占失效的。
7. 大读者锁
大读者锁是读写锁的高性能版,读者可以非常快地获得锁,但写者获得锁的开销比较大。
只存在于2.4中,2.6中已经没有。大读者锁的实现机制是:每一个大读者锁在所有CPU上都有一个本地读者写者锁,一个读者仅需要获得本地CPU的读者锁,而写者必须获得所有CPU上的锁。
8. RCU(Read Copy Update)
对于被RCU保护的共享数据结构,读者不需要获得任何锁就可以访问它,但写者在访问它时首先拷贝一个副本,然后对副本进行修改,最后使用一个回调
(callback)机制在适当的时机把指向原来数据的指针重新指向新的被修改的数据。这个时机就是所有引用该数据的CPU都退出对共享数据的操作。
RCU也是读写锁的高性能版本,但是它比大读者锁具有更好的扩展性和性能。
RCU既允许多个读者同时访问被保护的数据,又允许多个读者和多个写者同时访问被保护的数据(注意:是否可以有多个写者并行访问取决于写者之间使用的同步
机制),读者没有任何同步开销,而写者的同步开销则取决于使用的写者间同步机制。但RCU不能替代读写锁,因为如果写比较多时,对读者的性能提高不能弥补
写者导致的损失。
Array. 顺序锁
顺序锁也是对读写锁的一种优化,对于顺序锁,读者绝不会被写者阻塞,也就说,读者可以在写者对被顺序锁保护的共享资源进行写操作时仍然可以继续读,
而不必等待写者完成写操作,写者也不需要等待所有读者完成读操作才去进行写操作。但是,写者与写者之间仍然是互斥的,即如果有写者在进行写操作,其他写者
必须自旋在那里,直到写者释放了顺序锁。
这种锁有一个限制,它必须要求被保护的共享资源不含有指针,因为写者可能使得指针失效,但读者如果正要访问该指针,将导致OOPs。
已投稿到:
以上网友发言只代表其个人观点,不代表新浪网的观点或立场。trackbacks-0
1、原子操作,是其它同步方法的基础。
2、自旋锁,线程试图获取一个已经被别人持有的自旋锁,当前线程处于忙等待,占用cpu资源。
3、读写自旋锁,根据通用性和针对性的特点,普通自旋锁在特定场景下的表现会退化。因此,提供了读写自旋锁,读锁可以加读锁,不能加写锁,写锁不能加任何锁。
4、需要注意的几项:
  普通自旋锁是不能递归的。读锁可以递归,写锁也不能递归。
  表面上锁的是代码,实际上锁的是共享数据。
  使用读写锁的时候,需要注意,读锁可以加读锁,多个线程都占用读锁,必须所有的线程都释放,才能加上写锁,这往往会导致写锁长时间处于饥饿状态。
5、自旋锁存在的问题,线程试图获取一个已经被别人持有的自旋锁,当前线程处于忙等待,占用cpu资源。怎么解决这个问题?
  使用信号量,信号量是一种睡眠锁。一个任务试图获取被别人占有的信号量,信号量会将其推进一个等待队列,让其睡眠,当请求的信号量被释放,处于等待队列的任务被唤醒,并获得信号量。
6、需要注意的是,信号量是一种睡眠锁,但它本身也会带有开销,上下文切换,被阻塞的线程要换出换入,也即是说让其睡眠并唤醒它,花费一定的开销。如果每个线程锁的时间很短,一般使用自旋锁,忙等待的时间也很短。如果锁的时间长,使用信号量。
7、相比自旋锁,信号量还有更广泛的用处,使用PV操作不仅能保护共享资源,还能够控制同时访问的数量,还能够控制访问顺序。对于锁,是谁加锁谁释放,而信号量可以再不同线程之间PV操作。
8、考虑信号量的一种特殊使用场景,可以睡眠的互斥锁。创建的信号量容量为1(也就是允许同时访问的数量也就是1),可用数量为0,先进行V操作放入一个资源,使可用资源为1。这就是互斥体,互斥体加锁可以认为是P操作,然后做一些事情,然后解锁,也就是V操作。
阅读(...) 评论()114网址导航20. 内核同步
20. 内核同步
20.&内核同步
由于对同一资源的访问代码可能从多个路径触发并得到执行(比如:SMP,内核线程,中断处理程序,软中断处理函数,系统调用代表的用户程序在调度中体现出的并发访问等),而在访问该资源的代码执行过程中被打断,并在打断和重新执行期间,执行了其他路径引发的同一资源访问的代码,将造成意料之外的结果。
为了更好地理解内核代码是如何执行的,我们把内核看做必须满足以下几种请求的侍者:一种请求来自普通的顾客,另一种请求来自数量有限但是拥有VIP等级资格的顾客,还有一种请求来自店老板。对不同的请求,侍者采用如下的策略:
侍者为普通顾客依次服务,或者空闲。
VIP顾客提出请求时,如果侍者正空闲,则侍者开始为其服务。如果正在为普通顾客服务,那么侍者停止服务,开始为VIP顾客服务。
如果侍者正在为VIP顾客服务,另一个更高级一级的VIP客户发出请求,那么中断VIP顾客的服务,然后为高级VIP客户服务,服务完毕后再为原VIP顾客服务。
如果侍者正在为顾客服务,并且不管是普通顾客,还是VIP级很高的客户,当老板命令侍者停止它的服务,而执行新的服务任务,侍者此时在完成老板的任务后,可能暂时并不理会原来的顾客而去为新选中的顾客服务。
侍者提供的服务对应于CPU处于内核态时所执行的代码。如果CPU在用户态执行,则认为侍者处于空闲状态。
普通顾客的请求则相当于用户态进程发出的系统调用或异常。VIP顾客的请求相当于中断,不同等级的VIP顾客相当于不同优先级的中断。老板的请求则相当于内核抢占。
20.1.&内核抢占
内核抢占和内核的调度策略相关。进程是具有优先级的,这跟调度策略有关:Linux中进程的优先级是动态的,调度程序更总进程的行为,并周期性的调整它们的优先级。通常进程可以分为三类:
交互式进程:用户的思考时间要长于操作的间隔时间,但是一旦操作就必须立即相应,比如Shell,编辑程序,多媒体应用等。
批处理进程:这些进程几乎不与用户交互,经常在后台运行。因此该进程不必很快的相应,因此受到调度程序的慢待。比如:编译程序,数据库引擎和科学计算。
实时进程:这类进程有很强的调度需求,不可被低优先级的进程阻塞,响应时间必须很短并且稳定在一个小的范围内。这类程序有音视频应用,机器人控制和传感器的信息收集程序等。
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),即从进程变为可执行状态到它实际开始运行之间的时间间隔。
当然并不是低优先级的进程在任何时候都是可被抢占的:
内核正在执行中断服务例程。
可延迟函数被禁止(当内核正在执行软中断或tasklet时经常如此)。
通过抢占计数器设置为正数而显示地禁用内核抢占。
当被current_thread_info宏引用的thread_info描述符中的preempt_count成员大于0是,当前进程就禁止了内核抢占。该成员是一个32位的int类型,但是同时表达了三个不同的计数器:
b[7:0] 抢占计数器。记录显式禁用本地CPU内核抢占的次数,值等于0表示允许内核抢占。范围为0~255。
b[15:8] 软中断计数器:表示可延迟函数被禁用的程度,范围为0~255。
b[27:16] 硬中断计数器:表示在本地CPU上中断处理程序的嵌套数(irq_enter和irq_exit分别对它进行递加和递减)。
b[28] PREEMPT_ACTIVE标志。它和调度有关。
针对这几个字段,内核提供了以下的宏方便对它们的获取操作: 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.&处理器抢占计数器字段宏
preempt_count
在thread_info中选择preempt_count成员
preempt_disable
使抢占计数器的值加1。
preempt_enable
使抢占计数器的值减1。
preempt_enable_no_resched
使抢占计数器的值减1。
preempt_enable
使抢占计数器的值减1,并在thead_info描述符的TIF_NEED_RESCHED标志被置1的情况下,调用preempt_schedule。
与preempt_disable相似,但要返回本地CPU的数量。
与preempt_enable相同。
put_cpu_no_resched
与preempt_enable_no_resched相同。
内核必须配置CONFIG_PREEMPT,才启用内核抢占。否则上面的宏均被定义为空。 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()))
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,而非其自身。另外应该保持这部分代码被约束在可控的范围,而避免不必要的扩散。
preempt_schedule首先判断当前进程preempt_count值是否为正值,如果是,则说明该进程当前禁止抢占,不可被调度。
然后通过irqs_disabled判断当前是否处于中断处理中,如果是则不可被抢占。
置PREEMPT_ACTIVE标记,调用schedule实施调度。
如果调度失败,且进程含有TIF_NEED_RESCHED标记,则持续调度,直至成功。
一些额外的宏并用在SMP系统处理中。 #define get_cpu()
({ preempt_disable(); smp_processor_id(); })
#define put_cpu()
preempt_enable()
#define put_cpu_no_resched() preempt_enable_no_resched()
20.2.&内存屏障
内存屏障保证高级语言,比如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_
*tmp = 100;
// barrier();
if(*tmp == 100)
编译命令如下,为了得到间接的代码,参数中加上了-O2优化选项。 arm-linux-gcc test.c -o test -O2
首先编译没有内存屏障宏的代码,并反汇编得到main函数对应的汇编指令:
r3, [pc, #12] 8348 &main+0x14&
r2, #100 0x64
r0, #0 0x0
0x000104fc
这里找不到*tmp == 100对应的汇编指令,显然编译器认为这句话是多余的,因为从*tmp = 100这句话开始,*tmp的值没有被任何语句改变过,所以它尝试了优化。接下来打开barrier()。
r3, [pc, #20] 8350 &main+0x1c&
r2, #100 0x64
r0, r0, r2
r0, #1 0x1
可以看到subs和movne指令,所以确实执行了比较操作。考虑何时需要这种需求呢?一个典型的示例就是内核抢占,在可被抢占前后的代码是必须严格顺序执行的,不然禁止抢占所保护的操作将丧失本来的意义。
20.3.&临界区控制
临界区是一段代码,在任何内核控制路径进入临界区后必须全部执行完这段代码,而不被打断。如何确定系统调用,异常处理程序,可延迟函数和内核线程中的临界区是是首要任务。一旦临界区被确定,就必须对其采用适当的保护措施,以确保在任何时刻只有一个内核控制路径处于临界区。
例如,假设两个不同的中断处理程序要访问同一个包含了几个相关变量的数据结构,比如一个缓冲区和一个表示缓冲区大小的类型变量。所有影响该数据结构的语句都必须放入一个单独的临界区。如果是单CPU系统,可以采取访问共享数据结构时关闭中断的方式来实现临界区,因为只有在开中断的情况下,才可能发生内核控制路径的嵌套。
另外,如果相同的数据结构仅被系统调用服务例程所访问,而且系统中只有一个CPU,就可以非常简单地通过在访问共享数据结构时禁用内核抢占功能来实现临界区。
但是如果该共享数据即可能被某一中断ISR访问,又可能被系统调用访问呢?事实上内核约束了这种情况的产生,它们不会操作同一数据结构,而要么是原数据结构,要么是副本。
在SMP系统中,情况要复杂得多,由于多个CPU可能同时执行内核路径,因此内核开发者不能假设只要禁用内核抢占功能,而且中断,异常和软中断处理程序都没有访问过该数据结构,才能保证这个数据结构能够安全地被访问。内核提供了各种不同的同步技术。
什么时候同步是不必要的?基于以下内核约束,它使得内核同步相对简单了:
操作只存在于同一中断ISR中,那么禁中断即可。
中断ISR,软中断和tasklet既不可以被抢占也不能被阻塞,所以它们不可能长时间处于挂起状态。在最坏情况下,它们的执行将有轻微的延迟,因为在其执行的过程中可能发生其他的中断(内核控制路径的嵌套执行)。
执行中断处理的内核控制路径不能被执行可延迟函数或系统调用服务例程的内核控制路径中断。
软中断和tasklet不能在一个给定的CPU上交错执行。
同一个tasklet不可能同时在几个CPU上执行。
基于以上这些内核编码的约束限制,内核同步在多数时候不那么紧迫:
ISR和tasklet不必编写成可重入的函数。
仅被软中断和tasklet访问的每CPU变量不需要同步。
仅被一种tasklet访问的数据结构不需要同步。
20.4.&同步技术"适用范围"一栏表示该同步技术是适用于系统中的所有CPU还是单个CPU。例如,本地中断的禁止只适用于一个CPU(系统中的其他CPU不受影响);相反,原子操作影响系统中的所有CPU(当 访问同一个数据结构时,几个CPU上的原子操作不能交错)。
表&44.&内核使用的同步技术
在CPU指尖赋值数据结构
对一个计数器原子地"读-修改-写"的指令
避免指令重新排序
本地CPU或所有CPU
加锁时忙等
加锁时阻塞等待(睡眠)
基于访问计数器的锁
本地中断的禁止
禁止单个CPU上的中断处理
本地软中断的禁止
禁止单个CPU上的可延迟函数处理
通过指针而不是锁来访问共享数据结构
20.4.1.&每CPU变量
最好的同步技术就是把设计无需同步的内核放在首位。事实上每种显式的同步技术都有不容忽视的性能开销。
最简单也是最重要的同步技术包括把内核变量声明为每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
#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
. = 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_
EXPORT_SYMBOL(__per_cpu_offset);
static void __init setup_per_cpu_areas(void)
unsigned long size,
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_
memcpy(ptr, __per_cpu_start, __per_cpu_end - __per_cpu_start);
#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(type, name) 静态分配一个每CPU数组,数组名为name,结构类型为type。在单CPU系统上,就是per_cpu__##name变量,而对于SMP来说它是一个维数为CPU个数的数组,代表该数组的起始地址。
per_cpu(name, cpu) 为CPU选择一个每CPU数组元素,CPU由参数cpu指定,数组名为name。
__get_cpu_var(name) 选择每CPU数组name的本地CPU元素
get_cpu_var(name) 先禁用内核抢占,然后在每CPU数组name中,为本地CPU选择元素。
put_cpu_var(name) 启用内核抢占(不使用name)。
alloc_percpu(type) 动态分配type类型数据结构的每CPU数组,并返回它的地址。
free_percpu(pointer) 释放被动态分配的每CPU数组,pointer指示其地址。
per_cpu_ptr(pointer, cpu) 返回每CPU数组中与参数cpu对应的CPU元素地址,参数pointer给出数组地址。
#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))
#ifndef __per_cpu_offset
extern unsigned long __per_cpu_offset[NR_CPUS];
#define per_cpu_offset(x) (__per_cpu_offset[x])
#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系统上,它总是以单个独立的变量存在的。
20.4.2.&原子操作若干汇编语言指令序列具有"读——修改——写"的特性,它们访问存储器单元两次,第一次读原值,第二次写新值。假设运行在两个CPU上的里那个个内核控制路径试图通过执行非原子操作来同时"读——修改——写"同一存储单元。首先,两个CPU都试图读同一单元,但是存储器仲裁器(对访问RAM芯片的操作进行串行化的硬件电路)插手,只允许其中的一个访问而让另一个延迟。然而,当第一个读操作完成后,延迟的CPU从那个存储器单元正好读到同一个(旧)值。然后,两个CPU都试图向那个存储器单元写一新值,总线存储器访问在一次被存储器仲裁器串行化,最后,两个写操作都成功。但是,全局的结果是不对的,因为两个CPU写入了同一(新)值。因此,两个交错的"读——修改——写"操作成了一个单独的操作。
避免"读——修改——写"指令引起的竞争条件的最容易的办法,就是确保这样的操作在芯片级是原子的。任何一个这样的操作都必须以单个指令执行,中间不能中断,且避免其它的CPU访问同一存储器单元。这些很小的原子操作(atomic operations)可以建立在其他更灵活机制的基础之上以创建临界区。
回顾一下80x86的指令:
进行零次或一次对齐内存访问的汇编指令是原子的,但是对非对齐内存的访问通常不是原子的。
如果在读操作之后,写操作之前没有其他处理器占用内存总线,那么从内存中读取数据,更新数据并把更新后的数据写回内存中的这些"读——修改——写"汇编语言指令(如inc或dec)是原子的。当然,在单处理器系统中,永远不会发生内存总线窃用的情况。
操作码前缀是lock字节(0xf0)的"读——修改——写"汇编语言指令即使在多处理器系统中也是原子的。对于ARM来说,这些指令带有ex后缀,它们只在ARMv6和更高版本的指令集中才被提供,所以ARMv6之前的指令集是不支持SMP的。当控制单元检测到这些指令中的锁定字段时,就"锁定"内存总线,直到这条指令执行完成为止。因此,当加锁的指令执行时,其他CPU不能访问这个内存单元。
操作码前缀是一个rep字节的汇编指令不是原子的,这条指令强行让控制单元多次重复执行相同的指令。控制单元在执行新的循环之前要检查挂起的中断。
在编写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)
__asm__ __volatile__("@ atomic_set\n"
%0, [%1]\n"
%0, %2, [%1]\n"
: "=&r" (tmp)
: "r" (&v-&counter), "r" (i)
可以通过锁总线指令在一个指令中完成,比如x86。对于ARM来说,从ARMv6指令集开始引入了两个锁总线访问的指令ldrex和strex。它们必须成对出现:ldrex在读取数据之前会锁定总线,这个操作被称为MarkExclusiveGlobal,而在strex中才会执行ClearExclusiveGlobal用来解锁总线,这其中的操作都是原子的。但这样做就可以避免被中断打断吗?不能,这只是保证了总线访问的锁定,但是并没有禁中断,中断总是在一个指令执行完毕后被检查,所以如果在ldrex中发生了中断,并且中断ISR尝试了对v的操作,那么这个循环就可能会执行多次,并且中断对v的操作将丢失,所以中断处理中不应该调用用于原子操作的宏函数。最新的ARM指令集支持monitor标记机制,它不会锁总线,此时原子操作包含如下四个步骤:
step 1.ldrex r0,[addr] ; 从addr中读取值,并借助monitor在对应的地址上做一个tag
step 2.对读出的值做一些操作
step 3. strex r1,r0,[addr] ;将处理后的r0写回[addr],r1指示此次写操作是否成功,而此次写操作能够成功的一个条件是,在step 1的ldrex中标记的tag仍然存在.
step 4. teq r1,#0;bne 1如果step 3写失败,返回step 1.
在linux中,在所有中断的入口都会调用clrex来清除掉monitor标记的这个tag,那么如果step 1和step 3之间有中断发生,在中断处理完成返回之后step 3会失败(因为step1中的tag已经被清除),然后又会进入step 1重新读[addr]中的值。也就是无论如何都不会发生“交错读”这种现象。因为每次strex失败之后都会重新再读一次[addr]的值。类似于atomic_set,Linux定义了一些列的宏和函数:
表&45.&Linux中的原子操作
atomic_read(v)
atomic_set(v, i)
atomic_add(i, v)
atomic_sub(i, v)
atomic_sub_and_test(i, v)
atomic_inc(v)
atomic_dec(v)
atomic_dec_and_test(v)
*v减1,如果为0,返回1,否则返回0
atomic_inc_and_test(v)
*v加1,如果为0,返回1,否则返回0
atomic_add_negative(i, v)
*v加i,结果为负责返回1,否则返回0
atomic_inc/dec_return(v)
加/减1后返回新值
atomic_add/sub_return(i, v)
加/减i后返回新值
内核通过汇编混合编程实现了核心函数atomic_set,atomic_add_return等函数,然后对它们进行宏扩展: #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中的原子位处理函数
test_bit(nr, addr)
返回*addr的第nr位的值
set_bit(nr, addr)
设置*addr的第nr位
clear_bit(nr, addr)
清*addr的第nr位
change_bit(nr, addr)
转换*addr的第nr位
test_and_set_bit(nr, addr)
设置*addr的第nr位,并返回原值
test_and_clear_bit(nr, addr)
清*addr的第nr位,并返回原值
test_and_change_bit(nr, addr)
转换*addr的第nr位,并返回原值
atomic_clear_mask(mask, addr)
清mask指定的*addr的所有位
atomic_set_mask(mask, addr)
设置mask指定的*addr的所有位
x86中提供了完善针对位的原子操作指令,但是当前ARM的非SMP系统的bit实现除atomic_clear_mask/atomic_set_mask外只是考虑了中断的干扰因素,并没有使用strex和ldrex指令,所以如果需要支持SMP,需要修改这些函数的实现。
20.4.3.&自旋锁
一种广泛应用的同步技术是加锁(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 {
} raw_spinlock_t;
asm/spinlock_types.h 包含raw_spinlock_t/raw_rwlock_t的定义和初始化。
asm/spinlock.h 定义了汇编代码实现的__raw_spin_*()系列函数。
linux/spinlock_api_smp.h 封装了SMP上的_spin_*()系列函数。
对于单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
linux/spinlock_api_up.h:
builds the _spin_*() APIs.
typedef struct { } raw_spinlock_t;
linux/spinlock_type_up.h 包含了通用,简化的单CPU系统上的spinlock_t类型,显然
asm/spinlock_up.h 定义了基于内核抢占的__raw_spin_*()系列函数。
linux/spinlock_api_up.h 封装了单系统上的_spin_*()系列函数。
linux/spinlock_types.h根据单系统和SMP的头文件,定义了spinlock_t的通用类型: #if defined(CONFIG_SMP)
# include &asm/spinlock_types.h&
# include &linux/spinlock_types_up.h&
typedef struct {
raw_spinlock_t raw_
#ifdef CONFIG_GENERIC_LOCKBREAK
unsigned int break_
} spinlock_t;
在Linux的SMP系统中中,每个自旋锁都用spinlock_t结构表示,其中包含两个字段:
raw_lock 该字段表示自旋锁的状态:值为1表示"未加锁"。而任何赋值和0都表示"加锁"状态。
break_lock 表示进程正在忙等自旋锁(只在内核支持SMP和内核抢占的情况下使用该标志)。
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)
__asm__ __volatile__(
%0, [%1]\n"
#ifdef CONFIG_CPU_32v6K
strexeq %0, %2, [%1]\n"
: "=&r" (tmp)
: "r" (&lock-&lock), "r" (1)
这里就如原子操作一样尝试在锁总线的情况下来进行循环等待,在strexeq,teqeq和bne另一个CPU将会释放锁,也即将增加lock的值,这在下一次ldrex处理中将原子的获取该锁。但是这并不能保证中断不会打断该spinlock,所以依然会进行中断处理,并且在中断处理结束时,由于禁止内核抢占而跳过调度处理(参考irq_svc的处理)。
表&47.&自旋锁宏
spin_lock_init()
把自旋锁置为1(未锁)
spin_lock()
spin_unlock()
把自旋锁置为1(未锁)
spin_unlock_wait()
等待,直到自旋锁变为1(未锁)
spin_is_locked()
如果自旋锁被置为1(未锁),返回0;否则,返回1。
spin_trylock()
把自旋锁置为0(锁上),如果原来锁的值为1,否则,返回0。
以上的操作均针对SMP系统,如果是单CPU系统,那么根本不会操作lock成员,而只是对内核抢占的使能或者取消。如果单系统没有使能内核抢占,那spinlock就什么也不会做了。 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释放锁,然后使能内核抢占,注意它们的顺序。
20.4.4.&读写自旋锁
读写自旋锁的引入是为了增加内核的并发能力。只要没有内核控制路径对数据结构进行修改,读写自旋锁就允许多个内核控制路径同时读同一数据结构。如果一个内核控制路径相对这个结构进行写操作,那么它首先获取读写锁的写锁,写锁授权独占访问这个资源。当然你,允许对数据结构并发读可以提高系统性能。
每个读写自旋锁都是一个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 {
} raw_rwlock_t;
SMP系统上,lock字段是一个32位的字段,分为两个不同的部分:
24为计数器,表示对受保护的数据结构并发地进行读操作的内核控制路径的数目。这个计数器的二进制补码存放在b[23:0]位。
"未锁"标志字段,当没有内核控制路径在读或写时设置该位,否则清0。这个"未锁"标志存放在lock字段的b[24]。
注意,如果自旋锁为空(设置了"未锁"标志且无读者),那么lock字段的值为0x;如果写者已经获得自旋锁("未锁"标志清0且无读者),那么lock字段的值为0;如果一个、两个或多个进程因为读获取了自旋锁,那么lock字段的值为0x00ffffff,0x00fffffe等("未锁"标志清0,读者个数的二进制补码在0~23位上)。
20.5.&Sandbox
表&48.&Memory Hierarchy
&figure&&title&内核RAM布局&/title&&graphic fileref="images/mdio/p.gif"/&&/figure&
100=1 100=1
& & [] 强调
[] 到底位于哪里呢?
发表评论:
TA的最新馆藏}

我要回帖

更多关于 内核是什么 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信