Linuxにおけるデバイスファイルの仕組み
Linuxにおけるデバイスファイルはデバイスをファイルという概念を通して扱えるようにしたものです。デバイスファイルは通常のファイルと同様に読み書きを行うことができます。しかし実際には、その読み書きはデバイスドライバを通じてデバイスの制御に変換されます。
この記事では、デバイスファイルへの読み書きがどのようにデバイスの制御に変換されるのかを説明します。デバイスファイルはデバイスドライバとファイルの2つのコンポーネントに依存したものであるので、最初にデバイスドライバ、次にファイルについて説明し、最後にデバイスファイルがどのようにデバイスドライバと結び付けられるかを解説します。
この記事の内容は主に詳解 Linuxカーネル 第3版及びhttps://github.com/torvalds/linux/tree/v6.1によります。
目次
デバイスドライバ
デバイスドライバとはカーネルルーチンの集合です。デバイスドライバは後で説明するVirtual File System(VFS)の各オペレーションをデバイス固有の関数に結びつけます。
デバイスドライバの実例
デバイスドライバを作って実際に動かしてみます。以下のような read_write.c
と Makefile
を用意します。この2つはJohannes4Linux/Linux_Driver_Tutorial/03_read_writeを一部改変したものです。[1]
/ *read_write.c * /
#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/uaccess.h>
MODULE_LICENSE("GPL");
#define DRIVER_MAJOR 333
#define DRIVER_NAME "read_write_driver"
static ssize_t driver_read(struct file *File, char *user_buffer, size_t count,
loff_t *offs) {
user_buffer[0] = 'A';
return 1;
}
static ssize_t driver_write(struct file *File, const char *user_buffer,
size_t count, loff_t *offs) {
return 1;
}
static int driver_open(struct inode *device_file, struct file *instance) {
printk("read_write_driver - open was called!\n");
return 0;
}
static int driver_close(struct inode *device_file, struct file *instance) {
printk("read_write_driver - close was called!\n");
return 0;
}
static struct file_operations fops = {.open = driver_open,
.release = driver_close,
.read = driver_read,
.write = driver_write};
static int __init ModuleInit(void) {
printk("read_write_driver - ModuleInit was called!\n");
register_chrdev(DRIVER_MAJOR, DRIVER_NAME, &fops);
return 0;
}
static void __exit ModuleExit(void) {
printk("read_write_driver - ModuleExit was called!\n");
unregister_chrdev(DRIVER_MAJOR, DRIVER_NAME);
}
module_init(ModuleInit);
module_exit(ModuleExit);
# Makefile
obj-m += read_write.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
これをビルドしてインストールし、デバイスファイルを作成するとA
が無限に読み出されるデバイスファイルができます。
$ make
$ sudo insmod read_write.ko
$ sudo mknod /dev/read_write c 333 1
$ cat /dev/read_write
AAAAAA...
read_write.c
からわかること
read_write.c
からは次のことがわかります。
- このデバイスドライバのmajor番号は
333
である。 - デバイスドライバは単なる関数の集合である。
-
open(2)
とmyDevice_open
、release(2)
とdriver_close
、read(2)
とmyDevice_read
、write(2)
とdriver_write
が対応している。
以上からcat /dev/read_write
は このデバイスドライバの driver_read
を呼び出すので A
が無限に読み出されます。なお、major番号の333
に意味はありません。
insmod
insmod(8)
はLinuxカーネルにカーネルモジュールを挿入するコマンドです。この章では sudo insmod read_write.ko
がどのようにread_write.ko
をカーネルに登録するかを確認します。
insmodのユーザ空間での処理
strace(1)
を使ってinsmod(8)
が呼び出すシステムコールを確認するとfinit_module(2)
が呼ばれています。
# strace insmod read_write.ko
...
openat(AT_FDCWD, "/home/akira/misc/linux-device-file/driver_for_article/read_write.ko", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1", 6) = 6
lseek(3, 0, SEEK_SET) = 0
newfstatat(3, "", {st_mode=S_IFREG|0664, st_size=6936, ...}, AT_EMPTY_PATH) = 0
mmap(NULL, 6936, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7fc8aae77000
finit_module(3, "", 0) = 0
munmap(0x7fc8aae77000, 6936) = 0
close(3) = 0
exit_group(0) = ?
+++ exited with 0 +++
insmodのカーネル空間での処理
finit_module(2)
はLinuxカーネル内の kernel/module/main.c#29l6 で定義されています。
SYSCALL_DEFINE3(finit_module, int, fd, const char __user *, uargs, int, flags)
ここから追っていくと do_init_module関数 で初期化処理を行っていることがわかります。
/*
* This is where the real work happens.
*
* Keep it uninlined to provide a reliable breakpoint target, e.g. for the gdb
* helper command 'lx-symbols'.
*/
static noinline int do_init_module(struct module *mod)
更に追っていくと insmod(8)
を行った際には ret = do_one_initcall(mod->init); 経由でデバイスドライバ内のModuleInit
が呼び出されることがわかります。
/* Start the module */
if (mod->init != NULL)
ret = do_one_initcall(mod->init);
if (ret < 0) {
goto fail_free_freeinit;
}
printk
を駆使して調べると、この mod->init
は __apply_relocate_addで設定されていました。この関数は名前から推測できるようにカーネルモジュール内の再配置を行う関数です。再配置情報とmod->init
の関係については調べきれなかったため今後の課題とします。
static int __apply_relocate_add(Elf64_Shdr *sechdrs,
const char *strtab,
unsigned int symindex,
unsigned int relsec,
struct module *me,
void *(*write)(void *dest, const void *src, size_t len))
mod->init
経由で呼び出されたModuleInit
は register_chrdev
を呼び出し、最終的にカーネル内の __register_chrdev を経由して kobj_mapに到達します。kobj_map は cdev_map にデバイスドライバを登録します。
int kobj_map(struct kobj_map *domain, dev_t dev, unsigned long range,
struct module *module, kobj_probe_t *probe,
int (*lock)(dev_t, void *), void *data)
{
...
mutex_lock(domain->lock);
for (i = 0, p -= n; i < n; i++, p++, index++) {
struct probe **s = &domain->probes[index % 255];
while (*s && (*s)->range < range)
s = &(*s)->next;
p->next = *s;
*s = p;
}
...
}
ファイル
この章では「デバイスファイル」の「ファイル」について簡単に説明します。
VFS(Virtual File System)
VFSとは標準的なUNIXファイルシステムのすべてのシステムコールを取り扱う、カーネルが提供するソフトウェアレイヤです。提供されているシステムコールとしてopen(2)
、close(2)
、write(2)
等があります。このレイヤがあるので、ユーザはext4
、NFS
、proc
などの全く異なるシステムをインターフェイスで取り扱うことができます。
例えばcat(1)
は cat /proc/self/maps
も cat ./README.md
も可能ですが、前者はメモリ割付状態を、後者ははディスク上のファイルの中身を読み出しており、全く異なるシステムを同じインターフェイスで扱っています。
LinuxにおいてVFSは構造体と関数ポインタを使ったオブジェクト指向で実装されていて、関数ポインタを持つ構造体がオブジェクトとして使われています。
inode
inodeオブジェクトはVFSにおいて「普通のファイル」に対応するオブジェクトです。定義は fs.h にあります。inodeオブジェクト以外の他のオブジェクトとして、ファイルシステムそのものの情報を保持するスーパーブロックオブジェクト、オープンされているファイルとプロセスのやり取りの情報を保持するファイルオブジェクト、ディレクトリに関する情報を保持するdエントリオブジェクトがあります。
struct inode {
umode_t i_mode;
unsigned short i_opflags;
kuid_t i_uid;
kgid_t i_gid;
unsigned int i_flags;
...
union {
struct pipe_inode_info *i_pipe;
struct cdev *i_cdev;
char *i_link;
unsigned i_dir_seq;
};
...
};
普通のファイルのinode
stat(1)
を使うとファイルのiノード情報を表示することができ、struct inode
と対応した内容が表示されます。
[@goshun](master)~/misc/linux-device-file
> stat README.md
File: README.md
Size: 20 Blocks: 8 IO Block: 4096 regular file
Device: fd01h/64769d Inode: 49676330 Links: 1
Access: (0664/-rw-rw-r--) Uid: ( 1000/ akira) Gid: ( 1000/ akira)
Access: 2023-01-28 11:19:15.104727788 +0900
Modify: 2023-01-28 11:19:13.748734093 +0900
Change: 2023-01-28 11:19:13.748734093 +0900
Birth: 2023-01-28 11:19:13.748734093 +0900
デバイスファイルのinode
デバイスファイルのiノード情報も表示してみます。ls -il
で表示したときに先頭にc
がついているとキャラクタデバイス、b
がついているとブロックデバイスです。
[@goshun]/dev
> ls -il /dev/nvme0*
201 crw------- 1 root root 240, 0 1月 29 19:02 /dev/nvme0
319 brw-rw---- 1 root disk 259, 0 1月 29 19:02 /dev/nvme0n1
320 brw-rw---- 1 root disk 259, 1 1月 29 19:02 /dev/nvme0n1p1
321 brw-rw---- 1 root disk 259, 2 1月 29 19:02 /dev/nvme0n1p2
322 brw-rw---- 1 root disk 259, 3 1月 29 19:02 /dev/nvme0n1p3
[@goshun](master)~/misc/linux-device-file
> stat /dev/nvme0n1
File: /dev/nvme0n1
Size: 0 Blocks: 0 IO Block: 4096 block special file
Device: 5h/5d Inode: 319 Links: 1 Device type: 103,0
Access: (0660/brw-rw----) Uid: ( 0/ root) Gid: ( 6/ disk)
Access: 2023-01-28 10:03:26.964000726 +0900
Modify: 2023-01-28 10:03:26.960000726 +0900
Change: 2023-01-28 10:03:26.960000726 +0900
Birth: -
デバイスドライバとファイルの接続
mknod
mknod(1)
はブロックデバイスファイルもしくはキャラクタデバイスファイルを作るためのコマンドです。デバイスドライバの実例 では sudo mknod /dev/read_write c 333 1
を使ってデバイスファイル /dev/read_write
を作成しました。mknod(2)はこれに対応するシステムコールであり、ファイルシステム上にノード(おそらくinodeのこと)を作るために使われます。
mknodのユーザ空間での処理
strace(1)
を使ってmknod(2)
がどのように呼び出されているかを調べます。0x14d
は10進で333
なので /dev/read_write
にメジャー番号と333
、マイナー番号を1
を指定してinodeを作っていることがわかります。ちなみに、mknod
とmnknodat
はパス名が相対パスになるかどうかという違いです。
# strace mknod /dev/read_write c 333 1
...
close(3) = 0
mknodat(AT_FDCWD, "/dev/read_write", S_IFCHR|0666, makedev(0x14d, 0x1)) = 0
close(1) = 0
close(2) = 0
exit_group(0) = ?
+++ exited with 0 +++
mknodのカーネル空間での処理
mknodat
の本体は do_mknodat にあります。ここからデバイスファイルとデバイスドライバがどのように接続されるかを追っていきます。ここではデバイスはキャラクタデバイス、ファイルシステムはext4であるとします。
static int do_mknodat(int dfd, struct filename *name, umode_t mode,
unsigned int dev)
キャラクタデバイス、ブロックデバイスを扱う場合、do_mknodat はfs/namei.c#L3970-L3972で vfs_mknod
を呼び出します。
case S_IFCHR: case S_IFBLK:
error = vfs_mknod(mnt_userns, path.dentry->d_inode,
dentry, mode, new_decode_dev(dev));
vfs_mknod
の定義はfs/namei.c#L3874-L3891にあります。
/**
* vfs_mknod - create device node or file
* @mnt_userns: user namespace of the mount the inode was found from
* @dir: inode of @dentry
* @dentry: pointer to dentry of the base directory
* @mode: mode of the new device node or file
* @dev: device number of device to create
*
* Create a device node or file.
*
* If the inode has been found through an idmapped mount the user namespace of
* the vfsmount must be passed through @mnt_userns. This function will then take
* care to map the inode according to @mnt_userns before checking permissions.
* On non-idmapped mounts or if permission checking is to be performed on the
* raw inode simply passs init_user_ns.
*/
int vfs_mknod(struct user_namespace *mnt_userns, struct inode *dir,
struct dentry *dentry, umode_t mode, dev_t dev)
vfs_mknod
はdエントリの mknod
を呼びます。ファイルシステムごとに mknod
の実装が異なるが今回はext4
のものを追ってみます。vfs_mknod
は
fs/namei.c#L3915 で mknod
を呼んでいます。
error = dir->i_op->mknod(mnt_userns, dir, dentry, mode, dev);
ext4
の mknod
はfs/ext4/namei.c#L4191で定義されています。
const struct inode_operations ext4_dir_inode_operations = {
...
.mknod = ext4_mknod,
...
};
ext4_mknod
の本体はここにあり、fs/ext4/namei.c#L2830-L2862の init_special_inode
がデバイスに関係していそうに見えます。
static int ext4_mknod(struct user_namespace *mnt_userns, struct inode *dir,
struct dentry *dentry, umode_t mode, dev_t rdev)
{
...
init_special_inode(inode, inode->i_mode, rdev);
...
}
キャラクタデバイスの場合は fs/inode.c#L2291-L2309 でdef_chr_fops
が設定されています。
void init_special_inode(struct inode *inode, umode_t mode, dev_t rdev)
{
inode->i_mode = mode;
if (S_ISCHR(mode)) {
inode->i_fop = &def_chr_fops;
inode->i_rdev = rdev;
} else if (S_ISBLK(mode)) {
...
}
}
EXPORT_SYMBOL(init_special_inode);
def_chr_fops
はfs/char_dev.c#L447-L455で定義されています。
/*
* Dummy default file-operations: the only thing this does
* is contain the open that then fills in the correct operations
* depending on the special file...
*/
const struct file_operations def_chr_fops = {
.open = chrdev_open,
.llseek = noop_llseek,
};
chrdev_open
が怪しいので定義を見ると fs/char_dev.c#L370-L424 のkobj_lookup
でドライバを探していそうです。
/*
* Called every time a character special file is opened
*/
static int chrdev_open(struct inode *inode, struct file *filp)
{
...
kobj = kobj_lookup(cdev_map, inode->i_rdev, &idx);
...
}
insmod
のときに見た kobj_mapと同じファイルにたどりついたのでここで間違いなさそうです。drivers/base/map.c#L95-L133でファイルにデバイスドライバを紐付けています。
struct kobject *kobj_lookup(struct kobj_map *domain, dev_t dev, int *index)
{
struct kobject *kobj;
struct probe *p;
unsigned long best = ~0UL;
retry:
mutex_lock(domain->lock);
for (p = domain->probes[MAJOR(dev) % 255]; p; p = p->next) {
struct kobject *(*probe)(dev_t, int *, void *);
struct module *owner;
void *data;
if (p->dev > dev || p->dev + p->range - 1 < dev)
continue;
if (p->range - 1 >= best)
break;
if (!try_module_get(p->owner))
continue;
owner = p->owner;
data = p->data;
probe = p->get;
best = p->range - 1;
*index = dev - p->dev;
if (p->lock && p->lock(dev, data) < 0) {
module_put(owner);
continue;
}
mutex_unlock(domain->lock);
kobj = probe(dev, index, data);
/* Currently ->owner protects _only_ ->probe() itself. */
module_put(owner);
if (kobj)
return kobj;
goto retry;
}
mutex_unlock(domain->lock);
return NULL;
}
最後に実際にカーネルにパッチを当てて確認してみましょう。drivers/base/map.c#L114-L115 にログ出力を足してカーネルをインストールして、デバイスファイルを デバイスドライバの実例 と同様に作成します。
> git diff --patch "device-file-experiment~1"
diff --git a/drivers/base/map.c b/drivers/base/map.c
index 83aeb09ca161..57037223932e 100644
--- a/drivers/base/map.c
+++ b/drivers/base/map.c
@@ -111,6 +111,8 @@ struct kobject *kobj_lookup(struct kobj_map *domain, dev_t dev, int *index)
break;
if (!try_module_get(p->owner))
continue;
+
+ printk("%s:%d MAJOR(dev)=%u MINOR(dev)=%u\n", __FILE__, __LINE__, MAJOR(dev), MINOR(dev));
owner = p->owner;
data = p->data;
probe = p->get;
cat /dev/read_write
したときの dmesg -wH
の様子が以下です。cat(2)
が/dev/read_write
を開いたときに対応するデバイスドライバが検索されて read_write_driver
が呼ばれていることがわかります。
# dmesg -wH
...
[ +18.898110] drivers/base/map.c:115 MAJOR(dev)=136 MINOR(dev)=2
[ +10.920752] drivers/base/map.c:115 MAJOR(dev)=136 MINOR(dev)=3
[ +9.170364] loop0: detected capacity change from 0 to 8
[ +1.212845] drivers/base/map.c:115 MAJOR(dev)=333 MINOR(dev)=1
[ +0.000010] read_write_driver - open was called!
[ +2.141643] read_write_driver - close was called!
参考
- 詳解 Linuxカーネル 第3版
- init_module(2) — Linux manual page
- linux kernelにおけるinsmodの裏側を確認
- https://github.com/torvalds/linux/tree/v6.1
- 組み込みLinuxデバイスドライバの作り方
- Linuxのドライバの初期化が呼ばれる流れ
連絡先
この記事に誤りがあった場合はTwitter等で連絡をください。修正します。その他の連絡先は https://akawashiro.github.io/ にあります。
-
このソースコードは一部の環境で動かないというコメントを頂いています。動作しない場合はhttps://zenn.dev/link/comments/97a1f0e14f5ad5 に従って修正してください。 ↩︎
Discussion
素晴らしい記事をありがとうございます。
カーネルやデバイスドライバ等のレイヤに興味があり学習中で、大変参考になりました。
学習中にて詳細な内容は理解できておらず恐縮ですが、記事中の実例を実験した際に手元でそのままでは動かなかったのでコメントとして残させていただきたいと思います。
(Twitter 上で連絡可能なアカウントが無いため記事中のコメントにて失礼いたします。)
デバイスドライバの実例において、
read_write.c
をコンパイルしてinsmod
,mknod
でインストールしたのち、にて動作確認しようとしたところ、
Segmentation fault
になってしまいました。/var/log/syslog
を確認すると、以下のメッセージがありました。Linux Device Drivers 等を参考に
read_write.c
を以下のように修正するとSegmentation fault
が解消するようでした。利用している環境 (VM) は以下になります。
記事の改善のためのご参考になれば幸いです。
ありがとうございます。手元に
Linux primary 6.8.0-41-generic
の VM がないのですぐに再現できないのですが、再現と修正を試みます。自分でビルドした Linux 6.8.0 (e8f897f4afef0031fe618a8e94127a0934896aba) では再現しませんでした。config による差異である可能性が高いと思うのでもう少しいろいろ見てみます。