カーネルスレッドとは


カーネルスレッドで代表的ななものとして、プロセス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関数で、切り替え後処理でその判断をするためです。
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を持たせるような込み入った実装となっています。改めてこの辺りの実装を追ってみたいと思います。

最終更新 2013/08/19 14:08:40 - north
(2010/10/12 05:15:09 作成)


検索

アクセス数
3570105
最近のコメント
コアダンプファイル - sakaia
list_head構造体 - yocto_no_yomikata
勧告ロックと強制ロック - wataash
LKMからのファイル出力 - 重松 宏昌
kprobe - ななし
ksetの実装 - スーパーコピー
カーネルスレッドとは - ノース
カーネルスレッドとは - nbyst
asmlinkageってなに? - ノース
asmlinkageってなに? - よろしく
Adsense
広告情報が設定されていません。