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

1. ブート

1.1 Linuxカーネルイメージの作成

この章では、Linuxカーネルをコンパイルをする時に取られるステップと各ステージの生成物について解説します。 ビルド工程はアーキテクチャにより異なりますが、ここではLinux/x86カーネルのビルドについてだけ考えることにします。

ユーザが、'make zImage'あるいは'make bzimage'とタイプすると、その結果の起動イメージは、それぞれarch/i386/boot/zImagearch/i386/boot/bzImage になります。 ここでは、どのようにイメージが作られるかを見ていきましょう。

  1. C とアセンブリのソースファイルは、ELF 再配置可能オブジェクト形式 (.o) へとコンパイルされる。中には論理的なグループとして ar(1) を使ってアーカイブ形式 (.a) にされるものもある。
  2. ld(1) を使って、上記の .o と .a は 'vmlinux' ファイルへとリンクされる。'vmlinux'ファイルは、静的にリンクされたストリップ前の ELF 32-bit LSB 80386 実行形式ファイルである。
  3. System.map は、'nm vmlinux' から作成される。関連がないシンボルや些細なシンボルは、fマップファイルから除外される。
  4. arch/i386/boot ディレクトリに移る。
  5. ブートセクタのアセンブラコード bootsect.S は、ターゲットが bzImage か zImage かによって -D__BIG_KERNEL__ をつけるか、あるいはつけないかされて、プリプロセッサが 処理する。そして、各々の場合で bbootsect.s かあるいは bootsect.s が生成される。
  6. bbootsect.s はアセンブルされ、その後、bbootsect という名前の'rawバイナリ'形式へと変換される。(あるいは、bootsect.s がアセンブルされ zImage 向けの bootsect へrawバイナリ変換される)
  7. セットアップ用のコード setup.S (setup.Sは、video.Sを含む)は、 bzImageのときはbsetup.sへ、zImageの時はsetup.sへプリプロセッサの処理結果が出力される。ブートセクタのコードと同じ方法で処理されるが、bzImageの場合に、-D__BIG_KERNEL__タグが付与されるところに違いがある。生成物は、bsetupという名前の'rawバイナリ'形式へと変換される。
  8. arch/i386/boot/compressed というディレクトリへとうつり、 /usr/src/linux/vmlinux をrawバイナリ形式の、$tmppiggy (テンポラリファイル名)へと変換する。またこの際に、ELF フォーマットでの .note セクションと .comment セクションが削除される。
  9. gzip -9 < $tmppiggy > $tmppiggy.gz
  10. $tmppiggy.gz を ELF 再配置可能形式の (ld -r) piggy.o へリンクする。
  11. head.S と misc.c からなる圧縮ルーチンをコンパイルする(まだ arch/i386/boot/compressed ディレクトリ)。そして ELF オブジェクト形式の head.o と、misc.o を生成する。
  12. head.o と misc.o、そして piggy.o をリンクし、bvmlinuxとする(あるいは zImageの場合は、vmlinux にする。 /usr/src/linux/vmlinux と間違わないこと)。ここで、vmlinux では-Ttext 0x1000 だが、bvmlinux の場合 -Ttext 0x100000 というところに違いがある。bzImage の圧縮ローダは high エリアにロードされる。
  13. .note と .comment の2つの ELF セクションを削除して bvmlinux は 'raw バイナリ'形式の bvmlinux.outへと変換される。
  14. ディレクトリ arch/i386/boot へ戻り、tools/build にあるプログラムを使って、 bbootsect + bsetup + 圧縮された/bvmlinux.out を bzImage へとつなぎ合わせる(zImage の場合は、各々の b を取ったものをつなぎ合わせる)。これは、さらに setup_sects や、root_dev のような重要な変数を、ブートセクタの最後のところへと書き込む。

ブートセクタの大きさは、常に512バイトです。setupのサイズは、4セクタより大きくなければなりませんが、最大でも約12Kに制限されています。これは次のようなルールで計算されます。

0x4000 bytes >= 512 + setup のセクタ数 * 512 + ブートセクタ/setupを実行するときのスタックの領域

後で、この制限がどこからきているのか学ぶことにしましょう。

bzImage のサイズの上限は、現時点では LILO からのブートで約2.5M となっています。そして、たとえば、フロッピーディスクや、CD-ROM (El-Torito エミュレーションモードにおいて)といった、raw イメージのブートでは 0xFFFF パラグラフ分(0xFFFF0 = 1048560 バイト)になります。

ここで tools/build は、カーネルイメージのブートセクタやカーネルイメージ、setup の下限サイズを検証します。しかし、setup の上限については、チェックしません。したがって、setup.S の最後に余分な大きな".space"を付加するだけで、簡単に壊れたカーネルが作成できてしまいます。

1.2 ブート: 概論

ブートプロセスの詳細部分はアーキテクチャに依存しています。そこで IBM PC/IA32 のアーキテクチャへ注目することにします。古いデザインですし、また過去への互換性への問題から、PC のファームウエアは、オペレーティングシステムをブートするときには、古い方式で起動してきます。このプロセスは、つぎの 6 つの論理的なステップへと分割できます。

  1. BIOS はブートデバイスを選ぶ。
  2. BIOS はブートデバイスのブートセクタをメモリーへと読み込む。
  3. ブートセクタは、setup と解凍ルーチンそして、圧縮されたカーネルを読み込む。
  4. カーネルは、プロテクトモードで解凍される。
  5. ローレベルの初期化が asm コードで行われる。
  6. ハイレベルの C での初期化が行われる。

1.3 ブート: BIOS POST

  1. 電源が入り、クロックジェネレータが開始する。また、バス上の #POWERGOOD 信号がアサートされる。
  2. CPU #RESET 信号がアサートされる (この時 CPU は、8086 互換のリアルモードである)。
  3. %ds=%es=%fs=%gs=%ss=0, %cs=0xFFFF0000, %eip = 0x0000FFF0 (ROM BIOS POST コード)。
  4. 割り込みが禁止された状態で、すべての POST チェックが行われる。
  5. IVT (割り込みベクトルテーブル) がアドレス 0 へ初期化される。
  6. BIOS の ブートストラップローダ関数が、int 0x19 により呼び出される。このとき、%dl は、ブートデバイスの「ドライブ番号」である。この関数は、 トラック 0 セクタ 1 番を物理アドレスの 0x7C00 (0x07C0:0000) へと読み込む。

1.4 ブート: ブートセクタと setup

Linuxカーネルをブートするために使われるブートセクタは、

のいずれかになります。 ここで、Linuxブートセクタの詳細を見てみます。最初の数行では、セグメント値に使う便宜上のマクロが初期化されています。

29 SETUPSECS = 4                /* default nr of setup-sectors */
30 BOOTSEG   = 0x07C0           /* original address of boot-sector */
31 INITSEG   = DEF_INITSEG      /* we move boot here - out of the way */
32 SETUPSEG  = DEF_SETUPSEG     /* setup starts here */
33 SYSSEG    = DEF_SYSSEG       /* system loaded at 0x10000 (65536) */
34 SYSSIZE   = DEF_SYSSIZE      /* system size: # of 16-byte clicks */

(左の数字は bootsect.S ファイルの行番号である) DEF_INITSEG, DEF_SETUPSEG, DEF_SYSSEG および DEF_SYSSIZE の値は、 include/asm/boot.h で定義されており、

/* Don't touch these, unless you really know what you're doing. */
#define DEF_INITSEG     0x9000
#define DEF_SYSSEG      0x1000
#define DEF_SETUPSEG    0x9020
#define DEF_SYSSIZE     0x7F00

となっています。 さて、実際のbootsect.Sのコードを見ていきましょう。


    54          movw    $BOOTSEG, %ax
    55          movw    %ax, %ds
    56          movw    $INITSEG, %ax
    57          movw    %ax, %es
    58          movw    $256, %cx
    59          subw    %si, %si
    60          subw    %di, %di
    61          cld
    62          rep
    63          movsw
    64          ljmp    $INITSEG, $go
       
    65  # bde - changed 0xff00 to 0x4000 to use debugger at 0x6400 up (bde).  We
    66  # wouldn't have to worry about this if we checked the top of memory.  Also
    67  # my BIOS can be configured to put the wini drive tables in high memory
    68  # instead of in the vector table.  The old stack might have clobbered the
    69  # drive table.
       
    70  go:     movw    $0x4000-12, %di         # 0x4000 is an arbitrary value >=
    71                                          # length of bootsect + length of
    72                                          # setup + room for stack;
    73                                          # 12 is disk parm size.
    74          movw    %ax, %ds                # ax and es already contain INITSEG
    75          movw    %ax, %ss
    76          movw    %di, %sp                # put stack at INITSEG:0x4000-12.

54-63行は、アドレス 0x7C00 から 0x90000 へブートセクタコードを移動しています。これは、次のような手順で実行されています。

  1. %ds:%si へ $BOOTSEG:0 (0x7C0:0 = 0x7C00) を設定する。
  2. %es:%di へ $INITSEG:0 (0x9000:0 = 0x90000)を設定する。
  3. %cx へ16ビットのワード値を代入する (256 ワード = 512 バイト = 1 セクタ)。
  4. 自動的にアドレスを加算するよう(cld)に、EFLAGS の DF (direction) フラグをクリアする。
  5. 先に進み、 512 bytes (rep movsw)コピーする。

このコードがrep movsdを使わないのは、特別な理由があります。(ヒント .code16) 64行目では、新しく作られたブートセクタのコピーのラベルgo:へジャンプする。つまり、セグメント 0x9000です。これと続く3つの命令(64-76行)では、$INITSEG:0x4000-0xCへスタックを設定しています。つまり、%ss = $INITSEG (0x9000) と %sp = 0x3FF4 (0x4000-0xC) です。ここに前出のsetupのサイズ制限がどこからきているかの理由があります (Linux カーネルイメージの作成 参照)。

77-103行では、一つめのディスクパラメータテーブルを上書きして、マルチセクタ読み込みができるようにします。


    77  # Many BIOS's default disk parameter tables will not recognise
    78  # multi-sector reads beyond the maximum sector number specified
    79  # in the default diskette parameter tables - this may mean 7
    80  # sectors in some cases.
    81  #
    82  # Since single sector reads are slow and out of the question,
    83  # we must take care of this by creating new parameter tables
    84  # (for the first disk) in RAM.  We will set the maximum sector
    85  # count to 36 - the most we will encounter on an ED 2.88.  
    86  #
    87  # High doesn't hurt.  Low does.
    88  #
    89  # Segments are as follows: ds = es = ss = cs - INITSEG, fs = 0,
    90  # and gs is unused.
       
    91          movw    %cx, %fs                # set fs to 0
    92          movw    $0x78, %bx              # fs:bx is parameter table address
    93          pushw   %ds
    94          ldsw    %fs:(%bx), %si          # ds:si is source
    95          movb    $6, %cl                 # copy 12 bytes
    96          pushw   %di                     # di = 0x4000-12.
    97          rep                             # don't need cld -> done on line 66
    98          movsw
    99          popw    %di
   100          popw    %ds
   101          movb    $36, 0x4(%di)           # patch sector count
   102          movw    %di, %fs:(%bx)
   103          movw    %es, %fs:2(%bx)

BIOS のサービスの int 0x13 ファンクション0 (reset FDC) を使って、フロッピーディスクコントローラをリセットします。そして、setup セクタが bootsector のすぐ後へ読み込まれます。つまり 物理アドレスの0x90200 ($INITSEG:0x200) です。そして、再度BIOSサービスのint 0x13 ファンクション 2(read sector(s)) を呼び出します。この辺りは、107-124行に記述されています。


   107  load_setup:
   108          xorb    %ah, %ah                # reset FDC 
   109          xorb    %dl, %dl
   110          int     $0x13   
   111          xorw    %dx, %dx                # drive 0, head 0
   112          movb    $0x02, %cl              # sector 2, track 0
   113          movw    $0x0200, %bx            # address = 512, in INITSEG
   114          movb    $0x02, %ah              # service 2, "read sector(s)"
   115          movb    setup_sects, %al        # (assume all on head 0, track 0)
   116          int     $0x13                   # read it
   117          jnc     ok_load_setup           # ok - continue
       
   118          pushw   %ax                     # dump error code
   119          call    print_nl
   120          movw    %sp, %bp
   121          call    print_hex
   122          popw    %ax     
   123          jmp     load_setup
       
   124  ok_load_setup:

もし、なにかの原因(フロッピーが劣化しているとか、使用中にディスケットを抜き去ったとか)で読み込みが失敗したら、エラーコードを表示しながら、無限に再試行されます。再試行が成功していない状態から抜け出すには、パソコンを再起動するほかありません。しかし、通常このようなことは起りません (もしなにかが間違えているとしたら、単におかしくなったのです)。 もし、setupのコードのsetup_sectセクタの読み込みがうまく行くと、ラベル ok_load_setup: へジャンプします。 その後、圧縮されたカーネルイメージを物理アドレス 0x10000 へと読み込みます。これは、低位のメモリ領域 (0-64K) にあるファームウエアのデータ領域を保護するために行われています。カーネルが読み込まれると、$SETUPSEG:0 (arch/i386/boot/setup.S)にジャンプします。ファームのデータがもう要らなくなれば (例えば、もうBIOSをコールしないなど)、(圧縮された)すべてのカーネルのイメージを 0x10000 から 0x1000 (当然、物理アドレス)へ移します。その結果、この領域は上書きされます。 これは、setup.Sで実行されます。このコードでは、プロテクトモードへの移行に必要なことを行い、圧縮カーネルの先頭である 0x1000 へとジャンプするようになっています。つまり、arch/i386/boot/compressed/{head.S,misc.c} です。 さらに、スタックを設定し、decompress_kernel() を呼び出します。このルーチンではカーネルをアドレス 0x100000 へ展開します。そして、その展開されたカーネルへジャンプします。

ここで古いブートローダ(古いLILO)では、setupの最初の4セクタしか読み込むことが できません。そのため、もし必要であれば自分自身の残りを読み込むようなsetupの コードが存在しています。もちろん、setup のコードは様々なタイプ/バージョンのローダとzImage/bzImageの組合わせを取り扱わなければなりません。そのため、非常に複雑です。

ここで、"bzImage" として知られている大きなカーネルのロードを行うブートセクタコードでの対処方法を見てみましょう。 setupのセクタは、通常0x90200にロードされますが、この時のカーネルは、一度に64Kの固まりで読み込まれるような特別な補助ルーチンを使います。このルーチンは、データを低位アドレスから、高位アドレスへ移動するBIOSコールを呼び出します。この補助ルーチンは、setup.Sbootsect_kludgeラベルから参照されます。そしてbootsect_helperとしてsetup.Sで定義されています。 setup.Sbootsect_klughラベルは、setupセグメント値と、その中のbootsect_helperコードのオフセットからなっています。そのため、ブートセクタでは、ジャンプするのにlcall命令を使うことができます。(つまりセグメント内ジャンプです) setup.Sにこれがあるのは、単にbootsect.Sには、もう余分なスペースが無いからなのです。(これは厳密には正しくはありません -- bootsect.Sには、約4バイトと少なくとも1バイトの余裕があります。しかし、明らかに十分とはいえません)。このルーチンは高位のメモリへ移動するのに、int 0x15(ax=0x8700)のBIOSサービスを使い、%esを常に0x10000を示すようリセットします。これは、bootsect.Sのコード内で、ディスクからデータをコピーするときに低位メモリが足らなくならないことを保証します。

1.5 LILOをブートローダとして使う

Linuxブートセクタを裸で使わず、特別なブートローダ(LILO)を使うことで、利点が生まれます。

  1. 複数の Linux カーネルから選択したり、さらには複数の OS も選択できるようになります。
  2. カーネルにコマンドラインパラメタを渡すことができるようになります (裸の bootsector+setup にこの機能を付加できる BCP というパッチがあります)。
  3. よりサイズの大きな bzImage カーネルをロードすることができます。1M までのところが 2.5M まで可能になります。

LILO の古いバージョン(v17以前)では、bzImage カーネルをロードすることができません。新しいバージョンでは(ここ数年前より最近では)、bootsect+setup と同じように、BIOS サービスにある低位メモリから高位メモリへデータを移動するテクニックを使っています。人によっては(特に Peter Anvinは)、zImage サポートは削除すべきだと主張しています。それでも残されている主な理由は、(Alan Cox によると) zImage のロードは可能だが、bzImage カーネルのブートができないような壊れた BIOS が明らかに存在しているためだということです。

LILO は最後に、setup.Sへジャンプし、通常どおりの処理を続けます。

1.6 高いレベルの初期化

「高いレベルの初期化」では、ブートに直接関連しないものについて考えます。しかし、これを行うコードの一部は、 展開されたカーネルの先頭にあるarch/i386/kernel/head.Sと呼ばれるアセンブラで書かれています。そこでは、以下のような処理が行われます。

  1. セグメント値を初期化する (%ds = %es = %fs = %gs = __KERNEL_DS = 0x18)。
  2. ページテーブルを初期化する。
  3. %cr0のPGビットをセットし、ページングを有効にする。
  4. BSSをゼロで埋める (SMPでは、1つめのCPUだけがこの処理を行う)。
  5. ブートパラメータの最初の2kをコピーする (カーネルコマンドライン)。
  6. CPUタイプをEFLAGSと、可能であれば386以上を確認できるcpuidでチェックする。
  7. 1つめのCPUはstart_kernel()を呼び出し、他のCPUは、ready=1であれば単に esp/eip をリロードするのみで戻ってこない関数の arch/i386/kernel/smpboot.c:initialize_secondary() を呼び出す。

init/main.c:start_kernel()はCで書かれており、以下のような処理を行います。

  1. (初期化の間には1つのCPUだけが動作することが必要なため)グローバルのカーネルロックを取得する。
  2. アーキテクチャ独自のセットアップを実行する(メモリレイアウトの解析、ブートコマンドラインの再度のコピーなど)。
  3. Linux カーネルのバージョン、ビルドに使用したコンパイラなどからなる「バナー」をメッセージ用のカーネルリングバッファへ書き込む。このメッセージは、init/version.c で定義されている linux_bannar 変数から取得する。このメッセージは、cat /proc/version コマンドを実行したときと同じメッセージになっている。
  4. trap を初期化する。
  5. irq を初期化する。
  6. スケジューラの使うデータを初期化する。
  7. 時刻を保持するデータを初期化する。
  8. ソフト割り込みサブシステムを初期化する。
  9. ブートコマンドラインオプションを解析する。
  10. コンソールを初期化する。
  11. もしモジュールサポートがカーネルに組み込まれていたら、動的モジュール読み込み機構を初期化する。
  12. もし、"profile=" コマンドラインがあれば、プロファイルバッファを初期化する。
  13. kmem_cache_init() を実行し、ほとんどのスラブアロケータを初期化する。
  14. 割り込みを有効にする。
  15. 使用している CPU の BogoMips を計算する。
  16. max_mapnrtotalram_pageshigh_memoryを計算するmem_init()を呼び出し、"Memory: ..." の行を表示する。
  17. kmem_cache_sizes_init() を実行し、スラブアロケータの初期化が完了する。
  18. procfs が使うデータ構造体を初期化する。
  19. fork_init()を呼び出し、uid_cache を作成して、利用可能なメモリ量に基づきmax_threadsを初期化し、 init_taskmax_threads/2 になるよう RLIMIT_NPROC を設定する。
  20. VFS、VM、バッファキャッシュなどで必要な種々のスラブキャッシュを作成する。
  21. System V IPC サポートがカーネルに含まれていたら、IPC サブシステムを初期化する。System V shm の場合は、shmfsファイルシステムの(カーネルの)内部インスタンスをマウントすることに注意すること。
  22. quota サポートがカーネルに含まれていたら、quota 用のスラブキャッシュを作成し初期化する。
  23. アーキテクチャ特有の「バグのチェック」を行う。そして現時点で可能な限り、プロセッサやバス、その他の対処を有効にする。各種のアーキテクチャを比較すると、「IA64 にはバグはなく」、「IA32 はかなりバグがある」。この良い例として「f00fバグ」がある。このバグは、カーネルが 686 より前の CPU 向けにコンパイルされたときだけチェックされ、チェック結果に従い対処を行なう。
  24. スケジューラが「次の機会」に起動される事を示すフラグをセットする。そして、init カーネルスレッドを作成する。このカーネルスレッドは、もし、"init=" ブートパラメータが与えられていた場合は、 execute_command を実行する。もし指定がなければ、/sbin/init/etc/init/bin/init/bin/shの順にファイルを探し、実行しようとする。もし全て失敗したら、"init="パラメータを使うよう「忠告」してパニック状態になる。
  25. アイドルループに入る。これは pid=0 のアイドルスレッドとなる。

ここで重要なことは、init() カーネルスレッドが do_basic_setup() を呼び出していることです。この関数はさらに、__initcallmodule_init() マクロによって登録された関数のリストを読み出して実行する do_initcalls() を呼び出します。 これらの関数は、各々が相互に依存していないか、Makefileでリンクの順序を入れ替えることで、依存関係を手動で修正してあります。 これはすなわち、ツリーの中のディレクトリの位置とMakefileの構成によって、初期化関数の実行順序が入れ替えられるということを意味しています。 ときには、二つのサブディレクトリAとBがあった場合、BがAの中の初期化関数に依存しているような場合に、この順序が重要になります。もし、Aが静的にカーネルにリンクされ、Bはモジュールであった場合は、Bの実行タイミングは、Aが必要な環境を整えた後であることが保証されます。もし、Aがモジュールであり、Bも当然モジュールである場合にも問題はありません。しかし、AとBが両方静的にカーネルにリンクされる場合はどうでしょう? 2つの実行順序は、カーネルイメージの.initcall.init ELF セクションにおける位置の差に依存しているのです。 Rogier Wolffは階層的な「優先度」構造を提案し、それによって リンカがどの(相互的な)順序でモジュールをリンクするかが分かるようにしました。しかし、いまのところ、これをカーネルへ受け入れられるような効果的にエレガントな方法で実装したパッチは存在していません。 したがって、リンクの順序を正しくしないといけないのです。もし、上記の例で、AとBがともに静的にコンパイルされたとき良好に動作したなら、常に動作しますが、そのためにはおなじMakefileに順序よくリストしなければなりません。 もしうまく働かないようなら、オブジェクトファイルのリスト順を変えることになります。 注意する価値のあるもう一つの事柄として、"init="ブートコマンドラインを渡すことにより、「別のinitプログラム」を実行するLinuxの機能があります。これは、/sbin/init を誤って上書きしたときの回復や、初期化(rc)スクリプトや /etc/initttab を、一回に一つずつ手で実行することでデバッグするのに有益です。

1.7 x86でのSMPブート

SMPにおいて、BP は start_kernel()へ進み、そしてsmp_init()と特にsrc/i386/kernel/smpboot.c:smp_boot_cpus()に進むまで、通常のブートセクタ、setupなどのブートシーケンスを進んでいきます。 smp_boot_cpus()は、(NR_CPUSまで)ループで各apicidごとに実行され、その中でdo_boot_cpu()が呼ばれます。 do_boot_cpu()はターゲットのCPU用のアイドルタスクを生成(i.e. fork_by_hand)します。そして、Intel MP仕様で定義されている既定の位置 (0x467/0x469) へと、trampoline.SにあるトランポリンコードのEIPを書き込みます。そして、このAPが trampoline.Sのコードを実行するように、ターゲット CPU の STARTUP IPI を生成します。

ブートした CPU は、低位メモリにある各 CPU のトランポリンコードのコピーを作成します。APコードはマジックナンバーを自身のコードに埋め込むことで、BPによりそのAPがトランポリンコードを実行してよいかの判断を行わせます。Intel MP仕様によって規定されているため、トランポリンコードは低位メモリに置かれる必要があります。

トランポリンコードは単純に %bx レジスタを 1 にします。そして、プロテクトモードに移り、arch/i386/kernel/head.Sのメインエントリーポイントであるstartup_32 へとジャンプします。

さて、APはhead.Sの実行を開始し、自身がBPではないことに気が付きます。 すると、BSSをクリアするコードの実行をスキップして、initalize_secondary()へと進みます。そして、この CPU はアイドルタスクへと単に進みます。 -- init_tasks[cpu]は、BPがdo_boot_cpu(cpu) を実行したときにすでに初期化されていたことを思い起こしましょう。

ここで、init_taskは共有できますが、各アイドルスレッドで各々 TSS を持たなければならないことに注意しましょう。これは、init_tss[NR_CPUS]が配列になっている理由になっています。

1.8 初期化データおよびコードの解放

オペレーティングシステムが自身を初期化するとき、そのためのほとんどのコードとデータ構造体は二度と使われることはありません。ほとんどのオペレーティングシステム(BSD, FreeBSD etc...)では、この不必要な情報を破棄することができません。すなわち、貴重な物理カーネルメモリを浪費していることになるのです。 彼らの使う言い訳(McKusickの4.4BSD本を参照)は、「関連のコードが各種のサブシステムに広がっており、これらを解放するのは現実的ではない。」ということです。Linux はもちろん、このような言い訳をしません。なぜなら、Linux では、「もしなにかが原理的に可能であれば、それはすでに実装されているか、誰かが作業している」からです。

そして、前の章で述べたように、Linux カーネルはELF バイナリとしてのみコンパイルできることから、私たちにもそれが可能なことが(あるいは、その根拠の一つ)が分かっています。以下のように使われる2つのマクロを Linux が提供しており、初期化コード/データを廃棄することができるようになっています。

これらはinclude/linux/init.hに定義されるgccの属性指示子("gcc magic"としても知られる)を評価します。


#ifndef MODULE
#define __init        __attribute__ ((__section__ (".text.init")))
#define __initdata    __attribute__ ((__section__ (".data.init")))
#else
#define __init
#define __initdata
#endif

これが意味することは、もしコードがカーネルに静的にコンパイルされているなら(つまり MODULEが定義されていなければ)、特殊な ELF セクションのtext.initにこれらのコードが配置されるということです。そしてその配置は、arch/i386/vmlinux.ldsのリンカマップに定義されるのです。 逆に(つまりモジュールであれば)マクロはなにもしないということです。

ブート時に、アドレス__init_beginから__init_endの間の全てのページを解放する、アーキテクチャ特有の関数free_initmem()を、"init"カーネルスレッド(関数init/main.c:init())が呼び出すのです。

(私のワークステーションのような)一般的なシステムでは、この結果、約260Kのメモリの解放になります。

module_init()によって登録された関数は、.initcall.initに配置され、静的にリンクされていたときには同様に解放されます。Linux 開発の現在の方向性では、将来問題のサブシステムが必要に応じてモジュール化できるように、(当初モジュールの必要性のない)サブシステムの場合にも、デザインの初期段階から、init/exit エントリーポイントが提供されるようにしています。fs/pipe.cのpipefsが、このよい例です。たとえ、bdflush (fs/buffer.c参照)のように、あるサブシステムが決してモジュールになることがなく、その時点でその関数を呼ぶことが重要でないとしても、それでも初期化関数としてmodule_init()マクロを使うことはよいことです。

さらに同じような使い方をする__exit__exitdataという名前の2つのマクロがあります。しかしこれらは、モジュールサポートにより直接的につながるため、後のセクションにて説明します。

1.9 カーネルコマンドラインを処理する

ここで、ブート時にカーネルに渡されたコマンドラインに何が起こるかを考えてみましょう。

  1. LILO(ないしはBCP) は BIOS のキーボードサービスを使って、コマンドラインを受け取る。そして、物理メモリの既定の位置に、そこに有効なコマンドラインがあることを示す印といっしょに格納する。
  2. arch/i386/kernel/head.S は自身の最初の 2k をゼロページの外へとコピーする。
  3. (start_kernel() から呼ばれた setup_arch()から呼び出される) arch/i386/kernel/setup.c:parse_mem_cmdline()は、ゼロページから 256 バイトを /proc/cmdline で表示されるのと同じ saved_command_line へとコピーする。これと同じルーチンは、"mem=" オプションがもしあれば処理し、VM パラメータを適切に調整する。
  4. 話を(start_kernel()から呼ばれる) parse_options() のコマンドラインに戻すと、この関数は「カーネル内部」パラメータ(現時点では、"init="と init の環境変数、引数)も処理し、各ワードをchecksetup()へと渡す。
  5. checksetup() は ELF セクション.setup.initのコードについて、そこの各関数を起動し、コマンドラインがマッチしていた時は、コマンドラインのワードを渡していく。ここで、__setup()で登録された関数からの返り値が 0 のときは、同じ"variable=value" を一つ以上の関数へ渡せ、その"value"が一方の関数では無効で、他方では有効であるということを表している。 Jeff Garzikは、「こういうことをするハッカーはおしりペンペンだ :) 」とコメントしている。なぜか? これは明らかに ID の順序に特有で、つまりある順序でリンクされたカーネルは関数Aを関数Bの前に起動するが、そうでない場合は、逆の順序になり結果が順序に依存してしまうからだ。

さて、ブートコマンドラインを処理するコードはどのようになっているのでしょうか。include/linux/init.hで定義される __setup() マクロを使います。



/*
 * Used for kernel command line parameter setup
 */
struct kernel_param {
        const char *str;
        int (*setup_func)(char *);
};

extern struct kernel_param __setup_start, __setup_end;

#ifndef MODULE
#define __setup(str, fn) \
   static char __setup_str_##fn[] __initdata = str; \
   static struct kernel_param __setup_##fn __initsetup = \
   { __setup_str_##fn, fn }

#else
#define __setup(str,func) /* nothing */
endif

次に、実際のコードでの典型的な使い方は以下のようになります (実際のドライバの BusLogic HBA drivers/scsi/BusLogic.cから引用)。


static int __init
BusLogic_Setup(char *str)
{
        int ints[3];

        (void)get_options(str, ARRAY_SIZE(ints), ints);

        if (ints[0] != 0) {
                BusLogic_Error("BusLogic: Obsolete Command Line Entry "
                                "Format Ignored\n", NULL);
                return 0;
        }
        if (str == NULL || *str == '\0')
                return 0;
        return BusLogic_ParseDriverOptions(str);
}

__setup("BusLogic=", BusLogic_Setup);

ここで __setup()はモジュールに対しては何もしません。そしてモジュールであれ静的にリンクされているのであれ、ブートコマンドラインを処理したいコードでは、モジュールの初期化ルーチンにおいて、自身の解析関数を持っていてそれを起動しなければならないからです。これは、静的にコンパイルされる時などだけでなく、モジュールとしてコンパイルされる時にも、パラメータを処理するコードを書くことができるという事でもあります。


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