内核的文件预取逻辑及blockdev的相关配置
一、背景
在之前的博客 文件页的预映射逻辑 里,我们讲到了缺页异常的内核预映射的逻辑,要注意预映射和预取是不一样的。预映射操作的是物理内存和虚拟内存的映射关系,而预取操作的是磁盘空间到内存。如何来理解这两句话呢?虽然从CPU角度来看,软件上所有需要用到的数据,如果需要给CPU使用的话,都是需要有对应内存的,但是需要有物理内存和虚拟内存的这个区分,对于用户态而言,我们代码进行读写的都是虚拟内存,其对应的物理内存在真正使用的时候是需要进行对应的,假设应用程序A在读取一个磁盘上的文件的头一个PAGE时,它想用的是第一个PAGE,但是内核为了性能是会进行预取的,也就是说,它会往后多读这个文件一定大小。为了在你真正在使用后面的PAGE时可以进行快速的映射,也就是完成缺页异常。这里面就涉及两种虚拟地址空间,一种是给用户态使用,一种是给内核使用,给内核使用的就是内核虚拟地址空间。对于文件页所进行内核层面的page cache的缓存而言,只需要有内核虚拟地址空间就足够了,因为CPU层面可以用内核的虚拟地址空间的页表来去管理和维护page cache。而用户态在触发缺页异常后,内核的预映射逻辑就是把这些内核管理的page cache内容给映射给用户态,也就是当前正在使用该page cache的进程,也就是用户地址空间。
在之前的 内存管理相关——malloc,mmap,mlock与unevictable列表 博客里的 3.2.3 一节里我们就讲过用blockdev命令来进行读取和设置文件页的预取,我们在下面第二章里,先用strace来去抓在使用blockdev命令时实际调用了什么ioctl,在第三章里,我们讲述相关的原理细节。
二、使用strace来分析blockdev的读取和设置预取的操作
2.1 blockdev读取和设置预取的命令
读取/dev/sda的预取数值的命令:
blockdev --getra /dev/sda
一般默认的这个数值都是256,后面在 3.1 里会介绍这个256是个什么单位。
设置/dev/sda的预取数值的命令,如下面设置成2048:
blockdev --setra 2048 /dev/sda
2.2 通过strace来抓取blockdev读取预取数值时ioctl的情况
下面的命令就是通过strace来抓取blockdev读取预取数值时进行了哪些系统调用的操作
strace -o testblockdev.txt blockdev --getra /dev/sda
抓到的testblockdev.txt里内容如下:
execve("/usr/sbin/blockdev", ["blockdev", "--getra", "/dev/sda"], 0x7ffcb227b3e0 /* 30 vars */) = 0
brk(NULL) = 0x623a8d000000
arch_prctl(0x3001 /* ARCH_??? */, 0x7fffe1073e30) = -1 EINVAL (无效的参数)
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x728728e53000
access("/etc/ld.so.preload", R_OK) = -1 ENOENT (没有那个文件或目录)
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
newfstatat(3, "", {st_mode=S_IFREG|0644, st_size=76932, ...}, AT_EMPTY_PATH) = 0
mmap(NULL, 76932, PROT_READ, MAP_PRIVATE, 3, 0) = 0x728728e40000
close(3) = 0
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0P\237\2\0\0\0\0\0"..., 832) = 832
pread64(3, "\6\0\0\0\4\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0"..., 784, 64) = 784
pread64(3, "\4\0\0\0 \0\0\0\5\0\0\0GNU\0\2\0\0\300\4\0\0\0\3\0\0\0\0\0\0\0"..., 48, 848) = 48
pread64(3, "\4\0\0\0\24\0\0\0\3\0\0\0GNU\0O{\f\225\\=\201\327\312\301P\32$\230\266\235"..., 68, 896) = 68
newfstatat(3, "", {st_mode=S_IFREG|0755, st_size=2220400, ...}, AT_EMPTY_PATH) = 0
pread64(3, "\6\0\0\0\4\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0"..., 784, 64) = 784
mmap(NULL, 2264656, PROT_READ, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x728728c00000
mprotect(0x728728c28000, 2023424, PROT_NONE) = 0
mmap(0x728728c28000, 1658880, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x28000) = 0x728728c28000
mmap(0x728728dbd000, 360448, PROT_READ, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1bd000) = 0x728728dbd000
mmap(0x728728e16000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x215000) = 0x728728e16000
mmap(0x728728e1c000, 52816, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x728728e1c000
close(3) = 0
mmap(NULL, 12288, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x728728e3d000
arch_prctl(ARCH_SET_FS, 0x728728e3d740) = 0
set_tid_address(0x728728e3da10) = 9502
set_robust_list(0x728728e3da20, 24) = 0
rseq(0x728728e3e0e0, 0x20, 0, 0x53053053) = 0
mprotect(0x728728e16000, 16384, PROT_READ) = 0
mprotect(0x623a8b936000, 4096, PROT_READ) = 0
mprotect(0x728728e8d000, 8192, PROT_READ) = 0
prlimit64(0, RLIMIT_STACK, NULL, {rlim_cur=8192*1024, rlim_max=RLIM64_INFINITY}) = 0
munmap(0x728728e40000, 76932) = 0
getrandom("\x6b\x70\xa1\x81\x12\x48\x9e\x82", 8, GRND_NONBLOCK) = 8
brk(NULL) = 0x623a8d000000
brk(0x623a8d021000) = 0x623a8d021000
openat(AT_FDCWD, "/usr/lib/locale/locale-archive", O_RDONLY|O_CLOEXEC) = 3
newfstatat(3, "", {st_mode=S_IFREG|0644, st_size=8876560, ...}, AT_EMPTY_PATH) = 0
mmap(NULL, 8876560, PROT_READ, MAP_PRIVATE, 3, 0) = 0x728728200000
close(3) = 0
openat(AT_FDCWD, "/dev/sda", O_RDONLY) = 3
ioctl(3, BLKRAGET, [256]) = 0
newfstatat(1, "", {st_mode=S_IFCHR|0620, st_rdev=makedev(0x88, 0), ...}, AT_EMPTY_PATH) = 0
write(1, "256\n", 4) = 4
close(3) = 0
dup(1) = 3
close(3) = 0
dup(2) = 3
close(3) = 0
exit_group(0) = ?
下图是我们看抓到的testblockdev.txt产出物里的有关blockdev的--getra操作对应的ioctl:

可以从上图里看到,关键的ioctl是/dev/sda节点的BLKRAGET的操作,我们在内核代码里找到是在block/ioctl.c里:

可以从上图看到BLKRAGET和BLKFRAGET在执行时是一样的效果,虽然从include/uapi/linux/fs.h下面的定义来看,它们一个对应的是block device的read ahead,一个对应的是filesystem的read ahead:

其实在set时,它们也是一样的效果:

三、内核文件预取的默认值和相关逻辑细节
3.1 内核文件预取的默认值和单位
我们来看一下BLKRAGET在ioctl时的相关逻辑:

可以从上图看到,它是通过bdev->bd_disk->bdi->ra_pages的数值乘上PAGE_SIZE再除以一般用的扇区大小512字节。
我们看一下这个ra_pages是在哪里赋值的:

可以从上图里看到,ra_pages是在bdi_alloc时赋值了一个默认的初值,VM_READAHEAD_PAGES内核里定义的是128K:
![]()
所以一般的getra得到的就是128K/PAGE_SIZE*PAGE_SIZE/512也就是256。
但是并不是所有的系统上的该数值都是256的,比如如下系统默认是:
![]()
从/sys/block/sda/bdi/下的相关节点也可以获取该信息:

只是它们俩单位不一样。
现在问题来了,为什么默认就是128K,计算出来就应该是256,怎么会有2048的情况,因为它会在下图的block/blk-settings.c里的disk_update_readahead函数里进行调整:

3.2 blockdev里的read ahead数值如何影响内核page cache的逻辑
上面是获取和设置该blockdev的read ahead设置的方法及相关细节,那么它是怎么真正影响到实操层面的page cache里的逻辑的呢,我们也给出相关细节,page cache里的相关readahead的操作是在ondemand_readahead函数里,如下图,它是在mm/readahead.c里:

上图里的max_pages变量被赋值成ra->ra_pages的数值,而这个就是预取的page数。
我们看一下这个ra->ra_pages的数值是如何和blockdev里的bdi->ra_pages进行关联的,它们是在下图里的file_ra_state_init里进行关联的:

上图里的inode_to_bdi的实现如下:

可以看到就是bdi->ra_pages赋值给ra->ra_pages的。
