意外と知られていない、Linuxのメモリホットプラグのインタフェースについて

# 0.はじめに
## 0.1 今年を振り返って
この記事は[Fujitsu Advent Calendar 2023](https://qiita.com/advent-calendar/2023/fujitsu) の24日目の記事です。
なお、本記事は個人の意見に基づくものであり、組織を代表するものではありません。

今年もAdvent Calendarの季節となりましたね。皆様お変わりないでしょうか？ 私の方はというと、全体的に多忙ではありましたが、夏には国際学会[IEEE NVMSA](https://nvmsa2023.github.io/)での発表、秋から年末にかけて海外出張や[Open Source Summit Japanでの講演](https://youtu.be/OsND7T1SyGQ)など、今年後半は怒涛のように仕事がくる状態になってました。おまけに __「Linus Torvaldsと会談せよ」__ という[ミッションまで降ってきて](https://twitter.com/YasunoriGoto1/status/1719287301703082262)しまい、[正直どうなることかと思いました]( https://twitter.com/YasunoriGoto1/status/1737084927022432563 )。人生、何が起こるかわからないものです。学生時代、英語は超苦手だったのに…。

## 0.2 今年の記事について
さて、例年このAdvent Calendarでは、不揮発メモリやCXLについて私はまとめてきたのですが、今年は前述のとおり、IEEE NVMSA 2023やOpen Source Summit Japan 2023などの国際カンファレンスなど色々な場でCXLについて発表する機会があり、新たにQiitaで記載できることはあまり多くはありません。
（とりあえず、OSSJの発表資料は[こちらに置いておきます](https://speakerdeck.com/yasunorigoto1/compute-express-link-cxl-the-next-generation-interconnect-overview-and-the-status-of-linux-open-source-summit-japan-2023-ver-dot)。）

このため、CXLそのものの動向や解説などは一旦おいておいて、今年はCXLのメモリプール[^1]機能でも使うであろう、Linuxのメモリホットプラグのインタフェースについて改めて解説しておこうと思います。

このLinuxのメモリホットプラグという機能は、実はもう10年以上前から存在する機能です。私をはじめとして様々な人が2004年頃[^history]から開発していて2007年頃にはkernelに取り込まれています。そのときに体験した苦労話について[「七転び八起きのLinuxカーネルコミュニティ開発体験記」](https://xtech.nikkei.com/it/article/COLUMN/20090501/329459/)として日経の記事[^nikkei]にまとめたこともあります。このため、私にとっては非常に思い入れのある機能です。

[^nikkei]: 当時のkernel内のデータ構造の問題や、それをどう克服したかの技術的な内容については、こちらの記事のほうがくわしいです。
[^history]: 私が開発に着手したのは2003年頃なので、それを含めるともう20年経過したわけですね…。

更に、現在でもコミュニティで機能強化されていて、ドキュメントも以下のようにきちんとメンテナンスされています。最初にこのファイルを作ったのは恐らく私なのですが、いろいろあったのか、私の名前は空行にしか残っていないという状態です。

[^1]:メモリプールとはあるサーバーのメモリを抜いて、他のサーバにメモリを追加するような仕組みです。つけ外しの際にOSをshutdownさせておくのが確実ですが、片方のサーバーからmemory hotremoveの機能でメモリを削除して、そのメモリを他のサーバにmemory hotaddすることで、shutdownに伴うダウンタイムなしに資源の移動が可能となります。


```
ac3332c44767b Documentation/admin-guide/mm/memory-hotplug.rst (David Hildenbrand     2021-09-07 19:54:49 -0700   1) ==================
ac3332c44767b Documentation/admin-guide/mm/memory-hotplug.rst (David Hildenbrand     2021-09-07 19:54:49 -0700   2) Memory Hot(Un)Plug
ac3332c44767b Documentation/admin-guide/mm/memory-hotplug.rst (David Hildenbrand     2021-09-07 19:54:49 -0700   3) ==================
6867c9310d5da Documentation/memory-hotplug.txt                (Yasunori Goto         2007-08-10 13:00:59 -0700   4) 
ac3332c44767b Documentation/admin-guide/mm/memory-hotplug.rst (David Hildenbrand     2021-09-07 19:54:49 -0700   5) This document describes generic Linux support for memory hot(un)plug with
ac3332c44767b Documentation/admin-guide/mm/memory-hotplug.rst (David Hildenbrand     2021-09-07 19:54:49 -0700   6) a focus on System RAM, including ZONE_MOVABLE support.
6867c9310d5da Documentation/memory-hotplug.txt                (Yasunori Goto         2007-08-10 13:00:59 -0700   7) 
6bf53999a3a26 Documentation/admin-guide/mm/memory-hotplug.rst (Mike Rapoport         2018-10-05 01:11:00 +0300   8) .. contents:: :local:
6bf53999a3a26 Documentation/admin-guide/mm/memory-hotplug.rst (Mike Rapoport         2018-10-05 01:11:00 +0300   9) 
ac3332c44767b Documentation/admin-guide/mm/memory-hotplug.rst (David Hildenbrand     2021-09-07 19:54:49 -0700  10) Introduction
ac3332c44767b Documentation/admin-guide/mm/memory-hotplug.rst (David Hildenbrand     2021-09-07 19:54:49 -0700  11) ============
c18c1cce0c1a0 Documentation/memory-hotplug.txt                (Mauro Carvalho Chehab 2017-05-14 22:52:22 -0300  12) 
ac3332c44767b Documentation/admin-guide/mm/memory-hotplug.rst (David Hildenbrand     2021-09-07 19:54:49 -0700  13) Memory hot(un)plug allows f
```


しかしながら、そのわりにはこの機能が __一般の人にはこの機能があまり知られていない__ ということに最近気が付きました。memory hotplugを使えば簡単に実験できることを、その機能の存在自体を知らなかったため、研究職の方でもこれを使わずに苦労して実験いたことを耳にしたぐらいです。さすがにこれは良くないと感じたというのが今年の記事を書く気になった動機です。

それでははじめましょう。

# 1. 想定とする読者やハードウェアについて

## 1.1 対象読者
今から述べるインタフェースは、どちらかというとある程度kernelについて知識のある人向けのインタフェースです。例えば以下のような方です。

- この機能を使うシステム、ユーザプロセスやコマンドなどの開発者
- この機能でシミュレーションなど実験を行うことができる研究・開発者

:::note warn
これらのインタフェースはエンドユーザ向けのものとはなっていません。メモリホットプラグに対応したハードウェアのエンドユーザの方は、そのハードウェアに添付されたエンドユーザ向けの案内やコマンドなどがあるかもしれません。ご確認の上、そちらに従ってください。
:::

## 1.2 対応ハードウェア

意外と知られていませんが、これらのメモリホットプラグの機能の一部は、__実は一般的なサーバやデスクトップマシン、VMゲストなどでも試すことは可能__ です。
テストなどで、一時的にメモリの容量を減らしたいとか、システム内のメモリ構成を変更したい時などにこのメモリホットプラグの機能を使うことができます。本記事では、一般的なPCで使い方を試す方法を中心にご紹介します。

もちろん実際にmemoryがhotplugできる対応ハードがあった方や、より実践的な操作ができますが、上記のような身近な環境でも使えることは覚えておいてください。

また、CXLでの操作についても簡単にご紹介します。ただ実際には、この機能の完成度が上がってくるのはこれからになると思われるので、新しい情報が今後出てくるかもしれません。（私もこの辺りはまだ十分に試せていません）。

本記事の実行例における動作環境はディストリビューションはubuntu 20.04LTSですが、RedHat系列のOSでも大きくは変わらないはずです。また、ハードは20GiBのメモリを搭載した筆者のNote PCで実施しています。

# 2. メモリホットプラグの大まかなシーケンス

大きく分けて以下の2段階になっています。

### Memory Hotaddの場合
1. ハード的にメモリを認識(a)
 この段階では、追加されたメモリに物理アドレスが割り当てられた状態です。しかしOSのメモリ管理機構がこの領域を管理するためのデータ構造を初期化していない状態です。したがって、OSやユーザープロセスからはこのメモリはまだ利用することができません。
1. OSへメモリを組み込み、kernel/driverやプロセスに利用可能にさせる(b)
OSのメモリ管理機構が上記の初期化を終えた状態です。この段階を踏まえることで、OS自身やユーザープロセスから、追加されたメモリを利用することが可能となります。

### Memory Hotremoveの場合
1. OSの管理下からメモリを解放し、プロセスから利用できないようにする(b)
上記とは逆に、OSの制御からメモリを解放する段階です。プロセスが利用中の場合、可能であればmemory migration(後述)の力で他のメモリ領域にこっそりデータを移動します。その後、メモリ管理機構からこの領域を解放します。CPUからはまだこのメモリの物理アドレスは認識できていますが、OSからは切り離されたため、このメモリを利用することはできなくなります。

1. ハード的にメモリを切り離し(a)
ハード・ファームウェアにこの領域の返却を通知します。これにより、CPUから当該領域の物理アドレスにはアクセスできなくなります。

![memory_hotplug_flow.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/101351/433a5d00-1b8b-18c1-403d-0a13659c472a.png)


上記を分類しなおすと、メモリホットプラグの操作は以下の2段階に分かれていることになります。

操作a. ハード・ファーム的なつけ外しの段階
操作b. OSへの組み込み・切り離し

先にbについて解説しますが、bはOSの仕組みとして動作し、ハード・ファームは介入しません。この操作が前述した、 __特殊なハード・ファームは必要ではなく一般的なPCやサーバー、VMゲストなど一般的な環境でも動作する__ 箇所になります。

一方、aについてはハード・ファームウェアの力でメモリをつけ外しする機能なので、基本的にはプラットフォームがメモリーホットプラグをサポートしていないと動作できません。また、そのしくみに対応するためのドライバ(PCIeのhotplugのドライバや、ACPIのhotplugのドライバなど)が必要です。

ただし本記事では、HotAddについてちょっとした設定や操作によって、それに近い体験ができる方法を紹介します。

:::note info
筆者は試したことはありませんが、QEMUの操作でもmemory hotplugの操作が可能なようです。本記事内では使いませんが、その方法が記載されたドキュメントについては後述します。
:::


なお、このa.ハード・ファーム的なつけ外しについては、CXLでは以下の2通りの仕組みが仕様上定義されています。（本記事ではこの仕様についてはあまり触れません）

1. PCIeのhotplugの仕様に基づいた、デバイス単位のhotplug
1. CXL 3.0以降で導入されたDynamic Capacity Deviceによる、メモリデバイスの一部をあり当てる形のhotplug


# 3. メモリホットプラグのインタフェース

では、インタフェースを少しずつ触って体験してみましょう。

## 3.1 OSへの組み込み・切り離しの基本的な操作

まず、身近に確認できる「b.OSへの組み込み・切り離し」について、そのインタフェースを見ていきましょう。以下のようなsysfsのディレクトリの配下にmemory hotplug関係のファイルやディレクトリが配置されてます。これがインタフェースとなります。

:::note info
これ以降の操作はroot権限で行っていることに注意してください
:::

```
# pwd
/sys/devices/system/memory
# ls
 ls
auto_online_blocks  memory114  memory133  memory152  memory171  memory47  memory66  memory85
block_size_bytes    memory115  memory134  memory153  memory172  memory48  memory67  memory86
hard_offline_page   memory116  memory135  memory154  memory18   memory49  memory68  memory87
memory0             memory117  memory136  memory155  memory2    memory5   memory69  memory88
memory1             memory118  memory137  memory156  memory3    memory50  memory7   memory89

(中略)

memory109           memory128  memory147  memory166  memory41   memory60  memory8   memory99
memory11            memory129  memory148  memory167  memory42   memory61  memory80  power
memory110           memory13   memory149  memory168  memory43   memory62  memory81  probe
memory111           memory130  memory15   memory169  memory44   memory63  memory82  soft_offline_page
memory112           memory131  memory150  memory17   memory45   memory64  memory83  uevent
memory113           memory132  memory151  memory170  memory46   memory65  memory84
```

まずは、上記のうち以下のファイルの内容を出力してみます。

```
# cat block_size_bytes
8000000
```

これは、メモリホットプラグの(b)の操作をどれくらいの単位で行うかの表示です。以後、この単位を便宜上、メモリブロックと記述します。この環境では0x8000000と表示されているわけですから１つのメモリブロックのサイズが128MiByteとなっていることがわかります。このサイズは基本的にアーキテクチャごとに異なり、 __基本的には__ 変更不可能な固定値です。

筆者がmemory hotplugの開発に関わっていた頃、言い換えるとLinuxのmemory hotplug機能が初めて搭載された頃はx86-64はこの128MiB固定、ia64では1GiB、powerpcでは(筆者の記憶では)16MiB固定のサイズとなっていました。この値は固定値のほうが、以下のような理由でいろいろな面で都合がよかったためです。
- kernel内部の実装がシンプルになり、高速な処理が可能となるため都合がよいこと
- powerpcではプラットフォームの力で、16MiB単位という小さな単位でmemory hotplugが出来ること
- ia64ではもっと大きな単位でメモリがhotplugされ、またメモリの最大容量が当時はx86-64よりもかなり大きかったこと

しかし、現在では当時とは状況がかわってきており、x86-64サーバのメモリ搭載量もずいぶん大きくなってきました。このため、今ではkernelが起動時にメモリブロックサイズとしてどれくらいのサイズがちょうど良いかをある程度判定して決めるという形になっています。搭載されているメモリ量など様々な状況によって適切なサイズが変わってくるためです。
例えば、筆者の別の環境ではこのサイズが2GiBのサイズになっています。

```
# cat block_size_bytes 
80000000
```
x86_64では以下の判定箇所で決めています。興味が有る人は調べてみてください。
```c
arch/x86/mm/init_64.c

static unsigned long probe_memory_block_size(void)
{
        unsigned long boot_mem_end = max_pfn << PAGE_SHIFT;
        unsigned long bz;

        /* If memory block size has been set, then use it */
        bz = set_memory_block_size;
        if (bz)
                goto done;

        /* Use regular block if RAM is smaller than MEM_SIZE_FOR_LARGE_BLOCK */
        if (boot_mem_end < MEM_SIZE_FOR_LARGE_BLOCK) {
                bz = MIN_MEMORY_BLOCK_SIZE;
                goto done;
        }
       (以下略)

```



さて、`/sys/devices/system/memory`のディレクトリには、memory`###`の数字を持つディレクトリがありますが、この`###`の数字の部分は、物理アドレスに対して、この`block_size_bytes`の値を割った値となっています。つまり、memory0のディレクトリは物理アドレス0x0の位置のメモリから128MiByteのサイズの領域を、memory8のディレクトリは 8 x 128MiB = 1024MiBから128MiBのサイズの物理アドレスの領域を管理するディレクトリとなっているわけです。

それでは、例としてmemory160のディレクトリの中身を見てみましょう。160*128MiB=20GiBなので、物理アドレスとして20GiBの物理アドレスから128MiByteのサイズのメモリブロック領域を管理するためのディレクトリです[^phys_addr]。

[^phys_addr]:搭載メモリが20GiBのはずなのに20GiB以上の物理アドレスがあることに不思議に思う人がいるかもしれません。しかし、x86-64のアーキでは物理アドレスの途中に、メモリではなくデバイスのために割り当てられている領域があったりします。そういう領域をスキップしてメモリのアドレスが割り当てられるため、物理アドレスとしては20GiBより少しはみ出たところまでが割り当てられます。実際このマシンでは21.5GiBぐらいまでが物理アドレスとして割り当てられていました。このような物理アドレスのマップ情報はdmesg中に出力されています。

```
# pwd
/sys/devices/system/memory/memory160
# ls -al
合計 0
drwxr-xr-x   3 root root    0 12月 23 11:14 .
drwxr-xr-x 163 root root    0 12月 23 11:14 ..
lrwxrwxrwx   1 root root    0 12月 23 11:27 node0 -> ../../node/node0
-rw-r--r--   1 root root 4096 12月 23 11:27 online
-r--r--r--   1 root root 4096 12月 23 11:27 phys_device
-r--r--r--   1 root root 4096 12月 23 11:27 phys_index
drwxr-xr-x   2 root root    0 12月 23 11:27 power
-r--r--r--   1 root root 4096 12月 23 11:27 removable
-rw-r--r--   1 root root 4096 12月 23 11:27 state
lrwxrwxrwx   1 root root    0 12月 23 11:14 subsystem -> ../../../../bus/memory
-rw-r--r--   1 root root 4096 12月 23 11:14 uevent
-r--r--r--   1 root root 4096 12月 23 11:27 valid_zones
```

メモリホットプラグにおいて、もっとも重要なファイルがこの中のstateというファイルです。まずはこの値を表示してみましょう。
```
# cat state
online
```

これは、この160*128MiB=20GiBの物理アドレスから始まる128MiBのメモリ領域がonline、すなわちOSによって管理されていて、プロセスにメモリが利用できる状態になっていることを示します。それでは、(b)の操作、すなわちこの領域に対してOSの切り離し操作をしてみましょう。

```
# echo offline > state
# cat state
offline
```

このように、stateのファイルに対してofflineの文字列を書き込むことにより切り離しの操作を行うことができます。ユーザープロセスがこの物理アドレスのメモリを使っていた場合は、この時にmemory migrationの機能によってほかのアドレスに移動されています。

それでは元に戻してみましょう。
```
# echo online > state
# cat state
online
```
これで、再びOSに組み込まれて、この領域が利用できる状態になりました。
これが、(b)OSへの組み込み、切り離しの最も基本的な操作になります。

## 3.2 切り離しを確実にするために

### 3.2.1 メモリが抜けなくなる要因

実は、offlineの操作は失敗することがあります。試しにmemory0のメモリブロックをofflineにしてみましょう。
```
# pwd
/sys/devices/system/memory/memory0
# echo online > state
-bash: echo: 書き込みエラー: 無効な引数です
# cat state
online
```
エラーになってしまいました。状態もonlineのままですね。これはmemoryが以下のような条件などにより、memory migrationが動作できず、offlineできない状態になっていることが原因です。

##### カーネルやドライバがその領域を利用している
  一般的にユーザープロセスが使っているメモリについては、kernelは図に示すようなmemory migrationという機能を使って、hotplugで抜こうとしている領域からほかの場所に移動させます。これは仮想アドレスはそのままにしつつ、データが保持されているメモリの物理アドレスを変えるという機能です。
  
![memory_migration.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/101351/63e4af0c-31ae-242b-2f5c-afc58fc448b3.png)

ですが、カーネルやドライバのメモリ空間はこの機能が使えません。カーネルやドライバが使うメモリの仮想アドレスは物理アドレス＋固定値(PAGE_OFFSET)という固定値で決められていて、物理アドレスをこっそりかえるということができない場合があります。また、そうでなくても割り込み禁止で動作している場合など、migraitionの時間が待てないというケースもあります。このため、これらが使っているメモリブロックをofflineしようとするとエラーになってしまうのです

##### ユーザプロセスのメモリが動かされないように、ドライバなどが固定している

速度を優先して、kernelをスキップして直接プロセスが使っているメモリにDMA/RDMAなどの手段でデータ転送中の場合、memory migrationを行うことはできません。物理アドレスを移動しても、転送中のハードウェアを止めることができないからです。
また、VMゲストが速度を優先してメモリを固定する場合もあるようです。このような場合もmigrationを行うことはできません。

#### その他
Linux Kernelのソースコードの中の ドキュメント`Documentation/admin-guide/mm/memory-hotplug.rst`には、他にもメモリが抜けない状態になるような原因について、この記事よりくわしく書いてあります。興味があるかたは参照してみてください。（私もこの記事を書くにあたって初めて知ったものもあります）。

### 3.2.2 ZONE_MOVABLE

このような抜けないケースをできるだけ避けるため、LinuxではZONE_MOVABLEという領域を定義することができます。ZONE_MOVABLEはkernelやドライバが使うことができない領域です。
また、ユーザプロセスのメモリを動かないようにを固定する場合、利用者が適切なパラメタ(FOLL_LONGTERM)を指定していれば、固定する前にZONE_MOVABLEの外にmigrateしてから固定するように動作します。

この領域はデフォルトでは存在しませんが、kernelのブートオプションを指定することで作成できます。kernelのソースコードに付属しているドキュメントには、その指定方法が書かれています。
```
$ pwd
/Documentation/admin-guide
$ less kernel-parameters.txt
  :
  :
        kernelcore=     [KNL,X86,IA-64,PPC]
                        Format: nn[KMGTPE] | nn% | "mirror"
                        This parameter specifies the amount of memory usable by
                        the kernel for non-movable allocations.  The requested
                        （中略）
  :
        movablecore=    [KNL,X86,IA-64,PPC]
                        Format: nn[KMGTPE] | nn%
                        This parameter is the complement to kernelcore=, it
                        specifies the amount of memory used for migratable
                        allocations.  If both kernelcore and movablecore is
                        （後略）
  :                        
```
movablecoreはZONE_MOVABLEの領域の量を指定する方法です。kernelcoreはZONE_MOVABLE __以外__ の領域の量を指定する方法です。通常はどちらかを使えば十分です。具体的な容量を指定するか、あるいは全体の容量に対する比率を％で指定します。

:::note warn
この指定をするということは、kernelやドライバが使えるメモリの量が通常より少なくなるということです。どれくらいの容量を指定すればよいかについては条件によって異なります。本番環境などで使いたいという場合には、この値の量の指定には本番移行前までに検証を行うなど、十分注意して使うようにしてください。
:::

:::note info
なお、ここの記述でkernelcoreにはmirrorという指定がありますが、これは特殊なプラットフォームのための指定であり、通常はまず使うことがないオプションです。この文の注釈でどういうオプションなのかを補足しておきます[^mirror]。
:::

[^mirror]: 一部のエンタープライズ向けサーバにはDDR DRAMのメモリの一部をmirroringして冗長化することができるものがあるのですが、その領域をkernelが使えるようにし、それ以外の領域をZONE_MOVABLEにしてhotplug可能にするための指定です。（その場合、kernelはファームウェアからmirrorされた範囲をファームウェア、より正確にはEFI経由で教えてもらうことになります）。


では、このブートオプションの効果を確認してみましょう。
まず、これを指定する前のメモリの状態を確認して見ましょう。例としてmemory block 100の値を確認します。
```
(再起動前)
# pwd
/sys/devices/system/memory/memory100
# cat valid_zones
Normal
```

ではこのブートオプションを指定してシステムを起動してみましょう。50%のメモリをkernelやドライバが利用できる領域とし、 __それ以外の領域__ を ZONE_MOVABLE の領域として設定します。

```
(再起動後)
# cat /proc/cmdline
BOOT_IMAGE=/vmlinuz-5.15.0-91-generic root=(中略) kernelcore=50%
 
# pwd
/sys/devices/system/memory/memory100
# cat valid_zones
Movable
```
memory100のブロックがZONE_MOVABLEに代わったことが分かります。このような設定をすることで、memoryを抜きやすくすることが可能です。

また、メモリブロックをonlineするときに、ZONE_MOVABLEにするか、それ以外の通常の領域(ZONE_NORMALなど)にするかも指定可能です。この環境において、Normalだったメモリブロックで、たまたまofflineにできるメモリブロックがありました。これをZONE_MOVABLEに変えてみましょう。以下のように操作します。

```
# pwd
/sys/devices/system/memory/memory85
# cat state
online
# cat valid_zones 
Normal  <------------------------------- この時点ではZONE_NORMALの領域です
# echo offline > state
# cat state
offline <------------------------------- 一旦offlineにすることが出来ました。
# echo online_movable > state <--------- onlineする領域をZONE_MOVABLEにすることを指定します
# cat state
online
# cat valid_zones 
Movable  <------------------------------ MOVABLE_ZONEの領域になりました
```
逆にZONE_NORMAL、すなわちkernelやドライバが使えるメモリ領域にしたい場合は`online_kernel`を指定してstateに書き込むと良いです。


# 4. ハード・ファーム的な取付、つけ外しについて
## 4.1 手動での操作

基本的にはハード・ファーム的なつけ外しはメモリホットプラグに対応したプラットフォームでないと実行できないと述べましたが、以下の条件が揃っている場合、取り付けすなわちhotaddについてエミュレーション的な動作をさせることが出来ます。

- アーキテクチャがこのエミュレーションに対応していること(kernel-6.7-rc5の時点ではx86, powerpc, sh, LoogArchが対応しているようです)
- CONFIG_ARCH_MEMORY_PROBEがkernelコンパイル時にonになっていること。

では、このエミュレーションをちょっと実験してみましょう。20GiBあるマシンに対して、`mem=` のブートオプションによって、システム起動時にkernelにはメモリの半分の10GiBのみを認識させるオプションを追加し、残りはkernelが認識していない状態としています。
なお、ここで`memhp_default_state=offline`という指定も追加していますが、これについては後で説明します。

```
# cat /proc/cmdline 
BOOT_IMAGE=/boot/vmlinuz-6.2.0-39-generic root=(中略) kernelcore=50% mem=10G memhp_default_state=offline

# pwd
/sys/devices/system/memory

# ls
auto_online_blocks  memory18  memory41  memory53  memory65  memory77
block_size_bytes    memory2   memory42  memory54  memory66  memory78
hard_offline_page   memory3   memory43  memory55  memory67  memory79
memory0             memory32  memory44  memory56  memory68  memory8
memory1             memory33  memory45  memory57  memory69  memory9
memory10            memory34  memory46  memory58  memory7   power
memory11            memory35  memory47  memory59  memory70  probe
memory12            memory36  memory48  memory6   memory71  soft_offline_page
memory13            memory37  memory49  memory60  memory72  uevent
memory14            memory38  memory5   memory61  memory73
memory15            memory39  memory50  memory62  memory74
memory16            memory4   memory51  memory63  memory75
memory17            memory40  memory52  memory64  memory76
```

ここで80番目のメモリを認識させるには以下のように、そのメモリのブロック番号に対応する物理アドレスを指定します。block_size_bytesが128MiBの環境なので、128MiB*80=0x280000000の物理アドレスを指定します。

```
# echo 0x280000000 > probe <------ 80番目のブロックを認識させます。

# ls
auto_online_blocks  memory18  memory41  memory53  memory65  memory77
block_size_bytes    memory2   memory42  memory54  memory66  memory78
hard_offline_page   memory3   memory43  memory55  memory67  memory79
memory0             memory32  memory44  memory56  memory68  memory8
memory1             memory33  memory45  memory57  memory69  memory80 <--- 指定した領域のメモリが認識され、ディレクトリが追加された！
memory10            memory34  memory46  memory58  memory7   memory9
memory11            memory35  memory47  memory59  memory70  power
memory12            memory36  memory48  memory6   memory71  probe
memory13            memory37  memory49  memory60  memory72  soft_offline_page
memory14            memory38  memory5   memory61  memory73  uevent
memory15            memory39  memory50  memory62  memory74
memory16            memory4   memory51  memory63  memory75
memory17            memory40  memory52  memory64  memory76

# cd memory80
# cat state
offline  <--- まだoffline状態のままですが、これは後で説明します
```

上記のようにmemory80のメモリブロックが認識され、ディレクトリが追加されました。このようにハード・ファームがメモリをhotaddで追加したときには、このメモリブロックのディレクトリが新たに追加されるという形になります。上記のprobeによる操作は、これをエミュレートするような動作というわけですね。


## 4.2 CXLでの操作

CXLではcxlコマンドによって、デバイスを認識させることができます。
なお、CXLコマンドのrepositoryは不揮発メモリの管理コマンドであるndctlと同じ場所 ( https://github.com/pmem/ndctl ) にあります。
また、コマンドの解説も( https://docs.pmem.io/ndctl-user-guide/cxl-man-pages/cxl-enable-memdev-1 )[^2]とpmem.ioに置いてあり、不揮発メモリのときの開発者がそのままCXLの開発へ移行した名残がのこっています。

[^2]: 2023/12/16日現在は、ちょっと記載が変になっていますね…。

解説によるとCXLデバイスのデバイスの認識、取り外しは以下のコマンドで実行できるようです。
```
cxl enable-memdev <memory device>
```
```
cxl disable-memdev <memory device>
```
なお、CXLではデバイスを取り付けただけではだめで、仕様上regionやinterleaveなどの設定などを行う必要があります。interleaveの設定方法などについては、私もまだ勉強中なのですが、CXLではデバイスの認識のあとでonline操作の前に必要な設定や、offline後で取り外し前にも操作が必要なことがわかれば、とりあえずは充分です。
本記事ではregionの設定( https://manpages.ubuntu.com/manpages/lunar/man1/cxl-enable-region.1.html )についてだけ示しておきます。

```
cxl enable-region <region> [<options>]
```
```
cxl disable-region <region> [<options>]
```

また、筆者はまだ試していないのですが、QEMUによるCXLエミュレーションの開発も盛んに行なわれています。以下の手順でCXLのエミュレーション環境を試すことができるようなので、興味の有る人は挑戦してみてください。

https://stevescargall.com/blog/2022/01/20/how-to-emulate-cxl-devices-using-kvm-and-qemu/

## 4.3 QEMUでの操作

QEMUでもmemory hotplugについてドキュメントが書かれており、この配下のゲストへのmemory hotplugの操作ができるようです。こちらも参考になるでしょう。
https://github.com/qemu/qemu/blob/master/docs/memory-hotplug.txt


# 5. aとbの動作の連動について
## 5.1 aとbとの連動とは
現在のmemory hotplugのデフォルトの動作では、aのハードウェアの認識後、自動的にb.すなわちkernelがonlineするようになっています。しかし、かつては自動ではなく一旦kernelからユーザランドのエージェントに通知して、エージェント側からonlineするような動作になっていました。そうすると、そもそもなぜこのような2段階に分かれていたのでしょうか？

正直言うと、なぜこうなったか？という点については私もだいぶ記憶から薄れているのですが、理由の一つとして以下のようなものがあったと記憶しています。

世の中には、メモリだけでなく、次の図のように複数のCPUとDIMMが複数刺さったマザーボードのような基盤がまるごとhotplugできるケースがあります。

![SB_hotplug.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/101351/19d3a7bb-b0d7-65c6-6c3b-3e8853868119.png)


一般的に、hotplugされたデバイスに対してudevと呼ばれる仕組みでユーザランドのエージェントなどが起動し、hotplugされたデバイスを識別して特定のデバイス名をつけたりすることに使われています。

:::note info
udevについては[debianの解説](https://debian-handbook.info/browse/ja-JP/stable/sect.hotplug.html
)などが参考になるので、そちらをご覧ください。
:::

このように、基盤ごとhotplugされる場合、例えばhotaddの動作は以下のようになります。

1. aの動作として、ドライバ経由で基板上のCPUとメモリがすべて認識される。
1. ホットプラグされる単位として、その基盤まるごと１つがhotplug eventとしてudev経由で通知される。（この通知はCPUやメモリごとではなく、基盤丸ごと１つの通知となる)
1. ユーザー空間のエージェントが通知に応じて動作する。エージェントは基板上に載っているCPUsやメモリブロックを判別する。
1. 判別した基板上のCPU群とメモリブロックを、エージェントがすべてonlineにする。

![SB_hotplug2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/101351/bf99f44b-3022-50c6-d13c-79e9e07182e8.png)

このように、hotplugされるデバイス構成が複雑な場合、ユーザー空間で動作するエージェントからデバイス自身やその内部の構成を認識したうえで、システムに組み込む方が都合が良いというケースがあるわけです。
このため、memory hotplugが初めて開発されたときは、初期状態のメモリブロックはofflineとなっており、エージェントやベンダーが提供するコマンドからオンライン化するのがデフォルトとなっていました。 

しかし、私がmemory hotplugの開発から離れた後、他にもmemory hotplugのユーザが増えるにつけ、a.のあとにb.がudev経由で行なわれなければならないというのは、「難しい」と言われるようになったようです。このため、現在ではa.のメモリの認識のあと直接b.のonline操作が連続で行なわれるのがデフォルトの動作となっています。

## 5.2 自動でonlineしたときのデフォルトのZONEの指定

このようにデフォルトでは、上記のようにメモリデバイスの認識後すぐにonlineするようになったので、sysfsからstateへの入力が不要となりましたが、逆にZONE_MOVABLEにするかそれ以外の領域にするかどうかを決めるチャンスがなくなってしまいました。このため、`memhp_default_state`というkernelのbootオプションで、onlineしたときのデフォルトのZONEを決めることが出来るようになりました。

`memhp_default_state`には以下の値を指定することができます。online_movableを指定すると、a.の操作でメモリが接続されると、そのままZONE_MOVABLEの領域としてb.のonline化の動作がkernel側で行われます。

```bash
 memhp_default_state=online/offline/online_kernel/online_movable
```

4.1節ではこの値をofflineに指定しましたが、こうするとa.の動作の後は一旦offlineで待機する動作となります。なので、probeした直後の80番のメモリブロックは連動してonlineの動作をせず、offlineのままだったということです。あとからエージェント経由で動作させたいという場合は、この設定が役に立つでしょう。今後CXLによるリソースプールが実現されるときも、もしかすると役に立つかもしれません。

また、この値は、以下のsysfsのファイルでも変えることが出来ます。どう変わるか実験してみましょう。

```
# pwd
/sys/devices/system/memory
# cat auto_online_blocks 
offline
# echo online_movable > auto_online_blocks 
# cat auto_online_blocks 
online_movable <------------------ 設定値が変わりました
# echo 0x288000000 > probe <------ 81番目のブロックを認識させます。
# cat memory81/state  
online   <------------------------ 81番めのブロックが一気にonlineにされたことがわかります。
root@racailum:/sys/devices/system/memory# cat memory81/valid_zones 
Movable  <------------------------ ZONE_MOVABLEとしてonlineされたことがわかります。
```
このように、sysfsの設定でもauto onlineの動作を変えることができます。

# 6. まとめ

メモリホットプラグの操作方法について最も重要な点を述べてきました。
ほかにも細かい話は色々あるのですが、ここまでの説明でもかなり長くなってしまったので、それについて興味のある方はkernelのソースコード内にある
- [memory hotplugのドキュメント Documentation/admin-guide/mm/memory-hotplug.rst]( https://www.kernel.org/doc/Documentation/admin-guide/mm/memory-hotplug.rst ) 
- [bootパラメータの解説 Documentation/admin-guide/kernel-parameters.txt]( https://www.kernel.org/doc/Documentation/admin-guide/kernel-parameters.txt )

なども参照してください。

それでは良いお年を！