可移植性是指代码从一种体系结构移植到另外一种体系结构的方便程度。Linux是一个可移植性非常好的操作系统,它广泛支持许多不同体系结构的计算机。
1.可移植操作系统
可移植性往往是与性能相矛盾的,Linux在这个方面走的是中间路线。差不多所有的接口和核心代码独立于硬件体系结构的C语言代码。但是对性能要求很严格的部分,内核的特性会根据不同的硬件体系进行调整。需要快速执行的和底层的与硬件相关的用汇编语言,折中实现方式使Linux在保持可移植性的同时兼顾对性能的优化。
调度程序就是一个好例子,调度程序的主体程序存放在kernel/sched.c文件中,用C语言编写,与体系结构无关。但是,调度程序需要进行的一些工作,比如切换处理器上下文和切换地址空间等,与硬件相关,用汇编写成。所以内核用C语言编写了函数context_switch()用于实现进程切换,而在它的内部,则会调用switch_to()和switch_mm()分别完成处理器上下文和地址空间切换。
对于Linux支持的每种体系结构,它们的switch_to()和switch_mm()实现都各不相同,所以当Linux需要移植到新的体系结构上时,只需要重新编写和提供这样的函数就可以了。
与体系结构相关的代码都存放在arch/architecture/目录中,2.6系列内核支持的体系结构包括alpha, arm, avr32, m68k, mips, … x86等。
在Linux升级过程中,为了追求完美,内核开发者是绝不会介意重写大段代码的。
2.字长和数据类型
机器一次完成处理的数据称为字。处理器通用寄存器的大小和它的字长是相同的,一般来说,对于一个体系结构,它的各个部件宽度(比如内存总线)最少要和它字长一样大。Linux中,long类型是等于字长的。
Linux在不同体系架构中,没有什么标准来规范,但是应牢记下述准则:
1)ANSIC标准规定,一个char的长度一定是1字节;
2)尽管没有规定Int类型长度是32位,但在Linux当前所有支持的体系结构中,都是32位的。
3)short类型也类似,在当前所有支持体系结构中,没有明文规定,但都是16位;
4)绝不应该假定指针和long的长度,在Linux当前支持的体系结构中,它们可以在32位和64位中变化。
5)由于体系结构long的长度不同,绝不应该假设sizeof(int) = sizeof(long);
6)也不要假设指针和int长度相等。
2.1 不透明类型,原则是:
1)不要假设该类型的长度;在某些系统中是32位,在其他系统中可能是64位;
2)不要将该类型转化回其对应的C标准类型使用;
3)编程时,保证该类型实际存储空间和格式发送变化时代码不受影响;
2.2 制定数据类型
内核中还有一些数据虽然无须用不透明的类型表示,但它们定义成了制定的数据类型,比如中断控制时用到的flag参数,应该存放在unsigned longl类型中,访问该数据是存放其他类型(比如unsigned int等)是常见错误,在32位机没有问题,64位机可能就出错。
2.3长度明确的类型
有些时候程序中需要使用明确的数据类型,比如声卡可能用的32位寄存器,一个网络包有一个16位字段,内核在
中定义了这些长度明确的类型
这些长度明确的类型大部分通过typedef对标准的C类型进行映射得到,在一个64位机上,
此处)折叠或打开
1. typedef signed char s8;
2. typedef unsigned char u8;
3. typedef signed short s16;
4. typedef unsigned short u16;
5. typedef signed int s32;
6. typedef unsigned int u32;
7. typedef signed long s64;
8. typedef unsigned long u64;
在一个32位机上,可能如此实现:
此处)折叠或打开
1. typedef signed char s8;
2. typedef unsigned char u8;
3. typedef signed short s16;
4. typedef unsigned short u16;
5. typedef signed int s32;
6. typedef unsigned int u32;
7. typedef signed long long s64;
8. typedef unsigned long long u64;
这些类型只能在内核使用,不可以在用户空间出现,这样做是为了保护命名空间不被污染。
实际上这些类型也做了用户可见变量类型,每个类型都增加了两个下划前缀。
2.4 char 型的符号问题
C标准表示char类型可以带符号也可以不带符号,由具体的编译器,处理器或由它们共同决定。
大部分体系结构上,char默认是带符号的,范围是-128~127. 但也有例外,比如ARM体系结构上,char可以不带符号,范围是0~255.
在默认不带符号的情况下,
char i = -1; //实际把255赋给I, 而不是-1
如果要确切把-1赋值给I,那么代码应修改成
signed char I = -1;
若赋值255
unsigned char = 255;
如果自己的代码中使用了char类型,要么保证在带符号和不带符号情况下,代码都没问题,要么明确选用哪一个,就直接声明它。
3.数据对齐
一个大小为2n 字节的数据类型,它地址的最低有效位的后n位后应该为0。
3.1实际上,内核开发者在对齐上不用花费太多心思—只有搞gcc开发的才为此犯愁。可是,当程序员使用指针太多,对数据的访问方式超出编译器预期时,就会引发问题。
一个数据类型长度较小,它本来是对齐的,如果用一个指针进行类型转换,并且转换后的类型长度较大,那么指针访问数据时就会引发对齐问题。
此处)折叠或打开
- char wolf[] = “Like a wolf”;
- char *p = &wolf[1];
- unsigned long l = *(unsigned long *)p;
代码视图从p指向的不能被4或8整除的地址上载入32位或64位的unsigned long型数据。
3.2非标准类型的对齐
1)对于数组,只要按照基本数据类型进行对齐就可以了,随后的所有元素自然能够对齐。
2)对于联合体,只要它包含了长度最大的数据类型对齐就可以了;
3)对于结构体,只要结构体中每个元素能够正确地对齐就可以了;
3.3 为了保证结构体每一个成员都能够自然对齐,结构体要被填补,这点确保了当处理器访问结构中一个给定元素时,元素本身是对齐的。
例如,一个32位机上,如下结构体:
此处)折叠或打开
1. struct animal_struct {
2. char dog; /* 1 byte */
3. unsigned long cat; /* 4 bytes */
4. unsigned short pig; /* 2 bytes */
5. char fox; /* 1 byte */
6. };
编译器会在内存中创建一个类型下面给出的结构体:
此处)折叠或打开
1. struct animal_struct {
2. char dog; /* 1 byte */
3. u8 __pad0[3]; /* 3 bytes */
4. unsigned long cat; /* 4 bytes */
5. unsigned short pig; /* 2 bytes */
6. char fox; /* 1 byte */
7. u8 __pad1; /* 1 byte */
8. };
4.字节顺序
是指一个字中各个字节的顺序。
大段模式:字的高有效位,排在内存低字节位置上;
小端模式:字的低有效位,排子内存的低字节位置上;
比如有一个字数据,0x12345678,按内存由低到高排序0x0~0x03
大端模式:0x12 0x34 0x56 0x78
小端模式:0x78 0x56 0x34 0x12
一个判断机器使用大端或小端方法:
此处)折叠或打开
1. int x = 1;
2. if (*(char *) &x == 1) {
3. /*小端模式*/
4. } else {
5. /*大端模式*/
6. }
这种方法,在内核,用户可见都能用。
对于Linux支持的每一种体系结构,相应的内核都会根据机器使用的字节顺序在它的 中定义__BIG_ENDIAN或__LITTERLE_ENDIAN中的一个。
这个头文件还从Include/linux/byteorder/中包含了一组宏命令用于完成字节序的相互转换,最常用的有:
此处)折叠或打开
1. u32 __cpu_to_be32(u32); /* convert cpu’s byte order to big-endian */
2. u32 __cpu_to_le32(u32); /* convert cpu’s byte order to little-endian */
3. u32 __be32_to_cpu(u32); /* convert big-endian to cpu’s byte order */
4. u32 __le32_to_cpus(u32); /* convert little-endian to cpu’s byte order */
5.时间
时间测量,随着体系结构甚至内核版本的不同而不同,所以绝对不要假定时钟中断发生的频率,也就是每秒产生的jiffies数目。应该使用HZ来正确计量时间,即乘以或除以HZ,比如:
此处)折叠或打开
- HZ /*1秒*/
- (2*HZ) // 2s
- (HZ/100) //10ms
- (2*HZ/100) //20ms
6.页长度
通用的不要假设页长度,尽管大多时候是4KB,当处理用页管理的内存时,通过PAGE_SIZE以字节数来表示页长度。
7.处理器排序
有些处理器严格限制指令排序,而有些体系结构对排序要求则很弱,可以自行排序。
在代码中,如果对排序要求最弱的体系结构上,要保证指令执行顺序,那么必须使用诸如rmb()和wmb()等恰当的内存屏障来确保处理器以正确顺序提交装载和存储指令。
8. SMP, 内核抢占,高端内存
虽然SMP, 内核抢占,高端内存似乎都跟硬件体系结构无关,但是只有在编程时就针对SMP/内核抢占/高端内存进行考虑,代码才会无论内核怎样配置,都能身处安全之中。
1)假设你的代码会在SMP系统上运行,要正确地选择和使用锁;
2)假设你的代码会在内核抢占的情况下运行,要正确地选择和使用锁和内核抢占语句。
3)假设你的代码会运行在使用高端内存(非永久映射内存)的系统上,必要时使用kmap().
要想写出可移植性好、简洁、合适的内核代码,要注意一下两点:
编码尽量选取最大公因子:假定任何事情都可能发生,任何潜在的约束也都存在。
编码尽量选取最大公约数:不要假定给定的内核特性是可用的,仅仅需要最小的体系结构功能。