一、环境选择
虚拟机软件:VMware Workstation 17.6.3
操作系统:Ubuntu20.04
交叉编译工具链:gcc version 9.4.0 (Ubuntu 9.4.0-1ubuntu1~20.04.2)
QEMU版本:QEMU emulator version 4.2.1 (Debian 1:4.2-3ubuntu6.30)
Linux源码版本:6.13.6
二、代码实现
字符设备驱动程序
这段代码是一个Linux 字符设备驱动程序,主要功能是实现内核空间与用户空间的双向数据通信。以下是对代码的具体说明:
-
驱动类型
- 属于字符设备驱动(Character Device Driver),遵循 Linux 内核驱动模型,通过文件操作接口(
open/read/write等)与用户空间交互。
- 属于字符设备驱动(Character Device Driver),遵循 Linux 内核驱动模型,通过文件操作接口(
-
核心功能
- 动态分配设备号并注册字符设备
- 实现设备文件的打开、关闭、读取、写入操作
- 通过设备类机制自动创建设备节点,避免手动执行
mknod - 利用
copy_to_user/copy_from_user实现内核与用户空间的数据安全传输
hello_drv.c代码实现:
#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/cdev.h>
#include <linux/device.h>
#define DEV_NAME "hello_drv"
#define BUFFER_SIZE 256
static dev_t dev_num;
static struct cdev hello_cdev;
static char kernel_buffer[BUFFER_SIZE] = "Hello from the kernel module!\n";
static struct class *hello_class;
static struct device *hello_device;
// 设备打开操作
static int hello_open(struct inode *inode, struct file *file) {
printk(KERN_INFO "[打开设备] Hello: Device opened\n");
return 0;
}
// 设备释放操作
static int hello_release(struct inode *inode, struct file *file) {
printk(KERN_INFO "[关闭设备] Hello: Device closed\n");
return 0;
}
// 设备读取操作
static ssize_t hello_read(struct file *file, char __user *buf, size_t count, loff_t *loff) {
size_t len = strlen(kernel_buffer);
size_t read_len = min(count, len);
if (copy_to_user(buf, kernel_buffer, read_len))
return -EFAULT;
printk(KERN_INFO "[读取操作] Hello: Sent %zu characters to the user\n", read_len);
return read_len;
}
// 设备写入操作
static ssize_t hello_write(struct file *file, const char __user *buf, size_t count, loff_t *loff) {
size_t write_len = min(count, BUFFER_SIZE - 1);
if (copy_from_user(kernel_buffer, buf, write_len))
return -EFAULT;
kernel_buffer[write_len] = '\0';
printk(KERN_INFO "[写入操作] Hello: Received %zu characters from the user: %s\n",
write_len, kernel_buffer);
return write_len;
}
// 设备操作函数表
static const struct file_operations hello_fops = {
.open = hello_open,
.release = hello_release,
.read = hello_read,
.write = hello_write,
};
// 模块初始化
static int __init hello_init(void) {
int ret = alloc_chrdev_region(&dev_num, 0, 1, DEV_NAME);
if (ret < 0) {
printk(KERN_ERR "Failed to allocate device number\n");
return ret;
}
printk(KERN_INFO "[加载模块] Hello: Module loaded successfully! Major number: %d\n", MAJOR(dev_num));
cdev_init(&hello_cdev, &hello_fops);
hello_cdev.owner = THIS_MODULE;
ret = cdev_add(&hello_cdev, dev_num, 1);
if (ret < 0) {
unregister_chrdev_region(dev_num, 1);
printk(KERN_ERR "Failed to add cdev\n");
return ret;
}
// 创建设备类(用于自动创建设备节点)
hello_class = class_create("hello_class");
if (IS_ERR(hello_class)) {
cdev_del(&hello_cdev);
unregister_chrdev_region(dev_num, 1);
printk(KERN_ERR "Failed to create class\n");
return PTR_ERR(hello_class);
}
// 创建设备节点(自动在/dev下生成hello_drv)
hello_device = device_create(hello_class, NULL, dev_num, NULL, "hello_drv");
if (IS_ERR(hello_device)) {
class_destroy(hello_class);
cdev_del(&hello_cdev);
unregister_chrdev_region(dev_num, 1);
printk(KERN_ERR "Failed to create device\n");
return PTR_ERR(hello_device);
}
printk(KERN_INFO "[加载模块] Hello: Create device file with: mknod /dev/hello_drv c %d 0\n",
MAJOR(dev_num));
return 0;
}
// 模块卸载
static void __exit hello_exit(void) {
// 先释放设备和类
if (hello_device)
device_destroy(hello_class, dev_num);
if (hello_class)
class_destroy(hello_class);
cdev_del(&hello_cdev);
unregister_chrdev_region(dev_num, 1);
printk(KERN_INFO "[卸载模块] Hello: Module unloaded successfully!\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");

在drivers目录下char目录创建hello_drv目录,同时touch创建hello_drv.c和Kconfig和Makefile编写Kconfig和Makefile如图所示
同时修改divers/char目录下的Makefile文件,在文件末尾添加
将模块静态编译进内核,内核启动时自动加载,随内核关闭而卸载。
obj-y +=hello_drv/
在Kconfig文件末尾添加
source "drivers/char/hello_drv/Kconfig"
配置环境
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- defconfig
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- menuconfig
编译内核
make -j8 ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu-
用户空间测试代码
hello_test.c
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#define DEV_PATH "/dev/hello_drv" // 与驱动中的DEV_NAME保持一致
#define BUFFER_SIZE 256
int main() {
int fd;
char buffer[BUFFER_SIZE];
// 打开设备
fd = open(DEV_PATH, O_RDWR);
if (fd < 0) {
perror("Failed to open device");
return -1;
}
// 读取初始数据
memset(buffer, 0, BUFFER_SIZE);
if (read(fd, buffer, BUFFER_SIZE) < 0) {
perror("Failed to read from device");
close(fd);
return -1;
}
printf("[用户程序] 读取数据: %s", buffer);
// 写入数据
const char* write_data = "Hello from user space!\n";
if (write(fd, write_data, strlen(write_data)) < 0) {
perror("Failed to write to device");
close(fd);
return -1;
}
printf("[用户程序] 写入数据: %s", write_data);
// 再次读取数据
memset(buffer, 0, BUFFER_SIZE);
if (read(fd, buffer, BUFFER_SIZE) < 0) {
perror("Failed to read from device");
close(fd);
return -1;
}
printf("[用户程序] 读取更新后数据: %s", buffer);
// 关闭设备
close(fd);
return 0;
}
编译测试程序
aarch64-linux-gnu-gcc -static -o hello_test hello_test.c
复制到共享文件夹sharedir
cp hello_test ../../sharedir
三、用户空间测试
启动qemu模拟器
qemu-system-aarch64 \
-M virt \
-cpu cortex-a53 \
-kernel arch/arm64/boot/Image.gz \
-initrd initramfs_arm64.img \
-nographic \
-serial mon:stdio \
-append "console=ttyAMA0" \
-fsdev local,security_model=passthrough,id=fsdev0,path=sharedir \
-device virtio-9p-pci,id=fs0,fsdev=fsdev0,mount_tag=hostshare
挂载共享目录,可以看到用户空间测试程序

通过日志输出可以看到,字符设备驱动已经自动加载了

现在运行用户空间测试程序,可以看到输出结果符合预期

通信的本质其实就是看见同一份资源, 用户空间的 fd 与内核的 file 结构体通过文件系统层建立映射。
内核驱动通过
class_create和device_create自动创建设备节点(如/dev/hello_drv),用户空间通过访问该节点与内核通信。内核驱动实现
open/read/write/release等文件操作函数,用户空间通过系统调用read(fd, buf, len))触发内核逻辑。

887

被折叠的 条评论
为什么被折叠?



