呼呼呼呼,本篇主要讲下如何利用Kernel自带的内存参数进行性能优化。
性能是个很玄乎的存在,同样的配置参数,在服务器上可能健步如飞,在ARM手机上可能烧成火龙果了,高端机可能流畅如丝,低端机却卡出残影...所以一套参数是不能走遍天下的,内存优化的一大主要思路就是对这些参数的调整,以便达到更好的性能优化效果!
先介绍下,Linux内存的调整参数大部分在目录/proc/sys/vm下,他们都在kernel/sysctl.c中定义:
OP4EC1:/proc/sys/vm # ls
admin_reserve_kbytes dirty_writeback_centisecs min_free_kbytes panic_on_oom user_reserve_kbytes
block_dump dirtytime_expire_seconds mmap_min_addr percpu_pagelist_fraction vfs_cache_pressure
compact_memory drop_caches mmap_rnd_bits reap_mem_on_sigkill vm_swappiness_threshold1
compact_unevictable_allowed extfrag_threshold mmap_rnd_compat_bits stat_interval vm_swappiness_threshold2
direct_swappiness extra_free_kbytes oom_dump_tasks stat_refresh want_old_faultaround_pte
dirty_background_bytes kswapd_threads oom_kill_allocating_task swap_ratio watermark_boost_factor
dirty_background_ratio laptop_mode overcommit_kbytes swap_ratio_enable watermark_scale_factor
dirty_bytes legacy_va_layout overcommit_memory swappiness
dirty_expire_centisecs lowmem_reserve_ratio overcommit_ratio swappiness_threshold1_size
dirty_ratio max_map_count page-cluster swappiness_threshold2_size
先挑几个常用的重点部分讲一下
min_free_kbytes
内核在每个zone中都预留一部分内存,用来给高优先级进程或者紧急分配。
一般需要分配标志位带有GFP_HIGH、GPF_ATOMIC或者GFP_MEMALLOC才能分配出内存。
我们可以通过cat /proc/zoneinfo查看,单位都是page哦,《奔跑吧Linux内核》书上标的是KB是有问题滴:
pages free 184710
min 2705
low 19113
high 19789
spanned 2095616
present 1980484
managed 1921719
protection: (0, 8675, 8677)
三个水位分别是:
- high 内存回收到该值时停止回收。
- low 内存到该值时触发kswapd线程的内存回收。
- min 如果剩余内存减少到触及这个水位,可认为内存严重不足,当前进程就会被堵住,kernel会直接在这个进程的进程上下文里面做直接页面回收。
这三个值的计算是通过min_free_kbytes来决定的,
1、先通过init_per_zone_wmark_min() 来计算min_free_kbytes的值:
lowmem_kbytes = nr_free_buffer_pages() * (PAGE_SHIFT - 10);
//所有管理的页面减去高水位页面 managed - high_pages
min_free_kbytes = sqrt(lowmem_kbytes*16)
2、再通过setup_per_zone_wmarks()函数计算各项水位:
大体的计算公式如下:
pages_min = min_free_kbytes >> (PAGE_SHIFT - 10); //换算成page_number
pages_low = **extra_free_kbytes** >> (PAGE_SHIFT - 10);
//PAGE_SHIFT 为12,相当于一个page的size,4K,2的12次方是4096
对每个非highmem的zone,lowmem_pages += zone->managed_pages
for_each_zone(){
min = (u64)pages_min * zone->managed_pages;
do_div(min, lowmem_pages); //商保存在min,余数是返回值
low = (u64)pages_low * zone->managed_pages;
do_div(min, vm_total_pages);
zone->**watermark_boost**= 0; //这个参数下一节讲
zone->watermark[WMARK_MIN] = min;
zone->watermark[WMARK_LOW] = min_wmark_pages(zone) + low + min;
zone->watermark[WMARK_HIGH] = min_wmark_pages(zone) + low + min*2;
}
网上资料看/proc/sys/vm/min_free_kbytes 范围在128KB~64M 这部分我没看到,看了下自己手机是40M左右,先不管了
min_free_kbytes大小调节有2个方向:
- min_free_kbytes设置过大,相当于提高了了低水位,会提前唤醒kswapd(在实际项目中,尤其是ARM平台,会影响功耗)。另外预留太多,给普通进程的内存就少了。
- min_free_kbytes设置过小,高优先级进程或者内核线程在紧急情况可能分配不到内存,容易进死锁。min_free_kbytes设的过小,则会导致系统预留内存过小。kswapd回收的过程中也会有少量的内存分配行为(会设上PF_MEMALLOC)标志,这个标志会允许kswapd使用预留内存;另外一种情况是被OOM选中杀死的进程在退出过程中,如果需要申请内存也可以使用预留部分。这两种情况下让他们使用预留内存可以避免系统进入deadlock状态。
extra_free_kbytes
眼尖的老哥可能在上一节中已经发现这个参数了
这个参数是从上层框架中传设置的,在这个文件中frameworks/base/services/core/java/com/android/server/am/ProcessList.java
通过计算屏幕分辨率 长宽公式得出 ,所以高端手机,尤其2K屏的机器这个值就特别大
需要注意,高低端机frameworks如果共基线,这里就会出现问题!
int reserve = displayWidth * displayHeight * 4 * 3 / 1024;
int reserve_adj = Resources.getSystem().getInteger(
com.android.internal.R.integer.config_extraFreeKbytesAdjust);
/*通过名单控制,看了下我手机,默认为0*/
int reserve_abs = Resources.getSystem().getInteger(
com.android.internal.R.integer.config_extraFreeKbytesAbsolute);
/*通过名单控制,看了下我手机,默认为-1*/
if (reserve_abs >= 0) {
reserve = reserve_abs;
}
if (reserve_adj != 0) {
reserve += reserve_adj;
if (reserve < 0) {
reserve = 0;
}
}
我们在日常项目中,这个值一般都会增大,2倍(中高端)到3倍(低端机)左右,卡顿和低内存问题会有明显改善!
lowmem_reserve_ratio
就是控制这个数组的protection: (0, 8675, 8677),单位是page。
数组大小就是zone的个数,这个在函数setup_per_zone_lowmem_reserve中计算
这个数组是为了防止过度从低端zone分配,比如DMA_ZONE;防止系统过早在低端zone中触发oom,而高端zone中内存充足。
ZONE_NORMAL中的protection都是0,无需预留。
这个参数是用来判断某次分配是否满足
详看zone_watermark_ok(): 判断的条件很多,不过这里用到的就这部分
if (free_pages <= min + z->lowmem_reserve[classzone_idx])
return false;
classzone_idx是一通计算决定的首选zone,判断当前free页面是否大于min水位加上zone对应的预留页面
watermark_boost_factor
这个看名字很唬人,其实很简单。就是为了防止外碎片化,临时提高水位提前唤醒kswapd回收内存,这个是Linux 5.0后新增的,不过高通也都移植到了4.19平台。
默认是15000,相当于把HIGH水位提升到150%,设置为0就关闭了。
#define min_wmark_pages(z) (z->_watermark[WMARK_MIN] + z->watermark_boost/2)
#define low_wmark_pages(z) (z->_watermark[WMARK_LOW] + z->watermark_boost/2)
#define high_wmark_pages(z) (z->_watermark[WMARK_HIGH] + z->watermark_boost)
#define wmark_pages(z, i) (z->_watermark[i] + (((i) == WMARK_HIGH) ? (z->watermark_boost) : (z->watermark_boost / 2)))
看到没!好家伙给每个水位都加上去了!
在偷页函数中
/*补充偷页行为,如果order比较大,直接偷一个block(4MB)
*否则的话,把块上的空闲页移动到需要的类型,再查看已经分配了多少页,如果兼容的或者free页超过一半,就修改这个block的类型
*/
static void steal_suitable_fallback()
{
boost_watermark(zone);
}
static inline void boost_watermark(struct zone *zone)
{
unsigned long max_boost;
if (!watermark_boost_factor || !boost_eligible(zone)) //设置0就return掉
return;
/* high水位 x watermark_boost_factor / 10000 */
max_boost = mult_frac(zone->_watermark[WMARK_HIGH],
watermark_boost_factor, 10000);
/*
* high watermark may be uninitialised if fragmentation occurs
* very early in boot so do not boost. We do not fall
* through and boost by pageblock_nr_pages as failing
* allocations that early means that reclaim is not going
* to help and it may even be impossible to reclaim the
* boosted watermark resulting in a hang.
*/
if (!max_boost)
return;
/*pageblock_nr_pages 一半就是1024*/
max_boost = max(pageblock_nr_pages, max_boost);
zone->watermark_boost = min(zone->watermark_boost + pageblock_nr_pages, max_boost);
}
有个问题就是,watermark_boost_factor节点在sysctl.c中的回调函数,只是个节点的读写,没有用,15000的默认值似乎只能通过修改代码去调整...
watermark_scale_factor
控制两个水位间的差距,取zone总内存的百分比和min/4取最大值,作为水位间的间隔:
/*
* Set the kswapd watermarks distance according to the
* scale factor in proportion to available memory, but
* ensure a minimum size on small systems.
*/
min = max_t(u64, min >> 2,
mult_frac(zone->managed_pages,
watermark_scale_factor, 10000));
看节点信息,这个数值在1-1000之间,所以两个水位之间的差值可以是总内存的0.1%到10%,看了下我手机上默认是1。
{
.procname = "watermark_scale_factor",
.data = &watermark_scale_factor,
.maxlen = sizeof(watermark_scale_factor),
.mode = 0644,
.proc_handler = watermark_scale_factor_sysctl_handler,
.extra1 = &one,
.extra2 = &one_thousand,
},
而且这个参数的回调函数中,每次修改都会重新设定水位哟
int watermark_scale_factor_sysctl_handler(struct ctl_table *table, int write,
void __user *buffer, size_t *length, loff_t *ppos)
{
int rc;
rc = proc_dointvec_minmax(table, write, buffer, length, ppos);
if (rc)
return rc;
if (write)
setup_per_zone_wmarks();
return 0;
}
以上