--- title: 'Linux 設備開發 - 概述、編譯、安裝、HelloWrold' disqus: kyleAlien --- Linux 設備開發 - 概述、編譯、安裝、HelloWrold === ## Overview of Content 使用 make 4.8 版本 [TOC] ## Linux 內核板號規則 Linux 內核版號由幾個部份組成,可以用指令查看當前系統版本號 ```shell= uname -r ``` * 主版號:`6` * 次版號:`2` * 修正版本號:`0` * 微調版本號:`-` * 特製 Linux 系統調校描述:`generic` > ![](https://hackmd.io/_uploads/HJhKSkEz6.png) <!-- ### C 語言的編譯環境 - GNU * `GNU C` 是對標準 C 的拓展,是 Linux/UNIX 下最常用的 **C 語言編譯環境**, --> ### 常見開源協議 * 不同的開源協議可以不同程度的保障開源者的權益,如果違反協議很可能被起訴(雖然很多中小企業都不太考慮開源協議的限制 :joy:) :::info * 完整的開源協議搜尋,可以到 [**opensource**](https://opensource.org/licenses/) 網站搜尋 ::: * 以下來看看幾種常見的開源協議 | 協議 | 有利於 | 概述 | 補充 | | - | - | - | - | | GPL 協議 | 技術拓展 | GPL 會 **強迫全面的開源**,公開技術,但不利於商業 | 它也是具有傳染性的協議,因為它會包含 **衍生程式也需要開源** | | LGPL 協議 | 商業、Library | 基於 GPL 協議,但他是 **為了 Library 使用設計的原則**,LGPL 允許商業軟體 **通過 Library 引用而不用開源** | 採用 LGPL 商業代碼 **可以被發布、銷售** | | BSD 協議 | 使用者拓展、同時尊重原作 | **讓使用者自由拓展、修改源碼**;可將修改後的程式作為開源、軟體再發布 | 再次發布條件如下:^1.^在源代碼中帶有原來的 BSD 協議、^2.^ 如果發布二進制庫,也須聲明源代碼中的 BSD、^3.^ 不可用開源碼的作者、機構名、原來的名稱發布 | | Apache 2.0 協議 | 使用者拓展、同時尊重原作、商業友善 | 同樣允許程式修改、再發布,不過細節規範較多 | 不同點在於:^1.^ 程式中有有一份 `Apache License`、^2.^修改的代碼須在文件中說明、^3.^ 在拓展代碼終須帶有源有的協議、商標、專利、原作規定;^4.^再次發布須帶有 Notice 文件 | | MIT 協議 | 作者可以保留版權 | 寬鬆的協議許可 | 也就是說必須在你發行版裡面,必須包含原作的許可聲明協議 | ## Linux 設備驅動 在最初的系統中,並沒有所謂的「**驅動**」,軟體都可以直接訪問電腦的硬體元件;這造就了純軟體工程師要了解 **硬體的通訊協定**(`I2C`、`SPI`、`UART`、`CAN BUS`... 等等) :::danger * 這種直接訪問造成了「**高依賴**」 而高依賴,就說明了「**高耦合**」,模組與模組之間的依賴度高,無法相互替換(因為依賴了細節,直接通訊會有關於到裝置細節的設置) > 這也間接說明了 **「抽象」的重要**,抽象度越高,耦合度越度 ``` mermaid graph LR; 軟體工程師 -.-> |要了解通訊細節| 裝置A 軟體工程師 --> 直接通訊 直接通訊 -.-> |通訊 ok| 裝置A 直接通訊 -.-> |無法通訊 error | 裝置B ``` * **而這個「抽象的任務」就交由給「驅動」去設計** 驅動會統一界面(接口),這讓上層工程師只須專注了解界面所提供的功能即可,不必關注功能的實現 ``` mermaid graph LR; 軟體工程師 -.-> |只要了解驅動界面| 驅動 驅動 -.-> |通訊 ok| 裝置A 驅動 -.-> |通訊 ok| 裝置B ``` ::: ### Linux 驅動是甚麽? * 事實上 Linux 驅動是 **使用 Linux 為驅動開發者提供的 API**,本質上沒有很大的差異,不過就是在開發時要使用 **Linux 提供的框架** * **Linux 驅動的工作方式、特色** * **驅動存在的形式**: Linux 會將每個「**驅動映射為文件**」,這些文件會稱為 **設備、驅動文件**;並會將這種文件存放在 `/dev` 資料夾之下 > 這種設計方案使用訪問 Linux 驅動,就跟訪問文件一樣的簡單(跟普通文件一樣) ```mermaid graph TB; subgraph PC subgraph /dev 驅動_設備文件_影印機 驅動_設備文件_錄音裝置 驅動_設備文件_無限網卡 end 影印機 -.-> 驅動_設備文件_影印機 錄音裝置 -.-> 驅動_設備文件_錄音裝置 無限網卡 -.-> 驅動_設備文件_無限網卡 end ``` * **與驅動交互的方式**: 所以跟 Linux 驅動交互的方式,只需要與 **設備文件交換數據** 即可,也就是在程式中「開啟驅動文件」,並對文件下達指令即可 > 像是,要 Linux 系統下的影印機打印指令 > > 只需要使用 C 語言,透過系統提供的 IO API 來操作文件;常見的像是 > > 透過 `open` 函數開啟驅動文件 > 透過 `ioctl` 來操作該文件… 並下達影印指令 ```mermaid graph LR; 開發者 -.-> |open、ioctl|驅動_設備文件 驅動_設備文件 -.-> 開發者 subgraph PC subgraph /dev 驅動_設備文件 end 實體設備 -.-> 驅動_設備文件 end ``` 而文件與設備的映射關係,則是由 Linux 系統去負責,開發驅動的人員不必當心硬體文件映射的部份(當然你要研究也可以的~) * 然而 Linux 驅動文件並非使用手動去觸發才會執行(不然真的很麻煩,每次影印都要手動觸發驅動行為)… 通常會需要「**事件驅動機制**」,**由驅動來監聽事件**! :::success * **事件驅動機制** 是一種 **IoC 的軟體設計概念**,它會 **反轉高低層模組之間的關係** 如果使用 C 語言的話,可以通過 Callback 的方式達成這種事件驅動機制 ::: ### 驅動 & 內核 - 關係 * Linux 驅動開發只會與 Linux 內核有關!也就是說 **Linux 驅動與 Linux 內核是強耦合關係**,**相同的 Linux 驅動,對於不同版本的 Linux 內核不一定能互通** 而驅動又與應用沒有直接的關係,兩者屬於弱耦合 ```mermaid graph LR; 應用 --> 驅動界面 驅動界面 -.-> Linux_版本A 驅動界面 -.-> Linux_版本B subgraph Linux_版本A Linux_驅動A --> |強關聯| Linux_內核A end subgraph Linux_版本B Linux_驅動B --> |強關聯| Linux_內核B end ``` ### 物理設備 - 軟體抽象的分類、特點 * 電腦系統的硬體主要有幾個元件 CPU、記憶體、存儲器、外部設備,隨著晶片的進步;CPU 內部其實就自己包有這幾個部件了 > 有些 CPU 內部有 `RAM`、`Flash` 裝置 ```mermaid graph LR; subgraph PC CPU 記憶體 存儲器 外部設備 end ``` * 驅動則是針對存儲器(`register`)、外部設備,而 Linux 主要將這些設備為三大類 * **字符設備**(`Character devices`) 主要針對須以「**串行順序依次**」訪問的設備(也就是須照順序排隊的設備),它不須經過系統快速緩存 > Eg. 觸碰螢幕、滑鼠、鍵盤... 等等 * **塊設備**(`Block devices`) 可以用來進行「**任意順序**」訪問,以「塊」為單位訪問裝置,它會須經過系統快速緩存 > Eg. 最常見的就是硬碟 :::info * 在使用上: 塊設備有時候與字符設備類似,並沒有非常明顯的邊界,可以把塊設備當作字符設備訪問 > 都有實現 `Open`、`close`、`read`、`write`... 等等函數指標 * 在驅動實做上: 字符設備、塊設備有很大的差異 ::: * **網路設備**(`Network devices`) 主要是處理「數據的發送」、「接收」... 它沒有對應的文件系統節點;它與字符、塊設備的 **通訊方式有很大的不同** ## Linux 驅動開發 Linux 驅動程式與其他類型的 Linux 程式開發一樣,都由系統提供固定框架,由使用者 **按照框架** 約定去開發程式 這也就造就了 **Linux 驅動開發可以「有順序、規則」可尋** ### 驅動框架、步驟 1. **建立 Linux 驅動骨架**: * **初始化設備**,**告訴 Linux 有這個設備的存在、做基礎設置** ```c= // Linux 初始化設備時使用的函數(宏) module_init(...); ``` Linux 內核在使用驅動時需要 **裝載驅動**,而在裝載前需要進行 **初始化** 工作,而在這個奏中可能會涉及如下 > 可大概理解為 main 函數註冊(只不過是指驅動部份) * **註冊 ++設備文件++**,在 `/dev` 目錄下建立設備文件: **任何一個驅動「都需要設備文件」**,否則應用就無法操控設備(應用透過開啟文件、寫入... 等等行為來操作設備) ```c= // Linux 建立設備文件(宏) misc_register(...); ``` * **指定驅動訊息**: 也就是對於驅動程式的描述(像是 Manifest);像是可以透過 `modinfo` 命令獲取驅動程式的資訊 ```c= // Linux 描述設備(宏) MODULE_LICENSE(...); // 開源協議 MODULE_AUTHOR(...); // 作者姓名 MODULE_ALIAS(...); // 驅動別名 MODULE_DESCRIPTION(...); // 驅動描述 ``` * **指定系統回調**: Linux 驅動中,包還多種行為回調;**每當符合某些條件時… 系統就會呼叫這些驅動回調,並發送事件給 Linux 驅動** > 像是 `read`、`write`... 等等 ```c= // 像 Linux 註冊驅動回調(宏) misc_register(...); ``` :::info * IoC 的反轉點:由底層(系統)作為高層模組,呼叫驅動開發(低層) ::: * **釋放設備** Linux 系統退出時需要卸載 Linux 驅動,在卸載的過程中就會 **釋放 Linux 驅動佔用的資源**,像是 * **註銷 ++設備文件++**,刪除 `/dev` 目錄下的設備文件 ```c= // Linux 移除設備文件(宏) misc_deregister(...) ``` * 釋放虛擬內存地址空間 ```c= // Linux 釋放設備時使用的函數(宏) module_exit(...) ``` 2. **撰寫驅動的業務邏輯**: 這個步驟是開發者主要要處理的關鍵,在可以做一些與業務邏輯相關的工作,像是 * COM 驅動會根據傳輸率進行數據交換 * 傳送打印指令給影印機 3. **編寫 Linux 驅動**: * **Makefile 文件**: Linux 內核源碼的編譯歸則是通過 **Makefile 文件定義**,因此一個驅動會對應一個 Makefile 文件 * **編寫 Linux 驅動程序** Linux 驅動可以選擇一同編譯進內核,也可以作為單獨模塊,單獨編譯 :::info * **指令安裝、卸載 Linux 驅動**: * 如果驅動編譯進內核源碼,那在 Linux 啟動時,就會自動被裝載(不須指令安裝) * 如果使用單獨模塊,那就要使用 `insmod`、`modprobe` 命令裝載驅動(卸載使用 `rmmod` 指令) ::: ### 編譯檔 Makefile * Makefile 檔案的編寫設置如下 ```shell= obj-m := hello_world.o ## 依賴 process.o、data.o 模塊 hello_word-y := process.o data.o ``` 以上是設置將 `hello_world` 驅動編譯為模組(`obj-m`),並依賴兩個模塊;而對於 Makefile 不同設置會對編譯起到不同作用,如下表 | Makefile 設置 | 說明 | 補充 | | - | - | - | | `obj-m` | 將驅動作為模塊編譯 | 以上例來說,**會將 `hello_world.o` 編譯 `hello_world.ko` 文件中** | | | | 之後讓使用者使用 `insmod`、`modprobe` 命令裝載驅動 | | `obj-y` | 將驅動編譯進核心 | **`hello_world.o` 會編譯進 `built-in.o` 文中** | | | | 這時的文件是中間文件,最終所有驅動都會編譯進 `<核心源碼/drivers/char/built-in.o>` 文件 | | | | 是否真正編譯可以在編譯核心前,使用 `make menuconfig` 指令設置 | | `<module>-y`、`<module>-obj` | 當 Linux 驅動依賴其他模塊時,須使用這個方式標示 | `<module>` 代表的是模塊名 | ### 編寫 Linux 驅動檔 HelloWorld - 用戶、核心空間 * 「**用戶空間**」、「**內核空間**」不能相互訪問,那要如何交互傳遞訊息呢? 可以使用以下兩個方案 * 使用存在 `/proc` 資料夾下的「**虛擬文件**」 * 使用存在 `/dev` 資料夾下的「**設備文件**」 > 所以用戶空間假設要透過 設備文件訪問內核空間的話,就要做一個可以訪問內核空間的驅動程式,然後用戶空間的程式通過「設備文件」與「驅動程式」交互 ```mermaid graph TB; subgraph 使用者程式 內核空間 使用者空間 end subgraph /dev 設備文件 end 使用者空間 -.-> 設備文件 設備文件 --> 驅動 -.-> 內核空間 ``` * **Linux 最簡單的驅動檔** 如下(不需要 `/dev`, `/proc`... 單純註冊一個驅動) ```c= #include <linux/module.h> #include <linux/init.h> #include <linux/kernel.h> #include <linux/fs.h> #include <linux/miscdevice.h> #include <asm/uaccess.h> static int hello_world_init(void) { printk("hello_world_init_success.\n"); return 0; } static void hello_world_exit(void) { printk("hello_world_exit_success.\n"); } // 註冊初始化、退出的驅動函數 module_init(hello_world_init); module_exit(hello_world_exit); ``` :::warning * **為甚麽不使用平常常用的 `printf` 就好了**? 因為在虛擬記憶體的規劃中,有分兩個大區塊「**用戶空間**」、「**內核空間**」,**這兩個空間是無法相互訪問的** * `printf` 就存在使用者空間中 > 如果在驅動開發中使用 `printf`,那編譯就會拋出異常,因為找不到 `stdio.h` 檔 :::info * 用戶空間的程式使用的頭文件 **是存在 `usr/include` 目錄** 之下 > ::: * `printk` 就存在內核空間中(核心空間也不可以使用用戶空間的函數,像是 `malloc`、`free`... 等等) > 在驅動開發中就要使用 `printk`、`kmalloc` :::info * 核心空間如果要使用內核空間的功能,可以到 Linux 內核源碼之下的 `include/` 目錄找尋需要的頭文件 > ![image](https://hackmd.io/_uploads/Sy_3hO1H6.png) ::: ::: ### 編譯 Linux 驅動檔 - ko 檔 * 編譯以上,最簡單的 Linux 驅動檔 HelloWorld,請用以下命令 ```shell= # 編譯命令如下 ## `-C` -> 指定 Header ## `M` -> 以編譯模塊 make -C /usr/src/linux-headers-3.13.0-170-generic/ M=/root/drivers/1/hello_world/ ``` 1. 編譯輸出結果: 編譯 Linux 驅動模塊後,**會輸出 `hello_world.ko`、`hello_world.o` 檔** > ![image](https://hackmd.io/_uploads/BJCDyc1H6.png) 2. 編譯輸出的檔案: 除了 `.o`、`.ko` 之外還有以下文件(一般來說不會使用到) > ![image](https://hackmd.io/_uploads/r11eXPgBa.png) :::warning * **缺少 `linux-header` 標頭檔,請先安裝以下套件** ```shell= apt install build-essential apt install linux-headers-generic ``` * **「`insmod Unknown symbol mcount (err 0)`」錯誤**? **make 版本如果是 4.8 以下,在 `insmod` 安裝驅動時就會出這個問題** > 安裝 make 4.8 版本 * 使用 `insmod` 加載時,如果 **出現 `disagrees about version of symbol module_layout` 錯誤**,那要注意編譯時使用的 `linux-header` > 或是 `Invalid module format` 錯誤 > > ![image](https://hackmd.io/_uploads/rk6GRP-S6.png) 如果使用與核心不同的 `linux-header` 可能就會造成這樣的問題,這時請按照核心版本去下載 `linux-header` > 我原先使用的是 Docker 容器的 `Ubuntu 14.04`,但就發生這個問題;所以我後來改用 `Oracle VM VirtualBox` ```shell= ## 查看核心版本 uname -r ## 使用核心版本對應的 linux-header 去編譯 make -C /usr/src/linux-headers-4.4.0-148-generic/ M=/home/kyle/Desktop/test/ ``` > ![image](https://hackmd.io/_uploads/r1r_HYgr6.png) ::: ### 使用 ko 檔 - 安裝、依賴、卸載 * **手動操作 `insmod`、`rmmod`... 指令** 1. **`depmod` 命令**:檢查驅動的依賴 ```shell= # 1. 記得使用 sudo 權限 # 2. 要用完整路徑,否則會錯誤 sudo depmod /home/kyle/Desktop/drivers/1/hello_world.ko ``` :::warning * 如果你之後要使用 `insmod` 命令安裝驅動,那這個步驟非必須;但如果你要用 `modprobe` 命令安裝驅動,那這個步驟就是必須 > `depmod` 會將 Linux 驅動模塊文件(包括路徑),添加到 `lib/module/<像是 4.4.0-148-generic>/modules.dep` 文件中 > ![image](https://hackmd.io/_uploads/ByFtgKKHa.png) ::: 2. **`insmod` 命令**:**加載 Linux 驅動(`.ko`)檔** ```shell= # 記得使用 sudo 權限 # 安裝驅動 - 方案 1 sudo insmod hello_world.ko ## --------------------------------------------------------------- # 安裝驅動 - 方案 2 # 安裝時不須使用全路徑(因為 depmod 已經設定),也不需要 `.ko` 結尾! sudo modprobe hello_world ``` :::danger * **加載失敗**? * 如果使用 Docker 容器,那可能會無法正常加載成功;如果要模擬 insmod 加載,建議使用「模擬器」 (像是 `Oracle VM VirtualBox`) * 如果已經加載過,請先執行卸載 ::: * **`dmesg` - 查看日誌**: 如果是第一次加載,並加載成功的話不會返回任何資訊,但可以使用 `dmesg` 查看 ```shell= dmesg | grep hello_world | tail -n 2 ``` > ![image](https://hackmd.io/_uploads/SyzP7YxHa.png) * **`lsmod` - 查看模塊**: 加載完成後就可以使用 `lsmod` 指令查看是否有 `hello_world` 模塊 ```shell= lsmod | grep hello_world ``` > ![image](https://hackmd.io/_uploads/rkMCmYgHp.png) * **`syslog` - 查看 `printk` 打印出來的訊息**: ```shell= cat /var/log/syslog | grep hello_world | tail -n 2 ``` > ![image](https://hackmd.io/_uploads/HkuqwFlr6.png) 3. **`rmmod` 命令**:**卸載 Linux 驅動(`.ko`)檔** ```shell= # 記得使用 sudo 權限 sudo rmmod hello_world.ko ``` > 同樣可以使用 日誌(`dmesg`)、模塊(`lsmod`) 查看… 這裡就不特別演示了 ### 驅動相關訊息 - MODULE_XXX * 我們可以透過 **`modinfo` 指令**,來查看模塊的相關訊息,其中就包括如下表 ```shell= modinfo hello_world.ko ``` | 模塊資訊 | 程式中使用的設置 | | - | - | | 作者 | `MODULE_AUTHOR` | | 描述 | `MODULE_DESCRIPTION` | | 別名 | `MODULE_ALIAS` | | 開源協議 | `MODULE_LICENSE` | > 其中也包括模塊的依賴 > ![image](https://hackmd.io/_uploads/BksnwdeB6.png) * 一般來說會將這些設置放在驅動程式的最下方,接下來我們將使用這些宏,再加上指令的方式查看編譯過後的 `.ko` 檔 ```c= #include <linux/module.h> #include <linux/init.h> #include <linux/kernel.h> #include <linux/fs.h> #include <linux/miscdevice.h> #include <asm/uaccess.h> static int hello_world_init(void) { printk("hello_world_init_success.\n"); return 0; } static void hello_world_exit(void) { printk("hello_world_exit_success.\n"); } // 註冊初始化、退出的驅動函數 module_init(hello_world_init); module_exit(hello_world_exit); // 模塊資訊 MODULE_AUTHOR("Hello author"); MODULE_DESCRIPTION("Hello world device"); MODULE_ALIAS("Hello world module."); MODULE_LICENSE("GPL"); ``` 重新編譯後(編譯指令請看上面小節),使用 `modinfo` 指令查看編譯模塊 `.ko` 檔的資訊… 可以看到編譯過後的結果確實跟程式的設置相同 > ![image](https://hackmd.io/_uploads/Skvis_xST.png) :::info * **`vermagic` 表示什麼**? **Linux 驅動模塊在哪一個 Linux 內核版本下被編譯**(當然也包括了一些其他資訊) 從上可以看出,我使用的 Linux 內核是 `3.13.0-170-generic SMP mod_unload modversions retpoline` > `3.13.0-170-generic` 是 Kernel 版本號、`SMP` 代表支持對稱多處理器 > > * `mod_unload` 是 Linux kernel 模組支持卸載的一個特徵 > * `modversions` 是一個用於 kernel 模組的特殊標記 > * `retpoline ` 是一種用於減緩 Spectre 變體 2 攻擊的一種技術。 ::: ### 註冊、註銷 - dev 設備文件 * 設備文件被創建後,會存在 **`/dev` 目錄** 下;**建立時使用 `misc_register(...)` 函數、註銷時使用 `misc_deregister(...)` 函數** :::danger * **設備文件不能用一般的 `I/O` 函數建立** ::: 1. 這裡主要關注 「`miscdevice` 結構」的實現,它的幾個成員如下 | 成員 | 功能概述 | 補充 | | - | - | - | | `.name` | 設備文件名稱 | - | | `.minor` | 次要設備號 | 動態生成可以使用 `MISC_DYNAMIC_MINOR` 宏;也可以透過 `register_chrdev_region`、`alloc_chrdev_region` 函數同時指定主設備、次設備號 | | `.fops` | 設備被操作時,響應系統的函數 | 該成員的類型是 `file_operations`,然而這個類型有許多函數指標可以設置 | :::info * 主設備號不用設置? **雜項設備(`misc`)只能設置次要設備號**,主設備號預設為 10;主設備號 10 的設備是 **Linux 系統中擁有共同特性的簡單字符設備**(就是雜項設備) 也可以透過 `/proc/devices` 查看系統預設的主設備號 > ![image](https://hackmd.io/_uploads/S1o2Z_QS6.png) ::: 2. 再將該模塊自己實現的 `miscdevice` 的結構,透過 `misc_register` 函數註冊 ```c= #include <linux/module.h> #include <linux/init.h> #include <linux/kernel.h> #include <linux/fs.h> #include <linux/miscdevice.h> #include <asm/uaccess.h> #define DEVICE_NAME "hello_world" // 描述設備文件觸發的事件、對應的回調函數指標(下一節說明) static struct file_operations dev_fops = { .owner = THIS_MODULE }; // 描述存在 `/dev` 之下文件資訊 // 先關注這個 static struct miscdevice misc = { .name = DEVICE_NAME, .minor = MISC_DYNAMIC_MINOR, .fops = &dev_fops }; static int hello_world_init(void) { int ret; ret = misc_register(&misc); printk("hello_world_init_success, ret = %d.\n", ret); return ret; } static void hello_world_exit(void) { misc_deregister(&misc); printk("hello_world_exit_success.\n"); } module_init(hello_world_init); module_exit(hello_world_exit); MODULE_AUTHOR("Hello author"); MODULE_DESCRIPTION("Hello world device"); MODULE_ALIAS("Hello world module."); MODULE_LICENSE("GPL"); ``` * 再次編譯為 `.ko` 檔案,並動態掛載驅動到系統中 ```shell= make -C /usr/src/linux-headers-4.4.0-148-generic/ M=/home/kyle/Desktop/drivers/2/ ``` > ![image](https://hackmd.io/_uploads/ryAS9mGBa.png) ```shell= ## 動態安裝 sudo insmod hello_world.ko ## 查看設備號(主 10, 次要 53) ls -laF /dev/hello_world ``` > ![image](https://hackmd.io/_uploads/rkKAcQfBa.png) ### 設定設備回調函數 * 在這裡要操縱「**用戶空間**」的應用程式、「**內核空間**」的驅動程式,並在驅動的地方撰寫兩邊交互的邏輯 :::success * **驅動開發的角度** **==驅動所站的角度是「核心」==**,所以會以「核心的角度」來操控內核函數(內核函數的名稱也是這樣設計的) * **用戶空間 跟 內核空間 無法相互訪問,如何傳遞數據**? 透過以下函數 | 函數 | 功能 | | - | - | | `copy_from_user` | 將數據從「使用者空間」複製到「核心空間」 | | `copy_to_user` | 將數據從「核心空間」複製到「使用者空間」 | ::: * 在 Linux 驅動中,回調函數 **關注的是 `file_operations` 結構**!(像是最常見的是 `read`、`write`) | `file_operations` 結構的成員 | 概述 | | - | - | | `read` | 函數指標,當系統需要 **讀取設備** 時,會回調 read 的函數 | | `write` | 函數指標,當系統需要 **寫入設備** 時,會回調 write 的函數 | ```c= #include <linux/module.h> #include <linux/init.h> #include <linux/kernel.h> #include <linux/fs.h> #include <linux/miscdevice.h> #include <asm/uaccess.h> #define DEVICE_NAME "hello_world" static unsigned char mem[10000]; static char is_data_read_done = 'y'; static int written_count = 0; static struct file_operations dev_fops = { .owner = THIS_MODULE, // 註冊 Kernel Callback! .read = word_count_read, .write = word_count_write }; static struct miscdevice misc = { .name = DEVICE_NAME, .minor = MISC_DYNAMIC_MINOR, .fops = &dev_fops }; // Kernel 收到有數據「外部」傳給 hello 設備 // 就會呼叫這個 Callback,要求 hello 驅動處理 // // 站在 Kernel 的角度,將數據從 Kernel 傳送到 User,所以叫 read(核心複製到使用者) static ssize_t word_count_read(struct file *file, char __user *buf, size_t count, loff_t *ppos) { if(is_data_read_done == 'y') { return 0; } // 將數據從「核心空間」複製到「使用者空間」 copy_to_user(buf, (void*) mem, written_count); printk("--- read count to user(from kernel): %d\n", (int) written_count); is_data_read_done = 'y'; return written_count; } // Kernel 收到有數據從「應用」傳給該設備 // 就會呼叫這個 Callback,要求 hello 驅動處理 // // 站在 Kernel 的角度,將數據從 User 複製到 Kernel,所以叫 write(應用對核心寫入) static ssize_t word_count_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos) { // 將數據從「使用者空間」複製到「核心空間」 copy_from_user(mem, buf, count); is_data_read_done = 'n'; written_count = count; printk("--- user written count(to kernel): %d\n", (int)count); return count; } static int hello_world_init(void) { int ret; ret = misc_register(&misc); printk("--- hello_world_init_success, ret = %d.\n", ret); return ret; } static void hello_world_exit(void) { misc_deregister(&misc); printk("--- hello_world_exit_success.\n"); } module_init(hello_world_init); module_exit(hello_world_exit); MODULE_AUTHOR("Hello author"); MODULE_DESCRIPTION("Hello world device"); MODULE_ALIAS("Hello world module."); MODULE_LICENSE("GPL"); ``` :::warning * **參數中使用的 `__user` 宏**? 因為虛擬記憶體有將記憶體劃分為兩個空間(核心、使用者),但 **兩方不能相互訪問**;所以我們 **可以透過「`__user` 宏」**,來了解到 **「數據存在使用者空間」** * **為甚麽不使用** `word_count_write` 函數的 `count` 參數? 這裡另外使用 `written_count` 參數來紀錄的原因是因為,接下來要使用 **cat 命令測試讀取數據**,而這個命令會一次讀取 65536 字節 > **與實際真正字節數不同**,所以要另外紀錄 ::: 1. **編譯、安裝驅動** ```shell= ## 編譯 ko 檔 make -C /usr/src/linux-headers-4.4.0-148-generic/ M=/home/kyle/Desktop/drivers/3/ ## 移除先前驅動 sudo rmmod hello_world.ko ## 再次動態安裝驅動 sudo insmod hello_world.ko ``` > ![image](https://hackmd.io/_uploads/SkyW93uHp.png) 2. **測試寫入、讀取** * 使用 echo 對 `/dev/hello_world` 裝置 **寫入** 這個行為會觸發 `hello_world` 的 **`.write` 函數指標**,也就是 `word_count_write` 函數;**++將使用者空間數據寫入核心空間++** * 使用 cat **讀取** `/hello_world` 裝置的資訊 這個行為會觸發 `hello_world` 的 **`.read` 函數指標**,也就是 `word_count_read` 函數;**++把數據從核心空間讀取到使用者空間++**(使用者空間應用就是 cat) ```shell= # 切換到 root sudo su echo "hello 1 2 3" > /dev/hello_world cat /dev/hello_world exit ``` > ![image](https://hackmd.io/_uploads/SJJG5hurT.png) :::danger * **寫入裝置時 `Permission denied` 錯誤**? 命令中使用了管線(`>`)操作符,而這個操作符是由 shell 處理的;在這種情況下,sudo 只會應用於 echo 命令,而不會應用於重定向操作符 所以要先切換到 root(`sudo su`)再操作 ```shell= ## 出問題的命令 ------------------------------------------------------- sudo echo "hello123" > /dev/hello_world ## 返回輸出 ------------------------------------------------------- kyle@kyle-VirtualBox:~/Desktop/drivers/3$ sudo echo "hello123" > /dev/hello_world bash: /dev/hello_world: Permission denied ``` ::: 3. 使用 `dmesg` 命令查看 `printk` 的輸出 ```shell= dmesg | grep "\-\-\-" ``` > ![image](https://hackmd.io/_uploads/HkFis2_rp.png) ### 進階 - 計算輸入字元數量 * 將上面的範例進行拓展,目標是計算使用者輸入的「**單字數量**」;也就是重點多了兩個判斷 1. **判斷當前字元是否是算是一個 空白字元** ```java= static int is_space_char(char c) // ASCII 判斷 // 以空格、換行、`\t`、回車 為關鍵,遇到這幾個字元,就算是新字元的開始 if(c == ' ' || c == 9 || c == 13 || c== 10) { return TRUE; } else { return FALSE; } } ``` > ![image](https://hackmd.io/_uploads/ByW19sYSp.png) 2. 將字元陣列傳入,並計算有這個陣列中有幾個「**單字**」,細節請看註解 ```java= static int get_word_count(const char *buf) { // 當遇到 `\0`,代表陣列的資料已經讀完 if(*buf == '\0') { return 0; } // 計算單字數量,預設為一個單字 int count = 1; // 如果第一個單字是空格(或是 \t, \n... 等等) if(is_space_char(*buf) == TRUE) { // 改成從 0 開始計算 count--; } int index = 0; int is_space = 0; // 是否已經遇到空格 char curChar = ' '; // 掃描字符陣列中的所有「單字」 for(; (curChar = *(buf + index)) != '\0'; index++) { // 已經遇到空格,並且下一個非空格的單字 if(is_space == 1 && is_space_char(curChar) == FALSE) { // 設定沒有遇到空字元 is_space = 0; } // 已經遇到空格,並且下一個仍是空格 else if(is_space == 1 && is_space_char(curChar) == TRUE) { // 繼續往陣列中下一個字元判斷 continue; } // 遇到空格 if(is_space_char(curChar) == TRUE) { // 代表多了一個單字 count++; // 設定遇到空字元 is_space = 1; } } // 字符串用一個、多個空格結尾,不計數 if(is_space_char(*(buf + index - 1)) == TRUE) { count--; } return count; } ``` 3. 完整的驅動程式如下 ```java= #include <linux/module.h> #include <linux/init.h> #include <linux/kernel.h> #include <linux/fs.h> #include <linux/miscdevice.h> #include <asm/uaccess.h> #define TRUE 1 #define FALSE 0 #define DEVICE_NAME "hello_world" static unsigned char mem[10000]; static int word_count = 0; static int is_space_char(char c) { if(c == ' ' || c == 9 || c == 13 || c== 10) { return TRUE; } else { return FALSE; } } static int get_word_count(const char *buf) { if(*buf == '\0') { return 0; } int count = 1; if(is_space_char(*buf) == TRUE) { count--; } int index = 0; int is_space = 0; char curChar = ' '; for(; (curChar = *(buf + index)) != '\0'; index++) { if(is_space == 1 && is_space_char(curChar) == FALSE) { is_space = 0; } else if(is_space == 1 && is_space_char(curChar) == TRUE) { continue; } if(is_space_char(curChar) == TRUE) { count++; is_space = 1; } } if(is_space_char(*(buf + index - 1)) == TRUE) { count--; } return count; } static ssize_t word_count_read(struct file *file, char __user *buf, size_t count, loff_t *ppos) { unsigned char tmp[4]; // big endian 轉換 tmp[0] = word_count >> 24; printk("--- tmp[0]: %d\n", (int) tmp[0]); tmp[1] = word_count >> 16; printk("--- tmp[1]: %d\n", (int) tmp[1]); tmp[2] = word_count >> 8; printk("--- tmp[2]: %d\n", (int) tmp[2]); tmp[3] = word_count; printk("--- tmp[3]: %d\n", (int) tmp[3]); copy_to_user(buf, (void*) tmp, 4); printk("--- count: %d\n", (int) count); printk("--- read word count: %d\n", (int) word_count); return count; } static ssize_t word_count_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos) { copy_from_user(mem, buf, count); mem[count] = '\0'; // 計算對該設備的輸入「單字」數量 word_count = get_word_count(mem); printk("--- write word count: %d\n", (int) word_count); return count; } static struct file_operations dev_fops = { .owner = THIS_MODULE, .read = word_count_read, .write = word_count_write }; static struct miscdevice misc = { .name = DEVICE_NAME, .minor = MISC_DYNAMIC_MINOR, .fops = &dev_fops }; static int hello_world_init(void) { int ret; ret = misc_register(&misc); printk("--- hello_world_init_success, ret = %d.\n", ret); return ret; } static void hello_world_exit(void) { misc_deregister(&misc); printk("--- hello_world_exit_success.\n"); } module_init(hello_world_init); module_exit(hello_world_exit); MODULE_AUTHOR("Hello author"); MODULE_DESCRIPTION("Hello world device"); MODULE_ALIAS("Hello world module."); MODULE_LICENSE("GPL"); ``` :::danger * 以上程式不能使用 `cat` 命令測試 ::: * 編譯以上程式 ```shell= make -C /usr/src/linux-headers-4.4.0-148-generic/ M=/home/kyle/Desktop/drivers/4/ ``` > ![image](https://hackmd.io/_uploads/HydTkWQIa.png) ## Linux 驅動測試 對於 Linux 驅動,一開始可以在 Ubuntu Linux 中開發、測試;但在基礎開發完後仍有在 真實硬體(目標機)上測試 > 上面那種手動使用指令對 `/dev` 裝置輸入,並透過 `dmesg` 確認的方法,並不算真正的測試 ### Ubuntu 下測試驅動 :::warning 進行以下測試之前,請先確保「**進階 - 計算輸入字元數量**」小節的驅動已經安裝運行 ::: * 為了要讓測試效果正接近真正使用的環境,一般來說需要特別寫個測試程式;以下透過開啟文件來直接測試驅動 ```java= #include <stdio.h> #include <fcntl.h> #include <unistd.h> #include <stdlib.h> #include <string.h> int main(int argc, char *argv[]) { int testDev; unsigned char buf[4]; testDev = open("/dev/hello_world", O_RDWR); if(testDev == -1) { printf("Cannot open hello_world file, did you insmod?\n"); return -1; } if(argc > 1) { char* str = argv[1]; write(testDev, str, strlen(str)); printf("The string (%s) been written.", str); } read(testDev, buf, 4); int word_count = ((int) buf[0] << 24 | (int) buf[1] << 16 | (int) buf[2] << 8 | (int) buf[3] ); printf("word byte display: %d, %d, %d, %d\n", buf[0], buf[1], buf[2], buf[4]); printf("word count: %d\n", word_count); close(testDev); return 0; } ``` 編譯測試程式並運行 ```shell= # 編譯測試程式 gcc hello_world_test.c -o hello_world_test # 空單字 sudo hello_world_test # 兩個單字 sudo hello_world_test "Yoyo 123" ``` > ![image](https://hackmd.io/_uploads/S1Tm02YBT.png) ## Appendix & FAQ :::info ::: ###### tags: `設備開發`