解析Linux內核的基本的模塊管理與時間管理操作
內核模塊管理
Linux設備驅動會以內核模塊的形式出現(xiàn),因此學會編寫Linux內核模塊編程是學習linux設備驅動的先決條件。
Linux內核的整體結構非常龐大,其包含的組件非常多。我們把需要的功能都編譯到linux內核,以模塊方式擴展內核功能。
先來看下最簡單的內核模塊
#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,它們允許釋放內核占用的內存。
module_init()和hello_exit()是模塊編程中最基本也是必須的兩個函數(shù)。
module_init()是驅動程序初始化的入口點。
hello_exit是模塊的退出和清理函數(shù)。此處可以做所有終止該驅動程序時相關的清理工作。
內核模塊中用于輸出的函數(shù)式內核空間的printk()而非用戶空間的printf(),printk()的用法和printf()相似,但前者可定義輸出級別。printk()可作為一種最基本的內核調試手段
前者可以定義輸出級別,在 <內核目錄>/include/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 condition */ #define KERN_INFO "<6>" /* informational */ #define KERN_DEBUG "<7>" /* debug-level messages */
未設定級別的,在<內核目錄>/kernel/printk.c中定義
/* printk's without a loglevel use this.. */ #define DEFAULT_MESSAGE_LOGLEVEL 4 /* KERN_WARNING */ #define DEFAULT_CONSOLE_LOGLEVEL 7 /* anything MORE serious than KERN_DEBUG */
只有當printk打印信息時的loglevel小于DEFAULT_CONSOLE_LOGLEVEL的值(優(yōu)先級高于console loglevel),這些信息才會被打印到console上。
模塊聲明與描述
- 在linux模塊中,我們可以使用
- MODULE_LICENSE(license) //定義模塊的license,一般為GPL,或相關公司的license
- MODULE_AUTHOR //模塊的作者
- MODULE_DESCRIPTION //對模塊程序的描述,string
- MODULE_VERSION //版本
- MODULE_DEVICE_TABLE //模塊程序所支持的設備,string
- MODULE_ALIAS //別名
- MODULE_PARM(var,type) //模塊參數(shù)
模塊編譯
首先看看Makefile文件:
obj-m := hello.o KERNEL_BUILD := /lib/modules/$(shell uname -r)/build all: make -C $(KERNEL_BUILD) M=$(shell pwd) modules clean: -rm -rf *.o *.ko *.mod.c .*.cmd *.order *.symvers .tmpversions KERNELBUILD :=/lib/modules/$(shell uname -r)/
build是編譯內核模塊需要的Makefile的路徑,Ubuntu下是/lib/modules/2.6.31-14-generic/build
如果是Arm平臺的開發(fā)板,則-C選項指定的位置(即內核源代碼目錄),其中保存有內核的頂層Makefile文件.
make -C $(KERNEL_BUILD) M=$(shell pwd) modules 編譯內核模塊。-C 將工作目錄轉到KERNEL_BUILD,調用該目錄下的Makefile,并向這個Makefile傳遞參數(shù)M的值是$(shell pwd) modules。
M=選項讓該makefile在構造modules目標之前返回到模塊源代碼目錄。然后modules目標指向obj-m變量中設定的模塊
執(zhí)行make命令開始編譯模塊,生成hello.ko,執(zhí)行make clean可清除編譯產(chǎn)生的文件。
1、添加模塊
insmod hello.ko
2、查看模塊
lsmod | grep hello
lsmod命令實際上讀取并分析/proc/modules文件,也可以cat /proc/modules文件
在模塊所在目錄下執(zhí)行
modinfo hello.ko可以查看模塊信息,如下所示
filename: hello.ko alias: a simple module description: A simple Module author: Mikcy Liu license: GPL srcversion: 875C95631F4F336BBD4216C depends: vermagic: 3.5.0-17-generic SMP mod_unload modversions 686
3、刪除模塊
rmmod hello
模塊加載函數(shù)
Linux內核模塊加載函數(shù)一般以__init標識聲明,典型的模塊加載函數(shù)的形式如下:
static int __init initialization_function(void) { //初始化代碼 } module_init(initialization_function);
模塊加載函數(shù)必須以“module_init(函數(shù)名)”的形式指定。它返回整形值,若初始化成功,應返回0。而在初始化失敗時。應該返回錯誤編碼。
在linux內核里,錯誤編碼是一個負值,在<linux/errno.h>中定義,包含-ENODEV、-ENOMEM之類的符號值。返回相應的錯誤編碼是種非常好的習慣,因為只有這樣,用戶程序才可以利用perror等方法把它們轉換成有意義的錯誤信息字符串。
在linux2.6內核中,所有標識為__init的函數(shù)在連接的時候都會放在.init.text(這是module_init宏在目標代碼中增加的一個特殊區(qū)段,用于說明內核初始化函數(shù)的所在位置)這個區(qū)段中,此外,所有的__init函數(shù)在區(qū)段.initcall.init中還保存著一份函數(shù)指針,在初始化時內核會通過這些函數(shù)指針調用這些__init函數(shù),并在初始化完成后釋放init區(qū)段(包括.init.text和.initcall.init等)。所以大家應注意不要在結束初始化后仍要使用的函數(shù)上使用這個標記。
模塊卸載函數(shù)
Linux內核卸載模塊函數(shù)一般以__exit標識聲明,典型的模塊卸載函數(shù)的形式如下:
static void __exit cleanup_function(void) { //釋放代碼 } module_exit(cleanup_function);
模塊卸載函數(shù)在模塊卸載時被調用,不返回任何值,必須以”module_exit(函數(shù)名)”的形式來指定
與__init一樣__exit也可以使對應函數(shù)在運行完成后自動回收內存。
一般來說,模塊卸載函數(shù)完成與模塊加載函數(shù)相反的功能:
如果模塊加載函數(shù)注冊了 XXX模塊,則模塊卸載函數(shù)應注銷XXX。
若模塊加載函數(shù)動體申請了內存,則模塊卸載函數(shù)應釋放該內存。
若模塊加載函數(shù)申請了硬件資源,則模塊卸載函數(shù)應釋放這些硬件資源。
若模塊加載函數(shù)開啟了硬件,則模塊卸載函數(shù)應關閉硬件。
內核時間管理
(1)內核中的時間概念
時間管理在linux內核中占有非常重要的作用。
相對于事件驅動而言,內核中有大量函數(shù)是基于時間驅動的。
有些函數(shù)是周期執(zhí)行的,比如每10毫秒刷新一次屏幕;
有些函數(shù)是推后一定時間執(zhí)行的,比如內核在500毫秒后執(zhí)行某項任務。
要區(qū)分:
*絕對時間和相對時間
*周期性產(chǎn)生的事件和推遲執(zhí)行的事件
周期性事件是由系統(tǒng)系統(tǒng)定時器驅動的
(2)HZ值
內核必須在硬件定時器的幫助下才能計算和管理時間。
定時器產(chǎn)生中斷的頻率稱為節(jié)拍率(tick rate)。
在內核中指定了一個變量HZ,內核初始化的時候會根據(jù)這個值確定定時器的節(jié)拍率。
HZ定義在<asm/param.h>,在i386平臺上,目前采用的HZ值是1000。
也就是時鐘中斷每秒發(fā)生1000次,周期為1毫秒。即:
#define HZ 1000
注意!HZ不是個固定不變的值,它是可以更改的,可以在內核源代碼配置的時候輸入。
不同的體系結構其HZ值是不一樣的,比如arm就采用100。
如果在驅動中要使用系統(tǒng)的中斷頻率,直接使用HZ,而不要用100或1000
a.理想的HZ值
i386的HZ值一直采用100,直到2.5版后才改為1000。
提高節(jié)拍率意味著時鐘中斷產(chǎn)生的更加頻繁,中斷處理程序也會更頻繁地執(zhí)行。
帶來的好處有:
*內核定時器能夠以更高的頻率和更高的準確度運行
*依賴定時器執(zhí)行的系統(tǒng)調用,比如poll()和select(),運行的精度更高
*提高進程搶占的準確度
(縮短了調度延時,如果進程還剩2ms時間片,在10ms的調度周期下,進程會多運行8ms。
由于耽誤了搶占,對于一些對時間要求嚴格的任務會產(chǎn)生影響)
壞處有:
*節(jié)拍率要高,系統(tǒng)負擔越重。
中斷處理程序將占用更多的處理器時間。
(3)jiffies
全局變量jiffies用于記錄系統(tǒng)啟動以來產(chǎn)生的節(jié)拍的總數(shù)。
啟動時,jiffies初始化為0,此后每次時鐘中斷處理程序都會增加該變量的值。
這樣,系統(tǒng)啟動后的運行時間就是jiffies/HZ秒
jiffies定義于<linux/jiffies.h>中:
extern unsigned long volatile jiffies;
jiffies變量總是為unsigned long型。
因此在32位體系結構上是32位,而在64位體系上是64位。
對于32位的jiffies,如果HZ為1000,49.7天后會溢出。
雖然溢出的情況不常見,但程序在檢測超時時仍然可能因為回繞而導致錯誤。
linux提供了4個宏來比較節(jié)拍計數(shù),它們能正確地處理節(jié)拍計數(shù)回繞。
#include <linux/jiffies.h> #define time_after(unknown, known) // unknow > known #define time_before(unknown, known) // unknow < known #define time_after_eq(unknown, known) // unknow >= known #define time_before_eq(unknown, known) // unknow <= known
unknown通常是指jiffies,known是需要對比的值(常常是一個jiffies加減后計算出的相對值)
例:
unsigned long timeout = jiffies + HZ/2; /* 0.5秒后超時 */ ... if(time_before(jiffies, timeout)){ /* 沒有超時,很好 */ }else{ /* 超時了,發(fā)生錯誤 */
time_before可以理解為如果在超時(timeout)之前(before)完成
*系統(tǒng)中還聲明了一個64位的值jiffies_64,在64位系統(tǒng)中jiffies_64和jiffies是一個值。
可以通過get_jiffies_64()獲得這個值。
*使用
u64 j2; j2 = get_jiffies_64();
(4)獲得當前時間
驅動程序中一般不需要知道墻鐘時間(也就是年月日的時間)。但驅動可能需要處理絕對時間。
為此,內核提供了兩個結構體,都定義在<linux/time.h>:
a.
struct timeval { time_t tv_sec; /* seconds */ suseconds_t tv_usec; /* microseconds */ };
較老,但很流行。采用秒和毫秒值,保存了1970年1月1日0點以來的秒數(shù)
b.
struct timespec { time_t tv_sec; /* seconds */ long tv_nsec; /* nanoseconds */ };
較新,采用秒和納秒值保存時間。
c.do_gettimeofday()
該函數(shù)用通常的秒或微秒來填充一個指向struct timeval的指針變量,原型如下:
#include <linux/time.h> void do_gettimeofday(struct timeval *tv);
d.current_kernel_time()
該函數(shù)可用于獲得timespec
#include <linux/time.h> struct timespec current_kernel_time(void);
確定時間的延遲執(zhí)行
設備驅動程序經(jīng)常需要將某些特定代碼延遲一段時間后執(zhí)行,通常是為了讓硬件能完成某些任務。
長于定時器周期(也稱為時鐘嘀嗒)的延遲可以通過使用系統(tǒng)時鐘完成,而非常短的延時則通過軟件循環(huán)的方式完成
(1)短延時
對于那些最多幾十個毫秒的延遲,無法借助系統(tǒng)定時器。
系統(tǒng)通過軟件循環(huán)提供了下面的延遲函數(shù):
#include <linux/delay.h> /* 實際在<asm/delay.h> */ void ndelay(unsigned long nsecs); /*延遲納秒 */ void udelay(unsigned long usecs); /*延遲微秒 */ void mdelay(unsigned long msecs); /*延遲毫秒 */
這三個延遲函數(shù)均是忙等待函數(shù),在延遲過程中無法運行其他任務。
(2)長延時
a.在延遲到期前讓出處理器
while(time_before(jiffies, j1)) schedule();
在等待期間可以讓出處理器,但系統(tǒng)無法進入空閑模式(因為這個進程始終在進行調度),不利于省電。
b.超時函數(shù)
#include <linux/sched.h> signed long schedule_timeout(signed long timeout);
使用方式:
set_current_state(TASK_INTERRUPTIBLE); schedule_timeout(2*HZ); /* 睡2秒 */
進程經(jīng)過2秒后會被喚醒。如果不希望被用戶空間打斷,可以將進程狀態(tài)設置為TASK_UNINTERRUPTIBLE。
#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)等待隊列
使用等待隊列也可以實現(xiàn)長延遲。
在延遲期間,當前進程在等待隊列中睡眠。
進程在睡眠時,需要根據(jù)所等待的事件鏈接到某一個等待隊列。
a.聲明等待隊列
等待隊列實際上就是一個進程鏈表,鏈表中包含了等待某個特定事件的所有進程。
#include <linux/wait.h> struct __wait_queue_head { spinlock_t lock; struct list_head task_list; }; typedef struct __wait_queue_head wait_queue_head_t;
要想把進程加入等待隊列,驅動首先要在模塊中聲明一個等待隊列頭,并將它初始化。
靜態(tài)初始化
DECLARE_WAIT_QUEUE_HEAD(name);
動態(tài)初始化
wait_queue_head_t my_queue; init_waitqueue_head(&my_queue);
b.等待函數(shù)
進程通過調用下面函數(shù)可以在某個等待隊列中休眠固定的時間:
#include <linux/wait.h> long wait_event_timeout(wait_queue_head_t q,condition, long timeout); long wait_event_interruptible_timeout(wait_queue_head_t q, condition, long timeout);
調用這兩個函數(shù)后,進程會在給定的等待隊列q上休眠,但會在超時(timeout)到期時返回。
如果超時到期,則返回0,如果進程被其他事件喚醒,則返回剩余的時間數(shù)。
如果沒有等待條件,則將condition設為0
使用方式:
wait_queue_head_t wait; init_waitqueue_head(&wait); wait_event_interruptible_timeout(wait, 0, 2*HZ); /*當前進程在等待隊列wait中睡2秒 */
(4)內核定時器
還有一種將任務延遲執(zhí)行的方法是采用內核定時器。
與前面幾種延遲方法不同,內核定時器并不會阻塞當前進程,
啟動一個內核定時器只是聲明了要在未來的某個時刻執(zhí)行一項任務,當前進程仍然繼續(xù)執(zhí)行。
不要用定時器完成硬實時任務
定時器由結構timer_list表示,定義在<linux/timer.h>
struct timer_list{ struct list_head entry; /* 定時器鏈表 */ unsigned long expires; /* 以jiffies為單位的定時值 */ spinlock_t lock; void(*function)(unsigned long); /* 定時器處理函數(shù) */ unsigned long data; /* 傳給定時器處理函數(shù)的參數(shù) */ }
內核在<linux/timer.h>中提供了一系列管理定時器的接口。
a.創(chuàng)建定時器
struct timer_list my_timer;
b.初始化定時器
init_timer(&my_timer); /* 填充數(shù)據(jù)結構 */ my_timer.expires = jiffies + delay; my_timer.data = 0; my_timer.function = my_function; /*定時器到期時調用的函數(shù)*/
c.定時器的執(zhí)行函數(shù)
超時處理函數(shù)的原型如下:
void my_timer_function(unsigned long data);
可以利用data參數(shù)用一個處理函數(shù)處理多個定時器。可以將data設為0
d.激活定時器
add_timer(&my_timer);
定時器一旦激活就開始運行。
e.更改已激活的定時器的超時時間
mod_timer(&my_timer, jiffies+ney_delay);
可以用于那些已經(jīng)初始化但還沒激活的定時器,
如果調用時定時器未被激活則返回0,否則返回1。
一旦mod_timer返回,定時器將被激活。
f.刪除定時器
del_timer(&my_timer);
被激活或未被激活的定時器都可以使用,如果調用時定時器未被激活則返回0,否則返回1。
不需要為已經(jīng)超時的定時器調用,它們被自動刪除
g.同步刪除
del_time_sync(&my_timer);
在smp系統(tǒng)中,確保返回時,所有的定時器處理函數(shù)都退出。不能在中斷上下文使用。
#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");
不確定時間的延遲執(zhí)行
(1)什么是不確定時間的延遲
前面介紹的是確定時間的延遲執(zhí)行,但在寫驅動的過程中經(jīng)常遇到這種情況:
用戶空間程序調用read函數(shù)從設備讀數(shù)據(jù),但設備中當前沒有產(chǎn)生數(shù)據(jù)。
此時,驅動的read函數(shù)默認的操作是進入休眠,一直等待到設備中有了數(shù)據(jù)為止。
這種等待就是不定時的延遲,通常采用休眠機制來實現(xiàn)。
(2)休眠
休眠是基于等待隊列實現(xiàn)的,前面我們已經(jīng)介紹過wait_event系列函數(shù),
但現(xiàn)在我們將不會有確定的休眠時間。
當進程被置入休眠時,會被標記為特殊狀態(tài)并從調度器的運行隊列中移走。
直到某些事件發(fā)生后,如設備接收到數(shù)據(jù),則將進程重新設為運行態(tài)并進入運行隊列進行調度。
休眠函數(shù)的頭文件是<linux/wait.h>,具體的實現(xiàn)函數(shù)在kernel/wait.c中。
a.休眠的規(guī)則
*永遠不要在原子上下文中休眠
*當被喚醒時,我們無法知道睡眠了多少時間,也不知道醒來后是否獲得了我們需要的資源
*除非知道有其他進程會在其他地方喚醒我們,否則進程不能休眠
b.等待隊列的初始化
見前文
c.休眠函數(shù)
linux最簡單的睡眠方式為wait_event宏。該宏在實現(xiàn)休眠的同時,檢查進程等待的條件。
A.
void wait_event( wait_queue_head_t q, int condition);
B.
int wait_event_interruptible(wait_queue_head_t q, int condition);
q: 是等待隊列頭,注意是采用值傳遞。
condition: 任意一個布爾表達式,在條件為真之前,進程會保持休眠。
注意!進程需要通過喚醒函數(shù)才可能被喚醒,此時需要檢測條件。
如果條件滿足,則被喚醒的進程真正醒來;
如果條件不滿足,則進程繼續(xù)睡眠。
d.喚醒函數(shù)
當我們的進程睡眠后,需要由其他的某個執(zhí)行線程(可能是另一個進程或中斷處理例程)喚醒。
喚醒函數(shù):
#include <linux/wait.h>
1.
void wake_up( wait_queue_head_t *queue);
2.
void wake_up_interruptible( wait_queue_head_t *queue);
wake_up會喚醒等待在給定queue上的所有進程。
而wake_up_interruptible喚醒那些執(zhí)行可中斷休眠的進程。
實踐中,約定做法是在使用wait_event時使用wake_up,而使用wait_event_interruptible時使用wake_up_interruptible。