Linux のページテーブルのサイズの見方と見積式

Linux Kernel 2.6 (x86-64) でのページテーブルのサイズの確認方法と見積式を調べてみた。
あっているか自信のないところもある&まだ書きかけ。

ページテーブルのサイズの見方

  • OS全体のページテーブルのサイズ
$ cat /proc/meminfo 
MemTotal:       16158544 kB
MemFree:        13134056 kB
(中略)
PageTables:        34428 kB ★ 34MB
  • プロセス毎のページテーブルのサイズ
$ cat /proc/10225/status # 10255 は PID
Name:	zsh
State:	S (sleeping)
Tgid:	10225
Pid:	10225
PPid:	10222
(中略)
VmPTE:	     124 kB ★ 124KB

ページテーブルのサイズの見積式

見積式
(プロセスが使用している物理メモリサイズ / 4KB(ページサイズ)) * 8bytes

正確には、PTE は 8bytes * 512 の単位で1セットで、x86-64 のページサイズは 4KB なので、以下の式になると思います。

ROUNDUP((プロセスが使用している物理メモリサイズ / 4KB(ページサイズ)) / 512 entry) * 4KB
= プロセスが使用している物理メモリサイズ / 512

Oracle Database でSGA(共有メモリ)が 1GB の場合、1プロセスが共有メモリに使うページテーブルのサイズは以下の通り。

( 1,073,741,824 / 512 ) = 2,097,152 = 2MB
絵で見てわかる見積式の仕組み

  • /proc//status を参照すると fs/proc/array.c の proc_pid_status() -> get_task_mm(task) -> task_mm(mm, buffer) という具合に呼ばれて、VmPTE が表示されます。
  • PageTableStructure - linux-mm.org WikiLinux Kernel のソースコードを参考にすると、上図のようになると思います。

補足説明

ページテーブルとは
  • メインフレームの時代からOSは仮想記憶という仕組みで、物理メモリ以上のサイズ(物理メモリ + スワップ領域)をメモリ領域として使えるようになっています。
    • Linux はデフォルトで、物理メモリ + スワップ領域を超えるサイズを仮想的に割当てができたはず(オーバーコミット)
  • 仮想記憶には仮想アドレス空間をページング方式(固定長で分割)とセグメント方式(可変長で分割)があり、ほとんどのOSはページング方式を採用していると思います。
  • ページング方式では、ページテーブルと呼ばれるデータ構造にアドレス変換テーブル(仮想ページ番号と物理ページ番号のマッピング情報)が格納されます。
  • ページテーブルはユーザ空間ではなくカーネル空間の領域です。ps や pmap などで見れるプロセスがユーザー空間で使用するメモリ領域には含まれません。
  • ページング方式などの仮想記憶はメモリ管理ユニット(MMU)と呼ばれるハードウェアで実現され、OSはその仕様に準じた実装をしています。
Linux のページテーブル
Oracle Database on Linux でのページテーブル
  • Oracle Database はマルチプロセスで共有メモリを使うため、SGA(共有メモリ)が大きく、セッション数が多いと、塵も積もれば山となるで、ページテーブルのサイズが大きくなります。
  • OS内の管理領域であるページテーブルに何〜何十GBのページテーブルを使うのはもったいないので、SGAが大きくセッション数がか多い場合は HugePages を使うとページテーブルのサイズが小さくなり、メモリを節約できます。
  • 通常のページは4KBですが、HugePagesでは2MBになります。512倍になるため、ページを管理するPTE の数が少なくなり、ページテーブルのサイズも小さくなります。

検証結果(追記予定)

SGA が1GB のインスタンスに100セッションの接続を張ると、1セッションで400KB弱、100セッションで 40MB 程度をページテーブルとして使いましたという検証結果を記載予定

  • 100セッション接続前のメモリ使用量とページテーブルのサイズ
  • 100セッション接続する
  • ページテーブルのサイズが大きくなる
  • 1プロセス当りのベージテーブルのサイズは●●KB程度
  • 100セッション切断するとベージテーブルは解放される

Linux Kernel 2.6.32.67 のソースコードより(引用部除く)

id:naoya さんのブログエントリでわかりやすく解説されているので、そのまま引用します。

/proc//status の出力の詳細を知る

/proc//status はプロセスのメモリ利用状況を詳細に出力するので、重宝します。各行の意味するところを正確に把握しておきたいところです。Linux カーネルソースの Documentation/filesystems/proc.txt に一応ドキュメントがありますが、残念ながら詳細な言及はありません。

そこで、ソースを見ます。少し古いですが、linux-2.6.23 のソースを見ていきます。/proc//status を read すると、fs/proc/array.c にある proc_pid_status() 関数が呼ばれます。

int proc_pid_status(struct task_struct *task, char * buffer)
{
    char * orig = buffer;
    struct mm_struct *mm = get_task_mm(task);

    buffer = task_name(task, buffer);
    buffer = task_state(task, buffer);

    if (mm) {
        buffer = task_mem(mm, buffer);
        mmput(mm);
    }
    buffer = task_sig(task, buffer);
    buffer = task_cap(task, buffer);
    buffer = cpuset_task_status_allowed(task, buffer);
#if defined(CONFIG_S390)
    buffer = task_show_regs(task, buffer);
#endif
    return buffer - orig;
}

引数の task は /proc//status で指定した PID のプロセスのプロセスディスクリプタ (task_struct 構造体)で、task->mm でメモリディスクリプタ (mm_struct 構造体) が得られます。status の出力で表示されているメモリ関連の行の値はメモリディスクリプタに収められています。
proc_pid_status() では get_task_mm(task) でメモリディスクリプタを取得し、task_mm(mm, buffer) でメモリディスクリプタ内から必要な値を取得し、出力を作っています。task_mm() は以下のような実装になっていました。

char *task_mem(struct mm_struct *mm, char *buffer)
{
    unsigned long data, text, lib;
    unsigned long hiwater_vm, total_vm, hiwater_rss, total_rss;

    /*
     * Note: to minimize their overhead, mm maintains hiwater_vm and
     * hiwater_rss only when about to *lower* total_vm or rss.  Any
     * collector of these hiwater stats must therefore get total_vm
     * and rss too, which will usually be the higher.  Barriers? not
     * worth the effort, such snapshots can always be inconsistent.
     */
    hiwater_vm = total_vm = mm->total_vm;
    if (hiwater_vm < mm->hiwater_vm)
        hiwater_vm = mm->hiwater_vm;
    hiwater_rss = total_rss = get_mm_rss(mm);
    if (hiwater_rss < mm->hiwater_rss)
        hiwater_rss = mm->hiwater_rss;

    data = mm->total_vm - mm->shared_vm - mm->stack_vm;
    text = (PAGE_ALIGN(mm->end_code) - (mm->start_code & PAGE_MASK)) >> 10;
    lib = (mm->exec_vm << (PAGE_SHIFT-10)) - text;
    buffer += sprintf(buffer,
        "VmPeak:\t%8lu kB\n"
        "VmSize:\t%8lu kB\n"
        "VmLck:\t%8lu kB\n"
        "VmHWM:\t%8lu kB\n"
        "VmRSS:\t%8lu kB\n"
        "VmData:\t%8lu kB\n"
        "VmStk:\t%8lu kB\n"
        "VmExe:\t%8lu kB\n"
        "VmLib:\t%8lu kB\n"
        "VmPTE:\t%8lu kB\n",
        hiwater_vm << (PAGE_SHIFT-10),
        (total_vm - mm->reserved_vm) << (PAGE_SHIFT-10),
        mm->locked_vm << (PAGE_SHIFT-10),
        hiwater_rss << (PAGE_SHIFT-10),
        total_rss << (PAGE_SHIFT-10),
        data << (PAGE_SHIFT-10),
        mm->stack_vm << (PAGE_SHIFT-10), text, lib,
        (PTRS_PER_PTE*sizeof(pte_t)*mm->nr_ptes) >> 10);
    return buffer;
}

この実装を見ることで、status の各行の意味は明確になるでしょう。

あるプロセスが利用しているメモリサイズを procfs 経由で調べる - naoyaのはてなダイアリー

VmPTE は以下の式で計算されていることがわかります。

(PTRS_PER_PTE*sizeof(pte_t)*mm->nr_ptes) >> 10);
512個 * 8bytes(PTEの1エントリのサイズ) * ページエントリのセットの数 
/*
 * entries per page directory level
 */
#define PTRS_PER_PTE	512
typedef struct {
	unsigned long pte;
} pte_t;
  • x86_64 では pte_t(long) は 8byte でした。
    • crash コマンドで確認
# crash
crash> struct pte_t
typedef struct {
    pteval_t pte;
} pte_t;
SIZE: 8
    • Cのプログラムで確認
$ cat pte_size.c 
#include <stdio.h>
void main(void) {
    typedef struct {
        unsigned long pte;
    } pte_t;
    printf("Size of pte_t: %ubytes\n", sizeof(pte_t));
}
$ gcc -m64 -o pte_size pte_size.c
$ ./pte_size 
Size of pte_t: 8bytes
  • mm/memory.c
    • nr_ptes はページテーブルを割当てるときにカウントアップしている。おそらく、4KBページ(8bytes * 512)割当てて1カウントアップしていると想定。
int __pte_alloc(struct mm_struct *mm, pmd_t *pmd, unsigned long address)
{
	pgtable_t new = pte_alloc_one(mm, address);
	if (!new)
		return -ENOMEM;

	/*
	 * Ensure all pte setup (eg. pte page lock and page clearing) are
	 * visible before the pte is made visible to other CPUs by being
	 * put into page tables.
	 *
	 * The other side of the story is the pointer chasing in the page
	 * table walking code (when walking the page table without locking;
	 * ie. most of the time). Fortunately, these data accesses consist
	 * of a chain of data-dependent loads, meaning most CPUs (alpha
	 * being the notable exception) will already guarantee loads are
	 * seen in-order. See the alpha page table accessors for the
	 * smp_read_barrier_depends() barriers in page table walking code.
	 */
	smp_wmb(); /* Could be smp_wmb__xxx(before|after)_spin_lock */

	spin_lock(&mm->page_table_lock);
	if (!pmd_present(*pmd)) {	/* Has another populated it ? */
		mm->nr_ptes++;
		pmd_populate(mm, pmd, new);
		new = NULL;
	}
	spin_unlock(&mm->page_table_lock);
	if (new)
		pte_free(mm, new);
	return 0;
}

前提

補足

  • 本エントリでは扱いませんが、HugePage を使うとTLBヒット率が向上します。

To Do

  • mm->nr_ptes の算出ロジックを調べる。
  • デマンドページングにより、物理メモリにマップされて初めてPTEを使うか確認する
  • Oracle Database での検証結果を書く
  • /proc/meminfo の PageTables の算出ロジックを Kernel のソースコードから調査する
    • PageTables にはページディレクトリのサイズも含まれるか
  • HugePages を使った場合の見積式を書く
  • Oracle Database の PRE_PAGE_SGA、LOCK_SGA について書く