首发于术道经纬
Linux中的内存压缩

Linux中的内存压缩

为什么要压缩

CPU、内存和外设的速度差异可以说是一个老生常谈的问题,I/O传输一直以来都是系统性能的瓶颈。那既然你CPU那么牛,那么快,与其让你干等着I/O慢吞吞的传输数据,不如花点时间把这些数据压缩一下,这样需要传输的数据量不就减小了么。这种做法在英语里叫"tradeoff",用CPU处理压缩/解压缩时间的增加换来了I/O传输时间的减少,就像一场交易(trade)一样。

在Linux的内存回收机制中,"dirty"的pages(包括anonymous page和可读写的page cache)需要经过I/O传输,swap out/writeback同步到外部存储介质(磁盘/Flash),之后这部分内存才可以被回收。当这个被换出到外部存储介质的page再次被使用时,将触发page fault,此时再通过I/O传输,换入内存中。

图片来源于zhuhui@xiaomi.com

如果把内存数据经过压缩后再传递到外部存储介质,将节省swap out和swap in时所需的I/O传输量。支持这种机制的文件系统不在少数,比如可读可写的Btrfs,只读且压缩输入的大小固定(fixed-sized input)的Squashfs,以及华为最近推出的只读且压缩输出大小固定(fixed-sized output)的EROFS。虽然减少了I/O的传输量,但并没有完全避免I/O操作。

而如果把压缩后的pages直接放在内存里,发生page fault时再解压缩出来,那么从时间上,将只有CPU的压缩和解压缩的开销,而没有I/O传输的开销。当然代价就是,从空间上,需要占用一部分的内存资源。

压缩什么

Linux中内存压缩的实现方案并不唯一,有zcache, zswapzram等(可以统称zproject)。zcache最初设计为处理page cache,而zswap主要针对anonymous pages(由Seth Jennings设计,核心源码位于"/mm/zswap.c",于2013年9月进入内核3.11版本)。

至于zram(前身为compcache,经历了4年的staging后,于内核3.14版本进入mainline(2014年3月),目前是谷歌家Android系统(自4.4版本)和Chrome OS(自2013年)使用的默认方案。

压缩后的数据是不可以直接读写的,需要经过解压缩后才能访问,但相对来说,其访问速度依然较快,这样的一种特殊内存被称之为transcendent memory(简称tmem)。作为内存中的一部分区域,一段时间没有被操作的APP可以被压缩后放到tmem中,当用户再次操作这个应用时,再从tmem中恢复,这比从Flash中恢复要迅速,这也是我们可以快速打开一个APP的关键(保活度)。

Pages数量众多,选择哪些内存页面来压缩是个重要的问题。zproject的目标是那些近期不会使用,但以后的某个时刻可能会被访问到的pages,马上要用和永远不会再用的pages都不会出现在zproject的考虑范围内。

如何压缩

压缩算法

要压缩,自然需要一定的压缩算法,目前Linux/Android中使用较为广泛的是LZO(5.1内核加入了LZO-RLE)。同其他众多的压缩算法一样,其基本原理是也是压缩那些连续相同的字节,比如多个连续的0,如果一个page全是0(zero page),那么LZO可以将其压缩到28字节。

LZO并不追求最高的压缩率,而是主要强调一个“快”字。压缩率越高,越节省空间, 但CPU用于压缩和解压缩的耗时也越长,所以需要取得一个合理的平衡。LZO目前只支持固定大小的压缩输入,而不支持固定大小的压缩输出,因而前面提到的以固定大小的输出为特点的EROFS采用的是LZ4,一个LZO的衍生算法。

在拥有硬件加速引擎的系统中,可通过使用内核crypto子系统的API,利用硬件加速器来进行压缩和解压缩,以减轻CPU的负担。

压缩结果

内存压缩是以page为单位进行的,压缩后的page frame成为了一串字节,被称为"zpage",zpage的大小叫做"zsize",zsize除以PAGE_SIZE就是压缩率。压缩率大于0小于1,平均为0.5。

小于0.5的属于thin zpages,表明压缩效果比较理想;大于0.5的则属于fat zpages,表明压缩效果一般。Page本来都是大小固定,像砖墙一样排列的整整齐齐的,可一旦压缩,就变得七零八落,胖瘦不一了。

那发生page fault的时候,如何找到对应的被压缩的page?这就需要对zpage的信息进行记录,对此不同的内存压缩方案的处理方法不尽相同。zram的做法相对比较简单,它使用的是table,每个page对应table中的一个entry,而zswap是用red-black tree的结构组织的。

zsmalloc分配器

压缩后的zpage是要占据内存空间的,内核中分配内存最基本的接口是kmalloc(),但使用普通的kmalloc()为zpage分配空间的效果并不十分理想。为此,内核开发者为zpage定制了一些新的内存分配方案,其中最为常用的是"zsmalloc"(核心源码位于"/mm/zsmalloc.c")。

zsmalloc的底层实现同kmalloc类似,也是基于slab分配器。对应于slab的机制,zpage就是一个object,允许一个zpage跨越两个物理pages,比如一个zpage的大小是2/3个page frame,那么2个page frames就可以容纳3个zpages。

大小在同一范围(比如33-48字节)的zpage属于同一个size_class,同一class的多个zpage对象构成一个了slab,slab在zsmalloc中被实现为zspage结构体。

struct size_class {   
    int size;  /* object size */
    int objs_per_zspage;
    int pages_per_zspage; /* number of PAGE_SIZE sized pages */
    struct list_head fullness_list[NR_ZS_FULLNESS];
    ...
};

struct zspage {
    unsigned int inuse;
    unsigned int freeobj;
    struct page *first_page;
    struct list_head list; /* fullness list */
    ...
};

根据slab的规则,zspage所占据的page frame数目也必须是2的幂次方。同普通slab的分配类似,zspage根据使用状态被分为了ZS_FULL, ZS_ALMOST_FULL, ZS_ALMOST_EMPTY和ZS_EMPTY四个链表。

图片来源于zhuhui@xiaomi.com

实现差异

zswap/zcache

zswap/zcache在实现上借助的分别是内存管理子系统中frontswap/cleancache的API,作为frontswap/cleancache的backend,zswap/zcache可以截获swap out时,准备发往外部存储介质(HDD/SSD)的内存page,进行压缩和存储。

但zswap/zcache的内存区域总有耗尽的时候,因此要使用zswap或者zcache,系统中必须同时具有后备/外部存储器(backing store)。当zswap/zcache溢出时,其中的一部分压缩pages将以writeback的形式,转移到后备存储器中。

那哪些压缩pages会被转移出去呢?通常还是按照LRU(Least Recently Used)的原则,选择最近最少使用的。此外,像JPEG图像那种已经被压缩过的数据, 就很难再进一步压缩,即便压缩,形成的也是非常"fat"的zpage。对于这种pages,浪费CPU的资源去做压缩实在划不来,直接放入后备存储即可。

此时,zswap/zcache就相当于后备存储的一个cache,只不过同page cache不同,这里缓存的不是原始的page,而是压缩后的zpage。

发生page fault时,page fault的信息也会被zswap/zcache所截获,如果对应的pages在zswap/zcache中,就直接从zswap/zcache中取出并解压,否则经I/O传输,从后备存储中取出。

虽然可能还是会需要I/O传输,但至少在zswap/zcache中,I/O传输是被尽力避免和defer的。而且对于擦写次数有限的Flash,减少Flash的写操作也有利于延长Flash的使用寿命。

zram

与zswap/zcache不同,zram的方案默认是不需要后备/外部存储介质的(如果配置了"CONFIG_ZRAM_WRITEBACK"内核选项,zram也可以配合后备存储使用,参考这个patch),因此zram自身就作为一个swap设备存在,只是其对应的swap分区不在外部存储介质上,而是位于内存中。使用"/proc/swapino"命令查看可得到类似这样的信息:

作为swap分区,zram可以通过mkswap命令创建,swapon命令激活,而且其优先级高于其他的swap设备。也就是说,如果系统中存在zram,那么在页面swap out时,将优先选择zram作为目的地。

而且,为了和其他的swap设备保持形式上的统一,zram在Linux中也隶属于block子系统,但这里存在一个问题,block子系统本身是为固定大小的block设备设计的,而zram中管理的对象(即zpage)的大小是不固定的,所以需要进行一定的修改以适配zram的需要。

作为设备,自然离不开驱动程序,具体的实现位于"/drivers/block/zram"中。生成设备节点后,可通过"/sys/block/zram(n)"进行属性的配置(参考这篇文档),包括zram内存区域的大小、使用的压缩算法、压缩流的数目等。

在多核系统中,为了加快压缩速度,是由多个CPU一起进行的并行压缩,因而压缩流的数目也就等于CPU的数目,如果在运行过程中,某个CPU被关闭了,这个数目也会相应地减少。


参考:

LWN - The zswap compressed swap cache

LWN - In-kernel memory compression


原创文章,转载请注明出处。

发布于 04-01

文章被以下专栏收录