ソースコード・リテラシーのススメ

第12回システム起動用のスクリプトを読む

前回紹介したように、シェルスクリプトは現在でもUNIX/Linux環境の基盤技術としてさまざまな場面で利用されています。なかでもシステム起動時の処理はシェルスクリプトの独擅場といってもいいでしょう。今回はこのシステム起動時に働くシェルスクリプトを読んでみましょう。

システムの起動、終了の仕組みは、パッケージ管理システムと共にディストリビューションの個性が反映される部分であり、関係するスクリプトの種類や構造などはディストリビューションごとに大きく異なっています。そのため、今回はFedora Core 9のβ版の環境を取りあげることにしました。Fedora Coreのシリーズの中ではスクリプトの機能や配置などの概要はそれほど変っていないとは思いますが、細部はバージョンによって多少異なるかも知れません。

Fedora Core の起動用スクリプトの構成

Fedora Coreに代表されるRed Hat Linux系のディストリビューションでは、システム起動時に働くスクリプト群は /etc/rc.d/以下に集められています。

[kojima@localhost $ ls -F /etc/rc.d/
init.d/  rc*  rc.local*  rc.sysinit*  rc0.d/  rc1.d/  rc2.d/  rc3.d/  rc4.d/  rc5.d/  rc6.d/

Linuxの起動の仕組みについては回を改めて紹介するつもりですが、Fedora Coreが採用している sysvinit形式では、各種サービスの起動/終了スクリプトは/etc/rc.d/init.d/ディレクトリに集めて、そこからランレベルに応じたrc0.d/からrc.6.d/のディレクトリにシンボリックリンクを配置する、という構成になっています。これらのスクリプトはそれぞれのソフトウェアごとのrpmパッケージに収録されており、それらのパッケージをインストールすることで /etc/rc.d/init.d/ディレクトリに追加されます。

[kojima@localhost$ ls -F /etc/rc.d/init.d/
 NetworkManager*            cups*                isdn*           nscd*         sendmail*
 NetworkManagerDispatcher*  cups-config-daemon*  kerneloops*     ntpd*         setroubleshoot*
 acpid*                     dund*                killall*        ntpdate*      single*
 ...
 crond*                     irqbalance*          nfslock*        saslauthd*

一方、/etc/rc.d/の直下にあるrcやrc.local、rc.sysinitは特定のランレベルではなくシステム全体を管理するためのスクリプトです。rcは指定したランレベルに応じて必要なスクリプトを走らせる機能を、rc.localは汎用的な設定には馴染まないそのシステム固有の設定処理を、rc.sysinitはシステムの起動時に一度だけ実行される処理を、それぞれ担っています。

これらの中から、今回はシステム起動時に最初に実行されるrc.sysinitスクリプトを眺めてみます。ただしrc.sysinitは900行近くある大きなスクリプトなので、要点だけを摘み読みすることにします。

必須のファイルシステムのマウント

まずは先頭部分です。

  1  #!/bin/bash
  2  #
  3  # /etc/rc.d/rc.sysinit - run once at boot time
  4  #
  5  # Taken in part from Miquel van Smoorenburg's bcheckrc.
  6  #
  7  
  8  HOSTNAME=`/bin/hostname`
  9  HOSTTYPE=`uname -m`
 10  unamer=`uname -r`
 11  
 12  set -m
 13  
 14  if [ -f /etc/sysconfig/network ]; then
 15      . /etc/sysconfig/network
 16  fi
 17  if [ -z "$HOSTNAME" -o "$HOSTNAME" = "(none)" ]; then
 18      HOSTNAME=localhost
 19  fi
 20  
 21  if [ ! -e /proc/mounts ]; then 
 22          mount -n -t proc /proc /proc
 23          mount -n -t sysfs /sys /sys >/dev/null 2>&1
 24  fi
 25  if [ ! -d /proc/bus/usb ]; then
 26          modprobe usbcore >/dev/null 2>&1 && mount -n -t usbfs /proc/bus/usb /proc/bus/usb
 27  else
 28          mount -n -t usbfs /proc/bus/usb /proc/bus/usb
 29  fi
 30   
 31  . /etc/init.d/functions

1行目はシェルスクリプトのお約束であるシェバン、2行目から6行目はコメントで、実際の処理を行うのは8行目からです。8行目から10行目ではHOSTNAME、HOSTTYPE、unamerという変数にそれぞれ/bin/hostnameuname -m(CPUの種類⁠⁠、uname -r(カーネルのリリース番号)の各コマンドを実行した結果を設定しています。前回も述べましたが、シェルスクリプトの中ではシングルクォート('⁠⁠、ダブルクォート("⁠⁠、バッククォート(`)の3種の引用符が使い分けられ、バッククォートの部分はコマンドとして実行した結果に展開されます。

12行目のsetはbashの動作を制御するためのコマンドで、set -mはジョブ制御(指定したジョブを一時停止したり、バックグラウンドで実行させる機能)を有効にする指定です。このスクリプトは一部のジョブをバックグラウンドで実行するような処理をするため、この設定を明示しているようです。

14行目から16行目では/etc/sysconfig/networkというネットワーク関係の設定ファイルの有無を調べ(-f⁠⁠、このファイルが存在すれば.(ピリオド)コマンドで読み込ませます。手元の環境では/etc/sysconfig/networkは以下のような内容になっていて、rc.sysinitを実行しているシェル環境の中にNETWORKINGという変数とHOSTNAMEという変数を追加(HOSTNAMEという変数は8行目で設定しているので上書き)することになります。

 NETWORKING=yes
 HOSTNAME=localhost.localdomain

.(ピリオド)はbashの内部コマンドの一つで、指定したファイルを読み込んで、現在のシェル環境の中で実行します。. はsourceとも書くことできます。

17行目から19行目では、$HOSTNAMEという変数に何らかの値が設定されているかを改めてチェックして、設定されていなければlocalhostという値を設定しています。

21行目から24行目では、/proc/mountsというファイルが存在するかをチェック-eして、存在しなければ/proc/sysをマウントしています。

25行目から29行目も同様に/proc/bus/usbというディレクトリの存在をチェック-dし、存在しなければusbcoreというモジュールをロードした上でUSBファイルシステムという仮想ファイルシステムを/proc/bus/usbにマウントしています。

31行目は15行目と同様 .(ピリオド)コマンドを使って/etc/init.d/functionsというファイルを読み込んでいます。/etc/init.d/functionsには/etc/rc.d/以下にあるシェルスクリプト群が使う関数をまとめて定義してあり、このファイルを読み込めばそれぞれのスクリプトごとに関数を定義する手間を省けるようになっています。

バナーメッセージの表示

この先の32行目から230行目あたりはセキュリティを強化するSELinuxや、ファイルシステムを暗号化するcrypto-loopの設定で、シェルスクリプトというよりはそれぞれのシステムの専門的な話になるため割愛し、232行目からを眺めてみます。

232  # Print a text banner.
233  echo -en $"\t\tWelcome to "
234  read -r redhat_release 235  if [[ "$redhat_release" =~ "Red Hat" ]]; then
236   [ "$BOOTUP" = "color" ] && echo -en "\\033[0;31m"
237   echo -en "Red Hat"
238   [ "$BOOTUP" = "color" ] && echo -en "\\033[0;39m"
239   PRODUCT=`sed "s/Red Hat \(.*\) release.*/\1/" /etc/redhat-release`
240   echo " $PRODUCT"
241  elif [[ "$redhat_release" =~ "Fedora" ]]; then
242   [ "$BOOTUP" = "color" ] && echo -en "\\033[0;34m"
243   echo -en "Fedora"
244   [ "$BOOTUP" = "color" ] && echo -en "\\033[0;39m"
245   PRODUCT=`sed "s/Fedora \(.*\) \?release.*/\1/" /etc/redhat-release`
246   echo " $PRODUCT"
247  else
248   PRODUCT=`sed "s/ release.*//g" /etc/redhat-release`
249   echo "$PRODUCT"
250  fi

この部分は/etc/redhat-releaseというファイルを$redhat_releaseという変数に読み込んで、その内容に応じて、メッセージを"Welcome to Red Hat"か"Welcome to Fedora"に切り替える処理になっています。

echo -en "\\033[0;31m"の部分は、エスケープシークエンスと呼ばれるコンソール画面の表示色の指定です。ここではRed HatやFedoraという名前を強調するために文字の色を赤(31)か青(34)に変更し、39で元に戻しています。

236行目に見られる$BOOTUPは/etc/init.d/functions経由で読み込まれる/etc/sysconfig/initに設定されている変数です。/etc/sysconfig/initには$BOOTUP以外にもさまざまな変数が設定されており、それらを使って起動時の振舞を細かく調節できるようになっています。

モジュールドライバの読み込み

256行目くらいからは雑多な起動時処理という感じですが、ざっと眺めて行きます。

256  # Fix console loglevel
257  if [ -n "$LOGLEVEL" ]; then
258          /bin/dmesg -n $LOGLEVEL
259  fi
260  
261  # Only read this once.
262  cmdline=$(cat /proc/cmdline)
263  
264  # Initialize hardware
265  if [ -f /proc/sys/kernel/modprobe ]; then
266     if ! strstr "$cmdline" nomodules && [ -f /proc/modules ] ; then
267         sysctl -w kernel.modprobe="/sbin/modprobe" >/dev/null 2>&1
268     else
269         # We used to set this to NULL, but that causes 'failed to exec' messages"
270         sysctl -w kernel.modprobe="/bin/true" >/dev/null 2>&1
271     fi
272  fi
273  
 ……
290  /sbin/start_udev
291  
292  # Load other user-defined modules
293  for file in /etc/sysconfig/modules/*.modules ; do
294    [ -x $file ] && $file
295  done
296  
297  # Load modules (for backward compatibility with VARs)
298  if [ -f /etc/rc.modules ]; then
299          /etc/rc.modules
300  fi

256行目から259行目は$LOGLEVELという変数が設定されているかを調べて、設定されていれば/bin/dmesg -n $LOGLEVELというコマンドを実行します。$LOGLEVELという変数も236行目にあった$BOOTUP同様、/etc/sysconfig/initの中で定義されています。

/bin/dmesg-nオプションはカーネルのログメッセージのうち、コンソール画面に出力されるメッセージレベルを指定するもので、0から7の数字を指定でき、数字が大きくなるほど詳細なメッセージがコンソール画面に出力されるようになります。

262行目は$( .. )内のコマンドの結果を$cmdlineという変数に代入する処理で、右辺は`cat /proc/cmdline`と同じです。/proc/cmdlineは起動時にブートローダからカーネルに与えられるパラメータを記録しているファイルで、これらの情報はルートファイルシステムの位置やマウントオプション、カーネルの機能やシステムの動作を調整するために利用されます。

264行目以降はモジュールドライバを読み込んで各種ハードウェアを利用可能にするための処理です。267行目で実行しているsysctlコマンドは/procファイルシステム経由でカーネルの動作を変更するコマンドで、ここではカーネルが必要なモジュールを組み込む際に利用するコマンドの指定であるkernel.modprobe/sbin/modprobeを設定しています。もし起動時カーネルパラメータとしてnomodulesが指定されると270 行目のようにkernel.modprobeに/bin/trueが設定され、カーネル自身によるモジュールの読み込み処理はできなくなります。

少し飛ばして、290行目で実行している/sbin/start_udevは、最近のLinuxで採用されているudev機能を開始するコマンドです。udevとは、アプリケーションがハードウェアを操作する際に利用するデバイスファイルを動的に生成する仕組みで、udevdというデーモンがカーネルの認識してしているハードウェア情報を利用して、それぞれのハードウェアに応じたデバイスファイルを/dev以下に作っていきます。この/sbin/start_udevは必要な環境を整えてudevdを起動するためのコマンドです。

通常はカーネルが自動認識して組み込むモジュールドライバだけで十分なはずですが、自動的には組み込まれないモジュールドライバを組み込ませたい場合は 293行目に見られるように/etc/sysconfig/modules/の下にmy.modulesのようなファイルを用意して、そこで必要なモジュールを組み込む指定をしておけば、rc.sysinitの中で組み込んでくれます。また、/etc/rc.modulesというファイルでモジュールを組み込むことも可能です。

対話モードの確認とrc.sysinitの終了

この後、Red Hat系Linuxに独自のrhgbというコマンドを起動してXウィンドウを用いた綺麗な画像を表示し、ユーザにはプログレスバーによる進捗表示だけを示しながら、処理は背後で進めていきます。rc.sysinitの中ではLVMを用意したり、ファイルシステムのチェックをしたりと大忙しになりますが、それらの部分はばっさりと飛ばして、最後の部分だけを見てみます。

826  # Now that we have all of our basic modules loaded and the kernel going,
827  # let's dump the syslog ring somewhere so we can find it later
828  [ -f /var/log/dmesg ] && mv -f /var/log/dmesg /var/log/dmesg.old
829  dmesg -s 131072 > /var/log/dmesg
830  
831  # create the crash indicator flag to warn on crashes, offer fsck with timeout
832  touch /.autofsck &> /dev/null
833  
834  if [ "$PROMPT" != no ]; then
835      while :; do
836          pid=$(/sbin/pidof getkey)
837          [ -n "$pid" -o -e /var/run/getkey_done ] && break
838          usleep 100000
839      done
840      [ -n "$pid" ] && kill -TERM "$pid" >/dev/null 2>&1
841  fi
842  } &
843  if strstr "$cmdline" confirm ; then
844          touch /var/run/confirm
845  fi
846  if [ "$PROMPT" != "no" ]; then
847          /sbin/getkey i && touch /var/run/confirm
848          touch /var/run/getkey_done
849  fi
850  wait
851  [ "$PROMPT" != no ] && rm -f /var/run/getkey_done
852  
853  # Let rhgb know that we're leaving rc.sysinit
854  if [ -x /usr/bin/rhgb-client ] && /usr/bin/rhgb-client --ping ; then
855      /usr/bin/rhgb-client --sysinit
856  fi
857  

このあたりまでにファイルシステムのチェックとマウントが終了し、各種ログファイルの初期化やスワップファイルの設定なども済ませて、ほぼ基本的なコマンド類は利用可能になっています。

828、829行目ではすでに同じ名前のファイルがあれば別名で保存した上で、dmesgコマンドでここまでのカーネルのログを/var/log/dmesgに保存しています。

832行目では/.autofsckという隠しファイルを作ります。このファイルは、haltやshutdownコマンドによりシステムが正常に終了した場合には削除されるようになっており、もし起動時にこのファイルが残っていれば、何らかの理由で前回は正常終了していないことから、ファイルシステムをチェックした方がいいことがわかります。今回は省略しましたが、rc.sysinitの375行目からがそのためのチェックになっています。

834行目から851行目はキーボードからの入力をチェックする部分です。今回読んでいるrc.sysinitを採用しているRed Hat系のディストリビューションでは、rc.sysinitの動作中にiを入力すると、rc.sysinit以降で各種サービスを起動するかどうかを手動で指定できるようになっており、この部分がそのためのチェックになっています。

具体的には847行目の/sbin/getkey iでiが入力されたかを調べ、入力されていれば/var/run/confirmというファイルを作り、848行目で/var/run/getkey_doneを作成して入力チェックが終わったことを他のプロセスに知らせます。作成された/var/run/confirmファイルは、rc.sysinitの次に起動されるスクリプトから参照され、起動すべきサービスごとに確認を求めるようになります。

シェルスクリプト的に見ると、⁠リストを省略した)771行目から842行目が中括弧({ })で囲われたグループコマンドとして別プロセスで実行され(842行目の&⁠⁠、そちらのプロセスと850行目のwaitで同期を取りながらキーボードからの入力チェックを行う、という複雑な作りになっています。このような構成を取った理由はよく分かりませんが、独立性の高い処理を並列化することで処理速度を上げるような意図があるのでしょう。

最後の853行目から856行目は、グラフィカルな画面を表示しているrhgbにrhgb-client経由でrc.sysinitが終了したことを伝えています。このコマンドを受けた rhgb はXウィンドウでのキーボードの設定を行なって対話モードの入力に備えます。

以上、駆け足でrc.sysinitスクリプトを眺めてみました。rc.sysinitが終了すれば、/sbin/initがランレベルを引数にして/etc/rc.d/rcスクリプトを起動し、/etc/rc.d/rcスクリプトがそれぞれのレベルに応じたサービスを起動してシステムの準備を整えていきます。それらの過程については次回にでも読んでみる予定です。

おすすめ記事

記事・ニュース一覧