Linux下的驱动开发一

设备驱动

  1. 设备驱动程序(Device Driver)是操作系统中的一种软件组件,负责管理和控制计算机硬件设备的工作。驱动程序通过提供 操作系统和硬件设备之间的接口 ,使得操作系统和应用程序能够与硬件设备进行交互,而无需了解硬件的具体细节。
  2. 主要功能
    • 硬件抽象:设备驱动程序屏蔽了硬件的复杂性,提供了统一的接口。
    • 设备控制:设备驱动程序负责管理硬件设备的初始化、配置、运行和关闭。
    • 中断处理:很多硬件设备通过中断与CPU通信。设备驱动程序要能够处理 这些中断信号,并在需要的时候通知操作系统进行相应的处理。
    • 数据传输:设备驱动程序通常负责在设备和主存储器之间传输数据。
    • 设备管理:设备驱动程序还负责管理设备资源的分配和访问,确保多个进程或线程能够安全地访问同一个硬件设备。
  3. 分类
    • 字符设备驱动:处理按字符流进行输入输出的设备,例如串口、键盘等。这类设备允许按字符读取或写入数据。
    • 块设备驱动:处理按数据块读写的设备,例如硬盘、固态硬盘等。这类设备允许随机访问数据。
    • 网络设备驱动:处理网络接口设备的驱动程序,负责在操作系统与网络硬件之间传输数据。
  4. Linux中的设备驱动
  • 在Linux系统中,设备驱动程序一般以内核模块(Kernel Module)的形式存在,可以动态地加载和卸载。
  • 设备驱动开发者需要了解Linux内核的编程接口(API)以及设备驱动的体系结构。
  1. 驱动开发流程步骤
    1. 注册设备:通过适当的API 将设备驱动程序注册到内核中,以便内核能够识别并管理设备。
    2. 实现文件操作接口:Linux中的设备通常以文件的形式呈现,设备驱动需要实现诸如open()、read()、write()等文件操作接口。
    3. 处理中断:如果设备支持中断,驱动程序需要注册中断处理程序,处理硬件中断请求。
    4. 内存映射:某些设备可能需要将硬件寄存器或内存区域映射到用户空间,以提高数据传输效率。

一、内核模块

  1. 可以动态加载和卸载的代码段,用来扩展或修改Linux内核的功能,而无需重启系统或重新编译内核。
  2. 内核模块可以在系统运行时通过insmod命令加载到内核中,或者通过rmmod命令卸载,而无需重启系统。
  3. 通过内核模块机制,内核的功能可以按需扩展。
  4. 内核模块直接运行在内核空间,与内核共享相同的地址空间,因此模块能够直接访问内核的功能和数据结构。
  5. 一个简单的内核模块例子
cpp 复制代码
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>

MODULE_LICENSE("GPL");               // 指定GPL许可证
MODULE_AUTHOR("Author Name");        // 作者信息
MODULE_DESCRIPTION("A Simple Kernel Module");  // 模块描述

// 初始化函数
static int __init my_module_init(void)
{
    printk(KERN_INFO "Hello, Kernel! My module has been loaded.\n");
    return 0;  // 返回0表示加载成功
}

// 退出函数
static void __exit my_module_exit(void)
{
    printk(KERN_INFO "Goodbye, Kernel! My module has been unloaded.\n");
}

// 指定初始化和退出函数
module_init(my_module_init);
module_exit(my_module_exit);

二、内核模块必须包含的部分

  1. 模块初始化函数(module_init):这是内核加载模块时调用的函数,用来初始化模块,注册设备驱动或其他功能。通常通过module_init()宏指定初始化函数。

  2. 模块退出函数(module_exit):这是在卸载模块时调用的函数,用于清理资源、注销设备驱动等。通过module_exit()宏指定退出函数。

  3. 模块许可证声明(MODULE_LICENSE):指定模块的许可证类型,通常为"GPL"(GNU General Public License),表明模块可以与内核兼容,否则内核可能拒绝加载非GPL的模块。

三、常用的模块指令

  1. lsmod :列出已加载的模块
    lsmod 用于显示当前内核中加载的模块列表。它读取 /proc/modules 文件,并以更友好的格式显示已加载的模块。
  2. modinfo - 显示模块的详细信息
    modinfo 用于显示已加载模块或模块文件的详细信息,例如作者、许可证、依赖项等。你可以使用它来查看内核模块的元数据。
    m o d i n f o 模块名 modinfo 模块名 modinfo模块名
  3. insmod - 手动加载模块
    insmod 是一个用于手动加载模块到内核的命令。它需要指定模块的完整路径(通常是.ko文件)。不指定的话,默认为当前目录
  4. rmmod - 卸载模块
    rmmod 用于卸载已经加载的模块。它会移除指定的模块,并解除与之相关的依赖关系。如果模块正在被其他模块使用或正在使用的资源没有被释放,它会阻止卸载。不用加ko
  5. dmesg - 查看内核日志
    dmesg 用于查看内核日志输出。加载或卸载内核模块时,内核日志会记录相关信息,比如模块加载成功与否、是否存在错误等。
  • 如果模块加载时有任何 printk() 输出,它应该出现在 dmesg 中。你可以使用 grep 来过滤特定的输出,
cpp 复制代码
dmesg | grep "Device registered"
找到所有包含 Device registered 的日志条目,这些通常是由 printk 函数输出的设备注册信息(假设驱动程序中有 printk("Device registered...") 这样的语句)。
cpp 复制代码
dmesg | grep my_module//查询特定模块的日志信息

一、字符设备驱动

  1. 注册字符设备
  • 在字符设备驱动中,首先要将设备注册到系统中。Linux内核通过register_chrdev ()或**alloc_chrdev_region()**函数分配设备号,并注册字符设备。
    • register_chrdev():用于直接注册字符设备。
    • alloc_chrdev_region():用于动态分配主设备号。
c 复制代码
int alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, const char *name);
参数:
	dev_t *dev:指向保存设备号的变量的指针。该函数成功执行后,会通过这个指针将分配到的主设备号和次设备号返回给调用者。包含了主设备号和次设备号。在设备注册时,系统通过 dev_t 识别不同的设备。
	unsigned int firstminor:表示要分配的第一个次设备号(minor number)。通常设置为 0,表示从第一个次设备号开始。
	unsigned int count:表示需要分配的连续次设备号的数量。对于一个简单的字符设备,通常设置为 1。
如果你希望一个驱动程序支持多个次设备,可以设置 count 为更大的值。
	const char *name:设备的名称,它通常是一个用于标识设备的字符串。例如,你可以将它设置为你的驱动程序名称或设备名称。
	
返回值:
	成功时返回 0,表示设备号分配成功。
	失败时返回负数的错误代码(如 -ENOMEM,表示内存不足)。	
  • 使用范例
cpp 复制代码
static int major;
static int minor;
 dev_t dev;
    if (alloc_chrdev_region(&dev, 0, 1, "my_device") < 0) {
        printk(KERN_ERR "Failed to allocate device number.\n");
        return -1;
    }
major = MAJOR(dev);//获取主设备号 
minor = MINOR(dev);//获取从设备号
  1. 定义file_operation结构
  • file_operations结构体是字符设备驱动的核心,定义了设备的各种 操作接口。需要实现其中几个函数来支持常见的文件操作。
  • 常见的几个
cpp 复制代码
static struct file_operations fops = {
    .open = device_open,     // 打开设备
    .release = device_release,  // 关闭设备
    .read = device_read,     // 读取设备数据
    .write = device_write,   // 写入设备数据
};
  1. 实现设备操作函数
cpp 复制代码
static int device_open(struct inode *inode, struct file *file) {
    printk(KERN_INFO "Device opened.\n");
    return 0;
}
static int device_release(struct inode *inode, struct file *file) {
    printk(KERN_INFO "Device closed.\n");
    return 0;
等等
  1. 注册和注销字符设备
  • 注册字符设备并将设备号绑定到file_operations结构体。通常在模块加载时完成这一过程
  • 声明字符设备结构体,用于表示并管理驱动程序中的一个字符设备。之后可以通过 cdev_init() 初始化它,通过 cdev_add() 将它注册到内核,使其作为字符设备提供给用户使用。
  • class_create
    • 用于在 Linux 内核中创建一个设备类的函数。
    • 设备类是用于分组和管理设备的抽象概念,它使得多个相似类型的设备可以被组织和识别,比如分为字符设备、块设备等。
    • 通常,在字符设备驱动程序开发中,创建设备类是用于创建 /sys/class 下的设备节点目录,以便用户空间可以通过 /dev 目录访问设备文件。
cpp 复制代码
struct class *class_create(struct module *owner, const char *name);
参数:
	owner:指向拥有该类的模块。通常设置为 THIS_MODULE
	name:类的名称,用于识别和创建类目录。它会出现在 /sys/class/ 目录下。
返回值:
	class_create 成功时会返回一个指向 struct class 的指针,表示创建的类。如果失败,返回 ERR_PTR(-ENOMEM) 或者其他负值错误指针。
  • device_create 是用于在 Linux 内核中创建设备节点的函数,常与 class_create 搭配使用。它会在 /sys/class/ 下创建设备相关的目录,并且通常会在 /dev 下创建设备文件,以供用户空间使用。用户可以通过该设备文件与内核中的字符设备进行交互,比如使用 open、read、write 系统调用。
cpp 复制代码
struct device *device_create(struct class *cls, struct device *parent, dev_t devt, void *drvdata, const char *fmt, ...);
参数:
	cls: 指向通过 class_create 创建的设备类(struct class)。它指定了该设备属于哪个设备类,即设备会被归属到 /sys/class/cls_name/ 目录下。
	parent: 设备的父设备。如果当前设备没有父设备(通常在多数情况下是 NULL),传 NULL 即可。
	devt: 设备号,类型为 dev_t。这是通过 MKDEV(major, minor) 创建的一个 32 位的设备号,包含主设备号和次设备号,标识设备在系统中的唯一性。
	drvdata: 驱动私有数据的指针。这个指针通常会指向设备的私有数据,设备驱动可以通过该指针存储和管理与设备相关的数据。
	fmt: 设备名称格式字符串。设备文件的名称可以使用格式化字符串,类似于 printf。例如,可以为设备命名为 "my_device%d",其中 %d 表示动态分配的设备号或某些其他标识符。	
cpp 复制代码
static struct cdev my_cdev;
  • 驱动初始化函数
    • 主要任务
      • 分配和注册设备号 register_chrdev_region()
      • 初始化 cdev 结构体 cdev_init(&my_cdev, &fops);
      • 将 cdev 添加到系统中 cdev_add(&my_cdev, dev, 1);
      • 创建设备类,并在设备类下创建设备文件节点
cpp 复制代码
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>  // 设备类和设备节点所需的头文件

static int major;            // 存储主设备号
static dev_t dev;            // 存储设备号(包含主设备号和次设备号)
static struct cdev my_cdev;  // 定义字符设备结构体
static struct class *my_class = NULL;  // 设备类,用于设备节点的创建
static struct device *my_device = NULL; // 设备节点
extern struct file_operations fops;  // 假设文件操作函数集合在其他地方定义

// 模块初始化函数
static int __init my_driver_init(void) {
    int ret;

    // 动态分配设备号
    // dev:指向设备号变量的指针,用于存储分配到的设备号
    // 0:从次设备号0开始分配
    // 1:分配一个次设备号
    // "my_device":设备名称,用于区分设备
    ret = alloc_chrdev_region(&dev, 0, 1, "my_device");
    if (ret < 0) {
        // 如果分配失败,打印错误信息并返回错误代码
        printk(KERN_ERR "Failed to allocate device number.\n");
        return ret;  // 返回错误代码,模块加载失败
    }

    // 获取分配到的主设备号
    major = MAJOR(dev);
    // 打印信息,通知设备已注册并显示主设备号
    printk(KERN_INFO "Device registered with major number %d\n", major);

    // 初始化 cdev 结构体,并将其与文件操作函数 fops 关联
    // cdev_init 用于设置 cdev 结构体的操作函数,cdev 是字符设备的核心结构体
    cdev_init(&my_cdev, &fops);

    // 将字符设备添加到内核
    // dev:设备号(包含主设备号和次设备号)
    // 1:表示注册的次设备数量
    ret = cdev_add(&my_cdev, dev, 1);
    if (ret < 0) {
        // 如果添加字符设备失败,注销设备号,并打印错误信息
        printk(KERN_ERR "Failed to add cdev.\n");
        unregister_chrdev_region(dev, 1);  // 释放分配的设备号
        return ret;  // 返回错误代码,模块加载失败
    }

    // 创建设备类,"my_device_class" 是类的名称
    // class_create 用于创建设备类,它会在 /sys/class/ 下创建相应的目录
    my_class = class_create(THIS_MODULE, "my_device_class");
    if (IS_ERR(my_class)) {
        // 如果设备类创建失败,清理已添加的字符设备,并注销设备号
        printk(KERN_ERR "Failed to create class.\n");
        cdev_del(&my_cdev);
        unregister_chrdev_region(dev, 1);
        return PTR_ERR(my_class);
    }

    // 在设备类下创建设备节点
    // device_create 会在 /dev 下创建一个设备文件
    // dev 是设备号,"my_device" 是设备文件名
    my_device = device_create(my_class, NULL, dev, NULL, "my_device");
    if (IS_ERR(my_device)) {
        // 如果设备节点创建失败,清理设备类、字符设备,并注销设备号
        printk(KERN_ERR "Failed to create device.\n");
        class_destroy(my_class);
        cdev_del(&my_cdev);
        unregister_chrdev_region(dev, 1);
        return PTR_ERR(my_device);
    }

    // 模块加载成功,返回0
    printk(KERN_INFO "Device node created successfully\n");
    return 0;
}

// 模块卸载函数
static void __exit my_driver_exit(void) {
    // 销毁设备节点
    device_destroy(my_class, dev);

    // 销毁设备类
    class_destroy(my_class);

    // 从内核中删除字符设备
    cdev_del(&my_cdev);

    // 注销设备号,释放分配的主设备号和次设备号
    unregister_chrdev_region(dev, 1);

    // 打印信息,通知设备已被注销
    printk(KERN_INFO "Device unregistered.\n");
}

// 将初始化和退出函数分别设置为模块加载和卸载时的回调函数
module_init(my_driver_init);  // 注册模块初始化函数
module_exit(my_driver_exit);  // 注册模块卸载函数

// 申明许可证,防止内核拒绝加载模块(GPL是开源驱动的常用许可证)
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Author");
MODULE_DESCRIPTION("A simple Linux char driver with device node creation");

五、字符设备文件与普通文件的区别

属性 字符设备文件 普通文件
定义 用于与硬件设备交互的特殊文件 用于存储用户数据的常规文件
位置 通常位于 /dev/ 目录下 位于文件系统的任意位置(如 /home/user/
交互方式 通过字符流与设备驱动交互 通过文件系统读写数据,支持随机访问
文件类型标记 c (字符设备) - (普通文件)
文件内容 不存储数据,作为与设备交互的接口 存储持久化数据,如文本、二进制等
主/次设备号 通过主设备号和次设备号标识 无主次设备号概念,使用文件路径访问
读写操作 顺序访问,通过驱动传递数据到硬件设备 支持顺序或随机访问,读写磁盘上的数据

六、字符设备驱动的结构

七、内核空间与用户空间的相互访问

  1. read

    1. file_operations 结构体中的 read 函数用于处理从设备读取数据到用户空间的请求。该函数的实现需要将设备数据传递给用户空间的进程,这通常通过内核空间和用户空间之间的数据拷贝来实现。
    2. 函数
    cpp 复制代码
    ssize_t (*read) (struct file *filp, char __user *buf, size_t count, loff_t *f_pos);
    
    filp:指向打开文件的文件结构体指针。通过它可以访问与该文件(或设备)相关的特定信息。
    buf:这是用户空间中的缓冲区指针,数据需要通过它传递给用户空间。
    count:需要传输的数据字节数,即用户想读取的最大数据量。
    f_pos:文件位置指针,指示当前文件读写的偏移量。
    1. copy_to_user数据从内核空间传输到用户空间
    cpp 复制代码
    long copy_to_user(void __user *to, const void *from, unsigned long n);
    参数:
    to 是用户空间的目标缓冲区地址
    from是内核空间的源数据地址
    n是要拷贝的字节数	
  2. write

    1. file_operations 中的 write 函数,从用户空间向内核空间传递数据。在 Linux 驱动开发中,write 函数用于将用户空间的数据写入到设备,或更新内核空间中的数据。
    2. 函数
cpp 复制代码
ssize_t (*write) (struct file *filp, const char __user *buf, size_t count, loff_t *f_pos);
参数:
	filp:指向文件结构体的指针,代表打开的文件(或设备)。
	buf:这是来自用户空间的缓冲区指针,内核需要从该缓冲区读取数据。
	count:要写入的字节数,即用户空间进程传递给内核的最大数据量。
	f_pos:文件位置指针,指示文件当前写操作的位置偏移量。
  1. copy_from_user从用户空间拷贝数据到内核空间的关键函数
    • 直接访问用户空间的地址是危险的,因此必须通过此函数进行拷贝。
    • 可以将数据写入硬件设备或进行其他操作。
    • write 函数返回实际写入的字节数,表示驱动程序从用户空间成功接收的数据量。如果传输失败(例如 copy_from_user 失败),则返回负数以指示错误。
  2. 举例如下
cpp 复制代码
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/uaccess.h>  // for copy_to_user, copy_from_user

#define DEVICE_NAME "my_char_device"
#define BUFFER_SIZE 1024

static int major;
static char kernel_buffer[BUFFER_SIZE];  // 用于存储用户写入的数据
static size_t buffer_len = 0;  // 追踪缓冲区中的数据长度

// 函数声明
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 *);

// file_operations 结构体,定义了设备操作
static struct file_operations fops = {
    .read = device_read,
    .write = device_write,
    .open = device_open,
    .release = device_release,
};

// 打开设备时调用的函数
static int device_open(struct inode *inodep, struct file *filep) {
    printk(KERN_INFO "Device opened\n");
    return 0;
}

// 关闭设备时调用的函数
static int device_release(struct inode *inodep, struct file *filep) {
    printk(KERN_INFO "Device closed\n");
    return 0;
}

// 读取设备时调用的函数
static ssize_t device_read(struct file *filep, char __user *user_buffer, size_t len, loff_t *offset) {
    ssize_t bytes_read;

    // 如果文件位置偏移量大于数据长度,表示读取完毕
    if (*offset >= buffer_len)
        return 0;

    // 限制读取的字节数,避免超出缓冲区
    if (len > buffer_len - *offset)
        len = buffer_len - *offset;

    // 将数据从内核缓冲区拷贝到用户空间
    if (copy_to_user(user_buffer, kernel_buffer + *offset, len)) {
        return -EFAULT;
    }

    // 更新文件偏移量
    *offset += len;

    // 返回实际读取的字节数
    bytes_read = len;
    printk(KERN_INFO "Sent %zu bytes to the user\n", bytes_read);

    return bytes_read;
}

// 写入设备时调用的函数
static ssize_t device_write(struct file *filep, const char __user *user_buffer, size_t len, loff_t *offset) {
    // 如果写入的数据大于缓冲区容量,进行裁剪
    if (len > BUFFER_SIZE - 1)
        len = BUFFER_SIZE - 1;

    // 从用户空间拷贝数据到内核缓冲区
    if (copy_from_user(kernel_buffer, user_buffer, len)) {
        return -EFAULT;
    }

    // 确保缓冲区内容是以 NULL 结尾的字符串
    kernel_buffer[len] = '\0';
    buffer_len = len;  // 更新缓冲区中的数据长度

    printk(KERN_INFO "Received %zu bytes from the user\n", len);
    return len;
}

// 模块加载时调用的初始化函数
static int __init my_char_device_init(void) {
    major = register_chrdev(0, DEVICE_NAME, &fops);  // 动态分配主设备号

    if (major < 0) {
        printk(KERN_ALERT "Failed to register char device\n");
        return major;
    }

    printk(KERN_INFO "Registered char device with major number %d\n", major);
    return 0;
}

// 模块卸载时调用的清理函数
static void __exit my_char_device_exit(void) {
    unregister_chrdev(major, DEVICE_NAME);  // 注销设备
    printk(KERN_INFO "Unregistered char device\n");
}

module_init(my_char_device_init);
module_exit(my_char_device_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Example Author");
MODULE_DESCRIPTION("A simple character device driver");

八、模块的卸载mod_exit(void)

  1. 模块卸载时要逆序释放资源
    • 在模块加载过程中,资源是按照一定的顺序分配的。在卸载模块时,必须按照相反的顺序释放这些资源。
    • 这是为了避免在使用已被释放的资源时出现问题,例如,在设备节点被销毁后,类或设备号还未释放,可能会导致系统崩溃。
  2. 正确顺序
c 复制代码
删除设备节点
删除类
删除字符设备对象
释放设备号
  1. 举例说明
cpp 复制代码
// 模块卸载函数
static void __exit my_char_device_exit(void) {
    // 1. 删除设备节点
    device_destroy(cls, MKDEV(major, 0));

    // 2. 销毁设备类
    class_destroy(cls);

    // 3. 删除字符设备对象
    cdev_del(&cdev_obj);

    // 4. 释放设备号
    unregister_chrdev_region(MKDEV(major, 0), 1);

    printk(KERN_INFO "Device unregistered and resources released\n");
}
module_exit(my_char_device_exit);

九、ioctl

  1. ioctl(Input/Output Control)是一种强大的机制,允许用户空间程序与设备驱动程序进行复杂的交互,不仅限于标准的读写操作。
  2. 通过 ioctl,可以执行设备特定的操作,如配置设备参数、获取设备状态等。
  3. 举例说明
cpp 复制代码
/* my_char_device.c */
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/uaccess.h>  // for copy_to_user, copy_from_user
#include "tv_ioctl.h"       // 包含命令码和结构体定义

#define DEVICE_NAME "my_char_device"
#define CLASS_NAME "my_char_class"

static int major;
static struct class *cls;
static struct cdev cdev_obj;

// 设备数据
static struct tv_stat current_stat = {
    .chnl = 1,
    .light = 50,
    .vol = 30
};

// 打开设备
static int my_device_open(struct inode *inode, struct file *file) {
    printk(KERN_INFO "Device opened\n");
    return 0;
}

// 释放设备
static int my_device_release(struct inode *inode, struct file *file) {
    printk(KERN_INFO "Device closed\n");
    return 0;
}

// 读取设备(示例)
static ssize_t my_device_read(struct file *file, char __user *buf, size_t count, loff_t *ppos) {
    const char *msg = "Hello from kernel space!\n";
    size_t len = strlen(msg);

    if (*ppos >= len)
        return 0; // EOF

    if (count > len - *ppos)
        count = len - *ppos;

    if (copy_to_user(buf, msg + *ppos, count))
        return -EFAULT;

    *ppos += count;
    printk(KERN_INFO "Sent %zu bytes to the user\n", count);
    return count;
}

// 写入设备(示例)
static ssize_t my_device_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos) {
    char kernel_buffer[100];

    if (count > sizeof(kernel_buffer) - 1)
        count = sizeof(kernel_buffer) - 1;

    if (copy_from_user(kernel_buffer, buf, count))
        return -EFAULT;

    kernel_buffer[count] = '\0';
    printk(KERN_INFO "Received %zu bytes from the user: %s\n", count, kernel_buffer);
    return count;
}

// ioctl 函数实现
static long chdev_ioctl(struct file *file, unsigned int cmd, unsigned long addr) {
    int ret = 0;
    struct tv_menu menu;
    struct tv_sw sw;
    struct tv_chnl chnl;
    struct tv_stat stat;

    switch(cmd){
        case TV_CMD_MENU:
            if (copy_from_user(&menu, (void __user *)addr, sizeof(menu))) {
                return -EFAULT;
            }
            printk(KERN_INFO "%s-%d ioctl MENU: color=%d, dbd=%d, light=%d\n",
                   __func__, __LINE__, menu.color, menu.dbd, menu.light);
            // 在此处处理 menu 数据,例如更新设备配置
            break;

        case TV_CMD_SW:
            if (copy_from_user(&sw, (void __user *)addr, sizeof(sw))) {
                return -EFAULT;
            }
            printk(KERN_INFO "%s-%d ioctl SW: sw=%d\n",
                   __func__, __LINE__, sw.sw);
            // 在此处处理 sw 数据,例如切换设备状态
            break;

        case TV_CMD_CHNL:
            if (copy_from_user(&chnl, (void __user *)addr, sizeof(chnl))) {
                return -EFAULT;
            }
            printk(KERN_INFO "%s-%d ioctl CHNL: channel=%d\n",
                   __func__, __LINE__, chnl.channel);
            // 在此处处理 chnl 数据,例如切换频道
            break;

        case TV_CMD_STAT:
            // 将当前状态复制到用户空间
            stat = current_stat;  // 假设 current_stat 已在设备中维护
            if (copy_to_user((void __user *)addr, &stat, sizeof(stat))) {
                return -EFAULT;
            }
            printk(KERN_INFO "%s-%d ioctl STAT: chnl=%d, light=%d, vol=%d\n",
                   __func__, __LINE__, stat.chnl, stat.light, stat.vol);
            break;

        default:
            printk(KERN_WARNING "Unknown ioctl command: %u\n", cmd);
            return -ENOTTY;  // 命令无效
    }

    return ret;
}

// file_operations 结构体
static struct file_operations fops = {
    .owner = THIS_MODULE,
    .open = my_device_open,
    .release = my_device_release,
    .read = my_device_read,
    .write = my_device_write,
    .unlocked_ioctl = chdev_ioctl,  // 注册 ioctl 函数
};

// 模块加载函数
static int __init my_char_device_init(void) {
    dev_t dev;
    int ret;

    // 1. 分配设备号
    ret = alloc_chrdev_region(&dev, 0, 1, DEVICE_NAME);
    if (ret < 0) {
        printk(KERN_ALERT "Failed to allocate device number\n");
        return ret;
    }
    major = MAJOR(dev);
    printk(KERN_INFO "Allocated device number: %d\n", major);

    // 2. 初始化字符设备对象
    cdev_init(&cdev_obj, &fops);
    cdev_obj.owner = THIS_MODULE;

    // 3. 添加字符设备对象到系统中
    ret = cdev_add(&cdev_obj, dev, 1);
    if (ret < 0) {
        unregister_chrdev_region(dev, 1);
        printk(KERN_ALERT "Failed to add cdev\n");
        return ret;
    }

    // 4. 创建设备类
    cls = class_create(THIS_MODULE, CLASS_NAME);
    if (IS_ERR(cls)) {
        cdev_del(&cdev_obj);
        unregister_chrdev_region(dev, 1);
        printk(KERN_ALERT "Failed to create class\n");
        return PTR_ERR(cls);
    }

    // 5. 创建设备节点
    if (device_create(cls, NULL, dev, NULL, DEVICE_NAME) == NULL) {
        class_destroy(cls);
        cdev_del(&cdev_obj);
        unregister_chrdev_region(dev, 1);
        printk(KERN_ALERT "Failed to create device\n");
        return -1;
    }

    printk(KERN_INFO "Device created successfully\n");
    return 0;
}

// 模块卸载函数
static void __exit my_char_device_exit(void) {
    dev_t dev = MKDEV(major, 0);

    // 1. 删除设备节点
    device_destroy(cls, dev);

    // 2. 销毁设备类
    class_destroy(cls);

    // 3. 删除字符设备对象
    cdev_del(&cdev_obj);

    // 4. 释放设备号
    unregister_chrdev_region(dev, 1);

    printk(KERN_INFO "Device unregistered and resources released\n");
}

module_init(my_char_device_init);
module_exit(my_char_device_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Example Author");
MODULE_DESCRIPTION("A simple character device driver with ioctl");

十、硬件访问

  1. 驱动使用的是虚拟地址,ARM中访问的是物理地址
  2. 寄存器-物理地址转换为 虚拟地址
    • 虚拟地址 = ioremap(物理地址, 空间大小);
    • iounmap: 取消映射
  3. 小例子
cpp 复制代码
#include <linux/io.h>

void __iomem *base_addr;
unsigned long phys_addr = 0xFE000000;  // 假设设备寄存器物理地址

// 在设备初始化时,映射物理地址到内核地址空间
base_addr = ioremap(phys_addr, 0x100);  // 映射 0x100 字节

// 读取设备寄存器(假设偏移 0x04 处的寄存器)
u32 value = readl(base_addr + 0x04);

// 写入设备寄存器
writel(value | 0x1, base_addr + 0x04);

// 在设备释放时,取消映射
iounmap(base_addr);
  1. 综合LED灯驱动开发
cpp 复制代码
//chdev_ioctl
long chdev_ioctl(struct file *file, unsigned int cmd, unsigned long addr)
{
    int ret;
    struct led_ctl ctl;

    switch (cmd) {
    case LED_CMD_CTL:
        // 从用户空间拷贝数据到内核空间
        ret = copy_from_user(&ctl, (void __user *)addr, sizeof(ctl));
        if (ret) {
            pr_err("Failed to copy data from user space\n");
            return -EFAULT;  // 返回错误码
        }

        // 调用硬件控制函数,控制 LED
        led2_ctl(ctl.sw);
        break;

    default:
        pr_err("Unsupported ioctl command: %u\n", cmd);
        return -EINVAL;  // 返回无效命令错误
    }

    return 0;  // 成功返回
}

//led_ctl
void led2_ctl(int sw)
{
#define GPX2CON 0x11000c40  
#define GPX2DAT 0x11000c44

    void __iomem *conf = ioremap(GPX2CON, 4);
    void __iomem *data = ioremap(GPX2DAT, 4);

    // 检查是否映射成功
    if (!conf || !data) {
        pr_err("Failed to ioremap registers\n");
        if (conf) iounmap(conf);
        if (data) iounmap(data);
        return;
    }

    long val = readl(conf);
    val &= ~(0xF << 28);  // 清除 GPX2_7 的配置位
    val |= (0x1 << 28);   // 设置 GPX2_7 为输出模式
    writel(val, conf);

    // 控制 LED 开关状态
    val = readl(data);
    if (sw) {
        val |= (1 << 7);  // 打开 LED(设置 GPX2_7 为高电平)
    } else {
        val &= ~(1 << 7); // 关闭 LED(设置 GPX2_7 为低电平)
    }
    writel(val, data);

    // 解除映射
    iounmap(conf);
    iounmap(data);
}
  1. 用户空间调用代码
cpp 复制代码
int main() {
    int fd;
    struct led_ctl ctl;

    // 1. 打开设备文件(假设设备节点为 /dev/my_led_device)
    fd = open("/dev/my_led_device", O_RDWR);
    if (fd < 0) {
        perror("Failed to open device");
        return -1;
    }

    // 2. 控制 LED 打开
    ctl.sw = 1;  // 设置为 1 表示打开 LED
    if (ioctl(fd, LED_CMD_CTL, &ctl) < 0) {
        perror("Failed to control LED (turn on)");
        close(fd);
        return -1;
    }
    printf("LED turned on\n");

    // 3. 控制 LED 关闭
    ctl.sw = 0;  // 设置为 0 表示关闭 LED
    if (ioctl(fd, LED_CMD_CTL, &ctl) < 0) {
        perror("Failed to control LED (turn off)");
        close(fd);
        return -1;
    }
    printf("LED turned off\n");

    // 4. 关闭设备文件
    close(fd);

    return 0;
}

十一、内核中的锁

  1. 自旋锁
  • 自旋锁是最常用的锁之一,适用于不允许 睡眠的上下文(如中断上下文)。当一个线程尝试获取自旋锁时,如果锁已经被其他线程持有,线程会在一个循环(自旋)中不断检查锁是否释放,而不会主动让出 CPU 时间片。
  • 自旋锁不会引发上下文切换。
  • 自旋锁不允许在睡眠上下文中使用,因为自旋锁期望锁能够很快释放。
cpp 复制代码
#include <linux/spinlock.h>

spinlock_t my_lock;

void my_function(void) {
    unsigned long flags;

    // 初始化自旋锁
    spin_lock_init(&my_lock);

    // 加锁(可被中断打断的代码)
    spin_lock_irqsave(&my_lock, flags);  // 保存中断状态并禁用中断

    // 临界区代码(操作共享资源)
    // ...

    // 解锁
    spin_unlock_irqrestore(&my_lock, flags);  // 恢复中断状态
}
  1. 互斥锁(Mutex)
  • 互斥锁是一种允许睡眠的锁。如果一个线程试图获取一个已经被其他线程持有的互斥锁,该线程会进入睡眠状态,直到锁可用。
  • 互斥锁适用于在进程上下文中保护共享资源,而不适用于中断上下文,因为它允许持有锁的线程阻塞。
cpp 复制代码
#include <linux/mutex.h>

struct mutex my_mutex;

void my_function(void) {
    // 初始化互斥锁
    mutex_init(&my_mutex);

    // 加锁
    mutex_lock(&my_mutex);

    // 临界区代码(操作共享资源)
    // ...

    // 解锁
    mutex_unlock(&my_mutex);
}
  1. 读写锁
  • 读写锁允许多个读者同时获取锁,但写者只能独占锁。
  • 适用于读多写少的场景,因为多个线程可以同时进行读取,而不必相互等待。
csharp 复制代码
#include <linux/rwlock.h>

rwlock_t my_rwlock;

void read_function(void) {
    unsigned long flags;

    // 初始化读写锁
    rwlock_init(&my_rwlock);

    // 读锁
    read_lock_irqsave(&my_rwlock, flags);

    // 临界区代码(读取共享资源)
    // ...

    // 解锁
    read_unlock_irqrestore(&my_rwlock, flags);
}

void write_function(void) {
    unsigned long flags;

    // 写锁
    write_lock_irqsave(&my_rwlock, flags);

    // 临界区代码(写入共享资源)
    // ...

    // 解锁
    write_unlock_irqrestore(&my_rwlock, flags);
}
  1. 原子变量
  • 操作是在 CPU 层面一次性完成的,保证了其操作是不可分割的,因此不会被中断。
  • 原子操作的性能通常比使用锁的同步机制更高,但它只适用于简单的整数操作,不适合复杂的临界区保护。
  • 使用示例
cpp 复制代码
#include <linux/atomic.h>

atomic_t counter;

void init_counter(void) {
    // 初始化原子变量
    atomic_set(&counter, 0);
}

void increment_counter(void) {
    // 增加原子变量值
    atomic_inc(&counter);
}

void decrement_counter(void) {
    // 减少原子变量值
    atomic_dec(&counter);
}

int get_counter_value(void) {
    // 获取原子变量的当前值
    return atomic_read(&counter);
}
  1. 它们之间的区别
锁类型 允许睡眠 适用场景 操作粒度 性能 特点
自旋锁 短时间的临界区 粗粒度 自旋等待,适合中断上下文,不适合长时间持有
互斥锁 需要睡眠的进程上下文 粗粒度 允许睡眠,不适合中断上下文,适合长时间持有
读写锁 读多写少的场景 粗粒度 允许多个读者同时获取锁,但写者独占
信号量 进程同步或资源访问 粗粒度 计数机制,支持多个进程并发访问,可阻塞进程
原子变量 简单的计数或位操作 细粒度 无需加锁,适用于简单的数值操作和比较交换操作
RCU (Read-Copy Update) 极端读多写少的场景 细粒度 非常高 高效的读操作,无需加锁,写操作需更新后同步
相关推荐
眠修10 分钟前
Kuberrnetes 服务发布
linux·运维·服务器
即将头秃的程序媛3 小时前
centos 7.9安装tomcat,并实现开机自启
linux·运维·centos
fangeqin3 小时前
ubuntu源码安装python3.13遇到Could not build the ssl module!解决方法
linux·python·ubuntu·openssl
爱奥尼欧4 小时前
【Linux 系统】基础IO——Linux中对文件的理解
linux·服务器·microsoft
超喜欢下雨天5 小时前
服务器安装 ros2时遇到底层库依赖冲突的问题
linux·运维·服务器·ros2
Natsume17105 小时前
嵌入式开发:GPIO、UART、SPI、I2C 驱动开发详解与实战案例
c语言·驱动开发·stm32·嵌入式硬件·mcu·架构·github
tan77º6 小时前
【Linux网络编程】网络基础
linux·服务器·网络
笑衬人心。6 小时前
Ubuntu 22.04 + MySQL 8 无密码登录问题与 root 密码重置指南
linux·mysql·ubuntu
chanalbert8 小时前
CentOS系统新手指导手册
linux·运维·centos
星宸追风8 小时前
Ubuntu更换Home目录所在硬盘的过程
linux·运维·ubuntu