解析Linux核心的基本的模組管理與時間管理操作---超時處理
核心模組管理
Linux裝置驅動會以核心模組的形式出現,因此學會編寫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()是模組程式設計中最基本也是必須的兩個函式。
module_init()是驅動程式初始化的入口點。
hello_exit是模組的退出和清理函式。此處可以做所有終止該驅動程式時相關的清理工作。
核心模組中用於輸出的函式式核心空間的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的值(優先順序高於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) //模組引數
模組編譯
首先看看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平臺的開發板,則-C選項指定的位置(即核心原始碼目錄),其中儲存有核心的頂層Makefile檔案.
make -C (shell pwd) modules 編譯核心模組。-C 將工作目錄轉到KERNEL_BUILD,呼叫該目錄下的Makefile,並向這個Makefile傳遞引數M的值是$(shell pwd) modules。
M=選項讓該makefile在構造modules目標之前返回到模組原始碼目錄。然後modules目標指向obj-m變數中設定的模組
執行make命令開始編譯模組,生成hello.ko,執行make clean可清除編譯產生的檔案。
1、新增模組
insmod hello.ko
2、檢視模組
lsmod |grep hello
lsmod命令實際上讀取並分析/proc/modules檔案,也可以cat /proc/modules檔案
在模組所在目錄下執行
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
模組載入函式
Linux核心模組載入函式一般以__init標識宣告,典型的模組載入函式的形式如下:
static int __init initialization_function(void) { //初始化程式碼 } module_init(initialization_function);
模組載入函式必須以“module_init(函式名)”的形式指定。它返回整形值,若初始化成功,應返回0。而在初始化失敗時。應該返回錯誤編碼。
在linux核心裡,錯誤編碼是一個負值,在<linux/errno.h>中定義,包含-ENODEV、-ENOMEM之類的符號值。返回相應的錯誤編碼是種非常好的習慣,因為只有這樣,使用者程式才可以利用perror等方法把它們轉換成有意義的錯誤資訊字串。
在linux2.6核心中,所有標識為__init的函式在連線的時候都會放在.init.text(這是module_init巨集在目的碼中增加的一個特殊區段,用於說明核心初始化函式的所在位置)這個區段中,此外,所有的__init函式在區段.initcall.init中還儲存著一份函式指標,在初始化時核心會通過這些函式指標呼叫這些__init函式,並在初始化完成後釋放init區段(包括.init.text和.initcall.init等)。所以大家應注意不要在結束初始化後仍要使用的函式上使用這個標記。
模組解除安裝函式
Linux核心解除安裝模組函式一般以__exit標識宣告,典型的模組解除安裝函式的形式如下:
static void __exit cleanup_function(void) { //釋放程式碼 } module_exit(cleanup_function);
模組解除安裝函式在模組解除安裝時被呼叫,不返回任何值,必須以”module_exit(函式名)”的形式來指定
與__init一樣__exit也可以使對應函式在執行完成後自動回收記憶體。
一般來說,模組解除安裝函式完成與模組載入函式相反的功能:
如果模組載入函式註冊了 XXX模組,則模組解除安裝函式應登出XXX。
若模組載入函式動體申請了記憶體,則模組解除安裝函式應釋放該記憶體。
若模組載入函式申請了硬體資源,則模組解除安裝函式應釋放這些硬體資源。
若模組載入函式開啟了硬體,則模組解除安裝函式應關閉硬體。
核心時間管理
(1)核心中的時間概念
時間管理在linux核心中佔有非常重要的作用。
相對於事件驅動而言,核心中有大量函式是基於時間驅動的。
有些函式是週期執行的,比如每10毫秒重新整理一次螢幕;
有些函式是推後一定時間執行的,比如核心在500毫秒後執行某項任務。
要區分:
*絕對時間和相對時間
*週期性產生的事件和推遲執行的事件
週期性事件是由系統系統定時器驅動的
(2)HZ值
核心必須在硬體定時器的幫助下才能計算和管理時間。
定時器產生中斷的頻率稱為節拍率(tick rate)。
在核心中指定了一個變數HZ,核心初始化的時候會根據這個值確定定時器的節拍率。
HZ定義在<asm/param.h>,在i386平臺上,目前採用的HZ值是1000。
也就是時鐘中斷每秒發生1000次,週期為1毫秒。即:
#define HZ 1000
注意!HZ不是個固定不變的值,它是可以更改的,可以在核心原始碼配置的時候輸入。
不同的體系結構其HZ值是不一樣的,比如arm就採用100。
如果在驅動中要使用系統的中斷頻率,直接使用HZ,而不要用100或1000
a.理想的HZ值
i386的HZ值一直採用100,直到2.5版後才改為1000。
提高節拍率意味著時鐘中斷產生的更加頻繁,中斷處理程式也會更頻繁地執行。
帶來的好處有:
*核心定時器能夠以更高的頻率和更高的準確度執行
*依賴定時器執行的系統呼叫,比如poll()和select(),執行的精度更高
*提高程序搶佔的準確度
(縮短了排程延時,如果程序還剩2ms時間片,在10ms的排程週期下,程序會多執行8ms。
由於耽誤了搶佔,對於一些對時間要求嚴格的任務會產生影響)
壞處有:
*節拍率要高,系統負擔越重。
中斷處理程式將佔用更多的處理器時間。
(3)jiffies
全域性變數jiffies用於記錄系統啟動以來產生的節拍的總數。
啟動時,jiffies初始化為0,此後每次時鐘中斷處理程式都會增加該變數的值。
這樣,系統啟動後的執行時間就是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個巨集來比較節拍計數,它們能正確地處理節拍計數迴繞。
#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加減後計算出的相對值)
例:
unsignedlong timeout = jiffies + HZ/2;/* 0.5秒後超時 */ ... if(time_before(jiffies, timeout)){ /* 沒有超時,很好 */ }else{ /* 超時了,發生錯誤 */
time_before可以理解為如果在超時(timeout)之前(before)完成
*系統中還聲明瞭一個64位的值jiffies_64,在64位系統中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點以來的秒數
b.
struct timespec { time_t tv_sec;/* seconds */ long tv_nsec;/* nanoseconds */ };
較新,採用秒和納秒值儲存時間。
c.do_gettimeofday()
該函式用通常的秒或微秒來填充一個指向struct timeval的指標變數,原型如下:
#include <linux/time.h> void do_gettimeofday(struct timeval *tv);
d.current_kernel_time()
該函式可用於獲得timespec
#include <linux/time.h> struct timespec current_kernel_time(void);
確定時間的延遲執行
裝置驅動程式經常需要將某些特定程式碼延遲一段時間後執行,通常是為了讓硬體能完成某些任務。
長於定時器週期(也稱為時鐘嘀嗒)的延遲可以通過使用系統時鐘完成,而非常短的延時則通過軟體迴圈的方式完成
(1)短延時
對於那些最多幾十個毫秒的延遲,無法藉助系統定時器。
系統通過軟體迴圈提供了下面的延遲函式:
#include <linux/delay.h> /* 實際在<asm/delay.h> */ void ndelay(unsignedlong nsecs);/*延遲納秒 */ void udelay(unsignedlong usecs);/*延遲微秒 */ void mdelay(unsignedlong msecs);/*延遲毫秒 */
這三個延遲函式均是忙等待函式,在延遲過程中無法執行其他任務。
(2)長延時
a.在延遲到期前讓出處理器
while(time_before(jiffies, j1)) schedule();
在等待期間可以讓出處理器,但系統無法進入空閒模式(因為這個程序始終在進行排程),不利於省電。
b.超時函式
#include <linux/sched.h> signed long schedule_timeout(signed long timeout);
使用方式:
set_current_state(TASK_INTERRUPTIBLE); schedule_timeout(2*HZ);/* 睡2秒 */
程序經過2秒後會被喚醒。如果不希望被使用者空間打斷,可以將程序狀態設定為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)等待佇列
使用等待佇列也可以實現長延遲。
在延遲期間,當前程序在等待佇列中睡眠。
程序在睡眠時,需要根據所等待的事件連結到某一個等待佇列。
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;
要想把程序加入等待佇列,驅動首先要在模組中宣告一個等待佇列頭,並將它初始化。
靜態初始化
DECLARE_WAIT_QUEUE_HEAD(name);
動態初始化
wait_queue_head_t my_queue; init_waitqueue_head(&my_queue);
b.等待函式
程序通過呼叫下面函式可以在某個等待佇列中休眠固定的時間:
#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);
呼叫這兩個函式後,程序會在給定的等待佇列q上休眠,但會在超時(timeout)到期時返回。
如果超時到期,則返回0,如果程序被其他事件喚醒,則返回剩餘的時間數。
如果沒有等待條件,則將condition設為0
使用方式:
wait_queue_head_t wait; init_waitqueue_head(&wait); wait_event_interruptible_timeout(wait, 0, 2*HZ); /*當前程序在等待佇列wait中睡2秒 */
(4)核心定時器
還有一種將任務延遲執行的方法是採用核心定時器。
與前面幾種延遲方法不同,核心定時器並不會阻塞當前程序,
啟動一個核心定時器只是聲明瞭要在未來的某個時刻執行一項任務,當前程序仍然繼續執行。
不要用定時器完成硬實時任務
定時器由結構timer_list表示,定義在<linux/timer.h>
struct timer_list{ struct list_head entry;/* 定時器連結串列 */ unsignedlong expires;/* 以jiffies為單位的定時值 */ spinlock_t lock; void(*function)(unsignedlong);/* 定時器處理函式 */ unsignedlong data;/* 傳給定時器處理函式的引數 */ }
核心在<linux/timer.h>中提供了一系列管理定時器的介面。
a.建立定時器
struct timer_list my_timer;
b.初始化定時器
init_timer(&my_timer); /* 填充資料結構 */ my_timer.expires = jiffies + delay; my_timer.data = 0; my_timer.function = my_function;/*定時器到期時呼叫的函式*/
c.定時器的執行函式
超時處理函式的原型如下:
void my_timer_function(unsignedlong data);
可以利用data引數用一個處理函式處理多個定時器。可以將data設為0
d.啟用定時器
add_timer(&my_timer);
定時器一旦啟用就開始執行。
e.更改已啟用的定時器的超時時間
mod_timer(&my_timer, jiffies+ney_delay);
可以用於那些已經初始化但還沒啟用的定時器,
如果呼叫時定時器未被啟用則返回0,否則返回1。
一旦mod_timer返回,定時器將被啟用。
f.刪除定時器
del_timer(&my_timer);
被啟用或未被啟用的定時器都可以使用,如果呼叫時定時器未被啟用則返回0,否則返回1。
不需要為已經超時的定時器呼叫,它們被自動刪除
g.同步刪除
del_time_sync(&my_timer);
在smp系統中,確保返回時,所有的定時器處理函式都退出。不能在中斷上下文使用。
#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(unsignedlong 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(unsignedlong 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");
不確定時間的延遲執行
(1)什麼是不確定時間的延遲
前面介紹的是確定時間的延遲執行,但在寫驅動的過程中經常遇到這種情況:
使用者空間程式呼叫read函式從裝置讀資料,但裝置中當前沒有產生資料。
此時,驅動的read函式預設的操作是進入休眠,一直等待到裝置中有了資料為止。
這種等待就是不定時的延遲,通常採用休眠機制來實現。
(2)休眠
休眠是基於等待佇列實現的,前面我們已經介紹過wait_event系列函式,
但現在我們將不會有確定的休眠時間。
當程序被置入休眠時,會被標記為特殊狀態並從排程器的執行佇列中移走。
直到某些事件發生後,如裝置接收到資料,則將程序重新設為執行態並進入執行佇列進行排程。
休眠函式的標頭檔案是<linux/wait.h>,具體的實現函式在kernel/wait.c中。
a.休眠的規則
*永遠不要在原子上下文中休眠
*當被喚醒時,我們無法知道睡眠了多少時間,也不知道醒來後是否獲得了我們需要的資源
*除非知道有其他程序會在其他地方喚醒我們,否則程序不能休眠
b.等待佇列的初始化
見前文
c.休眠函式
linux最簡單的睡眠方式為wait_event巨集。該巨集在實現休眠的同時,檢查程序等待的條件。
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: 任意一個布林表示式,在條件為真之前,程序會保持休眠。
注意!程序需要通過喚醒函式才可能被喚醒,此時需要檢測條件。
如果條件滿足,則被喚醒的程序真正醒來;
如果條件不滿足,則程序繼續睡眠。
d.喚醒函式
當我們的程序睡眠後,需要由其他的某個執行執行緒(可能是另一個程序或中斷處理例程)喚醒。
喚醒函式:
#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喚醒那些執行可中斷休眠的程序。
實踐中,約定做法是在使用wait_event時使用wake_up,而使用wait_event_interruptible時使用wake_up_interruptible。
本文永久更新連結:http://embeddedlinux.org.cn/emb-linux/kernel-driver/201905/10-8659.html