もろず blog

もろちゃんがITに関しての様々なトピックを解説します


イケてるエンジニアになろうシリーズ 〜メモリとプロセスとスレッド編〜



f:id:chanmoro999:20140911012444p:plain


最近はとても便利なプラットフォームがいっぱいあって、
詳細な中身を意識しなくても簡単に使えるという素晴らしい時代ですが、
深くまで突っ込んで考えないといけない場面が たまーーーーーーーに でてきます

その時になって初めて、
誰かが用意してくれた快適な環境を使っていただけで自分では何も解決できない
という現実を叩きつけられるわけです


大げさに言いましたすみません


ちょっと前に仕事でサーバーのパフォーマンス調査をする機会があり、
その中で "プロセス" と "スレッド" って一体何が違うの!? って疑問にぶち当たりました

わかるような、わからないような


何となくわかったつもりになっている事でもそろそろちゃんと理解して、
今日からイケてるエンジニアになりましょう



この記事では
1. マルチタスクOSとプロセス
2. メモリの役割
3. 物理アドレスと仮想アドレス空間
4. プロセスがメモリに保持している情報
5. プロセスとスレッドの違い
6. パフォーマンスの違い
7. まとめ
について説明します


今回は内容が盛りだくさんなので、
死ぬほどヒマなときに読んでみてください

(参考読了時間 15分)


※全体的にUNIXLinuxベースのOSでの話ですが、Windows であっても思想はだいたい同じです



1. マルチタスクOSとプロセス

僕らが普段使う PC、スマホやサーバーで利用される OS のほとんどは、マルチタスクと呼ばれる機能を持っています

この機能があるおかげで、音楽を聞きながらインターネットを見てる時に電話の着信が来るように、複数のアプリが同時に動くことができます

今やあまりにも当たり前すぎてそんなことを意識することがまずないですよね


さて、そんなマルチタスクの機能は "同時に動いているように見せているだけ" というようによく言われています

これはマルチタスクの OS であっても、厳密には CPU の数しかプログラムを同時に実行することはできないからです


CPU が1つであればそれぞれの瞬間には1つのアプリケーションしか動いていません

マルチタスクの機能によって 実行するアプリケーションをものすごく細かく切り替えることで "同時に動いているように見せる" ことができます
※でもホントに同時に動いてるように見えるのは、いろんな所にあるバッファのおかげです


このとき、実行されているアプリケーションの1つ1つは "プロセス"という名前で管理されます

なので、マルチタスクOS は「実行するプロセスをものすごく細かく切り替えている」ということになります


記事の先頭に戻る

2. メモリの役割

最近の PC はどんどん搭載するメモリが大容量化していて、64ビットOSであれば 8GBや16GBなんていうのも普通になっています

これはスマホも同じで、ついに発表された iPhone6 は 1GB のメモリを搭載しているそうです

そんな、よくコンピューターの性能を比較するときに登場する "メモリ"
これは一体なにものなんでしょうか?
※RAM、ROM がありますが、ここでは RAM の話です


メモリにはコンピューターが動作するための様々な情報が保持されます

  • 画面に表示する画像
  • キーボードから入力された文字
  • ハードディスクやUSBメモリに保存したファイル
  • 通信して受け取ったデータ

など、実行中のプログラムが利用する情報は全てメモリに入れてから使います

ここでの "実行中のプログラム" というのは先ほど出てきたプロセスのことです

全てのデータをメモリ上に保持しておけば そのプログラムを実行する時にはメモリだけを見ればよくなり、データが元々どこにあったかは関係なく同じように扱うことができます

CPU があるプロセスを実行する時は、そのプロセスが持っているメモリデータに対して演算を行います


メモリにデータを出し入れする作業はデバイスドライバを使って OS がうまいことやってくれています
CPU からはメモリだけを見ればいいという状況を OS が用意してくれているんです


OS ってすごいですね


記事の先頭に戻る

3. 物理アドレスと仮想アドレス空間

さて、メモリの使われ方についてもう少し細かくみてみましょう

メモリは 1Byte 単位で全ての領域にアドレスが振られています
1GB のメモリであれば全部で

1 * 1024 * 1024* 1024 = 2^30 (約43億) 個

のアドレスが存在します


メモリにデータを読み書きするときには1つ1つメモリのアドレスを指定して読み書きを行っています

また、1つのアドレスを使おうとしたときに そこを他のプロセスが使っていると競合してしまうので、誰も使っていない所を探して利用する必要があります

何億もあるメモリのアドレスをいちいち管理するのはめちゃくちゃ大変なのが想像つきますよね?

そんな大変なメモリ管理の仕事を、プロセッサに含まれる MMU という部品が男気を見せて 一挙に引き受けてくれています
MMU:Memory Management Unit


MMU はプロセス毎に専用の物理メモリ領域を確保し、その物理領域にアクセスするための仮想アドレスを用意してくれます

プロセスからは仮想アドレスを指定してメモリアクセスを行い、仮想アドレス→物理アドレスへの解決は MMU が行います
※このマッピング情報は "ページテーブル" という名前でメモリ上に保持されます


これによりそれぞれのプロセスは物理領域を意識することなく、独立したメモリ空間を利用することができます
あるプロセスが使っているメモリ領域に他のプロセスからはアクセスできないよう制御されるので、セキュリティ的なメリットもあります


また、物理メモリ上はバラバラの領域であっても、仮想アドレス上は連続した領域のように扱うことができます
このおかげでメモリの隙間を有効に使えます



ちなみに、コンピューターが認識できるメモリの容量はメモリアドレスに扱う数値の桁数に依存します
その OS や CPU のビット数はこの数値の桁数のことを指しています

  • 32ビットシステムの場合は 2^32 Byte ≒ 4GByte
  • 64ビットシステムの場合は 2^64 Byte = 16EByte ≒ 172億GByte

までのメモリを認識できます


EByte (エクサバイト) なんて初めて知ったんですが、
日本語でいう "京" と同じ単位なんだそうです

64ビットにすることで扱えるメモリが途方もないくらいでかい数字にぶちあがりますが、
いつかこれも足りなくなって 128ビットの時代が来るんでしょうか・・・


記事の先頭に戻る

4. プロセスがメモリに保持している情報

それではプロセスとメモリについて更に掘り下げましょう
具体的にプロセスがメモリに保持しているデータの中身を見てみます


プロセスの1つ1つは全て、以下のように2つのセグメントに分かれた構造のデータをメモリ上に持っています

テキストセグメント

  • プログラムの命令列 (実行されるプログラムそのもの)
    ※読み取り専用

データセグメント

  • PDA (Processor Data Area)
    • プロセッサの情報やプロセス管理用のデータ領域
      スタックポインタ(sp)、プログラムカウンタ(pc) などがここに格納される
  • データ領域
    • 静的領域
    • ヒープ領域
      • 通常の変数などが格納される領域
        プロセスが領域を増やしたり減らしたりするところなので、実行時までサイズが分からない
        java などでのガベージコレクションはヒープ領域のデータが対象になる
  • スタック領域
    • 引数やローカルスコープのデータなどを一時的に置くところ

(補足)
スタックポインタ (sp): スタック領域のどこを見ているかを指す値
プログラムカウンタ (pc): プログラムのどこを実行しているかを指す値


急に細かい話になりましたがビビらないでください

よーく見ると 何となく分かるかもしれませんが、
これらのデータはまさにプロセスそのものを表しています


プロセスがこのデータをメモリに持っている というよりは、
プロセスがこのデータ構造としてメモリ上に存在している という方が正しい表現かもしれないです


記事の先頭に戻る

5. プロセスとスレッドの違い

それでは、プロセスとスレッドの違いを見ていきましょう
今回の記事で1番書きたかったのところなので、一生懸命調べました


プログラムを並列で動かすときに、
「プロセスがXX個立ち上がっていて」とか
「マルチスレッドで動く」なんていう話をよく聞きますが、
プロセス と スレッド の正確な違いって何でしょうか?



あるプログラムを並列に実行する場合には、

  1. 子プロセスを複数立ち上げて並列に動かす
  2. スレッドを複数立ち上げて並列に動かす

このどちらかの方法で 複数の処理を並列に動かすことができます

複数のプロセス、スレッドを同時に動かした場合に
どんな違いがあるかを見ていきましょう

プロセス

プロセスを fork して子プロセスを複数立ち上げることで、同じプログラムを並列に動かすことができます

子プロセスを立ち上げたときには子プロセス用のメモリ領域が新たに割り当てられます
そこに親プロセスのデータセグメントをコピーして、その子プロセス専用のデータセグメントを確保します

動かすプログラムの命令自体は親プロセスと全く同じなので、テキストセグメントは親プロセスと同じ領域を参照します



実行するプログラム自体は親プロセスと同じデータを共有し、実行状態に関わる変数などのデータは専用のものを利用することになります
子プロセスも1つのプロセスとして扱われるので、他のプロセスのメモリに直接アクセスすることはできません

Webブラウザを何個か開いたときにそれぞれの画面で別々の操作ができる状態を想像すると分かりやすいです
※ほんと?

スレッド

プロセスからスレッドを生成することで同じプログラムを並列に動かすことができます

スレッドが生成された時には親プロセスの仮想アドレス空間内に、

  • スタック領域
  • SP (スタックポインタ)
  • PC (プログラムカウンタ)

の値をスレッド毎にコピーして利用します

それ以外のデータは全て親プロセスと同じものを利用します
スレッドは子プロセスとは違い自分専用のメモリ領域は用意してもらえません


つまり、親プロセスの領域の中に全てのスレッドが存在していることになります
そのスレッドがいまプログラムのどの部分を実行しているかという情報だけを持ち、その他のデータは全てのスレッド間で同じものを利用します


ほぼ全てのデータを他スレッドと共有するので、あるスレッドが利用しようとしている変数が他のスレッドによって既に書き換えられていたり消されていたりする状況が発生します

シューティングゲームなどで、同じ画面上に 主人公、敵キャラ、レーザービームが それぞれバラバラに動くのを想像すると分かりやすいです
※ほんと?

この状況で動作しても問題が発生しないプログラムのことを、
"スレッドセーフ" と言います


ここがプロセスとの大きな違いです


記事の先頭に戻る

6. パフォーマンスの違い

いままでの話をまとめると、

  • プロセスは専用のメモリ領域を利用する
  • スレッドは共有のメモリ領域を利用する

ということになります

なんとなくスレッドの方が扱いづらそうな印象を受けますよね


ですが、たくさんのプロセスやスレッドを動かしたときにスレッドを使うメリットが出てきます

その違いを見ていきましょう


プロセスをたくさん立ち上げて動かす場合

プロセスはそれぞれに専用の仮想アドレス空間を保持しており、データセグメントのデータはそれぞれのメモリ領域内に保持されています

そのため、プロセスを切り替える場合は 仮想アドレス ⇔ 物理アドレスマッピングをそのプロセス用のものへの切り替える必要が出てきます

1度アドレスを解決した結果は MMU がキャッシュしているため、そのままの状態でプロセスを切り替えてしまうと前のプロセスの領域が見えてしまうことになります
なので、プロセスを切り替える時は MMU が持っているキャッシュをクリアする必要があります
※TLB フラッシュ と呼ばれます


この TLB フラッシュがパフォーマンスに与える影響はかなり大きいらしいです

どれくらい高コストなのかはよくわからないのですが、フラッシュ直後は全てキャッシュミスになるので 確かにコストが大きくなりそうな想像はできます


トランスレーション・ルックアサイド・バッファ - Wikipedia
wikiペディア兄さんの情報では ヒット時とミス時で100倍くらい処理時間が違うそうです

スレッドをたくさん立ち上げて動かす場合

スレッドが持つデータは 全て親プロセスのメモリ領域内に保持されているため、利用する仮想アドレス空間を切り替える必要がありません
そのため TLB フラッシュは必要ありません

実行するスレッドを切り替える場合は、スタック領域、SP、PC の切り替えだけで済みます


説明の長さの違いから見ても、スレッドの切り替えの方がプロセスの切り替えよりも遥かに簡単に済むということが分かりますね

なので、単純に同じプログラムを並列で動作させる場合は、スレッドを利用した方が切り替えにかかる時間が短く より高速に処理できるということです

※厳密には、カーネルモードで切り替える場合、ユーザーモードで切り替える場合で処理が違います


記事の先頭に戻る

7. まとめ

だいぶ長かったですね
ここまで読んでくださって本当にありがとうございます

今回の話をサクっとまとめましょう


マルチタスクOS はプロセス、スレッドを細かく切り替えながら処理しています

メモリはコンピューターの動作に必要なすべての情報を一時的に保持するところです

プロセスはメモリ領域が独立しているので安全ですが、スレッドと比較すると切り替えにかかるコストが大きいです

スレッドは切り替えにかかるコストは小さいですが、実行されるプログラムが "スレッドセーフ" でなければいけません



Apache の設定にある prefork と worker は、まさにプロセス、スレッドのどちらで動かすかの違いです

worker はスレッドで動作させる設定なので高速と言われていますが、いままで説明した通りの理由で、単純に切り替えてもうまく動かない場合が多いようです


JavaJVM の1プロセス上で全て動作しています
JVM がメモリ管理を全てやるので、1プロセス上じゃないとこれが実現できないんですね

プロセスが分かれればメモリ管理も当然別々になります



パフォーマンスという点で考えると、スレッドセーフなプログラムは "スレッドで動かしても問題が出ない" ということを保証しているだけで、スレッドで動かすと高速化することを保証しているわけではありません

スレッド間での排他制御による待ち時間が多くなれば、プロセス以上に処理効率が落ちる可能性もあります


そもそも、ディスクなどのデバイスや他のサーバーとのやりとりで待ちがでてしまえば、プロセスやスレッドに関係なくパフォーマンスは劣化します
結局のところ、共有しているリソースがある限りそこがボトルネックになる可能性は常に存在するので、スレッドの方がいいという単純な話で解決できる場面は少ないと思います


高速化のためには1つ1つ地道に原因を探っていくしかありません
こういうところが一番難しくて悩まされます


ああ、ややこしい



今回の話の延長として、
プロセスやスレッドの数で対応する仕組みだといつか大量になりすぎて管理できなくなる、という 何年か前に話題になった C10K問題が出てきます


また、最近アツイらしい関数型言語は、

  • 副作用のある処理を実装しない
  • 明示的な状態を持たせない

という思想なので、処理の競合が発生しにくく 並列処理を実装するのに向いていると言われています


ハードウェアがいくら進化してもソフトウェアがそれを十分に使いきることができなければ意味がないので、こういった部分が注目されているんですね



中途半端な理解でモヤモヤしていたのがスッキリしました

今回はそんな低レイヤーの処理に注目した内容でした


これでまたイケてるエンジニアに1歩近づきましたね


※参考資料

読んでると異常に眠くなるのでプロセスの章あたりで挫折しました
Amazon.co.jp: はじめてのOSコードリーディング ~UNIX V6で学ぶカーネルのしくみ (Software Design plus): 青柳 隆宏: 本

細かい所まで解説されていてとても分かりやすいです
マルチスレッドのコンテキスト切り替えに伴うコスト - naoyaのはてなダイアリー

今回はあまり触れていませんが、メモリ管理はいろんなアイディアがつまっていて面白いです
The Linux Kernel: メモリ管理



記事の先頭に戻る