I/O APICについて

前回LAPICについて書きました.LAPICは今ではCPU内に埋め込まれており,IntelのSDMにその使用方法が書いてあります.一方でI/O APICは今ではチップセットに埋め込まれているため,I/O APICに関して知りたい場合はチップセットのデータシートを見ることになります.

もともとはLAPICもI/O APICもCPUとは別の独立したチップとして実装されていました.特に,Intel 82093AAが最初のI/O APICチップです.例によって基本的に製品の互換性が保たれているため,I/O APICについて知りたい場合は82093AAのデータシートも参考になります.巨大なチップセットのデータシートと比べコンパクトにまとまっています.

文献

I/O APICの基本

f:id:mm_i:20170327201320p:plain (Intel SDMより)

図に示したように,I/O APICは外部からの割り込みを受け取り,それを各LAPICへ送ります.外部からの割り込みをどのLAPICへ送るかはRemaping Tableで設定します.I/O APICを使用するデバイスは,タイマやキーボードコントローラ(i8042)などです./proc/interruptsから何がI/O APICを利用しているか確認できます.

% cat /proc/interrupts
           CPU0       CPU1       CPU2       CPU3       CPU4       CPU5       CPU6       CPU7
  0:         16          0          0          0          0          0          0          0   IO-APIC   2-edge      timer
  1:          1          0          0          0          1          0          0          0   IO-APIC   1-edge      i8042
  8:          0          0          0          0          0          1          0          0   IO-APIC   8-edge      rtc0
  9:          0          0          0          0          4          0          0          0   IO-APIC   9-fasteoi   acpi
 12:          3          0          0          0          1          0          0          0   IO-APIC  12-edge      i8042
 16:        188          0          0          0        187         48          0          0   IO-APIC  16-fasteoi   ehci_hcd:usb1
 18:          0          0          0          0          0          2          0          0   IO-APIC  18-fasteoi   firewire_ohci
 23:         28          0          0          0          2          3          0          0   IO-APIC  23-fasteoi   ehci_hcd:usb4
...

なお,PCIeデバイスMSIを利用する場合は,I/O APICを介さずに直接割り込みがLAPICへ送られます.

I/O APICレジスタ

LAPICではアドレスfee00000h以下にLAPIC レジスタがmemory-mappedされていました(x2APICの場合はMSRを使います).I/O APICの場合,memory-mappedされているレジスタを利用して,I/O APICレジスタへアクセスします.以下にmemory-mappedされているレジスタを示します.

f:id:mm_i:20170409131928p:plain (9-series datasheetより)

具体的には,Indexレジスタ(IOREGSEL レジスタ),Dataレジスタ(IOWINレジスタ)の2つを使います*1.この2つのレジスタはmemory-mappedされており,IndexでI/O APICレジスタのインデックス(下図参照)を指定し,Dataレジスタでデータの読み書きをおこないます.間接的にアクセスできるレジスタは以下の通りです.

f:id:mm_i:20170409132110p:plain (9-series datasheetより)

IndexレジスタやDataレジスタがどこにmemory-mappedされているかはACPIから取得します.linuxの場合,iaslを利用して以下のようにACPIの情報が取得できます.

% sudo apt-get install iasl
% sudo cp /sys/firmware/acpi/tables/APIC .
% iasl -d APIC

これを実行すると,APIC.dslというファイルが作成されます.このファイルを見ると,以下のようにI/O APICについて書かれた部分が見つかります.

...
[06Ch 0108   1]                Subtable Type : 01 [I/O APIC]
[06Dh 0109   1]                       Length : 0C
[06Eh 0110   1]                  I/O Apic ID : 02
[06Fh 0111   1]                     Reserved : 00
[070h 0112   4]                      Address : FEC00000
[074h 0116   4]                    Interrupt : 00000000
...

これより,Indexレジスタが fec00000hにマッピングされていることがわかります.また,Dataレジスタはfec00010hにマッピングされています.

I/O APICレジスタの読み出し

カーネルモジュールからI/O APICレジスタを読み出してみます.読み出し方法は,まずIndexレジスタにoffsetを書き込み,その後Data Registerを読み出すことでおこないます(書き込みたい場合は,Data Registerに直接書き込みます).これらのレジスタのアクセスは全て32bit単位でおこないます.

オフセット0にあるI/O APIC IDと,オフセット1にあるI/O APIC Versionは以下のようにして読み出すことができます.

static int hello_init(){
    void *addr = ioremap(0xFEC00000, 0x10);

    writel(0, addr);
    pr_info("I/O APIC ID : %08x\n", readl(addr+0x10));

    writel(1, addr);
    pr_info("I/O APIC VER : %08x\n", readl(addr+0x10));

    iounmap(addr);

    return 0;
}
% sudo insmode hello.ko && rmmod helo && dmesg | tail -n2
[663870.752166] I/O APIC ID : 02000000
[663870.752185] I/O APIC VER : 00170020

IDは以下に示すように27-24bit目に格納されており,その値は02です.これはACPIで得られたI/O APIC IDと一致します.

f:id:mm_i:20170409132150p:plain (9-series datasheetより)

また,I/O APIC VERは7-0bit目がバージョン,23-16bit目がRedirection Tableのエントリインデックスの最大値です.したがって,このマシンではI/O APICのバージョンは0x20,Redirection Tableのエントリ数の最大(サポートする割り込みの数)は0x17+1 (=24) になります.どのI/O APICでもRedirection Tableの最大エントリ数は24のようです. → 200 seriesのデータシート見たらエントリ数の最大は120でした.

f:id:mm_i:20170409132217p:plain (9-series datasheetより)

Redirection Table

I/O APICレジスタの中でも重要なのが,Redirection Table Registerです.Redirectoin Table Registerは64bitのレジスタで,24個のRedirection Table RegisterそれぞれにはIndexレジスタで10-11h, 12-13h, …, 3e-3fhを指定することでアクセスします..

Redirection Table Registerの構造は以下の通りです.

f:id:mm_i:20170409132231p:plain f:id:mm_i:20170409132238p:plain (9-series datasheetより)

63-56bitでDestinationを決めます.LAPICと同様にDestinationにはLogical modeとPhysical modeがあって,11bit目でどちらのモードかを指定します.下位8bitが割り込みベクタ番号,10-8bit目がDelivery modeになっています.

各Redirection Table Registerと実際の割り込みの対応付けは以下のようになっています.

f:id:mm_i:20170409132320p:plain f:id:mm_i:20170409132328p:plain (9-series datasheetより)

Redirection Tableの読み出し

実際にRedirection Tableを読み出して見ます. 以下のようなカーネルモジュールで読み出すことができます.

long read_redirection_table(int idx){
    void *addr = ioremap_nocache(0xFEC00000, 0x14);
    int t1,t2;
    writel(0x10+idx*2, addr);
    t1 = readl(addr+0x10);

    writel(0x11+idx*2, addr);
    t2 = readl(addr+0x10);
    iounmap(addr);

    return ((long)(t2) << 32) | (long)(t1);
}

void read_rt(){
    int i = 0;
    for(i = 0; i < 24; i++){
        long reg = read_redirection_table(i);
        pr_info("RT Entry%2d : %016lx\n", i, reg);
    }
 }

実行結果:

[869496.874591] RT Entry 0 : 0000000000010000
[869496.874623] RT Entry 1 : ff00000000000931
[869496.874655] RT Entry 2 : ff00000000000930
[869496.874686] RT Entry 3 : ff00000000000933
[869496.874717] RT Entry 4 : ff00000000000934
[869496.874750] RT Entry 5 : ff00000000000935
[869496.874781] RT Entry 6 : ff00000000000936
[869496.874812] RT Entry 7 : ff00000000000937
[869496.874844] RT Entry 8 : ff00000000000938
[869496.874874] RT Entry 9 : ff00000000008939
[869496.874906] RT Entry10 : ff0000000000093a
[869496.874937] RT Entry11 : ff0000000000093b
[869496.874968] RT Entry12 : ff0000000000093c
[869496.875946] RT Entry13 : ff0000000000093d
[869496.876926] RT Entry14 : ff0000000000093e
[869496.877858] RT Entry15 : ff0000000000093f
[869496.878775] RT Entry16 : ff0000000000a971
[869496.879704] RT Entry17 : 0000000000010000
[869496.880615] RT Entry18 : ff0000000000a9a1
[869496.881520] RT Entry19 : 0000000000010000
[869496.882421] RT Entry20 : 0000000000010000
[869496.883306] RT Entry21 : 0000000000010000
[869496.884220] RT Entry22 : 0000000000010000
[869496.885118] RT Entry23 : ff0000000000a973

これだとちょっと分かりにくいので,以下の様な関数で表示してみます.

void print_rt(long reg){
    int dest      = (reg >> 56) & 0xff;
    int edid      = (reg >> 48) & 0xff;
    int mask      = (reg >> 16) & 0x1;
    int trigger   = (reg >> 15) & 0x1;
    int irr       = (reg >> 14) & 0x1;
    int polarity  = (reg >> 13) & 0x1;
    int status    = (reg >> 12) & 0x1;
    int dest_mode = (reg >> 11) & 0x1;
    int delv_mode = (reg >>  8) & 0x3;
    int vector = reg & 0xff;

    pr_info("  Dest           : %02X\n",  dest);
    pr_info("  EDID           : %02X\n",  edid);
    pr_info("  Mask           : %s\n",  mask ? "masked" : "not masked");
    pr_info("  Trigger mode   : %s\n",  trigger ? "level" : "edge");
    pr_info("  Remote IRR mode: %d\n",  irr);
    pr_info("  Polarity       : %s\n",  polarity ? "active low" : "active high");
    pr_info("  Delivery status: %s\n",  status ? "pending" : "idle");
    pr_info("  Dest mode      : %s\n",  dest_mode ? "logical" : "physical");
    pr_info("  Delivery mode  : %03x\n",  delv_mode);
    pr_info("  vector         : %02x\n",  vector);
}

たとえばエントリ16に関しては以下の様になります.

[865418.043710] RT Entry16 : ff0000000000a971
[865418.044335]   Dest           : FF
[865418.044967]   EDID           : 00
[865418.045572]   Mask           : not masked
[865418.046177]   Trigger mode   : level
[865418.046779]   Remote IRR mode: 0
[865418.047381]   Polarity       : active low
[865418.047989]   Delivery status: idle
[865418.048612]   Dest mode      : logical
[865418.049224]   Delivery mode  : 001
[865418.049806]   vector         : 71

ちなみに,ここでdestination modeがlogical, delivery modeは001 (lowest priority), destinationはffですが,LAPICの時と同様自分の環境では割り込みは散りませんでした.. 何故でしょうか..

x2APICの場合

x2APICを利用する場合は,MSIの場合と同様にInterrupt Remappingを使うことになります.下図のように,Redirection Tableの63-49bit目でInterrupt Rmeppaing Tableのインデックスを指定します.

f:id:mm_i:20170409132351p:plain (Intel® Virtualization Technology for Directed I/Oより)

*1:82093AAではIOREGSELとIOWINという名前ですが,9 seriesのデータシートではIndexとDataという名前になっています