技術

[xv6 #28] Chapter 2 – Traps, interrupts, and drivers – Code: Interrupts

テキストの37〜39ページ

本文

マザーボード上のデバイスは、割り込みを生成することが出来、xv6はそれらの割り込みを制御するためにハードウェアをセットアップしなければならない。
デバイスのサポート無しでは、xv6は使い物にならないだろう。
ユーザはキーボードでタイプ出来ないし、ファイルシステムはディスクにデータを保管することが出来ないし、他にもたくさんの問題が起きる。
幸いにも、割り込みを追加したり、単純なデバイスをサポートする事は、難しくない。
今まで見てきたように、割り込みは、システムコールと例外のためのコードと同じもの流用することが出来る。

割り込みは、いつでもデバイスに生成され得るという事を除いて、システムコールに似ている。
マザーボード上には、デバイスに注意が必要なとき(例えばユーザがキーボードの文字をタイプしたとき)にCPUへ信号を送るハードウェアがある。
我々は、デバイスに割り込みを生成させ、CPUが受け取る割り込みを整備するためにプログラムしなければならない。

タイマーデバイスとタイマー割り込みを見てみよう。
我々は、カーネルが時間の経過を認識出来るようにするため、カーネルが複数のプロセスの間で時分割出来るようにするため、タイマーハードウェアに1秒間に100回の割り込みを生成して欲しい。
1秒間に100回という選択は、プロセッサが割り込みの制御に忙殺されてない間、適正な対話パフォーマンスを可能にする。

x86プロセッサそれ自身のように、PCのマザーボードは進化してきた。
そして、提供される割り込みの方法も同じく進化してきた。
初期のボードは、単純なプログラム可能な割り込みコントローラ(PICと呼ばれる)を持っていた。
それを管理するためのコードをpicirq.cで見つけることが出来るだろう。

picirq.c(一応載せときます)

// Intel 8259A programmable interrupt controllers.

#include "types.h"
#include "x86.h"
#include "traps.h"

// I/O Addresses of the two programmable interrupt controllers
#define IO_PIC1         0x20    // Master (IRQs 0-7)
#define IO_PIC2         0xA0    // Slave (IRQs 8-15)

#define IRQ_SLAVE       2       // IRQ at which slave connects to master

// Current IRQ mask.
// Initial IRQ mask has interrupt 2 enabled (for slave 8259A).
static ushort irqmask = 0xFFFF & ~(1<<IRQ_SLAVE);

static void
picsetmask(ushort mask)
{
  irqmask = mask;
  outb(IO_PIC1+1, mask);
  outb(IO_PIC2+1, mask >> 8);
}

void
picenable(int irq)
{
  picsetmask(irqmask & ~(1<<irq));
}

// Initialize the 8259A interrupt controllers.
void
picinit(void)
{
  // mask all interrupts
  outb(IO_PIC1+1, 0xFF);
  outb(IO_PIC2+1, 0xFF);

  // Set up master (8259A-1)

  // ICW1:  0001g0hi
  //    g:  0 = edge triggering, 1 = level triggering
  //    h:  0 = cascaded PICs, 1 = master only
  //    i:  0 = no ICW4, 1 = ICW4 required
  outb(IO_PIC1, 0x11);

  // ICW2:  Vector offset
  outb(IO_PIC1+1, T_IRQ0);

  // ICW3:  (master PIC) bit mask of IR lines connected to slaves
  //        (slave PIC) 3-bit # of slave's connection to master
  outb(IO_PIC1+1, 1<<IRQ_SLAVE);

  // ICW4:  000nbmap
  //    n:  1 = special fully nested mode
  //    b:  1 = buffered mode
  //    m:  0 = slave PIC, 1 = master PIC
  //      (ignored when b is 0, as the master/slave role
  //      can be hardwired).
  //    a:  1 = Automatic EOI mode
  //    p:  0 = MCS-80/85 mode, 1 = intel x86 mode
  outb(IO_PIC1+1, 0x3);

  // Set up slave (8259A-2)
  outb(IO_PIC2, 0x11);                  // ICW1
  outb(IO_PIC2+1, T_IRQ0 + 8);      // ICW2
  outb(IO_PIC2+1, IRQ_SLAVE);           // ICW3
  // NB Automatic EOI mode doesn't tend to work on the slave.
  // Linux source code says it's "to be investigated".
  outb(IO_PIC2+1, 0x3);                 // ICW4

  // OCW3:  0ef01prs
  //   ef:  0x = NOP, 10 = clear specific mask, 11 = set specific mask
  //    p:  0 = no polling, 1 = polling mode
  //   rs:  0x = NOP, 10 = read IRR, 11 = read ISR
  outb(IO_PIC1, 0x68);             // clear specific mask
  outb(IO_PIC1, 0x0a);             // read IRR by default

  outb(IO_PIC2, 0x68);             // OCW3
  outb(IO_PIC2, 0x0a);             // OCW3

  if(irqmask != 0xFFFF)
    picsetmask(irqmask);
}&#91;/sourcecode&#93;

マルチプロセッサPCボードの登場によって、新しい割り込みの制御方法が必要とされた。
なぜなら、どのCPUも割り込みを制御するための割り込みコントローラを必要とし、プロセッサに対するルーチン割り込みの為の手法が存在すべきだからである。
この方法は、2つの部分から成る。
1つは、I/Oシステム部分(IO APIC, ioapic.c)、もう1つは、それぞれのプロセッサにアタッチする部分(ローカルAPIC, lapic.c)である。
xv6は、マルチプロセッサ用のボードのためにデザインされ、そしてどのプロセッサも割り込みを受け取るためにプログラムされる。

ioapic.c
&#91;sourcecode language="c"&#93;// The I/O APIC manages hardware interrupts for an SMP system.
// http://www.intel.com/design/chipsets/datashts/29056601.pdf
// See also picirq.c.

#include "types.h"
#include "defs.h"
#include "traps.h"

#define IOAPIC  0xFEC00000   // Default physical address of IO APIC

#define REG_ID     0x00  // Register index: ID
#define REG_VER    0x01  // Register index: version
#define REG_TABLE  0x10  // Redirection table base

// The redirection table starts at REG_TABLE and uses
// two registers to configure each interrupt.  
// The first (low) register in a pair contains configuration bits.
// The second (high) register contains a bitmask telling which
// CPUs can serve that interrupt.
#define INT_DISABLED   0x00010000  // Interrupt disabled
#define INT_LEVEL      0x00008000  // Level-triggered (vs edge-)
#define INT_ACTIVELOW  0x00002000  // Active low (vs high)
#define INT_LOGICAL    0x00000800  // Destination is CPU id (vs APIC ID)

volatile struct ioapic *ioapic;

// IO APIC MMIO structure: write reg, then read or write data.
struct ioapic {
  uint reg;
  uint pad&#91;3&#93;;
  uint data;
};

static uint
ioapicread(int reg)
{
  ioapic->reg = reg;
  return ioapic->data;
}

static void
ioapicwrite(int reg, uint data)
{
  ioapic->reg = reg;
  ioapic->data = data;
}

void
ioapicinit(void)
{
  int i, id, maxintr;

  if(!ismp)
    return;

  ioapic = (volatile struct ioapic*)IOAPIC;
  maxintr = (ioapicread(REG_VER) >> 16) & 0xFF;
  id = ioapicread(REG_ID) >> 24;
  if(id != ioapicid)
    cprintf("ioapicinit: id isn't equal to ioapicid; not a MP\n");

  // Mark all interrupts edge-triggered, active high, disabled,
  // and not routed to any CPUs.
  for(i = 0; i <= maxintr; i++){
    ioapicwrite(REG_TABLE+2*i, INT_DISABLED | (T_IRQ0 + i));
    ioapicwrite(REG_TABLE+2*i+1, 0);
  }
}

void
ioapicenable(int irq, int cpunum)
{
  if(!ismp)
    return;

  // Mark interrupt edge-triggered, active high,
  // enabled, and routed to the given cpunum,
  // which happens to be that cpu's APIC ID.
  ioapicwrite(REG_TABLE+2*irq, T_IRQ0 + irq);
  ioapicwrite(REG_TABLE+2*irq+1, cpunum << 24);
}&#91;/sourcecode&#93;

lapic.c
&#91;sourcecode language="c"&#93;// The local APIC manages internal (non-I/O) interrupts.
// See Chapter 8 & Appendix C of Intel processor manual volume 3.

#include "types.h"
#include "defs.h"
#include "memlayout.h"
#include "traps.h"
#include "mmu.h"
#include "x86.h"

// Local APIC registers, divided by 4 for use as uint&#91;&#93; 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 &#91;63:32&#93;
#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

volatile uint *lapic;  // Initialized in mp.c

static void
lapicw(int index, int value)
{
  lapic&#91;index&#93; = value;
  lapic&#91;ID&#93;;  // wait for write to finish, by reading
}
//PAGEBREAK!

void
lapicinit(int c)
{
  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&#91;TICR&#93; 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&#91;VER&#93;>>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);
}

int
cpunum(void)
{
  // Cannot call cpu when interrupts are enabled:
  // result not guaranteed to last long enough to be used!
  // Would prefer to panic but even printing is chancy here:
  // almost everything, including cprintf and panic, calls cpu,
  // often indirectly through acquire and release.
  if(readeflags()&FL_IF){
    static int n;
    if(n++ == 0)
      cprintf("cpu called from %x with interrupts enabled\n",
        __builtin_return_address(0));
  }

  if(lapic)
    return lapic[ID]>>24;
  return 0;
}

// Acknowledge interrupt.
void
lapiceoi(void)
{
  if(lapic)
    lapicw(EOI, 0);
}

// Spin for a given number of microseconds.
// On real hardware would want to tune this dynamically.
void
microdelay(int us)
{
}

#define IO_RTC  0x70

// Start additional processor running entry code at addr.
// See Appendix B of MultiProcessor Specification.
void
lapicstartap(uchar apicid, uint addr)
{
  int i;
  ushort *wrv;
  
  // "The BSP must initialize CMOS shutdown code to 0AH
  // and the warm reset vector (DWORD based at 40:67) to point at
  // the AP startup code prior to the [universal startup algorithm]."
  outb(IO_RTC, 0xF);  // offset 0xF is shutdown code
  outb(IO_RTC+1, 0x0A);
  wrv = (ushort*)P2V((0x40<<4 | 0x67));  // Warm reset vector
  wrv&#91;0&#93; = 0;
  wrv&#91;1&#93; = addr >> 4;

  // "Universal startup algorithm."
  // Send INIT (level-triggered) interrupt to reset other CPU.
  lapicw(ICRHI, apicid<<24);
  lapicw(ICRLO, INIT | LEVEL | ASSERT);
  microdelay(200);
  lapicw(ICRLO, INIT | LEVEL);
  microdelay(100);    // should be 10ms, but too slow in Bochs!
  
  // Send startup IPI (twice!) to enter code.
  // Regular hardware is supposed to only accept a STARTUP
  // when it is in the halted state due to an INIT.  So the second
  // should be ignored, but it is part of the official Intel algorithm.
  // Bochs complains about the second one.  Too bad for Bochs.
  for(i = 0; i < 2; i++){
    lapicw(ICRHI, apicid<<24);
    lapicw(ICRLO, STARTUP | (addr>>12));
    microdelay(200);
  }
}

ユニプロセッサ上でもちゃんと動くようにするために、xv6は、プログラム可能な割り込みコントローラ(PIC)をプログラムする。
(picirq.cのpicinit関数)
どのPICも最大で8個の割り込み(例えば複数のデバイス)を制御出来、そしてそれらをプロセッサの割り込みピンへ多重送信する。
8個以上のデバイスに対応するため、複数のPICはカスケード接続され、典型的なボードなら最低でも2つのPICを持つ。
inb命令とoutb命令を使うことで、xv6は、マスタにIRQ 0からIRQ 7を生成させるため、スレーブにIRQ 8からIRQ 15(原文では16になってる)を生成させるために、それらをプログラムする。
最初に、xv6はPICに全ての割り込みをマスクさせるためにプログラムする。
timer.cのコードで、タイマーを1にセットしPIC上のタイマー割り込みを有効にしている。
(timer.cのtimerinit関数)
今は、PICのプログラミングに関して詳細をいくつか省いている。
それらの詳細(PIC、IOAPIC、LAPIC)は、このテキストでは重要ではないが、興味を持った読者はそれらのソースファイルで参照されているそれぞれのデバイスのマニュアルを調べる事ができる。

timer.c

// Intel 8253/8254/82C54 Programmable Interval Timer (PIT).
// Only used on uniprocessors;
// SMP machines use the local APIC timer.

#include "types.h"
#include "defs.h"
#include "traps.h"
#include "x86.h"

#define IO_TIMER1       0x040           // 8253 Timer #1

// Frequency of all three count-down timers;
// (TIMER_FREQ/freq) is the appropriate count
// to generate a frequency of freq Hz.

#define TIMER_FREQ      1193182
#define TIMER_DIV(x)    ((TIMER_FREQ+(x)/2)/(x))

#define TIMER_MODE      (IO_TIMER1 + 3) // timer mode port
#define TIMER_SEL0      0x00    // select counter 0
#define TIMER_RATEGEN   0x04    // mode 2, rate generator
#define TIMER_16BIT     0x30    // r/w counter 16 bits, LSB first

void
timerinit(void)
{
  // Interrupt 100 times/sec.
  outb(TIMER_MODE, TIMER_SEL0 | TIMER_RATEGEN | TIMER_16BIT);
  outb(IO_TIMER1, TIMER_DIV(100) % 256);
  outb(IO_TIMER1, TIMER_DIV(100) / 256);
  picenable(IRQ_TIMER);
}

マルチプロセッサの場合、xv6は、それぞれのプロセッサ上でIOAPICとLAPICをプログラムしなければならない。
IO APICはテーブルを持ち、プロセッサはinb命令やoutb命令を使う代わりに、メモリマップドI/Oを通してそのテーブルのエントリをプログラムすることが出来る。
初期化中、xv6は、割り込み0をIRQ 0に対応付けるようプログラムする。(1以降も同様に)
しかし、それら全てを無効化する。
特定のデバイスは、特有の割り込みを有効にし、その割り込みを担当すべきであるとプロセッサに伝える。
例えば、xv6はキーボード割り込みをプロセッサ0に送る。
(console.cのconsoleinit関数)
後で見るが、xv6はディスク割り込みをそのシステムで一番番号が大きいプロセッサに割り当てる。

console.cのconsoleinit関数

void
consoleinit(void)
{
  initlock(&cons.lock, "console");
  initlock(&input.lock, "input");

  devsw[CONSOLE].write = consolewrite;
  devsw[CONSOLE].read = consoleread;
  cons.locking = 1;

  picenable(IRQ_KBD);
  ioapicenable(IRQ_KBD, 0);
}

タイマーチップはLAPICの中にあり、そのおかげでどのプロセッサも個別にタイマー割り込みを受け取ることが出来る。
xv6は、それをlapic.cのlapicinit関数でセットアップしている。
重要な行は、タイマーをプログラムしてる部分である。
(lapicw(TIMER, PERIODIC | (T_IRQ0 + IRQ_TIMER));の部分)
この行は、LAPICにIRQ_TIMERで割り込みを生成させる事を伝えている。
(traps.hに#define IRQ_TIMER 0とありIRQ 0にあたる)
lapicinitの最後の行(lapicw(TPR, 0);)は、ローカルプロセッサに割り込みを運ぶためにCPUのLAPIC上の割り込みを有効にする。

プロセッサは、eflagレジスタの中のIFフラグを通して、割り込みを受け取りたいかどうかを制御することが出来る。
cli命令は、IFをクリアする事によってそのプロセッサへの割り込みを無効化し、sti命令はそのプロセッサへの割り込みを有効化する。
xv6は、起動中は、メインCPUと他のプロセッサに対する割り込みをcli命令を使って無効化する。
(スペースの都合とその内容は今は重要じゃないのでソースは載せませんがそれぞれ、bootasm.S、entryother.Sでcli命令が使われています。)
それぞれのプロセッサのスケジューラが、割り込みを有効にする。
(こちらもソースは載せませんが、proc.cのscheduler関数でsti命令が使われています。)
特定のコードの断片を割り込みされないよう制御するため、xv6はそれらのコードの断片の間、割り込みを無効化する。
(例えば、switchuvmを見よ)
(pushcli関数、popcli関数の中でそれぞれcli命令、sti命令が呼ばれます。)

vm.cのswitchuvm関数

// Switch TSS and h/w page table to correspond to process p.
void
switchuvm(struct proc *p)
{
  pushcli();
  cpu->gdt[SEG_TSS] = SEG16(STS_T32A, &cpu->ts, sizeof(cpu->ts)-1, 0);
  cpu->gdt[SEG_TSS].s = 0;
  cpu->ts.ss0 = SEG_KDATA << 3;
  cpu->ts.esp0 = (uint)proc->kstack + KSTACKSIZE;
  ltr(SEG_TSS << 3);
  if(p->pgdir == 0)
    panic("switchuvm: no pgdir");
  lcr3(v2p(p->pgdir));  // switch to new address space
  popcli();
}

タイマー割り込みは、ベクタ32(xv6がIRQ 0を制御するために選んだ)を通り、xv6はidtinit関数の中でセットアプする。
(idtinit関数は最終的にlidt命令を実行する)
ベクタ32とベクタ64(システムコール用)の違いは、ベクタ32はトラップゲートではなく割り込みゲートであるという事だけである。
割り込みを受けたプロセッサが、現在の割り込みを制御している間に割り込みを受けないようにするために、割り込みゲートはIFフラグをクリアする。
ここからtrap関数に至るまで、割り込みは、システムコールや例外や、トラップフレームの構築と同じコードパスをたどる。

main.cのmpmain関数(idtinit関数を呼び出す部分)

// Common CPU setup code.
static void
mpmain(void)
{
  cprintf("cpu%d: starting\n", cpu->id);
  idtinit();       // load idt register
  xchg(&cpu->started, 1); // tell startothers() we're up
  scheduler();     // start running processes
}

タイマー割り込みで呼ばれたとき、trap関数は、2つの事だけを行う。
変数ticksのインクリメント。
wakeup関数の呼び出し。
第4章で見るが、後者は違うプロセスへ戻る割り込みを引き起こす。

trap.cのtrap関数のタイマー関連の処理の部分

  switch(tf->trapno){
  case T_IRQ0 + IRQ_TIMER:
    if(cpu->id == 0){
      acquire(&tickslock);
      ticks++;
      wakeup(&ticks);
      release(&tickslock);
    }
    lapiceoi();
    break;

感想

タイマー割り込みを例にした割り込みの説明といったところですかね。
タイマー割り込みもかなり重要なので、例というのはちょっと言い過ぎですね。

ベクタというのは、前々々回Code: Assembly trap handlersの節の最初に出てきた、xv6における割り込みの区別の為の概念ですね。
実際はただの関数の配列のようなものでした。

個別の節の理解度は少しづつ上がってきてる気がします。
それぞれの関連や全体の中でどこに位置するのかがピンと来るようになればもっと理解が深まるんじゃないかと思います。

コメントを残す

メールアドレスが公開されることはありません。



※画像をクリックして別の画像を表示