カーネルスレッドとは
カーネルスレッドで代表的ななものとして、プロセスIDが1のすべての親となる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関数で、切り替え後処理でその判断をするためです。
カーネルスレッドはカーネルの補助的な処理を行うものだと推測できても、ユーザプロセスとどう違うのでしょうか? 実はカーネルとしては、スケージューリングにおいてユーザプロセスと同じ物だということです。カーネルスレッドの作成は、ユーザプロセス作成と同じように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にもネームスペースを持たせていて、このネームスペースですが、initプロセスからのネームスペース/カレントプロセスからのネームスペース等と、pidレベルと称するもので、それぞれに独自にpidを持たせるような込み入った実装となっています。改めてこの辺りの実装を追ってみたいと思います。