web-dev-qa-db-ja.com

Linux x86 GASアセンブリでシステムコールなしでスレッドを作成することは可能ですか?

「アセンブラー言語」(GNUをアセンブラーとして使用するx86アーキテクチャーのLinux))を学習している間、ahaの瞬間の1つは システムコール を使用する可能性でした。これらのシステムコールは非常に便利で、プログラム ser-spaceで実行される として必要になる場合もあります。
ただし、システムコールは割り込み(およびもちろんシステムコール)を必要とするため、パフォーマンスの点でかなり高価です。つまり、ユーザー空間で現在アクティブなプログラムから実行中のシステムにコンテキストスイッチを行う必要があります。カーネル空間で。

私が言いたいのはこれです:私は現在(大学のプロジェクト用に)コンパイラーを実装しており、追加したい追加機能の1つは、コンパイル済みプログラムのパフォーマンスを向上させるためのマルチスレッドコードのサポートです。 。一部のマルチスレッドコードはコンパイラ自体によって自動的に生成されるため、これにより、マルチスレッドコードのほんの小さなビットも含まれることがほぼ保証されます。パフォーマンスを向上させるためには、スレッドを使用してこれを実現する必要があります。

しかし私の恐れは、スレッディングを使用するために、システムコールと必要な割り込みをする必要があることです。したがって、小さな(自動生成された)小さなスレッドは、これらのシステムコールを実行するのにかかる時間に大きく影響され、パフォーマンスの低下につながる可能性さえあります...

したがって、私の質問は2つあります(その下に追加のボーナス質問があります):

  • システムコールを必要とせずに、複数のコアで複数のスレッドを同時に実行できるアセンブラコードを作成することは可能ですか
  • スレッドが本当に小さい(スレッドの合計実行時間のように小さい)場合、パフォーマンスが低下するか、または努力する価値がない場合、パフォーマンスは向上しますか?

私の推測では、マルチスレッドアセンブラコードは、システムコールなしではできません。これが事実であるとしても、スレッドを可能な限り効率的に実装するための提案がありますか(またはさらに良い:いくつかの実際のコード)?

36
sven

簡単に言えば、それは不可能です。アセンブリコードを作成すると、1つだけの論理(つまりハードウェア)スレッドで順次(または分岐を使用して)実行されます。コードの一部を別の論理スレッドで実行する場合(同じコア上、同じCPU上の別のコア上、または別のCPU上でも)、OSに他のスレッドの命令ポインターを設定する必要があります( CS:EIP)実行するコードをポイントします。これは、システムコールを使用してOSに必要な処理を行わせることを意味します。

ユーザースレッドはすべて同じハードウェアスレッドで実行されるため、必要なスレッドサポートが提供されません。

編集: Ira Baxterの回答をParlanseに組み込みます。まず、各論理スレッドで実行中のスレッドがプログラムにあることを確認すると、OSに依存せずに独自のスケジューラを構築できます。どちらの方法でも、あるスレッドから別のスレッドへのホッピングを処理するスケジューラが必要です。スケジューラの呼び出しの間に、マルチスレッドを処理するための特別なアセンブリ命令はありません。スケジューラ自体は、特別なアセンブリに依存することはできませんが、各スレッドのスケジューラの部分間の規則に依存することができます。

どちらの方法でも、OSを使用するかどうかにかかわらず、クロススレッド実行を処理するには、いくつかのスケジューラーに依存する必要があります。

24
Nathan Fellman

「医者、医者、私がこれを行うと痛いです」。医者:「それをしないでください」。

つまり、高価なOSタスク管理プリミティブを呼び出さなくてもマルチスレッドプログラミングを実行できます。スレッドスケジューリング操作ではOSを無視してください。つまり、独自のスレッドスケジューラを記述し、OSに制御を渡さないでください。 (そして、かなりスマートなOSの人よりも、スレッドのオーバーヘッドについてどういうわけか賢い必要があります)。 Windowsプロセス/スレッド/ファイバーの呼び出しはすべて数百の命令の計算粒度をサポートするには高すぎるため、このアプローチを選択しました。

私たちのPARLANSEプログラミング言語は並列プログラミング言語です: http://www.semdesigns.com/Products/Parlanse/index.html を参照してください

PARLANSEはWindowsで実行され、抽象的な「グレイン」を抽象的な並列構成として提供し、高度に調整された手書きスケジューラと、グレインのコンテキストを考慮してスケジューリングを最小化するPARLANSEコンパイラによって生成されるスケジューリングコードの組み合わせによって、そのようなグレインをスケジュールします。オーバーヘッド。たとえば、コンパイラーは、スケジューリング(「待機」など)が必要になる可能性がある時点で、穀物のレジスターに情報が含まれていないことを確認します。したがって、スケジューラー・コードはPCとSPを保存するだけで済みます。実際、スケジューラコードがまったく制御を取得しないことがよくあります。フォークされたグレインは、フォークしたPCとSPを格納し、コンパイラが事前に割り当てたスタックに切り替えて、グレインコードにジャンプします。グレインが完了すると、forkerが再起動します。

通常、粒度を同期するためのインターロックがあり、セマフォのカウントに相当するものを実装するネイティブのLOCK DEC命令を使用してコンパイラーによって実装されます。アプリケーションは論理的に数百万のグレインをフォークできます。スケジューラーは、作業キューが十分に長く、それ以上の作業が役に立たない場合に、親粒度がより多くの作業を生成することを制限します。スケジューラーは、ワークスティーリングを実装して、作業不足のCPUが隣接するCPUのワークキューから準備完了したグレインを取得できるようにします。これは、最大32個のCPUを処理するように実装されています。しかし、x86ベンダーが今後数年間でそれ以上の使用を実際に抱える可能性があることを少し心配しています!

PARLANSEは成熟した言語です。 1997年から使用しており、数百万行の並列アプリケーションを実装しています。

15
Ira Baxter

ユーザーモードのスレッドを実装します。

歴史的に、スレッドモデルはN:Mとして一般化されています。つまり、Mカーネルモデルスレッドで実行されるNユーザーモードスレッドです。現代の使用法は1:1ですが、常にそうであるとは限らず、必ずしもそうである必要はありません。

単一のカーネルスレッドで、任意の数のユーザーモードスレッドを自由に維持できます。それらが同時に見えるように十分頻繁にそれらを切り替えるのはあなたの責任であるというだけです。もちろん、スレッドは先制ではなく協調的です。定期的な切り替えが確実に行われるように、基本的にはコード全体でyield()呼び出しをスキャッティングしています。

7
user82238

パフォーマンスを得たい場合は、カーネルスレッドを利用する必要があります。カーネルだけが、複数のCPUコアで同時にコードを実行するのに役立ちます。プログラムがI/Oバインドされている(または他のブロック操作を実行している)場合を除き、ユーザーモードの協調マルチスレッド( fibers とも呼ばれます)を実行してもパフォーマンスは向上しません。追加のコンテキストスイッチを実行するだけですが、実際のスレッドが実行している1つのCPUは、どちらの方法でも100%で実行されます。

システムコールが速くなりました。最近のCPUはsysenter命令をサポートしており、古いint命令よりも大幅に高速です。 Linuxがシステムコールを最速で実行する方法については、 この記事 も参照してください。

自動生成されたマルチスレッドで、パフォーマンスが向上するのに十分な時間スレッドが実行されていることを確認してください。短いコードを並列化しようとしないでください。スレッドの生成と結合に時間を浪費するだけです。メモリの影響にも注意してください(これらの測定と予測は困難ですが)-複数のスレッドが独立したデータセットにアクセスしている場合、 キャッシュコヒーレンシにより、同じデータに繰り返しアクセスしている場合よりもはるかに速く実行されます。 問題。

5
Adam Rosenfield

最初に、Cでスレッドを使用する方法を学ぶ必要があります(pthreads、POSIX theads)。 GNU/Linuxでは、おそらくPOSIXスレッドまたはGLibスレッドを使用する必要があります。次に、アセンブリコードからCを呼び出すだけです。

ここにいくつかのポインタがあります:

  • Posixスレッド: リンクテキスト
  • アセンブリからC関数を呼び出す方法を学習するチュートリアル: link text
  • POSIXスレッドに関するブテンホフの本 link text
3

syscallの代わりにsysenterまたはintを使用して、システムコールはそれほど遅くありません。それでも、スレッドを作成または破棄するときにオーバーヘッドが発生するだけです。いったん実行されると、システムコールはありません。ユーザーモードスレッドは1つのコアでのみ実行されるため、実際には役に立ちません。

3
Zifre

今はかなり遅れましたが、私はこの種のトピックに私自身興味を持っていました。実際、並列化/パフォーマンスのためにカーネルがEXCEPTに介入することを特に必要とするスレッドについては、特別なことは何もありません。

必須BLUF

Q1:いいえ。さまざまなCPUコア/ハイパースレッドにわたって複数のカーネルスレッドを作成するには、少なくとも初期システムコールが必要です。

Q2:場合によります。小さな操作を実行するスレッドを作成または破棄すると、リソースが無駄になります(スレッドの作成プロセスは、トレッドが終了する前にトレッドが使用する時間を大幅に超過します)。 N個のスレッド(Nはシステム上のコア/ハイパースレッドの数)を作成して再タスク化すると、実装に応じて答えは「はい」になります。

Q3:事前に正確な操作の順序付け方法を知っている場合は、操作を最適化できます。具体的には、ROPチェーン(または転送呼び出しチェーン)に相当するものを作成できますが、実際には実装がより複雑になる可能性があります。このROPチェーン(スレッドによって実行される)は、「ret」命令を(それ自体のスタックに対して)継続的に実行し、そのスタックは継続的に先頭に追加されます(または最初にロールオーバーする場合は追加されます)。そのような(奇妙な!)モデルでは、スケジューラーは各スレッドの「ROPチェーンの終わり」へのポインターを保持し、そこに新しい値を書き込みます。これにより、コードはメモリを循環し、最終的にret命令になります。繰り返しますが、これは奇妙なモデルですが、それでも興味深いものです。

2セント相当のコンテンツに。

私は最近、さまざまなスタック領域(mmapを介して作成)を管理し、「スレッド」の制御/個別化情報を格納する専用領域を維持することにより、純粋なアセンブリでスレッドとして効果的に機能するものを作成しました。この方法では設計していませんが、mmapを使用して単一の大きなメモリブロックを作成し、各スレッドの「プライベート」領域に分割することができます。したがって、必要なシステムコールは1つだけです(ただし、その間のガードページは賢く、追加のシステムコールが必要になります)。

この実装は、プロセスが生成されたときに作成されたベースカーネルスレッドのみを使用し、プログラムの実行全体を通じて単一のユーザーモードスレッドのみが存在します。プログラムは自身の状態を更新し、内部制御構造を介して自身をスケジュールします。 I/Oなどは、可能であれば(複雑さを軽減するために)ブロックオプションを介して処理されますが、これは厳密には必須ではありません。もちろん、ミューテックスとセマフォを利用しました。

このシステムを実装するには(完全にユーザー空間で、必要に応じて非ルートアクセスを介して)、以下が必要です。

スレッドが沸騰する概念:スタック操作用のスタック(自己説明的で明白)実行する一連の命令(これも明白)個々のレジスタの内容を保持するメモリの小さなブロック

スケジューラーの要約:スケジューラー指定の順序付きリスト(通常は優先順位)内の一連のスレッドのマネージャー(プロセスが実際に実行されることはなく、スレッドのみが実行されることに注意)。

スレッドコンテキストスイッチャー:コードのさまざまな部分に挿入されるMACRO(通常、これらをヘビーデューティー関数の最後に配置します)。これは、スレッドの状態を保存し、別のスレッドの状態をロードする「スレッドイールド」にほぼ相当します。

したがって、(ルート全体で、初期mmapとmprotect以外のシステムコールなしで)非ルートプロセスでユーザーモードのスレッドのような構成を作成することは確かに可能です。

X86アセンブリについて具体的に言及しているため、この回答のみを追加しました。この回答は、システムコールを最小化し、システム側のスレッドを最小化するという目標(マルチコア機能を除く)を完全に達成するx86アセンブリで完全に記述された自己完結型プログラムによって完全に導き出されました。オーバーヘッド。

3
Nick