BROKEN's Advanced Vehicle Laboratory

Real-Time Linuxによるリアルタイム処理とOpenGL によるシミュレータの構築 (2)

index section 1 section 2 section 3 section 4 section 5 section 6
bibrography appendix A appendix B appendix C

2. RTLinuxによるリアルタイム処理

まずはじめに、RTLinuxによるリアルタイム処理プログラムについて説明する。

2.1 RTLinux とは

Real-Time Linux は、Linux OS と共存可能な リアルタイムOSである。 しかし、厳密な意味ではリアルタイムOSではなく、 RTLinuxが提供するのは スケジューラとプロセス間通信のみである。 RTLinuxは、仮想マシンをLinuxに提供し、 Linuxを優先順位の低い1つのリアルタイムプロセスとして実行することで、 リアルタイム処理と従来のLinux OSの処理の共存を実現している。 このリアルタイム処理というのは ある幅で処理開始タイミングと処理終了までの時間制限を保証した処理 のことである。 RTLinuxはリアルタイムOSであり、リアルタイムOSは、 リアルタイム処理を行うマルチタスクOSである。( CPUの処理能力を越える処理は、とうぜん制約された時間内では処理できない。)

ここで、MS-DOS上で割り込み処理プログラムを走らせたのと、どう違うと 言うのだろうかという疑問もわいてくる。 たしかにタイマーカウンタのハードウエアを用意してDOSの割り込みを使えば、 DOSでもリアルタイム処理は可能である。 しかし、MS-DOSは一度に1つのプログラムしか走らせることができない(つまり、マルチタスクではなくシングルタスクである)上、 開発環境が UNIXの資産を受け継いでいるLinux に比べると遥かに貧弱である。 RTLinuxならば、リアルタイム処理を実行しながら、複数のプログラムを 実行できる上、開発環境も整っているので開発も楽にできるという強みがある。 また、タイマーカウンタ等のハードウェアを用意しなくても、 (CPUにそこまでの処理能力があるかどうかは別として) OS自体が 10-9 s ( 1ns )程度の時間制約まで守れる設計になっている。 MS-DOSやMS-Windowsといった OS の設計では 1ms 以下のサンプリングは 保証されない*1。 したがって、安価で開発環境の整っているRTLinuxの方が、 リアルタイム制御を行うのに(開発行程も含めて)向いているといえるだろう。

*1 Intel社の 80386命令アーキテクチャ AI-32 の仮想86モードが使用できないため。 MS-DOSベースのOSはAI-32のリアルモードしか使用できないが、 RTLinuxはAI-32の能力をフル活用することができる。

2.2 RTLinux カーネルの入手とインストール

RTLinuxカーネルは、非商用で開発されているいわゆる Free Soft である。 カーネルはインターネットで公開されており、www.rtlinux.org から だれでも自由に入手できる。 RTLinux のカーネルをダウンロードするときは、 現在使用中のLinuxカーネルのバージョンに合わせてダウンロードする。 例えば、カーネルが 2.2.x ならば、rtl-v2.x を選ぶ。 インストールする際には Linuxカーネルに RTLinuxのパッチを当てることになるので、 パッチ済カーネルをダウンロードしてきた方が簡単でよい (ただし、ダウンロードには時間がかかる)。 ダウンロードしてきたファイル(例えば rtlinux-2.0-prepatched.tgz ) は、展開する場所 /usr/src に展開する。 それからインストールが完了するまでの手順は、以下のとおりである。

  1. README, INSTLL, GettingStarted.txt, FAQ などの文書に目を通す。
  2. INSTALL に書いてある手順にしたがって、カーネルをコンパイルする。
    1. コンフィギュレーション
    2. コンパイル
    3. インストール、再起動
  3. man pathの追加

rtl-v2.x には RTLinux で使用する関数の man page がついている。 再起動したら、実際にRTLinuxがインストールされたことを確認するために、 rtl/examples 以下にあるサンプルプログラムを実行してみよう。

2.3 moduleを作る

RTLinxuでは、リアルタイム処理のプログラムをモジュールの形で作成するので、 まずはじめにモジュールについて説明する。 モジュールはカーネルを拡張する小さな部品である。 Linux module のソースコードは、 mainのかわりに、一組の init_module と cleanup_module という関数をもっている。 まず、モジュールプログラムの第一歩として、 何もしない hello_module.o を作ってみよう。


/*  何もしない module の作成    hello_module.c  */
#define MODULE
#include <linux/module.h>

int  init_module(void)    {  printk("Hello world !\n");  return 0; }
void cleanup_module(void) {  printk("Goodbye world \n"); }

コンパイル、実行、確認には、


[root]# gcc -c hello_module.c
[root]# insmod hello_module.o
[root]# lsmod
Module                  Size  Used by
hello_module             180   0  (unused)
[root]# rmmod hello_module

と入力すればよい。 X上のターミナルから実行したのでは何も表示されない。 printk の出力は、コンソールから実行して確認するか、 dmesg により吐き出される文字列から確認できる。

insmod,lsmod,rmmod といったコマンドは、スーパーユーザ(root) しか実行できない。 insmod により モジュールがカーネルに組み込まれると、 最初に 関数 init_module が呼び出される。 デバイスドライバ等を作成する場合は、ここでデバイスの登録や I/Oポートの設定、メモリの確保などを行う。 init_module は、普通は戻値として 0 を返し、 モジュールの初期化処理に失敗した場合は 負の値を返すことになっている。 逆に、rmmod によりモジュールがカーネルから削除されると、 関数 cleanup_module が呼び出される。

RTLinux では、このモジュールの中でリアルタイム・スレッド*2の生成、スケジューリング、実行、停止、削除等を行う。 したがって、RTLinuxにおける リアルタイム処理 のプログラムは、 大まかに言えば以下のような作りになる。

このリアルタイム処理プログラムの大まかな構造をもとに、 次はサンプルプログラムについて説明する。

*2 ここで タスク(task)プロセス(process)スレッド(thread)という概念が出てくるが、すべて実行中のプログラムという意味で理解してほしい。 厳密に言えば、プロセスは並列処理システムをCPU側からでなく、 プログラム側から考えた概念であり、 同じものをIntelのCPUアーキテクチャであるIA-32の用語ではタスクと呼ぶ。 これがカーネル内ではスレッドという処理単位になるのだが、 CPUが1つしかない場合は、視点が違うだけで同じものを指しているだけである。

(追記: プロセス、スレッド、タスク、ジョブについて 2002/05/01)

プロセスとスレッドは、(カーネルの構成要素である)スケジューラの制御対象です。 プロセスとスレッドの違いはメモリの実装方法にあります。 プロセスは独立した排他的なメモリ空間を持っていますが、スレッドのメモリ空間は独立しておらず、OSによって保護されません。 プロセスの中には1つまたは複数のスレッドが走っていますが、スレッドの中でプロセスが走ることはありません。

プロセスやスレッドをスケジューラが時間的制限を守れるかどうかから見た場合、これらをタスクといいます。タスクがプロセスとして実装されるかスレッドとして実装されるかは、OSがプロセス型のRTOSか、スレッド型のRTOSかに拠ります。

ジョブは一連の仕事をしているプロセスの集合ですが、RTOSによってはジョブでもタスクと呼ぶものがあるそうです。同じタスクが何度も起動される場合、起動されるたびに異なるジョブとして扱われます。

2.4 簡単なリアルタイム処理プログラム

こんどは実際に簡単なリアルタイム処理プログラムについて説明する。 プログラムの例として、付録Aに示すプログラムを使用する。 このサンプルプログラムはリアルタイム・スレッドを1つだけ生成し、 ユーザプログラムからこのスレッドを起動/停止し、 スレッドでリアルタイム処理されたデータをユーザプログラムに返すという、 リアルタイム処理とFIFOを用いたプログラム間通信の雛形となる プログラムである。さらに、このプログラムはスレッド内での 浮動小数点演算の実行も実現している。 このプログラムの大まかな流れを示すと、以下のようになる。

  1. timer_module.o をカーネルに組み込む
    1. RT-FIFOの生成
    2. リアルタイム・スレッドの生成とスケジューリング
    3. FIFO ハンドラの登録
  2. アプリケーションを実行
    1. RT-FIFOを開く
    2. リアルタイム・スレッドを起動する指示を FIFOを通してtimer_module に送る
    3. timer_module の FIFO ハンドラが指示を受け取り、スレッドに渡す
    4. スレッドが周期的に実行されると、データの値が計算される
    5. 変化したデータの値をFIFOを通して usr_app に送る
    6. ループが回っている間、timer_moduleからデータを受け取り表示する
    7. スレッドを停止する
  3. timer_module をカーネルから削除する
    1. FIFOを削除する
    2. スレッドを削除する

この流れに沿ってソースコードを追ってみることにする。 このプログラムのエントリーポイントとなるのは、 timer_module.c の 関数 init_module である。 コマンド insmod により timer_module.o がカーネルに組み込まれると、


  rtf_destroy(1);
  rtf_destroy(2);
  rtf_destroy(3);

  rtf_create(1, 200);
  rtf_create(2, 200);
  rtf_create(3, 1000);

の部分で /dev/rtf1, /dev/rtf2, /dev/rtf3 にRT-FIFOの生成を行う。 続いて、


  pthread_attr_init (&attr);
  sched_param.sched_priority = 4;
  pthread_attr_setschedparam (&attr, &sched_param);
  ret = pthread_create (&theThread,  &attr, thread_code, (void *)junk );

で、リアルタイム・スレッドの生成とスケジューリングを行う。 この処理により、優先順位が 4 のスレッドが生成され、 このスレッドは 関数 thread_code を実行するものとなる。


   pthread_setfp_np (theThread, 1);
  
  rtf_create_handler(COMMAND_FIFO, &my_handler); 

その後、先ほど生成したスレッドで浮動小数点演算を行うことを許可し、 FIFOハンドラーに 関数 my_handler を登録して、 関数 init_module が終了する。 つぎに、ユーザプログラムが起動すると usr_app.c の 関数 main に が始まり、


  if ((ctl = open("/dev/rtf1", O_WRONLY)) < 0) {
    fprintf(stderr, "Error opening /dev/rtf1\n");
    exit(1);
  }
  
  if ((fd0 = open("/dev/rtf3", O_RDONLY)) < 0) {
    fprintf(stderr, "Error opening /dev/rtf3\n");
    exit(1);
  }

の部分で/dev/rtf1 と /dev/rtf3 のRT-FIFOが開かれ通信可能な状態になる。 /dev/rtf1 はスレッド制御メッセージ用に書き込み専用、 /dev/rtf3 はデータ用に読み込み専用としてオープンする。 ユーザプログラムからスレッドに制御メッセージを送ると、 設定した周期でスレッドが走り始める。


  // start the thread  
  Msg.command = START_THREAD;
  Msg.period = 0.5;
  if (write(ctl, &Msg, sizeof(STMsg)) < 0) {
    fprintf(stderr, "Can't send a command to RT-thread\n");
    exit(1);
  }

ここでは周期を 0.5[s] に指定した。 ここでまた処理を timer_module の方に戻って追い掛ける。 (ただし、ここでユーザアプリケーションが一時停止するわけではない) timer_module側ではFIFOからメッセージを受け取ると、 関数 my_handler が呼び出される。


  while( (err = rtf_get(COMMAND_FIFO, &Msg, sizeof(STMsg)))==sizeof(STMsg) ){
    rtf_put (COMMAND_FIFO + 1, &Msg, sizeof(STMsg));
    pthread_wakeup_np (theThread);
  }

関数 my_handler は FIFO から STMsg の大きさだけメッセージを読み出し、 これをスレッドに引き渡す。この rtf_put の第1引数がメッセージの 引き渡し先である。引き渡し先を複数用意すれば、複数のスレッドを 制御することが可能である。 関数 my_handler からスレッドがメッセージを受け取ると、 処理がスレッドの本体である関数 thread_code に移る。

スレッドの本体

  while (1) {
    int ret;
    int err;

    ret = pthread_wait_np();    
    err = rtf_get (COMMAND_FIFO + 1, &Msg, sizeof(STMsg));
    
    if( err == sizeof(STMsg) ){
      
      switch (Msg.command) {
      case START_THREAD :
        pthread_make_periodic_np(pthread_self(), 
                                 gethrtime(), 
                                 real2in(Msg.period) );
        break;

      case STOP_THREAD :
        pthread_suspend_np(pthread_self());
        break;

      default:
        rtl_printf("RTL task: bad command\n");
        return 0;
      }
    }

    // Write something here to execute periodicaly.
    
    Data.up++;
    Data.down -= 0.5;
    rtf_put(DATA_FIFO, (char *)(&Data), sizeof(STData) );
  } // end of while

thread_code の無限ループは、普段は関数 pthead_wait_np により 次の実行周期を待機する状態になっている。 関数 my_handler から pthread_wakeup_np によりスレッドが起動されると、 スレッドは my_handler から渡されたメッセージを読みに行く。 このメッセージに基づき、 関数 pthread_make_periodic_np で スレッドの実行周期を決定して周期処理を開始する。 ここで、周期を指定している第3引数の値 real2in(Msg.period) は、 マクロ定義による関数で、指定された秒数をコンピュータの内部時間に 置き換える関数である。その実体は、


#define in2real(i)      ( (double)(i) / NSECS_PER_SEC )

のように定義している。 スレッドが動き始めると、データの値を計算してデータ構造体の中身を DATA_FIFOに送り、関数 pthread_wait_np によりCPUの使用権を放棄して 次の周期まで待つ。そして次の周期が来るとまた COMMAND_FIFO に メッセージを読みに行き、メッセージがなければデータの値を更新して データ構造体の中身をDATA_FIFOに送り・・・という処理を続ける。 スレッドが動き続けている間、DATA_FIFOにはデータ構造体の中身が 送られ続け、ユーザプログラム側では


  // main loop
  for(i=0; i<10; i++){

    FD_ZERO(&rfds);
    FD_SET(fd0, &rfds);
    tv.tv_sec = 1; 
    tv.tv_usec = 0;
    
    retval = select(FD_SETSIZE, &rfds, NULL, NULL, &tv);
    if (retval > 0) {
      if (FD_ISSET(fd0, &rfds)) {
        read(fd0, &Data, sizeof(STData) );
        printf("count : %d  %3.1f \n", Data.up, Data.down );
      }
    }
    
  }

という部分でDATA_FIFOからデータの読み出しを行うことができる。 もしここで、スレッドの周期が速いためにユーザプログラムが DATA_FIFOの中身を受け取りきらないまま、 スレッドからどんどん書込があれば、 FIFOを生成したときに指定したバッファの大きさまでは保存され、 バッファの大きさを越えると古いメッセージから破棄されることになる。 逆に、スレッドの周期が遅く、ユーザプログラムがDATA_FIFOの 読み出しをまっていてもなかなかデータが送られてこない場合は、


    tv.tv_sec = 1; 
    tv.tv_usec = 0;

で指定したタイムアウト期間だけ待ち、それでもデータが来なければ、 その回は読み出しを行わない。 このようにして、サンプルプログラムは、ユーザプログラムのメインループが 回っている間はスレッドからのデータを読み込んでは表示するという作業を 繰り返す。 メイン関数のループが終了すると 今度はスレッドに停止命令を出す。


  // stop the thread
  Msg.command = STOP_THREAD;
  if (write(ctl, &Msg, sizeof(STMsg)) < 0) {
    fprintf(stderr, "Can't send a command to RT-thread\n");
    exit(1);
  }

するとスレッドは DATA_FIFO から関数 my_mandler を介して メッセージを受け取り、


      case STOP_THREAD :
        pthread_suspend_np(pthread_self());
        break;

このようにしてスレッドを停止する。 スレッドはこの状態で停止していても、また起動するメッセージを 受け取れば、いつでも周期処理に入ることができる。 ユーザアプリケーションが終了すれば、もう timer_module は不要なので コマンド rmmod によりカーネルから削除する。 そうすると、関数 cleanup_module が呼び出され、


void cleanup_module(void)
{
  rtf_destroy(1);
  rtf_destroy(2);
  rtf_destroy(3);
 
  pthread_delete_np (theThread);  
}

使用した RT-FIFO とスレッドを削除して、サンプルプログラムの 全行程が終了する。

2.5 小まとめ

next >>