本記事では、サンプルコードを用いてデバイスファイルについて解説します。
デバイスファイル概要
デバイスファイルは、OS(オペレーティングシステム)において、アプリケーションによるハードウェアデバイスへのアクセスのためのインターフェースとなる特殊なファイルのことをいいます。
主にUNIX系OS(Linux,macOS,BSDなど)で使用されており、アプリケーションはファイルの読み書きでデバイスドライバを操作し、ハードウェアを制御できます。
以下の図のようなイメージです。
デバイスファイルには以下の2つのタイプがあります。
- キャラクタデバイスファイル
- ブロックデバイスファイル
キャラクタデバイスファイル
キャラクタデバイスファイルは、1回の操作で1文字(バイト)ずつデータを処理するデバイス(キャラクタデバイス)に使用されるデバイスファイルです。
キャラクタデバイスファイルを介して、アプリケーションはバッファリングなしで直接デバイスとデータを交換できるため、逐次的でリアルタイムなデータ処理が可能です。
キャラクタデバイスには、以下のようなデバイスがあります。
- マウス
- キーボード
- シリアルポート
- 仮想コンソール
ブロックデバイスファイル
ブロックデバイスファイルは、1回の操作で固定サイズのデータブロックずつデータを処理するデバイス(ブロックデバイス)に使用されるデバイスファイルです。
これは、主にストレージデバイスに使用され、ランダムアクセス1やバッファリング2・キャッシュを用いて、大量のデータを効率的に読み書きするように設計されています。
ブロックデバイスには、以下のようなデバイスがあります。
- HDD(ハードディスクドライブ)
- SSD(ソリッドステートドライブ)
- SDカード
Linuxでは、「dev」ディレクトリ配下にデバイスファイルが置かれています。「ls -l」コマンドでデバイスファイルをリストすると、各デバイスファイルがキャラクタデバイスファイルかブロックデバイスか確認することが可能です。 例えば、以下の表示において、1列目の部分(「crw-------」など)で1つ目のアルファベットが「c」の場合はキャラクタデバイスファイル、「b」の場合はブロックデバイスファイルとなります。 ------------------------------------------------------------ $ ls -l /dev total 0 crw-r--r-- 1 root root 10, 235 Feb 11 18:55 autofs drwxr-xr-x 2 root root 40 Feb 11 18:55 block drwxr-xr-x 2 root root 100 Feb 11 18:55 bsg crw------- 1 root root 10, 234 Feb 11 18:55 btrfs-control ... brw------- 1 root root 7, 0 Feb 11 18:55 loop0 brw------- 1 root root 7, 1 Feb 11 18:55 loop1 brw------- 1 root root 7, 2 Feb 11 18:55 loop2 ... ------------------------------------------------------------
サンプルコードの解説
Ubuntu 22.04 LTS(カーネルバージョン:6.5.0-17-generic)環境で、キャラクタデバイスファイルを使用するカーネルモジュール(デバイスドライバ)のサンプルコード(simple_char_driver.c)を用意したので、内容について解説していきます。
カーネルモジュールの実装の基本的な内容については、以下の記事を参考にしてください。
「simple_char_driver.c」は、ユーザー空間のアプリケーションの基本的な読み書き操作をサポートするプログラムです。ユーザー空間からのデータの書き込みを保存し、後でそれを読み出すことができます。
「simple_char_driver.c」では、実際のハードウェアデバイスを使用しているわけではありません。Linuxのユーザー空間とカーネル空間(デバイスドライバ)の間での基本的なデータの読み書きをシミュレートしたものです。
以下は、「simple_char_driver.c」の中身です。
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/uaccess.h>
#define DEVICE_NAME "simplechar"
#define CLASS_NAME "simplechar"
MODULE_LICENSE("GPL");
MODULE_AUTHOR("furu");
MODULE_DESCRIPTION("A simple Linux char driver");
MODULE_VERSION("0.1");
static int majorNumber;
static char message[256] = {0};
static short size_of_message;
static struct class *simplecharClass = NULL;
static struct device *simplecharDevice = NULL;
static int dev_open(struct inode *, struct file *);
static int dev_release(struct inode *, struct file *);
static ssize_t dev_read(struct file *, char *, size_t, loff_t *);
static ssize_t dev_write(struct file *, const char *, size_t, loff_t *);
static struct file_operations fops =
{
.open = dev_open,
.read = dev_read,
.write = dev_write,
.release = dev_release,
};
static int __init simplechar_init(void)
{
printk(KERN_INFO "SimpleChar: Initializing the SimpleChar LKM\n");
majorNumber = register_chrdev(0, DEVICE_NAME, &fops);
if (majorNumber < 0)
{
printk(KERN_ALERT "SimpleChar failed to register a major number\n");
return majorNumber;
}
printk(KERN_INFO "SimpleChar: registered correctly with major number %d\n", majorNumber);
simplecharClass = class_create(CLASS_NAME);
if (IS_ERR(simplecharClass))
{
unregister_chrdev(majorNumber, DEVICE_NAME);
printk(KERN_ALERT "Failed to register device class\n");
return PTR_ERR(simplecharClass);
}
printk(KERN_INFO "SimpleChar: device class registered correctly\n");
simplecharDevice = device_create(simplecharClass, NULL, MKDEV(majorNumber, 0), NULL, DEVICE_NAME);
if (IS_ERR(simplecharDevice))
{
class_destroy(simplecharClass);
unregister_chrdev(majorNumber, DEVICE_NAME);
printk(KERN_ALERT "Failed to create the device\n");
return PTR_ERR(simplecharDevice);
}
printk(KERN_INFO "SimpleChar: device class created correctly\n");
return 0;
}
static void __exit simplechar_exit(void)
{
device_destroy(simplecharClass, MKDEV(majorNumber, 0));
class_unregister(simplecharClass);
class_destroy(simplecharClass);
unregister_chrdev(majorNumber, DEVICE_NAME);
printk(KERN_INFO "SimpleChar: Goodbye from the LKM!\n");
}
static int dev_open(struct inode *inodep, struct file *filep)
{
printk(KERN_INFO "SimpleChar: Device has been opened\n");
return 0;
}
static ssize_t dev_read(struct file *filep, char *buffer, size_t len, loff_t *offset)
{
int error_count = 0;
if (*offset >= size_of_message)
{
return 0;
}
if (len > size_of_message - *offset)
{
len = size_of_message - *offset;
}
error_count = copy_to_user(buffer, message + *offset, len);
if (error_count == 0)
{
*offset += len;
printk(KERN_INFO "SimpleChar: Sent %zu characters to the user\n", len);
return len;
}
else
{
printk(KERN_ERR "SimpleChar: Failed to send %d characters to the user\n", error_count);
return -EFAULT;
}
}
static ssize_t dev_write(struct file *filep, const char *buffer, size_t len, loff_t *offset)
{
unsigned long not_copied;
if (len > sizeof(message) - 1)
{
printk(KERN_WARNING "SimpleChar: Input data is too long\n");
return -EINVAL;
}
not_copied = copy_from_user(message, buffer, len);
if (not_copied == 0)
{
message[len] = '\0';
size_of_message = strlen(message);
printk(KERN_INFO "SimpleChar: Received %zu characters from the user\n", len);
return len;
}
else
{
return -EFAULT;
}
}
static int dev_release(struct inode *inodep, struct file *filep)
{
printk(KERN_INFO "SimpleChar: Device successfully closed\n");
return 0;
}
module_init(simplechar_init);
module_exit(simplechar_exit);
サンプルコードの処理
ロード時の処理
カーネルモジュールがロードされたとき、module_initマクロで指定されている simplechar_init() が呼び出されます。simplechar_init() は、以下の処理を行っています。
- キャラクタデバイスの登録
- デバイスクラスの作成
- デバイスインスタンスの作成
キャラクタデバイスの登録
majorNumber = register_chrdev(0, DEVICE_NAME, &fops);
register_chrdev() はカーネルに新しいキャラクタデバイスを登録する関数です。
第1引数にメジャー番号を指定します。0を指定するとカーネルは利用可能なメジャー番号を自動的に割り当てます。
第2引数にデバイス名を指定します。サンプルコードでは “simplechar” がデバイス名となります。
第3引数にデバイスファイルに対するファイル操作を行った際に実行される関数ポインタをまとめたfile_operations構造体を指定します。サンプルコードでは、open に dev_open()、read に dev_read()、write に dev_write()、release に dev_release() をそれぞれ指定しています。
実行に成功すると、戻り値で登録されたメジャー番号を返します。
メジャー番号とマイナー番号
メジャー番号とマイナー番号は、デバイスファイルがカーネルのどのデバイスドライバに対応しているかを示すために使用されます。
メジャー番号は、デバイスのタイプを識別するために使用されます。例えば、ハードディスク、シリアルポート、プリンターなど、異なる種類のデバイスは異なるメジャー番号を持ちます。基本的に、メジャー番号1つに対してデバイスドライバが1つ割り当てられています。
マイナー番号は、同じメジャー番号を持つデバイス群の中で、個々のデバイスを区別するために使用されます。システムに複数のハードディスクがある場合、それらはすべて同じメジャー番号を持ちますが、各ハードディスクは異なるマイナー番号を持ちます。デバイスドライバは、マイナー番号に応じて、デバイスごとに異なる処理を行います。
file_operations構造体
file_operations構造体は、キャラクタデバイスドライバの動作を定義します。
この構造体は、デバイスファイルに対するユーザー空間の操作(open(開く)、close(閉じる)、read(読み取り)、write(書き込み)など)をカーネル空間の関数にマッピングします。
具体的には、アプリケーションがデバイスファイルを操作するために呼び出すシステムコールと、それらのシステムコールに対応するカーネル内のドライバ関数を関連付ける役割を持ちます。
デバイスクラスの作成
simplecharClass = class_create(CLASS_NAME);
class_create()はデバイスクラスの作成を行う関数です。
引数にデバイスクラス名を指定し、実行に成功すると作成されたデバイスクラスのポインタを返します。
サンプルコードでは “simplechar” というデバイスクラスの作成を行います。
デバイスクラス
デバイスクラスは、デバイスをカテゴリに分類するために使用されます。
例えば、イーサネットカードや無線LANアダプタなどは、net クラスに属し、USBメモリなどは usb クラスに属します。
/sys/classディレクトリ配下にデバイスクラスのサブディレクトリが存在し、「ls /sys/class」コマンドを実行することでデバイスクラスの一覧を確認することができます。
デバイスインスタンスの作成
simplecharDevice = device_create(simplecharClass, NULL, MKDEV(majorNumber, 0), NULL, DEVICE_NAME);
device_create() は、デバイスインスタンスとデバイスファイルを作成する関数です。
第1引数にclass_create()で作成されたデバイスクラスへのポインタを指定します。
第2引数には親デバイスインスタンスへのポインタを指定しますが、サンプルコードでは不要なのでNULLを指定しています。
第3引数にデバイスIDを指定します。MKDEVマクロの引数にメジャー番号とマイナー番号を指定して取得することができます。サンプルコードではマイナー番号として0を指定していますが、これはデバイスクラスに対する最初のデバイスインスタンスの場合は0となるためです。
第4引数にはデバイスに関連付けられるドライバ固有のデータへのポインタを指定しますが、サンプルコードでは不要なのでNULLを指定しています。
第5引数に作成されるデバイスファイル名を指定します。ここで指定されたファイル名のデバイスファイルが、/dev配下に作成されます。サンプルコードでは、/dev/simplecharが作成されます。
実行に成功すると、作成されたデバイスインスタンスへのポインタを返します。
デバイスインスタンス
デバイスインスタンスとは、Linuxカーネル内における、個々のハードウェアデバイスの抽象表現です。これはソフトウェアの観点から見たデバイスの表現であり、物理的または仮想的なデバイスをシステム上で識別し管理するために使用されます。
デバイスインスタンスは、特定のデバイスクラスに関連付けられます。特定のデバイスクラスのサブディレクトリ配下に、それに関連付けられるデバイスインスタンスのサブディレクトリがあります。
例えば、デバイスクラス「net」のデバイスインスタンスは、「ls /sys/class/net」で確認することができます。
アンロード時の処理
カーネルモジュールがアンロードされたとき、module_exitマクロで指定されているsimplechar_exit() が呼び出されます。simplechar_exit() は、以下の処理を行っています。
- デバイスインスタンスの破棄
- デバイスクラスの登録解除
- デバイスクラスの破棄
- デバイスの登録解除
デバイスインスタンスの破棄
device_destroy(simplecharClass, MKDEV(majorNumber, 0));
device_destroy()は、device_create()で作成されたデバイスインスタンスを破棄する関数です。
第1引数にデバイスクラスのポインタ、第2引数にデバイスIDを指定し、それに該当するデバイスインスタンスを破棄します。
また、同時に/dev配下のデバイスファイルも削除されます。
デバイスクラスの登録解除
class_unregister(simplecharClass);
class_unregister()により、引数に指定されたポインタが指し示すデバイスクラスがカーネルから登録解除されます。
デバイスクラスを登録解除するとシステムはそのクラスに属する新しいデバイスの作成を認識しなくなります。
ただし、この時点ではクラス自体はまだメモリに残っています。
デバイスクラスの破棄
class_destroy(simplecharClass);
class_destroy()は、class_unregister()に続いて呼び出され、引数のポインタが指し示すデバイスクラスに関連付けられたメモリを開放し、クラス自体を完全に破棄します。
デバイスの登録解除
unregister_chrdev(majorNumber, DEVICE_NAME);
unregister_chrdev()は、register_chrdev()によって登録されたキャラクタデバイスを解除します。
第1引数にメジャー番号、第2引数にデバイス名を指定します。
デバイスファイルopen時の処理
デバイスファイルをopenしたとき、file_operations構造体のメンバ open に指定されている関数 dev_open() が実行されます。dev_open() はカーネルログを出力するのみです。
デバイスファイルread時の処理
デバイスファイルをreadしたとき、file_operations構造体のメンバ read に指定されている関数 dev_read() が実行されます。
dev_read() での主な処理は、copy_to_user() でカーネル空間にあるdev_write() によって書き込まれたデータ(message)をユーザー空間に送るためのバッファ(buffer)にコピーしています。
error_count = copy_to_user(buffer, message + *offset, len);
また、コピーが成功すると、読み取り位置(offset)を更新し、データ長を呼び出し元に返しています。
error_count = copy_to_user(buffer, message + *offset, len);
if (error_count == 0)
{
*offset += len;
printk(KERN_INFO "SimpleChar: Sent %zu characters to the user\n", len);
return len;
}
デバイスファイルwrite時の処理
デバイスファイルにwriteしたとき、file_operations構造体のメンバ write に指定されている関数 dev_write() が実行されます。
dev_write() での主な処理は、copy_from_user() でユーザー空間から書き込まれたデータ(buffer)をカーネル空間のバッファ(message)にコピーしています。
not_copied = copy_from_user(message, buffer, len);
また、コピーが成功すると、終端文字(‘\0’)の追加とメッセージサイズ(size_of_message)の更新を行い、データ長を呼び出し元に返しています。
not_copied = copy_from_user(message, buffer, len);
if (not_copied == 0)
{
message[len] = '\0';
size_of_message = strlen(message);
printk(KERN_INFO "SimpleChar: Received %zu characters from the user\n", len);
return len;
}
デバイスファイルclose時の処理
デバイスファイルをcloseしたとき、file_operations構造体のメンバ release に指定されている関数 dev_release() が実行されます。dev_release() はカーネルログを出力するのみです。
ビルド
simple_char_driver.c をビルドするための以下の Makefile を用意します。
obj-m += simple_char_driver.o
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
simple_char_driver.c と同じディレクトリ階層に Makefile を置き、makeコマンドを実行します。
$ ls
Makefile simple_char_driver.c
$ make
ビルドに成功すると中間生成ファイルとカーネルモジュール「simple_char_driver.ko」が生成されます。
$ ls
Makefile Module.symvers modules.order simple_char_driver.c simple_char_driver.ko simple_char_driver.mod simple_char_driver.mod.c simple_char_driver.mod.o simple_char_driver.o
動作確認
insmodコマンドで simple_char_driver.ko をカーネルにロードします。
$ sudo insmod simple_char_driver.ko
lsmodコマンドでロードされているか確認します。
$ lsmod | grep simple_char_driver
simple_char_driver 12288 0
また、デバイスファイルが生成されていることも確認します。
$ ls -l /dev | grep simplechar
crw------- 1 root root 234, 0 2月 28 20:28 simplechar
パーミッションが一般ユーザーが読み書き不可になっているので、chmodコマンドで読み書きできるように変更します。
$ sudo chmod 666 /dev/simplechar
$ ls -l /dev | grep simplechar
crw-rw-rw- 1 root root 234, 0 2月 28 20:28 simplechar
echoコマンドで”Hello, world!”を書き込んだ後、catコマンドで読み出しを行います。”Hello, world”と読み出せれば、実行成功です。
$ echo "Hello, world" > /dev/simplechar
$ cat /dev/simplechar
Hello, world
echoコマンドでファイルに文字列をリダイレクトした際、そのファイルに対して、open,write,closeのシステムコールが順番に呼ばれます。また、catコマンドでファイルの中身を読み出した際、そのファイルに対して、open,read,closeのシステムコールが順番に呼ばれます。
最後に、rmmodコマンドで、ロードしたモジュールをカーネルからアンロードします。
$ sudo rmmod simple_char_driver
dmesgコマンドで動作確認時のカーネルログを確認すると、以下のようなカーネルログが出力されます。
$ sudo dmesg | tail -n 20
...
[ 8214.716086] SimpleChar: Initializing the SimpleChar LKM
[ 8214.716093] SimpleChar: registered correctly with major number 234
[ 8214.716114] SimpleChar: device class registered correctly
[ 8214.716216] SimpleChar: device class created correctly
[ 8239.849213] SimpleChar: Device has been opened
[ 8239.849232] SimpleChar: Received 13 characters from the user
[ 8239.849249] SimpleChar: Device successfully closed
[ 8244.938661] SimpleChar: Device has been opened
[ 8244.938681] SimpleChar: Sent 13 characters to the user
[ 8244.938709] SimpleChar: Device successfully closed
[ 8250.749724] SimpleChar: Goodbye from the LKM!
...
最後に
本記事で使用したサンプルコードは、GitHubにアップしています。