7.2 Polling and interrupts

7.2.1 Polling mode

polling ではドライバは常にハードウェアに問い合わせする.無駄に思えるかもしれないがときにはハードウェアとの通信が最速になる方法である.

パラレルインタフェースのデバイスドライバはpollingによって動く.

drivers/char/lp.c

/* BIOSマニュアルには最大4つのlptデバイスがあると言われているが,
 * 4番目のアドレスが記載されているボードは見つからなかった.
 * 異なるハードウェアを使っている場合は以下の表を変更すること.
 * もし3台以上のプリンタを接続しているなら,
 * LP_NOを増やすことを忘れないで.
 */
struct lp_struct lp_table[] = {
    { 0x3bc, 0, 0, LP_INIT_CHAR, LP_INIT_TIME, LP_INIT_WAIT, NULL, NULL, },
    { 0x378, 0, 0, LP_INIT_CHAR, LP_INIT_TIME, LP_INIT_WAIT, NULL, NULL, },
    { 0x278, 0, 0, LP_INIT_CHAR, LP_INIT_TIME, LP_INIT_WAIT, NULL, NULL, },
}; 
#define LP_NO 3
...
static int lp_char_polled(char lpchar, int minor)
{
    int status = 0, wait = 0;
    unsigned long count  = 0; /* データ端末デバイスのエラー検出用 */

    do {
        status = LP_S(minor); /* LP_S() -- ポートの状態を返す */
        count ++;
        if(need_resched)
            schedule();
    } while(!LP_READY(minor,status) && count < LP_CHAR(minor));

    /* ビジー状態でタイムアウトしているなら? */
    if (count == LP_CHAR(minor)) {
        return 0;
        /* タイムアウトし,文字は出力されない */
    }

    /* ...デバッグ用の処理のため省略... */

    outb_p(lpchar, LP_B(minor)); /* インタフェースにlpcharを渡す */
    /*
     * ストロボを付ける前と消した後で待つ必要がある.
     * 必要とするプリンタもあれば不要なプリンタもある.
     */
    while(wait != LP_WAIT(minor)) wait++; /* lp_table[(minor)].wait になるまで*/
        /* 制御ポートはストロボをつける */
    outb_p(( LP_PSELECP | LP_PINITP | LP_PSTROBE ), ( LP_C( minor )));
    while(wait) wait--;
        /* ストロボを消す */
    outb_p(( LP_PSELECP | LP_PINITP ), ( LP_C( minor )));

    return 1;
}

include/linux/lp.h

struct lp_struct {
    int base;
    unsigned int irq;
    int flags;
    unsigned int chars;
    unsigned int time;
    unsigned int wait;
    struct wait_queue *lp_wait_q;
    char *lp_buffer;
};

include/asm-i386/io.h

/*
 * __builtin_constant_p()が動作するため,これをインライン関数の中で
 * 使うことはできない(これは決してtrueにならない).
 * __builtin..の副作用は心配しなくていい.
 */
...
#define outb_p(val,port) \
((__builtin_constant_p((port)) && (port) < 256) ? \
    __outbc_p((val),(port)) : \
    __outb_p((val),(port)))

タイムアウトのエラー処理により'lpn off-line','lpn out of page'または'lpn reported invalid error status (on fire, eh?)'いずれかのメッセージが返される.

LP_CHARカウントは標準でLP_INIT_CHARに設定され,システムコールioctlによって変更可能である.

7.2.2 Interrupt mode

interruptsはハードウェアでサポートされている場合のみ可能である.ここで,デバイスは割り込みの終了をInterrupt Channel(IRQ)を介してCPUに通知する.これによって,現在の操作が中断され,割り込みサービスルーチン(ISR)が実行される.その後,デバイスとのさらなる通信はISR内でおこなわれる.

このように,割り込みモードでパラレルインタフェースに書き込もうとしているプロセスは,文字が書き込まれたあとに,デバイスドライバのこの関数によって停止させられる.

drivers/char/lp.c

static int lp_write_interrupt(struct inode * inode, struct file * file, char * buf, int count)
{
    unsigned int minor = MINOR(inode->i_rdev);
    unsigned long copy_size;
    unsigned long total_bytes_written = 0;
    unsigned long bytes_written;
    struct lp_struct *lp = &lp_table[minor];
    unsigned char status;

    do {
        bytes_written = 0;
        copy_size = (count <= LP_BUFFER_SIZE ? count : LP_BUFFER_SIZE);
        memcpy_fromfs(lp->lp_buffer, buf, copy_size);

        while (copy_size) {
            if (lp_char_interrupt(lp->lp_buffer[bytes_written], minor)) {
                --copy_size;
                ++bytes_written;
            } else {
                int rc = total_bytes_written + bytes_written;
                status = LP_S(minor);
                // ...エラー処理のため省略...
                }
                cli();
                outb_p((LP_PSELECP|LP_PINITP|LP_PINTEN), (LP_C(minor)));
                status = LP_S(minor);
                if ((!(status & LP_PACK) || (status & LP_PBUSY))
                  && LP_CAREFUL_READY(minor, status)) {
                    outb_p((LP_PSELECP|LP_PINITP), (LP_C(minor)));
                    sti();
                    continue;
                }
                current->timeout = jiffies + LP_TIMEOUT_INTERRUPT;
                interruptible_sleep_on(&lp->lp_wait_q);
                outb_p((LP_PSELECP|LP_PINITP), (LP_C(minor)));
                sti();
                if (current->signal & ~current->blocked) {
                    if (total_bytes_written + bytes_written)
                        return total_bytes_written + bytes_written;
                    else
                        return -EINTR;
                }
            }
        }

        total_bytes_written += bytes_written;
        buf += bytes_written;
        count -= bytes_written;

    } while (count > 0);

    return total_bytes_written;
}

パラレルインタフェースが文字をもっと受け入れられるならIRQが引き起こされる.その後,手続きを処理するISRがプロセスを起こし,手続きが繰り返される.これはISRを非常にシンプルにしている.

static void lp_interrupt(int irq, struct pt_regs *regs)
{
    struct lp_struct *lp = &lp_table[0];
    struct lp_struct *lp_end = &lp_table[LP_NO];

    while (irq != lp->irq) {
        if (++lp >= lp_end)
            return;
    }

    wake_up(&lp->lp_wait_q);
}

割り込みを起こしたインタフェースを探し,待ちプロセスがwake_up()で起こされる.

マウスはシリアルポートにデータを送信するたびにIRQが引き起こされる.シリアルポートからのデータはISR操作によって最初に読み取られ,アプリケーションプログラムに渡される.

arch/i386/kernel/irq.c

/*
 * irqflags -- どのタイプの割り込みを使うか指定する.
 *             低速IRQは0を与え,高速IRQはSA_INTERRUPTを与える.
 * devname  -- IRQの所有者を示すためにProcファイルシステムによって使われる.
 *             カーネルにとってはあまり重要ではない.
 *             IRQを使うドライバの名前を指す必要がある.
 */
int request_irq(unsigned int irq, void (*handler)(int, struct pt_regs *),
    unsigned long irqflags, const char * devname)
{
    struct irqaction * action;
    unsigned long flags;

    if (irq > 15)
        return -EINVAL;
    action = irq + irq_action;
    if (action->handler)
        return -EBUSY;
    if (!handler)
        return -EINVAL;
    save_flags(flags);
    cli();
    action->handler = handler;
    action->flags = irqflags;
    action->mask = 0;
    action->name = devname;
    if (!(action->flags & SA_PROBE)) { /* SA_ONESHOT is used by probing */
        if (action->flags & SA_INTERRUPT)
            set_intr_gate(0x20+irq,fast_interrupt[irq]);
        else
            set_intr_gate(0x20+irq,interrupt[irq]);
    }
    if (irq < 8) {
        cache_21 &= ~(1<<irq);
        outb(cache_21,0x21);
    } else {
        cache_21 &= ~(1<<2);
        cache_A1 &= ~(1<<(irq-8));
        outb(cache_21,0x21);
        outb(cache_A1,0xA1);
    }
    restore_flags(flags);
    return 0; /* IRQが見つかり,取得できたなら0を返す */
}

低速割り込みは割り込みフラグがセットされた状態で実行されるので,他の割り込みによって中断される可能性がある.低速割り込みが完了すると,システムコールの終了時と同じアルゴリズムが使われる.

ただし,高速割り込みは割り込みフラグをオフにして実行される.高速割り込みルーチンへ他の割り込みを許可する場合はsti()マクロを呼び出す必要がある.高速割り込みはiret命令で終了し,中断されたプロセスに直接戻る.

IRQの処理ルーチンは以下の通り

arch/i386/kernel/irq.c

/*
 * do_IRQはSA_INTERRUPTフラグなしで導入されたIRQを処理する.
 * 完全なシグナル処理の戻り値を使い,他の割り込みを有効にして実行される.
 * 比較的遅いすべてのIRQ,特にキーボードやタイマーのルーチンは
 * このフォーマットを使う必要がある.
 */
/*
 * int irq -- この関数を呼び出すIRQの数.つまり理論的には複数のIRQに対して
 *            ひとつのISRを使用できる.
 * struct pt_regs * regs -- pt_regs構造体には,IRQによって中断された
 *                          プロセスのすべてのレジスタが格納されている.
 *
 **/
asmlinkage void do_IRQ(int irq, struct pt_regs * regs)
{
    struct irqaction * action = irq + irq_action;

    kstat.interrupts[irq]++;
    action->handler(irq, regs);
}

include/asm-i386/ptrace.h

/*
 * この構造体は,システムコール中にレジスタがスタックに格納される方法を定義する.
 */

struct pt_regs {
    long ebx;
    long ecx;
    long edx;
    long esi;
    long edi;
    long ebp;
    long eax;
    unsigned short ds, __dsu;
    unsigned short es, __esu;
    unsigned short fs, __fsu;
    unsigned short gs, __gsu;
    long orig_eax;
    long eip;
    unsigned short cs, __csu;
    long eflags;
    long esp;
    unsigned short ss, __ssu;
};

高速割り込みの導入の例(drivers/char/lp.c).

static int lp_open(struct inode * inode, struct file * file)
{
    unsigned int minor = MINOR(inode->i_rdev);
    int ret;
    unsigned int irq;

    // ...エラー処理...

    MOD_INC_USE_COUNT;

    /*
     * ABORTOPENが設定されていて,プリンタがオフラインまたは用紙切れの場合,
     * ioctl()を実行するためにプリンタを開くことができる.
     * したがって非標準的な方法で使われているのにもかかわらず,
     * 我々ははO_NONBLOCKを命令している.
     * これはLinuxのハックであり,tunelpアプリケーションでのみ
     * 使われる可能性が高い.
     */
    if ((LP_F(minor) & LP_ABORTOPEN) && !(file->f_flags & O_NONBLOCK)) {
        int status = LP_S(minor);
        // ...エラー処理...
    }

    if ((irq = LP_IRQ(minor))) {
        lp_table[minor].lp_buffer = (char *) kmalloc(LP_BUFFER_SIZE, GFP_KERNEL);
        // ...エラー処理...

        ret = request_irq(irq, lp_interrupt, SA_INTERRUPT, "printer");
        if (ret) {
            kfree_s(lp_table[minor].lp_buffer, LP_BUFFER_SIZE);
            lp_table[minor].lp_buffer = NULL;
            printk("lp%d unable to use interrupt %d, error %d\n", minor, irq, ret);
            MOD_DEC_USE_COUNT;
            return ret;
        }
    }

この高速割り込みは通常,ハードウェアとの通信に使われる.

7.2.3 Bottom halves

ただし,割り込みが発生した直後にすべての関数を実行する必要があるわけではない場合も頻繁にある.重要なものは一度に処理する必要があるものの,後で処理できるものもあれば,比較的時間がかかるものもある.時間がかかるなら割り込みをブロックしないことが望ましい.このようなケースにために, bottom halvesが作られた.すべてのret_from_syscallのあと,つまり低速割り込みごとに,その時点でそれ以上割り込みが実行されていない場合は,ボトムハーフの32のリストがスキャンされる.アクティブである場合は順番に一度実行されたあと,自動的に非アクティブとして記録される.これらのボトムハーフはアトミックである.ボトムハーフがアクティブである限り他のものは実行できないためcli()を使って割り込みから保護する必要はない.

ボトムハーフを導入する機能はカーネルにはないので,プログラムによってbh_baseテーブルに入れる必要がある.

include/linux/interrupt.h

struct bh_struct {
    void (*routine)(void *);
    void *data;
};

...

extern struct bh_struct bh_base[32];

...
/* 誰がbh_baseのどのエントリを取るのか.最も頻繁に起こる
   NETがSERIAL/TQUEUEの先頭にくるはずだ. */

enum {
    TIMER_BH = 0,
    CONSOLE_BH,
    TQUEUE_BH,
    SERIAL_BH,
    NET_BH,
    IMMEDIATE_BH,
    KEYBOARD_BH,
    CYCLADES_BH
};

任意のデータへのポインタを引数として渡すことが可能である.標準ではすべてのボトムハーフが許可されているが,これらの関数を使って無効にしたり有効にしたりできる.

extern inline void disable_bh(int nr)
{
    clear_bit(nr, &bh_mask);
}

extern inline void enable_bh(int nr)
{
    set_bit(nr, &bh_mask);
}

7.2.4 DMA mode

大量のデータがデバイス間で連続的に転送される場合,DMAモードが選択肢となる.このモードではDMAコントローラはプロセッサを介さずにメモリからデバイスへ直接データを転送する.デバイスは転送の後にIRQを引き起こすため,次のDMA転送はISR内の操作で済ませることが可能である.DMAモードは,データ転送中にCPUが他のタスクを処理できるためマルチタスクに最適である.

まずプロセッサを用いてDMAバッファにデータをコピーしたあと,DMAを介してデバイスに転送する.

drivers/sound/dev_table.h

struct dma_buffparms {
    int      dma_mode;  /* DMODE_INPUT, DMODE_OUTPUT or DMODE_NONE */

    /*
     * Pointers to raw buffers
     */

    char     *raw_buf[DSP_BUFFCOUNT];
    unsigned long   raw_buf_phys[DSP_BUFFCOUNT];
    int             raw_count;

    /*
     * Device state tables
     */

    unsigned long flags;
#define DMA_BUSY    0x00000001
#define DMA_RESTART 0x00000002
#define DMA_ACTIVE  0x00000004
#define DMA_STARTED 0x00000008
#define DMA_ALLOC_DONE  0x00000020

    int      open_mode;

    /*
     * Queue parameters.
     */
    int      qlen;
    int      qhead;
    int      qtail;

    int      nbufs;
    int      counts[MAX_SUB_BUFFERS];
    int      subdivision;
    char    *buf[MAX_SUB_BUFFERS];
    unsigned long buf_phys[MAX_SUB_BUFFERS];

    int      fragment_size;
    int  max_fragments;

    int  bytes_in_use;

    int  underrun_count;
};