差分

移動先: 案内検索

記憶管理

3,902 バイト追加, 2018年1月16日 (火) 10:07
/* Linuxのスワップ */
==== モデルと現実 ページ ====
プロセスからアクセスできるメモリリソースを考えてみましょう。単純なモデルにしてみると下の図のようにCPUリソースからメモリリソースに対しアクセスし読み書きする形になります。[[File:Memory_fig1.png|thumb|right|300px|シンプルなアクセスモデル]]
+----+プロセスからアクセスできるメモリリソースを考えてみましょう。 +-----+ |----| | CPU +---->|----| メモリ +-----+ |----| +----+単純なモデルにしてみると右図のようにCPUリソースからメモリリソースに対しアクセスし読み書きする形になります。
しかし、みなさんは今時の記憶管理はこのような単純なモデルではなく、物理的なメモリサイズよりも大きなメモリ空間を扱える仮想記憶があることは既に承知しているはずです。仮想記憶はメモリ空間をページと呼ばれる一定サイズのフレーム(区分)にし、そのページをハードディスクのような外部記憶装置と物理的なメモリである実メモリ間で書き出し、読み込みを行うことによって、実際の実メモリの容量よりも多くのメモリ空間を使えるようにしています。しかし、みなさんは今時の記憶管理はこのような単純なモデルではなく、物理的なメモリサイズよりも大きなメモリ空間を扱える仮想記憶があることは既に承知しているはずです。仮想記憶はメモリ空間をページと呼ばれる一定サイズのフレーム(区分)にし、そのページをハードディスクのような外部記憶装置と物理的なメモリである実メモリ間で書き出し、読み込みを行うことによって、実際の実メモリの容量よりも多くのメモリ空間を使えるようにしています。
書き込みや読み込みをする単位をページといい、サイズは32ビットアーキテクチャーのCPUは4KBのページサイズで、64ビットアーキテクチャーのCPUは8KBのものがほとんどです。この書き込みや読み込みをする単位をページといい、サイズは32ビットアーキテクチャーのCPUは4KBのページサイズで、64ビットアーキテクチャーのCPUは8KBのものがほとんどです。<ref>ただしCPUのアーキテクチャーによっては異なる場合があります。これらはCPUのアーキテクチャーに依存するので異なる場合があります。例えばIBM POWER5+ や POWER6 プロセッサーは64KBのページサイズも利用することが出来ます。[https://www.ibm.com/developerworks/community/wikis/home?lang=en#!/wiki/Welcome%20to%20High%20Performance%20Computing%20%28HPC%29%20Central/page/64KB%20pages%20on%20Linux%20for%20Power%20systems 64KB pages on Linux for Power systems (IBMサイト)]</ref>たとえばページサイズ4KBで512MB分のメモリチップを搭載しているマシンは、内部で131072ページを持つ計算になります。
==== 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を持っていないものもあります。
==== MMU ====
プロセスは仮想アドレス空間にアクセスしています。
MMU がアドレス変換をして仮想アドレス空間より実メモリアドレス空間をマップしてくれています。
これによってプロセスが連続したメモリ空間を確保することができるようになります。
仮想アドレス空間と実メモリアドレス空間のマッピングは、それを管理する MMU (Memory Management Unit) というハードウェアによってマップされています。今時はMMU はCPUチップ上に組み込まれていますが、概念的にはCPUとは違うハードウェアです。また広大なメモリ空間を必要としない組込み用途向けCPUにはMMUを持っていないものもあります。
実メモリに入りきらないものを外部記憶装置に書き出し、必要になったら実メモリに読み込もうというのが仮想記憶です。
実メモリアドレス空間より仮想アドレス空間よりが大きい状態になっていれば、実メモリ上にない仮想アドレスを要求することが発生します。
例えば32ビットアーキテクチャーの場合(2<sup>32</sup> / 4GB ) は仮想記憶空間として1048576(2<sup>20</sup>)のページを持つことになります。<ref>IA-32アーキテクチャで物理アドレス拡張(PAE)を使うことはひとまずおいておきます。</ref>もし512MBの物理メモリしか搭載していないマシンは内部で131072ページしか持っていません。
+----+ >+----+実メモリ
+---------+ |----| / |----|アドレス空間
|プロセス +-->|----|---->|----|
+---------+ |----| / +----+
|----|/ ^
仮想アドレス|----| |
空間+----+ |
| |アドレス変換
+->MMU-->--+
 プロセスは仮想アドレス空間にアクセスしています。MMUがアドレス変換をして仮想アドレス空間より実メモリアドレス空間をマップしてくれています。これによってプロセスが連続したメモリ空間を確保することができるようになります。ですが、プロセスはアクセスしているアドレスは連続していると見えていても、実メモリ上では不連続だったり、あるいは不連続な上に逆転している場合もありえますし、たぶん、実際に動いている最中はそうなっているでしょう。  実メモリに入りきらないものを外部記憶装置に書き出し、必要になったら実メモリに読み込もうというのが仮想記憶です。実メモリアドレス空間より仮想アドレス空間よりが大きい状態になっていれば、実メモリ上にない仮想アドレスを要求することが発生します。アクセス時はアドレス変換を行うためのキャッシュであるTLB アクセス時はアドレス変換を行うためのキャッシュであるTLB (Translation Lookaside Buffer)にアドレスを問い合わせます。MMUの中にTLBの機能が含まれているタイプ、あるいはCPUチップ上にMMUとは別にTLBを用意しているタイプなどがあります。いずれにしてもTLBにアドレスが見つからなかった場合、つまり実メモリ上に該当ページを見つけられないとMMUはシグナルを発生させます。これをページフォルトいいます。にアドレスを問い合わせます。MMUの中にTLBの機能が含まれているタイプ、あるいはCPUチップ上にMMUとは別にTLBを用意しているタイプなどがあります。いずれにしてもTLBにアドレスが見つからなかった場合、つまり実メモリ上に該当ページを見つけられないとMMUはシグナルを発生させます。これをページフォルトいいます。
== ページング ==
=== ページフォルト ===
 ページフォルトの発生をうけてオペレーティングシステムは該当のページを外部記憶装置、たとえばハードディスクから実メモリ上に読み込んできます。実メモリのサイズは限られているわけですから、必要であれば実メモリのページ内容を外部記憶装置に書き出します。ページを読み込むのがページイン、ページを書き出すのはページアウトといいます。   +----+ +----+実メモリ空間 +---------+ [[File:Memory_fig3.png|----thumb|-->right|----300px|---------+ページフォルト時のモデル]] |プロセス +-->|----| |----|<--+ |ページフォルト(Page Fault)の発生をうけてオペレーティングシステムは実メモリ上に必要な記憶領域を確保しようとします。 +---------+ |----| +----+ | V実メモリのサイズは限られているわけですから、 |----|---> X | 書き出し必要であれば実メモリのページ内容を外部記憶装置に書き出し、 仮想アドレス|空きを作りそこに新しいページをマッピングします。再度、その外部に書き出されたページをアクセスすることが発生したら、該当のページを外部記憶装置から実メモリ上に読み込んできます。ページを読み込むのがページイン (page----| ページ 読み込み|in)、 空間+ページを書き出すのはページアウト(page----+ フォルト | | [ハードディスク]out)といいます。
=== スワップ ===
本来のスワップの定義はメモリが少なくなるとプロセスが使っているメモリをまるごと補助記憶装置に移動させる方法です。ページングはページ単位ですからページングとスワップとは技術的には区別されます。その意味でLinuxはページングだけやってスワップはしません。ちなみにLinuxで仮想記憶が動き始めたのは1991年12月18日だそうです厳密にいえば本来のスワップの定義はメモリが少なくなるとプロセスが使っているメモリをまるごと補助記憶装置に移動させる方法です。ページングはページ単位ですからページングとスワップとは技術的には区別されます。その意味でLinuxはページングだけやってスワップはしません。<ref>mmちなみにソースコードmm/memory.cを見るとLinuxで仮想記憶が動き始めたのは1991年12月18日だそうです。mm/memory.c</ref>尚、 GNU/Linux 関連のドキュメントを読んでいると、あまりスワップとページングの言葉の使い分けを明確に区別しているよう感じではなく、仮想記憶でページング(paging)機能全体をざっくり指してスワップみたいな括りで呼んでいるような印象です。
;調べてみよう: 本来のスワップを持っているオペレーティングシステムには、どんなものがあるだろうか。
 
 
;補足: スワップの具体的な運用に関する私見 [[スワップの運用について考えてみる]]
=== 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>
どのファイルはハードディスクのパーティションであっても構いません。複数のハードディスクを同時に利用している時、スワップファイルを各々のハードディスク上に設定しスワップのプライオリティを同じ値にすることで、スワップディスクへの読み込み/書き込みなどディスクI/Oが分散させることができます。これはraid0のような効果となり高速化することになります。プライオリティを指定して、例えば高速なハードディスク、あるいはスワップファイルアクセス時にディスクスラッシングを避けるために作業しているハードディスクとは物理的に別のハードディスクをアクセスするようにするなどといったことも可能です。
== 局所参照性 ==
;補足: Linux 2.6.32でスワップについて試したことのメモランダム [[linuxのswapについて私が知っている二、三の事柄]]
局所参照性とは「プログラムは同じ場所を繰り返しアクセスする性質を持ちやすい」というものです。常に成立するわけではありません。もしプログラムがランダムにメモリ空間全域をアクセスする性質を一般に持っていたならば、たぶん仮想記憶のようなシステムは無意味だったでしょう。なぜならば大量のページイン・ページアウトが発生しまうからです。このように大量のページイン・ページアウトが発生してしまう状態をスラッシングと呼びます。しかし殆んどのプログラムは局所参照性を持つので、使われていない部分を外部に移しても全体としてみた場合、リーズナブルなパフォーマンスが得られているのです。== 局所参照性 ==[[File:Memory_fig4.png|thumb|left|300px|キャッシュのモデル]]
局所参照性とは「プログラムは同じ場所を繰り返しアクセスする性質を持ちやすい」というものです。
常に成立するわけではありません。
もしプログラムがランダムにメモリ空間全域をアクセスする性質を一般に持っていたならば、
たぶん仮想記憶のようなシステムは無意味だったでしょう。
なぜならば大量のページイン・ページアウトが発生しまうからです。
このように大量のページイン・ページアウトが発生してしまう状態をスラッシングと呼びます。
しかし殆んどのプログラムは局所参照性を持つので、
使われていない部分を外部に移しても全体としてみた場合、
リーズナブルなパフォーマンスが得られています。
この考え方は仮想記憶に限らずCPUのキャッシュメモリなどでも使われています。最近のCPUはキャッシュが512KB、1MB、2MBとどんどん増えています。これはメモリからデータをフェッチしてきて、CPU内のキャッシュにおき、そこをアクセスします。当然、外にあるメモリよりCPU内のキャッシュの方がアクセス速度は格段に良いですから、それだけ速度が出るということになります。
この考え方は仮想記憶に限らずCPUのキャッシュメモリなどでも使われています。
最近のCPUはキャッシュが512KB、1MB、2MB という具合にどんどん増えています。
これはメモリからデータをフェッチしてきて、CPU内のキャッシュにおき、そこをアクセスすることで高速化を狙います。
当然、外にあるメモリよりCPU内のキャッシュの方がアクセス速度は格段に良いですから、それだけ速度が出るということになります。
+------+ +----------+ +------------------------+
| CPU |<->|キャッシュ|<---->| メインメモリ |
+------+ +----------+ +------------------------+
これを一般化してみると、
「データを高速にアクセスできるほど装置は容量当たりのコストが高いので、'''容量とコストを勘案し多段に装置を組合せることによって、リーズナブルに高速で大容量のアクセスできる記憶装置を用意'''することができる」
ということがいえるでしょう。
メモリ、仮想記憶、ファイルシステムなど色々な場面でこの考え方が現れます。
これを一般化してみると、「データを高速にアクセスできるほど装置は容量当たりのコストが高いので、容量とコストを勘案し多段に装置を組合せることによって、リーズナブルに高速で大容量のアクセスできる記憶装置を用意することができる」ということになります。メモリ、仮想記憶、ファイルシステムなど色々な場面でこの考え方が使われます。
このアクセスが高速で小容量の装置と低速で大容量の装置との間でデータを移動させなければなりませんが、このアクセスが高速で小容量の装置と低速で大容量の装置との間でデータを移動させなければなりませんが、この時どのデータを選ぶかが問題になってきます。よく使われるデータは速い方へ、なかなか使われないデータは遅い方へ移すのが合理的です。ただし、「良く使われる」というのは過去の話ではなく将来の話だという所がポイントです。「良く使われるだろう」と判断するルールを決めなければなりません。このルールでよく使われるのがLRU この時どのデータを選ぶかが問題になってきます。よく使われるデータは速い方へ、なかなか使われないデータは遅い方へ移すのが合理的です。ただし、「良く使われる」というのは過去の話ではなく将来の話だという所がポイントです。「良く使われるだろう」と判断するルールを決めなければなりません。このルールでよく使われるのがLRU (least recently used)です。LRUは直訳すると「最近、最も使われていないもの」ということで、簡単に言えば一番暇なものを入れ換えるという単純な話です。LinuxのページングもLRUのポリシーで行っています。です。LRUは直訳すると「最近、最も使われていないもの」ということで、簡単に言えば一番暇なものを入れ換えるという単純な話です。LinuxのページングもLRUのポリシーで行っています。
== 動的なメモリ領域確保 ==
* プログラム: malloctest.c<syntaxhighlight lang='C' line="1" >
#include <stdlib.h>
main() { char *p; size_t areasize=1024*1024*512; if ((p=(char *)malloc(areasize)) == NULL) { perror("malloc"); } sleep(10); free(p); }</syntaxhighlight> * 実行例:  <pre class="bash">
$ cc malloctest.c
$ ./a.out &
---この並びは下の様になっています---
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
</pre>
次の 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>
keventdやksoftirqd_CPU0などRSSとVSZが両方0のものはカーネルレベルで動いているスレッドです。ここではユーザレベルのプロセスであるemacs20と/usr/sbin/cannaserverのメモリの使い方に着目しましょう。emacs20はRSSは9076KBで、仮想記憶も含めたサイズは11112KBです。多くの場合、十分に実メモリに余裕があっても、プロセス中で生成された記憶空間がすべて実メモリの上に載っているわけではありません。必要のあるもののみ実メモリ上にページインされます。CD-ROMから立ち上げるため仮想記憶のスワップ先がないKNOPPIXでも同じです。RSSもVSZも2つとも表示されて両者に差があります。keventdやksoftirqd_CPU0などRSSとVSZが両方0のものはカーネルレベルで動いているスレッドです。ここではユーザレベルのプロセスであるemacs23のメモリの使い方に着目しましょう。emacs20はRSSは15732KBで、仮想記憶も含めたサイズは42140KBです。多くの場合、十分に実メモリに余裕があっても、プロセス中で生成された記憶空間がすべて実メモリの上に載っているわけではありません。必要のあるもののみ実メモリ上にページインされます。
;調べてみよう: 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="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 ==
* プログラムmmap.c <syntaxhighlight lang='C' line="1" >#include <stdio.h>#include <unistd.h> #include <fcntl.h> #include <sys/types.h> #include <sys/stat.h> #include <sys/mman.h> main() { int fd; struct stat st; char *p; int i; fd=open("dat",O_RDWR); fstat(fd, &st); p=mmap(0,st.st_size,(PROT_READ|PROT_WRITE),MAP_PRIVATE,fd,0); for(i=0; i < st.st_size -1 ; i++) { printf("[%c]",p[i]); } close(fd); }</syntaxhighlight> * 実行<pre class="bash">
$ echo 'abcdefg' > dat
$ cc mmap.c
$ ./a.out
[a][b][c][d][e][f][g]$
</pre>
=== 単一レベル記憶 ===
ファイルもメモリも、さらにデバイスすらも単純な1つの記憶空間として扱おうというコンセプトが単一レベル記憶です。現在、オペレーティングシステムレベルでサポートしているのはIBM社のSystem iシリーズぐらいしかないので、インターネット上で説明を探すとIBM ファイルもメモリも、さらにデバイスすらも単純な1つの記憶空間として扱おうというコンセプトが単一レベル記憶です。現在、オペレーティングシステムレベルでサポートしているのはIBM社のSystem iシリーズ<ref>IBMのサイトにあるIBM System iの機能=単一レベル記憶の定義みたいな説明しかありませんが、実は、その歴史は古く1960年代に作られたMultics上に既に実装されています。iの単一レベル記憶の解説 : http://www-06.ibm.com/systems/jp/i/seminar/reconf/reconf1.shtml</ref>ぐらいしかないので、インターネット上で説明を探すとIBM System iの機能=単一レベル記憶の定義みたいな説明しかありませんが、実は、その歴史は古く1960年代に作られたMultics上に既に実装されています。 
UNIXは単一レベル記憶を前提としているデザインはありませんが、現在のUNIX系のオペレーティングシステムはmmapを実装することによってMulticsが持っていたファイルもメモリも、そしてデバイスも同様に扱うことができる利益を得ています。例としては今時のLinuxやその他UNIX系のオペレーティングシステムでは実行バイナリやライブラリを動かすとき、一々ファイルの中身を読み込む動作をせず、実行するバイナリデータを内部でマップしてしまいます。ですから古典的なUNIXで言われるような、「実行バイナリファイルをメモリに読み込み実行する」という表現は少なくとも現在のLinuxの実行時の表現としては適切ではないという状況になっています。あとデバイスの利用例としては、mallocUNIXは単一レベル記憶を前提としているデザインはありませんが、現在のUNIX系のオペレーティングシステムはmmapを実装することによってファイルもメモリも、そしてデバイスも同様に扱うことができる利益を得ています。例としては今時のLinuxやその他UNIX系のオペレーティングシステムでは実行バイナリやライブラリを動かすとき、一々ファイルの中身を読み込む動作をせず、実行するバイナリデータを内部でマップしてしまいます。ですから古典的なUNIXで言われるような、「実行バイナリファイルをメモリに読み込み実行する」という表現は、少なくとも現在のLinuxの実行時の表現としては適切ではないという状況になっています。あとデバイスの利用例としては、malloc()のいくつかの実装では内部で記憶領域を確保するとき/dev/zeroをmmapでオープンして使っています。mmapの機能の背景にある単一レベル記憶というキーワードは知っていて損はないでしょう。zeroをmmapでオープンして使っています。mmapの機能の背景にある単一レベル記憶というキーワードは知っていて損はないでしょう。
== 脚注 ==