这个是我认为小内存处理中比较靠谱的方式——zram。它就像压缩饼干一样,虽然小小一块饼干看起来不大(zram的压缩页面占用内存),但是一喝水,感觉立马饱了(释放一个页面的内容)。
1.简介
2.如何使能
3.工作流程
4.还有什么能做的?
LINUX/android/kernel/drivers/staging/zram/zram_drv.c
相关代码后面会解说。
zram在高通平台下默认是不打开的,MTK平台有的是打开的,根据Google官方的教程,需要在配置文件中打开开关,分为一下几个步骤:
1)在源代码:LINUX/android/kernel/arch/arm/configs下找到msm8974_defconfig,添加下列配置选项:
CONFIG_SWAP=y CONFIG_CGROUP_MEM_RES_CTLR=y CONFIG_CGROUP_MEM_RES_CTLR_SWAP=y CONFIG_ZRAM=y CONFIG_ZSMALLOC=y
前四个选项是Android官方教程中提示要打开的,但是笔者实际的操作中,发现一直无法加载zram0这个设备文件,即zram根本没有被编译进内核,后经查证,原来CONFIG_ZRAM依赖于CONFIG_ZSMALLOC,BLOCK和SYSFS这三个选项,故加上之后编译过后,在target目录下找到了编译好的目标文件
2)更新fstab,位于LINUX/android/device/qcom/msm8974,目录下有fstab.qcom
这个文件强依赖于当前所使用cpu的厂商,在教程中以fstab.X代替,X代表的就是不同厂商,在这个文件中添加fstab的格式语句,添加后的效果图如图所示,对于fstab的命令格式,此处不赘述。
3)修改init.rc文件
改文件目录位于:LINUX/android/device/qcom/msm8974,可能读者并没有发现inir.rc文件,不错,的确没有这个文件,这个文件真正的名字叫 inir.target.rc。它与fstab.qcom位于同一个目录下。
默认情况下,使用交换分区的时候,内存在同一时刻是要读8个内存页面,而当使用zram的时候,同一时刻读一个内存页面,这样的做的好处是,读取一个内存页面的时间是可以忽略不计的,并且可以减少内存的压力。
为了使得同一时刻,置换的页面数为1,请加入如下指令:
write/proc/sys/vm/page-cluster 0,
在mount_all /fstab.qcom这条语句下面(也可能是mount_allfstab.qcom,因为编译之后,fstab.qcom位于根目录”/”下,也在当前脚本的目录下,所以两种写法都可以),添加语句:swapon_all /fstab.qcom
4)编译内核,等待机器重启
验证/dev/block/zram0设备字符是否存在,free指令或者cat/proc/swaps验证swap分区是否已经挂载并正常使用。
是不是感觉上面的过程很繁琐,第一次我看了并实践之后,感觉太TM繁琐了,敢不敢再简单点,MTK就写了一个脚本,在初始化的时候,运行这个脚本就ok了,我只能说,MTK,干得好。
#No path is set up at this point so we have to do it here. PATH=/sbin:/system/sbin:/system/bin:/system/xbin export PATH mkswap /dev/block/zram0 swapon /dev/block/zram0
以上就是MTK的使能脚本,只要执行我上述的第一步后(后面那些繁琐的过程都可以省去),在启动的时候执行这个脚本,就可以使能zram了,是不是很简单?
知道了压缩与解压的算法,页面到底是怎么被压缩,它的一个流程是怎么样的呢?一切都要回到zram的块设备驱动。
设备驱动文件路径:LINUX/android/kernel/drivers/staging/zram/zram_drv.c
众所周知,zram是可随机存储的设备,它不同于磁盘需要考虑IO调度的问题,所以在编写zram的设备驱动的时候,选择不是默认的request_queue,而是自定义了一个“请求制造函数”:
zram->queue = blk_alloc_queue(GFP_KERNEL);
blk_queue_make_request(zram->queue, zram_make_request);
blk_queue_make_request函数将申请的队列,与函数zram_make_request联系起来,在有IO数据产生的时候,就会调用zram_make_request这个函数。
在块数据传输的过程中,除了request_queue ,struct bio 也是一个非常重要的数据结构,zram_make_request会遍历struct bio,根据参数rw的值,判断是要zram中读出数据,还是往zram中写入数据,所以压缩和解压的函数就在这里被调用。
当判断出是往zram中写入数据的时候,会调用:
ret =zram_bvec_write(zram,bvec, index, offset);
而读出数据的时候会调用:if (unlikely(!meta->table[index].handle) || zram_test_flag(meta, index, ZRAM_ZERO)) { handle_zero_page(bvec); //处理零页面 return 0; }
如果不是零页面,函数就会对解压后的页面做一个临时内核映射,保证解压后的页面能正确写入内存。
随后就是真正开始解压缩的过程,解压缩函数为:zram_decompress_page,但是这个函数并不是真正只是完成解压缩的功能,它会做一些解压缩前的工作,如做一些映射,读出将要解压的页面数据,再进行一些判断,真正解压缩的工作由函数
lzo1x_decompress_safe完成,该函数位于LINUX/android/kernel/lib/lzo/lzo1x_decompress.c中调用语句为:
ret =lzo1x_decompress_safe(cmem,meta->table[index].size,mem, &clen);
最后解压出的数据,存在mem指针所在内存中,meta->table[index].size是压缩数据的大小,而cmem则是解压前的数据,
Clen为解压后的大小。
那么压缩过程又是咋样的呢?通过阅读zram_bvec_write的源代码,发现它也调用了zram_decompress_page这个函数,这是怎么回事?为什么压缩过程会调用解压函数呢?代码的注释解释为:即使不完整页面的IO传输,也需要读取整个页面。细细查看,发现zram_decompress_page中有这样几条语句:
if (meta->table[index].size == PAGE_SIZE) copy_page(mem, cmem); else ret = lzo1x_decompress_safe(cmem, meta->table[index].size, mem, &clen);
当判断所要解压的页面大小等于PAGE_SIZE的时候,只是简单的拷贝页面而不是解压它,这就是在压缩之前调用zram_decompress_page的原因。
在进行了一系列的处理之后,如零页面处理,最后调用了:
ret =lzo1x_1_compress(uncmem,PAGE_SIZE,src, &clen,meta->compress_workmem);
uncmem是压缩前的指针地址,src则是压缩后的数据指针,clen是压缩后的长度。而src在初始的时候被赋值:
src =meta->compress_buffer;即一开始就设置了压缩的缓冲区。
得到了压缩后的数据和长度,接下来的任务并不是写入到压缩页面中,而是判断压缩后的数据是不是比PAGE_SIZE大,一般来说,压缩之后的大小小于PAGE_SIZE/2,则称为“瘦压缩”,大于PAGE_SIZE/2的称为“胖压缩”,大于PAGE_SIZE的称为“坏压缩”,如果出现了坏压缩,则调用am->stats.bad_compress++;说明此页面没有也所的必要了,所以存入zram的是原来的页面,出现了“瘦压缩”,则调用zram->stats.good_compress++;说明这个压缩是成功的,内存总是希望越多的“瘦压缩”越好,而事实情况下,一般都是一个“胖压缩”与一个“瘦压缩”共用一个页面,如果压缩密度过高,则解压的过程也会非常耗时,这会影响系统的总体性能。
正常压缩的情况下,去的压缩页面数据后,存入页面即可,最后更新zram数据结构中需要保存的数据,还有解除一些内存映射就完成了压缩的过程。