リソースが潤沢とはいえない組込みシステムにおいて,ある機能の使用前と使用後でリソース(特にメモリの使用量)が同じ状態に戻るということはとても重要なことです.しかし組込Linuxシステムにおいて,メモリリークのような不具合が見当たらないにも関わらずある機能の使用後にすべてのメモリが開放されていないと思われる動きをすることがあります.
本稿ではメモリアロケータの動きからこういった現象が起きる原因を紐解き,メモリの確保や解放をどのようにするべきなのかを考えていきたいと思います.
なお,ここでは広くLinuxで使用されているglibcのメモリアロケータを想定しています.
malloc() で確保したメモリは free() で開放しても物理メモリが解放されないケースがあることに注意が必要です.
MMAP_THRESHOLD (デフォルトでは128KB) 以上のメモリを malloc() で確保する場合,malloc() は内部で mmap() を用いて指定サイズの領域を確保し,そのアドレスを返します.mmap() によるメモリ確保には以下のような特徴があります.
MMAP_THRESHOLD 未満のサイズのメモリを malloc() で確保する場合,”解放済メモリリスト”を検索し指定サイズの領域が確保できた場合はそのアドレスを返します.ヒープ領域が足りない場合は malloc() 内部で sbrk() を呼び出しヒープの拡張を行います.こうして確保されたメモリが free() されると”解放済メモリリスト”に登録され,物理的には解放されません.
sleep状態で常駐するプログラムが多い組込システムでは,この動作が問題となります.
ではすべてmmapすれば良いのではないか?というのは間違いではないのですが,メモリ確保の性能は malloc() > mmap() なのでなんでもかんでも mmap() するわけにもいきません.
上述の問題を回避するための対策として,
MMAP_THRESHOLD は128KB以上512KB未満の malloc() と free() を行うことによってそのサイズが最大512KBまで引き上げられます.これにより,以降の malloc() / free() では引き上げられた MMAP_THRESHOLD のサイズ未満の物理メモリは解放されなくなります.この動作を抑制するために mallopt() というインタフェースが存在します.mallopt() で MMAP_THRESHOLDの値を設定することで動的な閾値変更を無効化し malloc() の閾値を変更できます.これにより,MMAP_THRESHOLD 以上のサイズの malloc() では mmap() が使用されるようになり,free() を実行した際に物理的にメモリが解放されるようになります.
通信用バッファなどで単方向のメモリアクセスをする場合,ページキャッシュを解放することで物理メモリの空き容量増加が期待できます.ページキャッシュの解放は madvise() で MADV_DONTNEED を指定します.これでカーネルに対して,リソースはもう解放しても良いと伝える(アドバイスする)ことができます.このアドバイスを受け入れるかどうかはカーネルに任されます.この方法は静的メモリを解放する場合に有効です.静的メモリの場合,いちど物理メモリが割り当てられてしまうと(わたしの知る限りでは)他の方法でメモリを解放することができません.
動的メモリの確保と解放は上述の通りそれぞれ対応するインタフェースがあるので,madvise() は基本的に使用すべきではありません.しかし,メモリを解放したいけれどそのメモリにそれ以上アクセスしないことを保証できない場合は madvise() で解放するといった戦略を採ることもできます.その場合,物理メモリの解放が行われるますが,論理メモリの解放は行われないため,もしアクセスがあった場合0が読まれることになります.
madvise() で解放するメモリは4KBバウンダリから4KB単位であることに注意しましょう.解放したい領域が madvise() する領域に完全に包含されていない場合,予期しない動作を引き起こすことになります.
上述の通り,MMAP_THRESHOLD を超えるサイズのメモリを malloc() で確保すると,mmap() が呼び出されます.この時 mmap() は物理メモリをまだ割り当てておらず,指定サイズの論理アドレスのみが割り当てられます.この領域に対して読み書きを行う段階ではじめて4KB単位で物理メモリが確保され,確保された物理メモリは free() / munmap() されるまでは解放されません.また,上述の通りこの領域はゼロ保証されています.したがって,malloc() した後 memset() などでゼロ初期化する場合,以下の2つの問題があります.