内存学习(3)
内存过程的锁(们)
我们今天来看看do_page_fault
中的锁,为什么要看这东西?平时老说什么死锁,互相抢占,不释放如何如何。我们来看看linux怎么避免了死锁的出现,这里我们假设面对的是SMP机器,我记得最早开始看的时候就很疑惑,首先每个线程如果都申请一块内存,怎么操作的?然后就是说页表是怎么同步的?这都是我当时的问题。
- 一开始预取mm->mmap_sem(信号量说白了也就是个内存里的值,所以预取是没问题的),然后判断是不是
user_mode_vm
或者(regs->flags & X86_EFLAGS_IF
异常开中断。这里需要注意这里如果是不同的不同的线程,那么获取的task_struct是不同的,但是所指向的mm_struct是相同的。同一个自旋锁的入口,总能保证只能有一个抢到,但是这里是读写信号量,可以进来多个读尝试。这里保证了可以有多个任务(task)进来。对于页表的保护必须必须通过自旋锁。 - 尝试获取读写信号量(为什么使用?确保当前进程获取写锁没有已经抢占了该信号量),如果直接获取了长生命周期的信号量,调用
might_sleep
函数,检查可不可以发生内核抢占,这里内核抢占的条件和《linux内核设计与实现》P53完全一致,检查need_resched
函数,同时检查检查preempt_count的值,也就是当前使用锁的个数是否为0。如果锁争用失败,而且当前地址是系统调用的地址,那么直接调用down_read
函数去等待。为什么要检测这个东西?这是因为down_read_trylock
函数成功的情况下可能是已经获得了锁。这里还有个很有意思的事情,这里是read_lock,为什么要read_lock?立刻让尽量多的task进来修改不同的vma,只需要保护vma,没什么问题。那么什么时候对mmap_sem做写抢占呢?munmap就会做这种事情,为什么munmap要做这事?倒是不难理解,毕竟大块操作可能涉及到很多vma。 - 从当前函数调用
expand_stack
===>expand_downwards
,中调用anon_vma_lock(vma);
锁住vma的自旋锁,操作完毕释放再释放自旋锁,保护vma。这里要修改所以加了锁,前面查找的时候加锁了吗?并没有。有没有同时两个进程修改vma呢?不可能,那么为什么vma还要加锁呢?实际上点进去看,可以发现这个锁操作实际上是vma->anon_vma->root->lock
,保证同时只有一个修改这东西。 - 从
expand_stack
返回之后,释放掉了vma的锁。然后alloc_pud
&alloc_pmd
中都锁住mm的保护线性区和页表的自旋锁,spin_lock(&mm->page_table_lock)
。这里注意,这几个都存在内存屏障smp_wmb
,这里为什么需要写内存屏障?SMP系统,缓存一致性协议保证每个CPU看到的缓存是一样的,但是即使分配操作成功,对于new的赋值很可能不会立即发生,pgtable_t new = pte_alloc_one(mm, address)
,这里使用内存屏障保证所有的CPU看到的缓存和内存是一致的。rmb
和wmb
的是两种不同的内存屏障,分别对应于load和存储。这里需要注意的是,锁只能保证顺序执行,不能保证锁之外,返回值被写回内存立刻发生。后面还得再多赘述一句volatile。 - 最后
__alloc_pages_nodemask
,这里锁的情况比较复杂。如果是split_lock,那就使用page的lock,否则使用mm->page_table_lock,锁住这个自旋锁。注意此时我们可以对当前vma做修改。分配出来anon_vma之后,立刻锁住改anon_vma->lock,如果!(flags & FAULT_FLAG_WRITE)
,那一开始不会抢锁,回去抢zone的lock。
mm = tsk->mm;
prefetchw(&mm->mmap_sem); /* struct rw_semaphore mmap_sem 也就是说这是在预取一个读写信号量 */
if (user_mode_vm(regs)) {
local_irq_enable(); /* 开中断 */
error_code |= PF_USER;
} else {
if (regs->flags & X86_EFLAGS_IF)
local_irq_enable(); /* 开中断 */
}
...
if (unlikely(!down_read_trylock(&mm->mmap_sem))) { /* 如果此时,锁被征用,返回0。成功获得放回非0值*/
if ((error_code & PF_USER) == 0 &&
!search_exception_tables(regs->ip)) {
bad_area_nosemaphore(regs, error_code, address);
return;
}
down_read(&mm->mmap_sem); /* 如果锁征用失败了,回到这里来 ,
} else {
/*
* The above down_read_trylock() might have succeeded in
* which case we'll have missed the might_sleep() from
* down_read():
*/
might_sleep();
}
说点内存屏障和voliate
多线程环境下,类似下面的条件并不能保证flag被不被优化掉,编译器优化可能把单线程执行的flag判断为不会变化,也就是说很可能if (flag == true)
会直接被优化成死循环。此外如果多线程环境下,其他线程做了什么事情,再修改flag,并不能保证flag之前的指令先执行(没错我说的就是乱序执行)。
flag = false;
while (true) {
if (flag == true) {
apply(value);
break;
}
}
说说小结论
这块内存申请简直是多锁操作的典范,用锁来保护关键数据,用内存屏障来保证有依赖的执行顺序。每个锁只在自己的最小范围内进行操作。使用内存屏障保证关键操作不会出现缓存和内存不一致的情况。这里典型的锁之内的顺序不用担心,锁之外的指令如果有明显的顺序依赖最好加个内存屏障。
结尾的闲言碎语
写到这里差不多就可以结束了,就不多说了。