動的タイマー
Rev.1を表示中。最新版はこちら。
タイマー処理として動的タイマーとインターバルタイマーがあります。動的タイマーはカーネルが、インターバルタイマーはユーザプロセスが使用するためのものです。なぜこのようにカーネル用とユーザ用の2つのタイマーを有するかという事ですが、動的タイマーはソフト割り込みで実装されており、インターバルタイマーは動的タイマーからシグナルを送信することで、ユーザスレッドとして実装されています。動的タイマーがソフト割り込みで実装されているということは、その処理で遅延が発生する処理はできません。従って遅延処理の伴うユーザプロセスには、直接動的タイマーを利用できないようにするためと理解しました。
カーネル起動時start_kernel(void)関数から、 init_timers(void)関数がコールされ、 open_softirq(TIMER_SOFTIRQ, run_timer_softirq)としてタイマーのソフト割り込みが設定されます。すなわちksoftirqdカーネルスレッドからrun_timer_softirq関数が起動されることになります。動的タイマーはにタイムアウトしたときに起動するよう関数ポインターを有したリストをtvec_base構造体にリストし、そのリストから、起動すべきタイマー処理を呼び出します。まさにタスクレットと同じ考え方です。
動的タイマーの使い方をschedule_timeout関数で見てみます。schedule_timeout関数は指定したtimeout後にプロセスの起動要求をかけるものです。まず、expire = timeout + jiffiesで、タイムアウト時間をexpireにセットしています。jiffiesはハード的なタイマー割り込みが発生毎に、割り込みハンドラでインクリメントされていくものです。ハードウエアーに直結したカーネルの時計みたいなものでしょうか。
setup_timer_on_stack関数で、timer_list構造体に、タイムアウト時に起動すべき関数process_timeoutとその引数currentでtimer_list構造体を初期化し、__mod_timer関数でタイムアウト時間を設定しています。動的タイマーの設定はこれだけで、schedule関数でタスク切り替えして他のタスクに実行権は移りますが、あとはksoftoirqdが起動される毎に、タイマー処理が起動され、所定の時間がくれば、process_timeoutがcurrentを引数にして、currentプロセスが再度スケジューリングされという具合です。
signed long __sched schedule_timeout(signed long timeout) { struct timer_list timer; unsigned long expire; : : expire = timeout + jiffies; setup_timer_on_stack(&timer, process_timeout, (unsigned long)current); __mod_timer(&timer, expire); schedule(); del_singleshot_timer_sync(&timer); destroy_timer_on_stack(&timer); : }個々のタイマー処理はtimer_list構造体で設定し、それをtvec_baseにリストしていきます。そしてソフト割り込みから__run_timers(struct tvec_base *base)関数がコールされ、tvec_baseのリストを辿っていきタイマー処理を行います。
struct timer_list { struct list_head entry; unsigned long expires; void (*function)(unsigned long); unsigned long data; struct tvec_base *base; }; struct tvec_base { spinlock_t lock; struct timer_list *running_timer; unsigned long timer_jiffies; struct tvec_root tv1; struct tvec tv2; struct tvec tv3; struct tvec tv4; struct tvec tv5; } ____cacheline_aligned;__run_timers関数を見る前に、tvec_base 構造体を実装を見る必要がります。メンバーにtv1,tv2,tv3,tv4,tv5があります。それらはtimer_listのexpiresのタイムアウト時間に依存するリストの先頭となります。そして__run_timersはtv1に繋がれたリストのみを処理します。もしtv1にタイマー待ち処理が無ければ、tv2からその時に処理が必要なtimer_listをtv1に繋ぎ変ええ、再度tv1をリスト検索しその処理を行います。tv2にリストが無ければtv3,tv4という具合です。
まずjiffiesがbase->timer_jiffiesより大きいかチェックします。base->timer_jiffiesはtvec_base作成時に、jiffiesで初期化され、タイマー処理(同じタイムアウトリスト群)するたびに、インクリメントされていきます。従ってiffiesがbase->timer_jiffiesより大きいということは、タイムアウトチェックする必要(タイムアウト処理があるということでない。)があるということです。
タイムアウト処理リストを検索するためのインデックスとして、index = base->timer_jiffies & TVR_MASKとしています。これはtv1,tv2等はstruct list_head vec[TVR_SIZE](TVR_SIZE=TVR_MASK-1となります。)となっていて、タイムアウト時間毎のリスト作成をビットレベルで簡単に管理できるような構造となっているからです。従ってindex にはタイムアウト処理しなければならない、tv1配列のインデックスとなります。
indexが0ならtv1にリストはありません。cascade関数で、tv2からタイム処理しなければならないtimer_listがあると、tv1に繋ぎ変えます。もしなければtv3,tv4という具合です。
そして次のタイムアウト処理のため、++base->timer_jiffiesとし、list_replace_init関数でtv1のリストをwork_listに繋ぎ買え、fn(data)でその処理を行っています。
kernel/timer.c static inline void __run_timers(struct tvec_base *base) { struct timer_list *timer; spin_lock_irq(&base->lock); while (time_after_eq(jiffies, base->timer_jiffies)) { struct list_head work_list; struct list_head *head = &work_list; int index = base->timer_jiffies & TVR_MASK; if (!index && (!cascade(base, &base->tv2, INDEX(0))) && (!cascade(base, &base->tv3, INDEX(1))) && !cascade(base, &base->tv4, INDEX(2))) cascade(base, &base->tv5, INDEX(3)); ++base->timer_jiffies; list_replace_init(base->tv1.vec + index, &work_list); while (!list_empty(head)) { void (*fn)(unsigned long); unsigned long data; timer = list_first_entry(head, struct timer_list,entry); fn = timer->function; data = timer->data; timer_stats_account_timer(timer); set_running_timer(base, timer); detach_timer(timer, 1); spin_unlock_irq(&base->lock); { int preempt_count = preempt_count(); fn(data); if (preempt_count != preempt_count()) { printk(KERN_ERR "huh, entered %p " "with preempt_count %08x, exited" " with %08x?\n", fn, preempt_count, preempt_count()); BUG(); } } spin_lock_irq(&base->lock); } } set_running_timer(base, NULL); spin_unlock_irq(&base->lock); }list_replace_init関数で繋ぎかえる(tv2,3,4,5のどれか)リストの先頭を、tv_listとし、元のリストを初期化します。(これでそのリストは無いということ。)そしてtv_listリストからエントリを抜き出して、internal_add_timer関数でbase->tv1に追加しています。
kernel/timer.c static int cascade(struct tvec_base *base, struct tvec *tv, int index) { struct timer_list *timer, *tmp; struct list_head tv_list; list_replace_init(tv->vec + index, &tv_list); list_for_each_entry_safe(timer, tmp, &tv_list, entry) { BUG_ON(tbase_get_base(timer->base) != base); internal_add_timer(base, timer); } return index; }