C
Linux
RaspberryPi
kernel
デバイスドライバ

組み込みLinuxデバイスドライバの作り方 (1)

1回目: ビルド環境準備と、簡単なカーネルモジュールの作成

本連載について

組み込みLinuxのデバイスドライバをカーネルモジュールとして開発するためのHowTo記事です。本記事の内容は全てラズパイ(Raspberry Pi)上で動かせます。

Linuxデバドラ開発は最初の一歩が難しいと思います。資料も少なく、たいていの人はオライリー本に手を出して挫折すると思います。(僕がそうでした。。。)

この記事では、「Linuxデバイスドライバプログラミング (平田 豊) 」の内容に沿って進めていこうと思います。この本は非常に分かりやすく良書だと思います。ただ、2008年発行と古いので、現在(2017年12月)の環境でも動くように、実際にRaspberry Piで動かしながら確認していこうと思います。(途中から、本の内容とは離れます)

また、出来るだけ簡単にしたかったので、クロス開発環境は整えず、ラズパイ本体で開発を行います。そのため、ホストOSが何であろうと、ラズパイを持っている方はすぐに試してみることが出来ます。(コーディングをWindows等のホストPCで行って、ラズパイにSFTP転送するという開発スタイルがおすすめです)

今回の内容

まずは、環境の準備と、簡単なカーネルモジュールを作ってみます。

本記事に登場するソースコード全体

https://github.com/take-iwiw/DeviceDriverLesson/tree/master/01_01
https://github.com/take-iwiw/DeviceDriverLesson/tree/master/01_02

環境

  • Raspberry Pi 2 (Raspbian stretch)

環境の準備

冒頭で述べたように、ラズパイ上で開発を行います。つまり、ネイティブ環境でビルドを行います。そのため、基本的にはラズパイ上のgccなどをそのまま使えるので、何の準備もいらないはずです。しかし、カーネルの機能にアクセスするためのヘッダ等が必要になります。下記コマンドでインストールします。これによってヘッダ一式とビルド用Makefileがここ(/usr/src/linux-headers-4.9.41-v7+/)にインストールされます。また、ここへのシンボリックリンクがここ(/lib/modules/4.9.41-v7+/build)に作成されます。(このディレクトリは僕の環境の場合)

sudo apt-get install raspberrypi-kernel-headers

カーネル全体をビルドするわけではないので、準備はこれだけです。

簡単なカーネルモジュールを作る

コードを書く

まずは、カーネルモジュールがロード(insmod)されたときと、アンロード(rmmod)されたときの挙動だけを書いてみます。Hello Worldのようなものです。ファイル名をtest.cとします。module_initでinsmod用のエントリポイントを、module_exitでrmmod用のエントリポイントを指定します。カーネル内ではprintfは使用できないのでprintkを使用します。

test.c
#include <linux/module.h>

static int test_init(void)
{
    printk("Hello my module\n");
    return 0;
}

static void test_exit(void)
{
    printk("Bye bye my module\n");
}

module_init(test_init);
module_exit(test_exit);

Makefileを用意する

カーネルモジュールのビルドは、自分でgccコマンドを打ってもできると思うのですが、色々とインクルードパスの指定とかが面倒です。上述したようにビルド用のMakefileが用意されているので、それを使います。下記のようなMakefileを作り、ソースファイル名を設定して、/lib/modules/4.9.41-v7+/buildにあるMakefileを呼びます。実際には4.9.41-v7+の部分は環境によって変わるのでunameコマンドで取得します。

Makefile
obj-m := test.o

all:
    make -C /lib/modules/$(shell uname -r)/build M=$(shell pwd) modules
clean:
    make -C /lib/modules/$(shell uname -r)/build M=$(shell pwd) clean

ビルドして実行する

上述のtest.cとMakefileのあるディレクトリで、makeします。すると、test.koが出来上がるはずです。その後、insmodでモジュールのロード、rmmodでモジュールのアンロードをしてみます。

make
sudo insmod test.ko
sudo rmmod test.ko
dmesg

最後に、dmesgでprintkで出力した結果を見てみます。すると、下記のように、実装したコードが実行されていることが分かります。

[ 2324.171986] Hello my module
[ 2327.753108] Bye bye my module

ログについての注意点

printkをするときに\nがないと、dmesgのログに出力されないようです。どうも、\nのタイミングでdmesg用のバッファに出力しているようです。

printkは、ターミナルではなく、メモリ上のバッファにその内容を出力します。そのバッファはリングバッファのようになっているのでそのうち上書きされます。dmesgはそのバッファを出力しているだけです。ファイルとしてはcat /var/log/syslogに保存されます。が、即時にこのファイルに書かれるのではなく、程よい頻度で書き込みが行われるようなので、ご注意ください。クラッシュした時などは、欲しいログが書き込まれていない、という可能性もあります。

複数ファイルから成るモジュールをビルドする

test01.cとtest02.cから、MyModule.koを作成する例です。以下のようなMakefileにします。MyModule.oを生成するのに必要な.oファイルをMyModule-objsで指定しています。そのため、MyModule-objsのプリフィックスはMyModuleである必要があります。出来上がったモジュールは、sudo insmod MyModule.koでロードできます。

CFILES = test01.c test02.c

obj-m := MyModule.o
MyModule-objs := $(CFILES:.c=.o)

all:
    make -C /lib/modules/$(shell uname -r)/build M=$(shell pwd) modules
clean:
    make -C /lib/modules/$(shell uname -r)/build M=$(shell pwd) clean

おわりに

という感じで、少しずつまとめていこうと思います。ぶっちゃけ僕の勉強メモです。
何か間違えていることを書いていたら、ぜひ教えてください。