【世界播資訊】Linux內核模塊編程基礎知識

2023-06-08 11:15:30 來源:Linux二進制

一、內核簡介

內核(Kernel)在計算機科學中是操作系統最基本的部分,主要負責管理系統資源。它是為眾多應用程序提供對計算機硬件的安全訪問的一部分軟件,這種訪問是有限的,并由內核決定一個程序在什么時候對某部分硬件操作多長時間。直接對硬件操作是非常復雜的。所以內核通常提供一種硬件抽象的方法,來完成這些操作。通過進程間通信機制及系統調用,應用進程可間接控制所需的硬件資源(特別是處理器及IO設備)。

二、內核分類

內核在設計上分為宏內核與微內核兩大架構。


(資料圖片僅供參考)

宏內核:簡單來說,就是把很多東西都集成進內核,例如Linux內核,除了最基本的進程、線程管理、內存管理外,文件系統,驅動,網絡協議棧等都在內核里面。優點是效率高。缺點是穩定性差,開發過程中的bug經常會導致整個系統掛掉。做驅動開發的應該經常有按電源鍵強行關機的經歷。

微內核:內核中只有最基本的調度、內存管理。驅動、文件系統等都是用戶態的守護進程去實現的。優點是超級穩定,驅動等的錯誤只會導致相應進程死掉,不會導致整個系統都崩潰,做驅動開發時,發現錯誤,只需要kill掉進程,修正后重啟進程就行了,比較方便。缺點是效率低。

三、內核模塊及其好處

Linux是一個宏內核,運行在單獨的內核地址空間。不過,Linux汲取了微內核的精華:其引以為豪的是模塊化設計、搶占式內核、支持內核線程以及動態裝載內核模塊的能力。不僅如此,Linux還避免其微內核設計上性能損失的缺陷,讓所有事情都運行在內核態,直接調用函數,無需消息傳遞。至今,Linux是模塊化的、多線程的以及內核本身可調度的操作系統,實用主義再次占了上風。

模塊是具有獨立功能的程序,它可以被 單獨編譯,但 不能獨立運行。它在運行時被鏈接到內核作為內核的一部分在內核空間運行。模塊通常由一組函數和數據結構組成,用來實現一種文件系統、一個驅動程序或其他內核上層的功能。

內核模塊是Linux內核向外部提供的一個插口,其全稱為動態可加載內核模塊(Loadable Kernel Module,LKM),簡稱為模塊。

同時內核模塊的這一特點也有助于減小內核鏡像文件的體積,自然也就減少了內核所占的內存空間(因為整個內核鏡像將會被加載到內存中運行)。不必把所有的驅動都編譯內核,而是以模塊的形式單獨編譯驅動程序,這是基于不是所有的驅動都會同時工作原理。因為不是所有的硬件都要同時接入系統,比如一個無線網卡討論完內核模塊的這些特性后,我們正式開始編寫模塊程序。

四、內核模塊編程基礎

眾所周知,內核模式下的編程和用戶模式下有所不同,會有如下限制條件:

不能使用用戶模式下的C標準庫。不能使用浮點運算,因為linux內核切換模式時不保存處理器的浮點狀態。盡可能保持代碼的清潔易懂,因為內核調試不方便。模塊編程和內核版本密切相連,不同的內核版本,某些函數的函數名會有變化。因此模塊編程也可以說是內核編程。只有超級用戶才可以運行模塊 。

應用程序編程和內核模塊編程的對比:

應用程序內核模塊程序
使用函數libc庫內核函數
運行空間用戶空間內核空間
運行權限普通用戶超級用戶
入口函數main()module_init()
出口函數exit()module_exit()
編譯工具gccmake
鏈接工具gccinsmod
運行方式直接運行insmod
調試方法gdbkdbug、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.oelseKDIR :=/lib/modules/$(shell uname -r)/buildPWD  :=$(shell pwd)all:        $(info "1st")        make -C $(KDIR) M=$(PWD) modulesclean:        rm -f *.ko *.o *.mod.o *.symvers *.cmd *.mod.c *.order *.modendif

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 modulesmake[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.komake[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 myownfuncmyownfunc              16384  0

3、卸載模塊

rmmod 模塊名

例如卸載系統中的myownfunc模塊:

rmmod myownfunc

4、查看模塊信息

1)查看模塊注冊的信息

modinfo 模塊名.ko

例如查看自己添加的myownfunc模塊的注冊信息:

[root@nj-rack01-06 29]# modinfo myownfunc.kofilename:       /tmp/29/myownfunc.kolicense:        GPLauthor:         Lebron Jamesdescription:    First Personel Modulesrcversion:     8748FD633F9276BD38A9934depends:retpoline:      Yname:           myownfuncvermagic:       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驅動的朋友趕緊親自動手試一試吧。

標簽:

上一篇:C函數調用機制與棧幀原理詳解|要聞
下一篇:最后一頁