Linux2.0是第一个支持SMP(对称多处理器)的内核。在早些时候, inux2.0 内核通过使用一种粗粒度的锁来保证系统的完整性,其原则就是:一个正在内核态运行的进程除非交出控制权或者要求进入睡眠,否则不能被另一个欲进入内核态的进程打断。 也就是说在任意时刻只能有一个处理器是运行内核态的操作系统代码。这是一种安全而易于实现的方式,不过这种方式对于充分利用多处理器的性能存在着明显的不足。在 Linux 以后改进的版本中,操作系统内核采用了一种细粒度的锁来支持这种多处理器的体系机构。通过把操作系统的内核代码划分为临界区的方式,在保证系统完整性的前提下,提升了操作系统利用多处理器的性能优势。
前面转载了一篇写自旋锁的文章,写的非常好,深感佩服!我也觉得内核同步其实非常复杂,与其贪大求全,不如小鸡啄米慢慢来。可能写的东西比较凌乱,就当笔记了吧!
原子操作
首先说说最简单的原子操作,每本书都会复制一堆内联函数的原型。其实看半天都不知道原子操作怎么弄的。tomic_t use_cnt , atomic_set(&use_cnt, 2), atomic_add(4, &use_cnt), atomic_inc(use_cnt),这些就是定义方法和基本操作方法。这几个函数只对应一个指令周期,而中断是不可能发生在指令周期中的。那么在多CPU中呢?这就要说到SMP的结构了,每个CPU都有一条LOCK线连接到北桥芯片上。这条LOCK线的功能就是在某个指令周期内锁住访问内存的总线。这样的功能配合volatile关键字的直接访问内存的特点,可以保证在指令周期内,其他CPU根本无法访问到这个原子变量。所以这些操作的原子性是可以绝对保证的。
不过还有两个不太一样的函数,比较复杂的是atomic_inc_and_test()和atomic_dec_and_test()。先看atomic_dec_and_test()的代码。
static inline int atomic_dec_and_test(atomic_t *v) { unsigned char c; asm volatile(LOCK_PREFIX "decl %0; sete %1" : "+m" (v->counter), "=qm" (c) : : "memory"); return c != 0; }
里面其实就两句汇编代码,LOCK_PREFIX就是使能LOCK线的功能;然后decl v->counter就是把v->counter自减一,sete c是如果ZF=1,则置位c。看明白了,这个函数对应的机器代码有两个指令周期。所以两条指令之间是可以被中断的,v->counter也可能被其他CPU访问,所以这个函数并不是原子的。我当时也是脑子一懵,这是什么情况呀?其实是我忘了一个基本知识——中断对于被中断的函数来说是透明的。根据自己的程序上下文是无法知道自己被中断过,因为现场被完美恢复了。而这个sete指令利用的ZF信息仍然是decl指令产生的,所以这个信息准确地反映了第一条指令的执行结果,但是无法保证现在的v->counter仍是那个状态。另一个函数就完全同理可得了。
分析了原子变量操作,然后就应该是自旋锁了吧!但是我转的那篇文章太犀利,让我完全没有了分析的胆量。不过我有一点要补充的是,自旋锁是无法被抢占的。它的代码里明确的关闭了内核抢占,也关闭的本地CPU中断,它确实会忙等待。好像这个问题网上还是有争议的,大概觉得这东西很可能会导致系统性能下降吧!不过个人猜想,linux这样弄应该是实践验证过的,而且都说用自旋锁要谨慎。
信号量
到了信号量了,其实这东西就是在自旋锁的基础上实现的。
struct semaphore { spinlock_t lock; unsigned int count; struct list_head wait_list; }
信号量的结构体里面明目张胆地包含了一个自旋锁,这个锁的作用就是保护这个count。任何程序想对信号量进行P或V操作,都必须获得这个自旋锁。如果没有获得信号量,就会把进程挂在信号量的等待队列上,进入休眠状态。所以说信号量是不会忙等待的!咦!!!不对吧!可是它使用了自旋锁呀!如果一个控制路径连锁都无法获得,它岂不是要一直忙等待了。听起来好像很有道理的样子,但其实完全是杞人忧天。我们这样分析,为什么会得不到锁?当然是另一个控制路径得到了锁而且还没有释放。根据我们前面的分析,控制路径到到锁以后是不会被抢占的,所以可以肯定的是这个持有锁的控制路径一定是正在另一个CPU上运行着。在正常的情况下,它很快就会释放锁,无论它有没有得到信号量,对count进行操作的工作量是非常小的。
本来说着信号量,但是却谈到了多核操作系统的调度,索性就继续跑题吧!记录一下前面看到的一点资料。在2.6版本的 Linux 中,提出了一种新的 O(1)的进程调度器,此调度器可以更好的支持SMP 系统。它的优势就是在平衡了多个 CPUs 的负载均衡的同时又能有效的兼顾 cache 的有效性。Linux 2.6 内核设计为给每个 CPU 建立一个就绪运行队列, 并把就绪任务划分为 140 个优先级, 高 100 个优先级用于实时任务, 低 40 个优先级用户普通任务。并给每个就绪任务一段时间片,当任务把时间片用完后转移到另一个期满就绪队列并重新分配时间片,等到原活动就绪队列的所有任务的时间片均用完之后启用期满就绪队列来替换活动就绪队列。这样,调度器提供了一个各个任务公平使用 CPU 的机会,并实现了任务和 CPU 的绑定。通过给每一个 CPU 设立单独的任务队列,可以有效的实现系统中各个 CPUs 的负载均衡。
发表评论