Raspberry Pi3上でキャラクタ型デバイスドライバを作る

Raspberry Pi3上でキャタクタ型デバイスドライバを作る.
以下の文献を参考にした.特に3件目のコードをいじって動かした.

ロード・アンロードだけできるカーネルモジュール開発

カーネルヘッダ等をインストールする.

sudo apt-get install raspberrypi-kernel-headers

ロード・アンロードだけできるカーネルモジュール

#include <linux/module.h>

MODULE_LICENSE("Dual BSD/GPL");
MODULE_LICENSE("Yusuke Ujitoko");

static int char_bufsize = 1;

static int test_init(void)
{
  printk(KERN_ALERT "driver loaded\n");
  return 0;
}

static void test_exit(void)
{
  printk(KERN_ALERT "driver unloaded\n");
  return;
}

module_param(char_bufsize, int, S_IRUGO | S_IWUSR);
module_init(test_init);
module_exit(test_exit);
  • module_initマクロに渡した関数がロード時に呼ばれる
  • module_exitマクロに渡した関数がアンロード時に呼ばれる

makefileをかく。

obj-m := test.o

all:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) V=1 modules

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

ビルドしたカーネルモジュールをinsmodrmmod でロード・アンロードできる。 その上でdmesgなどでカーネルバッファを確認できる。

メジャー番号とマイナー番号の動的な登録

カーネルからドライバを識別するためのメジャー番号や、 ドライバからデバイスを識別するためのマイナー番号を登録する。 静的に行う方法もあるが、動的に行うのが一般的。

メジャー番号の動的確保はalloc_chrdev_region() で行う。 またメジャー番号の削除は unregister_chrdev_region() で行う。 これらの処理を加えたコードを次に示す。

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/types.h>
#include <linux/kdev_t.h>

#define DEVICE_NAME "char_dev"
#define MINOR_BASE 0
#define MINOR_NUM 2


MODULE_LICENSE("Dual BSD/GPL");
MODULE_LICENSE("Yusuke Ujitoko");

static int char_bufsize = 1;
static int char_major = 0;
static int char_minor = 0;

static int test_setup_device_number(void)
{
  printk(KERN_ALERT "setup device number dynamically\n");

  int alloc_ret = 0;
  int cdev_err = 0;
  dev_t dev;
  
  /* alloc free major number */
  alloc_ret = alloc_chrdev_region(&dev, MINOR_BASE, MINOR_NUM, DEVICE_NAME);
  if (alloc_ret != 0){
    printk(KERN_ERR "alloc_chrdev_region = %d\n", alloc_ret);
    return -1;
  }

  char_major = MAJOR(dev);
  char_minor = MINOR(dev);
  printk("major num: %d\n", char_major);
  printk("minor num: %d\n", char_minor);
  
  return 0;
}

static int test_clear_device_number(void)
{
  dev_t dev = MKDEV(char_major, MINOR_BASE);
  unregister_chrdev_region(dev, MINOR_NUM);

  return 0;
}

static int test_init(void)
{
  printk(KERN_ALERT "driver loaded\n");
  test_setup_device_number();
  return 0;
}

static void test_exit(void)
{
  printk(KERN_ALERT "driver unloaded\n");
  test_clear_device_number();
  return;
}

module_param(char_bufsize, int, S_IRUGO | S_IWUSR);
module_init(test_init);
module_exit(test_exit);

カーネルモジュールをカーネルへ登録する

cdev_init()でシステムコードハンドラを登録し、 cdev_addでカーネルへドライバ登録する。

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/types.h>
#include <linux/kdev_t.h>
#include <linux/cdev.h>

#define DEVICE_NAME "char_dev"
#define MINOR_BASE 0
#define MINOR_NUM 2


MODULE_LICENSE("Dual BSD/GPL");
MODULE_LICENSE("Yusuke Ujitoko");

static int char_bufsize = 1;
static int char_major = 0;
static int char_minor = 0;
static dev_t dev;
static struct cdev my_device_cdev;
struct file_operations my_device_fops = {
};

static int test_setup_device_number(void)
{
  printk(KERN_ALERT "setup device number dynamically\n");

  int alloc_ret = 0;
  
  /* alloc free major number */
  alloc_ret = alloc_chrdev_region(&dev, MINOR_BASE, MINOR_NUM, DEVICE_NAME);
  if (alloc_ret != 0){
    printk(KERN_ERR "alloc_chrdev_region = %d\n", alloc_ret);
    return -1;
  }

  char_major = MAJOR(dev);
  char_minor = MINOR(dev);
  printk("major num: %d\n", char_major);
  printk("minor num: %d\n", char_minor);
  
  return 0;
}

static int test_setup_cdev(void)
{
  int cdev_err = 0;

  cdev_init(&my_device_cdev, &my_device_fops);
  my_device_cdev.owner = THIS_MODULE;
  
  cdev_err = cdev_add(&my_device_cdev, dev, MINOR_NUM);
  if (cdev_err != 0){
    printk(KERN_ERR "cdev_add = %d\n", cdev_err);
    unregister_chrdev_region(dev, MINOR_NUM);
  }
      
}

static int test_clear_device_number(void)
{
  dev_t dev = MKDEV(char_major, MINOR_BASE);
  unregister_chrdev_region(dev, MINOR_NUM);

  return 0;
}

static int test_clear_cdev(void)
{
  cdev_del(&my_device_cdev);
  return 0;
}

static int test_init(void)
{
  int result;
  printk(KERN_ALERT "driver loaded\n");
  result = test_setup_device_number();
  if (result != 0){
    return -1;
  }

  result = test_setup_cdev();
  if (result != 0){
    return -1;
  }
  
  return 0;
}

static void test_exit(void)
{
  printk(KERN_ALERT "driver unloaded\n");
  test_clear_cdev();
  test_clear_device_number();
  return;
}

module_param(char_bufsize, int, S_IRUGO | S_IWUSR);
module_init(test_init);
module_exit(test_exit);

open/close/read/writeハンドラを登録する

ユーザプロセスがデバイスファイに対してopen()やclose()などのシステムコールを発行できるようにするには、 ドライバがそれらに対応したハンドラをカーネルへ事前に登録しておく必要がある。 file_operations構造体でドライバのハンドラを設定する。

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/types.h>
#include <linux/kdev_t.h>
#include <linux/cdev.h>

#define DEVICE_NAME "char_dev"
#define MINOR_BASE 0
#define MINOR_NUM 2


MODULE_LICENSE("Dual BSD/GPL");
MODULE_LICENSE("Yusuke Ujitoko");

static int char_bufsize = 1;
static int char_major = 0;
static int char_minor = 0;
static dev_t dev;
static struct cdev my_device_cdev;

static int dev_open(struct inode *inode, struct file *file)
{
  printk("device open\n");
  return 0;
}

static int dev_close(struct inode *inode, struct file * file)
{
  printk("device close\n");
}

static ssize_t dev_read(struct file *filp, char __user * buf, size_t count, loff_t *f_pos)
{
  printk("device read\n");
  buf[0] = 'A';
  return 1;
}

static ssize_t dev_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos)
{
  printk("device write\n");
  return 1;
}

struct file_operations my_device_fops = {
  .open =  dev_open,
  .release = dev_close,
  .read = dev_read,
  .write = dev_write,
};

  
static int test_setup_device_number(void)
{
  printk(KERN_ALERT "setup device number dynamically\n");

  int alloc_ret = 0;
  
  /* alloc free major number */
  alloc_ret = alloc_chrdev_region(&dev, MINOR_BASE, MINOR_NUM, DEVICE_NAME);
  if (alloc_ret != 0){
    printk(KERN_ERR "alloc_chrdev_region = %d\n", alloc_ret);
    return -1;
  }

  char_major = MAJOR(dev);
  char_minor = MINOR(dev);
  printk("major num: %d\n", char_major);
  printk("minor num: %d\n", char_minor);
  
  return 0;
}

static int test_setup_cdev(void)
{
  int cdev_err = 0;

  cdev_init(&my_device_cdev, &my_device_fops);
  my_device_cdev.owner = THIS_MODULE;
  
  cdev_err = cdev_add(&my_device_cdev, dev, MINOR_NUM);
  if (cdev_err != 0){
    printk(KERN_ERR "cdev_add = %d\n", cdev_err);
    unregister_chrdev_region(dev, MINOR_NUM);
  }
      
}

static int test_clear_device_number(void)
{
  dev_t dev = MKDEV(char_major, MINOR_BASE);
  unregister_chrdev_region(dev, MINOR_NUM);

  return 0;
}

static int test_clear_cdev(void)
{
  cdev_del(&my_device_cdev);
  return 0;
}

static int test_init(void)
{
  int result;
  printk(KERN_ALERT "driver loaded\n");
  result = test_setup_device_number();
  if (result != 0){
    return -1;
  }

  result = test_setup_cdev();
  if (result != 0){
    return -1;
  }
  
  return 0;
}

static void test_exit(void)
{
  printk(KERN_ALERT "driver unloaded\n");
  test_clear_cdev();
  test_clear_device_number();
  return;
}

module_param(char_bufsize, int, S_IRUGO | S_IWUSR);
module_init(test_init);
module_exit(test_exit);

データがダーティであるとは

データがダーティである,という意味がわからなかったので調べた結果をメモ.
ある実体とそのコピーがある状態で, コピーが変更されてまだその変更が実体に反映される前の段階のことを言う.

wikipediaのページングのページにも当該用語が用いられている箇所がある.

ページングの主たる機能は、プログラムがその時点で物理メモリ (RAM) のマッピングされていないページにアクセスしようとしたときに実行される。これをページフォールトと呼ぶ。オペレーティングシステムページフォールトによって制御を得て、プログラムからは見えない形で処理を行う。その流れは次のようになる。
補助記憶装置内での要求されたデータの位置を特定する。 RAM上の空のページフレームを取得。 要求されたデータをそのページフレームにロードする。 ページテーブルを更新してそのページフレームをマッピングする。 要求元プログラムに制御を戻し、ページフォールトを発生した命令を透過的に再実行させる。
これを「ページイン」と呼ぶ。必要とされる全データを格納できるほどRAMがないという状態になるまで、空のページフレームを取得する処理はRAM上の使用中ページを奪う処理を伴わない。全ページフレームが使用中の場合、空のページフレームを得るには使用中のページフレームを選んで空にする処理が必要となる。選択したページフレーム内のデータが前回ロードされてから変更されている場合( いわゆる「ダーティ」状態 )、二次記憶装置の対応する位置に書き戻さないと解放できない。これを「ページアウト」と呼ぶ。そうでない場合、選択したページフレームの内容は二次記憶装置の所定の位置にあるものと同じなので、書き戻す必要がない。そのように使用中のページを奪った場合、もともとそのページを使っていたプロセスがそのページにアクセスしようとした場合、同様に空のページフレームを取得して、ページインする必要がある。

Linuxデバイスドライバプログラミングを読みつつRaspberry Piでデバドラ開発(5章)

5章はドライバプログラミングの基礎知識について.

連結リスト

Linuxカーネルには,ドライバへの連結リストの実装を容易化する構造体やマクロが提供されている.

連結リストは次のlist_head構造体を用いて実装する.
この構造体をどのように利用すればよいかが一見わからない.

struct list_head{
    struct list_head *next, *prev;
};

なにかの構造体にリスト構造を持たせるためには,
struct list_head型のメンバを定義する必要がある.
定義する場所はどこでもよい.
以下ではint型のidをリストで扱う例を示す.

struct sample_data {
    int no;
    struct list_head list;
};

このstruct list_headのメンバは,
ドライバのロード時にINIT_LIST_HEAD関数で以下のように一度だけ初期化しておく必要がある.

struct sample_data data
INIT_LIST_HEAD(&data.list);

リストに要素を追加するには,list_add関数を使用する.
第一引数には新しい要素のstruct list_head型のメンバをポインタで渡す.
第二引数にはリストのHEADとなるポインタを渡す.

末尾に追加するにはlist_add_tail()を代わりに使う.
これによりキュー型のリストを作れる.

仮想メモリ

readやwriteといったシステムコールを扱うときは,
ユーザプロセスとデバイスドライバ間でデータの受け渡しを行うことになるが,
別々のメモリ空間上で動作しているため,互いが直接アクセスできない.

そこでLinuxカーネルではユーザプロセスとドライバ間でのデータコピー用のカーネル関数(マクロ)が用意されている.

Linuxデバイスドライバプログラミングを読みつつRaspberry Piでデバドラ開発(4章)

4章

ここからはラズパイ上で簡単なカーネルモジュールを作成する

ドライバのビルドに必要なパッケージ

sudo apt-get install raspberypi-kernel-headers

これによりヘッダとビルド用Makefileがインストールされる.

簡単なカーネルモジュールとMakefile

#include <linux/module.h>

MODULE_LICENSE("Dual BSD/GPL");

static int hello_init(void)
{
  printk(KERN_ALERT "driver loaded\n");
  return 0;
}

static void hello_exit(void)
{
  printk(KERN_ALERT "driver unloaded\n");
}


module_init(hello_init);
module_exit(hello_exit);
  • MODULE_LICENSEマクロによるライセンス指定が必要。ない場合はドライバのロード時に警告が表示される。
  • module_initやmodule_exitマクロでロード時・アンロード時に呼ばれるエントリポイントの指定を行う。
  • printf()はドライバでは使えないのでprintk()を使う。
obj-m := hello.o

all:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) V=1 modules

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

makeすると5つのファイルが出てくる。

Linuxデバイスドライバプログラミングを読みつつRaspberry Piでデバドラ開発(1章~3章)

Linuxデバイスドライバプログラミングを読んで,
Raspberry Pi上でデバイスドライバを作ってみる.
挫折しないようにメモ.

1章~3章は読むだけで開発は含まれない.

1章

2章

  • insmodとmodprobeの違い
    • この2つのコマンドの違いは、modprobeコマンドは、modules.depファイルを参照し、依存関係のあるモジュールがあれば、事前にロードを行います。それに対し、insmodコマンドは、指定されたモジュールのみをロードします。そのため、事前にロードする必要のある依存するモジュールが存在する場合には、エラーとなります。
      https://users.miraclelinux.com/technet/document/linux/training/2_1_1.html

3章

  • ドライバのデバッグにはprintf()は使えない.
    「ドライバはカーネルにリンクされた状態で動作するため,標準出力という概念が存在しない」(←意味がわからない)
    printfはcのライブラリで内部でwriteシステムコールを使っていたりして,そもそも使えない.
  • 代わりにprintk()という関数が用意されている.printk()はカーネルバッファという小さなデータ領域(128KB)にメッセージを書き込む.
    カーネルバッファはリングバッファになっている.
    カーネルバッファの中身を確認するにはdmesgコマンド等を利用するか,もしくは,
    syslogdやklogdがカーネルバッファをsyslog(/var/log/message)に定期的に出力しているので、syslogを見て確認しても良い.

  • 不正なメモリアクセスを行った場合,ドライバ開発では最悪OSが落ちる. これをkernel panicという.