差分
記憶管理
,/* 動的なメモリ領域確保 */
==== ページ ====
([https://www.ibm.com/support/knowledgecenter/ssw_aix_72/performance/multiple_page_size_support.html Multiple page size support])
</ref>
<ref>
x86-64(Intel 64)アーキテクチャーの場合2MBと1GBの拡大したページサイズが使えます。
([https://software.intel.com/sites/default/files/managed/a4/60/325384-sdm-vol-3abcd.pdf Intel 64 and IA-32 Architectures Software Developer’s Manual])
</ref>
ですが、多くのCPUの最小ページサイズが4KBなのでLinuxのページサイズはデフォルトで4KBとなっています。
==== MMU ====
[[File:Memory_fig2.png|thumb|left|300px|仮想記憶のモデル]]
プロセスに割り当てられているメモリリソースとしてのメモリのアドレスは仮想的なアドレス空間に割り当てられます。仮想アドレス空間での仮想アドレスは、実際の実メモリの物理的な意味でのアドレスとは一致しません。
仮想アドレス空間と実メモリアドレス空間のマッピングは、それを管理する MMU (Memory Management Unit) というハードウェアによってマップされています。
今時はMMU はCPUチップ上に組み込まれていますが、概念的にはCPUとは違うハードウェアです。
むかしはCPUの中に入っていなかったので、CPUとは別にMMUのチップ
<ref>
MC68851: paged memory management unit user's manual (1986) [http://portal.acm.org/citation.cfm?id=16723]
</ref>
を用意していました。
また広大なメモリ空間を必要としない組込み用途向けCPUにはMMUを持っていないものもあります。
実メモリに入りきらないものを外部記憶装置に書き出し、必要になったら実メモリに読み込もうというのが仮想記憶です。
実メモリアドレス空間より仮想アドレス空間よりが大きい状態になっていれば、実メモリ上にない仮想アドレスを要求することが発生します。
例えば32ビットアーキテクチャーの場合(2<sup>32</sup> / 4GB ) は仮想記憶空間として1048576(2<sup>20</sup>)のページを持つことになります。<ref>IA-32アーキテクチャで物理アドレス拡張(PAE)を使うことはひとまずおいておきます。</ref>もし512MBの物理メモリしか搭載していないマシンは内部で131072ページしか持っていません。
== ページング ==
=== ページフォルト ===
[[File:Memory_fig3.png|thumb|right|300px|ページフォルト時のモデル]]
ページフォルト(Page Fault)の発生をうけてオペレーティングシステムは実メモリ上に必要な記憶領域を確保しようとします。
実メモリのサイズは限られているわけですから、
必要であれば実メモリのページ内容を外部記憶装置に書き出し、
空きを作りそこに新しいページをマッピングします。
再度、その外部に書き出されたページをアクセスすることが発生したら、
該当のページを外部記憶装置から実メモリ上に読み込んできます。
ページを読み込むのがページイン (page-in)、
ページを書き出すのはページアウト(page-out)といいます。
;調べてみよう: 本来のスワップを持っているオペレーティングシステムには、どんなものがあるだろうか。
;補足: スワップの具体的な運用に関する私見 [[スワップの運用について考えてみる]]
=== Linuxのスワップ ===
さて、先ほど説明したように記憶空間はページと呼ばれる単位で分割されています。主記憶と補助記憶とのやりとりはページ単位で行われ、ページイン、ページアウトを行います。一般的にはハードディスク上にスワップ用のパーティションを取って、そこをスワップ先に指定します。多くの場合はインストール時にセットアップするようになっています。通常はパーティションをマウントするための情報を書く/etc/fstabにスワップパーティションの割り当ての記述が作られているはずです。
<pre>
/dev/hda3 none swap sw,pri=0 0 0
</pre>
スワップの状態は/proc/swapsを見るとわかります。下の例はサイズが1453872K (1.45GB)バイトである/dev/hda3パーティションがスワップに使われているという意味です。
<pre class="bash">
% cat /proc/swaps
Filename Type Size Used Priority
/dev/hda3 partition 1453872 0 -1
</pre>
たとえばswapf01という名前のスワップファイルを作ってシステムにスワップファイルとして登録する時は次のようにします。
<pre>
# /bin/dd if=/dev/zero of=swapf01 bs=8192 count=256
# /sbin/mkswap swapf01 2048
# sync
# swapon swapf0
</pre>
zswap[https://www.kernel.org/doc/Documentation/vm/zswap.txt]は、 Linux kernel version 3.15 から入った仮想メモリ圧縮 (Virtual Memory Compression)を利用したスワップで、外部記憶装置にページをスワップするのではなく、実メモリ上に作った圧縮ブロックデバイスにページをスワップする機能でです。実メモリ上にスワップできなくなった時に、これまでのスワップ機能で用意しているスワップ領域にスワップを始めます。
== 局所参照性 ==
[[File:Memory_fig4.png|thumb|left|300px|キャッシュのモデル]]
この考え方は仮想記憶に限らずCPUのキャッシュメモリなどでも使われています。
最近のCPUはキャッシュが512KB、1MB、2MB という具合にどんどん増えています。
これはメモリからデータをフェッチしてきて、CPU内のキャッシュにおき、そこをアクセスすることで高速化を狙います。
当然、外にあるメモリよりCPU内のキャッシュの方がアクセス速度は格段に良いですから、それだけ速度が出るということになります。
これを一般化してみると、
「データを高速にアクセスできるほど装置は容量当たりのコストが高いので、'''容量とコストを勘案し多段に装置を組合せることによって、リーズナブルに高速で大容量のアクセスできる記憶装置を用意'''することができる」
ということがいえるでしょう。
メモリ、仮想記憶、ファイルシステムなど色々な場面でこの考え方が現れます。
このアクセスが高速で小容量の装置と低速で大容量の装置との間でデータを移動させなければなりませんが、この時どのデータを選ぶかが問題になってきます。よく使われるデータは速い方へ、なかなか使われないデータは遅い方へ移すのが合理的です。ただし、「良く使われる」というのは過去の話ではなく将来の話だという所がポイントです。「良く使われるだろう」と判断するルールを決めなければなりません。このルールでよく使われるのがLRU (least recently used)です。LRUは直訳すると「最近、最も使われていないもの」ということで、簡単に言えば一番暇なものを入れ換えるという単純な話です。LinuxのページングもLRUのポリシーで行っています。 === プログラム中でのメモリ =動的なメモリ領域確保 ==
このプログラムは512MBメモリ領域をmalloc(3)を使いアロケーションしています。実行時の状況をpsで見てみると、VSZが525528、RSSが284となっています。VSZはVirtual を使いアロケーションしています。実行時の状況をpsで見てみると、VSZが526788、RSSが580となっています。VSZはVirtual memory Sizeのことで仮想記憶もふくめて全部のメモリサイズです。RSSはReal Set Sizeのことで、実メモリ(物理的なメモリ)で専有しているメモリサイズです。つまり、このプログラムは約523.5MBもの記憶領域を取っているにもかかわらず、実際の実メモリ上では284KBしか取られていません。 で専有しているメモリサイズです。つまり、このプログラムは約527MBもの記憶領域を取っているにもかかわらず、実際の実メモリ上では580KBしか取られていません。
次の ps コマンドを実行してみましょう。するとRSSでソートされて、USER / COMMAND / RSS / VSZの順で表示されます。
<pre class="bash">
$ ps -Ao user,args,rss,vsize --sort rss
USER COMMAND RSS VSZ root [keventd] 0 0 root [ksoftirqd_CPU0] 0 0
...
hironobu emacs20 -geometr 9076 11112/usr/bin/emacs23 15732 42140 canna hironobu gedit 23204 173232 hironobu /usropt/sbingoogle/chrome/chrome - 107380 359600</cannas 17540 19152pre>
使われていない分、実メモリが空くわけですから、その分を動的にデバイスなどへのキャッシュなどに割り当ています。そのキャッシュが大きければ大きいほど、全体のパフォーマンスはよくなります。実メモリが必要になればキャッシュに使っていた実メモリをプロセスに回します。これはLinuxだけの話ではなく、今日多くのオペレーティングシステムではこのように全体のパフォーマンスを上げるためにメモリを効率よく使うというメカニズムが取り込まれています。
;調べてみよう: vmstatで観察してみましょう。vmstatは仮想記憶のステータス観察するためのツールです。1秒毎に表示するオプションで動作させながら、先程のプログラムを改造し徐々に記憶を取るようなプログラムにして動かし、観察してみましょう。
<pre class="bash">
% vmstat 1 <- 1秒毎に表示
procs memory swap io system cpu
2 1 0 29964 1140 368 2128 316 124 79 310 485 817 37 9 54
...
</pre>
== コピーオンライトとその実際 ==
Linuxのカーネルではプロセスが親から引き継がれたメモリ領域や実行コードなどを引き継いでいても、そこに書き換えが発生するまで、実際のメモリ領域はとりません。書き込みがあって始めてメモリ領域を確保し、中身をコピーして別なものにします。このことをコピーオンライト(CoW: Copy on Write)といいます。このような仕組みにより、高速に新しいプロセスを生成したり、あるいはメモリの効率的な利用が出きるようにしています。 /proc/(プロセスid)/smapsは、カーネル内の該当プロセスのメモリ利用状況を表示するAPIです。それを使って実験したいと思います。dashはDebian版軽量シェルでシステムのシェルスクリプトを実行するのに使われます。さて、$$はシェル自身のプロセスidですので、/proc/$$/smapsとすると現在使っているシェルのメモリ利用状況がわかります。注目するのは Shared_Clean と Private_Clean の値です。Shared_Clean はシェアしているメモリです。Private_Clean は自らのメモリです。クリーンな、という意味は、まだそのページは変更されていない(書き込みがおこっていない)という意味です。まず最初、dashを起動してdashシェル環境でメモリ利用状況をみます。この時、引き継ぐものがないのでPrivate_Cleanが76kB (=Rssの値と同じ)、Shared_Cleanが0kBとなっています。次にdashの中でさらにdashを起動します。つまり最初のdashが親プロセスとなった新しいdashです。この上で両方の値をみるとPrivate_Cleanが0kB、Shared_Cleanが76kB(=Rssの値と同じ)となっています。つまり、子プロセス側となったdashのメモリは、この時点ではシェアしているものを使っていることがわかります。 <pre class= mmap"bash"> $ dash $ cat /proc/$$/smaps | head -9 08048000-08060000 r-xp 00000000 08: ファイルとメモリ 01 4825 /bin/dash Size: 96 kB Rss: 76 kB Pss: 76 kB Shared_Clean: 0 kB Shared_Dirty: 0 kB Private_Clean: 76 kB Private_Dirty: 0 kB Referenced: 76 kB $ dash $ cat /proc/$$/smaps | head -9 08048000-08060000 r-xp 00000000 08:01 4825 /bin/dash Size: 96 kB Rss: 76 kB Pss: 34 kB Shared_Clean: 76 kB Shared_Dirty: 0 kB Private_Clean: 0 kB Private_Dirty: 0 kB Referenced: 76 kB</pre> == mmap ==
$ cc mmap.c
$ ./a.out
[a][b][c][d][e][f][g]$
</pre>
glibcでのmalloc実装は/dev/zeroをオープンし一定領域をmmapで確保し、その領域から必要な分を切り出してプログラム側に返却しています。つまりmallocを呼び出したプログラムは/dev/zeroの領域を使っています。/dev/zeroは物理的なデバイスの実体を持たないので、カーネルが与えたメモリ空間になります。mmapはこんな使い方もされているという一例です。
;補足: 内容をアップデートして同期するにはメモリに書き込み後、msync(2)やmunmap(2)を呼ぶことが必要です。
=== 単一レベル記憶 ===
ファイルもメモリも、さらにデバイスすらも単純な1つの記憶空間として扱おうというコンセプトが単一レベル記憶です。
現在、オペレーティングシステムレベルでサポートしているのはIBM社のSystem iシリーズ
<ref>
IBMのサイトにあるIBM System iの単一レベル記憶の解説 : http://www-06.ibm.com/systems/jp/i/seminar/reconf/reconf1.shtml
</ref>
ぐらいしかないので、
インターネット上で説明を探すとIBM System iの機能=単一レベル記憶の定義みたいな説明しかありませんが、
実は、その歴史は古く1960年代に作られたMultics上に既に実装
<ref>
Multics and More VM
https://users.cs.duke.edu/~chase/cps210-archive/slides/multics-vm.pdf
</ref>
されています。
UNIXは単一レベル記憶を前提としているデザインはありませんが、現在のUNIX系のオペレーティングシステムはmmapを実装することによってファイルもメモリも、そしてデバイスも同様に扱うことができる利益を得ています。
例としては今時のLinuxやその他UNIX系のオペレーティングシステムでは実行バイナリやライブラリを動かすとき、一々ファイルの中身を読み込む動作をせず、実行するバイナリデータを内部でマップしてしまいます。
ですから古典的なUNIXで言われるような、「実行バイナリファイルをメモリに読み込み実行する」という表現は、少なくとも現在のLinuxの実行時の表現としては適切ではないという状況になっています。あとデバイスの利用例としては、malloc()のいくつかの実装
<ref>
The GNU Allocator
https://www.gnu.org/software/libc/manual/html_node/The-GNU-Allocator.html
</ref>
では内部で記憶領域を確保するとき/dev/zeroをmmapでオープンして使っています。
== 脚注 ==
<references/>
----
[[目次]]へ