http://www.jb51.net/article/79960.htm
2016
內(nèi)核模塊管理
Linux設(shè)備驅(qū)動(dòng)會(huì)以內(nèi)核模塊的形式出現(xiàn),因此學(xué)會(huì)編寫Linux內(nèi)核模塊編程是學(xué)習(xí)linux設(shè)備驅(qū)動(dòng)的先決條件。
Linux內(nèi)核的整體結(jié)構(gòu)非常龐大,其包含的組件非常多。我們把需要的功能都編譯到linux內(nèi)核,以模塊方式擴(kuò)展內(nèi)核功能。
先來看下最簡(jiǎn)單的內(nèi)核模塊
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | #include <linux/init.h> #include <linux/module.h> static int __init hello_init( void ) { printk(KERN_ALERT "Hello world! %s, %d\n" , __FILE__, __LINE__); return 0; } static void __exit hello_exit( void ) { printk(KERN_ALERT "Hello world! %s, %d\n" , __FILE__, __LINE__); } module_init(hello_init); module_exit(hello_exit); MODULE_LICENSE( "GPL" ); MODULE_AUTHOR( "Mikcy Liu" ); MODULE_DESCRIPTION( "A simple Module" ); MODULE_ALIAS( "a simple module" ); |
頭文件init.h包含了宏_init和_exit,它們?cè)试S釋放內(nèi)核占用的內(nèi)存。
module_init()和hello_exit()是模塊編程中最基本也是必須的兩個(gè)函數(shù)。
module_init()是驅(qū)動(dòng)程序初始化的入口點(diǎn)。
hello_exit是模塊的退出和清理函數(shù)。此處可以做所有終止該驅(qū)動(dòng)程序時(shí)相關(guān)的清理工作。
內(nèi)核模塊中用于輸出的函數(shù)式內(nèi)核空間的printk()而非用戶空間的printf(),printk()的用法和printf()相似,但前者可定義輸出級(jí)別。printk()可作為一種最基本的內(nèi)核調(diào)試手段
前者可以定義輸出級(jí)別,在 <內(nèi)核目錄>/include/linux/kernel.h中
1 2 3 4 5 6 7 8 | #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 condition */ #define KERN_INFO "<6>" /* informational */ #define KERN_DEBUG "<7>" /* debug-level messages */ |
未設(shè)定級(jí)別的,在<內(nèi)核目錄>/kernel/printk.c中定義
只有當(dāng)printk打印信息時(shí)的loglevel小于DEFAULT_CONSOLE_LOGLEVEL的值(優(yōu)先級(jí)高于console loglevel),這些信息才會(huì)被打印到console上。
模塊聲明與描述
模塊編譯
首先看看Makefile文件:
build是編譯內(nèi)核模塊需要的Makefile的路徑,Ubuntu下是/lib/modules/2.6.31-14-generic/build
如果是Arm平臺(tái)的開發(fā)板,則-C選項(xiàng)指定的位置(即內(nèi)核源代碼目錄),其中保存有內(nèi)核的頂層Makefile文件.
make -C $(KERNEL_BUILD) M=$(shell pwd) modules 編譯內(nèi)核模塊。-C 將工作目錄轉(zhuǎn)到KERNEL_BUILD,調(diào)用該目錄下的Makefile,并向這個(gè)Makefile傳遞參數(shù)M的值是$(shell pwd) modules。
M=選項(xiàng)讓該makefile在構(gòu)造modules目標(biāo)之前返回到模塊源代碼目錄。然后modules目標(biāo)指向obj-m變量中設(shè)定的模塊
執(zhí)行make命令開始編譯模塊,生成hello.ko,執(zhí)行make clean可清除編譯產(chǎn)生的文件。
1、添加模塊
2、查看模塊
lsmod命令實(shí)際上讀取并分析/proc/modules文件,也可以cat /proc/modules文件
在模塊所在目錄下執(zhí)行
modinfo hello.ko可以查看模塊信息,如下所示
3、刪除模塊
模塊加載函數(shù)
Linux內(nèi)核模塊加載函數(shù)一般以__init標(biāo)識(shí)聲明,典型的模塊加載函數(shù)的形式如下:
模塊加載函數(shù)必須以“module_init(函數(shù)名)”的形式指定。它返回整形值,若初始化成功,應(yīng)返回0。而在初始化失敗時(shí)。應(yīng)該返回錯(cuò)誤編碼。
在linux內(nèi)核里,錯(cuò)誤編碼是一個(gè)負(fù)值,在<linux/errno.h>中定義,包含-ENODEV、-ENOMEM之類的符號(hào)值。返回相應(yīng)的錯(cuò)誤編碼是種非常好的習(xí)慣,因?yàn)橹挥羞@樣,用戶程序才可以利用perror等方法把它們轉(zhuǎn)換成有意義的錯(cuò)誤信息字符串。
在linux2.6內(nèi)核中,所有標(biāo)識(shí)為__init的函數(shù)在連接的時(shí)候都會(huì)放在.init.text(這是module_init宏在目標(biāo)代碼中增加的一個(gè)特殊區(qū)段,用于說明內(nèi)核初始化函數(shù)的所在位置)這個(gè)區(qū)段中,此外,所有的__init函數(shù)在區(qū)段.initcall.init中還保存著一份函數(shù)指針,在初始化時(shí)內(nèi)核會(huì)通過這些函數(shù)指針調(diào)用這些__init函數(shù),并在初始化完成后釋放init區(qū)段(包括.init.text和.initcall.init等)。所以大家應(yīng)注意不要在結(jié)束初始化后仍要使用的函數(shù)上使用這個(gè)標(biāo)記。
模塊卸載函數(shù)
Linux內(nèi)核卸載模塊函數(shù)一般以__exit標(biāo)識(shí)聲明,典型的模塊卸載函數(shù)的形式如下:
模塊卸載函數(shù)在模塊卸載時(shí)被調(diào)用,不返回任何值,必須以”module_exit(函數(shù)名)”的形式來指定
與__init一樣__exit也可以使對(duì)應(yīng)函數(shù)在運(yùn)行完成后自動(dòng)回收內(nèi)存。
一般來說,模塊卸載函數(shù)完成與模塊加載函數(shù)相反的功能:
如果模塊加載函數(shù)注冊(cè)了 XXX模塊,則模塊卸載函數(shù)應(yīng)注銷XXX。
若模塊加載函數(shù)動(dòng)體申請(qǐng)了內(nèi)存,則模塊卸載函數(shù)應(yīng)釋放該內(nèi)存。
若模塊加載函數(shù)申請(qǐng)了硬件資源,則模塊卸載函數(shù)應(yīng)釋放這些硬件資源。
若模塊加載函數(shù)開啟了硬件,則模塊卸載函數(shù)應(yīng)關(guān)閉硬件。
內(nèi)核時(shí)間管理
(1)內(nèi)核中的時(shí)間概念
時(shí)間管理在linux內(nèi)核中占有非常重要的作用。
相對(duì)于事件驅(qū)動(dòng)而言,內(nèi)核中有大量函數(shù)是基于時(shí)間驅(qū)動(dòng)的。
有些函數(shù)是周期執(zhí)行的,比如每10毫秒刷新一次屏幕;
有些函數(shù)是推后一定時(shí)間執(zhí)行的,比如內(nèi)核在500毫秒后執(zhí)行某項(xiàng)任務(wù)。
要區(qū)分:
*絕對(duì)時(shí)間和相對(duì)時(shí)間
*周期性產(chǎn)生的事件和推遲執(zhí)行的事件
周期性事件是由系統(tǒng)系統(tǒng)定時(shí)器驅(qū)動(dòng)的
(2)HZ值
內(nèi)核必須在硬件定時(shí)器的幫助下才能計(jì)算和管理時(shí)間。
定時(shí)器產(chǎn)生中斷的頻率稱為節(jié)拍率(tick rate)。
在內(nèi)核中指定了一個(gè)變量HZ,內(nèi)核初始化的時(shí)候會(huì)根據(jù)這個(gè)值確定定時(shí)器的節(jié)拍率。
HZ定義在<asm/param.h>,在i386平臺(tái)上,目前采用的HZ值是1000。
也就是時(shí)鐘中斷每秒發(fā)生1000次,周期為1毫秒。即:
#define HZ 1000
注意!HZ不是個(gè)固定不變的值,它是可以更改的,可以在內(nèi)核源代碼配置的時(shí)候輸入。
不同的體系結(jié)構(gòu)其HZ值是不一樣的,比如arm就采用100。
如果在驅(qū)動(dòng)中要使用系統(tǒng)的中斷頻率,直接使用HZ,而不要用100或1000
a.理想的HZ值
i386的HZ值一直采用100,直到2.5版后才改為1000。
提高節(jié)拍率意味著時(shí)鐘中斷產(chǎn)生的更加頻繁,中斷處理程序也會(huì)更頻繁地執(zhí)行。
帶來的好處有:
*內(nèi)核定時(shí)器能夠以更高的頻率和更高的準(zhǔn)確度運(yùn)行
*依賴定時(shí)器執(zhí)行的系統(tǒng)調(diào)用,比如poll()和select(),運(yùn)行的精度更高
*提高進(jìn)程搶占的準(zhǔn)確度
(縮短了調(diào)度延時(shí),如果進(jìn)程還剩2ms時(shí)間片,在10ms的調(diào)度周期下,進(jìn)程會(huì)多運(yùn)行8ms。
由于耽誤了搶占,對(duì)于一些對(duì)時(shí)間要求嚴(yán)格的任務(wù)會(huì)產(chǎn)生影響)
壞處有:
*節(jié)拍率要高,系統(tǒng)負(fù)擔(dān)越重。
中斷處理程序?qū)⒄加酶嗟奶幚砥鲿r(shí)間。
(3)jiffies
全局變量jiffies用于記錄系統(tǒng)啟動(dòng)以來產(chǎn)生的節(jié)拍的總數(shù)。
啟動(dòng)時(shí),jiffies初始化為0,此后每次時(shí)鐘中斷處理程序都會(huì)增加該變量的值。
這樣,系統(tǒng)啟動(dòng)后的運(yùn)行時(shí)間就是jiffies/HZ秒
jiffies定義于<linux/jiffies.h>中:
extern unsigned long volatile jiffies;
jiffies變量總是為unsigned long型。
因此在32位體系結(jié)構(gòu)上是32位,而在64位體系上是64位。
對(duì)于32位的jiffies,如果HZ為1000,49.7天后會(huì)溢出。
雖然溢出的情況不常見,但程序在檢測(cè)超時(shí)時(shí)仍然可能因?yàn)榛乩@而導(dǎo)致錯(cuò)誤。
linux提供了4個(gè)宏來比較節(jié)拍計(jì)數(shù),它們能正確地處理節(jié)拍計(jì)數(shù)回繞。
unknown通常是指jiffies,known是需要對(duì)比的值(常常是一個(gè)jiffies加減后計(jì)算出的相對(duì)值)
例:
time_before可以理解為如果在超時(shí)(timeout)之前(before)完成
*系統(tǒng)中還聲明了一個(gè)64位的值jiffies_64,在64位系統(tǒng)中jiffies_64和jiffies是一個(gè)值。
可以通過get_jiffies_64()獲得這個(gè)值。
*使用
(4)獲得當(dāng)前時(shí)間
驅(qū)動(dòng)程序中一般不需要知道墻鐘時(shí)間(也就是年月日的時(shí)間)。但驅(qū)動(dòng)可能需要處理絕對(duì)時(shí)間。
為此,內(nèi)核提供了兩個(gè)結(jié)構(gòu)體,都定義在<linux/time.h>:
a.
較老,但很流行。采用秒和毫秒值,保存了1970年1月1日0點(diǎn)以來的秒數(shù)
b.
較新,采用秒和納秒值保存時(shí)間。
c.do_gettimeofday()
該函數(shù)用通常的秒或微秒來填充一個(gè)指向struct timeval的指針變量,原型如下:
d.current_kernel_time()
該函數(shù)可用于獲得timespec
確定時(shí)間的延遲執(zhí)行
設(shè)備驅(qū)動(dòng)程序經(jīng)常需要將某些特定代碼延遲一段時(shí)間后執(zhí)行,通常是為了讓硬件能完成某些任務(wù)。
長(zhǎng)于定時(shí)器周期(也稱為時(shí)鐘嘀嗒)的延遲可以通過使用系統(tǒng)時(shí)鐘完成,而非常短的延時(shí)則通過軟件循環(huán)的方式完成
(1)短延時(shí)
對(duì)于那些最多幾十個(gè)毫秒的延遲,無法借助系統(tǒng)定時(shí)器。
系統(tǒng)通過軟件循環(huán)提供了下面的延遲函數(shù):
這三個(gè)延遲函數(shù)均是忙等待函數(shù),在延遲過程中無法運(yùn)行其他任務(wù)。
(2)長(zhǎng)延時(shí)
a.在延遲到期前讓出處理器
在等待期間可以讓出處理器,但系統(tǒng)無法進(jìn)入空閑模式(因?yàn)檫@個(gè)進(jìn)程始終在進(jìn)行調(diào)度),不利于省電。
b.超時(shí)函數(shù)
使用方式:
進(jìn)程經(jīng)過2秒后會(huì)被喚醒。如果不希望被用戶空間打斷,可以將進(jìn)程狀態(tài)設(shè)置為TASK_UNINTERRUPTIBLE。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | #include <linux/init.h> #include <linux/module.h> #include <linux/time.h> #include <linux/sched.h> #include <linux/delay.h> static int __init test_init( void ) { set_current_state(TASK_INTERRUPTIBLE); schedule_timeout(5 * HZ); printk(KERN_INFO "Hello Micky\n" ); return 0; } static void __exit test_exit( void ) { } module_init(test_init); module_exit(test_exit); MODULE_LICENSE( "GPL" ); MODULE_AUTHOR( "Micky Liu" ); MODULE_DESCRIPTION( "Test for delay" ); |
(3)等待隊(duì)列
使用等待隊(duì)列也可以實(shí)現(xiàn)長(zhǎng)延遲。
在延遲期間,當(dāng)前進(jìn)程在等待隊(duì)列中睡眠。
進(jìn)程在睡眠時(shí),需要根據(jù)所等待的事件鏈接到某一個(gè)等待隊(duì)列。
a.聲明等待隊(duì)列
等待隊(duì)列實(shí)際上就是一個(gè)進(jìn)程鏈表,鏈表中包含了等待某個(gè)特定事件的所有進(jìn)程。
要想把進(jìn)程加入等待隊(duì)列,驅(qū)動(dòng)首先要在模塊中聲明一個(gè)等待隊(duì)列頭,并將它初始化。
靜態(tài)初始化
動(dòng)態(tài)初始化
b.等待函數(shù)
進(jìn)程通過調(diào)用下面函數(shù)可以在某個(gè)等待隊(duì)列中休眠固定的時(shí)間:
調(diào)用這兩個(gè)函數(shù)后,進(jìn)程會(huì)在給定的等待隊(duì)列q上休眠,但會(huì)在超時(shí)(timeout)到期時(shí)返回。
如果超時(shí)到期,則返回0,如果進(jìn)程被其他事件喚醒,則返回剩余的時(shí)間數(shù)。
如果沒有等待條件,則將condition設(shè)為0
使用方式:
(4)內(nèi)核定時(shí)器
還有一種將任務(wù)延遲執(zhí)行的方法是采用內(nèi)核定時(shí)器。
與前面幾種延遲方法不同,內(nèi)核定時(shí)器并不會(huì)阻塞當(dāng)前進(jìn)程,
啟動(dòng)一個(gè)內(nèi)核定時(shí)器只是聲明了要在未來的某個(gè)時(shí)刻執(zhí)行一項(xiàng)任務(wù),當(dāng)前進(jìn)程仍然繼續(xù)執(zhí)行。
不要用定時(shí)器完成硬實(shí)時(shí)任務(wù)
定時(shí)器由結(jié)構(gòu)timer_list表示,定義在<linux/timer.h>
內(nèi)核在<linux/timer.h>中提供了一系列管理定時(shí)器的接口。
a.創(chuàng)建定時(shí)器
b.初始化定時(shí)器
c.定時(shí)器的執(zhí)行函數(shù)
超時(shí)處理函數(shù)的原型如下:
可以利用data參數(shù)用一個(gè)處理函數(shù)處理多個(gè)定時(shí)器??梢詫ata設(shè)為0
d.激活定時(shí)器
定時(shí)器一旦激活就開始運(yùn)行。
e.更改已激活的定時(shí)器的超時(shí)時(shí)間
可以用于那些已經(jīng)初始化但還沒激活的定時(shí)器,
如果調(diào)用時(shí)定時(shí)器未被激活則返回0,否則返回1。
一旦mod_timer返回,定時(shí)器將被激活。
f.刪除定時(shí)器
被激活或未被激活的定時(shí)器都可以使用,如果調(diào)用時(shí)定時(shí)器未被激活則返回0,否則返回1。
不需要為已經(jīng)超時(shí)的定時(shí)器調(diào)用,它們被自動(dòng)刪除
g.同步刪除
在smp系統(tǒng)中,確保返回時(shí),所有的定時(shí)器處理函數(shù)都退出。不能在中斷上下文使用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 | #include <linux/init.h> #include <linux/module.h> #include <linux/time.h> #include <linux/sched.h> #include <linux/delay.h> #include <linux/timer.h> struct timer_list my_timer; static void timer_handler(unsigned long arg) { printk(KERN_INFO "%s %d Hello Micky! arg=%lu\n" ,__func__, __LINE__, arg ); } static int __init test_init( void ) { init_timer(&my_timer); my_timer.expires = jiffies + 5 * HZ; my_timer.function = timer_handler; my_timer.data = 10; add_timer(&my_timer); return 0; } static void __exit test_exit( void ) { del_timer(&my_timer); } module_init(test_init); module_exit(test_exit); MODULE_LICENSE( "GPL" ); MODULE_AUTHOR( "Micky Liu" ); MODULE_DESCRIPTION( "Test for timer" ); #include <linux/init.h> #include <linux/module.h> #include <linux/time.h> #include <linux/sched.h> #include <linux/delay.h> #include <linux/timer.h> struct timer_list my_timer; static void timer_handler(unsigned long arg) { printk(KERN_INFO "%s %d Hello Micky! arg=%lu\n" ,__func__, __LINE__, arg ); } static int __init test_init( void ) { init_timer(&my_timer); //my_timer.expires = jiffies + 5 * HZ; my_timer.function = timer_handler; my_timer.data = 10; //add_timer(&my_timer); mod_timer(&my_timer, jiffies + 5 * HZ); return 0; } static void __exit test_exit( void ) { del_timer(&my_timer); } module_init(test_init); module_exit(test_exit); MODULE_LICENSE( "GPL" ); MODULE_AUTHOR( "Micky Liu" ); MODULE_DESCRIPTION( "Test for timer" ); |
不確定時(shí)間的延遲執(zhí)行
(1)什么是不確定時(shí)間的延遲
前面介紹的是確定時(shí)間的延遲執(zhí)行,但在寫驅(qū)動(dòng)的過程中經(jīng)常遇到這種情況:
用戶空間程序調(diào)用read函數(shù)從設(shè)備讀數(shù)據(jù),但設(shè)備中當(dāng)前沒有產(chǎn)生數(shù)據(jù)。
此時(shí),驅(qū)動(dòng)的read函數(shù)默認(rèn)的操作是進(jìn)入休眠,一直等待到設(shè)備中有了數(shù)據(jù)為止。
這種等待就是不定時(shí)的延遲,通常采用休眠機(jī)制來實(shí)現(xiàn)。
(2)休眠
休眠是基于等待隊(duì)列實(shí)現(xiàn)的,前面我們已經(jīng)介紹過wait_event系列函數(shù),
但現(xiàn)在我們將不會(huì)有確定的休眠時(shí)間。
當(dāng)進(jìn)程被置入休眠時(shí),會(huì)被標(biāo)記為特殊狀態(tài)并從調(diào)度器的運(yùn)行隊(duì)列中移走。
直到某些事件發(fā)生后,如設(shè)備接收到數(shù)據(jù),則將進(jìn)程重新設(shè)為運(yùn)行態(tài)并進(jìn)入運(yùn)行隊(duì)列進(jìn)行調(diào)度。
休眠函數(shù)的頭文件是<linux/wait.h>,具體的實(shí)現(xiàn)函數(shù)在kernel/wait.c中。
a.休眠的規(guī)則
*永遠(yuǎn)不要在原子上下文中休眠
*當(dāng)被喚醒時(shí),我們無法知道睡眠了多少時(shí)間,也不知道醒來后是否獲得了我們需要的資源
*除非知道有其他進(jìn)程會(huì)在其他地方喚醒我們,否則進(jìn)程不能休眠
b.等待隊(duì)列的初始化
見前文
c.休眠函數(shù)
linux最簡(jiǎn)單的睡眠方式為wait_event宏。該宏在實(shí)現(xiàn)休眠的同時(shí),檢查進(jìn)程等待的條件。
A.
B.
q: 是等待隊(duì)列頭,注意是采用值傳遞。
condition: 任意一個(gè)布爾表達(dá)式,在條件為真之前,進(jìn)程會(huì)保持休眠。
注意!進(jìn)程需要通過喚醒函數(shù)才可能被喚醒,此時(shí)需要檢測(cè)條件。
如果條件滿足,則被喚醒的進(jìn)程真正醒來;
如果條件不滿足,則進(jìn)程繼續(xù)睡眠。
d.喚醒函數(shù)
當(dāng)我們的進(jìn)程睡眠后,需要由其他的某個(gè)執(zhí)行線程(可能是另一個(gè)進(jìn)程或中斷處理例程)喚醒。
喚醒函數(shù):
#include <linux/wait.h>
1.
2.
wake_up會(huì)喚醒等待在給定queue上的所有進(jìn)程。
而wake_up_interruptible喚醒那些執(zhí)行可中斷休眠的進(jìn)程。
實(shí)踐中,約定做法是在使用wait_event時(shí)使用wake_up,而使用wait_event_interruptible時(shí)使用wake_up_interruptible。
聯(lián)系客服