1. 程式人生 > >《Linux核心設計與實現》讀書筆記(十九)- 可移植性

《Linux核心設計與實現》讀書筆記(十九)- 可移植性

linux核心的移植性非常好, 目前的核心也支援非常多的體系結構(有20多個).

但是剛開始時, linux也只支援 intel i386 架構, 從 v1.2版開始支援 Digital Alpha, Intel x86, MIPS和SPARC(雖然支援的還不是很完善).

從 v2.0版本開始加入了對 Motorala 68K和PowerPC的官方支援, v2.2版本開始新增了 ARMS, IBM S390和UltraSPARC的支援.

v2.4版本支援的體系結構數達到了15個, v2.6版本支援的體系結構數目提高到了21個.

目前的我使用的系統是 Fedora20, 支援的體系結構有31個之多.(原始碼樹中 arch目錄下有支援的體系結構, 每種體系結構一個資料夾)

考慮到核心支援如此之多的架構, 在核心開發的時候就需要考慮編碼的可移植性.

提高可移植性最重要的就是要搞明白不同體系結構之間究竟是什麼對移植程式碼的影響比較大.

主要內容:

  • 字長
  • 資料型別
  • 資料對齊
  • 位元組順序
  • 時間
  • 頁長度
  • 處理器順序
  • SMP, 核心搶佔, 高階記憶體
  • 總結

1. 字長

這裡的字是指處理器能夠一次完成處理的資料. 字長即使處理器能夠一次完成處理的資料的最大長度.

目前的處理器主要有32位和64為2種, 注意這裡的32位和64位並不是指作業系統的版本, 而是指處理器的能力.

一般來說, 32位的處理器只能安裝32位的作業系統, 而64位的處理器可以安裝32位的作業系統, 也可以安裝64位的作業系統.

對於一種體系結構來說, 處理器通用暫存器(general-purpose registers, GPR)的大小和它的字長是相同的.

C語言定義的long型別總是對等於機器的字長, 而int型有時會比字長小.

  • 32位的體系結構中, int型和long型都是32位的
  • 64位的體系結構中, int型是32位的, long型是64位的.

核心編碼中涉及到字長的部分時, 牢記以下準則:

  1. ANSI C標準規定, 一個char的長度一定是一個位元組(8位)
  2. linux當前所支援的體系結構中, int型都是32位的
  3. linux當前所支援的體系結構中, short型都是16位的
  4. linux當前所支援的體系結構中, 指標和long型的長度不定, 在32位和64位中變化
  5. 不能假設 sizeof(int) == sizeof(long)
  6. 類似的, 不能假定 指標的長度和int型相同.

此外, 作業系統有個簡單的助記符來描述此係統中資料型別的大小.

  • LLP64 :: 64位的Windows, long型別和指標都是64位
  • LP64 :: 64位的Linux, long型別和指標都是64位
  • ILP32 :: 32位的Linux, int型別, long型別和指標都是32位
  • ILP64 :: int型別, long型別和指標都是64位(非Linux)

2. 資料型別

編寫可移植性程式碼時, 核心中的資料型別有以下3點需要注意:

2.1 不透明型別

linux核心中定義了很多不透明型別, 它們是在C語言標準型別上的一個封裝, 比如 pid_t, uid_t, gid_t 等等.

例如, pid_t的定義可以在原始碼中找到:

typedef __kernel_pid_t        pid_t;  /* include/linux/types.h */

typedef int        __kernel_pid_t;    /* arch/asm/include/asm/posix_types.h */

使用這些不透明型別時, 以下原則需要注意:

  1. 不要假設該型別的長度(那怕通過原始碼看到了它的C語言型別), 這些型別在不同體系結構中可能長度會變, 核心開發者也有可能修改它們
  2. 不要將這些不透明型別轉換為C標準型別來使用
  3. 程式設計時保證不透明型別實際儲存空間或者格式發生變化時程式碼不受影響

2.2 長度確定的型別

除了不透明型別, linux核心中還定義了一系列長度明確的資料型別, 參見 include/asm-generic/int-l64.h 或者 include/asm-generic/int-ll64.h

typedef signed char s8;
typedef unsigned char u8;

typedef signed short s16;
typedef unsigned short u16;

typedef signed int s32;
typedef unsigned int u32;

typedef signed long s64;
typedef unsigned long u64;

上面這些型別只能在核心空間使用, 使用者空間無法使用. 使用者空間有對應的變數型別, 名稱前多了2個下劃線:

typedef __signed__ char __s8;
typedef unsigned char __u8;

typedef __signed__ short __s16;
typedef unsigned short __u16;

typedef __signed__ int __s32;
typedef unsigned int __u32;

typedef __signed__ long __s64;
typedef unsigned long __u64;

2.3 char型別

之所以把char型別單獨拿出來說明, 是因為char型別在不同的體系結構中, 有時預設是帶符號的, 有時是不帶符號的.

比如, 最簡單的例子:

/*
 * 某些體系結構中, char型別預設是帶符號的, 那麼下面 i 的值就為 -1
 * 某些體系結構中, char型別預設是不帶符號的, 那麼下面 i 的值就為 255, 與預期可能有差別!!!
 */
char i = -1;

避免上述問題的方法就是, 給char型別賦值時, 明確是否帶符號, 如下:

signed char i = -1;  /* 明確 signed, i 的值在哪種體系結構中都是 -1 */
unsigned char i = 255;  /* 明確 unsigned, i 的值在哪種體系結構中都是 255 */

3. 資料對齊

資料對齊也是增強可移植性的一個重要方面(有的體系結構對資料對齊要求非常嚴格, 載入未對齊的資料可導致效能下降, 甚至錯誤).

資料對齊的意思就是: 資料的記憶體地址可以被 4 整除

1. 通過指標轉換型別時, 不要轉換長度不一樣的型別, 比如下面的程式碼有可能出錯

/*
 * 下面的程式碼將一個變數從 char 型別轉換為 unsigned long 型別, 
 * char 型別只佔 1個位元組, 它的地址不一定能被4整除, 轉換為 4個位元組或者8個位元組的 usigned long之後,
 * 導致 unsigned long 出現數據不對齊的現象.
 */
char wolf[] = "Like a wolf";
char *p = &wolf[1];
unsigned long p1 = *(unsigned long*) p;

2. 對於陣列, 安裝基本資料型別進行對齊就行.(陣列元素的存放在記憶體中是連續的, 第一個對齊了, 後面的都自動對齊了)

3. 對於聯合體, 長度最大的資料對齊就可以了

4. 對於結構體, 保證結構體中每個元素能夠正確對齊即可

如果結構體中的元素沒有對齊, 編譯器會自動填充結構體, 保證它是對齊的. 比如下面的程式碼, 預計應該輸出12, 實際卻輸出了24

我的程式碼執行環境: Fedora20 x86_64

/******************************************************************************
 * @file    : struct_align.c
 * @author  : wangyubin
 * @date    : 2014-01-09
 * 
 * @brief   : 
 * history  : init
 ******************************************************************************/

#include <stdio.h>

struct animal_struct
{
    char dog;                   /* 1個位元組 */
    unsigned long cat;          /* 8個位元組 */
    unsigned short pig;         /* 2個位元組 */
    char fox;                   /* 1個位元組 */
};

int main(int argc, char *argv[])
{
    /* 在我的64bit 系統中是按8位對齊, 下面的程式碼輸出 24 */
    printf ("sizeof(animal_struct)=%d\n", sizeof(struct animal_struct));
    return 0;
}

測試方法:

gcc -o test struct_align.c
./test   # 輸出24

結構體應該被填充成如下形式:

struct animal_struct
{
    char dog;                   /* 1個位元組 */
    /* 此處填充了7個位元組 */
    unsigned long cat;          /* 8個位元組 */
    unsigned short pig;         /* 2個位元組 */
    char fox;                   /* 1個位元組 */
    /* 此處填充了5個位元組 */   
};

通過調整結構體中元素順序, 可以減少填充的位元組數, 比如上述結構體如果定義成如下順序:

struct animal_struct
{
    unsigned long cat;          /* 8個位元組 */
    unsigned short pig;         /* 2個位元組 */
    char dog;                   /* 1個位元組 */
    char fox;                   /* 1個位元組 */
};

那麼為了保證8位對齊, 只需在後面補充 4位即可:

struct animal_struct
{
    unsigned long cat;          /* 8個位元組 */
    unsigned short pig;         /* 2個位元組 */
    char dog;                   /* 1個位元組 */
    char fox;                   /* 1個位元組 */
    /* 此處填充了4個位元組 */   
};

調整後的程式碼會輸出 16, 不是之前的24

#include <stdio.h>

struct animal_struct
{
    unsigned long cat;          /* 8個位元組 */
    unsigned short pig;         /* 2個位元組 */
    char dog;                   /* 1個位元組 */
    char fox;                   /* 1個位元組 */
};

int main(int argc, char *argv[])
{
    /* 在我的64bit 系統中是按8位對齊, 下面的程式碼輸出 16 */
    printf ("sizeof(animal_struct)=%d\n", sizeof(struct animal_struct));
    return 0;
}

測試方法:

gcc -o test struct_align.c
./test  # 輸出16

注意: 雖然調整結構體中元素的順序可以減少填充的位元組, 從而降低記憶體的消耗.

但是對於核心中已有的那些結構, 千萬不能隨便調整其元素順序, 因為核心中很多現存的方法都是通過元素在結構體中位置偏移來獲取元素的.

4. 位元組順序

位元組順序其實只有2種:

  • 低位優先 :: little-endian 資料由低位地址->高位地址存放
  • 高位優先 :: big-endian 資料由高位地址->低位地址存放

比如佔有四個位元組的整數的二進位制表示如下:

00000001 00000002 00000003 00000004

記憶體地址方向:   高位  <--------------------> 低位

little-endian 表示如下: 

00000001 00000002 00000003 00000004

big-endian 表示如下:

00000004 00000003 00000002 00000001

判斷一個體繫結構是 big-endian 還是 little-endian 非常簡單.

int x = 1;  /* 二進位制 00000000 00000000 00000000 00000001 */

/* 
 * 記憶體地址方向:   高位  <--------------------> 低位
 * little-endian 表示: 00000000 00000000 00000000 00000001
 * big-endian 表示:    00000001 00000000 00000000 00000000
 */
if (*(char *) &x == 1)   /* 這句話把int型轉為char型, 相當於只取了int型的最低8bit */
    /* little-endian */
else
    /* big-endian */

5. 時間

核心中使用到時間相關概念時, 為了提高可移植性, 不要使用時間中斷的發生頻率(也就是每秒產生的jiffies), 而應該使用 HZ 來正確使用時間.

6. 頁長度

當處理用頁管理的記憶體時, 不要既定頁的長度為 4KB, 在不同的體系結構中長度會不一樣.

而應該使用 PAGE_SIZE 以位元組數來表示頁長度, 使用 PAGE_SHIFT 表示從最右端遮蔽了多少位能夠得到該地址對應的頁的頁號.

PAGE_SIZEPAGE_SHIFT 都是巨集, 定義在 include/asm-generic/page.h

下表是一些體系結構中頁長度:

體系結構

PAGE_SHIFT

PAGE_SIZE

alpha 13 8KB
arm 12, 14, 15 4KB, 16KB, 32KB
avr 12 4KB
cris 13 8KB
blackfin 12 16KB
h8300 14 4KB
12 4KB, 8KB, 16KB, 32KB
m32r 12, 13, 14, 16 4KB
m68k 12 4KB, 8KB
m68knommu 12, 13 4KB
mips 12 4KB
min10300 12 4KB
parisc 12 4KB
powerpc 12 4KB
s390 12 4KB
sh 12 4KB
sparc 12, 13 4KB, 8KB
um 12 4KB
x86 12 4KB
xtensa 12 4KB

7. 處理器順序

還有最後一個和可移植性相關的注意點就是處理器對程式碼的執行順序, 在有些體系結構中, 處理器並不是嚴格按照程式碼編寫的順序執行的,

可能為了優化效能或者其他原因, 處理器執行指令的順序與編寫的程式碼的順序稍有出入.

如果我們的某段程式碼需要嚴格的執行順序, 需要在程式碼中使用 rmb() wmb() 等記憶體屏障來確保處理器的執行順序.

8. SMP, 核心搶佔, 高階記憶體

SMP, 核心搶佔和高階記憶體本身雖然和可移植性沒有太大的關係, 但它們都是核心中重要的配置選項,

如果編碼時能夠考慮到這些的話, 那麼即使核心修改SMP等這些配置選項, 我們的程式碼仍然可以安全可靠的執行.

所以, 在編寫核心程式碼時最好加上如下假設:

  • 假設程式碼會在SMP系統上執行, 要正確選擇和使用鎖
  • 假設程式碼會在支援核心搶佔的情況下執行, 要正確使用鎖和核心搶佔語句
  • 假設程式碼會執行在使用高階記憶體(非永久對映記憶體)的系統上, 必要時使用 kmap()

9. 總結

編寫簡潔, 可移植性的程式碼還需要通過實踐來積累經驗, 上面的準則可以作為程式碼是否滿足可移植性的一些檢測條件.

書中還提到的2點注意事項, 我覺得不僅是編寫核心程式碼, 編寫任何程式碼時, 都應該注意:

  • 編碼儘量選取最大公因子 :: 假定任何事情都有可能發生, 任何潛在的約束也都存在
  • 編碼儘量選取最小公約數 :: 不要假定給定的核心特性是可用的, 僅僅需要最小的體系結構功能

雖然編寫可移植性程式碼需要遵守這麼多的原則, 但是不能畏懼, 在學習核心開發的過程中, 只有不斷的嘗試, 不斷的犯錯, 才能確實的掌握核心.