カーネルモジュール作成によるlinuxカーネル開発入門 - 第一回 hello world

はじめに

本記事の内容はUbuntu16.04でのみ確認しています。同名の電子書籍においてはUbuntu 18.04に対応しています。

本記事の目的は、linuxカーネルモジュール(以下カーネルモジュール)というものの作成を通じてlinuxカーネル(以下カーネル)の開発に最低限必要な知識をつけることです。C言語のポインタがわかる程度の開発スキルがあれば多分読めると思います。

本記事は、過去にセキュリティ&プログラミングキャンプ2010というイベントの中のLinux開発者育成コースにおいて使用した資料を加筆、修正したものです。1つの記事に納めるのは無理がある分量なので、(不定期)連載という形式をとることにしました。

本記事に記載されているコードを実際に試すためには、仮想化機能を持つCPUを搭載したPCにインストールされたUbuntu16.04が必要です。CPUの仮想化機能を持っているどうかは、shellから以下のプログラムを実行すればわかります。1以上の値を出力すれば仮想化機能は有効です。

$ egrep -c '^flags.*(vmx|svm)' /proc/cpuinfo

それに加えてBIOSにおいて仮想化機能が有効になっている必要があります。下記コマンドが成功すればOKです。

IntelのCPU

$ sudo modprobe kvm-intel

AMDのCPU

$ sudo modprobe kvm-amd

失敗する場合はBIOSの設定を見直してください。

本記事に書かれているソースが対応しているのはlinux v4.14までです。

第一回では、そもそもカーネルモジュールとは何かというところから初めて、なぜカーネル開発を始めるためにカーネルモジュール作成という手段をとるかを説明した上で、hello worldを出力するだけの簡単なカーネルモジュールを作成します。

カーネルモジュールとは

カーネルモジュールとは、マシンの起動中にカーネルに機能を追加するための部品です。webブラウザに対するプラグインを思い浮かべてもらえればいいかと思います。カーネルの機能のうちの多くの部分は最初からカーネルに組み込んでおくこともできますし、モジュールとして独立したファイルにしておいて1、必要になった時にカーネルに組み込むこともできます。たとえばみなさんのPCに繋がっている各種デバイスを操作するデバイスドライバなどがそうです。モジュールはカーネル本体と同時にビルドできますし、後から別途個別にビルドもできます。本記事は後者のアプローチをとります。

ディストリビューションカーネルは、カーネルが提供するほとんどのドライバをカーネルモジュールとして提供しています。起動時に読み込むカーネル本体は最小のサイズに抑え、その後でマシンに搭載されているデバイスに関するモジュールだけを必要に応じて読み込みます。これによって、

  • 高速な起動
  • カーネルによるメモリ使用量の最小化
  • なるべく多くのデバイスのサポート

を同時に達成しています。

なぜカーネルモジュールの作成なのか

カーネル開発は、カーネル本体の変更よりも、カーネルモジュールの作成から始めるほうが入門しやすいです。理由は次の通りです。

  • カーネルモジュールの開発に使用する言語(C言語。一部アセンブラ)もAPIカーネル本体と同じ
  • 巨大なカーネル本体に手を入れるより単機能かつ少ないコード量で作れる
  • ビルド時間が短いので手軽に試せる
  • システム全体を意のままにできる万能性、バグがあればシステム全体がパニック/ハングするなどのスリルはカーネル本体とほぼ同じ

準備

開発に必要な環境を用意します。まずはシステムに必要パッケージをインストールします。

$ sudo apt-get install git vagrant libvirt-bin libvirt-dev kernel-package qemu-kvm libssl-dev libncurses5-dev
$ sudo usermod -aG libvirtd <your user name>

ここでいったんログアウト&ログインします。

$ sudo sed -i'' "s/Specification.all = nil/Specification.reset/" /usr/lib/ruby/vendor_ruby/vagrant/bundler.rb         # See https://github.com/vagrant-libvirt/vagrant-libvirt/issues/575 for more details about this patching
$ vagrant plugin install vagrant-libvirt

次に開発環境を作成します。

$ git clone https://github.com/satoru-takeuchi/elkdat.git
Cloning into 'elkdat'...
$ git checkout v0.3
...
$ cd elkdat
$ ./init
...
$ 

ここまでで、カーネル開発に必要な資材(ソフトウェア、ライブラリ、カーネルソース)が全て揃いました。自作カーネル、およびカーネルモジュールをテストするためのVMも作成済です。

もし開発環境が不要になったら以下のコマンドを実行してください。

$ ./fini                                                                        
...                                                                             
Finished to cleanup.                                                            
Now it's safe to delete the source directory.                                   
$ 

本記事ではカーネルv4.9を対象に開発をします。このため、このカーネルをビルドしてVM上でブートさせます。今回のトピックはカーネルモジュールの作成なので、カーネル自体は変更せず、upstreamのものをそのまま利用します。

$ cd linux
$ git checkout v4.9
$ cd ../
$ ./test boot
...
*******************************************
*******************************************
KTEST RESULT: TEST 1 SUCCESS!!!!         **
*******************************************
*******************************************

    1 of 1 tests were successful

$ 

VMにログインして、v4.9でブートできているか確認します。

$ cd elkdat
$ vagrant ssh
...
vagrant@packer-qemu:~$ uname -r
4.9.0-ktest
vagrant@packer-qemu:~$ exit
...
$ ../
$ 

uname -rとは現在のカーネルバージョンを確かめるためのコマンドです。ちゃんと4.9が動作していることがわかります2

では次の節で実際にカーネルモジュールを作ってみましょう。

hello world カーネルモジュールの作成

まずは開発用のディレクトリを作成して、そこに移動します。

$ mkdir -p dev/module/hello
$ cd dev/module/hello
$ 

以下のようなファイルを作成します。これがカーネルモジュールのソースコードです。

#include <linux/module.h>

MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("Satoru Takeuchi <satoru.takeuchi@gmail.com>");
MODULE_DESCRIPTION("Hello world kernel module");

static int mymodule_init(void) {
        printk(KERN_ALERT "Hello world!\n");
        return 0;
}

static void mymodule_exit(void) {
        /* Do nothing */
}

module_init(mymodule_init);
module_exit(mymodule_exit);

20行程度の簡単なソースです。これだけでまがりなりにもカーネルの一機能を作成できます。見た目から、なんとなく何をしているのか想像できるかもしれません。

ここで覚えておいてほしいのは次のことです。

  • カーネルモジュールを作成するときは必ずlinux/module.hをincludeする必要がある
  • printk()は、おおよそprintfと同等に扱える。文字列先頭についているKERN_ALERTというのは、メッセージの重要度。今はあまり気にしなくてよい
  • mymodule_init()関数を上記のような引数、戻り値で作成した上でmodule_init()マクロに渡すことによって、このカーネルモジュールのロード時(insmod時)にこの関数が呼ばれる
  • mymodule_exit()関数を上記のような引数、戻り値で作成した上でmodule_exit()マクロに渡すことによって、このカーネルモジュールのロード時(rmmod時)にこの関数が呼ばれる
  • MODULE_LICENSE()マクロ内にライセンスを記載する。とくに理由がなければ"GPL v2"でよい。
  • MODULE_AUTHOR()マクロ内に作者の名前と連絡先となるメールアドレスを記載する。上記の例では筆者のものを使っているが、自分のものと書き換えてよい
  • MODULE_DESCRIPTION()マクロ内に、このモジュールが何をするものなのかという説明を記載する

ビルドするためには以下のようなMakefileの作成が必要です。

.PHONY: all clean install login

obj-m := hello.o

all:
    make -C ../../../output M=$(PWD) modules

clean:
    make -C ../../../linux M=$(PWD) clean

install:
    cp *.ko ../../../elkdat; cd ../../../elkdat; vagrant rsync

login:
    cd ../../../elkdat; vagrant ssh

"obj-m :=" の後にカーネルモジュールのソースコード名の.cを.oに置換したものを記載すれば、当該ファイルをビルドした結果得られるオブジェクトファイル、hello.oをカーネルモジュール化できます。カーネルモジュールは.koという拡張子をもちます。

他の部分については仕組みが複雑な上に知ってもあまり幸せになれないので気にしなくていいです3

ではビルドしましょう。といってもmakeを実行するだけです。

$ make
...
$ 

成功したら、作成したモジュールをVM上にコピーします。

$ make install
...
$ 

カーネルモジュールをロードします。自作カーネルがブートしている状態ではないと失敗しますのでご注意ください。

$ make login
...
vagrant@packer-qemu:~$ sudo su
root@packer-qemu:/home/vagrant# insmod /vagrant/hello.ko
root@packer-qemu:/home/vagrant# lsmod | grep hello
hello                  16384  0
root@packer-qemu:/home/vagrant# 

ロードは成功したようです。失敗した場合は一旦VMから抜けた上で4、次のコマンドを実行すればリカバリできます。その後でまたソースを書き換えて、再度ロードに挑戦してみてください。

$ cd ../../../elkdat
$ vagrant reload
...
$ vagrant ssh
...
vagrant@packer-qemu:~$ sudo su
root@packer-qemu:/home/vagrant# grub-reboot ktest
root@packer-qemu:/home/vagrant# exit
exit
vagrant@packer-qemu:~$ exit
...
$ vagrant reload
...
$ 

さて、成功した場合の続きに戻ります。プログラミングしたとおりにカーネルモジュールのロード時にメッセージが出力されたか確認します。

root@packer-qemu:/home/vagrant# dmesg | tail -3
[  314.198886] random: crng init done
[  516.935519] hello: loading out-of-tree module taints kernel.
[  516.936950] Hello world!
root@packer-qemu:/home/vagrant# 

ちゃんとメッセージが出たようです。成功です。

カーネルモジュールのライセンスなどの情報が正しく設定できているかについても確認しておきましょう。

root@packer-qemu:/home/vagrant# modinfo /vagrant/hello.ko
filename:       /vagrant/hello.ko
description:    Hello world kernel module
author:         Satoru Takeuchi <satoru.takeuchi@gmail.com>
license:        GPL v2
srcversion:     9A88917F1C1411370998811
depends:        
vermagic:       4.9.0-ktest+ SMP mod_unload modversions 
root@packer-qemu:/home/vagrant# 

すべて指定したとおりになっています。成功です。

最後にモジュールのアンロードをしてVMから抜けましょう。

root@packer-qemu:/home/vagrant# rmmod hello
root@packer-qemu:/home/vagrant# exit
exit
vagrant@packer-qemu:~$ exit
logout
...
$ 

ここまでで最も単純なカーネルモジュールの作成は終わりです。

演習問題

  • カーネルモジュールの説明文書を変更する
  • ロード時に出力するメッセージを変更する
  • アンロード時にもメッセージを出力するように変更する
  • 関数名を変更する
  • モジュール名を変更する

どれも「そんなのやらなくてもわかるよ」というくらい簡単に見えますが、意外とやってみると間違えるものです。

おわりに

本書で作成したソースと同じものをexample/module/hello以下に配置しています。みなさんが作成したモジュールがどうしてもうまく動かなければ、みなさんが作成したソースとこれを比較してバグの箇所を特定してください。

次回は、カーネル内において所定の時間後に所定の処理をさせたいときに使う、カーネルタイマーを紹介します。


  1. linux/modules/<uname -rによって得られるカーネルバージョン>/以下に存在する、拡張子が".ko"であるファイルがそうです。

  2. 末尾についている"-ktest"は気にしないでください)。

  3. 興味のあるかたはlinux/Documentation/kbuild以下のファイルを見て下さい。

  4. カーネルパニックが発生した場合は勝手にVMからホストに戻ります。

お手軽Linuxカーネル開発/自動テスト

はじめに

これはLinux Advent Calendar 2016 14日目の記事です。

本記事では、ワンコマンドでお手軽にlinuxカーネル開発/自動テスト環境を構築する方法を紹介します。その後開発、自動テストの流れについてもチュートリアル形式で紹介します。本記事ではelkdat(以下、本ツール)というツールを使用します。本記事の対象読者はバリバリのカーネル開発者だけでなく、自分のカーネルカーネルモジュールを一度作ってみたかった人、開発には興味がないけれどカーネルのテスト(たとえばカーネルのバグの原因となったコミットを突き止めたい)には興味のある人も含みます。

本記事に記載されているコードを実際に試すためには、仮想化機能を持つCPUを搭載したPCにインストールされたUbuntu16.04が必要です。CPUの仮想化機能を持っているどうかは、shellから以下のプログラムを実行すればわかります。1以上の値を出力すればCPUは仮想化機能を持っています。

$ egrep -c '^flags.*(vmx|svm)' /proc/cpuinfo

それに加えてBIOSにおいて仮想化機能が有効になっている必要があります。下記コマンドが成功すればOKです。

IntelのCPU

$ sudo modprobe kvm-intel

AMDのCPU

$ sudo modprobe kvm-amd

失敗する場合はBIOSの設定を見直してください。

本記事に書かれているソースが対応しているのはlinux v4.14までです。

linuxに限らず、カーネルの開発/テストは通常のソフトウェアのそれに比べて遥かに面倒です。理由は次のようなものです。

  • 設定、ビルド、インストール、ビルドしたカーネルを使ってのブートに、独特のお作法を覚える必要がある
  • ビルド時間が長い。とくにdistroのデフォルト設定の場合、数時間を要することもある
  • テスト前後にシステムのリブートが必要
  • バグによる被害が大きい。ブートしない、ブートしてもシステム全体が停止する、データが破壊される、など
  • VMなどの別マシンでテストしようとしても、そのための開発環境を作るのが面倒

本ツールはこれらの問題を解決しワンコマンドで

  1. linuxカーネルのソースを入手(必要なら)
  2. テスト用VMを作成
  3. テスト用の各種設定

などの開発/テスト環境の準備を全て済ませます。環境設定後のビルド、インストール、各種テストなどの処理もワンコマンドでできます。

環境

筆者は Ubuntu 16.04/x86_64 を使用しました。他のdistroでは未検証です。他のdistroをお使いのかたは、必要に応じて適宜読み替えて下さい。

本ツールの使用前に、以下のように必要なパッケージをインストールしておく必要があります。

$ sudo apt-get install git vagrant libvirt-bin libvirt-dev kernel-package qemu-kvm libssl-dev libncurses5-dev
$ sudo usermod -aG libvirtd <your user name>

ここで一旦ログアウト&ログインします。

$ sudo sed -i'' "s/Specification.all = nil/Specification.reset/" /usr/lib/ruby/vendor_ruby/vagrant/bundler.rb         # See https://github.com/vagrant-libvirt/vagrant-libvirt/issues/575 for more details about this patching
$ vagrant plugin install vagrant-libvirt

初期化

$ git clone https://github.com/satoru-takeuchi/elkdat.git
Cloning into 'elkdat'...
...
$ cd elkdat
$ git checkout v0.1
$ ./init
...
$ 

たったこれでカーネルソース、テスト用VMイメージのダウンロード、各種設定などの初期化がすべて完了し、開発/テストの準備が整います。./initの実行中にVMイメージやlinuxカーネルのgitリポジトリをダウンロードしますので、回線速度が遅ければ長時間を要します。

既にカーネルソースをお持ちの方は、./initの実行前にソースをelkdatのソースディレクトリの直下にlinuxという名前で配置してください(シンボリックリンクを張ってもよいです)。本ツールを使用する際には、カーネルソースに対してmake mrproperを実行して既存のビルド済カーネルや.configを削除してしまうのでご注意下さい。

開発環境が不要になったら

以下のコマンドを実行してください。

$ ./fini                                                                        
...                                                                             
Finished to cleanup.                                                            
Now it's safe to delete the source directory.                                   
$ 

チュートリアル

自分でビルドしたカーネルを動かす(ソース改変なし)

本記事執筆時点でリリースされたばかりのlinux v4.9を動かしてみましょう。次のコマンドでビルド、インストール、それを使ってのブートまですべてを実行できます。

$ cd linux
$ git checkout v4.9
$ cd ..
$ ./test boot
...
*******************************************
*******************************************
KTEST RESULT: TEST 1 SUCCESS!!!!         **
*******************************************
*******************************************

    1 of 1 tests were successful

$ 

成功したようです。ではVMにログインして本当に成功しているか確かめてみましょう。

$ cd elkdat
$ vagrant ssh
...
vagrant@packer-qemu:~$ uname -r
4.9.0-ktest
vagrant@packer-qemu:~$ 

uname -rとは現在のカーネルバージョンを確かめるためのコマンドです。ちゃんと4.9が動作していることがわかります。

最後に元のカーネルでブートしなおしましょう。

vagrant@packer-qemu:~$ exit
$ vagrant halt
...
$ vagrant reload
...
$ cd ../
$ 

これでおしまいです。再度自前カーネルで起動したい場合は次のようにします。

$ cd elkdat
$ vagrant ssh -c sudo grub-reboot ktest
...
$ vagrant reload 
...
$ 

上記 git checkout の v4.9 の箇所をお好きなtagやcommit IDに変更することによって、好きなカーネルを使えます。

自分でビルドしたカーネルを動かす(ソース改変あり)

何の変更もしていないものを動かすだけではあまり面白くないので、自分でソースを改変したカーネルを動かしてみましょう。ここでは例として用意したパッチをlinux v4.9に適用したカーネルを作ってみます。このパッチは、起動時に簡単なメッセージをカーネルのログに出力するだけのものです。

まずパッチの中身を見てみましょう。

$ cat example/kernel-patch/first/0001-Print-a-message-on-boot.patch 
From 93cc6bf35ed2850634cb1bcfe621b38d81c6ab25 Mon Sep 17 00:00:00 2001
From: Satoru Takeuchi <satoru.takeuchi@gmail.com>
Date: Wed, 14 Dec 2016 20:42:17 +0900
Subject: [PATCH] Print a message on boot

Signed-off-by: Satoru Takeuchi <satoru.takeuchi@gmail.com>
---
 init/main.c | 1 +
 1 file changed, 1 insertion(+)

diff --git a/init/main.c b/init/main.c
index 2858be7..9736dac 100644
--- a/init/main.c
+++ b/init/main.c
@@ -657,6 +657,7 @@ asmlinkage __visible void __init start_kernel(void)
        }

        ftrace_init();
+       printk("my patch is applied!\n");

        /* Do the rest non-__init'ed, we're now alive */
        rest_init();
--
2.10.2

カーネルを作って起動しましょう。

$ cd linux
$ git checkout -b test v4.9
Switched to a new branch 'test'
$ git am ../example/kernel-patch/first/0001-Print-a-message-on-boot.patch 
Applying: Print a message on boot
$ cd ../
$ ./test boot
...
*******************************************
*******************************************
KTEST RESULT: TEST 1 SUCCESS!!!!         **
*******************************************
*******************************************

    1 of 1 tests were successful

成功したようです。起動したカーネルバージョンを確認しておきましょう。

$ cd elkdat
$ vagrant ssh
...
vagrant@packer-qemu:~$ uname -r
4.9.0-ktest+
vagrant@packer-qemu:~$ 

ちゃんと自前カーネルが動いているようです。

では、カーネルのログに期待したメッセージは出ているか、確認してみましょう。

vagrant@packer-qemu:~$ dmesg | grep "my patch"
[    0.167288] my patch is applied!
$ 

期待したとおり、カーネル起動時に "my patch is applied!" というメッセージが出力されました。

このパッチを起点に、メッセージを変更したり、メッセージを挿入する場所を変えたり(そして挿入する場所を間違えてブートしなくなったり…)、別の関数を使ってみたり、と、いろんなことに挑戦してみてください。どうせ環境を復旧するのは簡単です。

この後、またカーネルをもとに戻しておいて下さい。

カーネルの設定を変更

カーネルの設定を変更するには(たとえばデフォルトではビルドされないファイルシステムを有効にするなど)、./testを実行する前に次のコマンドを実行してください。

$ cp ktest/minconfig{,.bak}
$ cp ktest/minconfig linux/.config
$ cd linux
$ make menuconfig
...                                      # ここで設定を好きに変更
$ mv .config ../ktest/minconfig
$ make mrproper
$ cd ../

変更した設定を使ってビルドしたカーネルがブートしない場合は、ktest/minconfig{.bak,}によって設定ファイルを正しく動作するものに復元してください。カーネルビルドに慣れていない人がカーネルの設定を変更する際のコツは、一気にたくさん変更しないことです。

カーネルの設定についての詳細は後述の参考資料「linux kernelのmakeターゲットについてのあれこれ」をごらんくださ

さらに高度なことをする

上記の独自カーネルのビルド、インストール、ブート以外にも、elkdatにはいくつかのさらに高度な機能があります。

ユーザ定義テストを流す

次のコマンドを実行してください。

$ ./test test <the path of your own test>

以下は独自カーネルをブート後にexample/test/helloというテストを流した例です。

$ ./test test example/test/hello
...
** Monitor flushed **
run test /home/sat/src/elkdat/example/test/hello
/home/sat/src/elkdat/example/test/hello ... [0 seconds] SUCCESS
kill child process 18446
closing!

Build time:   6 minutes 53 seconds
Install time: 8 seconds
Reboot time:  17 seconds
Test time:    1 second



*******************************************
*******************************************
KTEST RESULT: TEST 1 SUCCESS!!!!         **
*******************************************
*******************************************

    1 of 1 tests were successful

$ 

example/test/hello's のログはktest/ktest.logにあります。

以下に、テストを必ず失敗するexample/test/failに置き換えた結果も載せておきます。

$ ./test test example/test/fail
...
** Monitor flushed **
run test /home/sat/src/elkdat/example/test/fail
/home/sat/src/elkdat/example/test/fail ... [0 seconds] FAILED!
CRITICAL FAILURE... test failed
REBOOTING
ssh -i /home/sat/src/elkdat/private_key root@192.168.121.181 sync ... [1 second] SUCCESS
ssh -i /home/sat/src/elkdat/private_key root@192.168.121.181 reboot; ... Connection to 192.168.121.181 closed by remote host.
[0 seconds] SUCCESS
 See /home/sat/src/elkdat/ktest/ktest.log for more info.
test failed
$ 

パッチセットのテスト

みなさんがカーネル開発者ならば、機能拡張やバグ修正をする際にパッチセットをLKMLなどのカーネル開発MLに投稿するでしょう。(当然ながら)パッチセットは投稿前にテストする必要があります。このとき、パッチセット内の全パッチを適用したカーネルをテストするだけでは不十分です。パッチセット内の個々のパッチを全てテストする必要があります。これは、もしいずれかのパッチにバグがあった場合、その後のgit bisectが動作しなくなるからです(二分探索できない)。

elkdatは指定したパッチセット内の全パッチを自動テストできます。

4つのパッチから成るパッチセットのうち3つ目のパッチだけに、ブートしないというバグがある場合の例を示します。パッチセットはここにあります

$ git log --oneline -5 
f80a34f377c1 4/4: fine again
227ef171c7f5 3/4: BUG
d662eff22070 2/4: fine
925417fc1d36 1/4: fine
69973b830859 Linux 4.9
$ 

パッチセットをテストするには次のコマンドを実行します。

$ ./test patchcheck 925417fc1d36 f80a34f377c1
...
Going to test the following commits:
925417fc1d3670f994c26bb09369b5f6c02c60bb 1/4: fine
d662eff220707c43c7bce87cf0343e27e67ce848 2/4: fine
227ef171c7f59c570fb821a81581ef78eed5be89 3/4: BUG
f80a34f377c1832d450dc0cc402288ee86ae2836 4/4: fine again

Processing commit "925417fc1d3670f994c26bb09369b5f6c02c60bb 1/4: fine"
...
Build time:   6 minutes 58 seconds
Install time: 8 seconds
Reboot time:  21 seconds

Processing commit "d662eff220707c43c7bce87cf0343e27e67ce848 2/4: fine"
...
** Monitor flushed **
kill child process 30367
closing!

Build time:   1 minute 15 seconds
Install time: 9 seconds
Reboot time:  19 seconds

Processing commit "227ef171c7f59c570fb821a81581ef78eed5be89 3/4: BUG"

[    0.135879] ftrace: allocating 32412 entries in 127 pages
[    0.163806] 1/4 patch is applied!
[    0.164408] 2/4 patch is applied!
[    0.164933] 3/4 patch is applied!
[    0.165469] ------------[ cut here ]------------
[    0.166151] kernel BUG at /home/sat/src/elkdat/linux/init/main.c:663!
[    0.167041] invalid opcode: 0000 [#1] SMP
[    0.167647] Modules linked in:
[    0.168216] CPU: 0 PID: 0 Comm: swapper/0 Not tainted 4.9.0-ktest+ #3
[    0.169076] Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.9.3-20161025_171302-gandalf 04/01/2014
[    0.170451] task: ffffffff8ee0e540 task.stack: ffffffff8ee00000
[    0.171254] RIP: 0010:[<ffffffff8ef81fd9>]  [<ffffffff8ef81fd9>] start_kernel+0x460/0x462
[    0.172501] RSP: 0000:ffffffff8ee03f50  EFLAGS: 00010282
[    0.173241] RAX: 0000000000000015 RBX: ffffffffffffffff RCX: ffffffff8ee54108
[    0.174175] RDX: 0000000000000000 RSI: 0000000000000246 RDI: 0000000000000246
[    0.175109] RBP: ffffffff8ee03f80 R08: 0000000000000000 R09: 0000000000000000
[    0.176043] R10: ffff9b179ffd7000 R11: 0000000000000098 R12: ffff9b179ffd06c0
[    0.176987] R13: ffffffff8f030840 R14: ffffffff8f03d2e0 R15: 000000000008a000
[    0.177924] FS:  0000000000000000(0000) GS:ffff9b179fc00000(0000) knlGS:0000000000000000
[    0.179070] CS:  0010 DS: 0000 ES: 0000 CR0: 0000000080050033
[    0.179853] CR2: 00000000ffffffff CR3: 0000000013e07000 CR4: 00000000000406f0
[    0.180831] Stack:
[    0.181216]  ffffffff8f03d2e0 0000000000000000 000000000000008e 0000ffffffff8ef8
[    0.182537]  0000000000000020 ffffffff8ef81120 ffffffff8ee03f90 ffffffff8ef812ca
[    0.183854]  ffffffff8ee03fe8 ffffffff8ef81419 00000000ffffffff 8ef88e0000101117
[    0.185206] Call Trace:
[    0.185639]  [<ffffffff8ef81120>] ? early_idt_handler_array+0x120/0x120
[    0.186521]  [<ffffffff8ef812ca>] x86_64_start_reservations+0x24/0x26
[    0.187383]  [<ffffffff8ef81419>] x86_64_start_kernel+0x14d/0x170
[    0.188228] Code: 02 00 e8 b7 b1 02 00 48 c7 c7 5d 13 c5 8e e8 e6 a2 21 ff 48 c7 c7 76 13 c5 8e e8 da a2 21 ff 48 c7 c7 8f 13 c5 8e e8 ce a2 21 ff <0f> 0b 31 c0 80 3f 00 55 48 89 e5 75 0f c7 05 4c 80 17 00 01 00 
[    0.194669] RIP  [<ffffffff8ef81fd9>] start_kernel+0x460/0x462
[    0.195523]  RSP <ffffffff8ee03f50>
[    0.196097] ---[ end trace f68728a0d3053b52 ]---
[    0.196776] Kernel panic - not syncing: Attempted to kill the idle task!
[    0.197707] ---[ end Kernel panic - not syncing: Attempted to kill the idle task!
bug timed out after 1 seconds
Test forced to stop after 60 seconds after failure
CRITICAL FAILURE... failed - got a bug report
REBOOTING
ssh -i /home/sat/src/elkdat/private_key root@192.168.121.181 sync ... [18 seconds] FAILED!
ssh -i /home/sat/src/elkdat/private_key root@192.168.121.181 reboot; ... ssh: connect to host 192.168.121.181 port 22: No route to host
[3 seconds] SUCCESS
 See /home/sat/src/elkdat/ktest/ktest.log for more info.
failed - got a bug report
$ 

3つめのパッチにバグがあることを正しく検出できました。

Find which commit introduce a bug by bysect

もし独自カーネルにバグが見つかり、それより古いとあるバージョンでは当該バグが存在しない場合、バグを仕込んだコミットを見つけるためにtest bisectコマンドが使えます。このコマンドはgit bisect1のように働きます。

git bisectを直接カーネル開発に使うのは難しいです。理由は、ユーザプログラムとは異なり、一回のテストごとにシステムをリブートする必要があるからです。

ここでは

  • v4.9に対して10個のパッチを作成した
  • 上記すべてを適用したら起動時パニック障害が発生した
  • 1つ目のパッチにバグが無いことは明らか

という場合を考えます。パッチセットはこちら

実際には次のように6番目のパッチでバグが仕込まれました。

$ git log --oneline -11
e617cb9e8cc0 10/10: BUG
d5159dda90f5 9/10: BUG
ddd7cdeacf47 8/10: BUG
9f6c5fbcd327 7/10: BUG
966f935e572c 6/10: BUG
f4504cce28bc 5/10: fine
cacbea15ec6a 4/10: fine
ee916bd4a2a8 3/10: fine
b61a82b33071 2/10: fine
5b762eff2275 1/10: fine
69973b830859 Linux 4.9

バグを仕込んだコミット、つまり966f935e572cを見つけるために、以下のコマンドを実行します。

$ ./test bisect 5b762eff2275 e617cb9e8cc0 boot
...
RUNNING TEST 1 of 1 with option bisect boot

git rev-list --max-count=1 5b762eff2275 ... SUCCESS
git rev-list --max-count=1 e617cb9e8cc0 ... SUCCESS
git bisect start ... [0 seconds] SUCCESS
git bisect good 5b762eff2275a414938275c00ccae7d2847f10b4 ... [0 seconds] SUCCESS
git bisect bad e617cb9e8cc0b49a507bc2fd2840fb803da00436 ... SUCCESS
Bisecting: 4 revisions left to test after this (roughly 2 steps) [f4504cce28bcb56b15df0c936e1598cb733f1658]
...
git bisect good ... SUCCESS
Bisecting: 2 revisions left to test after this (roughly 1 step) [9f6c5fbcd3276216291f60f41504bab6003c95e6]
...
git bisect bad ... SUCCESS
Bisecting: 0 revisions left to test after this (roughly 0 steps) [966f935e572c728f17877ab8a8fac454e04deda6]
...
...
git bisect bad ... SUCCESS
Found bad commit... 966f935e572c728f17877ab8a8fac454e04deda6
...
Bad commit was [966f935e572c728f17877ab8a8fac454e04deda6]



*******************************************
*******************************************
KTEST RESULT: TEST 1 SUCCESS!!!!         **
*******************************************
*******************************************

    1 of 1 tests were successful

$ 

バグを仕込んだ修正は6/10であると、正しく見つけられました。

おわりに

この記事、このツールをきっかけに、カーネルに親しみを持ってくれるかたが増えることを願っています。

参考資料

linux kernel auto test by using ktest ... ktestのチュートリアル linux kernelのmakeターゲットについてのあれこれ ... カーネルの設定についての情報を記載しています カーネルモジュール作成によるlinuxカーネル開発入門 - 第一回 hello world


  1. このコマンドについてご存知ではないかたはman 1 git-bisectをごらんください。

スクリーンキャストを採取する簡単なbashスクリプト

はじめに

既存の単純なコマンドを組み合わせてスクリーンキャストを採取する方法を紹介します。 git repoはここです。本体は高々20行のbash scriptであるsimple-screencast1つだけです。

実行環境と必要なパッケージ

やってること

  1. コマンドラインから、スクリーンキャストの取得対象となるWindowのID(第一引数。xwininfoコマンドで取得可能)、フレームレート(第二引数。単位はfps)、長さ(第三引数。単位は秒)を取得
  2. 一定時間(1/フレームレート[s])ごとにimportコマンドで対象Windowのキャプチャ画像を採取。これを第三引数で指定した秒数だけ繰り返す
    1. で出来た画像(枚数はフレームレート*長さ)から動画(test.mp4)を作成

これだけです。このスクリプト自体は大したことをしていませんが、他のツールを組み合わせることによって、一見すごそうなコマンドをでっち上げることができました。

サンプル

これです。コンソール上でgolanghello worldプログラムを書いてビルド、実行するまでの流れを撮影しました。ちょっとポカしてます。恥ずかしい。

おわりに

主目的はコードを短くシンプルにすることであり効率や利便性は何も考えてないので、実際にスクリーンキャストを取りたいのなら、 ここに書かれている既存のものを使う方がいいと思います。

GNU linker scriptでhello world

はじめに

みなさんはGNU linker(以下ld)やlinker scriptという言葉をご存知でしょうか。組み込み系のかたやOS屋さん以外のかたにとっては、直接使うものではないので、馴染みのないものだと思います。ldの役割は、皆さんが書いたソースファイルからコンパイラが生成したオブジェクトファイル(実行ファイルの断片)をリンクして実行ファイルを作ることです。linker scriptの役割は、ldが各種オブジェクトファイルをどのようにリンクさせるのかを制御することです。

linker scriptの応用

linker scriptを応用すれば、コードを含むオブジェクトファイルが無くてもスクリプトだけからプログラムを生成できます。本書では例としてld-helloというプログラムを紹介します。linux on x86_64環境で動作します。このプログラムはいわゆるhello worldプログラムです。動かし方は簡単で、単にソースディレクトリに移動してから以下のコマンドを発行するだけです

$ make && ./hello
Hello, world!

何をしているのか

helloを生成する際には以下のコマンドを実行します(実際にはmakeコマンドから間接的に実行されます)。

ld -o hello -T hello.ld empty.o

このコマンドの意味は、linker scriptとしてデフォルトのものではなくhello.ldを使用して、empty.oを元に実行ファイルhelloを作成するというものです。

empty.oはas -o empty.o empty.sによって生成されますが、実はempty.sの中身は空です。

$ cat empty.s

つまり、これを使ってできたempty.oはコードもデータも何も入っていない、実質無意味なオブジェクトファイルです。では何故このようなオブジェクトファイルからhello worldプログラムを作れるのでしょうか。それは、linker scriptであるhello.ldが直接コードやデータを生成しているからです。

hello.ldの中身の簡単な解説

ENTRY(_start)とは、プログラムの実行をstartというシンボルの位置から開始するという意味です。startは.text : { _start = . ; ...によって、機械語命令を格納する.textセクションの先頭に設定されます。

_startの位置からしばらく続くBYTE()やLONG()の列によって、所定の機械語命令を直接.textセクションに書き込んでいます。この命令をC風に書くと、だいたい次のようになります。

int main(void)
{
  puts("Hello, world!¥n");
  exit(0);
}

LONG(message);内のmessageというシンボルは、"Hello, world!¥n"という文字列データを指しています。このデータは、その後に続く、読み取り専用データを保持する.rodataセクションにあります。.rodata : {...}の中のBYTE()の列がこの文字列データです。

あとの細かいところはソース内のコメント、および本記事の末尾に紹介している参考文献をごらんください。

なんだかバイナリエディタで書くのとあんまり変わらなくて無理矢理臭いですが、一応これで、linker scriptでもプログラムが書けるということがわかりました。その気になればコンパイラでもカーネルでもなんでも書けます。わたしは絶対にやりませんが。

おわりに

linker scriptにはシンボルへの値の代入、四則演算、および三項演算などの機能があるので一見色々遊べそうなのですが、致命的なことに繰り返し構文ないしジャンプ命令がありません。linker sciptでもっと楽しく遊ぶために、将来のldにこれら機能が実装されることを切に願います。

参考文献

LinuxにおけるOOM発生時の挙動

はじめに

これはLinux Advent Calendar 2015 3日目の記事を2016/2/2に編集したものです。

Linuxにおいてシステムの物理メモリが枯渇したOut-Of-Memory(OOM)という状態になった際の挙動について説明しています。OOMに関連が深いsysctlパラメタを紹介するとともに、カーネルの内部論理についても触れました。

本記事に記載されているファイル名は、とくに断りが無ければカーネルソースのトップディレクトリからの相対パス名です。調査に使用したカーネルバージョンは4.3です。

本書は話を単純化するために、細かい動作論理については説明を省いていることをご承知おきください。また、本書の中に誤りを見つけたかた、および、私が追いきれなかったソースについての詳細をご存知のかたは、指摘していただけると助かります。

Out-Of-Memory(OOM)とOOM-killer

OOM発生時は、デフォルトでは適当に選んだプロセスを殺して物理メモリを空けることにより、なんとかしてシステムを生き延びさせようとします。この仕組みをOOM-killerと呼びます。

OOM-killerの挙動には次のような問題があるため、下手にシステムを生き延びさせるよりも、即座にシステムをpanicさせたいことがあります。

  • 運用に必須なプロセスが殺されてしまった場合は、システムが生きていてもしょうがない
  • OOM発生後にシステムが生き続けると、そこに至った根本原因を突き止めにくくなる。panicした場合は、その時に採取したカーネルダンプを解析することによって、原因がわかることがある

このように、OOM発生時にOOM-killerを発動させるのか、それともシステムをpanicさせるのかを選択するsysctlパラメタがvm.panic_on_oomです。

vm.panic_on_oom

vm.panic_on_oomパラメタは 0, 1, 2 という3つの値をとることができます。それぞれの意味は次の通りです。

OOM発生時の挙動
0 (デフォルト)必ずOOM-killerが発動
1 システムの物理メモリがまだ残っている場合はOOM-killerが発動。それ以外はpanic
2 必ずpanic

0と2はわかりやすいのですが、1の、システムの物理メモリがまだ残っているのにOOMが発生するとは、一体どういうことでしょう。それは、以下のように、プロセスのメモリ獲得に制限がかかっている場合です。

制限の名前 意味 OOM-killer発動時に殺すプロセス 詳細情報のありか
memory cgroup 特定cgroup内プロセスのメモリの使用量を制限 同じmemory cgroup内に存在するプロセス Documentation/cgropus/memory.txt
cpuset 特定NUMAノードからのみメモリを獲得できる ソースを追っていないので不明 man cpuset
mempolicy 同上 同上 man {set,get}_mempolicy

vm.panic_on_oom=1の用途として次のようなものが考えられます。

  • 基本的にはOOM発生時にはシステムをpanicさせたい。および、
  • memory cgroupよるメモリ使用量を制限したDockerコンテナが複数ある。および、
  • あるコンテナ内のプロセスがメモリを獲得した際にOOMが発生した場合、そのコンテナ内に影響を留めたい。OOM-killerによって、そのコンテナ内のプロセスのみを殺したい。

vm.oom_kill_allocating_task

OOM-killer発動時にシステム内の適当なプロセスを殺すのではなく、メモリを獲得しようとしたプロセスのみを殺害対象にしたい場合があります。そのときはこのsysctlパラメタの値を1にします。デフォルトは0です。

カーネル内の実装

ここからは、上述の振る舞いについてのカーネル内実装の簡単な解説をします。

OOM発生時におけるpanic有無の判断

OOM発生時におけるpanic有無の判定をする関数はmm/oom_kill#check_panic_on_oom()です。ソースは以下の通りです。

 void check_panic_on_oom(struct oom_control *oc, enum oom_constraint constraint,
                        struct mem_cgroup *memcg)
 {
         if (likely(!sysctl_panic_on_oom)) // ...(1)
                 return;
         if (sysctl_panic_on_oom != 2) { // ...(2)
                 /*                                                                                                                                     
                  * panic_on_oom == 1 only affects CONSTRAINT_NONE, the kernel                                                                          
                  * does not panic for cpuset, mempolicy, or memcg allocation                                                                           
                  * failures.                                                                                                                           
                  */
                 if (constraint != CONSTRAINT_NONE) ...(3)
                         return;
         }
         ...
         panic("Out of memory: %s panic_on_oom is enabled\n",
                sysctl_panic_on_oom == 2 ? "compulsory" : "system-wide"); ...(4)
 }

(1)のsysctl_panic_on_oomという変数が、vm.panic_on_oom sysctlパラメタに対応します。panic有無は、この値と、メモリ獲得時に存在した制限を示す第二引数constraintによって決まります。constraintが取りうる値はinclude/linux/oom.hに定義されています。

 enum oom_constraint {
         CONSTRAINT_NONE, // メモリ獲得時の制限なし
         CONSTRAINT_CPUSET, // cpusetによる制限あり
         CONSTRAINT_MEMORY_POLICY, // mempolicyによる制限あり
         CONSTRAINT_MEMCG, // memory cgroupによる制限あり
 };

sysctl_panic_on_oomが2の際は、(2)のif文の条件が満たされず、必ず(4)においてpanicが発生します。また、CONSTRAINT_NONE以外の場合、つまりメモリ獲得時に何らかの制限があった場合は(3)の条件を満たすため、panicせずに復帰します。そうでなければ(4)においてpanicします。

OOMが発生した場合に呼ばれる関数

OOM発生時には、その原因によって入り口となる関数が2つ存在します。

OOM発生原因 関数
memory cgroupの場合 mm/memcontrol.c#mem_cgroup_out_of_memory()
それ以外 mm/oom_kill.c#out_of_memory()

それぞれの場合についてソースを追ってみます。

memory cgourpの場合

以下はmem_cgroup_out_of_memory()のソースの抜粋です。

 static void mem_cgroup_out_of_memory(struct mem_cgroup *memcg, gfp_t gfp_mask,
                                     int order)
 {       
         ...
         struct task_struct *chosen = NULL; // ...(1)
         ...
         check_panic_on_oom(&oc, CONSTRAINT_MEMCG, memcg); // ...(2)
         ...
         for_each_mem_cgroup_tree(iter, memcg) { // ...(3)
                ...
         }
         ...
         if (chosen) { 
                 ...
                 oom_kill_process(&oc, chosen, points, totalpages, memcg, 
                                  "Memory cgroup out of memory"); // ...(4)
         } 
         ...
 } 

(1)のchosenは、殺害対象となるプロセスを示します。(2)でpanic有無を判定し、panicしなかった場合は(3)に進んでchosenプロセスを選択します。この後(4)においてOOM-killerが発動し、chosenプロセスを殺します。

どのような場合にchosenがNULLになるのか、また、このとき何故プロセスを殺さないままで処理を先に進められるのかについてはソースを追っていないため、不明です。

それ以外の場合

以下、out_of_memory()のソースの抜粋です。

 bool out_of_memory(struct oom_control *oc)
 {
        ...
        constraint = constrained_alloc(oc, &totalpages); // ...(1)
        ... 
        check_panic_on_oom(oc, constraint, NULL); // ...(2)
        
        if (sysctl_oom_kill_allocating_task && current->mm && // ...(3)
             !oom_unkillable_task(current, NULL, oc->nodemask) &&
             current->signal->oom_score_adj != OOM_SCORE_ADJ_MIN) {
                 get_task_struct(current);
                 oom_kill_process(oc, current, 0, totalpages, NULL,
                                  "Out of memory (oom_kill_allocating_task)"); // ...(4)
                 return true;
         }
 
         p = select_bad_process(oc, &points, totalpages); // ...(5)
         /* Found nothing?!?! Either we hang forever, or we panic. */
         if (!p && !is_sysrq_oom(oc)) {
                 dump_header(oc, NULL, NULL);
                 panic("Out of memory and no killable processes...\n"); // ...(6)
         }
         if (p && p != (void *)-1UL) {
                 oom_kill_process(oc, p, points, totalpages, NULL,
                                  "Out of memory"); // ...(7)
                 ...
         }
         return true;
 }

(1)のconstrained_alloc()は、メモリを獲得しようとしたプロセスの制限を判定し、CONSTRAINT_NONE, CONSTRAINT_CPUSET, CONSTRAINT_MEMPOLICYのいずれかを返します。(2)においては、その結果を用いてpanic有無を判定します。

(3)においては、vm.oom_kill_allocating_taskが1の場合(他にも条件は色々ありますが、ここでは省略)には対応するカーネル内変数sysctl_oom_kill_allocating_taskが1になります。値が1の場合は(4)においてメモリを獲得しようとしたプロセスを殺します。

(3)の条件を満たさなかった場合は、(5)において、全プロセスの中からOOM-killerによる殺害対象となるプロセスpを適当に選択します。そのようなプロセスが見つからなかった場合は、これ以上システムを生き延びさせることができないため、(6)においてpanicします。見つかった場合は、(7)において選択されたプロセスを殺します。

プロセスpが(void *)-1ULになった場合はプロセスを殺すことなく先の処理に進んでいますが、どのような場合にこうなるのか、また、なぜそれでよいのかについてはソースを追っていないので不明です。

linux kernelのmakeターゲットについてのあれこれ

はじめに

本記事は、Linux Advent Calendar 2014 7日目の記事を2016/2/2に編集したものです。linux kernelをビルドする際に使用するmakeのターゲット(以下"makeターゲット"と記載)について、よく使われる便利なもの、面白いもの、小咄などの紹介を目的とします。本記事の執筆時点で、makeターゲットの数はゆうに50を超えますが、全部を網羅的に紹介するのではなく、適当にかいつまんで数個だけを紹介します。全てのターゲットについて知りたい方は、make helpコマンドの実行結果を参照してください。

本記事の調査に使用したkernelのバージョンは3.18-rc7です。本書の想定読者は、linux kernelをビルドしたことはあるけれども、makeターゲットの細かい話は知らないかたがたです。以下、それぞれのネタは個々に独立しているため、前から順番に読む必要はなく、興味のあるところだけ読んでくだされば問題ありません。

kernelビルド時間を短くしたい

linux kernelの設定項目は数千個も存在するため、どれを有効化/無効化すれば自分の環境でブートするkernelが出来上がるのかがわかりにくいです。様々な用途、環境向けに作成されたdistribution kernelの設定ファイルを流用すると、確かにブートはするものの、あなたが今後一生使うことがないような大量の機能、ドライバが組み込まれた巨大なkernelが出来上がってしまいます。そのビルド時間も長時間に及ぶため、そこから更に無効化可能な設定を逐一確かめていくのは、非常に面倒かつ困難です。

まず、distribution kernel の設定ファイルを流用した場合に、どれだけの設定項目が有効化されるかを見てみましょう。

 $ cd src/linux
 $ make kernelrelease
 scripts/kconfig/conf --silentoldconfig Kconfig
 3.18.0-rc7 # kernel source は v3.18-rc7 のもの
 $ uname -a
 Linux www2455ue 3.13-1-amd64 #1 SMP Debian 3.13.10-1 (2014-04-15) x86_64 GNU/Linux               # 現在動いているカーネル。これは debian/testing の distribution kernel
 $ cp /boot/config-$(uname -r) .config
 $ make olddefconfig                                     # 古い.conigをもとに、新しい項目はデフォルト値を設定
 scripts/kconfig/conf --olddefconfig Kconfig
 #
 # configuration written to .config
 #
  
 $ grep -c '^CONFIG_.*=[my]$' .config
 4168                                                    # 有効化されている設定項目は 4,000 個以上

そこで登場するのがlocalmodconigターゲットです。これは、既存の.configのうち、モジュールとしてビルドする設定になっているもの(CONFIG_*=m)について、make localmodconfig発行時にloadされていないmoduleをビルド対象から外してくれるという優れものです。これによって、あなたの環境でブートし、かつ、余計なモジュールが含まれないkernelを作成することができます。

以下、上記の実行例の続きです

 $ make localmodconfig
 ...                                                     # 新しい config について設定。気にしなければ適当にENTER連打でいい
 $ grep -c '^CONFIG_.*=[my]$' .config
 1028                                                    # 有効な設定項目数が 4,168 から 1,028 に、約 1/4 に減少。ビルド時間もそれに応じて大幅短縮可能。

以下、いくつか注意点と補足情報を紹介します。

  • kernelをビルドするシステム(以下「ビルドシステム」と記載)と、ビルドしたカーネルを動作させたいシステム(以下「ターゲットシステム」と記載)が異なる場合は、ターゲットシステムにおいてlsmodを採取した結果をファイル(ここでは lsmod.txt"とする)に保存しておき、ビルドシステムのkernelソースディレクトリにおいて"LSMOD=lsmod.txt make localmodconfig"コマンドを発行すればよい。わざわざターゲットシステムにkernelソースを持って行ってmake localmodconfigを発行する必要は無い。
  • "CONFIG_*=y"となっている設定項目については、localmodconigは残念ながら自動的に無効化しない。その理由は、これら設定項目に関する機能はkernelのバイナリに組み込まれてしまっているので、使用有無を自動的に判別する方法が無いため。必要ならば手動で設定する必要がある。参考までに、ktest("tools/testing/ktest")のminconfig機能によってを用いれば最小の.configを作れるようだが、筆者は未確認。
  • 本ターゲットの実行は、.configは、今現在動作中のkernel、あるいはそれより若干高いバージョンのものをベースにするのが無難。そうでないと、動作中のシステムのkernelとビルド対象kernelで名前が異なっているモジュールが存在する場合に、期待通りの動作をしないことがある。
  • "make localmodconig"実行時は、システムが普段使用している機能をすべて実行しておくほうがよい。たとえば、普段はNFSファイルシステムをmountしているが、"make localmodconfig"実行時は偶然それをmountしていなかったとする。この場合、コマンド発行時にはNFSのkernelモジュールがロードされていないため、.configのビルド対象からNFSが外されてしまうことがある。

kernelを全部ビルドするのではなく、自分が変更したソースのみ、コンパイルが通るかどうかを確認をしたい

自分で書いたkernel patchを適用したkernelのビルドが通らないことがよくあります。kernelビルドのために数十分待たされたあげく、「自分の追加した行の末尾にセミコロンが無かった」などという下らないバグによってビルドエラーが出ると、泣きたい気分になります。そのような事態を避けるために、linux kernelは特定ファイルのみをコンパイル可能です。たとえば"kernel/sched/core.c"というソースファイル(タスクスケジューラの中心部のソースファイル)のみを編集した場合は、"make kernel/sched/core.o"コマンドを発行すると、当該ファイルと、それが依存するファイルだけをコンパイルしてくれるため、ビルドエラーの有無を短時間で確認可能です。

特定の設定項目についての情報を得たい

make menuconfig.configの実行開始後に"/"(スラッシュ)を入力することによって、設定項目の検索機能を使用できます。そこで特定の設定項目を示す文字列を打ち込むと、当該項目に関する説明文、依存する設定項目、現在の設定値、menuconfigのメニューをどのように辿れば当該項目の設定画面に辿り着けるかがわかります。入力した文字列が複数の設定項目にマッチした場合は、マッチしたすべての項目についての情報が出力されます。

では、CONFIG_BTRFS_FS についての情報を参照したい場合の実行例を見てみましょう。

 $ make menuconfig

最初の画面

 .config - Linux/x86 3.18.0-rc7 Kernel Configuration                            
 qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq 
  lqqqqqqqqqqqqqqq Linux/x86 3.18.0-rc7 Kernel Configuration qqqqqqqqqqqqqqqk   
  x  Arrow keys navigate the menu.  <Enter> selects submenus ---> (or empty x   
  x  submenus ----).  Highlighted letters are hotkeys.  Pressing <Y>        x   
  x  includes, <N> excludes, <M> modularizes features.  Press <Esc><Esc> to x   
  x  exit, <?> for Help, </> for Search.  Legend: [*] built-in  [ ]         x   
  x lqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqk x   
  x x    [*] 64-bit kernel                                                x x   
  x x        General setup  --->                                          x x   
  x x    [*] Enable loadable module support  --->                         x x   
  x x    [*] Enable the block layer  --->                                 x x   
  x x        Processor type and features  --->                            x x   
  x mqqqqv(+)qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqj x   
  tqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqu   
  x        <Select>    < Exit >    < Help >    < Save >    < Load >         x   
  mqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqj   

ここで "/"(スラッシュキー) を入力

 .config - Linux/x86 3.18.0-rc7 Kernel Configuration                            
 qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq 
                                                                                
                                                                                
  lqqqqqqqqqqqqqqqqqqqq Search Configuration Parameter qqqqqqqqqqqqqqqqqqqqqk   
  x  Enter (sub)string or regexp to search for (with or without "CONFIG_")  x   
  x lqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqk x   
  x x                                                                     x x   
  x mqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqj x   
  x                                                                         x   
  x                                                                         x   
  tqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqu   
  x                         <  Ok  >      < Help >                          x   
  mqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqj   

知りたい設定項目の名前、"CONFIG_BTRFS_FS" を入力

 .config - Linux/x86 3.18.0-rc7 Kernel Configuration                            
 qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq 
                                                                                
                                                                                
  lqqqqqqqqqqqqqqqqqqqq Search Configuration Parameter qqqqqqqqqqqqqqqqqqqqqk   
  x  Enter (sub)string or regexp to search for (with or without "CONFIG_")  x   
  x lqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqk x   
  x xCONFIG_BTRFS_FS                                                      x x   
  x mqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqj x   
  x                                                                         x   
  x                                                                         x   
  tqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqu   
  x                         <  Ok  >      < Help >                          x   
  mqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqj   

続けて画面上の "< OK >" を選択すると、CONFIG_BTRFS_FS についての情報が出力される。同じく検索文字列にマッチした "CONFIG_BTRFS_FS_CHECK_INTEGRITY" などについての情報も表示される。

 .config - Linux/x86 3.18.0-rc7 Kernel Configuration                            
 > Search (CONFIG_BTRFS_FS) qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq 
  lqqqqqqqqqqqqqqqqqqqqqqqqqqqq Search Results qqqqqqqqqqqqqqqqqqqqqqqqqqqqqk   
  x Symbol: BTRFS_FS [=m]                                                   x   
  x Type  : tristate                                                        x   
  x Prompt: Btrfs filesystem support                                        x   
  x   Location:                                                             x   
  x (1) -> File systems                                                     x   
  x   Defined at fs/btrfs/Kconfig:1                                         x   
  x   Depends on: BLOCK [=y]                                                x   
  x   Selects: CRYPTO [=y] && CRYPTO_CRC32C [=m] && ZLIB_INFLATE [=y] && ZL x   
  x                                                                         x   
  x                                                                         x   
  x Symbol: BTRFS_FS_CHECK_INTEGRITY [=n]                                   x   
  tqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq( 32%)qqu   
  x                                < Exit >                                 x   
  mqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqj   
                                                                                

いつも、このような情報を確認する際に Kconfig ファイルを grep していたようなかたは、大きく時間が節約できるはずです。

ビルドしたkernelをrpmdebなどのパッケージ管理システムで管理したい

普通にkernelをビルドした場合、つまり、ターゲット指定なしでmakeコマンドを実行した場合、開発者は、distributionが提供するパッケージ管理システムを使うことなく、手動、ないし"make install"を用いてkernelをインストール/アンインストールする必要があります。管理が面倒などという理由でこれが不満なかたは、"make deb-pkg"、あるいは"make rpm-pkg"コマンドを発行すると、それぞれ.deb形式、.rpm形式のkernelパッケージを作成することができます。

linux kernelの新しいテスト用フレームワーク

自分でkernelを作った場合(とくにソースに変更を加えた場合)、そのカーネルを実際に動作させる前にテストをする必要があります(ビルドすら通らないkernel patchをLKMLに投げてくる人も稀にいますが…)。これまでkernelのテストには、バグ報告者や開発者が自作したテスト、LTP,およびxfstestsなどの様々なものが使われてきました。

kernel-3.17からは、カーネル自身がテスト用のフレームワークを持つようになりました。"make kselftests"コマンドを発行すると、tools/testing/selftests以下のテストが流れます。このフレームワークは、まだ出来たばかりなこともあり、どれだけ普及するかは現在のところまだ未知数ですが、いずれは多くの/すべての開発者が使うようになるかもしれません。他のテスト用ツールに比べて、変化の早いkernelそのものに同梱されているというのは大きな強みといえるでしょう。

変な名前のターゲット

"make mrproper" は、kernelソースのソース変更やビルドに伴い様々なファイルが新規に作成された状態において、kernelソースにもともと含まれていたファイル以外ほとんど全て(たとえば.config, .o, .ko, やkernelのバイナリ)を消去するmake ターゲットです。その機能自身はいいとして、この変な名前はどういう意味があるのでしょうか。

これは、P&Gのクリーニング製品のマスコットキャラクター、Mr. Properが由来だそうです。こういう特定の文化に依存した"うまいこと言ってる"系のネタは、その文化の外の人から見ると、なかなかわかりにくいところです。

余談ですが、日本ではこの手のマスコットには可愛いキャラ、萌えキャラ、ゆるキャラなどが採用されそうなものですが、Mr. Properは上記リンク先のイラストにあるように、ムキムキマッチョなスキンヘッドの親父です。文化の違いをひしひしと感じます。

最後に

本記事で紹介したもの以外にも、makeターゲットには便利なもの、面白いものがたくさんあります。また、makeターゲットは、比較的頻繁に更新されるため、しばらく確認を怠っていると、いつのまにか見慣れないものが追加されていることがよくあります。たまにはmake helpを眺めてみて、面白そうなものがあれば試してみるのもよいかもしれません。

おわり。最後まで見て頂いて、ありがとうございました

linuxのstable kernel reviewに参加してみませんか

はじめに

この文書は2013 Kernel/VM Advent Calendarの2日目のために書いたものを2016/2/2に編集したものです。

Linux の stable kernel というカーネルのリリース作業、およびそれに関わると、何がいいかということを説明します。自分がかつて2年ほどこの作業に関わっていた際は、自分自身の成長、および世界中のLinuxユーザに役立ったという実感が持てました。

stable kernel とは

Linux Kernel の公式サイトには、やたらとたくさんのカーネルソースが並んでいます。まずは、Linus Torvalds 氏が管理する mainline と呼ばれるカーネルです。この mainline カーネルに対して、機能追加をせずに(厳密には PCI の device ID なんかは追加されますが)バグフィックスのみを適用したカーネルが stable kernel です。上記サイトにおいて stable や longterm という印がついているものがそれです。これ以上の詳細を知りたいかたはここの説明をごらんください。

stable kernel review とは

stable kernel はリリースの 2,3 日前になると、メンテナの Greg KH 氏から、こんなかんじで「review cycle が始まったぜ!」という通知メールが飛びます。それから 2, 3 日後までが stable kernel の review cycle と呼ばれる期間です。この間にテストや取り込み候補のパッチをレビューしたりするのが stable kernel review という作業です。

参加する利点

以下に、異なる目標を持つ様々なかたにとっての利点を列挙します。ご自身に当てはまる箇所を読んでいただければと思います。

linux についての知識を得たい人

stable kernel に取り込まれるパッチは、通常、冒頭に「この機能は A の動作すべきだが、実際には B という誤った動作をする。このパッチはこれをこのように修正した」という記述の後に、ある 1 つだけのバグを直す短い修正パッチが続くという構成です。このため、全く知識が無い機能であっても、それなりの C 言語(と、たまにアセンブラ)の素養があれば、なんとか理解可能です。パッチレビュー作業をしばらく続ければ、linux kernel の様々な機能について相当詳しくなれます。

linux kernel を理解するには、この巨大な(総行数は現在一千万行超)ソフトウェアに真正面から突撃するより、細かいパッチをたくさん見る方が遥かに近道だと私は常々考えていましたし、実際やってみて、その思いは前よりも強まりました。

ソフトウェア開発に関する知見を得たい人

stable kernel への取り込み対象になるパッチは、強制終了やハングアップなどの、発生するとユーザがすごく困るようなバグの修正であり、かつ、実際に発生しうる(理論的に起こるだけでは駄目)ものに限られます。詳細についてはカーネルソース以下にある "Documentations/stable_kernel_rules.txt"内の"Rules on what …" の節をごらんください。

駆け出しのエンジニアは、とかく「バグであれば何でも直さなければ」と判断しがちですが、stable review を通じて、「実際に発生すると困るバグ、直すべきバグとは どのようなものか」という実務に必要な知見が得られます(もちろん人/組織によってポリシーは違うので、あくまで一例ですが)。

なにかしら社会的な貢献をしたい人

みなさんがリリースに貢献したカーネルは今後、何百万、何千万、あるいはそれ以上の数のコンピュータシステム上で動作します。自分で作成したパッチが取り込まれたりなんかした日には、 changelog に永久に名前が刻み込まれます。こんな経験が少しの労力でできてしまうんですよ、すごい!さらに、貢献度が高いと判断されれば、名うてのOSS エンジニアとして名を馳せられるかもしれません。

本職の linux エンジニア(とくにカーネルエンジニア)の人

上記の通り、stable kernel に取り込まれるパッチは、発生するとユーザが困り、かつ、実際に発生しうるバグです。さらに、どのバージョンのカーネルで発生するかがわかる(カーネルソースの "Documentations/stable_kernel_rules.txt" の"Procedure for submitting patches …" の節を参照)ことも多いです。これは、みなさん自身や、そのお客様の環境で使われているシステムにおいて発生しうる障害についての情報を、早期に知りうることを意味しています。これはエンジニアにとって非常に強いアドバンテージになることでしょう。

あらゆる人

この作業に貢献した人には、毎回Greg 氏から気持ちのよい「ありがとう」の言葉が得られます。「子供じゃあるまいし」とお考えかもしれませんが、自分の貢献が認められるということは想像以上に気持ちの良いものです。是非実際に体験してみてください。

作業内容

  1. linux stable ML (linux-stable@vger.kernel.org)を購読する ここを参照に購読手続きをしてください。

  2. review cycle が始まったら test/review を実施する。 linux stable ML には 1, 2 週間に 1 回、既存の stable kernelのreview cycle が始まります。メールの Subject は "x.y.z-stable review" です。そのメールの後に、個々のパッチのメールが返信形式で連なるという構造になっています。

作業内容は大きく分けるとテストとレビューの 2 つです。

2-a. テスト レビュー対象のパッチを適用したカーネル(全パッチをマージしたパッチも提供されます)、あなたの環境でビルドして、好きなテストを流すなりして、結果を Greg 氏に報告してください。現在私の知る限り、定期的にテストを実施している人は、それぞれ別の観点でテストしていますので、新たにテストをするかたは、なるべく別の観点でテストすると喜ばれるかと思います。

2-b. パッチレビュー レビュー依頼メールにくっついてくる個々のパッチセットをレビューし、レビューした事実と問題の有無を Greg 氏に報告します。問題がある場合は、修正パッチを添えると、さらに喜ばれます。「パッチがバグっていることなんてあるの?」と思われるかもしれませんが、ここだけの話、実はけっこうバグっています。わたしはこれまでに百個前後パッチをレビューしてきましたが、その中で 5 つくらいはバグを指摘した記憶があります。

一見簡単そうに見えるパッチでも、「バグってるわけない」という色眼鏡を外し、

  • 新たに増えたルートに新規バグを仕込んでいないか、
  • メモリリークはないか
  • 似たようなコードの他の箇所に同様のバグは無いか、

などの基本的なルールに従ってチェックしてゆくと、けっこうバグが見つかるもんです。慣れてくると怪しい箇所が光って見える特殊能力が身につきます(本当です)。

毎回死ぬほどパッチの数が多いので(たとえば 3.12.2 は 116 個)、全部見ようなどとせず、自分の興味ありそうなもの、まともなコメントがついているもの、コードが短いものを選ぶと、長く続けられると思います。同様に、「この作業をやらなきゃ」と考えるより「できたらやる」くらいでちょうどいいと思います。わたしも面倒くさい時や忙しい時にレビューをすっぽかしたことがたくさんありました。

他にも色々貢献方法はありますが、詳細が気になるかたは Greg 氏のブログエントリをごらんください。これは数年前のメールですが、別に今も実施する内容は変わっていませんし、いきなり参加して咎められることもありません。きっと歓迎されると思いますよ。

おしまい。