🍓

raspberry piで学ぶ組込みLinuxデバイスドライバ開発

2024/01/27に公開

はじめに

1/24~26の3日間 仕事をサボっ.... 調整をしてポリテクセンター関東で行われた組込みLinuxデバイスドライバ開発技術というセミナーを受講してきました。

カーネルのVersionが2.6、対象のマイコンボードがSH-4というとても古いものだったので今回はラズパイで復習しながら、セミナーの内容を共有したいと思います。

↑がセミナーで使用したボードです。
LEDやタクトスイッチ、赤外線センサやモータがボートに付いているのでそれを制御するドライバを作成しました。

セミナーのテキストは2部構成で内容は以下の通りです。

第1部CPUボード編

  • 1章 ターゲットボードの確認
  • 2章 CPUボードの機能とデバイスドライバの確認
  • 3章 デバイスドライバ概要
  • 4章 モジュールの作成
  • 5章 キャラクタデバイスの作成
  • 6章 デバイスドライバの作成

第2部拡張IOボード編

  • 7章 属性ファイルの利用
  • 8章 既存のクラスの利用
  • 9章 割り込み
  • 10章 オープンソースの利用
  • 11章 インテグレーション

1章から3章ではセミナーで使用するボードのLEDやスイッチを既存のデバイスドライバで付けたりした後、デバイスドライバの概要について学びます。
4章から6章で実際のデバイスドライバを作成します。
7章からは属性ファイルを使用したデバイスドライバの作成方法を学びます。

Linuxデバイスドライバの作り方には2通りあり、/dev 以下にファイルが作成される方法と /sys/class 以下に作成される方法をセミナーでは属性ファイルと呼びました。
カーネル2.6以後は /sys/class で作成する方が階層的にファイルが作成されるので見通しがよくなります。
セミナーの第1部では /dev にファイルが作成される従来通りの作り方を、第2部で /sys/class にファイルを作成する方法を行いました。

これからセミナーの3章以降の内容をラズパイで追体験していきます。

カーネルの構造体や関数にリンクを貼っていますが、間違っていたらすみません。
また解説も初学者なので間違っていたらご指摘くださいm(--)m

3. デバイスドライバの概要

Linuxではユーザ空間のアプリケーションからは直接ハードウェアにアクセスできません。カーネルがシステムコールを受け取ると、操作対象のデバイスドライバに処理を委ねます。

Linuxのデバイスドライバには3種類あります。

  • キャラクタデバイス
  • ブロックデバイス
  • ネットワークインターフェイス

ほとんどのデバイスがキャラクタ型で、ブロック型はHDDやCDROMなどになります。

デバイスドライバの組込み方法は2種類あります。

  • カーネル組込み
  • ローダブル

カーネル組込み方式はカーネルビルド時に静的に組込む方式です。ラズパイやubuntuなどはOSイメージの中にドライバが入っているので起動すればすぐに使えます。
ローダブルはOS起動後にinsmodやmodprobeコマンドで動的に組込む方式です。カーネル組込み方式では開発効率が悪いので、以後はローダブル方式で作業を進めていきます。

以後の文章でモジュールと書いてますが、デバイスドライバと置き換えて読んでください。

4. モジュールの作成

今回は自宅にあったraspberry pi 3をターゲットにします。
SDカードにイメージを焼いたら起動して固定IPを設定して、SSH接続をしてデバイスドライバを作っていきます。

セミナーでは使用したSH-4ボードがメモリ64MBと貧弱なマシンでラズパイのようにボード上でプログラムを書いてコンパイルすることはできません。
そのため受講生用のWindowsマシンのVMWare PlayerにCentOSが開発マシン&TFTPサーバとして用意されていました。
SH-4ボードはTFTPブートをして、CentOSにあるルートファイルシステムを使います。

この仕組みはこれはこれで便利だったのですが、ラズパイ3はそこまで貧弱でもないので直接SSHしてラズパイ上でドライバを書いていきます。

開発に必要なヘッダファイルをインストールします。

$ sudo apt install raspberrypi-kernel-headers

適当なディレクトリを作成してtest_module.cファイルを以下の内容で作成します。

#include <linux/module.h>

// 初期化関数
static int __init test_init(void)
{
    printk("Init module!\n");
    return 0;
}

// 終了関数
static void __exit test_exit(void)
{
    printk("Exit module!\n");
}

module_init(test_init); // 初期化関数宣言
module_exit(test_exit); // 終了関数宣言
MODULE_LICENSE("GPL");  // ライセンス宣言

Makefileも以下の内容で作成し、

MODULEDIR = /lib/modules/`uname -r`/build
obj-m := test_module.o

modules:
        $(MAKE) -C $(MODULEDIR) M=`pwd` modules

clean:
        $(MAKE) -C $(MODULEDIR) M=`pwd` clean

make modulesで最初のモジュールを作成します。

$ make modules
make -C /lib/modules/`uname -r`/build M=`pwd` modules
make[1]: Entering directory '/usr/src/linux-headers-6.1.0-rpi7-rpi-v8'
  CC [M]  /home/pi/test/test_module.o
  MODPOST /home/pi/test/Module.symvers
  CC [M]  /home/pi/test/test_module.mod.o
  LD [M]  /home/pi/test/test_module.ko
make[1]: Leaving directory '/usr/src/linux-headers-6.1.0-rpi7-rpi-v8'

ビルドされたファイルをinsmodコマンドでロードします。
正常にロードされるとlsmodコマンドでtest_moduleが存在していることがわかります。
rmmodコマンドで削除します。

pi@raspberrypi:~/try-lkm/test $ sudo insmod test_module.ko
pi@raspberrypi:~/try-lkm/test $ lsmod | grep test
test_module            16384  0
pi@raspberrypi:~/try-lkm/test $ sudo rmmod test_module.ko

↑の上記操作をする前に別のterminalを開き、journalctl -kfを実行しておきます。(dmesgと同じ)
insmod時にtest_init関数が呼ばれ、rmmod時にtest_exit関数が呼ばれていることがわかります。

pi@raspberrypi:~ $ journalctl -kf | grep module
Jan 27 01:39:15 raspberrypi kernel: Init module!
Jan 27 01:39:32 raspberrypi kernel: Exit module!

初めてのモジュール

作成したソースを説明します。
普通のCプログラミングではmain関数がありました。C言語を勉強された方はprintf("Hello World\n");をmain関数の中に書いたと思います。

しかしモジュールはカーネル内部で動作するのでmain関数は存在しません。必要な時にカーネルから呼ばれる関数を記述します。
初期化関数はinsmodやmodprobeでロードされる時に実行される関数を記述し、module_initマクロで宣言をします。
終了関数はrmmodやmodprobe -rでアンロードされる時に実行される関数で、module_exitマクロで宣言をします。

この辺に関してはカーネルのDocumentaionに書いてあります。
https://www.kernel.org/doc/html/latest/driver-api/basics.html#

メッセージの出力には printf ではなく printk を使用します。
printfはユーザ空間で使える標準ライブラリの関数であって、カーネル空間では使えません。
カーネル空間には printk が用意されているので、これを利用します。

カーネル空間で使える関数は man に書かれていないので、カーネルのソースを読むか、↓などのカーネル開発のドキュメントを読むしかないとのことです。

https://www.kernel.org/doc/html/v6.1/core-api/index.html

モジュールの情報

モジュールには情報を追加することができます。
以下の3行を追加してビルドします。

MODULE_AUTHOR("sat0ken <15720506+sat0ken@users.noreply.github.com>");
MODULE_DESCRIPTION("I Do Nothing.");
MODULE_VERSION("1.0");

modinfoコマンドで追記した情報が表示されるようになります。

pi@raspberrypi:~/try-lkm/test $ modinfo test_module.ko
filename:       /home/pi/try-lkm/test/test_module.ko
version:        1.0
description:    I Do Nothing.
author:         sat0ken <15720506+sat0ken@users.noreply.github.com>
license:        GPL
srcversion:     DF8C17594C45AA19C9EB850
depends:
name:           test_module
vermagic:       6.1.0-rpi7-rpi-v8 SMP preempt mod_unload modversions aarch64

モジュールのパラメータ

モジュールはロード時や実行中に値を持つことができます。
まず変数を宣言します。

static int param = 5;

変数をmodule_paramマクロでパラメータとして宣言します。
このパラメータは他のモジュールからも参照できるので、パーミッションを指定します。

module_param(param, int, 0644);

変数をprintkで出力するようにして実行すると、値が出力されました。

pi@raspberrypi:~ $ journalctl -kf | grep module
Jan 27 03:00:46 raspberrypi kernel: Init module! param is 5
Jan 27 03:00:55 raspberrypi kernel: Exit module! param is 5

insmodコマンドではなく、modprobeコマンドを使うと任意の値を渡すことができます。
modprobeコマンドは /lib/module/カーネルバージョン にある .koファイルをロードするので、makeファイルでビルドしたモジュールをインストールします。

インストールするときはrootでないといけないので、sudo bash をしてから make modules_install をします。

$ sudo bash
root@raspberrypi:/home/pi/try-lkm/test# make modules_install
make -C /lib/modules/`uname -r`/build M=`pwd` INSTALL_MOD_PATH=/ modules_install
make[1]: Entering directory '/usr/src/linux-headers-6.1.0-rpi7-rpi-v8'
  INSTALL //lib/modules/6.1.0-rpi7-rpi-v8/extra/test_module.ko
  XZ      //lib/modules/6.1.0-rpi7-rpi-v8/extra/test_module.ko.xz
  DEPMOD  //lib/modules/6.1.0-rpi7-rpi-v8
Warning: modules_install: missing 'System.map' file. Skipping depmod.
make[1]: Leaving directory '/usr/src/linux-headers-6.1.0-rpi7-rpi-v8'

depmodコマンドを打つとモジュールが登録されます。modules.depファイルにtest_moduleが存在しているのがわかります。

# cat /lib/modules/6.1.0-rpi7-rpi-v8/modules.dep | grep ^extra/test
extra/test_module.ko.xz:

この状態でmodprobeコマンドを実行します。

root@raspberrypi:/home/pi/try-lkm/test# modprobe test_module param=20
root@raspberrypi:/home/pi/try-lkm/test# modprobe -r test_module

ログにはmodprobeコマンドで渡した20で変数が上書きされていることがわかります。

pi@raspberrypi:~ $ journalctl -kf | grep module
Jan 27 03:25:50 raspberrypi kernel: Init module! param is 20
Jan 27 03:25:56 raspberrypi kernel: Exit module! param is 20

依存モジュール

モジュールは他のモジュールの関数を呼ぶことができます。
モジュールAがモジュールBの関数を呼ぶ場合、モジュールAはBに依存関係があります。

以下の内容で、test_call.cファイルを作成します。
EXPORT_SYMBOLマクロは他のモジュールから関数を呼べるようにシンボルを公開します。

#include <linux/module.h>

void test_print(void)
{
    printk("test_print is called\n");
}

EXPORT_SYMBOL(test_print);
MODULE_LICENSE("GPL");

test_module.cから関数を呼ぶように宣言と初期化関数と終了関数を追記します。

#include <linux/module.h>

// test_call.cの関数
void test_print(void);
static int param = 5;

// 初期化関数
static int __init test_init(void)
{
    printk("Init module! param is %d\n", param);
    test_print();
    return 0;
}

// 終了関数
static void __exit test_exit(void)
{
    printk("Exit module! param is %d\n", param);
    test_print();
}

Makfileでtest_call.oもビルドするように追記してmake modulesをします。

pi@raspberrypi:~/try-lkm/test $ make modules
make -C /lib/modules/`uname -r`/build M=`pwd` modules
make[1]: Entering directory '/usr/src/linux-headers-6.1.0-rpi7-rpi-v8'
  CC [M]  /home/pi/try-lkm/test/test_module.o
  CC [M]  /home/pi/try-lkm/test/test_call.o
  MODPOST /home/pi/try-lkm/test/Module.symvers
  CC [M]  /home/pi/try-lkm/test/test_call.mod.o
  LD [M]  /home/pi/try-lkm/test/test_call.ko
  CC [M]  /home/pi/try-lkm/test/test_module.mod.o
  LD [M]  /home/pi/try-lkm/test/test_module.ko
make[1]: Leaving directory '/usr/src/linux-headers-6.1.0-rpi7-rpi-v8'

ビルドができたら、insmodでtest_moduleをロードしますがエラーとなります。
これはtest_moduleが呼ぶtest_callがまだロードされていないからです。
先にtest_callがロードされていないと依存関係があるため、test_moduleがロードできません。

pi@raspberrypi:~/try-lkm/test $ sudo insmod ./test_module.ko
insmod: ERROR: could not insert module ./test_module.ko: Unknown symbol in module

test_callをロードしてからtesu_moduleをロードすると今度は成功しました。

pi@raspberrypi:~/try-lkm/test $ sudo insmod ./test_call.ko
pi@raspberrypi:~/try-lkm/test $ sudo insmod ./test_module.ko

modprobeコマンドでは依存関係を調べて必要なものも含めていっしょにロードしてくれるので、基本的にmodprobeコマンドを使います。

5. キャラクタデバイスの作成

デバイスドライバが存在することでユーザはデバイスの種類を意識することなく、スイッチやセンサーの状態、ファイルシステムをopen, write, read, closeなどのシステムコールで同じようにプログラムを書くことができます。

デバイスドライバを作るために必要なことは、open, write, read, closeシステムコールが実行された時にカーネルから呼ばれる各処理を実装することです。

雛形の作成

open, write, read, closeシステムコールに対応するモジュールを雛形のSKEL(スケルトン)として作成します。
雛形の仕様は以下になります。

  • デバイスファイル名: /dev/skel
  • メジャー番号: 60

メジャー番号はデバイスごとにセットする番号です。カーネルソースのDocumentaion/device.txtに一覧があります。
一覧からローカルテスト用の60を選択します。

また cat /proc/devicesで利用しているメジャー番号が表示されます。

pi@raspberrypi:~/try-lkm/test $ cat /proc/devices | head -n 5
Character devices:
  1 mem
  4 /dev/vc/0
  4 tty
  5 /dev/tty

skel.cファイルを作成したら、makeしてinstallします。
ファイルの内容は動作確認後に説明します。

root@raspberrypi:/home/pi/try-lkm/skel# make modules
make -j4 -C /lib/modules/`uname -r`/build M=`pwd` modules
make[1]: Entering directory '/usr/src/linux-headers-6.1.0-rpi7-rpi-v8'
  CC [M]  /home/pi/try-lkm/skel/skel.o
  MODPOST /home/pi/try-lkm/skel/Module.symvers
  CC [M]  /home/pi/try-lkm/skel/skel.mod.o
  LD [M]  /home/pi/try-lkm/skel/skel.ko
make[1]: Leaving directory '/usr/src/linux-headers-6.1.0-rpi7-rpi-v8'
root@raspberrypi:/home/pi/try-lkm/skel# make modules_install
make -C /lib/modules/`uname -r`/build M=`pwd` INSTALL_MOD_PATH=/ modules_install
make[1]: Entering directory '/usr/src/linux-headers-6.1.0-rpi7-rpi-v8'
  INSTALL //lib/modules/6.1.0-rpi7-rpi-v8/extra/skel.ko
  XZ      //lib/modules/6.1.0-rpi7-rpi-v8/extra/skel.ko.xz
  DEPMOD  //lib/modules/6.1.0-rpi7-rpi-v8
Warning: modules_install: missing 'System.map' file. Skipping depmod.
make[1]: Leaving directory '/usr/src/linux-headers-6.1.0-rpi7-rpi-v8'
root@raspberrypi:/home/pi/try-lkm/skel# modprobe skel

journalctlでちゃんとmodprobeできるか確認します、大丈夫ですね。

pi@raspberrypi:~ $ journalctl -kf | grep SKEL
Jan 27 07:23:33 raspberrypi kernel: SKEL Init
Jan 27 07:23:33 raspberrypi kernel: SKEL Init: class_register OK
Jan 27 07:23:33 raspberrypi kernel: SKEL Init: platform_driver_register OK
Jan 27 07:23:33 raspberrypi kernel: SKEL Init: register_chrdev OK
Jan 27 07:23:33 raspberrypi kernel: SKEL Probe
Jan 27 07:23:33 raspberrypi kernel: SKEL Init: platform_device_register_simple is OK

modprobeすると/dev/skelファイルができていることが確認できます。
メジャー番号も指定した、60になっています。

root@raspberrypi:/home/pi/try-lkm/skel# ls -l /dev/skel
crw------- 1 root root 60, 0 Jan 27 07:23 /dev/skel

この/dev/skelファイルに対し、echo hoge > /dev/skelを実行すると、journalctlのほうでは、Open→Write→Releaseと実装したドライバの関数が呼ばれていることがわかります。
cat /dev/skelを実行すると、Open→Read→Releaseと呼ばれています。

pi@raspberrypi:~ $ journalctl -kf | grep SKEL
Jan 27 07:25:16 raspberrypi kernel: SKEL Open
Jan 27 07:25:16 raspberrypi kernel: SKEL Write
Jan 27 07:25:16 raspberrypi kernel: SKEL Release

Jan 27 07:25:48 raspberrypi kernel: SKEL Open
Jan 27 07:25:48 raspberrypi kernel: SKEL Read
Jan 27 07:25:48 raspberrypi kernel: SKEL Release

modprobe -rをすると/dev/skelファイルは消えます。

root@raspberrypi:/home/pi/try-lkm/skel# modprobe -r skel
root@raspberrypi:/home/pi/try-lkm/skel# ls /dev/skel
ls: cannot access '/dev/skel': No such file or directory

初期化処理

動作確認が出来たのでソースを説明します。
初期化関数では順に以下処理をしています。

  • クラスの登録
  • ドライバの登録
  • デバイス番号とファイル操作関数の登録
  • デバイスの登録

順番に処理を説明します。

  • クラスの登録

クラスはデバイスを管理しやすいようにグループ分けしたものです。/sys/classにあるのが登録されているクラスです。
skelが登録されるべきクラスは存在しないのでclass構造体で新しく定義をし、class_register関数で登録します。

ラズパイの/sys/class

pi@raspberrypi:~/try-lkm/skel $ ls /sys/class
backlight  devcoredump  extcon    hwmon        iscsi_connection  iscsi_transport  misc          nvme-subsystem  ptp        rtc          sound       udc       video4linux
bdi        devlink      gpio      i2c-adapter  iscsi_endpoint    leds             mmc_host      pci_bus         pwm        scsi_device  spi_master  uio       vtconsole
block      dma          gpiomem   ieee80211    iscsi_host        lirc             net           phy             rc         scsi_disk    spi_slave   usb_role  watchdog
bluetooth  dma_heap     graphics  input        iscsi_iface       mdio_bus         nvme          power_supply    regulator  scsi_host    thermal     vc
bsg        drm          hidraw    iommu        iscsi_session     mem              nvme-generic  pps             rfkill     skel         tty         vc-mem

クラスの登録処理

    // クラスの登録
    ret = class_register(&skel_class);
    if (ret != 0) {
        return ret;
    }
    printk("SKEL Init: class_register OK\n");
  • デバイスの登録

次にCPUとデバイスの通信経路となるバスにドライバを登録します。
バスには以下のような種類がありますが今回はプラットフォームバスに登録します。

- プラットフォームバス
- PCIバス
- USB

プラットフォームバスに登録するため、platform_driver構造体を使い、platform_driver_register関数で登録します。

    // プラットフォームバスにデバイスを登録
    ret = platform_driver_register(&skel_driver);
    if (ret != 0) {
        printk("SKEL Init: platform_driver_register is err %d\n", ret);
        class_unregister(&skel_class);
        return ret;
    }
    printk("SKEL Init: platform_driver_register OK\n");
  • デバイス番号とファイル操作関数の登録

file_operation構造体を使いファイル操作関数を登録します。
構造体のシステムコールの名前がついたメンバにドライバで実装した関数のアドレスを登録します。
そうすることで、openやwirteシステムコールの時にドライバの関数が呼ばれます。

// ファイル操作構造体
static const struct file_operations skel_fops = {
    .owner   = THIS_MODULE,
    .open    = skel_open,
    .release = skel_release,
    .read    = skel_read,
    .write   = skel_write,
};

register_chrdev関数の引数にメジャー番号、デバイス名、file_operation構造体をセットしてキャラクタデバイスを登録します。

    // キャラクタドライバの登録
    ret = register_chrdev(SKEL_MAJOR_NUM, DEV_NAME, &skel_fops);
    if (ret != 0) {
        printk("SKEL Init: register_chrdev is err %d\n", ret);
        platform_driver_unregister(&skel_driver);
        return ret;
    }
    printk("SKEL Init: register_chrdev OK\n");
  • デバイスの登録

最後にプラットフォームバスにデバイスを登録するplatform_device_register_simple関数を呼びます。
USBであれば抜いたり挿したりがありますが、/dev/skelはmodprobeした時から存在するのでこの関数を呼び登録をします。

    // プラットフォームバスにデバイスを登録
    pdev = platform_device_register_simple(DEV_NAME, -1, NULL, 0);
    if (!pdev) {
        printk("SKLE Init: platform_device_register_simple is err %d\n", ret);
        unregister_chrdev(SKEL_MAJOR_NUM, "skel");
        return -1;
    }

終了時の処理

終了時は初期化処理の逆から登録の解除をするだけです。

static void __exit skel_exit(void)
{
    printk("SKEL Exit\n");
    platform_device_unregister(pdev);   // プラットフォームバスからデバイスの登録を解除
    unregister_chrdev(SKEL_MAJOR_NUM, DEV_NAME);  // キャラクタ登録を解除
    platform_driver_unregister(&skel_driver);   // プラットフォームバスからドライバの登録を解除
    class_unregister(&skel_class);  // クラス登録の解除
}

probe関数とremove関数

IDをキーにしてデバイスとドライバを紐付けるのをバインドといいます。
ドライバ登録時やデバイス登録時にバインドされる時に呼ばれるのがprobe関数です。

ドライバのprobe関数内でデバイスチェック、初期化、デバイスファイル作成などを行います。
ここではdevice_create関数を呼んで実際にファイルを作成しています。

// probe関数
static int skel_probe(struct platform_device *pdev)
{
    struct device *dev;
    printk("SKEL Probe\n");

    // デバイスファイルを作成する
    dev = device_create(&skel_class, NULL, MKDEV(SKEL_MAJOR_NUM, 0), NULL, DEV_NAME);
    if (IS_ERR(dev)) {
        dev_err(&pdev->dev, "failed to create device.\n");
        return PTR_ERR(dev);
    }
    return 0;
}

remove関数ではデバイス終了処理、デバイスファイル削除を行います。
device_destroy関数でデバイスファイルを削除します。

// remove関数
static int skel_remove(struct platform_device *pdev)
{
    printk("SKEL Remove\n");
    // デバイスファイルを削除する
    device_destroy(&skel_class, MKDEV(SKEL_MAJOR_NUM, 0));
    return 0;
}

雛形からSTACKドライバの作成

これまで作成した雛形のSKELドライバをそのままコピーしてSTACKドライバを作ります。
make installしたらmodprobeをして動作確認をします。

root@raspberrypi:/home/pi/try-lkm/stack# modprobe stack
root@raspberrypi:/home/pi/try-lkm/stack# ls /dev/stack
/dev/stack
root@raspberrypi:/home/pi/try-lkm/stack# ls -l /dev/stack
crw------- 1 root root 60, 0 Jan 27 10:09 /dev/stack

/dev/stackファイルにechoで文字を3つ送ります。ここでは111, 222, 333を送りました。
続いてcatコマンドを3回実行すると、333, 222, 111の順番で文字が出力されました。

つまりドライバではユーザ空間から受け取った文字をスタックしておいて、順に取り出したということです。

root@raspberrypi:/home/pi/try-lkm/stack# echo 111 > /dev/stack
root@raspberrypi:/home/pi/try-lkm/stack# echo 222 > /dev/stack
root@raspberrypi:/home/pi/try-lkm/stack# echo 333 > /dev/stack
root@raspberrypi:/home/pi/try-lkm/stack# cat /dev/stack
333
root@raspberrypi:/home/pi/try-lkm/stack# cat /dev/stack
222
root@raspberrypi:/home/pi/try-lkm/stack# cat /dev/stack
111
root@raspberrypi:/home/pi/try-lkm/stack# cat /dev/stack
cat: /dev/stack: No data available

STACKドライバの処理

SKELドライバでは、open, write, read, closeのシステムコールが/dev/skelに実行されても各ハンドラ内ではprintkをしているだけでした。
STACKドライバではちゃんと各ハンドラに処理を記述します。

  • openハンドラ

ユーザ空間のプログラムがデバイスファイルをopenしたときに呼ばれるのが、openハンドラです。
カーネルから渡されるfile構造体のメンバでファイルが書き込みでopenされたのか、読み込みでopenされたのかがわかるのでそれで処理を分岐します。

書き込み時はメッセージを受信するためにkmallocでカーネル空間でメモリを確保します。
writeのハンドラでメッセージサイズに応じてkmallocしたほうがいい気がするし、ポリテクの先生とも「そう思いますよね〜」という会話したのですがそこはスルーします。

読み込み時はスタックのメッセージのポインタをfile構造体private_dataにセットし、readハンドラでメッセージを取り出せるようにします。

// openハンドラ
static int stack_open(struct inode *inode, struct file *file)
{
    char *msg;
    printk("STACK Open\n");

    if (file->f_mode & FMODE_WRITE) { // 書き込み時のとき
        if (msg_num == MAX_MSG_NUM) { // スタックが満杯時はエラーを返す
            return -ENOMEM;
        }
        msg = kmalloc(MAX_MSG_SIZE + 1, GFP_KERNEL);    // kmallocでメモリを確保
        if (msg == NULL) {
            return -ENOMEM;
        }
        // 受信したメッセージのポインタをfile構造体のprivate_dataにセット
        // そうすることでread, releaseハンドラでメッセージを取り出す
        file->private_data = msg;
    } else if (file->f_mode & FMODE_READ) {
        if (msg_num == 0) { // スタックが空のときはエラーを返す
            return -ENODATA;
        }
        msg_num--;
        msg = pmsg[msg_num];
        file->private_data = msg;
    } else {
        return -EINVAL;
    }

    return 0;
}
  • writeハンドラ

writeハンドラではユーザ空間からwriteされたデータを受け取り保存しておきます。
メモリ空間が異なるので、copy_from_user関数でカーネル空間に書き込まれたデータをコピーします。

// writeハンドラ
static ssize_t stack_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos)
{
    // ファイル構造体のprivate_dataに記録していた
    // メッセージのアドレスを取り出す
    char *msg = file->private_data;
    printk("STACK Write\n");

    if (*ppos == MAX_MSG_NUM) { // メッセージが最大サイズの場合はエラーを返す
        return -ENOSPC;
    }
    if (count > MAX_MSG_NUM - *ppos) {
        count = MAX_MSG_NUM - *ppos;
    }
    // ユーザ空間のメッセージデータをカーネル空間にコピー
    if (copy_from_user(msg + *ppos, buf, count)) {
        return -EFAULT;
    }
    *ppos += count; // データ取得した分だけファイルポインタを進める
    msg[*ppos] = '\0';
    printk("message is %s\n", msg);

    return count;   // 取得したデータサイズを返す
}
  • readハンドラ

readハンドラはデバイスファイルに対して、readされた時に呼ばれます。動作確認のcatコマンドではこのハンドラが呼ばれます。
catコマンドにデータを返すために、スタックからメッセージを取り出しユーザ空間のメモリにコピーします。

引数で返す先となるユーザ空間のアドレスが渡されるので、copy_to_user関数でコピーします。

// readハンドラ
static ssize_t stack_read(struct file *file, char __user *buf, size_t count, loff_t *ppos)
{
    int len;
    char *msg = file->private_data;
    printk("STACK Read\n");

    len = strlen(msg); // メッセージサイズを取得
    if (len - *ppos == 0) { // メッセージがすべて転送された
        return 0;
    }
    if (count > len - *ppos) {
        count = len - *ppos;
    }
    // カーネル空間からユーザ空間にメッセージデータをコピー
    if (copy_to_user(buf, msg + *ppos, count)) {
        return -EFAULT;
    }
    *ppos += count;

    return count;   // 転送したデータサイズを返す
}
  • releaseハンドラ

closeした時に呼ばれるのがreleaseハンドラです。
STACKドライバでは書き込み時にメッセージをスタックに入れ、読み込み時はメッセージ領域のメモリを解放します。

// releaseハンドラ
static int stack_release(struct inode *inode, struct file *file)
{
    // ファイル構造体のprivate_dataに記録していた
    // メッセージのアドレスを取り出す
    char *msg = file->private_data;
    printk("STACK Release\n");

    if (file->f_mode & FMODE_WRITE) {   // 書き込み時
        pmsg[msg_num] = msg;
        msg_num++;
    } else {    // 読み込み時
        kfree(msg);
    }

    return 0;
}

STACKドライバの解説は以上です。

To be continued...

ここまで読んでいたいた方はお気づきかもしれませんが、SKELドライバとSTACKドライバはラズパイに関係ありません。
ラズパイに接続したLEDやスイッチ、センサを制御しているわけではないからです。

というわけでラズパイがなくても普通のLinuxマシン上で、ドライバ作成は可能です、たぶん。
※筆者は試してませんので、自己責任でお願いしますm(--)m

セミナーの第6章 デバイスドライバの作成では使用したボードに載っているLEDとスイッチのドライバを作成しました。
ラズパイでは回路を別途作ってドライバを作ることにします。

というわけで続く...

最後にレポジトリのURLを貼っておきます。

https://github.com/sat0ken/try-lkm

参考文献

https://qiita.com/iwatake2222/items/1fdd2e0faaaa868a2db2
https://www.kernel.org/doc/html/latest/driver-api/basics.html#driver-entry-and-exit-points
https://doc.kusakata.com/driver-api/index.html
https://manpages.debian.org/jessie/linux-manual-3.16/index.html

その他Tips

  • 久しぶりにラズパイをセットアップしたら dhcpcd.confがなくてつまずく。nmcliから設定するみたいでした。
$ sudo nmcli connection modify 'Wired connection 1' ipv4.method manual ipv4.addresses xxx.xxx.xxx.xxx/24 ipv4.gateway xxx.xxx.xxx.xxxx ipv4.dns xxx.xxx.xxx.xxx
$ sudo nmcli connection reload
$ sudo nmcli connection up 'Wired connection 1'

https://okamoto3.com/index.php/2023/11/16/bookworm-static-ip/3295/

Discussion