linux 驅動開發(fā) - 內核模塊

一、Linux內核簡介

1.宏內核與微內核

內核分為四大類:單內核(宏內核);微內核;混合內核;外內核。

  • 宏內核(Monolithickernel)是將內核從整體上作為一個大過程來實現(xiàn),所有的內核服務都在一個地址空間運行,相互之間直接調用函數(shù),簡單高效。
    • Linux雖是宏內核,但已吸收了微內核的部分精華。Linux是模塊化的、多線程的、內核本身可調度的系統(tǒng),既吸收了微內核的精華,又保留了宏內核的優(yōu)點,無需消息傳遞,避免性能損失。
  • 微內核(Microkernel)功能被劃分成獨立的過程,過程間通過IPC進行通信,模塊化程度高,一個服務失效不會影響另外一個服務。
image

2.Linux體系架構

從兩個層次上來考慮操作系統(tǒng)

  • 用戶空間:包含了用戶的應用程序和C庫
    • GNU C Library (glibc)提供了連接內核的系統(tǒng)調用接口,還提供了在用戶空間應用程序和內核之間進行轉換的機制。
  • 內核空間:包含了系統(tǒng)調用,內核,以及與平臺架構相關的代碼
image

劃分原因

  • 現(xiàn)代CPU通常都實現(xiàn)了不同的工作模式

    • 以ARM為例:ARM實現(xiàn)了7種工作模式,不同模式下CPU可以執(zhí)行的指令或者訪問的寄存器不同:

      • (1)用戶模式 usr
      • (2)系統(tǒng)模式 sys
      • (3)管理模式 svc
      • (4)快速中斷 fiq
      • (5)外部中斷 irq
      • (6)數(shù)據(jù)訪問終止 abt
      • (7)未定義指令異常;
    • 以X86為例:X86實現(xiàn)了4個不同級別的權限,Ring0—Ring3 ;Ring0下可以執(zhí)行特權指令,可以訪問IO設備;Ring3則有很多的限制。

  • 為了保護內核的安全,把系統(tǒng)分成了2部分:用戶空間和內核空間是程序執(zhí)行的兩種不同狀態(tài),我們可以通過“系統(tǒng)調用”和“硬件中斷“來完成用戶空間到內核空間的轉移;

3.Linux的內核結構

Linux內核是整體式結構(宏內核),各個子系統(tǒng)聯(lián)系緊密,作為一個大程序在內核空間運行。

系統(tǒng)調用接口(system call interface,SCI)提供了某些機制執(zhí)行從用戶空間到內核的函數(shù)調用。

image
1)Linux內核組成(子系統(tǒng))
  • 進程調度(SCHED):控制多個進程對CPU的訪問。當需要選擇下一個進程運行時,由調度程序選擇最值得運行的進程。可運行進程實際上是僅等待CPU資源的進程,如果某個進程在等待其它資源,則該進程是不可運行進程。Linux使用了比較簡單的基于優(yōu)先級的進程調度算法選擇新的進程。

  • 內存管理(memory management,MM):允許多個進程安全的共享主內存區(qū)域。Linux 的內存管理支持虛擬內存,即在計算機中運行的程序,其代碼,數(shù)據(jù),堆棧的總量可以超過實際內存的大小,操作系統(tǒng)只是把當前使用的程序塊保留在內存中,其余的程序塊則保留在磁盤中。必要時,操作系統(tǒng)負責在磁盤和內存間交換程序塊。內存管理從邏輯上分為硬件無關部分和硬件有關部分。硬件無關部分提供了進程的映射和邏輯內存的對換;硬件相關的部分為內存管理硬件提供了虛擬接口。

    • 一般而言,Linux的每個進程享有4GB的內存空間,03GB屬于用戶空間,34GB屬于內核空間。
  • 虛擬文件系統(tǒng)(Virtual File System,VFS):隱藏了各種硬件的具體細節(jié),為所有的設備提供了統(tǒng)一的接口,VFS提供了多達數(shù)十種不同的文件系統(tǒng)。虛擬文件系統(tǒng)可以分為邏輯文件系統(tǒng)和設備驅動程序。邏輯文件系統(tǒng)指Linux所支持的文件系統(tǒng),如ext2,fat等,設備驅動程序指為每一種硬件控制器所編寫的設備驅動程序模塊。

    image
  • 網(wǎng)絡接口(NET):提供了對各種網(wǎng)絡標準的存取和各種網(wǎng)絡硬件的支持。網(wǎng)絡接口可分為網(wǎng)絡協(xié)議和網(wǎng)絡驅動程序。網(wǎng)絡協(xié)議部分負責實現(xiàn)每一種可能的網(wǎng)絡傳輸協(xié)議。網(wǎng)絡設備驅動程序負責與硬件設備通訊,每一種可能的硬件設備都有相應的設備驅動程序。

    image
  • 進程間通訊(inter-process communication,IPC): 支持進程間各種通信機制。

    • 共享內存
    • 管道
    • 信號量
    • 消息隊列
    • 套接字

4.內核模塊

Linux內核是模塊化組成的,它允許內核在運行時動態(tài)地向其中插入或刪除代碼。

二、內核模塊結構

1.頭文件

內核模塊頭文件<linux/module.h>和<linux/init.h>是必不可少的 ,不同模塊根據(jù)功能的差異,所需要的頭文件也不相同 。

#include <linux/module.h>
#include <linux/init.h>

2.模塊初始化

模塊的初始化負責注冊模塊本身 ,只有已注冊模塊的各種方法才能夠被應用程序使用并發(fā)揮各方法的實際功能。

模塊并不是內核內部的代碼,而是獨立于內核之外,通過初始化,能夠讓內核之外的代碼來替內核完成本應該由內核完成的功能,模塊初始化的功能相當于模塊與內核之間銜接的橋梁,告知內核已經(jīng)準備好模塊了。

內核模塊初始化函數(shù)

//模塊初始化函數(shù)一般都需聲明為 static
//__init 表示初始化函數(shù)僅僅在初始化期間使用,一旦初始化完畢,將釋放初始化函數(shù)所占用的內存
static int __init module_init_func(void)
{
    初始化代碼
}
module_init(module_init_func);
//module_init宏定義會在模塊的目標代碼中增加一個特殊的代碼段,用于說明該初始化函數(shù)所在的位置。

當使用 insmod 將模塊加載進內核的時候,初始化函數(shù)的代碼將會被執(zhí)行。

3.模塊退出

模塊的退出相當于告知內核“我要離開了,將不再為您服務了”。

內核模塊退出函數(shù)

//模塊退出函數(shù)沒有返回值;
//__exit 標記這段代碼僅用于模塊卸載;
static void __exit module_exit_func(void)
{
    //模塊退出代碼
}
module_exit(module_exit_func);
//沒有 module_exit 定義的模塊無法被卸載

當使用 rmmod 卸載模塊時,退出函數(shù)的代碼將被執(zhí)行。

注意:如果模塊被編譯進內核,而不是動態(tài)加載,則__init的使用會在模塊初始化完成后丟棄該函數(shù)并回收所占內存, _exit宏將忽略“清理收尾”的函數(shù)。

4.模塊許可證聲明

Linux 內核是開源的,遵守 GPL 協(xié)議,所以要求加載進內核的模塊也最好遵循相關協(xié)議。

為模塊指定遵守的協(xié)議用 MODULE_LINCENSE 來聲明 :

MODULE_LICENSE("GPL");
  • 內核能夠識別的協(xié)議有
    • “GPL”
    • “GPL v2”
    • “GPL and additional rights(GPL 及附加權利)”
    • “Dual BSD/GPL(BSD/GPL 雙重許可)”
    • “Dual MPL/GPL(MPL/GPL 雙重許可)”
    • “Proprietary(私有)”

5.模塊導出符號 【可選】

使用模塊導出符號,方便其它模塊依賴于該模塊,并使用模塊中的變量和函數(shù)等。

  • 在Linux2.6的內核中,/proc/kallsyms文件對應著符號表,它記錄了符號和符號對應的內存地址。

    $ cat /proc/kallsyms 
    ...
    ffffff80084039b8 t shash_digest_unaligned
    ffffff8008403a30 T crypto_shash_digest
    ffffff8008403ac0 t shash_async_final
    ffffff8008403af0 T shash_ahash_update
    ffffff8008403b50 t shash_async_update
    ffffff8008403b80 t crypto_exit_shash_ops_async
    ffffff8008403bb0 t crypto_shash_report
    ffffff8008403c18 t crypto_shash_show
    ffffff8008403c78 T crypto_alloc_shash
    ffffff8008403cc8 T crypto_register_shash
    ffffff8008403d00 T crypto_unregister_shash
    ffffff8008403d30 T crypto_register_shashes
    ffffff8008403df8 T crypto_unregister_shashes
    ffffff8008403e90 T shash_register_instance
    ffffff8008403ed0 T shash_free_instance
    ffffff8008403f08 T crypto_init_shash_spawn
    ffffff8008403f58 T shash_attr_alg
    ffffff8008403fb0 T shash_ahash_finup
    ffffff8008404068 t shash_async_finup
    ffffff80084040b0 T shash_ahash_digest
    ffffff80084041e0 t shash_async_digest
    ...
    
  • 使用一下宏定義導出符號

    EXPORT_SYMBOL(module_symbol);
    //或
    EXPORT_GPL_SYMBOL(module_symbol);
    

6.模塊描述 [可選]

模塊編寫者還可以為所編寫的模塊增加一些其它描述信息,如模塊作者、模塊本身的描述或者模塊版本等

MODULE_AUTHOR("Abing <Linux@zlgmcu.com>");
MODULE_DESCRIPTION("ZHIYUAN ecm1352 beep Driver");
MODULE_VERSION("V1.00");

模塊描述以及許可證聲明一般放在文件末尾。

三、向Linux內核添加新內核模塊

1.添加模塊驅動文件

在linux/drivers/下新建目錄hello,并且在hello/目錄下新建hello.c、Makefile、Kconfig三個文件。

1)內核模塊程序hello.c
/* hello world module */

#include <linux/module.h>
#include <linux/init.h>
#include <linux/kernel.h>

static int __init hello_init(void)
{
  printk(KERN_INFO "Hello, I'm ready!\n");
  return 0;
}
static void __exit hello_exit(void)
{
  printk(KERN_INFO "I'll be leaving, bye!\n");
}
module_init(hello_init);
module_exit(hello_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("michael");
MODULE_DESCRIPTION("hello world module");
  • 內核通過 printk() 輸出的信息具有日志級別,日志級別是通過在 printk() 輸出的字符串前加一個帶尖括號的整數(shù)來控制的,如 printk(“<6>Hello, world!/n”);。內核中共提供了八種不同的日志級別

    // 在 linux/kernel.h 中有相應的宏對應
    #define KERN_EMERG    "<0>"    /* system is unusable */
    #define KERN_ALERT    "<1>"    /* action must be taken immediately */
    #define KERN_CRIT     "<2>"    /* critical conditions */
    #define KERN_ERR      "<3>"    /* error conditions */
    #define KERN_WARNING  "<4>"    /* warning conditions */
    #define KERN_NOTICE   "<5>"    /* normal but significant */
    #define KERN_INFO     "<6>"    /* informational */
    #define KERN_DEBUG    "<7>"    /* debug-level messages */
    
2)Kconfig
menu "HELLO TEST Driver "
comment "HELLO TEST Driver Config"
 
config HELLO
    tristate "hello module test"
    default m
    help
    This is the hello test driver.
 
endmenu
  • 在menuconfig的“driver”菜單下添加“HELLO TEST Driver”子菜單,并加入“HELLO”配置選項,選項默認為m。
  • 保存menuconfig后,會在kernel根目錄下的.config文件中生成“CONFIG_HELLO=m”,在編譯的時候會添加到臨時環(huán)境變量中。
3)Makefile
obj-$(CONFIG_HELLO) += hello.o

可用于動態(tài)模塊外部編譯的寫法

  • 編譯模塊的內核配置必須與所運行內核的編譯配置一樣 。

    ifneq ($(KERNELRELEASE),)
             obj-m += hello.o
    else
    KERNELDIR ?= /lib/modules/$(shell uname -r)/build  # 定義內核路徑
    PWD := $(shell pwd)
    default:
            $(MAKE) -C $(KERNELDIR) M=$(PWD) modules   # 表示在當前目錄下編譯
    clean:
            rm -rf .*.cmd *.o *.mod.c *.ko .tmp_versions
    endif
    
    • KERNELRELEASE是在內核源碼的頂層Makefile中定義的一個變量,在第一次讀取執(zhí)行此Makefile時,KERNELRELEASE沒有被定義,所以make將讀取執(zhí)行else之后的內容。
    • 當從內核源碼目錄返回時,KERNELRELEASE已被定義,kbuild也被啟動去解析kbuild語法的語句,make將繼續(xù)讀取else之前的內容,生成的目標模塊名。

2.修改上一級目錄的Kconfig和Makefile

進入linux/drivers/

  • 編輯Makefile,在后面添加一行:

    obj-$(CONFIG_HELLO) += hello/
    
  • 編輯Kconfig,在后面添加一行:

    source "drivers/hello/Kconfig"
    
    • 注:某些內核版本需要同時在arch/arm/Kconfig中添加:source "drivers/hello/Kconfig"

3.make menuconfig配置和編譯

  • 執(zhí)行:make menuconfig ARCH=arm進入配置菜單
  • 選擇并進入:Device Drivers選項
image
  • 進入 HELLO TEST Driver選項

    image
    • 可以選擇<m> <y> <n>,分別為編譯成內核模塊、編譯進內核、不編譯。

如果選擇編譯成動態(tài)模塊<m>

  • 編譯內核過程中,會有如下輸出:

    LD drivers/hello/built-in.o
    CC [M] drivers/hello/hello.o
    CC drivers/hello/hello.mod.o
    LD [M] drivers/hello/hello.ko
    

如果選擇編譯進內核<y>

  • 編譯內核過程中,會有如下輸出:

    CC drivers/hello/hello.o
    LD drivers/hello/built-in.o
    

4.動態(tài)模塊加載和卸載

加載模塊使用 insmod 命令,卸載模塊使用 rmmod 命令。

$ insmod hello.ko
$ rmmod hello.ko
#加載和卸載模塊必須具有 root 權限 。

對于可接受參數(shù)的模塊,在加載模塊的時候為變量賦值即可,卸載模塊無需參數(shù)。

$ insmod hello.ko num=8
$ rmmod hello.ko

四、帶參數(shù)的內核模塊

模塊參數(shù)必須使用 module_param 宏來聲明,通常放在文件頭部。

module_param 需要 3個參數(shù):變量名稱、類型以及用于 sysfs 入口的訪問掩碼。

static int num = 5;
module_param(num, int, S_IRUGO);
  • 內核模塊支持的參數(shù)類型有: bool、 invbool、 charp、 int、 short、 long、 uint、 ushort和 ulong。
  • 訪問掩碼的值在<linux/stat.h>定義, S_IRUGO 表示任何人都可以讀取該參數(shù),但不能修改。
  • 支持傳參的模塊需包含 moduleparam.h 頭文件。

能夠接收參數(shù)的模塊范例

#include <linux/module.h>
#include <linux/init.h>
// moduleparam.h 文件已經(jīng)包含在 module.h 文件中

static int num = 3;
static char *whom = "master";

module_param(num, int, S_IRUGO);
module_param(whom, charp, S_IRUGO);

static int __init hello_init(void)
{
  printk(KERN_INFO "%s, I get %d\n", whom, num); //KERN_INFO 表示這條打印信息的級別
  return 0;
}

static void __exit hello_exit(void)
{
  printk("I'll be leaving, bye!\n");
}

module_init(hello_init);
module_exit(hello_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("luxiaodai");
MODULE_DESCRIPTION("this is my first module");
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

  • make menuconfig過程解析作者 codercjg 在 28 九月 2015, 5:27 下午 make...
    codercjg閱讀 1,249評論 0 1
  • 1 Linux 內核模塊簡介 Linux 內核是一個十分龐大的系統(tǒng),如何能夠為其瘦身,訂制適合自己應用場景的 li...
    守拙圓閱讀 1,173評論 0 2
  • 一、Linux內核模塊簡介 1.1 Linux內核模塊介紹 Linux內核的整體結構已經(jīng)非常龐大,而其包含的組件也...
    konishi5202閱讀 3,041評論 0 4
  • 本人在開發(fā)Android Nfc POS之初,探索調試了一番驅動,目前在Nexus 5X 7.1.1上已經(jīng)調成,之...
    Eric_Y15閱讀 1,718評論 0 3
  • 告誡自己—— 幫助被教者最好的方式, 并非是“修正TA”。 而是在TA身上找到最大的優(yōu)點。 約翰.麥斯威爾把這個方...
    小旅閱讀 785評論 0 0

友情鏈接更多精彩內容