前言
了解 linux kernel内存管理,首先可以从用户空间的角度来看kernel的内存管理,执行ls /proc/sys/vm的命令,就可以看到vm运行的所有参数,其中就包含了跟overcommit相关的参数。
Memory overcommit概念介绍
要了解这类参数首先要理解什么是committed virtual memory?使用git版本管理工具都熟悉commit的含义,就是向代码仓库提交自己更新的意思。对于这个场景,实际上就是各个进程提交自己对虚拟地址空间的请求。虽然我们总是宣称每个进程都有自己独立的地址空间,但是这些地址空间都是虚拟地址空间。当进程需要内存时(例如通过brk分配内存),进程从内核获得的仅仅是一段虚拟地址的使用权,而不是实际的物理地址,进程并没有获得物理内存。实际的物理内存只有当进程真的去访问新获取的虚拟地址时(即开始要使用物理内存时),就会产生“page fault(缺页异常)”,从而进入分配实际物理地址的过程,也就是分配实际的page frame并建立page table。之后系统返回到产生page fault时的地址,此时已经得到物理内存的使用权了,重新执行内存访问,一切好像没有发生过。因此,看起来虚拟内存和物理内存的分配被分割开了,这是否意味着进程可以任意的申请虚拟地址空间呢?也不行,毕竟virtual memory需要physical memory做为支撑,如果分配了太多的virtual memory,和物理内存不成比例,超过了实际可用的内存,对性能会有影响。对于这个状况,我们称之为overcommit。有个概念需要谨记:内存申请不等于内存分配,内存只在实际用到的时候才分配。
Linux默认是允许memory overcommit的,所以Linux设计了一个OOM killer机制(OOM = out-of-memory)来处理overcommit:挑选一个进程出来杀死,以腾出部分内存,如果还不够就继续杀…也可通过设置内核参数 vm.panic_on_oom 使得发生OOM时自动重启系统。这都是有风险的机制,重启有可能造成业务中断,杀死进程也有可能导致业务中断。因此Linux 2.6之后允许通过内核参数 vm.overcommit_memory 禁止memory overcommit。
overcommit相关参数介绍
1、overcommit_memory
overcommit_memory这个参数就是用来控制内核对overcommit的策略。该参数可以设定的值如下:
- include/uapi/linux/mman.h
- #define OVERCOMMIT_GUESS 0
- #define OVERCOMMIT_ALWAYS 1
- #define OVERCOMMIT_NEVER 2
OVERCOMMIT_GUESS:这是linux的缺省值,它允许overcommit,但是它会根据当前的系统可用虚拟内存来判断是否允许虚拟内存的申请。内核利用__vm_enough_memory判断你的内存申请是否合理,它认为不合理就会拒绝overcommit。
OVERCOMMIT_ALWAYS:内核不限制overcommit,无论进程们commit了多少的地址空间的申请都不会拒绝。
OVERCOMMIT_NEVER:always的反面,禁止overcommit,不允许超过系统设置的虚拟内存限制。
2、overcommit_kbytes和overcommit_ratio
OVERCOMMIT_ALWAYS可以很任性,总是允许出现overcommit现象,但是OVERCOMMIT_NEVER不行,这个策略下,系统不允许出现overcommit。判断overcommit的标准,可以从vm_commit_limit这个函数看出端倪,需要注意的是这个标准的定义的内存大小既不是物理内存的大小,也不是free memory的大小:
- arch/arm/include/asm/page.h
- /* PAGE_SHIFT determines the page size */
- #define PAGE_SHIFT 12
-
- mm/util.c
- /*
- * Committed memory limit enforced when OVERCOMMIT_NEVER policy is used
- */
- unsigned long vm_commit_limit(void)
- {
- unsigned long allowed;
- //如果sysctl_overcommit_kbytes,则使用overcommit_kbytes
- if (sysctl_overcommit_kbytes)
- //这个操作实际上是将kB转换成以page为单位(1page=4K),右移2位,相当于除以4
- allowed = sysctl_overcommit_kbytes >> (PAGE_SHIFT - 10);
- else
- //否则使用overcommit_ratio
- allowed = ((totalram_pages - hugetlb_total_pages())
- * sysctl_overcommit_ratio / 100);
- allowed += total_swap_pages;
- //返回判断overcommit的阈值,是以page(1page=4K)为单位的
- return allowed;
- }
overcommit的标准有两个途径来设定,第一个是直接定义overcommit_kbytes(函数中sysctl_overcommit_kbytes非0),这时候标准值是overcommit_kbytes+total_swap_pages。对于total_swap_pages,稍微讲一下页面回收(page frame reclaim)机制。
在内核中,虽然有overcommit机制,但是一般对虚拟内存的使用是相对大方的,可是在用户空间,需要对进程的创建、动态内存分配、堆栈的分配等虚拟内存进行限制,因为内核对来自用户空间的内存申请是严格审查的,物理内存分配给用户进程也是卡控严格的。这么做的目的就是为了更好的使用内存,也就是说:在限定的物理内存资源下,可以尽量让更多的用户空间进程运行起来。如果让物理地址和虚拟地址空间是一一映射的时候,那么系统中的可以启动进程数目必定是受限的,进程可以申请的内存数目也是受限的,你的程序不得不频繁面临内存分配失败的问题。所以内核是将一个较小的物理内存空间映射到一个较大的各个用户进程组成的虚拟地址空间之上。怎么办,最简单的方法就是“拆东墙补西墙(换入换出策略)”。但是,拆东墙(swap out)也是技术活,并不是所有进程的任何虚拟空间都可以拆。比如说程序的正文段是可以拆,因为这些内存中的内容在磁盘上有保留,当再次需要的时候(补西墙),可以从磁盘上中重新加载进来。不是所有的进程地址空间都在磁盘上有备份的,像堆、stack这些进程的虚拟地址段都没有磁盘文件与之对应的,也就是传说中的anonymous page。对于anonymous page,如果我们建立swap file或者swap device,那么这些anonymous page也同样可以被交换到磁盘,并且在需要的时候load进内存。
现在回到total_swap_pages这个变量,它其实就是系统可以将anonymous page交换到磁盘的大小,如果我们建立32MB的swap file或者swap device,那么total_swap_pages就是(32M/page size),这个page size默认是4K。
overcommit的第二个标准是:在sysctl_overcommit_kbytes设定为0的时候使用,和系统当前可用的page frame相关。不是系统中的物理内存有多少,totalram_pages就有多少,实际上很多的page是不能使用的,例如linux kernel本身的正文段,数据段等就不能计入totalram_pages,还有一些系统reserve的page也不算数,最终totalram_pages实际上就是系统可以管理分配的总内存数目。overcommit_ratio是一个百分比的数字,50表示可以使用50%的totalram_pages,当然还有考虑total_swap_pages的数目,上文已经描述。
同时在使用overcommit_ratio时,要考虑是否使用了huge pages,传统的4K的page和huge page的选择也是一个平衡问题。normal page可以灵活的管理内存段,浪费少。但是不适合大段虚拟内存段的管理(因为要建立大量的页表,TLB side有限,因此会导致TLB miss,影响性能),huge page和normal page相反。内核可以同时支持这两种机制,不过是分开管理的。由于本节描述的overcommit相关参数都是和normal page相关的,因此在计算allowed page的时候要减去hugetlb_total_pages。
如下就是手机里面的跟overcommit相关的两个参数。其中CommitLimit 就是overcommit的阈值,申请的内存总数超过CommitLimit的话就算是overcommit。 Committed_AS 表示所有进程已经申请的内存总大小(注意是已经申请的,不是已经分配的),如果 Committed_AS 超过 CommitLimit 就表示发生了 overcommit,超出越多表示 overcommit 越严重。Committed_AS 的含义换一种说法就是,如果要绝对保证不发生OOM (out of memory) 需要多少物理内存。但是由于这个手机的overcommit_memory参数是1,根据前面描述,可以知道是不限制overcommit的,所以出现Committed_AS大于CommitLimit情况。如果参数为2,就绝对不会出现这种情况。同时当前手机使用的是overcommit_ratio(50%)而不是overcommit_kbytes(为0)。
- grep -i commit /proc/meminfo
- CommitLimit: 12174056 kB
- Committed_AS: 149507868 kB
-
- User:/proc/sys/vm # cat overcommit_memory
- cat overcommit_memory
- 1
-
- User:/proc/sys/vm # cat overcommit_kbytes
- cat overcommit_kbytes
- 0
- User:/proc/sys/vm # cat overcommit_ratio
- cat overcommit_ratio
- 50
3、admin_reserve_kbytes和user_reserve_kbytes
上述两个参数主要是防止内存管理模块把自己逼到绝境,避免出现意想不到的情况。上面我们提到拆东墙补西墙的机制,但是这种机制在某些情况下其实也不能正常的运作。例如进程A在访问自己的内存的时候,出现page fault,通过scan,将其他进程(B、C、D…)的“东墙”拆掉,分配给进程A,以便让A可以正常运行。需要注意的是,“拆东墙”不是那么简单的事情,有可能需要进行磁盘I/O操作(比如:将dirty的page cache flush到磁盘)。但是,系统很快调度到了B进程,而B进程立刻需要刚刚拆除的东墙,怎么办?B进程立刻需要分配物理内存,如果没有free memory,这时候也只能启动scan过程,继续找新的东墙。在极端的情况下,很有可能把刚刚补好的西墙拆除,这时候,整个系统的性能就会显著的下降,有的时候,用户点击一个button,很可能半天才能响应。面对这样的情况,用户当然想恢复,例如kill那个吞噬大量内存的进程。这个操作也需要内存(需要fork进程),因此,为了能够让用户顺利逃脱绝境,系统会保留user_reserve_kbytes的内存。
对于支持多用户的GNU/linux系统而言,恢复系统可能需要root用来来完成,这时候需要保留一定的内存来支持root用户的登录操作,支持root进行trouble shooting(使用ps,top等命令),找到那个闹事的进程并kill掉它。这些为root用户操作而保留的memory定义在admin_reserve_kbytes参数中。sysctl_admin_reserve_kbytes受内核参数/proc/sys/vm/admin_reserve_kbytes控制,同样sysctl_user_reserve_kbytes受内核参数/proc/sys/vm/user_reserve_kbytes控制。不过系统一般会给一个初始值,如下代码就是分别初始化sysctl_admin_reserve_kbytes和sysctl_user_reserve_kbytes参数,值分别为8M和128M,正常这点内存大小是够用的,实际上就是最终值。如下函数解释里面也描述了。
- /*
- * Initialise sysctl_user_reserve_kbytes.
- *
- * This is intended to prevent a user from starting a single memory hogging
- * process, such that they cannot recover (kill the hog) in OVERCOMMIT_NEVER
- * mode.
- *
- * The default value is min(3% of free memory, 128MB)
- * 128MB is enough to recover with sshd/login, bash, and top/kill.
- */
- static int init_user_reserve(void)
- {
- unsigned long free_kbytes;
-
- free_kbytes = global_zone_page_state(NR_FREE_PAGES) << (PAGE_SHIFT - 10);
- //初始化时,默认设置为128MB
- sysctl_user_reserve_kbytes = min(free_kbytes / 32, 1UL << 17);
- return 0;
- }
- subsys_initcall(init_user_reserve);
-
- /*
- * Initialise sysctl_admin_reserve_kbytes.
- *
- * The purpose of sysctl_admin_reserve_kbytes is to allow the sys admin
- * to log in and kill a memory hogging process.
- *
- * Systems with more than 256MB will reserve 8MB, enough to recover
- * with sshd, bash, and top in OVERCOMMIT_GUESS. Smaller systems will
- * only reserve 3% of free pages by default.
- */
- static int init_admin_reserve(void)
- {
- unsigned long free_kbytes;
-
- free_kbytes = global_zone_page_state(NR_FREE_PAGES) << (PAGE_SHIFT - 10);
- //初始化时,默认设置为8MB
- sysctl_admin_reserve_kbytes = min(free_kbytes / 32, 1UL << 13);
- return 0;
- }
- subsys_initcall(init_admin_reserve);
在 __vm_enough_memory函数对应文件下,也再次设置了sysctl_admin_reserve_kbytes和sysctl_user_reserve_kbytes参数
- //mm/util.c
- int sysctl_overcommit_memory __read_mostly = OVERCOMMIT_GUESS;
- int sysctl_overcommit_ratio __read_mostly = 50;
- unsigned long sysctl_overcommit_kbytes __read_mostly;
- int sysctl_max_map_count __read_mostly = DEFAULT_MAX_MAP_COUNT;
- unsigned long sysctl_user_reserve_kbytes __read_mostly = 1UL << 17; /* 128MB */
- unsigned long sysctl_admin_reserve_kbytes __read_mostly = 1UL << 13; /* 8MB */
从手机看,当前手机配置的user_reserve_kbytes和admin_reserve_kbytes分别也是128M和8M,使用默认值。
- User:/proc/sys/vm # cat admin_reserve_kbytes
- cat admin_reserve_kbytes
- 8192
- User:/proc/sys/vm # cat user_reserve_kbytes
- cat user_reserve_kbytes
- 131072
__vm_enough_memory函数介绍
用户空间进程在使用内存的时候,通过产生page fault,进入内核态,内核会审核用户空间的虚拟内存分配请求。此时,内核都会调用__vm_enough_memory函数来验证是否可以允许分配用户空间需要的这段虚拟内存,代码详解如下。
- //mm/util.c
- /*
- * __vm_enough_memory函数,返回0表示进程有足够内存分配,返回-ENOMEM表示没有足够内存分配
- * Check that a process has enough memory to allocate a new virtual
- * mapping. 0 means there is enough memory for the allocation to
- * succeed and -ENOMEM implies there is not.
- *
- * We currently support three overcommit policies, which are set via the
- * vm.overcommit_memory sysctl. See Documentation/vm/overcommit-accounting.rst
- *
- * Strict overcommit modes added 2002 Feb 26 by Alan Cox.
- * Additional code 2002 Jul 20 by Robert Love.
- *
- * 如果是root用户,cap_sys_admin为1,否则为0
- * cap_sys_admin is 1 if the process has admin privileges, 0 otherwise.
- *
- * Note this is a helper function intended to be used by LSMs which
- * wish to use this logic.
- */
- //内存描述符 mm_struct,此次申请的内存大小pages(1 page = 4K),cap_sys_admin是否是root用户还是普通用户
- int __vm_enough_memory(struct mm_struct *mm, long pages, int cap_sys_admin)
- {
- long free, allowed, reserve;
-
- VM_WARN_ONCE(percpu_counter_read(&vm_committed_as) <
- -(s64)vm_committed_as_batch * num_online_cpus(),
- "memory commitment underflow");
-
- vm_acct_memory(pages);
-
- /*
- * Sometimes we want to use more memory than we have
- */
- //由于内核不限制overcommit,直接返回0,
- //sysctl_overcommit_memory受内核参数/proc/sys/vm/overcommit_memory控制
- if (sysctl_overcommit_memory == OVERCOMMIT_ALWAYS)
- return 0;
- //若为OVERCOMMIT_GUESS,内核会进行判断内存分配是否合理
- if (sysctl_overcommit_memory == OVERCOMMIT_GUESS) {
- //1.首先会计算当前系统的可用内存
- //(1)计算有多少空闲page frame
- free = global_zone_page_state(NR_FREE_PAGES);
- //(2)计算有多少page cache使用的page frame,主要是用户空间进程读写文件造成的
- //这些cache都是为了加快系统性能而增加的,提高CPU读写命中率,因此,如果直接操作到磁盘,本质上这些page cache都是free的
- free += global_node_page_state(NR_FILE_PAGES);
-
- /*
- * shmem pages shouldn't be counted as free in this
- * case, they can't be purged, only swapped out, and
- * that won't affect the overall amount of available
- * memory in the system.
- */
- //(3)用于进程间的share memory机制的这些shmem page frame不能认为是free的,需要减去
- //而且它们不能被清除(free),只能换出(swap out)
- free -= global_node_page_state(NR_SHMEM);
- //(4)加上swap file或者swap device上空闲的“page frame”数目。
- //本质上,swap file或者swap device上的磁盘空间都是给anonymous page做腾挪之用,其实这里的“page frame”不是真的page frame,称之swap page好了
- //这里把free swap page的数目也计入free主要是因为可以把使用中的page frame swap out到free swap page上,因此也算是free page
- free += get_nr_swap_pages();
-
- /*
- * Any slabs which are created with the
- * SLAB_RECLAIM_ACCOUNT flag claim to have contents
- * which are reclaimable, under pressure. The dentry
- * cache and most inode caches should fall into this
- */
- //(5)加上被标记为可回收的slab对应的页面
- free += global_node_page_state(NR_SLAB_RECLAIMABLE);
-
- /*
- * Part of the kernel memory, which can be released
- * under memory pressure.
- */
- //(6)加上被标记为可回收的非slab的内核页面
- free += global_node_page_state(NR_KERNEL_MISC_RECLAIMABLE);
-
- /*
- * Leave reserved pages. The pages are not for anonymous pages.
- */
- //totalreserve_pages,这是一个能让系统运行需要预留的page frame的数目,
- //(7)因此我们要从减去totalreserve_pages。如果当前free page数目小于totalreserve_pages,则拒绝本次对内核空间里面的虚拟内存的申请。
- if (free <= totalreserve_pages)
- goto error;
- else
- free -= totalreserve_pages;
-
- /*
- * Reserve some for root
- */
- //(8)如果是普通的进程,还需要保留admin_reserve_kbytes的free page,以便在出问题时可以让root用户可以登录并进行恢复操作
- if (!cap_sys_admin)
- free -= sysctl_admin_reserve_kbytes >> (PAGE_SHIFT - 10);
- //最关键的判断来了
- //比对当前系统的可用内存(free)和本次请求分配virtual memory的page数目(pages),
- //如果在前面减去了所必须的page frame之后,还有足够的page可以满足本次分配,那么就批准本次对内核空间里面的虚拟内存的分配。
- if (free > pages)
- return 0;
- //否则进入error
- goto error;
- }
-
- //2.下面为OVERCOMMIT_NEVER的情况,是禁止出现overcommit
- //(1)这里调用vm_commit_limit得到判断是否出现overcommit的阈值allowed,以page为单位
- allowed = vm_commit_limit();
- /*
- * Reserve some for root
- */
- //(2)同前面一样,需要减去预留给root用户的内存
- if (!cap_sys_admin)
- allowed -= sysctl_admin_reserve_kbytes >> (PAGE_SHIFT - 10);//PAGE_SHIFT = 12
-
- /*
- * Don't let a single process grow so big a user can't recover
- */
- //(3)如果是用户空间的进程,要为用户能够从绝境中恢复而保留一些page frame,具体保留多少需要考量两个因素,
- //一个是单一进程的total virtual memory,一个是用户设定的运行时参数user_reserve_kbytes。
- //更具体的考量因素可以参考https://lkml.org/lkml/2013/3/18/812
- if (mm) {
- reserve = sysctl_user_reserve_kbytes >> (PAGE_SHIFT - 10);
- //注意:至于为什么要除以32,就跟上面链接里面描述的一样,取一个进程total_vm的3%,即接近是1/32,
- //然后跟用户设定的reserve取min,主要是避免用户把sysctl_user_reserve_kbytes设置的太大了,给了一个保护性措施
- allowed -= min_t(long, mm->total_vm / 32, reserve);
- }
-
- //allowed变量保存了判断overcommit的上限(CommitLimit),vm_committed_as(Committed-_AS)保存了当前系统中已经申请(包括本次)
- //的virtual memory的数目。如果大于这个上限就判断overcommit,本次申请虚拟内存失败
- if (percpu_counter_read_positive(&vm_committed_as) < allowed)
- return 0;
- error:
- //申请的pages大小的虚拟内存,被拒绝,不接受,返回-ENOMEM
- vm_unacct_memory(pages);
-
- return -ENOMEM;
- }
-
-
- //include/linux/mman.h
- extern struct percpu_counter vm_committed_as;
-
- //include/linux/percpu_counter.h
- struct percpu_counter {
- raw_spinlock_t lock;
- s64 count; //这里面记录当前系统里面所有进程申请的page数目
- #ifdef CONFIG_HOTPLUG_CPU
- struct list_head list; /* All percpu_counters are on a list */
- #endif
- s32 __percpu *counters;
- };
-
- /*
- * It is possible for the percpu_counter_read() to return a small negative
- * number for some counter which should never be negative.
- *
- */
- static inline s64 percpu_counter_read_positive(struct percpu_counter *fbc)
- {
- /* Prevent reloads of fbc->count */
- s64 ret = READ_ONCE(fbc->count);
-
- if (ret >= 0)
- return ret;
- return 0;
- }
参考资料
理解Linux的memory overcommit | Linux Performance