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