Linuxカーネルにおける文字デバイスドライバの開発

一、文字デバイスドライバの概要

文字デバイスはLinuxドライバの中で最も基本的なデバイスドライバの一つです。文字デバイスはバイト単位で、バイトストリームとして読み書き操作を行うデバイスです。読み書きデータは前後の順序に従います。例えば、私たちが最もよく知っているLED点灯、ボタン、IIC、SPI、LCDなどはすべて文字デバイスであり、これらのデバイスのドライバは文字デバイスドライバと呼ばれます。

Linuxアプリケーションがドライバプログラムを呼び出す方法:

ドライバが正常にロードされると、"/dev"ディレクトリに対応するファイルが生成されます。アプリケーションはこの"/dev/xxx"(xxxは具体的なドライバファイル名)という名前のファイルに対して対応する操作を行うことで、ハードウェアの操作を実現できます。例えば、現在"/dev/led"という名前のドライバファイルがあるとします。このファイルはLEDのドライバファイルです。アプリケーションはopen関数を使用して/dev/ledファイルを開き、使用後はclose関数を使用して/dev/ledファイルを閉じます。openとcloseはLEDドライバを開いたり閉じたりする関数です。LEDを点灯または消灯するには、write関数を使用して操作します。つまり、このドライバにデータを書き込み、そのデータがLEDを開いたり閉じたりする制御パラメータです。

アプリケーションはユーザースペースで実行され、Linuxドライバはカーネルの一部であるため、ドライバはカーネルスペースで実行されます。ユーザースペースからカーネルの操作を実現したい場合、例えば/dev/ledというドライバをopen関数で開く場合、ユーザースペースはカーネルを直接操作できないため、"システムコール"という方法を使用してユーザースペースからカーネルスペースに"陥る"必要があります。これにより、下位レベルのドライバの操作を実現できます。open、close、write、readなどの関数はCライブラリによって提供され、Linuxシステムでは、システムコールはCライブラリの一部として提供されます。open関数を呼び出すときのフローは以下の通りです:

アプリケーションで使用される関数には、具体的なドライバプログラムに対応する関数が存在します。例えば、アプリケーションでopenという関数を呼び出した場合、ドライバプログラムにもopenという名前の関数が必要です。

Linuxカーネルファイルinclude/linux/fs.hにはfile_operationsという構造体があります。この構造体はLinuxカーネルドライバ操作関数のコレクションです。内容は以下の通りです:

struct file_operations {
    struct module *owner;    // ownerはこの構造体を所有するモジュールへのポインタ、通常はTHIS_MODULEに設定
    loff_t (*llseek) (struct file *, loff_t, int);    // llseek関数はファイルの現在の読み書き位置を変更するために使用
    ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);    // read関数はデバイスファイルからデータを読み取るために使用
    ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *); // デバイスにデータを送信
    ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
    ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
    int (*iopoll)(struct kiocb *kiocb, bool spin);
    int (*iterate) (struct file *, struct dir_context *);
    int (*iterate_shared) (struct file *, struct dir_context *);
    __poll_t (*poll) (struct file *, struct poll_table_struct *);    // pollは非ブロッキングの読み書きが可能かどうかを照会するためのポーリング関数
    long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);    // unlocked_ioctl関数はデバイスの制御機能を提供し、アプリケーションのioctl関数に対応
    long (*compat_ioctl) (struct file *, unsigned int, unsigned long);    // compat_ioctl関数はunlocked_ioctl関数と同じ機能ですが、64ビットシステム上で32ビットのアプリケーションが呼び出す場合に使用
    int (*mmap) (struct file *, struct vm_area_struct *);    // mmap関数はデバイスのメモリをプロセススペース(ユーザースペース)にマッピングするために使用。通常、フレームバッファデバイス(LCDドライバのフレームバッファなど)がこの関数を使用し、フレームバッファ(LCDのフレームバッファ)をユーザースペースにマッピングすると、アプリケーションは直接フレームバッファを操作できます。これにより、ユーザースペースとカーネルスペースの間でデータをコピーする必要がなくなります。
    unsigned long mmap_supported_flags;
    int (*open) (struct inode *, struct file *);    // open関数はデバイスファイルを開くために使用
    int (*flush) (struct file *, fl_owner_t id);
    int (*release) (struct inode *, struct file *);    // release関数はデバイスファイルを解放(閉じる)するために使用し、アプリケーションのclose関数に対応
    int (*fsync) (struct file *, loff_t, loff_t, int datasync); // fasync関数は保留中のデータをフラッシュするために使用し、バッファ内のデータをディスクにフラッシュします
    int (*fasync) (int, struct file *, int);
    int (*lock) (struct file *, int, struct file_lock *);
    ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
    unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
    int (*check_flags)(int);
    int (*flock) (struct file *, int, struct file_lock *);
    ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
    ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
    int (*setlease)(struct file *, long, struct file_lock **, void **);
    long (*fallocate)(struct file *file, int mode, loff_t offset, loff_t len);
    void (*show_fdinfo)(struct seq_file *m, struct file *f);
#ifndef CONFIG_MMU
    unsigned (*mmap_capabilities)(struct file *);
#endif
    ssize_t (*copy_file_range)(struct file *, loff_t, struct file *, loff_t, size_t, unsigned int);
    loff_t (*remap_file_range)(struct file *file_in, loff_t pos_in, struct file *file_out, loff_t pos_out, loff_t len, unsigned int remap_flags);
    int (*fadvise)(struct file *, loff_t, loff_t, int);
} __randomize_layout;

二、文字デバイスドライバの開発手順

  1. ドライバモジュールのロードとアンロード

Linuxドライバには2つの実行方法があります。第一はドライバをLinuxカーネルにコンパイルし、Linuxカーネルが起動すると自動的にドライバプログラムが実行される方法です。第二はドライバをモジュール(Linuxでは.koという拡張子)にコンパイルし、Linuxカーネルが起動した後に"modprobe"または"insmod"コマンドを使用してドライバモジュールをロードする方法です。これら2つのコマンドの違いは、modprobeはモジュールとその依存関係を自動的に解析してロードし、insmodは基本的な手動ロード機能を提供し、特定のシナリオに適しています。ドライバをデバッグする場合は、通常ドライバをモジュールとしてコンパイルします。これにより、ドライバを修正した後はドライバコードをコンパイルするだけでよく、Linux全体のコードをコンパイルする必要がありません。また、デバッグ時にはシステム全体を再起動する必要なく、ドライバモジュールをロードまたはアンロードするだけで済みます。要するに、ドライバをモジュールとしてコンパイルする最大の利点は開発が容易なことです。ドライバの開発が完了し、問題がないことが確認できたら、ドライバをLinuxカーネルにコンパイルできます。もちろん、Linuxカーネルにコンパイルしないこともできます。

モジュールにはロードとアンロードの2つの操作があります。ドライバを記述する際には、これら2つの操作関数を登録する必要があります。モジュールのロードとアンロード登録関数は以下の通りです:

module_init(xxx_init); // モジュールロード関数を登録
module_exit(xxx_exit); // モジュールアンロード関数を登録

module_init関数はLinuxカーネルにモジュールロード関数を登録するために使用され、パラメータxxx_initは登録する具体的な関数です。ドライバを"modprobe"コマンドでロードすると、xxx_initという関数が呼び出されます。module_exit関数はLinuxカーネルにモジュールアンロード関数を登録するために使用され、パラメータxxx_exitは登録する具体的な関数です。ドライバを"rmmod"コマンドでアンロードすると、xxx_exit関数が呼び出されます。文字デバイスドライバモジュールのロードとアンロードテンプレートは以下の通りです:

/* ドライバエントリ関数 */
static int __init xxx_init(void)    // 静的で整数型を返すxxx_init関数を定義し、__initマクロで修飾;__init:関数をカーネル初期化関数としてマークするマクロ。これは関数がモジュールロード時にのみ実行され、ロード完了後に自動的にメモリから削除され、リソースを解放することを意味します
{
    /* エントリ関数の具体的な内容 */
    return 0;
}

/* ドライバエクスポート関数 */
static void __exit xxx_exit(void)    // 静的で戻り値なしのxxx_exit関数を定義し、__exitマクロで修飾;__exit:関数をカーネル終了関数としてマークするマクロ。これは関数がモジュールアンロード時にのみ実行され、アンロード完了後に自動的にメモリから削除されることを意味します
{
    /* エクスポート関数の具体的な内容 */
}

/* 上記2つの関数をドライバのエントリとエクスポート関数として指定 */
module_init(xxx_init);    // module_init関数を呼び出してxxx_initをドライバエントリ関数として宣言し、ドライバをロードするときにxxx_init関数が呼び出されます
module_exit(xxx_exit);    // module_exit関数を呼び出してxxx_exitをドライバエクスポート関数として宣言し、ドライバをアンロードするときにxxx_exit関数が呼び出されます

ドライバのコンパイルが完了すると拡張子は.koになります。例えばdrv.koというドライバモジュールをロードする場合:

/* ドライバのロードにはmodprobeが推奨されます。なぜなら、モジュールの依存関係の分析、エラーのチェック、エラーレポートなどの機能を提供するからです */
modprobe -a drv    // drv.koドライバモジュールをロード

/* ドライバのアンロードにはrmmodが推奨されます。なぜなら、modprobeはドライバモジュールが依存する他のモジュールもアンロードするからです。しかし、時には私たちもこれらのモジュールを使用しています */
rmmod drv.ko       // drv.koドライバモジュールをアンロード
  1. 文字デバイスの登録と登録解除

文字デバイスドライバの場合、ドライバモジュールが正常にロードされると文字デバイスを登録する必要があり、同様に、ドライバモジュールをアンロードする際にも文字デバイスを登録解除する必要があります。文字デバイスの登録と登録解除関数のプロトタイプは以下の通りです:

static inline int register_chrdev(unsigned int major,
                                  const char *name,
                                  const struct file_operations *fops)
static inline void unregister_chrdev(unsigned int major,
                                     const char *name)

/*
static:ここでのstaticは静的関数で、スコープは現在のファイル内で、現在のファイルの他の関数からのみ呼び出すことができます
inline:コンパイラにこの関数をインライン展開することを提案します。インライン展開とは、関数呼び出しの代わりに関数コードを直接挿入することを意味します。これにより、関数呼び出しのオーバーヘッドを減らすことができます

register_chrdev関数は文字デバイスを登録するために使用されます。この関数には合計3つのパラメータがあります:
major:主デバイス番号、Linuxでは各デバイスにデバイス番号があり、デバイス番号は主デバイス番号と副デバイス番号に分かれています
name:デバイス名、文字列へのポインタ
*fops:struct file_operations型のポインタ、デバイスの操作関数コレクション変数を指します

unregister_chrdev関数は文字デバイスを登録解除するために使用されます。この関数には2つのパラメータがあります:
major:登録解除するデバイスの主デバイス番号
name:登録解除するデバイスのデバイス名
*/

通常、文字デバイスの登録はドライバモジュールのエントリ関数xxx_initで行われ、文字デバイスの登録解除はドライバモジュールのエクスポート関数xxx_exitで行われます:

static struct file_operations test_fops;    // file_operations構造体変数test_fopsを定義。test_fopsはデバイス操作関数のコレクションです。現在メンバ変数を初期化していないため、この関数コレクションは空です

/* ドライバエントリ関数 */
static int __init xxx_init(void) 
{
    /* エントリ関数の具体的な内容 */
    int retvalue = 0;

    /* 文字デバイスドライバを登録 */
    retvalue = register_chrdev(200, "chrtest", &test_fops);    // 主デバイス番号200、デバイス名"chrtest"、デバイス操作関数コレクション"test_fops"。使用されていない主デバイス番号を選択し、コマンド"cat /proc/devices"で現在使用されているデバイス番号を確認できます
    if (retvalue < 0) {
        /* 文字デバイスの登録に失敗しました。独自の処理を行ってください */
    }
    return 0;
}

/* ドライバエクスポート関数 */
static void __exit xxx_exit(void) 
{
    /* 文字デバイスドライバを登録解除 */
    unregister_chrdev(200, "chrtest");    // unregister_chrdev関数を呼び出して主デバイス番号200のこのデバイスを登録解除
}

/* 上記2つの関数をドライバのエントリとエクスポート関数として指定 */
module_init(xxx_init);
module_exit(xxx_exit);
  1. デバイスの具体的な操作関数の実装

① chrtestを開いたり閉じたりできるようにする

デバイスの開閉は最も基本的な要件であり、ほぼすべてのデバイスが開閉機能を提供する必要があります。したがって、file_operationsのopenとreleaseという2つの関数を実装する必要があります。

② chrtestに対する読み書き操作

chrtestというデバイスがバッファ(メモリ)を制御していると仮定します。アプリケーションはreadとwriteという2つの関数を使用してchrtestのバッファに対して読み書き操作を行う必要があります。コードは以下の通りです:

/* デバイスを開く */
static int chrtest_open(struct inode *inode, struct file *filp) 
{
    /* ユーザーが具体的な機能を実装 */
    return 0;
}

/* デバイスから読み取る */
static ssize_t chrtest_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)    // __user:ユーザースペースのポインタをマーク
{
    /* ユーザーが具体的な機能を実装 */
    return 0;
}

/* デバイスにデータを書き込む */
static ssize_t chrtest_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt) 
{
    /* ユーザーが具体的な機能を実装 */
    return 0;
}

/* デバイスを閉じる/解放する */
static int chrtest_release(struct inode *inode, struct file *filp)
{
    /* ユーザーが具体的な機能を実装 */
    return 0;
}

// test_fopsという構造体変数の初期化操作を追加
static struct file_operations test_fops = 
{
    .owner = THIS_MODULE,
    .open = chrtest_open,
    .read = chrtest_read,
    .write = chrtest_write,
    .release = chrtest_release,
};

/* ドライバエントリ関数 */
static int __init xxx_init(void) 
{
    /* エントリ関数の具体的な内容 */
    int retvalue = 0;

    /* 文字デバイスドライバを登録 */
    retvalue = register_chrdev(200, "chrtest", &test_fops);
    if (retvalue < 0) {
        /* 文字デバイスの登録に失敗しました。独自の処理を行ってください */
    }
    return 0;
}

/* ドライバエクスポート関数 */
static void __exit xxx_exit(void) 
{
    /* 文字デバイスドライバを登録解除 */
    unregister_chrdev(200, "chrtest");
}

/* 上記2つの関数をドライバのエントリとエクスポート関数として指定 */
module_init(xxx_init);
module_exit(xxx_exit);

最初に4つの関数を記述しました:chrtest_open、chrtest_read、chrtest_write、chrtest_release。これら4つの関数はchrtestデバイスのopen、read、write、release操作関数です。

  1. LICENSEと作者情報の追加

最後にドライバにLICENSE情報と作者情報を追加する必要があります。その中でLICENSEは必須であり、そうでないとコンパイル時にエラーが発生します。作者情報は追加してもしなくてもかまいません。LICENSEと作者情報の追加には以下の2つの関数を使用します:

MODULE_LICENSE() // モジュールLICENSE情報を追加
MODULE_AUTHOR() // モジュール作者情報を追加

完全な文字デバイスドライバモジュールの内容は以下の通りです:

/* デバイスを開く */
static int chrtest_open(struct inode *inode, struct file *filp) 
{
    /* ユーザーが具体的な機能を実装 */
    return 0;
}

/* デバイスから読み取る */
static ssize_t chrtest_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)    // __user:ユーザースペースのポインタをマーク
{
    /* ユーザーが具体的な機能を実装 */
    return 0;
}

/* デバイスにデータを書き込む */
static ssize_t chrtest_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt) 
{
    /* ユーザーが具体的な機能を実装 */
    return 0;
}

/* デバイスを閉じる/解放する */
static int chrtest_release(struct inode *inode, struct file *filp)
{
    /* ユーザーが具体的な機能を実装 */
    return 0;
}

// test_fopsという構造体変数の初期化操作を追加
static struct file_operations test_fops = 
{
    .owner = THIS_MODULE,
    .open = chrtest_open,
    .read = chrtest_read,
    .write = chrtest_write,
    .release = chrtest_release,
};

/* ドライバエントリ関数 */
static int __init xxx_init(void) 
{
    /* エントリ関数の具体的な内容 */
    int retvalue = 0;

    /* 文字デバイスドライバを登録 */
    retvalue = register_chrdev(200, "chrtest", &test_fops);
    if (retvalue < 0) 
    {
        /* 文字デバイスの登録に失敗しました。独自の処理を行ってください */
    }
    return 0;
}

/* ドライバエクスポート関数 */
static void __exit xxx_exit(void) 
{
    /* 文字デバイスドライバを登録解除 */
    unregister_chrdev(200, "chrtest");
}

/* 上記2つの関数をドライバのエントリとエクスポート関数として指定 */
module_init(xxx_init);
module_exit(xxx_exit);

/* license情報と作者情報を追加 */
MODULE_LICENSE("GPL");          // LICENSE情報を追加
MODULE_AUTHOR("luoxuesong");    // 作者情報を追加

三、Linuxデバイス番号

  1. デバイス番号の構成

デバイス番号は主デバイス番号と副デバイス番号の2つの部分で構成されます。主デバイス番号は特定のドライバを表し、副デバイス番号はそのドライバを使用する各デバイスを表します。Linuxはdev_tというデータ型を提供しており、dev_tはファイルinclude/linux/types.hで定義されており、以下のように定義されています:

typedef __u32 __kernel_dev_t;
......
typedef __kernel_dev_t dev_t;

dev_tは__u32型であり、__u32はファイルinclude/uapi/asm-generic/int-ll64.hで定義されており、以下のように定義されています:

typedef unsigned int __u32;

dev_tは実際にはunsigned int型であり、32ビットのデータ型です。この32ビットのデータは主デバイス番号と副デバイス番号の2つの部分で構成されており、そのうち高12ビットが主デバイス番号、低20ビットが副デバイス番号です。したがって、Linuxシステムにおける主デバイス番号の範囲は0~4095であり、この範囲を超えることはできません。ファイルinclude/linux/kdev_t.hにはデバイス番号に関するいくつかの操作関数(実質はマクロ)が提供されています:

#define MINORBITS 20    // 副デバイス番号のビット数、合計20ビット
#define MINORMASK ((1U << MINORBITS) - 1)    // 副デバイス番号のマスク

#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))    // dev_tから主デバイス番号を取得し、dev_tを20ビット右シフト
#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))    // dev_tから副デバイス番号を取得し、dev_tの下位20ビットを取得
#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))    // 指定された主デバイス番号と副デバイス番号の値をdev_t型のデバイス番号に組み合わせる
  1. デバイス番号の割り当て

① 静的デバイス番号の割り当て

文字デバイスを登録する際には、デバイスにデバイス番号を指定する必要があります。デバイス番号はドライバ開発者が自分で指定することができ、例えば以前の200のようなものです。cat/proc/devicesでデバイス番号を確認し、どれが使用されているかを確認できます。

② 動的デバイス番号の割り当て

静的デバイス番号の割り当ては競合の問題が発生しやすいため、動的デバイス番号の割り当てを使用することが推奨されます。文字デバイスを登録する前にデバイス番号を申請し、システムが自動的に使用されていないデバイス番号を割り当て、競合を避けることができます。

デバイス番号の申請関数は以下の通りです:

int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);

/*
dev:申請されたデバイス番号を保存
baseminor:副デバイス番号の開始アドレス、この関数は連続した複数のデバイス番号を申請できます。これらのデバイス番号は主デバイス番号は同じですが、副デバイス番号は異なります。副デバイス番号はbaseminorを開始アドレスとして増加します。通常baseminorは0であり、つまり副デバイス番号は0から開始します
count:申請するデバイス番号の個数
name:デバイス名
*/

デバイス番号の解放関数は以下の通りです:

void unregister_chrdev_region(dev_t from, unsigned count);

/*
from:解放するデバイス
count:fromから開始して、解放するデバイス番号の数量
*/

四、chrdevbase文字デバイスドライバ開発実験

chrdevbaseは仮想デバイスです。chrdevbaseデバイスには2つのバッファがあります。1つは読み取りバッファ、もう1つは書き込みバッファで、両方のバッファのサイズは100バイトです。アプリケーションではchrdevbaseデバイスの書き込みバッファwritebufにデータを書き込むことができ、読み取りバッファreadbufからデータを読み取る操作を行うことができます。操作が完了したら、アプリケーションはclose関数を使用してchrdevbaseデバイスを閉じます(開く、読み取る、書き込む、閉じる)。

  1. プログラムの記述

アプリケーションはopen関数を使用してchrdevbaseというデバイスを開き、開いた後はwrite関数を使用してchrdevbaseの書き込みバッファwritebufにデータを書き込むことができます(100バイトを超えない)。また、read関数を使用して読み取りバッファreadbufからデータを読み取る操作を行うこともできます。

① VScodeプロジェクトの作成

この実験のすべてのファイルを保存する1_chrdevbaseフォルダを作成します。

VScodeプロジェクトを作成し、chrdevbase.cファイルを新規作成します。

② ヘッダーファイルパスの追加

Linuxドライバを記述するため、Linuxソースコードの関数を使用する必要があります。VSCodeでLinuxソースコードのヘッダーファイルパスを追加する必要があります。VScodeでSHIFT+CTRL+Pを押し、C/C++: Edit configurations(JSON)と入力します。

③ プログラムの記述

#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>

#define DEVBASE_MAJOR 200              /* 主デバイス番号 */
#define DEVBASE_NAME "devbase"      /* デバイス名 */

static char readbuf[100];                 /* 読み取りバッファ */
static char writebuf[100];                /* 書き込みバッファ */
static char kerneldata[] = {"カーネルデータ!"};

/*
 * @description : デバイスを開く
 * @param – inode : ドライバに渡されるinode
 * @param - filp : デバイスファイル、file構造体にはprivate_dataというメンバ変数があり、通常open時にprivate_dataをデバイス構造体に指向します。
 * @return : 0 成功;その他 失敗
 */
static int devbase_open(struct inode *inode, struct file *filp)
{
    // printk("devbase open!\r\n");  // printfはユーザースペースで実行され、printkはカーネルスペースで実行されます。
    return 0;
}

/*
 * @description : デバイスからデータを読み取る
 * @param - filp : 開くデバイスファイル(ファイルディスクリプタ)
 * @param - buf : ユーザースペースに返すデータバッファ
 * @param - cnt : 読み取るデータの長さ
 * @param - offt : ファイル先頭アドレスからのオフセット
 * @return : 読み取ったバイト数、負の値の場合は読み取り失敗を示します
 */
static ssize_t devbase_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)   // ssize_t:関数が返すバイト数またはエラーコード
{
    int retvalue = 0;

    /* ユーザースペースにデータを送信 */                                // memcpy()は指定されたサイズに基づいてkerneldataの内容をコピーし、readbufに貼り付けます。その中にはsizeof(kerneldata)バイトあります
    memcpy(readbuf, kerneldata, sizeof(kerneldata));     // kerneldataはユーザースペースが読み取るデータを保存しています
    retvalue = copy_to_user(buf, readbuf, cnt);          // カーネルスペースはユーザースペースのメモリを直接操作できないため、copy_to_user関数を介してカーネルスペースからユーザースペースへのコピーを完了する必要があります。
    if (retvalue == 0) {
        printk("カーネル送信データOK!\r\n");
    } else {
        printk("カーネル送信データ失敗!\r\n");
    }

    // printk("devbase read!\r\n");
    return 0;
}

/*
 * @description : デバイスにデータを書き込む
 * @param - filp : デバイスファイル、開いているファイルディスクリプタを表します
 * @param - buf : デバイスに書き込むデータ
 * @param - cnt : 書き込むデータの長さ
 * @param - offt : ファイル先頭アドレスからのオフセット
 * @return : 書き込んだバイト数、負の値の場合は書き込み失敗を示します
 */
static ssize_t devbase_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
    int retvalue = 0;

     /* ユーザースペースからカーネルに渡されたデータを受け取り、表示します */         // パラメータbufはアプリケーションがデバイスに書き込むデータです
    retvalue = copy_from_user(writebuf, buf, cnt);   // ユーザースペースのメモリはカーネルスペースのメモリに直接アクセスできないため、copy_from_user関数を介してユーザースペースのデータをwritebufというカーネルスペースにコピーする必要があります
    if (retvalue == 0) {
        printk("カーネル受信データ:%s\r\n", writebuf);
    } else {
        printk("カーネル受信データ失敗!\r\n");
    }

    // printk("devbase write!\r\n");
    return 0;
}

/*
 * @description : デバイスを閉じる/解放する
 * @param - filp : 閉じるデバイスファイル(ファイルディスクリプタ)
 * @return : 0 成功;その他 失敗
 */
static int devbase_release(struct inode *inode,struct file *filp)    // アプリケーションがcloseでデバイスファイルを閉じるときにこの関数が実行されます
{
    // printk("devbase release!\r\n");
    return 0;
}

/*
 * デバイス操作関数構造体
 */
static struct file_operations devbase_fops = {
    .owner = THIS_MODULE,
    .open = devbase_open,
    .read = devbase_read,
    .write = devbase_write,
    .release = devbase_release,
};

/*
 * @description : ドライバエントリ関数
 * @param : なし
 * @return : 0 成功;その他 失敗
 */
static int __init devbase_init(void)
{
    int retvalue = 0;

    /* 文字デバイスドライバを登録 */
    retvalue = register_chrdev(DEVBASE_MAJOR, DEVBASE_NAME, &devbase_fops);
    if (retvalue < 0) {
        printk("devbaseドライバ登録失敗\r\n");
    }
    return 0;
}

/*
 * @description : ドライバエクスポート関数
 * @param : なし
 * @return : 0 成功;その他 失敗
 */
static void __exit devbase_exit(void)
{
    /* 文字デバイスドライバを登録解除 */
    unregister_chrdev(DEVBASE_MAJOR, DEVBASE_NAME);
    printk("devbase_exit()\r\n");
}

/*
 * 上記2つの関数をエントリとエクスポート関数として指定
 */
module_init(devbase_init);
module_exit(devbase_exit);

/*
 * LICENSEと作者情報
 */
MODULE_LICENSE("GPL");
MODULE_AUTHOR("TECHCOMPANY");    
MODULE_INFO(intree, "Y");   // これを追加しないと"loading out-of-tree module taints kernel."という警告が表示されます

ここでprintfはユーザースペースで実行され、printkはカーネルスペースで実行されます。また、printkはログレベルに基づいて分類することができ、合計8つのレベルがあります。ファイルinclude/linux/kern_levels.hにあります:

#define KERN_SOH "\001"
#define KERN_EMERG KERN_SOH "0" /* 緊急イベント、通常はカーネルクラッシュ */
#define KERN_ALERT KERN_SOH "1" /* 直ちに行動を起こす必要がある */
#define KERN_CRIT KERN_SOH "2" /* 臨界条件、例えば重大なソフトウェアまたはハードウェアエラー */
#define KERN_ERR KERN_SOH "3" /* エラー状態、通常はデバイスドライバプログラムでKERN_ERRを使用してハードウェアエラーを報告 */
#define KERN_WARNING KERN_SOH "4" /* 警告情報、システムに重大な影響を与えない */
#define KERN_NOTICE KERN_SOH "5" /* 提示が必要な情報 */
#define KERN_INFO KERN_SOH "6" /* 提示情報 */
#define KERN_DEBUG KERN_SOH "7" /* デバッグ情報

ここで0が最も高い優先度、7が最も低い優先度です。例えば:

printk(KERN_EMERG "gsmi: ログシャットダウン理由\n");

具体的なメッセージの前にKERN_EMERGを追加すると、このメッセージの優先度がKERN_EMERGに設定されます。printkを使用する際にメッセージレベルを明示的に設定しない場合、MESSAGE_LOGLEVEL_DEFAULTというデフォルトレベルが使用され、ファイルinclude/linux/printk.hにはCONSOLE_LOGLEVEL_DEFAULTというマクロの定義があります:

#define CONSOLE_LOGLEVEL_DEFAULT CONFIG_CONSOLE_LOGLEVEL_DEFAULT

MESSAGE_LOGLEVEL_DEFAULTとCONFIG_CONSOLE_LOGLEVEL_DEFAULTはカーネルのグラフィカルインターフェースで設定されます:

-> Kernel hacking
    -> printk and dmesg options
        -> (7) Default console loglevel (1-15) // デフォルトのコンソールメッセージレベルを設定
        -> (4) Default message log level (1-7) // デフォルトのメッセージレベルを設定

CONSOLE_LOGLEVEL_DEFAULTはコンソールに表示されるメッセージのレベルを制御し、このマクロのデフォルトは7です。つまり、優先度が7より高いメッセージのみがコンソールに表示されます。デフォルトのメッセージレベルは4であり、4のレベルは7より高いので、printkを使用して直接出力された情報はコンソールに表示されます。

  1. アプリケーションの記述

① Cライブラリファイル操作基本関数

テストAPPを記述することはLinuxアプリケーションを記述することであり、open、read、write、closeという4つのファイル操作に関するCライブラリの関数を使用する必要があります。

1、open関数

/*
 * @param - pathname : 開くデバイスまたはファイル名
 * @param -  flags : ファイルオープンモード、以下の3つのモードのいずれかを選択する必要があります:                   
                     O_RDONLY 読み取り専用モード
                     O_WRONLY 書き込み専用モード
                     O_RDWR 読み書きモード
 * @return : ファイルが正常に開かれた場合はファイルディスクリプタを返します
 */
int open(const char *pathname, int flags);

2、read関数

/*
 * @param - fd : 読み取るファイルディスクリプタ、ファイルを読み取る前にopen関数を使用してファイルを開く必要があります。open関数がファイルを正常に開くとファイルディスクリプタが得られます
 * @param - buf : データがこのbufに読み込まれます
 * @param - count : 読み取るデータの長さ、バイト数
 * @return : 読み取りに成功した場合は読み取ったバイト数を返します;0が返された場合はファイルの終わりに達したことを示します;負の値が返された場合は読み取り失敗を示します
 */
ssize_t read(int fd, void *buf, size_t count);

3、write関数

/*
 * @param - fd : 書き込み操作を行うファイルディスクリプタ、ファイルを読み取る前にopen関数を使用してファイルを開く必要があります。open関数がファイルを正常に開くとファイルディスクリプタが得られます
 * @param - buf : 書き込むデータ
 * @param - count : 書き込むデータの長さ、バイト数
 * @return : 書き込みに成功した場合は書き込まれたバイト数を返します;0が返された場合はデータが書き込まれなかったことを示します;負の値が返された場合は書き込み失敗を示します
 */
ssize_t write(int fd, const void *buf, size_t count);

4、close関数

/*
 * @param - fd : 閉じるファイルディスクリプタ
 * @return : 0は正常に閉じられたことを示し、負の値は閉じる失敗を示します
 */
int close(int fd);

② テストAPPプログラムの記述

テストAPPはユーザースペースで実行されます。テストAPPは入力されたコマンドに基づいてchrdevbaseデバイスに対して読み取りまたは書き込み操作を実行します。1_chrdevbaseディレクトリにchrdevbaseApp.cファイルを新規作成し、このファイルに以下の内容を入力します:

#include "stdio.h"          // printf()やscanf()などの入出力関数の宣言を提供
#include "unistd.h"         // オペレーティングシステムへのアクセスや操作を提供し、プロセス制御、ファイル操作など
#include "sys/types.h"      // 基本的なデータ型を定義し、ファイルディスクリプタ型int、プロセスID pid_tなど
#include "sys/stat.h"       // ファイル状態の取得と設定関数を含み、stat()やchmod()など
#include "fcntl.h"          // ファイル制御関連の定数と関数を定義し、ファイルオープンモードの設定やファイルロック操作など
#include "stdlib.h"         // 一般的な関数とマクロ定義を提供し、メモリ割り当て関数malloc()や乱数生成関数rand()など
#include "string.h"         // 文字列処理関数の宣言を提供し、文字列コピー関数strcpy()や文字列比較関数strcmp()など


static char usrdata[] = {"ユーザーデータ!"};  // chrdevbaseデバイスに書き込むデータ

/*
* @description : mainメインプログラム
* @param - argc : argv配列の要素数
* @param - argv : 具体的なパラメータ
* @return : 0 成功;その他 失敗
*/
int main(int argc, char *argv[])
{
    int fd, retvalue;
    char *filename;
    char readbuf[100], writebuf[100];

/*
 例えばコマンドを入力:./chrdevbaseApp /dev/chrdevbase 1
 このコマンドには3つのパラメータがあり、"./chrdevbaseApp"、" /dev/chrdevbase"および"1"はそれぞれargv[0]、argv[1]およびargv[2]に対応します。
 最初のパラメータはchrdevbaseAPPソフトウェアを実行し、2番目のパラメータはテストAPPが/dev/chrdevbaseというデバイスを開くことを示し、3番目のパラメータは実行する操作で、1はchrdevbaseからデータを読み取ることを示し、2はchrdevbaseにデータを書き込むことを示します。
 */
    if(argc != 3)       // パラメータが3つあるかどうかを判断し、不足している場合はテストAPPの使用法が間違っていることを示します
    {      
        printf("エラー使用法!\r\n");
        return -1;
    }

    filename = argv[1];

    /* ドライバファイルを開く */
    fd = open(filename, O_RDWR);        // Cライブラリのopenを使用して/dev/chrdevbaseデバイスファイルを開く
    if(fd < 0){
        printf("ファイル %s を開けません\r\n", filename);
        return -1;
    }

    if(atoi(argv[2]) == 1)      // atoi:文字列形式の数字を実際の数字に変換
    { 
        /* ドライバファイルからデータを読み取る */
        retvalue = read(fd, readbuf, 50);
        if(retvalue < 0){
            printf("ファイル %s の読み取りに失敗しました!\r\n", filename);
        }else{
            /* 読み取りに成功し、読み取り成功したデータを表示 */
            printf("読み取ったデータ:%s\r\n",readbuf);
        }
    }

    if(atoi(argv[2]) == 2){
        /* デバイスドライバにデータを書き込む */
        memcpy(writebuf, usrdata, sizeof(usrdata));
        retvalue = write(fd, writebuf, 50);
        if(retvalue < 0){
            printf("ファイル %s の書き込みに失敗しました!\r\n", filename);
        }
    }

    /* デバイスを閉じる */
    retvalue = close(fd);
    if(retvalue < 0){
        printf("ファイル %s を閉じることができません\r\n", filename);
        return -1;
    }

    return 0;
}
  1. ドライバプログラムとテストAPPのコンパイル

① ドライバプログラムのコンパイル

まずドライバプログラム、つまりchrdevbase.cファイルをコンパイルする必要があります。これを.koモジュールにコンパイルする必要があります。Makefileファイルを作成します:

KERNELDIR = /home/tech/linux/atk-mpl/linux/my_linux/linux-5.4.31	# Linuxカーネルソースコードパス
CURRENT_PATH = $(shell pwd)		# 現在のパスを取得
obj-m := devbase.o		# chrdevbase.cをdevbase.koモジュールにコンパイル

build: kernel_modules

kernel_modules:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules	 
clean: 
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean

# modulesはモジュールをコンパイルすることを示し、-Cは現在の作業ディレクトリを指定ディレクトリに切り替えることを示し、つまりKERNELDIRディレクトリです。Mはモジュールソースコードディレクトリを示し、"make modules"コマンドにM=dirを追加すると、プログラムは自動的に指定されたdirディレクトリに移動し、モジュールのソースコードを読み取り、.koファイルにコンパイルします

コンパイルが成功するとdevbase.koというファイルが生成されます。このファイルがdevbaseデバイスのドライバモジュールです。

② テストAPPのコンパイル

テストAPPはARM開発ボードで実行する必要があるため、arm-linux-gnueabihf-gccを使用してコンパイルする必要があります:

arm-none-linux-gnueabihf-gcc chrdevbaseApp.c -o chrdevbaseApp
  1. テストの実行

① ドライバモジュールのロード

LinuxシステムはTFTP経由でネットワークから起動し、NFSでネットワークルートファイルシステムをマウントします。

// ubootのbootcmd環境変数の値:
setenv bootcmd tftp c2000000 uImage;tftp c4000000 stm32mp157d-atk.dtb;bootm c2000000 - c4000000

// bootargs環境変数の値:
setenv bootargs console=ttySTM0,115200 root=/dev/nfs nfsroot=192.168.1.100:/home/tech/linux/nfs/rootfs,proto=tcp rw ip=192.168.1.106:192.168.1.100:192.168.1.1:255.255.255.0::eth0:off

開発ボードのルートファイルシステムに"/lib/modules/5.4.31"というディレクトリがあるかどうかを確認します。このディレクトリは自分で作成する必要があります。現在、devbase.koとchrdevbaseAPPをrootfs/lib/modules/5.4.31ディレクトリにコピーする必要があります:

cd /linux/atk-mpl/Drivers/1_chrdevbase
sudo cp devbase.ko chrdevbaseApp /home/tech/linux/nfs/rootfs/lib/modules/5.4.31/ -f

modules.xxファイルがあるのは、depmodコマンドを入力したためです。depmodは自動的にmodules.depファイルを生成します。ドライバファイルをロードするコマンドを入力します:

modprobe devbase

lsmod    // このコマンドは現在のシステムに存在するモジュールを表示できます

cat/proc/devicesを入力すると、デバイス番号200のdevbaseデバイスが表示されます。

② デバイスノードファイルの作成

ドライバが正常にロードされると、対応するデバイスノードファイルを/devディレクトリに作成する必要があります。アプリケーションはこのデバイスノードファイルを操作することで、具体的なデバイスの操作を実現します。以下のコマンドを入力して/dev/devbaseというデバイスノードファイルを作成します:

mknod /dev/devbase c 200 0

ここで"mknod"はノード作成コマンド、" /dev/devbase"は作成するノードファイル、"c"はこれが文字デバイスであることを示し、"200"はデバイスの主デバイス番号、"0"はデバイスの副デバイス番号です。作成が完了すると/dev/devbaseファイルが存在し、ls /dev/devbase -lコマンドで確認できます。

devbaseAPPがdevbaseデバイスを読み書きするには、直接/dev/devbaseに対して読み書き操作を行うだけで済みます。つまり、/dev/devbaseというファイルはdevbaseデバイスがユーザースペースでの実装です。

③ devbaseデバイスの操作テスト

devbaseAppソフトウェアを使用してdevbaseデバイスを操作し、読み書きが正常かどうかを確認します。まず読み取り操作を実行します。以下のコマンドを入力します:

cd /
cd /lib/modules/5.4.31
./chrdevbaseApp /dev/devbase 1

"カーネルデータ!"が出力されました。なぜなら、devbaseAPPがread関数を使用してdevbaseデバイスからデータを読み取るため、devbase_read関数が実行されます。devbase_read関数はdevbaseAPPに"カーネルデータ!"というデータを送信し、devbaseAPPが受信して表示します。"読み取ったデータ:カーネルデータ!"がdevbaseAPPによって表示された受信データです。続いて書き込み操作をテストします。

./chrdevbaseApp /dev/devbase 2

"カーネル受信データ:ユーザーデータ!"が出力されました。これはドライバプログラムのdevbase_write関数が出力したものです。devbaseAPPがwrite関数を使用してdevbaseデバイスに"ユーザーデータ!"というデータを書き込みます。devbase_write関数が受信して表示します。

④ ドライバモジュールのアンロード

もはや使用しないデバイスがある場合は、アンロードできます:

rmmod devbase

# その後lsmodでモジュールが存在するか確認できます

タグ: Linuxカーネル 文字デバイスドライバ デバイスドライバ開発 カーネルモジュール file_operations

7月16日 21:24 投稿