《Linux内核设计与实现》读书笔记(十九)- 可移植性

简介:

linux内核的移植性非常好, 目前的内核也支持非常多的体系结构(有20多个).

但是刚开始时, linux也只支持 intel i386 架构, 从 v1.2版开始支持 Digital Alpha, Intel x86, MIPS和SPARC(虽然支持的还不是很完善).

从 v2.0版本开始加入了对 Motorala 68K和PowerPC的官方支持, v2.2版本开始新增了 ARMS, IBM S390和UltraSPARC的支持.

v2.4版本支持的体系结构数达到了15个, v2.6版本支持的体系结构数目提高到了21个.

目前的我使用的系统是 Fedora20, 支持的体系结构有31个之多.(源码树中 arch目录下有支持的体系结构, 每种体系结构一个文件夹)

 

考虑到内核支持如此之多的架构, 在内核开发的时候就需要考虑编码的可移植性.

提高可移植性最重要的就是要搞明白不同体系结构之间究竟是什么对移植代码的影响比较大.

主要内容:

  • 字长
  • 数据类型
  • 数据对齐
  • 字节顺序
  • 时间
  • 页长度
  • 处理器顺序
  • SMP, 内核抢占, 高端内存
  • 总结

 

1. 字长

这里的字是指处理器能够一次完成处理的数据. 字长即使处理器能够一次完成处理的数据的最大长度.

目前的处理器主要有32位和64为2种, 注意这里的32位和64位并不是指操作系统的版本, 而是指处理器的能力.

一般来说, 32位的处理器只能安装32位的操作系统, 而64位的处理器可以安装32位的操作系统, 也可以安装64位的操作系统.

 

对于一种体系结构来说, 处理器通用寄存器(general-purpose registers, GPR)的大小和它的字长是相同的.

C语言定义的long类型总是对等于机器的字长, 而int型有时会比字长小.

  • 32位的体系结构中, int型和long型都是32位的
  • 64位的体系结构中, int型是32位的, long型是64位的.

 

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

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

 

此外, 操作系统有个简单的助记符来描述此系统中数据类型的大小.

  • LLP64 :: 64位的Windows, long类型和指针都是64位
  • LP64 :: 64位的Linux, long类型和指针都是64位
  • ILP32 :: 32位的Linux, int类型, long类型和指针都是32位
  • ILP64 :: int类型, long类型和指针都是64位(非Linux)

 

2. 数据类型

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

 

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

 

2.2 长度确定的类型

除了不透明类型, linux内核中还定义了一系列长度明确的数据类型, 参见 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;

 

上面这些类型只能在内核空间使用, 用户空间无法使用. 用户空间有对应的变量类型, 名称前多了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;

 

2.3 char类型

之所以把char类型单独拿出来说明, 是因为char类型在不同的体系结构中, 有时默认是带符号的, 有时是不带符号的.

比如, 最简单的例子:

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

 

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

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

 

3. 数据对齐

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

数据对齐的意思就是: 数据的内存地址可以被 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;

 

2. 对于数组, 安装基本数据类型进行对齐就行.(数组元素的存放在内存中是连续的, 第一个对齐了, 后面的都自动对齐了)

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

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

如果结构体中的元素没有对齐, 编译器会自动填充结构体, 保证它是对齐的. 比如下面的代码, 预计应该输出12, 实际却输出了24

我的代码运行环境: 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个字节 */
    unsigned long cat;          /* 8个字节 */
    unsigned short pig;         /* 2个字节 */
    char fox;                   /* 1个字节 */
};

int main(int argc, char *argv[])
{
    /* 在我的64bit 系统中是按8位对齐, 下面的代码输出 24 */
    printf ("sizeof(animal_struct)=%d\n", sizeof(struct animal_struct));
    return 0;
}

测试方法:

gcc -o test struct_align.c
./test   # 输出24

 

结构体应该被填充成如下形式:

struct animal_struct
{
    char dog;                   /* 1个字节 */
    /* 此处填充了7个字节 */
    unsigned long cat;          /* 8个字节 */
    unsigned short pig;         /* 2个字节 */
    char fox;                   /* 1个字节 */
    /* 此处填充了5个字节 */   
};

 

通过调整结构体中元素顺序, 可以减少填充的字节数, 比如上述结构体如果定义成如下顺序:

struct animal_struct
{
    unsigned long cat;          /* 8个字节 */
    unsigned short pig;         /* 2个字节 */
    char dog;                   /* 1个字节 */
    char fox;                   /* 1个字节 */
};

那么为了保证8位对齐, 只需在后面补充 4位即可:

struct animal_struct
{
    unsigned long cat;          /* 8个字节 */
    unsigned short pig;         /* 2个字节 */
    char dog;                   /* 1个字节 */
    char fox;                   /* 1个字节 */
    /* 此处填充了4个字节 */   
};

 

调整后的代码会输出 16, 不是之前的24

#include <stdio.h>

struct animal_struct
{
    unsigned long cat;          /* 8个字节 */
    unsigned short pig;         /* 2个字节 */
    char dog;                   /* 1个字节 */
    char fox;                   /* 1个字节 */
};

int main(int argc, char *argv[])
{
    /* 在我的64bit 系统中是按8位对齐, 下面的代码输出 16 */
    printf ("sizeof(animal_struct)=%d\n", sizeof(struct animal_struct));
    return 0;
}

测试方法:

gcc -o test struct_align.c
./test  # 输出16

 

注意: 虽然调整结构体中元素的顺序可以减少填充的字节, 从而降低内存的消耗.

但是对于内核中已有的那些结构, 千万不能随便调整其元素顺序, 因为内核中很多现存的方法都是通过元素在结构体中位置偏移来获取元素的.

 

4. 字节顺序

字节顺序其实只有2种:

  • 低位优先 :: little-endian 数据由低位地址->高位地址存放
  • 高位优先 :: big-endian 数据由高位地址->低位地址存放

 

比如占有四个字节的整数的二进制表示如下:

00000001 00000002 00000003 00000004

 

内存地址方向:   高位  <--------------------> 低位

little-endian 表示如下: 

00000001 00000002 00000003 00000004

big-endian 表示如下:

00000004 00000003 00000002 00000001

 

判断一个体系结构是 big-endian 还是 little-endian 非常简单.

int x = 1;  /* 二进制 00000000 00000000 00000000 00000001 */

/* 
 * 内存地址方向:   高位  <--------------------> 低位
 * little-endian 表示: 00000000 00000000 00000000 00000001
 * big-endian 表示:    00000001 00000000 00000000 00000000
 */
if (*(char *) &x == 1)   /* 这句话把int型转为char型, 相当于只取了int型的最低8bit */
    /* little-endian */
else
    /* big-endian */

 

5. 时间

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

关于 jiffies 和 HZ 的概念, 可以参考之前的博客: 《Linux内核设计与实现》读书笔记(十一)- 定时器和时间管理

 

6. 页长度

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

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

PAGE_SIZE 和 PAGE_SHIFT 都是宏, 定义在 include/asm-generic/page.h 中

 

下表是一些体系结构中页长度:

体系结构

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. 处理器顺序

还有最后一个和可移植性相关的注意点就是处理器对代码的执行顺序, 在有些体系结构中, 处理器并不是严格按照代码编写的顺序执行的,

可能为了优化性能或者其他原因, 处理器执行指令的顺序与编写的代码的顺序稍有出入.

 

如果我们的某段代码需要严格的执行顺序, 需要在代码中使用 rmb() wmb() 等内存屏障来确保处理器的执行顺序.

关于rmb和wmb可以参考之前的博客: 《Linux内核设计与实现》读书笔记(十)- 内核同步方法  第 11 小节

 

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

SMP, 内核抢占和高端内存本身虽然和可移植性没有太大的关系, 但它们都是内核中重要的配置选项,

如果编码时能够考虑到这些的话, 那么即使内核修改SMP等这些配置选项, 我们的代码仍然可以安全可靠的运行.

所以, 在编写内核代码时最好加上如下假设:

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

 

9. 总结

编写简洁, 可移植性的代码还需要通过实践来积累经验, 上面的准则可以作为代码是否满足可移植性的一些检测条件.

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

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

 

虽然编写可移植性代码需要遵守这么多的原则, 但是不能畏惧, 在学习内核开发的过程中, 只有不断的尝试, 不断的犯错, 才能确实的掌握内核.



本文转自wang_yb博客园博客,原文链接:http://www.cnblogs.com/wang_yb/p/3512095.html,如需转载请自行联系原作者


目录
相关文章
|
2月前
|
监控 Linux 开发者
理解Linux操作系统内核中物理设备驱动(phy driver)的功能。
综合来看,物理设备驱动在Linux系统中的作用是至关重要的,它通过与硬件设备的紧密配合,为上层应用提供稳定可靠的通信基础设施。开发一款优秀的物理设备驱动需要开发者具备深厚的硬件知识、熟练的编程技能以及对Linux内核架构的深入理解,以确保驱动程序能在不同的硬件平台和网络条件下都能提供最优的性能。
129 0
|
5月前
|
并行计算 Linux
Linux内核中的线程和进程实现详解
了解进程和线程如何工作,可以帮助我们更好地编写程序,充分利用多核CPU,实现并行计算,提高系统的响应速度和计算效能。记住,适当平衡进程和线程的使用,既要拥有独立空间的'兄弟',也需要在'家庭'中分享和并行的成员。对于这个世界,现在,你应该有一个全新的认识。
237 67
|
3月前
|
存储 负载均衡 算法
Linux2.6内核进程调度队列
本篇文章是Linux进程系列中的最后一篇文章,本来是想放在上一篇文章的结尾的,但是想了想还是单独写一篇文章吧,虽然说这部分内容是比较难的,所有一般来说是简单的提及带过的,但是为了让大家对进程有更深的理解与认识,还是看了一些别人的文章,然后学习了学习,然后对此做了总结,尽可能详细的介绍明白。最后推荐一篇文章Linux的进程优先级 NI 和 PR - 简书。
103 0
|
5月前
|
存储 Linux
Linux内核中的current机制解析
总的来说,current机制是Linux内核中进程管理的基础,它通过获取当前进程的task_struct结构的地址,可以方便地获取和修改进程的信息。这个机制在内核中的使用非常广泛,对于理解Linux内核的工作原理有着重要的意义。
215 11
|
6月前
|
自然语言处理 监控 Linux
Linux 内核源码分析---proc 文件系统
`proc`文件系统是Linux内核中一个灵活而强大的工具,提供了一个与内核数据结构交互的接口。通过本文的分析,我们深入探讨了 `proc`文件系统的实现原理,包括其初始化、文件的创建与操作、动态内容生成等方面。通过对这些内容的理解,开发者可以更好地利用 `proc`文件系统来监控和调试内核,同时也为系统管理提供了便利的工具。
254 16
|
8月前
|
Ubuntu Linux 开发者
Ubuntu20.04搭建嵌入式linux网络加载内核、设备树和根文件系统
使用上述U-Boot命令配置并启动嵌入式设备。如果配置正确,设备将通过TFTP加载内核和设备树,并通过NFS挂载根文件系统。
449 15
|
8月前
|
安全 Linux 测试技术
Intel Linux 内核测试套件-LKVS介绍 | 龙蜥大讲堂104期
《Intel Linux内核测试套件-LKVS介绍》(龙蜥大讲堂104期)主要介绍了LKVS的定义、使用方法、测试范围、典型案例及其优势。LKVS是轻量级、低耦合且高代码覆盖率的测试工具,涵盖20多个硬件和内核属性,已开源并集成到多个社区CICD系统中。课程详细讲解了如何使用LKVS进行CPU、电源管理和安全特性(如TDX、CET)的测试,并展示了其在实际应用中的价值。
196 4
|
2月前
|
监控 Linux 网络安全
Linux命令大全:从入门到精通
日常使用的linux命令整理
626 13
|
3月前
|
Linux 网络安全 数据安全/隐私保护
使用Linux系统的mount命令挂载远程服务器的文件夹。
如此一来,你就完成了一次从你的Linux发车站到远程服务器文件夹的有趣旅行。在这个技术之旅中,你既探索了新地方,也学到了如何桥接不同系统之间的距离。
463 21
|
3月前
|
JSON 自然语言处理 Linux
linux命令—tree
tree是一款强大的Linux命令行工具,用于以树状结构递归展示目录和文件,直观呈现层级关系。支持多种功能,如过滤、排序、权限显示及格式化输出等。安装方法因系统而异常用场景包括:基础用法(显示当前或指定目录结构)、核心参数应用(如层级控制-L、隐藏文件显示-a、完整路径输出-f)以及进阶操作(如磁盘空间分析--du、结合grep过滤内容、生成JSON格式列表-J等)。此外,还可生成网站目录结构图并导出为HTML文件。注意事项:使用Tab键补全路径避免错误;超大目录建议限制遍历层数;脚本中推荐禁用统计信息以优化性能。更多详情可查阅手册mantree。
linux命令—tree