ローリングコンバットピッチなう!

AIとか仮想化とかペーパークラフトとか

周期タイマー(timer_create(),timerfd_create())でスレッドを定期的に起床する(C言語)[Linux][Pthread]

[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型の変数のみ変更したらとっとと戻れと...

www.jpcert.or.jp

シグナルハンドラって不自由すぎるので出来るだけ使いたく無いのだけど、そうもいかないのですよね...