ウォンツテック

そでやまのーと

OS作成

PIT(8254チップ)でどうやってタイマとスケジュール割り込みを共有しているのか謎だったけどコード見て謎が解けた。以下Linux-0.12の該当コード。(このバージョンはいい教材です。僕はLinux 2.6.xxを読んでいて理解不能でつらいときはminixと共にこのバージョンのLinuxを結構読みます。Linux 0.0.1はさすがに足り無さすぎなので読みませんが楽しいコードです)

sched.c

void sched_init(void)
{
(snip)
    outb_p(0x36,0x43);      /* binary, mode 3, LSB/MSB, ch 0 */
    outb_p(LATCH & 0xff , 0x40);    /* LSB */
    outb(LATCH >> 8 , 0x40);    /* MSB */
    set_intr_gate(0x20,&timer_interrupt);
    outb(inb_p(0x21)&~0x01,0x21);
    set_system_gate(0x80,&system_call);
}

outb_pで8254の初期化を行ってます。第2引数がI/Oポートアドレスで0x43はコントロール・ワードレジスタ、0x40はカウンタ0レジスタでシステムタイマ用です。
コントロール・ワードレジスタに対して0x36を設定してますがこれは以下のような意味があります。

  • bit7〜6
    • カウンタの指定で 10:カウンタ2 01:カウンタ1 00:カウンタ0 なのでカウンタ0を指定
  • bit5〜4(bit7,6で11以外を指定した場合)
    • カウンタ・リード/ロードの指定で11は16bitのカウンタ値をリードorロードする。(カウンタのI/Oは8bitなので下位8bit、上位8bitの順に行う)
  • bit3〜1
    • カウント・モード 011はモード3の方形波レート・ジェネレータ(カウンタ値が2ずつ減るモード)
  • bit0
    • カウント・モード 0はバイナリカウント

この0x43に対する設定の後下位、上位の順番にカウンタ値を設定しています。カウンタ値として設定している値は 「LATCH」でありこれは以下のように定義されてます。

sched.c

#define LATCH (1193180/HZ)

HZは100です。ちなみにカウンタレジスタのカウンタ値は0にるとカウント停止(再カウント開始)と考えられ、すなわちそれが1周期となるので、カウンタ値をNとしチップの周波数をfとするとf/Nがこのカウント操作により生成される周波数となります。ここではLATCHをカウンタ値として設定しているので、設定される周波数は

 f / LATCH = f / (1193180/HZ)

となりますが、fは8254のクロック周波数で固定(8254は外部回路でクロックをチップ内部で生成)でその値は1193180となっているので結局得られる周波数はHZ(100)となります。これがLinux-0.12におけるデフォルトのスケジューリングの周波数(秒間100回タイマ割り込みが発生する)です。

そして set_intr_gate(0x20,&timer_interrupt); で割り込み時に呼び出す関数を指定してます。
timer_interruptはsys_call.sで定義されていて

sys_call.s

_timer_interrupt:
(snip)
    call _do_timer      # 'do_timer(long CPL)' does everything from
    addl $4,%esp        # task switching to accounting ...
    jmp ret_from_sys_call

でdo_timerを呼び出してます。これはまたsched.cで定義されており

sched.c

void do_timer(long cpl)
{
(snip)
    if (cpl)
        current->utime++;
    else
        current->stime++;
    if (next_timer) {
        next_timer->jiffies--;
        while (next_timer && next_timer->jiffies <= 0) {
            void (*fn)(void);

            fn = next_timer->fn;
            next_timer->fn = NULL;
            next_timer = next_timer->next;
            (fn)();
        }
    }
    if (current_DOR & 0xf0)
        do_floppy_timer();
    if ((--current->counter)>0) return;
    current->counter=0;
    if (!cpl) return;
    schedule();
}

最後がスケジューリング関数schedule()で終わっているので最終的にはスイッチするのですがその前にタイマー関連の処理をしてます。cpl(Current Priviledge Level)の状態、すなわちカーネル内部の処理かユーザランドの処理かによってカウントアップする値を変えnext_timer(timerリストへのポインタ)から処理すべき関数を呼び出しているようです。