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

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という名前になっています

per cpu data シンボルのアドレス

kallsymsを見てみると,メモリ先頭のユーザ空間のアドレスにあるシンボルが複数存在していることが確認できます.

% sudo cat /proc/kallsyms | head
0000000000000000 A irq_stack_union
0000000000000000 A __per_cpu_start
0000000000004000 A exception_stacks
0000000000009000 A gdt_page
000000000000a000 A espfix_waddr
000000000000a008 A espfix_stack
000000000000a020 A cpu_llc_id
000000000000a028 A cpu_llc_shared_map
000000000000a030 A cpu_core_map
000000000000a038 A cpu_sibling_map

なんかおかしいなと思ってましたが,__per_cpu_start__per_cpu_endに囲まれた領域にあるシンボルはコアごとに存在するper cpu dataのシンボルのようです.

ソースは追ってませんが,ドキュメントをみると,

DEFINE_PER_CPU(int, x);
int z;

z = this_cpu_read(x);

このコードは,

mov ax, gs:[x]

のようにgsセグメントを介したアクセスになります. x86_64だとgsはIA32_GS_BASE_MSRに設定します(GDTは32bitしか対応してないため).

簡単なカーネルモジュールでIA32_GS_BASE_MSRの確認してみました.

#define MSR_GS_BASE             0xc0000101

static 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 hello_init(){
    long long gs = rdmsr(MSR_GS_BASE);
    pr_info("gs: %llx\n", gs);
    return 0;
}
% for i in `seq 0 1 7`; sudo taskset -c $i insmod hello.ko && sudo rmmod hello && dmesg|tail -n1
[35721.154116] gs: ffff8a729f200000
[35721.207429] gs: ffff8a729f240000
[35721.267899] gs: ffff8a729f280000
[35721.323081] gs: ffff8a729f2c0000
[35721.374967] gs: ffff8a729f300000
[35721.432406] gs: ffff8a729f340000
[35721.479421] gs: ffff8a729f380000
[35721.532804] gs: ffff8a729f3c0000

コアごとに0x40000 (=256KB)の領域が確保されているようです. またこの空間はメモリマップ的にはdirect mapping領域内に存在しています.

X540のMSI-X設定

実際のデバイスMSI-Xの設定の確認をしてみます. 今手元にX540があるので,それで実際に確認してみます.

文献

X540の仕様は以下から入手できます.

7.3章にX540におけるMSI-Xについて書いてあります.また,9.3章にPCIeのConfiguration Spaceの構造が書いてあります.

Configuration Space及びMSI-X Tableの確認

lspci -vvvとすると,デバイスのConfiguration Spaceの情報を表示してくれます(Capabilityを表示するには,管理者権限が必要です).

% sudo lspci -vvv -s 04:00.0
04:00.0 Ethernet controller: Intel Corporation Ethernet Controller 10-Gigabit X540-AT2 (rev 01)
        Subsystem: Intel Corporation Ethernet Converged Network Adapter X540-T2
        Control: I/O- Mem+ BusMaster+ SpecCycle- MemWINV- VGASnoop- ParErr- Stepping- SERR- FastB2B- DisINTx+
        Status: Cap+ 66MHz- UDF- FastB2B- ParErr- DEVSEL=fast >TAbort- <TAbort- <MAbort- >SERR- <PERR- INTx-
        Latency: 0, Cache Line Size: 64 bytes
        Interrupt: pin B routed to IRQ 17
        Region 0: Memory at f0200000 (64-bit, prefetchable) [size=2M]
        Region 4: Memory at f0404000 (64-bit, prefetchable) [size=16K]
        Expansion ROM at f0480000 [disabled] [size=512K]
        Capabilities: [40] Power Management version 3
                Flags: PMEClk- DSI+ D1- D2- AuxCurrent=0mA PME(D0+,D1-,D2-,D3hot+,D3cold-)
                Status: D0 NoSoftRst- PME-Enable- DSel=0 DScale=1 PME-
        Capabilities: [50] MSI: Enable- Count=1/1 Maskable+ 64bit+
                Address: 0000000000000000  Data: 0000
                Masking: 00000000  Pending: 00000000
        Capabilities: [70] MSI-X: Enable+ Count=64 Masked-
                Vector table: BAR=4 offset=00000000
                PBA: BAR=4 offset=00002000
... (省略) ...

この結果より,X540はMSIMSI-Xともに対応しており,またMSI-Xが有効(Enable+)になっていることが分かります.Count=64なので,デバイスには64個の割り込みが登録されています.またMSI-X TableはBAR 4のoffset 0から,PBAはBAR 4 のoffset 02000hからあることが分かります.BAR 4のアドレスは,Region 4: Memory at f0404000のアドレスになります.

lspci -xxx を利用すると,configuration spaceのダンプが見れるので,こちらも一応確認してみます.

% sudo lspci -xxx -s 04:00.0
04:00.0 Ethernet controller: Intel Corporation Ethernet Controller 10-Gigabit X540-AT2 (rev 01)
00: 86 80 28 15 06 04 10 00 01 00 00 02 10 00 80 00
10: 0c 00 20 f0 00 00 00 00 00 00 00 00 00 00 00 00
20: 0c 40 40 f0 00 00 00 00 00 00 00 00 86 80 01 00
30: 00 00 88 f7 40 00 00 00 00 00 00 00 0a 02 00 00
40: 01 50 23 48 00 20 00 00 00 00 00 00 00 00 00 00
50: 05 70 80 01 00 00 00 00 00 00 00 00 00 00 00 00
60: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
70: 11 a0 3f 80 04 00 00 00 04 20 00 00 00 00 00 00
80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
90: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
a0: 10 00 02 00 c2 8c 00 10 2f 28 09 00 82 fc 42 10
b0: 00 00 82 10 00 00 00 00 00 00 00 00 00 00 00 00
c0: 00 00 00 00 1f 00 00 00 00 00 00 00 00 00 00 00
d0: 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
e0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
f0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

まず,Configuration Spaceの0x34がCapability Pointerで,その部分は40です(ちなみに,Configuration Spaceはリトルエンディアンです).そこで,40h番地を見てみるとCapability IDが01h, Next Cap Ptrが50hです.Capability ID 01hはMSIでもMSI-Xでもないので,次に50h番地を見てみると,Capability IDが05h, Next Cap Ptrが70hです.Capability IDが05hなので,ここがMSIのCapabilityに相当します.また,次の70h番地を見てみると,Capability IDが11hで,ここがMSI-XのCapabilityです.この領域をよくみると,MSI-X Table BIRとMSI-X PBA BIRが4で,またTable Offsetが0,PBA Offsetが2000hであることが確認できます.

PCIバイスのBAR領域にアクセスするには,/sys/bus/pci/devices/<bus>:<device>:<num>:.<function num>/resource<BAR番号> のファイルをmmapすればアクセスすることができます.そのためのソフトウェアとして,pcimemがあります.pcimemを利用してX540のBAR4の領域にアクセスしてみると以下のようになりました.

% for j in `seq 0 1 64`; do; for i in `seq $((($j+1)*16-4)) -4 $(($j*16))`; sudo ./pcimem  /sys/bus/pci/devices/0000:04:00.0/resource4 $i w | tail -n1 | cut -d':' -f 2 | tr '\n' ' '; echo ; done
 0x0  0x41A2  0x0  0xFEE8000C
 0x0  0x41B2  0x0  0xFEE0400C
 0x0  0x41C2  0x0  0xFEE4000C
 0x0  0x41D2  0x0  0xFEE8000C
 0x0  0x41E2  0x0  0xFEE4000C
 0x0  0x4123  0x0  0xFEE1000C
 0x0  0x4143  0x0  0xFEE4000C
 0x0  0x4153  0x0  0xFEE1000C
 0x0  0x4163  0x0  0xFEEFF00C
 0x1  0x0  0x0  0x0
 0x1  0x0  0x0  0x0
 0x1  0x0  0x0  0x0
 0x1  0x0  0x0  0x0
 0x1  0x0  0x0  0x0
 0x1  0x0  0x0  0x0
 0x1  0x0  0x0  0x0
 0x1  0x0  0x0  0x0
 0x1  0x0  0x0  0x0
 0x1  0x0  0x0  0x0
 0x1  0x0  0x0  0x0
 0x1  0x0  0x0  0x0
 0x1  0x0  0x0  0x0
 0x1  0x0  0x0  0x0
 0x1  0x0  0x0  0x0
 0x1  0x0  0x0  0x0
 0x1  0x0  0x0  0x0
 0x1  0x0  0x0  0x0
 0x1  0x0  0x0  0x0
 0x1  0x0  0x0  0x0
 0x1  0x0  0x0  0x0
 0x1  0x0  0x0  0x0
 0x1  0x0  0x0  0x0
 0x1  0x0  0x0  0x0
 0x1  0x0  0x0  0x0
 0x1  0x0  0x0  0x0
 0x1  0x0  0x0  0x0
 0x1  0x0  0x0  0x0
 0x1  0x0  0x0  0x0
 0x1  0x0  0x0  0x0
 0x1  0x0  0x0  0x0
 0x1  0x0  0x0  0x0
 0x1  0x0  0x0  0x0
 0x1  0x0  0x0  0x0
 0x1  0x0  0x0  0x0
 0x1  0x0  0x0  0x0
 0x1  0x0  0x0  0x0
 0x1  0x0  0x0  0x0
 0x1  0x0  0x0  0x0
 0x1  0x0  0x0  0x0
 0x1  0x0  0x0  0x0
 0x1  0x0  0x0  0x0
 0x1  0x0  0x0  0x0
 0x1  0x0  0x0  0x0
 0x1  0x0  0x0  0x0
 0x1  0x0  0x0  0x0
 0x1  0x0  0x0  0x0
 0x1  0x0  0x0  0x0
 0x1  0x0  0x0  0x0
 0x1  0x0  0x0  0x0
 0x1  0x0  0x0  0x0
 0x1  0x0  0x0  0x0
 0x1  0x0  0x0  0x0
 0x1  0x0  0x0  0x0
 0x1  0x0  0x0  0x0
 0xDEADBEAF  0xDEADBEAF  0xDEADBEAF  0xDEADBEAF

これは各行につきMSI-X Tableのエントリ一つで,左からControl Vector, Message Data Register, Message Upper Address, Message Addressです.まず,今回Configuration Spaceで設定されたエントリ数は64でしたが,実際には9つのエントリしか有効になっていないことが分かります(Control Vector=1の場合,その割り込みはマスクされる).また,65番目以降のエントリにアクセスすると0xDEADBEAFが返ってきます(これは仕様です).ixgbeのドライバでは基本的にコア数分だけキューを用意して,割り込みはキュー間で分散させるよう設定するのだと思います(このあたり,また後で詳しく調べたい).

Message Address Registerを見てみると,下位4bitがC0であることから,RH=1, DM=1であることが分かります.Data Registerの値は下位8bitの割り込みベクタ番号のみが違い,割り込みベクタはそれぞれA2h, B2h, C2h, D2h, E2h, 23h, 43h, 53h, 63hとなっています.Local APICでは割り込みベクタの上位4bitで割り込みの優先度が決まりますが,それぞれの割り込みで同じ優先度にならないように割り込みベクタを割り振っているようです.

割り込みベクタ番号と,linuxカーネル内で利用されるirq番号の対応はvector_irq という名のstruct irq_desc*の配列に入っています.以下のようなカーネルモジュールでベクタ番号とirq番号の確認ができます(vector_irqEXPORT_SYMBOL()されていないので,kallsyms_lookup_nameを使う必要があります).

int hello_init(){
    vector_irq_t* vector_irq = (vector_irq_t*)kallsyms_lookup_name("vector_irq");
    int vector_num[] = {0xA2, 0xB2, 0xC2, 0xD2, 0xE2, 0x23, 0x43, 0x53, 0x63};
    int i = 0;
    for (i = 0; i < 9; i++) {
        struct irq_desc* desc = __this_cpu_read((*vector_irq)[vector_num[i]]);
        if (IS_ERR_OR_NULL(desc)){
            continue;
        }
        struct irq_data* data = irq_desc_get_irq_data(desc);
        pr_info("%x : %d\n", vector_num[i], data->irq);
    }

    return 0;
}

実行結果:

% sudo insmod hello5.ko && sudo rmmod hello5 && dmesg | tail -n9
[91213.998593] a2 : 33
[91213.999377] b2 : 34
[91214.000155] c2 : 35
[91214.000933] d2 : 36
[91214.001710] e2 : 37
[91214.002555] 23 : 38
[91214.003359] 43 : 39
[91214.004136] 53 : 40
[91234.077288] 63 : 41

これより,各割り込みはこの環境ではirq番号33-41に対応していることが分かります.

割り込み状況の確認

/proc/interruptsで割り込み状況を確認してみます.

% cat /proc/interrupts
           CPU0       CPU1       CPU2       CPU3       CPU4       CPU5       CPU6       CPU7
...
 33:         52       1158          0          0          0          0          0      44844   PCI-MSI 2097152-edge      enp4s0f0-TxRx-0
 34:          1         45      45965          0          0          0          0          0   PCI-MSI 2097153-edge      enp4s0f0-TxRx-1
 35:          1          0         45          0          0          0      46211          0   PCI-MSI 2097154-edge      enp4s0f0-TxRx-2
 36:          1          0          0         45          0          0          0      45965   PCI-MSI 2097155-edge      enp4s0f0-TxRx-3
 37:          0       1150          0          1         45          0      44872          0   PCI-MSI 2097156-edge      enp4s0f0-TxRx-4
 38:          0       1145          0          0      44816         51          0          0   PCI-MSI 2097157-edge      enp4s0f0-TxRx-5
 39:          1          0          0          0          0          0      46066          0   PCI-MSI 2097158-edge      enp4s0f0-TxRx-6
 40:          0          0          0          0      45969          0          0         45   PCI-MSI 2097159-edge      enp4s0f0-TxRx-7
 41:          1          0          0          0          0          0          0          0   PCI-MSI 2097160-edge      enp4s0f0
...

irqのsmp_affinityを確認すると,以下のようになっています.

% for i in `seq 33 1 41`; cat /proc/irq/$i/smp_affinity
80
04
40
80
40
10
40
10
08

この値をよく見てみると,ベクタ番号41を除き,smp_affinityの値はMSI Address Registerの12-19bit目と一致していることが分かります.また,それぞれのビットが立っている位置とCPU番号を対応付けて /proc/interrupts を見てみると,そのビットが立っているCPUに割り込みが多く入っていることが分かります.ということは,今現在MSI-Xのlogical apic IDはflat modeで運用されていると予想されます.flat modeかどうかはLocal APICDestination Fromat Register (DFR)の値によります.また,Logical APIC IDはLogical Destination Register (LDR)に格納されています.LDRは0fee000d0h, FDRは0fee000e0h 番地にmemory-mappedされています.

以下のようなカーネルモジュールを書いて確認してみます(CPUはi7 3770Kです).

int hello_init(){
    void *addr = ioremap(0xfee000D0, 32);
    pr_info("LDR,DFR : %08x, %08x \n", readl(addr), readl(addr+16));
    iounmap(addr);
    return 0;
}

実行結果:

% for i in `seq 0 1 7`; sudo taskset -c $i insmod hello.ko && dmesg | tail -n1 && sudo rmmod hello
[81324.591351] LDR,DFR : 01000000, ffffffff
[81324.651546] LDR,DFR : 02000000, ffffffff
[81324.702683] LDR,DFR : 04000000, ffffffff
[81324.758692] LDR,DFR : 08000000, ffffffff
[81324.802557] LDR,DFR : 10000000, ffffffff
[81324.855086] LDR,DFR : 20000000, ffffffff
[81324.905689] LDR,DFR : 40000000, ffffffff
[81324.945385] LDR,DFR : 80000000, ffffffff

DFRの31-28bit目が全て1なので,これはfalt modelです.また,各CPUコアごとに異なるビットがLogical APIC IDとして振られていることが分かります.この結果は/proc/interrupts の結果と矛盾しません(なんでデフォルトでMSI-Xの各エントリが各コアに対応していないのかはよく分かりませんが..).

smp_affintyを変化させた場合

smp_affinityを変更して各キューに一つのCPUが対応するようにしてみます.

% for i in `seq 0 1 7`;  echo "obase=16; $((1<<i))" | bc | sudo tee /proc/irq/$((33+i))/smp_affinity

ちゃんと変更されちるかsmp_affinityを確認してみます.

% for i in `seq 0 1 8`; cat /proc/irq/$((33+i))/smp_affinity
01
02
04
08
10
20
40
80

で,これで割り込みがコアごとに分散されるかと思ったのですが,しばらく/proc/interrupts観測を続けても特に変化なし..

おかしいと思ってMSI-X Tableを確認してみると,何も変わっていませんでした.

% for j in `seq 0 1 8`; do; for i in `seq $((($j+1)*16-4)) -4 $(($j*16))`; sudo ./pcimem  /sys/bus/pci/devices/0000:04:00.1/resource4 $i w | tail -n1 | cut -d':' -f 2 | tr '\n' ' '; echo ; done
 0x0  0x4183  0x0  0xFEE1000C
 0x0  0x4193  0x0  0xFEE4000C
 0x0  0x41A3  0x0  0xFEE0400C
 0x0  0x41B3  0x0  0xFEE0800C
 0x0  0x41C3  0x0  0xFEE1000C
 0x0  0x41D3  0x0  0xFEE4000C
 0x0  0x41E3  0x0  0xFEE0400C
 0x0  0x4124  0x0  0xFEE8000C
 0x0  0x4144  0x0  0xFEEFF00C

何が原因なのか分かりませんが,仕方ないので直接MSI-X TableのAddress RegisterのDestination Fieldを書き換えてみます.

% for i in `seq 0 1 7`; sudo ./pcimem /sys/bus/pci/devices/0000:04:00.1/resource4 $((i*16)) w "0xFEE"`echo "$((1<<i))"| awk '{printf "%02x",$1}'`"00C"

以下のように書き換わりました.

% for j in `seq 0 1 8`; do; for i in `seq $((($j+1)*16-4)) -4 $(($j*16))`; sudo ./pcimem  /sys/bus/pci/devices/0000:04:00.1/resource4 $i w | tail -n1 | cut -d':' -f 2 | tr '\n' ' '; echo ; done
 0x0  0x4183  0x0  0xFEE0100C
 0x0  0x4193  0x0  0xFEE0200C
 0x0  0x41A3  0x0  0xFEE0400C
 0x0  0x41B3  0x0  0xFEE0800C
 0x0  0x41C3  0x0  0xFEE1000C
 0x0  0x41D3  0x0  0xFEE2000C
 0x0  0x41E3  0x0  0xFEE4000C
 0x0  0x4124  0x0  0xFEE8000C
 0x0  0x4144  0x0  0xFEEFF00C

これで,割り込みがコアごとに分散されるはずです. しばらくpingを動かしてみると,以下のようになりました.

 33:       1130       1158          0          0          0          0          0      45552   PCI-MSI 2097152-edge      enp4s0f0-TxRx-0
 34:          1       1102      46673          0          0          0          0          0   PCI-MSI 2097153-edge      enp4s0f0-TxRx-1
 35:          1          0       1109          0          0          0      46923          0   PCI-MSI 2097154-edge      enp4s0f0-TxRx-2
 36:          1          0          0       1102          0          0          0      46673   PCI-MSI 2097155-edge      enp4s0f0-TxRx-3
 37:          0       1150          0          1       1805          0      45580          0   PCI-MSI 2097156-edge      enp4s0f0-TxRx-4
 38:          0       1145          0          0      45524       1108          0          0   PCI-MSI 2097157-edge      enp4s0f0-TxRx-5
 39:          1          0          0          0          0          0      47832          0   PCI-MSI 2097158-edge      enp4s0f0-TxRx-6
 40:          0          0          0          0      46677          0          0       1102   PCI-MSI 2097159-edge      enp4s0f0-TxRx-7
 41:          1          0          0          0          0          0          0          0   PCI-MSI 2097160-edge      enp4s0f0

数分後:

 33:       1343       1158          0          0          0          0          0      45552   PCI-MSI 2097152-edge      enp4s0f0-TxRx-0
 34:          1       1314      46673          0          0          0          0          0   PCI-MSI 2097153-edge      enp4s0f0-TxRx-1
 35:          1          0       1323          0          0          0      46923          0   PCI-MSI 2097154-edge      enp4s0f0-TxRx-2
 36:          1          0          0       1314          0          0          0      46673   PCI-MSI 2097155-edge      enp4s0f0-TxRx-3
 37:          0       1150          0          1       2017          0      45580          0   PCI-MSI 2097156-edge      enp4s0f0-TxRx-4
 38:          0       1145          0          0      45524       1320          0          0   PCI-MSI 2097157-edge      enp4s0f0-TxRx-5
 39:          1          0          0          0          0          0      48044          0   PCI-MSI 2097158-edge      enp4s0f0-TxRx-6
 40:          0          0          0          0      46677          0          0       1314   PCI-MSI 2097159-edge      enp4s0f0-TxRx-7
 41:          1          0          0          0          0          0          0          0   PCI-MSI 2097160-edge      enp4s0f0

この二つの差分をとると,以下のようになります.

33: 213        0        0        0        0        0        0        0
34:   0      212        0        0        0        0        0        0
35:   0        0      214        0        0        0        0        0
36:   0        0        0      212        0        0        0        0
37:   0        0        0        0      212        0        0        0
38:   0        0        0        0        0      212        0        0
39:   0        0        0        0        0        0      212        0
40:   0        0        0        0        0        0        0        212

目標通り,割り込みが分散されていることが確認できます.

smp_affinityを変更してもAddress Registerが書き変わらないのはixgbeドライバの 問題でしょうか.要調査ですね..

MSI/MSI-Xとx2APIC

MSI (Message Signaled Interrupts) はPCI Technology Specificationが策定した,PCI/PCIeで利用される割り込み方法の一つです.最初から存在したpin-basedな割り込み(INTx割り込み)と比べ,MSIを利用する利点として以下が挙げられます.

  • pin-base割り込み(INTx割り込み)と違って割り込みが共有されない
  • バイスは複数の割り込みを持てる

MSIではデータをメモリに書き込むことで割り込みを発生させますが,このとき割り込み発生時には必ずメモリの書き込みが完了していることが保証されます. MSI-XはMSIの拡張です.MSIMSI-Xの主な違いは以下の通りです.

  • MSIは32までの割り込み,MSI-Xは2048までの割り込みをサポート
  • MSIでは割り込み数は2のべき乗でなければならないが,MSI-Xにはそのような制約は ない
  • MSIでは割り込みベクタは連続でなければならないが,MSI-Xにはそのような制約はない

文献

MSI

MSIの設定はPCIのConfiguration Spaceの中でおこないます. PCIのConfiguration Spaceは以下の図のようになっていますが,ここで,図のCapability Pointerから数珠繋ぎのようにそのPCIバイスのCapabilityを表す構造が存在しています.

f:id:mm_i:20170328031807p:plain (Intel Arria 10 User Guideより)

MSIのCapabilityは以下のようになります.

f:id:mm_i:20170328032437p:plain (同上)

ここで,重要なのがMessage Addressと,Message Dataの部分です.PCIの仕様ではMessage AddressとMessage Dataの大まかな構造は規定しているものの,具体的な内容に関してはデバイス依存になっています.x86の場合,この構造はIntel SDMの10.11に書いてあります.

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

MSI Message Address Registerの19-12bit目のDestination IDと,3bit目のRH (Redirection Hint)と2bit目のDM(Destination Moede)で割り込み先を決定します.具体的には,

となります.Local APIC IDやLogical APIC IDに関しては前回の記事を参照してください.

Message Data Registerは,0-7bit目で割り込みベクタ番号を指定します.

Lowest Priority mode

Address Register の delivery mode で 001 を指定すると,lowest priority mode になります (SDM 10.6.2.4). lowest priority modeを指定し,destination field で複数のコアを指定した場合,そのコアの中で最も優先度が低いものが割り込みを受信します. 優先度はAPR (Application Priority Register) の値によって決まります.APR自体はread-onlyで,実際には read/writeできる TPR (Task Priority Register) と,現在処理中及び処理待ち中の割り込みの値からAPRが決まります.割り込みを処理していなければ APR = TPR です.SDMの10.6.2.4には APRの値が複数のコアで同じだった場合のどうなるかが書いてないような気がしますが,よく読むと SDMの10.10 に,もし低優先度のものが複数存在した場合,arbitration priority (Arb ID) を利用すると書いてあります.

このArb IDはバスアクセスの権限を得るために各LAPICが内部で持つ4bitの値で,

  • バスへアクセスしたい場合,バス要求を出し,Arb IDを1増やす
  • バスへのアクセス権は最も大きいArb IDのものが得る
  • バスへアクセスしたらArb IDを0にする

ということおおまかにをするようです. Linuxでは各LAPICのTPRの値はブート時に0に初期化し,それ以降変更することがないとのことです. したがって,lowest priority modeを選択した場合,Arb IDに基づいて送信先のコアが選ばれることになり,結果的に複数のCPUを選択した場合はラウンドロビン的にCPUが選択されます.

ただし,lowest priority modeに関してはうまく動かないといった情報もあったりします(e.g. http://forum.osdev.org/viewtopic.php?t=25372) *2

MSI-X

MSI-XのCapabilityは以下のようになっています.

f:id:mm_i:20170328032003p:plain (Intel Arria 10 User Guideより)

MSI-XではMSIと違い,Message Address RegisterやData RegsiterはBARで指定されるMMIO領域に存在します.Table BIRとPBA BIRが対応するBARの番号を,MSI-X Table Offset とMSI-X PBA Offset がBARからのオフセットを意味しています.またMessage Controlレジスタの26-16bit目がMSI-X Tableのエントリ数を表します.このビットが11bitであるため,MSI-Xでは最大2048までの割り込みが持てることになります.

MSI-X TableとMSI-X PBA は以下のようになっています.

f:id:mm_i:20170328032025p:plain f:id:mm_i:20170328032036p:plain (同上)

MSI-X Table内に,Message Data RegsiterやMessage Address Registerが存在します. また,Vector Controlは割り込みのマスクに利用します.PBAはpending messageがあるかどうかを示すビットのようです*3

MSI/MSI-X と x2APIC

前回の記事で説明した通り,x2APICはアドレス幅が8bitから32bitへ拡張されています.MSI/MSI-XではDestination Fieldが8bitなので,このままではx2APICが使用できないことになります.一体これをどうしてるんだろうと数日悩んでいましたが,どうやらIntel VT-d のinterrupt remappingを使用するようです.

VT-dのinterrupt remappingを利用する場合,まずInterrupt Remapping Table Entryというものを作成します.

f:id:mm_i:20170328032051p:plain (Intel VT-d Specより)

IRTEの63-32の32bitのフィールドでDesitinationを指定します.また24-16bit目がベクタ番号です.

IRTE自体はメモリ上に作成し,Interrupt Remapping Table Address Register (IRTA)にその先頭のアドレスを指定します.(IRTA自体はmemory-mappedされる)

f:id:mm_i:20170328032112p:plain (同上)

このとき,MSIのAddress Register とData Registerは以下のようになります.

f:id:mm_i:20170328032140p:plain (同上)

Address Registerの19-5bit目でIRTEのインデックスを指定します. Data registerは0になります.Interrupt Remappingが有効の場合,MSIでの割り込みはまずAddress RegisterのDestinationの値を使ってIRTEのエントリを取り出し,それに応じて実際の割り込みが発行されることになります. Address Registerの使ってない部分を利用してDestination Fieldを32bitにすればいいんじゃないかと思ったりもしましたが,VT-dの統一したInterrupt Remappingの仕組みを使うようにしてるんでしょうか.

*1:ここもしかすると理解が間違っているかもしれない..

*2:というか自分の環境でうまく動いてない.. 何か設定を見逃してるのだろうか

*3:これが何なのかよく分かってない..

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)

です.

x86_64 Linuxでの仮想アドレス/物理アドレス

Linux Device Driver Chapter 15では Linuxで利用されるアドレスを以下の5種類に分類しています.

  • User virtual address
  • Physical address
  • Bus address
  • Kernel logical address
  • Kenerl virtual address

User virtual addressはその名の通りユーザプロセスが利用する仮想アドレス, Physical addressは物理アドレス,Bus addressはデバイスが使用するアドレス (アーキテクチャ依存で,Physical addressと同じことも多い)です.残りの Kernel logical addressとKenel virtual addressの違いを理解するためには, Linuxでのメモリマップ(仮想ドレスの使い方)を知る必要があります.これはアー キテクチャ依存の話になりますが,ここではx86_64に関して見ていきます.

Linuxでのx86_64のメモリマップは Documentation/x86/x86_64/mm.txt に書いてあります.

0000000000000000 - 00007fffffffffff (=47 bits) user space, different per mm
hole caused by [48:63] sign extension
ffff800000000000 - ffff87ffffffffff (=43 bits) guard hole, reserved for hypervisor
ffff880000000000 - ffffc7ffffffffff (=64 TB) direct mapping of all phys. memory
ffffc80000000000 - ffffc8ffffffffff (=40 bits) hole
ffffc90000000000 - ffffe8ffffffffff (=45 bits) vmalloc/ioremap space
ffffe90000000000 - ffffe9ffffffffff (=40 bits) hole
ffffea0000000000 - ffffeaffffffffff (=40 bits) virtual memory map (1TB)
... unused hole ...
ffffec0000000000 - fffffbffffffffff (=44 bits) kasan shadow memory (16TB)
... unused hole ...
ffffff0000000000 - ffffff7fffffffff (=39 bits) %esp fixup stacks
... unused hole ...
ffffffef00000000 - fffffffeffffffff (=64 GB) EFI region mapping space
... unused hole ...
ffffffff80000000 - ffffffff9fffffff (=512 MB)  kernel text mapping, from phys 0
ffffffffa0000000 - ffffffffff5fffff (=1526 MB) module mapping space
ffffffffff600000 - ffffffffffdfffff (=8 MB) vsyscalls
ffffffffffe00000 - ffffffffffffffff (=2 MB) unused hole

0000000000000000 - 00007fffffffffffの領域がUser virtual addressに相当するも ので,プロセスごとに独自のメモリ空間になります(プロセスごとにpage tableが 切り替わる).それ以外がKernel Virtual Address( カーネルが利用する仮想アドレス,プロセス間で共通)になります. User virtual address以外の領域をカーネルが全部利用しているかというとそうで はなくて,一部の領域は使用されていません.例えばffff800000000000 - ffff87ffffffffffの領域はハイパーバイザ用に予約されています.また, “unused hole"と書いてある場所は何にも利用されていないようです.

さて,それではKernel logical addressが何かと言うと,ffff880000000000 - ffffc7ffffffffffの64TBの領域がそれに相当します.Kernel logical addressは Kernel virtual addressの一部です.この領域は “direct mapping of all phys” と書いてある通り,物理メモリ全体を連続した仮想アドレス空間にマッピン グしている領域です.MMUを利用して仮想アドレス経由で物理メモリにアクセスすると はいえ,カーネルが物理メモリ領域にアクセスできないことがあると困ります.そこ で,この領域に物理メモリ領域全体をマッピングします.こうすることでカーネルが常 に物理メモリ領域にアクセスできるようにします.x86 (32bit) の場合は仮想アドレス 空間が32bitであったため,32bitの物理アドレス全体を仮想アドレス空間マッピング することができず,物理メモリの先頭の1GB(カーネル領域のみ)をマッピングしてお り,さらにそれがhigh memory,low memory領域と別れていましたが,x86_64の場合は そのような問題はとりあえずはありません.(x86_64のアーキテクチャ的には256TBのメ モリ空間をサポートしていますが,今のところ64TBの空間があれば十分でしょう.ただ し,近々出現すると噂されるストレージクラスのメモリ (nvdimm) が登場してくるとも しかすると事情は変わるのかもしれません.)

また,ffffffff80000000 - ffffffff9fffffffアドレス空間が"kernel text mapping"として利用されており,この領域は物理メモリの先頭から512MBがマッピングさ れています.

kmalllc()を利用してカーネル内でメモリを取得した場合,Kernel logical address の範囲のメモリアドレスが得られます.そのため,kmalloc()で取得した仮想アドレ スは,物理メモリ的にも連続しています.vmalloc()で取得したアドレスは,上のメ モリーマップでの"vmalloc/ioremap space"の範囲のアドレスが得られます.要する に,この範囲のアドレスで適切にpage tableが設定されるわけで,必ずしもその仮想ア ドレスが物理的に連続しているとは限りません.

仮想アドレス <=> 物理アドレス 変換

カーネル内にはvirt_to_phys()という関数がありますが,この関数は任意の仮想アド レスを物理アドレスに変換するのではなく,Kernel logical addressあるいはkernel text mappingのアドレスを物理アドレスに変換します. その逆がphys_to_virt()です.virt_to_phys()の実装は以下のように なっています.

https://github.com/torvalds/linux/blob/v4.10/arch/x86/include/asm/io.h#L118

static inline phys_addr_t virt_to_phys(volatile void *address)
{
    return __pa(address);
}

https://github.com/torvalds/linux/blob/v4.10/arch/x86/include/asm/page.h#L41

#define __pa(x)     __phys_addr((unsigned long)(x))

https://github.com/torvalds/linux/blob/v4.10/arch/x86/mm/physaddr.c#L13

unsigned long __phys_addr(unsigned long x)
{
    unsigned long y = x - __START_KERNEL_map;

    /* use the carry flag to determine if x was < __START_KERNEL_map */
    if (unlikely(x > y)) {
        x = y + phys_base;

        VIRTUAL_BUG_ON(y >= KERNEL_IMAGE_SIZE);
    } else {
        x = y + (__START_KERNEL_map - PAGE_OFFSET);

        /* carry flag will be set if starting x was >= PAGE_OFFSET */
        VIRTUAL_BUG_ON((x > y) || !phys_addr_valid(x));
    }

    return x;
}

virt_to_phys()から__phys_addr()が呼ばれ,そこから物理アドレスを計算するた めにオフセットが引かれます.__phys_addr()ではif文で処理が別れていますが,if の最初のブロックがkernel text mappingのアドレスに対する処理,else部分のブロッ クがkernel logical addressのアドレスに対する処理です.__START_KERNEL_mapPAGE_OFFSETのアドレスは以下で定義されています.

https://github.com/torvalds/linux/blob/v4.10/arch/x86/include/asm/page_64_types.h#L39

#define __PAGE_OFFSET_BASE      _AC(0xffff880000000000, UL)
#ifdef CONFIG_RANDOMIZE_MEMORY
#define __PAGE_OFFSET           page_offset_base
#else
#define __PAGE_OFFSET           __PAGE_OFFSET_BASE

#define __START_KERNEL_map      _AC(0xffffffff80000000, UL)

上のメモリマップのアドレスと比較すれば,一致していることが分かります.

ちなみに,vmalloc()で得られた仮想アドレスから物理アドレスを求める場合は, PFN_PHYS( vmalloc_to_pfn( address )) を使えば良いようです.pfnとはpage frame numberの略で,要するに各ページに連番で振られた番号のことです.linuxは物理メモ リをpage単位で管理しており,pfnから物理アドレスを求めることが可能です.

https://github.com/torvalds/linux/blob/v4.10/mm/vmalloc.c#L269

unsigned long vmalloc_to_pfn(const void *vmalloc_addr)
{
    return page_to_pfn(vmalloc_to_page(vmalloc_addr));
}

https://github.com/torvalds/linux/blob/v4.10/mm/vmalloc.c#L235

struct page *vmalloc_to_page(const void *vmalloc_addr)
{
    unsigned long addr = (unsigned long) vmalloc_addr;
    struct page *page = NULL;
    pgd_t *pgd = pgd_offset_k(addr);

    /*
    * XXX we might need to change this if we add VIRTUAL_BUG_ON for
    * architectures that do not vmalloc module space
    */
    VIRTUAL_BUG_ON(!is_vmalloc_or_module_addr(vmalloc_addr));

    if (!pgd_none(*pgd)) {
        pud_t *pud = pud_offset(pgd, addr);
        if (!pud_none(*pud)) {
            pmd_t *pmd = pmd_offset(pud, addr);
            if (!pmd_none(*pmd)) {
                pte_t *ptep, pte;

                ptep = pte_offset_map(pmd, addr);
                pte = *ptep;
                if (pte_present(pte))
                    page = pte_page(pte);
                pte_unmap(ptep);
            }
        }
    }
    return page;
}

vmallod_to_page()ではこのようにpage tableを動的に参照して物理アドレスを求め ます.もしpageがメモリ常にない場合はNULLが返ります.

PFN_PHYS()はただ単にpage tableのオフセット分だけシフトするだけです. https://github.com/torvalds/linux/blob/v4.10/include/linux/pfn.h#L20

#define PFN_PHYS(x) ((phys_addr_t)(x) << PAGE_SHIFT)

あとはこれで得られたアドレスにオフセット(つまり x & ((1 << PAGE_SHIFT) -1) を足せば,物理アドレスが得られます.

また,あまりないと思いますがmalloc()等で取得したユーザ空間のアドレスを物理アドレスに変換したい場合 は,/proc/self/pagemapを見る方法があるようです(参考: http://stackoverflow.com/a/28987409).

実際のアドレスの確認

簡単なカーネルモジュールを書いてアドレスを確認してみると以下のように なりました.

#include <linux/kernel.h>
#include <linux/slab.h>
#include <linux/vmalloc.h>

static int hello_init(void)
{
    void *kaddr = kmalloc(1024, GFP_KERNEL);
    void *vaddr = vmalloc(1024);
    pr_info("kmalloc addr: %p (%lx), %d, %d\n", kaddr, (unsigned long)virt_to_phys(kaddr), __virt_addr_valid((unsigned long)kaddr), is_vmalloc_addr(kaddr));
    pr_info("vmalloc addr: %p, %d, %d\n", vaddr, __virt_addr_valid((unsigned long)vaddr), is_vmalloc_addr(vaddr));
    kfree(kaddr);
    vfree(vaddr);
    return 0;
}

static void hello_exit(void)
{
}

module_init(hello_init);
module_exit(hello_exit);
% make
% sudo insmod hello.ko
% dmesg | tail -n2
[1123700.799923] kmalloc addr: ffff9e5d971f6000 (1571f6000), 1, 0
[1123700.799925] vmalloc addr: ffffb5ee419a5000, 0, 1

…あれ…なんかvmallocのアドレスがメモリマップのドキュメントの"vmalloc/ioremap space"の範囲にない.. でもis_vmalloc_addr()の結果は正しい..

おかしい..と少し悩みましたがよくよくソースを見てみると__PAGE_OFFSETを設定し ている箇所でCONFIG_RANDOMIZE_MEMORYとかいう明らかに怪しいものがあることに 気づきました (https://github.com/torvalds/linux/blob/v4.10/arch/x86/include/asm/page_64_types.h#L40). このオプションですが,Linux 4.8から導入されたもので,その名の通 りこれが設定されているとメモリセクションのアドレスがセキュリティを高めるために ランダム化されるようです (http://lkml.iu.edu/hypermail/linux/kernel/1607.3/00404.html).自分のカーネルのconfig確認したら確かにCONFIG_RANDOMIZE_MEMORY=yとなっていました.

カーネルモジュールで__PAGE_OFFSETのアドレスを確認してみると,確かにランダム化 されていました.

    pr_info("__PAGE_OFFSET: %lx\n", __PAGE_OFFSET);
    pr_info("VMALLOC_START: %lx\n", VMALLOC_START);
    pr_info("VMALLOC_END:   %lx\n", VMALLOC_END);
[1123873.411375] __PAGE_OFFSET: ffff9e5c40000000
[1123873.411377] VMALLOC_START: ffffb5ee40000000
[1123873.411378] VMALLOC_END:   ffffd5ee3fffffff

このランダム化処理ですが,arch/x86/mm/kaslr.cでやっているようです. https://github.com/torvalds/linux/blob/v4.10/arch/x86/mm/kaslr.c#L147

rustでCで書いた関数を呼ぶ / Cからrustで書いた関数を呼ぶ

rustからCの関数を呼ぶ方法(あるいは,その逆)は公式のドキュメント (https://doc.rust-lang.org/book/ffi.html)に書いてあるのでそれを読めばいいんで すが,自分で書いたCの関数を呼ぶ方法について直接は書いてないのでここに簡単に まとめてみます.

rustでCで書いた関数を呼ぶ

以下のCで書いた関数をrustから呼ぶことを考えます.

foo.c

#include<stdio.h>

void foo(){
    printf("foo!!\n");
}

rustから以下のようにして関数の呼び出しをおこないます.

main.rs

#[link(name="foo", kind="static")]
extern{
    fn foo();
}

fn main() {
    unsafe {foo();};
}

#[link(name="foo", kind="static")] とするとリンカがlibfoo.aをリンクしてく れるようになります.また呼び出すCの関数はunsafeで囲む必要があります.

ここで,実際にプログラムをコンパイルする方法ですが,

1. rustcで直接リンクする

まずfoo.cからアーカイブを作成します.

$ gcc -o foo.c
$ ar rcs libfoo.a foo.c

それから,rustc-Lフラグでライブラリが置いてある場所を指定してあげればコ ンパイルできます(今回は全て同じディレクトリにあるとしています)

$ rustc -L. main.rs
$ ./main
foo!!

2. CargoでCのソースも一緒にビルドする

実際にはrustcを直接呼ぶ人はほとんどいないと思います. http://doc.crates.io/build-script.html に書いてある通り,build.rsというファイ ルにビルド処理を書けばcargoを使ってCのプログラムをコンパイルすることができま す.

ビルド処理を書く方法はいくつかありますが, https://github.com/alexcrichton/gcc-rs を利用するのが簡単だと思います. gcc-rsという名前ですがwindowsならMSVCを実行してくれるらしいです.ただ自分が windows環境で試したことはないです.

具体的には,以下のようなフォルダ構成にします.

Cargo.toml
build.rs
src/
 |----- main.rs
 |----- c
        |------- foo.c

ここで,Cargo.tomlに以下のように記述します.

[package]
links = "foo"
build = "build.rs"

[dependencies]
libc = "0.2.0"

[build-dependencies]
gcc = "0.3"

また,build.rsには以下のように記述します.

extern crate gcc;

fn main(){
    gcc::Config::new()
                .file("src/c/foo.c")
                .include("src")
                .compile("libfoo.a");
}

これで,cargo buildとすれば勝手にlibfoo.aコンパイルされて,さらに 本体のプログラムがコンパイル&リンクされます.もちろん,Cのコードを修正 すればcargo buildで自動で再コンパイルしてくれます.

Cからrustで書いた関数を呼ぶ方法

以下のrustの関数をCで呼ぶことを考えます.

foo.rs

#[no_mangle]
pub extern fn foo(){
   println!("foo");
}

ここで,extern をつけて関数がCの呼び出し規約に従うようにします.また, #[no_mangle]でマングリングされないようにしています.

呼び出し元のCの関数は以下の通りです.

main.c

#include <stdio.h>

void foo(void);

int main(){
    foo();
}

以下のようにすれば,プログラムがコンパイルできます.

$ rustc --crate_type="dylib" foo.rs
$ gcc -o main -L. -lfoo main.c
$ ./main

staticにリンクする場合は以下のようにします.

$ rustc --crate_type="staticlib" foo.rs
$ gcc -o main main.c libfoo.a
$ ./main

ということでrustからCの関数を呼ぶ方法(or その逆)について簡単に書きました. 実際には引数をどうするかとかいろいろ問題がありますが,公式のドキュメントに 書いてあると思うのでそちらを参照してみてください.というか自分が書けるほどまだ 詳しくないですし.. とりあえず何か既にあるライブラリの関数を呼びたいという場 合はlibcpuidを読ぶ例(http://siciarz.net/ffi-rust-writing-bindings-libcpuid/) が分かりやすいなと思いました.