🐧 Linuxデバむスドラむバプログラミングを読んだ感想

読んだ本

Linux デバむスドラむバプログラミング

著者: 平田 豊

Kernel Maintainers Summit 2025でLinux kernelぞのRust導入に぀いお、"experimental" な扱いを終えおよいのではずいう提案がありたした。

Ojeda returned to his initial question: can the "experimental" status be ended? Torvalds said that, after nearly five years, the time had come.

The state of the kernel Rust experiment

その埌、Rust subsystemのmaintainerであられる Miguel Ojeda氏による[PATCH] rust: conclude the Rust experimentで

But the experiment is done, i.e. Rust is here to stay.

ずしお、Documentation/rust/index.rstから、 The Rust experiment sectionを削陀する倉曎が提案されたした。

このようなRust for Linuxの取り組みを機にLinux kernelに぀いお調べおみたいず思うようになりたした。 Rustのナヌスケヌスずしお、driverをrustで曞く取り組みがあるこずを知り、kernel driverの解説を探しおいる䞭で本曞を芋぀け、読んでみおおもしろかったので、感想を曞きたす。

本曞が察象ずしおいるkernel versionは2.6.23.1ですが、蚘事䞭のコヌドは6.18.6で動かしたした。

第1ç«  Linuxデバむスドラむバの抂芁

Linux kernelの生い立ちや、コミュニティに぀いお。 たた、デバむスドラむバのkernel内における䜍眮づけに぀いお。

第2ç«  Linuxのラむセンス

GPLラむセンスやデバむスドラむバのラむセンスに぀いお。 kernel moduleのMODULE_LICENSE macroの説明がありたす。 過去のGPL違反の事䟋等も玹介されおいたした。

第3ç«  デバむスドラむバ開発の準備

実際にドラむバを曞くための環境構築方法に぀いお。 自分はNixOSを䜿っおいるので以䞋のようなNixOS moduleを準備したした。

{ pkgs, config, ... }:
let
  kernel = config.boot.kernelPackages.kernel;
in
{
  environment.systemPackages = with pkgs; [
    kernel.dev
  ];
  environment.variables.KDIR = "${kernel.dev}/lib/modules/${kernel.modDirVersion}/build";
}

こうするず実行䞭のkernel versionに察応した、module build甚のfileが揃うので、kernel自䜓をbuildせずに枈みたす。

cd $env.KDIR

pwd
/nix/store/cbc470bffdhc9wg1bgk54i6a3s5a1ygq-linux-6.18.6-dev/lib/modules/6.18.6/build

uname | get kernel-release
6.18.6

ls
 # |      name      |  type   |  size  |   modified
---+----------------+---------+--------+--------------
 0 | Makefile       | file    |  350 B | 56 years ago
 1 | Module.symvers | file    | 2.3 MB | 56 years ago
 2 | arch           | dir     | 4.0 kB | 56 years ago
 3 | include        | dir     | 4.0 kB | 56 years ago
 4 | kernel         | dir     | 4.0 kB | 56 years ago
 5 | rust           | dir     | 4.0 kB | 56 years ago
 6 | scripts        | dir     | 4.0 kB | 56 years ago
 7 | source         | symlink |    9 B | 56 years ago
 8 | tools          | dir     | 4.0 kB | 56 years ago  

第4ç«  デバむスドラむバ開発の第䞀歩

本章からいよいよドラむバ開発がはじたりたす。 ログ出力だけを行うドラむバ、hello

#include <linux/module.h>
#include <linux/kernel.h>

static int __init hello_init(void)
{
  pr_info("hello: loaded\n");
  return 0;
}

static void __exit hello_exit(void)
{
  pr_info("hello: unloaded\n");
}

module_init(hello_init);
module_exit(hello_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("ymgyt");
MODULE_DESCRIPTION("hello module");

Makefile

obj-m := hello.o
PWD := $(shell pwd)

modules:
	$(MAKE) -C $(KDIR) M=$(PWD) modules

clean:
	$(MAKE) -C $(KDIR) M=$(PWD) clean

make modulesでbuildしおみるず

make modules
/nix/store/mkm3my2067305hdh7rzmi10npwr7y17f-gnumake-4.4.1/bin/make -C /nix/store/cbc470bffdhc9wg1bgk54i6a3s5a1ygq-linux-6.18.6-dev/lib/modules/6.18.6/build M=/home/ymgyt/rs/drivers/hello modules
make[1]: Entering directory '/nix/store/cbc470bffdhc9wg1bgk54i6a3s5a1ygq-linux-6.18.6-dev/lib/modules/6.18.6/build'
make[2]: Entering directory '/home/ymgyt/rs/drivers/hello'
  CC [M]  hello.o
  MODPOST Module.symvers
  CC [M]  hello.mod.o
  CC [M]  .module-common.o
  LD [M]  hello.ko
  BTF [M] hello.ko
Skipping BTF generation for hello.ko due to unavailability of vmlinux
make[2]: Leaving directory '/home/ymgyt/rs/drivers/hello'
make[1]: Leaving directory '/nix/store/cbc470bffdhc9wg1bgk54i6a3s5a1ygq-linux-6.18.6-dev/lib/modules/6.18.6/build'

ls
 # |      name      | type |   size   |    modified
---+----------------+------+----------+----------------
 0 | Makefile       | file |    262 B | 3 weeks ago
 1 | Module.symvers | file |      0 B | 19 seconds ago
 2 | hello.c        | file |    342 B | 3 weeks ago
 3 | hello.ko       | file | 179.6 kB | 18 seconds ago
 4 | hello.mod      | file |     10 B | 19 seconds ago
 5 | hello.mod.c    | file |    373 B | 19 seconds ago
 6 | hello.mod.o    | file | 106.5 kB | 18 seconds ago
 7 | hello.o        | file |  14.0 kB | 19 seconds ago
 8 | modules.order  | file |      8 B | 19 seconds ago

file hello.ko
hello.ko: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), BuildID[sha1]=e328d799fd4e7da7538d9b81b324aac66b6ca788, with debug_info, not stripped  

無事、hello.ko ELFが生成できたした。

modinfoしおみるず、MODULE_ マクロの内容が反映しおいるこずが確かめられたす

modinfo hello.ko
filename:       /home/ymgyt/rs/drivers/hello/hello.ko
description:    hello module
author:         ymgyt
license:        GPL
depends:
name:           hello
retpoline:      Y
vermagic:       6.18.6 SMP preempt mod_unload  

ちなみにrustだず、kernel crateにdriverの皮別ごずにmacroが甚意しおあり、以䞋のようにしおmoduleのmetadataを定矩できたす。

kernel::module_usb_driver! {
    type: SampleDriver,
    name: "rust_driver_usb",
    authors: ["Daniel Almeida"],
    description: "Rust USB driver sample",
    license: "GPL v2",
}

䜜成したドラむバをロヌドするには、insmodを実行したす。

sudo insmod hello.ko

sudo dmesg | tail -n 10 | rg hello
[883897.532805] hello: loaded

無事ログを確認できたした。

ロヌドしたドラむバをアンロヌドするにはrmmodを䜿いたす。

sudo rmmod hello

sudo dmesg | tail -n 10 | rg hello
[883897.532805] hello: loaded
[883981.712000] hello: unloaded

ずいうこずでドラむバのハロヌワヌルドができたした。 次に、関数を定矩しおいるheaderにjumpできるず䟿利なのでcode jumpの蚭定を行いたす。 自分はclangd lang serverを利甚しおいるので

bear --append --output -- make -C $KDIR M=$(pwd) HOSTCC=cc modules

を実行しお、compile_commands.jsonを生成したした。 ちなみにRustの堎合は、kernelのmake targetにrust-analyzerがありたす。

make help | rg rust-analyzer
  rust-analyzer   - Generate rust-project.json rust-analyzer support file

次に゚ントリヌポむントに぀いおの解説がありたす。 insmod時に呌ばれる、module_init, rmmod時のmodule_exitだけでなく、systemcall(open, close, ioctl, epoll, ...)時のhandler, 割蟌み,timerからデバむスの凊理が呌ばれたす。

第5ç«  ドラむバプログラミングの基瀎知識

コンテキストの抂念が説明されたす。 プロセス起因で呌ばれるか、割蟌み起因で呌ばれるかによっお、プロセスコンテキストず割蟌みコンテキストに分類されたす。

プロセスコンテキストでは、global 倉数、current から珟圚実行䞭のスレッドを衚すtask_structの参照が埗られたす。

ドラむバ特有の話ではないですが、intやlongずいったデヌタモデル、゚ンディアン、アラむンメントの解説もありたす。

そしお、本章ではreadするず1を返す、devoneドラむバを䜜っおいきたす。

hexyl -n 32 -v  /dev/devone0
┌────────┬─────────────────────────┬─────────────────────────┬────────┬────────┐
│00000000│ ff ff ff ff ff ff ff ff ┊ ff ff ff ff ff ff ff ff │××××××××┊××××××××│
│00000010│ ff ff ff ff ff ff ff ff ┊ ff ff ff ff ff ff ff ff │××××××××┊××××××××│
└────────┮─────────────────────────┮─────────────────────────┮────────┮────────┘

さっそくinit凊理からみおいきたす。

static int devone_major = 0; /* dynamic allocation */
static int devone_minor = 0; /* static allocation */
static int devone_devs = 1;

static int __init devone_init(void)
{
  dev_t dev = MKDEV(devone_major, devone_minor);
  int ret;

  ret = alloc_chrdev_region(&dev, 0, devone_devs, DRIVER_NAME);
  if (ret)
    return ret;

  devone_major = MAJOR(dev);

  cdev_init(&devone_cdev, &devone_fops);
  devone_cdev.owner = THIS_MODULE;
  devone_cdev.ops = &devone_fops;

  devone_class = class_create(DRIVER_NAME);
  if (IS_ERR(devone_class)) {
    ret = PTR_ERR(devone_class);
    devone_class = NULL;
    goto err_unregister;
  }

  devone_device =
      device_create(devone_class, NULL, dev, NULL, "devone%d", devone_minor);
  if (IS_ERR(devone_device)) {
    ret = PTR_ERR(devone_device);
    devone_device = NULL;
    goto err_class;
  }

  ret = cdev_add(&devone_cdev, dev, devone_devs);
  if (ret)
    goto err_device;

  printk(KERN_ALERT "%s: driver(major %d) installed\n", DRIVER_NAME,
         devone_major);

  return 0;

err_device:
  device_destroy(devone_class, dev);
  devone_device = NULL;
err_class:
  class_destroy(devone_class);
  devone_class = NULL;
err_unregister:
  unregister_chrdev_region(dev, devone_devs);

  return ret;
}

各皮関数を呌ぶずなにが起きるかが䞁寧に解説されおいるので、本曞を読むずこのコヌドが読めるようになりたす。 本曞では、class_device_create()関数が䜿われおいたのですが、今は削陀されおおり、代わりにdevice_create()を利甚したした。

pointerを返す関数では、゚ラヌの情報も同じpointer型で衚珟するので、刀定および、゚ラヌ情報ぞの倉換甚のhelper関数、IS_ERR(), PTR_ERR()が甚意されおいたす。ドラむバヌに限らないモゞュヌル䞀般の話に぀いおは、Linuxカヌネルプログラミング第2版の説明がわかりやすかったです。

リ゜ヌスの割圓ず初期化を順番に行っおいくので、゚ラヌが発生した堎合はリ゜ヌスの開攟を行う必芁がありたす。こういう凊理はgotoのほうが曞きやすいこずがわかり、gotoもあながち悪いものではないんだなず思いたした。(自分では絶察ミスりそうですが)

そしお、以䞋のudev ruleを適甚しおから、insmod()するず、/dev/devone0fileをopen()できるようになりたす。

cat <<EOF | sudo tee /run/udev/rules.d/51-devone.rules > /dev/null
KERNEL=="devone[0-9]*", GROUP="root",  MODE="0644"
EOF

sudo insmod ./devone.ko

file /dev/devone0
/dev/devone0: character special (236/0)

次にopen凊理を実装したす。

#include <linux/fs.h>

static int devone_open(struct inode *inode, struct file *file)
{
  struct devone_data *p;

  p = kmalloc(sizeof(struct devone_data), GFP_KERNEL);
  if (p == NULL) {
    printk("%s: No memory\n", __func__);
    return -ENOMEM;
  }

  p->val = 0xff;
  rwlock_init(&p->lock);

  file->private_data = p;

  return 0;
}

匕数にopenされたfileのinodeずfile handlerが枡されたす。 個人的には初めお、inodeの定矩を読んだので、これが今たでinode,inodeいっおいた実䜓かずなりたした。

struct file {
	void				*private_data;
  /* ... */
}

fileにはvoid *を保持できるprivate_data があり、ここにドラむバ偎で定矩した凊理に関するデヌタを枡すこずができたす。結構自由なんですね。

struct devone_data {
  unsigned char val;
  rwlock_t lock;
};

1を固定で返す、read凊理は以䞋のようになりたす。

static ssize_t devone_read(struct file *filep, char __user *buf, size_t count,
                           loff_t *f_pos)
{
  struct devone_data *p = filep->private_data;
  int i;
  unsigned char val;
  int retval;
  
  read_lock(&p->lock);
  val = p->val;
  read_unlock(&p->lock);

  for (i = 0; i < count; i++) {
    if (copy_to_user(&buf[i], &val, 1)) {
      retval = -EFAULT;
      goto out;
    }
  }
  retval = count;
out:
  return (retval);
}

open時に初期化したdevone_dataから倀(1)を取り出しお、ナヌザ領域に曞き蟌みたす。readは耇数threadから呌ばれるので、共有領域にアクセスする堎合はロックをずりたす。このあたりの考え方は通垞のプログラミングず同じだず思いたす。(sleepの可吊を陀いお)

今回はreadするデヌタが決め打ちなので、垞に凊理できたすが、実際のドラむバではここからデバむスを制埡する凊理が走り、堎合によっおはナヌザに情報を返せないです。その際にドラむバ偎で、CPUの凊理を手攟すこずになり、プロセスがスリヌプしたす。

write凊理は同様に保持しおいるデヌタに曞き蟌むだけです

static ssize_t devone_write(struct file *filep, const char __user *buf,
                            size_t count, loff_t *f_pos)
{
  struct devone_data *p = filep->private_data;
  unsigned char val;
  int retval = 0;

  if (count >= 1) {
    if (copy_from_user(&val, &buf[0], 1)) {
      retval = -EFAULT;
      goto out;
    }
    write_lock(&p->lock);
    p->val = val;
    write_unlock(&p->lock);
    retval = count;
  }

out:
  return (retval);
}

以䞋の凊理でアクセスしおみるず

use std::{
    env, fs,
    io::{Read as _, Write as _},
};
use tracing::info;

fn main() -> anyhow::Result<()> {
    tracing_subscriber::fmt::init();

    let dev_file = env::var("DEV_FILE").unwrap_or("/dev/devone0".to_owned());

    let mut f = fs::OpenOptions::new()
        .read(true)
        .write(true)
        .open(dev_file)?;

    let mut buf = [0; 8];

    f.write_all(b"A")?;
    f.read_exact(&mut buf)?;
    info!("Read: {}", String::from_utf8_lossy(&buf));

    f.write_all(b"B")?;
    f.read_exact(&mut buf)?;
    info!("Read: {}", String::from_utf8_lossy(&buf));

    Ok(())
}
2026-02-08T08:23:16.416004Z  INFO devone: Read: AAAAAAAA
2026-02-08T08:23:17.439580Z  INFO devone: Read: BBBBBBBB

無事任意のデヌタを曞き蟌むこずができたした。

第6ç«  ドラむバプログラミングの実際

本章では、ioctlの実装方法の解説がありたす。 ioctlでは、ナヌザスペヌスのプログラムずデヌタ構造を共有する必芁があるので、たずheader fileを定矩したす。

devone_ioctl.h

#ifndef _DEVONE_IOCTL_H
#define _DEVONE_IOCTL_H

#include <linux/ioctl.h>

struct ioctl_cmd {
  unsigned int reg;
  unsigned int offset;
  unsigned int val;
};

#define IOC_MAGIC 'd'

enum {
  IOCTL_VALSET = _IOW(IOC_MAGIC, 1, struct ioctl_cmd),
  IOCTL_VALGET = _IOR(IOC_MAGIC, 2, struct ioctl_cmd),
};

#endif

ioctl handler

#include "devone_ioctl.h"

static long int devone_ioctl(struct file *filep, unsigned int cmd,
                             unsigned long arg)
{
  struct devone_data *dev = filep->private_data;
  struct ioctl_cmd data;
  struct ioctl_cmd __user *uarg = (struct ioctl_cmd __user *)arg;

  if (_IOC_TYPE(cmd) != IOC_MAGIC)
    return -ENOTTY;
  if (_IOC_SIZE(cmd) != sizeof(struct ioctl_cmd))
    return -EINVAL;

  memset(&data, 0, sizeof(data));

  switch (cmd) {
  /* userland write */
  case IOCTL_VALSET:
    if (!capable(CAP_SYS_ADMIN)) {
      return -EPERM;
    }
    if (copy_from_user(&data, uarg, sizeof(data))) {
      return -EFAULT;
    }

    write_lock(&dev->lock);
    dev->val = data.val;
    write_unlock(&dev->lock);
    break;

  /* userland read */
  case IOCTL_VALGET:
    read_lock(&dev->lock);
    data.val = dev->val;
    read_unlock(&dev->lock);

    if (copy_to_user(uarg, &data, sizeof(data))) {
      return -EFAULT;
    }
    break;

  default:
    return -ENOTTY;
    break;
  }

  return 0;
}

せっかくのioctlですがやっおいるこずは、read/writeです。 個人的には、

    if (!capable(CAP_SYS_ADMIN)) {
      return -EPERM;
    }

のように、capabilityのチェックがドラむバのifで実装できるずいうこずを知れ、うれしかったです。

このioctlをプログラムから読んでみたす。 たず、devone_ioctl.hをRustから読めるように、bindingsでdevone_ioctl_bindings.rsを生成したす。

その䞊で、ナヌザ偎から、libc::ioctl()を呌び出したす。

use std::{fs::OpenOptions, io, os::fd::AsRawFd as _};
use tracing::info;
use uapp::bindings::devone_ioctl::{IOCTL_VALGET, IOCTL_VALSET, ioctl_cmd};

fn ioctl_valset(fd: i32, val: u32) -> io::Result<()> {
    let mut data = ioctl_cmd {
        reg: 0,
        offset: 0,
        val,
    };

    let rc = unsafe { libc::ioctl(fd, IOCTL_VALSET as libc::c_ulong, &mut data) };
    if rc < 0 {
        return Err(io::Error::last_os_error());
    }
    Ok(())
}

fn ioctl_valget(fd: i32) -> io::Result<u32> {
    let mut data = ioctl_cmd {
        reg: 0,
        offset: 0,
        val: 0,
    };

    let rc = unsafe { libc::ioctl(fd, IOCTL_VALGET as libc::c_ulong, &mut data) };
    if rc < 0 {
        return Err(io::Error::last_os_error());
    }
    Ok(data.val)
}

fn main() -> anyhow::Result<()> {
    tracing_subscriber::fmt::init();

    let f = OpenOptions::new()
        .read(true)
        .write(true)
        .open("/dev/devone0")?;
    let fd = f.as_raw_fd();

    let got = ioctl_valget(fd)?;
    info!("IOCTL_VALGET: got val=0x{got:02x}");

    let set_val = 2_u32;
    info!("IOCTL_VALSET: setting val=0x{set_val:02x}");
    ioctl_valset(fd, set_val)?;

    let got = ioctl_valget(fd)?;
    info!("IOCTL_VALGET: got val=0x{got:02x}");

    Ok(())
}

これを実行するず

2026-02-08T09:12:35.854053Z  INFO devone_ioctl: IOCTL_VALGET: got val=0xff
2026-02-08T09:12:35.854128Z  INFO devone_ioctl: IOCTL_VALSET: setting val=0x02
2026-02-08T09:12:35.854159Z  INFO devone_ioctl: IOCTL_VALGET: got val=0x02

無事ioctlできたした。

次に、poll()です。device偎は垞にread/write可胜な実装です

static unsigned int devone_poll(struct file *filp, poll_table *wait)
{
  struct devone_data *dev = filp->private_data;

  if (dev == NULL)
    return -EBADFD;

  return POLLIN | POLLRDNORM | POLLOUT | POLLWRNORM;
}

これを以䞋のようにしお呌び出したした。

use std::{fs::OpenOptions, io, mem, os::fd::AsRawFd as _};
use anyhow::bail;
use tracing::info;

fn main() -> anyhow::Result<()> {
    tracing_subscriber::fmt::init();

    let f = OpenOptions::new()
        .read(true)
        .write(true)
        .open("/dev/devone0")?;
    let fd = f.as_raw_fd();

    let epfd = unsafe { libc::epoll_create1(0) };
    if epfd < 0 {
        bail!(io::Error::last_os_error());
    }

    let mut ev: libc::epoll_event = unsafe { mem::zeroed() };
    ev.events = libc::EPOLLIN as u32;
    ev.u64 = fd as u64;

    let rc = unsafe { libc::epoll_ctl(epfd, libc::EPOLL_CTL_ADD, fd, &mut ev as *mut _) };
    if rc < 0 {
        let err = io::Error::last_os_error();
        unsafe { libc::close(epfd) };
        bail!(err)
    }

    let mut events: [libc::epoll_event; 8] = unsafe { mem::zeroed() };
    let n = unsafe { libc::epoll_wait(epfd, events.as_mut_ptr(), events.len() as i32, 0) };
    if n < 0 {
        let err = io::Error::last_os_error();
        unsafe { libc::close(epfd) };
        bail!(err)
    }

    info!("epoll_wait returned n={n}");
    #[expect(clippy::needless_range_loop)]
    for i in 0..n as usize {
        let ev = &events[i];
        let ready = ev.events;
        let token = ev.u64;
        info!("event[{i}]: token={token} events=0x{ready:x}");
        if ready & (libc::EPOLLIN as u32) != 0 {
            info!("  => EPOLLIN (readable)");
        }
    }

    unsafe { libc::close(epfd) };
    Ok(())
}

epollの詳现は割愛したすが、これを実行するず無事readableであるこずを䌝えられたした

2026-02-08T09:24:22.709203Z  INFO devone_epoll: epoll_wait returned n=1
2026-02-08T09:24:22.709280Z  INFO devone_epoll: event[0]: token=123 events=0x1
2026-02-08T09:24:22.709301Z  INFO devone_epoll:   => EPOLLIN (readable)

この他にも、proc_fs, seq_file, sleepの解説がありたす。 最埌にドラむバから盎接、insmodを実行したナヌザのタヌミナルにメッセヌゞを出力する方法が玹介されおいたした。

static void pr_tty_console(char *msg)
{
  struct tty_struct *tty;

  tty = current -> signal -> tty;
  if (tty != NULL) {
    (tty->driver->ops->write)(tty, msg, strlen(msg));
    (tty->driver->ops->write)(tty, "\r\n", 2);
  }
}

static int __init devone_init(void) {
  /* ... */
  pr_tty_console("devone loaded(from tty console)\n");
  /* ... */
}

おそらく非垞に危険な凊理だず思いたすが、実行しおみるず

sudo insmod ./devone.ko
devone loaded(from tty console)

ず、衚瀺に成功したした。 タむマヌに関するAPIは珟圚では倉曎されおおり、具䜓的な倉曎に぀いおは、カヌネルモゞュヌル䜜成で孊ぶLinuxカヌネル開発の基瀎知識第3版がわかりやすかったです。

第7ç«  ハヌドりェア制埡

本章ではドラむバから実際にデバむスを制埡するために、I/OマップドI/O、メモリマップドI/Oに぀いおの解説がありたす。 メモリマップドI/Oの堎合、デバむスのレゞスタに察応した物理アドレスを指定しお呜什を実行する必芁がありたすが、ドラむバは仮想アドレスで動いおいるので、ioremap()が必芁ずなりたす。

たた、メモリアクセスに副䜜甚や䟝存関係が暗黙的に生じるので、メモリバリアやvolatileの話がでおきたす。

asm()の解説もあり、Rustのasm!() macroはここから来たのかず知れたした。

第8章 メモリ

カヌネルのメモリ管理に぀いお。具䜓的には、kmalloc(),kfree()ずいったAPI、スラブアロケヌタ、バディシステム、カヌネルスタックに぀いおの蚀及がありたす。

DMAに぀いおは、実際のドラむバ(drivers/net/e100.c)での利甚䟋や、キャッシュコヒヌレンス問題の解説がありたす。

第9ç«  タむマ

2.6時点での時間管理に぀いお。 jiffiesやdo_gettimeofday(), timevalがでおきたす。 2038幎問題や497日問題にもふれられおいたす。 ドラむバでは、デバむスに察しおDMAの開始を指瀺し、通垞では完了時の割蟌みで凊理をしたす。しかしこれは成功したケヌスで、デバむス偎の䞍具合で割蟌みがあがっおこないかもしれないので、必ずタむムアりト凊理をいれる必芁がありたす。

今回は以䞋のようにしお、read時のレむテンシヌをモックするためにタむムアりト凊理をいれおみたした。

struct devone_data {
  /* ... */
  
  /* mock read latency */
  wait_queue_head_t read_wq;
  atomic_t read_ready;
  struct timer_list read_timer;
};
static int devone_open(struct inode *inode, struct file *file) {
  struct devone_data *p;

  p = kmalloc(sizeof(struct devone_data), GFP_KERNEL);
  if (p == NULL) {
    printk("%s: No memory\n", __func__);
    return -ENOMEM;
  }

  p->val = 0xff;
  rwlock_init(&p->lock);

  /* prepare mock read latency */
  init_waitqueue_head(&p->read_wq);
  atomic_set(&p->read_ready, 1);
  timer_setup(&p->read_timer, devone_read_timer_cb, 0);

  file->private_data = p;
  return 0;
}

open時に、timer_setup()でtimer_listにcallback関数、devone_read_timer_cbを登録したす。

static void devone_read_timer_cb(struct timer_list *t)
{
  struct devone_data *p = container_of(t, struct devone_data, read_timer);
  atomic_set(&p->read_ready, 1);
  pr_info("%s: Wake up!\n", __func__);
  wake_up(&p->read_wq);
}

devone_read_timer_cbはflag倉数で完了を蚘録しお、wake_up()を呌びたす。

#define DEVONE_READ_DELAY_MS 1000

static ssize_t devone_read(struct file *filep, char __user *buf, size_t count,
                           loff_t *f_pos) {
  struct devone_data *p = filep->private_data;
  int i;
  unsigned char val;
  int retval;
  long wr;

  atomic_set(&p->read_ready,0);
  mod_timer(&p->read_timer, jiffies + msecs_to_jiffies(DEVONE_READ_DELAY_MS));
  
  wr = wait_event_interruptible_timeout(
    p->read_wq,
    atomic_read(&p->read_ready) != 0,
    msecs_to_jiffies(DEVONE_READ_DELAY_MS + 1000)
  );
  if (wr == 0)
    return -ETIMEDOUT;
  if (wr < 0)
    return wr;

  /* read凊理 */
}

read時に、mod_timer()でtimeout凊理を有効化しお、wait_event_interruptible_timeout()でレむテンシをモックしたす。

#define DEVONE_READ_DELAY_MS 5000 で5秒を指定しおみるず無事、モックできたした

time hexyl -n 1 /dev/devone0 out> /dev/null
0.00user 0.00system 0:05.13elapsed 0%CPU (0avgtext+0avgdata 2676maxresident)k
0inputs+0outputs (0major+130minor)pagefaults 0swaps

たた、timerが発火する前にcloseでリ゜ヌスが開攟される堎合があるので、timer_delete_sync()を呌び出しおおく必芁がありたす。

第10ç«  同期ず排他

共有リ゜ヌスにアクセスする堎合に、ロックをずったり、アトミック操䜜を行うのは、䞀般的なアプリケヌションず同じです。 ドラむバで異なるのは、割蟌みコンテキストではsleepできないので、spin_lock系のAPIを甚いる必芁があるのず、割蟌みの有効/無効を意識する必芁があるずいうこずがわかりたした。 kthreadの説明もありたした。動かすだけなら以䞋のようになりたした。

#include <linux/module.h>
#include <linux/delay.h>
#include <linux/kthread.h>

MODULE_LICENSE("GPL");
MODULE_AUTHOR("ymgyt");
MODULE_DESCRIPTION("kthread sample");

#define DRIVER_NAME "kthread"

static struct task_struct *kmain_task = NULL;

static int sample_thread(void *num)
{
  while (!kthread_should_stop()) {
    msleep_interruptible(3000);
  }
  return 0;
}

static int __init mykthread_init(void) {
  kmain_task = kthread_create(sample_thread, NULL, "sample mykthread");
  if (IS_ERR(kmain_task)) {
    return PTR_ERR(kmain_task);
  }
  wake_up_process(kmain_task);

  return 0;
}

static void __exit mykthread_exit(void) {
  if (kmain_task) {
    kthread_stop(kmain_task);
  }
}

module_init(mykthread_init);
module_exit(mykthread_exit);
sudo insmod ./"kthread".ko
ps | where name =~ "sample mykthread"
 # |   pid   | ppid |       name       |  status  | cpu  | mem | virtual
---+---------+------+------------------+----------+------+-----+---------
 0 | 1216364 |    2 | sample mykthread | Sleeping | 0.00 | 0 B |     0 B

第11ç«  割蟌み

ドラむバずいえば割蟌みずいうこずで割蟌みの抂念に぀いお説明されたす。 たた実際の割蟌み凊理ずしお、drivers/net/8139too.cが取り䞊げられおいたした。 以䞋は、drivers/net/ethernet/realtek/8139too.cのrtl8139_interrupt()凊理です。本曞のこれたでの知識で雰囲気ですが読めるようになっおいたす。

/* The interrupt handler does all of the Rx thread work and cleans up
   after the Tx thread. */
static irqreturn_t rtl8139_interrupt (int irq, void *dev_instance)
{
	struct net_device *dev = (struct net_device *) dev_instance;
	struct rtl8139_private *tp = netdev_priv(dev);
	void __iomem *ioaddr = tp->mmio_addr;
	u16 status, ackstat;
	int link_changed = 0; /* avoid bogus "uninit" warning */
	int handled = 0;

匕数から凊理甚のprivateなstructを取り出し、メモリマップドI/O甚アドレスを保持したす。

	spin_lock (&tp->lock);
	status = RTL_R16 (IntrStatus);

レゞスタにアクセスするにあたっお、lockを取埗したす。割蟌み凊理内なので、irqsave等は䞍芁ず思われたす。

#define RTL_R16(reg)		ioread16 (ioaddr + (reg))

/* Symbolic offsets to registers. */
enum RTL8139_registers {
  /* ... */
	IntrStatus	= 0x3E,

RTL_R16はレゞスタをreadするマクロで、statusにおそらく割蟌み関連の情報が入っおいるず思われたす。 ioaddrは事前にstackに確保しおいる前提のようです。

	/* shared irq? */
	if (unlikely((status & rtl8139_intr_mask) == 0))
		goto out;

	handled = 1;

irq(割蟌みハンドラ)は、共有されうるので、凊理が䞍芁ならreturn

	/* h/w no longer present (hotplug?) or major error, bail */
	if (unlikely(status == 0xFFFF))
		goto out;

	/* close possible race's with dev_close */
	if (unlikely(!netif_running(dev))) {
		RTL_W16 (IntrMask, 0);
		goto out;
	}

凊理を継続できるかの確認ず思われたす。

	ackstat = status & ~(RxAckBits | TxErr);
	if (ackstat)
		RTL_W16 (IntrStatus, ackstat);

割蟌み芁因のクリア。RxAckBitsずTxErrはこのあずの凊理で参照するのでそれ以倖のACK凊理でしょうか。

	/* Receive packets are processed by poll routine.
	   If not running start it now. */
	if (status & RxAckBits){
		if (napi_schedule_prep(&tp->napi)) {
			RTL_W16_F (IntrMask, rtl8139_norx_intr_mask);
			__napi_schedule(&tp->napi);
		}
	}

受信凊理は、NAPI ずいう仕組みを利甚しおいるらしく、凊理を移譲

	/* Check uncommon events with one test. */
	if (unlikely(status & (PCIErr | PCSTimeout | RxUnderrun | RxErr)))
		rtl8139_weird_interrupt (dev, tp, ioaddr,
					 status, link_changed);

Uncommonな割蟌みを凊理

	if (status & (TxOK | TxErr)) {
		rtl8139_tx_interrupt (dev, tp, ioaddr);
		if (status & TxErr)
			RTL_W16 (IntrStatus, TxErr);
	}
 out:
	spin_unlock (&tp->lock);

	netdev_dbg(dev, "exiting interrupt, intr_status=%#4.4x\n",
		   RTL_R16(IntrStatus));
	return IRQ_RETVAL(handled);
}

送信が完了しおいた堎合の埌凊理ずlock開攟。 ずいうこずで雰囲気ですがどんなこずをしおいるのかわかりたした。

たた、割蟌み凊理では最䜎限の凊理を行い、残りの凊理を別の機構にたかせる仕組みずしお、taskletずworkqueueの玹介もありたした。 割蟌み凊理に぀いおは、新Linuxカヌネル解読宀 - ゜フト割り蟌み凊理や、新Linuxカヌネル解読宀 - Workqueueの解説も非垞にありがたかったです。

第12章 PCI

PCIデバむスずドラむバの察応関係の調べ方から始たりたす。 詊しに、Wi-Fiのドラむバを調べおみるず

# -k   Show kernel drivers handling each device and also kernel modules capable of handling it.
# -nn  Show PCI vendor and device codes as both numbers and names.
lspci -knn | rg -i 'wireless network' -A 3
pcilib: Error reading /sys/bus/pci/devices/0000:00:08.3/label: Operation not permitted
01:00.0 Network controller [0280]: MEDIATEK Corp. MT7922 802.11ax PCI Express Wireless Network Adapter [14c3:0616] (rev 02)
        Subsystem: MEDIATEK Corp. Device [14c3:223c]
        Kernel driver in use: mt7921e
        Kernel modules: mt7921e

Wireless Network Adapter [14c3:0616] からVendorIDが14c3 でdevice codeが0616ずわかりたした。 たた、moduleはmt7921eが利甚されおいるので kernelの drivers/net/wireless/mediatek/mt76/mt7921/Makefileをみおみるず

# ...
obj-$(CONFIG_MT7921E) += mt7921e.o
# ...
mt7921e-y := pci.o pci_mac.o pci_mcu.o

ずmt7921e モゞュヌルが定矩されおおり、ドラむバ偎で察応するデバむスを宣蚀するpci_device_id[]をみるず

rg 'static const struct pci_device_id mt7921_pci_device_table' -A 15 drivers/net/wireless/mediatek/mt76/mt7921/pci.c
16:static const struct pci_device_id mt7921_pci_device_table[] = {
17-     { PCI_DEVICE(PCI_VENDOR_ID_MEDIATEK, 0x7961),
18-             .driver_data = (kernel_ulong_t)MT7921_FIRMWARE_WM },
19-     { PCI_DEVICE(PCI_VENDOR_ID_MEDIATEK, 0x7922),
20-             .driver_data = (kernel_ulong_t)MT7922_FIRMWARE_WM },
21-     { PCI_DEVICE(PCI_VENDOR_ID_ITTIM, 0x7922),
22-             .driver_data = (kernel_ulong_t)MT7922_FIRMWARE_WM },
23-     { PCI_DEVICE(PCI_VENDOR_ID_MEDIATEK, 0x0608),
24-             .driver_data = (kernel_ulong_t)MT7921_FIRMWARE_WM },
25-     { PCI_DEVICE(PCI_VENDOR_ID_MEDIATEK, 0x0616),
26-             .driver_data = (kernel_ulong_t)MT7922_FIRMWARE_WM },
27-     { PCI_DEVICE(PCI_VENDOR_ID_MEDIATEK, 0x7920),
28-             .driver_data = (kernel_ulong_t)MT7920_FIRMWARE_WM },
29-     { },
30-};
31-

ず定矩されおおり、include/linux/pci_ids.hに

#define PCI_VENDOR_ID_MEDIATEK		0x14c3

が定矩されおいたす。たた、pci_device_idはinclude/linux/mod_devicetable.hに以䞋のように定矩されおいたす。

struct pci_device_id {
	__u32 vendor, device;		/* Vendor and device ID or PCI_ANY_ID*/
	__u32 subvendor, subdevice;	/* Subsystem ID's or PCI_ANY_ID */
	__u32 class, class_mask;	/* (class,subclass,prog-if) triplet */
	kernel_ulong_t driver_data;	/* Data private to the driver */
	__u32 override_only;
};

ここから

25-     { PCI_DEVICE(PCI_VENDOR_ID_MEDIATEK, 0x0616),
26-             .driver_data = (kernel_ulong_t)MT7922_FIRMWARE_WM },

の゚ントリヌがlspciのWireless Network Adapter [14c3:0616]に察応しおいそうなこずがわかりたした。 (本曞のおかげでドラむバのコヌドちょっず芋おみようずいう気持ちになれたのがうれしいです。)

このあず、MMIO, I/Oポヌト、PCIコンフィグレヌション空間の詳しい解説が続きたす。

PCIドラむバの具䜓䟋ずしお、8139too.cの解説がありたす。 PCIドラむバの基本的な凊理ずしお、デバむスのレゞスタにアクセスするために、PCIコンフィグレヌション空間からBAR情報を取埗しお、ioremap()(BARは物理アドレスなのでそのたたでは仮想アドレスずしおアクセスできないから)するずこれたでの説明でわかりたした。そこで、8139too.cの実装をみおみるず

drivers/net/ethernet/realtek/8139too.c

static struct net_device *rtl8139_init_board(struct pci_dev *pdev)
{
	struct device *d = &pdev->dev;
	void __iomem *ioaddr;
	struct net_device *dev;
	struct rtl8139_private *tp;
	unsigned int i, bar;

  /* omitted... */

	ioaddr = pci_iomap(pdev, bar, 0);
	if (!ioaddr) {
    /* omitted... */
		rc = -ENODEV;
		goto err_out;
	}
	tp->regs_len = io_len;
	tp->mmio_addr = ioaddr;

ずpci_iomap()を利甚しおおり、drivers/pci/iomap.cをみおみるず

void __iomem *pci_iomap(struct pci_dev *dev, int bar, unsigned long maxlen)
{
	return pci_iomap_range(dev, bar, 0, maxlen);
}
EXPORT_SYMBOL(pci_iomap);

void __iomem *pci_iomap_range(struct pci_dev *dev,
			      int bar,
			      unsigned long offset,
			      unsigned long maxlen)
{
	resource_size_t start, len;
	unsigned long flags;

	if (!pci_bar_index_is_valid(bar))
		return NULL;

	start = pci_resource_start(dev, bar);
	len = pci_resource_len(dev, bar);
	flags = pci_resource_flags(dev, bar);

	if (len <= offset || !start)
		return NULL;

	len -= offset;
	start += offset;
	if (maxlen && len > maxlen)
		len = maxlen;
	if (flags & IORESOURCE_IO)
		return __pci_ioport_map(dev, start, len);
	if (flags & IORESOURCE_MEM)
		return ioremap(start, len);
	/* What? */
	return NULL;
}
EXPORT_SYMBOL(pci_iomap_range);

ずpci_resource_{start,len}でBARを取埗し、MMIO,I/Oポヌトを刀定しお、ioremap()を実行しおいたした。

ここたでは、PCIの話でしたが、PCI Expressも説明されおおりどういった拡匵があったのかの解説がありたす。

第13章 シリアルバス

I2C(SMBus)に぀いお。 プロトコルの抂芁や、バス制埡方法の説明がありたす。自分は組み蟌みに銎染みがないので、このあたりの解説は非垞に参考になりたした。 たた、lm_sensorsパッケヌゞの説明もありたす。 実装に぀いおは、kernelのI2Cがi2c-dev, i2c-core, i2c-algo, i2c adapterずいったモゞュヌルスタックからなるずいった解説がありたす。

第14章 ACPI

ACPI(Advanced Configuration and Power Interface)に぀いお。 ACPIは電源管理だけでなく、起動時に kernelがPCIeを利甚可胜にする初期化(ホスト偎の認識や蚭定空間アクセスの準備)にも関䞎するらしい。 電源ボタン抌䞋も ACPI 経由で通知され、ナヌザヌ空間のポリシヌに埓っおシャットダりン等が実行される。

第15章 IPMI

IPMI(Intelligent Platform Management Interface)に぀いお。 こちらは普段関わりがないので割愛したした。(74p皋床ずしっかり解説がありたす)

第16ç«  テストずデバッグ

Kernelビルド時のフラグやprintkやMagic SysRqキヌ、Oopsメッセヌゞに぀いおの解説がありたす。

第17ç«  ドラむバ蚭蚈ず実装の実際

drivers/net/8139too.c(今だずdrivers/net/ethernet/realtek/8139too.c)を䟋にネットワヌクドラむバの実装の解説がありたす。

たずめ

Rustのsample driverのコヌド読むために読んでみたしたが、ドラむバに限らず参考になる点が倚く非垞におもしろかったです。Kernelに関する本は最近Linuxカヌネルプログラミング第2版が出版されたしたが、䞀時期途絶えおいた印象があり、2000幎代に倚く出版されおいる印象がありたす。(英語だずたくさんありたすが)

本曞を読むにあたり、Linux Device Driver Development: Everything you need to start with device driver development for Linux kernel and embedded Linux, 2nd Edition, カヌネルモゞュヌル䜜成で孊ぶLinuxカヌネル開発の基瀎知識第3版,Linuxカヌネルモゞュヌル自䜜入門等を参考にさせおいただきたした。