Initial APIC IDとランキュー番号の対応を調べる
Initial APIC IDとLinuxカーネルにおけるランキューの番号との対応を調べました。
Intelのマニュアルによると2C/4TのCPUが2つ載っているマシンの各CPUのInitial APIC IDは下の表の様になると書いてあります。
Initial APCI ID | Package ID | Core ID | SMT ID |
---|---|---|---|
0H | 0H | 0H | 0H |
1H | 0H | 0H | 1H |
2H | 0H | 1H | 0H |
3H | 0H | 1H | 1H |
4H | 1H | 0H | 0H |
5H | 1H | 0H | 1H |
6H | 1H | 1H | 0H |
7H | 1H | 1H | 0H |
なので、私はマルチCPUのマシンではInitial APIC IDを10進数表記したものがそのままランキュー番号になるのだと思っていました。しかし、私が大学で常用しているCore i7 870のカーネルログには
CPU0 attaching sched-domain:
domain 0: span 0,4 level SIBLING
groups: 0 (cpu_power = 589) 4 (cpu_power = 589)
domain 1: span 0-7 level MC
groups: 0,4 (cpu_power = 1178) 1,5 (cpu_power = 1178) 2,6 (cpu_power = 1178) 3,7 (cpu_power = 1178)
CPU1 attaching sched-domain:
domain 0: span 1,5 level SIBLING
groups: 1 (cpu_power = 589) 5 (cpu_power = 589)
domain 1: span 0-7 level MC
groups: 1,5 (cpu_power = 1178) 2,6 (cpu_power = 1178) 3,7 (cpu_power = 1178) 0,4 (cpu_power = 1178)
CPU2 attaching sched-domain:
domain 0: span 2,6 level SIBLING
groups: 2 (cpu_power = 589) 6 (cpu_power = 589)
domain 1: span 0-7 level MC
groups: 2,6 (cpu_power = 1178) 3,7 (cpu_power = 1178) 0,4 (cpu_power = 1178) 1,5 (cpu_power = 1178)
CPU3 attaching sched-domain:
domain 0: span 3,7 level SIBLING
groups: 3 (cpu_power = 589) 7 (cpu_power = 589)
と出ています。(CPU#0, CPU#4)(CPU#1, CPU#5)(CPU#2, CPU#6)(CPU#3, CPU#7)の組み合わせでHTの論理CPUがグルーピングされています。SMTドメインだとコアを共有する論理CPUでグルーピングされるはずなので、番号の組み合わせとして私の認識と不整合が起きています。
#上記のログはCONFIG_SCHEDSTATS=yで出ます。
そこで疑問が出てきます。OSが割り付けるランキュー番号とInitial APIC IDにはどのような対応があるのでしょう?
ソースをひたすら追っていけば分かるのですが、Linuxカーネルのソースはマクロ関数が盛大に盛ってあり、私の頭はそれに耐えうる想像力とコンテキストを保存できる領域が無いので、プログラムを書いて実験してみました。百聞は一見にしかず、です。
#define _GNU_SOURCE /* CPU_ZERO, CPU_SET */ #include <stdio.h> #include <unistd.h> #include <sched.h> /* Description:コマンドオプションで指定されたCPUでCPUID命令を実行し結果からAPIC_IDをレポートするプログラム @eax:CPUID命令を実行した後のEAXレジスタの値を格納する変数のアドレス @ebx:CPUID命令を実行した後のEBXレジスタの値を格納する変数のアドレス @ecx:CPUID命令を実行した後のECXレジスタの値を格納する変数のアドレス @edx:CPUID命令を実行した後のEDXレジスタの値を格納する変数のアドレス */ void exec_cpuid(unsigned int *eax, unsigned int *ebx, unsigned int *ecx, unsigned int *edx) { unsigned int init = *eax; __asm__ ("cpuid\n\t" : "=a"(*eax), "=b"(*ebx), "=c"(*ecx), "=d"(*edx): "0"(init)); } int main(int argc, char *argv[]) { int nr_cpus = sysconf(_SC_NPROCESSORS_CONF); unsigned int eax, ebx, ecx, edx; cpu_set_t cset; int ret, run_cpu; if(argc == 2){ /* 引数が2個だったら */ run_cpu = atoi(argv[1]); if(run_cpu >= nr_cpus || run_cpu < 0){ puts("cpuの指定が不正です"); } else{ CPU_ZERO(&cset); CPU_SET(run_cpu, &cset); if((ret = sched_setaffinity(0 , sizeof(cpu_set_t), &cset)) == -1){ perror("sched_setaffinity"); } eax = 0x01; ebx = 0; ecx = 0; edx = 0; exec_cpuid(&eax, &ebx, &ecx, &edx); /* CPUID命令を実行 */ ebx = ebx >> 24; ebx = ebx & 0xFF; printf("arg = %d", run_cpu); putchar('\n'); print_binary32(ebx); /* 2進数でコンソールに表示 */ printf("%d\n", ebx); /* 10進数でコンソールに表示 */ putchar('\n'); } } else{ /* エラー */ puts("オプションでcpuの指定をしてください"); } return 0; }
コマンドライン引数でこのプログラムを実行するCPU番号を指定します。つまりコマンドライン引数で指定されたランキューで実行されるわけです。
0番のランキューに対応しているCPUのInitial APIC IDを知りたいときは引数に0を指定するとInitial APIC IDを2進数と10進数で表示してくれます。
そしてインラインアセンブラでCPUID命令を実行しています。その際EAXレジスタに0x01を入れておき、結果をEAX〜EDXレジスタからローカル変数に格納してInitial APIC IDを取得し、コンソールにバイナリ表示しています
#print_binary32()は私が作成したライブラリ関数です。実装は省きます。
つまり、コマンドライン引数で指定されたランキューに対応するCPUのInitial APIC IDを調べるプログラム、というわけです。
上記のプログラムを8CPU、24CPUのXeonでそれぞれHT有りとHT無しの設定で実行しました。
Xeon L5520 [4C/8T]
HT無し
arg | binary | decimal |
---|---|---|
0 | 0000 0000 0000 0000 0000 0000 0001 0000 | 16 |
1 | 0000 0000 0000 0000 0000 0000 0001 0010 | 18 |
2 | 0000 0000 0000 0000 0000 0000 0001 0100 | 20 |
3 | 0000 0000 0000 0000 0000 0000 0001 0110 | 22 |
HT有り
arg | binary | decimal |
---|---|---|
0 | 0000 0000 0000 0000 0000 0000 0001 0000 | 16 |
1 | 0000 0000 0000 0000 0000 0000 0001 0010 | 18 |
2 | 0000 0000 0000 0000 0000 0000 0001 0100 | 20 |
3 | 0000 0000 0000 0000 0000 0000 0001 0110 | 22 |
4 | 0000 0000 0000 0000 0000 0000 0001 0001 | 17 |
5 | 0000 0000 0000 0000 0000 0000 0001 0011 | 19 |
6 | 0000 0000 0000 0000 0000 0000 0001 0101 | 21 |
7 | 0000 0000 0000 0000 0000 0000 0001 0111 | 23 |
Xeon X5650 [6C/12T] x2
HT無し
arg | binary | decimal |
---|---|---|
0 | 0000 0000 0000 0000 0000 0000 0000 0000 | 0 |
1 | 0000 0000 0000 0000 0000 0000 0000 0010 | 2 |
2 | 0000 0000 0000 0000 0000 0000 0000 0100 | 4 |
3 | 0000 0000 0000 0000 0000 0000 0001 0000 | 16 |
4 | 0000 0000 0000 0000 0000 0000 0001 0010 | 18 |
5 | 0000 0000 0000 0000 0000 0000 0001 0100 | 20 |
6 | 0000 0000 0000 0000 0000 0000 0010 0000 | 32 |
7 | 0000 0000 0000 0000 0000 0000 0010 0010 | 34 |
8 | 0000 0000 0000 0000 0000 0000 0010 0100 | 36 |
9 | 0000 0000 0000 0000 0000 0000 0011 0000 | 48 |
10 | 0000 0000 0000 0000 0000 0000 0011 0010 | 50 |
11 | 0000 0000 0000 0000 0000 0000 0011 0100 | 52 |
HT有り
arg | binary | decimal |
---|---|---|
0 | 0000 0000 0000 0000 0000 0000 0000 0000 | 0 |
1 | 0000 0000 0000 0000 0000 0000 0000 0010 | 2 |
2 | 0000 0000 0000 0000 0000 0000 0000 0100 | 4 |
3 | 0000 0000 0000 0000 0000 0000 0001 0000 | 16 |
4 | 0000 0000 0000 0000 0000 0000 0001 0010 | 18 |
5 | 0000 0000 0000 0000 0000 0000 0001 0100 | 20 |
6 | 0000 0000 0000 0000 0000 0000 0010 0000 | 32 |
7 | 0000 0000 0000 0000 0000 0000 0010 0010 | 34 |
8 | 0000 0000 0000 0000 0000 0000 0010 0100 | 36 |
9 | 0000 0000 0000 0000 0000 0000 0011 0000 | 48 |
10 | 0000 0000 0000 0000 0000 0000 0011 0010 | 50 |
11 | 0000 0000 0000 0000 0000 0000 0011 0100 | 52 |
12 | 0000 0000 0000 0000 0000 0000 0000 0001 | 1 |
13 | 0000 0000 0000 0000 0000 0000 0000 0011 | 3 |
14 | 0000 0000 0000 0000 0000 0000 0000 0101 | 5 |
15 | 0000 0000 0000 0000 0000 0000 0001 0001 | 17 |
16 | 0000 0000 0000 0000 0000 0000 0001 0011 | 19 |
17 | 0000 0000 0000 0000 0000 0000 0001 0101 | 21 |
18 | 0000 0000 0000 0000 0000 0000 0010 0001 | 33 |
19 | 0000 0000 0000 0000 0000 0000 0010 0011 | 35 |
20 | 0000 0000 0000 0000 0000 0000 0010 0101 | 37 |
21 | 0000 0000 0000 0000 0000 0000 0011 0001 | 49 |
22 | 0000 0000 0000 0000 0000 0000 0011 0011 | 51 |
23 | 0000 0000 0000 0000 0000 0000 0011 0101 | 53 |
*結論
- OSが管理するランキュー番号とInital APIC IDはイコールではない
- Initial APIC IDはPackage IDとCore ID、SMT IDの3つのビットフィールドから成り立っているが、それぞれのフィールドのビット列はそれぞれのフィールドでの識別子に過ぎない。
*おまけ
ではなぜInitial APIC IDをそのままランキュー番号として使用しないのでしょうか?おそらくランキュー番号を「0番以上、実装されているCPU数未満」の数字で割り振るためだと思います。
Initial APIC IDはビットカウンタなので、4コアや6コアなど2ビットで表現できないコア数が実装されたマシンでは飛び飛びの値になります。そのため上記のi7やXeonではInitial APIC IDをそのまま10進数表記すると、実装されているCPU数を越えた値が出力されます。
#カーネルソースのどこかにInitial APIC IDをランキュー番号に変換するコードが存在するはずだと思うのですが・・・