カーネルスレッドとは
Rev.4を表示中。最新版はこちら。
カーネルスレッドで代表的ななものとして、プロセス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] :下記はlinux-2.6.0のpid取得の実装です。静的変数のlast_pid(初期値は0)に+1したものをpidとしています。そのpidをlast_pidにセットし、pidmapの対応するpage内のオフセットビットをセットしています。従って最初のpidは1となるわけですね。
#define PIDMAP_ENTRIES (PID_MAX_LIMIT/PAGE_SIZE/8) int pid_max = PID_MAX_DEFAULT; int last_pid; typedef struct pidmap { atomic_t nr_free; void *page; } pidmap_t; int alloc_pidmap(void) { int pid, offset, max_steps = PIDMAP_ENTRIES + 1; pidmap_t *map; pid = last_pid + 1; if (pid >= pid_max) pid = RESERVED_PIDS; offset = pid & BITS_PER_PAGE_MASK; map = pidmap_array + pid / BITS_PER_PAGE; if (likely(map->page && !test_and_set_bit(offset, map->page))) { return_pid: atomic_dec(&map->nr_free); last_pid = pid; return pid; } if (!offset || !atomic_read(&map->nr_free)) { next_map: map = next_free_map(map, &max_steps); if (!map) goto failure; offset = 0; } scan_more: offset = find_next_zero_bit(map->page, BITS_PER_PAGE, offset); if (offset >= BITS_PER_PAGE) goto next_map; if (test_and_set_bit(offset, map->page)) goto scan_more; pid = (map - pidmap_array) * BITS_PER_PAGE + offset; goto return_pid; failure: return -1; }なお、linux-3.3.8(現在読んでいるバージョンです。)では、pidにもネームスペースを持たせたた実装となっていて少し込み入った実装となっています。改めてこの辺りの実装を追ってみたいと思います。