/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
を求め,そのpte
をpte_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のデータシートも参考になります.巨大なチップセットのデータシートと比べコンパクトにまとまっています.
文献
- Intel 82093AA I/O Advanced Programmable Interrupt Controller (I/O APIC) Datasheet : 特に3.0章
- Intel 9 Series Chipset Platform Controller Hub Datasheet : Z97 Expressなどで利用されているチップセットのデータシート.手元のマシンがこのチップセットなので今回これを参照しています.特に12.5にI/O APICレジスタについて書いてあります.データシート内ではI/O APICは単にAPICと記されています.なお2017現在,Intel 200 Seriesというものが最も新しいチップセットです.
I/O APICの基本
(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されているレジスタを示します.
(9-series datasheetより)
具体的には,Indexレジスタ(IOREGSEL レジスタ),Dataレジスタ(IOWINレジスタ)の2つを使います*1.この2つのレジスタはmemory-mappedされており,IndexでI/O APICレジスタのインデックス(下図参照)を指定し,Dataレジスタでデータの読み書きをおこないます.間接的にアクセスできるレジスタは以下の通りです.
(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と一致します.
(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でした.
(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の構造は以下の通りです.
(9-series datasheetより)
63-56bitでDestinationを決めます.LAPICと同様にDestinationにはLogical modeとPhysical modeがあって,11bit目でどちらのモードかを指定します.下位8bitが割り込みベクタ番号,10-8bit目がDelivery modeになっています.
各Redirection Table Registerと実際の割り込みの対応付けは以下のようになっています.
(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のインデックスを指定します.
(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はMSI,MSI-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_irq
はEXPORT_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 APICのDestination 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の拡張です.MSIとMSI-Xの主な違いは以下の通りです.
- MSIは32までの割り込み,MSI-Xは2048までの割り込みをサポート
- MSIでは割り込み数は2のべき乗でなければならないが,MSI-Xにはそのような制約は ない
- MSIでは割り込みベクタは連続でなければならないが,MSI-Xにはそのような制約はない
文献
- PCI Local Bus Specification Revision 3.0 : MSI/MSI-Xの基本仕様
- Intel SDM 10.11 : x86アーキテクチャにおけるMSI/MSI-Xのレジスタの使われ方
- Intel Arria 10 Avalon-ST Interface with SR-IOV PCIe Solutions User Guide : 5.9と6.3-5にPCIとMSI/MSI-Xについて書いてあります
- Intel® Virtualization Technology for Directed I/O: Spec 5章 : 後述のx2APICのinterrupt remappingに関して
- Understanding the Linux Kernel, 3rd edition : p.159 の “IRQ distribution in multiprocessor systems” に lowest priority mode に関する記述があります (ただし情報が古いかもしれない)
MSI
MSIの設定はPCIのConfiguration Spaceの中でおこないます. PCIのConfiguration Spaceは以下の図のようになっていますが,ここで,図のCapability Pointerから数珠繋ぎのようにそのPCIデバイスのCapabilityを表す構造が存在しています.
(Intel Arria 10 User Guideより)
MSIのCapabilityは以下のようになります.
(同上)
ここで,重要なのがMessage Addressと,Message Dataの部分です.PCIの仕様ではMessage AddressとMessage Dataの大まかな構造は規定しているものの,具体的な内容に関してはデバイス依存になっています.x86の場合,この構造はIntel SDMの10.11に書いてあります.
(Intel SDMより)
MSI Message Address Registerの19-12bit目のDestination IDと,3bit目のRH (Redirection Hint)と2bit目のDM(Destination Moede)で割り込み先を決定します.具体的には,
- RHが0 => DMは無視され,physical or logical destination modeによらず,指定したLocal APIC IDへ送られる *1
- RHが1でDMが0 (physical destination mode) => Destination IDはLocal APIC ID
- RHが1でDMが1 (logical destination mode) => Destination IDはLogical APIC ID
となります.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の値で,
ということおおまかにをするようです. 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は以下のようになっています.
(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 は以下のようになっています.
(同上)
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というものを作成します.
(Intel VT-d Specより)
IRTEの63-32の32bitのフィールドでDesitinationを指定します.また24-16bit目がベクタ番号です.
IRTE自体はメモリ上に作成し,Interrupt Remapping Table Address Register (IRTA)にその先頭のアドレスを指定します.(IRTA自体はmemory-mappedされる)
(同上)
このとき,MSIのAddress Register とData Registerは以下のようになります.
(同上)
Address Registerの19-5bit目でIRTEのインデックスを指定します. Data registerは0になります.Interrupt Remappingが有効の場合,MSIでの割り込みはまずAddress RegisterのDestinationの値を使ってIRTEのエントリを取り出し,それに応じて実際の割り込みが発行されることになります. Address Registerの使ってない部分を利用してDestination Fieldを32bitにすればいいんじゃないかと思ったりもしましたが,VT-dの統一したInterrupt Remappingの仕組みを使うようにしてるんでしょうか.
Local APICについて
APICは今のx86アーキテクチャで使用されている割り込みコントローラのことです.仕様は,
などに書いてあります.以下,図はSDMから引用しています.
APICは各コアごとに存在するLocal APICと,外部からの割り込みを担当するI/O APICに分かれます.
Local APICの割り込みソースは以下のようなものがあります
- local interrupt source
- IPI
- I/O APIC
Pentium 4やXeon以降サポートされている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
を参照することで取得できます.
IA32_APIC_BASE MSR
はecx
に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に格納されています.
また,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)と呼ばれるレジスタで管理されます.
詳細はSDMを参照するとして,図のように各割り込みソースごとにLVTが存在します.このLVTで割り込みをマスクするかどうかであったり,割り込みのベクタ番号などを設定します.
IPI
ソフトウェアからIPIを送信するには,APICのInterrupt Command Register (ICR)を利用します.ICRは64bitのレジスタです.
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のエントリに書こうと思います.
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に拡張されています.
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の場合).
- もし割り込み要求がNMI, SMI, INIT, ExtINT, SIPIであればプロセッサのコア に直接割り込みを送る
- そうでなければ,割り込みキューの空きを探し,空きがあればそこに割り込みを追 加.空きがなければ割り込みを破棄し,retry messageを返す.
- local APICは処理待ちの割り込みがある場合,優先度に基づいて割り込みを発生させる.
- 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)
です.