1プロセスで複数周期
基本的に休眠をつかって周期実行をするこの手法は、1プロセス1周期が原則です。 もちろん、休眠時間をうまく設定してやれば複数周期のループを導入することは可能ですが、少しばかり面倒です。 そこで、1つのプロセスで、複数の周期を利用するのに便利なモジュールをつくってみました。

複数周期を利用するために開発したものですが、もちろん単周期でも使用可能です。 そのときは、最低タイマ割り込み2周期分という制限がなくなって、1周期単位から設定できるようになり、起きるべき時間をカーネル内部でチェックしてくれるようになるので、制御処理に要する時間を測定する必要もなくなります。

原理的には、カーネルにデバイスドライバとして、周期の面倒をみてくれる機能を追加します。

制御などを実行すべき時刻が来たかどうかをプロセスに通知する機能として、select() を使用しました(poll()も可)。 これは、複数のデバイスの読み書きの準備情況を待機し、準備が出来次第もどってくる関数です。 たとえば、キー入力待ちや、ネットワークからの情報到着待ちをすることが可能です。 これをつかって、次の動作を開始すべき時刻をプロセスに通知するようにしました。 select()をつかって周期実行まで待機させる利点は、その他デバイスとの共存にあります(Enterキーを押すと終了とか)。

実際にこの方法をつかうにはモジュールをカーネルに登録し、アクセスのための特別ファイル(/dev/なんとか、の類い)をつくらなければなりません(詳細は後述)。
登録したら、実行するプログラムでは open()でこのファイルを開き、ioctl()で周期設定をします。 あとは、ループの中で select()を実行し、戻ってきたら、その返り値を判定して、各周期に対応した処理を実行します。

  • モジュールとは、Linuxのカーネルに機能を追加するためのもの・仕組みです(組み込むものそのものを指すほうが一般的)。 最近は多くのデバイスドライバがこの形で供給され、必要に応じて組み込むスタイルが多くなってきました。 insmod モジュール名でモジュールを組み込み、rmmod モジュール名で取り外します。
    →参考:モジュールとは@デバイスドライバの作り方
  • デバイスドライバとは、ユーザのプログラムに代ってハードウェアにアクセスするためのものですが、このあたりのページでは、デバイスとしてカーネルに登録するもの全般を指しています。 そのため、今回のような、ただプロセスの実行タイミングを決めるだけでも、デバイスドライバです(^^;。
    デバイスにアクセスする場合には、メジャー番号・マイナー番号という数値が使用されます。 メジャー番号でドライバを識別し、マイナー番号で内部の機能を選択します。 アクセスする場合には直接この数値を指定するのではなく、予め、特殊なファイルをつくっておきます。 これが開かれたとき、デバイスへのアクセスを意図したものとLinuxが解釈し、適当に処理してくれます。 /dev/ には多くの特殊ファイルが作成されています。
  • selectはすでに述べたように、デバイスの読み書きの待機をするための関数です。 具体的には、読む準備の確認をするファイル記述子の一覧、書く準備、その他(デバイス固有)準備を指定して使用します。 どれか一つでも準備ができると、戻ってきて、準備ができたものの一覧を返します。 また、一定時間経過してももどって来ない場合に断念するための制限時間も設定可能です。
以下に、実際に用いるドライバのソースと、それを利用する周期実行プログラムのソースを示します。 Linux 2.0.30/2.2.10で動作を確認しています。 // ドライバ // gcc -c cyclesel.c -Wall -Wstrict-prototypes -O -pipe -m486 // -DUSE_RDTSC をつけると RDTSC利用:プログラムとそろえること // mknod /dev/csel c 60 0; chmod a+rw /dev/csel #define MODULE #define __KERNEL__ #include #include #include #include #include #include #if LINUX_VERSION_CODE >= 0x020100 #include #include #else static inline unsigned long copy_to_user(void *to, const void *from, unsigned long n) { memcpy_tofs(to,from,n); return 0; } static inline unsigned long copy_from_user(void *to, const void *from, unsigned long n) { memcpy_fromfs(to,from,n); return 0; } #endif static int devmajor=60; static char *devname="cyclesel"; static int basecycle=1; #if LINUX_VERSION_CODE > 0x20115 MODULE_PARM(basecycle, "i"); MODULE_PARM(devmajor, "i"); MODULE_PARM(devname, "s"); #endif // ファイルごとの管理領域 typedef struct { unsigned long f_version; // 識別用 f_version struct wait_queue *wait; // 休眠待機用領域 int sleep_mode; // 0: 起きてる 1: 待ち unsigned long long wtime; // 最後に起こした時刻 unsigned long wakeup_jiffies; // 起きるべき時間 unsigned int period; // 周期 } FileInfo; #define MAXFILE 30 static FileInfo fi[MAXFILE]; // 時間取得関数: unsigned long long get_time(void) { #ifdef USE_RDTSC // RDTSC 直読み unsigned int h,l; /* read Pentium cycle counter */ __asm__(".byte 0x0f,0x31" : "=a" (l),"=d" (h)); return ((unsigned long long int)h<<32)|l; #else // gettimeofday 使用 struct timeval tv; do_gettimeofday(&tv); return (unsigned long long)(tv.tv_sec)*1000000+tv.tv_usec; #endif } // Linux 2.0/2.2 共通 static int cycsel_open(struct inode * inode, struct file * file) { int i; printk("cycsel_open: (%ld)\n",file->f_version); for(i=0;if_version; fi[i].sleep_mode=0; fi[i].wait=NULL; fi[i].wakeup_jiffies=jiffies; fi[i].period=MINOR(inode->i_rdev)*basecycle; // マイナで周期設定 if(fi[i].period==0) fi[i].period=HZ; // デフォルト秒1 file->private_data=fi+i; // プライベートデータに構造体ポインタ設定 MOD_INC_USE_COUNT; file->f_pos=0; // 読み出し 書き込み 位置を 0 return 0; } // Linux 2.1 以降帰り値 int (事実上共通でも可: カーネル内で返り値使わず) #if LINUX_VERSION_CODE >= 0x020100 static int cycsel_close(struct inode * inode, struct file * file) #else static void cycsel_close(struct inode * inode, struct file * file) #endif { printk("cycsel_close: (%ld)\n",file->f_version); MOD_DEC_USE_COUNT; ((FileInfo *)(file->private_data))->f_version=0; ((FileInfo *)(file->private_data))->sleep_mode=0; ((FileInfo *)(file->private_data))->wtime=0; #if LINUX_VERSION_CODE >= 0x020100 return 0; #endif } // poll / select : 2.2 では poll. 2.0 では select #if LINUX_VERSION_CODE >= 0x020100 static unsigned int cycsel_poll(struct file *file, struct poll_table_struct *ptab) { FileInfo *fip=(FileInfo *)(file->private_data); poll_wait(file,&(fip->wait),ptab); fip->sleep_mode=1; if(fip->wakeup_jiffies <= jiffies) { fip->sleep_mode=0; fip->wakeup_jiffies= // 次に起きる時間 jiffies+(fip->period)-(jiffies-fip->wakeup_jiffies)%(fip->period); return POLLIN|POLLRDNORM; // 読み込みOK } else return 0; } #else // 2.0 系用 static int cycsel_select(struct inode *inode, struct file *file, int sel_type, select_table *stab) { FileInfo *fip=(FileInfo *)(file->private_data); if(sel_type==SEL_IN) { select_wait(&(fip->wait),stab); fip->sleep_mode=1; if(fip->wakeup_jiffies <= jiffies) { fip->sleep_mode=0; fip->wakeup_jiffies= // 次に起きる時間 jiffies+(fip->period)-(jiffies-fip->wakeup_jiffies)%(fip->period); return 1; // 読み込みOK } else return 0; } return 0; } #endif // (少なくとも現時点で)他のとだぶらないタイミングを見つける static unsigned long next_spare(void) { int i,j; for(j=0;j<20;j++) // とりあえず、この先20を想定 { for(i=0;iprivate_data))->wtime),8); return 0; case 2: // 周期の設定 if(arg==0) return -EINVAL; ((FileInfo *)(file->private_data))->period=arg; return 0; case 3: // 周期を意図的にずらす ((FileInfo *)(file->private_data))->wakeup_jiffies=next_spare(); default: return 0; } } #if LINUX_VERSION_CODE >= 0x020100 static struct file_operations cycsel_fops = { // Linux 2.2.10 より NULL, // loff_t llseek(struct file *, loff_t, int) NULL, // ssize_t read(struct file *, char *, size_t, loff_t *) NULL, // ssize_t write(struct file *, const char *, size_t, loff_t *) NULL, // int readdir(struct file *, void *, filldir_t) cycsel_poll, // u. int poll(struct file *, struct poll_table_struct *) cycsel_ioctl, // int ioctl(struct inode *, struct file *, u.int, u.long) NULL, // int mmap(struct file *, struct vm_area_struct *) cycsel_open, // int open(struct inode *, struct file *) NULL, // int flush(struct file *) cycsel_close, // int release(struct inode *, struct file *) NULL, // int fsync(struct file *, struct dentry *) NULL, // int fasync(int, struct file *, int) NULL, // int check_media_change(kdev_t dev) NULL, // int revalidate(kdev_t dev) NULL, // int lock(struct file *, int, struct file_lock *) }; #else static struct file_operations cycsel_fops = { // Linux 2.0.36 より NULL, // int lseek(struct inode *, struct file *, off_t, int) NULL, // int read(struct inode *, struct file *, char *, int) NULL, // int write(struct inode *, struct file *, const char *, int) NULL, // int readdir(struct inode *, struct file *, void *, filldir_t) cycsel_select,// int select(struct inode *, struct file *, int, select_table *) cycsel_ioctl, // int ioctl(struct inode *, struct file *, u.int, unsigned long) NULL, // int mmap(struct inode *, struct file *, struct vm_area_struct *) cycsel_open, // int open(struct inode *, struct file *) cycsel_close, // void release(struct inode *, struct file *) NULL, // int fsync(struct inode *, struct file *) NULL, // int fasync(struct inode *, struct file *, int) NULL, // int check_media_change(kdev_t dev) NULL, // int revalidate(kdev_t dev) }; #endif // 周期 static void cyclefunc(unsigned long /*dummy*/); static struct timer_list cyclefunc_list = {NULL, NULL, 0, 0, cyclefunc}; /* { NULL, NULL, 終了時間, 関数呼出引数, 呼出関数 } */ static void cyclefunc(unsigned long c) { int i; unsigned long long wtime=get_time(); for(i=0;i=fi[i].wakeup_jiffies) { fi[i].wtime=wtime; fi[i].sleep_mode=0; wake_up_interruptible(&(fi[i].wait)); // 起こす } } /* タイマ再登録 */ cyclefunc_list.expires = jiffies + basecycle; cyclefunc_list.data = c+1; add_timer (&cyclefunc_list); } int init_module(void) { int i; for(i=0;i // gcc -o cyclesel_user cyclesel_user.c // -DPRINT をつけてコンパイルすると状況を表示するようになる // -DUSE_RDTSC をつけると RDTSC利用:ドライバとそろえること #include #include #include #include #include #include #include #include #include // 時間取得関数: unsigned long long get_time(void) { #ifdef USE_RDTSC // RDTSC 直読み unsigned int h,l; /* read Pentium cycle counter */ __asm__(".byte 0x0f,0x31" : "=a" (l),"=d" (h)); return ((unsigned long long int)h<<32)|l; #else // gettimeofday 使用 struct timeval tv; gettimeofday(&tv,NULL); return (unsigned long long)(tv.tv_sec)*1000000+tv.tv_usec; #endif } #ifdef USE_RDTSC // 1ミリ秒あたりの数字 #define TickPerMSEC 366000.0 // CPU動作周波数 #else #define TickPerMSEC 1000.0 // 1ミリ/1マイクロ秒 #endif #define NC 10 struct cdata { int ch; /* チャネル */ unsigned long long utime; /* ユーザ空間で測定時間 */ }; int main(int argc,char **argv) { int fd[NC],nc,r,i,j,maxfd=0; struct cdata crec[10000]; int ccrec=0; setpriority(PRIO_PROCESS,0,-20); //{ struct sched_param sp; sp.sched_priority=99; //sched_setscheduler(0,SCHED_FIFO,&sp); } if(argc<2) { printf("cyclesel_user ...\n"); return 1; } nc=argc-1; if(nc>NC) nc=NC; // 初期化 for(i=0;i10000-nc) break; #ifdef PRINT printf(" at %lld delay: %lf [ms] (%lld ticks)\n",wtime, (utime-wtime)/TickPerMSEC, utime-wtime); #endif } for(i=0;i これらのプログラムのコンパイルおよび実行は以下のようになります。 % gcc -c cyclesel.c -Wall -Wstrict-prototypes -O -pipe -m486 % gcc -o cyclesel_user cyclesel_user.c -DPRINT % cat /proc/devices Character devices: 1 mem 2 pty 3 ttyp 4 ttyS 5 cua 7 vcs 10 misc 60が空いていることを確認 128 ptm 136 pts % su # insmod cyclesel # insmod cyclesel devmajor=60 (60が空いていないときは空いている数字を指定) # mknod /dev/csel -m 666 c 60 0 % ./cyclesel_user 20 40 60 80 普通のLinuxなら 2 4 6 8 set period of cycle0 to 20 set period of cycle1 to 40 set period of cycle2 to 60 set period of cycle3 to 80 0 1 2 3 at 0 delay: 955605636100.333008 [ms] (955605636100333 ticks) 0 at 955605636111503 delay: 7.241000 [ms] (7241 ticks) 0 1 at 955605636131491 delay: 0.025000 [ms] (25 ticks) 0 2 at 955605636151490 delay: 0.035000 [ms] (35 ticks) 0 1 3 at 955605636171493 delay: 0.040000 [ms] (40 ticks) 0 at 955605636191492 delay: 0.041000 [ms] (41 ticks) 0 1 2 at 955605636211491 delay: 0.040000 [ms] (40 ticks) 0 at 955605636231492 delay: 0.040000 [ms] (40 ticks) : 0 1 3 at 955605636891493 delay: 0.039000 [ms] (39 ticks) 0 at 955605636911492 delay: 0.040000 [ms] (40 ticks) 0 1 2 at 955605636931491 delay: 0.041000 [ms] (41 ticks) エンターキー key-input detected writing 'cycle0.xy' writing 'cycle1.xy' writing 'cycle2.xy' writing 'cycle3.xy' % モジュールについてはデバイスドライバに関する知識が要されるので、ここでは解説を省きます(ブラックボックスとして使用してもかまわない でしょう)。

周期実行プログラムは、起動時に引数を10個まで指定できます。 それぞれを周期とした周期実行がなされます。周期の単位は「1タイマ割り込み周期」です(普通のLinuxは2で20ms, 1k-Linuxで20=20ms)。 このプログラムはエンターキーで停止します。 または、合計で10000周期しても止ります。 最後にデータファイルに実測した周期を出力します(cycle0.xy-cycle?.xy, 第1カラム:時刻, 第2カラム:周期)。

周期を20[ms]にしてしまうと、ほとんど直線しか見えませんので、ここでは2,4,6,8[ms]にした結果例を示します。

(生データファイル: cycle0.xy, cycle1.xy, cycle2.xy, cycle3.xy )
1つの時の例同様、ほぼ一定の周期が保たれつつ、ときどき大きく周期がはずれます。 これまでの例と異なるのは、遅れた分は次回に早くなることです。 これは、ドライバ内部での処理の実装によっています。
このドライバは1周期につき1つopen()して使います。 open()の返り値である、ファイル記述子fdがこのあとこの周期の識別番号になります。

次に、ioctl()で設定します。 ioctl(fd,1,unsigned long long *wtime);   最後に起こされた時刻(マイクロ秒単位orタイムスタンプ単位) ioctl(fd,2,unsigned long cycle);   周期をタイマ割り込みcycle分に設定 ioctl(fd,3);   複数周期分散 最初の形式は最後に起こされた時刻を返します。 -DUSE_RDTSC をつけてモジュールをコンパイルした場合はPentium以降のCPUに搭載されたタイムスタンプカウンタ(リセット時に0で以後クロック毎に カウントアップ)の値で、つけてない場合は現在時刻をマイクロ秒単位で返します(gettimeofday()の秒、マイクロ秒成分を合成)。

2つ目の形式でこのファイル記述子を使用した場合の周期を設定します。 時間単位はタイマ割り込み周期です。

3つ目の形式は周期をちらす役目を持ちます。上の実行例では、公約数2を持つ周期群を設定しましたが、周期の基準点がopenした時刻であるため、同時にopenすると各実行タイミングがそろいます(0,1,2,3が1行に表示)。 それぞれの周期に制御動作などを割り当てた場合、その処理順によって周期にばらつきが出かねません。 それに対して、同時に複数の周期のタイミングが揃わないようにすれば、それぞれの処理をタイマ割り込みの直後に実行できるはずです(2,4,6,8だと(理論的に)むりですが、3,6,9では可能)。 この場合に、3つ目の形式を実行すると、適当にちらしてくれます(ちゃんとずれるように探すので乱数よりまともです:-)。が、均等にはなりません)。 なお、これは単一プロセスではなく複数プロセスからそれぞれ周期を設定した場合にも効果があります。 それ以前に、各周期設定は完全に独立なので、同じプロセスでも、異なるプロセスでも動作はかわりません。

設定したら、周期実行のループをつくります。基本的に、whileループなどの中で、selectで各周期用のfdをまとめて読み込み待機します。 unsigned long long utime,wtime; fd_set fds; struct timeval tv; FD_ZERO(&fds); // select の準備 for(i=0;i この例はselect()の忠実な使い方です。 fd_set型の変数に FD_ZERO(), FD_SET()で待機するファイル記述子を設定します。 最後に0をつけくわえているのは、標準入力(stdin)もまとめて待機するためです。 ネットワークにも応答する必要があるなら、ここにはソケットも同じようにならべます。 tvはselectの待機の上限時間設定です。 今回のような使い方の場合、タイムアウトは発生しないはずです。 発生したら、ドライバがおかしいなどの異常事態です。

ここまで準備した上でselect()を実行します。 書き込み・特別用途の待機はしませんので、NULLを設定しました。 maxfdはselectで待機しているファイル記述子群のなかで、一番数字の大きなものです(selectがそういう数字を要求する仕様)。 いずれかの周期が終了して、ドライバが実行タイミングを通知した場合や、標準入力から入力がくると、select()は、「準備ができた数」を返してもどってきます。

返り値が負の場合、普通はエラーですが、シグナルのこともあります。 返り値が0の場合はタイムアウトしています。返り値が正の場合は渡したファイル記述子のいずれかの準備が完了しています。 どの記述子の準備が出来たかを知るには、select()に渡したfd_set型の変数を調べます(selectが書き換えています)。 具体的には FD_ISSET(fd,fd_set *)を使います。 この例では0ならループ終了、それ以外なら、各周期設定ごとの処理として、時刻の記録と画面表示(-DPRINT指定時)を行っています。 実際には、ここで制御ルーチンなどを呼ぶことになるでしょう。 このように、ループの中が若干複雑になりますが、usleepのかわりにドライバとselectを使うようにして、複数周期に対応するとともに、実行間隔の平均値の確保が可能になります。 標準では、1タイマ割り込み毎にselectをしているプロセスのチェックをしますが、もっとチェックを荒らくしてもいい、という場合、insmod するときに オプションで # insmod cyclesel basecycle=10 とbasecycleを指定すれば、タイマ割り込みその数ごとに、チェックします。 周期自体は ioctl などでタイマ割り込み1つ単位で設定できますが、チェックがあらくなるので、妙な周期になります(長時間の平均値はかわりませんが、基本的にbasecycle単位になります。設定によっては2種類の周期がでます)。

ioctl()によって周期は設定できますが、その他にデバイスのマイナー番号をつかうと、ioctl使わずに周期を最初から設定できます。 特殊ファイルをつくるときに # mknod /dev/csel -m 666 c 60 0 # mknod /dev/csel1 -m 666 c 60 1 # mknod /dev/csel2 -m 666 c 60 2 # mknod /dev/csel3 -m 666 c 60 3 とすると、/dev/csel3open()すると 初期状態で タイマ割り込みbasecycle*3の周期になります。 マイナー番号は 255 まであるので、一応、そのくらい設定できます。なお、0の場合は周期1秒です。

コンパイル時のオプションに -DUSE_RDTSC があります。 これをつけない場合、ドライバもプロセス側も時刻情報の取得にはgettimeofday()を使います(応答速度をみるためだけのものですので、周期実行には関係ありません)。 Linuxのソースのコメントを信じるならマイクロ秒単位の精度があるそうです。それに対して、-DUSE_RDTSCをつけてコンパイルした場合、時刻の基準をCPUのクロックに求めます。 500MHzのCPUなら、分解能が2nsあることにはなります(そんなに要らないですが)。 ただし、Pentium以降のCPUでしかつかえませんし(いまどきPentiumより前が珍しいですが:-))、 CPUの速度に応じて実時間への変換係数を設定する必要があります。
前者は実時間の数値がそのまま得られ、汎用性に優れるのですが、プロセス側からはシステムコールを呼ばなければならず、時間が必要です(わずかですが)。 それに対して、後者は1クロックの命令一つで済むので非常に高速ですが、変換係数を設定しなければならず、汎用性が低下します。 固定のシステムでは後者をつかうといいとはおもいます。
いずれにせよ、今回のように複数の部分で時刻情報を使いたい場合は統一しなければならないでしょう。 このドライバは、本来Linuxにある制限の「最低2タイマ割り込み周期でしか周期実行できない」を回避できます。 この制限は、そもそも、usleepなどするとき、最低1周期分は休眠することによります。 そのため、1周期の9割くらいの時間で処理して短時間sleepしてタイマ割り込みを回避して優先順位を保ちつつ、また処理をする、というpsなどでみえるCPUの利用率が0%なのに、ほとんどCPUを使いっぱなし、という悪どいことができません。 が、このドライバをつかうと、それが原理的には回避できてしまうはずです。やって得するかどうかはわかりませんが(^^;。

以上のように、ちょっとした小細工を加えることで、キー入力やネットワーク、その他デバイスを待ちながら、あわせて複数の周期実行をこなす、といったプログラムをつくることが可能です。 主たる制御プログラムに、やたらと機能を追加することは素のLinuxのスケジューリングにたよる本手法では、できれば避けたいところですが、こういうことも可能、という例として示しておきます。