web-dev-qa-db-ja.com

ユーザーレベルのスレッドはどのようにスケジュール/作成され、カーネルレベルのスレッドはどのように作成されますか?

この質問が愚かである場合はお詫びします。かなり長い間オンラインで答えを見つけようとしましたが、見つけることができなかったため、ここで質問します。私はスレッドを学んでいて、 このリンクこのLinux Plumbers Conference 2013のビデオ カーネルレベルとユーザーレベルのスレッドについて、そして私が理解している限り、pthreadを使用すると、ユーザー空間にスレッドが作成され、カーネルはこれを認識せず、単一のプロセスとしてのみ表示します。多くのスレッドが内部にあります。このような場合には、

  • カーネルがそれを単一のプロセスとして認識し、スレッドを認識していないため、プロセスが取得するタイムスライス中にこれらのユーザースレッドのスケジューリングを決定するのは誰ですか?スケジューリングはどのように行われますか?
  • Pthreadがユーザーレベルのスレッドを作成する場合、必要に応じて、カーネルレベルまたはOSスレッドはユーザー空間プログラムからどのように作成されますか?
  • 上記のリンクによると、オペレーティングシステムカーネルは、スレッドを作成および管理するためのシステムコールを提供するという。では、clone()システムコールはカーネルレベルのスレッドまたはユーザーレベルのスレッドを作成しますか?
    • カーネルレベルのスレッドを作成する場合、単純な pthreadsプログラムstraceも、実行中にclone()を使用することを示しますが、その理由はユーザーレベルのスレッドと見なされますか?
    • カーネルレベルのスレッドを作成しない場合、カーネルスレッドはユーザー空間プログラムからどのように作成されますか?
  • リンクによると、「スレッドに関する情報を維持するには、各スレッドに完全なスレッド制御ブロック(TCB)が必要です。その結果、かなりのオーバーヘッドが発生し、カーネルの複雑さが増します。」と書かれているため、カーネルレベルのスレッドでは、ヒープは共有され、残りはすべてスレッドに個別のものですか?

編集:

私はユーザーレベルのスレッドの作成について尋ねていましたが、 ここで 多くのユーザーレベルのスレッドが存在する多対一モデルへの参照があるため、それはスケジュールですカーネルレベルの1つのスレッドにマッピングされ、スレッド管理はスレッドライブラリによってユーザー空間で行われます。私はpthreadの使用についての参照のみを見てきましたが、それがユーザーレベルのスレッドとカーネルレベルのスレッドのどちらを作成するのかはわかりません。

27
init

これは、トップのコメントで始まります。

あなたが読んでいるドキュメントは一般的であり[Linux固有ではありません]、少し時代遅れです。そして、もっと重要なことに、それは異なる用語を使用しています。それが混乱の原因だと思います。だから、読んで...

それが「ユーザーレベル」のスレッドと呼んでいるものは、私が[古い] LWPスレッドと呼んでいるものです。 「カーネルレベル」のスレッドと呼ばれるものは、Linuxではnativeスレッドと呼ばれるものです。 Linuxでは、「カーネル」スレッドと呼ばれるものは、まったく別のものです[以下を参照]。

pthreadを使用すると、ユーザー空間にスレッドが作成され、カーネルはこれを認識せず、内部にあるスレッドの数を認識せずに、単一のプロセスとしてのみそれを表示します。

これは、NPTL(ネイティブposixスレッドライブラリ)の前にユーザー空間スレッドされたがどのように行われたかです。これは、SunOS/SolarisがLWP軽量プロセスと呼んだものでもあります。

自分自身を多重化してスレッドを作成するプロセスが1つありました。 IIRC、それはスレッドマスタープロセスと呼ばれていました。カーネルはnotでした。カーネルはyetスレッドを理解またはサポートしませんでした。

しかし、これらの「軽量」スレッドは、ユーザー空間ベースのスレッドマスター(別名「軽量プロセススケジューラ」)のコードによって切り替えられたため(特別なユーザープログラム/プロセスのみ)、コンテキストの切り替えが非常に遅くなりました。

また、「ネイティブ」スレッドが登場する前は、10個のプロセスがあるかもしれません。各プロセスはCPUの10%を取得します。プロセスの1つが10スレッドのLWPである場合、これらのスレッドはその10%を共有する必要があったため、CPUの1%しか獲得できませんでした。

これはすべて、カーネルのスケジューラisが認識している「ネイティブ」スレッドに置き換えられました。この切り替えは10〜15年前に行われました。

さて、上記の例では、それぞれがCPUの5%を取得する20のスレッド/プロセスがあります。また、コンテキストの切り替えははるかに高速です。

ネイティブスレッドでLWPシステムを使用することも可能ですが、これは必要ではなく設計上の選択です。

さらに、LWPは、各スレッドが「協調」する場合に適切に機能します。つまり、各スレッドループは定期的にexplicitを呼び出して「コンテキストスイッチ」関数を呼び出します。 自発的にプロセススロットを放棄して、別のLWPを実行できるようにします。

ただし、glibcのNPTL以前の実装では、LWPスレッドを[強制的に]横取りする(つまり、タイムスライシングを実装する)必要がありました。使用された正確なメカニズムを思い出せませんが、ここに例があります。スレッドマスターは、アラームを設定し、スリープ状態になり、ウェイクアップして、アクティブなスレッドに信号を送信する必要がありました。シグナルハンドラーはコンテキストの切り替えに影響を与えます。これは厄介で、醜く、やや信頼できませんでした。

Joachimがpthread_create関数がカーネルスレッドを作成することを述べました

callそれはkernelスレッドに対して[技術的に]正しくありません。 pthread_createnativeスレッドを作成します。これはユーザー空間で実行され、プロセスと同等の立場にあるタイムスライスに適しています。いったん作成されると、スレッドとプロセスの間にほとんど違いがありません。

主な違いは、プロセスには独自のアドレス空間があることです。ただし、スレッドは、同じスレッドグループの一部である他のプロセス/スレッドとアドレス空間を共有するプロセスです。

カーネルレベルのスレッドを作成しない場合、カーネルスレッドはユーザー空間プログラムからどのように作成されますか?

カーネルスレッドはnotユーザースペーススレッド、NPTL、ネイティブなどです。これらは、kernel_thread関数を介してカーネルによって作成されます。それらはカーネルの一部として実行され、notはユーザースペースのプログラム/プロセス/スレッドに関連付けられています。彼らはマシンに完全にアクセスできます。デバイス、MMUなど。カーネルスレッドは最高の特権レベルであるリング0で実行されます。また、カーネルのアドレススペースおよびnotユーザープロセス/スレッドのアドレススペースでも実行されます。

ユーザースペースのプログラム/プロセスがnotカーネルスレッドを作成する場合があります。 pthread_createを使用してnativeスレッドを作成し、clone syscallを呼び出すことを覚えておいてください。

スレッドは、カーネルであっても、何かを行うのに役立ちます。そのため、さまざまなスレッドでコードの一部を実行します。これらのスレッドを確認するには、ps axを実行します。見てみると、kthreadd, ksoftirqd, kworker, rcu_sched, rcu_bh, watchdog, migrationなどが表示されます。これらはカーネルスレッドであり、notプログラム/プロセスです。


UPDATE:

カーネルはユーザースレッドを認識しないとのことですが、.

前述のとおり、2つの「時代」があることに注意してください。

(1)カーネルがスレッドをサポートする前(2004年頃?)。これはスレッドマスター(ここでは、LWPスケジューラーと呼びます)を使用しました。カーネルにはfork syscallがありました。

(2)その後のすべてのカーネルdoはスレッドを理解します。 noスレッドマスターがありますが、pthreadsclone syscallがあります。現在、forkcloneとして実装されています。 cloneforkに似ていますが、いくつかの引数を取ります。特に、flags引数とchild_stack引数。

これについては以下で詳しく説明します...

それでは、ユーザーレベルのスレッドが個別のスタックを持つことはどのようにして可能でしょうか。

プロセッサスタックには「魔法」はありません。私は[ほとんど]をx86に限定しますが、これは、スタックレジスタさえ持たないアーキテクチャ(たとえば、1970年代のIBMメインフレーム、たとえばIBM System 370)にも適用できます。

X86では、スタックポインタは%rspです。 x86にはPushpopの指示があります。 Push %rcxと[後で] pop %rcxの保存と復元に使用します。

しかし、x86に%rspまたはPush/popの命令があったとしたらnot?まだスタックがありますか?承知しました規約。私たちは(プログラマーとして)%rbxがスタックポインターであることに同意します。

その場合、%rcxの「プッシュ」は[AT&Tアセンブラを使用]になります。

subq    $8,%rbx
movq    %rcx,0(%rbx)

また、%rcxの「ポップ」は次のようになります。

movq    0(%rbx),%rcx
addq    $8,%rbx

簡単にするために、Cの「疑似コード」に切り替えます。上記の疑似コードでのプッシュ/ポップは次のとおりです。

// Push %ecx
    %rbx -= 8;
    0(%rbx) = %ecx;

// pop %ecx
    %ecx = 0(%rbx);
    %rbx += 8;

スレッドを作成するには、LWPスケジューラーはmallocを使用してスタック領域を作成する必要がありました。次に、このポインターをスレッドごとの構造体に保存し、子LWPを開始する必要がありました。実際のコードは少しトリッキーです。LWP_createに似た(たとえば)pthread_create関数があると仮定します。

typedef void * (*LWP_func)(void *);

// per-thread control
typedef struct tsk tsk_t;
struct tsk {
    tsk_t *tsk_next;                    //
    tsk_t *tsk_prev;                    //
    void *tsk_stack;                    // stack base
    u64 tsk_regsave[16];
};

// list of tasks
typedef struct tsklist tsklist_t;
struct tsklist {
    tsk_t *tsk_next;                    //
    tsk_t *tsk_prev;                    //
};

tsklist_t tsklist;                      // list of tasks

tsk_t *tskcur;                          // current thread

// LWP_switch -- switch from one task to another
void
LWP_switch(tsk_t *to)
{

    // NOTE: we use (i.e.) burn register values as we do our work. in a real
    // implementation, we'd have to Push/pop these in a special way. so, just
    // pretend that we do that ...

    // save all registers into tskcur->tsk_regsave
    tskcur->tsk_regsave[RAX] = %rax;
    // ...

    tskcur = to;

    // restore most registers from tskcur->tsk_regsave
    %rax = tskcur->tsk_regsave[RAX];
    // ...

    // set stack pointer to new task's stack
    %rsp = tskcur->tsk_regsave[RSP];

    // set resume address for task
    Push(%rsp,tskcur->tsk_regsave[RIP]);

    // issue "ret" instruction
    ret();
}

// LWP_create -- start a new LWP
tsk_t *
LWP_create(LWP_func start_routine,void *arg)
{
    tsk_t *tsknew;

    // get per-thread struct for new task
    tsknew = calloc(1,sizeof(tsk_t));
    append_to_tsklist(tsknew);

    // get new task's stack
    tsknew->tsk_stack = malloc(0x100000)
    tsknew->tsk_regsave[RSP] = tsknew->tsk_stack;

    // give task its argument
    tsknew->tsk_regsave[RDI] = arg;

    // switch to new task
    LWP_switch(tsknew);

    return tsknew;
}

// LWP_destroy -- destroy an LWP
void
LWP_destroy(tsk_t *tsk)
{

    // free the task's stack
    free(tsk->tsk_stack);

    remove_from_tsklist(tsk);

    // free per-thread struct for dead task
    free(tsk);
}

スレッドを理解するカーネルでは、pthread_createcloneを使用しますが、stillは新しいスレッドのスタックを作成する必要があります。カーネルはnotを実行して、新しいスレッドのスタックを作成/割り当てます。 clone syscallはchild_stack引数を受け入れます。したがって、pthread_createは新しいスレッドにスタックを割り当て、それをcloneに渡す必要があります。

// pthread_create -- start a new native thread
tsk_t *
pthread_create(LWP_func start_routine,void *arg)
{
    tsk_t *tsknew;

    // get per-thread struct for new task
    tsknew = calloc(1,sizeof(tsk_t));
    append_to_tsklist(tsknew);

    // get new task's stack
    tsknew->tsk_stack = malloc(0x100000)

    // start up thread
    clone(start_routine,tsknew->tsk_stack,CLONE_THREAD,arg);

    return tsknew;
}

// pthread_join -- destroy an LWP
void
pthread_join(tsk_t *tsk)
{

    // wait for thread to die ...

    // free the task's stack
    free(tsk->tsk_stack);

    remove_from_tsklist(tsk);

    // free per-thread struct for dead task
    free(tsk);
}

プロセスまたはメインスレッドのみに、通常は上位メモリアドレスで、カーネルによって初期スタックが割り当てられます。したがって、プロセスがnotでスレッドを使用する場合、通常は、事前に割り当てられたスタックのみを使用します。

ただし、スレッドが作成された場合、どちらか LWPまたはネイティブ 1の場合、開始プロセス/スレッドは、提案されたスレッドの領域をmalloc補足:mallocを使用するのが通常の方法ですが、スレッド作成者は、グローバルメモリの大きなプールchar stack_area[MAXTASK][0x100000];を使用することもできます。

notスレッドを使用する通常のプログラムがある場合、[anyタイプの]スレッドを使用すると、与えられたデフォルトのスタックを「オーバーライド」したい場合があります。

そのプロセスは、非常に再帰的な関数を実行している場合、mallocと上記のアセンブラーのトリックを使用して、はるかに大きなスタックを作成することを決定できます。

ここで私の答えを参照してください: メモリを使用するユーザー定義スタックと組み込みスタックの違いは何ですか?

27
Craig Estey

ユーザーレベルのスレッドは通常、何らかの形でコルーチンです。 userモードで実行のフロー間でコンテキストを切り替えます。カーネルは関与しません。カーネルPOVから、すべて1つのスレッドです。スレッドが実際にすることはユーザーモードで制御され、ユーザーモードは実行の論理フロー(コルーチンなど)を一時停止、切り替え、再開できます。それはすべて、実際のスレッドにスケジュールされたクォンタムの間に発生します。カーネルは、実際のスレッド(カーネルスレッド)を不正に中断し、別のスレッドにプロセッサの制御を与えることができます。

ユーザーモードコルーチンには、協調的なマルチタスクが必要です。ユーザーモードスレッドは、定期的に制御を他のユーザーモードスレッドに放棄する必要があります(基本的に、実行が変更コンテキストし、カーネルスレッドは何も通知せずに新しいユーザーモードスレッドに変更します)。通常、何が起こるかは、コードがカーネルが行う制御を解放したいときの方がずっとよくわかるということです。正しくコーディングされていないコルーチンは、制御を奪い、他のすべてのコルーチンを飢えさせる可能性があります。

歴史的な実装では setcontext を使用していましたが、現在は非推奨です。 Boost.context はその代わりを提供しますが、完全に移植可能ではありません。

Boost.Contextは、単一のスレッドで一種の協調マルチタスクを提供する基本的なライブラリです。スタック(ローカル変数を含む)とスタックポインター、すべてのレジスターとCPUフラグ、および命令ポインターを含む、現在のスレッドでの現在の実行状態の抽象化を提供することにより、execution_contextはアプリケーションの実行パスの特定のポイントを表します。

当然のことながら、 Boost.coroutine はBoost.contextに基づいています。

Windows提供 ファイバー 。 .Netランタイムには、タスクと非同期/待機があります。

8
Remus Rusanu

LinuxThreadsは、いわゆる「1対1」モデルに従います。各スレッドは、実際にはカーネル内の個別のプロセスです。カーネルスケジューラは、通常のプロセスをスケジュールするのと同じように、スレッドのスケジュールを処理します。スレッドはLinux clone()システムコールで作成されます。これはfork()の一般化であり、新しいプロセスが親のメモリスペース、ファイル記述子、およびシグナルハンドラーを共有できるようにします。

ソース-Xavier Leroyのインタビュー(LinuxThreadsを作成した人) http://pauillac.inria.fr/~xleroy/linuxthreads/faq.html#K

1
Harsha