简介:《Android编写驱动程序案例》压缩包面向具备一定Linux内核基础的Android开发者,以freg字符设备驱动为完整实例,展示从设备节点创建、数据读写操作到上层应用调用的驱动开发主线,帮助读者理解用户空间与内核空间驱动的交互机制,并了解ko模块的编译与加载方式。资源共11个文件,核心包含驱动源码与头文件(freg.c、freg.h)、编译生成的目标文件(.o)、Android.mk、Kconfig与Makefile等构建脚本,以及配套的freg用户态操作程序;通过modules.order还能理清驱动模块的编译组织关系,整体仅12KB,源码规模精简,适合逐行拆解与对照学习。已有865人学习下载。这份资料把内核驱动框架落到最小可读的代码集合中,既能解析freg驱动的实现细节,也能参照Makefile与Kconfig自行搭建Android内核驱动的编译环境,对想亲手编写驱动、建立内核编程实战认知的开发者有直接参考价值。
1. 为什么写一个驱动程序案例:用户态写好了,内核态才是分水岭
做 Android 开发越久越会发现一个尴尬的事实:很多人能熟练写 App、调接口,但一提到「写驱动程序」,第一反应是回避。原因不复杂——应用层代码跑在用户态,出了问题看日志还能猜个大概;驱动跑在内核态,一旦崩溃就是整个系统重启,日志还经常看不太懂。但反过来看,毕设和课设里一旦出现「驱动」两个字,题目含金量立刻就上去了。这套 Android 编写驱动程序案例,目标就是把从内核模块到 App 调用的完整链路讲清楚,让一个只写过 Java/Kotlin 的同学也能照着把驱动写出来、跑起来、并且能现场演示。适合正在做系统方向毕设的学生,也适合需要快速交付一个带底层模块方案的在职人员。
2. 总体架构设计:三层链路与驱动模块的职责边界
2.1 从内核到应用的完整链路:驱动、HAL、JNI、App 各层干什么
先解决一个认知问题:在 Android 里写驱动,绝不是在某个.c文件里写完字符设备逻辑就结束了。驱动代码编译进内核之后,还要经过硬件抽象层封装、JNI 桥接,最终被 App 调用,这是一条完整的三层链路。
链路的最底层是内核驱动模块,它负责和真实的硬件或者虚拟硬件打交道,向上暴露一个设备节点。我一般把驱动设计成字符设备驱动,在/dev下创建一个节点,App 侧对这个节点执行 open、read、write、ioctl 操作。内核驱动不做业务逻辑,它的职责是数据收发和控制命令解析。驱动之上是 HAL 层,也就是 hardware abstraction layer,这一层以动态库的形式存在,将驱动设备节点封装成一组 C 接口,向上层提供driver_open、driver_ioctl、driver_close这类函数。之所以在 JNI 之前插一层 HAL,是因为 Android 系统的官方架构要求硬件访问统一收敛到 HAL,将来换真实硬件时只需要替换 HAL 的实现,上层不需要动。
再往上就是 JNI 层,它把 HAL 的 C 接口暴露给 Java 层。JNI 代码里要做的事情并不复杂:加载 HAL 动态库,把 Java 的 native 方法和 C 函数绑定,然后逐层向上传递参数。最上层的 App 只是一个验证工具,提供几个按钮,分别触发驱动打开、发送命令、读取数据。整个链路里,驱动是心脏,HAL 和 JNI 是血管,App 是人机交互界面。缺任何一段,演示都跑不通。
2.2 技术选型与文件清单:miscdevice、ioctl 与权限模型
驱动本身的实现方式,我建议直接用 miscdevice 框架,而不是单独注册字符设备。原因是 miscdevice 会自动分配主设备号,并且会在/dev下自动创建设备节点,省去了手动mknod的麻烦,这对做一个演示项目来说能省掉大量调试时间。通信方式上,read/write 用来传输连续的数据流,ioctl 用来下发控制命令,比如设置驱动的工作模式、读取驱动内部状态。驱动里维护一个简单的内核缓冲区,App 端写进去的数据驱动能做简单处理并返回结果。
权限模型是另一个需要提前想清楚的点。驱动设备节点创建后,默认权限通常是660,属主是 root,App 普通进程根本打不开。在调试阶段,我会直接在驱动初始化里把设备节点的权限设为0666,也就是任何人可读写,同时把 SELinux 设置为 permissive 模式。这个做法在正式产品上不可取,但对一个课程设计或毕设来说是合理的取舍,因为你要在有限时间内把功能跑通。如果项目要求严格,再补 SELinux 策略,具体写法在第 5 章会给出。
方案的完整文件清单并不复杂,每一个文件都有明确的职责:
| 文件 | 所属部分 | 作用 |
|---|---|---|
virt_drv.c | 内核驱动 | miscdevice 注册、file_operations 实现、ioctl 命令处理 |
virt_drv.h | 内核驱动 | ioctl 命令号定义、数据结构定义,用户态和内核态共用 |
virt_hal.c | HAL 层 | 包装 open、ioctl、read、write 操作,向上提供稳定接口 |
virt_hal.h | HAL 层 | HAL 接口声明,JNI 层按此头文件链接 |
com_demo_driver_DriverManager.c | JNI 层 | Java native 方法到 HAL 函数的映射 |
DriverManager.java | App 层 | Java 侧封装,提供按钮事件调用 native 方法 |
virt_drv.te | SELinux 策略 | 允许特定进程访问设备节点的规则 |
Android.bp | 构建脚本 | HAL 与 JNI 库的编译配置 |
2.3 目录结构建议:内核源码树、HAL 源码树与 App 模块的划分
目录结构直接决定编译是否顺利。很多人在内核编译阶段翻车,就是源代码放错了位置,导致 Kconfig 和构建系统扫描不到。内核驱动部分,我建议放在内核源码树的drivers/misc/下面单独建一个目录,比如drivers/misc/virt_drv/,然后在drivers/misc/Kconfig中新增一个配置项。
kernel/ drivers/ misc/ virt_drv/ virt_drv.c virt_drv.h Kconfig device/ demo/ virt_drv/ hal/ virt_hal.c virt_hal.h Android.bp jni/ com_demo_driver_DriverManager.c Android.bp app/ DriverManager.java DriverApi.javaHAL 和 JNI 放在device/目录下,通过 Android.bp 编译成动态库。App 部分单独一个目录,作为独立的 Android 项目打开。这样划分的好处有两个:第一,内核代码和用户态代码物理隔离,避免编译时头文件互相污染;第二,评审老师问你工程结构时,你可以直接按「内核、HAL、JNI、App」四层讲清楚,逻辑上非常顺畅。
3. 核心实现:从字符设备到 JNI 通道的完整编码
3.1 内核驱动模块:file_operations 与 ioctl 命令号的定义
驱动主体代码是所有实现的基础。先看头文件,它定义了 ioctl 命令号和驱动能识别的数据结构。命令号必须用内核提供的_IO、_IOW宏来生成,不能自己随便编一个数字。这些宏会把魔数、序列号、参数大小编码进一个整数里,用户态和内核态用同一套定义,这样双方不会对不上命令。
#ifndef _VIRT_DRV_H_ #define _VIRT_DRV_H_ #include <linux/ioctl.h> #include <linux/types.h> #define VIRT_DRV_MAGIC 0xD1 #define VIRT_DRV_CMD_GET_STATE _IOR(VIRT_DRV_MAGIC, 1, int) #define VIRT_DRV_CMD_SET_MODE _IOW(VIRT_DRV_MAGIC, 2, int) #define VIRT_DRV_CMD_RESET _IO(VIRT_DRV_MAGIC, 3) #define VIRT_DRV_MODE_A 0 /* 模式 A:回显模式 */ #define VIRT_DRV_MODE_B 1 /* 模式 B:统计模式 */ #endif_IOR表示该命令需要从内核读数据出来,_IOW表示需要把用户态数据写进内核,_IO则是无数据传输的命令。MAGIC取一个不和其他驱动冲突的字节值,序列号在同一 magic 下唯一。这样设计的好处是 ioctl 命令不会产生歧义,调试时也可以直接按序号排查。
驱动主体文件里,最关键的部分是file_operations结构体和 miscdevice 注册。
#include <linux/module.h> #include <linux/miscdevice.h> #include <linux/fs.h> #include <linux/uaccess.h> #include <linux/mutex.h> #include "virt_drv.h" static int virt_drv_open(struct inode *inode, struct file *filp); static int virt_drv_release(struct inode *inode, struct file *filp); static long virt_drv_ioctl(struct file *filp, unsigned int cmd, unsigned long arg); static ssize_t virt_drv_read(struct file *filp, char __user *buf, size_t size, loff_t *off); static ssize_t virt_drv_write(struct file *filp, const char __user *buf, size_t size, loff_t *off); static int g_mode = VIRT_DRV_MODE_A; /* 当前模式 */ static unsigned long g_write_count = 0; /* 写入次数统计 */ static DEFINE_MUTEX(virt_drv_lock); /* 并发保护 */ static const struct file_operations virt_drv_fops = { .owner = THIS_MODULE, .open = virt_drv_open, .release = virt_drv_release, .unlocked_ioctl = virt_drv_ioctl, .read = virt_drv_read, .write = virt_drv_write, }; static struct miscdevice virt_drv_misc = { .minor = MISC_DYNAMIC_MINOR, /* 动态分配次设备号 */ .name = "virt_drv", /* 设备节点名 /dev/virt_drv */ .fops = &virt_drv_fops, }; static int virt_drv_open(struct inode *inode, struct file *filp) { /* 打开设备时可以做初始化,这里仅记录打开事件 */ return 0; } static int virt_drv_release(struct inode *inode, struct file *filp) { return 0; } static long virt_drv_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) { int value; int ret = 0; /* 对用户传入的参数做合法性检查 */ switch (cmd) { case VIRT_DRV_CMD_SET_MODE: if (copy_from_user(&value, (void __user *)arg, sizeof(value))) return -EFAULT; if (value != VIRT_DRV_MODE_A && value != VIRT_DRV_MODE_B) return -EINVAL; mutex_lock(&virt_drv_lock); g_mode = value; mutex_unlock(&virt_drv_lock); break; case VIRT_DRV_CMD_GET_STATE: value = g_mode; if (copy_to_user((void __user *)arg, &value, sizeof(value))) return -EFAULT; break; case VIRT_DRV_CMD_RESET: mutex_lock(&virt_drv_lock); g_write_count = 0; g_mode = VIRT_DRV_MODE_A; mutex_unlock(&virt_drv_lock); break; default: return -EINVAL; } return ret; } static ssize_t virt_drv_read(struct file *filp, char __user *buf, size_t size, loff_t *off) { char tmp[32]; int len; mutex_lock(&virt_drv_lock); /* 按当前模式返回不同内容 */ if (g_mode == VIRT_DRV_MODE_B) len = snprintf(tmp, sizeof(tmp), "count=%lu\n", g_write_count); else len = snprintf(tmp, sizeof(tmp), "mode=%d\n", g_mode); mutex_unlock(&virt_drv_lock); if (*off >= len) return 0; if (copy_to_user(buf, tmp, len)) return -EFAULT; *off += len; return len; } static ssize_t virt_drv_write(struct file *filp, const char __user *buf, size_t size, loff_t *off) { char kbuf[128]; if (size <= 0 || size >= sizeof(kbuf)) return -EINVAL; if (copy_from_user(kbuf, buf, size)) return -EFAULT; kbuf[size] = '\0'; mutex_lock(&virt_drv_lock); g_write_count++; mutex_unlock(&virt_drv_lock); return size; } static int __init virt_drv_init(void) { int ret = misc_register(&virt_drv_misc); if (ret) { pr_err("virt_drv: register failed, ret=%d\n", ret); return ret; } pr_info("virt_drv: registered, node=/dev/virt_drv\n"); return 0; } static void __exit virt_drv_exit(void) { misc_deregister(&virt_drv_misc); pr_info("virt_drv: unregistered\n"); } module_init(virt_drv_init); module_exit(virt_drv_exit); MODULE_LICENSE("GPL");这段代码里有几个设计点需要关注。unlocked_ioctl是 2.6.36 之后内核的标准 ioctl 入口,老代码里写ioctl字段在新内核上无法编译。MISC_DYNAMIC_MINOR让驱动不需要关心次设备号分配,设备节点名virt_drv会直接生成/dev/virt_drv。copy_from_user和copy_to_user是用户态数据访问的唯一正确方式,绝不能直接解引用用户指针,否则会触发内核崩溃,这是整个驱动里最容易出安全问题的地方。
3.2 关键实现:驱动如何安全地在用户态与内核态之间传数据
驱动里面向用户态的数据传递,本质上是「信任边界」的问题。用户态传来的指针可能是空指针、野指针、或者指向内核地址空间,所以必须用copy_to_user/copy_from_user做一次完整拷贝。我在上面代码中做了三层防护:第一层,ioctl 参数在进入 switch 前不做假设,每个 case 内部自己对 arg 做合法性判断;第二层,写入操作限制size不能超过内核缓冲区长度的 128 字节,防止栈缓冲区溢出;第三层,所有共享变量在读写时都加互斥锁,避免两个进程同时打开设备节点导致数据错乱。
内核驱动还有一个隐藏问题:如果不加锁,A 进程在写g_write_count的同时 B 进程在读,可能读到中间状态。这个案例里锁的开销微不足道,但在真实驱动里这是必修课。值得强调的是,copy_from_user之后要立即把数据拷贝进内核缓冲区再做业务处理,不要保留对用户指针的引用,因为用户态内存有可能在系统调用返回后被释放,形成悬垂指针。
3.3 用户态封装:HAL 接口与 JNI 桥接层的编码
HAL 层做的事情很纯粹:把 open、close、ioctl、read、write 这几个系统调用封装成语义更明确的接口。HAL 库的名称建议叫virt_drv.default,这是 Android 硬件模块命名惯例。
/* virt_hal.h */ #ifndef VIRT_HAL_H #define VIRT_HAL_H int virt_driver_open(void); int virt_driver_ioctl(int cmd, int *arg); int virt_driver_read(char *buf, size_t size); int virt_driver_write(const char *buf, size_t size); void virt_driver_close(void); #endif/* virt_hal.c */ #include <fcntl.h> #include <unistd.h> #include <sys/ioctl.h> #include <string.h> #include <stdio.h> #include "virt_hal.h" #include "virt_drv.h" static int g_fd = -1; int virt_driver_open(void) { if (g_fd >= 0) return 0; g_fd = open("/dev/virt_drv", O_RDWR); return g_fd < 0 ? -1 : 0; } int virt_driver_ioctl(int cmd, int *arg) { if (g_fd < 0) return -1; return ioctl(g_fd, cmd, arg); } int virt_driver_read(char *buf, size_t size) { if (g_fd < 0) return -1; return read(g_fd, buf, size); } int virt_driver_write(const char *buf, size_t size) { if (g_fd < 0) return -1; return write(g_fd, buf, size); } void virt_driver_close(void) { if (g_fd >= 0) { close(g_fd); g_fd = -1; } }HAL 层完成后,JNI 层引用这个动态库。JNI 层在JNI_OnLoad里用RegisterNatives显式注册方法,而不是依赖 Java 层的 native 方法名与 C 函数名按约定自动匹配。显式注册的好处是方法名可以自由设计,不受「Java_包名_类名_方法名」的长名字约束,而且注册失败时会有明确的错误日志。
/* com_demo_driver_DriverManager.c */ #include <jni.h> #include <string.h> #include "virt_hal.h" static jint nativeOpen(JNIEnv *env, jobject thiz) { return virt_driver_open(); } static jint nativeSetMode(JNIEnv *env, jobject thiz, jint mode) { int arg = mode; return virt_driver_ioctl(VIRT_DRV_CMD_SET_MODE, &arg); } static jint nativeGetState(JNIEnv *env, jobject thiz) { int arg = 0; if (virt_driver_ioctl(VIRT_DRV_CMD_GET_STATE, &arg) != 0) return -1; return arg; } static jstring nativeRead(JNIEnv *env, jobject thiz) { char buf[64] = {0}; int len = virt_driver_read(buf, sizeof(buf) - 1); if (len <= 0) return (*env)->NewStringUTF(env, ""); return (*env)->NewStringUTF(env, buf); } static const JNINativeMethod g_methods[] = { {"nativeOpen", "()I", (void *)nativeOpen}, {"nativeSetMode", "(I)I", (void *)nativeSetMode}, {"nativeGetState", "()I", (void *)nativeGetState}, {"nativeRead", "()Ljava/lang/String;", (void *)nativeRead}, }; jint JNI_OnLoad(JavaVM *vm, void *reserved) { JNIEnv *env = NULL; jclass cls; if ((*vm)->GetEnv(vm, (void **)&env, JNI_VERSION_1_6) != JNI_OK) return JNI_ERR; cls = (*env)->FindClass(env, "com/demo/driver/DriverManager"); if (cls == NULL) return JNI_ERR; if ((*env)->RegisterNatives(env, cls, g_methods, sizeof(g_methods) / sizeof(g_methods[0])) != 0) return JNI_ERR; return JNI_VERSION_1_6; }Java 侧的DriverManager只需要声明这几个 native 方法,不需要任何实现逻辑:
package com.demo.driver; public class DriverManager { static { System.loadLibrary("virt_drv_jni"); } public static native int nativeOpen(); public static native int nativeSetMode(int mode); public static native int nativeGetState(); public static native String nativeRead(); }JNI 方法签名是整个链路中最容易出错的地方。比如nativeRead返回的是String,签名必须写成()Ljava/lang/String;,分号不能丢;nativeSetMode接收一个int,签名是(I)I。如果签名写错,运行时直接抛UnsatisfiedLinkError,而且日志里不容易看出是签名问题。
4. 常见问题与排查:SELinux、权限与编译翻车的五个高频点
4.1 内核模块加载失败:格式错、符号错和设备号冲突
现象:编译生成.ko文件后,执行insmod virt_drv.ko报错Invalid module format或者Unknown symbol。原因往往是编译内核模块用的内核源码版本与当前运行的内核版本不一致,或者模块编译时没有开启相同的配置选项。解决方法是确认编译环境:用uname -r查看当前内核版本,编译模块时通过make M=drivers/misc/virt_drv/指定目标目录,而不是手工执行gcc编译。另一个翻车点是模块之间符号依赖顺序,如果当前驱动引用了其他模块的导出符号,需要先加载被依赖的模块。
4.2 App 打开设备节点报 Permission denied
现象:App 里调用nativeOpen()后返回-1,通过adb shell手动执行cat /dev/virt_drv提示Permission denied。原因有两个方向:设备节点权限不足,或者应用进程不在设备节点的属组内。调试阶段的解决方法是直接在驱动里设置节点权限:
static struct miscdevice virt_drv_misc = { ... .mode = 0666, /* 让所有进程都可读写,仅限调试 */ };这样/dev/virt_drv创建时权限就是crw-rw-rw-,任何进程都能 open。如果是已经编译好的内核不方便改代码,也可以通过adb shell chmod 666 /dev/virt_drv临时调整,但要记住重启后权限会失效。
4.3 SELinux 拦截:avc: denied 日志怎么读
现象:设备节点权限已经 666,App 调用还是返回Permission denied,dmesg里能看到类似下面这样的日志:
avc: denied { open } for pid=1234 comm="demo.app" name="virt_drv" dev="tmpfs" scontext=u:r:untrusted_app:s0:c512,c768 tcontext=u:object_r:device:s0 tclass=chr_file permissive=0这段日志是完整的线索:scontext是发起访问的进程域,tcontext是设备节点所在的域,tclass是对象类型,这里表示 App 进程untrusted_app没有权限打开device类型的字符设备。解决方法是写一条 SELinux 策略,允许untrusted_app域对该类型设备进行 open 和 ioctl 操作:
type virt_drv_device, dev_type; allow untrusted_app virt_drv_device:chr_file { open read write ioctl };将上述规则写入virt_drv.te,并在device/demo/virt_drv/下建立一个sepolicy目录引用它。很多人在这一步被卡了两三天,其实只要理解了avc denied的格式,问题就解决了一半。
4.4 JNI 找不到 native 方法或 so 加载失败
现象:App 启动时抛出java.lang.UnsatisfiedLinkError: No implementation found for void com.demo.driver.DriverManager.nativeOpen()I。原因通常是两个:JNI 动态库没有被打包进系统镜像,或者RegisterNatives注册的类名与 Java 层的完整包名不一致。检查思路是先确认动态库是否真的存在于/system/lib64/下,再确认FindClass使用的是全限定名,包名分隔符是/而不是.。另外,Android 10 及以上系统默认只允许加载public.libraries.txt中列出的库,如果 JNI 库是系统自编译路径下的,需要在构建配置中显式添加vendor_available: true。
4.5 ioctl 返回 -1 但驱动看起来正常
现象:nativeSetMode调用返回 -1,errno是EINVAL(无效参数)。原因多半是内核态和用户态的命令号定义不一致。比如用户态头文件里VIRT_DRV_CMD_SET_MODE用的是_IOW,内核态却用了_IO,两者的编码结果完全不同。解决方法很机械但有效:让 HAL 层直接包含内核源码目录下的virt_drv.h,或者把命令号定义单独抽到一个公共头文件中,两个工程都引用它。如果还是对不上,可以在驱动 ioctl 入口临时打印cmd值,和用户态计算出的值做比对,一眼就能看出是哪一边的问题。
5. 落地集成:把驱动模块装进系统镜像并验证功能
5.1 内核配置与编译:Kconfig、defconfig 和模块打包
驱动代码写完了,接下来是把它编进内核。在drivers/misc/virt_drv/下放一个 Kconfig,让内核配置界面能识别这个驱动:
config VIRT_DRV tristate "Demo virtual driver" default y help A simple char device driver for demo. Say Y to build it into kernel, M to build as module.然后在drivers/misc/Kconfig里加一行source "drivers/misc/virt_drv/Kconfig",这样make menuconfig时就能看到。终端编译时有两个选择:直接编进内核镜像,或者编成模块。推荐编成模块.ko,因为出问题时可以单独卸载重装,不用反复刷整个 boot 分区。
CONFIG_VIRT_DRV=m在 kernel 源码根目录执行编译:
make ARCH=arm64 virt_drv.ko编译产出的.ko文件需要手工 push 到设备的/data/local/tmp/目录,然后执行insmod /data/local/tmp/virt_drv.ko,再用dmesg | grep virt_drv看到注册日志。如果项目的设备有其他开发板,也可以直接把整个 boot.img 重编后烧录,那是另一套流程,但调试驱动阶段强烈建议先用模块方式,速度更快、风险更低。
5.2 SELinux 策略与设备节点权限:让 App 能真正访问驱动
如果不想把系统 SELinux 全局切成 permissive(评审时可能会被问为什么),就必须写策略。前面第 4.3 节已经给了一条最简策略,但实际项目里你还要解决设备节点的标签问题。设备节点创建后默认标签是u:object_r:device:s0,类型是device,这表示系统把它当作通用设备。要让自己的策略生效,需要通过restorecon或者初始化脚本给节点打上自定义标签,否则哪怕写了 allow 规则也不生效。
完整做法是三步:第一步,定义自己的设备类型;第二步,在file_contexts中给/dev/virt_drv配置标签;第三步,编写 allow 规则。file_contexts中这样加一行:
/dev/virt_drv u:object_r:virt_drv_device:s0这样节点创建后会被自动打上virt_drv_device类型,之前的 allow 规则就能精确命中。切忌直接用untrusted_app放行整个device类型,那样等于对所有/dev下的节点开放权限,过于宽松,答辩的时候容易被追问。
5.3 功能验证清单:从内核日志到 App 端到端的完整测试
集成完毕之后,不要急着打开 App 点按钮,按清单逐项验证,才能在答辩或交付时拿出可信的结论。
| 步骤 | 命令或操作 | 预期结果 |
|---|---|---|
| 检查驱动是否加载 | lsmod | grep virt_drv | 能看到模块记录 |
| 检查内核日志 | dmesg | grep virt_drv | 输出 registered 信息 |
| 检查设备节点 | ls -l /dev/virt_drv | 权限 666,主次设备号有效 |
| 内核态读写验证 | cat /dev/virt_drv | 能读到 mode=0 或其他状态文本 |
| App 层打开 | 点击 App 的 Open 按钮 | logcat 打印 open success |
| App 层命令下发 | 点击 SetMode 按钮 | 再次 cat 设备节点,输出变化 |
| 卸载并重新加载 | rmmod virt_drv后insmod | 设备节点重建,功能不受影响 |
最后一行的卸载重载测试特别重要。驱动开发里最容易犯的错是卸载后设备节点残留、或者内存没有释放干净,导致第二次加载时设备号不匹配。我的习惯是每完成一个功能点,就做一次完整的装、卸、再装循环,确认模块的生命周期管理没有问题。
6. 项目验收与答辩:用一张检查表证明你的驱动可用
6.1 验收标准:把「能跑」说清楚
驱动演示类项目最忌讳只说「我的驱动能跑」。评审老师想看的是可量化的结论。我一般准备三张检查表。功能验收表覆盖驱动注册、设备节点创建、open/ioctl 调用结果;稳定性验收表记录连续 1000 次 ioctl 调用无失败、无人为触发内核 panic;安全性验收表证明非法参数传入时驱动返回-EINVAL而不是系统重启。每张表附上测试命令和对应的日志截图,这样答辩时不用临时演示,一切都有据可查。
6.2 答辩话术:评审问你这四个问题怎么答
答辩时最可能被问到的问题其实很集中,提前准备好就不用临场发挥。第一个问题:为什么用 ioctl 而不是 sysfs 传数据?答:ioctl 适合不固定格式的命令交互,sysfs 适合简单的单值读写,驱动命令需要带参数和返回值,ioctl 更直接。第二个问题:内核态和用户态缓冲区怎么保证安全?答:所有跨边界数据一律走copy_to_user和copy_from_user,并且对长度做了上限校验,杜绝直接解引用用户指针。第三个问题:并发访问怎么办?答:用 mutex 保护共享变量,在 ioctl 的写路径和 read 路径上加了互斥,保证同一时刻只有一个进程在改状态。第四个问题:驱动卸载后设备节点去哪了?答:miscdevice 框架在misc_deregister时自动删除节点,驱动卸载后/dev/virt_drv会消失,下次加载时重新创建。
设计这套方案的时候,我自己第一次调驱动的翻车经历至今都记得很清楚:花了两个小时检查代码,最后发现 SELinux 日志里早就写了答案,只是没有去读。从那以后,我每次做完驱动都会强制走一遍「读 dmesg、看 avc 日志、再改代码」的流程,再没有出现过那种被黑匣子支配的焦虑。一个完整的驱动案例,能让你在交付和答辩时不心虚,希望帮到你。
本文还有配套的精品资源,点击获取