背景
Linuxのページキャッシュ攻撃対策の影響は大きく、長期化する見通し【翻訳】
この記事を読んでページキャッシュについて色々調べてみようと思って書いてみました。
OSのメモリ管理機構に関連した内容となっています。
キャッシュとは
カーネルは、ディスク上のデータへのアクセスを高速に行うため、 read/writeの際にメモリ上にデータをキャッシュします。
キャッシュするメモリ上の領域を「ディスクキャッシュ」と 呼びます。
ディスクキャッシュには種類が2種類があります。それが「バッファキャッシュ」と「ページキャッシュ」です。
それぞれの概要は下記へ記します。
ちなみに、空きメモリがある限り、基本的にキャッシュはどんどん増加します。
今回は取り挙げませんがキャッシュと聞くとCPUでよく見る単語なのでそちらをイメージすることが多いと思います。
そちらに関しては他記事をご参照ください。
キャッシュヒットやキャッシュミスとかについての測定なんかについては下記がとても参考になりました。
http://int.main.jp/txt/perf.html
キャッシュの種類
ページキャッシュ
ページというのは Linux の仮想メモリの最小単位。
ページキャッシュは、ディスク上のデータをページ単位で一時的に 保存するために使用されるメモリでファイルの読み書きの高速化に 使用されます。
バッファキャッシュ
バッファキャッシュは、プロセスがディスク上のデータに アクセスする際、アクセスすべきデータのブロックを高速に見つけるために使用されるメモリ
slabキャッシュ
Slabキャッシュはディレクトリのメタデータ情報を格納するdentryやファイルのメタデータ情報を格納するinode構造体などをキャッシュしているカーネル内のメモリ領域
それぞれは/proc/meminfoで確認できる
$ cat /proc/meminfo | grep ^Cached
Cached: 461708 kB
$ cat /proc/meminfo | grep Slab
Slab: 200076 kB
mincore(2)
mincore() は、呼び出し元プロセスの仮想メモリのページがコア (RAM) 内に存在し、 ページ参照時にディスクアクセス (ページフォールト) を起こさないか どうかを示すベクトルを返します。
カーネルは、アドレス addr から始まる length バイトの範囲のページに関する存在情報を返します。
mincore - ページがメモリー内にあるかどうかを判定する
書式
#include <unistd.h>
#include <sys/mman.h>
int mincore(void *addr, size_t length, unsigned char *vec);
使用方法の例としてはファイルをプロセスの仮想メモリへマッピングして、そのアドレスを元にmincore(2)を実行。
その結果を見てファイルがページキャッシュにあるかを判断します。
ちなみに実装はmm/mincore.cを確認することで見ることができます。
static long do_mincore(unsigned long addr, unsigned long pages, unsigned char *vec)
{
struct vm_area_struct *vma;
unsigned long end;
int err;
struct mm_walk mincore_walk = {
.pmd_entry = mincore_pte_range,
.pte_hole = mincore_unmapped_range,
.hugetlb_entry = mincore_hugetlb,
.private = vec,
};
vma = find_vma(current->mm, addr);
if (!vma || addr < vma->vm_start)
return -ENOMEM;
mincore_walk.mm = vma->vm_mm;
end = min(vma->vm_end, addr + (pages << PAGE_SHIFT));
err = walk_page_range(addr, end, &mincore_walk);
if (err < 0)
return err;
return (end - addr) >> PAGE_SHIFT;
}
使用例
ファイルがメモリ上にあるかどうかは上記のfincoreというコマンドを用いて調べることができる。
ソースも公開されていて結構な人がforkして遊んでいるようです。
そこまで難しいわけでもないので簡単に作ってみました。
今回の検証では下記を使用してみます。
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <sys/mman.h>
#include <errno.h>
char *file_path;
size_t page_size;
long fincore(int fd, size_t length) {
void *file_mmap;
size_t page_index = 0;
size_t cached = 0;
unsigned char *vec;
size_t ret = -1;
page_size = getpagesize();
vec = calloc(1, (length+page_size-1)/page_size);
if ( mincore_vec == NULL ) {
perror("calloc");
}
file_mmap = mmap((void *)0, length, PROT_NONE, MAP_SHARED, fd, 0 );
if ( file_mmap == MAP_FAILED ) {
perror("mmap");
goto cleanup;
}
if ( mincore(file_mmap, length, vec) != 0 ) {
perror("mincore");
goto cleanup;
}
for (page_index = 0; page_index <= length/page_size; page_index++)
if (vec[page_index]&1)
++cached;
ret = (size_t)((long)cached * (long)page_size);
cleanup:
if ( file_mmap != MAP_FAILED )
munmap(file_mmap, length);
if ( vec != NULL )
free(mincore_vec);
return ret;
}
void print_date(size_t s, long c) {
fprintf(stdout, "------------------------------\n");
fprintf(stdout, "FILE_NAME : %s\n", file_path);
fprintf(stdout, "FILE_SIZE : %ld\n", s);
fprintf(stdout, "PAGE_SIZE : %ld\n", page_size);
fprintf(stdout, "TOTAL_PAGES : %ld\n", c/page_size);
fprintf(stdout, "CACHE_SIZE : %ld\n", c);
}
int main(int argc, char **argv)
{
int fd;
long cached;
struct stat st;
file_path = argv[1];
fd = open(file_path, O_RDONLY);
if (fd < 0) {
perror("open");
return fd;
}
if (fstat(fd, &st)) {
perror("fstat");
close(fd);
return -1;
}
cached = fincore(fd, st.st_size); // ①
print_date(st.st_size, cached);
// ページキャッシュに乗せる為にread(2)
char *buf = malloc(st.st_size);
size_t s = read(fd, buf, st.st_size);
cached = fincore(fd, st.st_size); // ②
print_date(st.st_size, cached);
close(fd);
return 0;
}
上記を適当にビルドし、引数としてファイルのパスを与えます。
ファイルについてはデータがない空ファイルだと面白みがないので適当にデータを書き込んでおきます。
下記例ではddを用いて4MBのファイルをディスクへ書き込んでいます。
# 4MBのファイル作成
$ dd if=/dev/zero of=/home/fileA bs=4096 count=1000
$ ls -lh /home/fileA
-rw-r--r-- 1 root root 4.0M 2月 14 21:25 /home/fileA
# ページキャッシュを解放
$ echo 1 > /proc/sys/vm/drop_caches
# 実行
$ ./a.out /home/fileA
------------------------------
FILE_NAME : /home/fileA
FILE_SIZE : 40960000
PAGE_SIZE : 4096
TOTAL_PAGES : 0
CACHE_SIZE : 0
------------------------------
FILE_NAME : /home/fileA
FILE_SIZE : 40960000
PAGE_SIZE : 4096
TOTAL_PAGES : 10000
CACHE_SIZE : 40960000
fileを作成してページキャッシュを解放しmincore(2)を実行。
その後にプログラム内でread(2)し再度mincore(2)を実行しています。
①の結果よりmmap(2)しただけでは仮想メモリへマッピングしただけで実際にファイルへの操作がない限りは
ページキャッシュに乗らないことが分かりました。
mmap(2)において、 ファイルの内容は、プロセスの仮想アドレス空間に直接リンクされるだけのようです。
キャッシュクリアの種類
システムを再起動せずにメモリをクリアする方法は3つあります。
正直ここら辺の仕組みはよくわかっていません。。。
ケースバイケースで使い分けましょう。
# ページキャッシュのみクリア
$ echo 1 > /proc/sys/vm/drop_caches
# dentryとinodeのクリア
$ echo 2 > /proc/sys/vm/drop_caches
# ページキャッシュとdentry、inodeのクリア
$ echo 3 > /proc/sys/vm/drop_caches
あたり前ですがtmpfs上に作成したファイルは上記を実行してもキャッシュがクリアされることはありません。
遅延書き込み
Linux が write(2) なり何なりでプロセスから書き込み要求を受け取ったあとページにそのページは汚れてますとフラグを立てます。
フラグを立てたらすぐプロセスに処理は戻ります。このフラグは後でブロック型デバイスに書き出す必要があることを表しています。
カーネルスレッドの pdflush が定期的に汚れたページを検索して汚れたページとブロック型デバイスと同期を取っています。
カーネルスレッドの検索間隔は下記で確認できます。自分の環境だとデフォルト5秒のようです。
$ sysctl vm.dirty_writeback_centisecs
vm.dirty_writeback_centisecs = 500
汚れたページは/proc/meminfoを確認することで見れます。
以下はddで適当にファイルを書き込んだ状態からsync(1)で明示的にsyncさせています。
関係ないですがsync3回の伝承ってのがあるので面白いので見てみてください。
sync; sync; sync; haltの伝承
$ grep "Dirty" /proc/meminfo
Dirty: 468 kB
$ sync
$ grep "Dirty" /proc/meminfo
Dirty: 0 kB
この辺は調べ次第別記事で書きたいと思ってます。
まとめ
カーネル内でどのような動きで実装されているのか実際に見るまでは行っていないので次回あたりはそこらへんも踏まえて書こうと思ういます。dirtyページの話なんかも踏まえて。
使用例
Linux初心者の基礎知識 224.メモリ管理(4)
naoyaのはてなダイアリー Linux のページキャッシュ
Linuxカーネル@wiki ページキャッシュについて
Linuxの備忘録とか・・・ メモリーマップ