一、內核簡介
內核(Kernel)在計算機科學中是操作系統最基本的部分,主要負責管理系統資源。它是為眾多應用程序提供對計算機硬件的安全訪問的一部分軟件,這種訪問是有限的,并由內核決定一個程序在什么時候對某部分硬件操作多長時間。直接對硬件操作是非常復雜的。所以內核通常提供一種硬件抽象的方法,來完成這些操作。通過進程間通信機制及系統調用,應用進程可間接控制所需的硬件資源(特別是處理器及IO設備)。
二、內核分類
內核在設計上分為宏內核與微內核兩大架構。
宏內核:簡單來說,就是把很多東西都集成進內核,例如Linux內核,除了最基本的進程、線程管理、內存管理外,文件系統,驅動,網絡協議棧等都在內核里面。優點是效率高。缺點是穩定性差,開發過程中的bug經常會導致整個系統掛掉。做驅動開發的應該經常有按電源鍵強行關機的經歷。
微內核:內核中只有最基本的調度、內存管理。驅動、文件系統等都是用戶態的守護進程去實現的。優點是超級穩定,驅動等的錯誤只會導致相應進程死掉,不會導致整個系統都崩潰,做驅動開發時,發現錯誤,只需要kill掉進程,修正后重啟進程就行了,比較方便。缺點是效率低。
三、內核模塊及其好處
Linux是一個宏內核,運行在單獨的內核地址空間。不過,Linux汲取了微內核的精華:其引以為豪的是模塊化設計、搶占式內核、支持內核線程以及動態裝載內核模塊的能力。不僅如此,Linux還避免其微內核設計上性能損失的缺陷,讓所有事情都運行在內核態,直接調用函數,無需消息傳遞。至今,Linux是模塊化的、多線程的以及內核本身可調度的操作系統,實用主義再次占了上風。
模塊是具有獨立功能的程序,它可以被 單獨編譯 ,但 不能獨立運行 。它在運行時被鏈接到內核作為內核的一部分在內核空間運行。模塊通常由一組函數和數據結構組成,用來實現一種文件系統、一個驅動程序或其他內核上層的功能。
內核模塊是Linux內核向外部提供的一個插口,其全稱為動態可加載內核模塊(Loadable Kernel Module,LKM),簡稱為模塊。
同時內核模塊的這一特點也有助于減小內核鏡像文件的體積,自然也就減少了內核所占的內存空間(因為整個內核鏡像將會被加載到內存中運行)。不必把所有的驅動都編譯內核,而是以模塊的形式單獨編譯驅動程序,這是基于不是所有的驅動都會同時工作原理。因為不是所有的硬件都要同時接入系統,比如一個無線網卡討論完內核模塊的這些特性后,我們正式開始編寫模塊程序。
四、內核模塊編程基礎
眾所周知,內核模式下的編程和用戶模式下有所不同,會有如下限制條件:
- 不能使用用戶模式下的C標準庫。
- 不能使用浮點運算,因為linux內核切換模式時不保存處理器的浮點狀態。
- 盡可能保持代碼的清潔易懂,因為內核調試不方便。
- 模塊編程和內核版本密切相連,不同的內核版本,某些函數的函數名會有變化。因此模塊編程也可以說是內核編程。
- 只有超級用戶才可以運行模塊 。
應用程序編程和內核模塊編程的對比:
應用程序 | 內核模塊程序 | |
---|---|---|
使用函數 | libc庫 | 內核函數 |
運行空間 | 用戶空間 | 內核空間 |
運行權限 | 普通用戶 | 超級用戶 |
入口函數 | main() | module_init() |
出口函數 | exit() | module_exit() |
編譯工具 | gcc | make |
鏈接工具 | gcc | insmod |
運行方式 | 直接運行 | insmod |
調試方法 | gdb | kdbug、kdb、kgdb |
五、內核模塊代碼結構
1、頭文件引用
#include < linux/module.h >
#include < linux/kernel.h >
#include < linux/init.h >
編寫任何內核模塊程序所必須引用的 3 個頭文件 :
- module.h包含了對模塊結構的定義及模塊版本的控制
- kernel.h包含了常用的內核函數
- init.h包含了宏__init和__exit,以及一些其他初始化函數的調用宏。如宏module_init等。宏__init告訴編譯程序相關的函數僅用于初始化模塊的初始化的宏定義,宏__exit用于可加載模塊的卸載清理操作。
2、編寫內核模塊時必備的兩個函數
1)xxx_init():注冊函數(名字xxx可任起) 或模塊的初始化函數。如:
/* 不加void在調試時會出現報警 */
static int __init myfunc_init( void )
{
printk("Hello, This is my own module…
");
return 0;
}
2)xxx_exit( ):卸載函數(名字xxx可任起) 或模塊的退出和清理函數。如:
/* 不加void會出現報警,若改為static int也會報錯 , 因為出口函數是不能返回值的 */
static void __exit myfunc_exit( void )
{
printk("Goodbye, uninstall my own module…
");
}
3、加載模塊和卸載模塊
1) module_init() :向內核注冊模塊,提供新功能;告訴內核你編寫的模塊程序從哪里開始執行。
2) module_exit() :注銷由模塊提供的功能;告訴內核你編寫的模塊程序從哪里離開。
4、模塊許可權限聲明
MODULE_LICENSE(“GPL”);
從內核2.4.10開始,動態加載的模塊必須通過MODULE_LICENSE宏聲明此模塊的許可證。否則在動態加載此模塊時,會收到內核被污染"module license’unspecified’ taints kernel."的警告。
從Linux內核2.6開始,內核模塊的編譯采用Kbuild(kernel build)系統。Kbuild系統會兩次掃描Linux的Makefile:首先編譯系統會讀取Linux內核頂層的Makefile,然后根據讀到的內容第二次讀取Kbuild的Makefile來編譯Linux內核或者模塊。
Kernel Makefile:Kernel Makefile位于Linux內核源代碼的頂層錄/usr/src/kernels/xxx/,也叫Top Makefile。這個文件會被首先讀取,并根據讀到的內容配置編譯環境變量。對于內核或驅動開發人員來說,這個文件幾乎不用任何修改。
Kbuild Makefile:當Kernel Makefile被解析完成后,Kbuild會讀取相關的Kbuild Makefile進行內核或模塊的編譯。內核及驅動開發人員需要編寫這個Kbuild Makefile文件。
六、自定義內核模塊
1、選擇一個目錄,創建Makefile和myownfunc.c文件;
myownfunc.c代碼:
/* 源文件myownfunc.c */
#include < linux/module.h >
#include < linux/kernel.h >
#include < linux/init.h >
static int __init myfunc_init(void)
{
printk("Hello,this is my own module!
");
return 0;
}
static void __exit myfunc_exit(void)
{
printk("Goodbye,this is my own clean module!
");
}
module_init(myfunc_init);
module_exit(myfunc_exit);
MODULE_DESCRIPTION("First Personel Module");
MODULE_AUTHOR("Lebron James");
MODULE_LICENSE("GPL");
Makefile代碼:
ifneq ($(KERNELRELEASE),)
$(info "2nd")
obj-m:=myownfunc.o
else
KDIR :=/lib/modules/$(shell uname -r)/build
PWD :=$(shell pwd)
all:
$(info "1st")
make -C $(KDIR) M=$(PWD) modules
clean:
rm -f *.ko *.o *.mod.o *.symvers *.cmd *.mod.c *.order *.mod
endif
Makefile解析:
#KERNELRELEASE :在內核源碼樹的Makefile中定義,在當前的Makefile中,
# 它的值為空。
#$(shell uname-r) :獲得當系統的Linux內核版本
#KDIR :指定當前Linux操作系統源代碼路徑,即編譯生成的模塊是在當前系統中使用
# 如果想將你寫的模塊,用在你的開發板上運行的Linux系統中,只需在KDIR變量中指定
# 你開發板Linux系統源碼樹的路徑
#PWD:=$(shell pwd)獲得當前待編譯模塊的源文件路徑
2、make編譯執行過程分析
1)在模塊的源代碼目錄下執行make,此時,宏“KERNELRELEASE”沒有定義,因此進入else分支;
2)記錄內核路徑KDIR和當前工作目錄PWD;
3)因為make后面沒有目標,所以make會在Makefile中的第一個不是以.開頭的目標作為默認的目標執行,于是all成為make的目標;all:之后的第一個命令$(info “1st”) 類似于printf函數,編譯經過此處會打印提示信息。
4)make的第二條命令會執行make -C $(KDIR) M=$(PWD) modules
,翻譯過來就是
make -C /lib/modules/6.1.0-rc4+/build M=/tmp/29 modules
-C 表示到存放內核源碼的目錄執行其Makefile
M=$(PWD) 表示返回到當前待編譯模塊目錄
modules 表示編譯成模塊的意思
之所以這么寫是由內核源碼樹的頂層Makefile告訴我們的,當我們調用Linux內核源碼樹頂層的Makefile時,找到的是頂層Makefile的“modules”目標。
5)找到modules目標后,接下來Linux源碼樹的頂層Makeflle就需要知道是將哪些".c"文件編譯成模塊。誰告訴它呢?是的,待編譯模塊的Makefile文件。所以接下來就會回調模塊的Makefile。需要注意的是,此時KERNELRELEASE已經在Linux內核源碼樹的頂層Makefile中定義過了,所以此時它獲得信息是:
obj-m:=myownfunc.o
obj-m表示會將myownfunc.o目標編譯成.ko模塊;它告訴Linux源碼樹頂層Makefile是動態編譯(編譯成模塊)而不是編譯進內核(obj-y),Linux源碼樹頂層Makefile會根據myownfunc.o找到myownfunc.c文件。
6)將模塊文件myownfunc.c編譯為myownfunc.o,然后再將多個目標鏈接為.ko
最終編譯結果如下:
[root@localhost 29]# make
"1st"
make -C /lib/modules/6.1.0-rc4+/build M=/tmp/29 modules
make[1]: Entering directory `/usr/src/kernels/6.1.0-rc4+'
"2nd"
CC [M] /tmp/29/myownfunc.o
"2nd"
MODPOST /tmp/29/Module.symvers
CC [M] /tmp/29/myownfunc.mod.o
LD [M] /tmp/29/myownfunc.ko
make[1]: Leaving directory `/usr/src/kernels/6.1.0-rc4+'
由執行結果可知,待編譯模塊的Makefile最終被調用了三次
1) 執行命令make調用
2) 被Linux內核源碼樹的頂層Makefile調用,產生.o文件
3) 被Linux內核源碼樹頂層Makefile調用,將.o文件鏈接生成.ko文件
綜上,可將Linux模塊編譯的流程總結如下圖:
七、模塊加載與卸載
編譯好了xxx.ko文件以后,接下來就要考慮如何將ko模塊加載到Linux內核以及如何卸載ko模塊,讓我們學習Linux內核模塊加載與卸載。
1、模塊加載
insmod /absolute-path/模塊名.ko
例如添加上文編譯的內核模塊:
insmod ./myownfunc.ko
注意:Linux系統中只有超級用戶權限才可以添加模塊到內核。
modprobe命令也可以實現模塊加載到內核,具體差異本文不做詳細概述,后續會出專門的推文講解insmod和modprobe的區別。
2、查看系統中的模塊
lsmod 模塊名
例如在系統中搜索自己添加的myownfunc模塊:
[root@nj-rack01-06 29]# lsmod | grep myownfunc
myownfunc 16384 0
3、卸載模塊
rmmod 模塊名
例如卸載系統中的myownfunc模塊:
rmmod myownfunc
4、查看模塊信息
1)查看模塊注冊的信息
modinfo 模塊名.ko
例如查看自己添加的myownfunc模塊的注冊信息:
[root@nj-rack01-06 29]# modinfo myownfunc.ko
filename: /tmp/29/myownfunc.ko
license: GPL
author: Lebron James
description: First Personel Module
srcversion: 8748FD633F9276BD38A9934
depends:
retpoline: Y
name: myownfunc
vermagic: 6.1.0-rc4+ SMP preempt mod_unload modversions
如上結果所示,modinfo會顯示模塊的全路徑文件名,license信息,作者信息,描述信息,模塊名等。
2)查看模塊打印的信息
dmesg | tail
例如查看自己添加的myownfunc模塊打印信息:
dmesg主要是從Linux內核的ring buffer(環形緩沖區)中讀取信息的。
在Linux系統中,所有通過printk打印出來的信息都會送到ring buffer中。我們知道,我們打印出來的信息是需要在控制臺設備上顯示的。因為此時printk只是把信息輸送到ring buffer中,等控制臺設備初始化好后,在根據ring buffer中消息的優先級決定是否需要輸送到控制臺設備上。
如何清空ring buffer呢?
dmesg -c
到此,本文即成功實現了自定義內核模塊的加載、卸載以及打印信息的查看。
紙上得來終覺淺,絕知此事要躬行,想學習Linux驅動的朋友趕緊親自動手試一試吧。
評論
查看更多