Linuxにおけるメモリ割り当ての仕組み
システム上のすべてのアクティブなアプリケーションは、実行するためにメモリを必要とします。では、Linuxマシンでメモリはどのように割り当てられ、共有されるのでしょうか?
コンピュータでは、プロセスが実行可能であるためには、メモリに配置される必要があります。このためには、メモリ内のプロセスにフィールドが割り当てられなければなりません。メモリの割り当ては特にカーネルやシステムアーキテクチャにおいて認識しておくべき重要な問題です。
Linuxのメモリ割り当てについて詳しく見て、裏で何が起こっているのかを理解しましょう。
メモリ割り当てはどのように行われますか?
ほとんどのソフトウェアエンジニアはこのプロセスの詳細を知りません。しかし、もしあなたがシステムプログラマーの候補者であれば、もっと詳しく知っておくべきです。割り当てプロセスを見るとき、Linuxとglibcライブラリの詳細に少し踏み込む必要があります。
アプリケーションがメモリを必要とするとき、オペレーティングシステムにリクエストしなければなりません。このカーネルからのリクエストは、当然、システムコールを必要とします。ユーザーモードで自分自身でメモリを割り当てることはできません。
malloc() ファミリーの関数は、C言語におけるメモリ割り当てを担当しています。ここでの質問は、glibc 関数としての malloc() が直接システムコールを行うのかということです。
Linuxカーネルにはmallocというシステムコールは存在しません。しかし、アプリケーションのメモリ要求のために、brkとmmapという2つのシステムコールがあります。
あなたがglibc関数を介してアプリケーションでメモリを要求するので、現在glibcがどのシステムコールを使用しているのか疑問に思うかもしれません。その答えは両方です。
最初のシステムコール: brk
各プロセスには連続したデータフィールドがあります。brkシステムコールを使用すると、データフィールドの制限を決定するプログラムブレーク値が増加し、割り当てプロセスが実行されます。
この方法によるメモリ割り当ては非常に速いですが、未使用のスペースをシステムに返すことが常にできるわけではありません。
例えば、malloc()関数を介してbrkシステムコールでそれぞれ16KBのサイズの5つのフィールドを割り当てるとしましょう。これらのフィールドのうち2番目が完了した場合、その関連リソース(解放)を返すことはできません。なぜなら、フィールドの番号2が始まる場所を示すためにアドレス値を減らすと、brkを呼び出すことでフィールドの番号3、4、5に対して解放を行ってしまうからです。
このシナリオでメモリ損失を防ぐために、glibcのmalloc実装はプロセスデータフィールドで割り当てられた場所を監視し、free()関数を使ってそれをシステムに返すように指定するため、システムは空いているスペースをさらなるメモリ割り当てに使用できるようになります。
言い換えれば、5つの16KB領域が割り当てられた後、2番目の領域がfree()関数で解放され、その後しばらくして別の16KB領域が再度要求されると、brkシステムコールを使用してデータ領域を拡張する代わりに、以前のアドレスが返されます。
しかし、新しく要求された領域が16KBを超える場合、データ領域はbrkシステムコールを使って新しい領域を割り当てることで拡張されます。領域二は使用されていないのですが、サイズの違いからアプリケーションはそれを使用できません。このようなシナリオのために、内部断片化と呼ばれる状況が生じ、実際にはメモリのすべての部分を最大限に活用することは稀です。
より良い理解のために、以下のサンプルアプリケーションをコンパイルして実行してみてください:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char* argv[])
{
char *ptr[7];
int n;
printf("Pid of %s: %d", argv[0], getpid());
printf("Initial program break : %p", sbrk(0));
for(n=0; n<5; n++) ptr[n] = malloc(16 * 1024);
printf("After 5 x 16kB malloc : %p", sbrk(0));
free(ptr[1]);
printf("After free of second 16kB : %p", sbrk(0));
ptr[5] = malloc(16 * 1024);
printf("After allocating 6th of 16kB : %p", sbrk(0));
free(ptr[5]);
printf("After freeing last block : %p", sbrk(0));
ptr[6] = malloc(18 * 1024);
printf("After allocating a new 18kB : %p", sbrk(0));
getchar();
return 0;
}アプリケーションを実行すると、以下の出力に似た結果が得られます:
Pid of ./a.out: 31990
Initial program break : 0x55ebcadf4000
After 5 x 16kB malloc : 0x55ebcadf4000
After free of second 16kB : 0x55ebcadf4000
After allocating 6th of 16kB : 0x55ebcadf4000
After freeing last block : 0x55ebcadf4000
After allocating a new 18kB : 0x55ebcadf4000straceを使ったbrkの出力は以下のようになります:
brk(NULL) = 0x5608595b6000
brk(0x5608595d7000) = 0x5608595d7000ご覧のように、0x21000がデータフィールドの終了アドレスに追加されました。これは0x5608595d7000の値から理解できます。つまり、約0x21000、または132KBのメモリが割り当てられました。
ここで考慮すべき2つの重要なポイントがあります。1つは、サンプルコードに指定された量を超える割り当てです。もう1つは、どのコード行が割り当てを提供したbrk呼び出しを引き起こしたかです。
アドレス空間配置のランダム化: ASLR
上記のサンプルアプリケーションを連続して実行すると、毎回異なるアドレス値が表示されます。このようにアドレス空間をランダムに変化させることで、セキュリティ攻撃の作業が大幅に複雑になり、ソフトウェアのセキュリティが向上します。
しかし、32ビットアーキテクチャでは、アドレス空間をランダム化するために一般的に8ビットが使用されます。ビット数を増やすことは適切ではありません。残りのビットでアドレス可能な領域が非常に低くなるからです。また、8ビットの組み合わせだけを使用することは、攻撃者にとって十分に困難ではありません。
一方、64ビットアーキテクチャでは、ASLR操作に割り当てることができるビットが多すぎるため、はるかに大きなランダム性が提供され、セキュリティの度合いが向上します。
LinuxカーネルはAndroidベースのデバイスも動かしており、ASLR機能はAndroid 4.0.3以降で完全に有効になっています。この理由だけでも、64ビットスマートフォンは32ビット版よりも大きなセキュリティの利点を提供すると言っても過言ではありません。
次のコマンドでASLR機能を一時的に無効にすることによって、前回のテストアプリケーションが実行されるたびに同じアドレス値を返すように見えるようになります:
echo 0 | sudo tee /proc/sys/kernel/randomize_va_space
元の状態に戻すには、同じファイルに0の代わりに2を書き込むだけで十分です。
第二のシステムコール: mmap
mmapはLinuxでメモリ割り当てに使用される2番目のシステムコールです。mmapコールを使用すると、メモリの任意の領域の空きスペースが呼び出し元プロセスのアドレス空間にマッピングされます。
このように行われたメモリ割り当てにおいて、前の brk の例で free() 関数を使用して第二の 16KB パーティションを返したい場合、その操作を防ぐメカニズムはありません。関連するメモリセグメントはプロセスのアドレス空間から削除されます。これはもはや使用されていないとしてマークされ、システムに返されます。
mmapによるメモリ割り当てはbrkに比べて非常に遅いため、brkによる割り当てが必要です。
mmapを使用すると、任意の空きメモリ領域がプロセスのアドレス空間にマッピングされるため、割り当てられた領域の内容はこのプロセスが完了する前にリセットされます。もしこのようにリセットが行われなかった場合、その関連メモリ領域を以前に使用していたプロセスのデータに次の無関係なプロセスがアクセスできてしまう可能性があります。これでは、システムにおけるセキュリティについて語ることは不可能です。
Linuxにおけるメモリ割り当ての重要性
メモリ割り当ては非常に重要であり、特に最適化やセキュリティの問題において重要です。上記の例に見られるように、この問題を完全に理解していないと、システムのセキュリティを危うくすることにつながる可能性があります。
プッシュやポップのように、多くのプログラミング言語に存在する類似の概念も、メモリ割り当て操作に基づいています。組込みシステムプログラミングや、安全で最適化されたシステムアーキテクチャの開発において、システムメモリを上手に使用しマスターすることは重要です。
もしあなたもLinuxカーネル開発に触れてみたいのであれば、まずCプログラミング言語を習得することを考えてみてください。

