Diary of a Perpetual Student

Perpetual Student: A person who remains at university far beyond the normal period

arm64の場合/proc/cpuinfoでCPUのモデル名が取得できない件とその対策

arm64では/proc/cpuinfoからCPUのモデル名を取得できない

Linuxにはシステムの様子を知ることができる仮想ファイルがいくつかあります。そのうちの一つが/proc/cpuinfoで、その名の通りCPUの情報が以下のように閲覧できます。

$ cat /proc/cpuinfo
processor   : 0
vendor_id   : GenuineIntel
cpu family  : 6
model       : 63
model name  : Intel(R) Xeon(R) CPU E5-2650 v3 @ 2.30GHz
stepping    : 2
microcode   : 0x1
cpu MHz     : 2294.686
cache size  : 4096 KB
physical id : 0
siblings    : 1
core id     : 0
cpu cores   : 1
apicid      : 0
initial apicid  : 0
fpu     : yes
fpu_exception   : yes
cpuid level : 13
wp      : yes
flags       : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ss syscall nx pdpe1gb rdtscp lm constant_tsc arch_perfmon rep_good nopl cpuid tsc_known_freq pni pclmulqdq ssse3 fma cx16 pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand hypervisor lahf_lm abm invpcid_single pti fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid xsaveopt
bugs        : cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass l1tf mds swapgs itlb_multihit mmio_stale_data
bogomips    : 4589.37
clflush size    : 64
cache_alignment : 64
address sizes   : 46 bits physical, 48 bits virtual
power management:

(略、以下論理コアごと同じような出力が並ぶ)

「Intel(R) Xeon(R) CPU E5-2650 v3 @ 2.30GHz」のようなCPUの名前を見たい場合にはmodel nameの行を見れば良いわけです。しかし、このmodel nameは常にあるとは限りません。先ほどの実行例はx86_64のものでしたが、arm64での実行例を見ていきましょう。

$ cat /proc/cpuinfo
processor   : 0
BogoMIPS    : 108.00
Features    : fp asimd evtstrm crc32 cpuid
CPU implementer : 0x41
CPU architecture: 8
CPU variant : 0x0
CPU part    : 0xd08
CPU revision    : 3

(略、以下論理コアごと同じような出力が並ぶ)

先ほどと比べて情報量がかなり少なくなっており、model nameの行もないためCPUのモデル名が取得できなくなっています。

なぜこういった仕様になっているかは、Linuxカーネルのコード(下記参照)やパッチのディスカッションを見ることでわかります。

if (compat)
    seq_printf(m, "model name\t: ARMv8 Processor rev %d (%s)\n", MIDR_REVISION(midr), COMPAT_ELF_PLATFORM);

https://github.com/torvalds/linux/arch/arm64/kernel/cpuinfo.c#L193-L195

まとめると概ね以下の通りです:

  • 32bitではarmでもmodel nameが取得できたが64bitではできなくなっている(そういうカーネルのコードになっている)
  • arm64ではハードウェアのAPIからmodel nameを取得する術がないため上記実装になっている

lscpuではなぜarm64でもmodel nameが取得できるのか

lscpuというコマンドがあります。Ubuntuにはデフォルトで入っていそうですが、入っていないディストリビューションの場合にはutil-linuxをaptやらyumやらpacmanやらでインストールすると使えるはずです。

こちらのコマンドでもcat /proc/cpuinfo同様にCPUの情報を閲覧することができます。ここでは本題のModel nameにgrepで絞った結果をお見せします。

$ lscpu | grep "Model name"
Model name:                      Intel(R) Xeon(R) CPU E5-2650 v3 @ 2.30GHz

上記はx86_64での実行結果ですが、なんとlscpuではarm64の場合でもモデル名を取得することができます。

$ lscpu | grep "Model name"
Model name:                         Cortex-A72

これもソースコードから紐解いていきましょう。

lscpu_context_init_paths(cxt);

lscpu_read_cpulists(cxt);
lscpu_read_cpuinfo(cxt);
cxt->arch = lscpu_read_architecture(cxt);

lscpu_read_archext(cxt);
lscpu_read_vulnerabilities(cxt);
lscpu_read_numas(cxt);
lscpu_read_topology(cxt);

lscpu_decode_arm(cxt);

https://github.com/util-linux/util-linux/sys-utils/lscpu.c#L1359-L1370

lscpu_read_cpuinfoという関数がある通り、lscpuでも基本は/proc/cpuinfoファイルの内容を参照しているようです。しかし、一番下のlscpu_decode_arm関数の中身が気になりますね。

part = parse_model_id(ct);
if (part <= 0)
    goto done;

for (j = 0; parts[j].id != -1; j++) {
    if (parts[j].id == part) {
        free(ct->modelname);
        ct->modelname = xstrdup(parts[j].name);
        break;
    }
}

https://github.com/util-linux/util-linux/sys-utils/lscpu-arm.c#L366-L376

armの場合はlscpu_decode_arm関数から呼び出される上記の処理で、あらかじめなんらかの手段で得たモデルIDをモデル名に変換しているようです。以下のように変換テーブルを自前で管理していて味わい深いですね。

static const struct id_part arm_part[] = {
    { 0x810, "ARM810" },
    { 0x920, "ARM920" },
    { 0x922, "ARM922" },
    { 0x926, "ARM926" },
    ...
    { 0xd08, "Cortex-A72" },
    ...

https://github.com/util-linux/util-linux/sys-utils/lscpu-arm.c#L25-L97

そして、変換元のModel IDはどこから取得しているかというと、これは/proc/cpuinfoにちゃんと含まれていました。「CPU part」というカラムが対応するそうです。冒頭に挙げたarm64の出力例では「0xd08」という値になっているので、変換テーブルを引いた結果、モデル名として「Cortex-A72」が得られるという仕組みです。

Goのプログラムで/proc/cpuinfoを解析してモデル名を取得している際にどうするか

さて、お手持ちのGo製のアプリケーションで、/proc/cpuinfoを解析してCPUのモデル名を取得しているプログラムはありませんか?私はあります。

先ほどご覧いただいたlscpuの実装をgoでも真似すればarm64の対応ができるのですが、あの大量のidとmodel nameの対応マップを自前で用意したり場合分けしたりするのは大変ですよね。ここは巨人の肩に乗っかっていきたいところです。

shirou/gopsutilという、システムの情報を取得する際に便利なgoのライブラリがあり、この中にCPU情報を取得できる関数cpu.Info()があります。

pkg.go.dev

この実装を読むと、lscpu同様にテーブルを用意し、armの場合にはモデルIDをモデル名に変換しているようです。

case "model", "CPU part":
    c.Model = value
    // if CPU is arm based, model name is found via model number. refer to: arch/arm64/kernel/cpuinfo.c
    if c.VendorID == "ARM" {
        if v, err := strconv.ParseUint(c.Model, 0, 16); err == nil {
            modelName, exist := armModelToModelName[v]
            if exist {
                c.ModelName = modelName
            } else {
                c.ModelName = "Undefined"
            }
        }
    }

https://github.com/shirou/gopsutil/cpu/cpu_linux.go#L247-L259

var armModelToModelName = map[uint64]string{
    0x810: "ARM810",
    0x920: "ARM920",
    0x922: "ARM922",
    0x926: "ARM926",
    ...

https://github.com/shirou/gopsutil/cpu/cpu_linux.go#L21-L84

ついでにWindowsやFreeBSDなどの様々なOSの対応もできて嬉しいので、自前の実装をこういったメンテされているライブラリに移し替えるのを検討すると良いと思います。