0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Linuxカーネル3.2のメモリ管理システム:Cleancache

Last updated at Posted at 2023-08-05

はじめに

Linuxカーネルに関する包括的な書籍は[1],[2],[3]などがありますが、その多くはバージョン2.x系(主に2.6系)のカーネルに基づいて書かれています。このシリーズでは、メモリ管理機構に焦点をあてて、バージョン3.x系以降の進化を解説します。私個人の勉強記録でもあり、[1]のようなコードに基づいたできるだけ詳細な解説をめざします。ドキュメントを参照しながら読んでいますが、誤り等あれば指摘していただけると嬉しいです。

本記事ではバージョン2.6系からバージョン3.2系での進化について解説します。取り上げるのはcleancacheという機構です1。本記事で解説するソースコードはバージョン3.2.29のものです。

目次

Cleancache

概要

cleancacheとは、page cacheをより有効に使うための機構です。使用されなくなった、あるいは優先度が低いと判断されたページフレームは通常PFRA (Page Frame Reclaiming Algorithm) によって回収されますが、cleancache機構を有効にしている場合、回収対象になったページフレーム中のデータはまずcleancacheと呼ばれる領域に保持されます。cleancache機構を有効にしたファイルシステムが次にデータをページインしたくなった場合、ファイルシステムはまずcleancacheを探し、該当のデータがあればそこから獲得します。これはメモリとswap areaの間をつなぐswap cacheの役割に似ていますね。

cleancacheの特徴は、短命であることです。cleancache中のページフレームはいつでもPFRAの回収対象になるため、cleancacheには決まったサイズがありませんし、カーネルもその大きさを知りません。cleancache機構がデータを保持する領域のことを 'transcendent memory' (略してtmem)と呼びます。transcendentは「超越した」というような意味で、この領域はメモリ上にありますが、カーネルから直接アクセスできないためにこのように呼ばれています。

フロントエンド実装

データ構造

カーネル側(フロントエンド)でcleancacheを実現するデータ構造として、cleancacheを操作するメソッド群をまとめる構造体であるcleancache_opsがあります。その定義はinclude/linux/cleancache.hに含まれ、以下のメンバからなります。

メソッド 概要
init_fs 新規プールの作成を行います。
init_shared_fs 新規プールの作成を行います。
get_page cleancacheからページを取得します。
put_page cleancacheにページを退避します。
flush_page cleancache内のページをフラッシュします。
flush_inode cleancache内の、inodeで指定されたファイルを構成するページをすべてフラッシュします。
flush_fs プールを破棄します。

各用語の意味は次節以降で説明していきます。

カーネルはcleancache_register_ops()を呼び出すことで、バックエンドで実装されたメソッド群をcleancache_ops型のcleancache_opsという変数に登録します。以降、カーネルはこれらのメソッド群を直接呼び出さず、ラッパー関数を通して呼び出します。例えばinit_fs()の場合は以下のようになります。

mm/cleancache.c
void __cleancache_init_fs(struct super_block *sb)
{
	sb->cleancache_poolid = (*cleancache_ops.init_fs)(PAGE_SIZE);
}
EXPORT_SYMBOL(__cleancache_init_fs);

したがって、これらのメソッドの具体的な実装を見るには別の箇所(バックエンドにあたるtmemの実装)をあたる必要がありそうです。cleancacheの導入時点では、tmemの実装としてはXenという仮想化ソフトウェアを用いるもの、zcacheというメモリ圧縮機構を用いるものの2種類が用意されました。Xenはtype 1(ベアメタル型)のハイパーバイザで、Xenによる実装を理解するにはXenのカーネルを読む必要があります。ここではあくまでlinuxカーネルを読みたいという方針からそれは避けて、zcacheによる実装を見ていきます。

zcacheの実装

zcacheそのものの主要な機構は、drivers/staging/zcache/zcache-main.cに実装されています。tmemの解説に入る前に、まずzcacheの実装について、zcache_compress()zbud_create()という2つの関数をとりあげて解説します。

データ構造

zcacheのデータ構造について解説します。

zcache内の物理ページは、リスト構造で管理されます。一つの物理ページは、リストの連結部、2つのヘッダ領域と一つの圧縮されたデータ領域から構成され、圧縮されたデータ領域は2つの物理ページのデータを格納できるようになっています。圧縮されたデータは、64バイトのチャンクの集まりとして並べられます。zcache内での同じ物理ページ内に圧縮された2つの物理ページのペアのことをbuddyからとってzbudと呼び、zcache内でのページをzbud pageやzbpgと呼びます。また、ヘッダ領域は圧縮されたそれぞれのデータに対応します。これはzbud_hdrという構造体が表現します。主要なメンバを以下に示します。

メンバ 概要
unit16_t client_id オブジェクトのあるtmemのクライアントID。
uint16_t pool_id オブジェクトのあるtmemのプールID。
struct tmem_oid oid オブジェクトID。
uint32_t index オブジェクトが格納するデータのインデックス。
uint16_t size 圧縮後のデータサイズ(バイト単位)。

zbpgを表現する構造体はzbud_pageです。

メンバ 概要
struct list_head bud_list zbpgのリストヘッダ。
spinlock_t lock ロック。
struct zbud_hdr buddy[2] buddyのヘッダ。

実際のzbpgでは、この直後に圧縮されたデータが続きます。

1つのzbpgは2つの物理ページのデータを持つことができると述べました。そこでzbpgは、格納する物理ページの数との関係から以下の3種類に分類され、それぞれ別のリストで管理されます。カッコ内は、そのリストの先頭に対応するstruct list_head型の変数名を示します。

  1. zbudをもたないもの (zbud_unused_list)
  2. zbudを1つのみ持つもの。zbudがいくつのチャンクを使用しているかによって、PAGE_SIZE/64通りの場合が考えられ、それぞれ別のリストで管理されます。(zbud_unbuddied[NCHUNKS])
  3. zbudを2つ持つもの (zbud_buddied_list)

2.で使用しているチャンク数ごとにリストを分けているのは、もう一つのzbudが到着したときにどのzbpgと組み合わせればよいかを決めるためです。これは後ほど解説します。

zcache_compress

zcache_compress()はデータを圧縮するための関数で、簡単に書くと以下のようになっています。dmemwmemはCPUごとに確保されていて、それぞれ圧縮結果の保存用、作業用のメモリ領域です。

drivers/staging/zcache/zcache-main.c
static int zcache_compress(struct page *from, void **out_va, size_t *out_len)
{
	char *from_va;
	from_va = kmap_atomic(from, KM_USER0);
	mb();
	ret = lzo1x_1_compress(from_va, PAGE_SIZE, dmem, out_len, wmem);
	*out_va = dmem;
	kunmap_atomic(from_va, KM_USER0);
	return 1;
}

lzo1x_1_compress()の詳細にはここでは踏み込みませんが、データ圧縮アルゴリズムの一つであるLZO1X-1アルゴリズムのLinux kernel内での実装です。ここでは先に進むために、zcache_compress()からreturnするとき、*out_vaは圧縮後のメモリ領域を指し、*out_lenには圧縮後のバイト数が格納されているということだけ言及しておきます。

zbud_create

zbud_create()は、圧縮されたデータを受け取り、そこからbuddyを作成して適切なzbpgに登録する関数です。まず以下のような処理を行います。

drivers/staging/zcache/zcache-main.c
	nchunks = zbud_size_to_chunks(size) ;
	for (i = MAX_CHUNK - nchunks + 1; i > 0; i--) {
		spin_lock(&zbud_budlists_spinlock);
		if (!list_empty(&zbud_unbuddied[i].list)) {
			list_for_each_entry_safe(zbpg, ztmp,
				    &zbud_unbuddied[i].list, bud_list) {
				if (spin_trylock(&zbpg->lock)) {
					found_good_buddy = i;
					goto found_unbuddied;
				}
			}
		}
		spin_unlock(&zbud_budlists_spinlock);
	}

zbud_unbuddied[i]は、すでにiチャンク使用されているzbpgのリストです。このループでは、圧縮したデータを格納する余裕のあるzbpgを、できるだけデータがぴったり収まるように(データを格納したときに、余りのチャンクができるだけ少なくなるように)探しています。そのようなものが見つかると、found_unbuddiedラベルへ飛びます。主要な処理は以下になります。

drivers/staging/zcache/zcache-main.c
found_unbuddied:
	ASSERT_SPINLOCK(&zbpg->lock);
	zh0 = &zbpg->buddy[0]; zh1 = &zbpg->buddy[1];
	if (zh0->size != 0) {
		zh = zh1;
	} else if (zh1->size != 0) {
		zh = zh0;
	}
	list_del_init(&zbpg->bud_list);
	zbud_unbuddied[found_good_buddy].count--;
	list_add_tail(&zbpg->bud_list, &zbud_buddied_list);
	zcache_zbud_buddied_count++;

init_zh:
	zh->size = size;
	zh->index = index;
	zh->oid = *oid;
	zh->pool_id = pool_id;
	zh->client_id = client_id;
	to = zbud_data(zh, size);
	memcpy(to, cdata, size);
	spin_unlock(&zbpg->lock);
	spin_unlock(&zbud_budlists_spinlock);

	return zh;

まずzbpgのヘッダを確認し、未使用のヘッダはどちらか調べます。次にzbpgをunbuddiedリストからbuddiedリストへと付け替えます。

その後のinit_zhラベルでは、未使用のヘッダを適切に書き換え、データをzbpgに登録しています。zbpgでは、ヘッダの直後に圧縮されたデータが続くことを思い出してください。zbud_data()は、カーネルのoffsetof()マクロを用いて引数として与えられたヘッダが0番目か1番目か調べたあと、その情報からデータを格納するべきアドレスを適切に計算します。この計算部分のコードが以下になります。

drivers/staging/zcache/zcache-main.c
	p = (char *)zbpg;
	if (budnum == 0)
		p += ((sizeof(struct zbud_page) + CHUNK_SIZE - 1) &
							CHUNK_MASK);
	else if (budnum == 1)
		p += PAGE_SIZE - ((size + CHUNK_SIZE - 1) & CHUNK_MASK);
	return p;

上のコードで、budnumzbud_budnum()という関数を用いて調べられたヘッダ番号です。ヘッダが0番目であればzbud_hdr構造体の直後に、1番目であればページ最後尾からsizeバイト戻ったところに(より正確には、チャンクサイズで並べるために64バイトアラインされます)配置するようになっていることがわかります。

このアドレスを指定してmemcpy()を呼び出せば、無事zbpgに圧縮データを格納できます。ここまでがfound_unbuddiedに飛んだあとの流れです。

最後に、unbuddiedリストから適切なzbpgを見つけられなかった場合をみてみましょう。このときは、zbpgを新しく確保する必要があります。

drivers/staging/zcache/zcache-main.c
	zbpg = zbud_alloc_raw_page();
	spin_lock(&zbud_budlists_spinlock);
	spin_lock(&zbpg->lock);
	list_add_tail(&zbpg->bud_list, &zbud_unbuddied[nchunks].list);
	zbud_unbuddied[nchunks].count++;
	zh = &zbpg->buddy[0];
	goto init_zh;

まずはunusedリストを探しますが、それでも見つけられなければpreload(後述)しておいた空きページを使用することになっています。ここまでがzbud_alloc_raw_page()の仕事です。いずれにせよ、zbpgを確保したあとはinit_zhラベルに合流し、zbud_create()は新しく登録したbuddyのヘッダzbud_hdrを返して終了します。

zcacheを操作する主要な関数は、他にzbud_free()(データの解放)、zbud_decompress()(データの解凍)などがありますが、ここではそれらの実装は省略し、tmem実装に進むことにします。

zcacheによるtmem実装

ここから、ここまで説明したzcacheがどのようにtmemの実装に利用されているのかを見ていきます。tmemはput, get, flush, flush_object, new pool, destroy poolといった操作群によって制御されます。tmemの核となるこれらの関数は、drivers/staging/zcache/tmem.cに実装されています。ここではtmem_put()をとりあげて解説します。

データ構造

cleancacheではプールID、ファイルキー、インデックスの3つ組をhandleと呼び、それによってデータを管理していたのでした。これはcleancacheを実現するバックエンドであるtmemでも同様です。ただしファイルキーはtmemではオブジェクトIDと呼ばれ、オブジェクトという外枠の中にデータがあるという構造になっていることに注意してください。

プールは、tmem内で最も大きなデータ構造です。ふつうは一つのファイルシステムなど、独立したページの集合ごとに用意されます。それぞれのプールはプールIDとさらにハッシュテーブルを持ち、ハッシュテーブルそれぞれのバケットは赤黒木によってオブジェクトを管理しています。プールを表現するtmem_pool構造体の主要なメンバを以下に示します。

メンバ 概要
struct list_head pool_list プールどうしをつなぐリストのヘッド。
uint32_t pool_id プールID。
bool persistent tmemの属性を示す(後述)。
atomic_t obj_count プール内のオブジェクト数。
atomic_t refcount プールへの参照カウント。
struct tmem_hashbucket hashbucket[256] オブジェクトを保持するハッシュテーブル。

struct tmem_hashbucketのメンバは以下です。

メンバ 概要
struct ro_root obj_rb_root オブジェクトを保持する赤黒木の根ノード。
spinlock_t lock 赤黒木に対するロック。

あるプールの中では、 tmemはオブジェクトという単位で管理され、赤黒木につながれています。これを表現するのが構造体struct tmem_objです。主要なメンバを以下に示します。

メンバ 概要
struct tmem_oid oid オブジェクトID。
struct tmem_pool * pool 親となるプールへのポインタ。
struct rb_node rb_tree_node オブジェクトが所属する赤黒木のノード。
struct tmem_objnode * objnode_tree_root objnodeの木の根ノードへのポインタ。
unsigned int objnode_tree_height objnodeの木の高さ。
unsigned long objnode_count objnodeの数。
long pampd_count pampdの数。

新しい用語がいくつか出てきました。pampdというのはPage-Accessible Memory Page-Descriptorの略で、実際にページのデータを保持しています。これらはradix treeのような木で管理されていて、handleのインデックスを6ビットずつに区切って、それをキーとして子をたどっていきます。一つのpampdは、それぞれradix treeの葉に対応します。このradix treeの中間ノードにあたるのがobjnodeで、struct tmem_objnodeという構造体で表現されます。

メンバ 概要
struct tmem_obj * obj 親となるオブジェクトへのポインタ。
void * slots[64] radix treeにおける子へのポインタを保持した配列。
unsigned int slots_in_use 使用済みのスロットの数。

また、オブジェクトID(struct tmem_oidが表現)は192bitの整数で、ファイルシステム内のファイルに対して一意なIDをつけられるように大きくとられています。

tmem_put

tmem_put()は、その名の通りデータをtmemにputする関数です。関数は簡単に書くと以下のようになります。

drivers/staging/zcache/tmem.c
int tmem_put(struct tmem_pool *pool, struct tmem_oid *oidp, uint32_t index,
		char *data, size_t size, bool raw, bool ephemeral)
{
	struct tmem_obj *obj = NULL, *objfound = NULL, *objnew = NULL;
	void *pampd = NULL, *pampd_del = NULL;
	int ret = -ENOMEM;
	struct tmem_hashbucket *hb;

	hb = &pool->hashbucket[tmem_oid_hash(oidp)];
	spin_lock(&hb->lock);
	obj = objfound = tmem_obj_find(hb, oidp);
	if (obj != NULL) {
		pampd = tmem_pampd_lookup_in_obj(objfound, index);
		if (pampd != NULL) {
			pampd_del = tmem_pampd_delete_from_obj(obj, index);
			(*tmem_pamops.free)(pampd, pool, oidp, index);
			if (obj->pampd_count == 0) {
				objnew = obj;
				objfound = NULL;
			}
			pampd = NULL;
		}
	} else {
		obj = objnew = (*tmem_hostops.obj_alloc)(pool);
		tmem_obj_init(obj, hb, pool, oidp);
	}
	pampd = (*tmem_pamops.create)(data, size, raw, ephemeral,
					obj->pool, &obj->oid, index);
	ret = tmem_pampd_add_to_obj(obj, index, pampd);
	spin_unlock(&hb->lock);
	return ret;
}

突然ですが、ページをtmemにputする際、すでにそのページに対応するhandleと同一のhandleを持つオブジェクトがtmem内に存在していたらどうすればよいでしょうか?一般的には2つのアプローチが考えられます。

  • すでにtmem内に保存されているデータを、新しいデータで上書きする
  • すでにtmem内に保存されているデータをフラッシュし、後続のget命令ではアクセスできないようにする

前者と後者の違いがわかりにくいと思いますが、コードを読むと後者は「そのデータを誰も指さないようにする」ということだと思います。tmem/zcacheは後者のアプローチをとっています。いずれにせよ、実際のputの前に、一度tmem内に同じhandleのオブジェクトがあるか探す必要があります。

drivers/staging/zcache/tmem.c
	hb = &pool->hashbucket[tmem_oid_hash(oidp)];
	spin_lock(&hb->lock);
	obj = objfound = tmem_obj_find(hb, oidp);

tmemのプール内では、オブジェクトはtmem_hashbucketというバケットからなるハッシュテーブルで管理されていて、各バケットは赤黒木でオブジェクトを検索できるようになっていたのでした。このコードでは、これから保存したいデータに対応するオブジェクトがもしすでに存在するとしたらここにある、というバケットを取得し、実際にオブジェクトがあるかどうかを調べています。

ここでもしオブジェクトが見つかれば、中のデータをフラッシュする必要があります。見つからなければ、データをしまうためのオブジェクトを新たに作成する必要があります。次の分岐がその処理にあたります。

drivers/staging/zcache/tmem.c
	if (obj != NULL) {
		pampd = tmem_pampd_lookup_in_obj(objfound, index);
		if (pampd != NULL) {
			pampd_del = tmem_pampd_delete_from_obj(obj, index);
			(*tmem_pamops.free)(pampd, pool, oidp, index);
			if (obj->pampd_count == 0) {
				objnew = obj;
				objfound = NULL;
			}
			pampd = NULL;
		}
	} else {
		obj = objnew = (*tmem_hostops.obj_alloc)(pool);
		tmem_obj_init(obj, hb, pool, oidp);
	}

オブジェクトが見つかった場合の処理について見ていきます。まずはオブジェクト内でさらに該当するインデックスのデータが保持されているか調べます。この検索部分がtmem_pampd_lookup_in_obj()ですが、これもtmem.cに実装されていて、tmem内のオブジェクトが構造体tmem_objnodeでradix treeを使ってページを管理していることを念頭におけば実装は難しくないので、省略します。

ここでさらにオブジェクトが該当するインデックスのデータを保持していることがわかった場合、pampdからデータをフラッシュする必要があります。tmem_pampd_delete_from_obj()では、radix treeをたどって該当のpampdからデータをフラッシュします(pampdがNULLを指すようにします)。その後、最終的にはkmem_cache_free()によってobjnodeが解放されます。

オブジェクトが見つからなかった場合の処理を見ていきます。この場合、まずオブジェクト用のメモリ領域を確保します。tmem_hostopsはのちに出てくるtmem_pamopsと同様にカーネルの初期化時に登録されるメソッド群です。*tmem_hostops.obj_alloc()に登録されているzcache_obj_alloc()は単純なmalloc()ではなくpreloadという仕組みを用いており、別のタイミングでCPUごとに確保しておいたオブジェクトをもらってくるような実装になっています。tmem_obj_init()は作成したオブジェクトをプール内の赤黒木に登録する処理などを行います。

いずれの場合も、最終的に変数objnewがこれからしまうページに対応するオブジェクトを保持しています。オブジェクトを作成したら、次はオブジェクト内にページのデータをしまうことになります。これはtmem_pamops.create()に任されています。createメンバに登録されているのはzcache_pampd_create()という関数で、drivers/staging/zcache/zcache-main.cに実装があります。これを見ていきましょう。

tmem_put()からzcache_pampd_create()を呼び出すときに4番目にephemeralという引数を渡していますが、zcache_pampd_create()はこれによって処理を大きく2つに分けます。引数ephemeralの意味を理解するために、PAM (Page-Accessible Memory)について少し補足します。PAMという概念が導入されたとき、開発者はephemeral(短命である)か、persistent(長く存続する)か2通りの使い方があると考えていたようです。PAMがもしephemeralであった場合、putは必ず成功しますが、getのときにはすでにputされた内容が失われていることも考えられ、失敗する可能性があります。一方persistentなPAMはサイズが決まっているためputは失敗する可能性がありますが、putが成功していればgetは成功します。ここで関数の中身に戻ると、zcache_pampd_create()はここで使っているPAMがephemeralであるかどうかで処理を分岐させている、ということになります。本記事冒頭で説明したとおり、cleancacheはephemeralです。したがって、ここではそちらの処理を見ていくことにします。ちなみにpersistentなPAMは、frontswapという別の機構で使用されています。

drivers/staging/zcache/zcache-main.c
	if (eph) {
		ret = zcache_compress(page, &cdata, &clen);
		if (ret == 0)
			goto out;
		if (clen == 0 || clen > zbud_max_buddy_size()) {
			zcache_compress_poor++;
			goto out;
		}
		pampd = (void *)zbud_create(client_id, pool->pool_id, oid,
						index, page, cdata, clen);
	}
out:
	return pampd;

PAMがephemeralである場合、すぐにzcache_compress()が呼び出されます。これはすでに説明しましたね。ここではデータを圧縮してからオブジェクトに格納しようとしています。cdataが圧縮後のメモリ領域を指し、clenには圧縮後のバイト数が格納されているということも思い出してください。

zcache_compress()から戻ったあとのコードを見ましょう。2つのif文は、いずれもエラー処理を行っています。zcache_compress()は、成功すると1を返しますから、1つ目のif文は圧縮が成功すると飛ばされます。zbad_max_buddy_size()は、zbudの片割れがzbpg内で持ってよいデータ領域の最大サイズを返す関数です。つまり2つ目のif文では、望ましいサイズにまで圧縮が行われたかをチェックしています。

チェックを通過すると、次にzbud_create()によってzbudを作成し、zbpgに登録します。これもすでに説明しました。さらにzbud_create()は、新しく作成したbuddyのヘッダzbud_hdrへのポインタを返すのでした。データ構造の節で、pampdはデータに対応すると説明しましたが、正確にはzbud_hdrを通してデータと結びついていたということがここで分かります。

zcache_pampd_create()から戻りましょう。ここまでで、オブジェクトを適切に初期化し、データを圧縮し、pampdを作成してデータを紐付けるところまで終えています。最後にtmem_pampd_add_to_obj()を呼び出して、pampdとオブジェクトを対応させれば完成です。この関数は、radix treeをたどって適切な葉ノードにpampdを追加します。必要があれば木の高さを増やす必要があるため、若干コードが長くなっていますが、やっていることは単純なので省略します。

以上でtmem_put()の説明を終わります。かなり長くなりましたので、ここで説明した範囲の関数のコールグラフをつけておきます。

zcacheによるcleancache実装

ようやくcleancacheにたどり着きました。cleancache機構は、drivers/staging/zcache/zcache-main.czcache_xxという関数群が実現していますが、その本質はtmemであり、ここまでの実装を理解していればそこまで難しくありません。ここではcleancacheにページを退避するzcache_put_page()をとりあげます。

zcacne_put_page

関数は簡単に書くと以下のようになります。

zcache-main.c
static int zcache_put_page(int cli_id, int pool_id, struct tmem_oid *oidp,
				uint32_t index, struct page *page)
{
	struct tmem_pool *pool;
	int ret = -1;

	pool = zcache_get_pool_by_id(cli_id, pool_id); 
	if (!zcache_freeze && zcache_do_preload(pool) == 0) {
		ret = tmem_put(pool, oidp, index, (char *)(page),
				PAGE_SIZE, 0, is_ephemeral(pool));
		zcache_put_pool(pool);
		preempt_enable_no_resched();
	} else {
		zcache_put_to_flush++;
		if (atomic_read(&pool->obj_count) > 0)
			(void)tmem_flush_page(pool, oidp, index);
		zcache_put_pool(pool);
	}
out:
	return ret;
}

この関数に入るときには、IRQが禁止されている必要があります。これは後述するzcache_preload()がメモリ確保を伴うためだと思われます。

この関数では、まずデータを対比するプールをzcache_get_pool_by_id()によって取得します。プールはプールIDで指定できるはずなのに、cli_idという引数も指定されているのはなぜ?と思うかもしれませんが、実はzcacheはKVM環境を想定してプールのさらに上にクライアントという分類を持っており、各クライアントが独立してプール群を持つことができます。ここではLOCAL_CLIENTという単一のクライアント(カーネルそのもの)しか扱わないので、特に気にする必要はありません。

zcache_do_preload()関数は、以降の処理で必要なメモリをこの段階で確保する(プリロードする)ための関数で、以降の処理の実行中に直接mallocを行わせないようにしています。プリロードの対象となるのは、

  • 物理ページ(zbud_create()が使用)
  • tmem_obj構造体(zcache_obj_alloc()が使用)
  • tmem_objnode構造体(zcache_objnode_alloc()が使用)

で、それぞれzcache_preload構造体のpageメンバ、objメンバ、objnodesメンバによって指されます。zcache_preload構造体はCPUごとに用意され、CPUごとに割り当てできるようになっています。

zcache_do_preaload()の内部ではページフレームアロケータ・スラブアロケータによる割り当てが行われています。引数としてpoolが渡されていますが、内部では使われていないので意図はわかりません。

いずれにせよ、プリロードが成功すれば次はtmem_put()が呼び出され、データをtmemのプールに退避します。データの圧縮などもここで行われているのでした。後続のzcache_put_pool()は、プールへのデータ退避完了による参照カウントの減算を行っています。

プリロードが失敗した場合、メモリが足りないのでtmem_put()を読んでもデータの退避ができません。この場合、tmem_put()を呼び出すことはなく、tmem_put()が行っていた重複オブジェクトからのデータのフラッシュとオブジェクトの解放だけをtmem_flush_page()を呼び出して行います。この場合、retには-1が格納されたままなので、zcache_put_page()の呼び出し元はエラーを検出することができます。

以上がzcache_put_page()の実装です。これらの関数は、zcache_cleancache_xx()というラッパー関数を通してcleancache_opsに登録され(フロントエンド実装の節を参照)、cleancacheから呼び出されて使用されます。

参考文献

  • Bovet, Daniel P., Cesati, Marco. Understanding the Linux Kernel, Third Edition. O’Reilly Media, Inc, 2006.
  • 高橋 浩和、小田 逸郎、山幡 為佐久。『Linuxカーネル2.6解読室』。ソフトバンククリエイティブ、2006。
  • Love, Robert. Linux Kernel Development, Third Edition. Addison-Wesley Professional, 2010.
  1. cleancache機構は、Linux 5.17で削除され、現在では使用されていませんが、カーネルの進化をたどるという目的から解説します。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?