/proc/pid/pagemapの話

前回 /proc/pid/pagemap について簡単に触れたので,実際にpagemapを読み取るときどういうことが起きるか少し追ってみようと思います.

/proc/pid/pagemapに関する操作はカーネルfs/proc/task_mmu.c内でproc_pagemap_operationsに定義してあります.

https://github.com/torvalds/linux/blob/v4.11/fs/proc/task_mmu.c#L1463

const struct file_operations proc_pagemap_operations = {
    .llseek     = mem_lseek, /* borrow this */
    .read       = pagemap_read,
    .open       = pagemap_open,
    .release    = pagemap_release,
};

要するに,/proc/pid/pagemapをreadする場合はpagemap_read()が呼ばれるということです. pagemap_read()の処理を抜粋すると以下のようになります.

https://github.com/torvalds/linux/blob/v4.11/fs/proc/task_mmu.c#L1351

static ssize_t pagemap_read(struct file *file, char __user *buf,
               size_t count, loff_t *ppos)
{
   struct mm_struct *mm = file->private_data;
   struct pagemapread pm;
   struct mm_walk pagemap_walk = {};
   unsigned long src;
   unsigned long svpfn;
   unsigned long start_vaddr;
   unsigned long end_vaddr;
...
   pagemap_walk.pmd_entry = pagemap_pmd_range;
   pagemap_walk.pte_hole = pagemap_pte_hole;
#ifdef CONFIG_HUGETLB_PAGE
   pagemap_walk.hugetlb_entry = pagemap_hugetlb_range;
#endif
   pagemap_walk.mm = mm;
   pagemap_walk.private = ±

   src = *ppos;
   svpfn = src / PM_ENTRY_BYTES;
   start_vaddr = svpfn << PAGE_SHIFT;
   end_vaddr = mm->task_size;
...
   while (count && (start_vaddr < end_vaddr)) {
        ...
       down_read(&mm->mmap_sem);
       ret = walk_page_range(start_vaddr, end, &pagemap_walk);
       up_read(&mm->mmap_sem);
       start_vaddr = end;

       len = min(count, PM_ENTRY_BYTES * pm.pos);
       if (copy_to_user(buf, pm.buffer, len)) {
           ret = -EFAULT;
           goto out_free;
       }
       copied += len;
       buf += len;
       count -= len;
   }

この関数ではread()のオフセットから求めたい仮想アドレスを計算し(start_vaddr)その後 walk_page_range() 関数を呼びます.このwalk_page_range()関数は https://github.com/torvalds/linux/blob/master/mm/pagewalk.c#L283 に定義されており,指定したページ範囲を探索し,必要に応じて設定したコールバック関数を呼ぶ関数です.

今回の場合,基本的には walk_page_range() => __walk_page_range() => walk_pgd_range() => walk_pud_range() => walk_pmd_range() と探索していったのち,walk->pmd_entry() が呼ばれます.このwalk->pmd_entry()pagemap_read()の前半部分で設定したコールバック関数で,実体はpagemap_pmd_range()です.

pagemap_pmd_range()の主要部を抜き出すと以下のようになっています. https://github.com/torvalds/linux/blob/v4.11/fs/proc/task_mmu.c#L1205

static int pagemap_pmd_range(pmd_t *pmdp, unsigned long addr, unsigned long end,
                 struct mm_walk *walk)
{
    ...
    orig_pte = pte = pte_offset_map_lock(walk->mm, pmdp, addr, &ptl);
    for (; addr < end; pte++, addr += PAGE_SIZE) {
        pagemap_entry_t pme;

        pme = pte_to_pagemap_entry(pm, vma, addr, *pte);
        err = add_to_pagemap(addr, &pme, pm);
        if (err)
            break;
    }
    ....
}

また,add_to_pagemap()は以下のようになっています. https://github.com/torvalds/linux/blob/v4.11/fs/proc/task_mmu.c#L1121

static int add_to_pagemap(unsigned long addr, pagemap_entry_t *pme,
              struct pagemapread *pm)
{
    pm->buffer[pm->pos++] = *pme;
    if (pm->pos >= pm->len)
        return PM_END_OF_BUFFER;
    return 0;
}

やっていることはpmdからpteを求め,そのptepte_to_pagemap_entryでpagemapの64bitのエントリに変換し,add_to_pagemapでそれをバッファに保存します.こうして得られたpagemapのエントリを最終的にpagemap_read()のい中でcopy_to_user()でコピーしています.

ということで,pagemapをreadするとページテーブルを探索してそれの結果を返すという,そりゃそうだろうという話ですがこうなっているという話でした.

仮想アドレスから物理アドレスを求める

××な理由で仮想アドレスではなく実際の物理アドレスを求めたいことがあると思います.

linuxの場合,カーネルからならvirt2phys()等使えますが,ユーザランドからはそのような関数はありません. その代わり, /proc/self/pagemap を参照して解決することができます.

pagemap ファイルは各仮想ページがどの物理ページに対応しているかを保持している特殊ファイルです.一つのエントリの長さが64bitで,一つのエントリが一つの仮想ページの情報を保持しています.物理アドレスを求めたい場合はまず仮想アドレスページ番号をインデックスとしてpagemapのエントリを読み取り,そこから物理ページのアドレスを求めます.物理ページのアドレスが分かればそこにオフセットを足せば実際の物理アドレスが求まります.エントリの63bit目がページがメモリ上にあるかどうかを示すフラグになります.

DPDKにあるrte_mem_virt2phy() という関数を参考に*1以下のような関数で解決できました. http://dpdk.org/browse/dpdk/tree/lib/librte_eal/linuxapp/eal/eal_memory.c?h=v17.05-rc2#n159

#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/time.h>
#include <inttypes.h>

#define PFN_MASK_SIZE 8

unsigned long
virt2phy(const void *virtaddr)
{
    unsigned long page, physaddr, virt_pfn;
    off_t offset;
    int page_size = sysconf(_SC_PAGESIZE);
    int fd, retval;

    fd = open("/proc/self/pagemap", O_RDONLY);
    if (fd < 0) {
        printf("error: open");
        return 0;
    }

    virt_pfn = (unsigned long)virtaddr / page_size;
    offset = sizeof(uint64_t) * virt_pfn;
    if (lseek(fd, offset, SEEK_SET) == (off_t) -1) {
        printf("error: lseek\n");
        close(fd);
        return 0;
    }

    retval = read(fd, &page, PFN_MASK_SIZE);
    close(fd);
    if (retval < 0) {
        printf("error: read\n");
        return 0;
    } else if (retval != PFN_MASK_SIZE) {
        printf("error: read2\n");
        return 0;
    }

    if ((page & 0x7fffffffffffffULL) == 0){
        printf("pfn == 0\n");
        printf("page = %016lX\n", page);
        return 0;
    }

    physaddr = ((page & 0x7fffffffffffffULL) * page_size)
        + ((unsigned long)virtaddr % page_size);

    return physaddr;
}

実のところ,最初は以下のstack overflowの回答をみつけ,この関数を試してみたのですが,malloc()で求めたアドレスには正しく動くものの,mmap()やローカル変数のアドレスにはなぜか動きませんでした.fread()が失敗しているようですが何が原因でしょうか..

c - How to find the physical address of a variable from user-space in Linux? - Stack Overflow

*1:というかほぼそのまま

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)

です.