首页 > 系统相关 >《Linux设备驱动程序》(第三版)第3章 字符设备驱动程序(续)

《Linux设备驱动程序》(第三版)第3章 字符设备驱动程序(续)

时间:2025-01-09 10:33:38浏览次数:3  
标签:驱动程序 simple void dev init exit Linux my 设备

3.1 并发和竞争条件

3.1.1 竞争条件的概念

竞争条件是指当多个执行单元(如进程、线程或中断处理程序)同时访问和修改共享资源时,由于执行顺序的不确定性而导致的不可预测的结果。例如,两个进程同时读取一个共享变量,然后各自对其进行修改并写回,最终的结果可能取决于哪个进程最后写回,这可能导致数据不一致或程序逻辑错误。

3.1.2 原子操作

原子操作是指那些在执行过程中不会被中断的操作,要么完全执行,要么完全不执行。在Linux内核中,提供了一些原子操作函数来处理简单的共享数据类型,如整数和位操作。

#include <linux/init.h>
#include <linux/module.h>
#include <linux/atomic.h>

// 定义一个原子变量
atomic_t atomic_var;

// 模块加载函数
static int __init atomic_demo_init(void) {
    // 初始化原子变量为0
    atomic_set(&atomic_var, 0);
    // 原子地增加变量的值
    atomic_inc(&atomic_var);
    // 获取原子变量的值
    int value = atomic_read(&atomic_var);
    printk(KERN_INFO "Atomic variable value: %d\n", value);
    return 0;
}

// 模块卸载函数
static void __exit atomic_demo_exit(void) {
    // 原子地减少变量的值
    atomic_dec(&atomic_var);
    int value = atomic_read(&atomic_var);
    printk(KERN_INFO "Final atomic variable value: %d\n", value);
}

module_init(atomic_demo_init);
module_exit(atomic_demo_exit);
MODULE_LICENSE("GPL");
  • atomic_t atomic_var;:定义一个原子变量 atomic_var
  • atomic_set(&atomic_var, 0);:初始化原子变量 atomic_var 的值为0。&atomic_var 是原子变量的地址,0是要设置的值。
  • atomic_inc(&atomic_var);:原子地增加 atomic_var 的值。
  • int value = atomic_read(&atomic_var);:读取原子变量 atomic_var 的值并赋值给 value
  • atomic_dec(&atomic_var);:原子地减少 atomic_var 的值。
3.1.3 自旋锁

自旋锁是一种用于保护共享资源的同步机制,当一个进程获取自旋锁时,如果锁已经被占用,它会在原地自旋(即不断尝试获取锁),而不是进入睡眠状态,直到锁可用。自旋锁适用于保护短时间内被占用的共享资源,因为自旋会消耗CPU时间,长时间自旋会浪费CPU资源。

#include <linux/init.h>
#include <linux/module.h>
#include <linux/spinlock.h>

// 定义自旋锁
spinlock_t my_spinlock;
// 定义一个共享变量
int shared_variable;

// 模块加载函数
static int __init spinlock_demo_init(void) {
    // 初始化自旋锁
    spin_lock_init(&my_spinlock);
    shared_variable = 0;

    // 尝试获取自旋锁
    spin_lock(&my_spinlock);
    // 访问和修改共享变量
    shared_variable++;
    // 释放自旋锁
    spin_unlock(&my_spinlock);

    printk(KERN_INFO "Shared variable value: %d\n", shared_variable);
    return 0;
}

// 模块卸载函数
static void __exit spinlock_demo_exit(void) {
    // 尝试获取自旋锁
    spin_lock(&my_spinlock);
    // 访问和修改共享变量
    shared_variable--;
    // 释放自旋锁
    spin_unlock(&my_spinlock);

    printk(KERN_INFO "Final shared variable value: %d\n", shared_variable);
    // 销毁自旋锁
    spin_lock_destroy(&my_spinlock);
}

module_init(spinlock_demo_init);
module_exit(spinlock_demo_exit);
MODULE_LICENSE("GPL");
  • spinlock_t my_spinlock;:定义一个自旋锁 my_spinlock
  • spin_lock_init(&my_spinlock);:初始化自旋锁 my_spinlock&my_spinlock 是自旋锁的地址。
  • spin_lock(&my_spinlock);:尝试获取自旋锁。如果锁已被占用,调用者会自旋等待。
  • spin_unlock(&my_spinlock);:释放自旋锁,允许其他进程获取。
  • spin_lock_destroy(&my_spinlock);:销毁自旋锁,通常在模块卸载时调用。
3.1.4 信号量

信号量是一种更通用的同步机制,它可以控制对共享资源的访问数量。信号量有一个计数器,当计数器大于0时,进程可以获取信号量(计数器减1),当计数器为0时,获取信号量的进程会进入睡眠状态,直到计数器大于0。

#include <linux/init.h>
#include <linux/module.h>
#include <linux/semaphore.h>

// 定义信号量
struct semaphore my_semaphore;
// 定义一个共享变量
int shared_variable;

// 模块加载函数
static int __init semaphore_demo_init(void) {
    // 初始化信号量,设置初始计数值为1
    sema_init(&my_semaphore, 1);
    shared_variable = 0;

    // 尝试获取信号量
    if (down_interruptible(&my_semaphore)) {
        printk(KERN_ERR "Failed to acquire semaphore\n");
        return -1;
    }
    // 访问和修改共享变量
    shared_variable++;
    // 释放信号量
    up(&my_semaphore);

    printk(KERN_INFO "Shared variable value: %d\n", shared_variable);
    return 0;
}

// 模块卸载函数
static void __exit semaphore_demo_exit(void) {
    // 尝试获取信号量
    if (down_interruptible(&my_semaphore)) {
        printk(KERN_ERR "Failed to acquire semaphore\n");
        return;
    }
    // 访问和修改共享变量
    shared_variable--;
    // 释放信号量
    up(&my_semaphore);

    printk(KERN_INFO "Final shared variable value: %d\n", shared_variable);
}

module_init(semaphore_demo_init);
module_exit(semaphore_demo_exit);
MODULE_LICENSE("GPL");
  • struct semaphore my_semaphore;:定义一个信号量 my_semaphore
  • sema_init(&my_semaphore, 1);:初始化信号量 my_semaphore,初始计数值为1。&my_semaphore 是信号量的地址,1是初始计数值。
  • down_interruptible(&my_semaphore);:尝试获取信号量。如果信号量计数值为0,调用者会进入睡眠状态,并且可以被信号中断。返回0表示成功获取信号量,非0表示获取失败。
  • up(&my_semaphore);:释放信号量,使信号量的计数值加1。
3.1.5 互斥体

互斥体是一种特殊的二值信号量(计数值为1),用于实现对共享资源的互斥访问,即同一时间只有一个进程可以访问共享资源。

#include <linux/init.h>
#include <linux/module.h>
#include <linux/mutex.h>

// 定义互斥体
struct mutex my_mutex;
// 定义一个共享变量
int shared_variable;

// 模块加载函数
static int __init mutex_demo_init(void) {
    // 初始化互斥体
    mutex_init(&my_mutex);
    shared_variable = 0;

    // 尝试获取互斥体
    mutex_lock(&my_mutex);
    // 访问和修改共享变量
    shared_variable++;
    // 释放互斥体
    mutex_unlock(&my_mutex);

    printk(KERN_INFO "Shared variable value: %d\n", shared_variable);
    return 0;
}

// 模块卸载函数
static void __exit mutex_demo_exit(void) {
    // 尝试获取互斥体
    mutex_lock(&my_mutex);
    // 访问和修改共享变量
    shared_variable--;
    // 释放互斥体
    mutex_unlock(&my_mutex);

    printk(KERN_INFO "Final shared variable value: %d\n", shared_variable);
    // 销毁互斥体
    mutex_destroy(&my_mutex);
}

module_init(mutex_demo_init);
module_exit(mutex_demo_exit);
MODULE_LICENSE("GPL");
  • struct mutex my_mutex;:定义一个互斥体 my_mutex
  • mutex_init(&my_mutex);:初始化互斥体 my_mutex&my_mutex 是互斥体的地址。
  • mutex_lock(&my_mutex);:尝试获取互斥体。如果互斥体已被占用,调用者会进入睡眠状态。
  • mutex_unlock(&my_mutex);:释放互斥体,允许其他进程获取。
  • mutex_destroy(&my_mutex);:销毁互斥体,通常在模块卸载时调用。
3.1.6 完成变量

完成变量用于线程间的同步,一个线程可以等待某个事件完成,而另一个线程在事件完成后使用完成变量通知等待的线程。

#include <linux/init.h>
#include <linux/module.h>
#include <linux/completion.h>

// 定义完成变量
struct completion my_completion;
// 定义一个标志变量
int flag;

// 模拟一个工作函数
void work_function(void) {
    // 模拟一些工作
    flag = 1;
    // 标记完成变量
    complete(&my_completion);
}

// 模块加载函数
static int __init completion_demo_init(void) {
    // 初始化完成变量
    init_completion(&my_completion);
    flag = 0;

    // 启动工作函数(这里只是模拟,实际可能是一个线程或中断处理程序调用)
    work_function();

    // 等待完成变量
    wait_for_completion(&my_completion);
    if (flag) {
        printk(KERN_INFO "Work completed successfully\n");
    } else {
        printk(KERN_ERR "Work did not complete as expected\n");
    }
    return 0;
}

// 模块卸载函数
static void __exit completion_demo_exit(void) {
    // 不需要额外清理操作,因为完成变量没有资源需要释放
    printk(KERN_INFO "Module unloaded\n");
}

module_init(completion_demo_init);
module_exit(completion_demo_exit);
MODULE_LICENSE("GPL");
  • struct completion my_completion;:定义一个完成变量 my_completion
  • init_completion(&my_completion);:初始化完成变量 my_completion&my_completion 是完成变量的地址。
  • complete(&my_completion);:标记完成变量,通知等待的线程事件已完成。
  • wait_for_completion(&my_completion);:等待完成变量被标记。调用者会进入睡眠状态,直到完成变量被标记。

3.2 阻塞I/O

3.2.1 睡眠和唤醒

在Linux内核中,睡眠是进程主动放弃CPU资源,进入等待状态的过程,直到特定事件发生被唤醒。唤醒则是将处于睡眠状态的进程重新设置为可运行状态。

睡眠和唤醒机制常用于实现阻塞I/O,当设备数据未准备好时,进程通过睡眠避免占用CPU资源,待数据准备好后被唤醒继续执行。

内核提供了多种睡眠和唤醒函数,如 wait_eventwait_event_interruptible 用于睡眠,wake_upwake_up_interruptible 用于唤醒。

3.2.2 等待队列

等待队列是一种内核数据结构,用于管理处于睡眠状态的进程。当进程需要等待某个事件发生时,会将自身加入到对应的等待队列中。当事件发生时,等待队列中的进程会被唤醒。

下面是一个简单的等待队列使用示例:

#include <linux/init.h>
#include <linux/module.h>
#include <linux/wait.h>

// 定义等待队列头
wait_queue_head_t my_wait_queue;
// 定义一个标志变量,用于表示事件是否发生
int event_occurred;

// 模块加载函数
static int __init wait_queue_example_init(void) {
    // 初始化等待队列头
    init_waitqueue_head(&my_wait_queue);
    // 初始化事件标志为0,表示事件未发生
    event_occurred = 0;

    // 打印信息表示模块加载成功
    printk(KERN_INFO "Wait queue example module loaded.\n");
    return 0;
}

// 模拟一个函数,用于等待事件发生
void wait_for_event(void) {
    // 将当前进程加入等待队列并睡眠,直到event_occurred为真
    wait_event_interruptible(my_wait_queue, event_occurred);
    if (event_occurred) {
        printk(KERN_INFO "Event occurred, process awakened.\n");
    } else {
        printk(KERN_INFO "Process was interrupted before event occurred.\n");
    }
}

// 模拟一个函数,用于触发事件发生
void trigger_event(void) {
    // 设置事件标志为1,表示事件发生
    event_occurred = 1;
    // 唤醒等待队列中的所有进程
    wake_up(&my_wait_queue);
}

// 模块卸载函数
static void __exit wait_queue_example_exit(void) {
    // 打印信息表示模块卸载成功
    printk(KERN_INFO "Wait queue example module unloaded.\n");
}

module_init(wait_queue_example_init);
module_exit(wait_queue_example_exit);
MODULE_LICENSE("GPL");
  • wait_queue_head_t my_wait_queue;:定义一个等待队列头 my_wait_queue,用于管理等待该事件的进程。
  • init_waitqueue_head(&my_wait_queue);:初始化等待队列头 my_wait_queue
  • wait_event_interruptible(my_wait_queue, event_occurred);:将当前进程加入到 my_wait_queue 等待队列并进入睡眠状态,直到 event_occurred 变为真或者进程被信号中断。
  • wake_up(&my_wait_queue);:唤醒 my_wait_queue 等待队列中的所有进程。
3.2.3 实现阻塞I/O

阻塞I/O意味着当进程执行I/O操作时,如果设备没有准备好数据,进程会进入睡眠状态,直到数据准备好。

以下是在字符设备驱动中实现阻塞I/O的示例:

#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/wait.h>

// 定义设备号
dev_t dev_num;
// 定义文件操作结构体
struct file_operations simple_fops = {
  .read = simple_read,
};
// 定义等待队列头
wait_queue_head_t my_wait_queue;
// 定义一个标志,表示数据是否准备好
int data_ready;
// 定义一个缓冲区,用于存储数据
char buffer[100];

// 简单的read方法实现,支持阻塞I/O
ssize_t simple_read(struct file *filp, char __user *buf, size_t count, loff_t *off) {
    // filp:文件指针,包含文件的状态、位置等信息
    // buf:用户空间缓冲区,用于存储读取的数据
    // count:请求读取的字节数
    // off:文件偏移量,指定从文件的哪个位置开始读取

    // 等待数据准备好
    wait_event_interruptible(my_wait_queue, data_ready);

    // 将数据从内核缓冲区复制到用户空间
    if (copy_to_user(buf, buffer, count)) {
        return -EFAULT;
    }

    // 重置数据准备标志
    data_ready = 0;

    return count;
}

// 模拟数据准备函数
void data_prepare(void) {
    // 假设这里填充缓冲区数据
    snprintf(buffer, sizeof(buffer), "Data is ready");
    data_ready = 1;
    // 唤醒等待队列中的进程
    wake_up(&my_wait_queue);
}

// 模块加载函数
static int __init simple_char_driver_init(void) {
    if (alloc_chrdev_region(&dev_num, 0, 1, "simple_char_dev")) {
        printk(KERN_ERR "Failed to allocate device number.\n");
        return -1;
    }

    if (register_chrdev(MAJOR(dev_num), "simple_char_dev", &simple_fops)) {
        printk(KERN_ERR "Failed to register character device.\n");
        unregister_chrdev_region(dev_num, 1);
        return -1;
    }

    // 初始化等待队列头
    init_waitqueue_head(&my_wait_queue);
    data_ready = 0;

    printk(KERN_INFO "Character device registered successfully.\n");
    return 0;
}

// 模块卸载函数
static void __exit simple_char_driver_exit(void) {
    unregister_chrdev(MAJOR(dev_num), "simple_char_dev");
    unregister_chrdev_region(dev_num, 1);
    printk(KERN_INFO "Simple character driver unloaded.\n");
}

module_init(simple_char_driver_init);
module_exit(simple_char_driver_exit);
// 假设这里有一个外部函数可以调用data_prepare,例如在某个中断处理程序中调用
3.2.4 非阻塞I/O

非阻塞I/O是指当进程执行I/O操作时,如果设备没有准备好数据,进程不会进入睡眠状态,而是立即返回一个错误码(如 -EAGAIN),表示操作暂时无法完成。

在字符设备驱动中,可以通过检查 filp->f_flags 中的 O_NONBLOCK 标志来实现非阻塞I/O。

#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/uaccess.h>

// 定义设备号
dev_t dev_num;
// 定义文件操作结构体
struct file_operations simple_fops = {
  .read = simple_read,
};
// 定义一个缓冲区,用于存储数据
char buffer[100];
// 定义一个标志,表示数据是否准备好
int data_ready;

// 简单的read方法实现,支持非阻塞I/O
ssize_t simple_read(struct file *filp, char __user *buf, size_t count, loff_t *off) {
    // filp:文件指针,包含文件的状态、位置等信息
    // buf:用户空间缓冲区,用于存储读取的数据
    // count:请求读取的字节数
    // off:文件偏移量,指定从文件的哪个位置开始读取

    // 检查是否为非阻塞模式
    if (filp->f_flags & O_NONBLOCK) {
        if (!data_ready) {
            return -EAGAIN;
        }
    } else {
        // 如果是阻塞模式,等待数据准备好(这里简单模拟等待,实际可结合等待队列)
        while (!data_ready);
    }

    // 将数据从内核缓冲区复制到用户空间
    if (copy_to_user(buf, buffer, count)) {
        return -EFAULT;
    }

    // 重置数据准备标志
    data_ready = 0;

    return count;
}

// 模拟数据准备函数
void data_prepare(void) {
    // 假设这里填充缓冲区数据
    snprintf(buffer, sizeof(buffer), "Data is ready");
    data_ready = 1;
}

// 模块加载函数
static int __init simple_char_driver_init(void) {
    if (alloc_chrdev_region(&dev_num, 0, 1, "simple_char_dev")) {
        printk(KERN_ERR "Failed to allocate device number.\n");
        return -1;
    }

    if (register_chrdev(MAJOR(dev_num), "simple_char_dev", &simple_fops)) {
        printk(KERN_ERR "Failed to register character device.\n");
        unregister_chrdev_region(dev_num, 1);
        return -1;
    }

    data_ready = 0;

    printk(KERN_INFO "Character device registered successfully.\n");
    return 0;
}

// 模块卸载函数
static void __exit simple_char_driver_exit(void) {
    unregister_chrdev(MAJOR(dev_num), "simple_char_dev");
    unregister_chrdev_region(dev_num, 1);
    printk(KERN_INFO "Simple character driver unloaded.\n");
}

module_init(simple_char_driver_init);
module_exit(simple_char_driver_exit);
// 假设这里有一个外部函数可以调用data_prepare,例如在某个中断处理程序中调用
3.2.5 轮询

轮询是一种I/O操作方式,进程不断查询设备状态,以确定是否可以进行I/O操作。在Linux内核中,轮询通常通过实现 poll 方法来支持。

#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/poll.h>

// 定义设备号
dev_t dev_num;
// 定义文件操作结构体
struct file_operations simple_fops = {
  .poll = simple_poll,
};
// 定义一个标志,表示数据是否准备好
int data_ready;

// 简单的poll方法实现
unsigned int simple_poll(struct file *filp, poll_table *wait) {
    // filp:文件指针,包含文件的状态、位置等信息
    // wait:用于注册等待队列的结构体

    unsigned int mask = 0;

    // 检查数据是否准备好
    if (data_ready) {
        mask |= POLLIN | POLLRDNORM;
    }

    // 注册等待队列,以便在数据准备好时被唤醒
    poll_wait(filp, NULL, wait);

    return mask;
}

// 模拟数据准备函数
void data_prepare(void) {
    data_ready = 1;
}

// 模块加载函数
static int __init simple_char_driver_init(void) {
    if (alloc_chrdev_region(&dev_num, 0, 1, "simple_char_dev")) {
        printk(KERN_ERR "Failed to allocate device number.\n");
        return -1;
    }

    if (register_chrdev(MAJOR(dev_num), "simple_char_dev", &simple_fops)) {
        printk(KERN_ERR "Failed to register character device.\n");
        unregister_chrdev_region(dev_num, 1);
        return -1;
    }

    data_ready = 0;

    printk(KERN_INFO "Character device registered successfully.\n");
    return 0;
}

// 模块卸载函数
static void __exit simple_char_driver_exit(void) {
    unregister_chrdev(MAJOR(dev_num), "simple_char_dev");
    unregister_chrdev_region(dev_num, 1);
    printk(KERN_INFO "Simple character driver unloaded.\n");
}

module_init(simple_char_driver_init);
module_exit(simple_char_driver_exit);
// 假设这里有一个外部函数可以调用data_prepare,例如在某个中断处理程序中调用
  • unsigned int simple_poll(struct file *filp, poll_table *wait)poll 方法的实现。filp 是文件指针,wait 用于注册等待队列。
  • mask |= POLLIN | POLLRDNORM;:如果数据准备好,设置 mask 表示数据可读。
  • poll_wait(filp, NULL, wait);:注册等待队列,当数据状态改变时,进程会被唤醒并重新调用 poll 方法。

3.3 内存映射和DMA

3.3.1 内存映射

内存映射允许将设备的物理内存或文件的内容映射到用户空间的虚拟地址空间,这样应用程序可以直接通过操作虚拟地址来访问设备内存或文件数据,而不需要频繁地进行内核态与用户态之间的数据拷贝,从而提高数据传输效率。

下面是在字符设备驱动中实现内存映射的详细代码示例:

#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/mm.h>
#include <linux/slab.h>

// 定义设备号
dev_t dev_num;
// 定义文件操作结构体
struct file_operations simple_fops = {
 .mmap = simple_mmap,
};
// 定义一个用于内存映射的缓冲区
char *mmap_buffer;
// 定义缓冲区大小
#define BUFFER_SIZE 4096

// 简单的mmap方法实现
int simple_mmap(struct file *filp, struct vm_area_struct *vma) {
    // filp:指向与文件相关的结构体,包含文件的各种信息,如打开模式、当前位置等
    // vma:虚拟内存区域结构体,描述了用户空间中一段连续的虚拟地址范围及其属性

    // 检查虚拟内存区域的大小是否符合要求
    if (vma->vm_end - vma->vm_start > BUFFER_SIZE) {
        return -EINVAL;
    }

    // 分配内核内存用于映射
    mmap_buffer = kmalloc(BUFFER_SIZE, GFP_KERNEL);
    if (!mmap_buffer) {
        return -ENOMEM;
    }

    // 将内核缓冲区清零
    memset(mmap_buffer, 0, BUFFER_SIZE);

    // 将内核缓冲区映射到用户空间
    if (remap_pfn_range(vma, vma->vm_start, virt_to_phys(mmap_buffer) >> PAGE_SHIFT,
                          vma->vm_end - vma->vm_start, vma->vm_page_prot)) {
        kfree(mmap_buffer);
        return -EAGAIN;
    }

    return 0;
}

// 模块加载函数
static int __init simple_char_driver_init(void) {
    if (alloc_chrdev_region(&dev_num, 0, 1, "simple_char_dev")) {
        printk(KERN_ERR "Failed to allocate device number.\n");
        return -1;
    }

    if (register_chrdev(MAJOR(dev_num), "simple_char_dev", &simple_fops)) {
        printk(KERN_ERR "Failed to register character device.\n");
        unregister_chrdev_region(dev_num, 1);
        return -1;
    }

    printk(KERN_INFO "Character device registered successfully.\n");
    return 0;
}

// 模块卸载函数
static void __exit simple_char_driver_exit(void) {
    if (mmap_buffer) {
        kfree(mmap_buffer);
    }
    unregister_chrdev(MAJOR(dev_num), "simple_char_dev");
    unregister_chrdev_region(dev_num, 1);
    printk(KERN_INFO "Simple character driver unloaded.\n");
}

module_init(simple_char_driver_init);
module_exit(simple_char_driver_exit);
MODULE_LICENSE("GPL");
  • if (vma->vm_end - vma->vm_start > BUFFER_SIZE):检查用户请求映射的虚拟内存区域大小是否超过我们定义的缓冲区大小,如果超过则返回 -EINVAL 表示无效参数。
  • mmap_buffer = kmalloc(BUFFER_SIZE, GFP_KERNEL):使用 kmalloc 分配内核内存,BUFFER_SIZE 是需要分配的内存大小,GFP_KERNEL 是分配标志,表示从内核内存池中分配内存。
  • memset(mmap_buffer, 0, BUFFER_SIZE):将分配的内核缓冲区清零,以确保初始状态下数据的一致性。
  • remap_pfn_range(vma, vma->vm_start, virt_to_phys(mmap_buffer) >> PAGE_SHIFT, vma->vm_end - vma->vm_start, vma->vm_page_prot):将内核缓冲区的物理地址映射到用户空间的虚拟地址范围。vma 是虚拟内存区域结构体,vma->vm_start 是映射起始虚拟地址,virt_to_phys(mmap_buffer) >> PAGE_SHIFT 将内核缓冲区的虚拟地址转换为物理页帧号,vma->vm_end - vma->vm_start 是映射的长度,vma->vm_page_prot 是虚拟内存区域的保护属性。如果映射失败,释放已分配的内核内存并返回 -EAGAIN
3.3.2 直接内存访问(DMA)

直接内存访问(DMA)允许硬件设备直接访问系统内存,而无需CPU的频繁干预。这在大量数据传输时能显著提高系统性能,因为CPU可以在DMA传输数据的同时执行其他任务。

以下是一个简单的字符设备驱动中使用DMA的示例代码:

#include <linux/init.h>
#include <linux/module.h>
#include <linux/dma - mapping.h>
#include <linux/fs.h>
#include <linux/uaccess.h>

// 定义设备号
dev_t dev_num;
// 定义文件操作结构体
struct file_operations simple_fops = {
 .read = simple_read,
};
// 定义一个用于DMA传输的缓冲区
char dma_buffer[4096];
// 定义DMA地址
dma_addr_t dma_addr;

// 简单的read方法实现,使用DMA
ssize_t simple_read(struct file *filp, char __user *buf, size_t count, loff_t *off) {
    // filp:指向与文件相关的结构体,包含文件的各种信息,如打开模式、当前位置等
    // buf:用户空间缓冲区,用于存储从设备读取的数据
    // count:请求读取的字节数
    // off:文件偏移量,指定从文件的哪个位置开始读取

    // 将DMA缓冲区映射到总线地址空间
    dma_addr = dma_map_single(NULL, dma_buffer, sizeof(dma_buffer), DMA_FROM_DEVICE);
    if (dma_mapping_error(NULL, dma_addr)) {
        printk(KERN_ERR "DMA mapping error\n");
        return -EIO;
    }

    // 模拟设备通过DMA将数据传输到dma_buffer
    // 实际应用中,这里需要与设备的硬件进行交互,例如设置DMA控制器的寄存器等
    // 假设设备已经将数据传输到dma_buffer

    // 解除DMA映射
    dma_unmap_single(NULL, dma_addr, sizeof(dma_buffer), DMA_FROM_DEVICE);

    // 将DMA缓冲区的数据复制到用户空间
    if (copy_to_user(buf, dma_buffer, count)) {
        printk(KERN_ERR "Failed to copy data to user space\n");
        return -EFAULT;
    }

    return count;
}

// 模块加载函数
static int __init simple_char_driver_init(void) {
    if (alloc_chrdev_region(&dev_num, 0, 1, "simple_char_dev")) {
        printk(KERN_ERR "Failed to allocate device number.\n");
        return -1;
    }

    if (register_chrdev(MAJOR(dev_num), "simple_char_dev", &simple_fops)) {
        printk(KERN_ERR "Failed to register character device.\n");
        unregister_chrdev_region(dev_num, 1);
        return -1;
    }

    printk(KERN_INFO "Character device registered successfully.\n");
    return 0;
}

// 模块卸载函数
static void __exit simple_char_driver_exit(void) {
    unregister_chrdev(MAJOR(dev_num), "simple_char_dev");
    unregister_chrdev_region(dev_num, 1);
    printk(KERN_INFO "Simple character driver unloaded.\n");
}

module_init(simple_char_driver_init);
module_exit(simple_char_driver_exit);
MODULE_LICENSE("GPL");
  • dma_addr = dma_map_single(NULL, dma_buffer, sizeof(dma_buffer), DMA_FROM_DEVICE):使用 dma_map_single 函数将DMA缓冲区 dma_buffer 映射到总线地址空间,返回对应的DMA地址。NULL 表示使用默认的设备(如果是在设备驱动中,通常会传入设备结构体指针),dma_buffer 是要映射的缓冲区,sizeof(dma_buffer) 是缓冲区大小,DMA_FROM_DEVICE 表示数据传输方向是从设备到内存。如果映射失败,通过 dma_mapping_error 检查并返回 -EIO 表示I/O错误。
  • dma_unmap_single(NULL, dma_addr, sizeof(dma_buffer), DMA_FROM_DEVICE):在DMA传输完成后,使用 dma_unmap_single 函数解除DMA映射,释放相关资源。参数与映射时类似。
  • copy_to_user(buf, dma_buffer, count):将DMA缓冲区中的数据复制到用户空间缓冲区 buf 中,count 是要复制的字节数。如果复制失败,返回 -EFAULT 表示错误。

标签:驱动程序,simple,void,dev,init,exit,Linux,my,设备
From: https://blog.csdn.net/qq_40844444/article/details/145004904

相关文章

  • 清理linux的buff/cache缓存
    前言:在Linux系统中,buff/cache通常指的是缓冲区和缓存。这两个概念是内核管理的,用于加速对磁盘和文件的访问。清理这部分内存通常是为了释放内存空间供其他用途使用,但这并不是一个常见的操作,因为内核会自动管理这部分内存。如果你确实需要清理buff/cache,可以通过执行sync命令来确......
  • 思科网络设备常用巡检命令(建议收藏)
    在网络运维工作中,定期巡检设备是确保网络稳定运行的重要动作,思科设备作为网络架构中的重要组成部分,掌握相应巡检命令对于运维人员来说至关重要。下面将详细介绍常规的一些思科网络设备巡检命令。目 录1、思科路由交换2、思科ASA防火墙3、思科Nexus交换机1、思科路由......
  • 学习 - Linux - CentOS安装Tomcat8.5.85
    CentOS安装Tomcat8.5.851、保证已经安装了jdk运行环境java-version如果没有,请参考Centos安装jdk2、从Apache官方网站下载Tomcat8cd/optsudowgethttps://archive.apache.org/dist/tomcat/tomcat-8/v8.5.85/bin/apache-tomcat-8.5.85.tar.gz3、载完成后,解压缩Tomca......
  • Linux通过端口找到对应的服务
      首先执行netstat-tulnp|grep<端口号>例如:netstat-tulnp|grep:80参数解释:-t:显示 TCP 连接(只显示TCP协议的端口信息)。-u:显示 UDP 连接(只显示UDP协议的端口信息)。-l:显示正在监听(Listening)状态的端口(只显示处于监听状态的端口)。-n:以数字格式显......
  • linux上安装jdk
    linux上安装jdk注意:以下所有命令在centos7.6环境下测试,其他linux环境请自行测试注意:在linux中,不是管理员登录,最好在指令前加上sudo提权,避免权限不够执行失败带来的麻烦,以下命令请都加上sudo,这是一个好习惯。安装linux时预装jdk,选择软件时勾上开发组件卸载jdkjava-version ......
  • Java Bluetooth 蓝牙通讯 BlueCove 扫描附近的蓝牙设备
    目录BlueCove项目概述BlueCoveAPI架构API的设计原则和实现方式关键类和方法的功能描述测试代码获取本机(PC)蓝牙扫描蓝牙BlueCove项目概述BlueCove是一个开源的蓝牙协议栈实现,旨在为Java开发者提供一个全面的、易于使用的API,从而在应用程序中实现蓝牙功能。该项目支持多种操作......
  • [Linux]线程概念与控制
    目录一、线程概念1.什么是线程2.线程的轻量化3.LWP字段4.局部性原理5.线程的优缺点6.进程VS线程二、线程的控制1.线程创建2.获取线程id3.线程退出与等待4.创建轻量级进程三、线程的管理1.pthread库管理线程2.线程局部存储四、C++线程库1.构造函数 2.成员函......
  • Linux 下>> << > <介绍
    在Linux或类Unix系统的命令行中,>>、<<、>和<是用于重定向输入输出的操作符。它们允许你控制命令的输入和输出,改变数据流的方向。这些符号是常见的I/O重定向符号,在shell脚本和命令行操作中非常有用。(输出重定向,覆盖文件)用于将命令的标准输出(stdout)重定向到......
  • 学习 - Linux - Centos安装jdk8
    Centos安装jdk8安装包下载地址:通过网盘分享的文件:jdk链接:https://pan.baidu.com/s/1dQTMZk7foPZhOcw55QM_lQ?pwd=sky1提取码:sky1二、上传至Centos系统解压文件并移动到usr/local目录下解压tarzxvfjdk-8u181-linux-x64.tar.gz移动mvjdk1.8.0_181/usr/local/jdk8......
  • 深入解析IPoIB驱动中的PKey管理与设备初始化
    在Linux内核中,IPoverInfiniBand(IPoIB)是一种将IP协议运行在InfiniBand网络上的技术。为了实现IPoIB网络设备的高效管理,内核驱动需要处理许多复杂的任务,包括PKey(PartitionKey)的管理、设备的初始化与启动等。本文将深入分析IPoIB驱动中的关键函数和机制,特别是ib_find_pkey、ipo......