次のページ 前のページ 目次へ

2. プロセスと割り込み管理

2.1 タスク構造体とプロセステーブル

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 構造体の集合として表すことができます。

  1. pid によりハッシュされたハッシュテーブルと、
  2. p->next_taskp->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()exitptrace() ですが、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->statevolatile修飾子は、(割り込みハンドラから)非同期で変更され得ることを意味しています。

  1. TASK_RUNNING:タスクが runキューにある「であろう」ことを意味する。runキューにないかもしれないタスクへTASK_RUNNING印をつけておく理由は、runqueueに置く操作がアトミックでないからである。 runキューを調べるには読み込みのために runqueue_lock 読み書きスピンロックを取得する必要がある。もしそうすると、その後に runqueue 上の各タスクが TASK_RUNNING 状態にあるかどうか見るだろう。しかし、上記の理由からは、逆は真にならない。 同様に, ドライバが自身を(走行しているプロセスのコンテキストに関わらず)TASK_INTERRUPTIBLE (もしくは TASK_UNINTERRUPTIBLE) にマークできる。 そしてその後、 schedule() を呼び出す。これは (runqueue に残されるケースである保留されたシグナルがなければ) runqueueからそれを削除する。
  2. TASK_INTERRUPTIBLE: タスクは休止しているが、シグナルやタイマの終了で起こされることがある。
  3. TASK_UNINTERRUPTIBLE: TASK_INTERRUPTIBLEと同じだが、起こされない点が異なっている。
  4. TASK_ZONBIE: タスクは終了しているが、その状態が(通常かアダプションによる)親プロセスによって(wait()により)回収されていない。
  5. TASK_STOPPED: ジョブ制御シグナルかptrace(2)により、タスクが停止している。
  6. TASK_EXCLUSIVE: これは独立した状態ではなく、TASK_INTERRUPTIBLETASK_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_cpup->processor, p->counter, p->priority, p->policy そして p->rt_priorityフィールドはスケジューラと関連があるため、後ほど見ていきます。

p->mmp->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_domainp->personality フィールドはタスクのパーソナリティに関連しています。 つまり、あるシステムコールが他のUNIXの"personality"をエミュレートするために振る舞う方法になっています。

p->fsフィールドはファイルシステム情報からなり、Linuxにおいては、三つの情報要素を意味します。

  1. ルートディレクトリのdentryとマウントポイント
  2. 代替ルートディレクトリのdentryとマウントポイント
  3. 現在のワーキングディレクトリのdentryとマウントポイント

この構造体はリファレンスカウントも保持します。これは、CLONE_FSフラグがclone(2)システムコールに渡されたときに、クローンのタスク間で共有できるようにするためです。

p->filesフィールドは、ファイルディスクリプタテーブルからなっています。これもCLONE_FILESclone(2)システムコールで指定されたときにタスク間で共有されます。

p->sigフィールドは、シグナルハンドラからなっています。そしてCLONE_SIGHANDによりクローンのタスク間で共有されます。

2.2 タスクとカーネルスレッドの生成と終端

オペレーティングシステムの他の書籍では、「プロセス」をそれぞれ違うように定義しています。「実行時のプログラムのインスタンス」に始まり、「clone(2)やfork(2)システムコールにより生成されるもの」 まで様々です。Linuxにおいては、三つのプロセスの種類があります。

アイドルスレッドはコンパイル時に一つめのCPUのために作られます。そして「手動」でアーキテクチャ特有のarch/i386/kernel/smpboot.cfork_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に納められています。

以下のステップで実行されます。

  1. ローカル変数のretval-ENOMEMにセットされる。これはfork(2)が新しいタスク構造体の割り当てに失敗したときにセットされるerrnoの値になっている。
  2. もし CLONE_PIDclone_flags にセットされていた場合、呼び出し元がアイドルスレッド(ブート時のみ)でない限り、エラー(-EPERM)を返す。通常のユーザスレッドがCLONE_PIDclone(2)に渡すことはできないし、成功を期待することもできない。
  3. current->vfork_sem が初期化される(これは子によって後にクリアされる)。例えば、他のプログラムを exec() したり、exit(2) したときのように、これは sys_vfork() (clone_flags = CLONE_VFORK|CLONE_VM|SIGCHLDに関連する vfork(2) システムコール) により、子プロセスがmm_release()を実行するまで、親を休止させるために使われる。
  4. 新しいタスク構造体がアーキテクチャ依存のalloc_task_struct()マクロを使って割り当てられる。x86 上では、GFP_KERNEL 優先度の gfp になっている。これが fork(2) システムコールが休止する最大の理由になっている。もし割り当てが失敗すれば、-ENOMEMを返す。
  5. 現在のプロセスのタスク構造体の全ての値は、構造体の代入 *p = *current によって、新しいものへコピーされる。おそらくこれは、memcpyに置き換えられるべきものだ。その後で、子に引き継がれないフィールドは、正しい値に設定される。
  6. 残りのコードがリエントラント可能ではないことから、大きなカーネルロックが取得される。
  7. もし、親がユーザ資源をもっていた場合 (UIDの考え方において、Linuxは、事実としてではなく、その質問をするだけの十分な柔軟性をもっている) 、ユーザがRLIMIT_NPROCソフトリミットを越えているかどうかを確認する。もし越えているなら失敗し-EAGAINを返す。もし越えていなければ、与えられたuidのプロセスのカウンタp->user->countを1増加させる。
  8. もし、実行されるシステム全体のタスク数が、調整可能な値である max_threads を越えていた場合、失敗して-EAGAINを返す。
  9. もしバイナリがモジュール実行領域に関連していた場合、関連モジュールのリファレンスカウンタを増やす。
  10. もし、実行されるバイナリがモジュール化されたバイナリフォーマットに関連していた場合、関連モジュールのリファレンスカウンタを1増加させる。
  11. 子が「実行されていない」(p->pid_exec = 0)とマークされる。
  12. 子が「スワップ不可」(p->swappable = 0)とマークされる。
  13. 子が、「割り込み不可休止」状態、つまりp->state = TASK_UNINTERRUPTIBLEに入る(TODO: なぜこれが行われるのか?わたしには不要に思われる─これを削除するには、Linus が必要ないと認める必要がある)。
  14. 子のp->flagsが clone_flags の値にしたがって設定される。通常のfork(2)の時は、これはp->flags = PF_FORKNOEXECになる。
  15. 子のpid p->pidkernel/fork.c:get_pid()にある fast アルゴリズムを使用して、設定される。(TODO: lastpid_lockスピンロックはリダンダントにできる。do_fork()から大きなカーネルロックのもと常にget_pid()が呼ばれget_pid()のフラグ引数を削除すればリダンダントにできる。パッチは Alan に2000年6月20日に送った。その後をフォローすること)
  16. do_fork()の残りのコードは、子のタスク構造体の残りを初期化する。一番最後に、子のタスク構造体はpidhashハッシュ表にハッシュされ、子は起こされる(TODO: wake_up_process(p)p->state = TASK_RUNNINGを設定し、プロセスを runq に加える。したがって、do_fork()の最初のほうでp->stateTASK_RUNNINGに設定する必要はおそらくない)。 興味深いのは、fork(2)にとってSIGCHLDを意味する、p->exit_signalclone_flags & CSIGNALに設定することと、p->pdeath_signalを0に設定することである。 pdeath_signalは、(親が死ぬなどして)プロセスがオリジナルの親を「忘れた」ときに使われ、prctl(2)システムコールのPR_GET/SET_PDEATHSIGによって設定/取得することができる。(pdeath_signalの値は、prctl(2)がユーザ空間のポインタアーギュメントを経由して戻されるということが少し不思議だと主張されるかもしれない―ごめんなさい。Andries Brouwer がマニュアルページを更新したあとだったので、修正が間に合わなかった ;)
このようにタスクが生成されます。そして、タスクが終端するには、いくつか方法があります。

  1. exit(2)システムコールを発行する。
  2. デフォルトで死ぬ性質のシグナルが届く。
  3. 特定の例外によって、強制的に死ぬ。
  4. func == 1bdflush(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()についてのポイントとしては、

2.3 Linuxスケジューラ

スケジューラの仕事は、カレントの CPU へのアクセスを複数のプロセス間で仲裁することにあります。スケジューラは、「メインのカーネルファイル」kernel/sched.cに実装されています。関連のへッダファイルであるinclude/linux/sched.hは、(明示的にせよ間接的にせよ)事実上全てのカーネルソースファイルから読み込まれます。

スケジューラに関連するタスク構造体のフィールドには、次のものがあります。

schedule()関数は、見掛け上、非常に複雑ですが、スケジューラのアルゴリズムは単純です。関数が複雑なのは、3つのスケジューリングアルゴリズムを一つに実装しているのと同時に、SMP のデリケートな仕様を実装しているからです。

明らかに「無駄な」gotoがschedule()にありますが、これはi386で一番最適化されたコードを生成するためです。もちろん、スケジューラは2.4で完全に(カーネルの他の部分同様)書き直されました。そのため、以下の議論は2.2やそれ以前のカーネルには当てはまりません。

では詳細に関数を見ていきましょう。

  1. もしcurrent->active_mm == NULLなら、なにかがおかしい。カレントのプロセスは、カーネルスレッド(current->mm == NULL )であったとしても、正しいp->active_mmを常に持っていなければならない。
  2. もしtq_schedulerタスクキューに実行するものがあれば、いまそれを処理する。タスクキューは関数の実行を遅らせてスケジュールを行うカーネルの仕組みを提供するものである。その詳細は他の章で説明する。
  3. ローカル変数のprevthis_cpuを、それぞれ現在のタスクと現在の CPU に初期化する。
  4. schedule()が(バグにより)割り込みハンドラから起動されていないかチェックする。もしそうならカーネルパニックになる。
  5. グローバルカーネルロックを解放する。
  6. もしソフト割り込み機構経由で行うことがあれば、ここで実行する。
  7. ローカルポインタstruct schedule_data *sched_dataを初期化し、CPU毎の(キャッシュラインピンポンを避けるためキャッシュラインにアラインされる)スケジューリングデータ領域を指すようにする。これは、last_scheduleのTSC値や最後にスケジュールされたタスク構造体へのポインタからなっている。(TODO: sched_dataは SMP の時のみ使われるが、なぜinit_idle()は UPであっても同様に初期化を行うのか?)
  8. runqueue_lockスピンロックを取得する。私たちがspin_lock_irq()を使うのは、schedule()の中で、割り込み可になっていることを保証するためである。したがって、runqueue_lockを解除するときには、(spin_lock_irqsave/restoreの一種の) saving/restoring eflagsを使うのではなく、単に再度有効にするだけでよい。
  9. タスク状態機械: もしタスクがTASK_RUNNING状態にあれば、そのままにされる。もしTASK_INTERRUPTIBLE状態であり、シグナルが保留されていれば、TASK_RUNNING状態へ移行させる。それ以外の場合は、runqueueから削除する。
  10. (もっともスケジュールされやすい候補の)nextがこの CPU でのアイドルタスクにされる。しかし、この候補のグッドネスは、他のものがこれより良くなるように、とても低い値(-1000)となる。
  11. もしprev(カレント)のタスクがTASK_RUNNING状態であれば、カレントのグッドネスをそのグッドネスと同じに設定する。アイドルタスクよりスケジュールされやすくなるよう印をつける。
  12. さて、runqueueを調べ、このCPUでスケジュールされることが可能な各プロセスのグッドネスをカレントの値と比較する。そして、もっとも高いグッドネスを持つプロセスが勝つ。ここで、この「この CPU でスケジュールされることが可能な」という考え方を明確にすると、UP では、runqueue の各プロセスをスケジュール可能である。SMP においては、すでに他の CPU で走行可能になっていないプロセスだけがスケジュールできる。グッドネスは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をさしているプロセスや、(ユーザ)アドレス空間を持たない(カーネルスレッドのような)プロセスに若干の優先度を与える。
  13. もし、グッドネスの現在の値が 0 のときは、プロセスのリストの全て(runqueueにあるものだけではなく)が検査され、その動的な優先度が単純なアルゴリズムにより再計算される。

    
    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() が、動的優先度を再計算することができているからだ。
  14. この時点で、nextがスケジュールされるタスクを指しており、そのためnext->has_cpuを 1 にnext->processorthis_cpuに初期化していることが確実である。runqueue_lockはそうしたうえで解除できる。
  15. もし同じタスクに戻ってきたなら(next == prev)、単純にグローバルカーネルロックを再取得し返る。つまり、全てのハードウエアレベル(レジスタ、スタックなど)と VM 関連(スイッチページディレクトリ、active_mmの再計算など)のものはスキップされる。
  16. マクロswitch_to()はアーキテクチャ特有である。i386では、これは a)FPU ハンドリング、b) LDT ハンドリング、c) セグメントレジスタのリロード、d)TSSのハンドリング、e) デバッグレジスタのリロードに関係がある。

2.4 Linuxでのリンクリストの実装

ウエイトキューの実装に行く前に、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 つのマクロはnextprevポインタを自身を指すようにして、空リストを初期化するものです。例えば、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);
}

2.5 ウエイトキュー

プロセスがカーネルに依頼したことが現在不可能であるが、後に可能となる事であったなら、プロセスは休止状態へと入り、要求が満たされる条件が整いそうなときに起こされます。この時使われるカーネルのメカニズムの一つが「ウエイトキュー」です。

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() で起っている事というのは、

  1. ウエイトキューの要素をカレントプロセスのコンテキストを指すよう宣言する。
  2. この要素を rtc_wait ウエイトキューに追加する。
  3. 現在のコンテキストに、次回休止したあとに再スケジュールされないことを意味する TASK_INTERRUPTIBLEのマークづけをする。
  4. データが存在しないかどうかチェックする。もし存在するなら、処理を中断してデータをユーザバッファにコピーし、自身をTASK_RUNNINGとマークして、自身をウエイトキューから削除して、返る。
  5. データがなければ、ユーザがノンブロッキング I/O を指定したかどうかチェックし、もしそうなら、EAGAIN(これは、EWOULDBLOCKと同じ)エラーで終了する。
  6. ここでまた、シグナルが保留されていないかをチェックする。もし保留されていれば、「より上のレイヤー」に、必要があればシステムコールを再スタートさせるよう知らせる。「必要があれば」とは、sigaction(2)システムコールで指定されるシグナルの性質の詳細を意図している。
  7. そして、「switch out」つまり、割り込みハンドラに起こされるまで休止状態になる。もし自身を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()によって行われます。これは、必要なウエイトキューの操作を行います。我々がしなければならないことは、デバイス特有の割り込みハンドラによって起こすべきウエイトキューを指示することだけです。

2.6 カーネルタイマ

さて、次はカーネルタイマに注目しましょう。カーネルタイマとは、将来の特定の時刻に、特定の関数(「タイマーハンドラ」と呼ばれる)の実行を行わせるためにあるものです。 主なデータ構造体は、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()関数によって初期化しなければなりません。そして、追加前にfunctionexpiresフィールドを必ず設定しなければなりません。

2.7 ボトムハーフ

割り込みハンドラが実行する仕事を、すぐに行う(例えば、割り込みへの反応、状態の更新などの)仕事と、割り込みが有効(データの後処理)になってから、後で処理すれば良いもの(データの後処理の実施や、データを待っているプロセスを起こすなど)との2つに分けるのが理にかなっていることが、時々あります。

ボトムハーフは、カーネルタスクの実行を延期する最も古い機構で、Linux 1.xのときからあります。Linux 2.0になって、次の章で取り扱う「タスクキュー」という新しい機構が導入されました。

ボトムハーフはglobal_bh_lockスピンロックによってシリアライズされます。つまり一度にいずれかのCPUで実行できるのは、ただ一つのボトムハーフのみであるということです。ハンドラを実行しようとするとき、global_bh_lockが有効でなければ、ボトムハーフは実行の印をつけられ(つまりスケジュールされ)ます。そして、global_bh_lockでビジーループすることを避けるため処理は継続されます。

現在、全部で32のボトムハーフだけが登録されています。ボトムハーフを操作するために必要となる関数は、以下のようになります(すべてモジュール化されています)。

ボトムハーフは、グローバルロックされたタスクレットです。そこで「ボトムハンドラはいつ実行されるのか?」という質問は、本当は「タスクレットはいつ実行されるのか?」というのが本当なのです。答えは、a) 各schedule()と b)entry.Sの各割り込み/システムコールのリターンパスの二カ所です。(TODO: そのため、schedule()の場合は本当に遅いのです。もう一つの非常に遅い遅い割り込みを追加したようなものです。なぜ、schedule()からhandle_softirqラベルを完全に取り除かなかったのか?)

2.8 タスクキュー

タスクキューは古いボトムハーフのダイナミックな拡張として考えられます。実際のところ、ソースコードでは、「新しい」ボトムハーフとして参照されているところもあります。より具体的には、前章で取り上げた古いボトムハーフには、次に示すような制限があります。

  1. 決められた数(32)しか存在できません。
  2. 各ボトムハーフは一つのハンドラ関数としか関連づけられません。
  3. ボトムハーフは、ブロック出来ないように、持っているスピンロックが消費されます。

そこで、タスクキューにより任意の数の関数をつなぎ、後で次々と処理ができるようにします。 DECLARE_TASK_QUEUE マクロを使って新しいタスクキューを作成すると、queue_task()関数を使って、そのキューへとタスクをためることができます。そして、タスクキューはrun_task_queueによって処理されます。自身のタスクキューを作成する代りに(そして、手動で消費する代りに)、Linux にあらかじめ準備された、良く知られた個所で消費されるタスクキューを使うことができます。

  1. tq_timer: タイマのタスクキュー。各タイマ割り込み毎と tty デバイスを解放するとき(半オープン状態のターミナルデバイスをクローズないしは解放する時)に実行する。タイマハンドラが割り込みコンテキストで実行されたときは、tq_timerタスクも割り込みコンテキスト内で実行されるため、ブロックできない。
  2. tq_scheduler: スケジューラによって(tq_timer 同様 tty を閉じるときも)消費されるスケジューラタスクキュー。スケジューラが再スケジュールされるプロセスのコンテキストで実行されるため、tq_scheduler タスクはやりたいことが何でもできる。つまり、ブロックしてプロセスのコンテキストのデータを使用する(やりたいかどうかは別として)などである。
  3. tq_immediate: これは本当はボトムハーフIMMEDIATE_BHであり、そのためドライバはqueue_task(task, &tq_immediate) と、そして割り込みコンテキストで消費されるmark_bh(IMMEDIATE_BH)を行う。
  4. tq_disk: 実際の要求を開始するため、低レベルブロックデバイスのアクセス(とRAID)により使われる。このタスクキューはモジュールにエキスポートされるが、設計上の特定の目的を除いて使われない。

ドライバが自身のタスクキューを使わないかぎり、以下の場合を除いて、キューを処理するためにrun_tasks_queue()を呼ぶ必要はありません。

タスクキューtq_timer/tq_scheduler を消費するのが通常の場所だけでなく外の場所(一例としては、ttyデバイスのクローズ)でも行われる理由は、ドライバがキューにタスクをスケジュールしたとき、そのタスクが意味を持つのはデバイスの特定のインスタンスが有効であるとき(通常はアプリケーションがクローズするまで)なのを考えると自ずと明らかでしょう。 そのため、ドライバはrun_task_queueを呼んで、自身や(他のドライバが)キューにいれたタスクをフラッシュする必要があります。これは、後で実行することができるようにすることが無意味だからです。つまり、関連するデータ構造は他のインスタンスにより解放されたり再利用されるということです。 これが、それぞれタイマ割り込みやschedule()の代りにtq_timertq_schedulerrun_task_queue()がある理由になっています。

2.9 タスクレット

作成中。将来のリビジョンで記述。

2.10 ソフト割り込み

作成中。将来のリビジョンで記述。

2.11 システムコールがi386ではどのように実装されているか?

他の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のシステムコールエントリポイントへとジャンプします。これは、次のようなことを行います。

  1. レジスタを保存する。
  2. %ds と %es を KERNEL_DS に設定する。そのため、参照する全てのデータ(と外部のセグメント)はカーネルアドレス空間になる。
  3. もし %eax の値がNR_syscalls(現在 256) より大きかった場合には、ENOSYS エラーで失敗する。
  4. もしタスクがptraced されていた(tsk->ptrace & PF_TRADESYS )時は、特別な処理を行う。これは strace (SVR4でいうtruss(1))のようなプログラムやデバッガをサポートするためである。
  5. sys_call_table+4*(%eax の syscall_number)を呼ぶ。このテーブルは同じファイル(arch/i386/kernel/entry.S)で初期化されており、各々のシステムコールハンドラを指している。Linux においては、ハンドラには通常、例えば sys_opensys_exitのように sys_というプレフィックスがついている。これらの C システムコールハンドラはSAVE_ALLが格納したスタックから引数を見つけ出す。
  6. ``system call return path''に入る。int 0x80 だけでなく、lcall7, lcall27でも使うため、別のラベルになっている。これは、(ボトムハーフの)タスクレットの取り扱いや、schedule()が必要かどうか(tsk->need_resched != 0) を確認したり、シグナルが保留されていないか確認したり、そしてそれらのシグナルを処理したりすることに関連している。

Linux は システムコールに 6 つまでの引数をサポートしています。これらは %ebx, %ecx, %edx, %esi, %edi (および一時的に使われる %edp。asm-i386/unistd.h_syscall6()を参照)に入れて渡されます。システムコール番号は %eax で渡されます。

2.12 アトミックな操作

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, &microcode_status))
                return -EBUSY;

        MOD_INC_USE_COUNT;
        return 0;
}

ビットマップの操作は、

これらの操作は、LOCK_PREFIX マクロを使っています。このマクロは SMP カーネルにおいてバスのロック操作を行う接頭辞として評価されますが、UP カーネルでは何もしません。これによって、SMP 環境でのアトミックなアクセスを保証しています。

ときにはビット操作が不便な場面もありますが、その場合は算術演算を使う必要があります。加算、減算、インクリメント、デクリメントなどです。(inode のような)参照カウンタが典型的な例です。この機能はatmic_tデータタイプと以下の操作で実現されています

2.13 スピンロック、読み書きスピンロック、ビッグリーダースピンロック

初期の 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種類あります。

  1. プレーンspin_lock()/spin_unlock(): 割り込みが常に不可にされるか、割り込みコンテキストと競合しないことが分かっている(例えば割り込みハンドラ内部で)なら、これを使うことができる。現在の CPU の割り込み状態には触れることがない。
  2. 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()を使わないのは、全てのシステムコール割り込みが常に有効になっているからである。
  3. 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);
}

この例については、いくつか注記があります。

  1. 典型的なドライバがとる方法の(属性値と返り値が明確に省略されている)ioctl() によるプロセスのコンテキストでは、必ずspin_lock_irq()を使わなければならない。 これは、デバイスのioctl()呼び出しを実行している間は、割り込みが常に有効になっているためである。
  2. (属性値は明確に省略されている)my_irq_handler()によって表される割り込みコンテキスト では、plain spin_lock() form を使うことができる。これは、割り込みハンドラ内部では、割り込みは無効にされているためである。

2.14 セマフォと読み書きセマフォ

時折、共有データ構造体にアクセスするときに、操作の実行をブロックしなければならないことが あります。たとえばデータをユーザ空間へコピーする場合などです。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;
}

この例のポイントは以下の通りです。

  1. 関数は、copy_from_user()/copy_to_user()で、データをユーザスペースから、もしくはユーザスペースへコピーするときにブロックされることがある。 すなわち、ここではいかなるタイプのスピンロックもつかうことはできない。
  2. ベーシックではなく読み書きのセマフォが選ばれているのは gethostname(2)要求が同時にたくさん発生し得るが、それらは交互に実行する必要はない ためである。

Linux のセマフォならびに読み書きセマフォの実装は非常に精巧にできていますが、 まだ実装されていない考え得る可能なシナリオも存在しています。例えば、割り込み可能な 読み書きセマフォのコンセプトなどです。このようなエキゾチックな種類のプリミティブ が必要になる状況は、実世界でない状況なので、このようなコンセプトは明らかにありえません。

2.15 モジュール読み込みのカーネルサポート

マイクロカーネルデザインに基づくオペレーティングシステムの提供する"アドバンテージ"といった最近の宣伝にも関わらず、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 のロード可能モジュールとして実装されています。

  1. キャラクタならびにブロックデバイスドライバ。misc デバイスドライバを含む。
  2. ターミナルライン関係。
  3. /procと devfs における仮想(一般の)ファイル (例えば/dev/cpu/microcodedev/misc/micrococe)。
  4. バイナリファイルフォーマット (例えば ELF, aoutなど)。
  5. 実行ドメイン (例えば, Linux, UnixWare7, Solarisなど)。
  6. ファイルシステム。
  7. System V IPC。

一部には Linux におけるモジュールとして実装できていないものもあります。(おそらくそれらは モジュールについて考慮されていないためと思われます)

  1. スケジューリングアルゴリズム
  2. VMポリシー
  3. バッファキャッシュ、ページキャッシュやその他のキャッシュ

Linux にはロード可能モジュールを援助するいろいろなシステムコールがあります。

  1. caddr_t create_module(cost char *name, size_t size): vmalloc()を用いてsizeバイトを割り当て、モジュールの構造体をその先頭に割り付ける。 新しいモジュールは module_list が先頭にあるリストへとリンクされる。CAP_SYS_MODULEのプロセスのみが、このシステムコールを呼び出すことができ、他のプロセスはEPERMエラーが返る。
  2. long init_module(const char *name, struct module *image): 再配置されたモジュールイメージを読み込み、モジュールの初期化ルーチンが起動される。CAP_SYS_MODULEが設定されたプロセスのみがこのシステムコールを呼び出すことができ、それ以外の場合はEPERMエラーが返る。
  3. long delete_module(const char *name): モジュールをアンロードしようとする。name == NULLならば、全ての使用されていないモジュールを取り外そうとする。
  4. long query_module(const char *name, int which, void *buf, size_t bufsize, size_t *ret): モジュールの(ないしは全てのモジュールの)情報を返す。

ユーザが使えるコマンドインターフェースは次の通りです。

insmodmodprobeを使ってモジュールを手動で読み込むことができるのは当然ですが、 特定の機能が必要になったときに、カーネルが自動的にモジュールを読む機能もあります。 この機能のカーネルインターフェースは 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;
}

この関数では、いくつか注意する点があります。

  1. 最初に、与えられた名前のファイルシステムをすでに登録されているうちから見つけようとする。(登録されているファイルシステムのリストを更新しないため)読み込みのために取得する file_system_lock の保護のもと、これは実行される。
  2. もしファイルシステムが見つかったら、新しいリファレンスを取得しようとする。そして、モジュールのホールドカウントを 1 増やそうとする。これは静的にリンクされたファイルシステムでは常に 1 を返す。モジュールとしてロードしたファイルシステムでも、現在削除されているところでない限り 1 を返す。もし try_inc_mod_count() が 0 を返す場合は失敗したと考える。つまり、モジュールはあるが削除されているところで、それは元々なかったのと同じことである。
  3. (request_module())次は、ブロックする操作を行いたいため、file_system_lockを落とす。したがってこの時にはスピンロックを保持できない。実際のところ、この特別な場合には、たとえrequest_module()がノンブロックであることが保証されていて、モジュール読み込みが同じコンテキストでアトミックに実行されたとしても、file_system_lockを落とさなければならない。これは、モジュールの初期化ルーチンでは、書き込みのために同じfile_systems_lockを呼び出すregister_filesystem()を呼び出すことがあるからである。
  4. もしロードが成功したら、file_systems_lockスピンロックを取得し、新しく登録されたファイルシステムをリストへ加えようとする。これは少し悪い考えである。というのは、原理的にはmodprobe コマンドのバグになり得るからだ。 request_module()が失敗したが、新しいファイルシステムが登録されてしまって、しかもget_fs_type()が見つからない場合に、要求されたモジュールのロードが成功した後にコアダンプしてしまうことがあるためだ。
  5. もしファイルシステムが見つかり、そのリファレンスを取得できたら復帰する。そうでなければ、NULL を返す。

モジュールがカーネルにロードされているとき、カーネルの EXPORT_SYMBOL()マクロを使っている部分や現在ロードされている他のモジュールから、パブリックにエキスポートされたシンボルを参照することができます。 。もしモジュールが他のモジュールからのシンボルを使うときは、ブート時のdepmod -aコマンドの実行により行われる依存関係の再計算の間は(例えば新しいカーネルをインストールした後など)、そのモジュールは保留されることになります。

通常、使用するカーネルインターフェースのバージョンと、モジュール一式のバージョンは一致しなければなりません。これは Linux では、特別なカーネルインターフェースのバージョン機構を一般には持っていないため、単純な「カーネルバージョン」を意味しています。しかし、「モジュールバージョン」ないしはCONFIG_MODVERSIONS と呼ばれる限定された機能をもっています。これによって、新しいカーネルに移行したときにモジュールの再コンパイルを避けることができます。 カーネルのシンボルテーブルを、内部のアクセスとモジュールのアクセスを別々に扱うことで、これを実現しています。シンボルテーブルのパブリック(つまりエキスポートされた)要素は、Cの修飾子により32ビットチェックサムがつけられます。そして、ロード中にモジュールが使うシンボルを解決するために、ローダはチェックサムを含めてシンボルの表記名を比較します。もし一致しなければロードを拒否するようになっています。 これは、カーネルとモジュールを、モジュールバージョンを有効にしてコンパイルしたときのみ起ります。もし、どちらかがオリジナルのシンボル名を使っていた場合、ローダは単にモジュールによってカーネルバージョンの修飾のついたものと、カーネルによりエキスポートされているものを比較して、違っていればロードを拒否します。


次のページ 前のページ 目次へ