Linuxでは、全てのプロセスは struct tack_struct
構造体が動的に割り当てられます。Linux 上で動かせる最大のプロセス数の制限は、存在する物理メモリ容量で決まります (kernel/fork.c:fork_init()
参照)。その値は以下のとおりです。つまり、
/*
* The default maximum number of threads is set to a safe
* value: the thread structures can take up at most half
* of memory.
*/
max_threads = mempages / (THREAD_SIZE/PAGE_SIZE) / 2;
になります。
IA32 アーキテクチャでは、基本的に num_physpages/4
を意味しています。例えば、512M のマシンがあれば、32k 個のスレッドを生成できるという訳です。これは古い (2.2 やそれ以前の) カーネルの 4k 弱の制限に比べ相当な改善になっています。
そのうえ、実行時にカーネルを調整できる KERN_MAX_THREADS sysctl(2) や、単純に procfs インターフェースを使って変更することもできます。
# cat /proc/sys/kernel/threads-max
32764
# echo 100000 > /proc/sys/kernel/threads-max
# cat /proc/sys/kernel/threads-max
100000
# gdb -q vmlinux /proc/kcore
Core was generated by `BOOT_IMAGE=240ac18 ro root=306 video=matrox:vesa:0x118'.
#0 0x0 in ?? ()
(gdb) p max_threads
$1 = 100000
Linux システムでは、プロセス群は2種類の異なる方法でリンクされた struct task_struct
構造体の集合として表すことができます。
p->next_task
と p->prev_task
ポインタを用いた円環状のダブルリンクリスト。ハッシュテーブルは、pidhash[]
と呼ばれ、include/linux/sched.h
で定義されています。
/* PID hashing. (shouldnt this be dynamic?) */
#define PIDHASH_SZ (4096 >> 2)
extern struct task_struct *pidhash[PIDHASH_SZ];
#define pid_hashfn(x) ((((x) >> 8) ^ (x)) & (PIDHASH_SZ - 1))
タスクはpid値のハッシュにされ、上記のハッシュ関数はその(0
からPID_MAX-1
の)領域に要素が均一に分散されるようになっています。ハッシュテーブルは、include/linux/sched.h
の中にある find_task_pid()
を使って、与えられた pid のタスクをすぐに見つけられるよう使われています。
static inline struct task_struct *find_task_by_pid(int pid)
{
struct task_struct *p, **htable = &pidhash[pid_hashfn(pid)];
for(p = *htable; p && p->pid != pid; p = p->pidhash_next)
;
return p;
}
各ハッシュリストの(つまり同じ値にハッシュされた)タスクは、p->pidhash_next/pidhash_pprev
によって
リンクされ、hash_pid()
とunhash_pid()
がハッシュテーブルにあるプロセスを挿入したり削除したりするのに使われます。
これらの操作は、WRITEのために取得するtasklist_lock
という読み書きスピンロックの保護のもと行われます。
p->next_task/prev_task
が使うリング状の双方向リンクリストは、システムのすべてのタスクに容易に到達できるように管理されます。これは、include/linux/sched.h
で定義されるfor_each_task()
マクロによって実現されています。
#define for_each_task(p) \
for (p = &init_task ; (p = p->next_task) != &init_task ; )
for_each_task()
のユーザはREAD の tasklist_lock を取得する必要があります。
ここで、for_each_task()
がリストの始点(と終点)を表すのにinit_task
を使用しています。これは、アイドルタスク(pid 0)が終了することがないことから、安全な方法です。
プロセスハッシュテーブルまたはプロセステーブルリンクの変更時には、特に fork()
とexit
、ptrace()
ですが、WRITEのためにtasklist_lock
を取得しなければなりません。
もっと興味深いことには、書き込みを行う時は、ローカルCPUの割り込みも無効にしなければならないのです。これには明白な理由があります。send_sigio()
関数はタスクリストをたどって、READのためにtasklist_lock
を取得し、割り込みコンテキストで kill_fasync()
から呼び出されるからです。
これが、読み込みを行うときには不要でも、書き込みを行うときには必ず割り込みを無効にしなければならない理由です。
さて、task_struct
構造体がどのようにして相互にリンクしているか見ていきましょう。
task_struct
のメンバーを子細にみてみます。これらは相互に結合している UNIX の'struct proc'と 'struct user' と弱い関連があります。
UNIX の他のバージョンでは、タスク状態の情報は2つに分けて格納されます。一つは常にメモリ内に 無ければならない('proc structure'とよばれ、プロセス状態、スケジューリング情報などを含んでいる)ものです。 もう一方は、プロセスが走行するときにだけ必要になる('u area'と呼ばれ、ファイルディスクリプタテーブル、ディスククオタ情報などの)情報です。このような醜い実装がなされたのは、単にメモリが非常に貴重な資源であったという事情がありました。現代のOSでは、(もちろん今学んでいる Linux だけでなく、他の例えば FreeBSD では、Linuxのこの方向性をさらに進歩させたものになっている) このように分離する必要はなく、それゆえ常にメモリ上にあるデータ構造体によりプロセス状態の管理を行うようになっています。
task_struct構造体は、 include/linux/sched.h
で宣言され、そのサイズは現在 1680 byte です。
状態フィールドは次のように宣言されています。
volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */
#define TASK_RUNNING 0
#define TASK_INTERRUPTIBLE 1
#define TASK_UNINTERRUPTIBLE 2
#define TASK_ZOMBIE 4
#define TASK_STOPPED 8
#define TASK_EXCLUSIVE 32
なぜ TASK_EXCLUSIVE
が 16ではなく32と定義されているのでしょうか。実は16は
TASK_SWAPPING
により使われており、(2.3.xのどこかで)TASK_SWAPPING
のリファレンスを削除するときに TASK_EXCLUSIVE
をシフトするのを忘れてしまったからなのです。
p->state
のvolatile
修飾子は、(割り込みハンドラから)非同期で変更され得ることを意味しています。
TASK_RUNNING
印をつけておく理由は、runqueueに置く操作がアトミックでないからである。
runキューを調べるには読み込みのために runqueue_lock
読み書きスピンロックを取得する必要がある。もしそうすると、その後に runqueue 上の各タスクが TASK_RUNNING
状態にあるかどうか見るだろう。しかし、上記の理由からは、逆は真にならない。
同様に, ドライバが自身を(走行しているプロセスのコンテキストに関わらず)TASK_INTERRUPTIBLE
(もしくは TASK_UNINTERRUPTIBLE
) にマークできる。 そしてその後、 schedule()
を呼び出す。これは (runqueue に残されるケースである保留されたシグナルがなければ) runqueueからそれを削除する。
TASK_INTERRUPTIBLE
と同じだが、起こされない点が異なっている。
wait()
により)回収されていない。
TASK_INTERRUPTIBLE
や TASK_UNINTERRUPTIBLE
と OR される。他の多くのタスクといっしょにウエイトキューで休止しているとき、全ての待機タスクを起こすことで"thundering herd"問題を起こす代りに、自分だけ起きることを意味している。タスクフラグは相互排他的ではないプロセス状態についての情報からなっています。
unsigned long flags; /* per process flags, defined below */
/*
* Per process flags
*/
#define PF_ALIGNWARN 0x00000001 /* Print alignment warning msgs */
/* Not implemented yet, only for 486*/
#define PF_STARTING 0x00000002 /* being created */
#define PF_EXITING 0x00000004 /* getting shut down */
#define PF_FORKNOEXEC 0x00000040 /* forked but didn't exec */
#define PF_SUPERPRIV 0x00000100 /* used super-user privileges */
#define PF_DUMPCORE 0x00000200 /* dumped core */
#define PF_SIGNALED 0x00000400 /* killed by a signal */
#define PF_MEMALLOC 0x00000800 /* Allocating memory */
#define PF_VFORK 0x00001000 /* Wake up parent in mm_release */
#define PF_USEDFPU 0x00100000 /* task used FPU this quantum (SMP) */
p->has_cpu
や p->processor
, p->counter
, p->priority
, p->policy
そして p->rt_priority
フィールドはスケジューラと関連があるため、後ほど見ていきます。
p->mm
と p->active_mm
フィールドはそれぞれ、プロセスの mm_struct
構造体で表されるアドレス空間と、プロセスがリアルなアドレス空間を持たない(e.g. カーネルスレッド)時の、アクティブなアドレス空間を示しています。
これはタスクがスケジューラにより中止するときのアドレス空間スイッチにおけるTLBフラッシュを最小限にしてくれます。そして、もし(p->mm
をもたない)カーネルスレッドがスケジュールインするときは、このnext->active_mm
はスケジュールアウトされたタスクのprev->active_mm
に設定されます。このタスクは、もしprev->mm != NULL
であったときのprev->mm
に等しくなります。
CLONE_VM
フラグがclone(2)システムコールに渡されたときや、vfork(2)システムコールによるときは、アドレス空間はスレッドで共有されます。
p->exec_domain
と p->personality
フィールドはタスクのパーソナリティに関連しています。
つまり、あるシステムコールが他のUNIXの"personality"をエミュレートするために振る舞う方法になっています。
p->fs
フィールドはファイルシステム情報からなり、Linuxにおいては、三つの情報要素を意味します。
この構造体はリファレンスカウントも保持します。これは、CLONE_FS
フラグがclone(2)システムコールに渡されたときに、クローンのタスク間で共有できるようにするためです。
p->files
フィールドは、ファイルディスクリプタテーブルからなっています。これもCLONE_FILES
がclone(2)システムコールで指定されたときにタスク間で共有されます。
p->sig
フィールドは、シグナルハンドラからなっています。そしてCLONE_SIGHAND
によりクローンのタスク間で共有されます。
オペレーティングシステムの他の書籍では、「プロセス」をそれぞれ違うように定義しています。「実行時のプログラムのインスタンス」に始まり、「clone(2)やfork(2)システムコールにより生成されるもの」 まで様々です。Linuxにおいては、三つのプロセスの種類があります。
アイドルスレッドはコンパイル時に一つめのCPUのために作られます。そして「手動」でアーキテクチャ特有のarch/i386/kernel/smpboot.c
のfork_by_hand()
により各CPUごとに作られます。この関数では、fork(2)システムコールが(アーキテクチャによっては)手で展開されています。アイドルタスクは一つのinit_task構造体を共有しますが、TSS構造体は個別にCPUごとの配列init_tss
として持っています。アイドルタスクはすべて pid=0 となりますが、他のタスクは pidを共有できません。これはつまり CLONE_PID
フラグをclone(2)へと渡すということです。
カーネルスレッドは、カーネルモードで clone(2) システムコールを呼び出す kernel_thread()
関数を使って生成されます。カーネルスレッドは通常ユーザアドレス空間を持ちません(つまりp->mm = NULL
)。これは、これらのスレッドが、(たとえばdaemonize()
関数を通して)明白にexit_mm()
を行うからです。
カーネルスレッドは、いつでもカーネルアドレス空間へ直接アクセスできます。そして、小さな値のpid番号を割り当てられます。(x86のとき) プロセッサリング0 で走行しているときは、カーネルスレッドがすべてのI/Oの権利をもち、スケジューラに対してプリエンプティブではないということを意味しています。
ユーザタスクは、clone(2)ないしは、fork(2)システムコールにより作られます。これらのシステムコールは、内部的にkernel/fork.c:do_fork()を呼び出しています。
ここで、ユーザプロセスがfork(2)システムコールを呼び出したときに、何が起こっているかを解説しましょう。fork(2)がユーザスタックやレジスタを渡す方法が異なるという意味でアーキテクチャ依存だとしても、根底にある実際の仕事を行う関数の do_fork()
は可搬性があり、ソースコードのkernel/fork.c
に納められています。
以下のステップで実行されます。
retval
が-ENOMEM
にセットされる。これはfork(2)が新しいタスク構造体の割り当てに失敗したときにセットされるerrno
の値になっている。
CLONE_PID
が clone_flags
にセットされていた場合、呼び出し元がアイドルスレッド(ブート時のみ)でない限り、エラー(-EPERM
)を返す。通常のユーザスレッドがCLONE_PID
をclone(2)に渡すことはできないし、成功を期待することもできない。current->vfork_sem
が初期化される(これは子によって後にクリアされる)。例えば、他のプログラムを exec()
したり、exit(2) したときのように、これは sys_vfork()
(clone_flags = CLONE_VFORK|CLONE_VM|SIGCHLD
に関連する vfork(2) システムコール) により、子プロセスがmm_release()
を実行するまで、親を休止させるために使われる。alloc_task_struct()
マクロを使って割り当てられる。x86 上では、GFP_KERNEL
優先度の gfp になっている。これが fork(2) システムコールが休止する最大の理由になっている。もし割り当てが失敗すれば、-ENOMEM
を返す。*p = *current
によって、新しいものへコピーされる。おそらくこれは、memcpyに置き換えられるべきものだ。その後で、子に引き継がれないフィールドは、正しい値に設定される。
RLIMIT_NPROC
ソフトリミットを越えているかどうかを確認する。もし越えているなら失敗し-EAGAIN
を返す。もし越えていなければ、与えられたuidのプロセスのカウンタp->user->count
を1増加させる。
-EAGAIN
を返す。
p->pid_exec = 0
)とマークされる。
p->swappable = 0
)とマークされる。
p->state = TASK_UNINTERRUPTIBLE
に入る(TODO: なぜこれが行われるのか?わたしには不要に思われる─これを削除するには、Linus が必要ないと認める必要がある)。
p->flags
が clone_flags の値にしたがって設定される。通常のfork(2)の時は、これはp->flags = PF_FORKNOEXEC
になる。
p->pid
はkernel/fork.c:get_pid()
にある fast アルゴリズムを使用して、設定される。(TODO: lastpid_lock
スピンロックはリダンダントにできる。do_fork()
から大きなカーネルロックのもと常にget_pid()
が呼ばれget_pid()
のフラグ引数を削除すればリダンダントにできる。パッチは Alan に2000年6月20日に送った。その後をフォローすること)
do_fork()
の残りのコードは、子のタスク構造体の残りを初期化する。一番最後に、子のタスク構造体はpidhash
ハッシュ表にハッシュされ、子は起こされる(TODO: wake_up_process(p)
はp->state = TASK_RUNNING
を設定し、プロセスを runq に加える。したがって、do_fork()
の最初のほうでp->state
をTASK_RUNNING
に設定する必要はおそらくない)。
興味深いのは、fork(2)にとってSIGCHLD
を意味する、p->exit_signal
がclone_flags & CSIGNAL
に設定することと、p->pdeath_signal
を0に設定することである。
pdeath_signal
は、(親が死ぬなどして)プロセスがオリジナルの親を「忘れた」ときに使われ、prctl(2)システムコールのPR_GET/SET_PDEATHSIG
によって設定/取得することができる。(pdeath_signal
の値は、prctl(2)がユーザ空間のポインタアーギュメントを経由して戻されるということが少し不思議だと主張されるかもしれない―ごめんなさい。Andries Brouwer がマニュアルページを更新したあとだったので、修正が間に合わなかった ;)
func == 1
のbdflush(2)を呼ぶ (これは Linux 独特であり、'update'行が/etc/inittab
に残っているような古いディストリビューションとの互換性のためである。現在ではupdateの仕事は、カーネルスレッドのkupdate
が行っている)。
Linux でシステムコールを実装している関数は、sys_
のプレフィックスを持っています。しかし、これらの関数は、たいてい引数のチェックやアーキテクチャ独特の情報の渡し方についてのみ関心をもっており、実際の仕事は、do_
のついた関数が行います。そのため、sys_exit()
においては、do_exit()
を実際の仕事を行うために呼び出します。しかしながら、カーネルの他の部分で時折、実処理をしているdo_exit()
を呼び出さなければならない場面で、sys_exit()
を呼び出しているところが見受けられます。
do_exit()
関数は、kernel/exit.c
にあります。do_exit()
についてのポイントとしては、
schedule()
を最後に呼び出すが、戻ることはない。
TASK_ZOMBIE
に設定する。
current->pdeath_signal
により、すべての子に通知する。
current->exit_signal
によって通知する。これは通常SIGCHLD
になる。
スケジューラの仕事は、カレントの CPU へのアクセスを複数のプロセス間で仲裁することにあります。スケジューラは、「メインのカーネルファイル」kernel/sched.c
に実装されています。関連のへッダファイルであるinclude/linux/sched.h
は、(明示的にせよ間接的にせよ)事実上全てのカーネルソースファイルから読み込まれます。
スケジューラに関連するタスク構造体のフィールドには、次のものがあります。
p->need_resched
: このフィールドは、schedule()
が「次の機会」に起動されるべき時にセットされます。p->counter
: このスケジューリングスライスでの実行の残りのクロックチック数でタイマによって減少される。この値が0以下になったときは、p->need_resched
が設定される。これは自身が変更できるため、プロセスの「動的優先度」と呼ばれることもある。
p->priority
: プロセスの静的な優先度であり、よく使われるシステムコールのnice(2)や、POSIX.1bのsched_setparam(2)、4.4BSD/SVR4のsetpriority(2)によって変更される。p->rt_priority
: リアルタイム優先度。
p->policy
: スケジューリングポリシーであり、タスクが所属するスケジューリングのクラスを表す。sched_setscheduler(2)システムコールを使うことで、タスクのスケジューリングのクラスを変更することができる。取りうる値には、SCHED_OTHER
(伝統的なUNIXのプロセス)、SCHED_FIFO
(POSIX.1b FIFO リアルタイムプロセス)とSCHED_RR
(POSIX ラウンドロビンリアルタイムプロセス)がある。これらは、SCHED_YIELD
とORを取ることができ、たとえばsched_yield(2)システムコールを呼び出すことによって、プロセスが CPU を譲ることにしたことを示すことができる。FIFOリアルタイムプロセスは a) I/Oがブロックされるか、b) 明示的に CPU を解放するか、c) 他の高いp->rt_priority
値を持つリアルタイムプロセスから割り込まれるか、いずれかがあるまで実行される。schedule()
関数は、見掛け上、非常に複雑ですが、スケジューラのアルゴリズムは単純です。関数が複雑なのは、3つのスケジューリングアルゴリズムを一つに実装しているのと同時に、SMP のデリケートな仕様を実装しているからです。
明らかに「無駄な」gotoがschedule()
にありますが、これはi386で一番最適化されたコードを生成するためです。もちろん、スケジューラは2.4で完全に(カーネルの他の部分同様)書き直されました。そのため、以下の議論は2.2やそれ以前のカーネルには当てはまりません。
では詳細に関数を見ていきましょう。
current->active_mm == NULL
なら、なにかがおかしい。カレントのプロセスは、カーネルスレッド(current->mm == NULL
)であったとしても、正しいp->active_mm
を常に持っていなければならない。tq_scheduler
タスクキューに実行するものがあれば、いまそれを処理する。タスクキューは関数の実行を遅らせてスケジュールを行うカーネルの仕組みを提供するものである。その詳細は他の章で説明する。
prev
とthis_cpu
を、それぞれ現在のタスクと現在の CPU に初期化する。schedule()
が(バグにより)割り込みハンドラから起動されていないかチェックする。もしそうならカーネルパニックになる。
struct schedule_data *sched_data
を初期化し、CPU毎の(キャッシュラインピンポンを避けるためキャッシュラインにアラインされる)スケジューリングデータ領域を指すようにする。これは、last_schedule
のTSC値や最後にスケジュールされたタスク構造体へのポインタからなっている。(TODO: sched_data
は SMP の時のみ使われるが、なぜinit_idle()
は UPであっても同様に初期化を行うのか?)runqueue_lock
スピンロックを取得する。私たちがspin_lock_irq()
を使うのは、schedule()
の中で、割り込み可になっていることを保証するためである。したがって、runqueue_lock
を解除するときには、(spin_lock_irqsave/restore
の一種の) saving/restoring eflagsを使うのではなく、単に再度有効にするだけでよい。
TASK_RUNNING
状態にあれば、そのままにされる。もしTASK_INTERRUPTIBLE
状態であり、シグナルが保留されていれば、TASK_RUNNING
状態へ移行させる。それ以外の場合は、runqueueから削除する。
next
がこの CPU でのアイドルタスクにされる。しかし、この候補のグッドネスは、他のものがこれより良くなるように、とても低い値(-1000)となる。
prev
(カレント)のタスクがTASK_RUNNING
状態であれば、カレントのグッドネスをそのグッドネスと同じに設定する。アイドルタスクよりスケジュールされやすくなるよう印をつける。goodness()
という関数により計算される。リアルタイムプロセスのグッドネスでは非常に高く(1000 + p->rt_priority
)することで処理する。 1000 より大きいことで、SCHED_OTHER
ではないプロセスが勝つことを保証している。そして他のより大きなp->rt_priority
を持つリアルタイムプロセスだけが争うことができる。
プロセスのタイムスライス(p->counter
)が終了したときは、goodness 関数は 0 を返す。リアルタイムではないプロセスでは、グッドネスの初期値は p->counter
に設定されるため、すでにしばらくの間 CPU を占有したプロセスは、 CPU を得ることが少なくなる。つまり対話的プロセスは、CPUの割り当てをたくさんとってしまうプロセスより優先される。
アーキテクチャ固有の定数PROC_CHANGE_PENALTY
は「CPU との密接度」を実装しようとしたもの(つまり、同じ CPU のプロセスが優先される)で、これはカレントのactive_mm
をさしているプロセスや、(ユーザ)アドレス空間を持たない(カーネルスレッドのような)プロセスに若干の優先度を与える。
recalculate:
{
struct task_struct *p;
spin_unlock_irq(&runqueue_lock);
read_lock(&tasklist_lock);
for_each_task(p)
p->counter = (p->counter >> 1) + p->priority;
read_unlock(&tasklist_lock);
spin_lock_irq(&runqueue_lock);
}
再計算を行う前にrunqueue_lock
を解除することに注目しよう。全てのプロセスの配列を処理するため、時間を必要とするからだ。CPU が再計算をさせられているその間に、schedule()
が他の CPU において呼ばれ、その CPU にとって十分にグッドネスが良いプロセスを選ぶことができる。そう、これは明らかにちょっと矛盾している。私たちが(この CPU 上で)もっとも良いグッドネスを持つプロセスを選んでいる間に、他の CPU で実行されている schedudle()
が、動的優先度を再計算することができているからだ。
next
がスケジュールされるタスクを指しており、そのためnext->has_cpu
を 1 にnext->processor
をthis_cpu
に初期化していることが確実である。runqueue_lock
はそうしたうえで解除できる。
next == prev
)、単純にグローバルカーネルロックを再取得し返る。つまり、全てのハードウエアレベル(レジスタ、スタックなど)と VM 関連(スイッチページディレクトリ、active_mm
の再計算など)のものはスキップされる。
switch_to()
はアーキテクチャ特有である。i386では、これは a)FPU ハンドリング、b) LDT ハンドリング、c) セグメントレジスタのリロード、d)TSSのハンドリング、e) デバッグレジスタのリロードに関係がある。ウエイトキューの実装に行く前に、Linux標準のダブルリンクリストの実装に熟知しなければなりません。
ウエイトキュー(Linuxの他のものと同様)は、この実装を良く使いますし、include/linux/list.h
がもっとも関連していることから、ジャーゴンで「list.h の実装」とも呼ばれます。
ここでの基礎的なデータ構造体は、struct list_head
です。
struct list_head {
struct list_head *next, *prev;
};
#define LIST_HEAD_INIT(name) { &(name), &(name) }
#define LIST_HEAD(name) \
struct list_head name = LIST_HEAD_INIT(name)
#define INIT_LIST_HEAD(ptr) do { \
(ptr)->next = (ptr); (ptr)->prev = (ptr); \
} while (0)
#define list_entry(ptr, type, member) \
((type *)((char *)(ptr)-(unsigned long)(&((type *)0)->member)))
#define list_for_each(pos, head) \
for (pos = (head)->next; pos != (head); pos = pos->next)
最初の 3 つのマクロはnext
とprev
ポインタを自身を指すようにして、空リストを初期化するものです。例えば、LIST_HEAD_INIT()
は、宣言時に構造体の要素を初期化するために使われ、2つ目はスタティック変数の初期化を行う宣言で、3つ目は関数の中で使われると言ったように、どれがどこで使われるかは、 C の構文の制約からも明らかです。
マクロlist_entry()
はそれぞれのリスト要素へのアクセスを提供します。たとえば、
(fs/file_table.c:fs_may_remount_ro()
からの例で):
struct super_block {
...
struct list_head s_files;
...
} *sb = &some_super_block;
struct file {
...
struct list_head f_list;
...
} *file;
struct list_head *p;
for (p = sb->s_files.next; p != &sb->s_files; p = p->next) {
struct file *file = list_entry(p, struct file, f_list);
do something to 'file'
}
list_for_each()
マクロの使い方の良い例が、ちょうど注目しているスケジューラのなかの、もっとも高いグッドネスを持つプロセスを探してrunqueueを見る部分にあります。
static LIST_HEAD(runqueue_head);
struct list_head *tmp;
struct task_struct *p;
list_for_each(tmp, &runqueue_head) {
p = list_entry(tmp, struct task_struct, run_list);
if (can_schedule(p)) {
int weight = goodness(p, this_cpu, prev->active_mm);
if (weight > c)
c = weight, next = p;
}
}
ここで、p->run_list
は、task_struct
構造体の中のstruct list_head run_list
として宣言されています。そして、リストのアンカーを提供します。要素をリストから削除し、あるいは追加(リストの先頭にせよ、末尾にせよ)する場合には、list_del()/list_add()/list_add_tail()
マクロによって実行します。以下の例は、runqueueへタスクを追加および削除する例です。
static inline void del_from_runqueue(struct task_struct * p)
{
nr_running--;
list_del(&p->run_list);
p->run_list.next = NULL;
}
static inline void add_to_runqueue(struct task_struct * p)
{
list_add(&p->run_list, &runqueue_head);
nr_running++;
}
static inline void move_last_runqueue(struct task_struct * p)
{
list_del(&p->run_list);
list_add_tail(&p->run_list, &runqueue_head);
}
static inline void move_first_runqueue(struct task_struct * p)
{
list_del(&p->run_list);
list_add(&p->run_list, &runqueue_head);
}
プロセスがカーネルに依頼したことが現在不可能であるが、後に可能となる事であったなら、プロセスは休止状態へと入り、要求が満たされる条件が整いそうなときに起こされます。この時使われるカーネルのメカニズムの一つが「ウエイトキュー」です。
Linux の実装では、TASK_EXCLUSIVE
フラグを使う起動セマンティクスを許しています。ウエイトキューによって、well-knownキューを使って単純に sleep_on/sleep_on_timeout/interruptible_sleep_on/interruptible_sleep_on_timeout
を使うこともできますし、自身のウエイトキューを定義し、それに自身を追加/削除するために add/remove_wait_queue
を使用して、必要時に起きるためwake_up/wake_up_interruptible
を使用することもできます。
最初のウエイトキューの使い方の例としては、(mm/page_alloc.c:__alloc_pages()
の) ページアロケータ と (mm/vmscan.c:kswap()
の) kswapd
カーネルデーモンの間における、mm/vmscan.c
で定義されるウエイトキューkswapd_wait
による相互作用があります。kswapd
デーモンはこのキュー上で休止し、ページアロケータがページを解放してやる必要がでるまで起きることはありません。
自発的なウエイトキューの用例には、 read(2)システムコールでデータを要求するユーザプロセスと、データを提供するため、割り込みコンテキストで走行するカーネルの間の相互作用があります。割り込みハンドラは以下のようになっています (drivers/char/rtc_interrupt()
を簡略化)。
static DECLARE_WAIT_QUEUE_HEAD(rtc_wait);
void rtc_interrupt(int irq, void *dev_id, struct pt_regs *regs)
{
spin_lock(&rtc_lock);
rtc_irq_data = CMOS_READ(RTC_INTR_FLAGS);
spin_unlock(&rtc_lock);
wake_up_interruptible(&rtc_wait);
}
ここで割り込みハンドラは、デバイスに特有のI/O ポートを読み取る(CMOS_READ()
マクロは複数の outb/inb
になります)ことでデータを取得し、rtc_wait
ウエイトキューで休止中のものを起動します。
ssize_t rtc_read(struct file file, char *buf, size_t count, loff_t *ppos)
{
DECLARE_WAITQUEUE(wait, current);
unsigned long data;
ssize_t retval;
add_wait_queue(&rtc_wait, &wait);
current->state = TASK_INTERRUPTIBLE;
do {
spin_lock_irq(&rtc_lock);
data = rtc_irq_data;
rtc_irq_data = 0;
spin_unlock_irq(&rtc_lock);
if (data != 0)
break;
if (file->f_flags & O_NONBLOCK) {
retval = -EAGAIN;
goto out;
}
if (signal_pending(current)) {
retval = -ERESTARTSYS;
goto out;
}
schedule();
} while(1);
retval = put_user(data, (unsigned long *)buf);
if (!retval)
retval = sizeof(unsigned long);
out:
current->state = TASK_RUNNING;
remove_wait_queue(&rtc_wait, &wait);
return retval;
}
rtc_read()
で起っている事というのは、
rtc_wait
ウエイトキューに追加する。TASK_INTERRUPTIBLE
のマークづけをする。TASK_RUNNING
とマークして、自身をウエイトキューから削除して、返る。EAGAIN
(これは、EWOULDBLOCK
と同じ)エラーで終了する。TASK_INTERRUPTIBLE
にマークしないと、スケジューラはデータが有効になるより早くスケジュールするため、不必要な処理の原因になる。また、ウエイトキューを用いることで、poll(2)システムコールの実装がむしろ容易になっていることも指摘しておきましょう。
static unsigned int rtc_poll(struct file *file, poll_table *wait)
{
unsigned long l;
poll_wait(file, &rtc_wait, wait);
spin_lock_irq(&rtc_lock);
l = rtc_irq_data;
spin_unlock_irq(&rtc_lock);
if (l != 0)
return POLLIN | POLLRDNORM;
return 0;
}
全ての仕事は、デバイス非依存関数のpoll_wait()
によって行われます。これは、必要なウエイトキューの操作を行います。我々がしなければならないことは、デバイス特有の割り込みハンドラによって起こすべきウエイトキューを指示することだけです。
さて、次はカーネルタイマに注目しましょう。カーネルタイマとは、将来の特定の時刻に、特定の関数(「タイマーハンドラ」と呼ばれる)の実行を行わせるためにあるものです。
主なデータ構造体は、struct timer_list
であり、include/linux/timer.h
で定義されています。
struct timer_list {
struct list_head list;
unsigned long expires;
unsigned long data;
void (*function)(unsigned long);
volatile int running;
};
list
フィールドは内部のリストへとリンクするためのもので、timerlist_lock
スピンロックにより保護されています。expires
フィールドは、function
ハンドラがdata
をパラメータとして渡され起動されるときのjiffies
の値です。running
フィールドは、タイマハンドラが現在他の CPU で動作しているかテストするために SMP において用いられます。
関数add_timer()
と、del_timer()
は、与えられたタイマのリストへの追加ならびに削除を行います。タイマの時間が終了すると、自動的に削除されます。タイマが使われる前に、必ずinit_timer()
関数によって初期化しなければなりません。そして、追加前にfunction
とexpires
フィールドを必ず設定しなければなりません。
割り込みハンドラが実行する仕事を、すぐに行う(例えば、割り込みへの反応、状態の更新などの)仕事と、割り込みが有効(データの後処理)になってから、後で処理すれば良いもの(データの後処理の実施や、データを待っているプロセスを起こすなど)との2つに分けるのが理にかなっていることが、時々あります。
ボトムハーフは、カーネルタスクの実行を延期する最も古い機構で、Linux 1.xのときからあります。Linux 2.0になって、次の章で取り扱う「タスクキュー」という新しい機構が導入されました。
ボトムハーフはglobal_bh_lock
スピンロックによってシリアライズされます。つまり一度にいずれかのCPUで実行できるのは、ただ一つのボトムハーフのみであるということです。ハンドラを実行しようとするとき、global_bh_lock
が有効でなければ、ボトムハーフは実行の印をつけられ(つまりスケジュールされ)ます。そして、global_bh_lock
でビジーループすることを避けるため処理は継続されます。
現在、全部で32のボトムハーフだけが登録されています。ボトムハーフを操作するために必要となる関数は、以下のようになります(すべてモジュール化されています)。
void init_bh(int nr, void (*routine)(void))
: routine
で指示されるボトムハーフハンドラをスロットnr
へ導入する。スロットはinclude/linux/interrupt.h
に、XXXX_BH
の形で(例えば、TIMER_BH
やTQUEUE_BH
のように)列挙されていなければならない。通常、サブシステムの初期化ルーチン(モジュールの場合にはinit_module()
)は、この関数を使って必要なボトムハーフを導入する。
void remove_bh(int nr)
: init_bf()
の逆を実行する。つまり、スロットnr
に導入されたボトムハーフを削除する。ここではエラーチェックは行われない。そのため、例えば、remove_bh(32)
はシステムがpanicまたはoopsになる。通常、サブシステムのクリーンアップルーチンは(モジュールの場合、cleanup_module()
)、他のサブシステムが後で再利用できるよう、この関数をスロットを解放するために使用する。(TODO: システムの全ての登録されたボトムハーフが、/proc/bottom_halves
にリストアップされたらどうだろう? これは、blobal_bh_lock
が明示的に読み書きされなければならないということを意味する)void mark_bh(int nr)
: スロットnr
のボトムハーフに実行の印をつける。通常、割り込みハンドラはそのボトムハーフ(この名前がついた由来!)に「安全な時刻」に実行するよう印をつける。ボトムハーフは、グローバルロックされたタスクレットです。そこで「ボトムハンドラはいつ実行されるのか?」という質問は、本当は「タスクレットはいつ実行されるのか?」というのが本当なのです。答えは、a) 各schedule()
と b)entry.S
の各割り込み/システムコールのリターンパスの二カ所です。(TODO: そのため、schedule()
の場合は本当に遅いのです。もう一つの非常に遅い遅い割り込みを追加したようなものです。なぜ、schedule()
からhandle_softirq
ラベルを完全に取り除かなかったのか?)
タスクキューは古いボトムハーフのダイナミックな拡張として考えられます。実際のところ、ソースコードでは、「新しい」ボトムハーフとして参照されているところもあります。より具体的には、前章で取り上げた古いボトムハーフには、次に示すような制限があります。
そこで、タスクキューにより任意の数の関数をつなぎ、後で次々と処理ができるようにします。 DECLARE_TASK_QUEUE
マクロを使って新しいタスクキューを作成すると、queue_task()
関数を使って、そのキューへとタスクをためることができます。そして、タスクキューはrun_task_queue
によって処理されます。自身のタスクキューを作成する代りに(そして、手動で消費する代りに)、Linux にあらかじめ準備された、良く知られた個所で消費されるタスクキューを使うことができます。
tq_timer
タスクも割り込みコンテキスト内で実行されるため、ブロックできない。tq_timer
同様 tty を閉じるときも)消費されるスケジューラタスクキュー。スケジューラが再スケジュールされるプロセスのコンテキストで実行されるため、tq_scheduler
タスクはやりたいことが何でもできる。つまり、ブロックしてプロセスのコンテキストのデータを使用する(やりたいかどうかは別として)などである。
IMMEDIATE_BH
であり、そのためドライバはqueue_task(task, &tq_immediate)
と、そして割り込みコンテキストで消費されるmark_bh(IMMEDIATE_BH)
を行う。ドライバが自身のタスクキューを使わないかぎり、以下の場合を除いて、キューを処理するためにrun_tasks_queue()
を呼ぶ必要はありません。
タスクキューtq_timer/tq_scheduler
を消費するのが通常の場所だけでなく外の場所(一例としては、ttyデバイスのクローズ)でも行われる理由は、ドライバがキューにタスクをスケジュールしたとき、そのタスクが意味を持つのはデバイスの特定のインスタンスが有効であるとき(通常はアプリケーションがクローズするまで)なのを考えると自ずと明らかでしょう。
そのため、ドライバはrun_task_queue
を呼んで、自身や(他のドライバが)キューにいれたタスクをフラッシュする必要があります。これは、後で実行することができるようにすることが無意味だからです。つまり、関連するデータ構造は他のインスタンスにより解放されたり再利用されるということです。
これが、それぞれタイマ割り込みやschedule()
の代りにtq_timer
とtq_scheduler
にrun_task_queue()
がある理由になっています。
作成中。将来のリビジョンで記述。
作成中。将来のリビジョンで記述。
他のUNIX系OS(Solaris, Unixware 7 など)のバイナリではlcall7メカニズムを使っていますが、ネイティブな Linux プログラムは int 0x80 を使っています。'lcall7'の名前は、歴史的な過ちです。これは、lcall27(例えば Solaris/x86)も使用しているが、ハンドラ関数はlcall7_funcと呼ばれているからです。
システムがブートするとき、IDTを設定する関数 arch/i386/kernel/traps.c:trap_init()
が呼ばれ、(type 15, dpl 3 の)ベクタ 0x80 を、arch/i386/kernel/entry.S
のシステムコールエントリアドレスを示すように設定します。
ユーザ空間のアプリケーションがシステムコールを行う時は、引数をレジスタに入れて、'int 0x80' 命令を実行します。これは、カーネルモードにトラップされ、プロセッサはentry.S
のシステムコールエントリポイントへとジャンプします。これは、次のようなことを行います。
NR_syscalls
(現在 256) より大きかった場合には、ENOSYS
エラーで失敗する。
tsk->ptrace & PF_TRADESYS
)時は、特別な処理を行う。これは strace (SVR4でいうtruss(1))のようなプログラムやデバッガをサポートするためである。
sys_call_table+4*(%eax の syscall_number)
を呼ぶ。このテーブルは同じファイル(arch/i386/kernel/entry.S
)で初期化されており、各々のシステムコールハンドラを指している。Linux においては、ハンドラには通常、例えば sys_open
やsys_exit
のように sys_
というプレフィックスがついている。これらの C システムコールハンドラはSAVE_ALL
が格納したスタックから引数を見つけ出す。
schedule()
が必要かどうか(tsk->need_resched != 0
) を確認したり、シグナルが保留されていないか確認したり、そしてそれらのシグナルを処理したりすることに関連している。Linux は システムコールに 6 つまでの引数をサポートしています。これらは %ebx, %ecx, %edx, %esi, %edi (および一時的に使われる %edp。asm-i386/unistd.h
の_syscall6()
を参照)に入れて渡されます。システムコール番号は %eax で渡されます。
2つのタイプのアトミックな操作があります。ビットマップとatomic_t
です。ビットマップは、ある大きな集合から「割り当て」や「解放」の単位で、各単位がある番号で指定される、例えばフリー inodeやフリーブロックのようなコンセプトを維持するのにとても便利です。これらは単純なロックに広く使われています。例えばデバイスのオープンを排他的に行えるようにするために使われます。この例は、arch/i386/kernel/microcode.c
に見ることができます。
/*
* Bits in microcode_status. (31 bits of room for future expansion)
*/
#define MICROCODE_IS_OPEN 0 /* set if device is in use */
static unsigned long microcode_status;
BSSを Linux で明示的にゼロクリーンするように microcode_status
を0に初期化する必要はありません。
/*
* We enforce only one user at a time here with open/close.
*/
static int microcode_open(struct inode *inode, struct file *file)
{
if (!capable(CAP_SYS_RAWIO))
return -EPERM;
/* one at a time, please */
if (test_and_set_bit(MICROCODE_IS_OPEN, µcode_status))
return -EBUSY;
MOD_INC_USE_COUNT;
return 0;
}
ビットマップの操作は、
addr
で示されるビットマップのnr
のビットをセットする。
addr
で示されるビットマップのnr
ビットをクリアする。
addr
で示されるビットマップのnr
ビットを反転(クリアされていればセット、セットされていればクリア)する。
nr
ビットをセットし、古いビットの値を返す。
nr
ビットをクリアし、古いビットの値を返す。
nr
ビットを反転し、古いビットの値を返す。
これらの操作は、LOCK_PREFIX
マクロを使っています。このマクロは SMP カーネルにおいてバスのロック操作を行う接頭辞として評価されますが、UP カーネルでは何もしません。これによって、SMP 環境でのアトミックなアクセスを保証しています。
ときにはビット操作が不便な場面もありますが、その場合は算術演算を使う必要があります。加算、減算、インクリメント、デクリメントなどです。(inode のような)参照カウンタが典型的な例です。この機能はatmic_t
データタイプと以下の操作で実現されています
atomic_t
変数t
の値を読み込む。
atomic_t
変数 v
を整数値 i
に設定する。
i
をv
で示されるアトミック変数の値に加算する。
i
をv
で示されるアトミック変数の値から減算する。
i
をv
で示されるアトミック変数の値から減算し、その結果が0のときは 1 を返し、それ以外では 0 を返す。
i
をv
の値に加算する。そして、結果が負数ならば1を返し、結果が0以上であれば0を返す。この操作はセマフォの実装に用いられている。初期の Linux (前世紀 90 年代の初頭)から、開発者は、異なる型のコンテキストの間や、複数の CPU における同じ型のコンテキストであるが違ったインスタンス間での共有データのアクセスに関する古典的問題に直面していました。
1995 年 11 月 15 日に、SMP サポート(オリジナルのパッチは 同年 10 月の 1.3.37 に対するものでした)が Linux 1.3.42 で追加されました。
コードのクリティカル領域がプロセスコンテキストと割り込みコンテキストの両方で実行されることも考えられるので、UPでcli/sti
命令を使って保護する方法は、
unsigned long flags;
save_flags(flags);
cli();
/* critical code */
restore_flags(flags);
のようになります。
これが UP でOKだとしても、 SMP では明らかに意味がありません。同じコードシーケンスが他の CPU で同時に実行されるかも知れないからです。そして、cli()
は個々の CPU 上での割り込みコンテキストの競合からの保護しか提供しないため、異なった CPU で実行されるコンテキスト間の競合からの保護には全くならないのです。これでスピンロックが有用になります。
3 つのタイプのスピンロックがあります。バニラ(基本)、読み書き、ビッグリーダー スピンロックです。読み書きスピンロックは「読む者が多くて書く者が少ない」よくある傾向の場合に用いられます。この例としては、登録されたファイルシステムのリスト(fs/super.c
参照)へのアクセスがあります。
リストは、file_systems_lock
読み書きスピンロックにより保護され、ファイルシステムの登録/解除の際にだけ排他的なアクセスを行います。しかし、全てのプロセスは/proc/filesystems
を読めますし、sysfs(2)システムコールを使って、ファイルシステムの読み取り専用アクセスを行うことができるのです。このことが、読み書きスピンロックを使う際に気をつかう点になっています。読み書きスピンロックによって、同時に複数の読み取りができるか、単一の書き込みがあって、他の読み取りができない状況かになります。とはいうものの、書き込み者がロックを取得しようとしている間、新しい読み取り者がロックを取得できないなら、つまり複数の読み取り者による書き込み者の飢餓問題について、Linux が正しく処理しているなら、これは良いことだといえます。書き取り者がロックを取得しようとしている間は、読み取り者がブロックされなければならないという事を意味しています。
これは現在のところ問題ではなく、修正しなければならないことかどうかも明らかではありません。 逆にいうと、読み取り者は常に非常に短い時間だけのロックを取得していれば、 書き込み者が比較的長い時間間隔でロックを取得している間に本当に枯渇してしまう ことがあるのか? ということです。
ビッグリーダスピンロックは、読み書きスピンロックに、非常に軽い読込みアクセスへ強く最適化をかけたもので、書き込みアクセスにはペナルティを課しています。ビッグリーダスピンロックは限られた範囲、現在は2個所だけで使われています。一つは sparc64 (global irq) でのみ使われ、もう一つはネットワークスタックです。アクセスパターンがこの2つの型のどちらにも当てはまらない場合では、基本スピンロックを使っています。どの種類のスピンロックを保持している場合でもブロックすることはできません。
プレーン、_irq()
、_bh()
と、スピンロックには3種類あります。
spin_lock()/spin_unlock()
: 割り込みが常に不可にされるか、割り込みコンテキストと競合しないことが分かっている(例えば割り込みハンドラ内部で)なら、これを使うことができる。現在の CPU の割り込み状態には触れることがない。
spin_lock_irq()/spin_unlock_irq()
: もし割り込みが常に有効ならこのバージョンのスピンロックを使うことができる。これは単純に現在の CPU の割り込みを(ロック時に)無効にし、(アンロック時に)再度有効にする。
たとえば、rtc_interrupt()
がspin_lock(&rtc_lock)
(割り込みは割り込みハンドラの中では常に無効)を使うため、rtc_read()
はspin_lock_irq(&rtc_lock)
(read中割り込みは常に有効)を使っている。rtc_read()
がspin_lock_irq()
を使い、より汎用的なspin_lock_irqsave()
を使わないのは、全てのシステムコール割り込みが常に有効になっているからである。
spin_lock_irqsave()/spin_unlock_irqrestore()
: 割り込み状況が分からないときに使う、最強の形式。割り込みが問題になる場合だけでなく、割り込みが全く不明な場合にも使われる。
つまり、割り込みハンドラがクリティカルコードを実行しない場合には、これを使うポイントはないということだ。もし割り込みハンドラが競合するならば、素のspin_lock()
を使えません。
これを使ったときに、割り込みが同じCPU上で発生すると、永遠にロックを待ちつづけるためです。
割り込まれているロックを取得したハンドラは、割り込みハンドラから処理が返ってくるまで、
処理を継続できないためです。
ユーザプロセスのコンテキストと割り込みハンドラとで共有するデータ構造体にアクセスするときには、スピンロックがもっとも良く使われています。
spinlock_t my_lock = SPIN_LOCK_UNLOCKED;
my_ioctl()
{
spin_lock_irq(&my_lock);
/* critical section */
spin_unlock_irq(&my_lock);
}
my_irq_handler()
{
spin_lock(&lock);
/* critical section */
spin_unlock(&lock);
}
この例については、いくつか注記があります。
ioctl()
によるプロセスのコンテキストでは、必ずspin_lock_irq()
を使わなければならない。
これは、デバイスのioctl()
呼び出しを実行している間は、割り込みが常に有効になっているためである。
my_irq_handler()
によって表される割り込みコンテキスト
では、plain spin_lock()
form を使うことができる。これは、割り込みハンドラ内部では、割り込みは無効にされているためである。
時折、共有データ構造体にアクセスするときに、操作の実行をブロックしなければならないことが あります。たとえばデータをユーザ空間へコピーする場合などです。Linux におけるこのような場合に 使用できるロックの基本機構としてセマフォと呼ばれるものがあります。 セマフォには2つのタイプがあり、基本セマフォと読み書きセマフォと呼んでいます。 セマフォの初期値により、交互実行(初期値1)にも、より複雑なアクセスのタイプを提供するためにも 使うことができます。
読み書きセマフォと基本のセマフォとの違いは、読み書きスピンロックと基本のスピンロックの相違と 同様のものになっています。一方が同時に複数の読み手と一つの書き手を許すのに対して、もう一方は 書き込みのある間は読み出しがロックされるものになります。つまり、書き手が全ての読み手をブロックし、書き手が待ち状態にある間、新しい読み手はブロックします。
同様に、素のdown()/up()
の代りにdown/up_interruptible()
を使うことと、down_interruptible()
からの返り値をチェックすることで、基本セマフォは割り込みが可能になります。
もし操作が割り込まれたならば、返り値が0以外になります。
クリティカルコードが他のサブシステムやモジュールから登録される未知の関数から呼び出される状況では、典型的には交互実行にセマフォを使うことになります。 つまり、呼び出し側が先験的に関数がブロックされるかどうか知ることができない場合です。
セマフォの簡単な例は、gethostname(2)/sethostname(2)システムコールを実装したkernel/sys.c
にあります。
asmlinkage long sys_sethostname(char *name, int len)
{
int errno;
if (!capable(CAP_SYS_ADMIN))
return -EPERM;
if (len < 0 || len > __NEW_UTS_LEN)
return -EINVAL;
down_write(&uts_sem);
errno = -EFAULT;
if (!copy_from_user(system_utsname.nodename, name, len)) {
system_utsname.nodename[len] = 0;
errno = 0;
}
up_write(&uts_sem);
return errno;
}
asmlinkage long sys_gethostname(char *name, int len)
{
int i, errno;
if (len < 0)
return -EINVAL;
down_read(&uts_sem);
i = 1 + strlen(system_utsname.nodename);
if (i > len)
i = len;
errno = 0;
if (copy_to_user(name, system_utsname.nodename, i))
errno = -EFAULT;
up_read(&uts_sem);
return errno;
}
この例のポイントは以下の通りです。
copy_from_user()/copy_to_user()
で、データをユーザスペースから、もしくはユーザスペースへコピーするときにブロックされることがある。
すなわち、ここではいかなるタイプのスピンロックもつかうことはできない。
Linux のセマフォならびに読み書きセマフォの実装は非常に精巧にできていますが、 まだ実装されていない考え得る可能なシナリオも存在しています。例えば、割り込み可能な 読み書きセマフォのコンセプトなどです。このようなエキゾチックな種類のプリミティブ が必要になる状況は、実世界でない状況なので、このようなコンセプトは明らかにありえません。
マイクロカーネルデザインに基づくオペレーティングシステムの提供する"アドバンテージ"といった最近の宣伝にも関わらず、Linux はモノリシックなオペレーティングシステムです。 実際、(Linus Torvalds 彼自身の言葉によると)
... message passing as the fundamental operation of the OS is just an
exercise in computer science masturbation. It may feel good, but you
don't actually get anything DONE.
だから、Linux は ずっとモノリシックデザインをベースとしており、すべてのサブシステムは 同じ特権モードで動き、同じアドレス空間を共有しているのです。カーネル内の通信は 通常の C 関数呼び出しで行われます。
しかし、たとえマイクロカーネルが行うようにカーネルの機能をそれぞれの「プロセス」へと分けることは
明らかに悪い考えですが、オンデマンドで動的に読み込めるカーネルモジュールに分けることは、ある
条件下(例えば、マシンのメモリが少ない場合や、互いに排他的な複数の ISA 自動判別デバイスドライバを持つカーネルをインストールした場合)では、いい考えです。ローダブルモジュールのサポートを組み込むかどうかの判断は、コンパイル時に行われ、CONFIG_MODULES
オプションによって指定されます。
request_module()
機構によるモジュールの自動組み込みのサポートは、別のコンパイルオプション (CONFIG_KMOD
) になっています。
以下の機能は Linux のロード可能モジュールとして実装されています。
/proc
と devfs における仮想(一般の)ファイル (例えば/dev/cpu/microcode
やdev/misc/micrococe
)。
一部には Linux におけるモジュールとして実装できていないものもあります。(おそらくそれらは モジュールについて考慮されていないためと思われます)
Linux にはロード可能モジュールを援助するいろいろなシステムコールがあります。
caddr_t create_module(cost char *name, size_t size)
: vmalloc()
を用いてsize
バイトを割り当て、モジュールの構造体をその先頭に割り付ける。
新しいモジュールは module_list が先頭にあるリストへとリンクされる。CAP_SYS_MODULE
のプロセスのみが、このシステムコールを呼び出すことができ、他のプロセスはEPERM
エラーが返る。
long init_module(const char *name, struct module *image)
: 再配置されたモジュールイメージを読み込み、モジュールの初期化ルーチンが起動される。CAP_SYS_MODULE
が設定されたプロセスのみがこのシステムコールを呼び出すことができ、それ以外の場合はEPERM
エラーが返る。
long delete_module(const char *name)
: モジュールをアンロードしようとする。name == NULL
ならば、全ての使用されていないモジュールを取り外そうとする。
long query_module(const char *name, int which, void *buf, size_t bufsize, size_t *ret)
: モジュールの(ないしは全てのモジュールの)情報を返す。
ユーザが使えるコマンドインターフェースは次の通りです。
insmodやmodprobeを使ってモジュールを手動で読み込むことができるのは当然ですが、
特定の機能が必要になったときに、カーネルが自動的にモジュールを読む機能もあります。
この機能のカーネルインターフェースは request_module(name)
という関数で、モジュールへエキスポートされています。そして、モジュールは他のモジュールを同様に読み込むことができるのです。
request_module(name)
は内部的にカーネルスレッドを作成し、標準のexec_usermodehelper()
カーネルインターフェース(これもまたモジュールにエキスポートされています)を使って、ユーザ空間のコマンドのmodprobe -s -k module_nameを実行させます。
関数は成功すれば0を返しますが、request_module()
の返り値をチェックすることに通常は意味はありません。そのかわり、プログラミングでは、
if (check_some_feature() == NULL)
request_module(module);
if (check_some_feature() == NULL)
return -ENODEV;
のようにするのが通例です。
例えばこれは、fs/block_dev.c:get_blkfops()
により、メジャー番号がN
であるようなブロックデバイスをオープンしようとしたときに、モジュールblock-major-N
を読み込むために実行されます。明らかに、block-major-N
といったモジュールは存在していません(Linux 開発者は、モジュールにはそれなりの名前を選ぶものです)。でも、/etc/modules.conf
ファイルにより、あるモジュール名に割り当ててあるのです。しかし、たいていの良く知られたメジャー番号(とそのモジュール)については、modprobe/insmodコマンドは、/etc/modules.conf
に明示的なalias 文がなくとも、実際のモジュールがなにかを分かっています。
モジュールの読み込の良い例が、mount(2)システムコールの内部にあります。
mount(2)システムコールはファイルシステムタイプを文字列として受け取り、fs/super.c:do_mount()
はfs/super.c:get_fs_type()
へそれを渡します。
static struct file_system_type *get_fs_type(const char *name)
{
struct file_system_type *fs;
read_lock(&file_systems_lock);
fs = *(find_filesystem(name));
if (fs && !try_inc_mod_count(fs->owner))
fs = NULL;
read_unlock(&file_systems_lock);
if (!fs && (request_module(name) == 0)) {
read_lock(&file_systems_lock);
fs = *(find_filesystem(name));
if (fs && !try_inc_mod_count(fs->owner))
fs = NULL;
read_unlock(&file_systems_lock);
}
return fs;
}
この関数では、いくつか注意する点があります。
file_system_lock
の保護のもと、これは実行される。
try_inc_mod_count()
が 0 を返す場合は失敗したと考える。つまり、モジュールはあるが削除されているところで、それは元々なかったのと同じことである。
request_module()
)次は、ブロックする操作を行いたいため、file_system_lock
を落とす。したがってこの時にはスピンロックを保持できない。実際のところ、この特別な場合には、たとえrequest_module()
がノンブロックであることが保証されていて、モジュール読み込みが同じコンテキストでアトミックに実行されたとしても、file_system_lock
を落とさなければならない。これは、モジュールの初期化ルーチンでは、書き込みのために同じfile_systems_lock
を呼び出すregister_filesystem()
を呼び出すことがあるからである。
file_systems_lock
スピンロックを取得し、新しく登録されたファイルシステムをリストへ加えようとする。これは少し悪い考えである。というのは、原理的にはmodprobe コマンドのバグになり得るからだ。
request_module()
が失敗したが、新しいファイルシステムが登録されてしまって、しかもget_fs_type()
が見つからない場合に、要求されたモジュールのロードが成功した後にコアダンプしてしまうことがあるためだ。
モジュールがカーネルにロードされているとき、カーネルの EXPORT_SYMBOL()
マクロを使っている部分や現在ロードされている他のモジュールから、パブリックにエキスポートされたシンボルを参照することができます。
。もしモジュールが他のモジュールからのシンボルを使うときは、ブート時のdepmod -aコマンドの実行により行われる依存関係の再計算の間は(例えば新しいカーネルをインストールした後など)、そのモジュールは保留されることになります。
通常、使用するカーネルインターフェースのバージョンと、モジュール一式のバージョンは一致しなければなりません。これは Linux では、特別なカーネルインターフェースのバージョン機構を一般には持っていないため、単純な「カーネルバージョン」を意味しています。しかし、「モジュールバージョン」ないしはCONFIG_MODVERSIONS
と呼ばれる限定された機能をもっています。これによって、新しいカーネルに移行したときにモジュールの再コンパイルを避けることができます。
カーネルのシンボルテーブルを、内部のアクセスとモジュールのアクセスを別々に扱うことで、これを実現しています。シンボルテーブルのパブリック(つまりエキスポートされた)要素は、Cの修飾子により32ビットチェックサムがつけられます。そして、ロード中にモジュールが使うシンボルを解決するために、ローダはチェックサムを含めてシンボルの表記名を比較します。もし一致しなければロードを拒否するようになっています。
これは、カーネルとモジュールを、モジュールバージョンを有効にしてコンパイルしたときのみ起ります。もし、どちらかがオリジナルのシンボル名を使っていた場合、ローダは単にモジュールによってカーネルバージョンの修飾のついたものと、カーネルによりエキスポートされているものを比較して、違っていればロードを拒否します。