linux memory lock浅析

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都是以WRITE的形式来触发的,这意味着以MAP_PRIVATE映射的map在这个时候就会触发COW!

而如果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标记。

时间: 2024-12-02 23:14:31

linux memory lock浅析的相关文章

linux页面回收浅析

关于页面的使用 在之前的一些文章中,我们了解到linux内核会在很多情况下分配页面. 1.内核代码可能调用alloc_pages之类的函数,从管理物理页面的伙伴系统(管理区zone上的free_area空闲链表)上直接分配页面(见<linux内核内存管理浅析>).比如:驱动程序可能用这种方式来分配缓存:创建进程时,内核也是通过这种方式分配连续的两个页面,作为进程的thread_info结构和内核栈:等等.从伙伴系统分配页面是最基本的页面分配方式,其他的内存分配都是基于这种方式的: 2.内核中的

linux内核mem_cgroup浅析

memory cgroup mem_cgroup是cgroup体系中提供的用于memory隔离的功能. admin可以创建若干个mem_cgroup,形成一个树型结构.可以将进程加入到这些mem_cgroup中.(类似这样的管理功能都是由cgroup框架自带的.) 为了实现memory隔离,每个mem_cgroup主要有两个维度的限制: 1.res - 物理内存 2.memsw - memory + swap,物理内存 + swap 其中,memsw肯定是大于等于memory的. 另外注意,me

linux文件读写浅析

在<linux内核虚拟文件系统浅析>这篇文章中,我们看到文件是如何被打开.文件的读写是如何被触发的. 对一个已打开的文件fd进行read/write系统调用时,内核中该文件所对应的file结构的f_op->read/f_op->write被调用. 本文将顺着这条路走下去,大致看看普通磁盘文件的读写是怎样实现的. linux内核响应一个块设备文件读写的层次结构如图(摘自ULK3): 1.VFS,虚拟文件系统. 之前我们已经看到f_op->read/f_op->write如

linux异步IO浅析

知道异步IO已经很久了,但是直到最近,才真正用它来解决一下实际问题(在一个CPU密集型的应用中,有一些需要处理的数据可能放在磁盘上.预先知道这些数据的位置,所以预先发起异步IO读请求.等到真正需要用到这些数据的时候,再等待异步IO完成.使用了异步IO,在发起IO请求到实际使用数据这段时间内,程序还可以继续做其他事情). 假此机会,也顺便研究了一下linux下的异步IO的实现. linux下主要有两套异步IO,一套是由glibc实现的(以下称之为glibc版本).一套是由linux内核实现,并由l

linux slub分配器浅析

在<linux内存管理浅析>中提到内核管理自己使用的内存时,使用了SLAB对象池.SLAB确实是比较复杂,所以一直以来都没有深入看一看. 不过现在,linux内核中,SLAB已经被它的简化版--SLUB所代替.最近抽时间看了一下SLUB的代码,略记一些自己的理解. 尽管SLUB是在内核里面实现的,用户态的对象池其实也可以借鉴这样的做法. SLUB的总体思想还是跟SLAB类似,对象池里面的内存都是以"大块"为单位来进行分配与回收的.然后每个"大块"又按对象

linux内核cfs浅析

linux调度器的一般原理请参阅<linux进程调度浅析>. 之前的调度器 cfs之前的linux调度器一般使用用户设定的静态优先级,加上对于进程交互性的判断来生成动态优先级,再根据动态优先级决定进程被调度的顺序,以及调度后可以运行的时间片. 反过来,随着进程的运行,内核可能发现其交互性发生改变,从而调整其动态优先级(奖励睡眠多的交互式进程.惩罚睡眠少的批处理进程). cfs原理 cfs定义了一种新的模型,它给cfs_rq(cfs的run queue)中的每一个进程安排一个虚拟时钟,vrunt

linux虚拟文件系统浅析

虚拟文件系统(VFS) 在我看来, "虚拟"二字主要有两层含义: 1, 在同一个目录结构中, 可以挂载着若干种不同的文件系统. VFS隐藏了它们的实现细节, 为使用者提供统一的接口; 2, 目录结构本身并不是绝对的, 每个进程可能会看到不一样的目录结构. 目录结构是由"地址空间(namespace)"来描述的, 不同的进程可能拥有不同的namespace, 不同的namespace可能有着不同的目录结构(因为它们可能挂载了不同的文件系统). 操作已打开的文件 VFS

linux内存管理浅析

[地址映射](图:左中) linux内核使用页式内存管理,应用程序给出的内存地址是虚拟地址,它需要经过若干级页表一级一级的变换,才变成真正的物理地址. 想一下,地址映射还是一件很恐怖的事情.当访问一个由虚拟地址表示的内存空间时,需要先经过若干次的内存访问,得到每一级页表中用于转换的页表项(页表是存放在内存里面的),才能完成映射.也就是说,要实现一次内存访问,实际上内存被访问了N+1次(N=页表级数),并且还需要做N次加法运算. 所以,地址映射必须要有硬件支持,mmu(内存管理单元)就是这个硬件.

linux seqlock &amp; rcu 浅析

在linux内核中,有很多同步机制.比较经典的有spin_lock(忙等待的锁).mutex(互斥锁).semaphore(信号量).等.并且它们几乎都有对应的rw_XXX(读写锁),以便在能够区分读与写的情况下,让读操作相互不互斥(读写.写写依然互斥). 而seqlock和rcu应该可以不算在经典之列,它们是两种比较有意思的同步机制. seqlock(顺序锁) 用于能够区分读与写的场合,并且是读操作很多.写操作很少,写操作的优先权大于读操作. seqlock的实现思路是,用一个递增的整型数表示