LECTURE 9 簡単な割り込みハンドラ

○ はじめに

LECTURE 8 I/O ポートの制御 で、基本的なハードウェアの制御について説明しました。しかし、ハードウェア制御における重要な要素である割り込み処理がまだ残っています。

パラレルポートを例に挙げると、コントロールポートの 4 番ビットを 1 にすることで割り込みが有効になり、ステータスポートの 6 番ビット ( パラレルポートの 10 番ピン )が Low から High になる瞬間に割り込みが発生します。

前回自作した電子回路をそのまま使うと、プッシュボタンを押した状態から離した際にパラレルポートからの割り込みを発生させることができます。ここでは、自作回路を使って割り込み処理の実現方法を説明します。

○ サンプルプログラム

LECTURE 8 I/O ポートの制御 で作成した自作電子回路を使って、割り込みハンドラを実現するためのサンプルプログラム irq_io.hirq_io.cirq_main.c です。はじめに のページから、全てのサンプルプログラムを 固めた tar ボールをダウンロードすることもできます。

gcc -DMODULE -D__KERNEL__ -O -c irq_io.c
gcc -DMODULE -D__KERNEL__ -O -c irq_main.c
ld -r -o irq.o irq_main.o irq_io.o

としてコンパイル/リンクを行います。

割り込みハンドラ以外は、LECTURE 8 I/O ポートの制御 の時と同じ動きをします。プッシュボタンを押して離した瞬間に割り込みが発生しますが、割り込みハンドラ内で一番下の LED の状態を反転するため、プッシュボタンを押す度に一番下の LED が点灯/消灯を繰り返す点が異なります。

--- 全 LED 発光中 ---

全 LED 発光中

一番下の LED も点灯することで、全部の LED が点灯している状態です。

--- irq_io.h ---

    1    #ifndef ___irq_io_h_included_
    2    #define ___irq_io_h_included_
    3    
    4    
    5    #define IOPORT_BASE     0x378
    6    #define IOPORT_RANGE    3
    7    
    8    #define IOPORT_DATA     ( IOPORT_BASE + 0 )
    9    #define IOPORT_STATUS   ( IOPORT_BASE + 1 )
   10    #define IOPORT_CONTROL  ( IOPORT_BASE + 2 )
   11    
   12    #define IRQ_NUMBER      7
   13    
   14    #define DATA_LED_1      0
   15    #define DATA_LED_2      1
   16    #define DATA_LED_3      2
   17    #define DATA_PULLUP     3
   18    #define STATUS_ACK      6
   19    #define IRQ_ENABLE      4
   20    
   21    
   22    void
   23    io_write_or( unsigned short port, unsigned int value );
   24    
   25    void
   26    io_write_xor( unsigned short port, unsigned int value );
   27    
   28    void
   29    io_write_set( unsigned short port, unsigned int value );
   30    
   31    int
   32    io_is_button_pressed();
   33    
   34    void
   35    io_enable_irq();
   36    
   37    
   38    #endif /* ___irq_io_h_included_ */
   39    
   40    /* End of irq_io.h */

--- irq_io.c ---

    1    #include <asm/bitops.h>      /* test_and_set_bit, clear_bit */
    2    #include <asm/io.h>          /* outb(), inb() */
    3    #include <asm/system.h>      /* rmb() */
    4    #include "irq_io.h"
    5    
    6    static int lock;
    7    
    8    
    9    /*
   10     * 現在の指定ポートの値と指定値を or した結果を
   11     * 指定ポートに書き込む
   12     */
   13    void
   14    io_write_or( unsigned short port, unsigned int value )
   15    {
   16        unsigned int data;
   17    
   18        if ( test_and_set_bit( 0, ( void* )( &lock ) ) != 0 )
   19            return;
   20    
   21        data  = inb( port );
   22        data |= value;
   23        outb( data, port );
   24    
   25        clear_bit( 0, ( void* )( &lock ) );
   26    }
   27    
   28    
   29    /*
   30     * 現在の指定ポートの値と指定値を xor した結果を
   31     * 指定ポートに書き込む ( 指定ビットの値反転 )
   32     */
   33    void
   34    io_write_xor( unsigned short port, unsigned int value )
   35    {
   36        unsigned int data;
   37    
   38        if ( test_and_set_bit( 0, ( void* )( &lock ) ) != 0 )
   39            return;
   40    
   41        data  = inb( port );
   42        data ^= value;
   43        outb( data, port );
   44    
   45        clear_bit( 0, ( void* )( &lock ) );
   46    }
   47    
   48    
   49    /*
   50     * 指定値を指定ポートに書き込む
   51     */
   52    void
   53    io_write_set( unsigned short port, unsigned int value )
   54    {
   55        if ( test_and_set_bit( 0, ( void* )( &lock ) ) != 0 )
   56            return;
   57    
   58        outb( value, port );
   59    
   60        clear_bit( 0, ( void* )( &lock ) );
   61    }
   62    
   63    
   64    /*
   65     * ステータスポートの値を読み込み、ACK ( 10 番ピン )
   66     * が LOW かどうか調べる。
   67     */
   68    int
   69    io_is_button_pressed()
   70    {
   71        unsigned int data;
   72    
   73        data = inb( IOPORT_STATUS );
   74        rmb();
   75
   76        if ( ! ( data & ( 1 << STATUS_ACK ) ) )
   77            return 1;
   78    
   79        return 0;
   80    }
   81    
   82    
   83    /*
   84     * 割り込みを有効にする。
   85     * 割り込み制御用ピン以外のピンの状態は変えない。
   86     */
   87    void
   88    io_enable_irq()
   89    {
   90        unsigned int data;
   91    
   92        if ( test_and_set_bit( 0, ( void* )( &lock ) ) != 0 )
   93            return;
   94    
   95        data  = inb( IOPORT_CONTROL );
   96        data |= ( 1 << IRQ_ENABLE );
   97        outb( data, IOPORT_CONTROL );
   98        
   99        clear_bit( 0, ( void* )( &lock ) );
  100    }
  101    
  102    
  103    /* End of irq_io.c */

--- irq_main.c ---

    1    #include <asm/io.h>          /* outb(), inb() */
    2    #include <asm/uaccess.h>     /* copy_from_user, copy_to_user */
    3    #include <linux/fs.h>        /* inode, file, file_operations, ... */
    4    #include <linux/ioport.h>    /* check_region(), request_region(), release_region() */
    5    #include <linux/kernel.h>    /* printk() */
    6    #include <linux/module.h>
    7    #include <linux/sched.h>     /* task_struct */
    8    #include <linux/timer.h> 
    9    
   10    #include "irq_io.h"
   11    
   12    
   13    MODULE_LICENSE( "GPL" );
   14    
   15    
   16    static int   devmajor = 77;
   17    static char* devname  = "chardev";
   18    static char* msg      = "module [irq.o]";
   19    
   20    
   21    /* 100 tick = 1 sec. */
   22    static int timeout = 100; 
   23    MODULE_PARM( timeout, "i" );
   24    
   25    static struct timer_list chardev_timer;
   26    static int timerstop = 0;
   27    
   28    static unsigned int initial_data;
   29    static unsigned int initial_control;
   30    
   31    static void chardev_timer_handler( unsigned long data );
   32    static void chardev_del_timer();
   33    
   34    
   35    /*
   36     * ログ出力フォーマット用関数
   37     */
   38    static void 
   39    log_format( char* body )
   40    {
   41        if ( current != NULL ) {
   42            printk( KERN_INFO "%s : PID = %-5i cmdline = \"%-10s\" : %s\n", 
   43                msg, current->pid, current->comm, body );
   44        }
   45        else {
   46            printk( KERN_INFO "%s : %s\n", msg, body );
   47        }
   48    }
   49    
   50    /*
   51     * 割り込みハンドラ。本当はここでステータスポートを読まないと
   52     * 以降の割り込みが保留される模様。本サンプルでは cat している間中 chardev_read() 
   53     * でステータスポートを読むため大丈夫。本文の説明参照。
   54     */
   55    static void
   56    chardev_interrupt( int irq, void* dev_id, struct pt_regs* regs )
   57    {
   58        io_write_xor( IOPORT_DATA, ( 1 << DATA_LED_3 ) );
   59    }
   60    
   61    /* 
   62     * タイマー登録処理 
   63     */
   64    static void 
   65    chardev_timer_register( void )
   66    {
   67        init_timer( &chardev_timer );
   68    
   69        chardev_timer.expires  = jiffies + timeout;
   70        chardev_timer.data     = ( unsigned long )jiffies;
   71        chardev_timer.function = chardev_timer_handler;
   72    
   73        add_timer( &chardev_timer );
   74    }
   75    
   76    
   77    /*
   78     * タイマー削除関数
   79     * タイマ停止フラグを設定し、del_timer_sync で既存のタイマー関数を
   80     * 削除する(実行中の場合は終了するまで待つ)。
   81     */
   82    static void
   83    chardev_del_timer()
   84    {
   85        timerstop = 1;
   86        del_timer_sync( &chardev_timer );
   87    }
   88    
   89    
   90    /* 
   91     * タイムアウト処理 
   92     */
   93    static void 
   94    chardev_timer_handler( unsigned long data )
   95    {
   96        io_write_xor( IOPORT_DATA, ( 1 << DATA_LED_2 ) );
   97    
   98        if ( timerstop ) {
   99            printk( KERN_INFO "%s : Timer is not to be restarted.\n", msg );
  100            return;
  101        }
  102    
  103        chardev_timer_register();
  104    }
  105    
  106    
  107    /*
  108     * open()
  109     */
  110    static int 
  111    chardev_open( struct inode* inode, struct file* filp )
  112    {
  113        log_format( "open()  called" );
  114        return 0;
  115    }
  116    
  117    
  118    /*
  119     * release()
  120     */
  121    static int 
  122    chardev_release( struct inode* inode, struct file* filp )
  123    {
  124        log_format( "close() called" );
  125        return 0;
  126    }
  127    
  128    
  129    /*
  130     * read()
  131     */
  132    static ssize_t 
  133    chardev_read( struct file* filp, char* buf, size_t count, loff_t* pos )
  134    {
  135        char status[32];
  136        int copy_len = 0;
  137        int len = 0;
  138    
  139        log_format( "read()  called" );
  140    
  141        if ( *pos == 0 ) {
  142            if ( io_is_button_pressed() )
  143                len = sprintf( status, "[Switch] pressed.\n" );
  144            else
  145                len = sprintf( status, "[Switch] NOT pressed.\n" );
  146        }
  147    
  148        copy_len = ( len > count ) ? count : len;
  149    
  150        if ( copy_to_user( buf, status, copy_len ) ) {
  151            log_format( "copy_to_user failed\n" );
  152            return -EFAULT;
  153        }
  154    
  155        *pos += copy_len;
  156    
  157        return copy_len;
  158    }
  159    
  160    
  161    /*
  162     * write()
  163     */
  164    static ssize_t 
  165    chardev_write( struct file* filp, const char* buf, size_t count, loff_t* pos )
  166    {
  167        log_format( "write() called" );
  168        io_write_xor( IOPORT_DATA, ( 1 << DATA_LED_1 ) );
  169        return count;
  170    }
  171    
  172    
  173    static struct file_operations chardev_fops = 
  174    {
  175        owner   : THIS_MODULE,
  176        read    : chardev_read,
  177        write   : chardev_write,
  178        open    : chardev_open,
  179        release : chardev_release,
  180    };
  181    
  182    
  183    /*
  184     * モジュールの初期処理
  185     * insmod 時に呼ばれる
  186     */
  187    int 
  188    init_module( void )
  189    {
  190        EXPORT_NO_SYMBOLS;
  191    
  192        if ( register_chrdev( devmajor, devname, &chardev_fops ) ) {
  193            log_format( "register_chrdev failed" );
  194            return -EBUSY;
  195        }
  196    
  197    
  198        if ( request_region( IOPORT_BASE, IOPORT_RANGE, devname ) == NULL ) {
  199            log_format( "request_region returned NULL" );
  200            unregister_chrdev( devmajor, devname );
  201            return -EBUSY;
  202        }
  203    
  204    
  205        if ( request_irq( IRQ_NUMBER, chardev_interrupt, 0, devname, NULL ) ) {
  206            log_format( "request_irq failed" );
  207            release_region( IOPORT_BASE, IOPORT_RANGE );
  208            unregister_chrdev( devmajor, devname );
  209            return -EBUSY;
  210        }
  211    
  212        initial_data    = inb( IOPORT_DATA );
  213        initial_control = inb( IOPORT_CONTROL );
  214    
  215        io_write_or( IOPORT_DATA, ( 1 << DATA_PULLUP ) );
  216        io_enable_irq();
  217    
  218        chardev_timer_register();
  219    
  220        log_format( "loaded  into kernel" );
  221        return 0;
  222    }
  223    
  224    
  225    /*
  226     * モジュールの解放処理
  227     * モジュールの参照数が 0 であれば、rmmod 時に呼ばれる
  228     */
  229    void 
  230    cleanup_module( void )
  231    {
  232        chardev_del_timer();
  233        free_irq( IRQ_NUMBER, NULL );
  234    
  235        io_write_set( IOPORT_CONTROL, initial_control );
  236        io_write_set( IOPORT_DATA,    initial_data );
  237    
  238        release_region( IOPORT_BASE, IOPORT_RANGE );
  239    
  240        if ( unregister_chrdev( devmajor, devname ) ) {
  241            log_format( "unregister_chrdev failed" );
  242            /* 対策無し ... */
  243        }
  244    
  245        log_format( "removed from kernel" );
  246    }
  247    
  248    
  249    /* End of irq_main.c */

○ サンプルプログラムの説明

では、サンプルプログラムについて説明していきます。 LECTURE 8 I/O ポートの制御 で紹介した hw_io.hhw_io.chw_main.c をベースに必要な箇所を 追加して hw_ を irq_ に変えただけなので、追加箇所について説明します。


--- irq_io.h ---

  • 12 行目

    パラレルポートからの割り込み通知に使用される割り込み番号をマクロ定義しています。 ベースアドレスが異なる場合は、割り込み番号も異なるので注意してください。

  • 19 行目

    パラレルポートは初期状態では割り込みが無効になっているため、プログラム内 で有効にする必要があります。コントロールポートの 4 番ビット ( 対応するピンはありません ) を 1 にすることで割り込みが有効になります。ここでは 4 番ビットを表すマクロを 定義しています。

  • 34 〜 35 行目

    割り込みを有効にする io_enable_irq のプロトタイプを追加しています。


--- irq_io.c ---

  • 83 〜 100 行目 ( io_enable_irq )

    パラレルポートからの割り込みを有効にする関数です。 コントロールポートの 4 番ビットを 1 にし、それ以外のビットの状態を 変えない様にしています。


--- irq_main.c ---

  • 29 行目

    irq_main.c ではコントロールポートへの書き込みも行う為、 コントロールポートの初期状態保存用変数を追加しています。

  • 50 〜 59 行目 ( chardev_interrupt )

    割り込み発生時に実行される割り込みハンドラです。登録方法は後述しますが、 ここでは割り込みハンドラの形式を覚えておいてください。

    割り込みハンドラは、以下のプロトタイプに従って実装します。

    void interrupt_func( int irq, void* dev_id, struct pt_regs* regs );
    
    

    irq は割り込み番号です。dev_id には登録時に設定したポインタがそのまま渡されます。 また、regs は直前の CPU コンテキストが格納されていますが、ここでは触れません。

    chardev_interrupt では、io_write_xor によって一番下の LED の状態を反転しています。これにより、割り込みが発生する度 ( プッシュボタンスイッチを押して離す度 ) に一番下の LED が点灯/消灯を繰り返します。

    2008/08/11 追記。パラレルポートの割り込みを使う場合、ステータスポートのビット 6 -ACK が Low から High に変わる際に割り込みが発生しますが、この時にステータスポートのビット 2 -IRQ Status が 0 になる様です。ここを 1 にセットしないと、以降の割り込みが保留になってしまいます。1 にセットするためには、ステータスポートの読み込みが必要です。本サンプルでは、デバイスファイルを cat することで chardev_read() にてステータスポートの読み込みを行うため、その度に -IRQ Status が 1 になるので割り込みが継続して発生します。しかし、この様な副作用ではなくきちんと割り込みを扱うためには、割り込みハンドラにてステータスポートの読み出しを行う必要があります。木原さん、ありがとうございました。

    割り込みハンドラ実装時に考慮すべき点として、まずあまり長時間に渡る処理 を行わない様にしてください。割り込みハンドラ実行中は、同割り込み番号の割り込み が無効になっているため、複数回の割り込み発生を正常に扱えなくなってしまいます。 割り込みを契機とした長時間の処理が必要な場合は、割り込みハンドラで タスクレットと呼ばれる機能を起動し、タスクレットにて長時間かかる処理を行います。 そして割り込みハンドラ自体は迅速に終了する様にします。タスクレットについては、 次回に説明します。

    また、プロセスコンテキスト ( read や write 等のプロセスから実行される関数の実行状態 ) と割り込みハンドラとの間で排他処理を行う場合は、注意する必要があります。

    まず、セマフォはプロセスと関連付けられるため使用することができません。 また、複数プロセス間の排他処理用に通常のスピンロックを行うと、read 等の関数において スピンロックを取得した後に割り込みハンドラが起動し、割り込みハンドラ内で スピンロック取得待ちになることでデッドロックを起こしてしまいます。

    この様な場合は、ロック取得中のローカル CPU の割り込みを無効にする spin_lock_irqsave / spin_unlock_irqrestore を使用します。

    #include <linux/spinlock.h>
    spin_lock_irqsave( spinlock_t* lock, unsigned long flags );
    spin_unlock_irqrestore( spinlock_t* lock, unsigned long flags );
    
    

    spin_lock_irqsave の flags は、割り込みを無効にする直前の割り込み状態を 格納する領域です。ポインタではない点に注意してください。ここで退避した割り込み状態を spin_unlock_irqrestore に指定することで、割り込み状態を元に戻すことができます。

    read 等のプロセスコンテキストにおいて spin_lock_irqsave / spin_unlock_irqrestore を使うことで、プロセスコンテキストでのロック取得中は割り込みハンドラが起動せず、 競合を避けることができます。同じ割り込み番号に対する割り込みが二重に処理されることは ない ( 確か。。。 ) ため、割り込みハンドラが一つであればその中では spin_lock / spin_unlock で良いかもしれません。しかし、複数の割り込み番号に対する 割り込みハンドラ間で排他を行う場合は、割り込みハンドラ内でも spin_lock_irqsave / spin_unlock_irqrestore を使って割り込みを禁止するべきかもしれません。 この辺り筆者はあまり追求していないので、必要な場合は各自で調査してみてください。

    なお、irq_main.c では、 タイムアウトハンドラとの排他処理もあるため irq_io.c 内での test_and_set_bit / clear_bit によって処理しています。

  • 183 〜 222 行目 ( init_module )

    205 〜 210 行目で、request_irq によって割り込みハンドラを登録しています。 登録と同時に割り込みハンドラが有効になりますが、デバイスが open されていなければ 割り込みハンドラが必要ない様な場合は、init_module で実行する必要はないです。 irq_main.c では、ユーザプロセスとは無関係に 制御を行う必要があるため、init_module で request_irq を実行しています。

    #include <linux/sched.h>
    int request_irq( unsigned int irq, 
                     void (*handler)( int, void*, struct pt_regs* ),
                     unsigned long flags,
                     const char* dev_name,
                     void* dev_id );
    
    

    irq は割り込みハンドラを登録する割り込み番号で、handler は割り込みハンドラ のポインタを指定します。flags は、0 あるいは linux/sched.h で定義されている 以下の定数を設定したビットマスクを指定します。

    定数名 説明
    SA_INTERRUPT ローカル CPU における割り込み検出を無効にして、割り込みハンドラを起動する
    SA_SHIRQ 割り込み番号が複数デバイス間で共有可能である。これを指定する場合、 dev_id に NULL を指定してはならず、かつ各 dev_id はユニークでなければならない ( 割り込みハンドラを識別する必要があるため )。また、同じ割り込み番号で 複数の割り込みハンドラが起動されるため、ハンドラ内で割り込み状態を操作しては いけない
    SA_SAMPLE_RANDOM 生成された割り込みが、システムのエントロピー生成に使われる。エントロピーは乱数の生成源となり、暗号処理等で真の乱数を必要とするソフトウェアで必要となる。割り込みのタイミングが予測可能な場合や、外部から操作可能である場合は、エントロピーの複雑さを下げてしまうため、このフラグを指定してはいけない

    dev_name は /proc/interrupts において割り込み番号の所有者として使用される文字列を指定します。 また、dev_id は割り込み共有時に割り込みハンドラを識別するためのユニークな識別子となる データへのポインタを指定します。割り込み共有時には、dev_id を NULL にしてはいけません。 共有しない場合は NULL でも構いません。ここで指定したポインタは、割り込みハンドラ起動時に 引数として渡されます。

    212 〜 213 行目では、最後に初期状態に戻す為にデータポートおよび コントロールポートの状態を保存しています。

    216 行目で io_enable_irq によって、パラレルポートからの割り込みを有効にしています。 これ以降、プッシュボタンを押した状態から離した際に、割り込み番号 7 番 ( お使いの環境によって異なります ) の割り込みが発生します。

  • 225 〜 246 行目 ( cleanup_module )

    233 行目で、free_irq によって割り込みハンドラ登録を削除しています。

    #include <linux/sched.h>
    void free_irq( unsigned int irq, void* dev_id );
    
    

    割り込み番号の使用が終わったら、free_irq を使って必ず解放してください。

    また、235 〜 256 行目でデータポートおよびコントロールポートを 初期状態に戻しています。

以上で irq_io.h、irq_io.c、irq_main.c の解説は終わりです。