Linux內核設計與實現 學習筆記(9)可移植性

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 來正確使用時間.

關於 jiffies 和 HZ 的概念, 可以參考之前的博客: 《Linux內核設計與實現》讀書筆記(十一)- 定時器和時間管理

 

6. 頁長度

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

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

PAGE_SIZE 和 PAGE_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() 等內存屏障來確保處理器的執行順序.

關於rmb和wmb可以參考之前的博客: 《Linux內核設計與實現》讀書筆記(十)- 內核同步方法  第 11 小節

 

8. SMP, 內核搶佔, 高端內存

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

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

所以, 在編寫內核代碼時最好加上如下假設:

  • 假設代碼會在SMP系統上運行, 要正確選擇和使用鎖
  • 假設代碼會在支持內核搶佔的情況下運行, 要正確使用鎖和內核搶佔語句
  • 假設代碼會運行在使用高端內存(非永久映射內存)的系統上, 必要時使用 kmap()

 

9. 總結

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

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

  • 編碼儘量選取最大公因子 :: 假定任何事情都有可能發生, 任何潛在的約束也都存在
  • 編碼儘量選取最小公約數 :: 不要假定給定的內核特性是可用的, 僅僅需要最小的體系結構功能
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章