ExceptionとInterruptの挙動
linux2.6.11の場合のExceptionとInterruptの挙動をまとめようと思って、xv6との比較を書いていたが分量が多くなりすぎたので、簡単にxv6ではException、Interruptをどう処理しているのかをまとめる。
Reference
http://www.cse.iitm.ac.in/~chester/courses/16o_os/slides/6_Interrupts.pdf xv6向けのinterruptの解説で、かなりわかりやすい。
Intel® 64 and IA-32 Architectures Software Developer’s Manual(SDM) vol.3 (Order Number: 325384-067US May 2018) (公式ドキュメント) Figure, Sectionはこの本からの引用とします
https://github.com/mit-pdos/xv6-public/tree/xv6-rev11 xv6のソースコード
Understanding the Linux Kernel ch.4 (Interrupts and Exceptions) 本記事ではバージョンの断りが特に無ければ、linux2.6.11についての言及とする。
Glosary
ExceptionやInterruptに関係する用語としては以下の通り(ややこしい..):
- Exception: ゼロ除算やPage Faultなどのvector0~31までに割り当てられている。c.f) Table 6-1または、ch.6.15
Interrupt: timerやdiskなど外部I/O deviceが起こす。vector32~255まで割り当てることができる。
int
命令(software-generated interrupt)もinterruptに分類される。Process switch .. process間の切り替え(linux2.6では
switch_to
で、xv6ではswtch
でやったこと)Interrupt switch .. Interruptが生じたときの切り替え(linux2.6の場合、同じプロセス内で完結するが、Interruptの処理のstackが異なる。Interruptの処理中にInterruptが重なるのも許す。)
IRQ(Interrupt ReQuests):
- IRQ line: Hardware deviceがinterruptを発したいときのsignalを発する線(電話線みたいな). PIC(I/O APIC)につながっている
- interrupt vector(IV): interruptやexceptionを識別するための番号1。xv6やlinuxではdefaultで
IRQ+0x20
(#define FIRST_EXTERNAL_VECTOR 0x20)。0~31まではreserved(systemのinterrupt)のためなので。 - IRQ Descriptor: linuxでは
struct irq_desc_r
で定義されている。interruptが発生した時ISR(Interrupt Service Routine)が呼び出される。xv6にはISR(Interrupt Service Routine)という概念はないので、IRQ Descriptorは存在しない。 - PIC(Programmable Interrupt Controller): 外部のdeviceから来たInterrupt達をCPUに伝達するためのcontroller。"Programmable"なので、register(memory上のaddressに配置されてる場合2もある。I/O APICやlocal APICはこれに当てはまる)にRead or Write(もしくはRead/Write両方)することでdeviceの設定を変更できる
- 8259A PIC: [legacy] legacy CPUに搭載されていたInterrupt Controller。8つのinterruptまで受容できる。(slaveとmasterの2つを使い、15つまで対応) c.f) https://pdos.csail.mit.edu/6.828/2018/readings/hardware/8259A.pdf && pp.14 in http://www.cse.iitm.ac.in/~chester/courses/16o_os/slides/6_Interrupts.pdf
- I/O APIC(Advanced PIC): 8259A PICの後進で、multiprocessorに対応するためのAPIC。こちらはLocal APICと違い、CPUの搭載数に依存しない。(random|特定)のCPUに指定のinterruptの処理をお願いしたりできる。
- local APIC: I/O APICとセットで使われる。各CPUに存在するAPIC(なので"local")で、HardWare Deviceが発したinterruptがI/O APICを介してlocal APICに振り分けられる。LAPICとも言ったりする。 c.f.) Table10-1にregister一覧がある
- LVT(Local Vector Table): local APICにてvector number(32~)とLVTのindexを対応させる必要がある(32 ~ 255はuser-definedのinterruptなので)。その対応表。 c.f) Ch10.5.1 & Figure 10-8
- TPR(task priority register): arbitration priorityが格納されているregister.
- ICR(Interrupt Command Register): local APICにあるregister。IPI(InterProcessor Interrupt)を別のCPUに伝達する時の設定諸々を格納するregister c.f.) Figure10.12
- EOI(End Of Interrupt) Register: Interruptの終わりを知らせるレジスタ。
- LINT0, LINT1: local APIC interruptに予約されてるIRQ line.
- Local APICには他にもregister達があるようだ... c.f.) Table10-1
SMP(Symmetric MultiProcessing): 同等な2つ以上ののCPUが一つのmemoryとつながっている構造(CPU達がmaster-slaveの関係になっていない) 外部のI/O deviceから来たInterruptを各CPUに均等に振り分けたりできる。
NMI(NonMaskable Interrupt) .. disableにできないInterrupt. c.f) Interrupt 2—NMI Interrupt in ch. 6.15
IDT(Interrupt Descriptor Table) .. Gate Descriptorというentryが格納された配列。Interruptの番号が表のindexに対応してdispatchされる。
IDTR
命令でIDTのbase addressを登録。- gate descriptor: IDTのentryでOffset([63:48], [15:0])にcallbackのaddressを格納している
- Interrupt gate (DPL:0): used for all interrupt handlers
- System Interrupt gate .. same as Interrupt gate except DPL=3 (
int3
instruction)
- System Interrupt gate .. same as Interrupt gate except DPL=3 (
- Trap gate (DPL:0) .. used for most Linux exception handlers
- System gate: same as Trap gate except DPL=3. It's used for
int $0x80
instruction
- System gate: same as Trap gate except DPL=3. It's used for
- Task gate (DPL:0) .. only used for "Double Fault Exception"
- Interrupt gate (DPL:0): used for all interrupt handlers
- gate descriptor: IDTのentryでOffset([63:48], [15:0])にcallbackのaddressを格納している
CPL(Current Priviledge Level): %cs(code segment register)のRPL([1:0]) c.f) Figure3.6
DPL(Descriptor Priviledge Level): (Segment|Gate) DescriptorのDPL([46:45])
kernel control path: the sequence of instructions executed by the kernel to handle a system call, an exception, or an interrupt.
用語が山のようにあるが、次の節以降で断りなく各種用語を使っていくので、適宜参照してください。
Note) 最初LVTをIDTのEntryのOffset([63:48], [15:0])(callback)の表かと勘違いしていたが、Local APICにあるregisterであって、IDTとは関係がない。LVTについては、Intel SDMのTable 10.1を見れば雰囲気がよく分かると思う。
ExceptionとInterruptの挙動
xv6のExceptionやInterruptの処理を追っていく。
初期化
InterruptやExceptionが起こる時のために、IDTのentryにcallbackが呼ばれるように設定できる。加えて、Interruptの場合は、IRQからvector numberの変換(といってもIRQ+32
という単純なもの)並びに振り分けるcpuの設定をI/O APICのRedirection Tableに書き込む。
- local APICの初期化と設定 (main 関数のlapicinitで。timer(IRQ_TIMER), LINT0, LINT1, End Of Interrupt(EOI) register, Error Status Register(ESR), Task Priority Register(TPR)の設定 etc..)
I/O APICの初期化(main関数のioapicinitで
ioapicwrite(REG_TABLE+2*i, INT_DISABLED | (T_IRQ0 + i)); ioapicwrite(REG_TABLE+2*i+1, 0);
)exception, interruptの設定
- main() 関数の tvinitでIDTの初期化とsyscall(0x40)を呼び出すためのTrap-gate descriptorの設定(
SETGATE(idt[T_SYSCALL], 1, SEG_KCODE<<3, vectors[T_SYSCALL], DPL_USER);
- main() 関数の tvinitでIDTの初期化とsyscall(0x40)を呼び出すためのTrap-gate descriptorの設定(
- Interruptの設定(I/O APICの Interrupt Redirection Tableに設定を書き込む)
- idtinitでIDTのロード(
lidt
)
と設定。xv6の場合、Interruptの処理を任せるCPUは決め打ちにしている。linuxでは、もうちょっと洗練されて設定されてる。
Exception発生直後のhardware(x86)の対応
雑に言うと、Exception, Interruptが発生した時、権限の確認をした上で、各種レジスタをpushする。その後、Exception or Interruptの番号(vector number)から、IDTからそれに対応するentryがわかるので、callbackのアドレスに飛んで実行する。
この流れを詳しめにみていく。 User spaceでException, Interruptが発生することに注意する(linuxではinterruptについては入れ子を許すのでもうちょっと複雑) また、eflag.IFがONである必要がある。
- Exceptionが発生した(例えば、Divide Errorは#0) 以下の2~8はhardwareが自動的に行う処理
- IDTRからIDTの場所がわかるので、IDTのindexの
idx
を参照できる。(Base address(callback関数のアドレス)やselectorの場所がわかるよ) - check (CPL=%csのRPL)<=(%csのindex[15:3]の場所のSegment DescriptorのDPL) && (CPL=%csのRPL)<=(IDT[idx](Gate Descriptor)のDPL)をチェック
- 3で、もし、(CPL=%csのRPL)!=(%csのSegment DescriptorのDPL) ならば、(%csのSegment DescriptorのDPL) の権限に対応するssとespをロード。TR=>TSS DescriptorのBase Address([63:56], [39:32], [31:16]) => TSSと辿れる。xv6の場合、4は原則当てはまる。右辺=0なので、%ss0(stack segment register)と%esp0となる。
- 古いssとespを新しいスタックにpushしておく
- eflags, cs, eipをスタックにpush, hardware error codeもあったらそれもスタックにpush
- eflagsのIFをOFFに。c.f) ch.6.12.1.
- IDT[
idx
]にSegment Selector[31:16]とOffset([63:48], [15:0])があるので、それぞれ%csと%eipにloadする。 - (%eipはinstruction pointerなので、そのアドレスにある命令を順次実行していく)
xv6では、vector numberがiのときはIDTのOffset(callbackのbase address)がvector_##i
となっているので、そちらの関数に飛んでソフトウェア的な(OS側の)処理が開始される。
Interrupt発生直後のhardware(x86)の対応
こちらは外部I/Oがinterruptを発生させるので、最初の部分がプログラム内で発生するExceptionよりもやや複雑。
- HardwareがInterruptを発する(Diskが読み終わったとか、Timerが0になったとか)
- I/O APICがIRQ number(IRQ lineの状態)を受け取る。
- I/O APICはIRQの番号をvectorの番号に変換して、対象のCPUのlocal APICにsignalを伝達する。(Interrupt Redirection Tableに設定したのだった)
- 動的に振り分ける場合、各CPUにarbitration priorityが振り分けられる(task priority register(TRR)に格納されてる) 今回は振り分けるCPUは決め打ちなので、skip
以下、Exceptionの場合と同じ。要するに、外部I/O deviceがInterruptを発生させた時、 I/O APICがどのCPUにInterruptを処理させるのかを決め、該当のCPUにお願いをするところが最初に挟まる。もし、自動で振り分ける場合は、SMPなので、いい感じにInterruptの配分をhardware側で行ってくれる3
Exception発生時
0~31までのexception及び、int $T_SYSCALL
のsoftware generated interruptが起こったときが該当。
int $T_SYSCALL
が起こった場合は http://cstmize.hatenablog.jp/entry/2019/03/14/GDT%E3%81%A8IDT%E5%91%A8%E8%BE%BA%E3%81%AE%E7%90%86%E8%A7%A3%28xv6%E3%82%92%E4%BE%8B%E3%81%AB%29#switchuvm の"forkretからinitまで"のところでだいたい確認している。ポイントとしては、trap()
関数がException, Interruptを振り分けているところ。
// trap.c void trap(struct trapframe *tf) { // if `int $T_SYSCALL` occurs.. if(tf->trapno == T_SYSCALL){ if(myproc()->killed) exit(); myproc()->tf = tf; syscall(); if(myproc()->killed) exit(); return; } switch(tf->trapno){ // skip default: // If Interrupt or Exception is occured at DPL_KERNEL, something wrong happens.. if(myproc() == 0 || (tf->cs&3) == 0){ // skip panic("trap"); } // skip myproc()->killed = 1; } // if 0~31 exception occurs.. if(myproc() && myproc()->killed && (tf->cs&3) == DPL_USER) exit();
Interrupt発生時のOS側の対応
vector_##i
が呼ばれ、trap()
関数で、IRQ_TIMERの場合、IRQ_IDEの場合..とswitch文により振り分けられ、個別のinterruptの処理が走る。これが終わったら、I/O deviceによるinterruptの場合はlapicw(EOI, 0);
でEOI registerにInterruptの終わりをLocal APICに伝えて、Interruptが終わる。
コード的には、
// trap.c trap(struct trapframe *tf) { // skip // trapno means vector number. switch(tf->trapno){ case T_IRQ0 + IRQ_KBD: kbdintr(); lapiceoi(); break; // skip
linux2.6では、Interrupt発生時にvectorに紐付いているcallbackが呼ばれるところまでは同じだが、その後、ISR(Interrupt Service Routine)を呼び出す。この呼び出し方複雑なので、次あたりにまとめたい。
trap関数終了後のOS側の対応
trap関数が終了した後は、trapasm.Sにて、stackをpopして、最後にiret
命令を実行する。iret
命令はinterrupt or exceptionでhardwareが自動的にやったことの逆を行う。
詳細は、以前 https://qiita.com/knknkn1162/items/0bc9afc3ae304590e16c#iret%E3%81%A8ret%E3%81%AF%E3%81%A9%E3%81%86%E9%81%95%E3%81%86 でまとめた。または、Understanding the linux kernelのHardware Handling of Interrupts and Exceptions
で詳しめの解説がある。
ポイントとしては、interrupt発生した後は、kernel landで作業していたものが、iret
により、user landに引き戻されるということだ。勿論、%eipやら%ssやら%espは前の状態に戻る。
ちなみに、linux2.6では、ret_from_intr
および、ret_from_exception
が同じ役割を担っている。
-
pp.134に、Each interrupt or exception is identified by a number ranging from 0 to 255; Intel calls this 8-bit unsigned number a vectorとある。↩
-
memory-mapped I/Oという。https://qiita.com/knknkn1162/items/cb06f19e1f999bf098a1#io-port%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6 を参照のこと。↩
-
ただしhardwareによっては例外はあるらしく、linux2.6では、
kirqd
(kernel thread)で配分のバランスを適宜取るようだ。↩