気になったことのあれこれ。2.6.11のコードリーディングをしていく。CPUのアーキテクチャは書籍に沿ってIntelのx86とする。ここではコンテキストスイッチを見ていく。プロセスの切り替え(コンテキストスイッチ)ハードウェアコンテキスト各プロセスは自身のアドレス空間を持つがCPUのレジスタは共有する。よってカーネルはプロセスを再開する前にレジスタの値をプロセスが休止した瞬間の値に戻す必要がある。プロセスの実行再開の前にレジスタに戻す必要のある一連のデータのことをハードウェアコンテキストと呼ぶ。プロセス実行コンテキストのサブセットになる。Linuxでは一部をプロセスディスクリプタ内に、残りをカーネルモードスタックに保持している。プロセスの切り替えは、prev(休止するプロセス)のハードウェアコンテキストをnext(次に実行するプロセス)のハードウェアコンテキストに置き換える動作と定義することができる。jmp命令を実行するとプロセスの切り替え(csとipレジスタを変更)を行ってくれる)を使用していたが現在は以下の理由からソフトウェアでプロセスの切り替えを行っている。mov命令を用いて各レジスタを切り替えることで値を有効性を確認することができる(悪意あるユーザがesレジスタやdsレジスタの値を変更していた場合などハードウェアのコンテキストスイッチだけでは検出できない)ハードウェアコンテキストスイッチとソフトウェアコンテキストスイッチは速度的に同じだが、ハードウェアでのコンテキストスイッチは最適化が不可能であるのに対して、ソフトウェアでのコンテストスイッチは将来的に改良が可能である。プロセスの切り替えはカーネルモードにおいてのみ発生し、レジスタの値はプロセス切り替えが起こる前に全てカーネルモードスタックに保存される。退避の対象にはもちろんssやespレジスタも含まれている。TSS)x86アーキテクチャではTSSという特別なセグメントが存在し、ハードウェアコンテキストを保持する。Linuxではハードウェアコンテキスト時にTSSを用いていないが、システム内の各CPUにおいて以下の理由からTSSの初期化を行なっている。CPUがユーザモードからカーネルモードに切り替わる時に、カーネルスタックのアドレスをTSSから読みこむ。ユーザモードのプロセスがin/out命令によるI/Oポートアクセスを行う時、TSSに格納されているI/Oパーミッションビットマップを参照し対象のプロセスに当該ポートの利用許可があるのかを確認する。実際には以下のような流れになる。eflagsレジスタのIOPLフィールドを確認し、3だった場合にI/O命令を実行する。3以外の場合は以下の処理が続く。16bitのレジスタでTSSのセレクタ値を保持する)レジスタにアクセスし現在のTSSを取得、適切なI/Oパーミッションビットマップを取得する。命令で指定されたI/Oポートへのアクセスがあるか先ほど取得したビットマップを用いて確認し、対象ポートに対応するビットが0だった場合には命令を実行し、1だった場合には「一般保護例外」を発生させる。1];が先ほどのI/Oポートビットマップとなる。tss_structはTSSの構造となっている。システム内部の各CPUに対応するTSSはinit_tss配列に格納されており、プロセス切り替え時にはTSSのフィールドを更新し、CPUの制御回路が必要な情報を利用できるようにする。よってTSSにはカレントプロセスの特権状態が反映されている。Descriptor)が存在する。セグメントディスクリプタの一種でTSSのベースアドレスやリミットなどを保持する。gdtrレジスタからGDTを求め、TRレジスタに保持しているセレクタ値からTSSDを求める。TSSDが保持しているTSSのベースアドレスやリミットを用いてTSSにアクセスする。※Intelでは元々プロセス毎にTSSが存在し、カレントプロセスのTSSのみビジービット(タイプフィールド内に存在する)が立っていた。Linuxでは各CPUにTSSは1つしか存在しないため常に立っている。メンバプロセス切り替え時にはハードウェアコンテキストを全て退避させる必要があるが、Intelの設計のようにプロセス毎にTSSを持っていないため、Linuxではプロセスディスクリプタ内にあるthread_struct型のthreadメンバにハードウェアコンテキストを退避させる。thread_structはカーネルスタックに退避されるeaxやebx以外のほとんど全てのCPUレジスタ用のメンバを保持する。x86では以下のように定義されている。io_bitmap_max;};プロセス切り替えの実行プロセスの切り替えはschedule()関数においてのみ発生し、以下の処理からなる。ページグローバルディレクトリを切り替え、新しいアドレス空間を有効にする。カーネルモードスタックとハードウェアコンテキストを切り替える。switch_toマクロswitch_toマクロはカーネルモードスタックとハードウェアコンテキストの切り替え時に呼び出すマクロで、以下のように定義されている。(0)prevはswitch_to実行前に実行中であるプロセスのディスクリプタで、コンテキストスイッチ後、停止状態となる。nextは切り替え前は停止しており、切り替え後、実行中となるプロセスのディスクリプタである。lastは再度prevが実行される際に切り替え前に実行していたプロセスディスクリプタとなる。これはどういう事かというと、switch_toマクロは切り替え前の段階ではprev上で動作しているが、切り替え後はnext上で実行される、そのためコンテキストスイッチはswitch_toマクロ上で行われるということになり、単一のプロセスから見るとswitch_toマクロ上で処理が中断し、再度実行される時はその続きから処理が実行されるように見える。再度prevが実行される時switch_toマクロはprevで実行されるが、prevは現在実行中のプロセス、nextは先ほど切り替え実行したプロセスとなり、どのプロセスから処理が切り替えられたのかわからない状況になる。%eaxに保存し、プロセス切り替え後、別のswitch_toマクロ上で%eaxの値をlastにリストアすることでどのプロセスから切り替わったのか把握できるという仕組みになっている。次に実際のコードを見ていく。%eaxにprev及びlastを、%edxにnextを設定。(next)でnextに対して%edxを割り当てている。プロセスを切り替えた後も%eaxは値が変わらないためlastを次に実行されるプロセスでも参照できることになる。\eflagsレジスタ及びベースポインタの保存。スタックに退避しているのがわかる。\スタックポインタの切り替え処理。現在実行しているプロセスのディスクリプタであるprevの%espをthread_struct型のメンバであるthreadメンバのespに保存し、そしてnextの同一メンバを%espにリストアしている。ここでスタックポインタは切り替わる。%5実行のプロセス(prev)が再実行される際に読み込まれる%eipをスレッドのメンバに設定し、次に実行されるプロセスが保持する%eipをスタックへの保存する。$1fは1:のアドレスで再度実行される時は当該箇所からの実行となる。__switch_toにジャンプする。\(ここからは再度実行が移ってきた時の処理となる)ベースポインタとフラグレジスタ(eflag)をリストアする。次に先ほど見たswitch_toマクロ内でコールされていた__switch_to()を見ていく。以下のように定義されている。当該関数はswitch_to()マクロから始まるコンテキストスイッチのメインの処理となる。prev_p;}上記の関数定義では関数名の前にfastcallという文字列が付加されていることがわかるが、fastcallは以下のようにマクロとして以下のように定義されている。__attribute__((regparm(3)))__attribute__はGCCの拡張機能を付加するもので今回使用されているregparm(n)は関数の引数をスタックからではなくレジスタ経由で受けとるという指定で、nはいくつの引数をとるかを指定する。今回は3を指定しているので%eax、%edx及び%ecxとなる。先程のswitch_toマクロ内ではprevが%eax、nextが%edxに割り当てられていたため、__switch_to()の引数はprev_pが%eax、next_pが%edxになることがわかる。ここから__switch_to()の処理を見ていく。cpu);上記ではまず変数にスレッドの情報を書き出し、現在実行しているプロセスのCPU及びそのCPUが保持しているTSSを取得している。smp_processor_id()は以下のように定義されており、%espをスレッドのサイズでマスクしthread_info構造体のポインタを取得、そこから現在使用しているCPUを取得している。(8192)以下の__unlazy_fpu()はprev_pの数値演算コアプロセッサレジスタの値を退避する関数で、レジスタの退避は選択的に行なっている。__unlazy_fpu(prev_p);次にload_esp0()ではtssのesp0メンバにnext->threadのesp0メンバを登録する、これによってカーネルモードへ移行した際に当該espがスタックポインタ(レジスタ)に読み込まれるようになる。next);上記の関数は以下のように定義されており、スタックポインタをTSSに渡しているのが確認できる。}}以下のload_TLS()はCPU毎に存在するローカルストレージを読み込んでいる。cpu);load_TLS()は以下のように定義されており一時的なマクロを使用しているので分かりづらいが、現在使用しているCPUのGDT内にあるTLSを切り替えている。6以下ではfsとgsセグメントレジスタを保存する処理となる。コメントにもある通りesとdsセグメントレジスタはカーネルモードの間、カーネルが使用するレジスタであるため切り替えは必要ない。*)&prev->gs));そして続く以下の処理では先ほど保存したセグメントレジスタ及びnextのセグメントレジスタいずれかが使用されている場合に、その切り替えを行う。}ここで使用されているunlikelyという関数はGCCのビルトイン関数が用いられているのマクロで、以下のように定義されている。0)これはコンパイル結果に最適化を行うものでlikely()であれば結果が"真"の場合に対して、unlikely()であれば結果が"偽"の場合に対して最適化が行われる。!!(x)のような記述がなされているのは引数がポインタでNULLだった場合を考慮したものらしい。*)&(value)))マクロの文字列化演算を使用しているので読みづらいが、valueをsegで指定したレジスタに設定しているのがわかる。次にデバッグレジスタの設定の処理を行なっている以下の個所。デバッグレジスタを使用している時のみ値を更新している。}loaddebug()は以下のように定義されており、第一引数をマクロの文字列化演算でレジスタ名を指定し、第二引数のregisterをオフセットにデバッグレジスタを指定している。(thread->debugreg[register]))次にI/Oパーミッションビットマップの処理を見ていく。tss)を呼び出すという処理になっている。プロセスは基本的に独自のI/Oパーミッションビットマップは保持しないためunlikelyとなっている。prev若しくはnextがI/Oポートにアクセスした時のみローカルCPUのTSSに実際のビットマップが、io_bitmap_ptrポインタが指す場所にコピーされる。handle_io_bitmap()の定義を以下に示す。0x9000次に実行対象となっているタスクであるnextがI/Oパーミッションビットマップを保持していない場合はio_bitmap_baseにINVALID_IO_BITMAP_OFFSET(構造体のオフセットとして無効な値)を設定する。nextがすでにセットされているI/Oパーミッションマップのオーナーだった場合は有効なオフセットを設定する。条件分岐に引っかからない場合はレイジーコピーを行うような設定となる。これはhandle_io_bitmap()内ではビットマップのコピーは行わず、実際にI/Oパーミッションマップへのアクセスが発生した時にio_bitmap_baseがINVALID_IO_BITMAP_OFFSET_LAZYだった場合、実際のコピーを行う。prev_p;上記の処理でprevへのポインタを%eaxに設定する。これにより__switch_to()前後での%eaxの値が担保できる。switch_toマクロ内では__switch_to()にjmpしているため、上記のreturnでスタックから%eipとして取り出される値は__switch_to()にjmpする直前にスタックにプッシュされたswitch_toマクロ内の:1のアドレスとなり、しっかりと再実行時に意図した場所に戻ってくることがわかる。»Profile低レイヤ好きのソフトウェアエンジニア。(1)はてなブログをはじめよう!k--onishiさんは、はてなブログを使っています。ブログを報告するスターをつけました引用をストックしましたストック一覧を見る閉じる引用するにはまずログインしてくださいログイン閉じる引用をストックできませんでした。再度お試しください閉じる限定公開記事のため引用できません。