Info

此文档已基本完成,欢迎进一步提出补充和修改建议。

1 概述

内核模块

内核模块是扩展内核功能的一段代码,能在内核运行时加载到内核中。内存模块可使内核支持与特定设备交互(加载驱动程序时)或支持文件系统。

内核模块主要是提供指定设备的处理过程供应用程序调用,并且运用中断来及时响应设备事件;在根本上驱动程序模块以内核管理的线程形式进行工作;当调用到指定模块时,内核同时会调出此模块的上下文(此模块的全局变量等)。

设备号

Info

下述内容适用于 32 位 CPU 架构的 Linux 系统。

64 位 Linux 使用 u64 类型标识设备,主次设备号各 32 位。

Linux 使用 32 位无符号长整型 unsigned long 来唯一地标识接入设备。要使接入的设备能够注册并使用,驱动程序需要先向操作系统获取设备号。

高 12 位是主设备号 (0~4095),主要用于区分设备类型,受内核直接管理;

低 20 位是次设备号 (0~1,048,575),主要用于区分同种类的单个设备(aka. 区分同种设备实例),由驱动程序自行管理。

要查看已注册的字符设备与块设备的主设备号,可访问 /proc/devices,在文件系统中可查看到这种结构的内容:

Character devices:
  1 mem
  4 /dev/vc/0
  4 tty
  4 ttyS
  5 /dev/tty
  5 /dev/console
  5 /dev/ptmx
# ... omitted
Block devices:
  1 ramdisk
  7 loop
  8 sd               # Hard disk default id
  9 md
 65 sd
 66 sd
 67 sd
# ... omitted

要查看次设备号,可以:

  • 指定设备路径,用 ls -l 查看;
  • 对于块设备,使用 lsblk -o NAME,MAJ:MINblkid
  • 访问 /sys/block/$(dev)/$(partit)/dev

设备号 Macro

位于 linux/kdev.h

  • 确定从设备号位数:#define MINORBITS 20
  • 取得主设备号字段:#define MAJOR(dev) ((dev) >> 20)(右移后仅低 12 位有效)
  • 取得次设备号字段:#define MINOR(dev) ((dev) & 0xfffff)(取得低 20 位)
  • 组合主设备和从设备号:#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))(可用于指定两个数,生成设备号)

2 Shell 指令

载入内核模块

insmod <path_to_module> [parameter_list]

若此模块依赖于其他模块,需手动逐级加载或使用 modprobe

列出已载入的内核模块

模块使用状态位于 /proc/modules

lsmod

一般列出3个项目:模块名称、模块使用的内存、其他模块与进程的使用计数。

若一个模块使用了其他模块提供的函数,提供函数的模块使用计数+1,在卸载使用的模块后-1。

卸载内核模块

rmmod <module_name>

待卸载的模块的使用计数为0时才可卸载,或使用 modprobe -r <module_name> 逐级卸载。

创建设备文件

设备文件是 Linux 将物理设备抽象化的一种方法,用于应用程序与驱动程序进行交互。

mknod /dev/<dev_name> <dev_type> <major_dev_id> <minor_dev_id>

Info

此指令创建的设备文件是非存储的,下次启动操作系统消失。

3 内核模块设计要素

3.1 使模块接受外部参数

需要导入 linux/moduleparam.h

使用 module_param Macro

Macro 的定义是:

module_param(name, type, perm)

参数

  1. 此模块定义的全局变量名称,全局变量一般应当声明为 static

  2. 此变量的类型,支持的类型请见 Macro 原始解释;

  3. 此变量的权限标志值,决定此变量在 /sys/module/$(uname -a)/parameters/$(name) 的权限及可见性。基本构成是 0*** 3位8进制数,也可使用内置 Macro 结合或运算设置,这些权限标志 Macro 设置在 linux/stat.h;若设置为0,该变量不会出现在文件系统。可设置的最大值为 0664

    TIP

    权限 Macro 的标识符结构

    在一个条目内选择权限与分配目标即可构成一个权限 Macro。

    #define S_I****
    • 读写运行权限:RWX

      分配给:当前用户 - U;用户组 - G;其他用户 - O

    • 读权限:R 分配给:当前用户 - USR;用户组:GRP;其他用户:OTH; 以下相同

    • 写权限:W

    • 运行权限:X

关于此 Macro 的原始解释:

module_param - typesafe helper for a module/cmdline parameter @value: the variable to alter, and exposed parameter name. @type: the type of the parameter @perm: visibility in sysfs.

  • @value becomes the module parameter, or (prefixed by KBUILD_MODNAME and a ".") the kernel commandline parameter. Note that - is changed to _, so the user can use "foo-bar=1" even for variable "foo_bar".
  • @perm is 0 if the the variable is not to appear in sysfs, or 0444 for world-readable, 0644 for root-writable, etc. Note that if it is writable, you may need to use kernel_param_lock() around accesses (esp. charp, which can be kfreed when it changes).
  • The @type is simply pasted to refer to a param_ops_##type and a param_check_##type: for convenience many standard types are provided but you can create your own by defining those variables.

Standard types are: byte, short, ushort, int, uint, long, ulong charp: a character pointer bool: a bool, values 0/1, y/n, Y/N. invbool: the above, only sense-reversed (N = true).

  1. 声明全局静态变量,并设置初始值(这作为默认值);

  2. 使用 module_param() Macro 使变量可接受外部参数;

  3. 在使用 insmod 加载模块时,在模块路径之后给出此模块的参数。

    参数传递形式:NAME=VALUE,参数间以空格分隔;

    对于数组型参数,参数传递形式:NAME=VALUE_1,VALUE_2,...

    传递字符或字符串时需要添加双引号。

还有一些类似 Macro:

  • 为参数添加描述信息 MODULE_PARM_DESC(name, "description")

  • 设置为数组型参数 module_param_array(name, type, *num, perm)

  • 设置为字符数组型参数

    module_param_string(name, str_buffer, len, perm) 需要指定存储区域的起始地址与大小

3.2 将函数或变量设为公共

需要导入 linux/export.h

通过导出符号表 Macro,可以将模块内的函数或变量导出以供其他内核模块使用。

  1. 通过导出符号表 Macro 导出指定的函数或变量,传入函数或变量标识符

    EXPORT_SYMBOL(symbol_name)
  2. 在其他模块中完整声明此函数,特别是其返回值和参数列表;可添加 extern 以标注此函数从外部导入;自声明处之后,可以使用此函数

WARNING

在实践中,要避免函数名称与变量名称相同。若在两者作用域重叠的位置通过标识符传递指针时,编译器将报告错误。

导出符号表后,会将这些函数或变量暴露于整个内核区域,命名函数或变量时要减少在内核空间名称重复的可能性,同时要避免大量导出函数或变量;当符号名称标识重复时可产生冲突;若涉及变量操作,还要注意访问互斥↗

3.3 使应用程序调用模块接口

此节针对字符类存储设备编写,在驱动程序中需要导入 linux/fs.h;它是 Linux 提供的虚拟文件系统接口。

Info

此节使用现代型的设备号获取函数,需要进行步骤3以绑定操作函数到指定设备号;

对于不同设备类型,可能有略微不同的应用程序模块接口,请查看[此处↗](#4.4 自定模块接口函数)以获取函数种固有参数的解释。

  1. 在模块代码中实现以下4个函数,其中函数标识符、形式参数名称可自定,返回值和参数列表应保持一致

    // Connect to device
    int char_dev_open(struct inode* f_node, struct file* file_ptr);
    // Release device
    int char_dev_close(struct inode* f_node, struct file* file_ptr);
    // Read from device
    ssize_t char_dev_read(struct file* file_ptr, char __user* usr_buf, size_t size, loff_t* offset);
    // Write to device
    ssize_t char_dev_write(struct file* file_ptr, const char __user* usr_buf, size_t size, loff_t* offset)
  2. 定义一个 file_operations 结构体的变量,然后用初始化列表初始化

    const struct file_operations file_ops = {
        .owner = THIS_MODULE;  // Optional
        .open = char_dev_open,
        .release = char_dev_close,
        .read = char_dev_read,
        .write = char_dev_write
    }
  3. 将此结构体绑定到指定的设备号,此函数调用应设置在模块载入函数

    cdev_init(&var_cdev, &file_ops);
    cdev_add(&var_cdev, dev_id, 1);   // It returns status
  4. 创建设备文件,需要指定正确的设备号和设备类型,设备文件是应用程序使用驱动程序的接口

    通过 Shell 指令创建:

    mknod /dev/controller c 241 0
    # device_path   type   major_devid   minor_devid

    通过驱动程序创建:

    // Global variables
    static struct class* this_module;
    static struct device* char_device;
     
    this_module = class_create(THIS_MODULE, "test_driver"); // It returns status
    device_create(this_module, NULL, dev_id, NULL, "dev_name"); // It returns status
  5. 在应用程序导入头文件,编写操作过程

    #include "stdio.h"
    // read() write() from here
    #include "unistd.h"
    // open() close() from here
    #include "fcntl.h"
    #include "errno.h"
    #include "string.h"
  6. 在驱动程序卸载时,最好及时删除设备文件,归还操作系统分配的资源

    device_destroy(driver_class, dev_id);
    class_destroy(driver_class);

像操作普通文件那样,进行 open()(打开设备文件), close(), read(), write() 操作,驱动程序提供的函数将被调用,可以发现在内核信息中出现了驱动程序中的输出信息。

3.4 定义设备指令

需要导入 linux/ioctl.h,可通过此头文件查看指令结构定义。

WARNING

此 IO 设备控制为传统方法,需考虑避免设备类型号和指令号重叠问题造成 IO 设备误操作的问题,可考虑改用其他更现代的 IO 控制方式。

为了更好地控制不适合传统读写的非标准IO设备,驱动程序应当根据硬件操作文档定义一些设备指令。Linux 提供这些 Macro 以定义设备指令:

  • _IO(type,nr) 不附带参数的指令;
  • _IOR(type,nr,size) 读取驱动程序数据的指令;
  • _IOW(type,nr,size) 写入数据到驱动程序的指令;
  • _IOWR(type,nr,size) 读写驱动程序的指令。

Macro 参数

  1. type - 标识设备类型,8位;
  2. nr - 指令序号,8位;
  3. size - 数据类型大小**(字面意义,但应传入数据类型)**,单位是字节,14位(高版本 Linux)。

设备指令结构

设备指令由 32b 构成,按以下顺序组合:

数据方向 dir数据类型大小 size设备类型 type指令编号 nr
31~30 (2b)29~16 (14b) (高版本)15~8 (8b)7~0 (8b)
  • 数据方向 (0~3)

    #define _IOC_NONE 0U - 无数据传输;

    #define _IOC_READ 1U - 读数据(用户空间从内核空间读);

    #define _IOC_WRITE 2U - 写数据(用户空间向内核空间写);

    读写数据为 3。

  • 数据类型大小 标识一个数据项的大小,这决定这次操作要完成的字节传输量;传入时应直接给出数据类型,在头文件定义处有 sizeof() 转换。

  • 设备类型 (aka. Magic number) 标识一个设备,操作同一个设备的一组指令,此字段都应该相同。**避免使用已被其他驱动程序使用的设备类型号。**常设置为一个 ASCII 可见字符。

    TIP

    若 Linux 系统下有源码,可通过以下指令在文档中查看已注册的设备类型号

    或者在线查看内核文档↗

    CodeSeq# (hex)Include FileComments
    0x0000-1Flinux/fs.hconflict!
    0x0000-1Fscsi/scsi_ioctl.hconflict!
    0x0000-1Flinux/fb.hconflict!
    0x0000-1Flinux/wavefront.hconflict!
    0x02alllinux/fd.h

    以上为部分已使用设备类型号的示例。

    第1列是设备类型号;第2列是使用的设备指令编号范围;第3列是使用设备类型号和设备指令编号范围的模块,它们被 Linux 管理者采纳并分配;第4列是备注。

    要创建一个驱动程序的设备指令时,应当首选未出现在此文档的设备类型号,若设备类型号必须相同,那么必须避免设备指令编号落在已分配的编号范围。

    [2025.08.16] 未列出的部分可用设备类型号:0x01, 0x05, 0x23~0x3D, J, Y, x

  • 指令编号 (0~255)

    由驱动程序自行确定的区分不同指令的唯一编号。若与已存在模块使用了相同的设备类型号,必须避免与其他模块使用相同的指令编号。

获取指令属性的 Macro

  • _IOC_DIR(nr) 获取指令方向;
  • _IOC_TYPE(nr) 获取指令控制的设备类型;
  • _IOC_NR(nr) 获取指令序号;
  • _IOC_SIZE(nr) 获取数据大小。

向应用程序提供指令接口

为了能使指令能被接收并处理,需要创建 IO 设备指令处理函数,其中函数名及形参名可自定:

long test_ioctl(struct file* file, unsigned int cmd, unsigned long args) {
    switch(cmd) {
        case CONSTANT:
            // IO Operation for device
            break;
        case CONSTANT:
            // ...
            break;
    }
    return 0;
}

Info

指令处理函数的形参:

  1. 已打开的文件,来源于应用程序调用 ioctl() 的文件标识符;
  2. IO 指令编号;
  3. 传入的参数内存地址,来源于应用程序调用 ioctl() 的可选参数列表,若一条指令需要参数,需进行非空判断。必须先用 copy_from_user() 将内容复制到缓冲区,

常用的返回值有:

-ENOTTY (25) 驱动程序不支持此 IO 指令;

-EINVAL (22) 无效的参数;

-EFAULT (14) 应用程序提供的参数指针错误,例如调用 copy_xx_user() 时失败,返回此值;

-EPERM (1) 权限不足;

-EBUSY (16) 资源繁忙;

-EAGAIN (11) 资源未就绪,需要重试;

-ENODEV (19) 设备未初始化;

-EIO (5) 与设备通信失败;

-ENOMEM (12) 内存分配失败。

返回值必须带有负号,因为内核空间的错误码是负值,但转换到用户空间时设置的 errno 为正值;必须规范返回值,不当的返回值会影响应用程序的运行判断。

为了能使 Linux 内核找到 IO 处理函数,需要将其设置于驱动程序定义的 file_operations 结构体中:

const struct file_operations fops = {
    .unlocked_ioctl = test_ioctl;
    // ...
};

然后,需要在应用程序侧也导入这些定义的 IO 指令,并通过 ioctl() 调用它们:

ioctl(fd, TEST_IOCTL_CMD_1);          // IO 指令无参数时
ioctl(fd, TEST_IOCTL_CMD_2, &args);   // IO 指令有参数时

有关创建 IO 指令的完整流程,请查看文档末尾的示例程序。// TODO

3.5 避免临界资源访问竞争

只要是存在并发的系统,都必然有发生临界资源的访问竞争问题。Linux 内核提供了一些方案以避免访问竞争。若驱动程序提供了一些公共变量,应当在访问期间设置锁。

关中断

需要导入 linux/irqflags.h

Info

除非需要:访问中断处理程序共享变量;硬件级原子操作保证;对实时性要求高;

其他情况建议改用后续介绍的其他工具。

关中断是为避免产生数据竞争保障访问顺序性的最基本工具。在访问数据共享部分时关闭中断以确保访问期间不会因中断处理程序运行造成数据异常。

关闭当前核心的中断

local_irq_disable()

开启当前核心的中断

local_irq_enable()

WARNING

关中断在多核或多处理器系统中无效。

自旋锁

需要导入 linux/spinlock.h

自旋锁是一种基本同步互斥工具,若程序访问已被锁定的自旋锁时,它仍会被调度运行,且会循环检测锁状态,直到锁可用时继续运行。自旋锁是一种忙等待锁,只适合运行时间极短的临界区使用。

将自旋锁初始化

// 静态初始化
DEFINE_SPINLOCK(test_slock);
 
// 动态初始化
spinlock_t test_slock;
spin_lock_init(&test_slock);

加锁/解锁函数

spin_lock(&test_slock);
// Critical region
spin_unlock(&test_slock);

Info

若公共变量可能被中断程序修改,可以使用加锁/解锁函数的中断保存版本。

unsigned long flags;
spin_lock_irqsave(&test_slock, flags);
spin_unlock_irqrestore(&test_slock,flags);

WARNING

不要在自旋锁锁定期间使用可使程序进入休眠的函数,例如 sleep()。这会降低系统运行效率。

互斥锁

需要导入 linux/mutex.h

拥有简单同步机制的互斥访问工具,使用较为高效。若程序尝试获取已锁上的互斥锁,会进入休眠,直到锁可用后再继续运行。

将互斥锁初始化

// 静态初始化
DEFINE_MUTEX(test_mutex);
 
// 动态初始化
struct mutex test_mutex;
mutex_init(&test_mutex);

加锁/解锁函数

mutex_lock(&test_mutex);
mutex_unlock(&test_mutex);

Info

非休眠等待版本

mutex_trylock(&test_mutex);

信号量

需要导入 linux/semaphore.h

互斥锁的进阶版本,若公共资源可能需要较长时间持有,或者有多个同类资源可供访问,可以使用信号量实现同步互斥。信号量未能获取时程序会进入休眠状态而不会忙等待。

将信号量初始化

// 静态初始化
DEFINE_SEMAPHORE(test_sp);
 
// 动态初始化
struct semaphore test_sp;
sema_init(&test_sp, 1);      // 根据资源数量,修改第2个值

获取/归还信号量

down(&test_sp);
// Critical region
up(&test_sp);

Info

非休眠等待版本

down_trylock(&test_sp);

限时等待版本

down_timeout(&test_sp, jiffles + HZ);

TIP

jiffles 是 // TODO

原子操作

需要导入 asm/atomic.h

要使一个操作在运行期间不会被切换或被中断,需要使用到原子操作。原子操作是最轻量级的数据访问互斥工具,效率较高。

原子变量操作

初始化原子变量

atomic_t test_atom = ATOMIC_INIT(0);

使用函数修改原子变量的值,赋值过程不会被打断

atomic_set(&test_atom, 15);

获取原子变量值

int value = atomic_read(&test_atom);

进行加法或减法

atomic_add(12, &test_atom);   // test_atom + 12
atomic_sub(6, &test_atom);    // test_atom - 6
 
atomic_inc(&test_atom);       // test_atom += 1
atomic_sub(&test_atom);		  // test_atom -= 1

Info

运算函数也有能够返回结果值的变种:

int result;
result = atomic_add_return(12, &test_atom);
result = atomic_sub_return(6, &test_atom);
 
result = atomic_inc_return(&test_atom);
result = atomic_sub_return(&test_atom);

运行后条件检查,若运算后的结果为0,返回 true

atomic_inc_and_test(&test_atom);
atomic_dec_and_test(&test_atom);
atomic_add_and_test(&test_atom);
atomic_sub_and_test(&test_atom);

原子化位操作

需要导入 asm/bitops.h

Info

位从0开始计数,操作的变量应为整型。

将变量的指定位设置为1

set_bit(bit_order, &value);

将变量的指定位设置为0

set_bit(bit_order, &value);

将变量的指定位翻转

change_bit(bit_order, &value);

测试并设置值,函数会返回原始值,然后设定值

test_and_set_bit(bit_order, &value);
test_and_clear_bit(bit_order, &value);

4 函数接口

使用这些函数需要导入 linux/module.h, linux/init.h

4.1 基本

输出信息

int printk(KERN_LVL "STRING", __format)

参数

  • 要输出的字符串,字符串前能够使用 Macro 指定输出信息级别,定义的级别有:

    #define KERN_EMERG   "<0>" // 危急
    #define KERN_ALERT   "<1>" // 紧急
    #define KERN_CRIT    "<2>" // 关键
    #define KERN_ERR     "<3>" // 错误
    #define KERN_WARNING "<4>" // 警告   *默认*
    #define KERN_NOTICE  "<5>" // 提示
    #define KERN_INFO    "<6>" // 信息
    #define KERN_DEBUG   "<7>" // 调试
  • 字符串中的变量列表;

Info

  • 此函数不支持输出浮点数;

  • 此函数允许随处调用,包括中断;

  • 待输出完成后才会继续向后运行;

  • 可在 /proc/sys/kernel/printk 查看可输出消息级别,可通过 echo 修改它的值。

    4个值分别是:

    • 控制台输出级别 (4);(小于等于此值将显示)
    • 默认消息级别 (4);
    • 最低控制台输出级别 (1);(大于等于此值将显示)
    • 默认控制台输出级别 (7)。

    在标准 Linux 的分发版本,修改输出消息级别可能无效,因为内核消息可能仅输出至真实终端(例如进入操作系统后进入的不是图形界面,而是文本交互界面)。如果要尝试修改,请使用以下指令:

    echo "7 4 1 7" > sudo tee /proc/sys/kernel/printk

将内容复制到目标空间

需要导入 linux/uaccess.h

操作系统将内核空间与用户空间隔离开来,以确保系统运行的稳定性和安全性。若访问目标内存与当前代码不在同一空间,无法通过直接解引用访问。

内核空间→用户空间

unsigned long copy_to_user(void *to, const void *from, unsigned long n)

用户空间→内核空间

unsigned long copy_from_user(void *to, const void *from, unsigned long n)

参数

  1. 源内存空间;
  2. 目标内存空间;
  3. 要复制的字节数。

返回值

若复制成功,返回0;若复制失败,则返回未成功复制的数据字节数。

4.2 设备地址映射

需要导入 linux/io.h

为了提高内存的访问安全性,抽象化访问设备的方式,更好地支持多任务,Linux 使用虚拟内存机制管理内存。为了能够操作外部设备,需要将外部设备的物理地址转换为虚拟地址,这由内核管理。

将设备物理地址转换为虚拟地址

void __iomem *ioremap(phys_addr_t offset, size_t size)

4.3 字符设备

需要导入 linux/fs.h

IMPORTANT

在 Linux 2.6+ 版本中,不推荐使用 register_chrdev() 获取字符设备号;它固定分配 256 个次设备号,且依赖旧版字符设备 hash 表,性能较低;此处只列出新版设备号获取函数。

[测试于 ARM Linux] 获取设备号后,还需要将设备号绑定至设备文件才可被应用程序使用。

获取静态分配的字符设备号

此方法获取固定值的设备号,若已被使用会使设备号注册失败。

int register_chrdev_region(dev_t first, unsigned int count, const char *name)

参数

  1. 期望注册的设备号,可用 MKDEV() Macro 生成;
  2. 要连续注册的设备号数量;
  3. 设备名称,在 /proc/devices 中可查看到。

返回值

若注册成功,返回0;其他情况返回对应错误码。

获取动态分配的设备号

由内核分配一个未注册的字符设备号。

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

参数

  1. 注册后的设备号指针,需通过此指针获取设备号;
  2. 起始次设备号,常为 0;
  3. 次设备号请求数量;
  4. 设备名称。

返回值

若注册成功,返回0;其他情况返回对应错误码。

归还设备号

void unregister_chrdev_region(dev_t first, unsigned int count);

参数

  1. 设备号;若连续注销多个,传入起始设备号;
  2. 要连续注销的设备号数量;

[Macro] 创建设备文件

device_create()

[App] 向驱动程序发出设备指令

需要导入 sys/ioctl.h

Info

此函数在应用程序使用。

int ioctl(int fd, unsigned long request, ...);

参数

  1. 已打开设备的文件描述符;
  2. 预先定义的请求指令,应定义在驱动侧且可被应用程序获取;
  3. 可变参数,用于传递额外数据;应当传递地址,因为驱动程序可能通过此处返回结果。

返回值

若驱动程序接收指令并返回0,此函数会返回0;若驱动程序返回非0,此处返回 -1 并根据驱动程序返回的值设置 errno 值。

4.4 自定模块接口函数

要使接口函数能被调用,要为驱动程序内唯一定义的 file_operations 结构体中的特定成员设置函数指针。函数名称和形式参数名称可自定。

打开设备文件

当应用程序打开一个设备文件(一般通过 open())时,操作系统会调用此函数。在 file_operations 结构体中,应绑定此函数至 open 函数指针成员。

int open_device(struct inode* f_node, struct file* file_ptr)

参数

  1. 指向被打开设备在文件系统中的索引节点指针,常用成员有:
    • i_cdev 字符设备号 或 i_bdev 块设备号;
    • i_private 驱动程序针对设备自行管理的私有数据指针;
  2. 指向应用程序进程打开的此设备文件的指针,常用成员有:
    • f_mode 应用程序进程打开此文件的方式(读/写);
    • f_flags 应用程序进程打开此文件的标志信息(读/写/行为);
    • f_pos 当前文件指针的位置;
    • private_data 驱动程序针对当前进程自行管理的私有数据指针。

返回值

若成功打开,应返回0;若失败,应返回对应错误码。

规范化行为指导

  1. 若设备需要互斥访问,需要通过 f_node 检查设备是否占用,若占用应返回 -EBUSY
  2. 要检查打开的文件指针中的 f_flags 信息,以此决定驱动的行为(例如 fmode 若为写访问 FMODE_WRITE,需要检查是否占用,追加标志位被设置时要移动指针到文件尾);
  3. 必要时初始化设备。

自设备读(字符设备)

IMPORTANT

此函数结构仅限字符类设备(包括流设备)使用。

当应用程序读取设备文件(一般通过 read())时,操作系统会调用此函数。在 file_operations 结构体中,应绑定此函数至 read 函数指针成员。

ssize_t read_device(struct file* file_ptr, char __user* usr_buf, size_t size, loff_t* offset)

参数

  1. 指向应用程序进程打开的此设备文件的指针,查看常用成员请[转到此处↗](# 打开设备文件):
  2. 对应用户空间缓冲区的指针,需通过 copy_to_user() 将数据复制到此处,不能直接解引用复制; __user 是一个 Macro 标记,防止直接解引用使用;
  3. 应用程序进程请求读取的字节数量;
  4. 读取起始地址的字节偏移量指针,需要解引用以获取此偏移量值,若读取成功一般应当正偏移至已读取字节。

返回值

若成功读取,返回读取的字节数;若失败,应返回对应错误码。

规范化行为指导

  1. ⚡必须通过 file_ptr -> f_modefile_ptr -> f_flags检查设备文件的打开方式,若读位未设置立刻返回 -EBADF
  2. 需要检查给出的读起始地址是否超过设备地址范围,若已超出立刻返回 0 (EOF);
  3. ⚡必须根据 file_ptr -> f_flags 中的文件标志位决定读取行为。例如是否为等待型读取:若为非等待型读取且此时设备数据不可用,立刻返回 -EAGAIN;若等待读取期间被中止,要返回 -ERESTARTSYS
  4. ⚡必须根据缓冲区剩余数据量和 size 中请求的数据量,取其最小值作为读取数据量;
  5. 通过 copy_to_user() 将设备数据复制到用户空间,且确保其返回0;
  6. 若操作完成,要更新此应用程序进程的设备文件指针 offset,增加至已读取字节数;
  7. 操作完成,最终应返回实际读取字节数;
  8. 若操作的变量可能因并发被修改,必须使用锁确保互斥访问。

Info

不能通过用户缓冲区指针得到用户缓冲区的可用大小,而是由应用程序通过 size 给出可用大小,由应用程序确保空间大小合适。

写入设备(字符设备)

当应用程序写入一个设备文件(一般通过 write())时,操作系统会调用此函数。在 file_operations 结构体中,应绑定此函数至 write 函数指针成员。

ssize_t write_device(struct file, const char __user* usr_buf, size_t size, loff_t* offset)

参数与返回值和读取函数一致,但此处不应修改用户缓冲区。

Info

若希望获取用户缓冲区的字符串字节长度,可以使用 strnlen_user()

关闭设备文件

当应用程序关闭一个设备文件(一般通过 close())时,操作系统会调用此函数。在 file_operations 结构体中,应绑定此函数至 close 函数指针成员。

int close_device(struct inode* f_node, struct file* file_ptr)

参数与返回值和打开函数一致。

多路IO支持

需要导入 linux/poll.h

要使应用程序能够同时监听多个文件描述符而无需创建多个等待线程,需要由驱动程序实现多路IO支持函数,并将其绑定在 file_operations 中的 poll 成员。

__poll_t driver_poll(struct file* file_ptr, struct poll_table_struct* ptable_ptr)

参数

  1. 指向应用程序进程打开的此设备文件的指针,[详细信息↗](# 打开设备文件);
  2. 由内核提供的结构体指针,通过此指针可注册一个等待队列。

返回值

应返回特定的设备状态标志位,使内核知道设备当前状态,内核会根据此返回值决定是否通知应用程序进程。

规范化行为指导

  1. 必须事先设置好此驱动程序的私有数据结构体,其中设置 wait_queue_head_t 类型的成员,然后在驱动程序的文件打开函数中,初始化这个结构体(建议在动态内存初始化),并将其置于 file 中的 private_data 指针:

    // 按应用程序需要,定义读或写队列;
    // 在应用程序打开设备文件时(换句话说:在调用驱动程序的open()时),初始化它们
    struct ProcessData {
    	wait_queue_head_t read_queue;
    	wait_queue_head_t write_queue;
    }
     
    // 在文件打开函数中,kmalloc() 最大支持 128KB
    // 第2个参数也可以用 GFP_KERNEL
    struct ProcessData* pd_ptr = kmalloc(sizeof(struct ProcessData), GFP_ATOMIC);
    file_ptr->private_data = pd_ptr;
  2. driver_poll() 函数内,从 file 中获取 private_data 指向的结构体,将其中的等待队列成员传到 poll_wait() 的第2个参数:

    // 按应用程序需要,注册读或写队列;
    // 通过 file_ptr 获取应用程序的打开方式,然后为其分配队列
    poll_wait(file, &file_ptr->private_data->read_queue, ptable_ptr);
    poll_wait(file, &file_ptr->private_data->write_queue, ptable_ptr);
  3. 设置条件判断语句,对于读设备,当有数据可读时,返回:

    __poll_t mask = 0;
    mask |= POLLIN | POLLRDNORM;

    对于写设备,当有空间可写时,返回:

    __poll_t mask = 0;
    mask |= POLLOUT | POLLWRNORM;
  4. 在应用程序侧,需要使用

异步IO支持

需要导入 linux/poll.h

常规的应用程序IO请求,需要由应用程序等待完成或轮询检查,这在一定程度上降低了系统效率;如果希望在应用程序发起IO请求后继续运行,在IO完成后再告知应用程序转入运行,那么需要使用异步IO。这也要求驱动程序定义异步IO支持函数并绑定到 file_operations 的成员 fasync

int driver_fasync(int fd, struct file *file_ptr, int on)

参数

  1. 文件描述符;
  2. 指向应用程序进程打开的此设备文件的指针;
  3. 作为布尔值处理的整型,若为 true 就启用异步通知。

返回值

可返回 fasyn_help() 函数,此函数正常运行后返回0。

规范化行为指导

  1. 必须事先设置好此驱动程序的私有数据结构体,其中设置 fasync_struct* 类型的成员,然后在驱动程序的文件打开函数中,初始化这个结构体(建议在动态内存初始化),并将其置于 file 中的 private_data 指针;

  2. driver_fasync() 函数内,从 file 中获取 private_data 指向的结构体,将其中的 fasync_struct* 类型成员指针传到 poll_wait() 的第4个参数,并可将其作为返回值;

    (return) fasync_helper(fd, file_ptr, on, &file_ptr->private_data->fas_ptr)
  • 后续若设备状态变化,要通知应用程序:

    if (file_ptr->private_data->fas_ptr) {
        kill_fasync(&file_ptr->private_data->fas_ptr, SIGIO, POLL_IN);
    }
  • 在关闭文件时清除异步IO注册,调用驱动自己创建的 driver_fasync()

    driver_fasync(-1, file_ptr, 0);

在应用程序侧使用

4.5 事件处理等待队列

需要导入 linux/wait.h, linux/sched.h

要实现等待一些事件发生后再向下运行,需要使用 Linux 内核提供的等待队列。

通过以下任何方式初始化等待队列

// 静态初始化
DECLARE_WAIT_QUEUE_HEAD(test_wait_queue);
// 动态初始化
wait_queue_head_t test_wait_queue;
init_waitqueue_head(&test_wait_queue);

等待事件发生。实际使用时应接受函数的返回值以判断事件是否发生

// 等待形式参数2给出的条件成立
wait_event(test_wait_queue, CONDITION);
// 有限时间的等待
wait_event_timeout(test_wait_queue, CONDITION, TIME);
// 可被中止的等待
wait_event_interruptible(test_wait_queue, CONDITION);
// 有限时间且可被中止的等待
wait_event_timeout_interruptible(test_wait_queue, CONDITION, TIME);
  • CONDITION 中的表达式结果应为布尔(或整型)类型;
  • TIME 接受一个以 Linux 内核 jiffies 为单位的正整型,可使用 Macro HZ 乘等待秒数构成,或使用 msecs_to_jiffies(millisecond)(使用它们需要导入 linux/jiffies.h);
  • [限时型] 若返回正数,表示等待的事件发生,返回剩余时间;若返回0,表示等待已超时;
  • [可中止型] 若返回0,表示等待的事件发生;若返回 -ERESTARTSYS,表示等待期间被信号中止;
  • [限时+可中止型] 若返回正数,表示等待的事件发生,返回剩余时间;若返回0,表示等待已超时;若返回 -ERESTARTSYS,表示等待期间被信号中止。

当其他程序段修改了条件中的变量时,应手动唤醒;若调用唤醒函数后条件仍不满足,继续等待

// 唤醒队头的一个进程
wake_up(&test_wait_queue);
// 唤醒队列全部进程
wake_up_all(&test_wait_queue);
// 仅唤醒可中断进程
wake_up_interruptible(&test_wait_queue);

5 输入与输出

5.1 输入与输出模型

等待型

请求所需的设备时,若设备此刻不可用或正在准备数据,那么进入等待;

非等待型

请求所需的设备时,若设备此刻不可用或正在准备数据,那么直接返回设备忙错误码;

多路复用型

通过 select(), poll(), epoll() 同时监听多个文件描述符,有文件描述符可用后告知进程,进程进行操作;

信号驱动型

用信号 SIGIO() 通知线程进行后续处理。

异步型

使用 libaioio_uring 库,在请求IO后,内核处理数据(如果读设备,复制得到的数据到用户空间),然后发送 SIGIO 信号并调起程序指定的回调函数。

Ext. 驱动程序基本框架

Info

此框架仅适用于字符设备驱动程序。

// Linux driver must import these 2 headers
#include "linux/module.h"
#include "linux/init.h"
// Required when you copy data to or from user region
// copy_to_user()  copy_from_user()
#include "linux/uaccess.h"
// Let driver have ability to create device file automatically
#include "linux/cdev.h"
// If you want to use strlen(), then following header is required
#include "linux/string.h"
 
/* ----------------------------------------------------------- *
 * 
 *   Macro definitions / Global variables
 * 
 * ----------------------------------------------------------- */
 
#define BUFFER_MAXLEN 128
 
#define DRIVER_NAME "test_driver"
#define DEVICE_NAME "test_chardev"
 
static struct class* driver_class;
static struct device* driver_device;
static struct cdev var_cdev;
 
static dev_t device_id;
static char buffer[BUFFER_MAXLEN] = "Forza Horizon";
_Bool is_buffer_busy = false;
_Bool is_data_arrived = false;
 
/* ----------------------------------------------------------- *
 *
 *   Driver functions
 *
 * ----------------------------------------------------------- */
 
int char_dev_open(struct inode* f_node, struct file* file_ptr) {
    printk(KERN_INFO "[INFO] '%s()' called\n", __func__);
    return 0;
}
 
int char_dev_close(struct inode* f_node, struct file* file_ptr) {
    printk(KERN_INFO "[INFO] '%s()' called\n", __func__);
    return 0;
}
 
/* Variable 'offset' is ignored in current implements, function will always read from the beginning
 * '\0' will be read out of buffer
 */
ssize_t char_dev_read(struct file* file_ptr, char __user* usr_buf, size_t size, loff_t* offset) {
    unsigned int expected_read_length;
    int status;
 
    printk(KERN_INFO "[INFO] '%s()' called\n", __func__);
    if (!(file_ptr->f_mode & FMODE_READ)) {
        printk(KERN_ERR "[ERROR] Cannot process request of read device: READ bit is reset\n");
        return -EBADF;
    }
    
    if (strlen(buffer) + 1 > size)
        expected_read_length = size;
    else
        expected_read_length = strlen(buffer) + 1;
    
    if (file_ptr->f_flags & O_NONBLOCK) {
        if (is_data_arrived == false) {
            printk(KERN_WARNING "[WARNING] Device is not ready but user set NONBLOCK, return EAGAIN...\n");
            is_data_arrived = true;
            return -EAGAIN;
        }
    }
    else {
        printk(KERN_NOTICE "[NOTICE] Assume that the request has sent to device...\n");
    }
 
    printk(KERN_NOTICE "[NOTICE] Device responded, receiving...\n");
    status = copy_to_user(usr_buf, buffer, expected_read_length);
    if (status > 0) {
        return -EFAULT;
    }
    
    return expected_read_length;
}
 
ssize_t char_dev_write(struct file* file_ptr, const char __user* usr_buf, size_t size, loff_t* offset) {
    printk(KERN_INFO "[INFO] '%s()' called\n", __func__);
    return 0;
}
 
 
struct file_operations file_ops = {
    .owner = THIS_MODULE,
    .open = char_dev_open,
    .release = char_dev_close,
    .read = char_dev_read,
    .write = char_dev_write
};
 
/* ----------------------------------------------------------- *
 *
 *   Initialization / Removal functions
 *
 * ----------------------------------------------------------- */
 
static int __init on_enabled(void) {
    int status;
    printk(KERN_INFO "[INFO] Enabling %s\n", DRIVER_NAME);
    printk(KERN_INFO "[INFO] Setting device is %s\n", DEVICE_NAME);
    
    // Get device id
    status = alloc_chrdev_region(&device_id, 0, 1, DEVICE_NAME);
    if (status == -1) {
        printk(KERN_ERR "[ERROR] Cannot allocate device id for '%s': %d\n", DEVICE_NAME, status);
        return 1;
    }
 
    // Set driver class for automatically create device file
    driver_class = class_create(THIS_MODULE, "test_driver_class");
    if (IS_ERR(driver_class))
        return PTR_ERR(driver_class);
 
    // Create device file
    driver_device = device_create(driver_class, NULL, device_id, NULL, DEVICE_NAME);
    if (IS_ERR(driver_device)) {
        class_destroy(driver_class);
        unregister_chrdev_region(device_id, 1);
        return PTR_ERR(driver_device);
    }
 
    cdev_init(&var_cdev, &file_ops);
    status = cdev_add(&var_cdev, device_id, 1);
        if (status == -1) {
        printk(KERN_ERR "[ERROR] Cannot set cdev id\n");
        unregister_chrdev_region(device_id, 1);
        return 1;
    }
 
 
    printk(KERN_NOTICE "[NOTICE] %s is now available\n", DRIVER_NAME);
    printk(KERN_NOTICE "[NOTICE] Got device id %u:%u for device '%s'\n", MAJOR(device_id), MINOR(device_id), DEVICE_NAME);
 
    printk(KERN_NOTICE "[NOTICE] Created device file on /dev/%s\n", DEVICE_NAME);
 
    return 0;
}
 
static void __exit on_disabled(void) {
    printk(KERN_INFO "[INFO] Disabling %s\n", DRIVER_NAME);
    printk(KERN_INFO "[INFO] Removing device file on /dev/%s\n", DRIVER_NAME);
 
    device_destroy(driver_class, device_id);
    class_destroy(driver_class);
    unregister_chrdev_region(device_id, 1);
 
    printk(KERN_NOTICE "[NOTICE] %s has been uninstalled\n", DRIVER_NAME);
}
 
module_init(on_enabled);
module_exit(on_disabled);
 
/* ----------------------------------------------------------- *
 *
 *   Driver module properties / metadata
 *
 * ----------------------------------------------------------- */
 
MODULE_LICENSE("GPL");
MODULE_AUTHOR("yukaling");
MODULE_VERSION("v0.1");