LECTURE 7 カーネルタイマーを使う

○ はじめに

今回は、カーネル内で使用することができるタイマー処理について説明します。一定時間経過後に特定の処理を実行することができるタイマーは、プログラムで使用する機会が非常に多く ( ポーリングによるハードウェアの監視など )、覚えておくと便利だと思います。

○ サンプルプログラム

カーネルタイマーを使ったサンプルプログラム timer.c です。 はじめに のページから、 全てのサンプルプログラムを 固めた tar ボールをダウンロードすることもできます。

insmod でロードした直後から、以下の様な内容 ( 数値は異なる ) が一定時間毎にカーネルログに繰り返し表示されます。

<6>module [timer.o] : Timeout ( registered 1140490, now 1140990 )
<4>module [timer.o] : Register timer (1140990)

cat /dev/chardev の様にして /dev/chardev に対して読み込み処理を実行すると、"Delete registerd timer." と表示され、カーネルタイマーが停止します。

--- timer.c ---

    1    #include <linux/fs.h>       /* inode, file, file_operations, ... */
    2    #include <linux/kernel.h>   /* printk() */
    3    #include <linux/module.h>
    4    #include <linux/sched.h>    /* task_struct */
    5    #include <linux/timer.h> 
    6    
    7    
    8    MODULE_AUTHOR( "Ryo" );
    9    MODULE_DESCRIPTION( "Sample character device driver." );
   10    MODULE_LICENSE( "GPL" );
   11    
   12    
   13    static int   devmajor = 77;
   14    static char* devname  = "chardev";
   15    static char* msg      = "module [timer.o]";
   16    
   17    
   18    /* 500 tick = 5 sec. */
   19    static int timeout = 500; 
   20    MODULE_PARM( timeout, "i" );
   21    
   22    static struct timer_list chardev_timer;
   23    static int timerstop = 0;
   24    
   25    static void chardev_timer_handler( unsigned long data );
   26    static void chardev_del_timer();
   27    
   28    
   29    /*
   30     * ログ出力フォーマット用関数
   31     */
   32    static void 
   33    log_format( char* body )
   34    {
   35        if ( current != NULL ) {
   36            printk( KERN_INFO "%s : PID = %-5i cmdline = \"%-10s\" : %s\n", 
   37                msg, current->pid, current->comm, body );
   38        }
   39        else {
   40            printk( KERN_INFO "%s : %s\n", msg, body );
   41        }
   42    }
   43    
   44    
   45    /* 
   46     * タイマー登録処理 
   47     */
   48    static void 
   49    chardev_timer_register( void )
   50    {
   51        init_timer( &chardev_timer );
   52    
   53        chardev_timer.expires  = jiffies + timeout;
   54        chardev_timer.data     = ( unsigned long )jiffies;
   55        chardev_timer.function = chardev_timer_handler;
   56    
   57        printk( "%s : Register timer (%ld)\n", msg, jiffies );
   58        add_timer( &chardev_timer );
   59    }
   60    
   61    
   62    /* 
   63     * タイムアウト処理 
   64     */
   65    static void 
   66    chardev_timer_handler( unsigned long data )
   67    {
   68        printk( KERN_INFO "%s : Timeout ( registered %ld, now %ld )\n", 
   69            msg, data, jiffies );
   70    
   71        if ( timerstop ) {
   72            printk( KERN_INFO "%s : Timer is not to be restarted.\n", msg );
   73            return;
   74        }
   75    
   76        chardev_timer_register();
   77    }
   78    
   79    
   80    /*
   81     * open()
   82     */
   83    static int 
   84    chardev_open( struct inode* inode, struct file* filp )
   85    {
   86        log_format( "open()  called" );
   87        return 0;
   88    }
   89    
   90    
   91    /*
   92     * release()
   93     */
   94    static int 
   95    chardev_release( struct inode* inode, struct file* filp )
   96    {
   97        log_format( "close() called" );
   98        return 0;
   99    }
  100    
  101    
  102    /*
  103     * read()
  104     * タイマー削除関数を実行する。
  105     */
  106    static ssize_t 
  107    chardev_read( struct file* filp, char* buf, size_t count, loff_t* pos )
  108    {
  109        log_format( "read()  called" );
  110        log_format( "Delete registered timer." );
  111    
  112        chardev_del_timer();
  113        return 0;
  114    }
  115    
  116    
  117    /*
  118     * タイマー削除関数
  119     * タイマ停止フラグを設定し、del_timer_sync で既存のタイマー関数を
  120     * 削除する(実行中の場合は終了するまで待つ)。
  121     */
  122    static void
  123    chardev_del_timer()
  124    {
  125        timerstop = 1;
  126        del_timer_sync( &chardev_timer );
  127    }
  128    
  129    
  130    static struct file_operations chardev_fops = 
  131    {
  132        owner   : THIS_MODULE,
  133        read    : chardev_read,
  134        open    : chardev_open,
  135        release : chardev_release,
  136    };
  137    
  138    
  139    /*
  140     * モジュールの初期処理
  141     * insmod 時に呼ばれる
  142     */
  143    int 
  144    init_module( void )
  145    {
  146        if ( register_chrdev( devmajor, devname, &chardev_fops ) ) {
  147            log_format( "register_chrdev failed" );
  148            return -EBUSY;
  149        }
  150    
  151        chardev_timer_register();
  152    
  153        log_format( "loaded  into kernel" );
  154        return 0;
  155    }
  156    
  157    
  158    /*
  159     * モジュールの解放処理
  160     * モジュールの参照数が 0 であれば、rmmod 時に呼ばれる
  161     */
  162    void 
  163    cleanup_module( void )
  164    {
  165        chardev_del_timer();
  166    
  167        if ( unregister_chrdev( devmajor, devname ) ) {
  168            log_format( "unregister_chrdev failed" );
  169            /* 対策無し ... */
  170        }
  171    
  172        log_format( "removed from kernel" );
  173    }
  174    
  175    
  176    /* End of timer.c */

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

では、サンプルプログラムについて説明していきます。

  • 5 行目

    タイマー機能を使うために必要なヘッダファイルをインクルードしています。

  • 18 〜 20 行目

    タイマーを起動してからのタイムアウト時間を timeout 変数に設定しています。 今回使用するタイマーでは、タイムアウト値をシステムのタイマー割り込み回数で指定します。 ここではタイマー割り込み 500 回でタイムアウト値を初期化しています。

    システムのタイマー割り込み間隔は、1 秒間に発生するタイマー割り込み回数として HZ というマクロで定義されています。HZ は asm/param.h で定義されています。 この値はプラットフォームによって異なるので、特定値であるとは思わない様にして下さい。 なお、システムが起動してからの総タイマー割り込み回数は jiffies というグローバル変数に格納されています。jiffies は linux/sched.h 内で宣言されています。

    MODULE_PARM マクロは、外部から変数値の設定を可能にするマクロです。 変数名と型を引数とすることで、簡単に insmod 等で変数設定できる様になります。 型は以下のいずれかを文字列として指定します("" で囲む)。

    • b ( byte )
    • h ( short )
    • i ( int )
    • l ( long )
    • s ( 文字列 )

    外部から設定されない場合のため、変数には必ずデフォルト値を設定しておく様にしましょう。

  • 22 〜 23 行目

    chardev_timer がカーネルタイマー で、struct timer_list 構造体の 変数として宣言しています。timerstop は、タイムアウトハンドラ内で参照するタイマー停止フラグです ( タイムアウトハンドラの説明参照 )。

  • 45 〜 59 行目 ( chardev_timer_register )

    タイマーの登録処理を行います。timer.c では init_module でキャラクタデバイス としての登録を行った直後に実行されることで、最初のタイマーを起動しています。

    最初に init_timer でカーネルタイマーを初期化しています。 カーネルタイマーを使う際には、必ず最初に init_timer で初期化を行って下さい。

    #include <linux/timer.h>
    void init_timer( struct timer_list* timer );
         

    次に、カーネルタイマーに必要な設定を行っています。設定する必要があるのは

    • タイムアウト時間 ( expires )
    • タイムアウトハンドラに渡すデータ ( data )
    • タイムアウトハンドラ ( function )

    です。

    expires は、タイマー割り込み回数で設定します。ただし、現在値からの相対値ではなく、 システム起動直後からの絶対値で設定します。そのため、 jiffies + timeout を expires に設定しています。なお、jiffies + HZ を設定すると、 1 秒後にタイムアウトすることになります ( HZ は 1 秒間のタイマー割り込み回数のため )。

    data には、タイムアウトハンドラに渡すデータを設定します。timer.c では、 タイマー登録時の jiffies を設定しています。 ここにポインタを設定し ておくと、タイムアウトハンドラ内で任意のデータ構造を参照することもできる というわけです。

    function には、タイムアウトハンドラの関数ポインタを設定します。 タイムアウトハンドラは前述の通り void ( *function )( unsigned long ) 形式で宣言されている必要があります。

    設定を行った後に、add_timer 関数でタイマーをカーネルの タイマーリストに登録しています。

    #include <linux/timer.h>
    void add_timer( struct timer_list* timer );
         
  • 62 〜 77 行目 ( chardev_timer_handler )

    タイムアウトが発生した際に実行されるタイマー関数 ( タイムアウトハンドラ ) です。 タイムアウトハンドラは void ( *function )( unsigned long ) 形式の関数でなければなりません。

    中でやっていることは単純で、まずカーネルログにログを表示しています。 引数の data は、後述のタイマー登録処理 ( chardev_timer_register ) 実行時にタイムアウトハンドラの引数に渡す値として設定した値です。

    ログを表示した後はタイマー停止フラグを参照しています。タイマー停止 フラグが設定されていたらそのまま終了し、設定されていない場合は後述の タイマー登録処理 ( chardev_timer_register ) を実行することで、再度 タイマーを起動しています。このタイマー停止フラグは、 del_timer_sync ( 後述 ) 実行後にタイムアウトハンドラが実行されていないことを保証 するために使っています。

  • 102 〜 114 行目 ( chardev_read )

    timer.c では、chardev_read をタイマー停止を指示するための関数として利用しています。 実行されると、chardev_del_timer 関数を実行してから終了します。

  • 117 〜 127 行目 ( chardev_del_timer )

    タイマー削除関数で、chardev_read および cleanup_module で実行されます。 まず、スピンロックを取得してからタイマー停止フラグを設定します。 これにより、以後 chardev_timer_handler 実行時に再度タイマーが登録されるのを 防いでいます。その後 del_timer_sync を実行します。

    #include <linux/timer.h>
    int del_timer_sync( struct timer_list* timer );
         

    del_timer_sync が戻って来た際には、どの CPU でもタイムアウトハンドラが 実行されていないことが保証されます。ただし、タイムアウトハンドラ内で 再度タイマーが登録されてしまうと、del_timer_sync が戻って来た後に タイムアウトハンドラが実行される可能性があるため、別にタイマー停止フラグを 用意して再登録させないことで、保証を確実なものにしています。

  • 139 〜 155 行目 ( init_module )

    register_chrdev 直後に、chardev_timer_register によって 最初のタイマー登録を行っています。

  • 158 〜 173 行目 ( cleanup_module )

    unregister_chrdev を実行する前に cheardev_del_timer を実行しています。 タイマーが登録された状態で rmmod によりモジュールがアンロードされると、 カーネルが既に実体が無くなってしまったタイムアウトハンドラを実行しようとして ハングアップしてしまうため、この処理が必要です。

以上で timer.c の解説は終わりです。タイマーは覚えると結構使い途がある様な気がするので、ぜひみなさん遊んでみて下さい。

○ タイマー割り込み間隔未満の遅延処理について

カーネルタイマーは、システムのタイマー割り込み回数を指定することでタイムアウト時刻を設定します。 そのため、タイマー割り込み間隔より短い時間の遅延処理を実現することはできません ( 多くのプラットフォーム上では 1 / 100 sec. = 10 msec. 程度 )。

よって、マイクロ秒や数ミリ秒の遅延を実現したい場合は、以下の関数を使います。

#include <linux/delay.h>
void udelay( unsigned long usecs );
void mdelay( unsigned long msecs );

これらはソフトウェアループで遅延を実現する関数で、linux/delay.h をインクルードすることで使用できます。実行中は CPU を占有してしまうことに注意して下さい。また、udeley は 1000 マイクロ秒以上の遅延には使用しない様にした方がいい様です。

○ スリープについて

その場で単純にスリープしたい場合には、interruptible_sleep_on_timeout を使います。

#include <linux/sched.h>
void interruptible_sleep_on_timeout( wait_queue_head_t* queue, long timeout );

timeout はカーネルタイマー同様 jiffies で指定します。interruptible_sleep_on_timeout は、タイムアウト時間をぬけるか、wake_up_interruptible 等の起床関数あるいはシグナル等によって起床された場合に終了します。

シグナルが発生している場合は -ERESTARTSYS を返して即座に終了する必要があるため、signal_pending を使ってシグナルが発生しているかどうか確認します。

#include <linux/sched.h>
int signal_pending( struct task_struct* p  );

現在の CPU 状態を表すグローバル変数 current を引数にして実行すると、シグナルが発生している場合は 1 を、発生していない場合は 0 を返します。