Linuxプロセスのメモリレイアウトを徹底解説|Heap・Stack・Text・Data・BSS・mmapの役割

はじめに

Linuxでは、各プロセスに独立した仮想アドレス空間が割り当てられています。

しかし、この仮想アドレス空間は単なる一つの大きなメモリ領域ではありません。プログラムの実行コードやグローバル変数、動的に確保したメモリなど、用途に応じて複数の領域へ分割され、それぞれ異なる役割を担っています。

例えば、

  • プログラムの実行コードはどこに格納されるのか
  • malloc()やnewで確保したメモリはどこへ配置されるのか
  • ローカル変数はどこに保存されるのか
  • なぜメモリリークやスタックオーバーフローが発生するのか

といった疑問は、プロセスのメモリレイアウトを理解することで説明できるようになります。

また、Linuxの障害調査では、pmap や /proc/<PID>/maps などを使用してプロセスのメモリ使用状況を確認する場面があります。これらのコマンドを正しく読み取るためにも、各メモリ領域の役割を理解しておくことは重要です。

本記事では、Linuxプロセスのメモリレイアウトについて、各領域の役割や特徴を分かりやすく解説します。また、記事の後半では実際にコマンドを使用してプロセスのメモリレイアウトを確認する方法も紹介します。

Linuxのメモリレイアウト

Linuxでは、各プロセスに独立した仮想アドレス空間が割り当てられています。

このアドレス空間は用途ごとに複数の領域へ分かれており、プログラムの実行コードやグローバル変数、動的に確保したメモリなどがそれぞれ異なる領域で管理されています。

代表的なメモリレイアウトを以下に示します。

これらの領域は、プログラムの実行中にそれぞれ異なる役割を果たしています。

例えば、CPUがプログラムを実行する際は、Text領域から命令を読み出します。グローバル変数はData領域やBSS領域に格納され、関数を呼び出すとStack領域にスタックフレームが作成されます。また、malloc()newで動的に確保したメモリはHeap領域に割り当てられます。

さらに、共有ライブラリ(libc.soなど)やメモリマッピングされたファイルは、mmap領域に配置されます。そのため、Linuxでは一つのメモリ領域ですべてを管理するのではなく、用途ごとに役割を分けることで、効率的かつ安全なメモリ管理を実現しています。

なお、これらの領域はすべて仮想アドレス空間上に存在しています。CPUがアクセスする際には、MMU(Memory Management Unit)が仮想アドレスを物理アドレスへ変換することで、実際の物理メモリ(RAM)へアクセスしています。

次章からは、それぞれのメモリ領域について詳しく見ていきましょう。

※仮想アドレスおよび仮想メモリについては以下の記事で説明しているので、合わせて参照ください。Linuxメモリ管理を徹底解説|仮想メモリ・ページキャッシュ・Swap・OOM Killerの仕組み

各メモリ領域の特徴

それぞれのメモリ領域について、詳細に説明していきます。

Text領域

Text領域(Text Segment)は、プログラムの実行コード(機械語)が格納される領域です。

私たちがC言語やC++などで作成したソースコードは、コンパイラによってCPUが理解できる機械語へ変換されます。この機械語が配置されるのがText領域です。

例えば、以下のようなプログラムを考えてみます。

#include <stdio.h>

int main(void)
{
    printf("Hello Linux\n");
    return 0;
}

このソースコードをコンパイルすると、CPUが実行する命令列が生成されます。Linuxでは、この命令列がText領域へ配置され、CPUはここから命令を順番に読み出してプログラムを実行します。

Text領域には、次のような特徴があります。

  • プログラムの実行コードが格納される
  • 通常は読み取り専用(Read Only)
  • 実行可能(Executable)
  • 同じ実行ファイルを利用するプロセス間で共有できる

特に重要なのが、複数のプロセスで共有できるという点です。

例えば、同じWebサーバ(ApacheやNginxなど)を複数のプロセスで実行した場合、それぞれのプロセスが同じ実行コードを持つ必要はありません。そのため、LinuxではText領域を共有することで、物理メモリの使用量を削減しています。

一方で、プログラムの実行中に命令を書き換えてしまうと、予期しない動作やセキュリティ上の問題が発生する可能性があります。そのため、Text領域は通常、読み取り専用として保護されています。

Data領域

Data領域(Data Segment)は、初期値を持つグローバル変数やstatic変数が格納される領域です。

例えば、次のプログラムでは、count と value がData領域へ配置されます。

#include <stdio.h>

int count = 100;
static int value = 10;

int main(void)
{
    printf("%d %d\n", count, value);
    return 0;
}

これらの変数は、プログラムの開始時に指定した初期値が設定され、プログラムが終了するまで保持されます。

Data領域には、次のような特徴があります。

  • 初期値を持つグローバル変数を格納する
  • 初期値を持つ static 変数を格納する
  • プログラム開始時にメモリが確保される
  • プログラム終了まで保持される

ローカル変数とは異なり、Data領域に配置された変数は関数の終了によって解放されることはありません。そのため、プログラム全体で共有したいデータや、長期間保持したいデータの保存に利用されています。

例えば、アクセス数のカウンタや設定情報など、プログラムの実行中ずっと利用するデータがData領域へ配置される代表例です。

BSS領域

BSS領域(Block Started by Symbol)は、初期値を持たないグローバル変数や static 変数が格納される領域です。

例えば、次のプログラムでは、count と value がBSS領域へ配置されます。

#include <stdio.h>

int count;
static int value;

int main(void)
{
    printf("%d %d\n", count, value);
    return 0;
}

これらの変数には初期値を指定していませんが、Linuxではプログラム開始時に自動的に0で初期化されます。

BSS領域には、次のような特徴があります。

  • 初期値を持たないグローバル変数を格納する
  • 初期値を持たない static 変数を格納する
  • プログラム開始時に自動で0に初期化される
  • プログラム終了まで保持される

※Data領域とBSS領域を分ける理由
「初期値がないのであれば、Data領域にまとめてもよいのではないか」と思うかもしれませんが、Data領域とBSS領域を分けることには大きなメリットがあります。

例えば、「char buffer[1024 * 1024];」のような変数を宣言したとしますると、この変数は約1MBの大きさがあります。もし、この変数をData領域として実行ファイルに保存すると、実行ファイルの中にも1MB分の「0」のデータを書き込まなければなりません。その結果、実行ファイルのサイズが大きくなってしまいます。

そこでLinuxでは、初期値を持たない変数をBSS領域として管理し、実行ファイルには「このサイズのBSS領域を確保してください」という情報だけを記録しています。そして、プログラムの起動時にLinuxカーネル(正確にはプログラムローダ)が必要なメモリを確保し、自動的に0で初期化します。

Heap領域

Heap領域は、プログラムの実行中に動的にメモリを確保するための領域です。
プログラムを開発していると、実行前には必要なメモリサイズが分からないことがあります。

例えば、

  • ユーザーが入力した文字列
  • データベースから取得したデータ
  • Webサーバが受信したリクエスト
  • リストやツリーなどの可変長データ構造

などは、実行時にならないと必要なメモリ量が決まりません。このようなデータを扱うために利用されるのがHeap領域です。

例えば、C言語では malloc() を使用してメモリを確保します。

#include <stdlib.h>
int main(void)
{
    int *p = malloc(sizeof(int));
    *p = 100;
    free(p);
    return 0;
}

この例では、malloc() によって確保されたメモリはHeap領域に配置されます。

C++では new 演算子が利用されます。

class Person
{
public:
    int age;
};

int main()
{
    Person *p = new Person();
    delete p;
}

こちらも同様に、生成されたオブジェクトはHeap領域へ配置されます。

Heap領域には次のような特徴があります。

  • 実行中に必要なサイズだけメモリを確保できる
  • プログラム終了まで自由に利用できる
  • 必要がなくなったら明示的に解放する必要がある
  • 確保・解放を繰り返すため断片化(フラグメンテーション)が発生する場合がある

Heap領域は低位アドレスから高位アドレス方向へ伸びるように利用されるのが一般的です。
実際には仮想メモリ管理やアロケータ(glibcのmalloc()など)の実装によって異なる場合があります。

※Java Heap
Javaを利用していると「Heap不足」という言葉をよく耳にしますが、これは通常、LinuxのHeap不足ではなく、JVMが管理するJava Heapの空き領域が不足している状態を指します。LinuxのHeap領域とJava Heapは同じものではありませんのでご認識ください。

Javaのメモリ関連の情報は以下でまとめているので合わせて参照ください。
Java初心者必見!メモリ管理とガベージコレクションを基礎から理解する

Stack領域

Stack領域は、関数の呼び出しに必要な情報を管理する領域です。関数が呼び出されるたびに、Stackにはスタックフレーム(Stack Frame)が作成されます。

スタックフレームには、主に次のような情報が保存されます。

  • ローカル変数
  • 関数の引数
  • 戻りアドレス
  • 保存されたレジスタ

例えば、次のプログラムを考えてみます。

#include <stdio.h>

void func(void)
{
    int x = 100;
    printf("%d\n", x);
}

int main(void)
{
    func();
    return 0;
}

この例では、ローカル変数xはStack領域へ配置されます。func() が呼び出されるとスタックフレームが作成され、func() が終了するとスタックフレームごと削除されます。そのため、ローカル変数は関数終了後に自動的に解放されます。

Stack領域には次のような特徴があります。

  • ローカル変数を管理する
  • 関数呼び出し情報を管理する
  • 関数終了時に自動で解放される
  • 一般的に高位アドレスから低位アドレス方向へ伸びる

Heapとは異なり、プログラマが解放処理を記述する必要はありません。

※HeapとStackの違いまとめ

項目HeapStack
主な用途動的メモリ確保関数呼び出し
格納されるものmalloc()・newで確保したデータローカル変数、引数、戻りアドレス
確保タイミング実行中に必要に応じて確保関数呼び出し時に自動確保
解放方法free()・delete(JavaはGC)関数終了時に自動解放
サイズ比較的大きい比較的小さい
主なトラブルメモリリーク、断片化スタックオーバーフロー

mmap領域

mmap領域は、共有ライブラリやメモリマッピングされたファイル、大きな動的メモリなどを管理するための領域です。

Heap領域だけでは効率的に管理できない用途(共有ライブラリや大きなメモリ確保など)のために、Linuxではmmap領域も利用しています。

Text領域やHeap領域とは異なり、mmap() システムコールを利用して必要に応じて動的に作成されます。そのため、プロセスの実行中には複数のmmap領域が生成されることがあり、/proc//maps では多数のマッピング領域を確認できます。

一般的なプロセスのメモリレイアウトでは、mmap領域はHeap領域とStack領域の間に配置されます。

mmap領域は、主に次のような用途で利用されます。

  • 共有ライブラリ(libc.soなど)の読み込み
  • ファイルのメモリマッピング
  • 大きな動的メモリの確保
  • 匿名メモリ(Anonymous Mapping)の管理

以降では、用途について詳しく見ていきます。

共有ライブラリの読み込み

mmapが最もよく利用される例が、共有ライブラリ(Shared Library)です。

例えば、C言語のプログラムでprintf()を使用した場合、その処理は標準Cライブラリであるlibc.soによって提供されています。

include

int main(void)
{
printf("Hello Linux\n");
return 0;
}

プログラムを実行すると、動的リンカ(Dynamic Linker)が必要な共有ライブラリを読み込み、それらをmmap領域へ配置します。

ファイルをメモリへマッピングする

mmapは、ファイルをメモリ上へ直接マッピングするためにも利用されます。

通常、ファイルを読み込む場合はread()システムコールを使用します。

ディスク
│
read()
│
ユーザバッファ

一方、mmap()を利用すると、ファイルを仮想アドレス空間へ直接マッピングできます。

ディスク上のファイル
│
mmap()
│
mmap領域

その後は通常のメモリアクセスと同じようにデータを読み書きできるため、大きなファイルを効率よく扱えるというメリットがあります。

この仕組みは、データベース、検索エンジン、キャッシュシステムなど、大量のデータを扱うソフトウェアで広く利用されています。

大きなメモリ確保にも利用される

Heap領域は、malloc()による動的メモリ確保に利用されますが、Linuxでは非常に大きなメモリを確保する場合、Heapではなくmmapを利用することがあります。

例えば、glibcのmalloc()は、要求サイズに応じて内部で以下のように使い分けています。

比較的小さいメモリ:Heap領域(brk())
比較的大きいメモリ:mmap領域(mmap())

このように使い分けることで、大きなメモリを解放した際にOSへ返却しやすくなり、メモリの断片化(フラグメンテーション)も抑えられます。

なお、「大きい」と判断されるサイズは、glibcの設定や環境によって異なります。

メモリ関連トラブル

典型的なメモリ関連のトラブルをご紹介します。

メモリリーク

Heap領域を利用する際に注意したいのが、メモリリーク(Memory Leak)です。メモリリークとは、不要になったメモリを解放し忘れることで、利用可能なメモリが徐々に減少してしまう現象です。

例えば、次のコードには問題があります。

#include <stdlib.h>

void func(void)
{
    int *p = malloc(sizeof(int));
    *p = 100;
}

このプログラムではmalloc()で確保したメモリをfree()していません。func()が終了すると変数 p 自体は消えますが、Heap上に確保したメモリは残ったままになります。このような処理を繰り返すと、Heap領域の使用量が増え続け、最終的にはメモリ不足を引き起こす可能性があります。そのため、C/C++では、malloc()やnewで確保したメモリは、不要になった時点で必ずfree()やdeleteによって解放することが重要です。

スタックオーバーフロー

Stack領域はサイズが限られています。そのため、大きな配列を確保したり、深い再帰呼び出しを繰り返したりすると、Stack領域を使い切ってしまうことがあります。

例えば、次のようなコードです。

void func(void)
{
    char buffer[1024 * 1024];
    func();
}

このプログラムでは、関数が再帰的に呼び出されるたびに新しいスタックフレームが作成され、Stack領域が急速に消費されます。最終的には、利用可能なStack領域を超えてしまい、スタックオーバーフロー(Stack Overflow)が発生します。

Linuxでは、スタックサイズは ulimit -s コマンドで確認できます。

[root@localhost ~]# ulimit -s
8192
[root@localhost ~]#

この例では、スタックサイズは8MBに設定されています。

メモリの断片化(フラグメンテーション)

Heap領域では、malloc()やfree()によるメモリの確保と解放が繰り返し行われます。

この操作を長時間繰り返していると、解放されたメモリがあちこちに点在し、大きな連続領域を確保しにくくなることがあります。この現象をメモリの断片化(Memory Fragmentation)と呼びます。

例えば、次のような状態を考えてみましょう。

このように、空き領域が細かく分散すると、空き容量の合計は十分でも、大きな連続したメモリ領域を確保できない場合があります。

メモリの断片化が進むと、メモリ確保の効率が低下し、アプリケーションの性能へ影響を与えることがあります。特に、長時間稼働するWebサーバやデータベースなどでは、断片化によるオーバーヘッドが無視できないケースもあります。

ただし、現在のLinuxでは、glibcのmalloc()がメモリの利用状況に応じて管理方法を工夫しており、大きなメモリ領域についてはmmap()を利用するなど、断片化を抑える仕組みが備わっています。

そのため、通常のアプリケーションでは断片化を意識する場面はそれほど多くありません。しかし、大量のメモリ確保・解放を繰り返すアプリケーションでは、断片化が性能へ影響する可能性があるため、その存在を理解しておくことが重要です。

まとめ

本記事では、Linuxプロセスのメモリレイアウトについて解説しました。
Linuxでは、各プロセスに独立した仮想アドレス空間が割り当てられており、その中は用途に応じて複数のメモリ領域へ分割されています。

各領域の役割をまとめると、以下のようになります。

メモリ領域主な役割
Textプログラムの実行コードを格納
Data初期値を持つグローバル変数・static変数を格納
BSS初期値を持たないグローバル変数・static変数を格納
Heapmalloc()newによる動的メモリを管理
Stackローカル変数や関数呼び出し時の情報を管理
mmap共有ライブラリやファイルマッピング、大容量メモリを管理

これらの役割を理解することで、「なぜメモリリークが発生するのか」「なぜスタックオーバーフローが起こるのか」といった典型的なメモリ関連のトラブルを理解しやすくなります。

メモリ管理の詳細については、以下の通り詳細を記載していますので、合わせて読んでみてください。

記事内容リンク
第1回Linuxメモリ管理の全体像(本記事)Linuxメモリ管理を徹底解説|仮想メモリ・ページキャッシュ・Swap・OOM Killerの仕組み
第2回仮想メモリの仕組みLinuxの仮想メモリを徹底解説|仕組み・MMU・ページテーブルをわかりやすく解説
第3回プロセスのメモリレイアウト本記事
第4回Linuxカーネルのメモリ管理Linuxカーネルのメモリ管理とは?ページフレーム・メモリゾーン・Buddy Allocator・SLUBをわかりやすく解説
第5回ページキャッシュLinuxページキャッシュを徹底解説|Page Cache・Dirty Page・Writebackの仕組み
第6回SwapLinux Swapを徹底解説|Swap Out・Swap In・swappinessの仕組み
第7回メモリ不足時の動作(kswapd・OOM Killer)Linuxメモリ不足時の動作を徹底解説|kswapd・Direct Reclaim・OOM Killerの仕組み

コメント