[technology][linux] timer_createとtimerfd_createによるスレッド定期起床のCソースコードサンプル(インターバルタイマー)
最近、仕事では自分でプロダクションレベルのコーディングをすることはほぼ無く、仕事でもプライベートでも原理試作レベルもしくは内部ツール的なものをpythonで書くことがほとんどなのですが、ちょっと人が書いたCのソースコードをチェックしたりする事が増えてきました。
主にLinux上のソフトですが、C言語でのプログラミングは20年くらいマトモにはやっていないので、言語自体の規格だったり、POSIXの規格だったり色々変わっていて結構昔の常識が通じないなと思う今日この頃。
さて、組み込み系の制御ソフト的なものを作っていると、周期処理が必要になることが良くあります。pthreadでスレッド起こして置いて、寝かした状態で定期的に起床させて何かを処理する。処理が終わったらまた寝かせる。この手の処理は色々なところでちょいちょい出てくるのですが、結構人によって色々な書き方をしていて、定番のやり方が意外と無いなあという印象。
ネットで検索かけると、スレッド内でwhileループとか組んでループ内でnanosleep()で一定時間スリープ的なコードが多く見つかります。これは一見、お手軽ですがnanosleep()で指定するスリープ時間を固定値とした場合、起きている間の処理負荷の変動で周期処理のつもりが徐々にタイミングがずれていきます。
人によってはnanosleep()から起きた直後、もしくは寝る直前にシステム時刻を取得して、前回取得した時刻との時間差からスリープさせる時間を調整したりするコードを組む人もいます。しかしながら、やはり少しづつずれていきます。それなりの周期性が求められる用途にはイマイチです。中にはSIGALARMを拾うシグナルハンドラでフラグ立てて、待ち受けるスレッド側はそのフラグをひたすらポーリングするコード組む人がいて、それだとスレッドが無駄にCPU使いまくるので嬉しくない。
基本的にnanosleep()ではなく、timer_create()を使えば定周期のソフト割り込みを起こせるのですが、上述の様な、スレッドの中でループ組んでループの途中でスレッドをスリープ(ブロック)させて割り込みを待つというサンプルソースが案外みつからないので自分で書いてみました。
timer_create()の場合の定期的にSIGALARMを発生します。シグナルでスレッドに割り込ませるのは色々シグナルマスクの対応等が面倒で課題が多いので、シグナルハンドラから定期的にシグナル受信専用スレッドを呼び、そこからpthread_cond_signal()を叩く。割り込み待ち側のスレッドはpthread_cond_wait()でブロック状態で待たせておくコードを書いて実験しました。
#include <stdio.h> #include <stdlib.h> #include <signal.h> #include <pthread.h> #include <unistd.h> #include <errno.h> #include <sys/timerfd.h> #include <time.h> volatile sig_atomic_t stop_flag = 0; pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER; pthread_cond_t cond = PTHREAD_COND_INITIALIZER; void abort_handler(int sig); void thread_func(union sigval arg); void thread_func2(void); void abort_handler(int sig){ stop_flag = 1; } void thread_func(union sigval arg){ printf("Tid = %lu\n",pthread_self()); pthread_cond_signal(&cond); } void thread_func2(void){ int ret; struct timespec curTime,lastTime; clock_gettime(CLOCK_REALTIME,&lastTime); pthread_mutex_lock(&m); while(1){ ret = pthread_cond_wait(&cond,&m); if(ret == 0){ clock_gettime(CLOCK_REALTIME,&curTime); if(curTime.tv_nsec < lastTime.tv_nsec){ printf("Interval = %10ld.%09ld\n",curTime.tv_sec - lastTime.tv_sec - 1,curTime.tv_nsec + 1000000000 - lastTime.tv_nsec); } else{ printf("Interval = %10ld.%09ld\n",curTime.tv_sec - lastTime.tv_sec,curTime.tv_nsec - lastTime.tv_nsec); } lastTime = curTime; } if(stop_flag){ break; } } pthread_mutex_unlock(&m); } int main(int argc,char *argv[]){ timer_t timer_id; struct itimerspec ts; struct sigevent se; int status; pthread_t thread; int ret; if (signal(SIGINT,abort_handler) == SIG_ERR){ printf("Singal Handler set error!!\n"); exit(1); } se.sigev_notify = SIGEV_THREAD; se.sigev_value.sival_ptr = &timer_id; se.sigev_notify_function = thread_func; se.sigev_notify_attributes = NULL; ts.it_value.tv_sec = 1; ts.it_value.tv_nsec = 0; ts.it_interval.tv_sec = 1; ts.it_interval.tv_nsec = 0; status = timer_create(CLOCK_MONOTONIC,&se,&timer_id); if(status == -1){ printf("Fail to creat timer\n"); exit(1); } status = timer_settime(timer_id,0,&ts,0); if(status == -1){ printf("Fail to set timer\n"); exit(1); } ret = pthread_create(&thread,NULL,(void *)thread_func2,NULL); if(ret != 0){ printf("Cannot create thread!!\n"); exit(1); } ret = pthread_join(thread,NULL); if(ret != 0){ printf("Cannot join thread!!\n"); exit(1); } timer_delete(timer_id); return 0; }
thread_func()がタイマーが発火するたびに起床するスレッド内で呼ばれる関数で、timer_create()を実行する際に渡す、struct sigevent.sigev_notifyをSIGEV_THREADに設定、struct sigevent.sigev_notify_functionにthread_func()へのポインタを設定します。
コンパイルは下記。
$ gcc -O3 timer_test.c -o timer_test -lrt -pthread
ところで
Man page of SIGEVENT
に、この方法でタイマーから起床されるスレッドは、毎回新規スレッドを起こすか一つのスレッドを使い回すかは実装依存だと記載されています。
もし前者だと秒単位のタイマーなら良いですが、マイクロ秒やミリ秒オーダーの周期タイマーで使うにはちょっと重い感じがするので、確認のためthread_func()内でpthread_self()を呼んで、スレッドIDを表示させています。
Ubuntu 18.04 LTSで実行した結果は下記の様な感じで、スレッドIDは毎回同じでした。どうやらLinuxでは毎回同じスレッドを起床する様です。
実行するとこんな感じです。
$ ./timer_test Tid = 139982894376704 Interval = 1.001935040 Tid = 139982894376704 Interval = 0.999994341 Tid = 139982894376704 Interval = 0.999929061 Tid = 139982894376704 Interval = 0.999896739 Tid = 139982894376704 Interval = 0.999844149 Tid = 139982894376704 Interval = 1.000157898 ^CTid = 139982894376704 Interval = 0.999916040
色々ググってみるとtimerfd_create()というAPIが追加されているのですね。これはタイマーをファイルディスクリプタ−の形で提供してくれるもので、APIが返すファイルディスクリプターをread()すると、タイマー発火まで呼び出したスレッドがブロックされます。read()の代わりにselect()やpoll()が使えるため、タイマーとsocket経由の通信イベントやファイルイベント等を同時に待ち受ける事ができます。コーディング的にはこちらの方が好みなので、サンプルコードを書いてみました。
#include <stdio.h> #include <stdlib.h> #include <signal.h> #include <pthread.h> #include <unistd.h> #include <errno.h> #include <sys/timerfd.h> #include <time.h> volatile sig_atomic_t stop_flag = 0; void abort_handler(int sig); void thread_func2(void); void abort_handler(int sig){ stop_flag = 1; } void thread_func2(void){ int tfd; struct itimerspec its; ssize_t r_size; u_int64_t t_cnt; struct timespec curTime,lastTime; tfd = timerfd_create(CLOCK_MONOTONIC,0); its.it_value.tv_sec = 1; its.it_value.tv_nsec = 0; its.it_interval.tv_sec = 1; its.it_interval.tv_nsec = 0; clock_gettime(CLOCK_REALTIME,&lastTime); timerfd_settime(tfd,0,&its,NULL); while(1){ r_size = read(tfd,&t_cnt,sizeof(t_cnt)); if(r_size == sizeof(t_cnt)){ clock_gettime(CLOCK_REALTIME,&curTime); printf("Timer count = %lu,",t_cnt); if(curTime.tv_nsec < lastTime.tv_nsec){ printf("Interval = %10ld.%09ld\n",curTime.tv_sec - lastTime.tv_sec - 1,curTime.tv_nsec + 1000000000 - lastTime.tv_nsec); } else{ printf("Interval = %10ld.%09ld\n",curTime.tv_sec - lastTime.tv_sec,curTime.tv_nsec - lastTime.tv_nsec); } } if(stop_flag){ break; } lastTime = curTime; } close(tfd); } int main(int argc,char *argv[]){ pthread_t thread; int ret; if (signal(SIGINT,abort_handler) == SIG_ERR){ printf("Singal Handler set error!!\n"); exit(1); } ret = pthread_create(&thread,NULL,(void *)thread_func2,NULL); if(ret != 0){ printf("Cannot create thread!!\n"); exit(1); } ret = pthread_join(thread,NULL); if(ret != 0){ printf("Cannot join thread!!\n"); exit(1); } return 0; }
コンパイルと実行:
$ gcc -O3 timer_test2.c -o timer_test2 -pthread $ ./timer_test2 Timer count = 1,Interval = 1.000132248 Timer count = 1,Interval = 0.999989155 Timer count = 1,Interval = 1.000025406 Timer count = 1,Interval = 1.000112473 Timer count = 1,Interval = 0.999873694 Timer count = 1,Interval = 1.000019540 Timer count = 1,Interval = 0.999996840 ^CTimer count = 1,Interval = 0.999957891
mutexとか準備しなくて良いので、こちらの方が好みです。
raspi2 + raspbianだとこんな感じ。read()で読み出した64bitのintがraspbianだとカウントアップされていきますね...ARM版とx86_64版とのカーネルの実装の違い?
pi@raspberrypi:~/ctest $ ./timer_test2 Timer count = 1572358371,Interval = 1.000067095 Timer count = 1572358372,Interval = 1.000000563 Timer count = 1572358373,Interval = 0.999996211 Timer count = 1572358374,Interval = 0.999999616 Timer count = 1572358375,Interval = 0.999997860 Timer count = 1572358376,Interval = 1.000001413 Timer count = 1572358377,Interval = 0.999999598 Timer count = 1572358378,Interval = 1.000000748 Timer count = 1572358379,Interval = 0.999998979 Timer count = 1572358380,Interval = 1.000003559 ^CTimer count = 1572358381,Interval = 1.000000897 pi@raspberrypi:~/ctest $ uname -a Linux raspberrypi 4.19.66-v7+ #1253 SMP Thu Aug 15 11:49:46 BST 2019 armv7l GNU/Linux
追記:
そういえばsig_atomic_tという型が導入されていたのですね。
シグナルハンドラからはvolatile修飾されたsig_atomic_t型の変数のみ変更したらとっとと戻れと...
シグナルハンドラって不自由すぎるので出来るだけ使いたく無いのだけど、そうもいかないのですよね...