首页 > 代码库 > 巧妇能为少米之炊(3)——压缩饼干(ZRAM)

巧妇能为少米之炊(3)——压缩饼干(ZRAM)

这个是我认为小内存处理中比较靠谱的方式——zram。它就像压缩饼干一样,虽然小小一块饼干看起来不大(zram的压缩页面占用内存),但是一喝水,感觉立马饱了(释放一个页面的内容)。


1.简介

2.如何使能

3.工作流程

4.还有什么能做的?


简介:


zram就是在发生swap事件的时候,不把要置换的页面置换到外部存储中,手机中的外部存储就是EMMC,电脑中的外部存储就是硬盘。他们的读写速度比起内存的读写速度,就好比乌龟和汽车的速度相比(内存的读写速度远远大于外部存储的读写速度),所以聪明的开发者就把要置换的页面压缩后继续放在内存中,把一部分内存模拟成外部存储,那么swap事件的时候,所损耗的时间就是压缩和解压的时间,这就大大地提高了性能。

以下所说的平台为高通MSM8974平台。

如何使能ZRAM:


zram是以块设备的形式注册进内核,对于Linux内核来说,它就是一个外部存储设备,它的文件路径(高通):

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工作流程

ZRAM使用的是LZO的压缩和解压算法,选择ZRAM的压缩算法,必须权衡压缩率和压缩解压的速度,两种同样影响整体的性能

知道了压缩与解压的算法,页面到底是怎么被压缩,它的一个流程是怎么样的呢?一切都要回到zram的块设备驱动。

         设备驱动文件路径:LINUX/android/kernel/drivers/staging/zram/zram_drv.c

         众所周知,zram是可随机存储的设备,它不同于磁盘需要考虑IO调度的问题,所以在编写zram的设备驱动的时候,选择不是默认的request_queue,而是自定义了一个“请求制造函数”:

zram->queue = blk_alloc_queue(GFP_KERNEL);

对于zram设备,它首先向内核申请了一个队列,当有IO数据产生的时候,就会引流到自己定义的request,在自定义的request队列申请成功后:
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);

而读出数据的时候会调用:
ret = zram_bvec_read(zram,bvec,index, offset, bio);

zram_bvec_read中,首先会判断需要解压缩的页面是不是零页面(页面数据全为0),如果是,则直接向解压页面写0即可。部分代码:
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数据结构中需要保存的数据,还有解除一些内存映射就完成了压缩的过程。


还有什么能改进?

因为zram已经成熟了很多年,也一直被使用,又一次逛github,发现有人竟然用LZ4算法代替了LZO压缩算法,这真的试一次改进,后来成功移植之后,我个人没有感觉到明显的提升,理论上,LZ4的解压速度是LZO的3倍以上,但是由于内存自己解压和压缩的速度已经非常快了,所以感觉提升不明显,但是建议各位可以尝试着改变一下,毕竟科技日新月异,以后是否需要更大的数据呢?如果一个文件解压需要3分钟,和一个文件解压需要1分钟,这时候区别就出来了,笔者也尝试着将Linux内核用LZ4压缩和解压,经过试验,提升都不明显,但是很期待LZ4在以后的表现,也期待有更多优秀的算法出来。
LZ4算法参考:http://blog.csdn.net/zhangskd/article/details/17009111
GITHUB的LZ4算法移植:https://github.com/faux123/mako/commits/kk_mr2?page=11
移植的过程比较繁琐,有很多个commit,需耐心,移植之后,需要配置config打开相关的选项,这个作者并没有说明

ZRAM算法本身是会增加能耗,这也就是上一篇文章说的,会增加能耗,因为如果zram不停的解压与压缩,是非常耗电的,这就要合理的配置zram的大小,官方建议配置为内存的25%,个人建议根据实际情况来看,25%——50%之间都可以,zram越大,越耗电,因为会增加压缩解压的几率,但是可以增加内存,所以需要自己的权衡,还需要测试zram使能之后,应用的启动速度,运行流畅度,配合着测试得出一个合适的zram的值,每个手机和系统不同。
Google说512M内存里流畅运行Android,我拭目以待,是不是会更耗电呢?

巧妇能为少米之炊(3)——压缩饼干(ZRAM)