Local APICについて

APICは今のx86アーキテクチャで使用されている割り込みコントローラのことです.仕様は,

などに書いてあります.以下,図はSDMから引用しています.

APICは各コアごとに存在するLocal APICと,外部からの割り込みを担当するI/O APICに分かれます.

f:id:mm_i:20170327201320p:plain

Local APICの割り込みソースは以下のようなものがあります

  • local interrupt source
    • プロセッサのlocal intterupt pin(LINT0, LINT1)に接続されているデバイスからの割り込み
    • APIC timer
    • パフォーマンスモニタリングカウンタ
    • internal APIC error
    • 熱センサ
  • IPI
  • I/O APIC

Pentium 4Xeon以降サポートされているAPICは本当はオリジナルAPICの拡張でxAPICと呼ばれます.xAPICをさらに拡張したx2APICというものもあります(基本は,IDのアドレス幅が8bitだったのを32bitに拡張したものです).

Local APICレジスタ

Local APICはいくつかのレジスタを使用します.代表的なレジスタには,以下のもの等があります.

  • Local APIC ID Register : Local APIC IDの保持
  • Task Priority Register (TPR) : 優先度の管理
  • Processor Priority Register (PRR) : 優先度の管理
  • EOI Register : 割り込み終了を通知するレジスタ
  • Interrupt Request Register (IRR) : 割り込み処理に使用するレジスタ
  • Local Vector Table (LVT): ローカルに生成される割り込みを管理するためのレジスタ

Local APICレジスタは,メモリアドレス 0xfee0000 以下の領域にマッピングされます(Intel SDMのTable 10-1.x2APICの場合はMMIOではなくMSRで各レジスタにアクセスします).この0xfee0000というアドレスは後から変更することもできるらしく,実際にマッピングされているアドレスは IA32_APIC_BASE MSRを参照することで取得できます.

f:id:mm_i:20170327201456p:plain

IA32_APIC_BASE MSRecxに0x1bを設定してrdmsr命令を呼び出すと取得できます.なお,rdmsrはring 0でなければ実行できません.

簡単なカーネルモジュールで確認してみると,以下のようになりました(CPUはi7 3770Kです).

unsigned long long rdmsr(int msr)
{
    unsigned long msrl = 0, msrh = 0;
    asm volatile ("rdmsr" : "=a"(msrl), "=d"(msrh) : "c"(msr));
    return ((unsigned long long)msrh << 32) | msrl;
}

static int init_hello(void){
    long long base_msr = rdmsr(0x1b);
    pr_info("IA32_APIC_BASE MSR: %llx\n", base_msr);
    return 0;
}

static int exit_hello(void){
    return 0;
}

module_init(hello_init);
module_exit(hello_exit);
% for i in `seq 0 1 7`; sudo taskset -c $i insmod hello.ko && dmesg | tail -n1 && sudo rmmod hello
[276848.646044] IA32_APIC_BASE MSR: fee00900
[276848.729523] IA32_APIC_BASE MSR: fee00800
[276848.787947] IA32_APIC_BASE MSR: fee00800
[276848.838740] IA32_APIC_BASE MSR: fee00800
[276848.881542] IA32_APIC_BASE MSR: fee00800
[276848.931357] IA32_APIC_BASE MSR: fee00800
[276848.987969] IA32_APIC_BASE MSR: fee00800
[276849.042938] IA32_APIC_BASE MSR: fee00800

i7 3770Kは4コア8スレッドのCPUです.tasksetによって明示的に使用するCPUを使用してカーネルモジュールのロードをしています.結果より,taskset -c 0のときのみBSPフラグ(8bit目)が1であり,また全コアでAPIC Baseが同じ値であることが確認できます.

Local APIC ID

各Local APICにはLocal APIC IDと呼ばれる8bitの固有の値が振られています.LocalAPIC IDは0xfee00020にあるLocal APIC IDレジスタの24から32bitに格納されています.

f:id:mm_i:20170327201612p:plain

また,Local APIC IDは後から変更できるようですが,cpuidでeax=1としたときebxの32-24bitが初期化時のAPIC IDを保持しています.

こちらも簡単にカーネルモジュールを書いてみると以下のようになりました.

static int hello_init(){
   void *addr = ioremap(0xfee00020, 4);
   pr_info("Local APIC ID: %08x\n", readl(addr));
   iounmap(addr);
   return 0;
}
% for i in `seq 0 1 7`; sudo taskset -c $i insmod hello.ko && rmmod hello && dmesg | tail -n1
[122623.613952] Local APIC ID: 00000000
[122623.674262] Local APIC ID: 02000000
[122623.745998] Local APIC ID: 04000000
[122623.805664] Local APIC ID: 06000000
[122623.854586] Local APIC ID: 01000000
[122623.922925] Local APIC ID: 03000000
[122623.977602] Local APIC ID: 05000000
[122624.039771] Local APIC ID: 07000000

tasksetの値に応じてLocal APIC IDが変化していることがわかります.最初はコア間で同じアドレスでいいんだっけ?と思いましたが,HW的にコアに応じてコアごとの領域にアクセスするようです.(余談ですが,最初は0xfee00020のアドレスをphys_to_virt()で変換してアクセスすればいいのかと思いましたが,phys_to_virt()でアドレスを変換してもそのアドレスではpage tableが設定されていないようで,アクセスできませんでした.)

cpuidの方は以下のようになります.

struct abcd{
    unsigned int a,b,c,d;
};

void cpuid(struct abcd* r, unsigned int eax, unsigned int ecx){
    __asm__ volatile ("cpuid"
                      :"=a"(r->a), "=b"(r->b), "=c"(r->c), "=d"(r->d)
                      : "a"(eax), "c"(ecx));
}

int main(){
        struct abcd r;
        cpuid(&r, 0x1, 0x0);
        printf("%08lx\n", r.b);
}
% for i in `seq 0 1 7`; taskset -c $i ./cpuid
00100800
02100800
04100800
06100800
01100800
03100800
05100800
07100800

Local Vector Table

プロセッサのLINT0, LINT1ピンからの割り込みやAPICタイマなどのローカルな割り込みは,Local Vector Table (LVT)と呼ばれるレジスタで管理されます.

f:id:mm_i:20170327201629p:plain

詳細はSDMを参照するとして,図のように各割り込みソースごとにLVTが存在します.このLVTで割り込みをマスクするかどうかであったり,割り込みのベクタ番号などを設定します.

IPI

ソフトウェアからIPIを送信するには,APICのInterrupt Command Register (ICR)を利用します.ICRは64bitのレジスタです.

f:id:mm_i:20170327201643p:plain

ICRの下位32bitに書き込むとIPIが送信されるようです.

IPIの送信先

IPIはある特定のコアだけに送るのではなく,複数のコアやに対して一斉に送ることも可能です.そうしたこともあって,IPIの送信先の決定はやや複雑です.

まず,ICRにはdestination shorthandというフィールドがあります.

destination shorthand:

  • 00 : no shorthand (destination fieldの値を使う)
  • 01 : self (自分自身に送る)
  • 10 : all including self (broadcast (自分自身を含む))
  • 11 : all excluding self (broadcast (自分自身を含まない))

destination shorthandが00のときのみ,ICRの destination フィールド(8bit)の値が使用されます.ここで,ICRのdestination modeが0 (physical mode)のとき,destination fieldの値はLocal APIC IDを意味します.destination modeが1 (logical mode)のとき,destination fieldはMessage Destination Address (MDA)と呼ばれる値になります.

logical destination modeの場合,IPIメッセージを受信するとlocal APICはIPIのMDAを,自身のLocal Destination Register (LDR)のアドレスと比較し,さらにDstination Format Register (DFR)の値を利用してその割り込みを受け付けるかどうか決定します.delivery mode で lowest priority を選択すると,destinationに複数のコアの場合,その中で最も優先度が低いコアがIPIを受け取るようになるみたいですが,この機能はmodel specificということで非推奨のようです.lowest priority modeはMSIで割り込みをおこなう際にも利用できます.lowest priority modeに関してはMSIのエントリに書こうと思います.

f:id:mm_i:20170327201701p:plain f:id:mm_i:20170327201720p:plain

LDRは8bitのLogical APIC IDを保持するレジスタ,DFRはlLogical APIC IDのモデルを指定するためのレジスタです.

DFRで指定するLogical APIC IDのモデルには,FlatモデルとClusterモデルの2種類があります.

Flatモデルの場合,Logical APIC IDの各ビット一つ一つが一つのコアに対応します.Logical APIC IDは8bitなので,8つまでのコアについて,各ビットが立っている/立っていないでそのIPIを受け付けるべきかどうか指定できることになります.

Clusterモデルは,さらにFlat ClusterとHierarchical clusterに分かれます. Flat Clusterの場合,MDAの60-64bitでクラスタ先を指定し,56-69bitでFlatモデルのように各ビットで各コアを指定することになります.全てのビットが1の場合,ブロードキャストになります. 理論的にはクラスタ数は0-14までの15で,各クラスタ4つのコアが指定できることになりますが,APIC arbitration ID (MSIのlowest priority modeのところで説明) が15 APIC agentしかサポートしてないため,最大で15までしか対応できないようです. Hierarchical Clusterではcluster managerと呼ばれる特別なデバイスを使って複数のFlat Clusterを接続することで,最大60までのコアをサポートするようです.

x2APIC

x2APICはxAPICの拡張で,特に大きな変更点はアドレス幅が8bitから32bitになっていることと,APICレジスタへのアクセスがMSRを利用する点です.MMIOではなくてMSRを利用するようになったのは適当なアドレスが無かったからなのか何なのか分かりませんが,MSRを使用することでVTを利用する場合,指定したAPICレジスタのアクセスでVT-Exitを発生させたりできるようになるので,仮想化との相性は良いかと思います.

プロセッサがx2APICをサポートしているかどうかはeax=1としてcpuidを実行したとき,excの21bit目がセットされているかどうかでチェックできます.また,IA32_APIC_BASE MSRの10bit目がx2APIC modeの有効/無効を切り替えるビットになっています.

x2APICではlocal APIC IDレジスタが32bitになっているため,physical destination modeでは232-1のコアを指定できることになります.ICRはdestination fieldが32bitに拡張されています.

f:id:mm_i:20170327201745p:plain

LDRも32bitに拡張されています.LDRは2つの部分に分けられ,LDRの31-16bitがCluster ID,15-0bit目がlogical IDとなっています.

なお,IPIのlowest delivery mode はx2apicではサポートされていないようです.

Local APICの割り込み処理の流れ

Local APICに割り込み処理はSDMの10.8に書いてあります.処理の全貌は少々複雑ですが,大雑把には以下のようになります(Pentiumの場合).

  1. もし割り込み要求がNMI, SMI, INIT, ExtINT, SIPIであればプロセッサのコア に直接割り込みを送る
  2. そうでなければ,割り込みキューの空きを探し,空きがあればそこに割り込みを追 加.空きがなければ割り込みを破棄し,retry messageを返す.
  3. local APICは処理待ちの割り込みがある場合,優先度に基づいて割り込みを発生させる.
  4. CPUは割り込みを処理したら,EOIレジスタに書き込むことで割り込み終了を通知. (NMIなどの場合はEOIレジスタに書き込みはしない).

ここで,割り込みキューにはInterrupt Request Register(IRR)とIn-ServiceRegister(ISR)が利用されます.それぞれ256bitのレジスタで,各ビットが各割り込みベクタ番号に対応しています.local APICはIRRの対応するビットをセットすることで,その割り込みを処理待ちにします.プロセッサが割り込みを処理可能な状態になると,local APICはIRRの中で最も優先度が高いビットをクリア,対応するISRのビットをセットし,プロセッサへその割り込みを送ります.割り込みハンドラによってEOIレジスタへ書き込みがおこなわれたら,local APICは最も優先度が高いISRのビットをクリアします.

割り込みの優先度は,割り込みベクタの7-4bit目のinterrupt-priority classで判断され,その値が大きいほど優先度が高くなります.また,同じinterrupt-priority classの割り込みの中では,3-0bit目の値が大きいほど優先度が高くなります.

さらに,Processor Priority Register(PPR) というレジスタがあり,このPPRの値よりも優先度が低い割り込みはマスクされます.PPR自体はread-onlyなレジスタで,PPRの値はTask Priority Register(TPR)と,現在のISRの最も優先度が高いベクタ番号ISVRの値で決まります.具体的には,

  • PPR[7:4] = max(TPR[7:4], ISRV[7:4]
  • TPR[7:4] > ISVR[7:4] => PPR[3:0] = TPR[3:0]
  • TPR[7:4] < ISVR[7:4] => PPR[3:0] = 0
  • TPR[7:4] < ISVR[7:4] => PPR[3:0] = TPR[3:0] or 0 (model specific)

です.