---
title: 'Linux 設備開發 - 概述、編譯、安裝、HelloWrold'
disqus: kyleAlien
---
Linux 設備開發 - 概述、編譯、安裝、HelloWrold
===
## Overview of Content
使用 make 4.8 版本
[TOC]
## Linux 內核板號規則
Linux 內核版號由幾個部份組成,可以用指令查看當前系統版本號
```shell=
uname -r
```
* 主版號:`6`
* 次版號:`2`
* 修正版本號:`0`
* 微調版本號:`-`
* 特製 Linux 系統調校描述:`generic`
> 
<!-- ### 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/` 目錄找尋需要的頭文件
> 
:::
:::
### 編譯 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` 檔**
> 
2. 編譯輸出的檔案:
除了 `.o`、`.ko` 之外還有以下文件(一般來說不會使用到)
> 
:::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` 錯誤
>
> 
如果使用與核心不同的 `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/
```
> 
:::
### 使用 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` 文件中
> 
:::
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
```
> 
* **`lsmod` - 查看模塊**:
加載完成後就可以使用 `lsmod` 指令查看是否有 `hello_world` 模塊
```shell=
lsmod | grep hello_world
```
> 
* **`syslog` - 查看 `printk` 打印出來的訊息**:
```shell=
cat /var/log/syslog | grep hello_world | tail -n 2
```
> 
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` |
> 其中也包括模塊的依賴
> 
* 一般來說會將這些設置放在驅動程式的最下方,接下來我們將使用這些宏,再加上指令的方式查看編譯過後的 `.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` 檔的資訊… 可以看到編譯過後的結果確實跟程式的設置相同
> 
:::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` 查看系統預設的主設備號
> 
:::
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/
```
> 
```shell=
## 動態安裝
sudo insmod hello_world.ko
## 查看設備號(主 10, 次要 53)
ls -laF /dev/hello_world
```
> 
### 設定設備回調函數
* 在這裡要操縱「**用戶空間**」的應用程式、「**內核空間**」的驅動程式,並在驅動的地方撰寫兩邊交互的邏輯
:::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
```
> 
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
```
> 
:::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 "\-\-\-"
```
> 
### 進階 - 計算輸入字元數量
* 將上面的範例進行拓展,目標是計算使用者輸入的「**單字數量**」;也就是重點多了兩個判斷
1. **判斷當前字元是否是算是一個 空白字元**
```java=
static int is_space_char(char c)
// ASCII 判斷
// 以空格、換行、`\t`、回車 為關鍵,遇到這幾個字元,就算是新字元的開始
if(c == ' ' || c == 9 || c == 13 || c== 10) {
return TRUE;
} else {
return FALSE;
}
}
```
> 
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/
```
> 
## 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"
```
> 
## Appendix & FAQ
:::info
:::
###### tags: `設備開發`