複数周期を利用するために開発したものですが、もちろん単周期でも使用可能です。 そのときは、最低タイマ割り込み2周期分という制限がなくなって、1周期単位から設定できるようになり、起きるべき時間をカーネル内部でチェックしてくれるようになるので、制御処理に要する時間を測定する必要もなくなります。
原理的には、カーネルにデバイスドライバとして、周期の面倒をみてくれる機能を追加します。
制御などを実行すべき時刻が来たかどうかをプロセスに通知する機能として、
実際にこの方法をつかうにはモジュールをカーネルに登録し、アクセスのための特別ファイル(/dev/なんとか、の類い)をつくらなければなりません(詳細は後述)。
登録したら、実行するプログラムでは
以下に、実際に用いるドライバのソースと、それを利用する周期実行プログラムのソースを示します。 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 <linux/module.h>
#include <linux/kernel.h>
#include <linux/sched.h>
#include <linux/fs.h>
#include <linux/string.h>
#include <linux/time.h>
#if LINUX_VERSION_CODE >= 0x020100
#include <asm/uaccess.h>
#include <linux/poll.h>
#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;i<MAXFILE;i++)
{
if(fi[i].f_version==0) break;
}
if(i==MAXFILE)
{
printk("cycsel_open: busy\n");
return -EBUSY;
}
fi[i].f_version=file->f_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;i<MAXFILE;i++)
if((fi[i].f_version) && (fi[i].wakeup_jiffies == jiffies+j))
break;
if(i==MAXFILE) break; // あたらなかった
}
return jiffies+j;
}
// ioctl: Linux 2.0/2.2共通
static int cycsel_ioctl(struct inode *inode, struct file *file,
unsigned int cmd, unsigned long arg)
{
switch(cmd)
{
case 1: // 起こされ時間の記録
copy_to_user((void *)arg,
&(((FileInfo *)(file->private_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<MAXFILE;i++)
if((fi[i].f_version)&&(fi[i].sleep_mode)) // 待ちを
{
if(jiffies>=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<MAXFILE;i++) fi[i].f_version=0; // 未使用マーク
printk("install '%s' into major %d\n",devname,devmajor);
if(register_chrdev(devmajor,devname,&cycsel_fops)) // 登録
{
printk("device registration error\n");
return -EBUSY;
}
cyclefunc_list.expires = jiffies + basecycle;
cyclefunc_list.data = 0;
add_timer(&cyclefunc_list);
return 0;
}
void cleanup_module(void)
{
printk("remove '%s' from major %d\n",devname,devmajor);
if(unregister_chrdev(devmajor,devname))
printk ("unregister_chrdev failed\n");
del_timer(&cyclefunc_list);
};
// gcc -o cyclesel_user cyclesel_user.c
// -DPRINT をつけてコンパイルすると状況を表示するようになる
// -DUSE_RDTSC をつけると RDTSC利用:ドライバとそろえること
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <sched.h>
#include <sys/resource.h>
// 時間取得関数:
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 <cycle0> <cycle1> ...\n");
return 1;
}
nc=argc-1;
if(nc>NC) nc=NC;
// 初期化
for(i=0;i<nc;i++)
{
fd[i]=open("/dev/csel",O_RDWR);
if(fd[i]<0)
{ printf("cannnot open /dev/csel0\n"); return 1; }
}
for(i=0;i<nc;i++)
{
if(!ioctl(fd[i],2,atoi(argv[i+1])))
printf("set period of cycle%d to %d\n",i,atoi(argv[i+1]));
//ioctl(fd[i],3); // ずらし設定
if(maxfd<fd[i]) maxfd=fd[i];
}
while(1)
{
unsigned long long utime,wtime;
fd_set fds;
struct timeval tv;
FD_ZERO(&fds); // select の準備
for(i=0;i<nc;i++)
FD_SET(fd[i],&fds); // 各待機用fd
FD_SET(0,&fds); // 標準入力(stdin)
tv.tv_sec=1; tv.tv_usec=0; // タイムアウト1秒
r=select(maxfd+1,&fds,NULL,NULL,&tv); // 読みselect
if(r<0)
{ printf("select returned with signal or error\n"); break; }
if(r==0)
{ putchar('.'); fflush(stdout); continue; }
if(FD_ISSET(0,&fds))
{ printf("key-input detected\n"); break; }
utime=get_time();
for(i=0;i<nc;i++)
if(FD_ISSET(fd[i],&fds))
{
crec[ccrec].ch=i;
crec[ccrec].utime=utime;
ccrec++;
#ifdef PRINT
ioctl(fd[i],1,&wtime);
printf("%d ",i);
}
else
{
printf(" ");
#endif
}
if(ccrec>10000-nc) break;
#ifdef PRINT
printf(" at %lld delay: %lf [ms] (%lld ticks)\n",wtime,
(utime-wtime)/TickPerMSEC, utime-wtime);
#endif
}
for(i=0;i<nc;i++)
close(fd[i]);
for(i=0;i<nc;i++)
{
FILE *fp;
char file[20];
unsigned long long prev=0;
sprintf(file,"cycle%d.xy",i);
if((fp=fopen(file,"w"))==NULL)
{ printf("cannno open file %s\n",file); break; }
printf("writing '%s'\n",file);
for(j=100;j<ccrec;j++) // 最初の不安定期間をとばす
{
if(crec[j].ch!=i) continue;
if(prev!=0)
{
fprintf(fp,"%lf %lf\n",
(crec[j].utime-crec[0].utime)/TickPerMSEC,
(crec[j].utime-prev)/TickPerMSEC);
}
prev=crec[j].utime;
}
}
return 0;
}
これらのプログラムのコンパイルおよび実行は以下のようになります。
% 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つ
次に、
ioctl(fd,1,unsigned long long *wtime); 最後に起こされた時刻(マイクロ秒単位orタイムスタンプ単位) ioctl(fd,2,unsigned long cycle); 周期をタイマ割り込みcycle分に設定 ioctl(fd,3); 複数周期分散最初の形式は最後に起こされた時刻を返します。 -DUSE_RDTSC をつけてモジュールをコンパイルした場合はPentium以降のCPUに搭載されたタイムスタンプカウンタ(リセット時に0で以後クロック毎に カウントアップ)の値で、つけてない場合は現在時刻をマイクロ秒単位で返します(
2つ目の形式でこのファイル記述子を使用した場合の周期を設定します。 時間単位はタイマ割り込み周期です。
3つ目の形式は周期をちらす役目を持ちます。上の実行例では、公約数2を持つ周期群を設定しましたが、周期の基準点がopenした時刻であるため、同時にopenすると各実行タイミングがそろいます(0,1,2,3が1行に表示)。 それぞれの周期に制御動作などを割り当てた場合、その処理順によって周期にばらつきが出かねません。 それに対して、同時に複数の周期のタイミングが揃わないようにすれば、それぞれの処理をタイマ割り込みの直後に実行できるはずです(2,4,6,8だと(理論的に)むりですが、3,6,9では可能)。 この場合に、3つ目の形式を実行すると、適当にちらしてくれます(ちゃんとずれるように探すので乱数よりまともです:-)。が、均等にはなりません)。 なお、これは単一プロセスではなく複数プロセスからそれぞれ周期を設定した場合にも効果があります。 それ以前に、各周期設定は完全に独立なので、同じプロセスでも、異なるプロセスでも動作はかわりません。
設定したら、周期実行のループをつくります。基本的に、whileループなどの中で、
unsigned long long utime,wtime; fd_set fds; struct timeval tv; FD_ZERO(&fds); // select の準備 for(i=0;i<nc;i++) FD_SET(fd[i],&fds); // 各待機用fd FD_SET(0,&fds); // 標準入力(stdin) tv.tv_sec=1; tv.tv_usec=0; // タイムアウト1秒 r=select(maxfd+1,&fds,NULL,NULL,&tv); // 読みselectこの例は
ここまで準備した上で
返り値が負の場合、普通はエラーですが、シグナルのこともあります。
返り値が0の場合はタイムアウトしています。返り値が正の場合は渡したファイル記述子のいずれかの準備が完了しています。
どの記述子の準備が出来たかを知るには、
標準では、1タイマ割り込み毎にselectをしているプロセスのチェックをしますが、もっとチェックを荒らくしてもいい、という場合、insmod するときに オプションで
# insmod cyclesel basecycle=10とbasecycleを指定すれば、タイマ割り込みその数ごとに、チェックします。 周期自体は ioctl などでタイマ割り込み1つ単位で設定できますが、チェックがあらくなるので、妙な周期になります(長時間の平均値はかわりませんが、基本的にbasecycle単位になります。設定によっては2種類の周期がでます)。
# 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/csel3 を
コンパイル時のオプションに -DUSE_RDTSC があります。
これをつけない場合、ドライバもプロセス側も時刻情報の取得には
前者は実時間の数値がそのまま得られ、汎用性に優れるのですが、プロセス側からはシステムコールを呼ばなければならず、時間が必要です(わずかですが)。
それに対して、後者は1クロックの命令一つで済むので非常に高速ですが、変換係数を設定しなければならず、汎用性が低下します。
固定のシステムでは後者をつかうといいとはおもいます。
いずれにせよ、今回のように複数の部分で時刻情報を使いたい場合は統一しなければならないでしょう。
このドライバは、本来Linuxにある制限の「最低2タイマ割り込み周期でしか周期実行できない」を回避できます。 この制限は、そもそも、usleepなどするとき、最低1周期分は休眠することによります。 そのため、1周期の9割くらいの時間で処理して短時間sleepしてタイマ割り込みを回避して優先順位を保ちつつ、また処理をする、という ps などでみえるCPUの利用率が0%なのに、ほとんどCPUを使いっぱなし、という悪どいことができません。 が、このドライバをつかうと、それが原理的には回避できてしまうはずです。やって得するかどうかはわかりませんが(^^;。