動(dòng)開發(fā)入門:從Linux內(nèi)核模塊到字符設(shè)備驅(qū)動(dòng)實(shí)戰(zhàn))
1. 項(xiàng)目概述為什么驅(qū)動(dòng)開發(fā)是Android底層的基石如果你是一名Android應(yīng)用開發(fā)者可能每天都在和Activity、Fragment、Service打交道享受著Android SDK提供的豐富API。但你是否想過當(dāng)你點(diǎn)擊屏幕、連接藍(lán)牙耳機(jī)、拍攝照片時(shí)這些硬件操作是如何最終被你的Java或Kotlin代碼感知并處理的這背后正是驅(qū)動(dòng)在默默工作。驅(qū)動(dòng)全稱設(shè)備驅(qū)動(dòng)程序是操作系統(tǒng)內(nèi)核中用來管理和控制特定硬件設(shè)備的軟件模塊。它充當(dāng)了硬件與上層操作系統(tǒng)乃至應(yīng)用程序之間的“翻譯官”和“協(xié)調(diào)員”。在Android系統(tǒng)中由于其基于Linux內(nèi)核驅(qū)動(dòng)開發(fā)本質(zhì)上就是Linux內(nèi)核驅(qū)動(dòng)開發(fā)。這意味著你需要深入Linux內(nèi)核的編程模型、內(nèi)存管理、中斷處理等底層機(jī)制。對(duì)于想深入Android系統(tǒng)定制、ROM移植、性能優(yōu)化或是從事車載系統(tǒng)、物聯(lián)網(wǎng)設(shè)備等嵌入式Android開發(fā)的工程師來說驅(qū)動(dòng)開發(fā)是必須跨越的一道門檻。它讓你從“使用系統(tǒng)”的人變成“理解并塑造系統(tǒng)”的人。學(xué)習(xí)驅(qū)動(dòng)開發(fā)不僅能讓你在遇到諸如“某個(gè)傳感器不工作”、“新硬件無法識(shí)別”等棘手問題時(shí)有能力從根源上分析和解決更能極大地提升你對(duì)計(jì)算機(jī)系統(tǒng)整體運(yùn)作的理解深度。2. 驅(qū)動(dòng)開發(fā)核心概念與Linux內(nèi)核模塊2.1 驅(qū)動(dòng)在Android/Linux中的位置與角色要理解驅(qū)動(dòng)首先要明白它在整個(gè)軟件棧中的位置。一個(gè)簡化的Android系統(tǒng)層次結(jié)構(gòu)自上而下通常是應(yīng)用程序App - 應(yīng)用框架Framework - 本地庫Native Libraries/ Android運(yùn)行時(shí)ART - 硬件抽象層HAL - Linux內(nèi)核含驅(qū)動(dòng) - 硬件。驅(qū)動(dòng)位于Linux內(nèi)核空間直接與硬件寄存器打交道執(zhí)行最底層的讀寫操作。而應(yīng)用程序運(yùn)行在用戶空間出于安全和穩(wěn)定性考慮用戶空間的程序不能直接訪問硬件或內(nèi)核內(nèi)存。因此驅(qū)動(dòng)需要提供一套標(biāo)準(zhǔn)的接口給用戶空間這套接口在Linux中表現(xiàn)為“設(shè)備文件”。用戶空間的程序通過標(biāo)準(zhǔn)的文件操作如open,read,write,ioctl,close來與驅(qū)動(dòng)交互驅(qū)動(dòng)將這些操作“翻譯”成具體的硬件控制命令。在Android中HAL層進(jìn)一步封裝了內(nèi)核驅(qū)動(dòng)的接口為Framework提供統(tǒng)一的硬件服務(wù)API這使得更換底層硬件或驅(qū)動(dòng)時(shí)上層框架代碼無需改動(dòng)。2.2 內(nèi)核模塊驅(qū)動(dòng)的載體在Linux中驅(qū)動(dòng)通常以內(nèi)核模塊的形式存在。內(nèi)核模塊是一種可以在系統(tǒng)運(yùn)行時(shí)動(dòng)態(tài)加載到內(nèi)核或從內(nèi)核卸載的代碼這帶來了極大的靈活性。你不需要為了添加一個(gè)驅(qū)動(dòng)而重新編譯整個(gè)內(nèi)核。一個(gè)最簡單的內(nèi)核模塊“Hello World”代碼如下所示假設(shè)文件名為hello.c#include linux/init.h #include linux/module.h #include linux/kernel.h // 模塊加載函數(shù) static int __init hello_init(void) { printk(KERN_INFO Hello, Android Driver World!\n); return 0; // 返回0表示成功 } // 模塊卸載函數(shù) static void __exit hello_exit(void) { printk(KERN_INFO Goodbye, Android Driver World.\n); } // 注冊(cè)模塊的加載和卸載函數(shù) module_init(hello_init); module_exit(hello_exit); // 模塊聲明信息 MODULE_LICENSE(GPL); // 許可證必須聲明如GPL MODULE_AUTHOR(Your Name); MODULE_DESCRIPTION(A simple hello world driver module); MODULE_VERSION(1.0);編寫好代碼后你需要一個(gè)Makefile來告訴內(nèi)核構(gòu)建系統(tǒng)如何編譯它# 指向你當(dāng)前Linux內(nèi)核的構(gòu)建目錄 KDIR : /lib/modules/$(shell uname -r)/build # 當(dāng)前模塊源碼目錄 PWD : $(shell pwd) obj-m hello.o all: $(MAKE) -C $(KDIR) M$(PWD) modules clean: $(MAKE) -C $(KDIR) M$(PWD) clean編譯命令是make生成hello.ko文件。加載模塊使用sudo insmod hello.ko查看內(nèi)核日志使用dmesg | tail你應(yīng)該能看到輸出的“Hello”信息。卸載模塊使用sudo rmmod hello。注意內(nèi)核編程與用戶空間編程有巨大差異。沒有熟悉的C庫如printf要用printk錯(cuò)誤可能導(dǎo)致內(nèi)核崩潰Oops或Panic內(nèi)存管理需要格外小心使用kmalloc,kfree等。你的代碼運(yùn)行在最高特權(quán)級(jí)一個(gè)空指針解引用就足以讓系統(tǒng)宕機(jī)。2.3 字符設(shè)備與設(shè)備文件用戶空間的交互窗口Linux將設(shè)備分為三大類字符設(shè)備、塊設(shè)備和網(wǎng)絡(luò)設(shè)備。字符設(shè)備以字節(jié)流的形式進(jìn)行順序訪問沒有緩沖區(qū)例如鍵盤、鼠標(biāo)、串口、大部分傳感器如光線、距離傳感器等。塊設(shè)備以數(shù)據(jù)塊為單位進(jìn)行隨機(jī)訪問有緩沖區(qū)例如硬盤、eMMC存儲(chǔ)等。網(wǎng)絡(luò)設(shè)備則用于網(wǎng)絡(luò)通信。對(duì)于驅(qū)動(dòng)開發(fā)者字符設(shè)備驅(qū)動(dòng)是最常見和基礎(chǔ)的入門類型。它的核心任務(wù)就是創(chuàng)建一個(gè)“設(shè)備文件”如/dev/mydevice并實(shí)現(xiàn)與該文件關(guān)聯(lián)的操作函數(shù)集合file_operations結(jié)構(gòu)體。當(dāng)用戶在用戶空間對(duì)這個(gè)設(shè)備文件執(zhí)行open、read、write等操作時(shí)內(nèi)核會(huì)調(diào)用驅(qū)動(dòng)中對(duì)應(yīng)的函數(shù)。3. 字符設(shè)備驅(qū)動(dòng)開發(fā)全流程解析3.1 驅(qū)動(dòng)開發(fā)的核心數(shù)據(jù)結(jié)構(gòu)file_operationsstruct file_operations是驅(qū)動(dòng)開發(fā)中最重要的數(shù)據(jù)結(jié)構(gòu)之一它定義了一個(gè)函數(shù)指針的集合這些指針指向你實(shí)現(xiàn)的驅(qū)動(dòng)功能函數(shù)。當(dāng)應(yīng)用程序調(diào)用系統(tǒng)調(diào)用時(shí)內(nèi)核最終會(huì)查找到這個(gè)結(jié)構(gòu)體并調(diào)用相應(yīng)的函數(shù)。一個(gè)典型的、簡化版的file_operations初始化如下static struct file_operations mydev_fops { .owner THIS_MODULE, // 防止模塊在使用中被卸載 .open mydev_open, .release mydev_close, .read mydev_read, .write mydev_write, .unlocked_ioctl mydev_ioctl, // 用于實(shí)現(xiàn)自定義命令 // 還可以實(shí)現(xiàn) .poll, .mmap 等 };你需要為這些函數(shù)指針如mydev_open,mydev_read編寫具體的實(shí)現(xiàn)。例如mydev_read的函數(shù)原型通常是ssize_t mydev_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos);其中buf是用戶空間緩沖區(qū)指針不能直接讀寫必須使用copy_to_user()或copy_from_user()函數(shù)在內(nèi)核空間和用戶空間之間安全地拷貝數(shù)據(jù)。3.2 設(shè)備號(hào)與設(shè)備注冊(cè)讓系統(tǒng)識(shí)別你的驅(qū)動(dòng)在Linux中每個(gè)設(shè)備都有一個(gè)唯一的主設(shè)備號(hào)Major Number和次設(shè)備號(hào)Minor Number。主設(shè)備號(hào)標(biāo)識(shí)設(shè)備類型即對(duì)應(yīng)哪個(gè)驅(qū)動(dòng)次設(shè)備號(hào)標(biāo)識(shí)同一驅(qū)動(dòng)下的不同設(shè)備實(shí)例。注冊(cè)字符設(shè)備有兩種主要方式靜態(tài)注冊(cè)使用register_chrdev函數(shù)。這是較老的方式它會(huì)固定分配一個(gè)主設(shè)備號(hào)或由你指定并自動(dòng)注冊(cè)0-255范圍的所有次設(shè)備號(hào)。簡單但不靈活。動(dòng)態(tài)注冊(cè)推薦使用alloc_chrdev_regioncdev_initcdev_add。這種方式由內(nèi)核動(dòng)態(tài)分配一個(gè)未使用的主設(shè)備號(hào)更安全也是現(xiàn)代驅(qū)動(dòng)的主流做法。動(dòng)態(tài)注冊(cè)的代碼流程示例dev_t devno; // 設(shè)備號(hào)包含主次設(shè)備號(hào) struct cdev my_cdev; // 字符設(shè)備結(jié)構(gòu)體 // 1. 動(dòng)態(tài)申請(qǐng)一個(gè)設(shè)備號(hào)主設(shè)備號(hào)由內(nèi)核分配次設(shè)備號(hào)從0開始數(shù)量為1 int ret alloc_chrdev_region(devno, 0, 1, my_device); major MAJOR(devno); // 提取主設(shè)備號(hào) // 2. 初始化cdev結(jié)構(gòu)體并將其與file_operations綁定 cdev_init(my_cdev, mydev_fops); my_cdev.owner THIS_MODULE; // 3. 將cdev添加到內(nèi)核系統(tǒng)中 ret cdev_add(my_cdev, devno, 1); // 4. 在/dev目錄下創(chuàng)建設(shè)備文件節(jié)點(diǎn)可以手動(dòng)mknod或通過udev自動(dòng)創(chuàng)建 // 通常配合class_create和device_create使用讓udev自動(dòng)創(chuàng)建設(shè)備節(jié)點(diǎn)。3.3 自動(dòng)創(chuàng)建設(shè)備節(jié)點(diǎn)udev與sysfs的協(xié)作手動(dòng)使用mknod命令創(chuàng)建設(shè)備文件非常不便且需要root權(quán)限?,F(xiàn)代Linux發(fā)行版通過udev用戶空間設(shè)備管理器來自動(dòng)管理/dev下的設(shè)備節(jié)點(diǎn)。驅(qū)動(dòng)只需要在sysfs一個(gè)虛擬文件系統(tǒng)反映內(nèi)核對(duì)象結(jié)構(gòu)中暴露設(shè)備信息udev就會(huì)根據(jù)規(guī)則自動(dòng)創(chuàng)建或刪除節(jié)點(diǎn)。在驅(qū)動(dòng)中通常這樣操作static struct class *my_class; static struct device *my_device; // 在模塊初始化函數(shù)中hello_init之后 my_class class_create(THIS_MODULE, mydev_class); if (IS_ERR(my_class)) { /* 錯(cuò)誤處理 */ } my_device device_create(my_class, NULL, devno, NULL, mydev); if (IS_ERR(my_device)) { /* 錯(cuò)誤處理 */ }這段代碼會(huì)在/sys/class/下創(chuàng)建一個(gè)名為mydev_class的類并在其中創(chuàng)建一個(gè)名為mydev的設(shè)備。udev會(huì)監(jiān)聽到這個(gè)事件并自動(dòng)在/dev下創(chuàng)建名為mydev的設(shè)備節(jié)點(diǎn)。卸載模塊時(shí)需要按相反順序銷毀它們device_destroy-class_destroy。3.4 一個(gè)完整的簡易字符設(shè)備驅(qū)動(dòng)框架將以上所有概念整合一個(gè)具備基本讀寫功能的字符設(shè)備驅(qū)動(dòng)骨架如下#include linux/module.h #include linux/fs.h #include linux/cdev.h #include linux/device.h #include linux/uaccess.h // for copy_to/from_user #define DEVICE_NAME mydev #define BUFFER_SIZE 1024 static int major; static struct class *my_class; static struct device *my_device; static struct cdev my_cdev; static char device_buffer[BUFFER_SIZE]; // 一個(gè)簡單的內(nèi)核緩沖區(qū) static int buffer_offset 0; static int mydev_open(struct inode *inode, struct file *filp) { printk(KERN_INFO mydev: device opened.\n); return 0; } static int mydev_close(struct inode *inode, struct file *filp) { printk(KERN_INFO mydev: device closed.\n); return 0; } static ssize_t mydev_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos) { int bytes_to_read; int ret; // 計(jì)算還能讀多少字節(jié) bytes_to_read BUFFER_SIZE - *f_pos; if (bytes_to_read count) bytes_to_read count; if (bytes_to_read 0) return 0; // EOF // 將內(nèi)核緩沖區(qū)數(shù)據(jù)拷貝到用戶空間 ret copy_to_user(buf, device_buffer *f_pos, bytes_to_read); if (ret) { // 拷貝失敗返回未拷貝的字節(jié)數(shù)錯(cuò)誤 return -EFAULT; } *f_pos bytes_to_read; printk(KERN_INFO mydev: read %d bytes from offset %lld.\n, bytes_to_read, *f_pos); return bytes_to_read; } static ssize_t mydev_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos) { int bytes_to_write; int ret; // 計(jì)算還能寫多少字節(jié) bytes_to_write BUFFER_SIZE - *f_pos; if (bytes_to_write count) bytes_to_write count; if (bytes_to_write 0) return -ENOSPC; // 設(shè)備滿沒有空間了 // 將用戶空間數(shù)據(jù)拷貝到內(nèi)核緩沖區(qū) ret copy_from_user(device_buffer *f_pos, buf, bytes_to_write); if (ret) { return -EFAULT; } *f_pos bytes_to_write; buffer_offset *f_pos; // 更新緩沖區(qū)偏移 printk(KERN_INFO mydev: wrote %d bytes at offset %lld.\n, bytes_to_write, *f_pos); return bytes_to_write; } static struct file_operations mydev_fops { .owner THIS_MODULE, .open mydev_open, .release mydev_close, .read mydev_read, .write mydev_write, }; static int __init mydev_init(void) { dev_t devno; int ret; // 1. 動(dòng)態(tài)分配設(shè)備號(hào) ret alloc_chrdev_region(devno, 0, 1, DEVICE_NAME); if (ret 0) { printk(KERN_ERR Failed to allocate char device region\n); return ret; } major MAJOR(devno); // 2. 初始化并添加cdev cdev_init(my_cdev, mydev_fops); my_cdev.owner THIS_MODULE; ret cdev_add(my_cdev, devno, 1); if (ret) { printk(KERN_ERR Failed to add cdev\n); goto err_cdev; } // 3. 創(chuàng)建class和設(shè)備讓udev自動(dòng)創(chuàng)建設(shè)備節(jié)點(diǎn) my_class class_create(THIS_MODULE, mydev_class); if (IS_ERR(my_class)) { ret PTR_ERR(my_class); printk(KERN_ERR Failed to create class\n); goto err_class; } my_device device_create(my_class, NULL, devno, NULL, DEVICE_NAME); if (IS_ERR(my_device)) { ret PTR_ERR(my_device); printk(KERN_ERR Failed to create device\n); goto err_device; } printk(KERN_INFO mydev: driver loaded with major number %d\n, major); return 0; err_device: class_destroy(my_class); err_class: cdev_del(my_cdev); err_cdev: unregister_chrdev_region(devno, 1); return ret; } static void __exit mydev_exit(void) { dev_t devno MKDEV(major, 0); device_destroy(my_class, devno); class_destroy(my_class); cdev_del(my_cdev); unregister_chrdev_region(devno, 1); printk(KERN_INFO mydev: driver unloaded\n); } module_init(mydev_init); module_exit(mydev_exit); MODULE_LICENSE(GPL); MODULE_AUTHOR(Driver Learner); MODULE_DESCRIPTION(A simple character device driver for learning);編譯并加載這個(gè)模塊后你應(yīng)該能在/dev目錄下看到mydev設(shè)備文件??梢杂胑cho “hello” /dev/mydev和cat /dev/mydev進(jìn)行簡單的讀寫測(cè)試并通過dmesg查看內(nèi)核打印的日志。4. 從驅(qū)動(dòng)到Android HAL橋梁的搭建純粹的Linux內(nèi)核驅(qū)動(dòng)在Android中并不能直接被Framework使用。Android引入了硬件抽象層來定義硬件供應(yīng)商和Android框架之間的標(biāo)準(zhǔn)接口。HAL是一個(gè)位于內(nèi)核驅(qū)動(dòng)和Android運(yùn)行時(shí)/系統(tǒng)服務(wù)之間的中間層它封裝了底層驅(qū)動(dòng)的實(shí)現(xiàn)細(xì)節(jié)。一個(gè)典型的流程是內(nèi)核驅(qū)動(dòng)提供最基礎(chǔ)的設(shè)備控制通過sysfs、procfs或設(shè)備文件暴露接口。HAL模塊.so庫一個(gè)用C/C編寫的共享庫它通過hw_module_t結(jié)構(gòu)體定義自己并實(shí)現(xiàn)特定的HAL接口如lights.h、sensors.h中定義的函數(shù)。這個(gè)庫會(huì)通過dlopen的方式被Android系統(tǒng)服務(wù)加載。HAL Stub/Proxy在HAL庫內(nèi)部它會(huì)通過open、ioctl等系統(tǒng)調(diào)用去操作我們?cè)趦?nèi)核中創(chuàng)建的設(shè)備文件如/dev/mydev從而與內(nèi)核驅(qū)動(dòng)通信。JNI與FrameworkAndroid系統(tǒng)服務(wù)通常是C編寫的守護(hù)進(jìn)程如sensorservice調(diào)用HAL接口。然后通過JNI向上傳遞給Java層的Framework API如SensorManager最終供App使用。例如為一個(gè)虛擬的“學(xué)習(xí)用LED”編寫HAL你可能需要內(nèi)核驅(qū)動(dòng)創(chuàng)建/dev/led設(shè)備文件實(shí)現(xiàn)ioctl命令來控制LED亮滅。HAL庫實(shí)現(xiàn)hardware/libhardware/include/hardware/led_hal.h中定義的接口如set_on,set_off在接口函數(shù)內(nèi)部去open(“/dev/led”)并發(fā)送ioctl命令。在設(shè)備的manifest.xml對(duì)于Treble架構(gòu)是VINTF中聲明該HAL以便系統(tǒng)能發(fā)現(xiàn)并加載它。實(shí)操心得對(duì)于Android驅(qū)動(dòng)開發(fā)者尤其是涉及新硬件支持時(shí)工作往往是“兩頭抓”。一頭需要根據(jù)芯片手冊(cè)編寫或調(diào)試內(nèi)核驅(qū)動(dòng)確保硬件寄存器操作正確另一頭需要實(shí)現(xiàn)或適配對(duì)應(yīng)的HAL接口確保Android框架能正確調(diào)用。理解hw_module_t的加載機(jī)制以及HIDLHAL接口定義語言在Android 8.0后引入或AIDL用于更新版本的Android的使用是現(xiàn)代Android底層開發(fā)的必備技能。5. 驅(qū)動(dòng)開發(fā)環(huán)境搭建與調(diào)試實(shí)戰(zhàn)5.1 開發(fā)環(huán)境配置驅(qū)動(dòng)開發(fā)強(qiáng)烈推薦在Linux物理機(jī)或虛擬機(jī)上進(jìn)行。你需要Linux發(fā)行版Ubuntu、Fedora或Deepin等用于作為主機(jī)開發(fā)環(huán)境。內(nèi)核頭文件/源碼編譯模塊需要對(duì)應(yīng)版本的內(nèi)核頭文件。安裝命令如sudo apt install linux-headers-$(uname -r)。如果要進(jìn)行深度修改或調(diào)試則需要獲取完整的內(nèi)核源碼。編譯工具鏈gcc,make等。對(duì)于交叉編譯為ARM架構(gòu)的Android設(shè)備編譯則需要安裝對(duì)應(yīng)的交叉編譯工具鏈如aarch64-linux-gnu-gcc。目標(biāo)環(huán)境可以是另一臺(tái)開發(fā)板如樹莓派、RK3588開發(fā)板也可以是Android模擬器支持自定義內(nèi)核編譯或QEMU虛擬機(jī)。對(duì)于Android驅(qū)動(dòng)最終測(cè)試必須在Android系統(tǒng)上進(jìn)行。一個(gè)典型的交叉編譯Makefile示例針對(duì)ARM64ARCH : arm64 CROSS_COMPILE : aarch64-linux-gnu- KDIR : /path/to/your/android/kernel/source obj-m hello.o all: $(MAKE) -C $(KDIR) M$(PWD) ARCH$(ARCH) CROSS_COMPILE$(CROSS_COMPILE) modules clean: $(MAKE) -C $(KDIR) M$(PWD) ARCH$(ARCH) CROSS_COMPILE$(CROSS_COMPILE) clean5.2 調(diào)試方法與技巧內(nèi)核調(diào)試比用戶態(tài)程序困難因?yàn)闆]有方便的調(diào)試器可以直接附著。主要依靠以下方法printk最強(qiáng)大的武器printk是內(nèi)核的打印函數(shù)輸出到內(nèi)核日志緩沖區(qū)??梢酝ㄟ^dmesg命令查看。使用不同的日志級(jí)別KERN_DEBUG,KERN_INFO,KERN_ERR等有助于過濾信息。在驅(qū)動(dòng)關(guān)鍵路徑如初始化、打開、讀寫、中斷處理加入詳盡的printk是定位問題最基本有效的方法。/proc和/sys文件系統(tǒng)除了printk可以通過procfs和sysfs在運(yùn)行時(shí)向用戶空間暴露驅(qū)動(dòng)內(nèi)部狀態(tài)信息或提供簡單的控制接口。這對(duì)于調(diào)試非常有用例如導(dǎo)出一個(gè)文件來顯示驅(qū)動(dòng)當(dāng)前的緩沖區(qū)狀態(tài)或統(tǒng)計(jì)信息。內(nèi)核Oops信息當(dāng)內(nèi)核遇到非法操作如空指針解引用時(shí)會(huì)打印“Oops”信息其中包含出錯(cuò)的調(diào)用棧、寄存器狀態(tài)等是分析崩潰原因的關(guān)鍵。務(wù)必仔細(xì)閱讀并理解這些信息。KGDB/KDB內(nèi)核內(nèi)置的調(diào)試器功能強(qiáng)大可以設(shè)置斷點(diǎn)、單步執(zhí)行、查看變量。但配置和使用相對(duì)復(fù)雜通常用于解決極其棘手的問題。仿真與虛擬化在QEMU中運(yùn)行內(nèi)核可以配合GDB進(jìn)行源碼級(jí)調(diào)試。這對(duì)于學(xué)習(xí)內(nèi)核機(jī)制和復(fù)雜驅(qū)動(dòng)的工作原理非常有幫助。5.3 常見問題與排查實(shí)錄在驅(qū)動(dòng)開發(fā)中你會(huì)頻繁遇到一些問題。下面是一個(gè)速查表問題現(xiàn)象可能原因排查思路與解決方法insmod失敗提示Invalid module format模塊編譯所用的內(nèi)核版本/配置與當(dāng)前運(yùn)行內(nèi)核不匹配。檢查uname -r與編譯時(shí)KDIR指向的內(nèi)核版本是否一致。確保編譯環(huán)境正確特別是交叉編譯時(shí)。insmod失敗提示Unknown symbol in module模塊依賴的內(nèi)核符號(hào)函數(shù)或變量不存在。使用modinfo查看模塊依賴。可能是內(nèi)核配置未開啟相關(guān)功能如CONFIG_XXXy或符號(hào)未導(dǎo)出需要EXPORT_SYMBOL。設(shè)備文件/dev/xxx不存在設(shè)備號(hào)未正確注冊(cè)或udev未自動(dòng)創(chuàng)建設(shè)備節(jié)點(diǎn)。1. 檢查dmesg看驅(qū)動(dòng)加載時(shí)alloc_chrdev_region是否成功主設(shè)備號(hào)是多少。2. 檢查/sys/class/下是否有對(duì)應(yīng)的類和設(shè)備目錄。若無檢查class_create和device_create是否成功。3. 可以手動(dòng)mknod創(chuàng)建測(cè)試sudo mknod /dev/xxx c major minor。open設(shè)備文件失敗用戶空間驅(qū)動(dòng)open函數(shù)返回錯(cuò)誤文件權(quán)限問題。1. 檢查驅(qū)動(dòng)open函數(shù)實(shí)現(xiàn)是否有條件判斷導(dǎo)致返回錯(cuò)誤碼如-EBUSY。2. 檢查/dev/xxx文件權(quán)限是否為crw-rw----用戶組是否正確??赏ㄟ^udev規(guī)則設(shè)置。read/write返回-EFAULT用戶空間與內(nèi)核空間內(nèi)存拷貝失敗。檢查copy_to_user/copy_from_user的返回值。確保用戶空間緩沖區(qū)地址有效且內(nèi)核有權(quán)限訪問。在驅(qū)動(dòng)中不要直接解引用用戶空間指針。系統(tǒng)不穩(wěn)定或死機(jī)驅(qū)動(dòng)中有內(nèi)存泄漏、死鎖、或非法內(nèi)存訪問。1. 檢查所有kmalloc是否有對(duì)應(yīng)的kfree。2. 檢查自旋鎖spin_lock/mutex的使用是否正確避免死鎖。3. 使用slub調(diào)試工具檢查內(nèi)存損壞。4. 分析內(nèi)核崩潰后的Oops信息。中斷不觸發(fā)或觸發(fā)異常中斷號(hào)申請(qǐng)錯(cuò)誤、中斷處理函數(shù)未正確注冊(cè)或編寫有誤。1. 確認(rèn)硬件中斷號(hào)使用request_irq正確申請(qǐng)。2. 中斷處理函數(shù)要快不能阻塞需要耗時(shí)的工作應(yīng)使用tasklet或工作隊(duì)列workqueue。3. 中斷處理函數(shù)返回類型應(yīng)為irqreturn_t。避坑技巧內(nèi)存管理內(nèi)核空間內(nèi)存緊張。kmalloc分配的內(nèi)存是物理連續(xù)的適用于小緩沖區(qū)。vmalloc分配虛擬地址連續(xù)但物理不一定連續(xù)的內(nèi)存適用于大塊內(nèi)存但訪問效率稍低。務(wù)必配對(duì)使用kfree和vfree。并發(fā)控制驅(qū)動(dòng)必須考慮多進(jìn)程/多線程同時(shí)訪問的情況。使用互斥鎖mutex保護(hù)共享數(shù)據(jù)使用自旋鎖spinlock保護(hù)在中斷上下文或持有時(shí)間極短的臨界區(qū)。記住在中斷上下文中不能睡眠不能調(diào)用可能引起調(diào)度的函數(shù)如mutex_lock此時(shí)只能用自旋鎖。硬件操作在操作硬件寄存器前務(wù)必確認(rèn)已通過ioremap將物理地址映射到內(nèi)核虛擬地址空間。使用readl/writel等函數(shù)進(jìn)行IO內(nèi)存訪問以確保正確的字節(jié)序和內(nèi)存屏障。模塊參數(shù)使用module_param宏可以定義模塊加載參數(shù)方便測(cè)試時(shí)動(dòng)態(tài)調(diào)整行為例如static int debug_enable 0; module_param(debug_enable, int, 0644);加載時(shí)可用insmod mymod.ko debug_enable1。驅(qū)動(dòng)開發(fā)是一個(gè)需要耐心和細(xì)致的工作從最簡單的字符設(shè)備驅(qū)動(dòng)開始逐步理解中斷、DMA、平臺(tái)設(shè)備模型、設(shè)備樹等更復(fù)雜的概念是通往Android底層世界的堅(jiān)實(shí)道路。每一次成功的insmod和穩(wěn)定的硬件交互都是對(duì)系統(tǒng)理解的一次深刻提升。