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_event
、wait_event_interruptible
用于睡眠,wake_up
、wake_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
表示错误。