Tips of controlling robots with Linux

Linux を制御に使うためのテクニック


ここではLinuxを制御に使うためのテクニックを公開していきます。

→I/Oアクセス
→プログラムをroot権限で実行する:chmod +s
→CPU内蔵のカウンタで時間を計る:RDTSC
→プログラムの優先順位を上げる:setpriority
→プログラムの優先順位をさらにに上げる:sched_setscheduler

I/O アクセス

コンピュータの外にある何かを操作しようとしたとき、状態を取得しようと したとき、インターフェイスボードとのやり取りが必要になります。
Intel 系のCPUを積んでいるパソコンでは多くの場合 I/O ポート経由で 入出力を行うと思います。MS-DOS でも inport/outport などの関数が C言語のライブラリに用意されていて、これで制御を行っていました。

Linux の場合はというとやぱり in/out の関数が存在します。入出力 それぞれ 1,2,4バイト単位で

の計6関数が <asm/io.h> で定義されています。注意点としては、 out 関数の引数がMS-DOS等で一般的なものと逆、ということでしょうか。 また、これらの関数は gccでコンパイルする場合には最適化オプション -O2 をつける必要があります。

しかし、Linuxの場合、ただ、これら命令を実行すると Segmentation fault が発生してプログラムが停止します。これは保護機能によるものです。 ユーザがかってに I/O 命令を実行できるとなると、マルチタスク・ マルチユーザのOSの信頼性はとたんに無くなります。1ユーザのいたずらで システムが容易にとまるからです。
そこで、保護を解除する必要があります。方法は2通りあります。

  1. int ioperm(unsigned long from, unsigned long num, int turn_on);
    I/Oポート空間を部分的に保護解除してアクセスできるようにします。 ただし、適応範囲が 0-0x3ff のみなので、使い勝手はよろしくありません。 アドレス from から num バイトのみ許可するので間違ってへんなところを アクセスする危険性は少ないと言えます。
  2. int iopl(int level);
    I/O空間全体をまとめてアクセスできるようにします。 iopl(3); とすることでアクセス許可になります。通常は iopl(0); です。 ひとたびこの関数を実行すると、I/Oアクセスはどこでもできるので、 アドレスを間違うと危険です。
    (中には読むだけでOSがフリーズするポートもあったり...)
これらは <unistd.h> で定義されています。

さらに、この保護解除は root 権限でなければできないようになっています (一般ユーザができたら、保護でもなんでも無くなります)。
ただし、一度確定したプログラムを一般ユーザがroot権限で 実行することは可能で、後述します

結果的に、I/O アクセスを Linux のプログラムで行うには

  1. ioperm/iopl で保護解除して
  2. inb/inw/inl, outb/outw/outl で入出力、
  3. 実行は root で行う
となります。

なお、メモリに関しては事実上直接は操作できません(/dev/mem で ある程度可能)。メモリ関係のハードをアクセスするには、 メーカがデバイスドライバを供給してくれることを待つか、 自作する必要があります。


プログラムをroot権限で実行する:chmod +s

開発段階では root でいちいち実行するにしても、ひとたびプログラムが 完成した場合に、su で root になるのが面倒、ということがあります。 そのようなときには chmod +s を使います。これは実行時に、 実行ファイルの所有者の名義で実行する、というものです。 すなわち、実行ファイルの所有者を root にして、chmod +s しておけば、 だれが実行してもrootと同じ権限が得られるというものです。

具体例を示します。

% ls -l aaa
-rwxrwxr-x   1 kumagai  users        3976 Sep 18 16:24 aaa
% su
Password: 
# chown root:root aaa
# ls -l aaa
-rwxrwxr-x   1 root     root         3976 Sep 18 16:24 aaa
# chmod +s aaa
# ls -l aaa
-rwsrwsr-x   1 root     root         3976 Sep 18 16:24 aaa
まず、root で chown することでファイルの所有者をrootにします。 次に chmod +s で実行時set userID 属性をつけます。ファイルの 属性の 'x' だったところが 's' になります。

この操作で、いちいち root にならなくとも、I/O 操作のプログラム などを実行できるようになります。この set userID はよくみると /usr/bin などの多くのプログラムで使用されています。しかし、 だれでも root 権限で実行できるようになるということは、ある意味 危険なので、十分注意する必要があるでしょう。


CPU内蔵のカウンタで時間を計る:RDTSC

現在の経過時間を測定する方法の手軽なものとして、gettimeofday() が あります。これはマイクロ秒単位で現在時間を得ることができます (ただし、time() で得られる時間を等分したようなものなので、 絶対的な精度は当然ありません)。
手元の Celeron 400MHz のパソコンで試したところ、関数が帰ってくるまで 1マイクロ秒程度しかかからないようです。

Pentium 以降のCPUの場合には、もう1つ、CPU の内部カウンタを利用する、 という方法があります。実際のところは、Linux が gettimeofday の 時間を得るのにも、この内部カウンタをつかっています。

この内部カウンタを使用する利点は2つあります。第1に1クロックで 済むような命令を使うため、gettimeofday に比べ処理に時間が かからないこと。第2にgettimeofdayは秒以上の桁と秒未満の桁が 別の変数(をまとめた構造体)として帰ってくるため、両方まとめる 処理が必要であるのに対し、64bitの一つの数値として 扱うことができる、というものです。逆に欠点として、CPUの動作 クロックに直接依存するため、変換および変換定数の調整が 必要なことです。

この内部カウンタはCPUのクロックそのものをカウントしています。 400MHzのCPUでは1秒に約400*10^6だけカウントします。 カウンタの大きさは64bitです。こういうものを使うときには いつあふれるか?ということが重要な問題になりますが、試算すると 400MHz の場合で1000年以上もちます。つまり、常識の範囲では あふれません。

このカウンタを読み出す関数はライブラリには存在しないので、 インラインアセンブルで直接Pentiumの命令を仕込みます。 最近のアセンブラを使用すると rdtsc と書くだけですむのですが、 ここでは互換性を考えて、コードを直接埋めます。

inline unsigned long long int RDTSC(void)
{
  unsigned int h,l;
  /* read Pentium cycle counter */
  __asm__(".byte 0x0f,0x31"
          :"=a" (l),
          "=d" (h));
  return ((unsigned long long int)h<<32)|l;
}
ここで unsigned long long int は64bitの符合無し整数の型です。 __asm__ の部分でクロックの読み出しを行い、32bit づつ C言語側に 持ってきて、結合します。

変換係数はたとえば、こんなプログラムで 求めることができます。


プログラムの優先順位を上げる:setpriority

Linuxを制御に使う方法のところでも、 たびたび、実行の優先順位を上げる、という話がでて、そのとき nice というコマンドをrootで使用していました。しかし、実際に プログラムが完成し、いろいろパラメータを変えたりして実験する 場合には、いちいち nice を使うのが面倒です。そんな場合には、 プログラムに直接優先順位を指定する機能をつけておくと便利です。 これは setpriority という関数で行うことができます。
マニュアルより引用
NAME
       getpriority, setpriority - get/set program scheduling priority

SYNOPSIS
       #include 
       #include 

       int getpriority(int which, int who);
       int setpriority(int which, int who, int prio);
使い方は setpriority(PRIO_PROCESS,0,prio); と、対象に PRIO_PROCESS を指定の上、優先度を設定します。prio は nice の 数値と同様 0 で標準、20で最低、 -20で最優先になります。 負の数値は root の権限がなければ設定できませんので、 前述のように実行時 set user ID 属性を つけておくとよいでしょう。

プログラムの優先順位をさらにに上げる:sched_setscheduler

RT化されてない Linux でロボット制御を行う、という内容で ロボット学会で発表したところ、発表後の討論で、「Linux そのものの リアルタイムスケジューラを使ってはどうか」とのご示唆を頂きました。 早速、大学にもどって、調べたところ、sched_setscheduler という 関数を使用することで、通常のプロセスよりさらに優先度が上がることが 確認できました。

具体的には

#include 
 
        :
  struct sched_param sp;
  sp.sched_priority=99;
  sched_setscheduler(0,SCHED_FIFO,&sp);

という部分を追加すれば、さらに優先順位があがります。 sp.sched_priority には 1-99(最大) の優先順位を指定できます。 SCHED_FIFO のかわりに SCHED_RR を使用してもほとんど同じです (同じsched_priority のプロセスがあった場合の動作が若干異なります)。 通常は SCHED_OTHER になっています。

プロセスの優先度を SCHED_FIFO, SCHED_RR で指定した場合、ふつうの 特に指定していないプロセス(SCHED_OTHER)すべてより優先度が高くなります。 そのため、実験例で示したような周期安定性も、 よくなり、周期をはずすことも、1万回に1〜2回程度とさらに少なくなります。 依然としてはずすことがあるのは、プロセスよりも優先される、 ハードウェアの割り込み処理などの影響と考えられます。 処理内容によっては setpriority では効果が 足りない場合があり、その場合には、この方法が役立つことでしょう。

ただし、重要な注意点がひとつあります。それは、ふつうのプロセス すべてより優先度が高くなるということは、usleep などでOSに CPUを明示的に返さずに無限ループした場合(おそらく意図的には やらないとおもいますが)、kill を実行することはおろか、 kterm 上でControl-Cで止めようにも、X やシェルすら動作しない、という 状態が発生します。OSとしては動いているので、致命的な障害は 発生しないと思われるものの、シャットダウンしなければ戻りません。 十分注意する必要があるでしょう。

補足:
Linuxによる制御の実践のところで述べたような 周期変更を行う場合、注意が必要です。というのは、SCHED_FIFO などの プロセスがふつうのプロセスより優先される、という機構は 優先順位に下駄を履かせることによって実現されているのですが、 この下駄がたりなくなるためです。もし、HZを1000以上にした上で sched_setscheduler を使用する場合には、/usr/src/linux/kernel/sched.c の goodness という関数の最初のほうの

        if (p->policy != SCHED_OTHER) {
                weight = 1000 + p->rt_priority;
                goto out;
        }
の1000を10000などに上げる必要があります。

サンプルプログラム:


非RT Linux によるロボット制御
熊谷正朗/くまがいまさあき/Masaaki Kumagai
kumagai@emura.mech.tohoku.ac.jp