![コテイカと弟](https://habrastorage.org/files/c6c/410/b72/c6c410b72716448c84e60c48126dce02.jpg)
今回は、異なる視点から計画の問題にアプローチしたいと思います。 つまり、フローではなく、彼らの「弟」の計画についてお話します。 この記事は非常に膨大であることが判明したため、最後にいくつかの部分に分けることにしました。
- Linuxカーネルマルチタスク:割り込みとタスクレット
- Linuxカーネルマルチタスク:ワークキュー
- プロトスレッドと協調マルチタスク
3番目のパートでは、これらすべての異なるエンティティを一見して比較し、いくつかの有用なアイデアを抽出しようとします。 しばらくして、これらのアイデアをEmboxプロジェクトでどのように実践したか、そして小さなスカーフでほぼ完全なマルチタスクを使用してOSを起動した方法について説明します。
主なAPIについて説明し、実装機能を掘り下げて、特に計画作業に焦点を当てて、詳細に説明しようとします。
割り込みとその処理
ハードウェア割り込み( IRQ )は、ハードウェアからの外部非同期イベントであり、プログラムを一時停止し、このイベントを処理するためにプロセッサに制御を渡します。 ハードウェア割り込み処理は次のとおりです。
- 現在の制御フローは一時停止され、コンテキスト情報はフローに戻るために保存されます。
- ハンドラー関数( ISR )は、無効化されたハードウェア割り込みのコンテキストで実行されます。 ハンドラーは、この割り込みに必要なアクションを実行する必要があります。
- 機器には、割り込みが処理されたことが通知されます。 これで、新しい割り込みを生成できるようになります。
- コンテキストを復元して、割り込みを終了します。
ハンドラー関数は十分に大きくすることができますが、無効化されたハードウェア割り込みのコンテキストで実行される場合は許可されません。 したがって、割り込み処理を2つの部分に分割するというアイデアを思いつきました(Linuxでは上半分と下半分と呼ばれます)。
- ISR自体は、中断時に呼び出されますが、最小限の作業のみを行い、後まで延期することはできません。後続の処理に必要な中断に関する情報を収集し、何らかの方法でハードウェアと対話します。たとえば、デバイスからIRQをブロックまたはクリアします( jcmvbkbcおよびZyoma for clearification )および第2部の計画。
- メイン処理が実行される2番目の部分は、ハードウェア割り込みが有効になっている別のプロセッサコンテキストで開始されます。 ハンドラのこの部分は後で呼び出されます。
したがって、遅延割り込み処理に行き着きます。 Linuxは、この目的のためにタスクレットとワークキューを使用します。
タスクレット
つまり、 タスクレットは、独自のスタックもコンテキストも持たない、非常に小さなスレッドです。 このような「フロー」は、迅速かつ完全に機能します。 タスクレットの主な機能:
- タスクレットはアトミックであるため、sleep()およびmutex、セマフォなどの同期プリミティブを使用できません。 ただし、たとえば、スピンロック(スピンまたは循環ロック)を使用できます。
- ISRよりも柔らかいコンテキストで呼び出されます。 このコンテキストでは、ISRの期間中にタスクレットを移動するハードウェア割り込みが許可されます。 Linuxカーネルでは、このコンテキストはsoftirqと呼ばれ、タスクレットの起動に加えて、他のいくつかのサブシステムで使用されます。
- taskletは、計画どおりに同じカーネルで実行されます。 より正確には、softirqを呼び出すことで最初にそれを計画することができました。softirqのハンドラーは常に呼び出し側カーネルに関連付けられています。
- 異なるタスクレットを並行して実行できますが、同時に実行されるのは、最初に実行をスケジュールする1つのコアのみであるため、同時に実行されません。
- タスクレットは、プリエンプティブプランニングの原則に従って、キューの順序で次々に実行されます。 通常と高の2つの異なる優先順位で計画できます。
「フードの下」を見て、それらがどのように機能するかを見てみましょう。 まず、タスクレット構造自体( <linux / interrupt.h>で定義):
struct tasklet_struct { struct tasklet_struct *next; /* tasklet */ unsigned long state; /* TASKLET_STATE_SCHED TASKLET_STATE_RUN */ atomic_t count; /* , tasklet */ void (*func)(unsigned long); /* tasklet' */ unsigned long data; /* , func */ };
タスクレットを使用する前に、まず初期化する必要があります。
/* tasklet */ void tasklet_init(struct tasklet_struct *t, void (*func)(unsigned long), unsigned long data); DECLARE_TASKLET(name, func, data); DECLARE_TASKLET_DISABLED(name, func, data); /* tasklet */
タスクレットのスケジュール設定は簡単です。タスクレットは、優先度に応じて2つのキューのいずれかに配置されます。 キューは、単一のリンクリストとして整理されます。 さらに、各CPUには独自のキューがあります。 これは、関数を使用して行われます。
void tasklet_schedule(struct tasklet_struct *t); /* */ void tasklet_hi_schedule(struct tasklet_struct *t); /* */ void tasklet_hi_schedule_first(struct tasklet_struct *t); /* */
タスクレットがスケジュールされると、 TASKLET_STATE_SCHEDに設定され、キューに追加されます。 彼がこの状態にある間、それを再び計画することはうまくいきません-この場合、何も起こりません。 taskletは、tasklet_struct構造体の次のフィールドで編成される計画キューの複数の場所に同時に配置することはできません。 ただし、これは、 <linux / list.h>などのオブジェクトフィールドを介してリンクされたリストには当てはまります。
実行のために、タスクレットには状態TASKLET_STATE_RUNが割り当てられます。 ちなみに、タスクレットは実行前にキューから出て、 TASKLET_STATE_SCHED状態が削除されます。つまり、実行中に再びスケジュールできます。 これは、自分で行うことも、たとえば別のコアで中断することもできます。 ただし、後者の場合、彼は最初のコアで実行を完了した後にのみ呼び出されます。
興味深いことに、タスクレットはさらに再帰的にアクティブ化および非アクティブ化できます。 次の関数がこれを担当します。
void tasklet_disable_nosync(struct tasklet_struct *t); /* */ void tasklet_disable(struct tasklet_struct *t); /* tasklet' */ void tasklet_enable(struct tasklet_struct *t); /* */
タスクレットが非アクティブ化された場合、タスクレットは計画キューに追加できますが、再度アクティブ化されるまでプロセッサ上で実行されません。 さらに、タスクレットが複数回非アクティブ化された場合、それはまったく同じ回数アクティブ化される必要があります。構造体のカウントフィールドは、このためだけのものです。
タスクレットを強制終了することもできます。 このように:
void tasklet_kill(struct tasklet_struct *t);
さらに、すでに計画されている場合、彼はタスクレットが実行された後にのみ殺されます。 突然タスクレットがそれ自体を計画する場合、この関数を呼び出す前に、彼がこれを行うことを禁止することを忘れてはなりません-これはプログラマーの良心によるものです。
最も興味深いのは、スケジューラーの役割を果たす関数です。
static void tasklet_action(struct softirq_action *a); static void tasklet_hi_action(struct softirq_action *a);
それらはほとんど同一なので、両方の関数のコードを与えることは意味がありません。 しかし、ここでそれらを詳細に理解する価値があります:
static void tasklet_action(struct softirq_action *a) { struct tasklet_struct *list; local_irq_disable(); list = __this_cpu_read(tasklet_vec.head); __this_cpu_write(tasklet_vec.head, NULL); __this_cpu_write(tasklet_vec.tail, &__get_cpu_var(tasklet_vec).head); local_irq_enable(); while (list) { struct tasklet_struct *t = list; list = list->next; if (tasklet_trylock(t)) { if (!atomic_read(&t->count)) { if (!test_and_clear_bit(TASKLET_STATE_SCHED, &t->state)) BUG(); t->func(t->data); tasklet_unlock(t); continue; } tasklet_unlock(t); } local_irq_disable(); t->next = NULL; *__this_cpu_read(tasklet_vec.tail) = t; __this_cpu_write(tasklet_vec.tail, &(t->next)); __raise_softirq_irqoff(TASKLET_SOFTIRQ); local_irq_enable(); } }
tasklet_trylock()およびtasklet_lock()関数の呼び出しに注意してください。 tasklet_trylock()は、タスクレットをTASKLET_STATE_RUNに設定し、タスクレットをブロックします。これにより、異なるCPUで同じタスクレットが実行されるのを防ぎます。
実際、これらのスケジューラー機能は協調マルチタスクを実装しており、これについては私の記事で詳しく調べました 。 関数は、タスクレットをスケジュールするときにトリガーされるsoftirqハンドラーとして登録されます。
上記のすべての機能の実装は、/ linux / interrupt.hおよびkernel / softirq.cを 含むファイルにあります 。
続く
次の部分では、はるかに強力なメカニズムについて説明します。ワークキューは、割り込みの遅延処理にもよく使用されます。
PS広告用。 また、私たちのプロジェクトに興味のあるすべての人をcodefreeze.ruが主催する会議に招待したいと思います( ハブでの発表 )。 ライブでチャットしたり、主な悪役アボンダレフに興味深い質問をしたり、最後