読者です 読者をやめる 読者になる 読者になる

shimada-kの日記

ソフトウェア・エンジニアのブログです

Initial APIC IDとランキュー番号の対応を調べる

Linuxカーネル

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進数で表示してくれます。

f:id:shimada-k:20110531130529p:image:w360

そしてインラインアセンブラで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をランキュー番号に変換するコードが存在するはずだと思うのですが・・・