プログラムについて知ろう

プログラムとは何か(4)~メモリ編~

  • このエントリーをはてなブックマークに追加

本記事は、プログラムとは何か(3)~進化したプログラミング言語の翻訳処理~からの続編です。

OSによって進化したプログラムの実行

プログラムの実行(run)

プログラム実行(run)の準備は、OS(カーネル)が、指定された実行ファイル(executable)を、メモリにロード(load)するところから始まります。

ファイル単位でロード

そしてOSは、ABI(Application Binary Interface)によって、プログラムの実行に必要なデータ用のメモリ領域を確保したり(プロセスの生成)、ライブラリなど追加のモジュールをロードしたり、様々な初期化処理をしたりします。

そして、プロセッサのプログラムカウンタを、エントリーポイント(entry point)と呼ばれるプログラムの先頭に切り替えることで、実行(run)が始まります。

マルチタスク

さらにコンピューターを無駄なくフル稼働させるため、同時に複数のプログラムをマルチタスク処理させる機能がOSに追加されました。

マルチタスク(multitask)とは、複数(multi)の仕事(task)を並行してこなすことを意味します。実際には同時処理をしていなくても、ごく短時間で切り替えて、まるで同時に進んでいるかのように見せている場合も、マルチタスクに含まれます。

マルチタスクOSでは、プログラムの処理に優先順位をつけ細かく切り替えることで、プロセッサに待ち時間/すきま時間を無くし、複数プログラム全体での総使用時間を短縮します。

アドレス指定でインプット
OSがタスクを切り替え

カーネルモード

マルチタスクOSでは、カーネルプログラムが全体の管理者としての特権を持ち、他のプログラムの実行を中断したり、切り替えたり、再開させたりします。

具体的には、プロセッサの全オペコードを使用できる特権モード(カーネルモード)を、コンピューター起動(ブート)直後、最初にカーネルプログラムが握ってしまいます。

ユーザーモード

そして、あとからカーネルプログラムによってロードされるそれ以外のプログラム(ユーザープログラム)には、他プロセスもしくはシステム全体へ影響する一部の強権的なオペコードを利用できなくしたユーザーモードしか与えなくします。

そうなるとユーザープログラムは、どうしても強権的なオペコードを実行したい場合に、必ずカーネルを呼び出して、カーネルの力を借りなければなりません。カーネルは、ユーザープログラムの持つ権限から実行可否を判定し、その時の全体状況を見て、他のプログラムとの実行優先順位を決め、コンピューター全体を安全かつ効率的になるよう稼動させているのです。

CPUモード
プロセッサの特権モードの概念図

システムコール

このユーザープログラムからのカーネルの呼び出しをシステムコールと言い、プログラミングの中で使うシステムコール用のライブラリをAPI(Application Programming Interface)と言います。

プロセス

メモリにロードされて、プロセッサでの実行対象の一つになったプログラムをプロセス(process)と言います。

マルチタスクOSでは、複数のプログラムからそれぞれのプロセスを生成でき、1つのプログラムから複数のプロセスを生成することもできます。

たとえば、ウェブサイトからA,B,C,D,Eの5個のファイルをダウンロードしたい時に、1個ずつ順番にダウンロードするより、同時並行でいっぺんにダウンロードできると便利です。

その場合、同一のダウンロードプログラムを使って、

  • Aダウンロードのプロセス
  • Bダウンロードのプロセス
  • Cダウンロードのプロセス
  • Dダウンロードのプロセス
  • Eダウンロードのプロセス

が、同時にそれぞれでダウンロードをできるというイメージです。

マルチタスクのイメージ
マルチタスクのイメージ
(正確な時間配分ではありません)

プロセスとメモリアドレス

仮想アドレス空間

連続記録こそ理想像

メモリアドレスは、メモリの記録領域の先頭から順に、通常、バイト単位で連続番号が振られています。

メモリのイメージ

そして基本的には、ひと続きのデータはひと続きのアドレスに記録されているのが理想的です。ひと続きのアドレスであれば、「データの先頭アドレスから何バイト分」という簡潔な指定でひと続きを一気に扱え、最小限の動作かつ最短時間で、必要データの読み書きが完了します。

ひと続きで記録

逆にひと続きのデータが、飛び飛びのアドレスに分散していたら、読取も書込も、効率が悪くなります。

分散して記録

でも実際は離散記録が必要

しかし、様々なデータを扱っていれば、すべてが同じ大きさというわけにはいきません。

例えば、メモリの100番地から7バイトのデータ「saku-jo」を、200番地から11バイトのデータ「keshimasuyo」を削除して、それぞれ7バイトと11バイトの空き地ができたとします。

メモリ断片化

そこへちょうど7バイトのデータや11バイトのデータがまたすぐ入れられれば良いのですが、あいにく5バイトのデータ「short」と13バイトのデータ「data-length13」しか無かったとします。そして、これらも早急にメモリに入れなければなりません。

そんな時、13バイトの「data-length13」はまとめて入れられなくても、11バイトの「data-length」と2バイトの「13」に分割して、 11バイトの空き地に 11バイトの「data-length」、7バイトの空き地に2バイトの「13」と5バイトの「short」を入れれば、丸く収まります。ただし、あとでひと続きの13バイトのデータ「data-length13」として使えるよう、それぞれのアドレスは記録しておく必要があります。

  • 100~104番地:short
  • 105~106番地:13
  • 200~210番地:data-length

しかし、たとえ記録として残しておいたとしても、ユーザープログラムにこの記録を見る対応を入れるのは困難です。なぜなら、このメモリ管理はその時のOS(カーネルプログラム)にしかできないからです。

ユーザープログラムで指定する仮想アドレス

そこで、ユーザープログラムとしては、そのままひと続きのアドレスを仮定してコンパイルします。このユーザープログラムにとってひと続きと仮定されたアドレスを仮想アドレスと言います。

カーネルによる離散記録から連続データへの復元

そして、ユーザープログラムはメモリアクセスのためにシステムコールでカーネルを呼び出します。呼び出されたカーネルは、仮想アドレスを実際のメモリアドレスに変換して、メモリへアクセスします。仮想アドレスに対して、実際のメモリアドレスを物理アドレスと言います。

カーネルがアドレス変換をしてくれることで、ユーザープログラムは分散した物理アドレスを知る必要がなくなるのです。

カーネルアドレス空間とユーザーアドレス空間

仮想アドレスがひと続きと仮定できるようにするためには、競合する他のプログラムの存在があってはなりません。そこで、仮想アドレスには、実行中のプログラム(プロセス)ごとに、そのプログラム専用の仮想アドレス空間が割り当てられます。

カーネルプログラム専用の仮想アドレス空間をカーネルアドレス空間、ユーザープログラム専用の仮想アドレス空間をユーザーアドレス空間と言います。

仮想アドレスと物理アドレス
仮想アドレス空間と物理アドレスの対応イメージ
(プロセスAのように、物理アドレス空間では散在していても、仮想アドレスは0番地からひと続き)

カーネルが割り当てるユーザーアドレス空間と物理アドレス

プロセスの生成では、カーネルによってユーザーアドレス空間が割り当てられ、そこにプログラムとデータのマシンコードが入れられます。これがプロセスの実体になります。

マルチタスクで実際には複数のプログラムがロードされている場合でも、仮想アドレス空間は、 各プログラムごとに必ず0番地から、そのプログラムが必要とする分だけ割り当てられます。

そして、カーネルによって、それらの実際の物理アドレスは、決められた共有領域以外では、決して重なることがないように振り分けられます。

共有領域: プロセス間のデータ共有手段として確保された領域

プロセス制御ブロック(PCB)

さらにカーネルは、各プロセス毎の管理情報リストであるプロセス制御ブロック=PCB(Process Control Block)を、カーネルアドレス空間に作ります。

PCBには、

  • プロセスID
  • そのプロセスの各状態値
  • アドレス情報
  • 稼働状況
  • 実行待ち順位

などが記録されています。

カーネルは、各プロセス毎に作られるこのPCBを使って、各プロセスの実行切り替えを行います。

コンテキストスイッチ

プロセスの実体とPCBとを合わせたプロセスの全情報をコンテキスト(context)と言います。

カーネルは、複数のプロセスが全体として効率良くコンピューターを利用できるように、実行対象のプロセスをしょっちゅう切り替えます。

つまり、プロセスが未完了で実行途中であっても、カーネル自身の実行処理を優先して割り込ませたり、他のプロセスに実行を切り替えたりします。

この時、中断したプロセスの実行をまた再開できるように、実行されていない間もメモリに保存しておくのがコンテキストということです。

このコンテキストを使ってプロセスを中断・切替・再開する仕組みをコンテキストスイッチと言います。

コンテキストスイッチは、特にシステムコールで呼び出されたカーネルに実行が移される場合や、ファイル入出力などの時間がかかる処理をプロセスが待つ間、他のプロセスにプロセッサを明け渡す場合などに行われます。

仮想記憶

メモリスワッピング

仮想アドレスと違い、物理アドレスはひと続きである必要はありませんが、読み書きの効率を考えると、やはりできるだけひと続きのほうが好都合です。しかし、物理アドレスが重ならないように、複数のプログラムをロードしていくと、すぐにまとまった空き領域が足りなくなるかもしれません。

そこで、メモリ上にある記録(プログラムやデータ)の中から、あまり使われていない記録部分をいったんストレージの一時保存専用領域に移し、これからロードするプログラムのためにその物理アドレス領域を空き領域に換えるしくみが取り入れられました。これをメモリスワッピングと言います。スワッピングの元の英語のswapは、「取り換える」という意味です。

セグメンテーションとページング

メモリスワッピングには、ストレージに移す記録部分の単位として、セグメントとページの2種類があります。

セグメント

セグメントは、実行ファイル形式のセクションのように、中身によって区分けされた記録単位です。セグメント単位で記録部分をスワップする方式をセグメント方式と言います。セグメント方式での仮想アドレスと物理アドレスの対応表をセグメントテーブルと言います。

ページ

それに対してページは、中身に関係なく固定サイズで、通常4,096バイト(アドレスにして12ビット)程度の大きさをとります。このページ単位で記録部分をスワップする方式をページング方式と言います。ページング方式での仮想アドレスと物理アドレスの対応表をページテーブルと言います。

現在のメモリスワッピングは、ページング方式が主流になっています。ただし、メモリスワッピングの方式としてはページング方式でも、セグメントの区分けをアクセス制限や共有のために利用している場合もあります。

メモリ管理ユニット(MMU)

MMUとTLB

MMUは、Memory Management Unit の略で、CPUに付属され、主に仮想アドレスと物理アドレスの変換を、OSと連携して行うハードウェアです。MMUは、TLB(Translation Lookaside Buffer)というページテーブルの一部コピーをキャッシュに持っていて、それを使って高速アドレス変換をします。キャッシュは、レジスタのように、通常のメモリよりも高速にアクセスできるメモリです。さらにMMUは検索アルゴリズムをハードウェア(結線論理)で実装しているため、超高速でアドレス変換をします。

ページフォールト

TLBにもページテーブルにも、アクセス先仮想アドレスが存在しない場合、MMUはページフォールトをOSに通知します。ページフォールトが通知された場合、アクセス先データがページアウト(メモリスワッピングでストレージに移されること)されている可能性があります。

ページイン + ページアウト = メモリスワッピング

その場合OSは、ストレージからメモリに対象ページを戻し(ページイン)、その物理アドレスでページテーブルを更新します。

さらに、OSがアクセス先データをページインするときに、メモリの空き領域が不足していた場合は、先に空き領域を作るために他のデータをページアウトします。(メモリスワッピング)

ダイレクトメモリアクセス(DMA)

入出力に重要なメモリ

コンピューターにつながる入出力装置は、キーボードとディスプレイの他にも、マウスやストレージ、ネットワーク機器など、様々なものがあります。

これらの入出力装置が、コンピューター本体側とつながる先は、メモリになります。

  • コンピューターに入力されたデータは、直ちにメモリへ保存されて、いつでもすぐ利用できるようになります。
  • コンピューターから出力するデータは、あらかじめメモリに用意されて、いつでもすぐ取り出せるようになっています。

入出力の末端にメモリを使用することで、送受信を速く正確にできるのです。

プロセッサを待たせる入出力

そして、メモリアドレス管理や送受信制御をするのは、やはりプロセッサの役割になります。

しかし、現在のプロセッサやメモリのデータ送受信能力は、周りの入出力装置の送受信速度より、ケタ違いに速いのです。入出力装置とメモリの間の送受信制御を超高性能なCPUにやらせていたら、他のどんな作業も超高速で処理できるCPUに、無駄な待ち時間ばかりを与えて、もったいないことになってしまうのです。

CPUの代理で入出力制御をするDMAC

そこで、コンピューターの処理の中心となるCPUとは別に、入出力装置側に送受信制御専門のプロセッサ(コントローラー)を設置しておきます。そして、そのコントローラーに送受信制御を任せ、CPUは入出力に関係しない処理を先に進める「分業体制」をとれるようになりました。このようにCPUを待たせず、入出力装置側から直接メモリに対して行う送受信制御をDMA(Direct Memory Access)と言います。また、このようなコントローラーのことをDMAC(DMA Controller)と言います。

入出力の最初と最後だけ関わるCPU

送受信制御はDMACに任せても、メモリ管理は、OS(カーネル)からCPUを通して一元的に行う必要があります。

入出力の開始

入出力を始める時、CPUは通常、1行のマシンコードをDMACに送ります。そのマシンコードの中身は、以下3項目です。

  • 読取(メモリ→入出力装置) か 書込(メモリ←入出力装置) か
  • 入出力データの先頭メモリアドレス
  • 入出力先の指定(以下の1. および、複数データの中から指定する場合は2. 3. も)
    1. 入出力装置の指定
    2. 入出力装置内の先頭アドレス
    3. データのバイト数

入出力の終了

入出力開始のマシンコードを送ったら、CPUは入出力データに関係しない処理を先に進めます。一方DMACは、CPUから受け取ったマシンコードに従って入出力処理を進めます。入出力処理が完了したら、DMACはCPUへ割り込み信号を送り、入出力の完了をOSに知らせます。(入出力割込)

まとめ

たくさんのプログラムをどんどんコンピューターで処理するために、プログラムの実行環境もマルチタスクに対応したOSが標準となりました。マルチタスクで複数のプログラムにメモリをうまく割り当てるために、プロセスと仮想アドレス空間ができました。さらに、メモリをたくさん使用していくと、メモリ不足の課題を解決するために、仮想記憶がつくられました。マルチタスクでコンテキストスイッチを円滑にするため、入出力をDMAコントローラーに任せるようになりました。

  • このエントリーをはてなブックマークに追加

コメントを残す

*

CAPTCHA