RHEL 5 と KMP (Kernel Module Package)

カーネルのバージョンが上がる度にカーネルモジュールもビルドしなきゃいけない,というのはめんどくさい。

しかも。

バイナリパッケージでカーネルモジュールをインストールしているとする。

このカーネルモジュールがシステム動作の上で必須だとすると,新しいカーネル(本体)がリリースされても,それに対応するバージョンのカーネルモジュールがリリースされない限り新しいカーネルにアップデートすることができない。

これは不便。そして危険。

なので,RHEL 5 から KMP (Kernel Module Package) という新しいカーネルモジュールのパッケージング方式がでてきた。


たとえば,centosplus のバイナリパッケージ(IIJ さんのミラー)で XFS のカーネルモジュールのパッケージを見ると,

のようにいくつかある。

注目すべきポイントは,0.4-1 系統については,カーネルのバージョンが上がる度に新しいモジュールがリリースされているのに対して,0.4-2 系統は一つしかない(バージョンにカーネルバージョンが含まれていない)。0.4-1 は旧来のカーネルモジュールパッケージでパッケージングしていたのにたいして,0.4-2 では KMP に対応したパッケージングを行っているから。

なので,今後カーネルのリビジョンがあがろうとも,同一のカーネルモジュールパッケージ(kmod-xfs-0.4-2)をインストールしたままできちんと動く。


じゃあ,KMP 対応のカーネルモジュールパッケージには,カーネルのバージョン番号は含まれていないのか。

$ rpm2cpio kmod-xfs-0.4-2.i686.rpm | cpio -t

./lib/modules/2.6.18-92.1.13.el5
./lib/modules/2.6.18-92.1.13.el5/extra
./lib/modules/2.6.18-92.1.13.el5/extra/xfs
./lib/modules/2.6.18-92.1.13.el5/extra/xfs/xfs.ko
1064 blocks

このように含まれてしまってる。でも,これでもきちんと動くしくみが用意されている(後述)。

KMP と DKMS の違い

上記の問題点に対処する方法は KMP だけではない。一番有名なのは DKMS というしくみ。

以下,簡単な比較。

  • DKMS
    • カーネル本体がアップデートするたびに,自動的にソースからカーネルモジュールをビルドする
    • よってカーネルのバイナリインタフェース(kABI)が変更になっても,(ソースからビルドできる限り)DKMS 対応カーネルモジュールパッケージを更新する必要はない
    • そのかわり,カーネルビルドに必要な環境(gcc や kernel-devel パッケージ)が必要になる(と思う)
    • さまざまなディストリビューションdebian, Ubuntu, RHEL)に対応したフレームワークである
    • ただ,RHEL(および CentOS)の場合,DKMS 本体のパッケージを EPEL や RPMForge など外部レポジトリからとってこないといけない
  • KMP
    • カーネルバイナリインタフェース(kABI)が変更にならない限り,カーネルモジュールを再ビルドする必要はない(同一バイナリを使いまわす)
    • よって gcc や kernel-devel は必要とはならない
    • RHEL のように,カーネルのマイナーバージョン(2.6.18 など)が不変の環境ではこの条件で十分
    • ただし,RHELCentOS)や Fedora でしかサポートされていない(と思う)

kmodtool とは

後述するように,あるアーキテクチャの CPU に対応したカーネルでも,さまざまなバリエーション(PAE とか xen とか)が存在する。カーネルモジュールパッケージでは,これらのすべてのバリエーションに対応した単一の SPEC ファイルを書くこととなる。

しかし,そうすると %package -n ディレクティブを指定した SPEC 記述子を各バリエーションごとにたくさん書かなきゃいけない。それは非現実的。そこで,カーネルモジュールパッケージでは,kmodtool というヘルパスクリプトを利用して SPEC ファイルを書くことになっている。

基本的なカーネルモジュール用 SPEC ファイルでは,下記のような記述がある。

Source10: kmodtool-%{kmod_name}

# ...... snip snip snip ......

# Magic hidden here.
%define kmodtool sh %{SOURCE10}
%{expand:%(%{kmodtool} rpmtemplate_kmp %{kmod_name} %{kversion} %{kvariants} 2>/dev/null)}

Magic hidden here 以下の部分で,kmodtool コマンドを呼び出して展開しているのがわかる。


kmodtoolredhat-rpm-config パッケージがインストールされていれば,/usr/lib/rpm/redhat/ ディレクトリにインストールされている。(本来 SOURCES/ ディレクトリにコピーしてそれを使うべきなんだけど)直接叩いてみる。

$ /usr/lib/rpm/redhat/kmodtool --help

Error: Unknown option '--help'.
You called: kmodtool --help

Usage: kmodtool <command> <option>+
 Commands:
  verrel <uname>                               
    - Get "base" version-release.
  variant <uname>                               
    - Get variant from uname.
  rpmtemplate <mainpgkname> <uname> <variants> 
    - Return a template for use in a source RPM
  rpmtemplate_kmp <mainpgkname> <uname> <variants>
    - Return a template for use in a source RPM with KMP dependencies
  version  
    - Output version number and exit.

いくつかコマンドがあるけど,重要なのは rpmtemplate* というターゲット。これでさっきいった SPEC ファイルのテンプレート断片を生成することができる。

実際に生成されるテンプレート断面をみてみよう。

$ /usr/lib/rpm/redhat/kmodtool rpmtemplate_kmp foo `uname -r` ""

foo というのはダミーのカーネルモジュール名。`uname -r`カーネル本体のバージョン番号を指定している。最後の「""」というところで,「バリエーション」を指定している。

これを実行すると,標準出力に SPEC ファイルの断片が出力される。

%package       -n kmod-foo
Summary:          foo kernel module(s)
Group:            System Environment/Kernel
%global _use_internal_dependency_generator 0
Provides:         kernel-modules = 2.6.18-128.2.1.el5
Provides:         foo-kmod = %{?epoch:%{epoch}:}%{version}-%{release}
Requires(post):   /sbin/depmod
Requires(postun): /sbin/depmod
BuildRequires: kernel-devel-%{_target_cpu} = 2.6.18-128.2.1.el5
%description   -n kmod-foo
This package provides the foo kernel modules built for the Linux
kernel 2.6.18-128.2.1.el5 for the %{_target_cpu} family of processors.

...... snip snip snip ......

今回はバリエーションとして「""」(空文字列)を指定したからこのように単一の RPM の SPEC が生成されたけど,バリエーションに複数指定することで %package -n の値が異なる複数のテンプレート断片が出力される。

rpmtemplaterpmtemplate_kmp の違い

ちなみに,先ほどコマンドとして指定した rpmtemplate_kmp は KMP 版の SPEC ファイルテンプレートを生成するという意味になる。似たような rpmtemplate というのは旧来のカーネルモジュールパッケージ用の SPEC ファイルテンプレートだ。

両者の違いをみると,KMP 版の SPEC ファイルがどのようなことをしているかわかる。

$ diff -u foo.legacy foo.kmp
--- foo.legacy  2009-07-28 12:06:18.000000000 +0900
+++ foo.kmp     2009-07-28 12:06:25.000000000 +0900
@@ -1,9 +1,9 @@
 %package       -n kmod-foo
 Summary:          foo kernel module(s)
 Group:            System Environment/Kernel
+%global _use_internal_dependency_generator 0
 Provides:         kernel-modules = 2.6.18-128.2.1.el5
 Provides:         foo-kmod = %{?epoch:%{epoch}:}%{version}-%{release}
-Requires:         kernel-%{_target_cpu} = 2.6.18-128.2.1.el5
 Requires(post):   /sbin/depmod
 Requires(postun): /sbin/depmod
 BuildRequires: kernel-devel-%{_target_cpu} = 2.6.18-128.2.1.el5
@@ -14,8 +14,21 @@
 if [ -e "/boot/System.map-2.6.18-128.2.1.el5" ]; then
     /sbin/depmod -aeF "/boot/System.map-2.6.18-128.2.1.el5" "2.6.18-128.2.1.el5" > /dev/null || :
 fi
+
+#modules=( $(rpm -ql kmod-foo | grep '\.ko$') )
+modules=( $(find /lib/modules/2.6.18-128.2.1.el5/extra/foo           | grep '\.ko$') )
+if [ -x "/sbin/weak-modules" ]; then
+    printf '%s\n' "${modules[@]}"     | /sbin/weak-modules --add-modules
+fi
+%preun         -n kmod-foo
+rpm -ql kmod-foo | grep '\.ko$'     > /var/run/rpm-kmod-foo-modules
 %postun        -n kmod-foo
 /sbin/depmod -aF /boot/System.map-2.6.18-128.2.1.el5 2.6.18-128.2.1.el5 &> /dev/null || :
+modules=( $(cat /var/run/rpm-kmod-foo-modules) )
+#rm /var/run/rpm-kmod-foo-modules
+if [ -x "/sbin/weak-modules" ]; then
+    printf '%s\n' "${modules[@]}"     | /sbin/weak-modules --remove-modules
+fi
 %files         -n kmod-foo
 %defattr(644,root,root,755)
 /lib/modules/2.6.18-128.2.1.el5/

おおきな違いは,以下の2点。

  • KMP 版では,kernel に対する Requires がなくなっている
    • なのでカーネル本体のバージョンに束縛されない
  • KMP 版では %post%preun, %postun 時に /sbin/weak-modules というコマンドを呼び出してなんかやっている


/sbin/weak-modules というのはシェルスクリプトなので,一読してみるといい。

これがやっていることをおおまかにいうと,

  • カーネルのシンボルテーブルを調べて,互換性のあるバージョンかどうかを調べる(kABI)
  • 互換性がある場合,/lib/modules/VERSION/weak-updates/ ディレクトリにカーネルモジュールのリンクを貼る

なお,上記 SPEC template だと,この KMP カーネルモジュールパッケージのインストール・アンインストール時にのみ /sbin/weak-updates を呼び出しているみたいだけど,おそらく,カーネル本体のアップデート時にも呼び出されているんだと思う。

これによって,カーネルのバージョンがあがったときでも,もともとインストールしてあった KMP カーネルモジュールをロードできるようにしている。

Tips

RHEL (CentOS) では i686 カーネルモジュールビルド時は --target=CPU を付ける
$ rpmbuild -bb SPEC/kmod-foo.spec

ってやると,kernel-devel.i386 が見つからないよって怒られる。

なので,

$ rpmbuild -bb --target=i686 SPEC/kmod-foo.spec

のように,--target オプションで i686 を明示的に指定する。x86_64 の場合は指定しなくて大丈夫なんじゃないかな。

i686 の場合,(自分さえ都合がよければ)--define 'kvariants ""' を付ける

じゃあってんで i686

$ rpmbuild -bb --target=i686 SPEC/kmod-foo.spec

ってやると,(PAE カーネルじゃない場合)kernel-PAE-devel や kernel-xen-devel がないよって怒られる。

これは,(お手本に沿って書かれた spec では)対応するアーキテクチャのすべてのバリエーション(i686 だと PAE とか,x86_64 でも xen とか,sparc だと smp とか)のパッケージを生成しようとするから。

kvariants という変数を指定することで,ビルドするバリエーションを限定できる。

$ rpmbuild -bb --target=i686 --define 'kvariants ""' SPEC/kmod-foo.spec

とすると,無印版のカーネルに対応したモジュールだけビルドする。

あるいは,たとえば,無印版と xen に対応したモジュールをビルドしたいなら,

$ rpmbuild -bb --target=i686 --define 'kvariants "" xen' SPEC/kmod-foo.spec

のようにする。

KMP 対応の SPEC の書き方

HowTos/BuildingKernelModules - CentOS Wiki などのチュートリアルに沿って SPEC を書けばよい。

その上で,上記 kmodtool や KMP の知識があればいうことはない。

また,既存の KMP 対応モジュール(RHEL 本体に含まれるものや centosplus や CentOS extra に含まれるもの)の SPEC ファイルも参考になる。

kmodtool をどのようにソースパッケージ(SRPM)に含めるか

前述したように,redhat-rpm-config パッケージをインストールしてあれば((EPEL の rpmdevtools パッケージをインストールしてれば依存関係のおかげでたぶん同時にインストールされていると思う。)),kmodtool/usr/lib/rpm/redhat/ 以下にインストールされている。

だから SRPM に含めずにそれを使ってもよさそうなんだけど,SOURCES/ ディレクトリにコピーしておいて,それを利用するのが慣例になっているみたい。

その理由は,第一には redhat-rpm-config がアップデートした際に kmodtool も置き換わってしまうので,非互換になってしまう可能性があるからだと思う。

第二の理由は,自分が作成したい KMP カーネルモジュールが他のモジュールに依存している場合,つまり Requires: を書きたい場合は,SPEC ファイル本体でなく kmodtool の生成スクリプト部分をいじる必要があるから。

その場合は HowTos/BuildingKernelModules - CentOS Wiki に書かれていることを参考にする。また,その文書だと,kmodtool を,たとえば kmodtool-foo のようにリネームして使ったほうがよいと書いてある。他の KMP な SRPM で提供される kmodtool とのバッティングを防ぐには,たしかにそのようにしたほうがよい((しかし,個人的には kmodtool という名前そのもので利用しているカーネルモジュールパッケージしか見たことがない。もちろんプライベートコピーを作ったほうがよいのはいうまでもない。))。

KMP の場合の kversion のデフォルト値はどうするか

HowTos/BuildingKernelModules - CentOS Wiki のお手本だと,カーネルバージョンの指定部分は下記のようになっている。

# If kversion isn't defined on the rpmbuild line, build for the current kernel.
%{!?kversion: %define kversion %(uname -r)}

つまり,rpmbuild 実行時にコマンドラインで指定されていなかった場合,uname -r で指定されたカーネルバージョン――すなわち現在動いているカーネルのバージョン――になる。


自分のためにパッケージを作っているのなら,それでも構わないだろう。でも,もし汎用性のあるカーネルモジュールを作りたいのなら,RHEL 5.0 初期リリース時点のカーネルバージョン

%{!?kversion: %define kversion '2.6.18-8'}

や,(現行の最新アップデートである)RHEL 5.3 時点の初期リリース時のバージョン

%{!?kversion: %define kversion '2.6.18-128'}

を指定したほうがいい気がする。もちろん対応するバージョンの kernel-devel をインストールしておかなきゃいけないけど。

参考文献