障らぬ仮想メモリに祟りなし

2021年6月14日

前回はpsコマンドのSZについて調べました。

結局仮想メモリ含む全メモリという説明をしたのですが…じゃあSZが1GB使ってて、RSSが100MBだったら、残り900MBがスワップなのかというとそういうわけでもありません。スワップなんて全くされてない…なんてこともあります。今回はこのカラクリを説明しようと思います。

やること

  1. 仮想メモリを確保する
  2. 確保したメモリを0クリアする
  3. メモリを開放する

ただし、各実施手順直後にps -eo uid,pid,rss,vsz,argsを実施します。なお、事前にswapは使用しない設定にしておきます(今回はswap設定なし)。

$ free
              total        used        free      shared  buff/cache   available
Mem:        4030844     1343484     1189184       61208     1498176     2357696
Swap:             0           0           0
$

プログラム

#include <sys/mman.h>
#include <cstring>
#include <string>
#include <iostream>

using namespace std;

int main(int argc, char* argv[]) {
        const size_t length = 1024 * 1024 * 1024;
        void* pages = mmap(NULL, length, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
        try {
                if (pages == MAP_FAILED) {
                        throw "エラー";
                }
                cout << "仮想メモリ確保" << endl;
                string s;
                getline(cin, s);

                memset(pages, 0, length);
                cout << "0クリア" << endl;
                getline(cin, s);

                if (munmap(pages, length) != 0) {
                        throw "エラー";
                }
                cout << "開放" << endl;
                getline(cin, s);
        }
        catch(const char* s) {
                perror("hoge");
                if (pages != MAP_FAILED) {
                        munmap(pages, length);
                }
                return 1;
        }
        return 0;
}

何のことはないプログラムです。Linuxでページメモリを確保するにはmmapを使います。Cの標準関数mallocとかが内部で使ってるシステムコールです。

実行

$ make hoge

面倒なので暗黙ルールだけでビルドしちゃいます。あとはhogeを実行するだけです。

$ ./hoge
仮想メモリ確保

こんな状態になったら、確保だけしてEnter入力待ちしてるので、別端末からpsします。

$ ps -eo uid,pid,rss,vsz,args | grep hoge
 1000  121674  1516 1054452 ./hoge
 1000  121690   732  10084 grep --color=auto hoge
$ 

元の端末でEnterを押すと…

$ ./hoge
仮想メモリ確保

0クリア

0クリアされました。また別端末からpsします。

$ ps -eo uid,pid,rss,vsz,args | grep hoge
 1000  121674 1051520 1054452 ./hoge
 1000  121711   728  10084 grep --color=auto hoge
$

さらに元端末でEnterを押すと…

$ ./hoge
仮想メモリ確保

0クリア

開放

開放されました。また別端末からpsします。

$ ps -eo uid,pid,rss,vsz,args | grep hoge
 1000  121674  3076   5876 ./hoge
 1000  121735   728  10084 grep --color=auto hoge
$

さて、これで何が分かるのでしょう?

分かること

rss(実メモリ)vsz(仮想メモリ)
仮想メモリ確保15161054452
0クリア10515201054452
開放30765876

仮想メモリの0クリア時に、rss(実メモリ)が1GBほど増えています。そして仮想メモリの開放と同時にrss(実メモリ)が1GBほど減ってます。これはどういうことかというと、(mmapでMAP_ANONYMOUSを指定して確保した)「仮想メモリは使用すると実メモリが割り当てられる」ということです。

未使用の仮想メモリはそのアドレスにアクセスできるというだけで、実体がありません。アクセスされたときにはじめてページ(4KB)という単位で実体が割り当てられます。ここで実体とは実メモリのことなので、rss(実メモリ)が増えるという仕組みなのです。その際に実メモリが足りない場合、あまり使用されていない実メモリがスワップに書き込まれ、空いた実メモリがこのアドレスに割り当てられます。スワップが足りない場合は、セグメンテーション違反が発生します。メモリ確保時でもないのにそんな例外が発生することにまず驚愕すると思いますが、そういう仕組みなので仕方ありません。気になる場合は0クリアすればいいだけですしね。

何はともあれ、仮想メモリは確保しただけで未使用なら何も圧迫しないということが分かったと思います。

malloc(C++のnew)ならどうなるか?

今回はmallocを内部で使うC++のnew演算子(デフォルト)で実験です。

#include <cstring>
#include <string>
#include <iostream>

using namespace std;

int main(int argc, char* argv[]) {
        const size_t length = 1024 * 1024 * 1024;
        char* pages = nullptr;
        new char[length];
        try {
                pages = new char[length];
                cout << "メモリ確保" << endl;
                string s;
                getline(cin, s);

                memset(pages, 0, length);
                cout << "0クリア" << endl;
                getline(cin, s);

                delete[] pages;
                pages = nullptr;
                cout << "開放" << endl;
                getline(cin, s);
        }
        catch(const char* s) {
                perror("hoge");
                if (pages != nullptr) {
                        delete[] pages;
                }
                return 1;
        }
        return 0;
}

今回はこれを使います。手順は同じなので、いきなり結果だけ。

rss(実メモリ)vsz(仮想メモリ)
メモリ確保15202103036
0クリア10515882103036
開放31561054456

実メモリの動きはC++のnewでも同じですね。newで確保したサイズの倍程度の仮想メモリが用意されていますが、それ以外は想定どおり「確保したメモリは使用すると実メモリが割り当てられ」ています。

仮想メモリは不要?

実際にアドレスを確保しただけならほとんどメモリを圧迫しないということなので、なら実メモリだけが必要で、仮想メモリという指標は要らないのでは?と思うかもしれません。しかし待ってください。今回は意図的に外してますが、実際にはスワップもあるわけです。

swap on

まずは

https://www.digitalocean.com/community/tutorials/how-to-add-swap-space-on-ubuntu-20-04-ja

を読んでスワップ使ってみましょう。

$ sudo fallocate -l 4G /swapfile
$ sudo chmod 600 /swapfile
$ sudo mkswap /swapfile
$ sudo swapon /swapfile
$ free
              total        used        free      shared  buff/cache   available
Mem:        4030844     1337404     1187296       61720     1506144     2363220
Swap:       4194300           0     4194300
$

永続化は今回不要なのでしません。

4GBバージョン

hogeの4GBバージョンをhoge3として作成します。length変えるだけなので、ソースは再掲しません。

これを実行して一時停止したところでpsコマンドを打つわけですが、今回はさらにcat /proc/[pid]/statusも見て、有用そうなパラメータを抜き出しました。結果がコレ。

rss(実メモリ)vsz(仮想メモリ)VmSwapVmHWM
メモリ確保1528420018001528
0クリア3119744420018010874483386592
開放62058761203386592

VmSwapがないと使用メモリが良く分かりませんね。しかしこれpsでは表示できません。取得はしてるみたいなんですけど、出力するオプション指定がないのです。

VmHWMはVmHighWaterMarkです。最高水位の記録ですね。実際に(実メモリを)使用した仮想メモリのサイズのMax値を記録してるようです。

VmHWM: peak resident set size (“high water mark”)

https://www.kernel.org/doc/html/latest/filesystems/proc.html

結論

RSSが正義でVSZは微妙ですね。mallocが倍くらいページ確保しちゃうようだし、未使用分が常にあり、スワップが分からない以上、混乱しかしそうにないです。

まとめ

仮想メモリは使用されるまで実体がなく、使用時に実メモリが随時4KBずつ割り当てられる。ので注意。

続き的な記事を書きました。