はじめに
この記事は NetBSD Advent Calendar 2015 21日目のために書かれたエントリです。
このエントリでは、最近 NetBSD に実装された intrctl(8) の紹介がてら irqbalance の NetBSD への移植をしてみようと思います。
irqbalance とは
まず irqbalance に関してですが、irqbalance は Linux 向けに、というか Linux 専用に作られた割り込み負荷調整のためのデーモンです。名前の通りですね。
通常はデーモンとして動作しており、各割り込みに対して
- 割り込み総数
- デバイス種別
などを考慮して適切な CPU を選択し、定期的に割り込み先 CPU を更新してくれます。
前提となる Linux カーネルの機能
さて、この irqbalance ですが、かなりべったりと Linux カーネルの機能に依存しています。各種 procfs, sysfs に依存していますが、中でも以下2つの proc ファイルに強く依存しています。
- /proc/interrupts
- /proc/irq/"IRQ 番号"/smp_affinity
これらがどんな機能を持っているのか、それぞれ見てみましょう。
/proc/interrupts
この proc ファイルには、OS 起動後に発生した各割り込みの発生回数が記録されています。例えば私の手元の Ubuntu Linux の場合、以下のようになっていました。
CPU0 CPU1
0: 49 0 IO-APIC-edge timer
1: 10 0 IO-APIC-edge i8042
6: 3 0 IO-APIC-edge floppy
7: 0 0 IO-APIC-edge parport0
8: 1 0 IO-APIC-edge rtc0
9: 0 0 IO-APIC-fasteoi acpi
12: 151 1 IO-APIC-edge i8042
14: 0 0 IO-APIC-edge ata_piix
15: 2939816 2136274 IO-APIC-edge ata_piix
16: 0 0 IO-APIC-fasteoi vmwgfx
17: 332995 284544 IO-APIC-fasteoi ioc0
40: 0 0 PCI-MSI-edge PCIe PME, pciehp
41: 0 0 PCI-MSI-edge PCIe PME, pciehp
(略)
72: 3514430 52579904 PCI-MSI-edge eth0-rxtx-0
73: 3259334 922199 PCI-MSI-edge eth0-rxtx-1
74: 0 0 PCI-MSI-edge eth0-event-2
75: 10300930 5055506 PCI-MSI-edge eth1-rx-0
76: 1599688 1139361 PCI-MSI-edge eth1-tx-0
77: 1 0 PCI-MSI-edge eth1
78: 0 0 PCI-MSI-edge vmw_vmci
79: 0 0 PCI-MSI-edge vmw_vmci
NMI: 0 0 Non-maskable interrupts
LOC: 87520835 79960417 Local timer interrupts
SPU: 0 0 Spurious interrupts
PMI: 0 0 Performance monitoring interrupts
IWI: 11764142 14897147 IRQ work interrupts
RTR: 0 0 APIC ICR read retries
RES: 34649246 34965221 Rescheduling interrupts
CAL: 131 219 Function call interrupts
TLB: 396233 441810 TLB shootdowns
TRM: 0 0 Thermal event interrupts
THR: 0 0 Threshold APIC interrupts
MCE: 0 0 Machine check exceptions
MCP: 17327 17327 Machine check polls
一番左が IRQ 番号などの識別子になっており、その右側に各 CPU 毎の割り込み発生数が表示されています。さらに右側には割り込み種別や割り込み識別名 (デバイス名) が表示されています。
irqbalance では、このファイルから割り込み発生回数を定期的に読み取り、割り込み先 CPU 選択のヒントとしてます。
/proc/irq/"IRQ 番号"/smp_affinity
/proc/irq/"IRQ 番号"/ ディレクトリは各 IRQ 番号毎に用意されています。このディレクトリ配下には様々なファイルがありますが、そのうち smp_affinity ファイルを見ていきます。/proc/interrupts 同様、実例を見てみましょう。
3
3
このファイルには各割り込みの割り込み先 CPU がビットマスクとして指定できます。上の IRQ 72, 73 の例の場合、「割り込み先 CPU は CPU0, CPU1 のどちらでも良い」という指定になっています。このファイルは書き込み可能となっており、「割り込み先 CPU を CPU1 のみにする」場合、以下のように変更できます。
# echo 2 > /proc/irq/72/smp_affinity
# cat /proc/irq/72/smp_affinity
2
irqbalance 他にも様々なファイルを使用していますが、メインとなるのは上記2種のファイルです。
NetBSD の場合
上記の /proc/interrupts, /proc/irq/"IRQ 番号"/smp_affinity ファイルは Linux にしか存在しません。ですが、NetBSD-current には数カ月前に類似機能が実装されました。それが intrctl(8) です。
ただし残念ながら NetBSD-current/amd64 と NetBSD-current/i386 のみの対応で、NetBSD-7 以前の stable にはバックポートされていないため、使用する場合には NetBSD-current をご使用下さい。ちなみに NetBSD-current をインストールする場合、例えばこのあたり のバイナリをインストールするようにすると比較的楽にインストールできるかと思います。
intrctl(8)
この intrctl(8) には主に以下2つのサブコマンドがあります。
intrctl list
まずは割り込み発生数表示用サブコマンドである list から。
実例を見た方が解りやすいため、実例を見てみましょう。私の手元の NetBSD-current (2015/12/02 時点) では以下のようになりました。
# intrctl list
# intrctl list
interrupt id CPU#00 CPU#01 device name(s)
ioapic0 pin 9 0* 0 unknown
ioapic0 pin 1 0* 0 unknown
ioapic0 pin 12 0* 0 unknown
ioapic0 pin 14 0* 0 unknown
ioapic0 pin 15 2* 0 unknown
ioapic0 pin 17 2995* 0 unknown
ioapic0 pin 16 6* 0 wm0
ioapic0 pin 18 487* 0 unknown
msix0 vec 0 0* 0 wm1TX0
msix0 vec 1 0 0* wm1RX0
msix0 vec 2 0* 0 wm1RX1
msix0 vec 3 0* 0 wm1LINK
ioapic0 pin 7 0* 0 unknown
ioapic0 pin 4 0* 0 unknown
ioapic0 pin 3 0* 0 unknown
ioapic0 pin 6 0* 0 unknown
Linux の /proc/interrupts と似ていますね。
一番左側は IRQ 番号ではなく割り込み名となっており、その右側に各CPU毎の割り込み発生数が表示されています。'*' が付いているには現在割り込みが割り付けられている CPU になります。一番右には割り込み名 (デバイス名が) 表示される……はずですが、今のところデバイス名表示に対応しているドライバは wm しかないため、wm しか表示されていません。
intrctl affinity
次に割り込み先 CPU 変更の affinity サブコマンドです。これも同様に実例を見てみましょう。
# intrctl affinity -i 'ioapic0 pin 18' -c 1
# intrctl list
interrupt id CPU#00 CPU#01 device name(s)
ioapic0 pin 9 0* 0 unknown
ioapic0 pin 1 0* 0 unknown
ioapic0 pin 12 0* 0 unknown
ioapic0 pin 14 0* 0 unknown
ioapic0 pin 15 2* 0 unknown
ioapic0 pin 17 3080* 0 unknown
ioapic0 pin 16 8* 0 wm0
ioapic0 pin 18 883 9* unknown
msix0 vec 0 0* 0 wm1TX0
msix0 vec 1 0 0* wm1RX0
msix0 vec 2 0* 0 wm1RX1
msix0 vec 3 0* 0 wm1LINK
ioapic0 pin 7 0* 0 unknown
ioapic0 pin 4 0* 0 unknown
ioapic0 pin 3 0* 0 unknown
ioapic0 pin 6 0* 0 unknown
-i オプションで割り込み名を指定し、-c で割り込み先 CPUID を指定します。上記の例の場合、実行後に CPU#01 に割り込みが上がり始めていることが解ります。こちらは smp_affinity ファイルと同様の機能ですね。
このように、irqbalance 動作のために最低限必要となる機能に関して、NetBSD でも実装されていることが解りました。では移植してみましょう。が、その前に。
今回の目標
今回はとりあえずデーモンとしての起動は行わず、--oneshot オプションによる割り込み先CPU変更が動作することまでを目標とします。
Linux での --oneshot の動作
その目標とする動作を Linux で見てみましょう。まず初期状態として、全ての割り込みの割り込み先 CPU をどの CPU でも良い状態にしておきます。
# grep '.*' /proc/irq/*/smp_affinity
/proc/irq/0/smp_affinity:3
(略)
/proc/irq/70/smp_affinity:3
/proc/irq/71/smp_affinity:3
/proc/irq/72/smp_affinity:3
/proc/irq/73/smp_affinity:3
/proc/irq/74/smp_affinity:3
/proc/irq/75/smp_affinity:3
/proc/irq/76/smp_affinity:3
/proc/irq/77/smp_affinity:3
/proc/irq/78/smp_affinity:3
/proc/irq/79/smp_affinity:3
/proc/irq/8/smp_affinity:3
/proc/irq/9/smp_affinity:3
では irqbalance --oneshot を実行してみましょう。
# irqbalance --oneshot
# grep '.*' /proc/irq/*/smp_affinity
/proc/irq/0/smp_affinity:3
(略)
/proc/irq/70/smp_affinity:1
/proc/irq/71/smp_affinity:2
/proc/irq/72/smp_affinity:2
/proc/irq/73/smp_affinity:1
/proc/irq/74/smp_affinity:2
/proc/irq/75/smp_affinity:1
/proc/irq/76/smp_affinity:2
/proc/irq/77/smp_affinity:1
/proc/irq/78/smp_affinity:3
/proc/irq/79/smp_affinity:3
/proc/irq/8/smp_affinity:3
/proc/irq/9/smp_affinity:1
いくつかの割り込みに関して、割り込み先 CPU が CPU0 のみ (smp_affinity = 1) や CPU1 のみ (smp_affinity = 2) に変わりました。irqbalance はこのように smp_affinity の値を変えることで割り込み先 CPU を制御しています。
では NetBSD でもこの割り込み先 CPU が変更されるように修正していきます。
NetBSD への移植
作業準備
まず irqbalance 本体のソースはここにありますので、git clone しておきましょう。
またビルド時、autoconf, automake, libtool, pkg-config あたりが必要とされますので、インストールしておきます。
実作業
では実際の移植作業を行っていきます。修正方法はどの関数も似たような感じになりますので、代表的な関数に関して取り上げていきます。
rebuild_irq_db() Linux 向け実装
main()@irqbalance.c において、シグナルハンドラの設定やオプション解析の後にはまず build_object_tree() 関数が呼び出されます。build_object_tree() 内からさらにこの rebuild_irq_db() が呼び出されます。この関数の目的は、割り込みの一覧を作成することにあります。以下に実装概要を示します。
void rebuild_irq_db(void)
{
tmp_irqs = collect_full_irq_list();
devdir = opendir(SYSDEV_DIR);
do {
entry = readdir(devdir);
build_one_dev_entry(entry->d_name, tmp_irqs);
} while (entry != NULL);
}
collect_full_irq_list() で IRQ の一覧を作っておき、その後 SYSDEV_DIR (= /sys/bus/pci/devices) 配下の各ファイルを見に行き、各デバイスの詳細な情報を作成しています。
ここでもう一段、collect_full_irq_list() の中身も見てみましょう。
collect_full_irq_list() Linux 向け実装
同様に、Linux 向けの実装概要を以下に示します。
GList* collect_full_irq_list()
{
file = fopen("/proc/interrupts", "r");
while (!feof(file)) {
if (getline(&line, &size, file)==0)
break;
strncpy(savedline, line, sizeof(savedline));
irq_name = strtok_r(savedline, " ", &savedptr);
last_token = strtok_r(NULL, " ", &savedptr);
while ((p = strtok_r(NULL, " ", &savedptr))) {
irq_name = last_token;
last_token = p;
}
}
/proc/interrupts を直接読み込み、頑張ってパースしているようです。……はい。
collect_full_irq_list() NetBSD 向け修正
では NetBSD 向けの修正を行いましょう。
Linux と同じように、"intrctl list" の出力を頑張ってパース……はしなくても大丈夫なようになっています。NetBSD の intrctl(8) は "intrctl list" 出力部分がユーティリティ化されており、C 言語から多少は扱いやすくなっています。そのユーティリティは intrctl_io.h にまとめられています。
そのユーティリティを使用した collect_full_irq_list() の NetBSD 版実装概要を以下に示します。
GList* collect_full_irq_list()
{
handle = intrctl_io_alloc(intrctl_io_alloc_retry_count);
illine = intrctl_io_firstline(handle);
i = 1;
for (; illine != NULL; illine = intrctl_io_nextline(handle, illine), i++) {
struct irq_info *info;
info = calloc(sizeof(struct irq_info), 1);
if (info) {
info->irq = i;
info->hint_policy = global_hint_policy;
info->type = IRQ_TYPE_LEGACY;
info->class = IRQ_OTHER;
info->name = strdup(illine->ill_intrid);
tmp_list = g_list_append(tmp_list, info);
}
}
}
まず intrctl_io_alloc() でハンドラを作成し、そのハンドラを使用して intrctl_io_firstline() で最初の割り込みの情報を取得します。その後イテレータっぽく intrctl_io_nextline() で次の割り込みの情報が取得可能となり、最後まで読み込むと intrctl_io_nextline() は NULL を返すという仕様になっています。
ちなみにここで一つ Linux と NetBSD との仕様の違いが現れています。Linux では MSI/MSI-X も IRQ 番号で識別するのですが、NetBSD では MSI/MSI-X には IRQ 番号は振らないという仕様になっています。intrctl(8) で閉じている場合にはこの仕様で問題ないのですが、irqbalance では IRQ 番号を使用して割り込みを識別しているため、何とかして IRQ を用意する必要があります。とりあえず今回は、単に出現順に連番をふる (info->irq = i) ことにしてごまかしています。
activate_mapping()
もう一つの例として、activate_mapping() を取り上げます。この関数では、各割り込みの割り込み先 CPU の変更を行っています。
activate_mapping() Linux 向け実装
Linux 向けの実装概要は以下のようになっています。
static void activate_mapping(struct irq_info *info, void *data __attribute__((unused)))
{
sprintf(buf, "/proc/irq/%i/smp_affinity", info->irq);
file = fopen(buf, "w");
if (!file)
return;
cpumask_scnprintf(buf, PATH_MAX, applied_mask);
fprintf(file, "%s", buf);
collect_full_irq_list() 同様、/proc/irq/"IRQ 番号"/smp_affinity に直接書き込むということをしています。
activate_mapping() NetBSD 向け修正
ではこの関数に関しても同様にユーティリティ関数を使って書き直し……といきたいところですが、残念ながら "intrctl affinity" 相当の機能に関してはユーティリティ関数が用意されていません。おそらくそのうちに実装をした人 (knakahara という developer らしい) が実装してくれることでしょうが、とりあえず今回は intrctl_io.c の実装を見つつ直接書いてしまいましょう。
static void activate_mapping(struct irq_info *info, void *data __attribute__((unused)))
{
handle = intrctl_io_alloc(intrctl_io_alloc_retry_count);
cpuset = cpuset_create();
strlcpy(iset.intrid, info->name, INTRIDBUF);
cpuset_zero(cpuset);
cpuset_set(first_cpu(applied_mask), cpuset);
iset.cpuset = cpuset;
iset.cpuset_size = cpuset_size(cpuset);
if (sysctlbyname("kern.intr.affinity", NULL, NULL, &iset, sizeof(iset)) < 0)
err(EXIT_FAILURE, "sysctl kern.intr.affinity");
}
少しごちゃごちゃしていますが、やっていることは collect_full_irq_list() 向け修正と似ており、intrctl_io_alloc() でハンドラを作成し、そのハンドラを使用して後続の処理を行っています。ただし、こちらでは sysctlbyname(3) を呼び出すという内部実装が見えてしまっています。実際のところ、intrctl_io_alloc() の内部実装でも同様に sysctlbyname(3) を呼び出していますので、intrctl_io.h の関数を使用せずに実行することも可能は可能となっています。
その他色々
他にも /proc/stat を見ていたり、他の sysfs のファイルを見ていたりと色々としているのですが、今回は PoC ということで割り切り、最小限の修正にとどめます。
できた修正はこのあたり に置いています。
動作確認
では実際の動作を確認してみましょう。
上記ソースは NetBSD-current 上で、本家と同じビルド方法でビルドできます。ただし、./configure はオプションではなく必須となるようです。
まず実行前の状態を確認します。
# intrctl list
interrupt id CPU#00 CPU#01 device name(s)
ioapic0 pin 9 0* 0 unknown
ioapic0 pin 1 0* 0 unknown
ioapic0 pin 12 0* 0 unknown
ioapic0 pin 14 0* 0 unknown
ioapic0 pin 15 7* 0 unknown
ioapic0 pin 17 1653* 0 unknown
ioapic0 pin 16 11* 0 wm0
ioapic0 pin 18 915* 0 unknown
msix0 vec 0 0* 0 wm1TX0
msix0 vec 1 0 0* wm1RX0
msix0 vec 2 0* 0 wm1RX1
msix0 vec 3 0* 0 wm1LINK
ioapic0 pin 7 0* 0 unknown
ioapic0 pin 4 0* 0 unknown
ioapic0 pin 3 0* 0 unknown
ioapic0 pin 6 0* 0 unknown
では移植した irqbalance を実行してみましょう。
# ./irqbalance --oneshot
# intrctl list
interrupt id CPU#00 CPU#01 device name(s)
ioapic0 pin 9 0 0* unknown
ioapic0 pin 1 0* 0 unknown
ioapic0 pin 12 0 0* unknown
ioapic0 pin 14 0* 0 unknown
ioapic0 pin 15 7 0* unknown
ioapic0 pin 17 1731* 0 unknown
ioapic0 pin 16 11 12* wm0
ioapic0 pin 18 1352* 0 unknown
msix0 vec 0 0 0* wm1TX0
msix0 vec 1 0* 0 wm1RX0
msix0 vec 2 0 0* wm1RX1
msix0 vec 3 0* 0 wm1LINK
ioapic0 pin 7 0 0* unknown
ioapic0 pin 4 0* 0 unknown
ioapic0 pin 3 0 0* unknown
ioapic0 pin 6 0* 0 unknown
割り込み先 CPU が変更されました。デバイス種別を明確に設定しない場合、どうやら割り込み先 CPU はラウンドロビンで割り付けられるようです。
おわりに
主目的が intrctl(8) の紹介のわりに、なんだかとても遠回りしましたが気にしないことにします。
irqbalance の移植に手を出してみての感想ですが、かなり Linux にべったりの実装となっていますので、そのままの移植は行わず、設計のみを参考にしながら作りなおしたほうが良いように思いました。……やっておきながら言うのもなんですが。
以上、NetBSD も割り込み先 CPU の変更ができます、という紹介でした。