XV6操作系统代码阅读心得(三):锁
锁是操作系统中实现进程同步的重要机制。
基本概念
临界区(Critical Section)是指对共享数据进行访问与操作的代码区域。所谓共享数据,就是可能有多个代码执行流并发地执行,并在执行中可能会同时访问的数据。
同步(Synchronization)是指让两个或多个进程/线程能够按照程序员期望的方式来协调执行的顺序。比如,让A进程必须完成某个操作后,B进程才能执行。互斥(Mutual Exclusion)则是指让多个线程不能够同时访问某些数据,必须要一个进程访问完后,另一个进程才能访问。
当多个进程/线程并发地执行并且访问一块数据,并且进程/线程的执行结果依赖于它们的执行顺序,我们就称这种情况为竞争状态(Race Condition)。
Xv6操作系统要求在内核临界区操作时中断必须关闭。如果此时中断开启,那么可能会出现以下死锁情况:A进程在内核态运行并拿下了p锁时,触发中断进入中断处理程序,中断处理程序也在内核态中请求p锁,由于锁在A进程手里,且只有A进程执行时才能释放p锁,因此中断处理程序必须返回,p锁才能被释放。那么此时中断处理程序会永远拿不到锁,陷入无限循环,进入死锁。
Xv6中实现了自旋锁(Spinlock)用于内核临界区访问的同步和互斥。自旋锁最大的特征是当进程拿不到锁时会进入无限循环,直到拿到锁退出循环。显然,自旋锁看上去效率很低,我们很容易想到更加高效的基于等待队列的方法,让等待进程陷入阻塞而不是无限循环。然而,Xv6允许同时运行多个CPU核,多核CPU上的等待队列实现相当复杂,因此使用自旋锁是相对比较简单且能正确执行的实现方案。
Xv6的Spinlock
Xv6中锁的定义如下
// Mutual exclusion lock.
struct spinlock {
uint locked; // Is the lock held?
// For debugging:
char *name; // Name of lock.
struct cpu *cpu; // The cpu holding the lock.
uint pcs[10]; // The call stack (an array of program counters)
// that locked the lock.
};
核心的变量只有一个locked
,当locked
为1时代表锁已被占用,反之未被占用,初始值为0。
在调用锁之前,必须对锁进行初始化。
void initlock(struct spinlock *lk, char *name) {
lk->name = name;
lk->locked = 0;
lk->cpu = 0;
}
最困难的地方是如何对locked
变量进行原子操作占用锁和释放锁。这两步具体被实现为acquire()
和release()
函数。(注意v7版本和v11版本的实现略有不同,本文使用的是v11版本)
acquire()
函数
// Acquire the lock.
// Loops (spins) until the lock is acquired.
// Holding a lock for a long time may cause
// other CPUs to waste time spinning to acquire it.
void acquire(struct spinlock *lk) {
pushcli(); // disable interrupts to avoid deadlock.
if(holding(lk))
panic("acquire");
// The xchg is atomic.
while(xchg(&lk->locked, 1) != 0)
;
// Tell the C compiler and the processor to not move loads or stores
// past this point, to ensure that the critical section's memory
// references happen after the lock is acquired.
__sync_synchronize();
// Record info about lock acquisition for debugging.
lk->cpu = mycpu();
getcallerpcs(&lk, lk->pcs);
}
acquire()
函数首先禁止了中断,并且使用专门的pushcli()
函数,这个函数保证了如果有两个acquire()
禁止了中断,那么也必须调用两次release()
中的popcli()
后中断才会被允许。然后,acquire()
函数采用xchg
指令来实现在设置locked
为1的同时获得其原来的值的操作。这里的C代码中封装了一个xchg()
函数,在xchg()
函数中采用GCC的内联汇编特性,实现如下
static inline uint xchg(volatile uint *addr, uint newval) {
uint result;
// The + in "+m" denotes a read-modify-write operand.
asm volatile("lock; xchgl %0, %1" :
"+m" (*addr), "=a" (result) :
"1" (newval) :
"cc");
return result;
}
其中,volatile
标志用于避免gcc对其进行一些优化;第一个冒号后的"+m" (*addr), "=a" (result)
是这个汇编指令的两个输出值;newval
是这个汇编指令的输入值。假设newval
位于eax
寄存器中,addr
位于rax
寄存器中,那么gcc会翻译得到如下汇编指令
lock; xchgl (%rdx), %eax
由于xchg
函数是inline
的,它会被直接嵌入调用xchg
函数的代码中,使用的寄存器可能会有所不同。
下面我们来分析一下上面的指令的语义。·lock
是一个指令前缀,它保证了这条指令对总线和缓存的独占权,也就是这条指令的执行过程中不会有其他CPU或同CPU内的指令访问缓存和内存。由于现代CPU一般是多发射流水线+乱序执行的,因此一般情况下并不能保证这一点。xchgl
指令是一条古老的x86指令,作用是交换两个寄存器或者内存地址里的4字节值,两个值不能都是内存地址,他不会设置条件码。
那么,仔细思考一下就能发现,以上一条xchg
指令就同时做到了交换locked和1的值,并且在之后通过检查eax
寄存器就能知道locked的值是否为0。并且,以上操作是原子的,这就保证了有且只有一个进程能够拿到locked的0值并且进入临界区。
最后,acquire()
函数使用__sync_synchronize
为了避免编译器对这段代码进行指令顺序调整的话和避免CPU在这块代码采用乱序执行的优化。
release()
函数
// Release the lock.
void release(struct spinlock *lk) {
if(!holding(lk))
panic("release");
lk->pcs[0] = 0;
lk->cpu = 0;
// Tell the C compiler and the processor to not move loads or stores
// past this point, to ensure that all the stores in the critical
// section are visible to other cores before the lock is released.
// Both the C compiler and the hardware may re-order loads and
// stores; __sync_synchronize() tells them both not to.
__sync_synchronize();
// Release the lock, equivalent to lk->locked = 0.
// This code can't use a C assignment, since it might
// not be atomic. A real OS would use C atomics here.
asm volatile("movl $0, %0" : "+m" (lk->locked) : );
popcli();
}
release
函数为了保证设置locked为0的操作的原子性,同样使用了内联汇编。最后,使用popcli()来允许中断(或者弹出一个cli,但因为其他锁未释放使得中断依然被禁止)。
在Xv6中实现信号量
struct semaphore {
int value;
struct spinlock lock;
struct proc *queue[NPROC];
int end;
int start;
};
void sem_init(struct semaphore *s, int value) {
s->value = value;
initlock(&s->lock, "semaphore_lock");
end = start = 0;
}
void sem_wait(struct semaphore *s) {
acquire(&s->lock);
s->value--;
if (s->value < 0) {
s->queue[s->end] = myproc();
s->end = (s->end + 1) % NPROC;
sleep(myproc(), &s->lock)
}
release(&s->lock);
}
void sem_signal(struct semaphore *s) {
acquire(&s->lock);
s->value++;
if (s->value <= 0) {
wakeup(s->queue[s->start]);
s->queue[s->start] = 0;
s->start = (s->start + 1) % NPROC;
}
release(&s->lock);
}
上面的代码使用Xv6提供的接口实现了信号量,格式和命名与POSIX标准类似。这个信号量的实现采用等待队列的方式。当一个进程因信号量陷入阻塞时,会将自己放进等待队列并睡眠(18-22行)。当一个进程释放信号量时,会从等待队列中取出一个进程继续执行(29-33行)。
XV6操作系统代码阅读心得(三):锁的更多相关文章
- XV6操作系统代码阅读心得(一):启动加载、中断与系统调用
XV6操作系统是MIT 6.828课程中使用的教学操作系统,是在现代硬件上对Unix V6系统的重写.XV6总共只有一万多行,非常适合初学者用于学习和实践操作系统相关知识. MIT 6.828的课程网 ...
- XV6操作系统代码阅读心得(四):虚拟内存
本文将会详细介绍Xv6操作系统中虚拟内存的初始化过程. 基本概念 32位X86体系结构采用二级页表来管理虚拟内存.之所以使用二级页表, 是为了节省页表所占用的内存,因为没有内存映射的二级页表可以不用分 ...
- XV6操作系统代码阅读心得(二):进程
1. 进程的基本概念 从抽象的意义来说,进程是指一个正在运行的程序的实例,而线程是一个CPU指令执行流的最小单位.进程是操作系统资源分配的最小单位,线程是操作系统中调度的最小单位.从实现的角度上讲,X ...
- XV6操作系统代码阅读心得(五):文件系统
Unix文件系统 当今的Unix文件系统(Unix File System, UFS)起源于Berkeley Fast File System.和所有的文件系统一样,Unix文件系统是以块(Block ...
- 深入理解Java虚拟机阅读心得(三)
Java中提倡的自动内存管理最终可以归结为自动化的解决两个问题: 给对象分配内存 回收分配给对象的内存 先说说回收这一方面的两个主要知识点 一.垃圾收集算法 1.标记-清理算法 首先标记出所有需要回收 ...
- <<梦断代码>>阅读笔记三
看完了这最后三分之一的<梦断代码>,意味着这本软件行业的著作已经被我粗略地过了一遍. 在这最后三分之一的内容中,我深入了解了在大型软件项目的运作过程中存在的困难和艰辛.一个大型软件项目的成 ...
- 【原】FMDB源码阅读(三)
[原]FMDB源码阅读(三) 本文转载请注明出处 —— polobymulberry-博客园 1. 前言 FMDB比较优秀的地方就在于对多线程的处理.所以这一篇主要是研究FMDB的多线程处理的实现.而 ...
- 【原】SDWebImage源码阅读(三)
[原]SDWebImage源码阅读(三) 本文转载请注明出处 —— polobymulberry-博客园 1.SDWebImageDownloader中的downloadImageWithURL 我们 ...
- 看图写代码---看图写代码 阅读<<Audio/Video Connectivity Solutions for Virtex-II Pro and Virtex-4 FPGAs >>
看图写代码 阅读<<Audio/Video Connectivity Solutions for Virtex-II Pro and Virtex-4 FPGAs >> 1.S ...
随机推荐
- POJ 2007 Scrambled Polygon 极角序 水
LINK 题意:给出一个简单多边形,按极角序输出其坐标. 思路:水题.对任意两点求叉积正负判断相对位置,为0则按长度排序 /** @Date : 2017-07-13 16:46:17 * @File ...
- 在JavaScript中重写jQuery对象的方法
jQuery是一个很好的类库,它给我们解决了很多的客户端编程,任何东西都不是万能的,当它不能满足我们的需求时我们需要对它进行重写,同时也不要影响其原有的功能或者修改其原有的功能:我现在的web应用程序 ...
- rxjs自定义operator
rxjs自定义operator
- 正则表达式:Python 模块 re 简介
为了使文章更具可读性,本文将正则表达式冗长的 语法介绍 放在了文章的末尾. 一.正则表达式简介 正则表达式(RegExp)是一种文本模式,包括普通字符(例如,a 到 z 之间的字母)和特殊字符(元字符 ...
- 51Nod 1256 扩展欧几里得求乘法逆元
给出2个数M和N(M < N),且M与N互质,找出一个数K满足0 < K < N且K * M % N = 1,如果有多个满足条件的,输出最小的. Input 输入2个数M, N中间用 ...
- Problem 2278 YYS (FZU + java大数)
题目链接:http://acm.fzu.edu.cn/problem.php?pid=2278 题目: 题意: 有n种卡牌,每种卡牌被抽到的概率为1/n,求收齐所有卡牌的天数的期望. 思路: 易推得公 ...
- Shell-history命令加记录用户IP
记录输入的命令 history命令可以查看用户输入过的命令,一个典型history命令输出如下: 980 2017-05-29 20:17:37 cd - 981 2017-05-29 20:17:4 ...
- Deep Learning基础--理解LSTM/RNN中的Attention机制
导读 目前采用编码器-解码器 (Encode-Decode) 结构的模型非常热门,是因为它在许多领域较其他的传统模型方法都取得了更好的结果.这种结构的模型通常将输入序列编码成一个固定长度的向量表示,对 ...
- UVALive 3668 A Funny Stone Game
题目链接:UVALive - 3668 题目大意为给定n堆石子,每次的操作是选择三个数i<j<=k,从i中拿一枚石子,在j和k中分别放入一枚石子.不能操作者输.求先手是否能赢,若可以,则输 ...
- WPF之DataGrid--列的前台及后台实现
一.前台实现 在xaml里可以很轻松地实现一个如下图所示的DataGrid <StackPanel> <ComboBox Width="50" Horizonta ...