この章では、Linux 2.4のページキャッシュについて説明します。ページキャッシュは、
名前が示しているように、物理ページのキャッシュです。UNIX の世界ではページキャッシュの考え方は SVR4 UNIX の登場により、データI/O操作でのバッファーキャッシュを置き換えるようにして良く使われるようになりました。
SVR4 ページキャッシュでは、ファイルシステムのデータキャッシュとしてのみ使われており、
したがって vnode 構造体とファイルのオフセットをハッシュパラメータとして使っていますが、
Linux のページキャッシュはより一般的に設計されているため、(以下で説明する)構造体 address_space を一つ目のパラメタとして使います。
Linux ページキャッシュは、アドレス空間の表記法に強く結び付けられているため、
ページキャッシュの働きを理解するには、少なくとも address_space の基本的な理解が必要です。
address_space はソフトウエア MMU の一種で、あるオブジェクト (例えば inode) のすべてのページを他の対応する値(典型的には物理ディスクブロック)へと対応づけています。構造体 address_spece は include/linux/fs.h
で以下のように定義されています。
struct address_space {
struct list_head clean_pages;
struct list_head dirty_pages;
struct list_head locked_pages;
unsigned long nrpages;
struct address_space_operations *a_ops;
struct inode *host;
struct vm_area_struct *i_mmap;
struct vm_area_struct *i_mmap_shared;
spinlock_t i_shared_lock;
};
address_spaces の働く方法を理解するには、これらのメンバの一部を見るだけでいいでしょう。
clean_pages
、dirty_pages
、locked_pages
は、この address_space に属している clean、dirty、locked のそれぞれのページについての双方向リンクリストです。
また、 nrpages
はこの address_space の総ページ数になっています。
a_ops
は、このオブジェクトのメソッドを定義しており、
host
は、この address_space が属する inode へのポインタとなっています。これは NULL にもなります。例えば、swapper の address_space の場合 (mm/swap_state.c
) がそれにあたります。
clean_pages
や dirty_pages
、それに locked_pages
、 nrpages
の用法は自明だと思いますので、 address_space_operations
構造体をしっかりみていきます。これは同じへッダファイルに定義されており、
struct address_space_operations {
int (*writepage)(struct page *);
int (*readpage)(struct file *, struct page *);
int (*sync_page)(struct page *);
int (*prepare_write)(struct file *, struct page *, unsigned, unsigned);
int (*commit_write)(struct file *, struct page *, unsigned, unsigned);
int (*bmap)(struct address_space *, long);
};
となっています。
address_space と ページキャッシュの基本的な考え方からは、->writepage
と ->readpage
に注目する必要があります。しかし、実際には ->prepare_write
と ->commit_write
にも注目する必要があります。
おそらくその名前からの連想だけで address_space_operations メソッドが何を実行するか、当てることができるでしょう。それでも、若干の追加説明は必要です。ファイルシステムデータ I/O での使われかたは、ページキャッシュへ通じる王道であり、理解するのにうってつけです。 多くの他のUNIX系のオペレーティングシステムと違い、Linux では、ページキャッシュによるデータ I/O に対して一般的なファイル操作( SYSV の vnode 操作のサブセット)が可能です。これは、read/write/mmap においてはデータが直接ファイルシステムと作用せず、可能な限りページキャッシュへの書き込みや読み込みによって行われることを意味しています。ページキャッシュは、ユーザがメモリにないページから読み込みたい場合やメモリが少ない場合にディスクへデータを書き込みたい場合に、データを実際の低レベルファイルシステムから取得しなければなりません。
読み込みパスでは、一般的なメソッドでは、最初に必要とするinode/インデックスタプルに対応するページを見つけようとします。
hash = page_hash(inode->i_mapping, index);
そしてどのページが実際に存在しているかテストします。
hash = page_hash(inode->i_mapping, index);
page = __find_page_nolock(inode->i_mapping, index, *hash);
もし存在しないときは新しいフリーページを割り当てページキャッシュへ追加します。
page = page_cache_alloc();
__add_to_page_cache(page, mapping, index, hash);
ページがハッシュされたら、->readpage
アドレス空間操作を使って、ページを実際にデータで埋め(ファイルはinodeのオープンインスタンス)ます。
error = mapping->a_ops->readpage(file, page);
最後にデータをユーザスペースにコピーします。
ファイルシステムに書くには2つのパスがあります。1つはマップ(mmap)に書き込むこと、もう一つは write(2) のようなシステムコールを用いることです。mmap の場合がとてもシンプルなことから、まず最初に取り上げることにします。 ユーザがマップを変更するとき、VMサブシステムはページを dirty とマークします。
SetPageDirty(page);
バックグランドでの活動として、あるいはメモリが少なくなったのを理由として、ページを解放しようとする bdflush カーネルスレッドは、dirty とマークされたページの ->writepage
を呼ぼうとします。->writepage
メソッドは、ページの内容をディスクに書出し、ページを解放しなければなりません。
2つめの書き込みパスは、 *とても*たいへん複雑です。ユーザが書き込む各ページに対して、基本的に以下のことを行います(コードの全体は mm/filemap.c:generic_file_write()
を参照)。
page = __grab_cache_page(mapping, index, &cached_page);
mapping->a_ops->prepare_write(file, page, offset, offset+bytes);
copy_from_user(kaddr+offset, buf, bytes);
mapping->a_ops->commit_write(file, page, offset, offset+bytes);
最初に、ハッシュされたページを見つけようとするか新しいページを割り当てようとします。そして、->prepare_write
アドレス空間メソッドを呼び出し、ユーザバッファをカーネルメモリにコピーし、そして最後に ->commit_write
メソッドを呼び出します。おそらく ->prepare_write
と ->commit_write
は、->readpage
と ->writepage
からは基本的に異なるものに見えるでしょう。これは物理 I/O を実際に求められた時だけではなく、ユーザがファイルを変更したときにはいつも呼び出されるからです。
これを扱う方法は2つ(あるいはそれ以上?)あります。一つめは page->buffers
ポインタを try_to_free_buffers(fs/buffers.c
) で使われている buffer_heads で埋めることで、物理 IO の遅延に Linux バッファーキャッシュを使います。
もう一つの方法は、ページを dirty に単に設定し、後の全てを ->writepage
にまかせることです。
ページ構造体に有効なビットマップが欠けているため、PAGE_SIZE
より小さいgranualityをもっているファイルシステムでは働きません。