- Java锁的逻辑(结合对象头和ObjectMonitor)
- 还在用饼状图?来瞧瞧这些炫酷的百分比可视化新图形(附代码实现)⛵
- 自动注册实体类到EntityFrameworkCore上下文,并适配ABP及ABPVNext
- 基于Sklearn机器学习代码实战
这节课程的内容是锁(本节只讨论最基础的锁)。其实 锁本身就是一个很简单的概念 ,这里的简单包括 3 点:
至于CPU底层如何保证 atomic,这个话题已经超出了本节的讨论范围,甚至 我的看法更加激进 :如果不是CPU的设计者,压根没必要了解这一点,因为即使详尽如 CPU 的 data sheet,也不会和用户说明 atomic swap 是如何实现的,不过想要了解的童鞋看这里: atomic的底层实现 。
这里顺便说一下,Lab8本来是后面的lab,但是和这一节相关度较高,所以拿到这里讲解,Lab8有两个part,其中part1要求重新设计内存分配器,而part2涉及到文件系统,所以讲到文件系统时再来讲解.
首先考虑一个问题, 单线程的性能 是由什么决定:是CPU的 时钟频率 ,频率越快,执行一条指令所需的时间越短,从下图可以看出,大概从2000年开始:
但是多核带来的问题就是会有多个进程访问共享的数据结构,所以需要锁,锁可以保证共享数据的 正确性 。
锁就是一个对象,有一个结构体叫做 lock,它包含了一些字段,这些字段中维护了锁的状态,最典型也是最基本的一个锁应该有以下三个字段:是否加锁?锁的名字?哪个cpu核持有锁?
// 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.
};
锁应该有 非常直观的API :
锁的acquire和release之间的代码,通常被称为critical section,称为临界区,而锁就是保护这部分代码的原子性的 。
这里的关键问题是 锁到底是怎么做到原子性 的?即锁为什么不能被两个进程同时 acquire?要解答这个问题。就需要深入源码,看一看 acquire是如何实现的 。
// Acquire the lock.
// Loops (spins) until the lock is acquired.
void
acquire(struct spinlock *lk)
{
push_off(); // disable interrupts to avoid deadlock.
if(holding(lk))
panic("acquire");
// On RISC-V, sync_lock_test_and_set turns into an atomic swap:
// a5 = 1
// s1 = &lk->locked
// amoswap.w.aq a5, a5, (s1)
while(__sync_lock_test_and_set(&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 strictly after the lock is acquired.
// On RISC-V, this emits a fence instruction.
__sync_synchronize();
// Record info about lock acquisition for holding() and debugging.
lk->cpu = mycpu();
}
这里的关键是: while(__sync_lock_test_and_set(&lk->locked, 1) != 0) ,注释中已经给出了提示,__sync_lock_test_and_set 是一个C 标准库中的函数,用来实现 atomic swap,或者说用来实现原子性的 test and set.
这里是需要 重点解释 的地方:
首先解释什么是 test and set,就和他的名字一样,test locked 是否为1,是 1 则说明该锁已经被获取,是 0 则说明该锁没有被获取,就可以获取到锁,所谓获取锁就是 set locked 的值为 1,这样其他进程就 test 到 locked 值为1,从而无法获取锁.
然后解释 atomic swap 是什么,这是一种底层硬件指令,几乎所有真实的CPU都会支持这个指令,可以在硬件层面保证“原子交换”。在RISC-V中,这个特殊的指令叫 amoswap(atomic memory swap),这个指令接收3个参数,分别是address,寄存器r1,寄存器r2。这条指令可以会先锁定住address,将address中的数据保存在一个临时变量中(tmp),之后将r1中的数据写入到address中,之后再将tmp变量中的数据写入到r2中,最后再对于地址解锁。 相当于原子性地实现了 address->r2, r1->address 。
test and set 和 atomic swap 什么关系? 答案就是 atomic swap 就可以实现 test and set :
看下面的 test and set 函数,做的工作就是 *ptr->old, new->*ptr,然后返回old,这个模式是不是和上面的 address->r2, r1->address 一模一样? test 就是将locked的值取出赋值到old并返回,set就是将new值set到locked中 。
//test-and-set的C代码模拟
int TestAndSet(int *ptr, int new) {
int old = *ptr; //抓取旧值
*ptr = new; //设置新值
return old; //返回旧值
}
typedef struct __lock_t {
int locked;
} lock_t;
void init (lock_t *lock) {
lock->flag = 0;
}
void lock(lock_t *lock) {
// 如果为 1,说明锁被其他进程获取
// 如果为 0,说明该进程可以获取锁
while (TestAndSet(&lock->flag, 1) == 1)
; //spin-wait (do noting)
}
void unlock (lock_t *lock) {
lock->flag = 0;
}
什么时候才必须要加锁呢?课程给出了一个非常保守同时也是非常简单的规则: 如果两个进程访问了一个共享的数据结构,并且其中一个进程会更新共享的数据结构,那么就需要对于这个共享的数据结构加锁.
死锁的两个常见情形:
避免死锁的方法:使用锁定策略(locking strategy),对锁进行排序,所有的操作都必须 以相同的顺序获取 锁.
这个lab要求重新设计内存分配器,原来的内存分配器实现如下,可以看到使用了一个链表 freelist 来保存所有的空闲物理 page 。
// kernel/kalloc.c
void *
kalloc(void)
{
struct run *r;
acquire(&kmem.lock);
r = kmem.freelist;
if(r)
kmem.freelist = r->next;
release(&kmem.lock);
if(r)
memset((char*)r, 5, PGSIZE); // fill with junk
return (void*)r;
}
下图解释了 kalloc 的运行过程,如果有进程需要内存空间,就调用kalloc函数,kalloc就会返回一个page的地址 。
可以看到 kalloc 加了一把大锁(即在kalloc首尾acquire和release锁),来保护所有进程共享的数据结构 freelist,但是这样做会有效率问题,所以lab要求在保证正确性的前提下优化这把锁的性能。 优化方式也给出来了,只需要为每个cpu core维护一个freelist就好了 ,然后每个freelist都有自己的锁,之所以还要设计锁,是因为在一个 CPU core 的 freelist 中空闲页不足的情况下,仍需要从其他 CPU 的 freelist 中“偷”内存页,所以一个 CPU core 的 freelist 还可能在“偷”内存页的时候被其他 CPU core 访问,故仍然需要使用单独的锁来保护每个 CPU core 的 freelist.
至于怎么偷?雨露均沾地偷?还是全部从一个 other cpu core 中偷?lab就没有要求了,自己设计即可.
这里的一个关键、也是guide中没有提到的就是:要清楚 首次初始化 时所有的page都被分配给了一个 core,所以其他 core 首次调用 kalloc 时一定会执行 steal 动作,这里给出修改后的kalloc代码:
void *
kalloc(void)
{
struct run *r;
push_off();
int cpu_id = cpuid();
acquire(&kmem[cpu_id].lock);
//steal pages form other cpu's freelist
if(!kmem[cpu_id].freelist) {
int steal_page = 32;
for(int i = 0; i < NCPU; i++) {
if(i == cpu_id) continue;
acquire(&kmem[i].lock);
struct run *rr = kmem[i].freelist;
while(rr && steal_page) {//该cpu的freelist有page,且steal_page不为0
kmem[i].freelist = rr->next;
rr->next = kmem[cpu_id].freelist;
kmem[cpu_id].freelist = rr;
rr = kmem[i].freelist;
steal_page--;
}
release(&kmem[i].lock);
if(steal_page == 0) break;
}
}
r = kmem[cpu_id].freelist;
if(r)
kmem[cpu_id].freelist = r->next;
release(&kmem[cpu_id].lock);
if(r)
memset((char*)r, 5, PGSIZE); // fill with junk
pop_off();
return (void*)r;
}
获得更好的阅读体验,这里是我的博客,欢迎访问: byFMH - 博客园 。
所有代码见:我的 GitHub 实现(记得切换到相应分支) 。
最后此篇关于XV6中的锁:MIT6.s081/6.828lectrue10:Locking以及Lab8locksPart1心得的文章就讲到这里了,如果你想了解更多关于XV6中的锁:MIT6.s081/6.828lectrue10:Locking以及Lab8locksPart1心得的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。
这个问题在这里已经有了答案: Why don't Java's +=, -=, *=, /= compound assignment operators require casting? (11 个
我搜索了很多,但没有一个链接能帮助我解决这个问题。我得到了 ORA-21500: internal error code, arguments: [%s], [%s], [%s], [%s], [%s
我正在做 RegexOne 正则表达式教程,它有一个 question关于编写正则表达式以删除不必要的空格。 教程中提供的解决方案是 We can just skip all the starting
([\s\S]+|\s?) 中 |\s? 的目的或作用是什么?如果没有它,表达式会不会与 ([\s\S]+) 相同? 最佳答案 这不是完全相同的。 ([\s\S]+|\s?) 会匹配空字符串,而 ([
这个正则表达式有一组还是两组? 我正在尝试使用第二组访问 bookTitle 但出现错误: Pattern pattern = Pattern.compile("^\\s*(.*?)\\s+-\\s+
在 C 中给定一个字符串指针 s,下面的迭代会做什么?即它以什么方式遍历字符串? for (++s ; *s; ++s); 最佳答案 for (++s ; *s;++s) 表示 将指针 s 递增到字符
我正在用一个 node.js 应用程序解析一个大列表并有这段代码 sizeCode = dbfr.CN_DESC.split('\s+-\s*|\s*-\s+') 这似乎不起作用,因为它返回了 [ '
我正在编写一个简单的字符串连接程序。 该程序按照我发布的方式运行。但是,我首先使用以下代码编写它来查找字符串的结尾: while (*s++) ; 但是,这个方法并没有奏效。我传递给它的字符串
这个问题已经有答案了: What does (?和aramchand来自Mohandas Karamchand G 因此,在使用这些匹配来分割字符串后,您最终会得到 {"M", "K", "G"} 注
我正在尝试转换 Map到 List使用 lambda。 本质上,我想将键和值与 '=' 连接起来之间。这看起来微不足道,但我找不到如何去做。 例如 Map map = new HashMap<>();
我正在经历 K & R,并且在递增指针时遇到困难。练习 5.3(第 107 页)要求您使用指针编写一个 strcat 函数。 在伪代码中,该函数执行以下操作: 将 2 个字符串作为输入。 找到字符串
在下面的代码中,pS 和 s.pS 在最后一行是否保证相等?也就是说,在语句S s = S();中,是否可以确定不会构造一个临时的S? #include using namespace std; s
演示示例代码: public void ReverseString(char[] s) { for(int i = 0, j = s.Length-1; i < j; i++, j--){
我一直在寻找类似于 .NET examples 中的示例的 PowerShell 脚本.取一个 New-TimeSpan 并显示为 1 天 2 小时 3 分钟 4 秒。排除其零的地方,在需要的地方添加
def func(s): s = s + " is corrected" return s string_list = ["She", "He"] for s in string_li
我是 python 的新手。当我在互联网上搜索 lambda 时。我在 lambda_functions 中找到了这个声明. processFunc = collapse and (lambda s:
我最近开始学习正则表达式,并试图为上面的问题写一个正则表达式。如果限制只放在一个字母上(例如不超过 2 个“b”),这并不困难。 那么答案就是:a* c*(b|ε)a* c*(b|ε)a* c* 但是
当我运行 npm install 时出现以下错误,但我无法修复它。 我试过:npm install -g windows-build-tools 也没有修复这个错误 ERR! configure
有很多有趣的haskell网上可以找到片段。 This post可以在 this (awesome) Stack Overflow question 下找到. The author写道: discou
我知道以下三行代码旨在将字符串提取到$ value中并将其存储在$ header中。但是我不知道$value =~ s/^\s+//;和$value =~ s/\s+$//;之间有什么区别。 $val
我是一名优秀的程序员,十分优秀!