free 命令是Linux系统上查看内存使用状况最常用的工具,然而很少有人能说清楚 “buffers” 与 “cached” 之间的区别:
我们先抛出结论,如果你对研究过程感兴趣可以继续阅读后面的段落:
buffers
表示块设备(block device)所占用的缓存页,包括:直接读写块设备、以及文件系统元数据(metadata)
,比如SuperBlock
所使用的缓存页;
cached
表示普通文件数据所占用的缓存页。
下面是分析过程:
先用 strace
跟踪 free
命令,看看它是如何计算 buffers
和 cached
的:
# strace free
...
open('/proc/meminfo', O_RDONLY) = 3
lseek(3, 0, SEEK_SET) = 0
read(3, 'MemTotal: 3848656 kB\nMemF'..., 2047) = 1170
...
显然 free 命令是从 /proc/meminfo
中读取信息的,跟我们直接读到的结果一样:
# cat /proc/meminfo
MemTotal: 3848656 kB
MemFree: 865640 kB
Buffers: 324432 kB
Cached: 2024904 kB
...
SwapTotal: 2031612 kB
SwapFree: 2031612 kB
...
Shmem: 5312 kB
...
那么 /proc/meminfo
中的 Buffers
和 Cached
又是如何得来的呢?这回没法偷懒,只能去看源代码了。源代码文件是:fs/proc/meminfo.c
,我们感兴趣的函数是:meminfo_proc_show()
,阅读得知:
Cached
来自于以下公式:
global_page_state(NR_FILE_PAGES) – total_swapcache_pages – i.bufferram
global_page_state(NR_FILE_PAGES)
表示所有的缓存页(page cache
)的总和,它包括:
Cached
Buffers
也就是上面公式中的i.bufferram
,来自于nr_blockdev_pages()
函数的返回值。交换区缓存 (swap cache)
global_page_state(NR_FILE_PAGES)
来自 vmstat[NR_FILE_PAGES]
,vmstat[NR_FILE_PAGES]
可以通过 /proc/vmstat
来查看,表示所有缓存页的总数量:
# cat /proc/vmstat
...
nr_file_pages 587334
...
注意以上nr_file_pages
是以page
为单位(一个page等于4KB),而free命令是以KB为单位的。
直接修改 nr_file_pages
的内核函数是:__inc_zone_page_state(page, NR_FILE_PAGES)
和__dec_zone_page_state(page, NR_FILE_PAGES)
,一个用于增加,一个用于减少。
Swap Cache是什么?
用户进程的内存页分为两种:file-backed pages
(与文件对应的内存页)和anonymous pages
(匿名页)。匿名页(anonymous pages
)是没有关联任何文件的,比如用户进程通过malloc()
申请的内存页,如果发生swapping
换页,它们没有关联的文件进行回写,所以只能写入到交换区里。
交换区可以包括一个或多个交换区设备(裸盘、逻辑卷、文件都可以充当交换区设备),每一个交换区设备在内存里都有对应的swap cache
,可以把swap cache
理解为交换区设备的page cache
:page cache
对应的是一个个文件,swap cache
对应的是一个个交换区设备,kernel管理swap cache
与管理page cache
一样,用的都是radix-tree
,唯一的区别是:page cache
与文件的对应关系在打开文件时就确定了,而一个匿名页只有在即将被swap-out
的时候才决定它会被放到哪一个交换区设备,即匿名页与swap cache
的对应关系在即将被swap-out
时才确立。
并不是每一个匿名页都在swap cache
中,只有以下情形之一的匿名页才在:
匿名页即将被 swap-out
时会先被放进swap cache
,但通常只存在很短暂的时间,因为紧接着在pageout
完成之后它就会从swap cache
中删除,毕竟swap-out
的目的就是为了腾出空闲内存;【注:参见mm/vmscan.c: shrink_page_list()
,它调用的add_to_swap()
会把swap cache
页面标记成dirty
,然后它调用try_to_unmap()
将页面对应的page table mapping
都删除,再调用pageout()
回写dirty page
,最后try_to_free_swap()
会把该页从swap cache中
删除。】曾经被 swap-out
现在又被swap-in
的匿名页会在swap cache
中,直到页面中的内容发生变化、或者原来用过的交换区空间被回收为止。【注:当匿名页的内容发生变化时会删除对应的swap cache
,代码参见mm/swapfile.c: reuse_swap_page()
。】
cached:
Cached
表示除去 buffers
和 swap cache
之外,剩下的也就是普通文件的缓存页的数量:
global_page_state(NR_FILE_PAGES) – total_swapcache_pages – i.bufferram
所以关键还是要理解 buffers
是什么含义。
buffers:
从源代码中看到,buffers
来自于 nr_blockdev_pages()
函数的返回值:
long nr_blockdev_pages(void)
{
struct block_device *bdev;
long ret = 0;
spin_lock(&bdev_lock);
list_for_each_entry(bdev, &all_bdevs, bd_list) {
ret += bdev->bd_inode->i_mapping->nrpages;
}
spin_unlock(&bdev_lock);
return ret;
}
这段代码的意思是遍历所有的块设备(block device),累加每个块设备的inode
的i_mapping
的页数,统计得到的就是 buffers
。显然 buffers
是与块设备直接相关的。
那么谁会更新块设备的缓存页数量(nrpages
)呢?我们继续向下看。
搜索kernel源代码发现,最终点我更新mapping->nrpages字段的函数就是:
pagemap.h: add_to_page_cache
> filemap.c: add_to_page_cache_locked
C__add_to_page_cache_locked
> page_cache_tree_insert
和:
filemap.c: delete_from_page_cache
> __delete_from_page_cache
> page_cache_tree_delete
static inline int add_to_page_cache(struct page *page,
struct address_space *mapping, pgoff_t offset, gfp_t gfp_mask)
{
int error;
__set_page_locked(page);
error = add_to_page_cache_locked(page, mapping, offset, gfp_mask);
if (unlikely(error))
__clear_page_locked(page);
return error;
}
void delete_from_page_cache(struct page *page)
{
struct address_space *mapping = page->mapping;
void (*freepage)(struct page *);
BUG_ON(!PageLocked(page));
freepage = mapping->a_ops->freepage;
spin_lock_irq(&mapping->tree_lock);
__delete_from_page_cache(page, NULL);
spin_unlock_irq(&mapping->tree_lock);
mem_cgroup_uncharge_cache_page(page);
if (freepage)
freepage(page);
page_cache_release(page);
}
这两个函数是通用的,block device
和 文件inode
都可以调用,至于更新的是块设备的(buffers
)还是文件的(cached
),取决于参数变量mapping
:如果mapping
对应的是块设备,那么相应的统计信息会反映在 buffers
中;如果mapping
对应的是文件inode
,影响的就是 cached
。我们下面看看kernel
中哪些地方会把块设备的mapping
传递进来。免翻官方ChatGPT 4.0 和 Claude Pro,稳定有售后
首先是块设备本身,打开时使用 bdev->bd_inode->i_mapping
。
static int blkdev_open(struct inode * inode, struct file * filp)
{
struct block_device *bdev;
/*
* Preserve backwards compatibility and allow large file access
* even if userspace doesn't ask for it explicitly. Some mkfs
* binary needs it. We might want to drop this workaround
* during an unstable branch.
*/
filp->f_flags |= O_LARGEFILE;
if (filp->f_flags & O_NDELAY)
filp->f_mode |= FMODE_NDELAY;
if (filp->f_flags & O_EXCL)
filp->f_mode |= FMODE_EXCL;
if ((filp->f_flags & O_ACCMODE) == 3)
filp->f_mode |= FMODE_WRITE_IOCTL;
bdev = bd_acquire(inode);
if (bdev == NULL)
return -ENOMEM;
filp->f_mapping = bdev->bd_inode->i_mapping;
return blkdev_get(bdev, filp->f_mode, filp);
}
其次,文件系统的Superblock
也是使用块设备:
struct super_block {
...
struct block_device *s_bdev;
...
}
int inode_init_always(struct super_block *sb, struct inode *inode)
{
...
if (sb->s_bdev) {
struct backing_dev_info *bdi;
bdi = sb->s_bdev->bd_inode->i_mapping->backing_dev_info;
mapping->backing_dev_info = bdi;
}
...
}
sb表示SuperBlock
,s_bdev
就是块设备。Superblock
是文件系统的metadata
(元数据),不属于文件,没有对应的inode,所以,对metadata
操作所涉及的缓存页都只能利用块设备mapping
,算入 buffers
的统计值内。
如果文件含有间接块(indirect blocks
),因为间接块也属于metadata
,所以走的也是块设备的mapping
。查看源代码,果然如此:
ext4_get_blocks
-> ext4_ind_get_blocks
-> ext4_get_branch
-> sb_getblk
static inline struct buffer_head *
sb_getblk(struct super_block *sb, sector_t block)
{
return __getblk(sb->s_bdev, block, sb->s_blocksize);
}
这样我们就知道了buffers
是块设备(block device
)占用的缓存页,分为两种情况:
直接对块设备进行读写操作; 文件系统的 metadata
(元数据),比如SuperBlock
。
验证:
现在我们来做个测试,验证一下上述结论。既然文件系统的metadata
会用到 buffers
,我们用 find 命令扫描文件系统,观察 buffers
增加的情况:
# free
total used free shared buffers cached
Mem: 3848656 2889508 959148 5316 263896 2023340
-/+ buffers/cache: 602272 3246384
Swap: 2031612 0 2031612
# find / -name abc.def
# free
total used free shared buffers cached
Mem: 3848656 2984052 864604 5320 319612 2023348
-/+ buffers/cache: 641092 3207564
Swap: 2031612 0 2031612
再测试一下直接读取block device,观察buffers
增加的现象:
# free
total used free shared buffers cached
Mem: 3848656 3006944 841712 5316 331020 2028648
-/+ buffers/cache: 647276 3201380
Swap: 2031612 0 2031612
# dd if=/dev/sda1 of=/dev/null count=2000
2000+0 records in
2000+0 records out
1024000 bytes (1.0 MB) copied, 0.026413 s, 38.8 MB/s
# free
total used free shared buffers cached
Mem: 3848656 3007704 840952 5316 331872 2028692
-/+ buffers/cache: 647140 3201516
Swap: 2031612 0 2031612
结论:
free 命令所显示的 buffers
表示块设备(block device)所占用的缓存页,包括直接读写块设备、以及文件系统元数据(metadata
)如SuperBlock
所使用的缓存页;而 cached
表示普通文件所占用的缓存页。
---END---
来自潮汕IT智库公众号