锁是操作系统中实现进程同步的重要机制。

基本概念

临界区(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操作系统代码阅读心得(三):锁的更多相关文章

  1. XV6操作系统代码阅读心得(一):启动加载、中断与系统调用

    XV6操作系统是MIT 6.828课程中使用的教学操作系统,是在现代硬件上对Unix V6系统的重写.XV6总共只有一万多行,非常适合初学者用于学习和实践操作系统相关知识. MIT 6.828的课程网 ...

  2. XV6操作系统代码阅读心得(四):虚拟内存

    本文将会详细介绍Xv6操作系统中虚拟内存的初始化过程. 基本概念 32位X86体系结构采用二级页表来管理虚拟内存.之所以使用二级页表, 是为了节省页表所占用的内存,因为没有内存映射的二级页表可以不用分 ...

  3. XV6操作系统代码阅读心得(二):进程

    1. 进程的基本概念 从抽象的意义来说,进程是指一个正在运行的程序的实例,而线程是一个CPU指令执行流的最小单位.进程是操作系统资源分配的最小单位,线程是操作系统中调度的最小单位.从实现的角度上讲,X ...

  4. XV6操作系统代码阅读心得(五):文件系统

    Unix文件系统 当今的Unix文件系统(Unix File System, UFS)起源于Berkeley Fast File System.和所有的文件系统一样,Unix文件系统是以块(Block ...

  5. 深入理解Java虚拟机阅读心得(三)

    Java中提倡的自动内存管理最终可以归结为自动化的解决两个问题: 给对象分配内存 回收分配给对象的内存 先说说回收这一方面的两个主要知识点 一.垃圾收集算法 1.标记-清理算法 首先标记出所有需要回收 ...

  6. <<梦断代码>>阅读笔记三

    看完了这最后三分之一的<梦断代码>,意味着这本软件行业的著作已经被我粗略地过了一遍. 在这最后三分之一的内容中,我深入了解了在大型软件项目的运作过程中存在的困难和艰辛.一个大型软件项目的成 ...

  7. 【原】FMDB源码阅读(三)

    [原]FMDB源码阅读(三) 本文转载请注明出处 —— polobymulberry-博客园 1. 前言 FMDB比较优秀的地方就在于对多线程的处理.所以这一篇主要是研究FMDB的多线程处理的实现.而 ...

  8. 【原】SDWebImage源码阅读(三)

    [原]SDWebImage源码阅读(三) 本文转载请注明出处 —— polobymulberry-博客园 1.SDWebImageDownloader中的downloadImageWithURL 我们 ...

  9. 看图写代码---看图写代码 阅读<<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 ...

随机推荐

  1. flex属性设置详解

    CSS代码中常见这样的写法:flex:1 这是flex 的缩写: flex-grow.flex-shrink.flex-basis,其取值可以考虑以下情况: 1. flex 的默认值是以上三个属性值的 ...

  2. HTML不常用的表单属性-fieldset

    这是代码 这是生成的样子 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http: ...

  3. AngularJs附件上传下载

    首先:angular-file-upload 是一款轻量级的 AngularJS 文件上传工具,为不支持浏览器的 FileAPI polyfill 设计,使用 HTML5 直接进行文件上传. 第一步: ...

  4. js实现数组排序

    1. JavaScript的sort()方法 var array = [1,4,-8,-3,6,12,9,8]; function compare(val1,val2){ return val1-va ...

  5. ItemCF_基于物品的协同过滤

    ItemCF_基于物品的协同过滤 1.    概念 2.    原理 如何给用户推荐? 给用户推荐他没有买过的物品--103 3.    java代码实现思路 数据集: 第一步:构建物品的同现矩阵 第 ...

  6. spring boot(二):注解大全

    spring boot注解 @Autowired 注解的意思就是,当Spring发现@Autowired注解时,将自动在代码上下文中找到和其匹配(默认是类型匹配)的Bean,并自动注入到相应的地方去. ...

  7. 给你灵感!21个精美的 iOS APP 网站设计欣赏

    iOS 吹起了轰轰烈烈的扁平化设计风格,而做为承载 App 宣传重任的网页,整体设计风格的变迁如何?是否也如iOS的设计风格改革一样彻底的翻转?还是如往常一直深耕成熟的设计风格? Spendee Fo ...

  8. 一键切图 PS 动作 【收藏】

    使用方法 一键切图动作.zip 1. 下载动作 2. 打开PS 动作 窗口,导入动作 3. 选中图层后 点击 F2 一键切图 详情看原文链接 原文链接

  9. ubuntu 命令配置ip 网关 dns

    如果是在虚拟机中使用Ubuntu,先设置好主机的网络,然后配置虚拟机Ubuntu的IP和网关 如果主机操作系统就是Ubuntu,请直接参照下文进行设置 内容如下: 1. 检验是否可以连通,就使用pin ...

  10. tracert和traceroute使用

    Traceroute提取发 ICMP TTL到期消息设备的IP地址并作域名解析.每次 ,Traceroute都打印出一系列数据,包括所经过的路由设备的域名及 IP地址,三个包每次来回所花时间. 转自 ...