/proc/sys/vm/lowmem_reserve_ratio について


この記事は[Linux Advent Calendar 2015](http://qiita.com/advent-calendar/2015/linux)の12/13の記事です。
一応、kernel ver4.3のソースはチェックしたつもりです。
が、Kernelのソースを見るのはずいぶん久しぶりなので、最新情報を見落としているかもしれません。

# lowmem_reserve_ratioの目的
Linux Kernelには、物理アドレス上、低いアドレス(lowmem)のメモリをなるべく使わずに取っておく(reserve)機能があります。lowmem_reserve_ratioとは、この"取っておく"メモリをどれくらい確保するのかを調整するための値です。
これは、低位アドレスのメモリを使わないようにする理由は、以下の例ようにその領域を必要とするものがあるからです。

  - kernel自身  
    ざっくりした言い方をすると、x86の32bitのシステムでは「kernelが簡単に使うことができる領域」は0～896MBの間です。それより高いアドレスの領域ではSLABまたはSLUBとよばれるkernelが使うメモリ管理機構はこの領域では使えません。また、仮想アドレスがストレートマップされていないので、一時的に仮想アドレスを割り当てるなど、面倒なことをしなければなりません。
  - レガシーなデバイス  
0～16Mbyte、あるいは64bit環境における0～4Gbyteなど、低位のアドレスのメモリしかアクセスできないデバイスがあります。このようなデバイスのために、低位のメモリ領域を空けておく必要があります。  

通常のプロセスがメモリを浪費して上記のようなメモリまで食いつくしてしまうのはよくありません。ですから、このように低位のメモリをあらかじめreserveしておく必要があるわけです。

# Zone
lowmem_reserve_ratioについて知るには、前提知識として、ZONEのことを知っておかなければなりません。
低位アドレスを管理するためにLinux Kernelは物理メモリをZONEと呼ばれる領域に分けて管理していて、
Kernel4.3では以下のようなZONEが定義されています。一番上のZONE_DMAが最も低い物理アドレスを管理するためのZONEで、下に行くにつれ高位のアドレスを管理するZONEになります。


  - ZONE_DMA
  - ZONE_DMA32
  - ZONE_NORMAL
  - ZONE_HIGHMEM
  - ZONE_MOVABLE
  - ZONE_DEVICE

なお、これらのZONEのうち、実際にどのZONEにどれくらいのメモリが割り当てられているかは、アーキテクチャ、搭載されているメモリ量、ブートオプションなどによって異なります。例えばZONE_DMAが示す領域は、kernelのソースに書かれているようにそれぞれのアーキテクチャによって異なっています。

```

#ifdef CONFIG_ZONE_DMA
        /*
         * ZONE_DMA is used when there are devices that are not able
         * to do DMA to all of addressable memory (ZONE_NORMAL). Then we
         * carve out the portion of memory that is needed for these devices.
         * The range is arch specific.
         *
         * Some examples
         *
         * Architecture         Limit
         * ---------------------------
         * parisc, ia64, sparc  <4G
         * s390                 <2G
         * arm                  Various
         * alpha                Unlimited or 0-16MB.
         *
         * i386, x86_64 and multiple other arches
         *                      <16M.
         */
        ZONE_DMA,
#endif
#ifdef CONFIG_ZONE_DMA32
```

ZONEの構成は/proc/zoneinfoを参照することで把握することができます。各ZONEのpresentの数がそのZONEに割り当てられているメモリのpage数です。

```
(/proc/zoneinfo)

Node 0, zone      DMA
  pages free     3973
        min      21
        low      26
        high     31
        scanned  0
        spanned  4095
        present  3997
        managed  3973
     :
     :
Node 0, zone    DMA32
  pages free     788519
        min      4811
        low      6013
        high     7216
        scanned  0
        spanned  1044480
        present  893356
        managed  873600
     :
     :
Node 0, zone   Normal
  pages free     567443
        min      3612
        low      4515
        high     5418
        scanned  0
        spanned  655360
        present  655360
        managed  655360
    :
    :
Node 0, zone  Movable
  pages free     2450391
        min      14082
        low      17602
        high     21123
        scanned  0
        spanned  2620928
        present  2620928
        managed  2555065

```

ZONE_MOVABLEとZONE_DEVICEは実は他のZONEとは用途が異なります。ZONE_MOVABLEは当該領域をすべてmemory migrationが可能な領域とするZONEで、メモリホットプラグなどの用途で利用します。通常はこのZONEにメモリが割り当てられてはいませんが、ブートオプションで例えば movablecore=10G と指定すると、10GのMOVABLEなZONEが割り当てられます。
（ZONE_DEVICEは筆者も今回初めて知ったのですが、理解が間違っていなければNon Volatile MemoryをホットプラグするためのZONEのようです。現在のところ、このZONEはkernelのconfigとコンパイルが必要で、通常は有効になっていません。）

# メモリの確保とZone

さて、物理メモリの利用者がその確保を要求するときには、要求側でGFP_xxxなどのオプションを指定することで、どのZONEを利用するかを指定します。例えば、以下ではalloc_pages()の__GFP_DMA32によって、4GByte以下の領域であるZONE_DMA32、あるいはそれより低位のZONE_DMAのメモリを確保するように要求しています。

```c
(arch/x86/kvm/mmu.c)

static int alloc_mmu_pages(struct kvm_vcpu *vcpu)
{
        struct page *page;
        int i;

        /*
         * When emulating 32-bit mode, cr3 is only 32 bits even on x86_64.
         * Therefore we need to allocate shadow page tables in the first
         * 4GB of memory, which happens to fit the DMA32 zone.
         */
        page = alloc_page(GFP_KERNEL | __GFP_DMA32);
        if (!page)
                return -ENOMEM;
```

GFP_HIGHUSERであれば、ZONE_HIGHMEM以下の低位のZONEならなんでもOKという指定、GFP_HIGHUSER_MOVABLEなら、ZONE_REMOVABLE,ZONE_HIGHMEM,ZONE_NORMAL,ZONE_DMA32,ZONE_DMAのいずれのZONEでもOKという指定になります。

ところで、GFP_HIGHUSERが指定されても、ZONE_DMA32やZONE_DMAなどのメモリも使えるようにしているのはなぜでしょうか？ZONE_HIGHMEMのメモリだけを割り当てればよいように思いませんか？
これは、システムのメモリ構成によっては利用できないメモリの量が大きくなってしまうからです。仮に1Gbyteのメモリを搭載しているx86(32bit)のシステムを考えてみましょう。（ハードウェアやファームウェアが利用している領域については考えないことにします）。そうするとこの場合、

 - ZONE_DMA: 0から16Mbyteまで,
 - ZONE_NORMAL: 16Mbyteから896Mbyteまで,
 - ZONE_HIGHMEM: 896Mbyteから1Gまで

となります。つまり、ZONE_HIGHMEMはたったの128Mbyteしかないわけです。ZONE_HIGHUSERを指定すると1Gbyteのうち128Mbyteしか使えないのは少々バランスが悪いですね。なので、kernelは「高位のZONEでもOK」というメモリを要求しているものに対しては、

 - 高位のアドレスを管理するZONEを優先的に割り当てる。
 - 高位のZONEの空きメモリが無ければ、より低位のZONEを割り当てる。
 - ただし、低位のZONEを割り当てる量は、本当に低位のZONEを要求するものよりもハンディキャップをつける。

という戦略をとっています。このハンディキャップの元となるのがlowmem_reserve_ratioです。

# ハンディキャップの値の参照
まず、lowmem_reserve_ratioによって、それぞれのzoneが実際にどれくらいのハンディキャップを与えられているかを見てみましょう。これは、/proc/zoneinfoのprotectionを参照することでわかります。

```
Node 0, zone      DMA
  :
        protection: (0, 3412, 5972, 15953)
  :
  :
Node 0, zone    DMA32
  :
        protection: (0, 0, 2560, 12540)
  :
  :
Node 0, zone   Normal
  :
        protection: (0, 0, 0, 79845)
  :
  :
Node 0, zone  Movable
        protection: (0, 0, 0, 0)
  :
```
値がそれぞれ４つありますが、これは以下のように読み取ります。
まず、Zone Normalの値に注目しましょう。3つが0で、4つめが79845(page)となっています。
これは4番目のZONEとなっているMovable ZoneでもOKなメモリ要求に対して、3番目のZoneであるNormal Zoneからメモリ確保しようとすると、79845pageのハンディキャップが与えられていることになります。Normal Zoneを直接要求されたときと比べて、79845pageも余分にメモリに空きがないと、このNormal Zoneからメモリを確保できないということなのです。
DME32 zoneが(0, 0, 2560, 12540)なのは、Movable Zoneの要求に対してDMA32のZoneからメモリを確保する場合は、12540 pageの余分な空きメモリが必要で、Normal Zoneの要求に対しては2560 pageの余分な空きメモリが必要ということです。
同じようにDMA Zoneのprotectionが(0, 3412, 5972, 15953)ですから、それぞれDMA32 Zoneのメモリを確保したいときにZone DMAを使うときは3412page(13Mbyte)のハンディキャップが与えられています。Normal ZoneやMovable Zoneを要求した時はprotectionの値だけでDMA Zoneのサイズ(16Mbyte)の値を超えているので、これらの要求をしたときはDMA Zoneからメモリを確保できないことになります。

このハンディキャップはkernelのソースの以下の場所で使われています。

```c
(mm/page_alloc.c)

/*
 * Return true if free pages are above 'mark'. This takes into account the order
 * of the allocation.
 */
static bool __zone_watermark_ok(struct zone *z, unsigned int order,
                        unsigned long mark, int classzone_idx, int alloc_flags,
                        long free_pages)
{
        /* free_pages may go negative - that's OK */
        long min = mark;
        int o;
        long free_cma = 0;

        free_pages -= (1 << order) - 1;
        if (alloc_flags & ALLOC_HIGH)
                min -= min / 2;
        if (alloc_flags & ALLOC_HARDER)
                min -= min / 4;
#ifdef CONFIG_CMA
        /* If allocation can't use CMA areas don't use free CMA pages */
        if (!(alloc_flags & ALLOC_CMA))
                free_cma = zone_page_state(z, NR_FREE_CMA_PAGES);
#endif

        if (free_pages - free_cma <= min + z->lowmem_reserve[classzone_idx])  <-- ここでハンディキャップとして計算されています。
                return false;

```

# lowmem_reserve_ratioとreserve量の計算

ここまで説明してようやく/proc/sys/vm/lowmem_reserve_ratioについて説明することができます。lowmem_reserve_ratioの実際の値は/proc/sys/vm/lowmem_reserve_ratioを参照することで見ることができます。

```
$ cat /proc/sys/vm/lowmem_reserve_ratio
256     256     32
```
値が256とかになっていて比率のようには見えませんが、実際はkernelはこの値の逆数を比率として使用します。
また、値が３つ定義されている理由は、このkernelはZONEが全部で4つ(ZONE_DMA, ZONE_DMA32, ZONE_NORMAL, ZONE_MOVABLE)使えるようにコンパイルされており、その下位３つのZONEをガードするために3つの定義をしています。

kernelはこの値と各ZONEのメモリ量をもとにして、protectionの値を決めています。

```c

static void setup_per_zone_lowmem_reserve(void)
{
        struct pglist_data *pgdat;
        enum zone_type j, idx;

        for_each_online_pgdat(pgdat) {
                for (j = 0; j < MAX_NR_ZONES; j++) {
                        struct zone *zone = pgdat->node_zones + j;
                        unsigned long managed_pages = zone->managed_pages;

                        zone->lowmem_reserve[j] = 0;

                        idx = j;
                        while (idx) {
                                struct zone *lower_zone;

                                idx--;

                                if (sysctl_lowmem_reserve_ratio[idx] < 1)
                                        sysctl_lowmem_reserve_ratio[idx] = 1;

                                lower_zone = pgdat->node_zones + idx;
                                lower_zone->lowmem_reserve[j] = managed_pages /
                                        sysctl_lowmem_reserve_ratio[idx];<------ ★
                                managed_pages += lower_zone->managed_pages;<----- ★
                        }
                }
        }

```

この計算は、自分のZoneより上位のZoneのメモリ量がどれくらいあるかの値に対して、lowmem_reserve_ratioの値で割って、ハンディキャップの値として計算しています。
例としてDMA32 Zoneが4Gbyte, Normal Zoneが6Gbyte, Movable Zoneが8Gbyteあるとしましょう。
Movable Zoneのメモリ確保要求に対して、Normal Zone(3つ目のZone)から確保するときは8Gbyte / 32 (lowmem_reserve_ratioの3つめの値） = 256Mbyteのハンディキャップが与えられます。
Movable Zoneのメモリ確保要求に対して、DMA32 Zone(2つめのZone)からメモリを確保するときは (6+8)Gbyte / 256 (lowmem_reserve_ratioの2つめの値) = 56Mbyteのハンディキャップ、それに対してNormal Zoneの要求の時には6Gbyte / 256 = 24Mbyteのハンディキャップとなります。
結構ややこしいですね。

# lowmem_reserve_ratioの値の変更
/proc/sys/vm/lowmem_reserve_ratioは値を書き込んで更新することができ、それぞれの値をintの最大値に指定すれば、ハンディキャップがほとんどなくなります。

```
# echo "2147483647 2147483647 2147483647" > /proc/sys/vm/lowmem_reserve_ratio
```

```
Node 0, zone      DMA
  :
        protection: (0, 0, 0, 0)
                    ^^^^^^^^^^^
                    すべて0になっている
```
逆にlowmem_reserve_ratioをすべてを1に設定すれば、ハンディキャップが最大になります。

lowmem_reserve_ratioは普通は変更する必要のない値です。が、最近はPCI Expressのように64bitのアクセスが可能なデバイスが主流となり、DMA ZoneやDMA32 Zoneが必要なケースは随分と減ってきています。また、x86を32bitのシステムとして使うことももうほとんどないでしょう。よって、カリカリにチューニングしたい場合は上記のように変更するのもありといえばありかもしれません。

（変更はあくまでも自己責任で実施しましょう！　上記変更がもとでトラブルになっても筆者は保証できませんし、有償サポートをしている会社でもまず保証しないでしょう。特にout of memoryを回避したいからとかいう理由で変更するのはやめましょう。lowmem_reserve_ratioを変更するのではなく、メモリの見積もりを見直したり、メモリリークをなくすようにするべきです。）

# 参考
kernelのDocumentation/sysctl/vm.txtにはlowmem_reserve_ratioの解説が書かれています。

以上。