概要
その1では,実はちょっとずるをしていました.それは,スレッドの終了.ここでは,スレッドを正しく終了し,かつ2つ以上のスレッドにも対応できるようにする.
ズルしていたコード
以下のコードの(1)と(2)でズルしてました.
void secondt() {
printf("Second Thread\n");
int count = 0;
for (int i = 0; i < 3; ++i, ++count) {
printf("second [%d]\n", i);
thwait();
}
printf("total count in second = %d\n", count);
thwait(); -->(1)
}
void mainthread() {
printf("Main Thread Run\n");
create_thread(secondt, 1);
thwait();
int count = 0;
for (int i = 0; i < 3; ++i, ++count) {
printf("main [%d]\n", i);
thwait();
}
printf("total count in second = %d\n", count);
printf("Main Thread Rung\n");
exit(1); -->(2)
}
このコードを見ると分かるように,メインスレッドがセカンドスレッドを作ったあと,すぐにthwait()
で切り替えを行うため,先にループ処理(for
)を終えるのはセカンドスレッドであることがわかっている.そのため,secondt
を抜ける直前で明示的に(1)thwait()
を呼んでメインスレッドに制御を戻していた.また,メインスレッドでも,ループは同じ回数だけ回り,mainthread
を抜ける直前に(2)exit(1)
を呼び出すことでプロセス全体を終了させている.
もし,mainthread
とsecondt
のループ回数が違うと(mainthread
で3回以上にすると),どうなるか?ダサいスケジューラは全く対応していないので,再びセカンドスレッドに制御が戻り,関数を抜け,はてどうなる?また,secondt
のthwait()
を呼ばないとどうなるか? 結論から言うと,高い確率でセグメンテーションフォルトになります.
というのも,関数を定義すると,コンパイラによってアセンブリ言語に変換されますが,関数定義の最後にはret
がきます.一般的な関数呼び出しでは,戻りアドレスがスタックに積まれて実行が開始されるので,呼び出し元に戻ることができます.ところが,スレッドの実行では関数呼び出しというより,プログラムカウンタを設定してジャンプする,ということをやっているので,戻りアドレスがスタックに積まれていません.その結果,ret
によってわけのわからないアドレスにジャンプし,おそらく落ちる.ということになります.
ということで,関数の最後のret
が実行されるとき,ジャンプしたい関数のアドレスをスタックに積んで置けば解決です.
スレッドの生成
void create_thread(tfunc f, int idx) {
char* sp;
++thnum;
threads[idx].f = f;
threads[idx].tid = idx;
threads[idx].is_active = 1;
memset(threads[0].ctx, 0, sizeof(uint64_t) * 18);
threads[idx].stack = malloc(0x8000);
memset(threads[idx].stack, 0, 0x8000);
sp = threads[idx].stack + 0x8000 - 0x100;
((uint64_t*)sp)[0] = (uint64_t)threads[idx].f;
((uint64_t*)sp)[1] = (uint64_t)end_thread; -->ここ
threads[idx].ctx[0] = (uint64_t)sp;
}
このように,f
のアドレスをコピーしたあと,直後のメモリにend_thread
のアドレスをコピーしておきます.これで,スレッドで実行する関数を抜けたとき,end_thread
が実行されるようになります.
スレッドの終了
スレッドを終了では,
- スレッドを無効にする
- 別のスレッドを選択して,実行
する.まず,(1)のために,q_thread
構造体にuint8_t is_active
を追加し,1なら有効,0なら無効にすることにした.
次に,(2)のために,thresume
をNASMで定義した.別のスレッドを選択するには,dispatch()
を呼べば良い気がするが,関数呼び出しだと制御が返ってきてしまって,選択したスレッドを実行できない.それでは,thrun
を呼べばいい気がするけれど,thrun
はスレッドのコンテキストで初期化しない.そのため,thresume
を定義した.
これらの実装は以下の通り
void end_thread() {
printf("End Thread %d\n", current->tid);
printf("thnum = %d\n", thnum);
current->is_active = 0; // スレッドの無効化
if (--thnum == 0) {
printf("All thread end!\n");
thfin();
}
int nextth = get_next_tid(); // 次のスレッドIDの選択
current = &threads[nextth]; // currentに設定
thresume(); // currentのコンテキストで実行
}
コメントを見れば分かると思うけど,1つ補足をすると,有効なスレッドが1つも存在しない場合(thnum==0
),thfin()
を呼んでプロセス全体を終了する.この関数もnasmで書いていて,中では直接システムコールを実行している.exit(1)
を呼べば良いと思うかもしれないけど,exit(1)
を実行すると,メモリ違反(16バイトアライメントエラー(https://stackoverflow.com/questions/43354658/os-x-x64-stack-not-16-byte-aligned-error) )で落ちるので,苦肉の策.
誰か解決策を教えて下さい
_thresume:
mov rax, _current
mov rax, [rax]
mov rsp, [rax]
mov rbx, [rax + 64] ; flagqをrbxに代入
push rbx ; スタックに積む
mov rbx, [rax + 56] ; raxをrbxに代入
push rbx ; スタックに積む
add rsp, 16 ; spを戻す
mov rbp, [rax + 8]
mov rdi, [rax + 16]
mov rsi, [rax + 24]
mov rdx, [rax + 32]
mov rcx, [rax + 40]
mov rbx, [rax + 48]
mov r8, [rax + 72]
mov r9, [rax + 80]
mov r10, [rax + 88]
mov r11, [rax + 96]
mov r12, [rax + 104]
mov r13, [rax + 112]
mov r14, [rax + 120]
mov r15, [rax + 128]
mov rax, [rsp - 16] ; raxを復元
sub rsp, 8 ; flagqを復元
popfq
ret
実行
3つのスレッドを作って実行する,以下のコードを実行してみる.
#include <stdio.h>
#include <stdlib.h>
#include "./qthread.h"
void secondt() {
printf("Second Thread\n");
int count = 0;
for (int i = 0; i < 3; ++i, ++count) {
printf("second [%d]\n", i);
thwait();
}
printf("total count in second = %d\n", count);
}
void thirdt() {
printf("Third Thread\n");
int count = 0;
for (int i = 0; i < 5; ++i, ++count) {
printf("third [%d]\n", i);
thwait();
}
printf("total count in third = %d\n", count);
}
void mainthread() {
printf("Main Thread Run\n");
create_thread(thirdt, 2);
create_thread(secondt, 1);
thwait();
int count = 0;
for (int i = 0; i < 20; ++i, ++count) {
printf("main [%d]\n", i);
thwait();
}
printf("total count in main = %d\n", count);
printf("Main Thread Rung\n");
}
int main(void) {
current = NULL;
thnum = 0;
tid = 0;
create_mainthread(mainthread);
thrun();
printf("end of main\n");
return 0;
}
今度は,thwait()
も呼んでないし,exit(1)
もしていない.以下が結果
Main Thread Run
Second Thread
second [0]
Third Thread
third [0]
main [0]
second [1]
third [1]
main [1]
second [2]
third [2]
main [2]
total count in second = 3
End Thread 1
thnum = 3
third [3]
main [3]
third [4]
main [4]
total count in third = 5
End Thread 2
thnum = 2
main [5]
main [6]
main [7]
main [8]
main [9]
main [10]
main [11]
main [12]
main [13]
main [14]
main [15]
main [16]
main [17]
main [18]
main [19]
total count in main = 20
Main Thread Rung
End Thread 0
thnum = 1
All thread end!
メインスレッドだけ20回ループし,その他のスレッドは3回と5回だけループする.きちんと終了できていることが確認できる.
ここまでの実装は,以下のコマンドで確認できる(要MacOSX and x86-64)
>git clone https://github.com/hiro4669/qthread.git
>cd qthread
>git branch ver2 origin/ver2
>git checkout ver2
>make
>./hbs
おわりに
とりあえず最低限の実装はできたと思う.以下は,今後やるべきかもしれないこと.
- スレッドの切り替えをタイマー割り込みで実行.こうすれば完全に公平に各スレッドにリソースを割り当てられる
- main関数の扱い.今の実装は,作成したスレッドが全て実行を終えると
trun()
実行直後に戻らず,プロセス自身を終了する.それでいいのか?戻すべきなのか? - スレッドで実行する関数への引数を取れるようにする.