Linux内核设计与实现 第19章 可移植性

Linux是一个可移植性非常好的操作系统,它广泛支持许多不同体系结构的计算机。可移植性是指代码从一种体系结构移植到另外一种不同的体系结构上的方便程度。我们都知道Linux是可移植的,因为它已经能够在各种不同的体系结构上运行了。但这种可移植性不是凭空得来的——需要在编写可移植代码时就为此付出努力并坚持不懈。现在,这种努力已经开始得到回报了,移植Linux到新的系统上就很容易(相对来说)完成。
本章中我们将讨论如何编写可移植的代码——编写内核代码和驱动程序时,必须时刻牢记这个问题。

19.1可移植操作系统

  • a)追求可移植性的Minix、NetBSD
    有些操作系统在设计时把可移植性作为头等大事之一,尽可能少地涉及与机器相关的代码。
    汇编代码用得少之又少,为了支持各种不同类别的体系结构,界面和功能在定义时都 尽最大可能地具有普适性和抽象性。
    这么做最显著的回报就是需要支持新的体系结构时,所需完成的工作要相对容易许多。
    一些移植性非常高而本身又比较简单的操作系统在支持新的体系结构时,可能只需要为此体系结构编写几百行专门的代码就行了。
    问题在于,体系结构相关的一些特性往往无法被支持,也不能对特定的机器进行手动优化。
    选择这种设计,就是利用代码的性能优化能力换取代码的可移植性。Minix、NetBSD和许多研究用的系统就是这种高度可移植操作系统的实例。

  • b)追求性能表现的DOS和Windows 95
    与之相反,还有一种操作系统完全不顾及可移植性,它们尽最大的可能追求代码的性能表现,尽可能多地使用汇编代码,压根就是只为在一种硬件体系结构使用。 内核的特性都是围绕硬件提供的特性设计的。因此,将其移植到其他体系结构就等于再重新从头编写一个新的操作系统
    内核,而且即便进行移植,这种操作系统在其他体系结构上也会不适用。选择这种设计,就是用代码的可移植性换取代码的性能优化能力。这样的系统往往比移植性好的系统更难维护。当然,这种系统对性能的要求不见得比对可移植性系统更强,不过它们还是愿意牺牲可移植性,而不乐意让设计打折扣。DOS和Windows 95便是这种设计方案的最好例证。

  • c)在性能表现与可移植性之间追求折中的Linux
    Linux在可移植性这个方面走的是中间路线。 差不多所有的接口和核心代码都是独立于硬件体系结构的C语言代码。但是,在对性能要求很严格的部分,内核的特性会根据不同的硬件体系进行调整。
    举例来说,需要快速执行的和底层的代码都与硬件相关并且是用汇编语言写成的。这种实现方式使Linux在保持可移植性的同时兼顾对性能的优化。当可移植性妨碍性能发挥的时候,往往性能会被优先考虑。除此之外,代码就一定要保证可移植性。

一般来说,暴露在外的内核接口往往是与硬件体系结构无关的。如果函数的任何部分需要针对特殊的体系结构(无论是出于优化的目的还是作为一种必需的选择)提供支持的时候,这些部分都会被安置在独立的函数中,等待调用。每种被支持的体系结构都实现了一个与体系结构相关的函数,而且会链接到内核映像之中。.

调度程序就是一个好例子。调度程序的主体程序存放在kernel/sched.c文件中,用C语言编写,与体系结构无关。可是,调度程序需要进行的一些工作,比如说切换处理器上下文和切换地址空间等,却不得不依靠相应的体系结构完成。于是,内核用C语言编写了函数context_
switch()用于实现进程切换,而在它的内部,则会调用switch_to() 和switch_mm()分别完成处理器上下文和地址空间的切换。

而对于Linux支持的每种体系结构,它们的switch__to()和switch_mm()实现都各不相同。所以,当Linux需要移植到新的体系结构上的时候,只需要重新编写和提供这样的函数就可以了。

与体系结构相关的代码都存放在arch/architecture/目录中,architecture 是Linux支持的体系结构的简称。比如说,Intel x86体系结构对应的简称是x86 (这种体系结构既支持x86-32又支持x86-64)。与这种体系结构相关的代码都存放在arch/x86目录下。2.6系列内核支持的体系结构包括alpha、arm、 avr32、 blackin、 cris、 frv、 h8300、 ia64、 m32r、m68k、m68knommu、 mips、mn10300、parisc、powerpc、 s390、 sh、 sparc、 um、x86 和xtensa.。本章稍后给出的表19-1是一份更详尽的清单。

19.2 Linux移植史

  • 当Linus最初把Linux带到这个无法预测的大千世界的时候,它只能在i386.上运行。尽管这个操作系统通用性很强,代码也写得不错,可是可移植性在那时算不上是一个关注焦点。实际上,Linus还一度建议让Linux只在i386体系结构上驰骋。不过,人们还是在1993年开始
    把Linux向Digital Alpha体系结构上移植了。Digital Alpha是一种高性能现代计算机体系结构,它支持RISC和64位寻址。这与Linus最初选的i386无疑是天壤之别。虽然如此,最初的这次移植工作最终还是花了将近一年时间,Alpha机成为了i386后第一个被官方支持的体系结构。 万事开头难,这次移植的挑战性是最大的,为了提高可移植性,内核中不少代码都被重写了。尽管这给整个移植带来了不小的工作量,可是效果是显著的,自此以后,移植变得简单轻松多了。

  • 尽管第一个发行版只支持Intel i386,
    但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、HPPA_RISC、64位IBM S/390和Hitachi SH都被加进来。
    当前的2.6内核把体系结构的数目进一步提高到了21个,有不含MMU的AVR、FR-V和Motorola 68k以及M32xxx、H8/300、 IBMPOWER、Xtensa, 甚至还提供了用户模式(Usermode)Linux (一个在Linux虚拟机上运行的内核版本)。

  • 每一种体系结构本身就可以支持不同的芯片和机型。 像被支持的ARM和PowerPC等体系结构,它们就可以支持很多不同的芯片和机型。其他的体系结构,比如说x86和SPARC,它们可以支持32位和64位不同的处理器。所以说,尽管Linux移植到了21种基本体系结构上,但实际上可以运行它的机器的数目要大得多。

19.3字长和数据类型

  • 普及常识
    能够由机器一次完成处理的数据称为字。 这和我们在文档中用字符(8位)和页(许多字,通常是4KB或8KB)来计量数据是相似的。
    字是指位的整数数目——比如说, 1、 2、4或8等。
    但人们说某个机器是多少“位”的时候,他们其实说的就是该机器的字长。 比如说,当人们说Intel i7是64位芯片时,他们的意思是奔腾的字长为64位,也就是8字节。处理器通用寄存器( general-purpose registers, GPR) 的大小和它的字长是相同的。 一般来说,对于一个体系结构,它各个部件的宽度(比如说内存总线)最少要和它的字长一样大。虽然物理地址空间有时候会比字长小,但虚拟地址空间的大小也等于字长,至少Linux支持的体系结构中都是这样的。
    此外,C语言定义的long类型总是对等于机器的字长,而int类型有时会比字长小。 比如说,Alpha 是64位机器,所以它的寄存器、指针和long类型都是64位长度的,而int类型是32位的。Alpha 机每一次可以访问和操作一个 64位长的数据。

  • 字、双字以及混合
    有些操作系统和处理器不把它们的标准字长称作 ,相反,出于历史原因和某种主观的命名习惯,它们用字来代表一些固定长度的数据类型。比如说,一些系统根据长度把数据划分为字节(byte, 8位)、字(word, 16 位)、双字(double words, 32 位)和四字(quadwords 64位),而实际上该机是32位的(即标准字长是32位)。
    处理体系结构的特殊:在本书中(在Linux中一般也是这样),像我们前面所讨论的那样,一个字就代表处理器的字长。对于支持的每一种体系结构,Linux 都要将<asm/types.h>中的BITS PER LONG定义为C long类型的长度,也就是系统的字长。
    表19-1 是Linux支持的体系结构和它们的字长的对照表。

一般而言,Linux对于一种体系结构都会分别实现32位和64位的不同版本。
比如,在2.6内核的早期版本中,内核中就同时有i386和x86-64, mips 和mips64,以及ppc和ppc64。
但现在,经过大家的努力,这些体系结构均放在arch/目录下,每个代码库中既支持32位又支持64位。

表19-1 Linux 支持的体系结构

体系结构描述字长
alpbaDigital Alpha64位
armARM和增强型ARM32位
avtAVR32位
blackfinBlackfn32位
crisCRIS32位
frvFR-V32位
b8300H8/30032位
ia64IA-6464位
m32rM32xxx32位
m68kMotorola 68k32位
m68knommu无MMU型M68k32位
mipsMIPS32位和64位
pariscHP PA-RISC32位和64位
powerpcPowerPC32位和64位
s390IBM S/39032位和64位
ShHitachi SH32位
SparcSPARC32位和64位
UmUsermode Linux32位和64位
x86x86-32和x86-6432位和64位
xtensaXtensa32位

C语言虽然规定了变量的最小长度,但是没有规定变量具体的标准长度,它们可以根据不同体系结构的不同实现而变化。唯一的例外是char,它的长度总是8位。
C语言的标准数据类型长度随体系结构变化这一特性不断引起争议。
好的一面是标准数据类型可以充分利用不同体系结构变化的字长而无须明确定义长度。C语言中long类型的长度就被确定为机器的字长。
不好的一面是在编程时不能对标准的C数据类型进行大小的假定,没有什么能够保障int一定和long的长度是相同的。

情况其实还会更加复杂,因为用户空间使用的数据类型和内核空间的数据类型不一定要相互关联。
sparc64 体系结构就提供了32位的用户空间,其中指针、int和long的长度都是32位。而在内核空间,它的int长度是32位,指针和long的长度却是64。没有什么标准来规范这些。

牢记下述准则:
●ANSIC标准规定,一个char的长度一定是1字节。
●尽管没有规定int类型的长度是32位,但在Linux当前所有支持的体系结构中,它都是32位的。
●short类型也类似,在当前所有支持的体系结构中,虽然没有明文规定,但是它都是16位的。
●绝不应该假定指针和long的长度,在Linux当前支持的体系结构中,它们可以在32位和64位中变化。
●由于不同的体系结构long的长度不同,决不应该假设sizeof( int ) = sizeof( long )。
●类似地,也不要假设指针和int长度相等。

操作系统常用一个简单的助记符来描述此系统中数据类型的大小。
比如,64 位的Windows系统简称为LLP64,它说明long和指针的长度都是64位。
   64位的Linux系统可简记为LP64,即long和指针都是64位。
   32位的Linux系统简称为ILP32,即int、 long 和指针的长度均为32位。
这些助记符可以一目了然地显示出操作系统所提供的字长大小,因为这种方法涉及一种权衡问题。

现在依次来分析ILP64、LP64 和LLP64这三种情况。
ILP64 这种操作系统,int、 long 和指针的大小都是64位。这样的数据长度使得编程变得更加容易,因为C语言中主要的数据类型大小是一样的( 整型和指针大小的不匹配是编程中常出现的错误)。不过这样也会带来缺点,这
种整型比我们平常所需的整型要大很多。
在LP64操作系统中,程序员可以使用不同大小的整型,但必须注意整型的大小比指针类型要小。
对于LLP64系统而言,程序员不仅要被迫接受int和long的大小相同,还要担心整型和指针之间的大小不匹配。大多数程序员都喜欢LP64型,即Linux所采用的操作系统模型。

1)不透明类型

不透明数据类型隐藏了它们的内部格式或结构。在C语言中,它们就像黑盒一样。支持它们的语言不是很多。作为替代,开发者们利用typedef声明一个类型,把它叫做不透明类型,希望其他人别去把它重新转化回对应的那个标准C类型。通常开发者们在定义一套特别的接口时才会用到它们。

比如说用来保存进程标识符的pid_t类型。该类型的实际长度被隐藏起来了——尽管任何人都可以偷偷撩开它的面纱,发现它就是一个int。如果所有代码都不显式地利用它的长度,那么改变时就不会引起什么争议,这种改变确实可能会出现:在老版本的Unix系统中,pid_ t的定义是short类型。

另外一个不透明数据类型的例子是atomic_ t。 在第10章中介绍过,它放置的是-一个可以进行原子操作的整型值。尽管这种类型就是一个int,但利用不透明类型可以帮助确保这些数据只在特殊的有关原子操作的函数中才会被使用。不透明类型还帮助我们隐藏了atomic_ t类型的可用
长度,但是该类型也并不总是完整的32位,比如在32位SPARC体系下长度就被限制。

内核还用到了其他一些不透明类型,包括dev_t、gid_t和uid_t等。

处理不透明类型时的原则是:
●不要假设该类型的长度。这些类型在某些系统中可能是32位,而在其他系统中又可能是64位。并且,内核开发者可以任意修改这些类型的大小。
●不要将该类型转化回其对应的C标准类型使用。
●成为一个大小不可知论者。编程时要保证在该类型实际存储空间和格式发生变化时代码不受影响。

2)指定数据类型

内核中还有一些数据虽然无须用不透明的类型表示,但它们定义成了指定的数据类型。在中断控制时用到的flag参数就是个例子,它应该存放在unsigned long类型中。

当存放和处理这些特别的数据时,一定要搞清楚它们对应的类型后再使用。把它们存放在其他(如unsigned int等)类型中是一种常见错误。在32位机上这没什么问题,可是64位机上就会捅娄子了。

64位机上捅娄子就是64位机上unsigned int是32位、unsigned long是64位:

unsigned long flag = 100;
unsigned int cun_fang;
cun_fang = flag;

3)需要有长度明确的类型

作为一个程序员,你往往需要在程序中使用长度明确的数据。像操作硬件设备、进行网络通信和操作二进制文件时,通常都必须满足它们明确的内部要求。比如说,一块声卡可能用的是32位寄存器,一个网络包有一个16位字段,一个可执行文件有8位的cookie。在这些情况下,数据对应的类型应该长度明确。
内核在<asm/typs.h>中定义了这些长度明确的类型,而该文件又被包含在文件<linwx/types.h>中。表19-2有完整的清单。

表19-2长度明确的数据类型

类型描述
s8带符号字节
a8无符号字节
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 u16;
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 u16;
typedef signed int s32;
typedef unsigned int u32;
typedef signed 1ong long 864 ;
typedef unsigned 1ong 1ong u64;

上述的这些类型只能在内核内使用,不可以在用户空间出现(比如,在头文件中的某个用户可见结构中出现)。这个限制是为了保护命名空间。不过内核对应这些不可见变量同时也定义了对应的用户可见的变量类型,这些类型与上面类型所不同的是增加了两个下划线前缀。比如,无符号32位整型对应的用户空间可见类型就是_ u32. 该类型除了名字有区别外,其他方面与u32相同。 在内核中你可以任意使用这两个名字,但是如果是用户可见的类型,那必须使用下划线前缀的版本名,防止污染用户空间的命名空间。

4)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;
另外,如果程序员确实希望存储255,那么代码应该如下:
unsigned char = 255;
如果在自己的代码中使用了char类型,那么要保证在带符号和不带符号的情况下代码都没问题。如果能明确要用的是哪一个,就直接声明它。

19.4数据对齐

内存是按字节编址,每个地址的存储单元可以存放8bit的数据。
对齐是跟数据块在内存中的位置相关的话题。如果一个变量的内存地址正好是该变量字节数的整数倍,它就称作是自然对齐的。 举例来说,对于一个32位类型的数据,如果它在内存中的地址刚好可以被4整除(也就最低两位为0),那它就是自然对齐的。也就是说,一个大小为2n字节的数据类型,它地址的最低有效位的后n位都应该为0。
在这里插入图片描述

一些体系结构对对齐的要求非常严格。通常像RISC的系统,载入未对齐的数据会导致处理器陷入(一种可处理的错误)。还有一些系统可以访问没有对齐的数据,只不过性能会下降。编写可移植性高的代码要避免对齐问题,保证所有的类型都能够自然对齐。

为什么要数据对齐?

  • 通过 aligned 这个属性声明,我们虽然可以显式指定变量的地址对齐方式,但是也会因边界对齐造成一定的内存空洞,浪费一定的内存空间。比如在上面这个程序中,0x00402009~0x0040200b 这三个地址空间的存储单元就没有被使用。

  • 既然地址对齐会造成一定的内存空洞,那我们为什么还要按照这种对齐方式去存储数据呢?
    一个主要原因就是,这种对齐设置可以简化 CPU 和内存 RAM 之间的接口和硬件设计。比如一个32位的计算机系统,CPU 读取内存时,硬件设计上可能只支持4字节或4字节倍数对齐的地址访问,CPU 每次往内存 RAM 读写数据时,一个周期可以读写4个字节。如果我们把一个数据放在4字节对齐的地址上,那么CPU一次就可以把数据读写完毕;如果我们把一个 int 型数据放在一个非4字节对齐的地址上,那 CPU 就要分2次才能把这个4字节大小的数据读写完毕。

  • 为了配合计算机的硬件设计,编译器在编译程序时,对于一些基本数据类型,比如 int、char、short、float 等,会按照其数据类型的大小进行地址对齐,按照这种地址对齐方式分配的存储地址,CPU一次就可以读写完毕。虽然边界对齐会造成一些内存空洞,浪费一些内存单元,但是在硬件上的设计却大大简化了。这也是编译器给我们定义的变量分配地址时,不同类型变量按不同字节数地址对齐的原因。除了 int、char、short、float 这些基本类型数据,对于一些复合类型数据,也要满足地址对齐要求。

1)避免对齐引发的问题

编译器通常会通过让所有的数据自然对齐来避免引发对齐问题。实际上,内核开发者在对齐上不用花费太大心思——只有搞gcc的那些老兄才应该为此犯愁呢。可是,当程序员使用指针太多,对数据的访问方式超出编译器的预期时,就会引发问题了。
一个数据类型长度较小,它本来是对齐的,如果你用一个指针进行类型转换,并且转换后的类型长度较大,那么通过改指针进行数据访问时就会引发对齐问题(无论如何,某些体系结构会存在这种问题)。也就是说,下面的代码是错误的:

char wolf[]="Like a wolf";
char *p = &wo1f[1] ;
unaigned long l = * (unsigned long *)p;

这个例子将一个指向char型的指针当做指向unsigned long型的指针来用,这会引起问题,因为此时会试图从一个并不能被4或8整除的内存地址上载入32位或64位的unsigned long型数据。
这种复杂的访问可能看起来有些模糊,不过通常就是如此。无论如何,这种错误出现了,所以应该小心。实际编程时错误可能不会像一些例子中那么明显或复杂。

2)非标准类型的对齐

前面提到了,对于标准数据类型来说,它的地址只要是其长度的整数倍就对齐了。而非标准的(复合的) C数据类型按照下列原则对齐:
●对于数组,只要按照基本数据类型进行对齐就可以了,随后的所有元素自然能够对齐。
●对于联合体,只要它包含的长度最大的数据类型能够对齐就可以了。
●对于结构体,只要结构体中每个元素能够正确地对齐就可以了。
结构体还要引入填补机制,这会引出下一个问题。

3)结构体填补

为了保证结构体中每一个成员都能够自然对齐,结构体要被填补。这点确保了当处理器访问结构中一个给定元素时,元素本身是对齐的。举个例子,下面是一个在32位机上的结构体:

struct animal_ structI
char dog;/* 1字节*/
unsigned 1ong/* 4字节*/
unsigned short pig;/* 2字节*/
char fox;/* 1字节*/
};

由于该结构不能准确地满足各个成员自然对齐,所以它在内存中可不是按照原样存放的。编译器会在内存中创建一个类似下面给出的结构体:

struct animal_ gtruct{
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(animal struct) 都会返回12。C编译器自动进行填补以保证自然对齐。通常你可以通过重新排列结构体中的对象来避免填充。这样既可以得到一个较小的结构体,又能保证无须填补它也是自然对齐的。

struct animal_atruct {
unsigned long cat;/* 4字节*/
unsigned short pig;/* 2字节*/
char dog;/* 1字节*/
char fox; ./* 1字节*/
};

现在这个结构体只有8字节大小了。不过,不是任何时候都可以这样对结构体进行调整的。举个例子,如果该结构体是某个标准的一部分,或者它是现有代码的一部分, 那么它的成员次序就已经被定死了,虽然内核(缺少一个正式的ABI)相比用户空间来说,这种需求要少得多。还有些时候,因为一些原因必须使用某种固定的次序——比如说,为了提高高速缓存的命中率进行优化时设定的变量次序。注意,ANSI C明确规定不允许编译器改变结构体内成员对象的次序——它总是由程序员来决定的。虽然编译器可以帮助你做填充,但是,如果使用-Wpadded flag标志,那么将使gcc在发现结构体被填充时产生警告。
内核开发者需要注意结构体填补问题,特别是在整体使用时——这是指当需要通过网络发送它们或需要将它们写人文件的时候,因为不同体系结构之间所需要的填补也不尽相同。这也是为什么C语言没有提供一个内建的结构体比较操作符的原因之一。结构体内的填充字节中可能会包含垃圾信息,所以在结构体之间进行一字节一字节的比较就不大可能实现了。C语言的设计者(正确的)感觉到最好还是由程序员自己为不同的情况编写比较函数,这样才能利用到结构体次序信息。

19.5字节顺序

字节顺序是指在一个字中各个字节的顺序。处理器在对字取值时既可能将最低有效位所在的字节当做第一个字节(最左边的字节),也可能将其当做最后一个字节(最右边的字节)。如果最高有效位所在的字节放在低字节位置上,其他字节依次放在高字节位置上,那么该字节顺序称作高位优先(big-endian)。 如果最低有效位所在的字节放在高字节位置上,其他字节依次放在低字节位置上,那么就称作低位优先(ittle -endian)。
编写内核代码时不应该假设字节顺序是给定的哪一种 ( 当然,如果你编写的是与体系结构相关的那部分代码就另当别论了)。Linux内核支持的机器中使用哪一种字节顺序的都有(甚至包括一些可以在启动的时候选择字节顺序的机器),适用性强的代码应该两种字节顺序都支持。
图19-1是高位优先字节顺序的一个实例,图19-2是低位优先字节顺序的一个实例。
在这里插入图片描述
x86体系结构,不论32位机还是64位机,使用的都是低位优先字节顺序。而其他系统大多使用高位优先字节顺序。
让我们看看在实际编程时这些概念有什么意义。让我们考察一下存放在一个4字节的整型中的二进制数,它的十进制对应值是1027 :
0000000 0000000 00000100 0000011
在内存中用高位优先和低位优先两种不同字节顺序存放时的比较如表19-3所示。
表19-3字节顺序比较

地址高位优先低位优先
000000000000011
100000000000100
20000000000000
300000110000000

注意使用高位优先的体系结构把最高字节位存放在最小的内存地址上的。这和低位优先形成了鲜明的对照。
最后一一个例子,我们提供了如何判断给定的机器使用是高位优先还是低位优先字节顺序的代码:

int x=1;
if (*(char *x == 1)
/*低位优先*/
else
/* 高位优先*/

这段代码在用户空间和内核空间都能用。

  • 高位优先和低位优先的历史
    高位优先和低位优先源于乔纳森●撕威夫特写于1726年的讽刺小说《格列弗游记》。在小说中,虚构的小人国里最重要的政治问题就是应该把鸡蛋从大头敲开还是从小头敲开。那些支持从大头敲开的就是高位优先;而那些支持从小头敲开的,就是低位优先。
    高位优先与低位优先的孰优孰差就好像小人国中的政治争论一样,与其说是技术问题,倒不如说是政治问题啦。

对于Linux支持的每一种体系结构,相应的内核都会根据机器使用的字节顺序在它的<asm/byteorder.h>中定义__BIG_ENDIAN或_ LITTLE_ ENDIAN中的一个。
这个头文件还从include/inux/byteorder/中包含了一组宏命令用于完成字节顺序之间的相互转换。最常用的宏命令有:

u23 __cpu_to_be32 (u32);/*把cpu字节顺序转换为高位优先字节顺序*/
u32 __cpu_to_1e32 (u32); /* 把cpu字节顺序转换为低位优先字节顺序*/
u32 __be32_to_cpu(u32); /* 把高位优先字节顺序转换为cpu字节顺序*/
u32 __le32_to_cpus (u32); /* 把低位优先字节顺序转换为cpu字节顺序*/

这些转换能够把一种字节顺序变为另一种字节顺序。如果两种字节顺序本来就相同( 比如,希望从本地字节顺序转化为高位优先字节顺序,而处理器本身使用的就是高位优先字节顺序),那么宏就什么都不做。否则,它们就进行转换。

19.6时间

节拍率HZ:单位时间(秒)系统时钟产生的节拍总数。
定时器≈计数器,在音乐中,有一定强弱分别的一系列拍子在每隔一定时间重复出现。
即弹钢琴时节拍这个动作是手指按下某个钢琴键;在操作系统中,节拍这个动作是定时器加1。

间测量是另一个内核概念,它随着体系结构甚至内核版本的不同而不同。绝对不要假定时钟中断发生的频率,也就是每秒产生的jifes数目。相反,应该使用HZ来正确计量时间。这一点至关重要,因为不但不同的体系结构之间定时中断的频率不同,即使是在同一种体系机构上,两个不同版本的内核之间这种频率也不尽相同。

举个例子,在x86系统上,HZ设定为100。也就是说,定时中断每秒发生100次,也就是每10ms一次。可是在2.6版以前,x86上HZ定为1000。而其他体系机构上的数值各不相同:alpha的HZ是1024而ARM的HZ是100.

绝对不要用jifes直接去和1000这样的数值比较,认为这样做大体上不会出问题是要不得的。计量时间的正确方法是乘以或除以HZ。比如:

乘以或除以一秒节拍总数节拍个数代表的时间大小
HZ个节拍1秒
(2+HZ)个节拍2秒
(HZ/2)个节拍半秒
(HZ/100)个节拍10ms
(2 *HZ/100)个节拍20ms

HZ定义在文件<asm/param.b>中,在前面的第10章中曾经讨论过。

19.7页长度

当处理用页管理的内存时,绝对不要假设页的长度。
在x86-32下编程的程序员往往错误地认为一页的大小就是4KB。尽管x86-32机器上使用的页确实是4KB,但是其他不同的体系结构
使用的页长度可能不同。实际上有些体系结构还同时支持多种不同长度的页。表19-4列举了各种体系结构使用的页的长度。

当处理用页组织管理的内存时,通过PAGE _SIZE 以字节数来表示页长度。而PAGE_SHIFT这个值定义了从最右端屏蔽多少位能够得到该地址对应的页的页号。举例来说,在页长为4KB的x86-32机上,PAGE _SIZE为4096而PAGE_SHIFT 为12。它们都定义于<ams/page.h>中。

表19-4不同体系结构的页长度

体系结构PAGE_SHIFTPAGE_SIZE
alpha138KB
arm12,14, 154KB, 16KB, 32KB
avr124KB
cris138KB
blackfin1216KB
h8300144KB
-124KB,8KB, 16KB, 64KB
m32r12,13, 14. 164KB
m68k124KB,8KB
m68knommu12,134KB
mips124KB
mn10300124KB
parisc124KB
powerpc124KB
s390124KB
sh124KB
sparc12, 134KB, 8KB
um124KB
x86124KB
xtensa124KB

19.8处理器排序

回忆第9章和第10章,其中讨论过体系结构对指令序列的排序问题。有些处理器严格限制指令排序,代码指定的所有装载或存储指令都不能被重新排序:而另外一些体系结构对排序要求则很弱,可以自行排序指令序列。

在代码中,如果在对排序要求最弱的体系结构上,要保证指令执行顺序。那么就必须使用诸如rmb0和wmb0等恰当的内存屏障来确保处理器以正确顺序提交装载和存储指令。详情请参见第10章。

什么是重排序?

重排序通常是编译器或运行时环境为了优化程序性能而采取的对指令进行重新排序执行的一种手段。重排序分为两类:编译期重排序和运行期重排序,分别对应编译时和运行时环境。
在并发程序中,程序员会特别关注不同进程或线程之间的数据同步,特别是多个线程同时修改同一变量时,必须采取可靠的同步或其它措施保障数据被正确地修改,这里的一条重要原则是:不要假设指令执行的顺序,你无法预知不同线程之间的指令会以何种顺序执行。

但是在单线程程序中,通常我们容易假设指令是顺序执行的,否则可以想象程序会发生什么可怕的变化。理想的模型是:各种指令执行的顺序是唯一且有序的,这个顺序就是它们被编写在代码中的顺序,与处理器或其它因素无关,这种模型被称作顺序一致性模型,也是基于冯·诺依曼体系的模型。当然,这种假设本身是合理的,在实践中也鲜有异常发生,但事实上,没有哪个现代多处理器架构会采用这种模型,因为它是在是太低效了。而在编译优化和CPU流水线中,几乎都涉及到指令重排序。

  • 编译期重排序
    编译期重排序的典型就是通过调整指令顺序,在不改变程序语义的前提下,尽可能减少寄存器的读取、存储次数,充分复用寄存器的存储值。
    假设第一条指令计算一个值赋给变量A并存放在寄存器中,第二条指令与A无关但需要占用寄存器(假设它将占用A所在的那个寄存器),第三条指令使用A的值且与第二条指令无关。那么如果按照顺序一致性模型,A在第一条指令执行过后被放入寄存器,在第二条指令执行时A不再存在,第三条指令执行时A重新被读入寄存器,而这个过程中,A的值没有发生变化。通常编译器都会交换第二和第三条指令的位置,这样第一条指令结束时A存在于寄存器中,接下来可以直接从寄存器中读取A的值,降低了重复读取的开销。

  • 重排序对于流水线的意义
    现代CPU几乎都采用流水线机制加快指令的处理速度,一般来说,一条指令需要若干个CPU时钟周期处理,而通过流水线并行执行,可以在同等的时钟周期内执行若干条指令,具体做法简单地说就是把指令分为不同的执行周期,例如读取、寻址、解析、执行等步骤,并放在不同的元件中处理,同时在执行单元EU中,功能单元被分为不同的元件,例如加法元件、乘法元件、加载元件、存储元件等,可以进一步实现不同的计算并行执行。
    流水线架构决定了指令应该被并行执行,而不是在顺序化模型中所认为的那样。重排序有利于充分使用流水线,进而达到超标量的效果。

  • 确保顺序性
    尽管指令在执行时并不一定按照我们所编写的顺序执行,但毋庸置疑的是,在单线程环境下,指令执行的最终效果应当与其在顺序执行下的效果一致,否则这种优化便会失去意义。
    通常无论是在编译期还是运行期进行的指令重排序,都会满足上面的原则。

19.9 内核抢占、高端内存、SMP(对称多处理器架构)

在讨论可移植性的地方加入有关并发处理、内核抢占和高端内存的部分看起来似乎不太恰当。毕竟,这些都不是会影响到操作系统的硬件之间有所差异的那些特性;恰恰相反,它们都是Linux内核本身的一些功能,硬件体系结构根本感知不到它们的存在。但是,它们代表的其实都是可配置的重要选项,而你的代码应该充分考虑到对它们的支持。
就是说,只有在编程时就针对SMP(对称多处理器架构) /内核抢占/高端内存进行了考虑,代码才会无论内核怎样配置,都能身处安全之中。再在前面那些保证可移植性的规范下加上这几条:
●假设你的代码会在SMP系统上运行,要正确地选择和使用锁。
●假设你的代码会在支持内核抢占的情况下运行,要正确地选择和使用锁和内核抢占语句。
●假设你的代码会运行在使用高端内存(非永久映射内存)的系统上,必要时使用kmap()。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值