カーネルスレッドとは
Rev.3を表示中。最新版はこちら。
カーネルスレッドで代表的ななものとして、プロセスIDが0のすべての親となるinitです。これはカーネル起動時作成されます。他にkeventd(ワークキュ)、kswapd(メモリー回収)、ksoftoirqd(ソフト割り込み)等があり、必要に応じて作成されたりいたします。カーネルスレッドはカーネルの補助的な処理を行うものだと推測できても、ユーザプロセスとどう違うのでしょうか? 実はカーネルとしては、スケージューリングにおいてユーザプロセスと同じ物だということです。カーネルスレッドの作成は、ユーザプロセス作成と同じようにCLONE_VM属性でdo_fork関数で作成されます。すなわちカーネルとして1プロセスディスクリプターとして、処理しているに過ぎません。そうすることで、スケージューリングの中で、カーネルスレッドが動作することになり、全体的なパフォーマンスの効率化がはかられるわけです。
CLONE_VMは作成するプロセス(親)のメモリー空間を共有するという事ですが、実はここでのCLONE_VMの意味合いは、共有する目的でなく、あえてメモリー空間の実態を作成するのは意味がないゆえ、そのような無駄な処理を避けるということにあるようです。
ユーザプロセスはユーザモードとカーネルモードを行き来します。従ってユーザプロセスに割り当てられるメモリー空間は4G(ただしユーザモードは3G,カーネルモードは1Gとしてしかアクセスできません。)ですが、カーネルスレッドはメモリー空間を有していないのです。カーネルスレッドは、カーネルスレッドに切り替えるプロセスのメモリー空間で動作しています。タスクスイッチングでカーネルスレッドに切り替えられる毎に、その動作メモリー空間は異なる事になってしまいます。しかしカーネルスレッドはユーザメモリー空間(3G空間)を参照しません。参照するのはカーネルメモリー空間だけです。
ユーザプロセスはお互いに独立した異なるメモリー空間を有していますが、3G以降の1Gはカーネル空間として共通となっています。従ってカーネルスレッド自身にメモリー空間を有することなく、他の任意のユーザプロセスのメモリー空間で動作することが可能となっています。
タスク切り替えは、 schedule関数からcontext_switch関数をコールする事で行います。プロセスディスクリプターのtask_structには、メモリディスクリプターとしてmmとactive_mmの2つを有しています。mmはそのプロセスが有しているメモリ空間で、active_mmはそのプロセスが動作しているメモリ空間です。従ってユーザプロセスはmm=active_mmde、カーネルプロセスmm=NULLでmm=active_mmdeは切り替えるプロセスのmmとなります。(プログラム的にはカーネルスレッドからカーネルスレッドへの切り替えがあるわけで、mm=active_mmとしている。)
context_switch関数で上記の処理が行われます。mm = next->mmで次に動作するプロセスが有しているメモリーディスクリプタを取得し、oldmm = prev->active_mmで前の(現動作している。)プロセスが動作しているメモリー空間のメモリーディスクリプタを取得しています。
!mmというのは次に動作するプロセスがカーネルスレッドと言うことです。その場合、next->active_mm = oldmmで、現動作しているメモリー空間でカーネルスレッドが動作するようにしています。そしてそのメモリー空間カウントをインクリメントすることで、そのメモリー空間を有しているプロセスが削除されても、そのメモリー空間が削除されないようにしています。
!prev->mmは現動作プロセスがカーネルスレッドということです。カーネルスレッドはもう動作する必要がないのでprev->active_mm = NULLとしています。そしてカーネルスレッドが動作していたメモリー空間を、CPU変数にrq->prev_mm = oldmmとしています。これはfinish_task_switch関数で、切り替え後処理でその判断をするためです。
static inline void context_switch(struct rq *rq, struct task_struct *prev, struct task_struct *next) { struct mm_struct *mm, *oldmm; : : mm = next->mm; oldmm = prev->active_mm; : : if (unlikely(!mm)) { next->active_mm = oldmm; atomic_inc(&oldmm->mm_count); } else switch_mm(oldmm, mm, next); if (unlikely(!prev->mm)) { prev->active_mm = NULL; rq->prev_mm = oldmm; } switch_to(prev, next, prev); : finish_task_switch(this_rq(), prev); }finish_task_switch関数切り替えられたプロセスの後処理を行います。ここではそれがカーネルスレッドということで。rq->prev_mmがNULLでないなら、それはカーネルスレッドが実行権が無くなったことを意味します。そしてmmdrop関数を呼び出して、そのメモリー空間の参照カウントをデクリメントし、(カーネルスレッドが動作する場合インクリメントしていました。)。そしてそれが0(参照されていない)ならメモリー空間そのもの解放します。
static void finish_task_switch(struct rq *rq, struct task_struct *prev) __releases(rq->lock) { struct mm_struct *mm = rq->prev_mm; : rq->prev_mm = NULL; : if (mm) mmdrop(mm); : }
static inline void mmdrop(struct mm_struct * mm) { if (unlikely(atomic_dec_and_test(&mm->mm_count))) __mmdrop(mm); }
nbystさんへ
[root@localhost ~]# ps x | more PID TTY STAT TIME COMMAND 1 ? Ss 0:06 /sbin/init 2 ? S 0:00 [kthreadd] 3 ? S 0:00 [ksoftirqd/0] 5 ? S 0:00 [kworker/u:0] 6 ? S 0:00 [migration/0] 7 ? S 0:00 [watchdog/0] 8 ? S 0:00 [migration/1] :改めてinitのpidが1になる旨の実装を追ってみたいと思います。