文件系统缓存

#To free pagecache
echo 1 > /proc/sys/vm/drop_caches

#To free dentry and inode cache
echo 2 > /proc/sys/vm/drop_caches

#To free pagecache,dentry cache,inode cache
echo 3 > /proc/sys/vm/drop_caches

dentry cache

struct dentry反映的是文件系统对象(包括目录、文件)在内核中所在文件系统树的位置,在文件系统对文件的操作中inode是对应文件处理的核心对象,而要找到inode就需要先找到dentry,dentry对象内容指向了inode。因此在文件系统做dentry的搜索效率也一定程度上决定着文件系统操作效率,在linux系统中为了提高dentry的处理效率,实现了dentry高速缓存,dentry cache简称dcache。
在3.1.3章节中我们列出了dentry的数据结构组成,其中对于搜索的提升有两个关键数据结构。

struct dentry {
    struct hlist_bl_node d_hash;
    strcut list_head d_lru;
}

在文件系统初始化时,调用vfs_caches_init->dcache_init为对dcache进行初始化,先创建一个dentry的slab,用于后续dentry对象的分配,同时还分配了一个dentry_hashtable用于管理dentry的全局hash表。

static void __init dcache_init(void)
{
    /*
     * A constructor could be added for stable state like the lists,
     * but it is probably not worth it because of the cache nature
     * of the dcache.
     */
    dentry_cache = KMEM_CACHE_USERCOPY(dentry,
        SLAB_RECLAIM_ACCOUNT|SLAB_PANIC|SLAB_MEM_SPREAD|SLAB_ACCOUNT,
        d_iname);

    /* Hash may have been set up in dcache_init_early */
    if (!hashdist)
        return;

    dentry_hashtable =
        alloc_large_system_hash("Dentry cache",
                    sizeof(struct hlist_bl_head),
                    dhash_entries,
                    13,
                    HASH_ZERO,
                    &d_hash_shift,
                    NULL,
                    0,
                    0);
    d_hash_shift = 32 - d_hash_shift;
}

可以cat /proc/slabinfo | grep dentry查看dentry的情况。

dcache在kmem_cache_alloc的基础上定义了两个高层分配接口:d_alloc和d_alloc_root函数,用来从dentry_cache slab中分配dentry对象。d_alloc_root时用来为文件系统跟目录分配dentry对象的。
dentry释放后并不会马上清除掉,而是不缓存起来,所以通常有3种状态,而其中unused和negative是可以在主动触发释放是可以进行回收的。

  • inuse:正在被使用,引用技术d_lockref>0。
  • unused:未被内核使用,引用技术d_lockref为0,d_inode为空。
  • negative:相应的磁盘inode已经被删除,d_inode为空。
  • dentry结构通常在路径查找被创建,常用的有以下2种管理方式
  • 哈希链表:static struct hlist_bl_head *dentry_hashtable __read_mostly,以dentry->name作为索引在dentry全局hash表中管理,用于提高路径查找效率。
  • LRU链表:处于unused和negative状态不再使用的dentry都通过其dentry->d_lru指针链接到super_block->s_dentry_lru中,当需要内存回收时,由prune_dcache_sb回收使用较少的dentry。

在linux系统中定义了dentry_stat_t数据结构来统计dcache信息。

struct dentry_stat_t {
       int nr_dentry;       dentry的数量
       int nr_unused;       dentry为使用的数量
       int age_limit;/* age in seconds */
       int want_pages;/* pages requested by system */
       int dummy[2];
};
extern struct dentry_stat_t dentry_stat;

可以通过节点/proc/sys/fs/dentry-state来获取,可以发现dentry-state的nr_dentry:15227与slab info中已经分配出去的对象15229差不多,16957表示当前dentry cache中一共还有多少个对象包括使用和为使用的,528表示每个对象的大小,单位是字节,可以看出一个对象是528字节,对应struct dentry数据结构的大小。

当系统使用echo 2 > /proc/sys/vm/drop_caches可以释放dentry和inode的缓存。

#To free dentry and inode cache
echo 2 > /proc/sys/vm/drop_caches

执行上面的命令后,可以查看节点/proc/sys/fs/dentry-state或cat /proc/slabinfo | grep dentry可以看出dentry的数量变少了,系统回收了。

inode cache

Inode是用于描述一个文件的,通常有一个或多个dentry与之对应,dentry已经描述了系统的目录树,提供了路径查找的方法,所以inode就不需要在搜索查询上处理复杂关系,管理方式也相对简单不少。

struct inode {
    ...
    /* Stat data, not accessed from path walking */
    unsigned long       i_ino;    //与其i_sb一起计算hash作为在inode_hashtable中的索引
    struct hlist_node   i_hash;    //inode_hashtable全局hash表中的节点
    struct list_head    i_lru;      /* inode LRU list */    //其i_sb->s_inode_lru中的节点
    ...
}

与dentry类似,在内核初始化阶段,会调用inode_init初始化一个inode_cache的slab对象,用于inode的分配。同时还创建了一个用于管理inode的全局哈希表inode_hashtable。

void __init inode_init(void)
{
    /* inode slab cache */
    inode_cachep = kmem_cache_create("inode_cache",
                     sizeof(struct inode),
                     0,
                     (SLAB_RECLAIM_ACCOUNT|SLAB_PANIC|
                     SLAB_MEM_SPREAD|SLAB_ACCOUNT),
                     init_once);

    /* Hash may have been set up in inode_init_early */
    if (!hashdist)
        return;

    inode_hashtable =
        alloc_large_system_hash("Inode-cache",
                    sizeof(struct hlist_head),
                    ihash_entries,
                    14,
                    HASH_ZERO,
                    &i_hash_shift,
                    &i_hash_mask,
                    0,
                    0);
}

Inode结构主要涉及两种管理方式

  • inode哈希表:inode_hashtable, super_block和i_ino作为索引在inode在全局inode_hashtable中管理,主要通过i_ino查找到inode。
  • LRU链表:通过inode->i_lru链接到super_block->s_inode_lru上,表示不再使用的空闲inode,当需要内存回收是,从中选取最少使用的inode进行回收。
struct inodes_stat_t {
    long nr_inodes;
    long nr_unused;
    long dummy[5];      /* padding for sysctl ABI compatibility */
};
struct inodes_stat_t inodes_stat;

与dentry类似,inode也定义了一个inodes_stat用于统计inode信息,可以通过cat /proc/sys/fs/inode-state来查询。

page cache

free显示中的buff和cache怎么理解,使用free -w会将buff和cache分开,字面上buffer缓冲,而cache是缓存。
free的数据来源与/proc/meminfo之间的关系。

  • buffers:内核缓冲区的内存,对应的是/proc/meminfo中的Buffers
  • cache:是内核页缓存和slab用到的内存,对应的是/proc/meminfo中cached+SReclainmable之和。
  • buff/cache:则是buffers和cache之和。

所以要理解free中buff/cache的关键是要理解buffers、cached、SReclaimable三个的概念,下面是man proc的意思。

Buffers 
Relatively temporary storage for raw disk blocks that shouldn't get tremendously large (20MB or so).

Cached 
In-memory cache for files read from the disk (the page cache).  Doesn't include SwapCached.

SReclaimable 
Part of Slab, that might be reclaimed, such as caches.
  • Buffers:是对原始磁盘块的零时存储,用于缓存磁盘的数据,通常不会特别大(20MB左右),因为对应用户来说读写数据可能是几个字节,但是对应磁盘来说是按block来操作的,这样内核就可以把分散写集中起来,如把多次小的写集合并成单次大的写。
  • Cached:是磁盘读取文件的页缓存,也就是用来缓存从文件读取的数据,这样下次访问这些文件数据时,旧可以直接从内存快速获取,而不需要再次访问缓慢的磁盘,但是不包括交互到swap分区的。
  • SReclaimable:是slab的一部分。Slab包含可回收和不可回收两部分,可回收的用SReclaimable记录(dentry cache、inode cache属于slab的一部分),不可回收的用SUnrecalaim记录。

从上面buffers和cached的来看,buffer是对磁盘的,而cache是针对文件页缓存读的,看起来是分开的,但实际上buffers和cached并没有分开,都是page cache,下面将来进行阐述。
CPU的读写速度与磁盘的读写速度是有很大差距的,因此文件系统在访问磁盘时,并不会直接与磁盘进行交互,而是在具体的文件系统与磁盘之间申请一段缓冲buffer,这段缓冲buffer实际就是申请了一块内存,将磁盘的数据缓存到这块内存中,具体的文件系统对磁盘的读写就转换为对缓冲buffer的读写这样就可以提高处理的速度,系统负责定期的将缓冲buff与磁盘进行数据同步,当然也可以手动sync的方式进行同步。
早期linux0.11阶段,buffer cache在linux中对应的数据结构是struct buffer_head,简称bh,顾名思义,表示缓冲区头部,这个缓冲区缓冲的是磁盘块设备数据,早期阶段为了提高磁盘的读写消息,在磁盘之上增加了一个缓冲,磁盘的读写单位是block,一般block的大小是1KB,每个block就对应一个bh,而bh的内存申请是以原始的内存分配方式,还没有基于page来分配内存。

strcut buffer_head {
  char *b_data;         /* pointer to data block (1024 bytes) *///指针。
  unsigned long b_blocknr;  /* block number */// 块号。
  unsigned short b_dev;     /* device (0 = free) */// 数据源的设备号。
  unsigned char b_uptodate; // 更新标志:表示数据是否已更新。
  unsigned char b_dirt;     /* 0-clean,1-dirty *///修改标志:0 未修改,1 已修改.
  unsigned char b_count;    /* users using this block */// 使用的用户数。
  unsigned char b_lock;     /* 0 - ok, 1 -locked */// 缓冲区是否被锁定。
  struct task_struct *b_wait;   // 指向等待该缓冲区解锁的任务。
  struct buffer_head *b_prev;   // hash 队列上前一块(这四个指针用于缓冲区的管理)。
  struct buffer_head *b_next;   // hash 队列上下一块。
  struct buffer_head *b_prev_free;  // 空闲表上前一块。
  struct buffer_head *b_next_free;  // 空闲表上下一块。
};

发展到中间阶段时linux2.2版本,buffer 就基于page来分配内存,但是page cache和buffer没有什么直接连续,是各自独立的作用,page cache只用于负责mmap的部分处理,buffer 依旧负责对磁盘IO的访问缓冲,如下图。但这样就会出现一个问题,由于read/write是绕过了page cache,就会导致mmap和read/write操作同步问题,磁盘的同一份数据在page cache中有一份,在buffer中也有一份,这样分离的设计还会导致浪费内存。

struct buffer_head {
    /* First cache line: */
    struct buffer_head * b_next;    /* Hash queue list */
    unsigned long b_blocknr;    /* block number */
    unsigned long b_size;       /* block size */
    kdev_t b_dev;           /* device (B_FREE = free) */
    kdev_t b_rdev;          /* Real device */
    unsigned long b_rsector;    /* Real buffer location on disk */
    struct buffer_head * b_this_page;   /* circular list of buffers in one page */
    unsigned long b_state;      /* buffer state bitmap (see above) */
    struct buffer_head * b_next_free;
    unsigned int b_count;       /* users using this block */

    /* Non-performance-critical data follows. */
    char * b_data;          /* pointer to data block (1024 bytes) */
    unsigned int b_list;        /* List that this buffer appears */
    unsigned long b_flushtime;      /* Time when this (dirty) buffer
                     * should be written */
    struct wait_queue * b_wait;
    struct buffer_head ** b_pprev;      /* doubly linked list of hash-queue */
    struct buffer_head * b_prev_free;   /* doubly linked list of buffers */
    struct buffer_head * b_reqnext;     /* request queue */

    /*
     * I/O completion
     */
    void (*b_end_io)(struct buffer_head *bh, int uptodate);
    void *b_dev_id;
};

发展到第三阶段linux 2.4版本,对cache和buffer进行了融合,因为buffer的申请也是基于page的,那这两个为什么不融合在一起了,融合后buffer cache就直接存储在page cache中,但是依旧保留了buffer cache的描述。

struct buffer_head {
    unsigned long b_state;      /* buffer state bitmap (see above) */
    struct buffer_head *b_this_page;/* circular list of page's buffers */
    struct page *b_page;        /* the page this bh is mapped to */

    sector_t b_blocknr;     /* start block number */
    size_t b_size;          /* size of mapping */
    char *b_data;           /* pointer to data within the page */

    struct block_device *b_bdev;
    bh_end_io_t *b_end_io;      /* I/O completion */
    void *b_private;        /* reserved for b_end_io */
    struct list_head b_assoc_buffers; /* associated with another mapping */
    struct address_space *b_assoc_map;  /* mapping this buffer is
                           associated with */
    atomic_t b_count;       /* users using this buffer_head */
    spinlock_t b_uptodate_lock; /* Used by the first bh in a page, to
                     * serialise IO completion of other
                     * buffers in the page */
};

至此page cache和buffer 两者的关系融合了。磁盘的读写是按block为单位操作,一个block对应一个buffer_head缓冲,如块单位是1KB,那么1个page cache就有4个buffer_head。
对于文件系统的操作来说,buffer和cache都是page cache,对应的是进程打开文件内存的缓存,缓存在address_space::i_pages的xarray上,缓存的数量为address_space::nrpages。

Buffer_head的原义是与block对应,按照磁盘的布局分为元数据区和数据区域,元数据是用于管理磁盘的如supper_block、inode等,数据区域才是正在的对应文件系统操作的数据。所以的对于cache和buffer的区别,buffer还存储了对磁盘管理的元数据缓存,这部分
再后来在linux2.4版本之后,新的文件系统开始引入了bio结构来替换buffer_head,但是原有的一些文件系统并没有完全替换使用bio,所以了就需要做bio兼容buffer_head,在磁盘操作前会调用到submit_bh_wbc函数,该函数会将buffer_head组装成bio,因此最终对磁盘的操作都会转为bio的方式,新的文件系统就直接使用bio。

static int submit_bh_wbc(int op, int op_flags, struct buffer_head *bh,
             enum rw_hint write_hint, struct writeback_control *wbc)
{
    struct bio *bio;

    BUG_ON(!buffer_locked(bh));
    BUG_ON(!buffer_mapped(bh));
    BUG_ON(!bh->b_end_io);
    BUG_ON(buffer_delay(bh));
    BUG_ON(buffer_unwritten(bh));

    /*
     * Only clear out a write error when rewriting
     */
    if (test_set_buffer_req(bh) && (op == REQ_OP_WRITE))
        clear_buffer_write_io_error(bh);

    bio = bio_alloc(GFP_NOIO, 1);

    fscrypt_set_bio_crypt_ctx_bh(bio, bh, GFP_NOIO);

    bio->bi_iter.bi_sector = bh->b_blocknr * (bh->b_size >> 9);
    bio_set_dev(bio, bh->b_bdev);
    bio->bi_write_hint = write_hint;

    bio_add_page(bio, bh->b_page, bh->b_size, bh_offset(bh));
    BUG_ON(bio->bi_iter.bi_size != bh->b_size);

    bio->bi_end_io = end_bio_bh_io_sync;
    bio->bi_private = bh;

    if (buffer_meta(bh))
        op_flags |= REQ_META;
    if (buffer_prio(bh))
        op_flags |= REQ_PRIO;
    bio_set_op_attrs(bio, op, op_flags);

    /* Take care of bh's that straddle the end of the device */
    guard_bio_eod(bio);

    if (wbc) {
        wbc_init_bio(wbc, bio);
        wbc_account_cgroup_owner(wbc, bh->b_page, bh->b_size);
    }

    submit_bio(bio);
    return 0;
}

以上的page cache和buffer cache的融合是针对使用文件系统的方式访问块设备的场景。但是如果以下的两个方式访问磁盘①使用文件②使用块设备节点,这两种方式对应的page cache是不同的,文件的方式是buff/cache,而块设备节点是buff(实际也是page cache分配),这种情况下一个物理磁盘的block数据仍然对应linux内核的两份page,一个是通过通用文件层访问的文件page cache(page cache),另一个是通过块设备节点访问的page cache(buffer cache)。另外需要注意的是,如果通过块设备节点访问声明了O_DIRECT,将会直接访问磁盘不会经过buff。

上图如果分别使用文件系统方式/mnt/test的方式或者裸设备/dev/sda的方式去写磁盘,即使是一个位置,但是将会有两份page cache,前者的page cache对应的就是free中的cache,而后边的page cache对应的是free中的buff。
可以从/proc/meminfo的实现来研究一下buff和cache的区别。

static int meminfo_proc_show(struct seq_file *m, void *v)
{
    struct sysinfo i;
    unsigned long committed;
    long cached;
    long available;
    unsigned long pages[NR_LRU_LISTS];
    unsigned long sreclaimable, sunreclaim;
    int lru;

    si_meminfo(&i);
    si_swapinfo(&i);
    committed = vm_memory_committed();

    cached = global_node_page_state(NR_FILE_PAGES) -
            total_swapcache_pages() - i.bufferram;
    if (cached < 0)
        cached = 0;

    for (lru = LRU_BASE; lru < NR_LRU_LISTS; lru++)
        pages[lru] = global_node_page_state(NR_LRU_BASE + lru);

    available = si_mem_available();
    sreclaimable = global_node_page_state_pages(NR_SLAB_RECLAIMABLE_B);
    sunreclaim = global_node_page_state_pages(NR_SLAB_UNRECLAIMABLE_B);

    show_val_kb(m, "MemTotal:       ", i.totalram);
    show_val_kb(m, "MemFree:        ", i.freeram);
    show_val_kb(m, "MemAvailable:   ", available);
    show_val_kb(m, "Buffers:        ", i.bufferram); //show_val_kb会将page数量转化为kb。
    show_val_kb(m, "Cached:         ", cached);
show_val_kb(m, "SwapCached:     ", total_swapcache_pages());
......
}

先来看看buff,buffers为i.bufferram,而该值在si_meminfo(&i)中计算而来,在该函数中调用nr_blockdev_pages计算而来。

void si_meminfo(struct sysinfo *val)
{
    val->totalram = totalram_pages();
    val->sharedram = global_node_page_state(NR_SHMEM);
    val->freeram = global_zone_page_state(NR_FREE_PAGES);
    val->bufferram = nr_blockdev_pages();
    val->totalhigh = totalhigh_pages();
    val->freehigh = nr_free_highpages();
    val->mem_unit = PAGE_SIZE;
}

再看看下面的函数调用,buffers是遍历所有的块设备,累加address->i_mapping上nrpages值,该值就是xarray树上对应的page数量,对应上图中直接操作磁盘节点读写的方式 “open /dev/sda”。

long nr_blockdev_pages(void)
{
    struct inode *inode;
    long ret = 0;

    spin_lock(&blockdev_superblock->s_inode_list_lock);
    list_for_each_entry(inode, &blockdev_superblock->s_inodes, i_sb_list)
        ret += inode->i_mapping->nrpages;
    spin_unlock(&blockdev_superblock->s_inode_list_lock);

    return ret;
}

再来看看cached的计算cached = global_node_page_state(NR_FILE_PAGES) -total_swapcache_pages() - i.bufferram,其中 global_node_page_state(NR_FILE_PAGES) 为vm_node_stat[NR_FILE_PAGES]的值。内核中定义了一个全局变量atomic_long_t vm_node_stat[],用于统计全局内存的信息,搜索内核中关于NR_FILE_PAGES的标志,主要有以下三个函数修改。

__mod_lruvec_page_state(item,val) //对全局变量vm_node_stat[item]增加val
__dec_lruvec_page_state()//对全局变量vm_node_stat[item]减1
__inc_lruvec_page_state()//对全局变量vm_node_stat[item]加1

示例

__mod_lruvec_page_state(page, NR_FILE_PAGES, -nr)
    __mod_node_page_state(page_pgdat(page), idx, val)
        node_page_state_add(delta, pgdat, item) //delta=-nr
            atomic_long_add(x, &pgdat->vm_stat[item]);
                atomic_long_add(x, &vm_node_stat[item]);

从上面的函数调用,__mod_lruvec_page_state(struct page *page,enum node_stat_item idx, int val)的作用就是根据item类型(这里是NR_FILE_PAGES)对全局变量vm_node_stat[item]增加val。

从上的函数调用流程可知,无论是文件系统的写还是读,当分配新的page时,会调用add_to_page_cache_lru将page添加到xarray树上,并调用__inc_lruvec_page_state增加page计数,同时会添加到LRU链表用于后续回收。除了调用generic_file_read/write读写文件系统会修改vm_node_stat[NR_FILE_PAGES]外,还有不少操作也会让vm_node_stat[NR_FILE_PAGES]会增加,如通过dev节点直接对磁盘的操作(buffers),以及匿名页交换到swap分区的也会修改。因此vm_node_stat[NR_FILE_PAGES]是一个总的值,实际在计算cached的时候会把交换到swap分区、以及buffers的值减去。cached = global_node_page_state(NR_FILE_PAGES) -total_swapcache_pages() - i.bufferram。
总结下:

  • 对于文件系统的操作方式来说(如/mnt/UDISK/test),大部分是cached(buffers和cached以及合并了),会占用少部分的buffer,主要是存储一些元数据的存储是划到buffer中。
  • 对于磁盘的操作方式来说(如/dev/sda),只有buffers。

可以使用测试使用vmstat观察buff和cache的变化情况。

通过文件系统的方式

写入到文件系统中

echo 3 > /proc/sys/vm/drop_caches
dd if=/dev/urandom of=/mnt/UDISK/test bs=1M count=5

从文件系统中读

echo 3 > /proc/sys/vm/drop_caches
dd if=/mnt/UDISK/test4 of=/tmp/test bs=1M count=10

通过vmstat统计来看,写入到文件系统ext4上或从文件系统读,基本都是cache在变化。

通过磁盘的方式操作

echo 3 > /proc/sys/vm/drop_caches
dd if=/dev/urandom of=/dev/mmcblk0p9 bs=1M count=8

echo 3 > /proc/sys/vm/drop_caches
dd if=/dev/mmcblk0p9 of=/dev/null bs=1M count=8

通过磁盘的方式操作,可以发现buff和cache都有变化(这里的cache还包括了slab可以回收部分,如果查询/proc/meminfo下的cached变化会更少些),但是buff变化更大些,然后突然又降回去了(被系统回收了)读磁盘数据会缓存到buffer中。

Direct IO写

dd if=/dev/urandom of=/dev/mmcblk0p9 bs=1M count=10 oflag=direct

可以发现buff,cache基本没变化。