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"

验证方法

  1. 设备节点检查:加载后执行ls -l /dev/demo_char,应显示字符设备且主设备号为动态分配值(如c 240:0)。
  2. 写读回环测试:执行以下命令验证数据通路:
# 写入字符串
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。
用户态 (User Space) echo "hello" > /dev/demo_char cat /dev/demo_char /dev/demo_char (设备节点) VFS (虚拟文件系统) 内核态 (Kernel Space) struct file_operations .open/.read/.write/.release kernel_buf[128] (kmalloc 缓冲区) major_num + class_create device_create → /dev 节点 数据流方向 write() 路径 read() 路径 设备节点创建