Chinaunix首页 | 论坛 | 博客

  • 博客访问: 11960
  • 博文数量: 8
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 81
  • 用 户 组: 普通用户
  • 注册时间: 2015-04-09 22:57
个人简介

高中学历的渣渣

文章分类
文章存档

2015年(8)

我的朋友

分类: LINUX

2015-04-15 18:21:41


ARM Linux 源码分析系列文章基于 Linux 2.6.22 讲解,转载

请标明原处!


所谓的大内核锁,顾名思义,就是给整个内核上的一把锁,那么为什么需要这么一把锁呢?这就要追溯到 
Linux 早期了,当时 Linux 对 SMP 的支持非常不足,于是为了保证内核能在 SMP 环境下正常运行,开发者们就想出了一个权宜之计,即用一把锁把整个内核用自旋锁“锁”起来,这把锁就是所谓的大内核锁。每个进程在进入内核态运行内核代码时都要先得到大内核锁,这样就能保证每次只有 个进程在内核态运行(第一个获得锁的进入内核态,后来者就只能在内核态门口等待),这样由于多个处理器不会同时在内核态运行,自然也不会发生同步上的问题。这种做法实现起来非常简单,但缺点却也非常的明显,即:只有 个处理器的程序能进入内核态,而其他处理器上的程序要进入内核态时则会被阻止,这会极大影响多处理器的威力。因此开发者们不停的缩小大内核锁的管制范围,他们首先把管住内核态入口的大内核锁去掉,但这样还不够,因为这样虽然能让多个处理器上的程序进入内核态,但有些资源上的使用还是非常麻烦,比如说 和 是两个完全不相关的资源,而且他们都被大内核锁锁住,那么只要有个进程因使用 资源而得到大内核锁,其他处理器上的进程就无法同时得到 资源(因为使用 资源也需要获得大内核锁,而大内核锁已经被使用 资源的进程率先获得了),所以他们还把这些资源一一的挑出来单独保护,这样大内核锁就只需要保护少量关键资源即可。现在随着内核开发者对大内核锁的逐步改进,在最新版的 Linux 内核中,大内核锁已经不复存在了。

 

对大内核锁的操作

大内核锁起初是用自旋锁实现的,但后来因为考虑到系统的实时性,我们需要进程在获得大内核锁后依然能进行抢占,这很显然就不能使用自旋锁来实现(得到自旋锁后抢占会被禁止),所以后来从 Linux 2.6.11 开始内核里多了一个由信号量实现的大内核锁,如果内核在编译时打开了获得大内核锁后依然能进行抢占的功能,那么大内核锁就会使用这个信号量来实现,否则依然使用自旋锁,后来因为信号量的过于复杂和费时,为了一点实时性牺牲这些并不值得,所以信号量版大内核锁没有使用多久就被取消掉了,因此尽管本文所参考的内核版本存在着信号量版大内核锁,但下面我们还是讲解以自旋锁实现的大内核锁。

 

在讲解大内核锁的操作之前,我们必须了解进程描述符(struct task_struct)的 lock_depth 字段,他记录了进程得到大内核锁的次数,其值含义如下:

-1 :进程未获得大内核锁

 0 :获得一次大内核锁

>0 :大于 时,其值为得到的大内核锁次数减一。

 

大内核锁在内核中被定义成一个名为 kernel_flag 的自旋锁对象,我们可以调用 lock_kernel() 来为当前进程得到他,其代码如下:



他先取出当前进程的 lock_depth 字段 + 1 的值,如果该值是 0,就说明当前进程没有得到大内核锁,那么就会调用 __lock_kernel() 来为当前进程获取大内核锁,此时如果大内核锁被其他处理器上的进程所占用,那么本进程就会进入自旋等待状态,直到大内核锁被释放。我们在这里也可以看到,如果进程之前已经得到了大内核锁,那么 lock_kernel() 只是递增 lock_depth 的值,这是正常的,如果在已经获得大内核锁的情况下再去获得一次大内核锁,岂不是把自己锁住了?所以这里只需要递增获得大内核锁的次数而无需再次获得一次大内核锁。

lock_kernel() -> __lock_kernel() 




   lock_kernel() 会调用 __lock_kernel() 来执行真正的取锁操作。他首先禁止抢占(这会让当前进程的 thread_info 对象的preempt_count 成员递增),然后在 142 行里尝试取得大内核锁,如果取锁成功就直接退出,否则他就会进入这个 if,然后在 144 行会先判断当前进程在调用本函数前是否已经禁止了抢占(因为在 140 行禁止抢占递增了preempt_count 值,所以这里判断的是大于1),如果他原本就已经禁止了抢占,那我们也没有必要去考虑什么实时性了,直接在 145 行“暴力”取锁,如果他原本没有禁止抢占,那么就进入到 149 行的循环,他首先使能抢占(解除函数开头对抢占的限制),然后 151 行的循环会等待大内核锁被释放,由于之前已经使能了抢占,所以这个循环并不会占用太大的系统资源,他会在需要的时候调度出去,然后只要大内核锁一被释放,他就会立刻跑到 153 行禁止抢占,并在 154 行尝试获得大自旋锁,如果他又获取失败,就说明被别的进程抢先一步得到大内核锁了,那么程序又会跑到149 行重新开始等待,直到成功获取大内核锁。__lock_kernel() 得到大内核锁后,当前进程的抢占就被禁止了(153),当然,在释放大内核锁后,在这里被禁止的抢占会被重新使能。

和 lock_kernel() 相对的,我们可以使用 unlock_kernel() 来释放当前进程的大内核锁,其代码如下:



unlock_kernel() 先递减当前进程的 lock_depth 字段,如果他被递减后依然大于等于 0,就说明当前进程多次得到了大内核锁,本次释放的只是其中一个而已,那么就无需释放大内核锁,当然,如果他被递减后小于 0了,那么就说明当前进程不再持有大内核锁了,可以调用 __lock_kernel() 释放掉大内核锁。




__unlock_kernel() 非常的简单,他直接释放大内核锁并使能被 lock_kernel() 禁止的抢占后就返回。

 

 

schedule() 和大内核锁

当一个已经获得大内核锁的进程在被调度出去时,内核会释放他已经得到的大内核锁(释放他得到的信号量,不会修改 lock_depth),而这个进程被调度回来时,内核会检测出他之前获得过大内核锁(lock_depth > -1),于是又会恢复他的大内核锁。也就是说,当进程未在 CPU 上运行时是不会占用大内核锁的。为什么要这样?实际上大内核锁在很多情况下还是使用自旋锁实现的,而自旋锁就有一个毛病,那就是:在获得自旋锁后不允许在释放自旋锁前调用 schedule() 切换进程,否则一旦有另外一个进程也来获得这个自旋锁,就很有可能造成死锁(另一个进程会不停的循环检测,而内核方面除非发生抢占,内核态的时钟中断是不会执行调度的,所以会一直卡死在自旋之中)。使用自旋锁实现的大内核锁为了避免这个弊端,在内核执行调度时,若当前进程已经得到了大内核锁,他会强制释放这个大内核锁,这样即使有另一个进程来获得大内核锁也不会进入死锁状态了




阅读(731) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~