LECTURE 1 カーネルモジュールを作る

○ はじめに

Linux® でのデバイスドライバは、カーネルに静的にリンクする形式と、動的にロードすることができるモジュール形式で作成することができます。本 Webではカーネルモジュールの作成方法を説明します。

"C 言語によるプログラミングでは main() 関数から実行される" という約束事があるように、モジュールプログラミングにもこうしなければならないという約束事があります。

ここでは、簡単なモジュールを作ってみることでモジュールプログラミングに最低限必要な知識を説明します。

○ サンプルプログラム

カーネルへのロードおよびカーネルからのアンロードのみに対応したサンプルプログラム message.c です。はじめに のページから、全てのサンプルプログラムを固めた tar ボールをダウンロードすることもできます。

カーネルモジュールをコンパイルする際には、linux/module.h をインクルードし、module.h のインクルードよりも前に MODULE マクロを定義しなければいけません。

#define MODULE
#include <linux/module.h>

と先頭に記述するか、コンパイル時に -DMODULE として定義して下さい。本 Web ではコンパイルスイッチを使用します。

gcc -DMODULE -c message.c

また、SMP システム用にコンパイルする際は、カーネルヘッダをインクルードする前に __SMP__ マクロを定義する必要があります。

作成された message.o がカーネルモジュールです。カーネルへの組込みはinsmod コマンドを実行します。

/sbin/insmod message.o

何も表示されなければ、カーネルへ無事組込まれたはずです。カーネルログを見ると

<6>module [message.o] : loaded  into kernel

の様に表示されていると思います ( 普通は /var/log/messages あたりに出力されると思いますが、筆者の環境では klogd に直接出力ファイルを指定しているため、そのファイルに出力されています )。この状態で /sbin/lsmod を実行すると、組み込まれているモジュールの一覧に message が入っていると思います。

カーネルからモジュールをアンロードする場合には、rmmod コマンドを実行します。

/sbin/rmmod message.o

この場合、カーネルログには

<6>module [message.o] : removed from kernel

の様に表示されます。

--- message.c ---

    1    #include <linux/module.h> /* module 作成には必須 */
    2    #include <linux/kernel.h> /* printk */
    3    
    4    MODULE_LICENSE( "GPL" );
    5    
    6    static char* msg = "module [message.o]";
    7    
    8    
    9    /*
   10     * モジュールの初期処理
   11     * insmod 時に呼ばれる
   12     */
   13    int 
   14    init_module( void )
   15    {
   16        printk( KERN_INFO "%s : loaded  into kernel\n", msg );
   17    
   18        /* モジュール初期化に成功したので 0 を返す
   19           0 以外の値を返せば初期化失敗とみなされモジュールは
   20           カーネルには組み込まれない */
   21        return 0;
   22    }
   23    
   24    
   25    /*
   26     * モジュールの解放処理
   27     * モジュールの参照数が 0 であれば、rmmod 時に呼ばれる
   28     */
   29    void 
   30    cleanup_module( void )
   31    {
   32        printk( KERN_INFO "%s : removed from kernel\n", msg );
   33    }
   34    
   35    
   36    /* End of message.c */

○ サンプルプログラムの説明

では、サンプルプログラムについて説明していきます。

  • 1 〜 2 行目

    カーネルモジュールプログラミングに必要なヘッダファイル <linux/module.h> をインクルードしています。このヘッダファイルは、 カーネルモジュールをコンパイルする際には MODULE マクロを定義した上で 必ずインクルードする必要があります。

    <linux/kernel.h> は、printk を使うためにインクルードしています ( 実際には module.h をインクルードするだけで使えますが )。

  • 4 行目

    MODULE_LICENSE マクロは、カーネル 2.4.10 から新しく用意されたマクロ です。このマクロには、カーネルモジュールのライセンスを表す文字列を 引数として指定します。指定できる文字列は module.h 内に用意されています (本 Web では GPL を使用します )。

    このマクロを何のために使っているかについてですが、これを使わないで コンパイルして insmod を実行すると、カーネル 2.4.10 以降では以下の様な表示が出ます。

    Warning: loading message.o will taint the kernel: no license
      See http://www.tux.org/lkml/#export-tainted for information about tainted modules
    Module message loaded, with warnings
          

    要約すると、"ライセンスが無いカーネルモジュール message.o は、 カーネルを汚染すると思われます。詳細は http://www.tux.org/lkml/#export-tainted を参照して下さい。" ということです。 これは動作上問題があるという意味ではなく、該当モジュールは ( module.h で定義されている ) オープンソースライセンスで提供されていないため、 何か問題が生じた場合は Linux カーネル開発者ではなくモジュール提供者に 問い合わせてくれ、ということを言いたいらしいです。

  • 9 〜 22 行目 ( init_module )

    カーネルモジュールを組み込む際に、必ず実行される関数です。通常は、 この関数の中で必要な初期化等を行います。0 を返すと初期化成功を表し、 0 以外を返すと失敗とみなしてカーネルには組み込まれません。

    message.c では、printk を使って カーネルログにメッセージを表示した後、0 を返してカーネルに正常終了を 通知しているだけです。

    printk は、カーネル版 printf です。printk については、 printk の使い方 を参照してください。

  • 25 〜 33 行目 ( cleanup_module )

    カーネルモジュールの解放処理を行う関数です。この関数は rmmod を行う度に実行されるわけではありません。通常、ユーザプロセスが デバイスファイルを通してカーネルモジュールを open したりして使用 するのですが、その度にカーネルモジュールの参照カウンタが増加していきます。

    参照していたプロセスがデバイスファイルを閉じると、カーネルモジュール の参照カウンタが減少していくのですが、この参照カウンタが 0 になった時点で cleanup_module が実行されます。

    message.c ではプロセスから 参照するための口を用意していないため、参照カウンタは常に 0 です。 そのため、rmmod を実行する度に cleanup_module が実行されます。

以上で message.c の解説は終わりです。とっても単純なカーネルモジュール でしたが、まがりなりにもカーネル内部で動作させることが出来ました! これから、少しづつ使える機能を増やしていこうと思いますので、message.c の内容はしっかり理解しておいてください。

○ 複数ファイルに分割して作成する

今後しばらくは、一つのファイルでカーネルモジュールのサンプルプログラムを作っていく予定ですが、説明用や練習用ではなくちゃんとしたプログラムを作成する場合は、ファイルを適切に分割するのが普通だと思います。

ここであらかじめ、複数ファイルに分割してカーネルモジュールを作成する方法について説明しておきます。

sample.o というカーネルモジュールを sample_entry.c、sample_func.c、sample_func.h という 3 つのファイルで作成するとします。また、sample_entry.c には init_module および cleanup_module が定義されており、init_module 内で sample_func.c 内の関数を実行して終了するのみとします。

各ファイルの例を以下に挙げます。

--- sample_func.c ---

#include "sample_func.h"

static int inner_add( int a, int b );

int 
sample_add( int a, int b )
{
    return inner_add( a, b );
}

static int
inner_add( int a, int b )
{
    return a + b;
}    

--- sample_func.h ---

#ifndef ___SAMPLE_FUNC_H_INCLUDED___
#define ___SAMPLE_FUNC_H_INCLUDED___

int
sample_add( int a, int b );


#endif

--- sample_entry.c ---

#include <linux/module.h>
#include <linux/kernel.h>
#include "sample_func.h"    

    
int
init_module( void )
{    
    EXPORT_NO_SYMBOLS;

    printk( KERN_INFO "loaded into kernel\n" );
    printk( KERN_INFO "value = %i\n", sample_add( 1, 2 ) );

    return 0;
}
    
    
void    
cleanup_module( void )    
{
    printk( KERN_INFO "removed from kernel\n" );
}

ポイントは、sample_entry.c の init_module 内にある EXPORT_NO_SYMBOLSマクロです。sample_func.c では sample_add 関数のみを外部に公開しています。このおかげで sample_entry.c から sample_add 関数を参照することができます。

ところで、EXPORT_NO_SYMBOLS マクロが無いとどうなるかというと、カーネルに組み込まれた際に static 以外のシンボル ( 関数だけでなく、変数等も ) が全て他のカーネル内 ( カーネルモジュールを含む ) から参照可能になってしまいます。つまり、全然関係無い場所から sample_add 関数を実行できてしまうのです。

EXPORT_NO_SYMBOLS マクロを実行すると、static ではないシンボルが外部から参照されることが無くなりますので、他のモジュールに機能を提供するのでない場合はこのマクロを実行してください。なお、実行する場所はどこでも良い様ですが、筆者は init_module 内で行う様にしています。

また複数ファイルで作成する場合は、linux/module.h のインクルードはいずれか一つのファイルで行う様にしてください。

次にコンパイル / リンクについてです。各ファイルをコンパイルしてオブジェクトファイルにした後、ld でリンクを行います。

gcc -DMODULE -D__KERNEL__ -O -c sample_func.c
gcc -DMODULE -D__KERNEL__ -O -c sample_entry.c
ld -r -o sample.o sample_entry.c sample_func.c

これで sample.o というカーネルモジュールが作成されます。__KERNEL__ マクロおよび -O オプションについては、今後キャラクタデバイスドライバを作成していく際に触れます。なお ld の -r オプションは、再配置可能形式を作成する様に ld に指示するためのオプションです。カーネルモジュールは動的にロードされるため、このオプションが必要になります。

○ printk の使い方

カーネル内で簡単にメッセージを出力するために、printk という関数を使うことができます。printk は、ユーザ空間で使える printf の様に使うことができます。

#include <kernel.h>
int printk( const char* format, ... );

出力した文字数を返します。printf と異なる点はログレベルを指定する必要があることです。ログレベルは、以下の定数のいずれかを format 文字列の先頭に指定します。

ログレベル定数 説明
KERN_EMERG システムが使いものにならない状態
KERN_ALERT 早急に対処しなければならない事象
KERN_CRIT 致命的な事象発生、ハードウェア / ソフトウェアに重大な問題発生
KERN_ERR エラー発生
KERN_WARNING 深刻な問題ではないが通常ではない事象発生に対する警告
KERN_NOTICE 注意を促す事象発生
KERN_INFO 情報表示
KERN_DEBUG デバッグメッセージ

以下の様に指定して使います。

printk( KERN_ERR "Something happened.\n" );

レベル定数と後続の文字列が直接連結されている点に注意してください。なお、レベルを指定しない場合は、kernel/printk.c で定義されているデフォルトのログレベルが使用されます。

printk によって出力されたメッセージは、まずカーネル内のリングバッファに格納されます。その後、klogd が動作している場合は klogd がリングバッファからメッセージを読み込み、設定に応じてコンソールに出力したり syslogd にメッセージを送ったりします。

klogd からコンソールに出力されるメッセージは、予め設定されているログレベルより大きいログレベルのみとなります。例えば、klogd を -c オプション付きで実行している場合は、それより小さなログレベルのメッセージのみがコンソールに出力されます。詳しくは man klogd を参照してください。

フォーマット文字は、以下を使うことが出来ます ( kernel 2.4.19 時点 )。

フォーマット文字 説明
c 1 文字
s 文字列
p ポインタ
n 直前までの文字数を変数に格納する
o 8進数
X 大文字表記 16進数
x 16進数
d 符号付き整数
i 符号付き整数
u 符号無し整数

○ kmalloc / kfree によるメモリの確保 / 解放

カーネル内で動的にメモリを確保 / 解放するためにはいくつかの方法がありますが、簡単な方法は、ユーザ空間で malloc / free を使う感覚で kmalloc / kfree を使うことです。

#include <slab.h>
void* kmalloc( size_t size, int flags );
void kfree( void* ptr );

size には確保したいバイト数を指定します。物理メモリはある単位でまとめて扱う必要があるため、kmalloc で実際に確保するバイト数は size 以上になる可能性があります。なお、自分ではまだ良く分かっていないのですが、kmalloc で確保できる最大バイト数は 128 KBらしいですので気を付けてください。

flags には、linux/mm.h で定義されている以下のいずれかを通常は指定します。

flag 定数 説明
GFP_KERNEL 即座にメモリ領域を用意できない場合、メモリ管理機能がスワップアウト 等によって領域を確保するまでスリープする可能性がある
GFP_ATOMIC スリープせずにメモリ領域を確保しようとする。 必要な領域が確保できない場合は失敗する。 割り込みハンドラやタスクレット、タイムアウトハンドラ等の プロセスコンテキストと無関係に実行される箇所ではこれを使う

また、DMA に使用できるメモリ領域が必要な場合は、上のいずれかのフラグと GFP_DMA の or をとったビットマスクを指定します。

○ module_init / module_exit マクロについて

前述の説明では、init_module() がモジュールの初期化処理を行い、cleanup_module() が解放処理を行うと書きました。この二つの関数名は決められている名称ですが、”自分で自由に名前を付けたい”とか”名前の衝突が起きるのが怖い”という場合には、module_init / module_exit マクロを使用することで初期化処理/解放処理を行う関数名を変えることができます。

例えば、初期化処理を initialize_kernel_module() という関数で行い、解放処理を shutdown_kernel_module() という関数で行いたいとします。

#include <linux/init.h>

static int
initialize_kernel_module( void )
{
    /* 初期化処理を記述する */
}


static int
shutdown_kernel_module( void )
{
    /* 解放処理を記述する */
}


module_init( initialize_kernel_module );
module_exit( shutdown_kernel_module );

この様にすることで、モジュール組み込み時には initialize_kernel_module() が、解放時には shutdown_kernel_module() が実行される様になります。