Personal tools
You are here: Home ブログ 井上 Windowsのタスクマネージャの闇
« December 2010 »
Su Mo Tu We Th Fr Sa
      1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31  
Recent entries
Apache2.4のリリース予定は来年(2011年)初め(あくまで予定) inoue 2010-12-23
Herokuの発音 inoue 2010-12-20
雑誌記事「ソフトウェア・テストPRESS Vol.9」の原稿公開 inoue 2010-12-18
IPA未踏のニュース inoue 2010-12-15
労基法とチキンゲーム inoue 2010-12-06
フロントエンドエンジニア inoue 2010-12-03
ASCII.technologies誌にMapReduceの記事を書きました inoue 2010-11-25
技術評論社パーフェクトシリーズ絶賛発売中 inoue 2010-11-24
雑誌連載「Emacsのトラノマキ」の原稿(part8)公開 inoue 2010-11-22
RESTの当惑 inoue 2010-11-22
「プログラマのためのUXチートシート」を作りました inoue 2010-11-19
「ビューティフルコード」を読みました inoue 2010-11-16
Categories
カテゴリなし
 
Document Actions

Windowsのタスクマネージャの闇

和歌山で一番のオシャレボーイが、Windowsのタスクマネージャの仮想メモリは、Unixのps(1)やtop(1)などで見えるVSZ(仮想メモリサイズ)とは違うので、見えないメモリリークに気をつけるべし、と教えてくれました。

参考サイト

タスクマネージャのメモリ関連の列は3つあります(デフォルトでは非表示の列です)。メモリ使用量、最大メモリ使用量、仮想メモリサイズの3つです。上のサイトの英語との対応を書くと、それぞれ、"Mem Usage"、"Peak Mem Usage"、"VM Size"です。

今まで、語感から、「メモリ使用量」がUnixでのresident size(=物理(実)メモリの使用量)に相当し、「仮想メモリサイズ」がUnixでのvirtual memory size(=仮想メモリの使用量)に相当するのだろうと思っていました(最大メモリ使用量はピーク時の値と予想)。

「メモリ使用量」が物理メモリの使用量(resident size相当)であるのは正しいようです。しかし、「仮想メモリサイズ」の方は、非共有メモリの仮想メモリの使用量と書いてあります。具体的なAPIとの関係を明確にするため、試してみました。

#include <stdio.h>
#include <assert.h>
//#include <stdlib.h>  //malloc
#include <windows.h>

#define MEMSZ (1024 * 1024 * 32)
int main(int argc, char **argv)
{
    HANDLE hh = GetProcessHeap();
    while (1) {
        char* pt = (char*)HeapAlloc(hh, 0, MEMSZ);
        //      char* pt = (char*)malloc(MEMSZ);
        //      char* pt = (char*)GlobalAlloc(GMEM_FIXED, MEMSZ);
        assert(pt);
        printf("%p\n", pt);
        getchar();
    }
    return 0;
}

Windows APIのメモリ確保周りはカオスで良く分からないのですが、HeapAlloc()、GlobalAlloc()、malloc()の3つぐらい見ておけば事足りるだろうと思います。上のサンプルはGlobalAlloc()とmalloc()をコメントアウトしてあります。3つのどれかひとつのコメントアウトを外して動かしました。無限ループに入るので、リターンキーを押し続けてメモリを確保し続けます。

上のサンプルを動かして、タスクマネージャのプロセス一覧を観察すると、「仮想メモリサイズ」はリターンキーを押すごとに増加します。「メモリ使用量」はほとんど増えません。これは予想どおりの動作です。

#include <stdio.h>
#include <assert.h>
//#include <stdlib.h>  //malloc
#include <windows.h>

#define MEMSZ (1024 * 1024 * 32)
int main(int argc, char **argv)
{
    HANDLE hh = GetProcessHeap();
    while (1) {
        char* pt = (char*)HeapAlloc(hh, 0, MEMSZ);
        //      char* pt = (char*)malloc(MEMSZ);
        //      char* pt = (char*)GlobalAlloc(GMEM_FIXED, MEMSZ);
        assert(pt);
        for (int i = 0; i < MEMSZ; i++) {
            pt[i] = '\0';
        }
        printf("%p\n", pt);
        getchar();
    }
    return 0;
}

確保したメモリを参照するコードをいれます。このサンプルコードを動かしてタスクマネージャを見ると、「仮想メモリサイズ」も「メモリ使用量」も同時に増えていきます。

確保したメモリを参照すると、物理メモリに(割り当てたページを)マップするからです。他のプロセスが物理メモリを要求して、物理メモリの容量が足りなくなれば、物理メモリにマップされたページはスワップファイルにページアウト(スワップアウト)されます。そうすると、タスクマネージャの「メモリ使用量」の値は減るはずです。この辺の動作はUnixと同じはずなので、わざわざ確認はしていません。

さて、本題はここからです。タスクマネージャの「仮想メモリサイズ」の値が増加しないのに、プロセスのメモリ使用量が増えるパターンがあります。次のサンプルです。

#include <stdio.h>
#include <assert.h>
#include <windows.h>

#define MEMSZ (1024 * 1024 * 32)

int main(int argc, char **argv)
{
    while (1) {
        HANDLE h = CreateFileMapping(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, 0, MEMSZ, NULL);
        assert(h);
        char* pt = (char*)MapViewOfFile(h, FILE_MAP_READ|FILE_MAP_WRITE, 0, 0, MEMSZ);
        assert(pt);
        /*
        for (int i = 0; i < MEMSZ; i++) {
            pt[i] = '\0';
        }
        */
        printf("%p\n", pt);
        getchar();
    }
    return 0;
}

確保したメモリを参照するforループはコメントアウトしてあります。コメントアウトした状態で動かしてタスクマネージャのプロセス一覧を見ると、「メモリ使用量」も「仮想メモリサイズ」も値はほとんど増加しません。しかし、タスクマネージャのパフォーマンスタブを見ると、システムのメモリ使用量は上がっていきます。

メモリを参照するforループのコメントアウトを外すと、タスクマネージャの「メモリ使用量」の値は上がり、「仮想メモリサイズ」の値は上がらない、というUnix的な感覚からすると奇妙な状況になります。

対比のために、GNU/Linuxでも似たようなことをしてみます。

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>

#define MEMSZ  (1024*1024*32)

int main()
{
    while (1) {
        int i;
        char* pt = malloc(MEMSZ);
        assert(pt);
        for (i = 0; i < MEMSZ; i++) {
            pt[i] = '\0';
        }
        printf("%p\n", pt);
        getchar();
    }
    return 0;
}

このサンプルを動かして、top(1)、ps(1)、あるいは/proc/${process-id}/statmなどを見ると、resident size(以下、RES)もvirtual memory size(以下、VSZ)もどちらの値も増加します。メモリを参照するforループを無くせば、RESの値は増加しなくなります。

GNU/Linuxのglibcのmalloc(3)は、要求メモリのサイズが一定量より大きい場合、内部でmmap(2)を呼びます(strace(1)で見ると、実際にはmmap2(2)でした)。上のサンプルコードのMEMSZ(=32MB)の値の場合、mmap2(2)を呼びます。

MEMSZを32KBに変えると、システムコールとしてはbrk(2)を呼ぶようになります。top(1)やps(1)で観測できるRESやVSZは、期待どおりの変化を見せます。ちなみに、/proc/${process-id}/mapsファイルの見え方は、mmap(2)とbrk(2)で少し違いがあります。興味がある人は実験してみてください。

WindowsのCreateFileMapping()のコードに相当するサンプルを試してみます。

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <sys/mman.h>

#define MEMSZ  (1024*1024*32)

int main()
{
    while (1) {
        int i;
        char* pt = mmap(NULL, MEMSZ, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0);
        assert(pt);
        for (i = 0; i < MEMSZ; i++) {
            pt[i] = '\0';
        }
        printf("%p\n", pt);
        getchar();
    }
    return 0;
}

これを動かしても、top(1)やps(1)で見えるRESやVSZは期待どおりの動きです。つまり、アプリケーションがメモリを確保すればVSZの値は増えますし、メモリを参照して物理メモリに乗ればRESの値は増えます。ちなみに、このサンプルの/proc/${process-id}/mapsファイルの見え方も少しだけ違うので、興味のある人は試してみてください。

メモリ管理に関してOSの奥深くではどちらのOSもそれなりに複雑なことをしているはずですが、ユーザ空間への見せ方という観点では、Unix系がシンプルで分かりやすいです。

もっとも、かつてのUnixには暗黒の歴史もありました。SVR4系のshm(共有メモリ)やセマフォ周りは、プロセスが不当に死ぬと確保されたリソース(共有メモリやセマフォ)が残り続ける上に、特別なコマンドを使わないとリソースが残っていることが分からず、それらのリソースを解放することもできない、というひどい状況でした。

The URL to Trackback this entry is:
http://dev.ariel-networks.com/Members/inoue/win-taskmgr/tbping

Re:Windowsのタスクマネージャの闇

Posted by horii at 2008-09-09 21:29
>和歌山で一番のオシャレボーイ

表現が古いww
Add comment

You can add a comment by filling out the form below. Plain text formatting.

(Required)
(Required)
(Required)
This helps us prevent automated spamming.
Captcha Image


Copyright(C) 2001 - 2006 Ariel Networks, Inc. All rights reserved.