本記事の信頼性
- リアルタイムシステムの研究歴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本以上執筆.イギリスのロンドンの会社で仮想通貨の英語の記事を日本語に翻訳する業務委託の経験あり.
こういった私から学べます.
前回を読んでいない方はこちらからどうぞ.
Linuxカーネルの記事一覧はこちらからどうぞ.
LinuxカーネルはC言語で書かれています.
私にC言語の無料相談をしたいあなたは,公式LINE「ChishiroのC言語」の友だち追加をお願い致します.
私のキャパシティもあり,一定数に達したら終了しますので,今すぐ追加しましょう!
独学が難しいあなたは,元東大教員がおすすめするC言語を学べるオンラインプログラミングスクール5社で自分に合うスクールを見つけましょう.後悔はさせません!
今回のテーマはタイマと時間管理です.
Linuxカーネルの時間の概念がわかります!
目次
カーネルの時間経過の概念
カーネルに時間経過の概念を持たせることは,以下のケースで必須です.
- 定期的なタスクを実行する(例:CFSの時間計算)
- ある処理を将来の相対的な時間に遅延させる
- 一日の時間を示す
System Timerの中心的役割は以下になります.
- 定期的な割り込み,System Timerの割り込み
- システムの稼働時間,時刻の更新,ランキューのバランス,統計情報の記録等
- あらかじめプログラムされた頻度,タイマのティックレート(tick rate):tick = 1/(tickレート)秒
また,将来の相対的な時間のイベントをスケジュールする動的なタイマがあります.
tick rateとjiffies
tick rate
tick rate(System Timerの周波数)は,HZ変数で定義します.
linux/include/asm-generic/param.hで,HZはCONFIG_HZと定義されています.
1 |
# define HZ CONFIG_HZ /* Internal kernel timer frequency */ |
CONFIG_HZの値は,カーネルコンパイル時の設定オプションとなります.
HZのデフォルトはアーキテクチャ依存となり,x86-64とARM64は以下になります.
- x86-64:1,000HZ(1ms)
- ARM64:100HZ(10ms)
タイマの周波数(HZ)が高いと高精度になり,以下の特徴があります.
- 分解能が細かいカーネルタイマを実現できる.
- タイムアウト値を持つシステムコール(例:poll)は,アプリケーションによっては大幅な性能改善が見込める.
- 時間測定の精度が向上する.
- プロセスのプリエンプションがより正確に行われるため,低周波数ではタイムスライス終了後にプロセスがより多くのCPU時間を得る可能性がある.
しかし,タイマ周波数が高いことは,タイマ割り込みの頻度が多くなり,結果としてオーバーヘッドが大きくなってしまいます.
また,最近のハードウェアではタイマの周波数を高くすることはあまり意味がありません.
Linuxカーネルではtickless kernelというカーネルのtickを一時的に停止することができるコンパイル時のオプションがあります.
具体的には,以下のNO_HZファミリーのオプションを利用します.
- CONFIG_NO_HZ:tickless kernelの古い設定項目で,古い設定ファイルとの後方互換性を確保するために,しばらくの間は残すとのこと(将来的には削除予定).
- CONFIG_NO_HZ_COMMON:tickless kernelを利用する時の共通のオプションで,以下のどちらかを選択する.
- CONFIG_NO_HZ_IDLE:CPUがアイドル状態の場合,tickは必要に応じてのみトリガーする.
- CONFIG_NO_HZ_FULL:CPUがタスクを実行しているときでも,可能な限りtickを停止するように試みます.通常,CPU上で1つのタスクを実行する必要があります.
カーネルは現在のタイマの状態に応じて動的にSystem Timerを再プログラムします.
一時的にSystem Timerを停止するため,数百ミリ秒の間,イベントがない状況が発生します.
tickless kernelにより,オーバーヘッドの削減や省エネルギーが期待できます.
また,CPUが低消費電力のアイドル状態の時間が長くなります.
jiffies
jiffiesは,システムが起動してからのタイマのtick数を保持するunsigned long型のグローバル変数です.
jiffiesとseconds(秒数)の変換は以下になります.
- jiffies = seconds * HZ;
- seconds = jiffies / HZ;
jiffiesとsecondsの変換例は以下になります.
1 2 3 4 |
unsigned long time_stamp = jiffies; /* Now */ unsigned long next_tick = jiffies + 1; /* One tick from now */ unsigned long later = jiffies + 3 * HZ; /* 3 seconds from now */ unsigned long fraction = jiffies + HZ / 100; /* 10 ms from now */ |
jiffiesの内部表現を紹介します.
「sizeof(jiffies)」は,32ビットアーキテクチャでは32ビット(4バイト),64ビットアーキテクチャでは64ビット(8バイト)です.
HZ == 100の32ビット変数では497日,HZ == 1000の32ビット変数では50日でオーバーフローします.
しかし,64ビットの変数では,非常に長い間オーバーフローが発生しません.
上図のようにリンカマジックを利用することで,32ビットと64ビット両アーキテクチャでunsigned long型を維持したまま64ビット変数にアクセスできます.
つまり,アーキテクチャ毎のjiffiesの変数名と型は以下のようになります.
- 64ビットアーキテクチャ:jiffies_64(unsigned long long型),jiffies(unsigned long型)
- 32ビットアーキテクチャ:jiffies(unsigned long型)
jiffiesのラップアラウンドについて解説します.
ここで,ラップアラウンドとは,unsigned long型(符号なし整数型)が最大値を超えると0に折り返されることです.
32ビットでは,0xffffffff + 0x1 == 0x0になることです.
以下のコードの5行目のようにif文で「timeout > jiffies」と比較するとjiffiesがラップアラウンドして0になった時に正常に動作しなくなります.
1 2 3 4 5 6 7 8 9 10 |
/* WARNING: THIS CODE IS BUGGY */ unsigned long timeout = jiffies + HZ / 2; /* timeout in 0.5s */ /* do something... */ /* then see whether we took too long */ if (timeout > jiffies) { /* we did not time out, good ... */ } else { /* we timed out, error ... */ } |
そこで,linux/include/linux/jiffies.hにある以下のマクロを利用することで,ラップアラウンドの問題を解決します.
- time_after(a, b)マクロ:時刻aが時刻bより後の場合に真を返す.
- time_before(a, b)マクロ:時刻bが時刻aより後の場合に真を返す.
- time_after_eq(a, b)マクロ:時刻aが時刻bより同じか後の場合に真を返す.
- time_before_eq(a, b)マクロ:時刻bが時刻aより同じか後の場合に真を返す.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
/* * These inlines deal with timer wrapping correctly. You are * strongly encouraged to use them * 1. Because people otherwise forget * 2. Because if the timer wrap changes in future you won't have to * alter your driver code. * * time_after(a,b) returns true if the time a is after time b. * * Do this with "<0" and ">=0" to only test the sign of the result. A * good compiler would generate better code (and a really good compiler * wouldn't care). Gcc is currently neither. */ #define time_after(a,b) \ (typecheck(unsigned long, a) && \ typecheck(unsigned long, b) && \ ((long)((b) - (a)) < 0)) #define time_before(a,b) time_after(b,a) #define time_after_eq(a,b) \ (typecheck(unsigned long, a) && \ typecheck(unsigned long, b) && \ ((long)((a) - (b)) >= 0)) #define time_before_eq(a,b) time_after_eq(b,a) |
先述したコードをtime_beforeマクロで書き換えた場合は以下になります.
1 2 3 4 5 6 7 8 9 10 |
/* ------------------------------------ */ /* An example of using a time_*() macro */ unsigned long timeout = jiffies + HZ/2; /* timeout in 0.5s */ /* do some work ... */ /* then see whether we took too long */ if (time_before(jiffies, timeout)) { /* Use time_*() macros */ /* we did not time out, good ... */ } else { /* we timed out, error ... */ } |
アーキテクチャ固有のjiffiesとユーザ空間のclock tickを変換するために,linux/include/linux/jiffies.hにあるjiffies_to_clock/jiffies_64_to_clock_t関数を利用します.
1 2 |
extern clock_t jiffies_to_clock_t(unsigned long x); extern u64 jiffies_64_to_clock_t(u64 x); |
linux/inlcude/asm-generic/param.hでマクロとして定義されているユーザ空間のclock tickとしてUSER_HZがあります.
1 |
# define USER_HZ 100 /* some user interfaces are */ |
上記の関数は,ユーザ空間のAPIであるclock関数から呼ばれます.
ハードウェアのクロックとタイマ
ハードウェアのクロックとタイマは以下になります.
- Real-Time Clock(RTC)
- System Timer
- CPU's Time Stamp Counter(TSC)
RTCは,wall clockの時刻を保存するものです.
マザーボード上の小さなバッテリでバックアップされているため,RTCはコンピュータの電源が切れても増分されます.
Linuxはブート時にデータ構造にwall clockの時刻を保存します.
Linuxカーネルでは,xtimeという変数でwall clockを管理します.
System Timerは,アーキテクチャ共通の一定の周期で割り込みを駆動する機構を提供します.
x86のSystem Timerは,ローカルAPICタイマ(現在のプライマリタイマ)です.
Linuxカーネルのバージョン2.6.17まではProgrammable Interval Timer(PIT)がプライマリタイマでした.
ARM64はGeneric Timerを利用します.
CPU's Time Stamp Counter(TSC)は,最も正確な(CPUクロック分解能の)カウンタです.
x86-64ではクロック周波数に不変で,秒数 = クロック / 最大CPUクロック Hzになります.
x86-64ではTSCをrdtsc命令またはrdtscp命令で読み出します.
ARM64ではTSCをPerformance Monitors Cycle Count Register(PMCCNTR_EL0)で読み出します.
タイマ割り込み
タイマ割り込み処理は,アーキテクチャ依存部,アーキテクチャ共通部の2つから構成されます.
アーキテクチャ依存部はタイマ割込みのハンドラ(Top Half)として登録されます.
- System Timer割り込みを認識する(必要に応じてリセットする).
- wall clockの時刻をRTCに保存する.
- アーキテクチャ共通関数を呼び出す(Top Halfの一部としてまだ実行される).
アーキテクチャ共通部では,linux/kernel/time/tick-common.cにあるtick_handle_periodic関数を呼び出します.
- tick_periodic関数を呼び出す.
- do_timer関数でjiffies_64をインクリメントする.
- update_wall_time関数でwall timeを更新する.
- update_process_times関数を呼び出す(必要に応じてロードバランサーを実行する).
- account_process_tick関数で現在実行中のプロセスとシステム全体の統計情報を更新する.
- run_local_timers関数でdynamic timerを実行する.
- scheduler_tick関数を呼び出す.
linux/kernel/time/tick-common.cにあるtick_periodic/tick_handle_periodic関数は以下になります.
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 |
/* * Periodic tick */ static void tick_periodic(int cpu) { if (tick_do_timer_cpu == cpu) { raw_spin_lock(&jiffies_lock); write_seqcount_begin(&jiffies_seq); /* Keep track of the next tick event */ tick_next_period = ktime_add_ns(tick_next_period, TICK_NSEC); do_timer(1); write_seqcount_end(&jiffies_seq); raw_spin_unlock(&jiffies_lock); update_wall_time(); } update_process_times(user_mode(get_irq_regs())); profile_tick(CPU_PROFILING); } /* * Event handler for periodic ticks */ void tick_handle_periodic(struct clock_event_device *dev) { int cpu = smp_processor_id(); ktime_t next = dev->next_event; tick_periodic(cpu); #if defined(CONFIG_HIGH_RES_TIMERS) || defined(CONFIG_NO_HZ_COMMON) /* * The cpu might have transitioned to HIGHRES or NOHZ mode via * update_process_times() -> run_local_timers() -> * hrtimer_run_queues(). */ if (dev->event_handler != tick_handle_periodic) return; #endif if (!clockevent_state_oneshot(dev)) return; for (;;) { /* * Setup the next period for devices, which do not have * periodic mode: */ next = ktime_add_ns(next, TICK_NSEC); if (!clockevents_program_event(dev, next, false)) return; /* * Have to be careful here. If we're in oneshot mode, * before we call tick_periodic() in a loop, we need * to be sure we're using a real hardware clocksource. * Otherwise we could get trapped in an infinite * loop, as the tick_periodic() increments jiffies, * which then will increment time, possibly causing * the loop to trigger again and again. */ if (timekeeping_valid_for_hres()) tick_periodic(cpu); } } |
linux/kernel/time/timekeeping.cにあるdo_timer関数は以下になります.
1 2 3 4 5 6 7 8 |
/* * Must hold jiffies_lock */ void do_timer(unsigned long ticks) { jiffies_64 += ticks; calc_global_load(); } |
linux/kernel/time/timer.cにあるrun_local_timers/update_process_times関数は以下になります.
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 |
/* * Called by the local, per-CPU timer interrupt on SMP. */ static void run_local_timers(void) { struct timer_base *base = this_cpu_ptr(&timer_bases[BASE_STD]); hrtimer_run_queues(); /* Raise the softirq only if required. */ if (time_before(jiffies, base->next_expiry)) { if (!IS_ENABLED(CONFIG_NO_HZ_COMMON)) return; /* CPU is awake, so check the deferrable base. */ base++; if (time_before(jiffies, base->next_expiry)) return; } raise_softirq(TIMER_SOFTIRQ); } /* * Called from the timer interrupt handler to charge one tick to the current * process. user_tick is 1 if the tick is user time, 0 for system. */ void update_process_times(int user_tick) { struct task_struct *p = current; PRANDOM_ADD_NOISE(jiffies, user_tick, p, 0); /* Note: this timer irq context must be accounted for as well. */ account_process_tick(p, user_tick); run_local_timers(); rcu_sched_clock_irq(user_tick); #ifdef CONFIG_IRQ_WORK if (in_irq()) irq_work_tick(); #endif scheduler_tick(); if (IS_ENABLED(CONFIG_POSIX_TIMERS)) run_posix_cpu_timers(); } |
update_process_times関数は以下の関数を呼び出します.
- account_process_tick関数:ユーザ空間やカーネル空間のプロセスやアイドルタスクの経過時間に1 tick加算する.
- run_local_timers関数:期限切れのタイマを実行し,TIMER_SOFTIRQ softirqを発生させる.
- scheduler_tick関数:現在実行中のプロセスのスケジューラクラスのtask_tick関数を呼び出す.タイムスリック情報の更新し,必要に応じてneed_reschedを設定する.CPUランキューのロードバランサーを実行する(SCHED_SOFTIRQ softirqを発生させる).
タイマ
タイマは,あるコードの実行を一定時間遅らせるために使用します.
「timer == dynamic timer == kernel timer」という意味になります.
linux/include/linux/timer.hにタイマを管理するtimer_list構造体と,タイマの操作マクロ・関数があります.
- timer_setupマクロ:タイマをセットアップする.
- add_timer関数:タイマを追加する.
- del_timer関数:タイマを削除する.
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 |
struct timer_list { /* * All fields that change during normal runtime grouped to the * same cacheline */ struct hlist_node entry; unsigned long expires; void (*function)(struct timer_list *); u32 flags; #ifdef CONFIG_LOCKDEP struct lockdep_map lockdep_map; #endif }; /** * timer_setup - prepare a timer for first use * @timer: the timer in question * @callback: the function to call when timer expires * @flags: any TIMER_* flags * * Regular timer initialization should use either DEFINE_TIMER() above, * or timer_setup(). For timers on the stack, timer_setup_on_stack() must * be used and must be balanced with a call to destroy_timer_on_stack(). */ #define timer_setup(timer, callback, flags) \ __init_timer((timer), (callback), (flags)) extern void add_timer(struct timer_list *timer); extern int del_timer(struct timer_list * timer); |
タイマを利用するカーネルモジュールのコード一式はこちらからダウンロードして下さい.
タイマのコードtimer_kernel_module.cは以下になります.
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 |
#include <linux/module.h> #include <linux/kernel.h> #include <linux/init.h> #include <linux/timer.h> struct timer_list mytimer; static void myhandler(struct timer_list *) { printk("myhandler() is called!\n"); } static int __init timer_kernel_module_init(void) { printk("timer_kernel_module_init()\n"); timer_setup(&mytimer, myhandler, 0); mytimer.expires = jiffies + 2 * HZ; /* timeout == 2 seconds */ printk("Timer started!\n"); add_timer(&mytimer); return 0; } static void __exit timer_kernel_module_exit(void) { del_timer(&mytimer); printk("timer_kernel_module_exit()\n"); } module_init(timer_kernel_module_init); module_exit(timer_kernel_module_exit); MODULE_LICENSE("GPL"); MODULE_AUTHOR("Hiroyuki Chishiro"); MODULE_DESCRIPTION("Timer Kernel Module"); |
実行結果は以下になります.
16行目の「[17171.765538] Timer started!」が呼ばれた約2秒後に,17行目の「[17173.773958] myhandler() is called!」が表示されていることがわかります.
※正確には17173.773958 - 17171.765538 = 2.00842秒になります.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
$ make make -C /lib/modules/5.15.0-48-generic/build M=/home/chishiro/c-language/timer_kernel_module modules make[1]: Entering directory '/usr/src/linux-headers-5.15.0-48-generic' CC [M] /home/chishiro/c-language/timer_kernel_module/timer_kernel_module.o MODPOST /home/chishiro/c-language/timer_kernel_module/Module.symvers CC [M] /home/chishiro/c-language/timer_kernel_module/timer_kernel_module.mod.o LD [M] /home/chishiro/c-language/timer_kernel_module/timer_kernel_module.ko BTF [M] /home/chishiro/c-language/timer_kernel_module/timer_kernel_module.ko Skipping BTF generation for /home/chishiro/c-language/timer_kernel_module/timer_kernel_module.ko due to unavailability of vmlinux make[1]: Leaving directory '/usr/src/linux-headers-5.15.0-48-generic' $ sudo insmod timer_kernel_module.ko $ sudo rmmod timer_kernel_module.ko $ sudo dmesg | tail ... [17171.765536] timer_kernel_module_init() [17171.765538] Timer started! [17173.773958] myhandler() is called! [17179.380916] timer_kernel_module_exit() |
実行の遅延
カーネルはタイマ(Bottom Half)を使わず,ある程度の時間を待つ(実行を遅延する)必要がある場合があります.
例えば,ハードウェアと通信するドライバです.
必要な遅延は非常に短く,タイマのtick周期よりも短いことがあります.
解決策は以下になります.
- ビジーループ(Busy Loop)
- 短い遅延とBogoMIPS
- schedule_timeout関数
ビジーループ(Busy Loop)
ビジーループ(Busy Loop)は,与えられたtick数が経過するまでループで回転させます.
jiffies,HZ,rdtscが使用可能です.
ビジーループは非常に短い時間を遅らせるには良いが,一般的にはCPUサイクルを浪費するので最適とは言えません.
より良い解決策は,cond_resched関数を利用して待ち時間にCPUサイクルを残しておくことです.
cond_resched関数はneed_reschedフラグが設定されている場合のみスケジューラを起動します.
ただし,割り込みコンテキストから使用することはできません(スケジューラではありません).
割り込みハンドラは高速であるべきなので,純粋なビジーループはおそらく良いアイデアではありません.
ビジーループは,ロック中や割込み禁止中に性能に重大な影響を与える可能性があります.
短い遅延とBogoMIPS
クロックの1 tickより短い時間だけ遅延させたい場合はどうすればよいでしょうか?
- HZが100の場合,1 tickは10msです.
- HZが1000の場合,1 tickは1msです.
上記より短い遅延を実現したい場合,mdelay/udelay/ndelay関数を利用します.
これらの関数は,ビジーループとして実装しています.
udelay/ndelay関数は,オーバーフローの危険性があるため,1ms未満の遅延の場合のみ呼び出す必要があります.
linux/include/linux/delay.hにmdelay/ndelay関数の定義があります.(正確にはmdelayはマクロです.)
※udelay関数はアーキテクチャ依存部にあります.(現状はコードが綺麗に整理されていないです.)
1 2 3 4 5 6 7 8 9 10 11 12 13 |
#ifndef mdelay #define mdelay(n) (\ (__builtin_constant_p(n) && (n)<=MAX_UDELAY_MS) ? udelay((n)*1000) : \ ({unsigned long __ms=(n); while (__ms--) udelay(1000);})) #endif #ifndef ndelay static inline void ndelay(unsigned long x) { udelay(DIV_ROUND_UP(x, 1000)); } #define ndelay(x) ndelay(x) #endif |
上記のコードは少しわかりにくいので,mdelay/udelay/ndelay関数は以下のようなプロトタイプ宣言だと考えるとわかりやすいです.
1 2 3 |
void mdelay(unsigned long msecs); void udelay(unsigned long usecs); /* only for delay <1ms due to overflow */ void ndelay(unsigned long nsecs); /* only for delay <1ms due to overflow */ |
BogoMIPSは,Linuxカーネルのブート時にCPU速度をビジーループを使って非科学的に測定した結果です.
BogoMIPSの単位はiteration/jiffyで,ブート時にキャリブレーションします.
私のPCのBogoMIPSは,/proc/cpuinfoで確認すると4799.99になります(23行目).
9行目のCPUの周波数「2399.999」(約2.4GHz)の約2倍になっていることがわかります.
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 |
$ cat /proc/cpuinfo processor : 0 vendor_id : GenuineIntel cpu family : 6 model : 158 model name : Intel(R) Core(TM) i9-9980HK CPU @ 2.40GHz stepping : 13 microcode : 0xea cpu MHz : 2399.999 cache size : 16384 KB physical id : 0 siblings : 16 core id : 0 cpu cores : 16 apicid : 0 initial apicid : 0 fpu : yes fpu_exception : yes cpuid level : 22 wp : yes flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ss ht syscall nx pdpe1gb rdtscp lm constant_tsc arch_perfmon nopl xtopology tsc_reliable nonstop_tsc cpuid tsc_known_freq pni pclmulqdq ssse3 fma cx16 pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand hypervisor lahf_lm abm 3dnowprefetch cpuid_fault invpcid_single ssbd ibrs ibpb stibp ibrs_enhanced fsgsbase tsc_adjust bmi1 avx2 smep bmi2 invpcid mpx rdseed adx smap clflushopt xsaveopt xsavec xsaves arat md_clear flush_l1d arch_capabilities bugs : spectre_v1 spectre_v2 spec_store_bypass swapgs itlb_multihit srbds mmio_stale_data retbleed bogomips : 4799.99 clflush size : 64 cache_alignment : 64 address sizes : 43 bits physical, 48 bits virtual power management: ... |
schedule_timeout関数
schedule_timeout関数は,呼び出したタスクを少なくともn tickの間スリープさせます.
タスクの状態をTASK_INTERRUPTIBLEまたはTASK_UNINTERRUPTIBLEに変更する必要があります.
また,ロックせずにプロセスコンテキストから呼び出さなければなりません.
タイムアウトを伴う待ち行列でのスリーピングでは,タスクは特定のイベントを待つために待ち行列に入れることができます.
このようなイベントをタイムアウトで待つには,schedule関数の代わりにschedule_timeout関数を呼び出します.
linux/kernel/time/timer.cにschedule_timeout関数があります.
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 |
/** * schedule_timeout - sleep until timeout * @timeout: timeout value in jiffies * * Make the current task sleep until @timeout jiffies have elapsed. * The function behavior depends on the current task state * (see also set_current_state() description): * * %TASK_RUNNING - the scheduler is called, but the task does not sleep * at all. That happens because sched_submit_work() does nothing for * tasks in %TASK_RUNNING state. * * %TASK_UNINTERRUPTIBLE - at least @timeout jiffies are guaranteed to * pass before the routine returns unless the current task is explicitly * woken up, (e.g. by wake_up_process()). * * %TASK_INTERRUPTIBLE - the routine may return early if a signal is * delivered to the current task or the current task is explicitly woken * up. * * The current task state is guaranteed to be %TASK_RUNNING when this * routine returns. * * Specifying a @timeout value of %MAX_SCHEDULE_TIMEOUT will schedule * the CPU away without a bound on the timeout. In this case the return * value will be %MAX_SCHEDULE_TIMEOUT. * * Returns 0 when the timer has expired otherwise the remaining time in * jiffies will be returned. In all cases the return value is guaranteed * to be non-negative. */ signed long __sched schedule_timeout(signed long timeout) { struct process_timer timer; unsigned long expire; switch (timeout) { case MAX_SCHEDULE_TIMEOUT: /* * These two special cases are useful to be comfortable * in the caller. Nothing more. We could take * MAX_SCHEDULE_TIMEOUT from one of the negative value * but I' d like to return a valid offset (>=0) to allow * the caller to do everything it want with the retval. */ schedule(); goto out; default: /* * Another bit of PARANOID. Note that the retval will be * 0 since no piece of kernel is supposed to do a check * for a negative retval of schedule_timeout() (since it * should never happens anyway). You just have the printk() * that will tell you if something is gone wrong and where. */ if (timeout < 0) { printk(KERN_ERR "schedule_timeout: wrong timeout " "value %lx\n", timeout); dump_stack(); __set_current_state(TASK_RUNNING); goto out; } } expire = timeout + jiffies; timer.task = current; timer_setup_on_stack(&timer.timer, process_timeout, 0); __mod_timer(&timer.timer, expire, MOD_TIMER_NOTPENDING); schedule(); del_singleshot_timer_sync(&timer.timer); /* Remove the timer from the object tracker */ destroy_timer_on_stack(&timer.timer); timeout = expire - jiffies; out: return timeout < 0 ? 0 : timeout; } EXPORT_SYMBOL(schedule_timeout); |
時刻
Linuxには時刻を取得,設定する機能が豊富にあります.
また,ある時点の時刻を表現するためのデータ構造は以下になります.
- timespec構造体:linux/include/uapi/linux/time.hで定義
- timespec64構造体:linux/include/linux/time64.hで定義
- ktime_t:linux/include/linux/ktime.hで定義
1 2 3 4 |
struct timespec { __kernel_old_time_t tv_sec; /* seconds */ long tv_nsec; /* nanoseconds */ }; |
1 2 3 4 |
struct timespec64 { time64_t tv_sec; /* seconds */ long tv_nsec; /* nanoseconds */ }; |
1 2 |
/* Nanosecond scalar representation for kernel time values */ typedef s64 ktime_t; |
ktimeの関連関数はこちらが詳しいです.
現在時刻を取得するカーネルモジュールのコード一式はこちらからダウンロードして下さい.
現在時刻を取得するコードtime_of_day_kernel_module.cは以下になります.
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 |
#include <linux/module.h> #include <linux/kernel.h> #include <linux/init.h> #include <linux/timekeeping.h> #include <linux/ktime.h> #include <asm-generic/delay.h> static int __init time_of_day_kernel_module_init(void) { struct timespec64 ts, start, stop; ktime_t kt, start_kt, stop_kt; printk("time_of_day_kernel_module_init()\n"); /* # of seconds with seconds + nanoseconds using struct timespec * since the epoch (1970/01/01). */ ktime_get_real_ts64(&ts); printk("ktime_get_real_ts64(): %llu.%09lu [s]\n", ts.tv_sec, ts.tv_nsec); /* same thing using ktime_t. */ kt = ktime_get_real(); printk("ktime_get_real(): %llu [ns]\n", kt); /* get the boottime offset. */ ktime_get_boottime_ts64(&ts); printk("ktime_get_boottime_ts64(): %llu.%09lu [s]\n", ts.tv_sec, ts.tv_nsec); printk("Boottime offset: %llu.%09lu [s]\n", ts.tv_sec, ts.tv_nsec); /* same thing using ktime_t. */ kt = ktime_get(); printk("ktime_get(): %llu [ns]\n", kt); /* measure uptime. */ ktime_get_boottime_ts64(&start); ktime_get_ts64(&stop); ts = timespec64_sub(stop, start); printk("Uptime (stop - start): %llu.%09lu [s]\n", ts.tv_sec, ts.tv_nsec); /* measure 100us */ start_kt = ktime_get(); udelay(100); stop_kt = ktime_get(); kt = ktime_sub(stop_kt, start_kt); printk("Measured execution time: %llu.%03llu [us]\n", kt / 1000, kt % 1000); return 0; } static void __exit time_of_day_kernel_module_exit(void) { printk("time_of_day_kernel_module_exit()\n"); } module_init(time_of_day_kernel_module_init); module_exit(time_of_day_kernel_module_exit); MODULE_LICENSE("GPL"); MODULE_AUTHOR("Hiroyuki Chishiro"); MODULE_DESCRIPTION("Time of Day Kernel Module"); |
実行結果は以下になります.
以下の処理を実行しています.
- ktime_get_real_ts64/ktime_get_real関数:1970年1月1日からの経過時間
- ktime_get_boottime_ts64/ktime_get関数:ブートからの経過時間
- ktime_get_boottime_ts64/ktime_get_ts64/timespec64_sub関数:経過時間の測定
- ktime_get/udelay関数:マイクロ秒単位の短い遅延時間の測定
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
$ make make -C /lib/modules/5.15.0-48-generic/build M=/home/chishiro/c-language/time_of_day_kernel_module modules make[1]: Entering directory '/usr/src/linux-headers-5.15.0-48-generic' CC [M] /home/chishiro/c-language/time_of_day_kernel_module/time_of_day_kernel_module.o MODPOST /home/chishiro/c-language/time_of_day_kernel_module/Module.symvers CC [M] /home/chishiro/c-language/time_of_day_kernel_module/time_of_day_kernel_module.mod.o LD [M] /home/chishiro/c-language/time_of_day_kernel_module/time_of_day_kernel_module.ko BTF [M] /home/chishiro/c-language/time_of_day_kernel_module/time_of_day_kernel_module.ko Skipping BTF generation for /home/chishiro/c-language/time_of_day_kernel_module/time_of_day_kernel_module.ko due to unavailability of vmlinux make[1]: Leaving directory '/usr/src/linux-headers-5.15.0-48-generic $ sudo insmod time_of_day_kernel_module.ko $ sudo rmmod time_of_day_kernel_module.ko $ sudo dmesg | tail ... [28282.607626] time_of_day_kernel_module_init() [28282.607629] ktime_get_real_ts64(): 1664354058.032619874 [s] [28282.607631] ktime_get_real(): 1664354058032621374 [ns] [28282.607631] ktime_get_boottime_ts64(): 28282.622156032 [s] [28282.607632] Boottime offset: 28282.622156032 [s] [28282.607633] ktime_get(): 28282622157548 [ns] [28282.607634] Uptime (stop - start): 0.000000040 [s] [28282.607734] Measured execution time: 99.406 [us] [28286.433609] time_of_day_kernel_module_exit() |
まとめ
今回はタイマと時間管理を紹介しました.
Linuxカーネルで時間を計測したい場合は参考にして下さい.
タイマと時間管理を深掘りしたいあなたは,以下の記事を読みましょう!
- NO_HZ: Reducing Scheduling-Clock Ticks
- CPU Idle Time Management
- Driver Basics
- delays - Information on the various kernel delay / sleep mechanisms
- ktime accessors
LinuxカーネルはC言語で書かれています.
私にC言語の無料相談をしたいあなたは,公式LINE「ChishiroのC言語」の友だち追加をお願い致します.
私のキャパシティもあり,一定数に達したら終了しますので,今すぐ追加しましょう!
独学が難しいあなたは,元東大教員がおすすめするC言語を学べるオンラインプログラミングスクール5社で自分に合うスクールを見つけましょう.後悔はさせません!
次回はこちらからどうぞ.