核驅(qū)動開發(fā)實戰(zhàn):從drivers目錄到可加載模塊與設(shè)備樹匹配)
簡介這份資源是《Linux設(shè)備驅(qū)動開發(fā)詳解基于最新的Linux 4.0內(nèi)核》一書的配套樣例代碼包面向具備一定C語言與操作系統(tǒng)基礎(chǔ)、希望系統(tǒng)學習Linux內(nèi)核驅(qū)動開發(fā)的工程師與高校學生幫助讀者把書中理論落到可編譯、可運行的代碼實踐上。壓縮包共122個文件約659KB以c源碼與cmd編譯腳本為主輔以makefile構(gòu)建文件、o目標文件、ko內(nèi)核模塊、mod模塊信息及symvers符號版本等覆蓋字符設(shè)備、塊設(shè)備、網(wǎng)絡(luò)設(shè)備、中斷處理、I/O調(diào)度、內(nèi)存管理、模塊化設(shè)計與電源管理等核心主題。資源中已有197人學習下載樣例圍繞globalmem、globalfifo、vmem_disk等典型驅(qū)動展開讀者可借此理解設(shè)備注冊、讀寫請求處理、設(shè)備文件創(chuàng)建與內(nèi)核接口調(diào)用方式為實際項目中的驅(qū)動開發(fā)與調(diào)試打下基礎(chǔ)。1. drivers_drivers_linux_Kernel_從目錄名到可加載模塊的完整路徑第一次看到drivers_drivers_linux_Kernel_這個標題很多人會以為它指向某個具體的驅(qū)動倉庫或內(nèi)核補丁集。實際上它更像一個信號你手上有一份 Linux 內(nèi)核源碼樹或者一個按drivers/目錄組織過的驅(qū)動代碼包需要把它變成能跑、能加載、能調(diào)試的東西。Linux 內(nèi)核驅(qū)動開發(fā)不是寫一個.c文件然后gcc一下就能收工的它涉及內(nèi)核配置、模塊編譯、設(shè)備樹匹配、符號導出、版本適配這一整條鏈路。這篇文章面向的是嵌入式 Linux 工程師、BSP 移植人員以及正在從應(yīng)用層往內(nèi)核層過渡的開發(fā)者。我會按“先理解 drivers 目錄的組織邏輯再動手編譯一個可加載模塊最后處理真實硬件匹配和排錯”的順序展開每一步都給出可復(fù)現(xiàn)的命令和參數(shù)說明。如果你正在做國產(chǎn) Linux 發(fā)行版適配、高通 CAF kernel 移植或者只是想把一個字符設(shè)備驅(qū)動跑通下面的內(nèi)容可以直接抄作業(yè)。2. Linux 內(nèi)核 drivers 目錄的組織邏輯與模塊編譯鏈路2.1 drivers 目錄不是隨便放的Kconfig、Makefile 與模塊的三方約定Linux 內(nèi)核源碼樹里的drivers/目錄是整棵樹上最龐大的部分之一按設(shè)備類型分成char/、block/、net/、i2c/、spi/、gpio/、usb/、pci/等子目錄。每個子目錄下通常有三類文件驅(qū)動源碼.c、Kconfig 配置項、Makefile 編譯規(guī)則。這三者構(gòu)成一個閉環(huán)Kconfig 決定這個驅(qū)動是否出現(xiàn)在make menuconfig的菜單里Makefile 決定它編譯成內(nèi)置還是模塊源碼里的module_init/module_exit決定加載和卸載行為。我一般會先確認三件事第一目標內(nèi)核版本是多少uname -r和源碼樹頂層Makefile里的VERSION/PATCHLEVEL是否一致第二交叉編譯工具鏈前綴是什么比如aarch64-linux-gnu-還是arm-linux-gnueabihf-第三當前內(nèi)核的.config里CONFIG_MODULES是否打開。這三件事沒確認就動手后面大概率會翻車。一個典型的驅(qū)動子目錄結(jié)構(gòu)如下drivers/mydriver/ ├── Kconfig ├── Makefile ├── mydriver.c └── mydriver.hKconfig 內(nèi)容示例config MYDRIVER tristate My example driver depends on GPIOLIB help This is a demo driver for GPIO-based device. Say M to build as module.Makefile 內(nèi)容示例obj-$(CONFIG_MYDRIVER) mydriver.otristate表示這個配置項有三種狀態(tài)N不編譯、Y編進內(nèi)核、M編譯成模塊。obj-$(CONFIG_MYDRIVER)會根據(jù)配置值展開成obj-y或obj-m分別對應(yīng)內(nèi)置和模塊。這里的關(guān)鍵點是如果你希望驅(qū)動能動態(tài)加載必須讓CONFIG_MYDRIVERm并且內(nèi)核本身開啟了模塊支持。2.2 從零編譯一個可加載模塊命令、參數(shù)與產(chǎn)物驗證假設(shè)你已經(jīng)有一份內(nèi)核源碼樹路徑是/home/user/kernel交叉編譯工具鏈是aarch64-linux-gnu-目標架構(gòu)是 arm64。下面是我常用的編譯流程。第一步準備配置。如果源碼樹里還沒有.config可以用默認配置或廠商配置cd /home/user/kernel make ARCHarm64 CROSS_COMPILEaarch64-linux-gnu- defconfig如果廠商提供了xxx_defconfig優(yōu)先用廠商的因為默認配置可能沒打開你需要的子系統(tǒng)。第二步打開你的驅(qū)動配置項make ARCHarm64 CROSS_COMPILEaarch64-linux-gnu- menuconfig在菜單里找到Device Drivers下的My example driver按M選中保存退出?;蛘咧苯佑媚_本改scripts/config --module MYDRIVER make ARCHarm64 CROSS_COMPILEaarch64-linux-gnu- olddefconfigolddefconfig會把新增配置項的默認值補齊避免交互式提問。第三步編譯模塊make ARCHarm64 CROSS_COMPILEaarch64-linux-gnu- Mdrivers/mydriver modulesMdrivers/mydriver告訴內(nèi)核構(gòu)建系統(tǒng)只編譯這個子目錄不重新編譯整個內(nèi)核。編譯完成后你會在drivers/mydriver/下看到mydriver.ko。第四步驗證模塊信息modinfo drivers/mydriver/mydriver.ko輸出里會包含filename、license、description、depends、vermagic等字段。vermagic必須和目標板運行內(nèi)核的版本字符串完全一致否則insmod會報invalid module format。這是最常見的坑之一。第五步推送到目標板并加載scp drivers/mydriver/mydriver.ko root192.168.1.100:/tmp/ ssh root192.168.1.100 insmod /tmp/mydriver.ko dmesg | tail -20如果驅(qū)動里用了printkdmesg里能看到對應(yīng)輸出。卸載用rmmod mydriver前提是模塊沒有被引用。提示insmod不會自動解決依賴如果模塊依賴其他模塊用modprobe并確保/lib/modules/$(uname -r)/下有正確的modules.dep。2.3 內(nèi)置驅(qū)動與模塊驅(qū)動的選擇邊界不是所有驅(qū)動都適合編譯成模塊。我一般按下面的邊界來判斷場景推薦方式原因啟動階段必須初始化內(nèi)置Y模塊加載時機晚于根文件系統(tǒng)掛載調(diào)試階段頻繁改代碼模塊M改完只需重編模塊不用重啟內(nèi)核依賴早期中斷或時鐘內(nèi)置Y模塊加載時中斷子系統(tǒng)可能還沒就緒廠商 BSP 強制要求按廠商有些 SoC 的電源域和時鐘依賴順序固定高通 CAF kernel 里很多驅(qū)動默認是內(nèi)置的因為涉及電源管理和時鐘樹初始化順序。如果你強行改成模塊可能會出現(xiàn) probe 時時鐘未使能、寄存器讀寫失敗的情況。這類問題不是代碼寫錯了而是加載時機不對。3. 設(shè)備樹匹配與 probe 流程驅(qū)動怎么找到硬件3.1 compatible 字符串驅(qū)動和設(shè)備樹的握手協(xié)議在 ARM/ARM64 嵌入式 Linux 里驅(qū)動和硬件的匹配主要靠設(shè)備樹。驅(qū)動里寫一個of_device_id表設(shè)備樹里寫一個compatible屬性兩者字符串完全一致時內(nèi)核才會調(diào)用驅(qū)動的probe函數(shù)。驅(qū)動側(cè)代碼示例#include linux/module.h #include linux/platform_device.h #include linux/of.h static int mydriver_probe(struct platform_device *pdev) { struct device *dev pdev-dev; dev_info(dev, mydriver probed\n); return 0; } static int mydriver_remove(struct platform_device *pdev) { dev_info(pdev-dev, mydriver removed\n); return 0; } static const struct of_device_id mydriver_of_match[] { { .compatible vendor,mydriver-v1 }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, mydriver_of_match); static struct platform_driver mydriver_driver { .probe mydriver_probe, .remove mydriver_remove, .driver { .name mydriver, .of_match_table mydriver_of_match, }, }; module_platform_driver(mydriver_driver); MODULE_LICENSE(GPL); MODULE_AUTHOR(Your Name); MODULE_DESCRIPTION(Demo platform driver);設(shè)備樹側(cè)節(jié)點示例mydriver10000000 { compatible vendor,mydriver-v1; reg 0x0 0x10000000 0x0 0x1000; interrupts 0 42 4; status okay; };compatible是匹配的關(guān)鍵reg描述寄存器基地址和長度interrupts描述中斷號和觸發(fā)類型。status okay表示啟用這個節(jié)點如果寫disabled驅(qū)動不會 probe。3.2 probe 失敗的排查順序從 dmesg 到 /sys 的完整鏈路probe 失敗是驅(qū)動開發(fā)里最耗時的環(huán)節(jié)。我一般按下面的順序排查第一看dmesg里有沒有probe failed或failed to get resource之類的輸出。內(nèi)核會在 probe 失敗時打印錯誤碼比如-ENODEV、-EINVAL、-EPROBE_DEFER。第二確認設(shè)備樹節(jié)點是否被內(nèi)核解析到ls /sys/firmware/devicetree/base/如果節(jié)點存在再檢查compatible是否和驅(qū)動里的字符串完全一致。注意大小寫和連字符vendor,mydriver-v1和vendor,myDriver-v1是不匹配的。第三檢查EPROBE_DEFER。這個錯誤碼表示驅(qū)動依賴的資源還沒準備好內(nèi)核會稍后重試。常見原因是時鐘、 regulator、GPIO 控制器還沒初始化。解決辦法是確認依賴驅(qū)動的 probe 順序必要時在設(shè)備樹里調(diào)整節(jié)點順序或使用phandle引用。第四看/sys/bus/platform/drivers/mydriver/下有沒有綁定成功的設(shè)備ls /sys/bus/platform/drivers/mydriver/如果目錄下只有bind、unbind、uevent沒有設(shè)備名說明沒有設(shè)備綁定到這個驅(qū)動。第五用dev_info在 probe 入口打印確認函數(shù)是否被調(diào)用。如果根本沒進 probe問題在匹配階段如果進了 probe 但失敗問題在資源獲取階段。注意pr_info和dev_info的輸出級別不同dev_info會帶上設(shè)備名更容易定位。生產(chǎn)驅(qū)動里建議用dev_err打印錯誤用dev_dbg打印調(diào)試信息。3.3 字符設(shè)備注冊file_operations 與設(shè)備號的分配platform_driver 負責和硬件匹配字符設(shè)備負責和用戶空間交互。兩者可以合在一個驅(qū)動里也可以分開。下面是一個最小的字符設(shè)備注冊示例#include linux/fs.h #include linux/cdev.h #include linux/uaccess.h static dev_t mydev_num; static struct cdev mydev_cdev; static struct class *mydev_class; static int mydev_open(struct inode *inode, struct file *file) { return 0; } static ssize_t mydev_read(struct file *file, char __user *buf, size_t len, loff_t *offset) { char msg[] hello from kernel\n; if (*offset sizeof(msg)) return 0; if (copy_to_user(buf, msg, sizeof(msg))) return -EFAULT; *offset sizeof(msg); return sizeof(msg); } static const struct file_operations mydev_fops { .owner THIS_MODULE, .open mydev_open, .read mydev_read, }; static int __init mydev_init(void) { alloc_chrdev_region(mydev_num, 0, 1, mydev); cdev_init(mydev_cdev, mydev_fops); cdev_add(mydev_cdev, mydev_num, 1); mydev_class class_create(THIS_MODULE, mydev); device_create(mydev_class, NULL, mydev_num, NULL, mydev); return 0; } static void __exit mydev_exit(void) { device_destroy(mydev_class, mydev_num); class_destroy(mydev_class); cdev_del(mydev_cdev); unregister_chrdev_region(mydev_num, 1); } module_init(mydev_init); module_exit(mydev_exit); MODULE_LICENSE(GPL);alloc_chrdev_region動態(tài)分配設(shè)備號cdev_add注冊字符設(shè)備class_create和device_create自動在/dev下創(chuàng)建設(shè)備節(jié)點。用戶空間用open(/dev/mydev, O_RDONLY)打開read讀取。copy_to_user是內(nèi)核向用戶空間傳數(shù)據(jù)的標準接口不能直接用memcpy否則會觸發(fā)內(nèi)核頁錯誤。4. 驅(qū)動開發(fā)避坑從編譯報錯到運行時崩潰的 5 個真實記錄4.1 現(xiàn)象insmod 報 invalid module format原因模塊的vermagic和目標內(nèi)核版本不一致。常見于換了內(nèi)核源碼樹但沒重新編譯模塊或者交叉編譯工具鏈版本不同導致內(nèi)核版本字符串帶或-dirty后綴。解決用modinfo對比模塊和內(nèi)核的vermagic確保源碼樹頂層Makefile的版本號和目標板uname -r一致。如果目標板內(nèi)核是廠商定制的必須用廠商提供的源碼樹編譯模塊。4.2 現(xiàn)象probe 函數(shù)不執(zhí)行dmesg 無任何輸出原因設(shè)備樹compatible字符串和驅(qū)動of_device_id不匹配或者設(shè)備樹節(jié)點status是disabled或者驅(qū)動根本沒編譯進內(nèi)核。解決先確認/sys/firmware/devicetree/base/下節(jié)點存在且status為okay再確認驅(qū)動.ko文件已加載且modinfo里alias字段包含正確的of:前綴。如果驅(qū)動是內(nèi)置的檢查.config里對應(yīng)配置項是否為Y。4.3 現(xiàn)象probe 返回 -EPROBE_DEFER反復(fù)重試但不成功原因驅(qū)動依賴的時鐘、regulator、GPIO 控制器還沒 probe 完成。內(nèi)核會延遲重試但如果依賴驅(qū)動永遠不 probe你的驅(qū)動也永遠不會成功。解決檢查依賴驅(qū)動是否已加載設(shè)備樹里phandle引用是否正確??梢杂胏at /sys/kernel/debug/devices_deferred查看延遲設(shè)備列表。如果是依賴順序問題考慮把依賴驅(qū)動改成內(nèi)置或者調(diào)整設(shè)備樹節(jié)點順序。4.4 現(xiàn)象copy_to_user 返回 -EFAULT用戶空間讀不到數(shù)據(jù)原因用戶空間傳入的緩沖區(qū)地址無效或者長度參數(shù)超過了實際緩沖區(qū)大小。也可能是內(nèi)核里直接用了memcpy而不是copy_to_user。解決在read/write里先檢查access_ok再用copy_to_user/copy_from_user。注意copy_to_user返回的是未拷貝的字節(jié)數(shù)返回 0 表示成功。如果返回非 0說明有部分數(shù)據(jù)沒拷貝成功。4.5 現(xiàn)象rmmod 報 Device or resource busy原因模塊的引用計數(shù)不為 0可能有用戶空間進程還打開著設(shè)備節(jié)點或者內(nèi)核里有其他模塊依賴它。解決用lsmod查看模塊引用計數(shù)用fuser /dev/mydev或lsof /dev/mydev找到占用進程并關(guān)閉。如果是內(nèi)核依賴先卸載依賴模塊。調(diào)試階段可以在mydev_open里加try_module_get(THIS_MODULE)在release里加module_put確保引用計數(shù)正確。5. 用 QEMU 驗證驅(qū)動不接硬件也能跑通 probe 和讀寫5.1 QEMU buildroot 的最小驗證環(huán)境不是每個人都有現(xiàn)成的開發(fā)板。我常用 QEMU 加 buildroot 搭一個最小環(huán)境驗證驅(qū)動的基本邏輯。buildroot 可以生成內(nèi)核鏡像、根文件系統(tǒng)和設(shè)備樹QEMU 負責模擬運行。配置 buildroot 時關(guān)鍵選項如下配置項值說明Target ArchitectureARM (little endian)或 AArch64KernelLinux 4.19 或更高版本按需選擇ToolchainBuildroot toolchain內(nèi)置工具鏈省去交叉編譯配置Filesystemext2/ext4根文件系統(tǒng)格式Device tree啟用用于傳遞硬件描述編譯完成后用 QEMU 啟動qemu-system-arm -M vexpress-a9 \ -kernel output/images/zImage \ -dtb output/images/vexpress-v2p-ca9.dtb \ -drive fileoutput/images/rootfs.ext2,ifsd,formatraw \ -append root/dev/mmcblk0 consolettyAMA0 \ -nographic-M vexpress-a9指定模擬板型-dtb指定設(shè)備樹-append傳遞內(nèi)核命令行。啟動后進入 shell就可以用insmod加載模塊用dmesg看輸出。5.2 在 QEMU 里驗證字符設(shè)備讀寫把編譯好的mydev.ko通過scp或掛載共享目錄傳到 QEMU 里加載后檢查/dev/mydev是否存在insmod /tmp/mydev.ko ls -l /dev/mydev cat /dev/mydev如果cat輸出hello from kernel說明字符設(shè)備注冊和讀寫鏈路都通了。如果/dev/mydev不存在檢查class_create和device_create的返回值以及udev或mdev是否在運行。提示QEMU 里沒有真實硬件所以依賴具體寄存器操作的驅(qū)動無法完整驗證。但 probe 流程、字符設(shè)備注冊、文件操作接口這些邏輯可以跑通能提前發(fā)現(xiàn)大部分代碼錯誤。5.3 用 ftrace 跟蹤 probe 調(diào)用鏈如果 probe 沒執(zhí)行可以用 ftrace 看內(nèi)核函數(shù)調(diào)用cd /sys/kernel/debug/tracing echo function current_tracer echo mydriver_probe set_ftrace_filter echo 1 tracing_on insmod /tmp/mydriver.ko cat traceset_ftrace_filter只跟蹤指定函數(shù)避免輸出過多。如果trace里沒有mydriver_probe說明匹配階段就失敗了。如果有但后面報錯可以結(jié)合dmesg看具體錯誤碼。最后一章我想說的是驅(qū)動開發(fā)最怕的不是代碼寫不出來而是不知道哪一層出了問題。我的習慣是每加一個功能點就驗證一次先確認模塊能編譯再確認能加載再確認 probe 能進再確認字符設(shè)備能注冊最后才測讀寫。每一步都有對應(yīng)的檢查命令不要等全部寫完再一起調(diào)。另外內(nèi)核版本和工具鏈版本一定要鎖死換版本后先重新編譯一個已知能用的模塊確認環(huán)境沒問題再改代碼。希望幫到你。本文還有配套的精品資源點擊獲取