---
# System prepended metadata

title: Linux Driver 基本架構
tags: [driver]

---

# Linux Driver 基本架構

Linux Device Driver的基本架構:
:::success
每一種OS都會有驅動程式, 而驅動程式主要是做為應用程式要使用各種系統的週邊元件的一中間橋樑, kernel接收及管理應用程式的需求, 並將其需求轉送給驅動程式, 而kernel將會處理多工的問題, 為了讓各種驅動程式好撰寫, 所以每一種OS將會規定驅動程式的寫法, 就好像C語言也有規定應用程式的寫法, 而在linux的OS中規定驅動程式的寫法如下：
:::
:::info
*    目前都是使用C語言來撰寫驅動程式
*    程式務必include & include
*    每一個程式都會有一個進入點, 或者第一個指令的地方, linux device driver的進入點如下:
:::
```clike!
static int __init your_driver_name_init(void)
{
 return 0;
}
module_init(your_driver_name_init);
```
:::success
其進入點的名字你可以隨意取一個名字, 但是其原型的宣告方式必須是固定的, 請參考上面的範例程式, 最後必須使用一個巨集指令module_init()來宣告其為程式進入點。
:::
:::info
*    Linux的驅動程式可以是static link至kernel, 也可以是動態的方式, linux稱做module, 若是static的方式則必須要有完整的kernel source code, 並且在編譯時會將整個kernel source code編譯過, 而module的方式則不用, 只需要kernel的include檔即, 但是不管是static或者module其進入點宣告方式都一樣

*    離開或者是下載驅動程式時的進入點宣告如下 :
:::
```clike!
static void __exit your_driver_name_exit(void)
{
}
module_exit(your_driver_name_exit);
```
:::info
這和進入點一樣, 名字可以隨意取, 但是原型宣告則必須固定, 並且使用module_exit()巨集指令來確定離開驅動式的進入點。

以上是Linux的驅動程式的基本概念。
:::

---

### 最初的程式
```c=
#include <linux/module.h>
#include <linux/init.h>

MODULE_LICENSE("Dual BSD/GPL");

static int hello_init(void)
{
    printk(KERN_ALERT "driver loaded\n");

    return 0;
}

static void hello_exit(void)
{
    printk(KERN_ALERT "driver unloaded\n");
}

module_init(hello_init);
module_exit(hello_exit);
```

建構驅動程式 Makefile:
~~~makefile
obj-m := hello.o

all:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

clean:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
~~~

:::success
「obj-m」指定最後要建立的 .ko 檔是以哪些 .o 檔構成的。 想建立「hello.ko」，這邊指定的就是「hello.o」，「:=」為 GNU make 的擴充指派運算子。

make 指令的 「-C」選項，是指定 Makefile 的位置，此範例是以 uname 取得 kernel 版本，如果編譯給其它 kernel 使用，需修改路徑。

執行 make 即會得到所需的 「hello.ko」。
:::

| 指令 | 描述 |
|:------:|:-----------:|
| file hello.ko | 確認檔案類型，此處為 ELF binary 檔 |
| modinfo hello.ko | 取得驅動程式的授權與版本標記 |

:::info
如果想看 pre-processor (預處理;cpp) 之後的結果，並將這個結果存成檔案的話，可以在 Makefile 中多加這一行：

CFLAGS += -E

不需要行號的話，可以再加「-P」。
make 之後，就會在 gcc 的 -o 指定檔案存入 pre-process 結果。
:::

### 驅動程式的載入與卸載

|  指令   |     描述     |
|:------:|:-----------:|
| insmod hello.ko | 載入 module |
| lsmod hello | 列出現在載入的所有 kernel module |
| rmmod hello | 卸載驅動程式 |

:::warning

此外「/proc/modules」會列出所有已載入的驅動程式。

modprobe 同樣可用來載入驅動程式，但有一些不同：
*    引數不是檔名，而是「module名」。
*    會自動到 /lib/modules/'uname -r' 搜尋檔案會參考 /lib/modules/'uname -r'/modules.dep ，如果有需要用到其它的 modules，就會自動一起載入
:::


### 自訂 Makefile

加上 「V=1」就可以看到詳細的建構過程。
~~~makefile
make V=1 -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
~~~

支援許多個原始檔：

~~~makefile
CFILES := main.c sub.c
obj-m := hello.o

hello-objs := $(CFILES:.c=.o)

all:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

clean:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
~~~

這邊要注意的是 CFILES 中的檔案名稱，不能與 obj-m 的名稱有相同的情況，否則 make 會產生 confusion。


### 補充說明
:::success

對多個 .c 檔情況做一個說明：希望創建一個名叫 hello 的 module，有三個 .c 檔，分別為 hello.c, file1.c 和 file2.c。
:::
這樣是有問題的
:::info

*    在 Makefile 中 obj-m := hello.o，這是指定module的名稱
*    hello-objs := file1.o file2.o hello.o，這裡是說 hello module 包括的 .obj 檔，如果在裡面不填寫 hello.o，那麼實際並沒有編譯hello.c，而是在 CC[M] file1.o 和 file2.o，通過 LD[M] 得到模塊 hello.o，如果在這裡填寫了 hello.o，那麼在 obj-m 和 hello-objs 中都含有 hello.o，對 make 來講會產生循環和混淆，因此不能這樣寫。
:::

:::danger
CC[M]: 
The M stands for 'modules' as the source files are ultimately compiled into a loadable kernel module, an object file with the .ko extension
:::

:::success
如果我們由多個 .c 檔來構造一個模塊，那麼 .c 檔的名字不能和 module 名字一樣， 在這個例子中我們可以將 hello.c 改名為 main.c，在Makefile中obj-m := hello.o ，hello-objs = file1.o file2.o hello_main.o。
:::

*    main.c
```c=
#include <linux/module.h>
#include <linux/init.h>

MODULE_LICENSE("Dual BSD/GPL");

extern void sub(void);

static int hello_init(void)
{
    printk(KERN_ALERT "driver loaded\n");

    sub();

    return 0;
}

static void hello_exit(void)
{
    printk(KERN_ALERT "driver unloaded\n");
}

module_init(hello_init);
module_exit(hello_exit);
```

*    sub.c
```c=
#include <linux/module.h>
#include <linux/init.h>

void sub(void)
{
    printk("%s: sub() called\n", __func__);
}
```

*    Makefile
~~~makefile
CFILES = main.c sub.c
obj-m += hello.o
hello-objs := $(CFILES:.c=.o)

all:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

clean:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
~~~


### 加到 kernel 配置選單
:::success
可以將自己寫的驅動程式加到 kernel 配置選單內，這邊試著把它加到 「Character devices」分類底下。

首先，到 Linux kernel 的原始碼目錄(/usr/src/kernels/2.6.23.1-42.fc8-i686)內的 devices/char 建立子目錄，然後將原始檔放到裡面。
:::

```Linux=
cd drivers/char
mkdir hello
cd hello
ls
```
Makefile main.c sub.c 修改如此 Makefile：

~~~makefile
obj-$(CONFIG_HELLO) += hello.o
hello-objs := main.o sub.o
~~~

接著修改 Character devices 本身的 Makefile 與 Kconfig。

在 Makefile 中，新增一行：

~~~makefile
obj-$(CONFIG_HELLO) += hello/
~~~

在 Kconfig 中，新增四行：

```Kconfig=
config HELLO
tristate "hello driver"
---help---
This is a sample driver.
```

做好上述修正後，在配置 kernel 的選單中，就會看到「hello」驅動程式了。

```Linux=
make menuconfig
```
---

### LINUX Driver modules param
使用下面的巨集時需要包含標頭檔<linux/moduleparam.h>。

```C=
module_param(name, type, perm);
```
*    name既是使用者看到的參數名，又是模組內接受參數的變數； 

*    type表示參數的資料類型，是下列之一：byte, short, ushort, int, uint, long, ulong, charp, bool, invbool；

*    perm指定了在sysfs中相應文件的存取權限，存取權限與linux檔存取權限相同的方式管理。

0表示完全關閉在sysfs中相對應的項。
| perm 選項 | 對應的數組 |              功能              |
|:--------:|:---------:|:-----------------------------:|
|  S_IRUSR |   00400   |        允許文件所有者可讀        |
|  S_IWUSR |   00200   |        允許文件所有者可寫        |
|  S_IXUSR |   00100   |       允許文件所有者可執行       |
|  S_IRGRP |   00040   |    允許文件所有者同组的用户可讀    |
|  S_IWGRP |   00020   |    允許文件所有者同组的用户可寫    |
|  S_IXGRP |   00010   |   允許文件所有者同组的用户可執行   |
|  S_IROTH |   00004   |  允許與文件所有者不同组的用户可讀  |
|  S_IWOTH |   00002   |  允許與文件所有者不同组的用户可寫  |
|  S_IXOTH |   00001   | 允許與文件所有者不同组的用户可執行 |

用法如下：

```C=
static unsigned int int_var = 0;
module_param(int_var, uint, S_IRUGO);
```

*    這些必須寫在模組原始檔案的開頭部分。即int_var是全域的。
*    使模組原始檔案內部的變數名與外部的參數名有不同的名字:
```C=
module_param_named(name, variable, type, perm);
```

*    name是外部可見的參數名
*    variable是原始檔案內部的全域變數名
*    此外module_param通過module_param_named實現，只不過name與variable相同。

例如：

```C=
static unsigned int max_test = 9;
module_param_named(maximum_line_test, max_test, int, 0);
```

如果模組參數是一個字串時，通常使用charp類型定義這個模組參數。核心複製使用者提供的字串到記憶體，並且相對應的變數指向這個字串。

例如：

```C=
static char *name;
module_param(name, charp, 0);
```

另一種方法是通過巨集module_param_string()讓核心把字串直接複製到程式中的字元陣列內。

```C=
module_param_string(name, string, len, perm);
```

*    name是外部的參數名
*    string是內部的變數名
*    len是以string命名的buffer大小（可以小於buffer的大小，但是沒有意義）
*    perm表示sysfs的存取權限（或者perm是零，表示完全關閉相對應的sysfs項）。

例如：

```C=
static char species[BUF_LEN]；
module_param_string(specifies, species, BUF_LEN, 0);
``` 

如果需要傳遞多個參數可以通過巨集module_param_array()實現。 

```C=
module_param_array(name, type, nump, perm);
```

*    name既是外部模組的參數名又是程式內部的變數名，
*    type是資料類型
*    perm是sysfs的存取權限
*    指標nump指向一個整數，其值表示有多少個參數存放在陣列name中
*    值得注意是name陣列必須靜態配置。

例如：

```C=
static int fish[MAX_FISH];
static int nr_fish;
module_param_array（fish, int, &nr_fish, 0444）; //最終傳遞陣列元素個數存在nr_fish中
```

Example:

```C=
#include <linux/init.h>
#include <linux/module.h>
#include <linux/moduleparam.h>

static char *who= "World";
module_param(who, charp, 0);

static int hello_init(void)
{
        printk(KERN_INFO "Hello, %s!\n", who);
        return 0;
}
static void hello_exit(void)
{
        printk(KERN_INFO "Goodbye, %s!\n", who);
}

module_init(hello_init);
module_exit(hello_exit);
```

modinfo 指令:

```Linux=
[root@Pomelo-14-61 LKMP]# modinfo hello-param2.ko
filename:       hello-param2.ko
srcversion:     EE5675FFF077444D49D7DB2
depends:        
vermagic:       2.6.32-279.22.1.el6.x86_64 SMP mod_unload modversions
parm:           who:charp
```

Result 1:

```Linux=
[root@Pomelo-14-61 LKMP]# insmod hello-param2.ko
[root@Pomelo-14-61 LKMP]#
[root@Pomelo-14-61 LKMP]# tail -1 /var/log/messages
Oct  6 11:10:23 Pomelo-14-61 kernel: Hello, World!
[root@Pomelo-14-61 LKMP]#
[root@Pomelo-14-61 LKMP]# rmmod hello-param2
[root@Pomelo-14-61 LKMP]# tail -1 /var/log/messages
Oct  6 11:10:57 Pomelo-14-61 kernel: Goodbye, World!
```

Result 2:

```Linux=
[root@Pomelo-14-61 LKMP]# insmod hello-param2.ko who="Linux"
[root@Pomelo-14-61 LKMP]#
[root@Pomelo-14-61 LKMP]# tail -1 /var/log/messages
Oct  6 11:14:42 Pomelo-14-61 kernel: Hello, Linux!
[root@Pomelo-14-61 LKMP]#
[root@Pomelo-14-61 LKMP]# rmmod hello-param2
[root@Pomelo-14-61 LKMP]#
[root@Pomelo-14-61 LKMP]# tail -1 /var/log/messages
Oct  6 11:14:58 Pomelo-14-61 kernel: Goodbye, Linux!
```

參考資料:
> [大家一起來玩 Linux](https://victoryuembeddedlinux.blogspot.com/2010/10/linux-device-driver.html)
>> [Makefile教學](http://jasonblog.github.io/note/linux_device_driver_programming/04.html)
>>> [Linux kernel模組的開發](https://ithelp.ithome.com.tw/articles/10157518)
>>>> [超級資源網](https://meetonfriday.com/tags/#linux%20kernel)
>>>>> [傳遞模組參數](https://b8807053.pixnet.net/blog/post/339227792-linux-driver-modules-param)