1. 字符设备驱动基础框架
任务目标
在Android Linux内核环境中实现一个最小字符设备驱动:动态注册主设备号、创建device class并在/dev下生成节点,实现open、read、write、release四个基本文件操作。驱动加载后用户态可通过设备节点完成一次内核态数据读写。
环境与依赖
- Linux内核源码树已编译,且
CONFIG_DEVFS或CONFIG_DEVTMPFS已启用(Android默认启用devtmpfs) - 具备交叉编译工具链(Android内核通常使用
aarch64-linux-android-或arm-linux-androideabi-) - 内核头文件路径与目标平台匹配
驱动完整代码
文件:basic_char_dev.c
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/device.h>
#include <linux/uaccess.h>
#include <linux/slab.h>
#define DEVICE_NAME "demo_char"
#define CLASS_NAME "demo_class"
#define BUF_SIZE 128
static int major_num;
static struct class *demo_class;
static struct device *demo_device;
static char *kernel_buf;
static int dev_open(struct inode *inode, struct file *filep)
{
pr_info("demo_char: open() called\n");
return 0;
}
static ssize_t dev_read(struct file *filep, char __user *buffer,
size_t len, loff_t *offset)
{
size_t bytes_to_read;
if (*offset >= BUF_SIZE)
return 0;
bytes_to_read = min(len, (size_t)(BUF_SIZE - *offset));
if (copy_to_user(buffer, kernel_buf + *offset, bytes_to_read)) {
pr_err("demo_char: copy_to_user failed\n");
return -EFAULT;
}
*offset += bytes_to_read;
pr_info("demo_char: read %zu bytes\n", bytes_to_read);
return bytes_to_read;
}
static ssize_t dev_write(struct file *filep, const char __user *buffer,
size_t len, loff_t *offset)
{
size_t bytes_to_write;
if (*offset >= BUF_SIZE)
return -ENOSPC;
bytes_to_write = min(len, (size_t)(BUF_SIZE - *offset));
if (copy_from_user(kernel_buf + *offset, buffer, bytes_to_write)) {
pr_err("demo_char: copy_from_user failed\n");
return -EFAULT;
}
*offset += bytes_to_write;
pr_info("demo_char: wrote %zu bytes\n", bytes_to_write);
return bytes_to_write;
}
static int dev_release(struct inode *inode, struct file *filep)
{
pr_info("demo_char: release() called\n");
return 0;
}
static struct file_operations fops = {
.owner = THIS_MODULE,
.open = dev_open,
.read = dev_read,
.write = dev_write,
.release = dev_release,
};
static int __init demo_init(void)
{
kernel_buf = kzalloc(BUF_SIZE, GFP_KERNEL);
if (!kernel_buf)
return -ENOMEM;
major_num = register_chrdev(0, DEVICE_NAME, &fops);
if (major_num < 0) {
pr_err("demo_char: failed to register major number\n");
kfree(kernel_buf);
return major_num;
}
demo_class = class_create(THIS_MODULE, CLASS_NAME);
if (IS_ERR(demo_class)) {
unregister_chrdev(major_num, DEVICE_NAME);
kfree(kernel_buf);
return PTR_ERR(demo_class);
}
demo_device = device_create(demo_class, NULL,
MKDEV(major_num, 0), NULL, DEVICE_NAME);
if (IS_ERR(demo_device)) {
class_destroy(demo_class);
unregister_chrdev(major_num, DEVICE_NAME);
kfree(kernel_buf);
return PTR_ERR(demo_device);
}
pr_info("demo_char: initialized, major=%d\n", major_num);
return 0;
}
static void __exit demo_exit(void)
{
device_destroy(demo_class, MKDEV(major_num, 0));
class_destroy(demo_class);
unregister_chrdev(major_num, DEVICE_NAME);
kfree(kernel_buf);
pr_info("demo_char: exited\n");
}
module_init(demo_init);
module_exit(demo_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Android Driver Lab");
MODULE_DESCRIPTION("Minimal char device driver framework");
MODULE_VERSION("1.0");
编译与加载
将以下Makefile置于同一目录:
obj-m := basic_char_dev.o
KERNEL_DIR := /path/to/android/kernel
CROSS_COMPILE := aarch64-linux-android-
CC := $(CROSS_COMPILE)gcc
all:
$(MAKE) -C $(KERNEL_DIR) M=$(PWD) ARCH=arm64 CROSS_COMPILE=$(CROSS_COMPILE) modules
clean:
$(MAKE) -C $(KERNEL_DIR) M=$(PWD) ARCH=arm64 CROSS_COMPILE=$(CROSS_COMPILE) clean
编译后得到basic_char_dev.ko,通过adb推送到设备并加载:
adb push basic_char_dev.ko /data/local/tmp/
adb shell su -c "insmod /data/local/tmp/basic_char_dev.ko"
验证方法
- 设备节点检查:加载后执行
ls -l /dev/demo_char,应显示字符设备且主设备号为动态分配值(如c 240:0)。 - 写读回环测试:执行以下命令验证数据通路:
# 写入字符串
echo "hello kernel" > /dev/demo_char
# 读取内容(应输出 "hello kernel")
cat /dev/demo_char
# 查看内核日志确认open/read/write/release被调用
dmesg | grep demo_char
通过条件:cat输出与写入内容一致;dmesg中依次出现open() called、wrote 12 bytes、read 12 bytes、release() called日志。
卸载驱动
adb shell su -c "rmmod basic_char_dev"
卸载后/dev/demo_char消失,dmesg出现exited日志。
常见问题
- insmod失败提示
No such device:确认内核开启了devtmpfs,或手动执行mknod /dev/demo_char c 主设备号 0。 - 写入后读取为空:检查是否在写入前执行过
cat,因为read使用file->f_pos偏移,读取后偏移到达末尾,需重新打开设备再读。 - 权限拒绝:确认设备节点权限,可执行
chmod 666 /dev/demo_char。