はじめてのOSコードリーディング ~UNIX V6で学ぶカーネルのしくみにインスパイアされてxv6 OSを読んでます。
UNIX V6自体はx86CPUでは動作しないため、基本的には、UNIXv6をX86アーキテクチャで動くようにしたxv6 OSのリポジトリをForkしたkash1064/xv6-public: xv6 OSのソースコードを読んでいくことにしました。
前回はmain
関数で実行されるkvmalloc
関数によるページテーブル割り当ての挙動を確認しました。
今回はmpinit
関数の挙動を追っていきます。
もくじ
mpinit関数
mpinit
関数はmp.c
で定義されている以下の関数です。
「mp」はたぶんマルチプロセッサの意ですが、他のプロセッサを検出する役割を持つ関数がmpinit
関数です。
void mpinit(void)
{
uchar *p, *e;
int ismp;
struct mp *mp;
struct mpconf *conf;
struct mpproc *proc;
struct mpioapic *ioapic;
if((conf = mpconfig(&mp)) == 0) panic("Expect to run on an SMP");
ismp = 1;
lapic = (uint*)conf->lapicaddr;
for(p=(uchar*)(conf+1), e=(uchar*)conf+conf->length; p<e; ){
switch(*p){
case MPPROC:
proc = (struct mpproc*)p;
if(ncpu < NCPU) {
cpus[ncpu].apicid = proc->apicid; // apicid may differ from ncpu
ncpu++;
}
p += sizeof(struct mpproc);
continue;
case MPIOAPIC:
ioapic = (struct mpioapic*)p;
ioapicid = ioapic->apicno;
p += sizeof(struct mpioapic);
continue;
case MPBUS:
case MPIOINTR:
case MPLINTR:
p += 8;
continue;
default:
ismp = 0;
break;
}
}
if(!ismp)
panic("Didn't find a suitable machine");
if(mp->imcrp){
// Bochs doesn't support IMCR, so this doesn't run on Bochs.
// But it would on real hardware.
outb(0x22, 0x70); // Select IMCR
outb(0x23, inb(0x23) | 1); // Mask external interrupts.
}
}
それではソースコードを順に読んでいきます。
各種構造体変数の宣言
関数呼び出し後、複数の構造体変数を宣言しています。
uchar *p, *e;
int ismp;
struct mp *mp;
struct mpconf *conf;
struct mpproc *proc;
struct mpioapic *ioapic;
これらはいずれもmp.h
で定義されています。
構造体の定義は以下です。
詳細は実際に使用するソースコードを読む際に見ていくため、一旦割愛します。
// See MultiProcessor Specification Version 1.[14]
struct mp { // floating pointer
uchar signature[4]; // "_MP_"
void *physaddr; // phys addr of MP config table
uchar length; // 1
uchar specrev; // [14]
uchar checksum; // all bytes must add up to 0
uchar type; // MP system config type
uchar imcrp;
uchar reserved[3];
};
struct mpconf { // configuration table header
uchar signature[4]; // "PCMP"
ushort length; // total table length
uchar version; // [14]
uchar checksum; // all bytes must add up to 0
uchar product[20]; // product id
uint *oemtable; // OEM table pointer
ushort oemlength; // OEM table length
ushort entry; // entry count
uint *lapicaddr; // address of local APIC
ushort xlength; // extended table length
uchar xchecksum; // extended table checksum
uchar reserved;
};
struct mpproc { // processor table entry
uchar type; // entry type (0)
uchar apicid; // local APIC id
uchar version; // local APIC verison
uchar flags; // CPU flags
#define MPBOOT 0x02 // This proc is the bootstrap processor.
uchar signature[4]; // CPU signature
uint feature; // feature flags from CPUID instruction
uchar reserved[8];
};
struct mpioapic { // I/O APIC table entry
uchar type; // entry type (2)
uchar apicno; // I/O APIC id
uchar version; // I/O APIC version
uchar flags; // I/O APIC flags
uint *addr; // I/O APIC address
};
MP仕様について
ソースコードを読む前に、MP仕様についてまとめておきます。
MPテーブルとは、x86CPUの持っているOSにマルチプロセッサの情報を取得させるための仕組みです。
MPテーブルにはx86CPUのMP仕様に関連した情報が格納されています。
以下は、Interlのドキュメントに記載されていたMP仕様のデータ構造の図です。
FLOATING POINTER STRUCTURE
からFIXED-LENGTH HEADER
を参照しています。
参考画像:Intel MultiProcessor Specification | ManualsLib
このFLOATING POINTER STRUCTURE
は、MPフローティングポインタ構造体であり、xv6OSではmp
構造体として定義されていました。
システムにMPフローティングポインタ構造体が存在する場合、そのシステムはMP仕様に準拠していることを意味します。
MPフローティングポインタ構造体には以下の情報が含まれます。
- MPコンフィグレーションテーブルへのポインタ
- その他のMP情報へのポインタ
MPコンフィグレーションテーブルは、xv6OSでmpconf
構造体として定義されています。
後述するmpconfig
関数では、MPフローティングポインタ構造体を取得した後に、その情報からMPコンフィグレーションテーブルを取得する処理が定義されています。
では、OSはどうやってMPフローティングポインタ構造体を見つけるのかという点ですが、Interlの仕様書によると、MPフローティングポインタ構造体は以下のいずれかに存在するよう定義されているため、OSはこれらを検索してMPフローティングポインタ構造体の有無を確認することになります。
- 拡張BIOSデータ領域(EBDA)の最初の1KiB以内の領域
- システムベースメモリ領域の最後の1KiB以内の範囲
0x0F0000
から0x0FFFFFF
の間のBIOS ROMアドレス空間
xv6OSでも、mpsearch
関数とmpsearch1
関数によって、上記の領域の探索が行われます。
これらの関数については後述します。
次にMPコンフィグレーションテーブルですが、これは通常オプションの設定のようです。
システムがデフォルトの場合はMPコンフィグレーションテーブルの定義は不要ですが、CPUの数が変動する可能性のある場合などは必須になります。(実質的に汎用OSでは必須ってことでしょうか)
MPコンフィグレーションテーブルにはAPICやプロセッサ、バス、割込みに関する設定情報が含まれます。
また新しくAPICという単語がでてきましたが、これはIntelのマルチプロセッサCPUで使用される割込み制御の機構です。
APICについてはxv6OSで割込みコントローラを設定するときに詳しく見ていこうと思います。
MP仕様についてはWEBページや書籍からはあまり有益な情報が得られなかったので、詳しく知るにはIntelの仕様書を読むのが一番早いと思います。
MPフローティングポインタ構造体の取得
というわけでさっそく以下のコードを見ます。
if((conf = mpconfig(&mp)) == 0) panic("Expect to run on an SMP");
mpconfig
関数の引数としてmp
構造体を与え、戻り値をmpconf
構造体の変数であるconf
に格納しています。
ここでは、mpconf
構造体の変数conf
を初期化するとともに、システムがSMPで動作しているかをチェックします。
SMPとは、Symmetric multiprocessing
あるいはshared-memory multiprocessing
の略称で、要するに複数のCPUがメモリリソースを共有するマルチプロセッサシステムを意味しています。
参考:Symmetric multiprocessing - Wikipedia
参考:Symmetric Multiprocessing - OSDev Wiki
一旦mpconfig
関数のソースコードを見てみましょう。
mpconfig
関数は、mpinit
関数で宣言したmp
構造体オブジェクトのアドレスを引数として、mpconf
構造体を戻り値とする関数です。
この関数によってMPテーブルが検索され、引数として受け取ったmp
構造体オブジェクトと戻り値のmpconf
構造体の初期化が行われます。
// Search for an MP configuration table. For now,
// don't accept the default configurations (physaddr == 0).
// Check for correct signature, calculate the checksum and,
// if correct, check the version.
// To do: check extended table checksum.
static struct mpconf* mpconfig(struct mp **pmp)
{
struct mpconf *conf;
struct mp *mp;
if((mp = mpsearch()) == 0 || mp->physaddr == 0) return 0;
conf = (struct mpconf*) P2V((uint) mp->physaddr);
if(memcmp(conf, "PCMP", 4) != 0) return 0;
if(conf->version != 1 && conf->version != 4) return 0;
if(sum((uchar*)conf, conf->length) != 0) return 0;
*pmp = mp;
return conf;
}
mp
構造体は前述したMPフローティングポインタ構造体を指します。
mpconfig
関数が呼び出された時点ではまだシステムのMPフローティングポインタ構造体は取得できていないので、まずMPフローティングポインタ構造体を探索する必要があります。
このために呼び出されるのがmpsearch
関数とmpsearch1
関数です。
// Look for an MP structure in the len bytes at addr.
static struct mp* mpsearch1(uint a, int len)
{
uchar *e, *p, *addr;
addr = P2V(a);
e = addr+len;
for(p = addr; p < e; p += sizeof(struct mp))
{
if(memcmp(p, "_MP_", 4) == 0 && sum(p, sizeof(struct mp)) == 0) return (struct mp*)p;
}
return 0;
}
// Search for the MP Floating Pointer Structure, which according to the
// spec is in one of the following three locations:
// 1) in the first KB of the EBDA;
// 2) in the last KB of system base memory;
// 3) in the BIOS ROM between 0xE0000 and 0xFFFFF.
static struct mp* mpsearch(void)
{
uchar *bda;
uint p;
struct mp *mp;
bda = (uchar *) P2V(0x400);
if((p = ((bda[0x0F]<<8)| bda[0x0E]) << 4)){
if((mp = mpsearch1(p, 1024))) return mp;
} else {
p = ((bda[0x14]<<8)|bda[0x13])*1024;
if((mp = mpsearch1(p-1024, 1024))) return mp;
}
return mpsearch1(0xF0000, 0x10000);
}
前述した通り、MPフローティングポインタ構造体が存在する場合は、以下のいずれかに配置されています。
- 拡張BIOSデータ領域(EBDA)の最初の1KiB以内の領域
- システムベースメモリ領域の最後の1KiB以内の範囲
0x0F0000
から0x0FFFFFF
の間のBIOS ROMアドレス空間
これらの領域を検索してMPフローティングポインタ構造体が見つかった場合はmp
に格納されます。
このとき、MPフローティングポインタ構造体が見つからない場合、もしくはMPフローティングポインタ構造体が持つMPコンフィグレーションテーブルのアドレスが空の場合はカーネルを終了します。
if((mp = mpsearch()) == 0 || mp->physaddr == 0) return 0;
MPフローティングポインタ構造体は以下の構造になっています。
struct mp { // floating pointer
uchar signature[4]; // "_MP_"
void *physaddr; // phys addr of MP config table
uchar length; // 1
uchar specrev; // [14]
uchar checksum; // all bytes must add up to 0
uchar type; // MP system config type
uchar imcrp;
uchar reserved[3];
};
mp
構造体の定義は上記ですが、Intel仕様書の図の方がイメージしやすいので一緒に貼っておきます。
参考画像:Intel MultiProcessor Specification | ManualsLib
最初の4バイトのSIGNATURE
には_MP_
が格納されていることが期待されます。
mpsearch
関数とmpsearch1
関数で探索を行う場合には、このSIGNATURE
を探索しています。
physaddr
にはMPコンフィグレーションテーブルのアドレスが格納されており、ここからはこの情報を元にMPコンフィグレーションテーブルを取得していきます。
MPコンフィグレーションテーブルの取得
MPフローティングポインタ構造体を取得したら、MPコンフィグレーションテーブルの仮想アドレスを取得し、mpconf
構造のポインタ変数conf
として格納します。
struct mpconf *conf;
conf = (struct mpconf*) P2V((uint) mp->physaddr);
if(memcmp(conf, "PCMP", 4) != 0) return 0;
if(conf->version != 1 && conf->version != 4) return 0;
if(sum((uchar*)conf, conf->length) != 0) return 0;
MPコンフィグレーションテーブルは以下の構造になっています。
struct mpconf { // configuration table header
uchar signature[4]; // "PCMP"
ushort length; // total table length
uchar version; // [14]
uchar checksum; // all bytes must add up to 0
uchar product[20]; // product id
uint *oemtable; // OEM table pointer
ushort oemlength; // OEM table length
ushort entry; // entry count
uint *lapicaddr; // address of local APIC
ushort xlength; // extended table length
uchar xchecksum; // extended table checksum
uchar reserved;
};
以下はIntel仕様書から引用した構造図です。
参考画像:Intel MultiProcessor Specification | ManualsLib
最初の4バイト分の領域にSIGNATURE
が格納されていますが、これはPCMP
になることが期待されます。
xv6OSでは、取得したMPコンフィグレーションテーブルの確認のため、memcmp
関数にて先頭4バイトがPCMP
に一致するかをチェックしています。
if(memcmp(conf, "PCMP", 4) != 0) return 0;
if(conf->version != 1 && conf->version != 4) return 0;
if(sum((uchar*)conf, conf->length) != 0) return 0;
また、バージョン情報が適切であるか、データサイズが実際のサイズと一致するかについてもチェックを行っています。
これでmpinit
関数の以下の処理が終わり、MPフローティングポインタとMPコンフィグレーションテーブルを取得することができました。
if((conf = mpconfig(&mp)) == 0) panic("Expect to run on an SMP");
MPコンフィグレーションテーブルからIOAPICを取得する
つづいて、mpioapic
構造体のioapic
にMPコンフィグレーションテーブルのlapicaddr
から取得したアドレスを格納します。
int ismp;
struct mpioapic *ioapic;
ismp = 1;
lapic = (uint*)conf->lapicaddr;
mpioapic
構造体は以下の構造体です。
struct mpioapic { // I/O APIC table entry
uchar type; // entry type (2)
uchar apicno; // I/O APIC id
uchar version; // I/O APIC version
uchar flags; // I/O APIC flags
uint *addr; // I/O APIC address
};
Intel仕様書の図は以下です。
参考画像:Intel MultiProcessor Specification | ManualsLib
IOAPICは外部割込みを複数のCPUで分散するための機構です。
APICが外部割込みの機構であることは前述しましたが、APICにはローカルAPICとIOAPICの2種類があるようです。
xv6OSではローカルAPICについてはlapic.c
で、IOAPICについてはioapic.c
で実装されています。
ローカルAPICはCPUに内臓された割込みで、IOAPICはI/Oデバイスから受けとった割込みを、リダイレクションテーブルの情報を元にCPUに通知します。
IOAPICはIOAPICテーブルを持っており、x86CPUはmemory-mapped I/O
を通してこのテーブルのエントリを定義できます。
memory-mapped I/O
は簡単に言うとCPUとI/Oデバイス間で入出力を行う方法の一つで、物理アドレス空間にI/Oデバイスの入出力のための空間を用意し、CPUのメモリの読み書きの機能を利用して入出力を行う方法です。
PCIをI/Oデバイスに持つ一般的なシステムの場合、IOAPICはPCIの割込み信号の変化を検知し、リダイレクションテーブルの情報を元にCPUに割込みメッセージを発行します。
この情報はCPU内部のローカルAPICが受け取り、割込みハンドラを呼び出して割込み処理を行った後、EOI(End of Interrupt)コマンドをIOAPICに返すことで、IOAPICに対して割込みの完了を通知します。
詳しくは実際に割込み処理を実装するところまで進んだらやります。たぶん。
参考:82093AA I/O ADVANCED PROGRAMMABLE INTERRUPT CONTROLLER (IOAPIC)
APICの仕組みは、x86CPUとそのマザーボードのようなマルチプロセッサ環境での割込みを制御するための割込みコントローラが必要になったことで生まれました。
もともと実装されていたPICと呼ばれるシンプルな割込み機構では、マルチプロセッサ構成での割込み処理に対応できなかったようです。
xv6OSもマルチプロセッサ構成を前提としているので、PICからの割込みを無視してローカルAPICとIOAPICを使用した割込み処理を実装します。
参考:P45
xv6OSのコードではmpioapic
構造体のioapic
にMPコンフィグレーションテーブルのlapicaddr
から取得したアドレスを格納しています。
lapicaddr
に格納されているのはメモリマップドされたローカルAPICのアドレスです。
lapicaddr
を格納する変数lapic
はdefs.h
でグローバル変数として定義されています。
この関数の中では以降使用することはなく、lapic.c
で使用することになります。
extern volatile uint* lapic;
プロセッサの情報を取得する
lapicaddr
の取得が完了した後のループを見てみます。
conf
には、先ほど取得したMPコンフィグレーションテーブルが格納されています。
Intelの仕様書より、MPコンフィグレーションテーブルエントリは、MPコンフィグレーションテーブルヘッダ(MPコンフィグレーションテーブルの先頭アドレス)から、可変数で続いていくことがわかります。
MPコンフィグレーションテーブルエントリは、先ほど例示したProcessor Entries
の他に、Bus Entry
、I/O APIC Entry
、I/O Interrupt Entry
、Local Interrupt Entry
などがあります。(他にも拡張エントリが存在します)
これらはいずれも先頭1バイトにユニークなEntry Point
が定義されています。
- Processor Entry
- Bus Entry
- I/O APIC Entry
- I/O Interrupt Entry
- Local Interrupt Entry
参考画像:Intel MultiProcessor Specification | ManualsLib
xv6OSの以下のコードでも、MPコンフィグレーションヘッダの先頭から各エントリをチェックしていき、先頭のEntry Type
の値に応じて処理を分岐させています。
for(p=(uchar*)(conf+1), e=(uchar*)conf+conf->length; p<e; ){
switch(*p){
case MPPROC:
proc = (struct mpproc*)p;
if(ncpu < NCPU) {
cpus[ncpu].apicid = proc->apicid; // apicid may differ from ncpu
ncpu++;
}
p += sizeof(struct mpproc);
continue;
case MPIOAPIC:
ioapic = (struct mpioapic*)p;
ioapicid = ioapic->apicno;
p += sizeof(struct mpioapic);
continue;
case MPBUS:
case MPIOINTR:
case MPLINTR:
p += 8;
continue;
default:
ismp = 0;
break;
}
}
チェックしているエントリは以下のように定義されています。
// Table entry types
#define MPPROC 0x00 // One per processor
#define MPBUS 0x01 // One per bus
#define MPIOAPIC 0x02 // One per I/O APIC
#define MPIOINTR 0x03 // One per bus interrupt source
#define MPLINTR 0x04 // One per system interrupt source
Processor Entryの情報取得
まずはProcessor Entry
の場合です。
ここではmpproc
構造体のオブジェクトとしてProcessor Entriy
を取得し、Local APIC ID
の値をすべてのcpus
配列に順に格納しています。
case MPPROC:
proc = (struct mpproc*)p;
if(ncpu < NCPU) {
cpus[ncpu].apicid = proc->apicid; // apicid may differ from ncpu
ncpu++;
}
p += sizeof(struct mpproc);
continue;
ちなみにここで使用されているNCPU
はparam.h
で以下のように定数として定義されています。
xv6OSは最大で8CPUまでサポートしているようです。
#define NCPU 8 // maximum number of CPUs
IOAPICの情報取得
続いてはIOAPIC
の情報を取得します。
case MPIOAPIC:
ioapic = (struct mpioapic*)p;
ioapicid = ioapic->apicno;
p += sizeof(struct mpioapic);
continue;
各構造体などについては前述したので割愛します。
IMCRの変更
これでmp.c
の処理はほぼ完了しました。
最後にIMCR
を無効化します。
IMCRは割り込みモード構成レジスタと呼ばれ、PICモードから変更するためにはIMCR
の変更が必要になるようです。
参考:x86 - Where is the IMCR defined in the docs? - Reverse Engineering Stack Exchange
参考:OSDev.org • View topic - Set IMCR to 0x1 to mask external interrupts?
まとめ
次回はlapicinit
関数から始めます。
今回取得した情報を利用して割込みコントローラを実装するようです。
だんだん頭がついてこなくなってきたのでちょっと気合入れて頑張ろうと思います。