Linux内核设计与实现(19)第十九章:可移植性

1. linux移植性非常好

linux内核的移植性非常好, 目前的内核也支持非常多的体系结构(有20多个).
源码树中 arch 目录下有支持的体系结构, 每种体系结构一个文件夹

2. 字长 位

2.1 字长

这里的字是指处理器能够一次完成处理的数据。

字长即使处理器能够一次完成处理的数据的最大长度.
目前的处理器主要有32位和64为2种,注意这里的32位和64位并不是指操作系统的版本, 
而是指处理器的能力

2.2 位

cpu 的位是指一次性可处理的数据量是多少,1字节=8位,32位处理器可以一次性处理4个字节的数据量,
C语言定义的long类型总是对等于机器的字长

2.3 内核编码中字长准则

内核编码中涉及到字长的部分时, 牢记以下准则:

ANSI C标准规定, 一个char的长度一定是一个字节(8位)
linux当前所支持的体系结构中, int 型都是32位的
linux当前所支持的体系结构中, short 型都是16位的
linux当前所支持的体系结构中, 指针和long型的长度不定, 在32位和64位中变化 4/8字节
不能假设 sizeof(int) == sizeof(long)
类似的, 不能假定 指针的长度和int型相同.

3. 数据类型

编写可移植性代码时, 内核中的数据类型有以下3点需要注意:

3.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. 编程时保证不透明类型实际存储空间或者格式发生变化时代码不受影响

3.2. 长度确定的类型

3.2.1 内核空间的长度确定的类型

除了不透明类型, linux内核中还定义了一系列长度明确的数据类型

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;

上面这些类型只能在内核空间使用, 用户空间无法使用.

3.2.2 用户空间的长度确定的类型

用户空间有对应的变量类型, 名称前多了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;

3.3 char 类型

因为char类型在不同的体系结构中, 有时默认是带符号的, 有时是不带符号的.

char i = -1;
某些体系结构中, char类型默认是带符号的, 那么上面 i 的值就为 -1
某些体系结构中, char类型默认是不带符号的, 那么上面 i 的值就为 255, 与预期可能有差别

避免上述问题的方法就是, 给char类型赋值时, 明确是否带符号, 如下:

signed char i = -1;  /* 明确 signed, i 的值在哪种体系结构中都是 -1 */
unsigned char i = 255;  /* 明确 unsigned, i 的值在哪种体系结构中都是 255 */

4. 数据对齐

数据对齐也是增强可移植性的一个重要方面(有的体系结构对数据对齐要求非常严格, 载入未对齐的数据可导致性能下降, 甚至错误).

数据对齐的意思就是: 数据的内存地址可以被 4 整除

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;

4.2. 对于数组, 按照基本数据类型进行对齐

数组元素的存放在内存中是连续的, 第一个对齐了, 后面的都自动对齐了

4.3. 对于联合体, 长度最大的数据对齐就可以了

4.4. 对于结构体, 保证结构体中每个元素能够正确对齐即可

如果结构体中的元素没有对齐, 编译器会自动填充结构体, 保证它是对齐的.
注意:

虽然调整结构体中元素的顺序可以减少填充的字节, 从而降低内存的消耗.
但是对于内核中已有的那些结构, 千万不能随便调整其元素顺序, 
因为内核中很多现存的方法都是通过元素在结构体中位置偏移来获取元素的.

5. 字节序

字节序:顾名思义字节的顺序,
	就是大于一个字节类型的数据在内存中的存放顺序。

大端:就是高位字节排放在内存的低地址端,低位字节排放在内存的高地址端

小端:就是低位字节排放在内存的低地址端

主机序:一般是小端(mips架构是小端,x86大端)

网络序:一般是大端

举例:
	数据: 0x1234 大小端传输
	地址: 0x00  0x01
	大端:  12   34
	小端:  34   12

看之前
通俗易懂说字节序,大小端,网络序和主机序
https://blog.csdn.net/lqy971966/article/details/93602177

6. 时间

内核中使用到时间相关概念时, 为了提高可移植性,
不要使用时间中断的发生频率(也就是每秒产生的jiffies), 而应该使用 HZ 来正确使用时间.

linux 时间 time(1)-时间相关结构体和函数详解
https://blog.csdn.net/lqy971966/article/details/107975594

linux 时间 time(2)-频率 和 jiffies
https://blog.csdn.net/lqy971966/article/details/110234641

Linux内核设计与实现(11)第十一章:定时器和时间管理
https://blog.csdn.net/lqy971966/article/details/119607695

7. 页长度

7.1 页大小不一定是4KB

当处理用页管理的内存时, 不要既定页的长度为 4KB, 在不同的体系结构中长度会不一样.

而应该使用 PAGE_SIZE 以字节数来表示页长度, 使用 PAGE_SHIFT 表示从最右端屏蔽了多少位能够得到该地址对应的页的页号.
PAGE_SIZE 和 PAGE_SHIFT 都是宏

root@localhost home]# getconf -a | grep -i 'page'
PAGESIZE                           4096
PAGE_SIZE                          4096
_AVPHYS_PAGES                      162209
_PHYS_PAGES                        249489
[root@localhost home]#
PAGESIZE 就是当前机器页大小,即 4KB

8. 处理器顺序(内存屏障)

还有最后一个和可移植性相关的注意点就是处理器对代码的执行顺序, 在有些体系结构中, 处理器并不是严格按照代码编写的顺序执行的,
可能为了优化性能或者其他原因, 处理器执行指令的顺序与编写的代码的顺序稍有出入.
如果我们的某段代码需要严格的执行顺序, 需要在代码中使用 rmb() wmb() 等内存屏障来确保处理器的执行顺序.

Linux RCU机制+内存屏障
https://blog.csdn.net/lqy971966/article/details/118993557

9. SMP, 内核抢占, 高端内存

SMP, 内核抢占和高端内存本身虽然和可移植性没有太大的关系, 但它们都是内核中重要的配置选项,
如果编码时能够考虑到这些的话, 那么即使内核修改SMP等这些配置选项, 我们的代码仍然可以安全可靠的运行.
所以, 在编写内核代码时最好加上如下假设:

1. 假设代码会在SMP系统上运行, 要正确选择和使用锁
2. 假设代码会在支持内核抢占的情况下运行, 要正确使用锁和内核抢占语句
3. 假设代码会运行在使用高端内存(非永久映射内存)的系统上, 必要时使用 kmap()

10. 其他(最大公因子和最小公约数)

书中还提到的2点注意事项, 我觉得不仅是编写内核代码, 编写任何代码时, 都应该注意:

1. 编码尽量选取最大公因子 : 假定任何事情都有可能发生, 任何潜在的约束也都存在
2. 编码尽量选取最小公约数 : 不要假定给定的内核特性是可用的, 仅仅需要最小的体系结构功能

猜你喜欢

转载自blog.csdn.net/lqy971966/article/details/119823939
今日推荐