Linux のメモリ管理
自分用メモです。
内容は何番煎じかわかりません……
x86 系 CPU のメモリ管理
x86 系のメモリ空間には大きく分けて 3 つあり、MMU によって以下のように変換される。
論理アドレス
|
| セグメンテーション機構
V
リニアアドレス
|
| ページング機構
V
物理アドレス
CPU で実行される機械語プログラムでは論理アドレスを扱っており、物理アドレスへの変換は MMU という別のハードウェアユニットによって行われる。
セグメンテーション機構
x86 系 CPU のプロテクトモードではセグメント機構を持つ。
i486 のセグメント機構下の論理アドレスは以下のような構造になる。
+------------------------+-----------------------------------------+
| セグメントインデックス | オフセット |
+------------------------+-----------------------------------------+
|-------- 12 bit --------|--------------- 32bit -------------------|
|------------------------------- 64bit ----------------------------|
上記セグメントインデックスをもとにセグメントを同定、それから offset 値を足し、リニアアドレスに変換される仕組み。
セグメントは、セグメントディスクリプタテーブルに定義される。
セグメントディスクリプタテーブルには以下の 2 種類がある。
セグメントディスクリプタテーブル種別 | 対応するレジスタ | 説明 |
---|---|---|
GDT | GDTR | システム全体で唯一のセグメントディスクリプタテーブル |
LDT | - | プロセスごとなどに持てるセグメントディスクリプタテーブル |
セグメントディスクリプタは複数の属性を持つ。
セグメントディスクリプタの要素 | 説明 |
---|---|
セグメントベース | そのセグメントの先頭のリニアアドレス |
リミット値 | そのセグメントの長さ |
S | システムフラグ(0 ならLDTなど重要なセグメント、1 なら普通のセグメント) |
タイプ | セグメントの種類(データ・コード・スタック) |
DPL | 0 ~ 3 の特権レベル。数字が低いほど高権限。 |
etc. |
セグメントは大きく分けて以下の 3 種類がある。
セグメント種別 | 対応するレジスタ | 説明 |
---|---|---|
コードセグメント | cs | 機械語プログラムが格納されたセグメント |
データセグメント | ds | プログラムが必要とするデータが格納されたセグメント |
スタックセグメント | ss | プログラムのローカル変数や、関数呼び出しの戻り先を格納するセグメント |
CPU は上記レジスタ cs, ds, ss の 3 つ(そのほか汎用セグメントレジスタ 3 つを合わせて計 6 つ)にセグメントを設定することで、プログラムを実行する。
CPU のレジスタ cs, ds, ss などは、セグメントディスクリプタテーブル内のセグメントディスクリプタを指し示すインデックスを保持する。(メモリのアドレスを保持するわけではない)
特にコードセグメントの DPL は重要である。
CPU の特権レベルとして CPL があるが、実行中コードセグメントの DPL が元となっている。
例えば CPU の特権レベル CPL が 2 の時は、DPL 2,3 のセグメントにはアクセスできるが、DPL 0,1 のセグメントにはアクセスできない。
ページング機構
Web上にたくさん説明があるので割愛。
Linux のメモリ管理
セグメンテーション機構
さて、上記 x86 のセグメンテーション機構を頑張って勉強したのだが、実は Linux は x86 のセグメント機能をほとんど使っていない。
Linux で基本的に利用されるコードセグメント、データセグメントは以下の 4 つのみである。
すべてのセグメントはリニアアドレスの 0 ~ 最大値までの区画を持つ。
もはや "セグメンテーション" していない。
セグメント名 | セグメントベース | リミット | S | タイプ | DPL |
---|---|---|---|---|---|
__USER_CS | 0x00000000 | 0xfffff | 1 | コード | 3 |
__USER_DS | 0x00000000 | 0xfffff | 1 | データ | 3 |
__KERNEL_CS | 0x00000000 | 0xfffff | 1 | コード | 0 |
__KERNEL_DS | 0x00000000 | 0xfffff | 1 | データ | 0 |
さらに、特権レベル(DPL/CPL)は、カーネルモードに対応する 0 と、ユーザモードに対応する 3 のみしか利用していない。
ページング機構
32bit の Linux ではリニアアドレス空間は以下のように分割される。
+------------------------+--------------------------+
| ユーザ用領域 | カーネル用領域 |
+------------------------+--------------------------+
0x00000000 0xc0000000 0xffffffff
x86 の Linux において、プロセスごとのメモリアドレス空間の分割や、仮想メモリの仕組みはもっぱらページング機構を使って実現している。
重要なのは、ページテーブルがプロセスごとに固有である、ということである。
Linux ではプロセスは task_struct で表現される。
そして、プロセスごとにページテーブルを持つ。
task_struct :プロセス
+--state
+--thread_inf
+--mm : mm_struct *
| +--mmap * :メモリリージョンリスト
| | +-- vm_area_struct
| | +-- vm_area_struct
| | +-- vm_area_struct
: : :
: +--pgd : pgd_t * :ページテーブル
: :
つまり、プロセスごとのメモリ空間の実現は、コンテキストスイッチの度にページテーブルを差し替えること(CR3 レジスタの内容を変更すること)で実現されている。
ちなみに、カーネル用領域に対応するページテーブルエントリの内容は、全プロセスのページテーブルで共通である。
プロセスごとのメモリ空間
プロセスがリニアアドレス空間上に確保した領域はメモリリージョンと呼ばれる。
プロセスは複数のメモリリージョンを持つ。
メモリリージョンは vm_area_struct 構造体で表現される。
プロセスは vm_area_struct のリストを task_struct->mm->mmap
に保持している。
malloc(3) や free(3)、mmap(2)、mummap(2) はこの vm_area_struct を作成する、削除する、拡大する、縮小する操作に他ならない。
malloc(3) や mmap(2) はメモリリージョンを確保するが、この時点では物理メモリは確保されない。
(物理メモリの確保を遅延する仕組みをデマンドページングと呼ぶ。)
実際にその領域に対して読み書きのアクセスがあったときに、ページフォールトとなってハードウェア割込みが入り、カーネルによって物理メモリ領域が確保され、ページテーブルの内容が変更される。
ページフォールト
ページフォールト時の動作は複雑である。
カーネルは、ページフォールトが、健全なアクセスによるものなのか、不正なアクセスによるものなのかを判断する。
-
mmap(2) でメモリリージョンが確保されたが、対応する物理メモリが確保されていない領域へのアクセスでページフォールトとなったときとき
-> 健全なアクセスとしてデマンドページングを行う -
メモリリージョン外へのアクセスでページフォールトとなったとき
-> 不正なアクセスとして SIGSEGV をプロセスに対して発行する
詳細は以下を参考されたい。
参考
- 詳解Linuxカーネル 第3版
- はじめて読む486
- https://naoya-2.hatenadiary.org/entry/20071008/1191824562