thread 元ファイル process.cpp Threadクラス ThreadInfoクラスを内包している。このクラスはそれぞれのスレッドのCPUの状態を 保存しておくArchThreadInfoクラスを持っている。 カーネル起動プログラムでスレッドがはじめに出てくるのは次の部分である。 とりあえず、カレントスレッドにはdummy2->tinfoが、 直前のスレッドにはdummy1->tinfoが指定されている。この段階でdummy1, dummy2には まだ何の情報も入っていない。 /* dummy thread struct */ Thread* dummy1 = new Thread(); Thread* dummy2 = new Thread(); g_prevThread = dummy1->tinfo; g_currentThread = dummy2->tinfo; さらに次の処理を行う。 ProcessOperation::initializeではページマネジャーを設定している。これはプロセスに 新しいページディレクトリを与えて、独自のメモリ空間を作るためである。 また、スケジューラーの初期化では実行中のスレッドと休止中のスレッドのリストを 初期化している。 /* this should be called, before timer enabled */ ProcessOperation::initialize(g_page_manager); g_scheduler = new Scheduler(); スケジューラを起動し終えたらアイドルプロセスとINITプロセスを起動する。ここで 実際にプロセスを起動しスレッドを実行しているわけだ。この辺の処理を理解するために それぞれの関数の処理を追ってみよう。 /* at first create idle process */ Process* idleProcess = ProcessOperation::create(ProcessOperation::KERNEL_PROCESS, "IDLE"); Thread* idleThread = ThreadOperation::create(idleProcess, (dword)monaIdle); g_scheduler->join(idleThread); /* start up Process */ Process* initProcess = ProcessOperation::create(ProcessOperation::KERNEL_PROCESS, "INIT"); Thread* initThread = ThreadOperation::create(initProcess, (dword)mainProcess); g_scheduler->join(initThread); ProcessOperation::Createの実際の処理 Process* idleProcess = ProcessOperation::create(ProcessOperation::KERNEL_PROCESS, "IDLE"); のコードの動き type = ProcessOperation::KERNEL_PROCESS name = "IDLE" プロセスがカーネルプロセスと指定されているのでカーネルプロセスの作成に取り掛かる。 KernelProcessのインスタンスを作成して戻り値としている。インスタンスを作成する時、 コンストラクタの引数にプロセスを識別するための名前とページディレクトリを与えている。 とくに後者は「プロセスごとの独立したメモリ空間」の実現に大きく貢献している。 Process* ProcessOperation::create(int type, const char* name) { Process* result; switch (type) { case USER_PROCESS: result = new UserProcess(name, ProcessOperation::pageManager->createNewPageDirectory()); break; case KERNEL_PROCESS: result = new KernelProcess(name, ProcessOperation::pageManager->createNewPageDirectory()); break; default: result = (Process*)NULL; break; } return result; } 実はUserProcessのコンストラクタとKernelProcessのコンストラクタではプロセスのモードを ユーザー用にするかカーネル用にするかといった処理を行っているだけに過ぎない。 ほとんどの処理はベースクラスのコンストラクタで行われている。  余談だが、このような分岐はコンストラクタにもうひとつ引数を加えればいいかもしれない。  しかし、このような変更を繰り返すとプログラムがつぎはぎだらけになって収拾がつかなく  なることは目に見えている。したがって、クラスを派生して処理を分岐させるという方法は  プログラムのスパゲッティ化を防ぐひとつのテクニックと言っていいだろう。 次に示すのがベースクラスのコンストラクタである。 この中で気になるところといえばHeapSegmentだ。しかし、HeapSegmentというのはただ単に この辺のメモリを使いますよ、と宣言しているだけに過ぎない。ほかのクラスも, ここではただ単に初期化して後で使われることを待っているだけに過ぎない。 ひとつ重要なことといえば引数で指定されたページディレクトリを保存している、 といったことだろう。このプロセスで後にメモリが必要になればこのページディレクトリに 物理メモリ領域を追加すればいい。 Process::Process(const char* name, PageEntry* directory) : threadNum(0) { /* name */ strncpy(name_, name, sizeof(name_)); /* address space */ pageDirectory_ = directory; /* allocate heap */ heap_ = new HeapSegment(0xC0000000, 8 * 1024 * 1024); /* shared list */ shared_ = new HList(); /* message list */ messageList_ = new HList(); /* mutex tree */ kmutexTree_ = new BinaryTree(); /* argument list */ arguments_ = new HList(); threadList_ = new HList(); /* pid */ pid++; pid_ = pid; } 次、スレッド行きます。 有効なスレッドを作成するためにはただ単にスレッドクラスを作ればいいというわけではない。 スレッドを作ってからプロセスに関連付けるといった作業や、実行開始アドレスの 設定といった動作が必要になる。特に後者の設定は次に呼び出すarchCreateThreadや archCreateUserThread関数で行う。この関数の引数には目的のスレッドクラス、 実行開始アドレス、スタックトップのアドレスを指定する。 ところでスタックの割り当てはProcessクラスが「ただ単に数字を割り当てているだけ」 である。具体的に言うと0xXXXXXから4キロバイトはスレッドA、0xYYYYYから4キロバイトは スレッドB・・・というようにである。当然スレッドAがスタックメモリを使いすぎると ほかのスレッドのメモリ領域を破壊して一気に「吹っ飛ぶ」なんてこともありうる。 Thread* ThreadOperation::create(Process* process, dword programCounter) { Thread* thread = new Thread(); (process->threadNum)++; PageEntry* directory = process->getPageDirectory(); LinearAddress stack = process->allocateStack(); thread->tinfo->process = process; /* caution !!! it is temp code. wait object manager */ if (!strcmp(process->getName(), "MOUSE.SVR")) { thread->id = MOUSE_SERVER; } else if (!strcmp(process->getName(), "SHELL.SVR")) { thread->id = SHELL_SERVER; } else if (!strcmp(process->getName(), "KEYBDMNG.SVR")) { thread->id = KEYBOARD_SERVER; } else { thread->id = ThreadOperation::id++; } if (process->isUserMode()) { archCreateUserThread(thread, programCounter, directory, stack); } else { archCreateThread(thread, programCounter, directory, stack); } process->getThreadList()->add(thread); return thread; }; この関数でスレッドのスタックを確保し、スレッドのハードウエア固有の情報を格納する。 スタック領域のリニアアドレスは前の関数で割り当てられていたが、そのアドレスと 物理アドレスには何の関係もなかった。いわゆる「掛け声だけ」の状態である。この関数に来て やっと実態が伴ったわけである。また、ハードウエア固有の情報は後にスケジューラが この情報を用いてスレッドを実行するときに使われる。注意してほしいのはスレッドを 作った段階では実行されない、ということである。 void ThreadOperation::archCreateThread(Thread* thread, dword programCounter , PageEntry* pageDirectory, LinearAddress stack) { ProcessOperation::pageManager-> allocatePhysicalPage(pageDirectory, stack, true, true, true); ThreadInfo* info = thread->tinfo; ArchThreadInfo* ainfo = info->archinfo; ainfo->cs = KERNEL_CS; ainfo->ds = KERNEL_DS; ainfo->es = KERNEL_DS; ainfo->ss = KERNEL_SS; ainfo->eflags = 0x200; ainfo->eax = 0; ainfo->ecx = 0; ainfo->edx = 0; ainfo->ebx = 0; ainfo->esi = 0; ainfo->edi = 0; ainfo->dpl = DPL_KERNEL; ainfo->esp = stack; ainfo->ebp = stack; ainfo->eip = programCounter; ainfo->cr3 = (PhysicalAddress)pageDirectory; } これまでの処理は端的に言えば実行関数とメモリ空間の関連付けである。今度は関数が 実行されるように登録しなければならない。スケジューラへの登録が次の動作である。 行っていることは実行スレッドキューの最後に追加しているだけである。 void Scheduler::join(Thread* thread) { runq->addToPrev(thread); } IDLEプロセスとINITプロセスを作成してスケジューラに登録した結果、実行キューは次のような 状態になっている。 IDLEProcess <=> INITProcess <=> runq 次はタイマーを無効にして割り込みを許可している。g_prevThreadとg_currentThreadの 初期化が終わっていないからだ。この段階でスレッドの移行が起こってほしくないので タイマーを無効にしている。 disableTimer(); enableInterrupt(); /* dummy thread struct */ g_prevThread = dummy1->tinfo; g_currentThread = dummy2->tinfo; g_prevThread->archinfo->cr3 = 1; g_currentThread->archinfo->cr3 = 2; タイマーを有効にしてスケジューラーを実際に動かす。 enableTimer(); タイマが動くと約10msごとに割り込みが入る。ちなみにタイマーの割り込みベクタは0x60に 設定してある。割り込みが発生するとタイマーの割り込みハンドラが呼び出されるが この段階では「特権レベルの移行はない」ことに注意してほしい。次に示すコードが タイマーハンドラである。 pushAllで保存しなければならないレジスタをスタックに詰め込んで、changeDataで カーネルのデータにアクセスできるようにDSとESを変更している。カーネルデータに アクセスできないと後述するg_currentThread->tinfoが変更できないのでこの処理は 欠かすことはできない。そして、arch_save_thread_registersでスタックに保存しておいた レジスタ情報をg_currentThread->tinfoに保存する。このとき、カーネルコードから 割り込まれた場合とユーザーコードから割り込まれた場合とで保存されるレジスタが 若干異なる。といってもスタックの情報だけであるが。 ちなみに保存されるレジスタ情報は次のようになる。 typedef struct ArchThreadInfo { dword eip; // 0  :割り込まれた時点でのコードの位置 dword cs; // 4  :割り込まれた時点でのコードセグメントセレクタ dword eflags; // 8  :割り込まれた時点でのフラグレジスタ情報 dword eax; // 12 :割り込まれた時点でのEAXレジスタ情報 dword ecx; // 16 :割り込まれた時点でのECXレジスタ情報 dword edx; // 20 :割り込まれた時点でのEDXレジスタ情報 dword ebx; // 24 :割り込まれた時点でのEBSレジスタ情報 dword esp; // 28 :割り込まれた時点でのESPレジスタ情報 dword ebp; // 32 :割り込まれた時点でのEBPレジスタ情報 dword esi; // 36 :割り込まれた時点でのESIレジスタ情報 dword edi; // 40 :割り込まれた時点でのEDIレジスタ情報 dword ds; // 44 :割り込まれた時点でのDSレジスタ情報 dword es; // 48 :割り込まれた時点でのESレジスタ情報 dword fs; // 52 dword gs; // 56 dword ss; // 60 dword dpl; // 64 dword esp0; // 68 dword ss0; // 72 dword cr3; // 76 }; レジスタ情報を保存し終えたらtimerHandlerを呼び出す。これはC++コードになっている。 arch_timerhandler: pushAll changeData call arch_save_thread_registers call timerHandler popAll iretd timerHandlerの処理の始めはPICの出力ポートに割り込みが終了したことを通知している。 この通知がないとタイマーのハードウエアは次の割り込みを掛けることができない。 といっても、割り込み処理中は割り込みフラグがクリアされているから割り込まれることは ないのだが。 割り込みの終了を通知した後はスケジューラ、スレッドに時間の経過を 知らせている。ここで加算された時間が「スレッドの実行時間」になるわけだ。 そして、スケジューラに次に実行すべきスレッドを決定している。。 g_scheduler->schedule()の戻り値はプロセスの変更があったかどうかを 返している。スケジューラが起動した直後は1プロセス1スレッドなので プロセスの変更がある。 最後にThreadOperation::switchThread(isProcessChange, 1)を呼び出して スレッドを交換する。このときパラメータの1はデバッグ用コンソールに表示するための ものなので無視してもいい。 void timerHandler() { /* EOI */ outportb(0x20, 0x20); g_scheduler->tick(); g_currentThread->thread->tick(); bool isProcessChange = g_scheduler->schedule(); ThreadOperation::switchThread(isProcessChange, 1); /* does not come here */ } Scheduler::scheduleの実行は現在実行中のスレッドクラスを 実行リストの一番最後に持ってきて、もしあれば、タイマー待ち (何ms待つと指定されていたスレッド)のスレッドを起動して g_currentThreadを「次に実行すべきスレッド」に変更している。 ここで、アドレス空間の変更、つまり、プロセスの変更があれば それを通知することにしている。 bool Scheduler::schedule() { Thread* current = (Thread*)(runq->top()); current->remove(); runq->addToPrev(current); wakeupTimer(); g_prevThread = g_currentThread; g_currentThread = PTR_THREAD(runq->top()); return !(IN_SAME_SPACE(g_prevThread, g_currentThread)); } タイマー待ちをしているスレッドのなかでタイマーの期限が過ぎたスレッドを 実行スレッドの一番初めに追加している。ここでFOREACH_Nというのはマクロで 次のように展開される。 FOREACH_N(waitq, Thread*, thread)・・・top, type, element for (Thread* thread = (Thread* )((waitq)->next); thread != (top); thread = (Thread* )((thread)->next)) つまり、キューの始めから終わりまでをさらって処理をするときにこのマクロを使用する。 関数オブジェクトを定義するより楽かもしれない。 int Scheduler::wakeupTimer() { FOREACH_N(waitq, Thread*, thread) { if (thread->waitReason != WAIT_TIMER) { continue; } if (thread->wakeupTimer > getTick()) { continue; } Thread* target = thread; thread = (Thread*)(thread->prev); target->remove(); target->waitReason = WAIT_NONE; runq->addToNext(target); } return 0; } ここに来てようやくスレッドの切り替えが行われる。まず、切り替え先のスレッドが ユーザースレッドかどうかを判断する。この判断は切り替え先のスレッドの属する プロセスがユーザープロセスでかつ、切り替え先のスレッドのコードが属する セグメントを指定するセグメントセレクタのRPLの特権レベルがユーザーモード になっていることでユーザーモードと判断する。通常この2つの条件は常に一致する。 一致しない場合は何かとんでもないことが起きた、と判断していい。 切り替え先のスレッドがユーザーモードであるかどうかということと プロセスが切り替わったかどうか、ということでスレッドの切り替えは 4通りに分かれる。始めのカーネルのブートプロセス→IDLEプロセス の切り替えではarch_switch_thread2()が呼び出される。 int ThreadOperation::switchThread(bool isProcessChanged, int num) { bool isUser = g_currentThread->process->isUserMode() && (g_currentThread->archinfo->cs & 0x03); if (isProcessChanged && isUser) { /* address space & therad switch */ arch_switch_thread_to_user2(); } else if (!isProcessChanged && isUser) { /* only thread switch */ arch_switch_thread_to_user1(); } else if (isProcessChanged && !isUser) { /* address space & therad switch */ arch_switch_thread2(); } else { arch_switch_thread1(); } /* does not come here */ return NORMAL; } ArchThreadInfoにあるレジスタ情報をそれぞれのレジスタにストアする。 このとき無駄ではあるがTSSの特権モード用のスタックセグメントと スタックポインタを設定している。ほかにもページディレクトリの 設定もしている。それが終わるとスタックに適切な情報をインプットして iretdをコールする。このとき、あたかも切り替え先のスレッドから割り込まれたように 小細工することがキーである。 スタックに戻り先のフラグ、CS、EIPをプッシュした状態でiretd命令を実行すると 切り替え先のスレッドに(行く)帰ることができる。 arch_switch_thread2: mov eax, dword[g_currentThread] mov ebx, dword[eax + 0 ] ; ArchThreadInfo mov ecx, dword[g_tss] ; tss mov eax, dword[ebx + 68] ; get esp0 mov dword[ecx + 4], eax ; restore esp0 mov eax, dword[ebx + 76] ; page directory mov cr3, eax ; change page directory mov eax, dword[ebx + 12] ; restore eax mov ecx, dword[ebx + 16] ; restore ecx mov edx, dword[ebx + 20] ; restore edx mov esp, dword[ebx + 28] ; restore esp mov ebp, dword[ebx + 32] ; restore ebp mov esi, dword[ebx + 36] ; restore esi mov edi, dword[ebx + 40] ; restore edi mov es , word[ebx + 48] ; restore es mov ds , word[ebx + 44] ; restore ds push dword[ebx + 8] ; push eflags push dword[ebx + 4] ; push cs push dword[ebx + 0] ; push eip push dword[ebx + 24] pop ebx ; restore ebx iretd ; switch to next