C言語でタイマと割り込み処理の書き方を教えて!
こういった悩みにお答えします.
本記事の信頼性
- リアルタイムシステムの研究歴12年.
- 東大教員の時に,英語でOS(Linuxカーネル)の授業.
- 2012年9月~2013年8月にアメリカのノースカロライナ大学チャペルヒル校(UNC)コンピュータサイエンス学部で客員研究員として勤務.C言語でリアルタイムLinuxの研究開発.
- プログラミング歴15年以上,習得している言語: C/C++,Python,Solidity/Vyper,Java,Ruby,Go,Rust,D,HTML/CSS/JS/PHP,MATLAB,Verse(UEFN), Assembler (x64,ARM).
- 東大教員の時に,C++言語で開発した「LLVMコンパイラの拡張」,C言語で開発した独自のリアルタイムOS「Mcube Kernel」をGitHubにオープンソースとして公開.
- 2020年1月~現在はアメリカのノースカロライナ州チャペルヒルにあるGuarantee Happiness LLCのCTOとしてECサイト開発やWeb/SNSマーケティングの業務.2022年6月~現在はアメリカのノースカロライナ州チャペルヒルにあるJapanese Tar Heel, Inc.のCEO兼CTO.
- 最近は自然言語処理AIとイーサリアムに関する有益な情報発信に従事.
- (AI全般を含む)自然言語処理AIの論文の日本語訳や,AIチャットボット(ChatGPT,Auto-GPT,Gemini(旧Bard)など)の記事を50本以上執筆.アメリカのサンフランシスコ(広義のシリコンバレー)の会社でプロンプトエンジニア・マネージャー・Quality Assurance(QA)の業務委託の経験あり.
- (スマートコントラクトのプログラミングを含む)イーサリアムや仮想通貨全般の記事を200本以上執筆.イギリスのロンドンの会社で仮想通貨の英語の記事を日本語に翻訳する業務委託の経験あり.
こういった私から学べます.
C言語を独学で習得することは難しいです.
私にC言語の無料相談をしたいあなたは,公式LINE「ChishiroのC言語」の友だち追加をお願い致します.
私のキャパシティもあり,一定数に達したら終了しますので,今すぐ追加しましょう!
独学が難しいあなたは,元東大教員がおすすめするC言語を学べるオンラインプログラミングスクール5社で自分に合うスクールを見つけましょう.後悔はさせません!
目次
タイマと割り込み
タイマとは,予め設定した時間を経過した後に,その旨を通知するためのハードウェアの仕組みです.
タイマを利用することで,周期もしくは非周期にタスクを起床することができます.
また,タイマにより発生したソフトウェア割り込み(シグナル)をタイマ割り込みと呼び,タイマ割り込みが発生した時の処理をする関数をシグナルハンドラと呼びます.
シグナルハンドラ内では非同期安全な関数しか呼び出すことができませんので注意して下さい.
例えば,シグナルハンドラ内ではprintf関数を利用できないので,標準出力や標準エラー出力にデータを書き込みたい場合はwrite関数を利用しましょう.
sigaction関数
1 |
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact); |
sigaction関数は,シグナルの動作の確認と変更を行う関数です.
actはNULL以外であれば,シグナルsignumの新しい動作(action)としてactが設定されます.
oldactがNULLでなければ,今までの動作がoldactに格納されます.
※sigaction関数と似た処理をするsignal関数がありますが,Linuxのバージョンにより動作が異なるので利用を控えましょう.
sigaction構造体は以下になります.
※sa_handlerとsa_sigactionはunionで定義されていない場合があるので注意して下さい.
1 2 3 4 5 6 7 8 9 |
struct sigaction { union { void (*sa_handler)(int); void (*sa_sigaction)(int, siginfo_t *, void *); } sigset_t sa_mask; int sa_flags; void (*sa_restorer)(void); }; |
以降の説明では,sigaction関数を利用したタイマと割り込み処理の書き方を解説していきます.
getitimer/setitimer関数
1 2 |
int getitimer(int which, struct itimerval *curr_value); int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value); |
getitimer/setitimer関数は,インターバルタイマの値を取得または設定する関数です.
struct itimeval構造体の定義は以下になります.
1 2 3 4 5 6 7 8 9 |
struct itimerval { struct timeval it_interval; /* Interval for periodic timer */ struct timeval it_value; /* Time until next expiration */ }; struct timeval { time_t tv_sec; /* seconds */ suseconds_t tv_usec; /* microseconds */ }; |
getitimer関数は,whichで指定されたタイマ(ITIMER_REAL,ITIMER_VIRTUAL,ITIMER_PROFのいずれか)の現在の値(次のタイマ満了までの残り時間)をcurr_valueで指定された構造体に格納します.
it_valueフィールドのにはタイマの残り時間,it_intervalフィールドにはタイマのインターバル(期間)が設定されます.
setitimer関数は指定されたタイマにnew_valueの値を設定します.
old_valueがNULL以外の場合,タイマの古い値(すなわち getitimer関数で返されるのと同じ情報)がold_valueに格納されます.
new_value.it_value のいずれかのフィールドが非ゼロの場合,タイマは指定された時間で開始するために待機します.
new_value.it_valueの両方のフィールドがゼロであれば,タイマは解除されます.
new_value.it_intervalフィールドは,タイマの新しい間隔を指定します.
そのサブフィールド(new_value.it_interval.tv_secとnew_value.it_interval.tv_usec)の両方がゼロの場合,タイマはシングルショットになります.
getitimer/setitimer関数を利用するコードは以下になります.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 |
/* * Author: Hiroyuki Chishiro * License: 2-Clause BSD */ #include <stdio.h> #include <stdlib.h> #include <sys/time.h> #include <signal.h> #include <string.h> #include <time.h> #include <unistd.h> #include <errno.h> #define MESSAGE "timer_handler() is called.\n" #define ERROR_MESSAGE "Error: unknown signum " #define INIT_WAIT_SEC 2 #define INTERVAL_SEC 1 #define NR_TIMER_INTERRUPTS 3 static int remaining = NR_TIMER_INTERRUPTS; void timer_handler(int signum) { char s[4]; switch (signum) { case SIGALRM: write(1, MESSAGE, sizeof(MESSAGE)); remaining--; break; default: s[0] = signum / 10 + '0'; s[1] = signum % 10 + '0'; s[2] = '\n'; s[3] = '\0'; write(2, ERROR_MESSAGE, sizeof(ERROR_MESSAGE)); write(2, s, sizeof(s)); break; } } int main(void) { struct sigaction action, old_action; struct itimerval timer, old_timer; memset(&action, 0, sizeof(action)); memset(&old_action, 0, sizeof(struct sigaction)); action.sa_handler = timer_handler; action.sa_flags = SA_RESTART; if (sigaction(SIGALRM, &action, &old_action) == -1) { perror("sigaction"); exit(1); } timer.it_value.tv_sec = INIT_WAIT_SEC; timer.it_value.tv_usec = 0; timer.it_interval.tv_sec = INTERVAL_SEC; timer.it_interval.tv_usec = 0; if (setitimer(ITIMER_REAL, &timer, &old_timer) == -1) { perror("setitimer"); exit(2); } while (remaining > 0) { #if COUNTDOWN struct itimerval current_timer; if (getitimer(ITIMER_REAL, ¤t_timer) == -1) { perror("getitimer"); exit(3); } printf("current_timer.it_value: tv_sec = %ld, tv_usec = %ld\n", current_timer.it_value.tv_sec, current_timer.it_value.tv_usec); #endif } if (setitimer(ITIMER_REAL, &old_timer, NULL) == -1) { perror("setitimer"); exit(4); } if (sigaction(SIGALRM, &old_action, NULL) == -1) { perror("sigaction"); exit(5); } return 0; } |
実行結果は以下になります.
最初に2秒待機した後,「timer_handler() is called.」が1秒間隔で表示されて,3回表示されたら終了します.
1 2 3 4 5 |
$ gcc itimer.c $ a.out timer_handler() is called. timer_handler() is called. timer_handler() is called. |
実際にタイマが正常にカウントダウンされているかどうか不安になりますよね.
そんなあなたはgetitimer関数でカウントダウンしているタイマの値を表示してみましょう.
以下のように-DCOUNTDOWNオプションを付けてコンパイルして実行してみて下さい.
カウントダウンタイマの値が表示されることがわかります.(とても長いので注意して下さい.)
1 2 3 4 5 6 7 8 |
$ gcc itimer.c -DCOUNTDOWN $ a.out current_timer.it_value: tv_sec = 1, tv_usec = 999999 current_timer.it_value: tv_sec = 1, tv_usec = 999956 current_timer.it_value: tv_sec = 1, tv_usec = 999955 ... timer_handler() is called. ... |
timer_create/timer_gettime/timer_settime/timer_delete関数
1 |
int timer_create(clockid_t clockid, struct sigevent *restrict sevp, timer_t *restrict timerid); |
timer_create関数は,プロセスごとのインターバルタイマを新しく作成します.
新しいタイマのIDは,timeridが指すバッファに格納されます(IDはタイマが削除されるまではプロセス内でユニーク).
また,新しいタイマは初期状態は未起動になります.
引数clockidは,新しいタイマが時間を計測するために使用する時計を指定します.
引数sevpはsigevent構造体を指し,タイマが時間切れ(タイムアウト)した時に呼び出し側にどのように通知するかを指定します.
1 2 3 4 |
int timer_gettime(timer_t timerid, struct itimerspec *curr_value); int timer_settime(timer_t timerid, int flags, const struct itimerspec *new_value, struct itimerspec *old_value); |
timer_gettime/timer_settime関数は,timeridで識別されるタイマを起動または停止します.
引数new_valueは,タイマの新しい初期値と新しい間隔を指定するitimerspec構造体へのポインタです.
struct itimerspec構造体の定義は以下になります.
1 2 3 4 5 6 7 8 9 |
struct timespec { time_t tv_sec; /* Seconds */ long tv_nsec; /* Nanoseconds */ }; struct itimerspec { struct timespec it_interval; /* Interval for periodic timer */ struct timespec it_value; /* Initial expiration */ }; |
1 |
int timer_delete(timer_t timerid); |
timer_delete関数は,timeridで指定されたIDのタイマを削除します.
タイマの削除に成功すると0,失敗すると-1を返します.
timer_create/timer_gettime/timer_settime/timer_delete関数を利用するコードは以下になります.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 |
/* * Author: Hiroyuki Chishiro * License: 2-Clause BSD */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <signal.h> #include <time.h> #include <unistd.h> #define MESSAGE "timer_handler() is called.\n" #define ERROR_MESSAGE "Error: unknown signum " #define INIT_WAIT_SEC 2 #define INTERVAL_SEC 1 #define NR_TIMER_INTERRUPTS 3 static int remaining = NR_TIMER_INTERRUPTS; void timer_handler(int signum) { char s[4]; switch (signum) { case SIGALRM: write(1, MESSAGE, sizeof(MESSAGE)); remaining--; break; default: s[0] = signum / 10 + '0'; s[1] = signum % 10 + '0'; s[2] = '\n'; s[3] = '\0'; write(2, ERROR_MESSAGE, sizeof(ERROR_MESSAGE)); write(2, s, sizeof(s)); break; } } int main(int argc, char *argv[]) { struct sigaction action, old_action; struct itimerspec timer, old_timer; timer_t tid; memset(&action, 0, sizeof(struct sigaction)); memset(&old_action, 0, sizeof(struct sigaction)); action.sa_handler = timer_handler; action.sa_flags = SA_RESTART; if (sigaction(SIGALRM, &action, &old_action) == -1) { perror("sigaction()"); exit(1); } timer.it_value.tv_sec = INIT_WAIT_SEC; timer.it_value.tv_nsec = 0; timer.it_interval.tv_sec = INTERVAL_SEC; timer.it_interval.tv_nsec = 0; if (timer_create(CLOCK_REALTIME, NULL, &tid) == -1) { perror("timer_create"); exit(2); } if (timer_settime(tid, 0, &timer, &old_timer) == -1) { perror("timer_settime"); exit(3); } while (remaining > 0) { #if COUNTDOWN struct itimerspec current_timer; if (timer_gettime(tid, ¤t_timer) == -1) { perror("getitimer"); exit(4); } printf("current_timer.it_value: tv_sec = %ld, tv_nsec = %ld\n", current_timer.it_value.tv_sec, current_timer.it_value.tv_nsec); #endif } if (timer_delete(tid) == -1) { perror("timer_delete"); exit(5); } if (sigaction(SIGALRM, &old_action, NULL) == -1) { perror("sigaction"); exit(6); } return 0; } |
実行結果は以下になります.
setitimer関数と同様です.
コンパイルする時に-rtオプションを付けることを忘れないようにして下さい.
1 2 3 4 5 |
$ gcc timer.c -lrt $ a.out timer_handler() is called. timer_handler() is called. timer_handler() is called. |
-DCOUNTDOWNオプションを付けてコンパイルした場合も同様です.
1 2 3 4 5 6 7 8 |
$ gcc timer.c -lrt -DCOUNTDOWN $ a.out current_timer.it_value: tv_sec = 1, tv_nsec = 999998814 current_timer.it_value: tv_sec = 1, tv_nsec = 999986968 current_timer.it_value: tv_sec = 1, tv_nsec = 999986427 ... timer_handler() is called. ... |
timerfd_create/timerfd_gettime/timerfd_settime関数
1 |
int timerfd_create(int clockid, int flags); |
timerfd_create関数は,タイマオブジェクトを生成し,そのタイマを参照するファイルディスクリプタを返します.
引き数clockidは,タイマの進捗を管理するためのクロックを指定するもので,CLOCK_REALTIMEかCLOCK_MONOTONICのどちらかを設定します.
引数flagsは,以下のフラグを設定できます.
- TFD_NONBLOCK:ファイルディスクリプタの操作をノンブロッキングに設定します.
- TFD_CLOEXEC:明示的にファイルディスクリプタをクローズするのではなく,他のプログラムを実行する際に自動でファイルディスクリプタをクローズするように指定するclose-on-execオプションを設定します.
1 2 3 4 |
int timerfd_gettime(int fd, struct itimerspec *curr_value); int timerfd_settime(int fd, int flags, const struct itimerspec *new_value, struct itimerspec *old_value); |
timerfd_gettime関数は, ファイルディスクリプタfdで参照されるタイマの現在の設定が入ったitimerspec構造体を,curr_valueに格納して返します.
it_value フィールドはタイマが次に満了するまでの残り時間,it_intervalフィールドはタイマの間隔を返します.
timerfd_settime関数は,ファイルディスクリプタfdにより参照されるタイマを起動または停止します.
引数new_valueはタイマの満了時間の初期値と間隔を指定します.
timerfd_create/timerfd_gettime/timerfd_settimeシステムコールは,上記で紹介したsetitimer関数やtimer_create関数の代替になるものです.
この理由として,timerfd_create関数で生成したファイルディスクリプタをread関数,select関数,poll関数,epollファミリー関数で監視できるメリットがあるからです.
timerfd_create/timerfd_gettime/timerfd_settime関数を利用するコードは以下になります.
sigaction関数を利用しないことで,シンプルなコードになっていることがわかります.
また,タイマ割り込みの発生は51行目のread関数で待機し,割り込みが発生したらread関数から返ります.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 |
/* * Author: Hiroyuki Chishiro * License: 2-Clause BSD */ #include <stdio.h> #include <stdlib.h> #include <stdint.h> #include <sys/timerfd.h> #include <unistd.h> #define INIT_WAIT_SEC 2 #define INTERVAL_SEC 1 #define NR_TIMER_INTERRUPTS 3 static int remaining = NR_TIMER_INTERRUPTS; int main(int argc, char *argv[]) { struct itimerspec timer, old_timer; int fd; uint64_t exp; timer.it_value.tv_sec = INIT_WAIT_SEC; timer.it_value.tv_nsec = 0; timer.it_interval.tv_sec = INTERVAL_SEC; timer.it_interval.tv_nsec = 0; if ((fd = timerfd_create(CLOCK_REALTIME, 0)) == -1) { perror("timer_create"); exit(1); } if (timerfd_settime(fd, 0, &timer, &old_timer) == -1) { perror("timer_settime"); exit(2); } while (remaining > 0) { #if COUNTDOWN struct itimerspec current_timer; if (timerfd_gettime(fd, ¤t_timer) == -1) { perror("getitimer"); exit(3); } printf("current_timer.it_value: tv_sec = %ld, tv_nsec = %ld\n", current_timer.it_value.tv_sec, current_timer.it_value.tv_nsec); #endif if (read(fd, &exp, sizeof(uint64_t)) < 0) { perror("read"); exit(4); } remaining--; printf("remaining = %d\n", remaining); } if (timerfd_settime(fd, 0, &old_timer, NULL) == -1) { perror("timer_settime"); exit(5); } return 0; } |
実行結果は以下になります.
1 2 3 4 5 |
$ gcc timerfd.c $ a.out remaining = 2 remaining = 1 remaining = 0 |
-DCOUNTDOWNオプションを付けた場合の実行結果は以下になります.
1 2 3 4 5 6 7 8 |
$ gcc timerfd.c -DCOUNTDOWN $ a.out current_timer.it_value: tv_sec = 1, tv_nsec = 999999253 remaining = 2 current_timer.it_value: tv_sec = 0, tv_nsec = 999200221 remaining = 1 current_timer.it_value: tv_sec = 0, tv_nsec = 999416093 remaining = 0 |
まとめ
C言語でタイマと割り込み処理の書き方を紹介しました.
具体的には,以下の関数の使い方を解説しました.
- getitimer/setitimer関数
- timer_create/timer_gettime/timer_settime/timer_delete関数
- timerfd_create/timerfd_gettime/timerfd_settime関数
C言語を独学で習得することは難しいです.
私にC言語の無料相談をしたいあなたは,公式LINE「ChishiroのC言語」の友だち追加をお願い致します.
私のキャパシティもあり,一定数に達したら終了しますので,今すぐ追加しましょう!
独学が難しいあなたは,元東大教員がおすすめするC言語を学べるオンラインプログラミングスクール5社で自分に合うスクールを見つけましょう.後悔はさせません!