2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

NetBSDAdvent Calendar 2015

Day 21

irqbalance を NetBSD に移植してみる

Posted at

はじめに

この記事は 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 の場合、以下のようになっていました。

/proc/interrupts
           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 同様、実例を見てみましょう。

/proc/irq/72/smp_affinity
3
/proc/irq/73/smp_affinity
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() が呼び出されます。この関数の目的は、割り込みの一覧を作成することにあります。以下に実装概要を示します。

classify.c
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 向けの実装概要を以下に示します。

procinterrupts.c
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 向けの実装概要は以下のようになっています。

activate.c
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 の変更ができます、という紹介でした。

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?