Chinaunix首页 | 论坛 | 博客

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

高中学历的渣渣

文章分类
文章存档

2015年(8)

我的朋友

分类: LINUX

2015-04-16 23:51:04


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


标明原处!



信号量是内核中另一种非常常用的同步机制。他和自旋锁不同的是:他让后来者进入睡眠状态而不是自旋状态,且他可以自定义访问资源的进程数量。

内核使用 struct semaphore 类型来描述一个信号量的。



count : 他被初始化为可以访问信号量对应资源的进程个数,每有 个进程请求资源他就会被递减 1(不管是否成功得到资源他都会被递减),也就是说如果该值大于 就说明资源是空闲的、可用的。如果该值为 0,就说明资源是忙的、不可用的。另外,即使资源是忙状态,每当有请求要访问资源时他也会被递减(随后会被睡眠),所以 count 还存在着负数状态,若 count 为负数,那么就说明有进程正在等待该资源,进程数为负数的绝对值。

sleepers : 如果有进程在等待信号量,则他为 

wait ; 信号量的等待队列头,所有等待该信号量的进程都会在这个队列上睡眠。

 

信号量的获得和释放

我们可以调用 down() 函数来为当前进程得到一个信号量。




down 的 sem 参数就是我们需要得到的那个信号量的指针。

 

72 行的 might_sleep() 是调试用的,我们无视掉他。

73 行执行真正的获得信号量操作,并把 __down_failed() 函数传入。如果信号量是忙状态,那么 __down_failed() 就会被调用。

 

由于 ARM V6 加入了 ldrex/strex 指令,所以 __down_op 实际上分为两个版本,他们的区别就是一个使用 ldrex/strex 指令另一个则不使用,实际上后者是兼容 ARM V6 以上的处理器的,所以考虑到习惯使用低版本 ARM 处理器的读者,我们讲解后者版本(原理都是一样,理解了后者版本前一个也就不难了)。





ptr : 需要获得的自旋锁的指针

fail : 一个函数的指针,如果信号量是忙状态,fail就会被调用,他一般是 __down_failed()

 

147~149 行先禁止中断,防止被打断。

150~152 行读出信号量值并递减他,然后把值写回信号量,并根据递减后的值修改 CPSR 上的相关位。

153 行恢复中断

154~155 行在信号量于 151 行被递减后为负数的情况下(即信号量本身是忙状态,本次取信号量失败),把信号量的指针存放到 ip 寄存器,并调用 fail 函数准备睡眠当前进程

 

fail 一般就是 __down_failed() 函数。



 

188 行先把寄存器和返回地址压栈

189 行把 ip 寄存器里保存着的信号量指针存放到 r0 里,这是为了把信号量作为 __down 的参数

190 行跳到 __down() 来执行真正的操作

191 行寄存器出栈,并把返回地址存放到 pc 里,这样就成功返回了




64 行定义一个等待对象,并把当前进程和这个等待对象进行关联(即这个等待对象等待的进程是当前进程)

65~66 行把当前进程的状态设为不可中断的睡眠状态,并把他插入到信号量的等待队列。

68 行禁止抢占和中断,并取得信号量的锁

69~88 这段代码实际上就是让当前进程进入等待状态,这个循环不太容易理解,我们可以模拟一下相关情景来理解这段代码。

我们假设信号量的 count 被初始化为 1,那么当一个进程得到信号量后 count 就会被减为

此时第二个进程来了,他也来获得这个信号量,那么他把 count 递减为 -1 后就会调用到这个函数。他先在 69 行递增 sleepers 字段,被递增后他的值就成了 。然后进入循环,atomic_add_negative() 这里的作用是:让 sem->count 加上 sleepers - 1 ,若结果是一个负数,则返回 1,但别忘了外边那有个取非操作,所以这里是一个非负数才进入 if 代码段。所以在 74 行实际上是 -1 + (1-1) = -1,他是负数,所以他会执行到 78 行,然后把 sleepers 字段置一,并把自己调度出去。要注意,由于当前进程已经是睡眠状态,所以除非被唤醒,他是不会再继续执行了

此时又有第三个进程来获得信号量,那么他把 count 递减为 -2 后调用到本函数。他递增 sleepers 字段后,其值就成了 2。然后由于 74 行 -2 + 1 = -1,他不是正数,所以他把 sleepers 字段置一后就又把自己调度出去了,除非他被唤醒他不会再继续执行了。

我们在这里应该能发现,不管后面来多少个进程来获得这个信号量,他的结果都是一样的:被睡眠。

然后第一个进程使用完资源了,他在释放信号量的时候会发现有进程在等待(sem->count < 0),那么他就会唤醒第二个进程。

第二个进程被唤醒后,他就跑到 82 行继续执行。这里一定要注意,他现在的运行状态已经变成了 TASK_RUNNNG ,那么他在 82 行再次把进程状态设为不可中断睡眠状态,这是因为有可能再次被挂起。然后他又会回到 71 行重新开始,此时的 74 行已经变成了 -1 + 1 = 0(第一个进程释放信号量时,sem->count 被递增),他不是一个负数,所以他会进入 75 行把 sleepers 置为 然后就跳到85 行把自己从等待队列中删除并恢复自己的运行状态。此时要注意,他在 88 行又唤醒了一个进程,由于当前进程已经被删除,所以他在这里唤醒的实际上是第三个进程。那么为什么要唤醒他?我们继续往下看。

 

第三个进程被第二个进程唤醒后(要注意,此时第二个进程在使用信号量,所以第三个进程不可能被真正唤醒),他也会来到 74 行,现在的 74 行是 -1 + 0 = -1,他是一个负数,所以他又会在下面被挂起。直到第二个进程使用完信号量之后,第三个进程被第二次唤醒时,他才会被真正唤醒,其唤醒方式和第二个进程的唤醒方式如出一辙,笔者这里就不再写下去了。

 

我们再来看看信号量是如何被释放并唤醒等待队列上的进程的。我们可以调用 up() 函数来释放一个信号量。




up() 其实和 down() 非常像,down() 是在信号量处于忙状态时调用 __down_failed() 来睡眠当前进程。而 up 在释放信号量的过程中,如果发现有进程在等待信号量,则调用 __up_wakeup() 来唤醒等待队列上的某个进程。




194~196 禁止中断

197~199 递增信号量值,并根据递增后的值修改 CPSR 的某些位

200 行恢复中断

201~202 如果递增信号量后,其值小于等于 0,就说明有进程正在等待,那么把信号量的指针存放到 ip 寄存器并执行 __up_wakeup() 来唤醒等待队列上的进程。




__up_wakeup() __down_failed() 几乎是一样的,我们再来看 __up() 是怎么唤醒信号量上的进程的。




__up() 就是这么简单直接,直接唤醒信号量的等待队列上的进程。

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