dshimizu/blog

アルファ版

vm.overcommit_memoryの動きをソースを見ながら何となく調べてみたことのメモ書き

Linuxカーネルパラメータvm.overcommit_memory=2(メモリのオーバーコミットをさせない設定)の時、vm.overcommit_ratioの値を99とかではなく、カーネルが使うメモリを確保するために80とかにして余裕を持たせておく必要があるとかないとかの話を聞いたことがあり、実際には各プロセスごとに仮想メモリ空間を確保するので 80 とかでも大量のプロセスが立ち上がって物理メモリが確保されるとダメなのではと思いつつ、結局のところどうなっているのか、現時点での最新カーネルをなんとなく読みながら調べてみたので書いておく。

オーバーコミットとその制限

そもそもオーバーコミットとは何ぞやというと、プロセスが仮想メモリを確保するとき、各プロセスごとに仮想メモリ空間が割り当てられる。 その時、x86では4GB、x86_64では128TBの仮想アドレス空間を利用でき、実際に搭載されている物理メモリーより大きな値を扱う(≒オーバーコミットする)ことができる...という認識。

物理メモリーより大きい値を扱うと問題になることがあるため、オーバーコミットを制限するためのカーネルパラメータがvm.overcommit_memoryで、プロセス全体で使用できる仮想メモリ空間を、物理メモリ量を超えないように制限するための制限値が2である...と思っている。

ソースコードを見てみる

とりあえずGitHubに上がっているLinuxカーネルソースコードを読んでみることにした。 この時のLinuxカーネルのバージョンは Linux Kernel 4.16 RC のものだったと思う。

overcommit_memoryとかでgrepしながら追ってみると該当部分の処理はlinux/mm/util.c__vm_enough_memory()というところらへんに何かあった。 コメントの記載をみると、仮想アドレス空間を物理メモリにマッピングするときに、十分なメモリがあるかどうかチェックしているもののようだった。

__vm_enough_memory()関数の最初の vm_acct_memory(pages);で Committed_AS (実際に確保されているメモリ量)の加算が行われ、その後、カーネルパラメータvm.overcommit_memoryの値によって処理を分岐させている模様。

OVERCOMMIT_NEVER(vm.overcommit_memory=2 つまりオーバーコミットしない)の場合は同一ファイル内のvm_commit_limit()が呼び出されている。

/*
 * Check that a process has enough memory to allocate a new virtual
 * mapping. 0 means there is enough memory for the allocation to
 * succeed and -ENOMEM implies there is not.
 *
 * We currently support three overcommit policies, which are set via the
 * vm.overcommit_memory sysctl.  See Documentation/vm/overcommit-accounting
 *
 * Strict overcommit modes added 2002 Feb 26 by Alan Cox.
 * Additional code 2002 Jul 20 by Robert Love.
 *
 * cap_sys_admin is 1 if the process has admin privileges, 0 otherwise.
 *
 * Note this is a helper function intended to be used by LSMs which
 * wish to use this logic.
 */
int __vm_enough_memory(struct mm_struct *mm, long pages, int cap_sys_admin)
{
    long allowed;

    VM_WARN_ONCE(percpu_counter_read(&vm_committed_as) <
            -(s64)vm_committed_as_batch * num_online_cpus(),
            "memory commitment underflow");

    vm_acct_memory(pages);

    /*
    * Sometimes we want to use more memory than we have
    */
    if (sysctl_overcommit_memory == OVERCOMMIT_ALWAYS)
        return 0;

    if (sysctl_overcommit_memory == OVERCOMMIT_GUESS) {
        free = global_zone_page_state(NR_FREE_PAGES);
        free += global_node_page_state(NR_FILE_PAGES);

        /*
        * shmem pages shouldn't be counted as free in this
        * case, they can't be purged, only swapped out, and
        * that won't affect the overall amount of available
        * memory in the system.
        */
        free -= global_node_page_state(NR_SHMEM);

        free += get_nr_swap_pages();

        /*
        * Any slabs which are created with the
        * SLAB_RECLAIM_ACCOUNT flag claim to have contents
        * which are reclaimable, under pressure.  The dentry
        * cache and most inode caches should fall into this
        */
        free += global_node_page_state(NR_SLAB_RECLAIMABLE);

        /*
        * Part of the kernel memory, which can be released
        * under memory pressure.
        */
        free += global_node_page_state(NR_KERNEL_MISC_RECLAIMABLE);

        /*
        * Leave reserved pages. The pages are not for anonymous pages.
        */
        if (free <= totalreserve_pages)
            goto error;
        else
            free -= totalreserve_pages;
        
        
        /*
        * Reserve some for root
        */
        if (!cap_sys_admin)
            free -= sysctl_admin_reserve_kbytes >> (PAGE_SHIFT - 10);

        if (free > pages)
            return 0;

        goto error;
    }


    allowed = vm_commit_limit();
    /*
    * Reserve some for root
    */
    if (!cap_sys_admin)
        allowed -= sysctl_admin_reserve_kbytes >> (PAGE_SHIFT - 10);

    /*
    * Don't let a single process grow so big a user can't recover
    */
    if (mm) {
        reserve = sysctl_user_reserve_kbytes >> (PAGE_SHIFT - 10);
        allowed -= min_t(long, mm->total_vm / 32, reserve);
    }

    if (percpu_counter_read_positive(&vm_committed_as) < allowed)
        return 0;
error:
    vm_unacct_memory(pages);

    return -ENOMEM;

}
    :
    :
    :
/*
 * Committed memory limit enforced when OVERCOMMIT_NEVER policy is used
 */
unsigned long vm_commit_limit(void)
{
    unsigned long allowed;

    if (sysctl_overcommit_kbytes)
        allowed = sysctl_overcommit_kbytes >> (PAGE_SHIFT - 10);
    else
        allowed = ((totalram_pages - hugetlb_total_pages())
               * sysctl_overcommit_ratio / 100);
    allowed += total_swap_pages;

    return allowed;
}

vm_acct_memory()関数はinclude/linux/mman.hに定義されている。

static inline void vm_acct_memory(long pages)
{
    percpu_counter_add_batch(&vm_committed_as, pages, vm_committed_as_batch);
}

static inline void vm_unacct_memory(long pages)
{
    vm_acct_memory(-pages);
}

カーネルパラメータvm.overcommit_memoryの値を各条件分岐で見てovercommitの設定値に応じて必要な処理を行い、問題なければ0を、メモリ確保できないようなら-ENOMEM (Out of Memory)を返すようになっているように見える。

vm.overcommit_memory=2の場合、vm.overcommit_kbytes>vm.overcommit_ratioのどちらかの値が利用されるようで、 overcommit_kbytes0 以外の場合は overcommit_kbytes の設定値が利用され(allowed = sysctl_overcommit_kbytes >> (PAGE_SHIFT - 10); で算出され)、0 の場合は overcommit_ratio の値が使われている(allowed = ((totalram_pages - hugetlb_total_pages()) * sysctl_overcommit_ratio / 100);で割り当て可能なメモリ量が算出されている)。

vm.overcommit_memory=0 or vm.overcommit_memory=2の場合は、if(!cap_sys_admin)でプロセスに管理者権限があるかどうかチェックされており、cap_sys_adminが0の時、つまり管理者権限がない時にはadmin_reserve_kbytes分のメモリがallowからマイナスされて確保されるようになっているように思った。 (vm.overcommit_memory=0のときは free として、vm.overcommit_memory=2のときは予約ページとして確保されている?)

また、その後、reserve = sysctl_user_reserve_kbytes >> (PAGE_SHIFT - 10);で1つユーザプロセスが肥大化しないようにvm.user_reserve_kbytes分の値を確保していた。

最後のif (percpu_counter_read_positive(&vm_committed_as) < allowed)の部分で、committed_asの値がallowed(≒CommitLimit)を越えなければ大丈夫、ということなのかと思った。

いろいろたどっていくと__vm_enough_memory()security/security.c内のsecurity_vm_enough_memory_mm()で呼ばれていた。

int security_vm_enough_memory_mm(struct mm_struct *mm, long pages)
{
    struct security_hook_list *hp;
    int cap_sys_admin = 1;
    int rc;

    /*
    * The module will respond with a positive value if
    * it thinks the __vm_enough_memory() call should be
    * made with the cap_sys_admin set. If all of the modules
    * agree that it should be set it will. If any module
    * thinks it should not be set it won't.
    */
    hlist_for_each_entry(hp, &amp;security_hook_heads.vm_enough_memory, list) {
        rc = hp-&gt;hook.vm_enough_memory(mm, pages);
        if (rc &lt;= 0) {
            cap_sys_admin = 0;
            break;
        }
    }
    return __vm_enough_memory(mm, pages, cap_sys_admin);
}

admin_reserve_kbytesはドキュメント(/Documentation/sysctl/vm.txt)に記載があった。 vm.overcommit_memory=2とした時、何かあった場合に管理者がログインしてプロセスを強制終了したりできるようにするためには、sshdまたはlogin + bashあたりが実行できるサイズを確保できるよう十分な値に調整する必要がある値のように思った。

admin_reserve_kbytes

The amount of free memory in the system that should be reserved for users
with the capability cap_sys_admin.

admin_reserve_kbytes defaults to min(3% of free pages, 8MB)

That should provide enough for the admin to log in and kill a process,
if necessary, under the default overcommit 'guess' mode.

Systems running under overcommit 'never' should increase this to account
for the full Virtual Memory Size of programs used to recover. Otherwise,
root may not be able to log in to recover the system.

How do you calculate a minimum useful reserve?

sshd or login + bash (or some other shell) + top (or ps, kill, etc.)

For overcommit 'guess', we can sum resident set sizes (RSS).
On x86_64 this is about 8MB.

For overcommit 'never', we can take the max of their virtual sizes (VSZ)
and add the sum of their RSS.
On x86_64 this is about 128MB.

Changing this takes effect whenever an application requests memory.

==============================================================

user_reserve_kbytesはドキュメント(/Documentation/sysctl/vm.txt)を見ると、ユーザープロセスがメモリーを占有しないようにあらかじめ予約しておくためのもののようだった。

- user_reserve_kbytes

When overcommit_memory is set to 2, "never overcommit" mode, reserve
min(3% of current process size, user_reserve_kbytes) of free memory.
This is intended to prevent a user from starting a single memory hogging
process, such that they cannot recover (kill the hog).

user_reserve_kbytes defaults to min(3% of the current process size, 128MB).

If this is reduced to zero, then the user will be allowed to allocate
all free memory with a single process, minus admin_reserve_kbytes.
Any subsequent attempts to execute a command will result in
"fork: Cannot allocate memory".

Changing this takes effect whenever an application requests memory.

==============================================================

ちなみにUbuntu16.04で見ると、デフォルト値のパラメータはvm.overcommit_memory=0vm.admin_reserve_kbytes=8192でドキュメントの通りだった。

vm.admin_reserve_kbytes = 8192
vm.overcommit_kbytes = 0
vm.overcommit_memory = 0
vm.overcommit_ratio = 50

検証

メモリ4GBのLinux環境化で、以下の設定値の状態で、仮想メモリ空間を確保するプログラムを実行してみる。

vm.admin_reserve_kbytes = 8192
vm.overcommit_kbytes = 0
vm.overcommit_memory = 0
vm.overcommit_ratio = 50

下記のようなものとする。 get_vmem_test.c として保存する。

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

int main() {
  char *p;
  unsigned long i = 0;

  // 文字列変数に1GB(1073741824 B)のメモリを確保し続ける
  // メモリを確保できなくなるとmallocはnullを返すため、nullが返されるまで繰り返す
  do {
    p = (char *)malloc(1073741824);

    // printf でアウトプットするための値
    i=i+1073741824;
  } while (p != NULL);

  // iの値が確保できたメモリの総量となる(GB単位で出力)
  printf("%lu GB \n", i/1024/1024/1024);

  exit(0);
}

コンパイルする。

% gcc -o get_vmem_test get_vmem_test.c

実行する。 x86_64なので、get_vmem_testのプロセスを実行することで、128TB程度の仮想メモリが確保された。

% ./get_vmem_test
122216 GB

ではOverCommitをしないように変更して、割り当て可能なメモリを物理メモリ+スワップの合計値の99%にした場合の動きを見てみる。 この環境はメモリ4GBの環境なので、実行結果も4GB近くになるはず。

% sudo sysctl -w vm.overcommit_ratio=99 && sudo sysctl -w vm.overcommit_memory=2
% sudo sysctl -a | grep vm.over
vm.overcommit_ratio = 99
vm.overcommit_memory = 2

プログラムを実行すると、4GBになった。 なので、code>vm.overcommit_memoryやovercommit_ratioの設定値は、1プロセス毎の仮想メモリ割り当ての制御であることがわかる。

% ./get_vmem_test
4 GB

同じプログラムを同時に実行した場合、各プロセスごとに4GBの仮想メモリ空間が確保される。この時点では物理メモリは割り当てられていない。 もし複数のプロセスが動いている環境で、各プロセスが物理メモリを割り当てようとしたときにエラーになる可能性はある。なので、vm.overcommit_memory=2(メモリのオーバーコミットをさせない設定)の時、vm.overcommit_ratioが 99 とか 80 とかがどの環境でも必ずしも適切な値とは言えないのでは、と思った。

まとめ

vm.overcommit_memoryの値における挙動についてソースコードを読みながら調べてみた。

  1. vm.overcommit_memoryovercommit_ratioは、ある1つのプロセスへの仮想メモリ割り当てを制御するもの。仮想メモリ確保後に物理メモリを割り当てる際に、実際にはメモリ不足でそのプロセスがOutOfMemoryとかでエラーになって停止する可能性はある
  2. user_reserve_kbytesで1つのプロセスが肥大化しないような制御や、admin_reserve_kbytesで管理者用のメモリ領域が確保される動きになっているように思った。

参考