本文共 12381 字,大约阅读时间需要 41 分钟。
Linux是一个移植性非常好的操作系统,它广泛支持了许多不同体系结构的计算机。可移植性是指代码从一种体系结构移植到另外一种不同的体系结构上的方便程度。我们都知道Linux是可移植的,因为它已经能够在各种不同的体系结构上运行了。但这种可移植性不是凭空得来的——它需要在做设计时就为此付出诸多努力。现在,这种努力已经开始得到回报了,移植Linux到新的系统上就很容易(相对来说)完成。本章中我们将讨论的是如何编写可移植的代码——在编写内核心代码和驱动程序时,你必须时刻牢记这个问题。
有 些操作系统在设计时把可移植性作为头等大事之一。尽可能少的涉及与机器相关的代码。汇编代码用的少之又少,为了支持各种不同类别的体系结构,界面和功能在 定义时都尽最大可能地具有普适性和抽象性。这么做最显著的回报就是需要支持新的体系结构时,所需完成的工作要相对容易许多。一些移植性非常高而本身又比较 简单的操作系统在支持新的体系结构时,可能只需要为此体系结构编写几百行专门的代码就行了。问题在于,体系结构相关的一些特性往往无法被支持,也不能对特 定的机器进行手动优化。选择这种设计,就是利用代码的性能优化能力换取代码的可移植性。Minix,NetBSD和许多研究用的系统就是这种高度可移植操作系统的实例。
与 之相反,还有一种操作系统完全不顾及可移植性,它们尽最大的可能追求代码的性能表现,尽可能多的使用汇编代码,压根就是为在一种硬件体系结构使用。内核的 特性都是围绕硬件提供的特性设计的。因此,将其移植到其它体系结构就等于从头再编写一个新的操作内核。选择这种设计,就是用代码的可移植性换取代码的性能 优化能力。这样的系统往往比移植性好的系统难于维护。DOS和Windows 9x就是这样的典型。目前这些系统的性能也未必比教高移植性的系统更优,不过它们可以手动调整的代码相对更多。
Linux在可移植这个方面走的是中间路线。差不多所有的接口和核心代码都是独立于硬件体系结构的C语言代码。但是,在对性能要求很严格的部分,内核的特性会根据不同的硬件体系进行调整。举例来说,需要快速执行和底层的代码都是与硬件相关并且是用汇编语言写成的。这种实现方式使Linux在保持可移植性的同时兼顾对性能的优化。当可移植性妨碍性能发挥的时候,往往性能会被优先考虑。除此之外,代码就一定要保证可移植性。
一般来说,暴露在外的内核接口往往是与硬件体系结构无关的。如果函数的任何部分需要针对特殊的体系结构(无论是出于优化的目的还是作为一种必需的选择)提供支持的时候,这些部分都会被安置在独立的函数中,等待调用。
调度程序就是一个好例子。调度程序的主体程序存放在kernel/sched.c文件中,用C语言编写的,与体系结构无关。可是,调度程序需要进行的一些工作,比如说切换处理器上下文和切换地址空间等,却不得不依靠相应的体系结构完成。于是,内核用C语言编写了函数context_switch()用于实现进程切换,而在它的内部,会调用switch_to()和switch_mm()分别完成处理器上下文和地址空间的切换。
而对于Linux支持的每种体系结构,它们的switch_to()和switch_mm()实现各不相同。所以,当Linux需要移植到新的体系结构上的时候,只需要重新编写和提供这样的函数就可以了。
与体系结构相关的代码都存放在arch/<architecture>/和include/asm-<architecture>/目录中,<architecture>是Linux支持的体系结构的简称。比如说,Intel X86体系结构对应的简称是i386。与这种体系结构相关的代码都存放在arch/i386/以及include/asm-i386/目录下。2.6系列内核支持的体系结构包括alpah,arm,cris,h8300,i386,ia64,m68k,m68knommu,mips,mips64,parisc,ppc,ppc64,s390,sh,sparc,sparc64,um,v850和x86-64。表16.1是一份更详尽的清单。
当Linus最初把Linux带到这个无法预测的大千世界的时候,它只能在i386上运行。尽管这个操作系统通用性很强,代码也写得不错,可是可移植性在那时算不上是一个关注焦点。实际上,Linus一度还建议让Linux只在i386体系结构上驰骋。不过,人们还是在1993年开始把Linux向Digital Alpha体系结构上移植了。Digital Alpha是一种高性能现代计算机体系结构,它支持RISC和64位寻址。这与Linus最初选的i386无疑是天壤之别。虽然如此,最初的这次移植工作最终花了将近一年终于完成了,Alpha机成为了i386后第一个被官方支持的体系结构。万事开头难,这次移植的挑战性是最大的,为了提高可移植性,内核中不少代码都被重写了。尽管这给整个移植带来了不小的工作量,可是效果是显著的,自此以后,移植变得简单轻松多了。
尽管第一个发行版只支持Intel x86,但1.2版的内核就可以支持Digital Alpha,Intel X86,MIPS和SPARC——虽然支持的不是很完善,带些试验性质。
在2.0版内核中,加入了对Motorola 68k和PowerPC的官方支持,而原1.2版支持的体系结构也纳入了官方支持的范畴,并且稳定下来了。
2.2版内核加入了对更多体系结构的支持,新增了对ARMS,IBM S390和UltraSPARC的支持。没过几年,2.4版内核支持的体系结构就达到了15个,像CRIS,IA_64,64位MIPS,HP PA_RISC,64位IBM S390和Hitachi SH都被加进来了。
当前的2.6内核把这个数字进一步提高到了20,有不含MMU的Motorola 68k,H8/300,IBM POWR,v850,x86-64,甚至还提供了用户模式(Usermode) Linux——一个在Linux虚拟机上运行的内核版本。对64位S390的支持和32位S390的支持放在了一起,移去了重复之处。
值得注意的是,每一种体系结构本身就可以支持不同的芯片和机型。像被支持的ARM和PowerPC等体系结构,它们就可以支持很多不同的芯片和机型。所以说,尽管Linux移植到了20中基本体系结构上,但实际上可以运行它的机器的数目要大的多。
能够由机器一次就完成处理的数据被称为字。这和我们在文档中用字符(8位)和页(许多字)来计量数据是相似的。字是指位的数目——常用的包括16,32或64等。但人们说某个机器是多少“位”的时候,他们其实说的就是该机的字长。比如说,当人们说奔腾是32位芯片时,他们的意思是奔腾的字长为32位,也就是4字节。
处理器通用寄存器的大小和它的字长是相同的。一般来说,对于一个的体系结构来说
它各个部件的宽度——比如说内存总线——最少要和它的字长一样大。而一般来说,地址空间的大小也等于字长,至少Linux支持的体系结构中都是这样的。此外,C语言定义的long类型总对等于机器的字长,而int类型有时会比字长小。比如说,Alpha是64位机。所以它的寄存器、指针和long类型都是64位长度的,而int类型是32位的。Alpha机每一次可以访问和操作一个64位长的数据。
字,双字和一些容易混淆的地方
有些操作系统和处理器不把它们的标准字长称作字,相反,出于历史原因和某种主观的命名习惯,它们用字来代表一些固定长度的数据类型。比如说,一些系统根据长度把数据划分为字节(byte,8位),字(word,16位),双字(double words 32位)和四字(quad words 64位),而实际上该机是32位的。在本书中——在Linux中一般也是这样——象我们前面所讨论的那样,一个字就是代表处理器的字长。
对于支持的每一种体系结构,Linux都要将<asm/types.h>中的BITS_PER_LONG定义为C long类型的长度,也就是系统的字长。表16.1是Linux支持的体系结构和它们的字长的对照表
表16.1 Linux支持的体系结构
体系结构 | 描述 | 字长 |
Alpha | Digital Alpha | 64位 |
Arm | Arm and StrongArm | 32位 |
Cris | CRIS | 32位 |
h8300 | H8/300 | 32位 |
i386 | Intel 386 | 32位 |
ia64 | IA-64 | 64位 |
m68k | Motorola 68k | 32位 |
M68knommu | M68k without MMU | 32位 |
Mips | MIPS | 32位 |
mips64 | 64位MIPS | 64位 |
Parisc | HP PA_RISC | 32位或64位 |
Ppc | PowerPC | 32位 |
ppc64 | POWER | 64位 |
s390 | IBM S/390 | 32位或64位 |
Sh | Hitachi SH | 32位 |
Sparc | SPARC | 32位 |
sparc64 | UltraSPARC | 64位 |
Um | Usermode Linux | 32位或64位 |
v850 | V850 | 32位 |
x86_64 | X86-64 | 64位 |
C语言没有规定变量的标准长度,它们可以根据实现变化。C语言的标准数据类型长度随体系结构变化这一特性不断引起争议。好的一 面是标准数据类型可以充分利用不同体系结构变化的字长而无需明确定义长度。在Linux环境中,long就被确定为机器的实际字长。这并不完全符合ANSI C标准,但在Linux就是这么用的。不好的一面是在编程时不能对标准的C数据类型进行大小的假定,没有什么能够保障int一定和long的长度是相同的。
情况其实还会更加复杂,因为用户空间使用的数据类型和内核空间的数据类型不一定要相互关联。sparc64体系结构就提供了32位的用户空间,其中指针、int和long的长度都是32位。而在内核空间,它的int长度是32位,指针和long的长度却是64。没有什么标准来规范这些。
牢记下述准则:
¨ ANSI C标准规定,一个char的长度一定是8位(1字节)。
¨ 尽管没有规定int类型的长度是32位,但在当前所有支持的体系结构中,它都是32位的。
¨ short类型也类似,在当前所有支持的体系结构中,它都是16位的。
¨ 决不应该假定指针和long的长度,在当前支持的体系结构中,它们就可以在32位和64位中变化。
¨ 由于不同的体系结构long的长度不同,决不应该假设sizeof( int ) == sizeof( long )。
¨ 类似的,也不要假设指针和int长度相等。
不透明数据类型隐藏了它们内部格式或结构。在C语言中,它们就像黑盒一样。支持它们的语言不是很多。作为替代,开发者们利用typedef声明一个类型,把它叫做不透明类型,希望其他人别去把它重新转化回对应的那个标准C类型。通常开发者们在定义一套特别的接口时才会用到它们。比如说用来保存进程标识符的pid_t类型。该类型的实际长度被隐藏起来了——尽管任何人都可以偷偷撩开它的面纱,发现它就是一个int。如果所有代码都不显式的利用它的长度(译者注:显式利用长度这里指直接使用int类型的长度,比如说在编程时使用sizeof(int)而不是sizeof(pid_t)),那么改变时就不会引起什么争议,这种改变确实可能会出现:在老版本的Unix系统中,pid_t的定义是short类型。
另外一个不透明数据类型的例子是atomic_t。在第八章“内核同步方法”中介绍过,它放置的是一个可以进行原子操作的整型值。尽管这种类型就是一个int,但利用不透明类型可以帮助确保这些数据只在特殊的有关原子操作的函数中才会被使用。不透明类型还帮助我们隐藏了类型的长度,该类型也并不总是完整的32位。
内核还用到了其它一些不透明类型,包括dev_t,gid_t和uid_t等等。处理不透明类型时的原则是:
¨ 不要假设该类型的长度。
¨ 不要将该类型转化回其对应的C标准类型使用。
¨ 编程时要保证在该类型实际存储空间和格式发生变化时代码不受影响。
指定数据类型
内核中还有一些数据虽然无需用不透明的类型表示,但它们被定义成了指定的数据类型。jJiffy数目和在中断控制时用到的flags参数就是两个例子,它们都应该被存放在unsigned long类型中。
当存放和处理这些特别的数据时,一定要搞清楚它们对应的类型后再使用。把它们存放在其它如unsigned int等类型中是一种常见错误。在32位机上这没什么问题,可是64位机上就会捅娄子了。
长度明确的类型
作为一个程序员,你往往需要在程序中使用长度明确的数据。像操作硬件设备,进行网络通信和操作二进制文件时,通常都必须满足它们明确的内部要求。比如说,一块声卡可能用的是32位寄存器,一个网络包有一个16位字段,一个可执行文件有8位的cookie。在这些情况下,数据对应的类型应该长度明确。
内核在<asm/typs.h>中定义了这些长度明确的类型,而该文件又被包含在文件<linux/types.h>中。表16.2有完整的清单。
表16.2 长度明确的数据类型
类型 | 描述 |
s8 | 带符号字节 |
u8 | 无符号字节 |
s16 | 带符号16位整数 |
u16 | 无符号16位整数 |
s32 | 带符号32位整数 |
u32 | 无符号32位整数 |
s64 | 带符号64位整数 |
u64 | 无符号64位整数 |
其中带符号的变量用的比较少。
这些长度明确的类型大部分都是通过typedef对标准的C类型进行映射得到的。在一个64位机上,它们看起来像:
typedef signed char s8;
typedef unsigned char u8;
typedef signed short s16;
typedef unsigned short u 16;
typedef signed int s32;
typedef unsigned int u32;
typedef signed long s64;
typedef unsigned long u64;
而在32位机上,它们可能定义成:
typedef signed char s8;
typedef unsigned char u8;
typedef signed short s16;
typedef unsigned short u 16;
typedef signed int s32;
typedef unsigned int u32;
typedef signed long long s64;
typedef unsigned long long u64;
char型的符号问题
C标准表示char类型可以带符号也可以不带符号,由具体的编译器、处理器或由它们两者共同决定到底char是带符号合适还是不带符号合适。
大部分体系结构上,char默认是带符号的,它可以自-128到127之间取值。而也有一些例外,比如ARM体系结构上,char就是不带符号的,它的取值范围是0-255。
举例来说,在默认char不带符号,下面的代码实际会把255而不是-1赋予i:
char i = -1;
而另一种机器上,默认char带符号,就会确切的把-1赋予i。如果程序员本意是把-1保存在i中,那么前面的代码就该修改成:
signed char i = -1;
如果你在自己的代码中使用了char类型,那么你要保证在带符号和不带符号的情况下代码都没问题。如果你能明确要用的是那一个,那么就直接声明它。
对齐是跟数据块在内存中的位置相关的话题。如果一个变量的内存地址正好是它长度的整数倍,它就被称作是自然对齐的。举例来说,对于一个32位类型的数据,如果它在内存中的地址刚好可以被4整除(也就最低两位为0),那它就是自然对齐的。也就是说,一个大小为2 n 字节的数据类型,它地址的最低有效位的后n位都应该为0。
一些体系结构对对齐的要求非常严格。通常象基于RISC的系统,载入未对齐的数据会导致处理器陷入(一种可处理的错误)。还有一些系统可以访问没有对齐的数据,只不过性能会下降。编写可移植性高的代码要避免对齐问题,保证所有的类型都能够自然对齐。
避免对齐引发的问题
编译器通常会通过让所有的数据自然对齐来避免引发对齐问题。实际上,内核开发者在对齐上不用花费太大心思——只有搞gcc的那些老兄才应该为此犯愁呢。可是,当程序员跟指针靠的太近,对数据的访问方式超出编译器的预期时,就会引发问题了。
一个数据类型长度较小,它本来是对齐的,如果你用一个指针进行类型转换,并且转换后的类型长度较大,那么通过改指针进行数据访问时就会引发对齐问题(无论如何,对于某些体系结构会存在这种问题)。也就是说,下面的代码是错误的:
char dog[10];
char *p = &dog[1];
unsigned long l = *(unsigned long *)p;
这个例子将一个指向char型的指针当作指向unsigned long型的指针来用,这会引起问题,因为此时会试图从一个并不能被4整除的内存地址上载入32位的unsigned long型数据。
如果你能想到“我会在现实中这么做吗?”,你基本上就不会有问题了。无论如何,这种错误出现了,并且它还会发生,所以应该小心。实际编程时错误可能不会像这个例子中这么明显。
非标准类型的对齐
前面提到了,对于标准数据类型来说,它的地址只要是其长度的整数倍就对齐了。而非标准的(复合的)C数据类型按照下列原则对齐:
¨ 对于数组,只要按照基本数据类型进行对齐就可了(其实随后的所有元素自然能够对齐)。
¨ 对于联合,只要它包含的长度最大的数据类型能够对齐就可以了。
¨ 对于结构体,这要它包含的长度最大的数据类型能够对齐就可以了。
结构体还要引入填补机制,这会引出下一个问题。
结构体填补
为了保证结构体中每一个成员都能够自然对齐,结构体要被填补。举个例子,下面是一个在32位机上的结构体:
struct foo_struct {
char dog; /* 1字节*/
unsigned long cat; /* 4字节*/
unsigned short pig; /* 2字节 */
char fox; /* 1字节*/
};
由于该结构不能满足各个成员自然对齐,所以它在内存中可不是按照原样存放的。编译器会在内存中创建一个类似下面给出的结构体:
struct foo_struct {
char dog; /* 1字节*/
u8 __pad0[3]; /*3字节*/
unsigned long cat; /* 4字节*/
unsigned short pig; /* 2字节 */
char fox; /* 1字节*/
u8 __pad1; /*1字节*/
};
填补的变量都是为了能够让数据自然对齐而加入的。第一个填充物占用了3个字节的空间,保证cat可以按照4字节对齐。这也自动使其它生于的对象都被对齐了,因为它们长度都比cat要小。第二个也是最后的填充是为了填补struct本身的大小。额外的这个填补使结构体的长度能够被4整除,这样,在由该结构体构成的数组中,每个数组项也就会自然对齐了。
注意,在大部分32位系统上,对于任何一个这样的结构体,sizeof(foo_struct)都会返回12。C编译器自动进行填补以保证自然对齐。
通常你可以通过重新排列结构中的对象来避免填充。这样既可以得到一个较小的结构体,又能保证无需填补它也是自然对齐的。
struct foo_struct {
unsigned long cat; /* 4字节*/
unsigned short pig; /* 2字节 */
char dog; /* 1字节*/
char fox; /* 1字节*/
u8 __pad1; /*1字节*/
};
现在这个结构体只有8字 节大小了。不过,不是任何时候都可以这样对结构体进行调整的。举个例子,如果该结构体是为某个标准定义的,或者它是早就投入使用的代码的一部分,那么它的 成员次序就已经被定死了。还有些时候,你因为一些原因必须使用某种固定的次序——比如说,为了提高高速缓存的命中率进行优化时设定的变量次序。注意,ANSI C明确规定不允许编译器改变结构体内成员对象的次序——它总是由你,程序员来决定。
内核开发者需要注意结构体填补问题,特别是在整体使用时——这是指当需要通过网络发送它们或需要将它们写入文件的时候,因为不同体系结构之间所需要的填补也不尽相同。这也是为什么C没有提供一个内建的结构体比较操作符的原因之一。结构体内的填充字节中可能会包含垃圾信息,所以在结构体之间进行一字节一字节的比较就不大可能实现了。C语言的设计者(正确的)感觉到最好还是由程序员自己为不同的情况编写比较函数,这样才能利用到结构体次序信息。
字节序是指在一个字中各个字节的顺序。处理 器在对字取值时既可能将最低有效位所在的字节当作第一个字节(最左边的字节),也可能将其当作最后一个字节(最右边的字节)。如果最高有效位所在的字节放 在最高字节位置上,其它字节以次放在低字节位置上,那么该字节序被称作高位优先(big-endian)。如果最低有效位所在的字节放在最高字节位置上,其它字节以次放在低字节位置上,那么就称作低位优先(little-endian)。
编写内核代码时不应该假设字节序是给定的哪一种(当然,如果你编写的是与体系结构相关的那部分代码就另当别论了)。Linux内核支持的机器中使用哪一种字节序的都有——甚至包括一些可以在启动的时候选择字节序的机器——适用性强的代码应该两种字节序都支持。
图16.1是高位优先字节序的一个实例。图16.2是低位优先字节序的一个实例。
图16.1 高位优先字节序
i386体系结构使用的是低位优先字节序。而其它系统大多使用高位优先字节序。
让我们看看在实际编程时这些概念有什么意义。让我们考察一下存放在一个四字节的整型中的二进制数,它的十进制对应值是1027:
00000000 00000000 00000100 00000011
在内存中用高位优先和低位优先两种不同字节序存放时的比较如表16.3所示
图16.2 低位优先字节序
表16.3 字节序比较(原书这里是个错误,名字用成了上一张表的名字)
地址 | 高位优先 | 低位优先 |
0 | 00000000 | 00000011 |
1 | 00000000 | 00000100 |
2 | 00000100 | 00000000 |
3 | 00000011 | 00000000 |
注意使用高位优先的体系结构是如何把最高字节位存放在最小的内存地址上的。这和低位优先形成了鲜明的对照。
最后一个例子,我们提供了如何判断给定的机器使用是高位优先还是低位优先字节序的代码:
int x = 1;
if (*(char *) &x ==1)
/* 低位优先 */
else
/* 高位优先 */
这段代码在用户空间和内核空间都能用。
高位优先和低位优先源于乔纳森.斯威夫特写于1726年的讽刺小说《格列佛游记》。在小说中,虚构的小人国里最重要的政治问题就是应该把鸡蛋从大头敲开还是从小头敲开。那些支持从大头敲开的就是高位优先,而那些支持从小头敲开的,就是低位优先。
这个寓言表达的争论是由政治原因引起的,而不是因为技术原因导致的。
内核中的字节序
对于Linux支持的每一种体系结构,相应的内核都会根据机器使用的字节序在它的<asm/byteorder.h>中定义__BIG_ENDIAN或__LITTLE_ENDIAN中的一个。
这个头文件还从include/linux/byteorder/中包含了一组宏命令用于完成字节序之间的相互转换。最常用的宏命令有:
u32 __cpu_to_be32(u32); /* 把cpu字节序转换为高位优先字节序 */
u32 __cpu_to_le32(u32); /* 把cpu字节序转换为低位优先字节序 */
u32 __be32_to_cpu(u32); /* 把高位优先字节序转换为cpu字节序 */
u32 __le32_to_cpu(u32); /* 把低位优先字节序转换为cpu字节序 */
这些转换能够把一种字节序变为另一种字节序。如果两种字节序本来就相同(比如,希望从本地字节序转化为高位优先字节序,而处理器本身使用的就是高位优先字节序),那么宏就什么都不做。否则它们就进行转换。
绝对不要假定时钟中断发生的频率,也就是每秒产生的jiffies数目。相反,应该使用HZ来正确的计量时间。这一点至关重要,因为不但不同的体系结构之间定时中断的频率不同,即使是在同一种体系机构上,两个不同版本的内核之间这种频率也不尽相同。
举个例子,在x86系统上,HZ设定为1000。也就是说,定时中断每秒发生1000次,也就是每毫秒一次。可是在2.6版以前,x86上HZ定为100。而其它的体系机构上数值个不相同:Alpha的HZ是1024而ARM的HZ是100。
绝对不要用jiffies直接去和1000这样的数值去比较,认为这样做大体上不会出问题是要不得的。计量时间的正确方法是乘以或除以HZ。比如:
HZ /* 1秒 */
(2*HZ) /* 2秒 */
(HZ/2) /* 半秒 */
(HZ/100) /* 10毫秒 */
(2*HZ/100) /* 20毫秒 */
HZ定义在文件<asm/param.h>中,在前面的第九章“定时器和时间管理”中曾经讨论过。
当处理用页管理的内存时,绝对不要假设页的长度。在x86下编程的程序员往往错误的认为一页的大小就是4KB。尽管x86机器上使用的页确实是4KB,但是其它不同的体系结构使用的页长度可能不同。实际上有些体系结构还同时支持多种不同长度的页。表16.4列举了各种体系结构使用的页的长度。
当处理用页组织管理的内存时,通过PAGE_SIZE来使用以字节数来表示的页长度。而PAGE_SHIFT这个值定义了从最右端屏蔽多少位能够得到该地址对应的页的页号。
举例来说,在页长为4KB的x86机上,PAGE_SIZE为4096而PAGE_SHIFT为12。它们都定义于<ams/page.h>中。
表16.4 不同体系结构的页长度
体系结构 | PAGE_SHIFT | PAGE_SIZE |
Alpha | 13 | 8KB |
Arm | 12,14,15 | 4KB,16KB,32KB |
Cris | 13 | 8KB |
h8300 | 12 | 4KB |
i386 | 12 | 4KB |
ia64 | 12,13,14,16 | 4KB, 8KB, 16KB, 64KB |
m68k | 12,13 | 4KB, 8KB |
M68knommu | 12 | 4KB |
Mips | 12 | 4KB |
mips64 | 12 | 4KB |
Parisc | 12 | 4KB |
Ppc | 12 | 4KB |
ppc64 | 12 | 4KB |
s390 | 12 | 4KB |
Sh | 12 | 4KB |
Sparc | 12,13 | 4KB, 8KB |
sparc64 | 13 | 8KB |
v850 | 12 | 4KB |
x86_64 | 12 | 4KB |
回忆第8章“内核同步方法”,其中讨论过体系结构对指令序列的排序问题。有些处理器严格限制指令排序,代码指定的所有装载或存储指令都不能被重新排序;而另外一些体系结构对排序要求则很弱,可以自行排序指令序列。
在代码中,如果在对排序要求最弱的体系结构上,要保证指令执行顺序。那么就必须使用诸如rmb()和wmb()等恰当的内存屏障来确保处理器以正确顺序提交装载和存储指令。
在讨论可移植性的地方加入有关并发处理、内核抢占和高端内存的部分看起来似乎不太恰当。毕竟,这些都不是会影响到操作系统的硬件之间有所差异的那些特性,恰恰相反,它们都是Linux内核本身的一些功能,硬件体系结构根本感知不到它们的存在。但是,它们代表的其实都是可配置的重要选项,而你的代码应该充分考虑到对它们的支持。就是说,只有在编程时就针对SMP/内核抢占/高端内存进行了考虑,你的代码才会无论内核怎样配置,都能身处安全之中。再在前面那些保证可移植性的规范下加上这几条:
¨ 假设你的代码会在SMP系统上运行,要正确的选择和使用锁。
¨ 假设你的代码会在支持内核抢占的情况下运行,要正确的选择和使用锁和内核抢占语句。
¨ 假设你的代码会运行在使用高端内存(非永久映射内存)的系统上,必要时使用kmap()。
简而言之,要想写出移植性好、简洁、合适的代码要注意以下两点:
编码尽量选取最大公因子:假定任何事情都可能发生,任何潜在的约束也都存在。
编码尽量选取最小公约数:不要假定给定的内核特性是可用的,仅仅需要最小的体系结构功能。
编写可移植的代码需要考虑许多问题:字长、 数据类型、对齐、字节次序、页大小、处理器排序等等。对于绝大多数内核开发来说,可能你主要考虑的问题就是保证正确使用数据类型,虽然如此,说不定有朝一 日,还是会有些与古老的体系结构有关的问题突然跳出来开始困扰你——所以说理解移植性的重要性,并且在开发内核过程中时刻注意编写简洁、可移植的代码是非 常重要的。
在内核开发中这很普遍。如果打算做一件事,那么就要把它做好。为了追求完美,内核开发者们是决不会介意重写大段代码的。
不过实际上可寻址的内存空间也可能会比字长小一些。比如,一个64位的体系结构虽然可能会提供64位的指针,但可能只会用48位来进行寻址。此外,如果支持Intel的PAE,那么实际的物理内存也有比字长还大的可能。
唯一的例外是char,它的长度总是8位(一个字节)
事实上对于Linux支持的64位体系结构来说,long和int长度是不同的,int是32位的,而long是64位的。但对于我们所熟悉的32位体系结构而言两种数据类型都是32位的。
如果让编译器随心所欲的改变结构体中各个对象的位置的话,现存的程序大部分都会崩溃。在C语言中,函数往往通过在结构体地址上加上偏移量来计算变量的位置。
转载地址:http://bscdi.baihongyu.com/