気になったことのあれこれ。この広告は、90日以上更新していないブログに表示しています。2.6.11のコードリーディングをしていく。CPUのアーキテクチャは書籍に沿ってIntelのx86とする。今回は時間管理及びタイマ割り込みについて見ていく。時間管理カーネルが行う時間管理の主な機能は以下。ユーザプロセスはtime()、ftime()、gettimeofday()の3つのAPIを使用し日時を取得する。カーネルも当該APIを用いてネットワークパケットにタイムスタンプを付与する。カーネルはタイマを用いてカーネル若しくはユーザプロセスに特定の時間が経過したことを通知する。クロック回路とタイマ回路クロック回路は現在時刻の取得や正確な時間計測に使用する。タイマ回路は設定可能で予め設定しておいたタイミングで割り込みを発生させることができ、ソフトウェアタイマなどに使用される。Clockとも)呼ばれる時計を搭載しており、ボタン型の電池によってPCの電源が入っていない時にも動作を継続する。TRCは2Hz~8192Hzの範囲の周波数でIRQ8に割り込みを発生させることができる。加えてRTCが特定の値になった時にIRQ8に割り込みを発生させることもできる。デバイスファイル/dev/rtcを通してRTCを設定することもでき、カーネルはI/Oポートの0x70及び0x71からアクセスする。TSC)x86プロセッサにはCLKという入力ピンが存在し外部オシレータのクロック信号を受信する。Pentium以降のプロセッサではそのクロック信号毎に加算されるカウンタを持っており、TSCレジスタを通じてそのカウンタを使用することができる。rdtsc命令がTSCレジスタの値を読み込む。TSCレジスタの利用はクロック信号の周波数を考慮する必要があり、1GHzのクロック周波数で動作している場合、TSCは1ナノ秒毎に加算される。正確な時間管理を行うためにLinuxはシステムの初期化時にクロック信号の周波数を決定する必要がある。CPUの周波数はシステムの起動時に求め、calibrate_tsc()で約5ミリ秒間に発生したクロック信号の数から周波数を計算する。PIT)PITはタイマ割り込みという特別な割り込みを発生させ、設定した通知時間が経過したことをカーネルに伝える。PITのタイマ割り込みは一回きりではなく、設定した特定の周期で永久にタイマ割り込みを発生させる。CMOSチップで実装する。LinuxではPITに1000Hzの周波数で設定を行い、1ミリ秒に一回の感覚でIRQ0にタイマ割り込みを発生させる。このタイマ割り込みのインターバルを"tick"と呼ぶ。tickの値はナノ秒単位で以下の変数が保持する。*/tick値が小さいほどタイマの制度は上がり、動画再生や同期I/Oの多重化などの応答性が向上する。CPUがカーネルモードで動作する時間を増え、ユーザモードプロセスの動作が遅くなるというトレードオフが存在する。tick値は遅いマシンで10ミリ秒(1秒間に100回の割り込み)、速いマシンでは1ミリ秒(1秒間に1000回の割り込み)となることが多い。Linuxではタイマの同期周期を決定するマクロとして以下のようなものがある。CLOCK_TICK_RATEとHZの比。*/PITはsetup_pit_timer()で初期化する。I/Oポートに連続して2バイトのデータを書き込むことで新たな周期を設定している。Timer)を提供する。CPUローカルタイマはPITと同じく定期的な割り込みを発生させるが以下の点が異なる。PITのタイマカウンタが16bitであるのに対してローカルタイマは32bitであるため、より長い割り込み間隔を設定でき発生頻度を低くすることが可能。PITは全てのCPUが処理できるようなグローバルな割り込みを発生させるのに対して、CPUローカルタイマでは対象のCPUにのみ割り込みを発生させる。CPUローカルタイマではバスクロック信号を基にしており、1,2,4,8,16,32,64,128のバスクロック信号毎にタイマカウンタを減らすよう設定が可能。これに対しPITは自身のクロック信号を持っており柔軟な設定が可能。HPET)HPETはIntelとMicrosoftが共同開発したタイマチップで、Linux2.6から使用することができる。HPETは複数のタイマを提供し、最大8個の32bitまたは64bitのカウンタが存在する。各カウンタは固有のクロック信号で動作し、クロック信号は10MHz以上の周波数、カウンタは最低でも100ナノ秒で増加する。全てのカウンタは最大32個のタイマに対応しており比較機とマッチレジスタで構成される。比較機はカウンタ値とマッチレジスタの値を比較し値が等しくなった時にハードウェア割り込みを発生させる。一部のタイマは周期的に割り込みを発生させることも可能。HPETはメモリ空間にマップされたペリフェラルレジスタを使用し設定が可能。BIOSは起動時にそのレジスタをメモリ空間にマッピングしその先頭アドレスをカーネルに伝える。カーネルはそのアドレスを用いてメモリを読み書きすることでHPETの設定などを行う。PMT)はもう一つのクロックデバイスで、全てのACPIを持つマザーボードに搭載されており、クロック信号は約3.58MHzの固定周波数となっている。このデバイスはシンプルなカウンタでクロック毎に増加する。カーネルはI/Oポートを通してこのカウンタを値を読み出す。このI/Oポートアドレスは機動処理中にBIOSによって決定される。時間管理の仕組みLinuxでは定期的に以下のような処理を行う。各ソフトウェアタイマの経過時間の確認及び指定時間を経過した際の関数実行Linuxの時間管理は時間の経過に関連するデータ構造及び関数の組み合わせで構成されている。シングルプロセッサ環境とマルチプロセッサ環境では時間管理の仕組みが以下のように異なる。シングルプロセッサ環境ではPITまたはHPETのグルーバルタイマ割り込みを契機に時間管理に関連する処理を行う。マルチプロセッサ環境ではグローバルタイマ割り込みを契機に全体的な処理(ソフトウェアタイマ処理など)を行い、ローカルAPICタイマ割り込みなどでCPU毎の処理(カレントプロセスの実行時間の監視など)を行う。時間管理は搭載しているハードウェアに依存しており、ローカルAPICの有無やローカルタイマ割り込みの使用できないマザーボード、各タイマの有無などで処理の内容が少々異なる物となる。タイマオブジェクト利用可能なタイマを共通のインターフェースで扱うためのデータ構造で、定義は以下のようにされている。指定回数ループしてウェイトする関数};mark_offset()はタイマ割り込みハンドラが呼び出し、タイマ割り込みの発生時刻を記録する。この記録した時間からの経過時間(マイクロ秒)を計算することでタイマ割り込み(tick)よりも正確な時刻を求めることができる。Interpolation)と呼ぶ。以下のcur_timerにはカーネルの初期化時にselect_timer()で最適なタイマオブジェクトを設定する。&timer_none;タイマオブジェクトは利用可能なハードウェアタイマによって設定するタイマオブジェクトが決まっており、以下の表の上から(高性能な)順に利用可能なタイマのオブジェクトを設定する。短いループ各オブジェクトは以下のように定義されている。delay_none,};jiffies変数jiffies変数はシステム起動時からのtick回数を記録している(約50日で循環する)。jiffies変数は0xfffb6c20(-300,000)で初期化され5分後にオーバフローする。基本的にjiffies変数を扱うマクロや関数はオーバーフローに対応しているが、0xfffb6c20で初期されるのは意図的にこのオーバーフローに関連するバグをすぐに見つけられるようにするためのものである。32bitのjiffies変数は64bitのjiffies_64変数の下位32bitを使用ため、++jiffies_64はjiffies変数もインクリメントする事になる。32bitで扱うのには理由があり、仮にjiffiesが64bitだった場合32bitアーキテクチャでは64bitをアトミックに処理することができず排他処理必要になるため結果的に処理が遅くなってしまうためである。jiffies;jiffies_64変数には取得用の関数があり、以下のように定義されている。ret;}xtime変数xtime変数には現在の日時を保持しており、以下のように定義されている。(16)));timespec型は以下のような構造体として定義されている。現在時刻秒からの経過ナノ秒};一秒間に1000回発生するtick毎にxtime変数を更新する。ユーザプロセスは現在時刻と日付をxtime変数から取得する。時間管理の仕組みシングルプロセッサ環境シングルプロセッサでは時間関連の処理は全てIRQ0のPIT割り込みで処理する。カーネルの初期化時にtime_init()関数を呼び出し時間管理機能を初期化する。wall_to_monotonicの初期化。IRQ0の割り込みゲートを設定}select_timer()関数は以下のように定義されており、上から順にトラバースしていき存在すればそのタイマを使用するように初期化する。NULL;}time_init_hook()関数は以下のように定義されておりIRQ0をタイマ用のIRQオブジェクトで初期化しているのがわかる。&irq0);}タイマ割り込みのハンドラであるtimer_interrupt()はPITまたはHPETのサービスルーチンで以下のように定義されている。処理が適切に処理されたことを返す。0)内部で呼ばれているdo_timer_interrupt()の定義は以下。}}上記で呼び出されているdo_timer_interrupt_hook()は以下のように定義されている。smp_local_timer_interrupt(regs);#endif}まずdo_timer(regs);を見ていく。起動後5分でオーバーフローするため、オーバフローに起因するjiffiesのバグをすぐに発見できる。(-300*HZ))次にdo_timer_interrupt_hook()の以下の部分を見ていく。update_process_times(user_mode(regs));#endif上記はシングルプロセッサ環境でのみ行われる処理で、update_process_times()は以下のように定義されている。ソフトウェア割り込みを発生させる。HZ(1/1000秒)で呼ばれる。}次にdo_timer_interrupt_hook()の以下の部分を見ていく。smp_local_timer_interrupt(regs);#endif}シングルプロセッサ環境の場合、上記ではprofile_tick()関数が呼び出される。これはプロファイリング処理を行なっている。マルチプロセッサ環境マルチプロセッサシステムでは2つのタイマ割り込み発生源を利用し、1つは"PIT"又は"HPET"、もう1つはローカルCPUタイマとなる。"PIT"又は"HPET"が生成するグローバルタイマ割り込みではソフトウェアタイマ処理やシステム時間の更新処理などシステム全体に関連する処理を行う。ローカルCPUタイマが生成するローカルタイマ割り込みではカレントプロセスの実行時間の監視や資源使用量の統計情報を更新などのローカルCPUに関連する処理を行う。グローバル割り込みハンドラの初期化はシングルプロセッサ環境と同様time_init()関数で行う。ローカル割り込みは0xefがベクタとして予約されており、カーネルの初期中にapic_intr_init()関数内で初期化する。thermal_interrupt);#endif}上記からset_intr_gate()関数でLOCAL_TIMER_VECTORベクタの割り込みゲートに対してapic_timer_interrupt()関数のアドレスを登録しているのがわかる。APICタイマ割り込み周期の設定にはcalibrate_APIC_clock()関数を呼び出し、tick(1/1000秒)間に受け取るバスクロック数を用いてtick毎にローカル割り込みが走るようAPICを初期化する。(割愛)上記のコメントから複数あるCPUは共有バスで接続されているためcalibrate_APIC_clock()関数は一度しか実行されないことがわかる。実際のローカルタイマ割り込みの設定はsetup_APIC_timer()関数で行う。setup_APIC_timer()は各CPU毎に呼び出される。local_irq_restore(flags);}__setup_APIC_LVTT()は以下のように定義されており、初期化のための関数であるkとがわかる。clocks){グローバル割り込みハンドラグローバル割り込みハンドラとして呼び出すtimer_interrupt()は基本的にはシングルプロセッサ環境時と同じだが一部異なる点が存在する。IRQ_HANDLED;}まずtimer_interrupt()内で呼び出されるdo_timer_interrupt()関数の処理の一部。:上記ではAPICのI/Oポートに書き込みを行いタイマのIRQに対して割り込み応答を返す。次にdo_timer_interrupt_hook()関数。マルチプロセッサ環境ではここは呼ばれない。smp_local_timer_interrupt(regs);#endif}マルチプロセッサ環境ではupdate_process_times()やprofile_tick()は呼び出されない。ローカル割り込みハンドラローカル割り込みハンドラは特定のCPUに関連する時間処理(プロファイリングやカレントプロセスの時間管理など)を行う。先ほど見たローカル割り込みハンドラであるapic_timer_interrupt()は以下のように定義されている。LOCAL_TIMER_VECTOR,smp_apic_timer_interruptベクタ番号にバインドされているsmp_apic_timer_interrupt()関数の定義は以下。APICへの応答を返す。割り込みのネスト数を減算}上記呼び出されているsmp_local_timer_interrupt()関数の定義は以下。ローカルタイマ割り込みハンドラはプロファイリングやプロセスの統計及びリスケジューリングを行う。multiplier"のデフォルト値は1。"/proc/profile"に書き込みを行うことで任意の値を設定できる。multiplier"が変更された場合にここで調整を行う。}}profile_tick()及びupdate_process_times()はシングルプロセッサ環境ではグローバル割り込みハンドラ内で実行されていたが、マルチプロセッサ環境ではローカル割り込みハンドラで呼び出されているのがわかる。システム負荷の管理topやuptimeコマンドなどで表示されるシステム負荷(ロードアベレージ)はtick毎に呼び出される(前述の)update_times()関数内で算出している。実際に当該処理を行なっているのはcalc_load()関数で以下のように定義されている。ロードアベレージの値は10bitの整数と11bitの少数で記録される。FSHIFT;上記でタスク数を取得しているcount_active_tasks()関数は以下のように定義されている。sum;}nr_running()及びnr_uninterruptible()関数を見るとCPU毎に保持しているランキューがTASK_RUNNING状態のタスク数と及びTASK_UNINTERRUPTIBLE状態のタスク数をそれぞれ保持しているのがわかる。プロファイリングLinuxはreadprofileと呼ばれるプロファイラを持っており、カーネルの中で多く実行されるホットスポットを示す。プロファイラはタイマ割り込みの発生の際に呼び出されスタックに保存されているEIPレジスタの値から直前の処理を確認する。コードプロファイラの有効化はカーネルパラメータを指定してLinuxを起動する必要がある。プロファイリング結果は/proc/profileを通じてアクセス可能で、当該ファイルに書き込みを行うことでサンプリング周波数を変更することもできる。マルチプロセッサ環境ではここは呼ばれない。:マルチプロセッサ環境では以下のようにローカル割り込みハンドラ内で呼び出される。プロファイリング処理profile_tick()関数自体は以下のように定義されている。2.6ではoprofileというプロファイラもあり、柔軟性が高く微調整が可能でカーネル以外にもユーザやライブラリのホットスポットも見つけることが可能となる。NMIウォッチドッグの確認マルチプロセッサ環境ではウォッチドッグ機能という物を提供している。システムのストールを引き起こすようなバグを見つけるために役立つ。ウォッチドッグの有効化にはカーネルパラメータにnmi_watchdogを指定してカーネルを起動する必要がある。マクス不可割り込み)を発生させる。NMI割り込みはcli命令などでローカルCPUに対する割り込みを禁止している際にも有効である。tick毎にNMI割り込みハンドラであるdo_nmi()関数を実行する。:}上記からわかる通り、カウンタ値を確認し前回の値から変化がない場合にはエラーカウンタをインクリメントする。当該カウンタがある一定以上の値になった際に異常検知とみなす。カウンタ値は(前述の)tick毎に起こるローカルタイマ割り込みのハンドラ内で以下のようにインクリメントされている。APICへの応答を返す。割り込みのネスト数を減算}異常を検知した際に呼び出すdie_nmi()関数は以下のように定義されており、ログの出力やレジスタ値の表示などを行う。»Profile低レイヤ好きのソフトウェアエンジニア。(1)はてなブログをはじめよう!k--onishiさんは、はてなブログを使っています。ブログを報告するスターをつけました引用をストックしましたストック一覧を見る閉じる引用するにはまずログインしてくださいログイン閉じる引用をストックできませんでした。再度お試しください閉じる限定公開記事のため引用できません。