0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

スレッドの仕掛け2

Last updated at Posted at 2019-05-10

概要

その1では,実はちょっとずるをしていました.それは,スレッドの終了.ここでは,スレッドを正しく終了し,かつ2つ以上のスレッドにも対応できるようにする.

ズルしていたコード

以下のコードの(1)と(2)でズルしてました.

main.c
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)を呼び出すことでプロセス全体を終了させている.

もし,mainthreadsecondtのループ回数が違うと(mainthreadで3回以上にすると),どうなるか?ダサいスケジューラは全く対応していないので,再びセカンドスレッドに制御が戻り,関数を抜け,はてどうなる?また,secondtthwait()を呼ばないとどうなるか? 結論から言うと,高い確率でセグメンテーションフォルトになります.

というのも,関数を定義すると,コンパイラによってアセンブリ言語に変換されますが,関数定義の最後にはretがきます.一般的な関数呼び出しでは,戻りアドレスがスタックに積まれて実行が開始されるので,呼び出し元に戻ることができます.ところが,スレッドの実行では関数呼び出しというより,プログラムカウンタを設定してジャンプする,ということをやっているので,戻りアドレスがスタックに積まれていません.その結果,retによってわけのわからないアドレスにジャンプし,おそらく落ちる.ということになります.

ということで,関数の最後のretが実行されるとき,ジャンプしたい関数のアドレスをスタックに積んで置けば解決です.

スレッドの生成

qthread.c
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. スレッドを無効にする
  2. 別のスレッドを選択して,実行

する.まず,(1)のために,q_thread構造体にuint8_t is_activeを追加し,1なら有効,0なら無効にすることにした.
次に,(2)のために,thresumeをNASMで定義した.別のスレッドを選択するには,dispatch()を呼べば良い気がするが,関数呼び出しだと制御が返ってきてしまって,選択したスレッドを実行できない.それでは,thrunを呼べばいい気がするけれど,thrunはスレッドのコンテキストで初期化しない.そのため,thresumeを定義した.
これらの実装は以下の通り

qthread.c
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) )で落ちるので,苦肉の策.
誰か解決策を教えて下さい

ctx.asm
_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つのスレッドを作って実行する,以下のコードを実行してみる.

main.c
#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 

おわりに

とりあえず最低限の実装はできたと思う.以下は,今後やるべきかもしれないこと.

  1. スレッドの切り替えをタイマー割り込みで実行.こうすれば完全に公平に各スレッドにリソースを割り当てられる
  2. main関数の扱い.今の実装は,作成したスレッドが全て実行を終えるとtrun()実行直後に戻らず,プロセス自身を終了する.それでいいのか?戻すべきなのか?
  3. スレッドで実行する関数への引数を取れるようにする.
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?