由於前面的學習中有用到 第十一章 核心資料結構類型 的知識,所以我先看了。要點如下:
將linux 移植到新的體繫結構時,開發人員遇到的若干問題都與不正確的資料類型有關。堅持使用嚴格的資料類型和使用 -Wall -Wstrict-prototypes 進行編譯可能避免大部分的 bug。
核心資料使用的資料類型主要分為 3 個類型: 標準 C 語言類型、確定大小的類型和特定核心對象的類型。
標準 C 語言類型
當需要“一個2位元組填充符”或“用一個4位元組字串來代表某個東西”,就不能使用標準C語言類型,因為在不同的體繫結構,C 語言的資料類型所佔的空間大小不同。後面的datasize 程式實驗展示了使用者空間各種 C 的資料類型在當前平台所佔空間的大小。而且有的構架,核心空間和使用者空間的C 資料類型所佔空間大小也可能不同。kdatasize模組顯示了當前模組的核心空間C 資料類型所佔空間大小。
儘管概念上地址是指標,但使用一個無符號整型可以更好地實現記憶體管理; 核心把實體記憶體看成一個巨型數組, 記憶體位址就是該數組的索引。 我們可以方便地對指標取值,但直接處理記憶體位址時,我們幾乎從不會以這種方式對他取值。使用一個整數類型避免了這種取值,因此避免了 bug。所以,利用至少在 Linux 目前支援的所有平台上,指標和長整型始終是相同大小的這一事實,核心中記憶體位址常常是 unsigned long。
C99 標準定義了 intptr_t 和 uintptr_t 類型,它們是能夠儲存指標值的整型變數。但沒在 2.6 核心中幾乎沒使用。
確定大小的類型
當需要知道你定義的資料的大小時,可以使用核心提供的下列資料類型(所有的資料聲明在 <asm/types.h>, 被包含在 <linux/types.h> ):
u8; /* unsigned byte (8 bits) */u16; /* unsigned word (16 bits) */u32; /* unsigned 32-bit value */u64; /* unsigned 64-bit value */
/*雖然很少需要有符號類型,但是如果需要,只要用 s 代替 u*/
若一個使用者空間程式需要使用這些類型,可在符號前加一個雙底線: __u8和其它類型是獨立於 __KERNEL__ 定義的。
這些類型是 Linux 特定的,它們妨礙了移植軟體到其他的 Unix 機器。新的編譯器系統支援 C99-標準 類型,如 uint8_t 和 uint32_t。若考慮移植性,使用這些類型比 Linux特定的變體要好。
介面特定的類型(_t 類型
)
核心中最常用的資料類型由它們自己的 typedef 聲明,阻止了任何移植性問題。“介面特定(interface-specific)”由某個庫定義的一種資料類型, 以便為了某個特定的資料結構提供介面。很多 _t 類型在 <linux/types.h> 中定義。
注意:近來已經很少定義新的介面特定的類型。有許多核心開發人員已經不再喜歡使用 typedef 語句,他們寧願看到代碼中直接使用的真實類型資訊。很多老的介面特定類型在核心中保留,他們不會很快消失。
即使沒有定義介面特定類型,也應該始終是用和核心其他部分保持一致、適當的資料類型。只要驅動使用了這種“定製”類型的函數,但又不遵照約定,編譯器會發出警告,這時使用 -Wall 編譯器選項並小心去除所有的警告,就可以確信代碼的可移植性了。
_t 類型的主要問題是:列印它們時,常常不容易選擇正確的 printk 或 printf 格式。列印介面特定的資料的最好方法是:將其強制轉換為可能的最大類型(常常是 long 或 unsigned long ) 並用相應的格式列印。
其他移植性問題
移植的一個通常規則是:避免使用顯式的常量值,要使用預先處理宏使常量值參數化。
時間間隔
當處理時間間隔時,不要假定每秒的jiffies個數,不是每個 Linux 平台都以固定的速度運行.當計算時間間隔時,要使用 HZ ( 每秒的定時器中斷數 ) 來標定你的時間。s3c2410的HZ值預設為200。
頁大小
當使用記憶體時,記住一個記憶體頁是 PAGE_SIZE 位元組, 不是 4KB。相關的宏定義是 PAGE_SIZE 和 PAGE_SHIT(包含將一個地址移位來獲得它的頁號的位元),在 <asm/page.h> 中定義。如果使用者空間程式需要這些資訊,可以使用 getpagesize 庫函數。
若一個驅動需要 16 KB 來暫存資料,一個可移植得解決方案是 get_order:
#include <asm/page.h>int order = get_order(16*1024);buf = get_free_pages(GFP_KERNEL, order);/*get_order 的參數必須是 2 的冪*/
位元組序
不要假設位元組序。 代碼應該編寫成不依賴所操作資料的位元組序的方式。
標頭檔 <asm/byteorder.h> 定義:
#ifdef __ARMEB__#include <linux/byteorder/big_endian.h>#else#include <linux/byteorder/little_endian.h>#endif
在<linux/byteorder/big_endian.h>中定義了__BIG_ENDIAN ,而在<linux/byteorder/little_endian.h>中定義了__LITTLE_ENDIAN,這些依賴處理器的位元組序當處理位元組序問題時,需要編碼一堆類似 #ifdef __LITTTLE_ENDIAN 的條件陳述式。
但是還有一個更好的方法:Linux 核心有一套宏定義來處理處理器位元組序和特定位元組序之間的轉換。例如:
u32 cpu_to_le32 (u32);u32 le32_to_cpu (u32);/*這些宏定義將一個CPU使用的值轉換成一個無符號的32位小頭數值,無論 CPU 是大端還是小端,也不管是不是32 位處理器。在沒有轉換工作需要做時,返回未修改的值。*//*有很多類似的函數在 <linux/byteorder/big_endian.h> 和 <linux/byteorder/little_endian.h> 中定義*/
資料對齊
編寫可移植代碼而值得考慮的最後一個問題是如何訪問未對齊的資料。存取不對齊的資料應當使用下列宏:
#include <asm/unaligned.h>get_unaligned(ptr);put_unaligned(val, ptr);
這些宏是無類型的,並對各總資料項目,不管是 1、2、4或 8 個位元組,他們都有效,並且在所有核心版本中都有定義。
關於對齊的另一個問題是資料結構的跨平台移植性。同樣的資料結構在不同的平台上可能被不同地編譯。為了編寫可以跨體系移植的資料結構,應當始終強制資料項目的自然對齊。自然對齊(natural alignment)指的是:資料項目大小的整數倍的地址上儲存資料項目。 應當使用填充符避免強制自然對齊時編譯器移動資料結構的欄位,在資料結構中留下空洞。
dataalign 程式實驗展示了編譯器如何強制對齊。
為了目標處理器的良好效能,編譯器可能悄悄地插入填充符到結構中,來保證每個成員是對齊的。若定義一個和裝置要求的結構體相匹配結構,自動填滿符會破壞這個意圖。解決這個問題的方法是告訴編譯器這個結構必須是"緊湊的", 不能增加填充符。例如下列的定義:
struct
{
u16 id;
u64 lun;
u16 reserved1;
u32 reserved2;
}
__attribute__ ((packed)) scsi;
/*如果在 64-位平台上編譯這個結構,若沒有 __attribute__ ((packed)), lun 成員可能在前面被添加 2 個或 6 個填充符位元組。指標和錯誤值*/
你還可以在利用ARM9和USB網路攝影機進行視頻採集的servfox原始碼的spcaframe.h標頭檔中找到這種方法的實際應用:
struct frame_t{
char header[5];
int nbframe;
double seqtimes;
int deltatimes;
int w;
int h;
int size;
int format;
unsigned short bright;
unsigned short contrast;
unsigned short colors;
unsigned short exposure;
unsigned char wakeup;
int acknowledge;
} __attribute__ ((packed));
struct client_t{
char message[4];
unsigned char x;
unsigned char y;
unsigned char fps;
unsigned char updobright;
unsigned char updocontrast;
unsigned char updocolors;
unsigned char updoexposure;
unsigned char updosize;
unsigned char sleepon;
} __attribute__ ((packed));
指標和錯誤值
許多核心介面通過將錯誤值編碼到指標值中來返回錯誤資訊。這樣的資訊必須小心使用,因為它們的傳回值不能簡單地與 NULL 比較。為協助建立和使用這類介面, <linux/err.h>提供了這樣的函數:
void *ERR_PTR(long error);/*將錯誤值編碼到指標值中,error 是常見的負值錯誤碼*/long IS_ERR(const void *ptr); /*測試返回的指標是不是一個錯誤碼*/long PTR_ERR(const void *ptr); /*抽取實際的錯誤碼,只有在IS_ERR 返回一個真值時使用,否則一個有效指標*/
鏈表
作業系統核心常需要維護資料結構的鏈表。Linux 核心已經同時有幾個鏈表實現。為減少複製代碼的數量, 核心已經建立了一個標準環形雙向鏈表,並鼓勵需要操作鏈結表的人使用這個設施.
使用鏈表介面時,應當記住列表函數沒做加鎖。若驅動可能同一個列表並行作業,就必須實現一個鎖方案。
為使用鏈表機制,驅動必須包含檔案 <linux/list.h> ,它定義了一個簡單的list_head 類型 結構:
struct list_head { struct list_head *next, *prev; };
實際代碼中使用的鏈表幾乎總是由某個結構類型組成, 每個結構描述鏈表中的一項. 為使用 Linux 鏈表,只需嵌入一個 list_head 在構成在這個鏈表的結構裡面。鏈表頭常常是一個獨立的 list_head 結構。顯示了這個簡單的 struct list_head 是如何用來維護一個資料結構的列表的.
/*鏈表頭必須在使用前初始化,有兩種形式: *//*一是運行時初始化:*/struct list_head todo_list;INIT_LIST_HEAD(&todo_list);/*二是編譯時間初始化:*/LIST_HEAD(todo_list);list_add(struct list_head *new, struct list_head *head);/*在緊接著鏈表 head 後面增加新項 。注意:head 不需要是鏈表名義上的頭; 如果你傳遞一個 list_head 結構, 它在鏈表某處的中間, 新的項緊靠在它後面。 因為 Linux 鏈表是環形的, 鏈表頭通常和任何其他的項沒有區別*/list_add_tail(struct list_head *new, struct list_head *head);/*在給定鏈表頭前面增加新項,即在鏈表的尾部增加一個新項。*/list_del(struct list_head *entry);list_del_init(struct list_head *entry);/*給定的項從隊列中去除。 如果入口項可能註冊在另外的鏈表中, 你應當使用 list_del_init, 它重新初始化這個鏈表指標.*/list_move(struct list_head *entry, struct list_head *head);list_move_tail(struct list_head *entry, struct list_head *head);/*給定的入口項從它當前的鏈表裡去除並且增加到 head 的開始。為安放入口項在新鏈表的末尾, 使用 list_move_tail 代替*/list_empty(struct list_head *head);/*如果給定鏈表是空, 返回一個非零值.*/list_splice(struct list_head *list, struct list_head *head);/*將 list 緊接在 head 之後來串連 2 個鏈表.*//*list_entry是將一個 list_head 結構指標轉換到一個指向包含它的結構體的指標 。看了源碼你就會發現,似曾相識。是的,其實在模組的open方法中已經用到了container_of 。list_entry的變體好有很多,看源碼就知道了*/#define list_entry(ptr, type, member) \ container_of(ptr, type, member)
ARM9實驗板實驗
datasize實驗
實驗中有用到uname函數,介紹如下(載自《UNIX環境進階編程》,第6章系統資料檔案和資訊 6.8 系統標識):
POSIX.1定義了uname函數,它返回與主機和作業系統有關的資訊。
#include <sys/utsname.h>
int uname(struct utsname *n a m e) ;
返回:若成功則為非負值,若出錯則為-1
通過該函數的參數向其傳遞一個utsname結構的地址,然後該函數填寫此結構。POSIX.1隻定義了該結構中至少需提供的欄位(它們都是字元數組),而每個數組的長度則由實現確定。某些實現在該結構中提供了另外一些欄位。在曆史上,系統 V為每個數組分配9個位元組,其中有1個位元組用於字串結束符( null字元)。
struct utsname {
char sysname[9]; /* name of the operating system */
char nodename[9]; /* name of this node */
char release[9]; /* current release of operating system */
char version[9]; /* current version of this release */
char machine[9]; /* name of hardware type */
} ;
utsname結構中的資訊通常可用uname(1)命令列印。
實驗程式源碼連結:datasize
kdatasize模組實驗
實驗中有用到utsname函數,源碼如下:
//<linux/utsname.h>struct new_utsname {char sysname[65];char nodename[65];char release[65];char version[65];char machine[65];char domainname[65];};static inline struct new_utsname *utsname(void){return ¤t->nsproxy->uts_ns->name;}
實驗模組源碼連結:kdatasize
kdataalign模組實驗
具體實驗原理請看源碼
實驗模組源碼連結:kdataalign
實驗現象:
[Tekkaman2440@SBC2440V4]#cd /tmp/[Tekkaman2440@SBC2440V4]#./datasizearch Size: char short int long ptr long-long u8 u16 u32 u64armv4tl 1 2 4 4 4 8 1 2 4 8[Tekkaman2440@SBC2440V4]#cd /lib/modules/[Tekkaman2440@SBC2440V4]#insmod kdatasize.koarch Size: char short int long ptr long-long u8 u16 u32 u64armv4tl 1 2 4 4 4 8 1 2 4 8insmod: cannot insert 'kdatasize.ko': No such device (-1): No such device[Tekkaman2440@SBC2440V4]#insmod kdataalign.koarch Align: char short int long ptr long-long u8 u16 u32 u64armv4tl 1 2 4 4 4 4 1 2 4 4insmod: cannot insert 'kdataalign.ko': No such device (-1): No such device |