はじめての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レジスタの設定を行っていきます。
その際、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
}
index
とvalue
を引数として、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が割込みを受信できるようにするためには、Spurious Interrupt Vector
を設定する必要があります。
Spurious Interrupt Vector Register
の下位8bitにはSpurious Interrupt Vector
のIRQ番号がマッピングされます。
そのため、0x100
と0x3f
のORを取った0x13f
がSpurious 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 mode
とOne-shot mode
の2種類の挙動を定義できます。
Periodic mode
の場合は、0になったカウントは自動的に初期値に戻り、デクリメントが再開されます。
One-shot mode
の場合は、カウントが0になって割込みを発生させた場合、プログラムが明示的に初期値を設定するまでカウントは0のままとなります。
参考:タイマー割り込みとして、Local APICのタイマ割り込みを選択したことを示すメッセージ - ZDNet Japan
xv6OSでは、Timer Divide Configuration
の値は0x0000000B
に設定されます。
参考画像: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に送信される方法を指定することができます。
参考画像: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 Register
とLVT 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がセットされます。
対応は以下の図。
参考画像: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のセット
ICRHI
とICRLOはどちらも
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に設定しています。
参考画像: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
関数の処理が全部終わりました。
今回いまいち理解できないまま進んでしまった部分も多かったので、わかり次第追記していこうと思います。
次回はいよいよセグメントディスクリプタです。
だんだん本格的にカーネルの動きが見えてきて楽しくなってきました。