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 ...
随机推荐
- 使用git上传项目到GitHub上
之前的博客有<使用git拉取GitHub上的项目>的文章,那么现在说一下,如何上传项目到GitHub上. 1. Git的.gitignore 文档配置 因为项目中可能有很多的图片还有nod ...
- jQuery UI基本使用方法
其实jQuery UI早就在我的学习计划中,只不过因为计划安排始终处于待命状态,最近项目要用到jQuery UI,就提前学习一下,也想能够封装自己的UI库,这样就不用老按照别人的套路走了,像使用jQu ...
- RBAC权限系统设计
序言 RBAC表结构 用户表 角色表 权限表 用户角色(关系)表 角色权限(关系)表 资料 https://blog.csdn.net/ShrMuscles/article/details/80532 ...
- 常用的20个强大的 Sublime Text 插件
作为一个开发者你不可能没听说过 Sublime Text.不过你没听说过也没关系,下面让你明白. Sublime Text是一款非常精巧的文本编辑器,适合编写代码.做笔记.写文章.它用户界面十分整洁, ...
- 【51NOD】1135 原根
[题意]给定p,求p的原根g.3<=p<=10^9. [算法]数学 [题解]p-1= p1^a1 * p2^a2 * pk^ak,g是p的原根当且仅当对于所有的pi满足g^[ (p-1)/ ...
- 2017ACM暑期多校联合训练 - Team 8 1011 HDU 6143 Killer Names (容斥+排列组合,dp+整数快速幂)
题目链接 Problem Description Galen Marek, codenamed Starkiller, was a male Human apprentice of the Sith ...
- Linux命令之uptime
这是什么 uptime用来查看系统已经启动了多长时间了. 它显示的信息和w命令的头(第一行)是一样一样的. 举个栗子 举一个实际的应用场景: 比如发现服务器上的某些没有加入开机启动的服务挂了一片,这个 ...
- pycharm显示行号
在PyCharm 里,显示行号有两种办法: 1,临时设置.右键单击行号处,选择 Show Line Numbers. 但是这种方法,只对一个文件有效,并且,重启PyCharm 后消失. 2,永久设置. ...
- Coursera在线学习---第三节.归一化处理(Normalize)
一.归一化(也说标准化)作用 1)将有量纲特征转化为无量纲特征 2)能够加快收敛(主要指梯度下降法时) 二.Octave中计算 mean(A) 求解矩阵中每一列的均值 std(A ...
- ARP投毒攻击
原理:通过分别伪装成客户机和服务器IP,将自己的MAC地址绑定在IP上,ARP错误的将IP解析为中间人MAC地址,从而来欺骗服务器网关和客户机,使信息必须通过客户机.