《Linux內(nèi)核設(shè)計(jì)與實(shí)現(xiàn)》讀書筆記(十九)- 可移植性
linux內(nèi)核的移植性非常好, 目前的內(nèi)核也支持非常多的體系結(jié)構(gòu)(有20多個(gè)).
但是剛開始時(shí), linux也只支持 intel i386 架構(gòu), 從 v1.2版開始支持 Digital Alpha, Intel x86, MIPS和SPARC(雖然支持的還不是很完善).
從 v2.0版本開始加入了對(duì) Motorala 68K和PowerPC的官方支持, v2.2版本開始新增了 ARMS, IBM S390和UltraSPARC的支持.
v2.4版本支持的體系結(jié)構(gòu)數(shù)達(dá)到了15個(gè), v2.6版本支持的體系結(jié)構(gòu)數(shù)目提高到了21個(gè).
目前的我使用的系統(tǒng)是 Fedora20, 支持的體系結(jié)構(gòu)有31個(gè)之多.(源碼樹中 arch目錄下有支持的體系結(jié)構(gòu), 每種體系結(jié)構(gòu)一個(gè)文件夾)
考慮到內(nèi)核支持如此之多的架構(gòu), 在內(nèi)核開發(fā)的時(shí)候就需要考慮編碼的可移植性.
提高可移植性最重要的就是要搞明白不同體系結(jié)構(gòu)之間究竟是什么對(duì)移植代碼的影響比較大.
主要內(nèi)容:
- 字長
- 數(shù)據(jù)類型
- 數(shù)據(jù)對(duì)齊
- 字節(jié)順序
- 時(shí)間
- 頁長度
- 處理器順序
- SMP, 內(nèi)核搶占, 高端內(nèi)存
- 總結(jié)
1. 字長
這里的字是指處理器能夠一次完成處理的數(shù)據(jù). 字長即使處理器能夠一次完成處理的數(shù)據(jù)的最大長度.
目前的處理器主要有32位和64為2種, 注意這里的32位和64位并不是指操作系統(tǒng)的版本, 而是指處理器的能力.
一般來說, 32位的處理器只能安裝32位的操作系統(tǒng), 而64位的處理器可以安裝32位的操作系統(tǒng), 也可以安裝64位的操作系統(tǒng).
對(duì)于一種體系結(jié)構(gòu)來說, 處理器通用寄存器(general-purpose registers, GPR)的大小和它的字長是相同的.
C語言定義的long類型總是對(duì)等于機(jī)器的字長, 而int型有時(shí)會(huì)比字長小.
- 32位的體系結(jié)構(gòu)中, int型和long型都是32位的
- 64位的體系結(jié)構(gòu)中, int型是32位的, long型是64位的.
內(nèi)核編碼中涉及到字長的部分時(shí), 牢記以下準(zhǔn)則:
- ANSI C標(biāo)準(zhǔn)規(guī)定, 一個(gè)char的長度一定是一個(gè)字節(jié)(8位)
- linux當(dāng)前所支持的體系結(jié)構(gòu)中, int型都是32位的
- linux當(dāng)前所支持的體系結(jié)構(gòu)中, short型都是16位的
- linux當(dāng)前所支持的體系結(jié)構(gòu)中, 指針和long型的長度不定, 在32位和64位中變化
- 不能假設(shè) sizeof(int) == sizeof(long)
- 類似的, 不能假定 指針的長度和int型相同.
此外, 操作系統(tǒng)有個(gè)簡單的助記符來描述此系統(tǒng)中數(shù)據(jù)類型的大小.
- LLP64 :: 64位的Windows, long類型和指針都是64位
- LP64 :: 64位的Linux, long類型和指針都是64位
- ILP32 :: 32位的Linux, int類型, long類型和指針都是32位
- ILP64 :: int類型, long類型和指針都是64位(非Linux)
2. 數(shù)據(jù)類型
編寫可移植性代碼時(shí), 內(nèi)核中的數(shù)據(jù)類型有以下3點(diǎn)需要注意:
2.1 不透明類型
linux內(nèi)核中定義了很多不透明類型, 它們是在C語言標(biāo)準(zhǔn)類型上的一個(gè)封裝, 比如 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 */
使用這些不透明類型時(shí), 以下原則需要注意:
- 不要假設(shè)該類型的長度(那怕通過源碼看到了它的C語言類型), 這些類型在不同體系結(jié)構(gòu)中可能長度會(huì)變, 內(nèi)核開發(fā)者也有可能修改它們
- 不要將這些不透明類型轉(zhuǎn)換為C標(biāo)準(zhǔn)類型來使用
- 編程時(shí)保證不透明類型實(shí)際存儲(chǔ)空間或者格式發(fā)生變化時(shí)代碼不受影響
2.2 長度確定的類型
除了不透明類型, linux內(nèi)核中還定義了一系列長度明確的數(shù)據(jù)類型, 參見 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;
上面這些類型只能在內(nèi)核空間使用, 用戶空間無法使用. 用戶空間有對(duì)應(yīng)的變量類型, 名稱前多了2個(gè)下劃線:
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類型單獨(dú)拿出來說明, 是因?yàn)閏har類型在不同的體系結(jié)構(gòu)中, 有時(shí)默認(rèn)是帶符號(hào)的, 有時(shí)是不帶符號(hào)的.
比如, 最簡單的例子:
/* * 某些體系結(jié)構(gòu)中, char類型默認(rèn)是帶符號(hào)的, 那么下面 i 的值就為 -1 * 某些體系結(jié)構(gòu)中, char類型默認(rèn)是不帶符號(hào)的, 那么下面 i 的值就為 255, 與預(yù)期可能有差別!!! */ char i = -1;
避免上述問題的方法就是, 給char類型賦值時(shí), 明確是否帶符號(hào), 如下:
signed char i = -1; /* 明確 signed, i 的值在哪種體系結(jié)構(gòu)中都是 -1 */ unsigned char i = 255; /* 明確 unsigned, i 的值在哪種體系結(jié)構(gòu)中都是 255 */
3. 數(shù)據(jù)對(duì)齊
數(shù)據(jù)對(duì)齊也是增強(qiáng)可移植性的一個(gè)重要方面(有的體系結(jié)構(gòu)對(duì)數(shù)據(jù)對(duì)齊要求非常嚴(yán)格, 載入未對(duì)齊的數(shù)據(jù)可導(dǎo)致性能下降, 甚至錯(cuò)誤).
數(shù)據(jù)對(duì)齊的意思就是: 數(shù)據(jù)的內(nèi)存地址可以被 4 整除
1. 通過指針轉(zhuǎn)換類型時(shí), 不要轉(zhuǎn)換長度不一樣的類型, 比如下面的代碼有可能出錯(cuò)
/* * 下面的代碼將一個(gè)變量從 char 類型轉(zhuǎn)換為 unsigned long 類型, * char 類型只占 1個(gè)字節(jié), 它的地址不一定能被4整除, 轉(zhuǎn)換為 4個(gè)字節(jié)或者8個(gè)字節(jié)的 usigned long之后, * 導(dǎo)致 unsigned long 出現(xiàn)數(shù)據(jù)不對(duì)齊的現(xiàn)象. */ char wolf[] = "Like a wolf"; char *p = &wolf[1]; unsigned long p1 = *(unsigned long*) p;
2. 對(duì)于數(shù)組, 安裝基本數(shù)據(jù)類型進(jìn)行對(duì)齊就行.(數(shù)組元素的存放在內(nèi)存中是連續(xù)的, 第一個(gè)對(duì)齊了, 后面的都自動(dòng)對(duì)齊了)
3. 對(duì)于聯(lián)合體, 長度最大的數(shù)據(jù)對(duì)齊就可以了
4. 對(duì)于結(jié)構(gòu)體, 保證結(jié)構(gòu)體中每個(gè)元素能夠正確對(duì)齊即可
如果結(jié)構(gòu)體中的元素沒有對(duì)齊, 編譯器會(huì)自動(dòng)填充結(jié)構(gòu)體, 保證它是對(duì)齊的. 比如下面的代碼, 預(yù)計(jì)應(yīng)該輸出12, 實(shí)際卻輸出了24
我的代碼運(yùn)行環(huán)境: 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個(gè)字節(jié) */ unsigned long cat; /* 8個(gè)字節(jié) */ unsigned short pig; /* 2個(gè)字節(jié) */ char fox; /* 1個(gè)字節(jié) */ }; int main(int argc, char *argv[]) { /* 在我的64bit 系統(tǒng)中是按8位對(duì)齊, 下面的代碼輸出 24 */ printf ("sizeof(animal_struct)=%d\n", sizeof(struct animal_struct)); return 0; }
測試方法:
gcc -o test struct_align.c ./test # 輸出24
結(jié)構(gòu)體應(yīng)該被填充成如下形式:
struct animal_struct { char dog; /* 1個(gè)字節(jié) */ /* 此處填充了7個(gè)字節(jié) */ unsigned long cat; /* 8個(gè)字節(jié) */ unsigned short pig; /* 2個(gè)字節(jié) */ char fox; /* 1個(gè)字節(jié) */ /* 此處填充了5個(gè)字節(jié) */ };
通過調(diào)整結(jié)構(gòu)體中元素順序, 可以減少填充的字節(jié)數(shù), 比如上述結(jié)構(gòu)體如果定義成如下順序:
struct animal_struct { unsigned long cat; /* 8個(gè)字節(jié) */ unsigned short pig; /* 2個(gè)字節(jié) */ char dog; /* 1個(gè)字節(jié) */ char fox; /* 1個(gè)字節(jié) */ };
那么為了保證8位對(duì)齊, 只需在后面補(bǔ)充 4位即可:
struct animal_struct { unsigned long cat; /* 8個(gè)字節(jié) */ unsigned short pig; /* 2個(gè)字節(jié) */ char dog; /* 1個(gè)字節(jié) */ char fox; /* 1個(gè)字節(jié) */ /* 此處填充了4個(gè)字節(jié) */ };
調(diào)整后的代碼會(huì)輸出 16, 不是之前的24
#include <stdio.h> struct animal_struct { unsigned long cat; /* 8個(gè)字節(jié) */ unsigned short pig; /* 2個(gè)字節(jié) */ char dog; /* 1個(gè)字節(jié) */ char fox; /* 1個(gè)字節(jié) */ }; int main(int argc, char *argv[]) { /* 在我的64bit 系統(tǒng)中是按8位對(duì)齊, 下面的代碼輸出 16 */ printf ("sizeof(animal_struct)=%d\n", sizeof(struct animal_struct)); return 0; }
測試方法:
gcc -o test struct_align.c ./test # 輸出16
注意: 雖然調(diào)整結(jié)構(gòu)體中元素的順序可以減少填充的字節(jié), 從而降低內(nèi)存的消耗.
但是對(duì)于內(nèi)核中已有的那些結(jié)構(gòu), 千萬不能隨便調(diào)整其元素順序, 因?yàn)閮?nèi)核中很多現(xiàn)存的方法都是通過元素在結(jié)構(gòu)體中位置偏移來獲取元素的.
4. 字節(jié)順序
字節(jié)順序其實(shí)只有2種:
- 低位優(yōu)先 :: little-endian 數(shù)據(jù)由低位地址->高位地址存放
- 高位優(yōu)先 :: big-endian 數(shù)據(jù)由高位地址->低位地址存放
比如占有四個(gè)字節(jié)的整數(shù)的二進(jìn)制表示如下:
00000001 00000002 00000003 00000004
內(nèi)存地址方向: 高位 <--------------------> 低位
little-endian 表示如下:
00000001 00000002 00000003 00000004
big-endian 表示如下:
00000004 00000003 00000002 00000001
判斷一個(gè)體系結(jié)構(gòu)是 big-endian 還是 little-endian 非常簡單.
int x = 1; /* 二進(jìn)制 00000000 00000000 00000000 00000001 */ /* * 內(nèi)存地址方向: 高位 <--------------------> 低位 * little-endian 表示: 00000000 00000000 00000000 00000001 * big-endian 表示: 00000001 00000000 00000000 00000000 */ if (*(char *) &x == 1) /* 這句話把int型轉(zhuǎn)為char型, 相當(dāng)于只取了int型的最低8bit */ /* little-endian */ else /* big-endian */
5. 時(shí)間
內(nèi)核中使用到時(shí)間相關(guān)概念時(shí), 為了提高可移植性, 不要使用時(shí)間中斷的發(fā)生頻率(也就是每秒產(chǎn)生的jiffies), 而應(yīng)該使用 HZ 來正確使用時(shí)間.
關(guān)于 jiffies 和 HZ 的概念, 可以參考之前的博客: 《Linux內(nèi)核設(shè)計(jì)與實(shí)現(xiàn)》讀書筆記(十一)- 定時(shí)器和時(shí)間管理
6. 頁長度
當(dāng)處理用頁管理的內(nèi)存時(shí), 不要既定頁的長度為 4KB, 在不同的體系結(jié)構(gòu)中長度會(huì)不一樣.
而應(yīng)該使用 PAGE_SIZE 以字節(jié)數(shù)來表示頁長度, 使用 PAGE_SHIFT 表示從最右端屏蔽了多少位能夠得到該地址對(duì)應(yīng)的頁的頁號(hào).
PAGE_SIZE 和 PAGE_SHIFT 都是宏, 定義在 include/asm-generic/page.h 中
下表是一些體系結(jié)構(gòu)中頁長度:
|
體系結(jié)構(gòu) |
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. 處理器順序
還有最后一個(gè)和可移植性相關(guān)的注意點(diǎn)就是處理器對(duì)代碼的執(zhí)行順序, 在有些體系結(jié)構(gòu)中, 處理器并不是嚴(yán)格按照代碼編寫的順序執(zhí)行的,
可能為了優(yōu)化性能或者其他原因, 處理器執(zhí)行指令的順序與編寫的代碼的順序稍有出入.
如果我們的某段代碼需要嚴(yán)格的執(zhí)行順序, 需要在代碼中使用 rmb() wmb() 等內(nèi)存屏障來確保處理器的執(zhí)行順序.
關(guān)于rmb和wmb可以參考之前的博客: 《Linux內(nèi)核設(shè)計(jì)與實(shí)現(xiàn)》讀書筆記(十)- 內(nèi)核同步方法 第 11 小節(jié)
8. SMP, 內(nèi)核搶占, 高端內(nèi)存
SMP, 內(nèi)核搶占和高端內(nèi)存本身雖然和可移植性沒有太大的關(guān)系, 但它們都是內(nèi)核中重要的配置選項(xiàng),
如果編碼時(shí)能夠考慮到這些的話, 那么即使內(nèi)核修改SMP等這些配置選項(xiàng), 我們的代碼仍然可以安全可靠的運(yùn)行.
所以, 在編寫內(nèi)核代碼時(shí)最好加上如下假設(shè):
- 假設(shè)代碼會(huì)在SMP系統(tǒng)上運(yùn)行, 要正確選擇和使用鎖
- 假設(shè)代碼會(huì)在支持內(nèi)核搶占的情況下運(yùn)行, 要正確使用鎖和內(nèi)核搶占語句
- 假設(shè)代碼會(huì)運(yùn)行在使用高端內(nèi)存(非永久映射內(nèi)存)的系統(tǒng)上, 必要時(shí)使用 kmap()
9. 總結(jié)
編寫簡潔, 可移植性的代碼還需要通過實(shí)踐來積累經(jīng)驗(yàn), 上面的準(zhǔn)則可以作為代碼是否滿足可移植性的一些檢測條件.
書中還提到的2點(diǎn)注意事項(xiàng), 我覺得不僅是編寫內(nèi)核代碼, 編寫任何代碼時(shí), 都應(yīng)該注意:
- 編碼盡量選取最大公因子 :: 假定任何事情都有可能發(fā)生, 任何潛在的約束也都存在
- 編碼盡量選取最小公約數(shù) :: 不要假定給定的內(nèi)核特性是可用的, 僅僅需要最小的體系結(jié)構(gòu)功能
雖然編寫可移植性代碼需要遵守這么多的原則, 但是不能畏懼, 在學(xué)習(xí)內(nèi)核開發(fā)的過程中, 只有不斷的嘗試, 不斷的犯錯(cuò), 才能確實(shí)的掌握內(nèi)核.

浙公網(wǎng)安備 33010602011771號(hào)