实际上 Buffer 和 Cache 是两个用烂的词,在不同的场景下其语义会有所区别。在 Linux 的内存管理中,Buffer 是指 Buffer Cache(缓冲区缓存),Cache 是指 Page Cache(页面缓存)。
这里简单介绍其概念。
简介
曾经 Buffer 被用来做 IO 设备的写缓存,Cache 被用来作为 IO 设备的读缓存,这里的 IO 设备,主要指的是块设备文件和文件系统上的普通文件;但是现在,它们的意义已经不一样了。
在当前的内核中,PageCache 就是针对内存页的缓存,如果有内存是以 Page 进行分配管理的,都可以使用 PageCache 作为其缓存来管理使用。
当然,不是所有的内存都是以 Page 为单位进行管理,也有很多是针对块 Block 进行管理的,如果这部分需要使用到 Cache 功能,则都集中到 BufferCache 中,从这一角度来说,改称为 BlockCache 更为合适。
前世今生
简单来说,两者都是为了优化磁盘 IO 的读写速率,其中 PageCache 缓存了文件页用来优化文件 IO;而 BufferCache 缓存了磁盘块用来优化块设备的 IO 。
很多的类 Unix 系统采用了与 Linux 2.4 之前版本类似的策略,也就是文件缓存在 PageCache 而磁盘块缓存在 BufferCache。
而实际上,大部分文件是通过文件系统呈现,而且存储在磁盘上,这就会导致同一份文件保存了两份,不优雅也不高效,为此,在 Linux 2.4 版本之后,就将两者进行了统一。
如果被缓存的数据即是文件数据又是块数据 (对于文件来说大部分的数据是的,元数据不是),此时 BufferCache 会有指针指向 PageCache ,这样数据就只需要在内存中缓存一份。当讨论磁盘缓存时,其实就是 PageCache ,它缓存了磁盘文件数据,从而提高 IO 的吞吐量。
当然,目前 BufferCache 仍然是存在的,因为还存在需要执行的块 IO。因为大多数块都是用来存储文件数据,所以大部分 BufferCache 都指向了 PageCache;但还是有一小部分块并不是文件数据,例如元数据、RawBlock IO,此时还需要通过 BufferCache 来缓存。
明白了这两套缓存系统的区别,就可以理解它们究竟都可以用来做什么了。
Page Cache
主要用来作为文件系统上的文件数据的缓存,常见的是针对文件的 read()/write()
操作,另外也包括了通过 mmap()
映射之后的块设备,也就是说,事实上 Page Cache 负责了大部分的块设备文件的缓存工作。
Buffer Cache
BufferCache 用来在系统对块设备进行读写的时候,对块进行数据缓存的系统来使用,例如格式化文件系统时。
总结
磁盘有逻辑 (文件系统) 和物理 (磁盘块) 两种操作,分别对应了 Page Cache 和 Buffer Cache 。简单来说,如果直接通过 read()/write()
等直接去操作文件,那使用的就是 Page Cache 缓存,而使用 dd 等命令直接操作磁盘块,就是 Buffer Cache 缓存。
注意,块 Block 的大小由所使用块设备决定,而页在 x86 上无论是 32 位还是 64 位都是 4K 。
测试
那么 free 命令中的 buffers 和 cache 是什么意思?
该命令读取的是 /proc/meminfo
文件中的数据,可以从是否有 available 判断是否为最新版本,对于老版本计算方式如下:
cache = Cached + SwapCached + SReclaimable;
available = MemFree + Buffers + cache
free
新版的 free 命令输出如下。
$ free -wm
total used free shared buffers cache available
Mem: 7881 3109 797 1113 309 3665 3310
Swap: 7936 260 7676
- buffers,表示块设备 (block device) 所占用的缓存页,包括了直接读写块设备以及文件系统元数据 (metadata) 比如 SuperBlock 所使用的缓存页;
- cached,表示普通文件系统中数据所占用的缓存页。
如上所述,该命令读取的是 /proc/meminfo
文件中的 Buffers 和 Cached 数据,而在内核中的实现实际上对应了 meminfo_proc_show()@fs/proc/meminfo.c
函数,内容如下。
static int meminfo_proc_show(struct seq_file *m, void *v)
{
si_meminfo(&i); // 通过nr_blockdev_pages()函数填充bufferram
si_swapinfo(&i);
cached = global_page_state(NR_FILE_PAGES) -
total_swapcache_pages() - i.bufferram;
if (cached < 0)
cached = 0;
seq_printf(m,
"Buffers: %8lu kB\n"
"Cached: %8lu kB\n"
// ... ..
K(i.bufferram),
K(cached),
// ... ..
}
如上计算 cached 公式中,global_page_state(NR_FILE_PAGES)
实际读取的 vmstat[NR_FILE_PAGES]
,也就是用于统计所有缓存页 (page cache) 的总和,它包括:
swap cache 主要是针对匿名内存页,例如用户进程通过 malloc() 申请的内存页,当要发生 swapping 换页时,如果一个匿名页要被换出时,会先计入到 swap cache,但是不会立刻写入物理交换区,因为 Linux 的原则是除非绝对必要,尽量避免 IO。
所以 swap cache 中包含的是被确定要 swapping 换页,但是尚未写入物理交换区的匿名内存页。
验证
如上,读取 EXT4 文件系统的目录会使用到 “buffers”,这里使用 find 命令扫描文件系统,观察 “buffers” 增加的情况:
# sync
# echo 3 > /proc/sys/vm/drop_caches
$ free -wk; find ~ -name "not exits file" >/dev/null 2>&1; free -wk
total used free shared buffers cache available
Mem: 8070604 3260408 3445852 1102588 5236 1359108 3418844
Swap: 8127484 300172 7827312
total used free shared buffers cache available
Mem: 8070604 3249764 3207336 1087716 250484 1363020 3417764
Swap: 8127484 300172 7827312
再测试下直接读取 block device 并观察 “buffers” 增加的现象:
# sync
# echo 3 > /proc/sys/vm/drop_caches
# free -wk; dd if=/dev/sda1 of=/dev/null count=200M; free -wk
total used free shared buffers cache available
Mem: 8070604 3244516 3486124 1094648 932 1339032 3451048
Swap: 8127484 300172 7827312
532480+0 records in
532480+0 records out
272629760 bytes (273 MB) copied, 0.612241 s, 445 MB/s
total used free shared buffers cache available
Mem: 8070604 3245032 3218528 1094868 267196 1339848 3427012
Swap: 8127484 300172 7827312
找个比较大的文件,然后通过 cat 命令读取,可以看到对应的 cache 会增加。
# sync
# echo 3 > /proc/sys/vm/drop_caches
# free -wk; cat /your/big/file/path; free -wk
total used free shared buffers cache available
Mem: 8070604 3244516 3486124 1094648 932 423384 3451048
Swap: 8127484 300172 7827312
532480+0 records in
532480+0 records out
272629760 bytes (273 MB) copied, 0.612241 s, 445 MB/s
total used free shared buffers cache available
Mem: 8070604 3245032 3218528 1094868 2354 1019960 3427012
Swap: 8127484 300172 7827312
mincore
在内核中有个 man mincore(2)
的系统调用,其实现在 mm/mincore.c
中,主要用来判断页面的状态,可以使用 fincore
工具查看。
mincore -- determine whether pages are resident in memory
最早是由 google 开发,不过已经是七八年前的事情了,现在几乎不再维护,可以从 github 上查找相关的代码。
去掉了异常处理之外,其处理过程大致如下。
fd = open(fname, O_RDONLY);
if (fstat(fd, &stat) < 0) {
file_pages = (stat.st_size + page_size - 1) / page_size;
vec = malloc(file_pages);
fmap = mmap(NULL, stat.st_size, PROT_NONE, MAP_SHARED, fd, 0);
if (mincore(fmap, stat.st_size, vec) != 0 ) {
if (vec[i] & 1) {
open()
获得文件描述符,fstat()
获取文件的长度,页面的大小可以通过系统调用获取,一般是 4K ,有了文件大小,就知道了,需要多少个 int
来存放结果。
mmap()
建立映射关系,mincore()
获取文件页面的驻留情况,从起始地址开始,长度是 filesize,结果保存在 vec
数组里,如果 vec[i] & 1 == 1
那么表示该页面驻留在内存中,否则没有对应缓存。
另外,一个不错的工具可以参考 vmtouch 。
回收 Cache
内核在内存将要耗尽时,会触发内存回收的工作,一般来说主要释放的是 Buffer/Cache 的内存,但是这种清缓存的操作也并不是没有成本。
理解 Buffer Cache 的作用,那么如果要清理缓存,那么必须要保证数据的一致性,所以一般在清理的时候同时会伴随这 IO 彪高。因为内核要对比内存中的数据和对应硬盘文件上的数据是否一致,如果不一致需要写回,之后才能回收。
手动触发
在系统中除了内存将被耗尽的时候可以清缓存以外,还可以使用下面这个文件来人工触发缓存清除的操作。
# echo 1 > /proc/sys/vm/drop_caches
其中的取值可以是 1
2
3
,代表的含义为:
1
清除 PageCache;2
回收 slab 分配器中的对象 (包括目录项缓存和 inode 缓存),slab 是内核中管理内存的一种机制,其中很多缓存数据实现都是用的 PageCache;3
清除 PageCache 和 slab 分配器中的缓存对象。
这部分内核代码位于 fs/drop_caches.c
里面。
fadvise
除了上述粗暴的方法外,Linux 还提供了 posix_fadvise()
系统调用,允许用户给 Linux 提建议。
#include <fcntl.h>
int posix_fadvise(int fd, off_t offset, off_t len, int advice);
其中比较常用的有两个选项,可以通过 posix_fadvise(2)
查看,信息摘抄如下。
POSIX_FADV_WILLNEED
The specified data will be accessed in the near future.
POSIX_FADV_DONTNEED
The specified data will not be accessed in the near future.
其中,其含义如下:
POSIX_FADV_WILLNEED
相当于说,这个文件在不久的将来要用,请准备好相应的页面(从磁盘读入内存),相当于预读。POSIX_FADV_DONTNEED
相当于告知 Linux ,这个文件不用了,直接回收掉吧,类似于 sync 操作。
在 PostgreSQL 中有如下的应用:
int FilePrefetch(File file, off_t offset, int amount)
{
#if defined(USE_POSIX_FADVISE) && defined(POSIX_FADV_WILLNEED)
int returnCode;
Assert(FileIsValid(file));
DO_DB(elog(LOG, "FilePrefetch: %d (%s) " INT64_FORMAT " %d",
file, VfdCache[file].fileName,
(int64) offset, amount));
returnCode = FileAccess(file);
if (returnCode < 0)
return returnCode;
returnCode = posix_fadvise(VfdCache[file].fd, offset, amount,
POSIX_FADV_WILLNEED); //预读
return returnCode;
#else
Assert(FileIsValid(file));
return 0;
#endif
}
int pg_flush_data(int fd, off_t offset, off_t amount)
{
#if defined(USE_POSIX_FADVISE) && defined(POSIX_FADV_DONTNEED)
return posix_fadvise(fd, offset, amount, POSIX_FADV_DONTNEED);
#else
return 0;
#endif
}
通过 fadvise
就能把某文件彻底赶出缓存,代码非常简单。
int clear_file_cache(const char *filename)
{
struct stat st;
if(stat(filename , &st) < 0) {
fprintf(stderr , "stat localfile failed, path:%s\n",filename);
return -1;
}
int fd = open(filename, O_RDONLY);
if( fd < 0 ) {
fprintf(stderr , "open localfile failed, path:%s\n",filename);
return -1;
}
//clear cache by posix_fadvise
if( posix_fadvise(fd,0,st.st_size,POSIX_FADV_DONTNEED) != 0) {
printf("Cache FADV_DONTNEED failed, %s\n",strerror(errno));
} else {
printf("Cache FADV_DONTNEED done\n");
}
return 0;
}
其中 vmtouch -e
以及 linux-ftools
中的 linux-fadvise
提供了类似的功能,实际最终调用的都是 posix_fadvise()
接口。
关于 posix_fadvise()
接口的内核实现,可以参考霸爷的 posix_fadvise 清除缓存的误解和改进措施 。
其它
stap
可以通过 stap 脚本查看是谁在消耗 Cache,不过配置起来比较麻烦,暂不介绍了,后面补充吧。
参考
一个不错的工具 linux-ftools,可以直接从 本地下载 。