周期実行の手法拡張

[| ]  最終更新: 2023/02/14 18:28:40

もっと細かな周期を設定したい

この節では、10[ms]単位に制限されていた周期をより細かくするための手法とその効果について述べます。

カーネルの改造と再構築

このためには、カーネルの再構築が必要になります。カーネルの再構築手順はディストリビューションによって、異なりますので、それぞれのディストリビューションに固有の方法をご確認下さい。 もっとも古典的な方法では、Linuxのソースディレクトリ(/usr/src/linux) で、

% make clean; make dep
% make zImage(bzImage)
(% make modules; make modules_install)
# cp arch/i386/zImage /boot/zImage.test
# vi /etc/lilo.conf
# lilo
な手順だと思います。 古い本によると、いきなり make zlilo とかで自動的につっこむところまでやってしまう解説もありますが、新しいカーネルをつくったときは、既存のカーネルを残して別のラベルをつけて lilo したほうがいいと思います。
(うちのコンピュータは必ず zImage.t なるカーネルと、t なる起動時のラベルと。実験機には k=1k があります)。

さて、より細かな周期の設定は原理をみれば明らかで、たとえば、1[ms]単位で設定したいときは、タイマ割り込みの周期を1[ms]に すればいいわけです。

実際の修正個所ですが、/usr/src/linux/include/asm/param.h

#ifndef _ASMi386_PARAM_H         (Linux 2.2.13より)
#define _ASMi386_PARAM_H

#ifndef HZ
#define HZ 100
#endif

#define EXEC_PAGESIZE   4096

#ifndef NGROUPS
#define NGROUPS         32
#endif

#ifndef NOGROUP
#define NOGROUP         (-1)
#endif

#define MAXHOSTNAMELEN  64      /* max length of hostname */

#endif
の定数 HZ を変更します。 デフォルトで100になっており、周期100[Hz]=10[ms]を意味します。ので、1[ms]=1000[Hz]でこれを1000にします。

以上でカーネルの改造は終了です。カーネルを再構築して、lilo設定して再起動してください。 なお、モジュールの再構築・インストールはしたほうがいいでしょう。このHZに依存するモジュールがわりとあります。 そのままモジュールをインストールすると、いままでのと混じる心配がありますので、ご注意下さい。 環境によっては、Makefile の頭の EXTRAVERSION に "-1k" とでも書くと、標準と異なるディレクトリにインストールされ、区別されるようです。

いちど、問題なく日常生活がおくれることが確認されたら、そのままそれを主環境にしてしまってもいいとおもいます。 現に私は長いことそんなカーネルで生活していましたが不具合はありませんでした。 ちょっとはパフォーマンスが落ちているはずですが(その分は少しクロックを上げて...)。

なお、以後、「HZを1000にしたカーネル」といちいち言うのも面倒なので「1k-linux」(いちきろ りなっくす/りぬくす)と表記します。

改造による結果

実際に、1k-linuxで同じプログラムを走らせてみましょう。 前と同じプログラムで、今度は5msの休眠を指定します(nice --20 ./cycle 5)。
結果を右図に示します。
5ms sleep on 1k-linux
まれに、図のように 1[ms]遅れることがありますが、基本的には6[ms]周期で実行されていることが確認できます。 ちなみに、cycleのデフォルトの15[ms]のままやると、16[ms]あたりになります。

このように、カーネルの定数を1つ変えただけで、普通の制御をするには十分な単周期で安定した実行が可能となります。

原理的にはHZを大きくしていけばより細かくなるはずですが、1000程度で留めておくことをおすすめします。 この先はどんどんパフォーマンスが低下し始めるはずですし、そもそも、そこまで細かくしても、この手法だとばらつきが大きくなりそうです。 もし、10k以上をねらうなら、やはり、RT-Linuxの導入を検討すべきでしょう。


もっと周期実行を確実にしたい

優先度の向上

原理のあたりなどでも述べましたが、本手法は基本的には確実性は保証されません。ときどき周期をはずします。 しかし、優先度を向上させることで、状況にもよりますが、性能の向上を図ることが可能です (特に他の休眠プロセスとの差別化を図るためには少しでも上げておくべきです)。 また、複数のプロセスを周期実行する場合、どのプロセスを優先するかを決めるときにも優先度の指定が必要です。

優先度をあげるためには、1つのコマンド、2つの関数があります。

nice コマンド : 優先度を上げ下げする
  nice -(優先度) 実行コマンド および その引数

#include <unistd.h>
int nice(int inc); システムコール
  inc: 優先度の上下分

#include <sys/time.h>
#include <sys/resource.h>

int setpriority(int which, int who, int prio);
  which: PRIO_PROCESS(プロセス), PRIO_PGRP(プロセスグループ)
         PRIO_USER(ユーザ)
  who:   それぞれのIDを指定。0 なら呼んだプロセスIDなど
  prio:  優先度
これらは共通して優先度(Scheduling priority)を操作します。 nice は優先度の上下を行います(現在値に対する相対指定)。 setpriority は絶対値を指定します。優先度は −20〜+20で指定され、小さい方が優先度が高くなります(大きい方が"nice")。 いずれの関数も負の値を指定する(=優先度を上げる)には root の権限が必要となります。 とりあえずは、プログラムの頭にでもsetpriority(PRIO_PROCESS, 0, -20);と書いて、コンパイルしてrootで実行するか、
# chown root program;  chmod +s program
(programを実行するとき、だれがやってもrootで実行)するのがよいでしょう。

優先度の徹底的な向上

上の方法は、Linuxの普通のタイムシェアリングなスケジューラを使うため、"穏便に"優先度を上げるものです。 例え、優先度を最高にしているときに暴走させてしまったとしても、端末(kterm)などもちゃんと動作しますし、なんとでもなります。 それに対して、ここで紹介する方法は、スケジューリングの方法そのものを変え、絶対的な優先順位を指定します。

#include <sched.h>

int sched_setscheduler(pid_t pid, int policy, const struct sched_param *p);

struct sched_param {
   ...
   int sched_priority;
   ...
};
この関数で policy に SCHED_FIFO, SCHED_RR を指定した場合に、スケジューリングがかわります(普段は SCHED_OTHER)。 これらを指定すると、実行可能状態の場合には必ずふつうのプロセスより優先的に処理されます(HZがおおむね1250以下の場合:それ以上の時は 定数を書き換える必要あり)。 この状態のプロセス同士の優先度は sched_param.sched_priority の数値(0:最低〜99:最高)で決まります。

この設定の恐いところは、もし、プログラムが無限ループにはまったりしたときに、端末も動かないので kill すらできなくなります。 処理の重いプログラムを周期実行かした場合には大きな効果が得られるとおもわれますが、普段、それほどはっきりした効果がみられるわけでもないので(上の手法で十分なことが多かったです)、使ってみて効果がなさそうなら、使わない方がいいかもしれません。

なお、SCHED_FIFO と SCHED_RR の違いですが、同じ優先度のプロセスが2個あったときに、SCHED_FIFO は順に処理、SCHED_RR は時間を区切って交互に処理するようになるようです。

持ち時間計算の頻繁化

プロセスの状態によっては、持ち時間の再計算処理の回数を増やすことで、優先度が高いプロセスの実行の確実性を上げることが可能です。 そのためには、持ち時間がすぐに空になる=優先度最低のプロセスを常に走らせておきます。 優先度最低(20)のプロセスは、他のプロセスが全部休眠した状態で初めてCPUを得て、実行されます。 しかも、持ち時間がタイマ割り込み1回分しかないので、実行されるとすぐに持ち時間0。 スケジューリングの原理では実行可能な全プロセスの持ち時間が0になったら再計算、となっているので、すぐに再計算が行われます。 その結果、周期実行を含む休眠中のプロセスの持ち時間も増え、安定性が向上します。

やり方は簡単で、常に画像処理をしているようなプロセスがあれば、それの優先度を下げてやります。 そういったものが特にない場合は、ダミーのプログラムをつくって低い優先度で実行することで効果がありますが、無駄にCPUを動かすくらいなら、世界的な分散処理の研究のお手伝いをするのも手です。

ここまでの方法が"尊敬語"にあたるなら、この方法は"謙譲語"にあたります(謎)。


複数の周期実行プロセスを動かしたい

基本的に複数のプロセスを周期的に動かすこと自体は難しいことではありません。 ただし、それぞれを精度良く周期実行させるには、一つだけ条件があります。 それは周期に1以外の公約数を持たせることです。もし、周期が互いに素なら、何周期かしたところで同じタイミングで実行がかさなります。 このとき、優先度に差がなければその時の状態でどちらかが、差があれば高い方が先に実行されます。 すなわち、後で実行されることになったプロセスは前になにかはさまった時だけ実行が遅れ、周期にばらつきが生じます。

以下に実例を示します。
two cyclic process (1) two cyclic process (2)
第一の例(左or上)では周期に公倍数を持つ 5,10 を設定しました。この場合、両者とも、精度良く周期的に実行されています。 それに対して第二の例では一方がひどいことになっています。 これは周期として、5,6 を設定したものですが、その結果衝突が起って、優先度を若干下げておいた周期6のプロセスの周期が乱れました。 この時のプロセスの実行の様子を詳細に分析したものを以下に示します。
timing of two cyclic process (1) timing of two cyclic process (2)
第一の例は 3,6[ms]ですが、このとき、タイミングが一つずれることで、きれい噛み合って両者とも期待通りの周期で実行されています。 それに対して、もう一方の例では 4,5[ms]を指定しましたが、実行が連続して行われているところがあり、それによって平均周期も乱れています。

以上のように基本的には複数の周期を設定し、かつそれらを精度良く実行したい場合は公約数を持たせるようにしましょう。 なお、デュアルCPUの場合には2つまでならうまく行きそうにおもえますが、残念ながらパフォーマンス向上のための仕掛けにより、うまい具合いには なりませんでした。CPUが両方ともアイドリング(全プロセス休眠)の場合には期待通りの動作はしますが、そのような状態は期待しないほうが良いかとおもいます。



熊谷正朗 [→連絡]
東北学院大学 工学部 機械知能工学科 RDE
[| ]