0%

zram代码阅读

zram

斌斌陪你读之——zram

zram是啥

首先,zram是啥,zram是linux使用内存抽象出来的一个块设备。zram常用作交换设备使用,类似于一个ssd。zram会对写入其中的页进行压缩处理,从而起到节省内存同时提高程序启动速率的效果。对于没有交换设备或者磁盘读写次数有限的情况,zram可以起到比较好的加速效果。(zram wiki)

zram大体框架

如上图所示(不太会画图,权当没有图),zram向外部呈现一个block device的抽象,向外界提供块设备的接口;内部,由于zram使用的是物理内存,需要一个内存分配器,这里zram使用zsmalloc内存分配器;而zram则负责对用户向其写入的数据进行压缩处理,将压缩后的数据存储到zsmalloc分配的内存中。其实压缩这一部分也不是zram做的,压缩主要通过zcomp部分来进行,zram只是调用了zcomp部分的一些函数。感觉zram主要就是写了一个驱动,然后其他的功能大部分是用的linux中现成的代码做的。

在CSDN上找到了一篇关于zram的写的比较好的博客,可以先看看有一个较为宏观的了解:
zRAM内存压缩技术分析及优化方向

代码阅读

下面,对linux中zram部分的源码进行大致地阅读。内核版本为linux6.1

数据结构

zram结构体(zram类)

下方代码为zram类的实现,对其中的成员变量的含义进行解释。

  • table: 是一个zram_table_entry数组,数组的每一项对应了zram中的一页,存储了该页的元数据信息。
  • mem_pool: 前文提到zram使用zsmalloc进行内存的分配和管理。这个mem_pool就相当于zsmalloc给zram分配的一个内存池,zram可以向其读写数据。
  • comp: 这个是zram的压缩模块,zram对数据压缩和解压时通过comp的一些函数进行。
  • disk: linux内核中,用gendisk结构体表示一个磁盘设备或分区。其中存储了一些关于zram的信息,如设备号啥的,同时disk还指向了zram处理I/O的一些API,这个就相当于zram对外部提供的一个块设备抽象。一个参考blog
  • init_lock: 一个读写锁,就是可以有很多人同时读,但只能有一个人写的那种锁。
  • limit_pages: 这个就是zram这个块设备最多有几页,也是table数组的最大长度。
  • stats: statistics的缩写?这个变量存储了zram执行过程中的一些统计信息。
  • disksize: 整个zram磁盘有多大,字节为单位。
  • comperssor: 压缩算法的名字。
  • 之后的后边再写,别忘了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
struct zram {
struct zram_table_entry *table;
struct zs_pool *mem_pool;
struct zcomp *comp;
struct gendisk *disk;
/* Prevent concurrent execution of device init */
struct rw_semaphore init_lock;
/*
* the number of pages zram can consume for storing compressed data
*/
unsigned long limit_pages;

struct zram_stats stats;
/*
* This is the limit on amount of *uncompressed* worth of data
* we can store in a disk.
*/
u64 disksize; /* bytes */
char compressor[CRYPTO_MAX_ALG_NAME];
/*
* zram is claimed so open request will be failed
*/
bool claim; /* Protected by disk->open_mutex */
#ifdef CONFIG_ZRAM_WRITEBACK
struct file *backing_dev;
spinlock_t wb_limit_lock;
bool wb_limit_enable;
u64 bd_wb_limit;
struct block_device *bdev;
unsigned long *bitmap;
unsigned long nr_pages;
#endif
#ifdef CONFIG_ZRAM_MEMORY_TRACKING
struct dentry *debugfs_dir;
#endif
};

zram_table_entry结构体

描述了zram中一个页的元数据。

  • handle: 当前页在mem_pool中的句柄。注意,这个handle不指向具体的地址,而是在需要的时候调用一个函数,将handle对应的内容映射到一个地址处,数据访问完之后再撤销这个映射。
  • element: 如果当前页被相同的元素填充,element存储这个元素的值。此外,如果zram中的一个页被写回到了磁盘中,element会存储该页在磁盘中的句柄。
  • flags: zram为了节省内存空间,这个变量的低位存储当前页中存储的对象的大小,高位存储当前页的状态。
  • ac_time: 不知道干嘛的,看注释像是track计时用的。
1
2
3
4
5
6
7
8
9
10
struct zram_table_entry {
union {
unsigned long handle;
unsigned long element;
};
unsigned long flags;
#ifdef CONFIG_ZRAM_MEMORY_TRACKING
ktime_t ac_time;
#endif
};

zram_pageflags枚举

这个枚举就是上述zram_table_entry结构体中flags变量高位的含义,注释给的比较清楚了,这里不做过多说明。其中ZRAM_LOCK这一位比较有意思,这一位充当了一个自旋锁的作用,对该页加锁时将该位置1,解锁时将该位清零。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#define ZRAM_FLAG_SHIFT (PAGE_SHIFT + 1)

/* Flags for zram pages (table[page_no].flags) */
enum zram_pageflags {
/* zram slot is locked */
ZRAM_LOCK = ZRAM_FLAG_SHIFT,
ZRAM_SAME, /* Page consists the same element */
ZRAM_WB, /* page is stored on backing_device */
ZRAM_UNDER_WB, /* page is under writeback */
ZRAM_HUGE, /* Incompressible page */
ZRAM_IDLE, /* not accessed page since last idle marking */

__NR_ZRAM_PAGEFLAGS,
};

zram_stats结构体

这个结构体存储了zram运行过程中的一些统计信息,注释给的也比较清楚。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct zram_stats {
atomic64_t compr_data_size; /* compressed size of pages stored */
atomic64_t num_reads; /* failed + successful */
atomic64_t num_writes; /* --do-- */
atomic64_t failed_reads; /* can happen when memory is too low */
atomic64_t failed_writes; /* can happen when memory is too low */
atomic64_t invalid_io; /* non-page-aligned I/O requests */
atomic64_t notify_free; /* no. of swap slot free notifications */
atomic64_t same_pages; /* no. of same element filled pages */
atomic64_t huge_pages; /* no. of huge pages */
atomic64_t pages_stored; /* no. of pages currently stored */
atomic_long_t max_used_pages; /* no. of maximum pages stored */
atomic64_t writestall; /* no. of write slow paths */
atomic64_t miss_free; /* no. of missed free */
#ifdef CONFIG_ZRAM_WRITEBACK
atomic64_t bd_count; /* no. of pages in backing device */
atomic64_t bd_reads; /* no. of reads from backing device */
atomic64_t bd_writes; /* no. of writes from backing device */
#endif
};

一些宏

  • PAGE_SHIFT: 一个页几位,12位。
  • SECTOR_SHIFT: 一个扇区几位,linux中好像都是9,即一个扇区512字节。
  • SECTORS_PER_PAGE_SHIFT: 一页中的扇区号占几位,12-9=3,即一个页中有8个扇区。
  • SECTORS_PER_PAGE: 一个页中的扇区数。
  • ZRAM_LOGICAL_BLOCK_SHIFT: zram逻辑块占几位,这里是12,一页。
  • ZRAM_LOGICAL_BLOCK_SIZE: zram逻辑块大小,4K。
  • ZRAM_SECTOR_PER_LOGICAL_BLOCK: zram中每个逻辑块中有几个扇区,8个。

这里涉及到了扇区和块的一些定义,网上的解释挺多的。比如这个,请自行查阅。

1
2
3
4
5
6
#define SECTORS_PER_PAGE_SHIFT	(PAGE_SHIFT - SECTOR_SHIFT)
#define SECTORS_PER_PAGE (1 << SECTORS_PER_PAGE_SHIFT)
#define ZRAM_LOGICAL_BLOCK_SHIFT 12
#define ZRAM_LOGICAL_BLOCK_SIZE (1 << ZRAM_LOGICAL_BLOCK_SHIFT)
#define ZRAM_SECTOR_PER_LOGICAL_BLOCK \
(1 << (ZRAM_LOGICAL_BLOCK_SHIFT - SECTOR_SHIFT))

具体代码

看具体代码之前可以先看看linux驱动程序怎么编写的,以及一些关于设备I/O方面的知识。这里有一些参考网站:

设备初始化

zram_init

初始化函数为zram_init函数。函数首先调用cpuhp_setup_state_multi函数设置了一下cpu热插拔的回调函数(cpu热插拔我还没怎么看)。然后调用class_register注册一个zram_control_class类(这个类好像是用于文件系统相关的,还没怎么看)。然后调用register_blkdev注册了一个块设备,叫做”zram”。虽然注册好了,但是实际上管理zram的数据结构还都没咋建立起来,然后继续调用zram_add来分配和初始化实际的zram设备。如果上述过程中哪一步出错了,就会转去执行destory_devices函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
static int __init zram_init(void)
{
int ret;

// 这个是防止zram中的flag的高几位溢出
BUILD_BUG_ON(__NR_ZRAM_PAGEFLAGS > BITS_PER_LONG);

// 设置cpu热插拔回调函数
ret = cpuhp_setup_state_multi(CPUHP_ZCOMP_PREPARE, "block/zram:prepare",
zcomp_cpu_up_prepare, zcomp_cpu_dead);
if (ret < 0)
return ret;

// 注册一个类供上层文件系统和用户使用
ret = class_register(&zram_control_class);
if (ret) {
pr_err("Unable to register zram-control class\n");
cpuhp_remove_multi_state(CPUHP_ZCOMP_PREPARE);
return ret;
}

// 应该是调试相关的函数,估计作用不是很关键
zram_debugfs_create();
// 在内核中注册设备
zram_major = register_blkdev(0, "zram");
if (zram_major <= 0) {
pr_err("Unable to get major number\n");
class_unregister(&zram_control_class);
cpuhp_remove_multi_state(CPUHP_ZCOMP_PREPARE);
return -EBUSY;
}

// 这里num_devices是1
while (num_devices != 0) {
mutex_lock(&zram_index_mutex);
// 分配和初始化zram设备
ret = zram_add();
mutex_unlock(&zram_index_mutex);
if (ret < 0)
goto out_error;
num_devices--;
}

return 0;

out_error:
// 初始化出错了,以及模块结束的时候都会调用这个函数
destroy_devices();
return ret;
}
zcomp_cpu_*

接下来我们看看上述代码中设置的cpu热插拔的两个回调函数zcomp_cpu_up_prepare和zcomp_cpu_dead。我目前理解的是前一个函数是cpu上线时执行的函数,后一个函数是cpu下线时执行的函数。这俩逻辑都比较简单,直接给注释了。注意其中的压缩流,压缩流需要一个压缩缓冲区和一个执行压缩的实体,创建压缩流和释放压缩流其实就是操作这两个东西。这个压缩流就是用来压缩和解压页面的东西。这里有个问题,就是为啥要从一个链表中取comp嘞,这个链表节点啥时候加进去的呢?可以看看zcomp_init函数,比较简单,这里略过。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
int zcomp_cpu_up_prepare(unsigned int cpu, struct hlist_node *node)
{
// 获得zcomp结构体
struct zcomp *comp = hlist_entry(node, struct zcomp, node);
struct zcomp_strm *zstrm;
int ret;

// ?这里不需要判断zstrm是否为NULL吗
zstrm = per_cpu_ptr(comp->stream, cpu);
// 初始化一个锁,这个锁用于保护zstream中的数据结构
local_lock_init(&zstrm->lock);

// 初始化压缩流,就是分配一个压缩算法和两个页大小的压缩缓冲区(因为可能压缩之后内容反而超过一页,这里分配两页大小)
ret = zcomp_strm_init(zstrm, comp);
if (ret)
pr_err("Can't allocate a compression stream\n");
return ret;
}

int zcomp_cpu_dead(unsigned int cpu, struct hlist_node *node)
{
struct zcomp *comp = hlist_entry(node, struct zcomp, node);
struct zcomp_strm *zstrm;

zstrm = per_cpu_ptr(comp->stream, cpu);
// 把压缩流的数据结构都释放了,但是这里没有把comp->stream设置成NULL可能就是上边不用判断zstrm是否为NULL的原因,因为一直都不是NULL,我们只是对zstrm里面的数据结构进行创建和释放
zcomp_strm_free(zstrm);
return 0;
}
zram_add

接下来看添加zram设备的zram_add函数,这个函数主要做的就是创建zram、gendisk结构体,然后对这些结构体中的参数进行初始化。代码首先创建了一个zram结构体,然后调用idr_alloc函数将zram结构体和一个整数关联起来,这个整数就作为该zram设备的设备号。之后初始化了一下zram中的锁。然后创建gendisk结构体,这个结构体就相当于一个磁盘类的对象,里面存储了磁盘的信息以及磁盘的操作。gendisk的major为主设备号,first_minor为第一个次设备号,minors为分区个数,可以看到这里minors为1,同时将flag设为GENHD_FL_NO_PART表示不支持分区,所以一个zram应该只有一个分区。然后比较重要的是disk->fops变量,这个fops里含有这个zram磁盘对外的操作接口。它是一个block_device_operations类型的指针,其中zram_devops是文件中定义好的一个block_device_operations结构体,里面是一些zram处理IO的函数。接下来我们还需要设置disk的请求队列的一些参数,对应的就是那一堆blk_queue*函数,每个函数具体含义网上也比较多。初始化完数据结构之后,最后调用device_add_disk函数把zram->disk注册到通用块层。然后差不多就结束了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
static int zram_add(void)
{
struct zram *zram;
int ret, device_id;

zram = kzalloc(sizeof(struct zram), GFP_KERNEL);
if (!zram)
return -ENOMEM;

// 将zram和一个整数device_id关联
ret = idr_alloc(&zram_index_idr, zram, 0, 0, GFP_KERNEL);
if (ret < 0)
goto out_free_dev;
device_id = ret;

init_rwsem(&zram->init_lock);
#ifdef CONFIG_ZRAM_WRITEBACK
spin_lock_init(&zram->wb_limit_lock);
#endif

/* gendisk structure */
zram->disk = blk_alloc_disk(NUMA_NO_NODE);
if (!zram->disk) {
pr_err("Error allocating disk structure for device %d\n",
device_id);
ret = -ENOMEM;
goto out_free_idr;
}

zram->disk->major = zram_major;
zram->disk->first_minor = device_id;
zram->disk->minors = 1;
zram->disk->flags |= GENHD_FL_NO_PART;
zram->disk->fops = &zram_devops;
zram->disk->private_data = zram;
snprintf(zram->disk->disk_name, 16, "zram%d", device_id);

/* Actual capacity set using syfs (/sys/block/zram<id>/disksize */
set_capacity(zram->disk, 0);
/* zram devices sort of resembles non-rotational disks */
// 这两句表示zram像是一个ssd
blk_queue_flag_set(QUEUE_FLAG_NONROT, zram->disk->queue);
blk_queue_flag_clear(QUEUE_FLAG_ADD_RANDOM, zram->disk->queue);

/*
* To ensure that we always get PAGE_SIZE aligned
* and n*PAGE_SIZED sized I/O requests.
*/
blk_queue_physical_block_size(zram->disk->queue, PAGE_SIZE);
blk_queue_logical_block_size(zram->disk->queue,
ZRAM_LOGICAL_BLOCK_SIZE);
blk_queue_io_min(zram->disk->queue, PAGE_SIZE);
blk_queue_io_opt(zram->disk->queue, PAGE_SIZE);
zram->disk->queue->limits.discard_granularity = PAGE_SIZE;
blk_queue_max_discard_sectors(zram->disk->queue, UINT_MAX);

/*
* zram_bio_discard() will clear all logical blocks if logical block
* size is identical with physical block size(PAGE_SIZE). But if it is
* different, we will skip discarding some parts of logical blocks in
* the part of the request range which isn't aligned to physical block
* size. So we can't ensure that all discarded logical blocks are
* zeroed.
*/
if (ZRAM_LOGICAL_BLOCK_SIZE == PAGE_SIZE)
blk_queue_max_write_zeroes_sectors(zram->disk->queue, UINT_MAX);

blk_queue_flag_set(QUEUE_FLAG_STABLE_WRITES, zram->disk->queue);
ret = device_add_disk(NULL, zram->disk, zram_disk_groups);
if (ret)
goto out_cleanup_disk;

strscpy(zram->compressor, default_compressor, sizeof(zram->compressor));

zram_debugfs_register(zram);
pr_info("Added device: %s\n", zram->disk->disk_name);
return device_id;

out_cleanup_disk:
put_disk(zram->disk);
out_free_idr:
idr_remove(&zram_index_idr, device_id);
out_free_dev:
kfree(zram);
return ret;
}
disksize_store

前几部分相当于在内核中添加了一个zram设备驱动,最终通过zram_add函数完成了zram设备的添加,但是,在阅读上述代码的时候,我们发现,zram结构体中的一些变量如table、mem_pool、comp好像都还没有被初始化。创建zram驱动后,还需要对zram设备的大小进行配置,对应disksize_store函数。这个函数的逻辑也比较简单,首先调用memparse函数对buf进行解析,得到zram的实际大小。之后调用zram_meta_alloc函数创建zram中的table和mem_pool。然后调用zcomp_create函数创建一个压缩对象用于zram执行压缩操作。最后再给对应的变量赋一下值就行了。其中调用的zram_meta_alloc和zcomp_create都比较简单,这里先略过了,如有必要,之后会补充。这里有个问题,就是为啥要从dev里找zram呢?建议看看这个:块设备剖析之关键数据结构分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
static ssize_t disksize_store(struct device *dev,
struct device_attribute *attr, const char *buf, size_t len)
{
u64 disksize;
struct zcomp *comp;
struct zram *zram = dev_to_zram(dev);
int err;

disksize = memparse(buf, NULL);
if (!disksize)
return -EINVAL;

down_write(&zram->init_lock);
if (init_done(zram)) {
pr_info("Cannot change disksize for initialized device\n");
err = -EBUSY;
goto out_unlock;
}

disksize = PAGE_ALIGN(disksize);
if (!zram_meta_alloc(zram, disksize)) {
err = -ENOMEM;
goto out_unlock;
}

comp = zcomp_create(zram->compressor);
if (IS_ERR(comp)) {
pr_err("Cannot initialise %s compressing backend\n",
zram->compressor);
err = PTR_ERR(comp);
goto out_free_meta;
}

zram->comp = comp;
zram->disksize = disksize;
set_capacity_and_notify(zram->disk, zram->disksize >> SECTOR_SHIFT);
up_write(&zram->init_lock);

return len;

out_free_meta:
zram_meta_free(zram, disksize);
out_unlock:
up_write(&zram->init_lock);
return err;
}

zram I/O处理

看这一部分的时候建议先大概搞清楚bio这个结构体,因为内核和zram设备之间主要依靠这个bio结构体来进行IO操作。比较推荐的是这个blog:通用块层 struct bio 详解。也建议看看啥通用块层和request_queue之类的,虽然我搞得也不太懂。大概就是通用块层是对底层设备的一个抽象层,应该就是用gendisk结构体来抽象代表具体的块设备?然后request_queue就是一个I/O请求队列,每个gendisk都有一个这个队列,os可以向这个队列里push I/O请求,队列里的I/O请求会经过合并(比如把对相邻扇区的读写操作放在一起之类的),然后交给设备驱动处理。

zram_submit_bio

zram_submit_bio这个函数用于处理所有的zram的I/O请求,好像比较早的linux版本这个函数叫zram_make_request。如下所示,函数首先调用valid_io_request函数来判断当前的I/O请求是否是一个合法的I/O请求,主要就是判断一下扇区号有没有超过zram的界限之类的。如果合法的话,就会调用__zram_make_request函数来正式处理当前的这个I/O请求。

1
2
3
4
5
6
7
8
9
10
11
12
13
static void zram_submit_bio(struct bio *bio)
{
struct zram *zram = bio->bi_bdev->bd_disk->private_data;

if (!valid_io_request(zram, bio->bi_iter.bi_sector,
bio->bi_iter.bi_size)) {
atomic64_inc(&zram->stats.invalid_io);
bio_io_error(bio);
return;
}

__zram_make_request(zram, bio);
}
valid_io_request

我们先来看一下valid_io_request函数吧。首先判断起始扇区是否对齐到每个逻辑块中的扇区数以及I/O传输的字节数是否对齐到逻辑块大小,注意这里的if判断中的unlikely,unlikely主要是用于提示编译器这个if判断结果很有可能是假,从而让编译器调整代码的位置以使代码运行的更高效,除此之外unlikely对表达式的具体结果没有影响,我们可以将unlikely忽略。然后判断一下I/O操作是否越界即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/* 判断io是否有效 */
static inline bool valid_io_request(struct zram *zram,
sector_t start, unsigned int size)
{
u64 end, bound;

/* unaligned request */
// start应该就是I/O操作的起始扇区号,然后这里扇区号要对齐到每个块中的扇区个数
if (unlikely(start & (ZRAM_SECTOR_PER_LOGICAL_BLOCK - 1)))
return false;
// size是I/O字节数,size要按照块大小对齐
if (unlikely(size & (ZRAM_LOGICAL_BLOCK_SIZE - 1)))
return false;

// I/O操作终止扇区号
end = start + (size >> SECTOR_SHIFT);
bound = zram->disksize >> SECTOR_SHIFT;
// 不能越界,即超过zram存储的范围
if (unlikely(start >= bound || end > bound || start > end))
return false;

/* I/O request is valid */
return true;
}
__zram_make_request

如果valid_io_request判断I/O合法,驱动就会执行__zram_make_request函数来对I/O进行具体的处理。

接下来我们来具体看一下__zram_make_request的具体实现。首先index和offset用于定位zram磁盘中的位置,之前也讲到zram用一个table来描述每一页的元数据,这里index就是用于索引页的页号,而offset就是页内偏移。

考虑对I/O操作的处理方式,这里一共有三种可能的I/O操作,分别是丢弃、读和写。函数首先判断是否为丢弃discard I/O操作,如果是则调用zram_bio_discard函数进行处理。关于discard的概念可以看看这一篇blog:REQ_OP_DISCARD。这里注意,对于discard类型的bio,没有也不需要bio_vec结构体。

如果I/O操作不是discard操作,则代码会遍历bio中的每一个bio_vec,根据bio_vec中描述的内存中的位置和长度以及index和offset索引的zram中的位置来不断进行I/O的读或写操作,对应zram_bvec_rw函数的调用,然后更新index和offset。关于bio_vec中具体字段的值,还请看我一开始提到的博客:通用块层 struct bio 详解。下面给出代码注释:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
static void __zram_make_request(struct zram *zram, struct bio *bio)
{
// 索引zram的页
int offset;
// zram页内偏移
u32 index;
// 这俩用于遍历bio中的所有的bio_vec结构体
struct bio_vec bvec;
struct bvec_iter iter;
// 这个应该是用来统计I/O信息的,不管他
unsigned long start_time;

// 得到zram中的页号和页内偏移
index = bio->bi_iter.bi_sector >> SECTORS_PER_PAGE_SHIFT;
offset = (bio->bi_iter.bi_sector &
(SECTORS_PER_PAGE - 1)) << SECTOR_SHIFT;

// 若bio为discard操作,调用zram_bio_discard函数进行页的clear操作
switch (bio_op(bio)) {
case REQ_OP_DISCARD:
case REQ_OP_WRITE_ZEROES:
zram_bio_discard(zram, index, offset, bio);
bio_endio(bio);
return;
default:
break;
}

// 开始计时?不管他
start_time = bio_start_io_acct(bio);
// 这是一个宏,作用就是循环遍历bio中的bio_vec
bio_for_each_segment(bvec, bio, iter) {
struct bio_vec bv = bvec;
// 得到当前的bio_vec中未处理的字节数
unsigned int unwritten = bvec.bv_len;
// 循环处理当前bio_vec,这里主要是如果一个大的bio_vec跨越了多个zram页,
// 我们就把它给分成对多个页的小的bio_vec来处理
do {
// 得到当前小的bio_vec的长度
bv.bv_len = min_t(unsigned int, PAGE_SIZE - offset,
unwritten);
// 调用zram_bvec_rw对当前bio_vec进行处理
if (zram_bvec_rw(zram, &bv, index, offset,
bio_op(bio), bio) < 0) {
bio->bi_status = BLK_STS_IOERR;
break;
}

// 已经处理完了一部分bio_vec,需要更改下一个bio_vec的偏移
// 这里为啥不考虑bv_offset溢出回0嘞,就是模一个page_size?
// 我的考虑是bv_bio表示一个段,段的大小小于页的大小,所以不用考虑溢出
bv.bv_offset += bv.bv_len;
// 未处理字节减少
unwritten -= bv.bv_len;

// 由于已经处理了bv_len的字节,这里要更新index和offset,把它们后移
update_position(&index, &offset, &bv);
} while (unwritten);
}
bio_end_io_acct(bio, start_time);
bio_endio(bio);
}
zram_bio_discard

接下来我们先看zram_bio_discard这个函数,这里比较有意思的一个地方是如果offset不是0,即要清空的内容的起始地址在一个zram页的中间,按理来说应该先把当前这个zram页从mem_pool中取出来,然后把相应部分清零,之后再把这个页压缩并存回mem_pool中。但是这里对于这种情况zram选择直接skip掉这一部分空间的清空,而是从下一页的开头开始清空。清除操作也比较简单,遍历每一个待清除的页,先加锁,然后调用zram_free_page,最后解锁即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
static void zram_bio_discard(struct zram *zram, u32 index,
int offset, struct bio *bio)
{
size_t n = bio->bi_iter.bi_size;

/*
* zram manages data in physical block size units. Because logical block
* size isn't identical with physical block size on some arch, we
* could get a discard request pointing to a specific offset within a
* certain physical block. Although we can handle this request by
* reading that physiclal block and decompressing and partially zeroing
* and re-compressing and then re-storing it, this isn't reasonable
* because our intent with a discard request is to save memory. So
* skipping this logical block is appropriate here.
*/
if (offset) {
if (n <= (PAGE_SIZE - offset))
return;

n -= (PAGE_SIZE - offset);
index++;
}

while (n >= PAGE_SIZE) {
zram_slot_lock(zram, index);
zram_free_page(zram, index);
zram_slot_unlock(zram, index);
atomic64_inc(&zram->stats.notify_free);
index++;
n -= PAGE_SIZE;
}
}
zram_free_page

zram_free_page函数如下,根据zram结构体中的flag变量来判断当前页的类型,并清除flag中相应的位。这里,如果该页的ZRAM_WB置位,表示当前页被写回到一个磁盘中,然后调用free_block_bdev函数来释放磁盘中的页,但是linux 6.1版本中的free_block_bdev函数是个空函数,也许是不支持zram写回到磁盘?此外,对于ZRAM_SAME置位的页,该页的内容可以从table表中直接获得,不需要再存储到mem_pool中。在清除对应位之后,从table表中获得对应页在mem_pool中的handle,然后调用zs_free函数从mem_pool中释放该页即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
static void zram_free_page(struct zram *zram, size_t index)
{
unsigned long handle;

#ifdef CONFIG_ZRAM_MEMORY_TRACKING
zram->table[index].ac_time = 0;
#endif
if (zram_test_flag(zram, index, ZRAM_IDLE))
zram_clear_flag(zram, index, ZRAM_IDLE);

if (zram_test_flag(zram, index, ZRAM_HUGE)) {
zram_clear_flag(zram, index, ZRAM_HUGE);
atomic64_dec(&zram->stats.huge_pages);
}

if (zram_test_flag(zram, index, ZRAM_WB)) {
zram_clear_flag(zram, index, ZRAM_WB);
free_block_bdev(zram, zram_get_element(zram, index));
goto out;
}

/*
* No memory is allocated for same element filled pages.
* Simply clear same page flag.
*/
if (zram_test_flag(zram, index, ZRAM_SAME)) {
zram_clear_flag(zram, index, ZRAM_SAME);
atomic64_dec(&zram->stats.same_pages);
goto out;
}

// 这个handle用于查找mem_pool中的对象
handle = zram_get_handle(zram, index);
if (!handle)
return;

zs_free(zram->mem_pool, handle);

atomic64_sub(zram_get_obj_size(zram, index),
&zram->stats.compr_data_size);
out:
atomic64_dec(&zram->stats.pages_stored);
zram_set_handle(zram, index, 0);
zram_set_obj_size(zram, index, 0);
WARN_ON_ONCE(zram->table[index].flags &
~(1UL << ZRAM_LOCK | 1UL << ZRAM_UNDER_WB));
}
zram_bvec_rw

看完discard操作后,我们继续阅读zram I/O读写操作的代码,前边提到__zram_make_request函数对每一个小的bio_vec调用zram_bvec_rw函数执行I/O读写操作。zram_bvec_rw代码如下,可以看到,这个函数只是根据bio的操作类型来调用zram_bvec_read和zram_bvec_write函数。在I/O读操作后,调用了flush_dcache_page函数将读取出的页从dcache写回到内存中。在执行完读写操作后,调用zram_accessed函数将zram中被访问到的页的ZRAM_IDLE位清零。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
static int zram_bvec_rw(struct zram *zram, struct bio_vec *bvec, u32 index,
int offset, enum req_op op, struct bio *bio)
{
int ret;

if (!op_is_write(op)) {
atomic64_inc(&zram->stats.num_reads);
ret = zram_bvec_read(zram, bvec, index, offset, bio);
flush_dcache_page(bvec->bv_page);
} else {
atomic64_inc(&zram->stats.num_writes);
ret = zram_bvec_write(zram, bvec, index, offset, bio);
}

zram_slot_lock(zram, index);
zram_accessed(zram, index);
zram_slot_unlock(zram, index);

if (unlikely(ret < 0)) {
if (!op_is_write(op))
atomic64_inc(&zram->stats.failed_reads);
else
atomic64_inc(&zram->stats.failed_writes);
}

return ret;
}
zram_bvec_read

zram_bvec_read函数逻辑也较为简单,首先判断当前的bio_vec是否是对一整个页的读取,如果不是,则新申请一个临时页,先调用__zram_bvec_read函数将zram中一整个页读取到该临时页,再将临时页中bio_vec需要的一部分复制到bio_vec指示的相应位置处。

如果是对一整个页的读取,则直接调用__zram_bvec_read函数将zram中的页读取到bio_vec对应的内存页中即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
static int zram_bvec_read(struct zram *zram, struct bio_vec *bvec,
u32 index, int offset, struct bio *bio)
{
int ret;
struct page *page;

page = bvec->bv_page;
if (is_partial_io(bvec)) {
/* Use a temporary buffer to decompress the page */
page = alloc_page(GFP_NOIO|__GFP_HIGHMEM);
if (!page)
return -ENOMEM;
}

// 从zram中读取一页到page中
ret = __zram_bvec_read(zram, page, index, bio, is_partial_io(bvec));
if (unlikely(ret))
goto out;

if (is_partial_io(bvec)) {
// 注意这个kmap_atomic函数,作用为将一个物理页映射到内核高地址空间,
// 然后内核就可以访问该物理页了
void *src = kmap_atomic(page);

// 将临时页的内容移动到目标内存页中的目标位置
memcpy_to_bvec(bvec, src + offset);
kunmap_atomic(src);
}
out:
if (is_partial_io(bvec))
__free_page(page);

return ret;
}
__zram_bvec_read

I/O读操作的最后一个函数(读操作我想写的最后一个函数,代码太多了,先偷个懒),__zram_bvec_read首先要判断要zram中存储的页是否被写回到了磁盘中,如果被写回到了磁盘中,则zram调用read_from_bdev函数直接从磁盘将页读取到内存中的目标地址处。这里read_from_bdev函数的一个参数是zram_get_element(zram, index),这个参数就是table[index].element,代表了磁盘页的句柄。

如果没有被写回到磁盘,zram则判断该页的ZRAM_SAME位是否被置位,如果置位,表示这一页被相同的element填充,则zram可以很简单的从table[index].element获得页的填充内容element,使用这个element填充满一页即可。

最后一种情况,要读取的页存储在mem_pool中,则首先获得该页存储在mem_pool中的对象的大小,如果对象大小小于一页,表示该页被压缩过了,如果对象大小等于一页,则表示该页没有被压缩。首先调用zs_map_object函数把handle映射为一个src指针,这个指针就指向mem_pool中的一块区域,可以通过读这个指针来实现读mem_pool中的内容。然后根据上述对象大小的判断,如果大小为一页,直接将src指针处的内容copy到目标内存页即可;如果大小小于一页,则要先调用zcomp_decompress把src处的内容解压缩到目标内存页中。最后调用zs_unmap_object将handle和src指针的映射撤销掉即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
static int __zram_bvec_read(struct zram *zram, struct page *page, u32 index,
struct bio *bio, bool partial_io)
{
struct zcomp_strm *zstrm;
unsigned long handle;
unsigned int size;
void *src, *dst;
int ret;

zram_slot_lock(zram, index);
if (zram_test_flag(zram, index, ZRAM_WB)) {
struct bio_vec bvec;

zram_slot_unlock(zram, index);
/* A null bio means rw_page was used, we must fallback to bio */
if (!bio)
return -EOPNOTSUPP;

bvec.bv_page = page;
bvec.bv_len = PAGE_SIZE;
bvec.bv_offset = 0;
return read_from_bdev(zram, &bvec,
zram_get_element(zram, index),
bio, partial_io);
}

handle = zram_get_handle(zram, index);
if (!handle || zram_test_flag(zram, index, ZRAM_SAME)) {
unsigned long value;
void *mem;

value = handle ? zram_get_element(zram, index) : 0;
mem = kmap_atomic(page);
zram_fill_page(mem, PAGE_SIZE, value);
kunmap_atomic(mem);
zram_slot_unlock(zram, index);
return 0;
}

size = zram_get_obj_size(zram, index);

if (size != PAGE_SIZE)
zstrm = zcomp_stream_get(zram->comp);

src = zs_map_object(zram->mem_pool, handle, ZS_MM_RO);
if (size == PAGE_SIZE) {
dst = kmap_atomic(page);
memcpy(dst, src, PAGE_SIZE);
kunmap_atomic(dst);
ret = 0;
} else {
dst = kmap_atomic(page);
ret = zcomp_decompress(zstrm, src, size, dst);
kunmap_atomic(dst);
zcomp_stream_put(zram->comp);
}
zs_unmap_object(zram->mem_pool, handle);
zram_slot_unlock(zram, index);

/* Should NEVER happen. Return bio error if it does. */
if (WARN_ON(ret))
pr_err("Decompression failed! err=%d, page=%u\n", ret, index);

return ret;
}