转自:http://kernel.meizu.com/zram-introduction.html
zram 技术的由来
zram1(也称为 zRAM,先前称为 compcache)是 Linux 内核的一项功能,可提供虚拟内存压缩。zram 通过在 RAM 内的压缩块设备上分页,直到必须使用硬盘上的交换空间,以避免在磁盘上进行分页,从而提高性能。由于 zram 可以用内存替代硬盘为系统提供交换空间的功能,zram 可以在需要交换 / 分页时让 Linux 更好利用 RAM ,在物理内存较少的旧电脑上尤其如此。
即使 RAM 的价格相对较低,zram 仍有利于嵌入式设备、上网本和其它相似的低端硬件设备。这些设备通常使用固态存储,它们由于其固有性质而寿命有限,因而避免以其提供交换空间可防止其迅速磨损。此外,使用 zRAM 还可显著降低 Linux 系统用于交换的 I/O 。
zram 在 2009 年的时候就进了 kernel 的 staging 目录,并于 2014 年 3 月 30 日发布的 3.14 版本正式合并入 Linux 内核主线。在 2014 年 6 月 8 日发布的 3.15 版本的 Linux 内核中,zram 已可支持 LZ4 压缩算法,而 LZO 仍然作为默认的压缩后端。内核 3.15 中的修改还改进了性能,以及经由 sysfs 切换压缩算法的能力。
Lubuntu 于 13.10 开始使用 zram 。截至 2012 年 12 月,Ubuntu 考虑为小内存的计算机默认启用 zram 。 Google 在 Chrome OS 中使用 zram,它也成为了 Android 4.4 及以后版本设备的一个选项。
本文主要介绍在 Android 设备上使用的 zram swap,它可以让小内存的设备在多任务的情况下切换自如,提高用户体验。
zram swap 主要原理就是从内存分配一块区域出来用作 swap 分区,每次如果内存空间不够了,不是把应用程序杀掉,而是把应用程序所占用的内存数据复制到 swap 分区,等切换回来的时候就可以直接把这部分数据恢复到内存当中,节省重新开启所需的时间。而被放到 swap 分区的应用程序,所占用的内存都是被压缩过的,比如,微信在普通内存中占用 50 MB 的空间,如果压缩率为 0.4,则放到 swap 分区里面的数据只需要 20 MB 的空间,这样 swap 分区里面就可以存放更多后台临时不用的应用程序,变相扩展了内存的大小。
zram 配置步骤
1. 内核配置2
3.15 之前版本的 kernel
Device Drivers -> Staging drivers (STAGING [=y])
3.15 及之后版本的 kernel
Device Drivers -> [*] Block devices -> Compressed RAM block device support
具体的配置项如下:
CONFIG_RESOURCE_COUNTERS=y
CONFIG_MEMCG=y
CONFIG_MEMCG_SWAP=y
CONFIG_MEMCG_SWAP_ENABLED=y
CONFIG_MEMCG_KMEM=y
CONFIG_ZRAM=y
CONFIG_TOI_ZRAM_SUPPORT=y
CONFIG_ZRAM_DEBUG=y
2. zram 块设备个数设定:
- 如果是将 zram 编译成模块,则可以使用下面命令动态加载,这个命令会创建 4 个设备 /dev/zram{0,1,2,3}
modprobe zram num_devices=4
- 如果是直接将 zram 编译到内核,那只能在代码里面直接修改 num_devices,3.15 之前的版本代码路径是 drivers/staging/zram/zram_drv.c,3.15 及之后的版本代码路径是 ./drivers/block/zram/zram_drv.c ,默认 zram 设备个数是一个。
3. 压缩流的最大个数设定
这个是 3.15 版本及以后的 kernel 新加入的功能,3.15 版本之前的 zram 压缩都是使用一个压缩流(缓存 buffer 和算法私有部分)实现,每个写(压缩)操作都会独享压缩流,但是单压缩流如果出现数据奔溃或者卡住的现象,所有的写(压缩)操作将一直处于等待状态,这样效率非常低;而多压缩流的架构会让写(压缩)操作可以并行去执行,大大提高了压缩的效率和稳定性。
- 查看压缩流的最大个数,默认是 1
cat /sys/block/zram0/max_comp_streams
- 设定压缩流的最大个数
echo 3 > /sys/block/zram0/max_comp_streams
4. 压缩算法选择
- 查看目前支持的压缩算法
cat /sys/block/zram0/comp_algorithm
lzo [lz4]
- 修改压缩算法
echo lzo > /sys/block/zram0/comp_algorithm
5. zram 内存大小设定
分配部分内存作为 zram ,大小建议为总内存的 10%-25% 。
- 可以使用数值直接设置内存大小,单位是 bytes
echo $((512*1024*1024)) > /sys/block/zram0/disksize
- 也可以使用带内存单位作为后缀的方式设置内存大小
echo 256K > /sys/block/zram0/disksize
echo 512M > /sys/block/zram0/disksize
echo 1G > /sys/block/zram0/disksize
6. 启用 zram 设备为 swap
mkswap /dev/zram0
swapon /dev/zram0
7. 具体的 zram 相关对外接口说明
Name | Access | Description |
---|---|---|
disksize | RW | 显示和设置该块设备的内存大小 |
initstate | RO | 显示设备的初始化状态 |
reset | WO | 重置设备 |
num_reads | RO | 读数据的个数 |
failed_reads | RO | 读数据失败的个数 |
num_write | RO | 写数据的个数 |
failed_writes | RO | 写数据失败的个数 |
invalid_io | RO | 非页面大小对齐的I/O请求的个数 |
max_comp_streams | RW | 最大可能同时执行压缩操作的个数 |
comp_algorithm | RW | 显示和设置压缩算法 |
notify_free | RO | 空闲内存的通知个数 |
zero_pages | RO | 写入该块设备的全为的页面的个数 |
orig_data_size | RO | 保存在该块设备中没有被压缩的数据的大小 |
compr_data_size | RO | 保存在该块设备中已被压缩的数据的大小 |
mem_used_total | RO | 分配给该块设备的总内存大小 |
mem_used_max | RW | 该块设备已用的内存大小,可以写 1 重置这个计数参数到当前真实的统计值 |
mem_limit | RW | zram 可以用来保存压缩数据的最大内存 |
pages_compacted | RO | 在压缩过程中可用的空闲页面的个数 |
compact | WO | 触发内存压缩 |
8. 系统运行之后的内存统计情况
cat /proc/meminfo
1 MemTotal: 1958596 kB
2 MemFree: 40364 kB
3 Buffers: 3472 kB
4 Cached: 328080 kB
5 SwapCached: 1908 kB
6 Active: 906752 kB
7 Inactive: 426648 kB
8 Active(anon): 752824 kB
9 Inactive(anon): 252756 kB
10 Active(file): 153928 kB
11 Inactive(file): 173892 kB
12 Unevictable: 2516 kB
13 Mlocked: 0 kB
14 SwapTotal: 524284 kB
15 SwapFree: 378320 kB
16 Dirty: 480 kB
17 Writeback: 0 kB
18 AnonPages: 1003452 kB
19 Mapped: 167052 kB
20 Shmem: 1184 kB
21 Slab: 83104 kB
22 SReclaimable: 24368 kB
23 SUnreclaim: 58736 kB
24 KernelStack: 48736 kB
25 PageTables: 41908 kB
26 NFS_Unstable: 0 kB
27 Bounce: 0 kB
28 WritebackTmp: 0 kB
29 CommitLimit: 1503580 kB
30 Committed_AS: 94718220 kB
31 VmallocTotal: 251658176 kB
32 VmallocUsed: 181352 kB
33 VmallocChunk: 251373156 kB
从 Line 14,15 可以看到 swap 相关的统计信息,SwapTotal 的大小就是 zram 设备的大小,当系统开启了一段时间之后,就会将后台的一些优先级低的应用数据(匿名页面)压缩存放到 swap 区,然后再重新打开这些应用的时候,再从 swap 区将它们的数据解压出来。在 Android KitKat 版本之前,Android 设备因为没有 zram,所以查看 /proc/meinfo 看到的 swap 分区的大小和统计数据都会是零。
1 Total RAM: 1958596 kB (status normal)
2 Free RAM: 724527 kB (504283 cached pss + 183244 cached kernel + 37000 free)
3 Used RAM: 1014008 kB (656204 used pss + 357804 kernel)
4 Lost RAM: 220061 kB
5 ZRAM: 27296 kB physical used for 145952 kB in swap (524284 kB total swap)
6 Tuning: 256 (large 512), oom 286720 kB, restore limit 95573 kB (high-end-gfx)
Line 5 也可以看到 swap 相关的统计信息,如果需要查看具体某个进程使用了多少 swap 空间,可以通过 dumpsys meminfo pid
(该进程的 id 号)查看。
zram 具体原理分析
zram 本质是就是一个块设备,所以下面先简单介绍一下块设备的一些基础知识。
1. 块设备基础概念
块设备(block device)
块设备是一种具有一定结构的随机存取设备,对这种设备的读写是按块进行的,使用缓冲区来存放暂时的数据,待条件成熟后,从缓存一次性写入设备或者从设备一次性读到缓冲区。
扇区 (Sectors)
块设备中最小的可寻址单元,大小一般都是 2 的整数倍,最常见的是 512 字节。
块 (Blocks)
块是文件系统的一种抽象,只能基于块来访问文件系统,块必须是扇区大小的 2 的整数倍,并且要小于页面的大小,所以通常块的大小是 512 字节、1 KB 或 4 KB 。
段 (Segments)
由若干个相邻的块组成,是 Linux 内存管理机制中一个内存页或者内存页的一部分。
页面 (Page)
物理页是 Linux 内存管理的基本单位,一般一个页面是 4KB 或者 64 KB。
2. 块设备驱动整体框架3
3. 相关数据结构
block_device
描述一个分区或整个磁盘对内核的一个块设备实例。
gendisk
描述一个通用硬盘(generic hard disk)对象。
hd_struct
描述分区应有的分区信息。
bio
描述块数据传送时怎样完成填充或读取块给 driver,既描述了磁盘的位置,又描述了内存的位置。
bio_vec
描述 bio 中的每个段。
request
描述向内核请求一个列表准备做队列处理。
request_queue
描述内核申请 request 资源建立请求链表并填写 bio 形成队列。
4. zram 架构
zram 从架构上可以分为三部分:
驱动部分
该部分创建了一个块设备,然后提供了处理 IO 请求的接口;
数据流操作部分
该部分主要提供串行或者并行的压缩和解压操作;
解压缩算法部分
该部分主要是一个个压缩和解压算法,每个算法都提供统一的压缩和解压接口给数据流操作部分调用。
5. zram 驱动部分代码分析
zram_init
首先调用 register_blkdev 注册块设备驱动到内核中,然后再根据 num_devices 调用 create_device 来创建相应个数的块设备, 这里默认是创建一个块设备。
create_device
对于 flash、 RAM 等完全随机访问的非机械设备,并不需要进行复杂的 I/O 调度,所以这里直接调用 blk_alloc_queue 分配一个 “请求队列”,然后使用 blk_queue_make_request 函数绑定分配好的 “请求队列” 和 “请求处理”函数 zram_make_request。接着初始化块设备的操作函数集 zram_devops 及设备容量、名字、队列等其他属性,最后调用 add_disk 将该块设备真正添加到内核中。
disksize_store
zram 使用了 Zsmalloc 分配器来管理它的内存空间,Zsmalloc 分配器尝试将多个相同大小的对象存放在组合页(称为 zspage)中,这个组合页不要求物理连续,从而提高内存的使用率。
首先会根据 zram 的内存中页面的个数,创建相应个数的 zram table,每个 zram table 都对应一个页面;然后会调用 zs_create_pool 创建一个 zsmalloc 的内存池,以后所有的页面申请和释放都是通过 zs_malloc 和 zs_free 来分配和释放相对应的对象。
zram_make_request
在整个块设备的 I/O 操作中,贯穿于始终的就是“请求”,块设备的 I/O 操作会排队和整合。块设备驱动的任务就是处理请求,对请求的排队和整合则是由 I/O 调度算法解决,因此,zram 块设备驱动的核心这个请求处理函数,所有的 zram I/O 请求都是通过这个请求处理函数来处理的。
首先它判断这个 I/O 请求是否是有效的,即检测请求是否在 zram 逻辑块的范围以内,且是否对齐。然后调用 __zram_make_request 遍历 bio 中的每个段 bio_vec,根据 bio 的传输方向选择执行写 (zram_bvec_write) 或者读 (zram_bvec_read) 操作。
zram_bvec_write
在写数据之前,首先使用 GFP_NOIO 标志创建一个不允许任何 I/O 初始化的页面,然后将 zram_data 对应的数据先解压出来放到该创建的页面中。接着去调用 zcomp_strm_find 找到一个压缩操作流,如果是单压缩流,则实际调用的是 zcomp_strm_single_find,如果是多压缩流,则实际调用的是 zcomp_strm_multi_find。
然后,将段 bio_vec 中的页面临时映射到高端地址,并将高端地址空间页面的内容复制到已保存好 zram_data 压缩后的数据的页面。调用 zs_malloc 申请一个 zram table,使 zcomp_compress 压缩内容并将压缩后的内容存放到新申请的 zram table。最后调用 zram_free_page 删除旧内容所占用的 zram table。
zcomp_decompress 会根据 struct zcomp_backend 初始化时设定的压缩算法来调用相应的解压接口,lzo 压缩算法的解压接口是 lzo_compress ,而 lz4 压缩算法的解压接口是 zcomp_lz4_compress ,该接口还调用了压缩操作流,以此执行串行或者并行写操作。
zram_bvec_read
读操作首先将段 bio_vec 中的页面临时映射到高端地址,然后再调用 zram_decompress_page 将 zram_meta 所对应的数据解压到这块映射的高端内存空间,解压的接口是 zcomp_decompress,它会根据 struct zcomp_backend 初始化时设定的压缩算法来调用相应的解压接口,lzo 压缩算法的解压接口是 lzo_decompress ,而 lz4 压缩算法的解压接口是 zcomp_lz4_decompress 。
6. 数据流操作部分代码分析
zcomp_create
若最大可能同时执行压缩操作的个数来调用为一,则调用 zcomp_strm_single_create 来创建一个压缩流,而若最大可能同时执行压缩操作的个数来调用大于一,则调用 zcomp_strm_multi_create 先创建一个压缩流,然后创建一个压缩流链表,并将创建好的压缩流加到压缩流链表中,后面再根据需求来动态创建更多的压缩流。
zcomp_strm_multi_find
单压缩流非常简单,如果前一个压缩操作已经持有 strm_lock 锁,那么下一个压缩操作必须等待前一个压缩操作调用 zcomp_strm_single_release 释放该锁才可以接着执行。
zcomp_strm_multi_find
多压缩流就相对复杂一点,只要压缩流的个数没有达到最大的个数,那么压缩操作都可以分配到一个压缩流,并会加到压缩流链表中,当压缩流的个数达到最大限制之后,那么下一个压缩操作只能睡眠等待链表中有空闲的压缩流出现。