linux内核提供了用于锁定内存的系统调用,如:
mlock:lock一段地址范围内已map的内存
mlockall:lock进程虚拟地址空间内已map的内存,还可以选择对于此后新map的空间是否自动lock
mmap+MAP_LOCKED选项:在mmap的同时,对相应地址范围进行mlock
利用这些系统调用,用户进程可以对自己需要使用的内存进行lock。lock,则意味着相应的数据在unlock之前将一直存在于物理内存中,不会被回收。
对于应用程序来说,可以将内存中一些对程序性能影响较大的数据lock起来,避免非预期的页面回收引起性能波动。
lock
memory lock的实现思想说起来很简单。在《linux内存管理浅析》一文中说到,用户进程的地址空间由一组vma结构来管理,每一个vma代表一个已映射的、连续的、且属性相同的内存空间。
而memory lock要做的事情就是给相应的vma置一个VM_LOCKED标记,然后这个标记会影响到内存回收策略。
当然,memory lock时指定的地址范围可能跟现有的某个vma并不完全重合,所以lock操作可能导致现有的vma被合并或分割。(因为要满足同一个vma的内存属性一致,而“lock”也是其属性之一。)
在《linux页面回收浅析》一文中又说到,给用户空间使用的内存(包括用户分配的匿名内存和page cache中的内存)会由LRU来管理,然后会有内核线程扫描LRU,并从中回收page。
当page reclaim流程扫描到一个“最近最少访问”(非精确的)的page时,会试图回收它。在回收之前,需要通过反向映射,找到那些包含了它的vma,从而找到那些映射了它的page table,然后将映射取消掉。
而如果映射了这个page的某个vma带有VM_LOCKED标记呢?说明这个page已被某个进程lock(尽管不一定被所有映射它的进程都lock),这时就应该放弃回收。(直到所有映射了这个page的vma都unlock,这个page才有可能被回收。)
给相应的vma加VM_LOCKED标记,以及page reclaim流程通过反向映射检查该标记,就是memory lock最核心的处理逻辑。这两个动作就保证了被lock的page不会被回收。
除此之外,memory lock还会有一些附加动作:
1、分配并映射page。
内存空间的map并不代表物理内存页被分配并映射。而就算物理内存已映射过,也有被回收掉的时候。
memory lock会将lock vma区域内的page都安排就位。所要做的事情也就是遍历一下整个区域,对没有映射上page的地方手动触发一下page fault。
而如果page fault失败(比如page分配失败),除了mlock系统调用,其他情况下都是会忽略的。毕竟lock主要还是保证page不被回收。
2、将page移动到unevictable_list。
在page reclaim流程中,如果扫描了一堆page,都在试图回收之时,费尽气力走完反向映射,然后发现page被lock过,以至于无法回收,那实在就太悲剧了。
所以内核在LRU中新增了一个unevictable_list(除原有的active_list、inactive_list之外)。被lock的page将放到unevictable_list中,然后不再被page reclaim流程所扫描。
同时也给page置一个PG_mlocked标记,以表示该page已被lock,并将被放入unevictable_list。
跟前一个附加动作一样,这里并不一定保证成功。所以page reclaim流程通过反向映射检查VM_LOCKED标记的逻辑一定是需要存在的,那才是根本。
unlock
与lock相对应,munlock、munlockall用于对进程的某段内存空间进行解锁。
此外,当vma消失时(比如munmap、exit、等),也会自动解锁。
memory unlock是memory lock的逆过程。相应vma的VM_LOCKED标记会被去掉。然后还要通过反向映射去检查该page是否已经不再被其他的vma所lock,若是,则进一步,将page上的PG_mlocked标记去掉,并从unevictable_list中移走;否则说明该page尚未unlock完全,不需要进一步动作。
memory unlock并不立刻将解锁的page回收,而是让其走上自然回收的过程(放回active_list或inactive_list,然后交由page reclaim流程去处理)。
相比之下,memory lock只需要确保vma上的VM_LOCKED标记打上了,其他的都好说。就算page未分配成功、或者未放入unevictable_list,page reclaim流程总是能通过反向映射检查出page被lock的事实。
然而memory unlock就没有这样的补救措施。如果unlock之后,page还没从unevictable_list中移走,就再没有人会发现并回收它了(page reclaim流程不会去看unevictable_list)。所以memory unlock一定会确保成功。
不过话说回来,memory lock涉及到page分配,的确有失败的可能。而memory unlock是释放page,也的确是可以确保成功的。就好比malloc会失败、而free不会失败一样。
其他
限制
进程能够lock的page数目是有限制的,通过ulimit -l命令就能看到。当然你有权将其调整为unlimited。
关于fork操作的影响
fork系统调用会创建一个子进程,并拷贝父进程的整个地址空间,包括对所有vma的拷贝。
不过子进程的vma并不继承VM_LOCKED标记。
也就是说,一次memory lock只需要一次memory unlock来解锁,不会因为fork而把问题搞得复杂。
关于一些强制page cache回收的操作
madvise+MADV_DONTNEED:
这里其实并不会触发page cache的强制回收,仅仅是取消本进程到page cache中相应page的内存映射。
从逻辑上讲,page cache是全局的,而虚拟内存则是进程私有的,对私有的memory进行advise显然不应该直接影响全局的page cache;
fadvise+POSIX_FADV_DONTNEED:
与madvise不同,fadvise是对file的advise,而file正是全局的概念。所以fadvise确实会直接影响page cache。
对于指定的page,如果没有映射且不是脏页,fadvise+POSIX_FADV_DONTNEED会直接将其丢弃掉。
否则,fadvise+POSIX_FADV_DONTNEED会检查page是否有PG_mlocked标记,是则不做处理,否则如果page未被映射,试图将page移动到inactive_list的尾部,以便page reclaim流程优先将其回收。(因为回收涉及到走反向映射并取消映射、以及脏页写回等操作,直接在这里回收会把问题搞复杂,所以还是交给page reclaim。)(被映射的page其实也是不接受fadvise的。)
对于page被lock的情况,一般是有映射且有PG_mlocked标记,fadvise不会对其做处理。
极端情况下,memory lock时只设置了vma的VM_LOCKED标记,其他的都失败了,可能会导致应该被lock的page未被映射,从而被fadvise+POSIX_FADV_DONTNEED丢弃。但是这种情况其实跟page未分配成功是类似的。
/proc/sys/vm/drop_caches:
试图清除所有文件的所有page cache,对于单个page的处理跟fadvise+POSIX_FADV_DONTNEED是一致的。
shmem lock
除了前面提到的系统调用之外,对于ipc shmem还有另一种方法实现类似memory lock的功能,即使用shmctl系统调用的SHM_LOCK/SHM_UNLOCK操作。
我们知道,shmem可以通过shmat系统调用attach到用户进程空间,那么也就可以对attach的空间进行memory lock。而如果你不打算attach,也可以从全局范围内对一块shmem执行shmctl+SHM_LOCK。后者会在这个shmem的page cache所对应的address_space结构上加AS_UNEVICTABLE标记,而page reclaim流程也同样会检查该标记以判断page是否被lock。
类似的,能不能在不mmap一个file的情况下,对file的page cache所对应的address_space结构加AS_UNEVICTABLE标记呢?目前貌似还没有这样的操作。
关于unevictable_list
其实unevictable_list并不是专为memory lock服务的。所有不希望被回收的page都可以往里面放,memory lock的page只是其中一例(其实从字面意思就能看出来)。
前面提到的shmem lock严格来说就不属于memory lock一路。另外ramfs所用到的page也是不可回收的,内核会自动给相应inode所对应的address_space结构上加AS_UNEVICTABLE标记。