2012年7月10日 星期二

Linux Kernel Portability-Data type & Data Structure in Linux kernel (一)

  • 前言
Linux作為一個跨平台(處理器)的作業系統必須兼容許多不同的CPU 架構設計。也因此,Linux Kernel裡面有許多是為了Portability而產生的包含衍生的資料型態及提供的資料結構。
為了讓我們的Kernel 的module可以順利在我們的Target上或在各平台上順利執行,我們勢必就需要了解哪些方面會是Platform-Independent,Linux Kernel如何解決這個問題。
  • The Size of Data Types
在說明Kernel專有的資料型態前,我們先了解標準C資料型態在各平台的差異。以下圖為例,在常見的平台上,char、short、int、long long大小是一致的。但是long、ptr、word的大小則會因平台不同而有所差別。因此,如何在各平台確切的得到我們希望的變數大小就非常重要了。換句話說,假設我們希望某個變數他是8個bits,他就不會因為平台的不同而變成立如4個bits。
因此,Linux Kernel就定義了幾個標準大小的資料型態在include/linux/types.h。在include/linux/types.h裡他include了asm/types.h。以ARM平台為例,arch/arm/include/asm/types.h include了asm-generic/int-ll64.h。在int-ll64.h就定義了: 
u8    unsigned byte (8 bits)
u16   unsigned word (16 bits)
u32   unsigned 32-bit value
u64   unsigned 64-bit value
s8    signed byte (8 bits)
s16   signed word (16 bits)
s32   signed 32-bit value
s64   signed 64-bit value
u代表的是unsigned而s則是代表signed分別定義了8~64bits的標準大小型態。假設在user space的程式要使用固定長度的型別,記得前面要加上__例如"__u8"。

然而,C99規範了標準大小的資料型態uintN_t及intN_t對應著uN及sN定義在stdint.h。N代表8、16、 32、64。因此,不管是Kernel Module或是user space 的program特別是如果你的程式是想跨平台(OS)的,就盡量使用C99的資料型態而非Linux Kernel的資料型態。
  • Interface-Specific Types
Linux Kernel為了移植性也定義了許多_t結尾的變數在 <include/types.h>。舉例來說常見的size_t、ssize_t、pid_t、time_t等等根據特定意義的資料型態就稱為Interface-Specific Type。以process 的id pid_t為例,在x86來說是 int,但是假設我們直接將例如getpid()的回傳值用 int用 接收,哪天另外一個平台的pid是long long的話勢必我們程式必須作修改才能正確運行。因此,Interface-Specific Type可以避免移植的問題。 
  • Data Alignment
大多數的CPU都會要求變數擺在特定的記憶體上。例如Intel x86的CPU要求4 byte的integer是放置在4的倍數的記憶體位置。這個就叫做Memory Alignment。但假設不是擺在4的倍數上呢?那就必須要讀取兩次才能得到這個變數。大多數的CPU都可以自己處理這個狀況,但是有些CPU沒辦法處理這個問題,必須要產生exception靠exception handler來處理這個問題,但是這樣效率會非常的慢。有個方法是使用get_unaligned(ptr)取得資料及set_unaligned(val,ptr)寫入資料。例如,在driver/power/bq27x00_battery.c裡面
if (!single)
    ret = get_unaligned_le16(data);
else
    ret = data[0];
如果資料是1個byte就直接讀取否則用get_unaligned_le16()。le代表little-endian

順帶一提,compiler在做最佳化的時候可能會自動做Memory Aligned的動作。但也許我們定義的資料結構變數間的位址是必須連續的(的動作可能會在變數間填補一些變數讓memory可以對齊),這個時候我們可以利用__attribute__ ((packed))告訴編譯器不要亂動 。
struct{
    u16 uid;
    u64 gid;
    u16 val0;
    u32 val1;
}__attribute__ ((packed))structA;
  • Byte Order
假設有一個4 byte的int不同的CPU可能會有不同的記憶體擺法,例如低位的值先放(Little-Endian)還是大的先放(Big-Endian)?舉例來說,假設有一個整數值是0x12345678 Little-Endian的擺法就會是:
address        value
0x00000001     0x78
0x00000002     0x56
0x00000003     0x34
0x00000004     0x12
而如果是Big-Endian的話則是:
address        value
0x00000001     0x12
0x00000002     0x34
0x00000003     0x56
0x00000004     0x78
如果我們要得到Little-Endian順序的值可以使用u32 cpu_to_le32(u32),更多的轉換函式可以在找到。
  • Time
HZ定義了一秒鐘有幾次 Interrupt。他可以是100、250、300或是1000。預設的值可能會隨著 Kernel的版本或編譯Kernel 的設定而改變。因此,我們不可以假設HZ==1000(舊版的Kernel預設是1000)。
順帶一提,Tick是HZ的倒數,因此假設HZ=1000則一個tick就是1毫秒。
Jiffies是Linux Kernel用來紀錄系統自開機以來觸發了幾次Interrupt(開機時並非歸0)。Linux Kernelchewing每秒鐘會將jiffies加1HZ次。我們可以利用Linux Kernel定義的macro(timer_after、time_after_eq、time_before和time_before_eq)來判斷時間。因此,假設我們要判斷時間是否過了三秒可以:
//設定時間是現在的jiffies加上3秒的HZ次
unsigned long timeout = jiffies + (3*HZ);  
while (hwgroup->busy) {  
     ...
  /*如果迴圈超過三秒(也就是說現在的jiffies數超過三秒前的jiffies+3*HZ次)
   *則return -EBUSY
   */
  if (time_after(jiffies, timeout)) {  
    return -EBUSY;  
  }  
  ...  
}  
return SUCCESS; 
  • Page Size
不要假設Page Size是4KB。用Linux Kernel提供的PAGE_SIZE 來取得系統的page size及PAGE_SHIFT取得 要左移的bits數達到其memory page的number。

下一篇:Linux Kernel Portability-Data type & Data Structure in Linux kernel (二)