まずは、そもそもの疑問を整理しておきましょう。
お客さまは「メモリが足りなくなってリブートに至った」といっています。ですが今回の場合は、メモリ不足自体がリブートに直結したのではなく、ハングアップしていたところをASRによってハードウェア的に落とされたことが分かっています。ですから、本質的な疑問は「メモリ不足がOSハングアップを引き起こし得るか?」ということです。
そもそもハングアップとは、カーネルパニックなどを起こしてシステムがクラッシュしている場合と、何らかの原因で極端に動作が遅くなっている場合がありますが、残念ながら疑問に対する答えは「Yes」です。メモリ不足がOSのハングアップを引き起こすことはあります。
「そんなOS駄目じゃん!」と思うかもしれませんが、OSだって1個のプログラムです。メモリが足りなくなれば、そんなこともあります。
ただ、メモリ不足にもいろいろあって、ほとんどの場合、OSはメモリが不足しても動作し続けられるように努力しますから、そのような状況に陥ることはめったにありません。ここからは、どんな状況になったらOSハングアップになり得るのか、掘り下げてみましょう。
さて、「メモリ不足にもいろいろある」といいましたが、では「メモリ不足」にはどのような種類があるのでしょうか。これを整理するには、以下の2つの観点を整理しておく必要があります。
それでは、これらの概念について説明していきましょう。
最近のほとんどのOSは「仮想メモリ」という機能をサポートしています。これは文字通りメモリを仮想化するもので、この機能を利用することで、ユーザープロセスはハードウェアの物理メモリ搭載量やほかのプロセスのメモリ使用状況を意識せずに動作できます。
もう少し具体的に、この仮想メモリ機能がLinuxでどのように実装されているかを説明しましょう。x86_64アーキテクチャにおける仮想メモリのイメージを図2に示します。
仮想メモリ機能は、ユーザープロセスごとに「仮想アドレス空間」という連続なアドレス範囲を割り当てます。このユーザープロセスが使用するメモリ空間を「ユーザー空間」と呼びます。このユーザー空間はユーザープロセスごとに独立しており、与えられたアドレス範囲内で自由にメモリを確保/解放することができます(注3)。こうして確保/解放するメモリが「仮想メモリ」です。
「カーネル空間」というのは、その名のとおり、カーネルに割り当てられる仮想アドレス空間です。カーネルは、ユーザープロセスから見えてはいけないものをたくさん持っていますので、アドレス範囲を明確に区切り、アクセス制限しています。また、カーネルは1つしか存在しませんので、ユーザープロセスごとに個別に空間を用意する必要はなく、各ユーザープロセスは1つのカーネル空間を共用する形で処理を行います。
ユーザー空間、カーネル空間に割り当てられる仮想アドレス範囲は、アーキテクチャごとに固定です(表3参照)。x86_64では、ユーザー空間として128Tbytes、カーネル空間としては約64Tbytes(注4)が割り当て可能(注5)であることが分かります。
ユーザー空間の割り当て範囲 (サイズ) |
カーネル空間の割り当て範囲 (サイズ) |
|
---|---|---|
x86 | 00000000-bfffffff(3GB) | c0000000-ffffffff(1GB) |
x86_64 | 0000000000000000-00007fffffffffff (128TB) |
ffff810000000000-ffffc0ffffffffff (64TB:ダイレクトマッピング領域のみ) |
表3 仮想アドレスの割り当て範囲 |
実際のハードウェアでは表3のアドレス範囲をカバーするだけのメモリを搭載していないことが一般的ですから、仮想アドレスをそのまま物理アドレスに適用しても、物理メモリの実体がなかったりします。また、ほかにも並行していくつかのユーザープロセスが動作しているので、何も考えずに仮想アドレスを割り当ててしまうと、それらほかのユーザープロセスと衝突が起きたりします。
これを解決するためにLinuxカーネルは、ユーザープロセスごとに仮想アドレスと物理アドレスを対応付けする「表」(テーブル)を持ち、ユーザープロセスには仮想アドレスだけを見せ、ほかのプロセスと衝突しないようにアドレスを変換することで、ユーザープロセスがメモリの使い方について思い悩まない世界を実現しています。
注3:ただし、マルチスレッドの場合には、同じ仮想アドレス空間をスレッド間で共用しています。
注4:ダイレクトマッピングされる領域のみを計上した場合です。x86_64の場合、実際には64Tbytesの領域以外にもカーネルモジュール用の領域があったりします。
注5:昔のLinuxカーネルではここまでの領域が使えませんでした。例えばRHEL4ではユーザー空間、カーネル空間ともに512Gbytesしか使うことができません。
さて、仮想メモリが不足すると何が起こるのでしょうか? これを考える前に、まずは「仮想メモリを使う」とはどういうことなのかを考えましょう。
図3は、仮想メモリがどのように物理メモリに割り当てられるかを模式的に表したものです。赤い部分が実際に物理メモリに割り当てられている部分を意味しています。
ユーザー空間において、仮想メモリは、各種メモリ獲得関数(malloc、mmapなど)を発行すると増加します(注6)。各プロセスの仮想メモリ使用量は、psコマンドでは「VSZ」、topコマンドでは「VIRT」として確認できます。
ここで注意したいのは「仮想メモリを確保したからといって、その分、物理メモリも確保したわけではない」ということです。
mallocやmmapなどのメモリ獲得関数は、実は仮想メモリの確保を目的としており、その先で物理メモリが確保できたかまでは意識しません。実際に物理メモリが確保されるのはメモリ領域を読み書きするときで、そのタイミングでOSが物理アドレスを決定します。そのため、仮想アドレス空間が許す範囲なら、いくらメモリを確保しても、読み書きしない限りは何らオーバーヘッドにはなりません。
各ユーザープロセスの物理メモリ使用量を確認するには、psコマンドでは「RSS」、topコマンドでは「RES」があります。興味のある方は、シンプルにmmapを発行するようなプログラムを書いて、実際に仮想メモリ使用量と物理メモリ使用量とで差が生じることを確認してみてください。
なお、mmapやmallocでは、割り当てたいメモリ量以外に、管理用の物理メモリなどを使用します。ですから実際には仮想メモリのみを消費するわけではなく、物理メモリも消費しますので、その辺りに注意しながら確認すると面白いですよ。
さて、仮想メモリ不足の話に戻りましょう。
仮想メモリ使用量が仮想アドレス空間を消費し尽くすと、仮想メモリ不足状態に陥ります。ユーザー空間において、仮想メモリ不足状態に陥ると、ユーザープロセス内のメモリ獲得関数(mallocなど)がENOMEMエラーを返すようになります。しかし、ユーザー空間はユーザープロセスごとに独立していますから、アドレス空間を共有していない限り、ほかのプログラムの動作には影響しません。当然、ユーザー空間とカーネル空間は分かれていますので、カーネルの動作に影響を与えることもありません。さらに、物理メモリを確保しているわけでもないので、物理メモリ使用量を圧迫することもありません。
さらに今回のケースについていえば、x86_64環境であり、ユーザー空間が非常に広大なので、仮想メモリ不足によるENOMEM エラーはほぼ起こらないといっていいでしょう。
一方、カーネル空間における仮想メモリ事情は、ユーザー空間のそれとは異なります。
まず、カーネルモジュール用の領域確保の際に、仮想メモリが不足すると、カーネルモジュールの初期化などに失敗することがありますが、OSの動作としては問題にはなりにくいでしょう。また、原則としてカーネルが仮想メモリを確保する場合は、即座に物理メモリも確保します。そのため、メモリリークなどがあった場合、結局は物理メモリ不足に直結してきますので、OSがハングアップするかという観点では仮想メモリ不足よりも物理メモリ不足に着目するのがよさそうです。
注6:細かいことをいうと、ユーザープロセスがmallocやmmapなどを発行すると、glibcが本当に仮想メモリを確保する必要があるのか、あるとすればどれだけ確保すれば効率がよいのかを判断し、必要に応じてbrkシステムコールやmmapシステムコールを発行して、成功してはじめて仮想メモリが確保されます。mallocのたびに毎度毎度仮想メモリ領域を増やしているとオーバーヘッドが大きいので、最初になるべく大きく確保し、その中から利用していない仮想メモリを選んで割り当てています。非常に賢いですね。
Copyright © ITmedia, Inc. All Rights Reserved.