組込み Linux デバイスドライバの作り方 その1 Hello (まずuBuntuだけで)

初心者のいちりがいろいろなサイトを見て作ったのをメモしておきます。 Linuxの仕様はどんどん変わるので、2023年のKernel version 5.15で作成してみます。 まずクロスコンパイラを使わず手軽にuBuntu上でHelloをやってみます。 まず、クロスコンパイルの必要のないuBuntu上で実行して、理解を深めます。 その後、Beaglebone Black用のドライバとドライバにアクセスするアプリをuBuntuで作成〜クロスコンパイル〜転送・実行していきます。

デバイスドライバとは

Linuxはユーザーモードとカーネルモードの動作モードがあり、ユーザーモードで動作するアプリケーションは、直接ハードウェアにアクセス出来ません。 ハードウェアにアクセス出来るKernelにアクセスしてハードウェアにアクセスします。 KernelにアクセスできるのはKernelモジュールで、Kernelモジュールとしてデバイスドライバを作成します。 デバイスドライバはKernelモジュールの一種です。 アプリケーションはデバイスドライバーに直接アクセスできないので、デバイスドライバーをデバイス特殊ファイルにして、デバイス特殊ファイル経由でKernel経由でハードウェアにアクセスします。

アプリケーション => デバイス特殊ファイル => Kernel(=>デバイスドライバ) => ハードウェア

LinuxのデバイスドライバはOS起動時にメモリに常駐し、周辺装置からのハード的な割り込みで駆動される一種の関数群です。 ドライバは、KernelにビルドされるStatic(静的)方式とルートFSにファイルとして存在し、必要も応じてinsmodで組み込むDynamic(動的)方式があり、Kernelビルド時に選択します。(Dynamic モジュール操作コマンド:insmod, rmmod, lsmod, modprobe, depmod, mknod)

デバイスの種類

  • キャラクタ型デバイスドライバ
    • 1バイト単位の入出力をするデバイス。(シーケンシャルアクセス) シリアル通信、GPIOなど
  • ブロック型デバイス
    • 512B、1024Bなどのブロック単位の入出力をするデバイス。(ランダムアクセス) 主にストレージ(SSD,DDR)。 マウントするとファイルシステムとして利用できる。 Linuxではキャラクタ型デバイスのようにバイト単位でも読み書きできる。
  • ネットワークデバイスドライバ
    • 通信に使用される
    • デバイスファイルからのアクセスはできない

Linuxでは、全てのデバイスをファイルシステムの1ファイルとして扱います。 アプリケーションからデバイスドライバは見えず、mknodでデバイスノードとして仮想化された『/dev/xxx』というデバイス特殊ファイルだけが見えます。(create_class(), create_device()でも作成可能)

アプリケーションがハードウェハへのアクセス要求した時、Kernelはデバイス特殊ファイルで定義された『メジャー番号』を使ってデバイスドライバテーブルを検索しドライバ関数を呼び出します。

『マイナー番号』は、COM1、COM2など同一デバイスドライバを使用してポート番号が異なる場合などに使用されます。

デバイスドライバ構成

3つから構成されます。

  1. 初期化ルーチン
    • OS起動時にKernelから1度だけコールされる部分。
    • 対象のハードフェアの存在確認し、
    • OSにデバイスドライバとして認識させる。
  2. トップハーフルーチン(Top Half)
    • アプリケーションがopen、read、writeのシステムコールをすると、Kernelがこの関数をコールする。
    • この関数がハードウェアからデータ読書し、結果をKernelに返す。
  3. ボトムハーフルーチン(Bottom Half)
    • ハードウェア割込みによって駆動されるハンドラ

デバイスドライバ作成

uBuntu 20.04 LTSで作ってみます。 デバイスドライバは、Kernelモジュールの一種です。 Kernelモジュールは通常のアプリのようにユーザー空間で実行さないので、標準Cライブラリを使用できません。 KernelモジュールはKernel空間で実行される為、Kernel専用のコマンドや関数が必要になります。

headers

デバイスドライバ作成にはheadersが必要です。 このheadersには各種includeする.hファイルが含まれています。(module.h, kernel.h, cdev.h, proc_fs.h など)これらの.hファイル内にKernel空間で動作する必要な変数、関数、マクロが含まれています。

場所:/usr/src/linux-headers-5.15.0-56-generic/include/linux

まず、headersがあるかどうか確認。 以下のコマンドで表示されたら、headerが既に入っています。 ない場合はインストールする必要があります。 私の場合は、既にありました。

$ apt search linux-headers-$(uname -r)
Sorting... Done
Full Text Search... Done
linux-headers-5.15.0-56-generic/focal-updates,focal-security,now 5.15.0-56.62~20.04.1 amd64 [installed,automatic]
  Linux kernel headers for version 5.15.0 on 64 bit x86 SMP

headersがなければ、Linux kernel versionを確認して、同じversionのheadersをインストールする。

$ uname -r
5.15.0-56-generic

$ sudo apt install linux-headers-5.15.0-56-generic

test.c

まず、モジュール(デバイスドライバ)を作成するディレクトリを用意して、Permissionを変更します。 Permissionは775でも、『mkdir: cannot create directory ‘/home/ichiri/test/drivers/.tmp_番号’: Permission denied』が沢山表示されてモジュールは作成できませんでしたが、777にするとモジュールを作成出来ました。

$ sudo mkdir -p~/test/drivers
$ cd ~/test
$ sudo chmod 777 drivers  //775でもOK
$ cs drivers

  • 下のようなファイルを作ります。
  • MODULE_LICENSEがないとモジュール(.ko:Kernel Object)が作成されません。 
  • モジュールはKernelにアクセスするので、Kernelメッセージで表示するprintk(print kernel)を使います。
  • モジュール(.ko: Kernel Object)はシェルやアプリケーションなどユーザー空間で実行されなく、Kernel空間で動作します。 その為、プログラムの書き方はLinux Kernelと同じで、標準ライブラリは利用できず、Linux Kernel内部で用意されている関数やを利用します。 その為、printfは使用できず、printkを使います。 module_init()とmodule_exit()、MODULE_LICENSE()は、Linux Kernel Headerのlinux/module.hで用意されているマクロです。
~/test/drivers/test.c
#include <linux/module.h> // module_init(), module_exit()
MODULE_LICENSE("GPL");

static int __init test_init(void)
{
    printk("Hello Ichiri's module\n");
    return 0;
}

static void __exit test_exit(void)
{
    printk("Bye bye Ichiri's module\n");
}

module_init(test_init);
module_exit(test_exit);

__init, __exitマクロ

__initは、Compilerディレクティブ(指令)の__section(.init.text)のaliasで、メモリ上の初期化関数用の領域(.init.text)に関数を配置します。そして、__initマクロで指定した関数による初期化が終了後、その初期化関数はKernel build systemがメモリ上から破棄(解放)します。 これでメモリを節約出来ます。

__exitマクロも同様で、メモリ上の終了関数用の領域(.exit.text)に関数を配置します。その終了関数は、実行後にKernel build systemによってメモリ上から破棄(解放)されます。

// linux init.h
# define __init        __section(.init.text)
# define __initdata    __section(.init.data)    //<--変数用(Variable)
# define __initconst   __section(.init.rodata)

# define __exit        __section(.exit.text)

return 0

Kernel モジュールロードが成功した時は0を返さないと行けない。 0でない場合は失敗とみなされる。 exitも0を返さなければならない。

Makefile

Makefileを作ります。 Makefileはtest.cと同じディレクトリに作ってください。

下のコードをコピペする時は、コピペ後にmakeの前の空白を全て削除してタブでインデントを入れてください。 これをしないと空白が入り、『No rule to make target ‘make’』エラーでmakeを実行出来ません。 Makefileのコマンド部分の先頭はタブ出ないといけないからです。

~/test/drivers/Makefile
obj-m := test.o

all:TARGET1
TARGET1: test.c
        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

Makefileのコメントは行の先頭に#を入れます。

ビルド

  1. test.cとMakefileが存在するディレクトリでmakeコマンドでビルドします。
  2. モジュールをInsertして(insmod)
  3. モジュールをremoveして(rmmod)
  4. /var/log/syslogにログされたKernelメッセージを表示します。(dmesg)

HelloもBye Byeもちゃんと出ましたね。 『その1』はここまでです。

~/test/drivers/
$ make
$ sudo insmod test.ko
$ sudo rmmod test.ko
$ dmesg | tail -5
     :
[ 6152.583573] Hello Ichiri's module    // -5で最新のKernelメッセージ5行が表示され、最後にtest.koのprintkのメッセージが表示されます。
[ 6155.736013] Bye bye Ichiri's module

(参考)複数のターゲットをコンパイルする場合

Makefile
//方法1
obj-m := test.o
all: TARGET1 TARGET2
TARGET1: test1.c
     gcc -o TARGET1 -Wall hoge.c
TARGET2: test2.c
     gcc -o TARGET2 -Wall moge.c

//方法2
CFILES = test1.c test2.c

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

obj-mは、Kernelに組み込まないモジュールをビルドすることを示しています。 obj-yとすると、Kernelに組み込む[=y]としたことになります。 今回は、Kernelに組み込まないモジュール(.ko)を作るので、obj-mとしています。

Kernel message

Kernelモジュールをmakeでビルドした後、dmesgでKernelメッセージを見るとwarningが出ています。 これらは問題でないので無視出来ます。

『loading out-of-tree…』は、Kernelに組み込まない、動的(Dynamic)なモジュールとして生成した時に表示される警告です。

『module verification …』は、uBuntuがmodule signature verificationをonにしているので表示されます。 表示を消すには、signatureをuBuntuに登録する必要があるようです。 しかし、これも表示されていても問題ありません。

$ dmesg | tail -10
        :
main: loading out-of-tree module taints kernel.
main: module verification failed: signature and/or required key missing -  tainting kernel

コメント