Linux内核编程——字符设备驱动程序

引言

本章我们将学习许多 Linux 驱动中使用的字符设备驱动程序。这些驱动允许你管理大量设备,比如键盘、鼠标等等。这是最简单的驱动模型之一,但它可以用于多种用途。

结构

本章涵盖以下主题:

  • 驱动类型
  • mknod 命令
  • 用户空间与内核空间的通信
  • udev 守护进程及动态文件
  • 第一类只读字符设备驱动
  • 第二类读写字符设备驱动
  • 第三类 ioctl 字符设备驱动
  • 开机时加载驱动

目标

本书的目标是理解不同类型的驱动程序、预定义的节点以及用户空间与内核空间之间的通信方式。接下来,我们将从探索字符设备驱动开始。

驱动类型

Linux 驱动在使操作系统能够与硬件设备通信和控制硬件方面起着关键作用。

Linux 驱动有多种类型,每种类型承担特定功能:

  • 字符设备驱动:处理按字符传输数据的设备,如键盘、鼠标和串口。
  • 块设备驱动:管理以固定大小块存储和检索数据的设备,如硬盘和固态硬盘(SSD)。
  • 网络设备驱动:促进 Linux 内核与网络接口卡(NIC)或网络适配器之间的通信。
  • 文件系统驱动:使操作系统能够读取和写入不同的文件系统,如 ext4、NTFS、FAT32 等,实现兼容性和数据访问。
  • USB 驱动:管理系统连接的 USB 设备,如闪存盘、键盘、鼠标、打印机等。
  • 图形驱动:控制图形输出,使操作系统能够与显卡和显示器协作。
  • 声音驱动:管理音频设备,实现声音的输出和输入功能。
  • 虚拟设备驱动:用于虚拟化环境,处理来宾操作系统与主机系统之间的通信。
  • 平台驱动:管理特定平台的硬件,比如嵌入式系统或专用硬件的驱动程序。
  • 固件驱动:与各种设备的固件交互,协助设备的初始化和运行。

这些驱动对内核有效地与各种硬件组件交互至关重要。每种驱动都遵循特定的协议和接口,确保操作系统与硬件之间的无缝协作。

mknod 命令

在 Linux 中,mknod 命令是一个重要工具,用于在 /dev 目录或文件系统中任意位置创建设备节点。它的主要功能是生成表示设备的特殊文件,包括块设备和字符设备。

该命令允许用户手动创建设备节点,指定设备类型(字符设备或块设备)、主设备号和次设备号,以及要创建的文件名。

对于字符设备,mknod 用于创建按字符处理数据的特殊文件,比如终端或鼠标。块设备则以固定大小的块管理数据,包含硬盘或固态硬盘(SSD)等设备。

mknod 的语法需要指定要创建的文件名、类型(c 表示字符设备,b 表示块设备),以及主设备号和次设备号。主设备号用于标识与设备相关联的驱动程序,次设备号则指定特定的设备实例或单元。

该命令通常由系统管理员或开发人员使用,用于设备管理和配置,尤其是在处理系统启动时未自动创建的特殊或定制设备时。

使用时需谨慎,因为 mknod 是低层操作,错误使用可能导致系统不稳定或安全问题。正确的权限和对设备类型及设备号的理解对于安全准确使用至关重要。

用法:

xml 复制代码
mknod [选项] <设备名> <类型> <主设备号> <次设备号>

参数说明:

  • <设备名>:指定要创建的设备节点名称。

  • <类型>:指定设备类型:

    • c:字符设备
    • b:块设备
  • <主设备号>:主设备号,标识该设备关联的驱动程序。

  • <次设备号>:次设备号,标识具体的设备实例或单元。

mknod 命令选项不多,主要用法是指定设备类型、主设备号、次设备号和设备节点名称。

示例:

  • 创建终端的字符设备节点:
bash 复制代码
sudo mknod /dev/myterminal c 5 1

这会创建一个名为 myterminal 的字符设备节点,主设备号为 5,次设备号为 1。

  • 创建分区的块设备节点:
bash 复制代码
sudo mknod /dev/mydisk b 8 0

这会创建一个名为 mydisk 的块设备节点,主设备号为 8,次设备号为 0。

  • 创建先进先出管道(命名管道):
bash 复制代码
sudo mknod /tmp/myfifo p

这会在 /tmp 目录下创建一个名为 myfifo 的 FIFO 特殊文件(命名管道)。

  • 创建空设备节点:
bash 复制代码
sudo mknod /dev/null c 1 3

这会重新创建常用的 /dev/null 设备节点,主设备号为 1,次设备号为 3,所有写入的数据都会被丢弃。

  • 创建随机数生成设备节点:
bash 复制代码
sudo mknod /dev/random c 1 8

这会创建 /dev/random 设备节点,提供随机数据,主设备号为 1,次设备号为 8。

请注意,使用 mknod 需要超级用户权限(如使用 sudo),因为它涉及系统级设备节点的创建。手动创建设备节点时应格外小心,确保主次设备号正确,且理解创建或修改这些节点对系统功能的影响。

用户空间与内核空间的通信

用户空间主要存放用户应用程序和系统库,比如 C 标准库(libc)。这些应用程序,例如网页浏览器、文本编辑器和游戏,都在用户空间运行,利用系统库实现各种功能。

内核空间则构成操作系统的核心,负责管理硬件、内存和进程等关键任务,并向用户空间提供基础服务。

当用户应用程序需要内核提供的服务时,会发起一次系统调用------请求执行特定操作,比如输入输出操作或进程创建。该调用会触发上下文切换,使 CPU 从用户空间切换到内核空间执行。

在切换过程中,内核能够访问用户空间和内核空间的内存。但出于安全和稳定性考虑,用户空间的进程不能直接访问内核空间的内存。

内核为用户空间提供了丰富的服务,包括进程管理、内存管理、设备驱动等。这些服务通过受控接口(如系统调用)进行访问,保证交互的正确性和系统的完整性。

此外,硬件事件或软件指令产生的中断和信号,会促使 CPU 从执行用户空间代码切换到内核空间处理这些事件。

进程间通信(IPC)机制,如管道(pipe)、套接字(socket)和共享内存,支持用户空间不同进程之间的通信,由内核进行管理和调控。

用户应用对文件系统的访问同样通过内核管理的系统调用进行,提供读取、写入文件、目录等文件系统相关操作的接口。

总之,内核空间与用户空间的交互由系统调用、中断、IPC 等受控机制和接口管理,允许用户应用访问内核管理的服务和资源,同时保证系统安全、稳定和功能完整。

为了与 GNU/Linux 内核交换数据,用户空间主要使用:

  • 文件
  • 套接字
  • 一些非常特殊的节点(稍后会介绍)

这些节点上的数据交换可以是按字符(character)或按块(block)进行,这也正是区分不同类型驱动的关键。

在 Linux 中,万物皆文件。按照惯例,用于与驱动通信的文件通常位于 /dev(设备)目录下。

Linux 中的 /dev 目录是特殊设备文件的重要存放位置,这些设备文件作为硬件组件、外设和系统资源的接口。/dev 是 device(设备)的缩写,里面包含了代表系统中各种设备的文件,这些文件作为访问点或符号链接存在,而非传统的数据存储。

这些设备文件通常称为设备节点,分为不同类型,如字符设备和块设备。字符设备(例如终端或鼠标)通过逐字符处理数据,而块设备(如硬盘和固态硬盘)则以固定大小的数据块进行管理。

应用程序和用户通过 /dev 目录中的这些设备文件间接与物理设备和虚拟设备进行交互。例如,访问光驱时,应用程序会与 /dev 中对应的设备文件通信。

系统启动时,/dev 目录的内容由子系统(如 udevdevfs)进行管理或动态生成,这一过程对于设备的正确初始化和配置至关重要。

设备文件的权限设置对维护系统安全尤为关键。它们通常归 root 用户所有,权限控制哪些用户或用户组能够访问和操作特定设备。直接访问某些设备文件(如代表整个硬盘的 /dev/sda)将赋予用户对系统的重要控制权。

/dev 目录的内容具有动态变化的特性,随着设备的添加、移除或修改而实时更新,反映了系统当前状态及所连接的设备情况。

总的来说,/dev 目录作为内核与用户空间之间的重要接口,通过这些特殊设备文件为应用和用户提供了与硬件设备及系统资源交互的标准化通道。

在上面的列表中,第一个字母有不同的含义:

  • b 表示块设备驱动
  • c 表示字符设备驱动

这些文件用于内核空间和用户空间之间的通信,或反之。

如果参考以下官方文档,我们可以看到所有这些设备文件是如何定义的:
www.kernel.org/doc/Documen...

下面是主设备号为 1 的字符设备驱动示例:

javascript 复制代码
  1 char  内存设备
      1 = /dev/mem     物理内存访问
      2 = /dev/kmem    内核虚拟内存访问
      3 = /dev/null    空设备
      4 = /dev/port    I/O 端口访问
      5 = /dev/zero    空字节源
      6 = /dev/core    已废弃 --- 被 /proc/kcore 替代
      7 = /dev/full    写入时返回 ENOSPC(无空间错误)
      8 = /dev/random  非确定性随机数生成器
      9 = /dev/urandom 速度更快但安全性较低的随机数生成器
     10 = /dev/aio     异步 I/O 通知接口
     11 = /dev/kmsg    写入内容作为 printk 输出
     12 = /dev/oldmem  crashdump 内核用来访问崩溃内核内存

举例,我们可以查看 /dev/urandom 是否存在,它的主设备号为 1,次设备号为 9:

bash 复制代码
$ ls -al /dev/urandom
crw-rw-rw- 1 root root 1, 9 12月 30 17:32 /dev/urandom

/dev/urandom 设备文件提供对随机数生成器的访问。

通过该设备文件请求随机数示例:

yaml 复制代码
$ hexdump /dev/urandom

002a580 558b 4ec6 6225 f686 f7b7 d7df 749a 29be
002a590 871f 5e91 fcf4 362d a39f 9857 3956 9c4a
002a5a0 2d03 617f 56f4 8246 28ac c966 cc02 c709
002a5b0 d10f 9f3c c23d 6a1f fdaf b124 34cc 144d
002a5c0 1965 6148 0ddd 800b a9fd 88f4 20dd 9685
002a5d0 e2b7 66a7 03ab a808 a2f9 ee6b b368 d6d9^C

现在,让我们在 /dev 目录下创建一个名为 alea 的新设备文件:

shell 复制代码
$ sudo mknod /dev/alea c 1 9

$ ls -al /dev/alea
crw-r--r--. 1 root root 1, 9 9月 14 13:34 /dev/alea

现在我们可以通过这个新设备文件请求随机数:

yaml 复制代码
$ hexdump /dev/alea

002a580 558b 4ec6 6225 f686 f7b7 d7df 749a 29be
002a590 871f 5e91 fcf4 362d a39f 9857 3956 9c4a
002a5a0 2d03 617f 56f4 8246 28ac c966 cc02 c709
002a5b0 d10f 9f3c c23d 6a1f fdaf b124 34cc 144d
002a5c0 1965 6148 0ddd 800b a9fd 88f4 20dd 9685
002a5d0 e2b7 66a7 03ab a808 a2f9 ee6b b368 d6d9^C

因此,/dev/urandom/dev/alea 这两个设备文件都与 GNU/Linux 内核的同一个随机数生成器进行通信:

下图展示了两个设备实例拥有相同的主设备号和次设备号:

/dev 目录中的静态设备文件是表示系统中存在的硬件设备的设备节点。这些文件通常在系统启动时由 udevmdev 或其他设备管理机制创建。

/dev 目录中常见的静态设备文件包括:

  • tty 和 pty :终端由 /dev/tty/dev/pts 中的文件表示。伪终端由 /dev/ptmx 表示。

  • null、zero、random

    • /dev/null 表示一个丢弃所有写入数据的设备。
    • /dev/zero 输出全为零的数据。
    • /dev/random/dev/urandom 提供随机数据。
  • sda、sdb、hda 等 :这些文件表示硬盘或固态硬盘。例如,/dev/sda 可能表示第一块硬盘。

  • loopX/dev/loopX 表示用于挂载磁盘映像文件的环回设备。

  • console/dev/console 通常表示系统控制台。

  • fbX/dev/fbX 设备用于帧缓冲设备。

  • dsp/dev/dsp 表示音频设备。

  • lp0、lp1:这些文件表示打印机设备。

  • eventX :输入设备如键盘和鼠标的事件设备由 /dev/input/eventX 表示。

这些静态设备文件为与系统连接的硬件设备交互提供了接口,应用程序和用户通过它们访问系统的硬件功能。

udev 守护进程与动态设备文件

Linux 中 /dev 目录下的动态设备文件是表示设备的特殊文件,这些文件在系统运行时会根据设备的连接、断开或变化动态创建、删除或修改。与静态设备文件不同,动态设备文件随着设备的插拔或系统状态变化而自动生成和管理。

这些动态设备文件包括:

  • USB 设备 :当 USB 设备如闪存盘、外置硬盘或外设连接时,会动态创建对应的设备节点,如 /dev/sdb/dev/ttyUSB0/dev/input/mouse0
  • 热插拔设备 :任何支持热插拔功能的设备(即系统运行时可以连接或断开)都会生成动态设备节点。例如插入 SD 卡可能会创建 /dev/mmcblk0
  • 虚拟设备 :由 QEMU、VirtualBox 等虚拟化系统创建的设备,通常会在虚拟环境内新增或移除虚拟设备时,在 /dev 下动态生成相应设备节点。
  • 网络设备 :如 /dev/net/tun 这样的动态设备文件,用于网络相关功能,尤其是虚拟网络接口或 VPN。
  • 临时和瞬态设备:某些网络接口或临时存储设备可能生成临时设备节点,这些节点仅在设备使用时出现,断开或停止使用时消失。

这些动态设备文件使得内核与用户空间能够在系统运行时实时检测、连接和管理硬件资源,提供灵活且适应性强的硬件访问方式。

Linux 中的 udev 是一个设备管理器,负责动态管理 /dev 目录中的设备节点。其主要作用是响应硬件变化或设备事件,处理设备节点的创建、删除和管理。

下图展示了 udev 模块与内核之间的通信关系:

netlink 消息示例:

javascript 复制代码
recv(4, // 套接字 id
"add@/class/input/input9/mouse2\0 // 消息
ACTION=add\0 // 动作类型
DEVPATH=/class/input/input9/mouse2\0 // /sys 中的路径
SUBSYSTEM=input\0 // 子系统(类别)
SEQNUM=1064\0 // 序列号
PHYSDEVPATH=/devices/pci0000:00/0000:00:1d.1/usb2/2-2/2-2:1.0\0 // /sys 中的设备路径
PHYSDEVBUS=usb\0 // 总线
PHYSDEVDRIVER=usbhid\0 // 驱动
MAJOR=13\0 // 主设备号
MINOR=34\0", // 次设备号
2048, // 消息缓冲区大小
0) // 标志
= 221 // 实际消息大小

udev 的工作原理:

  • 内核事件:当硬件设备连接、断开或发生变化时,内核生成事件通知用户空间这些变化。这些事件可能包括设备检测、驱动加载或硬件配置变化。
  • 基于规则的系统 :udev 根据存储在配置文件中的一组规则工作。这些规则定义设备应如何命名、应具备哪些权限、应使用哪些驱动。规则存放在 /etc/udev/rules.d/,按文件名顺序处理。
  • 设备匹配:udev 将内核传来的事件与规则进行匹配,以确定如何处理这些事件,以及如何相应地创建或修改设备节点。
  • 动态节点创建 :根据规则,udev 动态地在 /dev 目录中创建或移除设备节点。它会分配合适的名称(如硬盘设备 /dev/sda),并设置权限,确保用户或应用可访问。
  • 持久命名:udev 还能为设备分配持久名称,确保设备即使物理位置或连接顺序发生变化,重启后仍保持相同名称。这对硬盘或网络接口等设备尤为重要。
  • 自定义规则:系统管理员可以编写自定义 udev 规则,为特定设备定义特定行为,分配特定名称,或在特定硬件事件发生时触发特定动作。
  • 实时响应:udev 以动态方式运行,实时响应硬件配置或设备连接的变化,使系统无需人工干预或重启即可适应硬件变更。

总之,udev 是 Linux 系统动态管理设备节点的核心组件,确保设备被正确识别、配置,并实现一致的命名,提供了灵活、适应性强的硬件资源管理环境。

下图详细展示了 udev 的内部工作机制:

第一个只读字符设备驱动

Linux 中的字符设备驱动是内核与基于字符传输数据的硬件设备之间的重要桥梁,实现对键盘、鼠标、传感器、终端等逐字符收发数据的透明通信和控制。

这里介绍的是只读版本,允许从设备读取信息,但不支持写入。

结构与功能:
  • 驱动注册 :驱动通过如 struct file_operationsstruct cdev 等结构体向内核注册,声明其在系统中的存在和功能。
  • 文件操作处理 :字符驱动通过 struct file_operations 中的函数指针管理多种文件操作,包括 open、close、read、write、ioctl 和 poll。
  • 用户与内核交互:用户空间通过系统调用与字符设备交互,触发驱动内相应函数,完成数据传输和设备控制。

字符驱动通常负责:

  • 文件操作实现:开发者编写特定函数(如 open、read、write、release)处理内核与设备间的交互。
  • 设备初始化和注册:驱动初始化时向内核注册自身,初始化必要资源,建立功能。
  • 同步与错误处理:健壮的字符驱动通常集成互斥锁或自旋锁以管理并发访问,并实现完善的错误处理应对各种异常。

字符驱动是 Linux 内核中关键的一层,为用户应用与字符设备间提供标准化接口,确保内核与硬件间高效、可靠的通信,实现无缝集成和运行。

代码部分详解

首先,包含了驱动所需的 Linux 内核头文件:

  • atomic.h:原子操作,防止竞态条件
  • cdev.hfs.h:管理字符设备和文件操作
  • uaccess.h:安全地在内核和用户空间间复制数据
  • module.h:支持内核模块
  • device.h:设备相关工具
  • errno.h:错误代码定义
arduino 复制代码
#include <linux/atomic.h>
#include <linux/cdev.h>
#include <linux/delay.h>
#include <linux/device.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/kernel.h> /* for sprintf() */
#include <linux/module.h>
#include <linux/printk.h>
#include <linux/types.h>
#include <linux/uaccess.h> /* for get_user and put_user */
#include <linux/version.h>
#include <asm/errno.h>

接着是设备文件操作的函数原型:

arduino 复制代码
static int device_open(struct inode *, struct file *);
static int device_release(struct inode *, struct file *);
static ssize_t device_read(struct file *, char __user *, size_t, loff_t *);
static ssize_t device_write(struct file *, const char __user *, size_t, loff_t *);

定义了一些宏:

arduino 复制代码
#define SUCCESS 0
#define DEVICE_NAME "chardev" /* 设备名,显示在 /proc/devices */
#define BUF_LEN 80 /* 设备消息缓冲区最大长度 */

定义了部分全局变量:

c 复制代码
static int major; /* 设备主设备号 */
enum {
    CDEV_NOT_USED = 0,
    CDEV_EXCLUSIVE_OPEN = 1,
};
static atomic_t already_open = ATOMIC_INIT(CDEV_NOT_USED); /* 设备是否已被打开 */
static char msg[BUF_LEN + 1]; /* 设备返回的消息缓冲区 */
static struct class *cls; /* 设备类,用于 /dev 设备文件创建 */

定义文件操作结构体,关联具体操作函数:

ini 复制代码
static struct file_operations chardev_fops = {
    .read = device_read,
    .write = device_write,
    .open = device_open,
    .release = device_release,
};

device_read 函数实现:

该函数负责将设备中的消息 msg 复制到用户空间缓冲区 buffer 中,支持处理偏移量以实现多次读取及文件结束(EOF)标志。

arduino 复制代码
static ssize_t device_read(struct file *filp, /* 见 include/linux/fs.h */
                           char __user *buffer, /* 用户缓冲区 */
                           size_t length, /* 缓冲区长度 */
                           loff_t *offset)
{
    int bytes_read = 0;
    const char *msg_ptr = msg;

    if (!*(msg_ptr + *offset)) { /* 到达消息末尾 */
        *offset = 0; /* 重置偏移 */
        return 0; /* 返回 EOF */
    }

    msg_ptr += *offset;

    /* 复制数据到用户缓冲区 */
    while (length && *msg_ptr) {
        /* 用户缓冲区在用户空间,不能直接赋值,使用 put_user */
        put_user(*(msg_ptr++), buffer++);
        length--;
        bytes_read++;
    }

    *offset += bytes_read;

    /* 返回实际读取的字节数 */
    return bytes_read;
}

对于这个只读版本,下面的写操作函数不会被使用,但在读写版本中会用到。目前,我们只显示一个警告信息:

简单地拒绝写操作并返回错误(-EINVAL)。

arduino 复制代码
/* 当进程写入设备文件时调用,例如:echo "hi" > /dev/hello */
static ssize_t device_write(struct file *filp, const char __user *buff,
                            size_t len, loff_t *off)
{
    pr_alert("Sorry, this operation is not supported.\n");
    return -EINVAL;
}

到这里,我们只需向 GNU/Linux 内核注册驱动的元数据:

  • 注册初始化和清理函数。
  • 指明模块采用 GPL 许可证。
scss 复制代码
module_init(chardev_init);
module_exit(chardev_exit);
MODULE_LICENSE("GPL");

这段代码定义了一个基础的字符设备驱动,支持打开、读取、写入和关闭操作。它创建了一个名为 chardev 的设备驱动,缓冲区大小为 1024 字节。这个示例展示了如何处理 Linux 内核中字符设备驱动的基本操作。

下面对该字符设备驱动代码的重要部分进行拆解说明:

包含的头文件和宏(第1--26行):

  • 第1--21行:包含内核各项功能头文件:

    • <linux/atomic.h>:处理并发的原子操作。
    • <linux/cdev.h>:字符设备工具。
    • <linux/uaccess.h>:安全的用户-内核数据交换。
    • <asm/errno.h>:错误码,如 -EINVAL 和 -EBUSY。
  • 第23--26行:定义常量:

    • DEVICE_NAME:设备名,显示于 /proc/devices
    • BUF_LEN:缓冲区长度(80个字符)。

全局变量和枚举(第28--41行):

  • 第28行:major,存储动态分配的主设备号。
  • 第30--32行:CDEV_NOT_USEDCDEV_EXCLUSIVE_OPEN,用于跟踪设备状态的标志。
  • 第35行:already_open,原子变量,防止设备被多次访问。
  • 第37行:msg,消息缓冲区。
  • 第39行:cls,用于 sysfs 中设备注册的类结构体。

文件操作结构体(第43--47行):

  • 第43行:chardev_fops 定义文件操作:

    • read 指向 device_read(第108行)。
    • write 指向 device_write(第173行)。
    • open 指向 device_open(第76行)。
    • release 指向 device_release(第95行)。

模块初始化(第49--66行):

  • chardev_init

    • 第51--52行:使用 register_chrdev 动态注册设备,获取主设备号。
    • 第55--56行:调用 class_create 创建设备类(考虑内核版本兼容)。
    • 第59行:调用 device_create 注册设备(/dev/chardev)。
    • 第54、61行:记录初始化成功消息。

模块退出(第68--74行):

  • chardev_exit

    • 第69行:使用 device_destroy 删除 /dev/chardev
    • 第70行:销毁之前创建的类。
    • 第73行:注销主设备号,释放资源。

设备打开(第76--93行):

  • device_open

    • 第80--81行:通过 atomic_cmpxchg 确保设备独占访问,若已打开返回 -EBUSY。
    • 第83行:更新消息缓冲区,包含访问次数。
    • 第84行:增加模块引用计数。

设备释放(第95--106行):

  • device_release

    • 第98行:重置 already_open,允许其他进程访问设备。
    • 第103行:减少模块引用计数。

设备读取(第108--145行):

  • device_read

    • 第113--114行:处理消息末尾,偏移归零并返回 EOF。
    • 第116行:调整消息指针到当前偏移位置。
    • 第119--127行:使用 put_user 将内核空间消息复制到用户缓冲区。
    • 第130行:更新偏移量。
    • 第133行:返回实际读取的字节数。

设备写入(第173--178行):

  • device_write

    • 第175行:记录不支持写操作的警告。
    • 第176行:返回错误码 -EINVAL。

模块元数据(第180--182行):

  • 第180--181行:注册模块初始化和退出函数。
  • 第182行:声明模块许可证为 GPL。

这段代码实现了一个只读字符设备,具有以下功能:

  • 打开:确保设备独占访问,并记录设备被访问的次数。
  • 读取:将内核缓冲区中的消息复制到用户空间。
  • 写入:拒绝写入操作并返回错误。
  • 关闭:释放设备访问权,并减少模块的使用计数。

执行示例:GitHub 上有多个字符设备驱动示例:
code_5_char_driver_readonly
code_5_char_driver_readwrite
code_5_char_driver_ioctl

我们开始编译第一个驱动(只读版本):

bash 复制代码
$ make      
make M=/home/tgayet/Documents/bpb/05-character-device-drivers/code_5_char_driver -C /lib/modules/6.2.0-39-generic/build modules
make[1]: Entering directory '/usr/src/linux-headers-6.2.0-39-generic'
warning: the compiler differs from the one used to build the kernel
  The kernel was built by: x86_64-linux-gnu-gcc-11 (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0
  You are using:           gcc-11 (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0
  CC [M]  /home/tgayet/Documents/bpb/05-character-device-drivers/code_5_char_driver/chardrv.o
  MODPOST /home/tgayet/Documents/bpb/05-character-device-drivers/code_5_char_driver/Module.symvers
  CC [M]  /home/tgayet/Documents/bpb/05-character-device-drivers/code_5_char_driver/chardrv.mod.o
  LD [M]  /home/tgayet/Documents/bpb/05-character-device-drivers/code_5_char_driver/chardrv.ko
  BTF [M] /home/tgayet/Documents/bpb/05-character-device-drivers/code_5_char_driver/chardrv.ko
Skipping BTF generation for /home/tgayet/Documents/bpb/05-character-device-drivers/code_5_char_driver/chardrv.ko due to unavailability of vmlinux
make[1]: Leaving directory '/usr/src/linux-headers-6.2.0-39-generic'

查看模块信息:

makefile 复制代码
modinfo ./chardrv.ko
filename:       /home/tgayet/Documents/bpb/05-character-device-drivers/code_5_char_driver/./chardrv.ko
license:        GPL
srcversion:     D4620EBD654E39173DAF5D4
depends:        
retpoline:      Y
name:           chardrv
vermagic:       6.2.0-39-generic SMP preempt mod_unload modversions

编译完成后,我们可以将驱动加载到 GNU/Linux 内核:

shell 复制代码
$ sudo insmod ./chardrv.ko

检查驱动是否加载成功:

shell 复制代码
$ lsmod | grep chardrv
chardrv                16384  0

$ cat /proc/modules | grep chardrv
chardrv 16384 0 -- Live 0x0000000000000000 (OE)

此时,字符驱动在 sysfs 中暴露了如下数据:

ruby 复制代码
$ tree /sys/module/chardrv 
/sys/module/chardrv
├── coresize
├── holders
├── initsize
├── initstate
├── notes
├── refcnt
├── sections
│   ├── __mcount_loc
│   └── __patchable_function_entries
├── srcversion
├── taint
└── uevent

共有 3 个目录和 9 个文件。

内核日志中显示了主设备号,可用于后续操作:

csharp 复制代码
$ dmesg 
(...)
[423384.382019] I was assigned major number 235.
[423384.382098] Device created on /dev/chardev

注意:此处获得的主设备号为 235。

此时,驱动尚不能直接与用户空间通信。我们可以利用此主设备号创建一个虚拟设备文件。

创建设备文件的语法为:

bash 复制代码
mknod <device> <b/c> MAJOR MINOR

我们将使用:

  • 设备名:/dev/mydriver
  • 设备类型:c(字符设备)
  • 主设备号:235(由内核动态分配)
  • 次设备号:0(从零开始)

创建命令如下:

shell 复制代码
$ sudo mknod /dev/mydriver c 235 0
$ ls -al /dev/mydriver
crw-r--r-- 1 root root 235, 0 Jan 4 15:18 /dev/mydriver

为设备文件赋予权限:

shell 复制代码
$ sudo chmod 777 /dev/mydriver

现在,用户空间可通过 /dev/mydriver 访问你的驱动。

读取设备内容示例:

bash 复制代码
$ cat /dev/mydriver       
I already told you 0 times Hello world!
$ cat /dev/mydriver
I already told you 1 times Hello world!
$ cat /dev/mydriver
I already told you 2 times Hello world!
$ cat /dev/mydriver
I already told you 3 times Hello world!
$ cat /dev/mydriver
I already told you 4 times Hello world!
(...)

该驱动目前为只读版本,尝试写入会失败:

javascript 复制代码
$ sudo echo "0" > /dev/mydriver
bash: /dev/mydriver: Permission denied

第二个读写字符设备驱动

现在我们可以进入第二个驱动,它是读写模式,既允许读取,也允许向设备写入数据。

但在此之前,先卸载当前驱动:

shell 复制代码
$ sudo rmmod ./chardrv.ko

切换到第二个驱动(读写版)。之前的命令都相同,但现在可以修改驱动内部缓冲区的内容:

shell 复制代码
$ make
$ sudo insmod ./chardrv.ko

我们可以修改驱动的内部缓冲区:

shell 复制代码
$ sudo echo "hi bpb" > /dev/mydriver

然后查看缓冲区内容:

shell 复制代码
$ cat /dev/mydriver

第三个 ioctl 字符设备驱动

这个驱动在前一个的基础上增加了对 ioctl 调用的支持。

编译完成后,可能会有两个用于单元测试的二进制文件:

  • chardrv_ioctl_userspace
  • test_ioctl

加载驱动:

shell 复制代码
$ sudo insmod ./chardrv.ko

查看内核日志:

csharp 复制代码
$ dmesg
[434216.878779] Device created on /dev/char_dev

驱动自动创建了设备文件,接下来为其添加权限:

shell 复制代码
$ sudo chmod 777 /dev/char_dev

第一次调用可以检查缓冲区是否为空:

scss 复制代码
$ cat /dev/char_dev
$ dmesg
[434254.461111] device_open(000000005b82830e)
[434254.461137] device_release(0000000047783d40,000000005b82830e)

写入数据:

scss 复制代码
$ echo 'hello bpb' > /dev/char_dev
$ dmesg
[434271.155629] device_open(00000000d75bfae3)
[434271.155641] device_write(00000000d75bfae3,00000000d561ffa7,6)
[434271.155649] device_release(0000000047783d40,00000000d75bfae3)

读取数据:

scss 复制代码
$ cat /dev/char_dev
hello bpb
$ dmesg 
[434276.769177] device_open(00000000ac6f6b22)
[434276.769192] Read 6 bytes, 131066 left
[434276.769208] device_release(0000000047783d40,00000000ac6f6b22)

我们还可以调用 ioctl:

csharp 复制代码
$ sudo ./chardrv_ioctl_userspace
get_nth_byte message:Message passed by ioctl
get_msg message:Message passed by ioctl

该二进制程序通过 ioctl 系统调用发送字符串。

开机加载驱动

在 GNU/Linux 系统中,有两种方式可以实现开机加载驱动:使用 /etc/modules/etc/modules-load.d/

使用 /etc/modules
  1. 打开文件 /etc/modules

    shell 复制代码
    $ sudo nano /etc/modules
  2. 在文件末尾添加模块名称(例如:v4l2loopback)。

  3. 保存并退出(按 Ctrl+O,回车,然后 Ctrl+X)。

使用 /etc/modules-load.d/
  1. /etc/modules-load.d/ 目录下创建配置文件:

    shell 复制代码
    $ sudo nano /etc/modules-load.d/<module_name>.conf
  2. 添加模块名称(例如:v4l2loopback)。

  3. 保存并退出。

如果驱动需要传递参数,需添加到 /etc/modprobe.d/
  1. /etc/modprobe.d/ 目录下创建配置文件:

    shell 复制代码
    $ sudo nano /etc/modprobe.d/<module_name>.conf
  2. 以如下格式添加参数:

    ini 复制代码
    options <module_name> option1=value1 option2=value2
  3. 保存并退出。

总结

本章到此结束,我们展示了字符设备驱动的多种使用形式。还介绍了驱动的类型,以及如何通过设备文件与这些模块进行通信,无论是手动创建还是动态生成的设备文件。

下一章,我们将学习其他重要的类型,主要是块设备驱动,以及文件系统和虚拟文件系统(VFS)抽象层。

相关推荐
2401_8735878228 分钟前
Linux常见指令以及权限理解
linux·运维·服务器
Arthurmoo36 分钟前
Linux系统之MySQL数据库基础
linux·数据库·mysql
森焱森43 分钟前
无人机三轴稳定控制(2)____根据目标俯仰角,实现俯仰稳定化控制,计算出升降舵输出
c语言·单片机·算法·架构·无人机
李洋-蛟龙腾飞公司1 小时前
HarmonyOS NEXT应用元服务常见列表操作分组吸顶场景
linux·运维·windows
链上Sniper1 小时前
智能合约状态快照技术:实现 EVM 状态的快速同步与回滚
java·大数据·linux·运维·web3·区块链·智能合约
晨曦丿2 小时前
双11服务器
linux·服务器·网络
go54631584652 小时前
修改Spatial-MLLM项目,使其专注于无人机航拍视频的空间理解
人工智能·算法·机器学习·架构·音视频·无人机
李迟3 小时前
在Linux服务器上使用kvm创建虚拟机
java·linux·服务器
A_New_World3 小时前
Linux性能分析工具
linux
鹏大师运维3 小时前
在银河麒麟V10 SP1上手动安装与配置高版本Docker的完整指南
linux·运维·docker·容器·麒麟·统信uos·中科方德