内存学习(3)--锁(们)

整理下思路

Posted by 大狗 on October 27, 2020

内存学习(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看到的缓存和内存是一致的。rmbwmb的是两种不同的内存屏障,分别对应于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;
        }
    }

说说小结论

这块内存申请简直是多锁操作的典范,用锁来保护关键数据,用内存屏障来保证有依赖的执行顺序。每个锁只在自己的最小范围内进行操作。使用内存屏障保证关键操作不会出现缓存和内存不一致的情况。这里典型的锁之内的顺序不用担心,锁之外的指令如果有明显的顺序依赖最好加个内存屏障。

结尾的闲言碎语

写到这里差不多就可以结束了,就不多说了。 狗头的赞赏码.jpg