Linuxカーネル解読室プロジェクト: 新Linuxカーネル解読室 – Workqueue – VA Linux エンジニアブログ

「Linuxカーネル2.6解読室」(以降、旧カーネル解読室)出版後、Linuxには多くの機能が追加され、エンタープライズ領域をはじめとする様々な場所で使われるようになりました。
それに伴いコードが肥大かつ複雑化し、多くのエンジニアにとって解読不能なブラックボックスとなっています。
世界中のトップエンジニア達の傑作であるLinuxカーネルにメスを入れ、ブラックボックスをこじ開けて、時に好奇心の赴くままにカーネルの世界を解読する「新Linuxカーネル解読室」プロジェクト。

執筆者 : Ryo Takakura

※ 「新Linuxカーネル解読室」連載記事一覧はこちら


遅延処理の仕組みであるWorkqueue (WQ) について紹介したいと思います。WQは旧カーネル解読室でも解説されていますが、その実装は当時 (v2.6) から大きく変わっています。本記事では、当時のWQ (以後旧WQ) が現在の実装に至るまでを、背景等を交えながら紹介できればと思います。なお本記事は、筆者がWQを調べていて面白いと感じたことをまとめたような記事です。読者の方が本記事を通してWQに興味を持って頂けたら嬉しいです。

WQは、Linuxにおいて用意されている遅延処理の仕組みの一つです。遅延処理を行う目的は、ソフト割り込みのようなハード割り込みの処理を遅延させカーネルの応答性を向上させるためであったり、NMIのようにコンテキスト自体に制約 (ロック取得など) があり他コンテキストに処理を委譲する必要があったりと様々です。WQは、処理をプロセスコンテキストへ遅延するために提供されている仕組みです。各種デバイスドライバやファイルシステムのIOなどカーネルで幅広く使われています。処理を実行するスレッドはワーカースレッド、実行される処理はワークアイテムと呼ばれます。本記事では、このワーカースレッドがワークアイテムを実行するまでの仕組みを、まずは比較的シンプルに実装されていた旧WQにおいて確認したいと思います。その後、現在のWQのベースとなるConcurrency Managed Workqueue (CMWQ) が取り込まれた直後の実装を参照しながら、WQにおける “Concurrency Management” についておさらいしたのち、旧WQと比較する形でその必要性を確認していきます。現在のWQの実装と直近のWQにおける取り組みについても、最後に簡単に紹介できればと思います。なお記事では、alloc_workqueue(), destroy_workqueue() 等で操作の対象となる “workqueue” は、システムとしての”WQ”と区別して使用しています。

WQでは、v2.6.36において取り込まれたCMWQ以前の実装をLegacy Workqueue (旧WQ)、以降の実装をConcurrency Managed Workqueue (CMWQ) として区別しています。本章では、旧WQの実装および当時挙げられていた旧WQの課題を確認したいと思います。なお、本章が解説のベースとするバージョンはv2.6.35です。

workqueue_struct構造体

workqueue_struct構造体は、旧WQから現在に至るまで、各workqueueに割り当てられるワーカースレッドとワークアイテムを管理する役割を担ってきた構造体です。ここでは、旧カーネル解読室 (旧WQ) のworkqueue_struct構造体の説明をそのまま参照してみたいと思います。workqueue_struct構造体の考え方は、旧カーネル解読室当時から基本的に変わっていませんが、旧WQとCMWQにおける大きな違いとして割り当てられるワーカースレッドが挙げられます。そのため、本記事のテーマは、(ざっくりと) workqueueに対するワーカースレッドの割り当て方の変遷です。

workqueueは、workqueue_struct構造体の配列で、目的ごとに専用のworkqueueを用意しています。それぞれのCPU用のエントリを持ち、そのCPU上で実行すべき処理 (work_struct構造体) を複数登録できます。
各workqueue_struct構造体には、専用のカーネルスレッドが割り当てられています。おのおののスレッドは、属しているworkqueue_struct構造体に登録されている処理を (work_struct構造体) を実行する役目を受け持ちます。マルチプロセッサシステムの場合、それぞれのCPU用に専用のカーネルスレッドを用意しています。

workqueueの作成から実行まで

先の旧カーネル解読室の説明を、実際に当時の実装を参照しながら確認したいと思います。以下は、旧WQにおけるworkqueue_struct構造体を確保するAPI (create_workqueue()) の延長で呼ばれる__create_workqueue_key()の実装になります。旧カーネル解読室の説明にもあった通り、ワーカースレッドはworkqueue専用・CPU毎 (L.1035) に確保されること (L.1039) が分かります (以後Multi Threaded (MT))。Single Threaded (ST) なworkqueueの場合 (L.1014) には、CPU毎のワーカースレッドの割り当ては行われず、システム全体に対して一つのワーカースレッドがworkqueue専用に確保されます (L.1016)。

(kernel/workqueue.c)

   986 struct workqueue_struct *__create_workqueue_key(const char *name,
   987                                                 int singlethread,
   988                                                 int freezeable,
   989                                                 int rt,
   990                                                 struct lock_class_key *key,
   991                                                 const char *lock_name)
   992 {
   993         struct workqueue_struct *wq;
   994         struct cpu_workqueue_struct *cwq;
   995         int err = 0, cpu;
   996
   997         wq = kzalloc(sizeof(*wq), GFP_KERNEL);
   998         if (!wq)
   999                 return NULL;
  1000
  1001         wq->cpu_wq = alloc_percpu(struct cpu_workqueue_struct);
                ...
  1009         wq->singlethread = singlethread;
  1010         wq->freezeable = freezeable;
  1011         wq->rt = rt;
                ...
  1013
  1014         if (singlethread) {
  1015                 cwq = init_cpu_workqueue(wq, singlethread_cpu);
  1016                 err = create_workqueue_thread(cwq, singlethread_cpu);
  1017                 start_workqueue_thread(cwq, -1);
  1018         } else {
                ...
  1035                 for_each_possible_cpu(cpu) {
  1036                         cwq = init_cpu_workqueue(wq, cpu);
  1037                         if (err || !cpu_online(cpu))
  1038                                 continue;
  1039                         err = create_workqueue_thread(cwq, cpu);
  1040                         start_workqueue_thread(cwq, cpu);
  1041                 }
  1042                 cpu_maps_update_done();

以下は、ワークアイテムをqueueするためのAPIであるqueue_work()の実装です。ワークアイテムは、MTの場合にはqueueされたCPU (L.288) においてqueueされた順に処理され、STの場合にはCPUグローバルにqueueされた順に処理されます。このワークアイテムがqueueされたCPUにおいて処理される点は、WQの仕様として当時から現在に至るまで変わっていません。queueされたワークアイテムは、queueに伴い起床したworkqueueに紐づく (MTの場合にはCPU毎な) カーネルスレッドによって実行されます。
なお当時のMTとSTの使い分けとしては、CPUグローバルにワークアイテムを逐次実行する必要がある等の場合にはSTが使用され、特に明示的な理由がない場合はMTなworkqueueが使用されていたようです。

(kernel/workqueue.c)

   284 int queue_work(struct workqueue_struct *wq, struct work_struct *work)
   285 {
   286         int ret;
   287 
   288         ret = queue_work_on(get_cpu(), wq, work);
   289         put_cpu();
   290 
   291         return ret;
   292 }
   293 EXPORT_SYMBOL_GPL(queue_work);

以下は、MTにおけるworkqueueとワーカースレッド・ワークアイテムの関係を示した図です。CPU毎のワーカースレッドがworkqueueごとに割り当てられていることがわかります。

旧WQにおける課題

次章でCMWQを紹介するにあたり、ここまでで簡単に確認した旧WQの特徴を踏まえて、当時挙げられていた旧WQの課題をいくつか紹介したいと思います。

増えすぎたワーカースレッドとその弊害

Linuxでは、システムにおける最大PID数のデフォルトは32Kとなっています。前節で確認した通り、旧WQではworkqueueごとにワーカースレッドが確保され、MTの場合にはさらにCPUの数だけ確保されます。このことから、システムのCPU数増加に伴うワーカースレッドの増加によるPIDの枯渇が問題となりました。この増えすぎたワーカースレッドは、PID等のリソース消費の原因になる他、ワーカースレッド間におけるコンテキストスイッチの増加など性能を低下させる原因となります。
また、MTにおいてCPUごとに割り当てられるワーカースレッド間では、お互いのワークアイテムの実行状況は連携されず、ワークアイテムはqueueされたタイミングに応じて各CPUのワーカースレッドによって独立に処理されていました。これは、ワークアイテム間に依存関係はあるが、マルチコアなシステムにおける並列性を生かしたいユーザーにとって並列性(MT)と逐次実行(ST)のトレードオフの問題となってしまいます。もしこのトレードオフに縛られず、ワークアイテム間の依存関係を考慮しながら並列に処理を実行したい場合には、当時のfs/ceph/messenger.cにおいて実装されているような、ワークアイテムの実行状況をトラッキングするような独自の仕組みを実装する必要があります。
次章では、このユースケースにみられるようなユーザーが多くを考慮する必要があった旧WQの側面を踏まえながら、CMWQにおけるConcurrency Managementの実装と必要性について確認したいと思います。なお、net/ceph/messenger.cの独自Concurrency Managementについては、CMWQにおいてcommit f363e45fd1184 (“net/ceph: make ceph_msgr_wq non-reentrant”) で解消されているので、気になる方は次章を読んだ後に確認してみてください。

概要

現在のWQであるConcurrency Managed Workqueue (CMWQ) は、前章で確認した旧WQにおける課題を踏まえた Concurrency Managed なWQです。しかし、CMWQがv2.6.36で実装されてから現在に至るまで、WQには様々な修正が入っています。そのため、まず本章では旧WQとの比較が容易なCMWQ実装直後のv2.6.36をベースに、CMWQが旧WQと比較して Concurrency Managed であることを確認したいと思います。現在のWQについては、本章でCMWQが旧WQと比較して Concurrency Managed であることを確認した上で、最後に簡単に紹介したいと思います。

Concurrency Managementとは

前章では、旧WQにおいてワーカースレッドが、workqueueごとに確保されることを確認しました。CMWQにおいては、workqueueごとにワーカースレッドは確保されず、あらかじめ用意されているworkqueue globalなワーカースレッドが割り当てられます。このworkqueue globalなワーカースレッドは、CMWQで新たに導入されたper cpuなglobal_cwq構造体 (以降gcwq) に紐付けられています。gcwqには、ワーカースレッドに加えて、ワークアイテムもworkqueueに関わらず紐付けられます。そのため、旧WQにおいてworkqueueごとに管理されていたワークアイテムやワーカースレッドは、CMWQではCPU単位で管理されます。

(kernel/workqueue.c)

   143 




   148 struct global_cwq {
   149         spinlock_t              lock;           
   150         struct list_head        worklist;       
   151         unsigned int            cpu;            
   152         unsigned int            flags;          
   153 
   154         int                     nr_workers;     

以下は、WQを確保する際に呼ばれる__alloc_workqueue_key()の抜粋です。旧WQの__create_workqueue_key()と比較すると分かるとおり、CMWQにおいてはworkqueueごとの新たなワーカースレッドの確保は行われず、per cpuなgcwqが紐付けられるのみ (L.2799) となります。

(kernel/workqueue.c)

  2757 struct workqueue_struct *__alloc_workqueue_key(const char *name,
  2758                                                unsigned int flags,
  2759                                                int max_active,
  2760                                                struct lock_class_key *key,
  2761                                                const char *lock_name)
  2762 {
  2763         struct workqueue_struct *wq;
  2764         unsigned int cpu;
...
  2794         for_each_cwq_cpu(cpu, wq) {
  2795                 struct cpu_workqueue_struct *cwq = get_cwq(cpu, wq);
  2796                 struct global_cwq *gcwq = get_gcwq(cpu);
  2797 
  2798                 BUG_ON((unsigned long)cwq & WORK_STRUCT_FLAG_MASK);
  2799                 cwq->gcwq = gcwq;
  2800                 cwq->wq = wq;
  2801                 cwq->flush_color = -1;
  2802                 cwq->max_active = max_active;
  2803                 INIT_LIST_HEAD(&cwq->delayed_works);
  2804         }

ここまでを整理すると、旧WQではworkqueueごとに割り当てられていたワーカースレッドは、CMWQでは以下図のように各CPUにworkqueue globalに割り当てられます。次節では、このgcwq導入に伴うworkqueueとワーカースレッドの関係を踏まえて、前章で挙げた旧WQの課題がどのようにCMWQにおいて解消されたかを確認します。

Concurrency Managementの具体例

Concurrency Managementに求められる要件は、以下ドキュメンテーションにもある通り、ワーカースレッド (Execution Context) が必要以上 (minimal) に存在せず、かつ実行が滞りなく (sufficient) 行われることです。本節では、このExecution Contextであるワーカースレッドがminimal but sufficient levelな状態であるという要件が満たされない具体的なケースを通して、Concurrency Managementについて考えてみたいと思います。

(Documentation/core-api/workqueue.rst)

For any thread pool implementation, managing the concurrency level
(how many execution contexts are active) is an important issue. cmwq
tries to keep the concurrency at a minimal but sufficient level.
Minimal to save resources and sufficient in that the system is used at
its full capacity.

WQ_CPU_INTENSIVE

まずは、実行時間が長いワークアイテムAと、ワークアイテムAとは異なるworkqueueに属するワークアイテムBが同じCPUにqueueされた状態を考えてみます。この場合、ワークアイテムBは、ワークアイテムAの完了を待ち続けることになります。旧WQにおいては、各WQが各々のワーカースレッドを割り当てられていたこと (Execution contextがmore than sufficient, resourceがwastedな状態) から、ワークアイテムAの処理に時間がかかるような場合でも、処理しているワーカースレッドはシステムのスケジューラによりスケジュールアウトされ、いずれワークアイテムBを処理するためのワーカースレッドがスケジューリングされました。しかし、CMWQにおいては、ワーカースレッドが共有されることから、旧WQのようにスケジューラ頼みではワークアイテムBは実行されません。このようなExecution Contextがless than sufficientな状態になった場合、新たなExecution Context (ワーカースレッド) が必要となります。WQ_CPU_INTENSIVEは、このワークアイテムAのような実行時間がかかるであろうとされるワークアイテムに対して用いられるフラグとなります。これから実行されるワークアイテムがWQ_CPU_INTENSIVE指定されていた場合 (L.1813) には、続くワークアイテムを処理するための新たなワーカースレッドを起床します (Execution Contextをminimal but sufficient levelな状態に保つ)。

(kernel/workqueue.c)

  1750 static void process_one_work(struct worker *worker, struct work_struct *work)
    ...
  1809         



  1813         if (unlikely(cpu_intensive))
  1814                 worker_set_flags(worker, WORKER_CPU_INTENSIVE, true);

起床するかどうかの判定が行われるworker_set_flags()では、このワーカースレッド以外のローカルCPUにおけるワーカースレッドの有無 (L.755)・処理待ちのワークアイテムの有無 (L.756) を確認します。ローカルCPUで実行されているワーカースレッドが無く、処理待ちのワークアイテムが有る場合には、新たにワーカースレッドを起床します (L.757) 。

(kernel/workqueue.c)

   738 static inline void worker_set_flags(struct worker *worker, unsigned int flags,
   739                                     bool wakeup)
   740 {
   741         struct global_cwq *gcwq = worker->gcwq;
   742 
   743         WARN_ON_ONCE(worker->task != current);
   744 
   745         




   750         if ((flags & WORKER_NOT_RUNNING) &&
   751             !(worker->flags & WORKER_NOT_RUNNING)) {
   752                 atomic_t *nr_running = get_gcwq_nr_running(gcwq->cpu);
   753 
   754                 if (wakeup) {
   755                         if (atomic_dec_and_test(nr_running) &&
   756                             !list_empty(&gcwq->worklist))
   757                                 wake_up_worker(gcwq);
   758                 } else
   759                         atomic_dec(nr_running);
   760         }
   761 
   762         worker->flags |= flags;
   763 }

ワークアイテムがsleepした場合

Execution Contextがminimal but sufficient levelな状態ではなくなる他のケースとして、ワークアイテムがsleepした場合が挙げられます。この場合、前節と同様にCMWQにおいてワーカースレッドがworkqueue間で共有されていることから、sleepしたワークアイテムに続くワークアイテムは処理されません。このようなワークアイテムのsleepに伴うExecution Contextがless than sufficientな状態になった場合のため、CMWQではスケジューラに対してフックを用意しています。スケジューラでは、sleep状態に移行するスレッドがワーカースレッドである場合 (L.3778)、新たにワーカースレッドを起床するかどうか確認します (L.3782)。wq_worker_sleeping() (L.3781)では、worker_set_flags()同様の確認を行い、ローカルCPUで実行されているワーカースレッドが無く、処理待ちのワークアイテムが有る場合には、新たにワーカースレッドを起床します (L.3783)。

(kernel/sched.c)

  3739 


  3742 asmlinkage void __sched schedule(void)
                ...
  3772                         





  3778                         if (prev->flags & PF_WQ_WORKER) {
  3779                                 struct task_struct *to_wakeup;
  3780 
  3781                                 to_wakeup = wq_worker_sleeping(prev, cpu);
  3782                                 if (to_wakeup)
  3783                                         try_to_wake_up_local(to_wakeup);
  3784                         }

現在のWQ

前節において紹介したCMWQでは、workqueueにはあらかじめworkqueueグローバルに確保されているワーカースレッドが割り当てられ、workqueueに割り当てるワーカースレッドのnice値といった属性をユーザーが指定することはできませんでした。しかし、現在のWQでは/sys/devices/virtual/workqueue/WQ_NAME/apply_workqueue_attrs()を通して、workqueueに割り当てるワーカースレッドの属性を指定・変更することできます。ユーザーから指定された属性を保持するワーカースレッドが存在しない場合には、新たに指定された属性を保持するワーカースレッドが作成されます。

$ ls /sys/devices/virtual/workqueue/writeback/
affinity_scope  affinity_strict  cpumask  max_active  nice  per_cpu  power  subsystem  uevent

そのため、現在のWQにおけるConcurrency Managementは属性を共有しているワーカースレッド間で行われます。なお、各ワーカースレッドとCPUの関係はこれまでと変わらないため、前節で紹介した主な構造体は、現在のWQにおいては以下のように対応しています。

struct global_cpu_workqueue -> struct worker_pools
struct cpu_workqueue_struct -> struct pool_workqueue

異なる属性が与えられたワーカースレッドが2つある場合 (ATTR 1とATTR 2) には、それぞれのワーカースレッドとCPUの対応は以下のようになります。

本記事では、WQの現在の実装に至るまでの経緯を辿ってきました。最後に、WQが導入されるに至った背景それ自体について、昨年のtaskletに関する取り組みを交えて余談的に取り挙げたいと思います。

WQへのtasklet統合

これまでにもtaskletについては、リファクタリングが必要として度々議論が行われてきましたが、WQに統合されるという形で昨年 (ついに?) 手が加えられました。taskletが導入されるに至った経緯を遡ると、旧カーネル解読室にも説明されているように、WQとは切り離せない関係にあります。

Linuxカーネル2.4以前は、taskqueueと呼ばれる遅延処理の仕組みが利用されていました。割り込みコンテキストの遅延処理もプロセスコンテキストの遅延処理も、このtaskqueueを利用して実現していました。Linuxカーネル2.6では、マルチプロセッサ動作時の効率化のために、割り込みコンテキストの遅延処理はすべてtaskletに書き直し、プロセスコンテキストの遅延処理のためにworkqueueを導入しました。

今回の統合のきっかけとなったのは、tasklet実行後に発生するuse-after-freeが問題 *1 となったことでしたが、過去にはtaskletがソフト割り込みコンテキストで実行されること自体が、正確なスケジューリングを妨げる・プロセスの処理完了を遅らせる原因になるとして問題に挙げられることもあったようです。今回の統合によって、taskletはAPIとしては廃止され、再びWQと統合されましたが、taskletたる所以であるソフト割り込みコンテキストにおける処理の遅延実行は仕組みとして残されました。このソフト割り込み処理において発生する遅延に対して、今後カーネルがどう取り組むのか筆者は気になっています。




Source link

関連記事

コメント

この記事へのコメントはありません。