Linux系统存在两种缓存机制,分别是Page Cache和Buffer Cache, 其中Page Cache缓存文件的页以优化文件IO。Buffer Cache缓存块设备的块以优化块设备IO。为了加快IO访问,操作系统在文件系统和快设备之间使用内存缓存硬盘上的数据,这个就是buffer head. 但是基于块的设计,文件系层和块设备耦合比较紧密,而且文件系统并不都是基于块设备的,比如内存文件系统,网络文件系统等,再加上内和使用页的方式管理内存,因此如果使用物理页面PAGE缓存数据,支持上层文件系统,就可以和内核中的内存管理系统很好的结合。所以综合种种考虑,后来LINUX使用页代替块支持文件系统,缓存硬盘数据,相对于buffer cache,这些页面的集合相应的称之为page cache.
在linux kernel 2.4之前,这两个cache是不同的:文件在page cache中,磁盘块儿在buffer cache中。考虑到大多数的文件是由disk上的文件系统表示的,数据会在系统中有两份缓存,一份在page cache中,一份在buffer cache中。许多类unix系统都是这种模式。这种实现是很简单,但是明显的低效和冗余。在linux kernel 2.4之后,这两种caches统一了。虚拟内存子系统通过page cache驱动IO。如果数据同时在page cache和buffer cache中,buffer cache会简单的指向page cache,数据只会在内存中保有一份。Page cache就是你所知道的文件在内存中的缓存:它会加快文件访问速度。
无论如何内核要对块设备执行基于块的IO而不是虚拟内存page。由于绝大部分的块表示文件的数据部,所以大部分的buffer cache都可由page cache表达,但是有少量的比如文件元数据仍然是保留在buffer cache中来缓存。
我们简单看一下大概的结构,以比较常见的4K page,512 bytes 块儿缓冲区为例:
从page到lbn的转换,则需要 struct address_space 结构体了,图中没有绘制出来。
偏技术流的表示如下,根据block的大小,去分配buffer_head,通常是4个buffer_head,指向一个page.另外,下图是针对b_size为512B的1K的情况,如果b_size足够大,则只需要一个BH即可描述。
代码实现的地方,页面和地层块设备之间使用已有的块机制桥接起来,每个页面划分为若干个块,在为新文件分配一个新的页面时,文件系统将调用create_empty_buffers为页面分配对应的块。块和页面之间通过数据结构buffer_head的字段b_data关联起来,使用页面缓存之后,块没有自己的数据区了,块的字段b_data指向页内偏移。支撑同一个页面的块之间通过b_this_page字段链接起来。
图中的逻辑可以用下面这段内核代码证明:
磁盘操作底层通过submit_bio调用发挥功能,这一层其实不关心BUFFER HEAD结构,也不需要这个结构,struct buffer_head只是一个中间媒介,所以你有时候会看到某个PAGE页面缓存了文件内容,但是却没有buffer_head与之对应,比如ext4_readpage是读区一个完整的页面,这个时候可以直接用PAGE构造bio_vec(bio_add_page),而不需要再经过buffer_head(submit_bh)再走一遭了。
这也就是为什么,从normal file page cache中获取的页面,没有struct buffer_head(page_has_buffers探测不成功)对应的原因。
下图是一幅经典的描述Linux块儿设备工作原理的架构图:
通过sendfile接口,可以BYPASS掉用户层的缓存,直接在内核态对数据进行拷贝。
#include <sys/sendfile.h>
#include <sys/stat.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
static int open_or_die(const char *filename, int flags)
{
int fd = open(filename, flags);
if (fd < 0) {
printf("Failed to open '%s'; "
"check prerequisites are available\n", filename);
exit(1);
}
return fd;
}
static void exe_cp(const char *src, const char *dest)
{
int in_fd = open_or_die(src, O_RDONLY);
int out_fd = open(dest, O_RDWR|O_CREAT|O_TRUNC, 0755);
struct stat info;
fstat(in_fd, &info);
sendfile(out_fd, in_fd, NULL, info.st_size);
close(in_fd);
close(out_fd);
}
int main(void)
{
exe_cp("./a.out", "./b.out");
return 0;
}
嗯,是的,上面的图有点乱,那我们再抽象一下看看。
关于扇区-簇-buffercache-pagecache的关系,可以用下图予以说明。
对文件的MMAP和文件的PAGE CACHE应该是共享同样的 PAGE CACHE物理页面,具体看文件映射的PAGE FAULT函数 filemap_fault.
[内核内存] page cache_pagecache是内核缓冲区吗_早起的虫儿有鹰吃的博客-CSDN博客
文件MMAP的物理页面和PAGE CACH的物理页面是同一个,核心关键函数是__do_page_cache_readahead,其分配了PAGE CACHE,并且进行预读操作,之后,映射给MMAP用户态。
口说无凭,我们实际调试一下内核,验证一下上述架构,同时我们也要澄清一个问题,这个问题可以描述为,文件存在于块设备中,如果从文件所在的设备文件,比如/dev/sda7等等读取块,和通过文件系统读取同样的文件块,他们在page cache中,乃至在buffer cache中,是否是同一份呢?
下面开始:
内核驱动模块:
#include <linux/init.h>
#include <linux/module.h>
#include <linux/moduleparam.h>
#include <linux/stat.h>
#include <linux/fs.h>
#include <linux/kdev_t.h>
#include <linux/cdev.h>
#include <linux/slab.h>
#include <linux/device.h>
#include <linux/file.h>
#include <linux/pagemap.h>
#include <linux/buffer_head.h>
/* 定义字符设备和驱动的名字 */
#define DEVICE_NAME "char_demo_test"
#define DRIVER_NAME "char_demo_test"
/* 定义设备号和设备数及大小 */
#define DEVICE_MAJOR 240
#define DRIVER_MINOR 6
#define DEVICE_NUM 2
#define DEVICE_SIZE 3000
/* 申明主设备号的次设备号 */
int major_num = DEVICE_MAJOR;
int minor_num = DRIVER_MINOR;
/* 定义模块参数 */
module_param(major_num, int , 0664);
module_param(minor_num, int, 0664);
static struct class *myclass;
struct reg_dev
{
char *data;
unsigned long size;
struct cdev cdev;
};
struct reg_dev *my_devices;
static int char_demo_open(struct inode *inode, struct file *file){
printk(KERN_EMERG "chardevnode_open is success!imajor %d, iminor %d.\n", imajor(inode), iminor(inode));
return 0;
}
static int char_demo_close(struct inode *inode, struct file *file)
{
printk(KERN_EMERG "chardevnode_release is success!\n");
return 0;
}
static void dump_memory(unsigned char *buf, long num)
{
long cnt;
printk("Dump:");
for(cnt = 0; cnt < num; cnt ++)
{
if(cnt % 16 == 0)
{
printk("\n0x%p", buf);
}
printk(" %02x", *buf);
buf ++;
}
}
void probe_file(long fd)
{
char buf[128];
char *name;
struct inode *fino;
struct address_space * mapping;
struct page * page1;
struct page * page2;
struct page * page3;
unsigned char *page1_addr, *page2_addr, *page3_addr;
struct buffer_head *bh1, *bh2, *bh3;
struct file * f = fget(fd);
memset(buf, 0x00, 128);
if (!f)
{
printk("%s line %d, file is null\n", __func__, __LINE__);
return ;
}
name = file_path(f, buf, sizeof(buf));
printk("%s line %d, name %s.\n", __func__, __LINE__, name);
fino = file_inode(f);
mapping = f->f_mapping;
printk("%s line %d, PAGE_SIZE = %ld, nrpages = %ld.\n", __func__, __LINE__, PAGE_SIZE, mapping->nrpages);
page1 = find_get_page(mapping, 0);
page2 = find_get_page(mapping, 1);
page3 = find_get_page(mapping, 2);
if(!page1 || !page2 || !page3)
{
printk("%s line %d, page is null.\n", __func__, __LINE__);
return;
}
printk("%s line %d, page1 = %p, page2 = %p, page3 = %p.\n", __func__, __LINE__,page1, page2, page3);
page1_addr = page_address(page1);
page2_addr = page_address(page2);
page3_addr = page_address(page3);
if(!page1_addr || !page2_addr || !page3_addr)
{
printk("%s line %d, page addr is null.\n", __func__, __LINE__);
return;
}
dump_memory(page1_addr, 16);
dump_memory(page2_addr, 16);
dump_memory(page3_addr, 16);
bh1 = page_buffers(page1);
bh2 = page_buffers(page2);
bh3 = page_buffers(page3);
if(!bh1 || !bh2 || !bh3)
{
printk("%s line %d, bh is null.\n", __func__, __LINE__);
return;
}
printk("%s line %d, lbn1=%lld, lbn2=%lld, lbn3=%lld.\n", __func__, __LINE__, bh1->b_blocknr, bh2->b_blocknr, bh3->b_blocknr);
printk("%s line %d, bsize1=%ld, bsize2=%ld, bsize3=%ld.\n", __func__, __LINE__, bh1->b_size, bh2->b_size, bh3->b_size);
printk("%s line %d, bh1->b_this_page = %p, bh1 = %p.\n", __func__, __LINE__, bh1->b_this_page, bh1);
printk("%s line %d, bh2->b_this_page = %p, bh2 = %p.\n", __func__, __LINE__, bh2->b_this_page, bh2);
printk("%s line %d, bh3->b_this_page = %p, bh3 = %p.\n", __func__, __LINE__, bh3->b_this_page, bh3);
if(bh1->b_this_page != bh1)
{
printk("%s line %d, bh1 is not identical with bh1->b_this_page\n", __func__, __LINE__);
}
else
{
printk("%s line %d, bh1 is identical with bh1->b_this_page\n", __func__, __LINE__);
}
if(bh2->b_this_page != bh2)
{
printk("%s line %d, bh2 is not identical with bh2->b_this_page\n", __func__, __LINE__);
}
else
{
printk("%s line %d, bh2 is identical with bh2->b_this_page\n", __func__, __LINE__);
}
if(bh3->b_this_page != bh3)
{
printk("%s line %d, bh3 is not identical with bh3->b_this_page\n", __func__, __LINE__);
}
else
{
printk("%s line %d, bh3 is identical with bh3->b_this_page\n", __func__, __LINE__);
}
if(bh3->b_page != page3)
{
printk("%s line %d, b_page is not identical with page3\n", __func__, __LINE__);
}
else
{
printk("%s line %d, b_page is identical with page3\n", __func__, __LINE__);
}
if(bh2->b_page != page2)
{
printk("%s line %d, b_page is not identical with page2\n", __func__, __LINE__);
}
else
{
printk("%s line %d, b_page is identical with page2\n", __func__, __LINE__);
}
if(bh1->b_page != page1)
{
printk("%s line %d, b_page is not identical with page1\n", __func__, __LINE__);
}
else
{
printk("%