組込Linuxでメモリ確保する時の注意点
リソースが潤沢とはいえない組込システムにおいて,ある機能の使用前と使用後でリソース(特にメモリの使用量)が同じ状態に戻るということはとても重要なことです.しかし組込Linuxシステムにおいて,ある機能の使用後にすべてのメモリが開放されていないと思われる動きをすることがあります.本稿ではメモリアロケータの動きからこういった現象が起きる原因を紐解き,メモリの確保や解放をどのようにするべきなのかを考えていきたいと思います.なお,ここでは広くLinuxで使用されているglibcのメモリアロケータを想定しています.
malloc() / free() で物理メモリが解放されないことがある?
malloc() の動作は確保するメモリ量で変化します.場合によっては free() で開放しても物理的にメモリが解放されないケースがあることに注意が必要です.
MMAP_THRESHOLD 以上の malloc() の動作
MMAP_THRESHOLD (デフォルトでは128KB) 以上のメモリを malloc() で確保する場合,malloc() は内部で mmap() を用いて指定サイズの領域を確保しそのアドレスを返します.mmap() によるメモリ確保には以下のような特徴があります. * 確保したメモリは munmap() で物理的にメモリを解放する.malloc() 内で mmap() された領域は free() する時に munmap() される. * 4KB単位でしかメモリの確保はできない * mmap() で確保したメモリはゼロ初期化は不要(ゼロ保証されている) * free() されたメモリは”解放済メモリリスト”には登録されず,物理的に解放される
MMAP_THRESHOLD 未満の malloc() の動作
MMAP_THRESHOLD 未満のサイズのメモリを malloc() で確保する場合,"解放済メモリリスト"を検索し指定サイズの領域が確保できた場合はそのアドレスを返します.ヒープ領域が足りない場合は malloc() 内部で sbrk() を呼び出しヒープの拡張を行います.こうして確保されたメモリが free() されると"解放済メモリリスト"に登録され,物理的には解放されません.
sleep状態で常駐するプログラムが多い組込システムでは,この動作が問題となります.
ではすべてmmapすれば良いのではないか?というのは間違いではないのですが,メモリ確保の性能は malloc() > mmap() であることを念頭に検討する必要があるでしょう.
物理メモリをできるだけ解放するには
上述の問題を回避するための対策として, * MMAP_THRESHOLD の動的な変化を抑制する * ページキャッシュの解放をカーネルにアドバイスする * 不要なゼロ初期化を避ける といった対策が考えられます.以下,1つづつみていきます.
MMAP_THRESHOLD の動的な変化を抑制する
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つの問題があります.
- ゼロ保証されている領域をゼロ初期化する(無駄な処理)
- 本来必要ないかもしれないメモリを物理的に確保してしまう
メモリプールやバッファと称して巨大なメモリをプロセス起動時に一括で確保しようとするようなプログラムが,特に古いシステムから移植してきたソースコードなどにみられます.あわせて,すぐに memset() で初期化するようなお作法もよく見かけます.Linuxベースの組込システムでこれをやると機能使用していないプロセスが巨大なメモリ領域を抱えたままsleep状態になりシステム全体のメモリが足りない!となりかねないため注意が必要です.
まとめ
- 確保するメモリサイズによって malloc() の動作が異なる
- 組込システムでは MMAP_THRESHOLD の動的変化は抑制しておいた方が良い
- madvise() でカーネルにリソースが不要であることを伝えることはできる(が解放されるかはカーネルしだい)
- 不要なゼロ初期化はやめよう
参考
- malloc(3)のメモリ管理構造 - VA LINUX SYSTEMS JAPAN
- man page of malloc(3)
- man page of mallopt(3)
- man page of mmap(2)
- man page of madvise(2)