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 が表示されます。 - http://linux-mm.org/PageTableStructure と Linux Kernel のソースコードを参考にすると、上図のようになると思います。
補足説明
ページテーブルとは
- メインフレームの時代からOSは仮想記憶という仕組みで、物理メモリ以上のサイズ(物理メモリ + スワップ領域)をメモリ領域として使えるようになっています。
- 仮想記憶には仮想アドレス空間をページング方式(固定長で分割)とセグメント方式(可変長で分割)があり、ほとんどのOSはページング方式を採用していると思います。
- ページング方式では、ページテーブルと呼ばれるデータ構造にアドレス変換テーブル(仮想ページ番号と物理ページ番号のマッピング情報)が格納されます。
- ページテーブルはユーザ空間ではなくカーネル空間の領域です。ps や pmap などで見れるプロセスがユーザー空間で使用するメモリ領域には含まれません。
- ページング方式などの仮想記憶はメモリ管理ユニット(MMU)と呼ばれるハードウェアで実現され、OSはその仕様に準じた実装をしています。
Linux のページテーブル
- Linux で共有メモリを複数のプロセスから使うと、同じ共有メモリのアドレス空間について、各プロセスで個別にページテーブルを持つため、共有メモリサイズが大きく、共有メモリを使うプロセスが多いと、ページテーブルとして使うサイズが大きくなります。
- Solaris は ISM や DISM などにより、共有メモリのページテーブルをプロセス間で共有されるため、Linuxのようにページテーブルのサイズが大きくなることはありません(詳しくはSolaris で Oracle を効率的に稼働させるための仕組み | Oracle やっぱり Sun がスキ! Blog参照)。
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; }
前提
- ページサイズは4KB(HugePagesは使っていない)
- Linux Kernel 2.6.32.67
- CPU の命令セットアーキテクチャ(ISA)は x86-64
補足
- 本エントリでは扱いませんが、HugePage を使うとTLBヒット率が向上します。
参考
P.169
100MBのメモリ領域を使うプログラムでは25Kページを必要とし、ページテーブルはかなり大きなサイズになります。
P.171
64ビットアドレス空間の場合は、1ページが4KB(12ビットのアドレス)の場合は、2の52乗ページとなり各エントリは8バイトになるので、ページテーブルのサイズは2の55乗バイトとなり現在のPCのメインメモリ全部を使っても収まりません。
- あるプロセスが利用しているメモリサイズを procfs 経由で調べる - naoyaのはてなダイアリー
- Oracle Real Application Clustersのインストール前の作業
- http://linux-mm.org/PageTableStructure
- https://www.utdallas.edu/~zxl111930/spring2012/public/lec11-handout.pdf
- http://www.cs.columbia.edu/~krj/os/lectures/L17-LinuxPaging.pdf
- kernel/git/stable/linux.git - Linux kernel stable tree
- Paging - OSDev Wiki
- Linux のプロセスが Copy on Write で共有しているメモリのサイズを調べる - naoyaのはてなダイアリー
- LKML: "Kirill A. Shutemov": [PATCH 1/2] mm: rename mm->nr_ptes to mm->nr_pgtables
- wikipedia:ページング方式
- Solaris で Oracle を効率的に稼働させるための仕組み | Oracle やっぱり Sun がスキ! Blog