歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
Linux教程網 >> Linux綜合 >> Linux資訊 >> Linux文化 >> 從 2.4 到 2.6:Linux 內核可裝載模塊機制的改變對設備驅動的影響

從 2.4 到 2.6:Linux 內核可裝載模塊機制的改變對設備驅動的影響

日期:2017/2/27 12:09:04   编辑:Linux文化

文章出處: http://www-128.ibm.com/developerworks/cn/linux/l-module26/

周婷 ,軟件工程師 2006 年 2 月 9 日

從 2.4 到 2.6,Linux 內核在可裝載模塊機制、設備模型、一些核心 API 等方面發生較大改變,設備驅動開發人員面臨著將驅動從 2.4 移植到 2.6 內核,或是使驅動同時支持2.4 與 2.6 內核的任務。站在設備驅動開發人員的角度,驅動由一個或幾個外部可加載內核模塊組成,本文針對 2.6 內核裡模塊機制的改變對編寫設備驅動程序的影響,從內核模塊的編譯、裝載時的版本檢查、初始化與退出、模塊使用計數、輸出內核符號、命令行輸入參數、許可證聲明等方面比較了 2.4 與 2.6 內核的區別;並總結了使設備驅動同時支持 2.4 與 2.6 內核的一系列模板。

1. 獲取內核版本

當設備驅動需要同時支持不同版本內核時,在編譯階段,內核模塊需要知道當前使用的內核源碼的版本,從而使用相應的內核 API。2.4 與 2.6 內核下,源碼頭文件 linux/version.h 定義有:

LINUX_VERSION_CODE ― 內核版本的二進制表示,主、從、修訂版本號各對應一個字節;

KERNEL_VERSION(major, minor, release) - 由主、從、修訂版本號構造二進制版本號。

在同時支持2.4與2.6 內核的設備驅動程序中,經常可以看到以下代碼段:

清單1:判斷內核版本的代碼段。

CODE:[Copy to clipboard]#include

#if LINUX_VERSION_CODE >= KERNEL_VERSION(2, 6, 0) #define LINUX26 #endif

#ifdef LINUX26 /*code in 2.6 kernel*/ #else /*code in 2.4 kernel */ #endif 2.內核模塊機制的改變

2.1模塊編譯

從2.4到2.6,外部可裝載內核模塊的編譯、連接過程以及Makefile的書寫都發生了改變。

2.4內核中,模塊的編譯只需內核源碼頭文件;需要在包含linux/modules.h之前定義MODULE;編譯、連接後生成的內核模塊後綴為.o。

2.6內核中,模塊的編譯需要配置過的內核源碼;編譯、連接後生成的內核模塊後綴為.ko;編譯過程首先會到內核源碼目錄下,讀取頂層的Makefile文件,然後再返回模塊源碼所在目錄。

清單2:2.4 內核模塊的Makefile模板

CODE:[Copy to clipboard] #Makefile2.4 KVER=$(shell uname -r) KDIR=/lib/modules/$(KVER)/build OBJS=mymodule.o CFLAGS=-D__KERNEL__ -I$(KDIR)/include -DMODULE -D__KERNEL_SYSCALLS__ -DEXPORT_SYMTAB -O2 -fomit-frame-pointer -Wall -DMODVERSIONS -include $(KDIR)/include/linux/modversions.h

all: $(OBJS) mymodule.o: file1.o file2.o ld -r -o $@ $^ clean: rm -f *.o 在2.4 內核下,內核模塊的Makefile與普通用戶程序的Makefile在結構和語法上都相同,但是必須在CFLAGS中定義-D__KERNEL__- DMODULE,指定內核頭文件目錄-I$(KDIR)/include。有一點需注意,之所以在CFLAGS中定義變量,而不是在模塊源碼文件中定義,一方面這些預定義變量可以被模塊中所有源碼文件可見,另一方面等價於將這些預定義變量定義在源碼文件的起始位置。在模塊編譯中,對於這些全局的預定義變量,一般在CFLAGS中定義。

清單3:2.6 內核模塊的Makefile模板

CODE:[Copy to clipboard]# Makefile2.6 ifneq ($(KERNELRELEASE),) #kbuild syntax. dependency relationshsip of files and target modules are listed here. mymodule-objs := file1.o file2.o obj-m := mymodule.o else PWD := $(shell pwd) KVER ?= $(shell uname -r) KDIR := /lib/modules/$(KVER)/build all: $(MAKE) -C $(KDIR) M=$(PWD) clean: rm -rf .*.cmd *.o *.mod.c *.ko .tmp_versions endif KERNELRELEASE是在內核源碼的頂層 Makefile中定義的一個變量,在第一次讀取執行此Makefile時,KERNELRELEASE沒有被定義,所以make將讀取執行else之後的內容。如果make的目標是clean,直接執行clean操作,然後結束。當make的目標為all時,-C $(KDIR) 指明跳轉到內核源碼目錄下讀取那裡的Makefile;M=$(PWD) 表明然後返回到當前目錄繼續讀入、執行當前的Makefile。當從內核源碼目錄返回時,KERNELRELEASE已被被定義,kbuild也被啟動去解析kbuild語法的語句,make將繼續讀取else之前的內容。else之前的內容為kbuild語法的語句, 指明模塊源碼中各文件的依賴關系,以及要生成的目標模塊名。mymodule-objs := file1.o file2.o表示mymoudule.o 由file1.o與file2.o 連接生成。obj-m := mymodule.o表示編譯連接後將生成mymodule.o模塊。

補充一點,"$(MAKE) -C $(KDIR) M=$(PWD)"與"$(MAKE) -C $(KDIR) SUBDIRS =$(PWD)"的作用是等效的,後者是較老的使用方法。推薦使用M而不是SUBDIRS,前者更明確。

通過以上比較可以看到,從Makefile編寫來看,在2.6內核下,內核模塊編譯不必定義復雜的CFLAGS,而且模塊中各文件依賴關系的表示簡潔清晰。

清單4: 可同時在2.4 與 2.6 內核下工作的Makefile

CODE:[Copy to clipboard]#Makefile for 2.4 & 2.6 VERS26=$(findstring 2.6,$(shell uname -r)) MAKEDIR?=$(shell pwd) ifeq ($(VERS26),2.6) include $(MAKEDIR)/Makefile2.6 else include $(MAKEDIR)/Makefile2.4 endif 2.2模塊裝載時的版本檢查

Linux內核一直在更新、完善,在a版本內核源碼下編譯的模塊在b版本內核下通常不能運行,所以必須有一種機制,限制在a版本內核下編譯生成的模塊在b版本內核下被加載。

2.4與2.6內核在可裝載內核模塊的版本檢查機制方面發生了根本性的改變,不過這些改變對設備驅動開發人員而言基本是透明的。為了使模塊裝載時的版本檢查機制生效,2.4 內核下,只需在CFLAGS中定義

CODE:[Copy to clipboard]-DMODVERSIONS -include $(KDIR)/include/linux/modversions.h; 2.6內核下,開發人員無須采用任何操作。

不過,在此仍有必要闡明2.4與2.6內核對可加載模塊的版本檢查機制。

2.4 內核下, 執行`cat /proc/ksyms`可看到內核符號在名字後還跟隨著一串校驗字符串,此校驗字符串與內核版本有關。在內核源碼頭文件linux/modules 目錄下存在許多*.ver文件,這些文件起著為內核符號添加校驗後綴的作用,如ksyms.ver 文件裡有一行 #define printk _set_ver(printk)。linux/modversions.h 文件會包含全部的 ver文件。所以當模塊包含linux/modversions.h文件後,編譯時,模塊裡使用的內核符號實質是帶有校驗後綴的內核符號。在加載模塊時,如果模塊中所使用內核符號的校驗字符串與當前運行內核所導出的相應的內核符號的校驗字符串不一致,即當前內核空間並不存在模塊所使用的內核符號,就會出現 "Invalid module format "的錯誤。

為內核符號添加校驗字符串來驗證模塊的版本與內核的版本是否匹配是繁雜和浪費內核空間的;而且隨著SMP(對稱多處理器)、PREEMPT(可搶占內核)等機制在2.6內核的引入和完善,模塊運行時對內核的依賴不僅取決於內核版本,還取決於內核的配置,此時內核符號的校驗碼是否一致不能成為判斷模塊可否被加載的充分條件。2.6 內核下,在linux/vermagic.h中定義有VERMAGIC_STRING,VERMAGIC_STRING不僅包含內核版本號,還包含有內核使用的gcc版本,SMP與PREEMPT等配置信息。模塊在編譯時,我們可以看到屏幕上會顯示"MODPOST"。在此階段, VERMAGIC_STRING會添加到模塊的modinfo段。在內核源碼目錄下scripts\mod\modpost.c文件中可以看到模塊後續處理部分的代碼。模塊編譯生成後,通過`modinfo mymodule.ko`命令可以查看此模塊的vermagic等信息。2.6 內核下的模塊裝載器裡保存有內核的版本信息,在裝載模塊時,裝載器會比較所保存的內核vermagic與此模塊的modinfo段裡保存的 vermagic信息是否一致,兩者一致時,模塊才能被裝載。譬如Fedora core 4 與core 2 使用的都是2.6 版本內核,在Fedore Core 2下去加載Fedora Core4下編譯生成的hello.ko,會出現"invalid module format" 錯誤。

CODE:[Copy to clipboard]#insmod hello.ko Invalid module format hello: version magic '2.6.11-1.1369_FC4 686 REGPARM 4KSTACKS gcc-4.0' should be '2.6.5-1.358 686 REGPARM 4KSTACKS gcc-3.3' 2.3模塊的初始化與退出

在2.6 內核中,內核模塊必須調用宏module_init 與module_exit() 去注冊初始化與退出函數。在2.4 內核中,如果初始化函數命名為init_module()、退出函數命名為cleanup_module(),可以不必使用module_init 與module_exit 宏。推薦使用module_init 與module_exit宏,使代碼在2.4與2.6內核中都能工作。

清單5:適用於2.4與2.6內核的模塊的初始化與退出模板

CODE:[Copy to clipboard]#include /* Needed by all modules */ #include /* Needed for init&exit macros */

static int mod_init_func(void) { /*code here*/ return 0; }

static void mod_exit_func(void) { /*code here*/ }

module_init(mod_init_func); module_exit(mod_exit_func); 需要注意的是初始化與退出函數必須在宏module_init和module_exit使用前定義,否則會出現編譯錯誤。

2.4 模塊使用計數

模塊在被使用時,是不允許被卸載的。2.4內核中,模塊自身通過MOD_INC_USE_COUNT、MOD_DEC_USE_COUNT宏來管理自己被使用的計數。2.6內核提供了更健壯、靈活的模塊計數管理接口try_module_get(&module)及module_put(& module)取代2.4中的模塊使用計數管理宏;模塊的使用計數不必由自身管理,而且在管理模塊使用計數時考慮到SMP與PREEMPT機制的影響。

int try_module_get(struct module *module):用於增加模塊使用計數;若返回為0,表示調用失敗,希望使用的模塊沒有被加載或正在被卸載中。

void module_put(struct module *module):減少模塊使用計數。

try_module_get 與module_put的引入與使用與2.6內核下的設備模型密切相關。模塊是用來管理硬件設備的,2.6 內核為不同類型的設備定義了struct module *owner 域,用來指向管理此設備的模塊。如字符設備的定義:

CODE:[Copy to clipboard]struct cdev { struct kobject kobj; struct module *owner; struct file_operations *ops; struct list_head list; dev_t dev; unsigned int count; }; 從設備使用的角度出發,當需要打開、開始使用某個設備時,使用try_module_get(dev->owner)去增加管理此設備的owner模塊的使用計數;當關閉、不再使用此設備時,使用 module_put(dev->owner)減少對管理此設備的owner模塊的使用計數。這樣,當設備在使用時,管理此設備的模塊就不能被卸載;只有設備不再使用時模塊才能被卸載。

2.6內核下,對於為具體設備寫驅動的開發人員而言,基本無需使用try_module_get與 module_put,因為此時開發人員所寫的驅動通常為支持某具體設備的owner模塊,對此設備owner模塊的計數管理由內核裡更底層的代碼如總線驅動或是此類設備共用的核心模塊來實現,從而簡化了設備驅動開發。

2.5 模塊輸出的內核符號

2.4 內核下,缺省情況時模塊中的非靜態全局變量及函數在模塊加載後會輸出到內核空間。

2.6 內核下,缺省情況時模塊中的非靜態全局變量及函數在模塊加載後不會輸出到內核空間,需要顯式調用宏EXPORT_SYMBOL才能輸出。所以在2.6 內核的模塊下,EXPORT_NO_SYMBOLS宏的調用沒有意義,是空操作。在同時支持2.4與2.6內核的設備驅動中,可以通過以下代碼段來輸出模塊的內核符號

清單6: 同時支持2.4與2.6的輸出內核符號代碼段

CODE:[Copy to clipboard]#include #ifndef LINUX26 EXPORT_NO_SYMBOLS; #endif EXPORT_SYMBOL(var); EXPORT_SYMBOL(func); 需要注意的是如需在2.4內核下使用 EXPORT_SYMBOL,必須在 CFLAGS中定義 EXPORT_SYMTAB,否則編譯將會失敗。

從良好的代碼風格角度出發,模塊中不需要輸出到內核空間且不需為模塊中其它文件所用的全局變量及函數最好顯式申明為static類型,需要輸出的內核符號以模塊名為前綴。

模塊加載後,2.4內核下可通過 /proc/ksyms、 2.6 內核下可通過/proc/kallsyms查看模塊輸出的內核符號

2.6 模塊的命令行輸入參數

在裝載內核模塊時,用戶可以向模塊傳遞一些參數,如`modprobe modname var=value`,否則,var將使用模塊內定義的缺省值。

2.4 內核下,linux/module.h中定義有宏MODULE_PARM(var,type) 用於向模塊傳遞命令行參數。var為接受參數值的變量名,type為采取如下格式的字符串[min[-max]]{b,h,i,l,s}。min及max 用於表示當參數為數組類型時,允許輸入的數組元素的個數范圍;b:byte;h:short;i:int;l:long;s:string。

2.6內核下,宏MODULE_PARM(var,type)不再被支持。在頭文件linux/moduleparam.h裡定義了如下宏:

module_param(name, type, perm) module_param_array(name, type, nump, perm)

type 類型可以是byte、short,、ushort、 int、 uint、long、ulong、charp, bool or invbool, 不再采用2.4內核中的字符串形式,而且在模塊編譯時會將此處申明的type與變量定義的類型進行比較,判斷是否一致。

perm表示此參數在sysfs文件系統中所對應的文件節點的屬性。2.6內核使用sysfs文件系統,這是一個建立在內存中比proc更強大的文件系統。sysfs文件系統可以動態、實時,有組織層次地反應當前系統中的硬件、驅動等狀態。當perm為0時,表示此參數不存在sysfs文件系統下對應的文件節點。模塊被加載後,在/sys/module/ 目錄下將出現以此模塊名命名的目錄。如果此模塊存在perm不為0的命令行參數,在此模塊的目錄下將出現parameters目錄,包含一系列以參數名命名的文件節點,這些文件的權限值等於perm,文件的內容為參數的值。

nump 為保存輸入的數組元素個數的變量的指針。當不需保存實際輸入的數組元素個數時,可以設為NULL。從2.6.0至2.6.10 版本,須將變量名賦給nump;從2.6.10 版本開始,須將變量的引用賦給nump,這更易為開發人員理解。加載模塊時,使用逗號分隔輸入的數組元素。

清單7: 適用於2.4與2.6內核的模塊輸入參數模板

CODE:[Copy to clipboard]#include #ifdef LINUX26 #include #endif

int debug = 0; char *mode = "800x600"; int tuner[4] = {1, 1, 1, 1};

#ifdef LINUX26 int tuner_c = 1; #endif

#ifdef LINUX26 MODULE_PARM(debug, "i"); MODULE_PARM(mode, "s"); MODULE_PARM(tuner,"1-4i"); #else module_param(debug, int, 0644); module_param(mode, charp, 0644); #if LINUX_VERSION_CODE >= KERNEL_VERSION(2, 6, 10) module_param_array(tuner, int, &tuner_c, 0644); #else module_param_array(tuner, int, tuner_c, 0644); #endif #endif 模塊編譯生成後,加載模塊時可以輸入:`modprobe my_module mode=1024x768 debug=1 tuner=22,33`。

在linux/moduleparam.h還定義有:

module_param_array_named(name, array, type, nump, perm) module_param_call(name, set, get, arg, perm) module_param_named(name, value, type, perm)

讀者可以參閱linux/moduleparam.h查看這些宏的詳細描述,有一點需注意,在2.6內核裡,module_param這一系列宏使用的都是小寫名字。

2.7 模塊的許可證聲明

從2.4.10 版本內核開始,模塊必須通過MODULE_LICENSE宏聲明此模塊的許可證,否則在加載此模塊時,會收到內核被污染"kernel tainted" 的警告。從linux/module.h文件中可以看到,被內核接受的有意義的許可證有 "GPL","GPL v2","GPL and additional rights","Dual BSD/GPL","Dual MPL/GPL","Proprietary"。

在同時支持2.4與2.6內核的設備驅動中,模塊可按如下方式聲明自己的許可證。

清單8: 適用於2.4與2.6內核的模塊許可證聲明模板

#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,4,10) MODULE_LICENSE("GPL"); #endif

2.8 小結

此外,2.6內核裡還有一些模塊機制的改變,不常為驅動開發人員用到。如加載內核模塊的接口request_module在2.4 下為request_module(const char * module_name);在2.6內核下為request_module(const char *fmt, ...)。在2.6 內核下,驅動開發人員可以通過調用

request_module("msp3400"); request_module("char-major-%d-%d", MAJOR(dev), MINOR(dev));

這種更靈活的方式加載其它內核模塊。

2.6內核在linux/module.h中還提供了MODULE_ALIAS(alias)宏,模塊可以通過調用此宏為自己定義一或若干個別稱。而在2.4內核下,用戶只能在/etc/modules.conf中為模塊定義別稱。

通過以上比較可以看到,從2.4到2.6內核,可裝載模塊管理機制的改變使設備驅動的開發變得更加簡潔、靈活、健壯。

參考資料

* Porting device drivers to the 2.6 kernel

關於作者

作者:周婷,軟件工程師,S3 Graphics 上海研發中心,工作方向: 視頻解碼。email: [email protected]


Copyright © Linux教程網 All Rights Reserved