OSを自作してみる11 ~メモリ管理1:物理メモリ管理の実装とページングの有効化~



今回はいよいよOSでメモリ管理を行っていく。

※過去回の記事とソースコードは下記から入手できる
https://github.com/Shadow5523/osdev/blob/master/README.md
また、ここで使用するコードはGitHubからダウンロードできる。
https://github.com/Shadow5523/osdev/releases/tag/version0.10.0

目次

 
 

概要

いよいよメモリ管理をしなくちゃいけないところまで来てしまったので実装を行う。今回は物理メモリを4KB単位で割当、開放する方法と仮想メモリ管理の準備としてcr3レジスタにページディレクトリのベースアドレスを渡し、cr0レジスタのページングビットを1にして、ページングを有効化してみる。

物理アドレス管理では4KBを一つのメモリブロックとし、その管理状況をビットマップにして、メモリブロックに対応するビットを1にしたり0にしたりすることで割当済メモリ/空きメモリの使用状況を保持する。このビットマップはカーネルイメージのすぐ後ろに置かれることが多く、今回もそれにならってビットマップをカーネルイメージの後ろへ配置する。

物理メモリ管理が4KB単位での管理なのは、次回実装する仮想メモリ管理において詳しく解説しようと思うが、ページング方式の実装において、ページのサイズ、ページを表すPTEをまとめたページテーブルのサイズ、ページテーブルをまとめたPDEのサイズが4KB(4MBであることも)であるためで、そのほうが管理しやすいからである。

今回行うことの流れとしては。。。

  • カーネルのサイズを割り出す
  • メモリマップを元に、使用してもよいメモリとダメなメモリの割当と開放を行う(メモリの初期化)
  • 4KB単位でメモリの割当/開放を行う関数を作成する
  • ページングの有効化
  • の流れで実装していこうと思う。

     
     

    カーネルイメージのサイズを算出

    1. 現在のカーネルイメージのサイズを算出するため、linker.ldを以下のように編集する。

    「__kernel_start」と「__kernel_end」に代入している”.”は現在のアドレスの位置を表す特殊な表記で、これでカーネルイメージが始めるアドレスと終わるアドレスを保持することができる。
     
     
    2.「getmmap.h」「getmmap.c」を改造し、返り値に全メモリサイズを返すようにする。まずはヘッダファイルから変更していく。

     
     
    3.「getmmap.c」を以下のように変更し、メモリ全体のサイズを取得し、その値を返すように設定する。

     
     
    4.カーネルイメージの大きさを取得するプログラムを書く。まずヘッダファイル「get_ksize.h」を以下のように作成する。

    「linker.ld」で定義したシンボルをCのソースファイルで使用する場合でも、extern宣言を行えば問題なく使用することができる。「uint32_t get_ksize(void)」は実際にサイズを算出する関数のプロトタイプで、サイズをuint32_tで返す。
     
     
    5.次にカーネルイメージのサイズを算出するプログラム本体を書いていく。「get_ksize.c」を以下のように作成する。

    単純に、カーネルイメージ終わりアドレスから、始まりのアドレスを引いて、その差を返すようになっている。

     
     

    物理メモリを初期化

    1.物理メモリの初期化を行う。ヘッダーファイル「init_pmemory.h」を以下のように作成する。

    p_memory_infoは実際に物理メモリを管理するビットマップのステータスを保持する構造体となっており、メモリの空きブロック数や割り当て済ブロック数、ビットマップへのポインタが格納されている。
    また、この構造体の構造は下記サイトを参考にして作成した。

    http://softwaretechnique.jp/OS_Development/kernel_development06.html
     
     
    2.「memory/init_pmemory.c」を作成し、関数「get_system_mblocks()」を作る。

    ここではメモリ管理を行うビットマップの初期化を行う。この関数で受け取る「msize」は「getmmap()」で取得したメモリサイズを格納する。「allocated_blocks」メンバは割り当て済のメモリブロック数を、「free_blocks」は空きメモリブロック数を格納しており、この初期化時はすべてのメモリを割り当て済にし、空きメモリブロック数は0に設定している。「mmap」メンバはビットマップへのポインタであり、カーネルイメージのすぐ後ろを指すようにし、「mmap_size」メンバにはそのビットマップのサイズを格納している。
    ビットマップでは4kb毎のメモリブロックの使用状況が格納されており、0であれば空き、1であれば割り当て済となる。
    そして最後にビットマップのすべてのフィールドに1をセットし初期化は完了。
     
     
    3.次にビットマップに割り当てられているメモリブロックに対応するビットを0にしたり1にしたりする関数を作成する。

    「setmemory()」では割り当てたいメモリブロックのビットを特定し、or演算で元のビット構成を崩さないように該当ビットに1をセットする。同様に「clearmemory()」では開放したいメモリブロックに対応するビットに0をセットする。

    メモリブロックに対応するビットの算出の仕方は以下の通り
    1 << (ビット番号 % 32) 32で剰余算してるのは、ビットマップの範囲外に書き込まないようにするため。
     
     
    4.次に、メモリ初期化時にGRUBで取得したメモリマップをもとにして使用していいブロックとそうでないブロックを初期化する為に以下の関数を作成する。

    「pbitmap_free()」ではメモリブロックの割り当てを、「pbitmap_alloc()」では開放を行っている。引数で受け取るアドレスは取得したいメモリ領域の開始アドレスであり、もうひとつの引数のsizeは取得したいメモリのサイズが入っている。メモリブロックが4kb単位となっているので、アドレス/サイズともに4096で割って対応するメモリブロックへと変換する。その後、「setmemory()」「clearmemory()」を呼び出し、pm_infoのメンバの割り当て済ブロック数/開放済ブロック数を増減させ、割り当て/開放をforループで割り当てたいブロックぶんだけ繰り返す。
     
     
    5.物理メモリを初期化させるパーツが揃ったので、それらを呼び出し初期化を行う関数を作成する。

    この関数では、GRUBで取得したmultiboot_info_t構造体とメモリの総量のサイズを引数に取る。
    multiboot_info_t構造体のポインタは現在のメモリマップの状態を指しており、これをもとにアドレス領域毎にメモリの割り当て/開放を行っていく。メモリタイプが「0x1」か「0x3」は使用してもOKなメモリ領域なので「pbitmap_free()」を呼び出して開放し、それ以外は「pbitmap_alloc()」を呼び出して割り当て済とする。

    またここでは、カーネルイメージが配置されているメモリ領域も使用可能メモリ領域として認識してしまうため、メモリの開始領域(send_addr)がカーネルイメージの開始位置と同じ(&__kernel_start)なら、カーネルイメージのサイズ分 + メモリマップのサイズ(メモリマップはカーネルの直後に配置されるように設定したため)だけメモリを割り当て済とし、それ以外を開放するようにした。

     
     

    4kb単位でメモリの確保/開放する関数を作成

    1.新規にヘッダファイル「pmalloc.h」を以下のように作成する。

    物理メモリの情報を格納する構造体「pm_info」は外部で宣言、初期化されてるのでextern宣言する。
     
     
    2.次に「memory/pmalloc.c」を作成する。まずは割り当てたいメモリブロックがすでに割当られてないか確認する関数「findfreememory()」を作成する。

    最初は、for文で32ビットずつ大雑把に検索をかけ、一つでもビットマップにゼロがあったら、対象の32ビットを1ビットずつ検索し、空いているメモリブロックを検索する。無事空いていたら引数で受け取ったポインタに対象のメモリアドレスを格納し1を返す。割当に失敗したら、-1を返すようにする。
     
     
    3.4kbのメモリを確保する「malloc4kb()」を作成する。

    この関数は呼び出されると、まず空きブロックがすでに無いか確認し、なければNULLを返す。もし、空きブロックがあれば、先程の「findfreememory()」で空きメモリブロックの位置を探し出し、そのメモリブロックが割り当てられているbitにフラグを立てて、割当済とする。最後にpm_info構想体の割当済メモリブロック数、空きブロック数を変更し、割り当てた物理メモリを返す。
     
     
    4.次にメモリを4kb単位で開放する関数「free4kb()」を作成する。

    この関数は、受け取った物理メモリを4096で割ってメモリブロックを特定し、「clearmemory()」で割り当てたフラグを取り除きメモリを開放する。最後に「malloc4kb()」と同様にpm_info構造体の割当済/空きメモリブロック数を更新し、処理を終了する。

    ここまでで、物理メモリの管理が完了した。次にページングの有効化を行う。

     
     

    ページング有効化

    1.まずはヘッダーファイル「include/init_vmemory.h」を以下のように作成する。

    定数で宣言している「PE_PFLAG」と「PE_RWFLAG」はPTE/PDEで使用するフラグ。
    下位1ビット目がPフラグと呼ばれており、ページ、またはページディレクトリが物理メモリにロードされているかを設定する。1であれば物理メモリ上に存在しているが、0であると物理メモリ上にないとCPUは判断してしまい、ページフォールト例外が発生する。

    下位2ビット名がR/Wフラグを呼ばれており、ページやページディレクトリに読み取り/書き込み属性を設定する。1がセットされている場合は読み書きができ、0であれば読み込み専用となる。

     
     
    2.ページングを有効にする関数をGASで記述する。「paging.s 」を作成する。

    ここでは、CR3レジスタにページディレクトリのベースアドレスを書き込み、その後、cr0レジスタをeaxレジスタと0x80000000とを、OR演算した値をcr0に書き込むことにおよってページングが有効になる。
     
     
    3.ページディレクトリ, ページテーブルを初期化する関数「init_paging(void)」を作成する。

    先程作成した「malloc4kb()」でページディレクトリ、ページテーブル用のメモリを確保し、初期化を行う。
    その後、ページディレクトリの要素には読み書き可能にするためにフラグをセットし、ページテーブルには仮想アドレスにPフラグ、RWフラグを加えたものをセットする。もし空きメモリブロックがゼロであればRWフラグは読み取りでフラグを立てるようにした。

    次にページディレクトリの先頭要素に、ページテーブルの先頭アドレスと、Pフラグ、RWフラグをセットし「enable_paging」にページディレクトリの先頭アドレスを送りページングを有効化している。

     
     
    4.「init_paging()」を呼び出し、正常に仮想メモリが初期化できたか調べる。

     
     
    5.そして「kernel.c」「kernel.h」を以下のように修正し、起動時に物理メモリが初期化されるようにする。また、今回追加したファイルを「Makefile」へ記述しコンパイル/アセンブルされるようにする。

     
     

    動作確認

    VMWareなどで実行してみて、CR0レジスタの値が0x8000〜になってたらページングは有効になっている。

     
    長くはなったけど、今日はここまで。。。
    次回は仮想メモリと物理メモリのマッピングを行う。

    Leave a Reply

    Your email address will not be published. Required fields are marked *