All Articles

xv6OSを真面目に読みこんでカーネルを完全に理解する -ローカルAPIC 編-

はじめてのOSコードリーディング ~UNIX V6で学ぶカーネルのしくみにインスパイアされてxv6 OSを読んでます。

UNIX V6自体はx86CPUでは動作しないため、基本的には、UNIXv6をX86アーキテクチャで動くようにしたxv6 OSのリポジトリをForkしたkash1064/xv6-public: xv6 OSのソースコードを読んでいくことにしました。

前回main関数で実行されるmpinit関数によるマルチプロセッサ構成でのCPU情報の取得を確認しました。

今回はlapicinit関数の挙動を追っていきます。

もくじ

lapicinit関数

今回はmain関数で最初に実行される関数群のlapicinit関数から見ていきます。

ここでは割込みコントローラの初期化を行います。

lapicinit();     // interrupt controller

lapicinit関数はlapic.cで以下のように定義されています。

void lapicinit(void)
{
  if(!lapic) return;

  // Enable local APIC; set spurious interrupt vector.
  lapicw(SVR, ENABLE | (T_IRQ0 + IRQ_SPURIOUS));

  // The timer repeatedly counts down at bus frequency
  // from lapic[TICR] and then issues an interrupt.
  // If xv6 cared more about precise timekeeping,
  // TICR would be calibrated using an external time source.
  lapicw(TDCR, X1);
  lapicw(TIMER, PERIODIC | (T_IRQ0 + IRQ_TIMER));
  lapicw(TICR, 10000000);

  // Disable logical interrupt lines.
  lapicw(LINT0, MASKED);
  lapicw(LINT1, MASKED);

  // Disable performance counter overflow interrupts
  // on machines that provide that interrupt entry.
  if(((lapic[VER]>>16) & 0xFF) >= 4) lapicw(PCINT, MASKED);

  // Map error interrupt to IRQ_ERROR.
  lapicw(ERROR, T_IRQ0 + IRQ_ERROR);

  // Clear error status register (requires back-to-back writes).
  lapicw(ESR, 0);
  lapicw(ESR, 0);

  // Ack any outstanding interrupts.
  lapicw(EOI, 0);

  // Send an Init Level De-Assert to synchronise arbitration ID's.
  lapicw(ICRHI, 0);
  lapicw(ICRLO, BCAST | INIT | LEVEL);
  while(lapic[ICRLO] & DELIVS)
    ;

  // Enable interrupts on the APIC (but not on the processor).
  lapicw(TPR, 0);
}

if(!lapic) return;ではグローバル変数lapicに値が格納されているかをチェックしています。

ローカルAPICレジスタ

変数lapic前回確認したMPテーブルの取得の中でMPフローティングポインタ内のlapicaddrのアドレスが格納されていました。

実際にこのグローバル変数に格納されている値をデバッガで確認してみます。

$ b *0x801027a0
$ continue

変数lapicの中身を見てみたところ、アドレス0xfee00000が格納されていました。

$ info variables lapic
File lapic.c:
44:	volatile uint *lapic;

$ p lapic
$1 = (volatile uint *) 0xfee00000

このlapicは、メモリマップされたローカルAPICレジスタです。

ローカルAPICレジスタはMPコンフィグレーションテーブルの指すアドレスにメモリマップされた32bitのデータです。

16バイト境界にアラインメントされたオフセットにある、32bitの各要素をローカルAPICレジスタとして設定します。

詳細は以下のページが参考になります。

参考:APIC - OSDev Wiki

以降は、ローカルAPICレジスタの設定を行っていきます。

その際、lapic.cで定義された以下の値を使用します。

// Local APIC registers, divided by 4 for use as uint[] indices.
#define ID      (0x0020/4)   // ID
#define VER     (0x0030/4)   // Version
#define TPR     (0x0080/4)   // Task Priority
#define EOI     (0x00B0/4)   // EOI
#define SVR     (0x00F0/4)   // Spurious Interrupt Vector
  #define ENABLE     0x00000100   // Unit Enable
#define ESR     (0x0280/4)   // Error Status
#define ICRLO   (0x0300/4)   // Interrupt Command
  #define INIT       0x00000500   // INIT/RESET
  #define STARTUP    0x00000600   // Startup IPI
  #define DELIVS     0x00001000   // Delivery status
  #define ASSERT     0x00004000   // Assert interrupt (vs deassert)
  #define DEASSERT   0x00000000
  #define LEVEL      0x00008000   // Level triggered
  #define BCAST      0x00080000   // Send to all APICs, including self.
  #define BUSY       0x00001000
  #define FIXED      0x00000000
#define ICRHI   (0x0310/4)   // Interrupt Command [63:32]
#define TIMER   (0x0320/4)   // Local Vector Table 0 (TIMER)
  #define X1         0x0000000B   // divide counts by 1
  #define PERIODIC   0x00020000   // Periodic
#define PCINT   (0x0340/4)   // Performance Counter LVT
#define LINT0   (0x0350/4)   // Local Vector Table 1 (LINT0)
#define LINT1   (0x0360/4)   // Local Vector Table 2 (LINT1)
#define ERROR   (0x0370/4)   // Local Vector Table 3 (ERROR)
  #define MASKED     0x00010000   // Interrupt masked
#define TICR    (0x0380/4)   // Timer Initial Count
#define TCCR    (0x0390/4)   // Timer Current Count
#define TDCR    (0x03E0/4)   // Timer Divide Configuration

Spurious Interrupt Vectorの設定とローカルAPICの有効化

先に進みます。

この行ではlapicw関数によってSpurious Interrupt Vectorを設定してローカルAPICの有効化を行います。

// Enable local APIC; set spurious interrupt vector.
lapicw(SVR, ENABLE | (T_IRQ0 + IRQ_SPURIOUS));

lapicw関数はこの後も頻繁に使用される以下の関数です。

static void
lapicw(int index, int value)
{
  lapic[index] = value;
  lapic[ID];  // wait for write to finish, by reading
}

indexvalueを引数として、lapicの値を書き換えます。

lapic[ID]のIDはlapic.c(0x0020/4)と定義されています。

lapic[ID];は特に設定変更などを行っている処理ではなく、この値を参照させることで、その前に命令したlapicの書き込みが終了するのを待つことを目的としています。

lapicw(SVR, ENABLE | (T_IRQ0 + IRQ_SPURIOUS));の行について見ていきます。

インデックスはSVRを指定しています。

これは(0x00F0/4)としてSpurious Interrupt Vector Registerのオフセットを指します。

以下記事に記載の通り、Spurious Interrupt Vector Registerのbit8(0x100)をセットすることによってAPICを有効にすることができます。

参考:APIC - OSDev Wiki

また、ローカルAPICが割込みを受信できるようにするためには、Spurious Interrupt Vectorを設定する必要があります。

Spurious Interrupt Vector Registerの下位8bitにはSpurious Interrupt VectorのIRQ番号がマッピングされます。

そのため、0x1000x3fのORを取った0x13fSpurious Interrupt Vector Registerにセットされます。

この下位ビット0x3fですが、T_IRQ0 + IRQ_SPURIOUSの演算結果として登場しています。

T_IRQ0などの値はtraps.hで定義されています。

// These are arbitrarily chosen, but with care not to overlap
// processor defined exceptions or interrupt vectors.
#define T_SYSCALL       64      // system call
#define T_DEFAULT      500      // catchall

#define T_IRQ0          32      // IRQ 0 corresponds to int T_IRQ

#define IRQ_TIMER        0
#define IRQ_KBD          1
#define IRQ_COM1         4
#define IRQ_IDE         14
#define IRQ_ERROR       19
#define IRQ_SPURIOUS    31

OSDev Wikiの解説だと、Spurious Interrupt Vectorにセットする一番簡単な値は0xffであると書かれていますが、xv6OSでは0x1fでした。

(この理由はいまいちよくわかってません。。)

タイマの設定

続いては以下の行です。

// The timer repeatedly counts down at bus frequency
// from lapic[TICR] and then issues an interrupt.
// If xv6 cared more about precise timekeeping,
// TICR would be calibrated using an external time source.
lapicw(TDCR, X1);
lapicw(TIMER, PERIODIC | (T_IRQ0 + IRQ_TIMER));
lapicw(TICR, 10000000);

TDCR(0x03E0/4)ですが、これはDivide Configuration Register (for Timer)のオフセットです。

この設定はローカルAPICタイマの周期を設定する際に使用します。

ローカルAPICタイマとは、各プロセッサのローカルAPICに内臓されているタイマで、そのプロセッサのみに対して割込みを発生させます。

ローカルAPICタイマは、カーネルからTimer Initial Countとして設定された値を初期値として一定の間隔でデクリメントしていき、0になったときに割込みを発生させます。

このデクリメント速度は、CPUのバス周波数をTimer Divide Configurationの値で割った値に依存します。

ローカルAPICタイマでは、Periodic modeOne-shot modeの2種類の挙動を定義できます。

Periodic modeの場合は、0になったカウントは自動的に初期値に戻り、デクリメントが再開されます。

One-shot modeの場合は、カウントが0になって割込みを発生させた場合、プログラムが明示的に初期値を設定するまでカウントは0のままとなります。

参考:APIC timer - OSDev Wiki

参考:タイマー割り込みとして、Local APICのタイマ割り込みを選択したことを示すメッセージ - ZDNet Japan

xv6OSでは、Timer Divide Configurationの値は0x0000000Bに設定されます。

2022/02/image.png

参考画像:Intel SDM vol3

また、Timer Initial Countの値はlapicw(TICR, 10000000);によって10000000に設定されます。

ちなみに以下の行は、Local Vector Table 0 (TIMER)の設定を行っています。

lapicw(TIMER, PERIODIC | (T_IRQ0 + IRQ_TIMER));

Local Vector Table(LVT)自体はローカルAPICにおけるイベントを割込みベクタに変換するためのテーブルです。

LVTによって、ソフトウェアはローカル割込みがCPUに送信される方法を指定することができます。

2022/02/image-1.png

参考画像:Intel SDM vol3

LVTは以下の32bitレジスタから構成されています。詳細は実際に使用するときに見ていきます。

  • LVT Timer Register (FEE0 0320H)
  • LVT Thermal Monitor Register (FEE0 0330H)
  • LVT Performance Counter Register (FEE0 0340H)
  • LVT LINT0 Register (FEE0 0350H)
  • LVT LINT1 Register (FEE0 0360H)
  • LVT Error Register (FEE0 0370H)

ここで、TIMER(0x0320/4)LVT Timer Registerを指定しています。

LVT Timer RegisterはAPICのタイマが割込みを発生させた場合の割込みを指定します。

ここではPERIODICによってLVTの18bit目のTimer Modeを1にセットすることで、タイマの動作をPeriodic modeに変更しています。

また、T_IRQ0 + IRQ_TIMERでは、割込みベクタを設定していますが、セットしている値は0x20となっているようです。 (ググってもダイレクトな情報ソース見つからなかったのですが、たぶんINT 0x20みたいなタイマ割込みを指している?わからん。)

Disable logical interrupt lines

次いきます。

(0x0350/4)(0x0360/4)にそれぞれ0x00010000をセットしています。

// Disable logical interrupt lines.
lapicw(LINT0, MASKED);
lapicw(LINT1, MASKED);

まず、(0x0340/4)(0x0350/4)LVT LINT0 RegisterLVT LINT1 Registerでした。

これらはどちらも、LINT0およびLINT1ピンからの割込みを定義する割込みベクタです。

ここで、17bit目をセットしてマスクを有効化しています。

(なんでこの2つを無効化する必要があるのかは全然わかりませんでした!なぜ!!)

Disable performance counter overflow interrupts

次行きます。

// Disable performance counter overflow interrupts
// on machines that provide that interrupt entry.
if(((lapic[VER]>>16) & 0xFF) >= 4) lapicw(PCINT, MASKED);

PCINTはオーバフローが発生したときに割込みを生成するLVT Performance Counter Registerを特定の条件下でマスクしています。

その条件は、ローカルAPICのVersion Registerの値を16bit右シフトして下位8bitを取ったときの値が4より大きくなることです。

意味は全然わかりませんでした。。

とりあえずデバッガで実行してみたらこの行は実行されなかったので、xv6OSはLVT Performance Counter Registerをマスクしないものとして先に進みます…。

Error Registerのセット

LVT Error Registerの割込みベクタをセットしています。

// Map error interrupt to IRQ_ERROR.
lapicw(ERROR, T_IRQ0 + IRQ_ERROR);

ESRのクリア

ここではESRをクリアしています。

// Clear error status register (requires back-to-back writes).
lapicw(ESR, 0);
lapicw(ESR, 0);

ESRはError Status Registerの略で、エラーが発生したときにbitがセットされます。

対応は以下の図。

2022/02/image-2.png

参考画像:Intel SDM vol3

なんで2回クリアしているのかは、またしても謎です。

EOIのチェック

EOIはEnd of interruptの略で、特定の割込み処理が完了したことを示す信号で、PICに送信されます。

// Ack any outstanding interrupts.
lapicw(EOI, 0);

参考:End of interrupt - Wikipedia

EOIレジスタには互換性のために0をセットしておく必要があります。

Interrupt Command Registerのセット

ICRHIICRLOはどちらもInterrupt Command Register`です。

これら2つのレジスタは、CPUに割込みを送信するために使用されます。

なお、(0x0300/4)にデータが書き込まれたときは割込みが発生しますが、(0x0310/4)に書き込まれた時には割込みは発生しないため、ICRHIからICRHOの順で値をセットしています。

// Send an Init Level De-Assert to synchronise arbitration ID's.
lapicw(ICRHI, 0);
lapicw(ICRLO, BCAST | INIT | LEVEL);
while(lapic[ICRLO] & DELIVS);

ここでは、INIT Level De-assertによってシステム内のすべてのローカルAPICに同期メッセージを送信し、そのarbitration IDをAPICに設定しています。

2022/02/image-3.png

参考画像:Intel SDM vol3

最後にTPR(Task Priority Register)に0をセットすることで、CPUがすべての割込みを処理できるようになるため、割込み機能が有効化されます。

// Enable interrupts on the APIC (but not on the processor).
lapicw(TPR, 0);

ちなみにTPRに15をセットすると、すべての割込みが禁止されます。

まとめ

これでmain関数から呼び出されたlapicinit関数の処理が全部終わりました。

今回いまいち理解できないまま進んでしまった部分も多かったので、わかり次第追記していこうと思います。

次回はいよいよセグメントディスクリプタです。

だんだん本格的にカーネルの動きが見えてきて楽しくなってきました。

参考書籍