3. 字符设备驱动基础
任务目标
在Android内核(Linux内核)环境中实现一个最小字符设备驱动:动态注册主设备号、创建device class与device节点,并实现open、read、write、release四个基本文件操作。通过用户态程序验证设备节点的读写功能。
环境与依赖
- Android内核源码树(或独立Linux内核源码树),已配置交叉编译工具链
- 内核版本:建议5.10+(适配Android GKI或AOSP common kernel)
- 目标设备已解锁并支持内核模块加载(或编译进内核)
- 用户态验证工具:adb shell或串口终端
驱动实现
创建设备节点路径为/dev/char_demo。write操作将用户数据暂存于内核缓冲区(最大4KB),read操作将缓冲区数据拷贝回用户态,open/release仅维护访问计数。
```c
#include
#include
#include
#include
#include
#include
#define DEVICE_NAME "char_demo"
#define CLASS_NAME "demo_class"
#define BUF_SIZE 4096
static int major_num;
static struct class *demo_class;
static struct device *demo_device;
static char *kernel_buf;
static int buf_len;
static int dev_open(struct inode *inode, struct file *file)
{
return 0;
}
static ssize_t dev_read(struct file *file, char __user *buf,
size_t count, loff_t *offset)
{
int ret;
if (buf_len == 0)
return 0;
if (count > buf_len)
count = buf_len;
ret = copy_to_user(buf, kernel_buf, count);
if (ret)
return -EFAULT;
buf_len = 0;
return count;
}
static ssize_t dev_write(struct file *file, const char __user *buf,
size_t count, loff_t *offset)
{
int ret;
if (count > BUF_SIZE)
return -ENOMEM;
ret = copy_from_user(kernel_buf, buf, count);
if (ret)
return -EFAULT;
buf_len = count;
return count;
}
static int dev_release(struct inode *inode, struct file *file)
{
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 char_demo_init(void)
{
major_num = register_chrdev(0, DEVICE_NAME, &fops);
if (major_num < 0) {
pr_err("Failed to register device\n");
return major_num;
}
demo_class = class_create(THIS_MODULE, CLASS_NAME);
if (IS_ERR(demo_class)) {
unregister_chrdev(major_num, DEVICE_NAME);
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);
return PTR_ERR(demo_device);
}
kernel_buf = kzalloc(BUF_SIZE, GFP_KERNEL);
if (!kernel_buf) {
device_destroy(demo_class, MKDEV(major_num, 0));
class_destroy(demo_class);
unregister_chrdev(major_num, DEVICE_NAME);
return -ENOMEM;
}
buf_len = 0;
pr_info("char_demo: initialized, major=%d\n", major_num);
return 0;
}
static void __exit char_demo_exit(void)
{
kfree(kernel_buf);
device_destroy(demo_class, MKDEV(major_num, 0));
class_destroy(demo_class);
unregister_chrdev(major_num, DEVICE_NAME);
pr_info("char_demo: exited\n");
}
module_init(char_demo_init);
module_exit(char_demo_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Kernel Dev");
MODULE_DESCRIPTION("Simple character device driver demo");
MODULE_VERSION("1.0");
```
编译与加载
将上述代码保存为char_demo.c,内核源码树内创建Kbuild文件:
```makefile
obj-m += char_demo.o
```
执行编译(以AOSP common kernel为例):
```bash
export ARCH=arm64
export CROSS_COMPILE=aarch64-linux-android-
make defconfig # 或使用目标设备的现有配置
make M=$PWD modules
```
将生成的char_demo.ko推送至设备并加载:
```bash
adb push char_demo.ko /data/local/tmp/
adb shell su -c "insmod /data/local/tmp/char_demo.ko"
```
加载后检查设备节点是否生成:
```bash
adb shell ls -l /dev/char_demo
```
用户态验证
编写最小测试程序test_demo.c:
```c
#include
#include
#include
#include
int main(void)
{
int fd;
char wbuf[] = "hello kernel";
char rbuf[64] = {0};
int ret;
fd = open("/dev/char_demo", O_RDWR);
if (fd < 0) {
perror("open");
return 1;
}
ret = write(fd, wbuf, strlen(wbuf) + 1);
printf("write ret = %d\n", ret);
ret = read(fd, rbuf, sizeof(rbuf));
printf("read ret = %d, data = %s\n", ret, rbuf);
close(fd);
return 0;
}
```
编译并运行:
```bash
# 使用Android NDK交叉编译,或直接在设备上使用toybox cc
adb push test_demo.c /data/local/tmp/
adb shell su -c "cd /data/local/tmp && cc test_demo.c -o test_demo && ./test_demo"
```
验证判定标准
insmod无报错,dmesg可见char_demo: initialized日志
/dev/char_demo节点存在且权限可读写
- 测试程序输出
write ret = 13(含结尾\0),read ret = 13, data = hello kernel,与代码逻辑一致
- 卸载模块后节点消失,
dmesg可见退出日志
常见问题
- 设备节点未生成:确认CONFIG_DEVTMPFS已启用,且udev或vold未拦截设备创建
- insmod失败提示
Invalid module format:内核版本与模块编译版本不匹配,需使用目标设备对应内核源码编译
- read返回0:驱动设计为单次读取后清空缓冲区,需先write再read